mirror of
https://github.com/vxcontrol/langchaingo.git
synced 2026-07-19 21:54:17 -04:00
llms: add prompt caching and reasoning token support (#1394)
* llms: add prompt caching and reasoning token support Add comprehensive infrastructure for prompt caching and reasoning/thinking tokens: - Add ReasoningModel interface for models supporting extended reasoning - Add CacheControl and CachedContent types for prompt caching - Add ThinkingConfig with modes (none, low, medium, high, auto) - Add token budget calculation and usage tracking for thinking tokens - Add WithPromptCaching, WithThinking, and related call options - Add model detection for reasoning-capable models (OpenAI o1/o3, Claude 3.7+, DeepSeek) - Update options documentation for better provider compatibility notes - Remove testify imports tracking file Supports provider-specific implementations while maintaining consistent API. * testing: add comprehensive LLM testing framework Add standardized testing infrastructure for all LLM providers following Go's testing/fstest design philosophy: - Add llmtest package with automatic capability discovery and parallel testing - Add TestLLM function that probes and tests all supported LLM features - Add provider-specific test files for all LLM implementations (anthropic, bedrock, cloudflare, cohere, ernie, fake, googleai, huggingface, llamafile, local, maritaca, mistral, ollama, openai, watsonx) - Add comprehensive tests for prompt caching, reasoning tokens, and token utilization - Add MockLLM implementation for testing without API calls - Add ValidateLLM function for basic model validation - Support automatic detection of streaming, tool calls, reasoning, and caching capabilities The framework provides a simple API (TestLLM) that automatically discovers and tests all supported capabilities, enabling consistent testing across all provider implementations. * llms: implement provider-specific prompt caching and reasoning support Add comprehensive provider-specific implementations for prompt caching and reasoning tokens: **Anthropic (Claude)**: - Add prompt caching with ephemeral cache controls and beta headers - Add extended thinking support for Claude 3.7+ with budget token configuration - Add interleaved thinking for tool calls and 128K output support - Add thinking content extraction from <thinking> tags - Add cache token tracking (creation/read input tokens) **Google AI (Gemini)**: - Add CachingHelper for pre-created cached content management - Add reasoning detection for Gemini 2.0+ models - Add cached content support via metadata - Add standardized token usage reporting with cache information **Ollama**: - Add ContextCache for in-memory conversation context caching - Add reasoning support detection for DeepSeek R1, QwQ, and thinking models - Add Think parameter support for reasoning-capable models - Add cache statistics and hit/miss tracking **OpenAI**: - Add reasoning support detection for o1/o3 series and future GPT-5 - Add system message handling for models without system support (o1/o3) - Add thinking content extraction and standardized token reporting - Add metadata filtering to prevent internal fields from reaching API - Add ReasoningEffort parameter preparation for future models **Cross-provider features**: - Implement ReasoningModel interface across all providers - Add standardized GenerationInfo fields (ThinkingContent, ThinkingTokens, etc.) - Add provider-specific options (WithPromptCaching, WithExtendedOutput, etc.) - Add comprehensive test coverage for caching and reasoning features - Add model capability detection and validation All providers now support the unified prompt caching and reasoning interfaces while maintaining their specific implementation details and capabilities. * examples: add comprehensive prompt caching and reasoning examples Add practical examples demonstrating prompt caching and reasoning token features across multiple LLM providers: **Anthropic Examples**: - Add anthropic-extended-capabilities example showing combined extended thinking + 128K output - Add anthropic-interleaved-thinking example demonstrating thinking between tool calls - Show token budget management and thinking content extraction - Include comprehensive README documentation for each example **Multi-Provider Examples**: - Add googleai-reasoning-caching example with Gemini 2.0+ reasoning and cached content - Add ollama-reasoning-caching example with DeepSeek R1/QwQ models and context caching - Add prompt-caching example showing Anthropic's 90% cost reduction on cached tokens - Add reasoning-tokens example comparing o1-mini, Claude 3.7, and standard models **Key Features Demonstrated**: - Prompt caching for cost optimization and faster responses - Reasoning/thinking tokens for improved response quality - Token usage analysis and cache hit/miss tracking - Provider-specific capabilities and configuration options - Real-world use cases like data analysis, Q&A systems, and multi-step reasoning All examples include detailed documentation, error handling, and clear output formatting to help developers understand and implement these advanced LLM features. * gofmt: fix formatting in messages.go
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# Extended Capabilities Example
|
||||
|
||||
This example demonstrates Claude 3.7+'s combined extended capabilities:
|
||||
- **Extended Thinking**: Deep reasoning with configurable thinking budget
|
||||
- **Extended Output**: Generate up to 128K tokens (vs standard 8K limit)
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
1. **Combined Capabilities**: Shows how to use both extended thinking AND extended output together
|
||||
2. **Complex Task**: Generates a comprehensive distributed systems guide requiring both deep reasoning and extensive output
|
||||
3. **Token Metrics**: Displays detailed token usage including thinking tokens and output tokens
|
||||
|
||||
## Running the Example
|
||||
|
||||
```bash
|
||||
# Set your API key
|
||||
export ANTHROPIC_API_KEY=your-api-key
|
||||
|
||||
# Run the example
|
||||
go run .
|
||||
```
|
||||
|
||||
## Key Implementation Points
|
||||
|
||||
```go
|
||||
// Enable both capabilities together
|
||||
opts := []llms.CallOption{
|
||||
// Extended thinking for complex reasoning
|
||||
llms.WithThinkingMode(llms.ThinkingModeHigh),
|
||||
|
||||
// Extended output for up to 128K tokens
|
||||
anthropic.WithExtendedOutput(),
|
||||
|
||||
// Set high token limit
|
||||
llms.WithMaxTokens(50000),
|
||||
}
|
||||
```
|
||||
|
||||
## What to Expect
|
||||
|
||||
- The model will use extended thinking to reason about the complex distributed systems topic
|
||||
- It will generate a comprehensive guide that may exceed standard token limits
|
||||
- Token metrics will show both thinking tokens used and total output generated
|
||||
- If the response is large (>10K chars), you'll have the option to save it to a file
|
||||
|
||||
## Requirements
|
||||
|
||||
- Claude 3.7 Sonnet model (`claude-3-7-sonnet-20250219`)
|
||||
- Valid Anthropic API key with access to Claude 3.7
|
||||
- Both extended thinking and extended output features enabled on your account
|
||||
@@ -0,0 +1,13 @@
|
||||
module github.com/tmc/langchaingo/examples/anthropic-extended-capabilities
|
||||
|
||||
go 1.23.8
|
||||
|
||||
require github.com/tmc/langchaingo v0.1.14-pre.3
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
||||
)
|
||||
|
||||
replace github.com/tmc/langchaingo => ../..
|
||||
@@ -0,0 +1,22 @@
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
@@ -0,0 +1,169 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
"github.com/tmc/langchaingo/llms/anthropic"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
fmt.Println("=== Claude 3.7+ Extended Capabilities Demo ===")
|
||||
fmt.Println("Demonstrating combined extended thinking + 128K output")
|
||||
fmt.Println()
|
||||
|
||||
// Complex prompt that benefits from both extended thinking and long output
|
||||
prompt := `Write a comprehensive technical guide about building distributed systems.
|
||||
|
||||
Include:
|
||||
1. Theoretical foundations (CAP theorem, consensus algorithms)
|
||||
2. Practical implementation patterns
|
||||
3. Real-world case studies from major tech companies
|
||||
4. Code examples in Go for key concepts
|
||||
5. Testing strategies for distributed systems
|
||||
6. Common pitfalls and how to avoid them
|
||||
7. Performance optimization techniques
|
||||
8. Monitoring and observability best practices
|
||||
|
||||
Make this guide as detailed and comprehensive as possible, targeting senior engineers
|
||||
who want to deeply understand distributed systems architecture.`
|
||||
|
||||
apiKey := os.Getenv("ANTHROPIC_API_KEY")
|
||||
if apiKey == "" {
|
||||
fmt.Println("ANTHROPIC_API_KEY not set. Skipping demo.")
|
||||
fmt.Println("Set the environment variable to run this example:")
|
||||
fmt.Println(" export ANTHROPIC_API_KEY=your-api-key")
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize Claude 3.7 with extended capabilities
|
||||
llm, err := anthropic.New(
|
||||
anthropic.WithModel("claude-3-7-sonnet-20250219"),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Error initializing Anthropic: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{llms.TextPart(prompt)},
|
||||
},
|
||||
}
|
||||
|
||||
// Configure with both extended thinking AND extended output
|
||||
opts := []llms.CallOption{
|
||||
// Enable extended thinking for complex reasoning
|
||||
llms.WithThinkingMode(llms.ThinkingModeHigh),
|
||||
// Enable 128K output for comprehensive response
|
||||
anthropic.WithExtendedOutput(),
|
||||
// Set high token limit to utilize extended output
|
||||
llms.WithMaxTokens(50000), // Can go up to 128K
|
||||
// Temperature must be 1 when thinking is enabled
|
||||
llms.WithTemperature(1.0),
|
||||
}
|
||||
|
||||
fmt.Println("Generating comprehensive guide with:")
|
||||
fmt.Println(" • Extended thinking (HIGH mode)")
|
||||
fmt.Println(" • Extended output (up to 128K tokens)")
|
||||
fmt.Println(" • Max tokens set to 50,000")
|
||||
fmt.Println()
|
||||
fmt.Print("Processing (this may take a while)... ")
|
||||
|
||||
resp, err := llm.GenerateContent(ctx, messages, opts...)
|
||||
if err != nil {
|
||||
fmt.Printf("\nError: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("done")
|
||||
|
||||
// Display response summary
|
||||
content := resp.Choices[0].Content
|
||||
contentLen := len(content)
|
||||
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println("RESPONSE SUMMARY")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
fmt.Printf("Total response length: %d characters\n", contentLen)
|
||||
|
||||
// Show first 1000 chars as preview
|
||||
preview := content
|
||||
if len(preview) > 1000 {
|
||||
preview = preview[:1000] + "..."
|
||||
}
|
||||
fmt.Println("\nPreview (first 1000 chars):")
|
||||
fmt.Println(strings.Repeat("-", 40))
|
||||
fmt.Println(preview)
|
||||
fmt.Println(strings.Repeat("-", 40))
|
||||
|
||||
// Display detailed token metrics
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println("TOKEN METRICS")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
var inputTokens, outputTokens, totalTokens int
|
||||
if v, ok := genInfo["PromptTokens"].(int); ok {
|
||||
inputTokens = v
|
||||
}
|
||||
if v, ok := genInfo["CompletionTokens"].(int); ok {
|
||||
outputTokens = v
|
||||
}
|
||||
if v, ok := genInfo["TotalTokens"].(int); ok {
|
||||
totalTokens = v
|
||||
}
|
||||
|
||||
fmt.Printf("Input Tokens: %d\n", inputTokens)
|
||||
fmt.Printf("Output Tokens: %d\n", outputTokens)
|
||||
fmt.Printf("Total Tokens: %d\n", totalTokens)
|
||||
|
||||
// Check for thinking tokens
|
||||
usage := llms.ExtractThinkingTokens(genInfo)
|
||||
if usage != nil && usage.ThinkingTokens > 0 {
|
||||
fmt.Printf("\nThinking Analysis:\n")
|
||||
fmt.Printf(" Thinking Tokens: %d\n", usage.ThinkingTokens)
|
||||
fmt.Printf(" Visible Output: %d\n", outputTokens-usage.ThinkingTokens)
|
||||
fmt.Printf(" Thinking Ratio: %.1f%% of output\n",
|
||||
float64(usage.ThinkingTokens)/float64(outputTokens)*100)
|
||||
|
||||
if usage.ThinkingBudgetAllocated > 0 {
|
||||
fmt.Printf(" Budget Allocated: %d\n", usage.ThinkingBudgetAllocated)
|
||||
fmt.Printf(" Budget Used: %d\n", usage.ThinkingBudgetUsed)
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight extended output usage
|
||||
if outputTokens > 8192 {
|
||||
fmt.Printf("\n✅ Extended Output Active: Generated %d tokens (standard limit is 8192)\n", outputTokens)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println("Demo complete!")
|
||||
fmt.Println("\nKey Features Demonstrated:")
|
||||
fmt.Println("• Extended thinking for complex reasoning about distributed systems")
|
||||
fmt.Println("• Extended output allowing comprehensive, detailed responses")
|
||||
fmt.Println("• Combined capabilities working together seamlessly")
|
||||
|
||||
// Optionally save full response to file
|
||||
if contentLen > 10000 {
|
||||
filename := "distributed-systems-guide.md"
|
||||
fmt.Printf("\nFull response is %d chars. Save to %s? (y/n): ", contentLen, filename)
|
||||
var answer string
|
||||
fmt.Scanln(&answer)
|
||||
if strings.ToLower(answer) == "y" {
|
||||
err := os.WriteFile(filename, []byte(content), 0644)
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving file: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Saved to %s\n", filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# Interleaved Thinking Example
|
||||
|
||||
This example demonstrates Claude 3.7+'s interleaved thinking capability, which allows the model to use thinking tokens between tool calls for better multi-step reasoning.
|
||||
|
||||
## What is Interleaved Thinking?
|
||||
|
||||
Interleaved thinking enables Claude to:
|
||||
- **Think between tool calls**: Use reasoning tokens to plan which tool to use next
|
||||
- **Interpret results**: Process tool outputs before deciding on next steps
|
||||
- **Synthesize information**: Combine results from multiple tools coherently
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
1. **Multi-step problem solving** with quarterly sales analysis
|
||||
2. **Tool orchestration** with calculate, search, and analyze functions
|
||||
3. **Thinking tokens** used for planning and interpretation
|
||||
4. **Token metrics** showing thinking vs visible output
|
||||
|
||||
## Running the Example
|
||||
|
||||
```bash
|
||||
# Set your API key
|
||||
export ANTHROPIC_API_KEY=your-api-key
|
||||
|
||||
# Run the example
|
||||
go run .
|
||||
```
|
||||
|
||||
## Key Implementation
|
||||
|
||||
```go
|
||||
// Enable interleaved thinking for tool use
|
||||
opts := []llms.CallOption{
|
||||
// Thinking mode for reasoning
|
||||
llms.WithThinkingMode(llms.ThinkingModeMedium),
|
||||
|
||||
// Enable interleaved thinking beta feature
|
||||
anthropic.WithInterleavedThinking(),
|
||||
|
||||
// Provide tools for the model to use
|
||||
llms.WithTools(tools),
|
||||
}
|
||||
```
|
||||
|
||||
## Example Flow
|
||||
|
||||
The demo presents a multi-step analysis task:
|
||||
1. Calculate year-over-year growth rates
|
||||
2. Analyze data trends
|
||||
3. Search for explanatory factors
|
||||
4. Make predictions based on findings
|
||||
|
||||
Between each tool call, Claude uses thinking tokens to:
|
||||
- Decide which tool to use next
|
||||
- Interpret the results from the previous tool
|
||||
- Plan the next step in the analysis
|
||||
|
||||
## Token Metrics
|
||||
|
||||
The example displays detailed token usage:
|
||||
- **Thinking Tokens**: Used for internal reasoning between tools
|
||||
- **Visible Output**: The actual response content
|
||||
- **Thinking Ratio**: Percentage of tokens used for thinking
|
||||
|
||||
## Requirements
|
||||
|
||||
- Claude 3.7 Sonnet model (`claude-3-7-sonnet-20250219`)
|
||||
- Valid Anthropic API key
|
||||
- Interleaved thinking feature access
|
||||
@@ -0,0 +1,15 @@
|
||||
module github.com/tmc/langchaingo/examples/interleaved-thinking
|
||||
|
||||
go 1.23.8
|
||||
|
||||
require github.com/tmc/langchaingo v0.1.14-pre.3
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/tmc/langchaingo => ../..
|
||||
@@ -0,0 +1,97 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
|
||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
@@ -0,0 +1,726 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
"github.com/tmc/langchaingo/llms/anthropic"
|
||||
)
|
||||
|
||||
// Define some tools for the model to use
|
||||
var (
|
||||
calculateTool = llms.Tool{
|
||||
Type: "function",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "calculate",
|
||||
Description: "Perform mathematical calculations",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"expression": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Mathematical expression to evaluate",
|
||||
},
|
||||
},
|
||||
"required": []string{"expression"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
searchTool = llms.Tool{
|
||||
Type: "function",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "search_knowledge",
|
||||
Description: "Search for information in a knowledge base",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"query": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Search query",
|
||||
},
|
||||
},
|
||||
"required": []string{"query"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
analyzeTool = llms.Tool{
|
||||
Type: "function",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "analyze_data",
|
||||
Description: "Analyze data and return insights",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"type": "array",
|
||||
"description": "Data points to analyze",
|
||||
"items": map[string]interface{}{
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"analysis_type": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Type of analysis: mean, median, std_dev, trend",
|
||||
},
|
||||
},
|
||||
"required": []string{"data", "analysis_type"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// New tool that depends on results from other tools
|
||||
generateReportTool = llms.Tool{
|
||||
Type: "function",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "generate_report",
|
||||
Description: "Generate a strategic report based on analysis results. ONLY use after completing all calculations and analyses.",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"growth_rate": map[string]interface{}{
|
||||
"type": "number",
|
||||
"description": "Year-over-year growth rate from calculations",
|
||||
},
|
||||
"trend_direction": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Trend direction from analysis (upward/downward/stable)",
|
||||
},
|
||||
"volatility_level": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Volatility level based on std deviation (low/moderate/high)",
|
||||
},
|
||||
"key_insights": map[string]interface{}{
|
||||
"type": "array",
|
||||
"description": "Key insights from research",
|
||||
"items": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"prediction": map[string]interface{}{
|
||||
"type": "number",
|
||||
"description": "Q1-2025 predicted value",
|
||||
},
|
||||
},
|
||||
"required": []string{"growth_rate", "trend_direction", "volatility_level", "key_insights", "prediction"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Tool for making final predictions based on all data
|
||||
makePredictionTool = llms.Tool{
|
||||
Type: "function",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "make_prediction",
|
||||
Description: "Make predictions based on calculated metrics. Requires growth rates and averages from previous calculations.",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"base_value": map[string]interface{}{
|
||||
"type": "number",
|
||||
"description": "The base value to predict from (e.g., Q4-2024 value)",
|
||||
},
|
||||
"growth_rate": map[string]interface{}{
|
||||
"type": "number",
|
||||
"description": "Growth rate to apply (as decimal, e.g., 0.05 for 5%)",
|
||||
},
|
||||
"seasonality_factor": map[string]interface{}{
|
||||
"type": "number",
|
||||
"description": "Seasonal adjustment factor",
|
||||
},
|
||||
"confidence_level": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "Confidence level: high, medium, low",
|
||||
},
|
||||
},
|
||||
"required": []string{"base_value", "growth_rate", "seasonality_factor", "confidence_level"},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// debugTransport wraps http.RoundTripper to log requests and responses
|
||||
type debugTransport struct {
|
||||
Transport http.RoundTripper
|
||||
Debug bool
|
||||
}
|
||||
|
||||
func (d *debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if d.Debug {
|
||||
fmt.Println("\n" + strings.Repeat("=", 80))
|
||||
fmt.Println("HTTP REQUEST")
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
|
||||
// Dump request
|
||||
reqDump, err := httputil.DumpRequestOut(req, true)
|
||||
if err != nil {
|
||||
fmt.Printf("Error dumping request: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("%s\n", reqDump)
|
||||
}
|
||||
}
|
||||
|
||||
// Make the actual request
|
||||
resp, err := d.Transport.RoundTrip(req)
|
||||
|
||||
if d.Debug && resp != nil {
|
||||
fmt.Println("\n" + strings.Repeat("-", 80))
|
||||
fmt.Println("HTTP RESPONSE")
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
|
||||
// Dump response
|
||||
respDump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
fmt.Printf("Error dumping response: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("%s\n", respDump)
|
||||
}
|
||||
fmt.Println(strings.Repeat("=", 80) + "\n")
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Parse command line flags
|
||||
debugHTTP := flag.Bool("debug-http", false, "Show raw HTTP requests and responses")
|
||||
flag.Parse()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
fmt.Println("╔════════════════════════════════════════════════════════════╗")
|
||||
fmt.Println("║ Claude 4 Interleaved Thinking Demo ║")
|
||||
fmt.Println("║ Demonstrating thinking between tool calls ║")
|
||||
fmt.Println("╚════════════════════════════════════════════════════════════╝")
|
||||
fmt.Println()
|
||||
|
||||
if *debugHTTP {
|
||||
fmt.Println("🔍 DEBUG MODE: HTTP requests/responses will be displayed")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
apiKey := os.Getenv("ANTHROPIC_API_KEY")
|
||||
if apiKey == "" {
|
||||
fmt.Println("❌ ANTHROPIC_API_KEY not set. Skipping demo.")
|
||||
fmt.Println(" Set the environment variable to run this example:")
|
||||
fmt.Println(" export ANTHROPIC_API_KEY=your-api-key")
|
||||
return
|
||||
}
|
||||
|
||||
// Stage 1: Initialization
|
||||
fmt.Println("🚀 STAGE 1: INITIALIZATION")
|
||||
fmt.Println("─────────────────────────")
|
||||
fmt.Println(" • Model: Claude Sonnet 4 (20250514)")
|
||||
fmt.Println(" • Beta: interleaved-thinking-2025-05-14")
|
||||
fmt.Println(" • Feature: Thinking between tool calls")
|
||||
|
||||
// Configure options for Anthropic client
|
||||
anthropicOpts := []anthropic.Option{
|
||||
anthropic.WithModel("claude-sonnet-4-20250514"),
|
||||
}
|
||||
|
||||
// Add debug HTTP client if flag is set
|
||||
if *debugHTTP {
|
||||
httpClient := &http.Client{
|
||||
Transport: &debugTransport{
|
||||
Transport: http.DefaultTransport,
|
||||
Debug: true,
|
||||
},
|
||||
}
|
||||
anthropicOpts = append(anthropicOpts, anthropic.WithHTTPClient(httpClient))
|
||||
}
|
||||
|
||||
// Using Claude Sonnet 4 for interleaved thinking
|
||||
llm, err := anthropic.New(anthropicOpts...)
|
||||
if err != nil {
|
||||
fmt.Printf(" ❌ Error initializing Anthropic: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(" ✅ Model initialized successfully")
|
||||
fmt.Println()
|
||||
|
||||
// Stage 2: Problem Setup
|
||||
fmt.Println("📊 STAGE 2: PROBLEM SETUP")
|
||||
fmt.Println("─────────────────────────")
|
||||
fmt.Println(" Complex multi-step analysis task:")
|
||||
fmt.Println(" • Quarterly sales data analysis")
|
||||
fmt.Println(" • Year-over-year growth calculation")
|
||||
fmt.Println(" • Trend analysis and prediction")
|
||||
fmt.Println()
|
||||
|
||||
// Complex multi-step problem requiring tool use and reasoning
|
||||
prompt := `You're helping a data scientist analyze quarterly sales data and make strategic decisions.
|
||||
|
||||
The quarterly sales (in millions) for the last 8 quarters are:
|
||||
Q1-2023: 12.5, Q2-2023: 14.2, Q3-2023: 13.8, Q4-2023: 16.1
|
||||
Q1-2024: 15.3, Q2-2024: 17.4, Q3-2024: 16.9, Q4-2024: 19.2
|
||||
|
||||
IMPORTANT: Complete this multi-stage analysis. Some tools depend on results from others, creating natural stages:
|
||||
|
||||
STAGE 1 - PARALLEL DATA GATHERING (invoke all these tools simultaneously):
|
||||
1. CALCULATIONS:
|
||||
- Year-over-year growth rate for Q4: (19.2 - 16.1) / 16.1 * 100
|
||||
- Average quarterly growth 2024: ((17.4/15.3 - 1) + (16.9/17.4 - 1) + (19.2/16.9 - 1)) / 3
|
||||
- Overall growth Q1-2023 to Q4-2024: (19.2 - 12.5) / 12.5 * 100
|
||||
- Average 2023: (12.5 + 14.2 + 13.8 + 16.1) / 4
|
||||
- Average 2024: (15.3 + 17.4 + 16.9 + 19.2) / 4
|
||||
|
||||
2. DATA ANALYSIS:
|
||||
- Trend analysis on [12.5, 14.2, 13.8, 16.1, 15.3, 17.4, 16.9, 19.2]
|
||||
- Standard deviation of the same data
|
||||
- Mean of all data points
|
||||
|
||||
3. RESEARCH:
|
||||
- "seasonal sales patterns in retail"
|
||||
- "factors driving quarterly sales growth"
|
||||
- "economic indicators affecting sales performance"
|
||||
|
||||
STAGE 2 - SYNTHESIS (use make_prediction tool AFTER Stage 1 completes):
|
||||
After receiving all Stage 1 results, use the make_prediction tool with:
|
||||
- base_value: Q4-2024 value (19.2)
|
||||
- growth_rate: Use the average quarterly growth rate from Stage 1
|
||||
- seasonality_factor: Derive from the seasonal patterns research
|
||||
- confidence_level: Based on the standard deviation analysis
|
||||
|
||||
STAGE 3 - REPORT GENERATION (use generate_report tool AFTER Stage 2):
|
||||
Finally, use generate_report to create a comprehensive summary using ALL previous results.
|
||||
|
||||
This demonstrates interleaved thinking: parallel execution where possible, sequential when dependencies exist.`
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{llms.TextPart(prompt)},
|
||||
},
|
||||
}
|
||||
|
||||
// Stage 3: Configuration
|
||||
fmt.Println("⚙️ STAGE 3: CONFIGURATION")
|
||||
fmt.Println("─────────────────────────")
|
||||
fmt.Println(" • Thinking Mode: MEDIUM")
|
||||
fmt.Println(" • Interleaved Thinking: ENABLED")
|
||||
fmt.Println(" • Temperature: 1.0 (required for thinking)")
|
||||
fmt.Println(" • Max Tokens: 4000")
|
||||
fmt.Println(" • Tools Available:")
|
||||
fmt.Println(" - calculate: Mathematical calculations")
|
||||
fmt.Println(" - search_knowledge: Information retrieval")
|
||||
fmt.Println(" - analyze_data: Statistical analysis")
|
||||
fmt.Println()
|
||||
|
||||
// Configure with interleaved thinking for tool use
|
||||
var streamedContent strings.Builder
|
||||
var contentBlockCount int
|
||||
|
||||
opts := []llms.CallOption{
|
||||
// Enable thinking mode for reasoning between tools
|
||||
llms.WithThinkingMode(llms.ThinkingModeMedium),
|
||||
// Add interleaved thinking beta header
|
||||
anthropic.WithInterleavedThinking(),
|
||||
// Temperature must be 1 when thinking is enabled
|
||||
llms.WithTemperature(1.0),
|
||||
// Provide tools
|
||||
llms.WithTools([]llms.Tool{
|
||||
calculateTool,
|
||||
searchTool,
|
||||
analyzeTool,
|
||||
makePredictionTool,
|
||||
generateReportTool,
|
||||
}),
|
||||
llms.WithMaxTokens(8000), // Increased for multiple tool calls
|
||||
// Add streaming to show progress
|
||||
// Note: The streaming function receives processed text content,
|
||||
// not raw SSE events. The anthropic client handles SSE parsing internally.
|
||||
llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
|
||||
chunkStr := string(chunk)
|
||||
|
||||
// Show progress dots for content generation
|
||||
if len(chunkStr) > 0 {
|
||||
if contentBlockCount == 0 {
|
||||
contentBlockCount++
|
||||
fmt.Printf("\n\n 📝 GENERATING RESPONSE: ")
|
||||
fmt.Print("\n │ ")
|
||||
}
|
||||
fmt.Print("•")
|
||||
streamedContent.WriteString(chunkStr)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
}
|
||||
|
||||
// Stage 4: Processing
|
||||
fmt.Println("🔄 STAGE 4: PROCESSING")
|
||||
fmt.Println("─────────────────────────")
|
||||
fmt.Println(" Starting multi-step analysis with interleaved thinking...")
|
||||
|
||||
// Make initial request
|
||||
resp, err := llm.GenerateContent(ctx, messages, opts...)
|
||||
if err != nil {
|
||||
fmt.Printf("\n ❌ Error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n ℹ️ Initial response received (streaming may still be in progress)\n")
|
||||
|
||||
// Handle tool calls in a conversation loop
|
||||
maxIterations := 10 // Prevent infinite loops
|
||||
iteration := 0
|
||||
allResponses := []llms.ContentChoice{} // Store all responses for final display
|
||||
|
||||
// Store initial response
|
||||
if resp != nil && len(resp.Choices) > 0 {
|
||||
allResponses = append(allResponses, *resp.Choices[0])
|
||||
}
|
||||
|
||||
for iteration < maxIterations && resp != nil && len(resp.Choices) > 0 {
|
||||
choice := resp.Choices[0]
|
||||
|
||||
// Check if there are tool calls to process
|
||||
// Note: Tool calls appear in the response AFTER streaming completes
|
||||
if len(choice.ToolCalls) > 0 {
|
||||
fmt.Printf("\n\n 🔄 Iteration %d: Processing %d tool calls...\n", iteration+1, len(choice.ToolCalls))
|
||||
for i, tc := range choice.ToolCalls {
|
||||
fmt.Printf(" Tool %d: %s\n", i+1, tc.FunctionCall.Name)
|
||||
}
|
||||
|
||||
// Add assistant message with tool calls to conversation
|
||||
assistantParts := []llms.ContentPart{}
|
||||
if choice.Content != "" {
|
||||
assistantParts = append(assistantParts, llms.TextPart(choice.Content))
|
||||
}
|
||||
assistantParts = append(assistantParts, convertToolCallsToParts(choice.ToolCalls)...)
|
||||
|
||||
messages = append(messages, llms.MessageContent{
|
||||
Role: llms.ChatMessageTypeAI,
|
||||
Parts: assistantParts,
|
||||
})
|
||||
|
||||
// Execute tools and add results to conversation
|
||||
for _, tc := range choice.ToolCalls {
|
||||
result := executeToolCall(tc.FunctionCall.Name, tc.FunctionCall.Arguments)
|
||||
|
||||
// Add tool result to messages
|
||||
messages = append(messages, llms.MessageContent{
|
||||
Role: llms.ChatMessageTypeTool,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.ToolCallResponse{
|
||||
ToolCallID: tc.ID,
|
||||
Name: tc.FunctionCall.Name,
|
||||
Content: result,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Continue conversation with tool results
|
||||
fmt.Printf("\n 📤 Sending tool results back to model (iteration %d)...\n", iteration+1)
|
||||
|
||||
// Remove streaming for subsequent calls to avoid duplicate output
|
||||
continuationOpts := make([]llms.CallOption, 0, len(opts))
|
||||
for _, opt := range opts {
|
||||
continuationOpts = append(continuationOpts, opt)
|
||||
}
|
||||
// Override streaming for continuation
|
||||
continuationOpts = append(continuationOpts, llms.WithStreamingFunc(nil))
|
||||
|
||||
resp, err = llm.GenerateContent(ctx, messages, continuationOpts...)
|
||||
if err != nil {
|
||||
fmt.Printf("\n ⚠️ Error in iteration %d: %v\n", iteration+1, err)
|
||||
break
|
||||
}
|
||||
|
||||
// Store this response
|
||||
if resp != nil && len(resp.Choices) > 0 {
|
||||
allResponses = append(allResponses, *resp.Choices[0])
|
||||
fmt.Printf("\n ✅ Received response with %d tool calls\n", len(resp.Choices[0].ToolCalls))
|
||||
}
|
||||
|
||||
iteration++
|
||||
} else {
|
||||
// No more tool calls, conversation complete
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if iteration >= maxIterations {
|
||||
fmt.Println("\n ⚠️ Reached maximum iterations, stopping conversation loop")
|
||||
}
|
||||
|
||||
// Ensure we have a clean line after streaming
|
||||
fmt.Println("\n")
|
||||
|
||||
// Stage 5: Results Analysis
|
||||
fmt.Println("✅ STAGE 5: RESULTS ANALYSIS")
|
||||
fmt.Println("─────────────────────────")
|
||||
|
||||
// Show final iteration count and tool call summary
|
||||
fmt.Printf("\n 📊 Completed after %d iteration(s)\n", iteration)
|
||||
|
||||
// Count total tool calls processed
|
||||
totalToolCalls := 0
|
||||
if resp != nil && len(resp.Choices) > 0 {
|
||||
totalToolCalls = len(resp.Choices[0].ToolCalls)
|
||||
if totalToolCalls > 0 {
|
||||
fmt.Printf(" ✅ Total tool calls executed: %d\n", totalToolCalls)
|
||||
|
||||
// Show if they were parallel
|
||||
if totalToolCalls > 1 {
|
||||
fmt.Printf(" 🚀 Tools were executed in PARALLEL!\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for tool calls in the final response
|
||||
if resp != nil && len(resp.Choices) > 0 {
|
||||
for i, choice := range resp.Choices {
|
||||
// Display any thinking content from GenerationInfo
|
||||
if choice.GenerationInfo != nil {
|
||||
if thinking, ok := choice.GenerationInfo["ThinkingContent"].(string); ok && thinking != "" {
|
||||
fmt.Println("\n 📝 Captured Thinking Process:")
|
||||
fmt.Println(" ├─────────────────────────────")
|
||||
// Display first 500 chars of thinking
|
||||
thinkingPreview := thinking
|
||||
if len(thinking) > 500 {
|
||||
thinkingPreview = thinking[:500] + "..."
|
||||
}
|
||||
// Indent the thinking content
|
||||
lines := strings.Split(thinkingPreview, "\n")
|
||||
for _, line := range lines {
|
||||
fmt.Printf(" │ %s\n", line)
|
||||
}
|
||||
fmt.Println(" └─────────────────────────────")
|
||||
}
|
||||
}
|
||||
|
||||
// Display tool calls
|
||||
if len(choice.ToolCalls) > 0 {
|
||||
fmt.Printf("\n 🔧 Tool Execution Summary:\n")
|
||||
fmt.Println(" ├─────────────────────────────")
|
||||
for j, tc := range choice.ToolCalls {
|
||||
fmt.Printf(" │ Call %d: %s\n", j+1, tc.FunctionCall.Name)
|
||||
fmt.Printf(" │ Args: %s\n", tc.FunctionCall.Arguments)
|
||||
// Simulate tool execution
|
||||
result := executeToolCall(tc.FunctionCall.Name, tc.FunctionCall.Arguments)
|
||||
fmt.Printf(" │ → %s\n", result)
|
||||
if j < len(choice.ToolCalls)-1 {
|
||||
fmt.Println(" │")
|
||||
}
|
||||
}
|
||||
fmt.Println(" └─────────────────────────────")
|
||||
}
|
||||
|
||||
// Display content if any
|
||||
if choice.Content != "" {
|
||||
fmt.Printf("\n 💬 Final Response %d:\n", i+1)
|
||||
fmt.Println(" ├─────────────────────────────")
|
||||
// Indent the response content
|
||||
lines := strings.Split(choice.Content, "\n")
|
||||
for _, line := range lines {
|
||||
if line != "" {
|
||||
fmt.Printf(" │ %s\n", line)
|
||||
}
|
||||
}
|
||||
fmt.Println(" └─────────────────────────────")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stream statistics
|
||||
if streamedContent.Len() > 0 {
|
||||
fmt.Println("\n 📊 Stream Statistics:")
|
||||
fmt.Println(" ├─────────────────────────────")
|
||||
fmt.Printf(" │ Streamed Text: %d bytes\n", streamedContent.Len())
|
||||
fmt.Printf(" │ Response Blocks: %d\n", contentBlockCount)
|
||||
fmt.Println(" └─────────────────────────────")
|
||||
}
|
||||
|
||||
// Stage 6: Token Metrics
|
||||
fmt.Println("\n📈 STAGE 6: TOKEN METRICS")
|
||||
fmt.Println("─────────────────────────")
|
||||
|
||||
// Try to get token info from GenerationInfo or from captured streaming data
|
||||
var inputTokens, outputTokens, totalTokens int
|
||||
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
// Try different possible field names
|
||||
if v, ok := genInfo["PromptTokens"].(int); ok {
|
||||
inputTokens = v
|
||||
} else if v, ok := genInfo["InputTokens"].(int); ok {
|
||||
inputTokens = v
|
||||
} else if v, ok := genInfo["prompt_tokens"].(int); ok {
|
||||
inputTokens = v
|
||||
}
|
||||
|
||||
if v, ok := genInfo["CompletionTokens"].(int); ok {
|
||||
outputTokens = v
|
||||
} else if v, ok := genInfo["OutputTokens"].(int); ok {
|
||||
outputTokens = v
|
||||
} else if v, ok := genInfo["completion_tokens"].(int); ok {
|
||||
outputTokens = v
|
||||
}
|
||||
|
||||
if v, ok := genInfo["TotalTokens"].(int); ok {
|
||||
totalTokens = v
|
||||
} else if v, ok := genInfo["total_tokens"].(int); ok {
|
||||
totalTokens = v
|
||||
} else {
|
||||
totalTokens = inputTokens + outputTokens
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fmt.Println(" Token Usage Summary:")
|
||||
fmt.Println(" ├─────────────────────────────")
|
||||
fmt.Printf(" │ Input Tokens: %d\n", inputTokens)
|
||||
fmt.Printf(" │ Output Tokens: %d\n", outputTokens)
|
||||
fmt.Printf(" │ Total Tokens: %d\n", totalTokens)
|
||||
fmt.Println(" └─────────────────────────────")
|
||||
|
||||
// Extract thinking token details
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
usage := llms.ExtractThinkingTokens(genInfo)
|
||||
if usage != nil && usage.ThinkingTokens > 0 {
|
||||
fmt.Println("\n Interleaved Thinking Analysis:")
|
||||
fmt.Println(" ├─────────────────────────────")
|
||||
fmt.Printf(" │ Thinking Tokens: %d\n", usage.ThinkingTokens)
|
||||
fmt.Printf(" │ Visible Output: %d\n", outputTokens-usage.ThinkingTokens)
|
||||
fmt.Printf(" │ Thinking Ratio: %.1f%% of output\n",
|
||||
float64(usage.ThinkingTokens)/float64(outputTokens)*100)
|
||||
|
||||
if usage.ThinkingBudgetAllocated > 0 {
|
||||
fmt.Printf(" │ Budget Allocated: %d\n", usage.ThinkingBudgetAllocated)
|
||||
fmt.Printf(" │ Budget Used: %d\n", usage.ThinkingBudgetUsed)
|
||||
fmt.Printf(" │ Budget Efficiency: %.1f%%\n",
|
||||
float64(usage.ThinkingBudgetUsed)/float64(usage.ThinkingBudgetAllocated)*100)
|
||||
}
|
||||
fmt.Println(" └─────────────────────────────")
|
||||
|
||||
fmt.Println("\n 💡 Thinking Benefits:")
|
||||
fmt.Println(" • Planning tool usage strategy")
|
||||
fmt.Println(" • Interpreting intermediate results")
|
||||
fmt.Println(" • Synthesizing multi-step analysis")
|
||||
fmt.Println(" • Ensuring logical coherence")
|
||||
}
|
||||
}
|
||||
|
||||
// Final Summary
|
||||
fmt.Println("\n╔════════════════════════════════════════════════════════════╗")
|
||||
fmt.Println("║ DEMO COMPLETE ║")
|
||||
fmt.Println("╚════════════════════════════════════════════════════════════╝")
|
||||
|
||||
fmt.Println("\n🎯 Key Features Demonstrated:")
|
||||
fmt.Println(" ✓ Interleaved thinking between tool calls")
|
||||
fmt.Println(" ✓ Real-time progress tracking with stages")
|
||||
fmt.Println(" ✓ Clear visualization of thinking blocks")
|
||||
fmt.Println(" ✓ Tool orchestration with results")
|
||||
fmt.Println(" ✓ Token usage analysis with thinking breakdown")
|
||||
|
||||
fmt.Println("\n📚 Use Cases:")
|
||||
fmt.Println(" • Complex multi-step analysis tasks")
|
||||
fmt.Println(" • Data processing with reasoning")
|
||||
fmt.Println(" • Research requiring tool coordination")
|
||||
fmt.Println(" • Decision-making with transparent logic")
|
||||
}
|
||||
|
||||
// convertToolCallsToParts converts tool calls to content parts
|
||||
func convertToolCallsToParts(toolCalls []llms.ToolCall) []llms.ContentPart {
|
||||
parts := make([]llms.ContentPart, 0, len(toolCalls))
|
||||
for _, tc := range toolCalls {
|
||||
parts = append(parts, llms.ToolCall{
|
||||
ID: tc.ID,
|
||||
Type: tc.Type,
|
||||
FunctionCall: tc.FunctionCall,
|
||||
})
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
// Simulate tool execution (in real use, these would call actual functions)
|
||||
func executeToolCall(name string, arguments string) string {
|
||||
var args map[string]interface{}
|
||||
json.Unmarshal([]byte(arguments), &args)
|
||||
|
||||
switch name {
|
||||
case "calculate":
|
||||
expr, _ := args["expression"].(string)
|
||||
// Simulate various calculations
|
||||
switch {
|
||||
case strings.Contains(expr, "19.2") && strings.Contains(expr, "16.1"):
|
||||
return "19.25"
|
||||
case strings.Contains(expr, "(17.4/15.3"):
|
||||
return "0.0534" // Average quarterly growth
|
||||
case strings.Contains(expr, "19.2") && strings.Contains(expr, "12.5"):
|
||||
return "53.6"
|
||||
case strings.Contains(expr, "12.5") && strings.Contains(expr, "14.2") && strings.Contains(expr, "/4"):
|
||||
return "14.15"
|
||||
case strings.Contains(expr, "15.3") && strings.Contains(expr, "17.4") && strings.Contains(expr, "/4"):
|
||||
return "17.2"
|
||||
default:
|
||||
// Generic calculation result
|
||||
return fmt.Sprintf("Result: %.2f", 15.5)
|
||||
}
|
||||
|
||||
case "search_knowledge":
|
||||
query, _ := args["query"].(string)
|
||||
// Return different results based on query
|
||||
if strings.Contains(query, "seasonal") {
|
||||
return "Seasonal patterns show Q4 typically strongest due to holiday shopping, Q1 weakest post-holiday"
|
||||
} else if strings.Contains(query, "factors") {
|
||||
return "Key growth drivers: digital transformation, market expansion, improved supply chain efficiency"
|
||||
} else if strings.Contains(query, "economic") {
|
||||
return "Economic indicators: consumer confidence up 12%, GDP growth 2.8%, inflation moderating at 3.2%"
|
||||
}
|
||||
return fmt.Sprintf("Research data for '%s' retrieved", query)
|
||||
|
||||
case "analyze_data":
|
||||
analysisType, _ := args["analysis_type"].(string)
|
||||
data, _ := args["data"].([]interface{})
|
||||
|
||||
switch analysisType {
|
||||
case "trend":
|
||||
return "Trend: Consistent upward trajectory with 7.2% quarter-over-quarter average growth"
|
||||
case "std_dev":
|
||||
return "Standard Deviation: 2.41 million (moderate volatility)"
|
||||
case "mean":
|
||||
return "Mean: 15.675 million average across all quarters"
|
||||
default:
|
||||
return fmt.Sprintf("Analysis complete for %s with %d data points", analysisType, len(data))
|
||||
}
|
||||
|
||||
case "make_prediction":
|
||||
baseValue, _ := args["base_value"].(float64)
|
||||
growthRate, _ := args["growth_rate"].(float64)
|
||||
seasonality, _ := args["seasonality_factor"].(float64)
|
||||
confidence, _ := args["confidence_level"].(string)
|
||||
|
||||
// Calculate prediction
|
||||
prediction := baseValue * (1 + growthRate) * seasonality
|
||||
|
||||
return fmt.Sprintf("Prediction: %.2f million (confidence: %s, growth: %.1f%%, seasonality: %.2fx)",
|
||||
prediction, confidence, growthRate*100, seasonality)
|
||||
|
||||
case "generate_report":
|
||||
growthRate, _ := args["growth_rate"].(float64)
|
||||
trendDir, _ := args["trend_direction"].(string)
|
||||
volatility, _ := args["volatility_level"].(string)
|
||||
prediction, _ := args["prediction"].(float64)
|
||||
|
||||
return fmt.Sprintf("Strategic Report Generated:\n"+
|
||||
"- YoY Growth: %.1f%%\n"+
|
||||
"- Trend: %s\n"+
|
||||
"- Volatility: %s\n"+
|
||||
"- Q1-2025 Forecast: %.2f million\n"+
|
||||
"- Recommendation: Continue growth strategy with focus on Q4 seasonality",
|
||||
growthRate, trendDir, volatility, prediction)
|
||||
|
||||
default:
|
||||
return "Tool execution completed"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/generative-ai-go/genai"
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
"github.com/tmc/langchaingo/llms/googleai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
apiKey := os.Getenv("GOOGLE_API_KEY")
|
||||
if apiKey == "" {
|
||||
log.Fatal("Please set GOOGLE_API_KEY environment variable")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
fmt.Println("=== Google AI Reasoning & Caching Demo ===")
|
||||
fmt.Println()
|
||||
|
||||
// Part 1: Demonstrate reasoning support
|
||||
demonstrateReasoning(ctx, apiKey)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
fmt.Println()
|
||||
|
||||
// Part 2: Demonstrate caching support
|
||||
demonstrateCaching(ctx, apiKey)
|
||||
}
|
||||
|
||||
func demonstrateReasoning(ctx context.Context, apiKey string) {
|
||||
fmt.Println("Part 1: Reasoning Support")
|
||||
fmt.Println(strings.Repeat("-", 40))
|
||||
|
||||
// Create client with Gemini 2.0 Flash (supports reasoning)
|
||||
client, err := googleai.New(ctx,
|
||||
googleai.WithAPIKey(apiKey),
|
||||
googleai.WithDefaultModel("gemini-2.0-flash"),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Check if model supports reasoning
|
||||
if reasoner, ok := interface{}(client).(llms.ReasoningModel); ok {
|
||||
fmt.Printf("Model supports reasoning: %v\n", reasoner.SupportsReasoning())
|
||||
}
|
||||
|
||||
// Test with a complex reasoning problem
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart(`A farmer has 17 sheep. All but 9 of them run away.
|
||||
How many sheep does the farmer have left? Think step by step.`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Println("\nSending reasoning query...")
|
||||
resp, err := client.GenerateContent(ctx, messages,
|
||||
llms.WithMaxTokens(300),
|
||||
llms.WithThinkingMode(llms.ThinkingModeMedium), // Note: Google AI may not use this yet
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Error generating content: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Choices) > 0 {
|
||||
fmt.Println("\nResponse:")
|
||||
fmt.Println(resp.Choices[0].Content)
|
||||
|
||||
// Show token usage
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
if tokens, ok := genInfo["TotalTokens"].(int32); ok {
|
||||
fmt.Printf("\nTotal tokens used: %d\n", tokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func demonstrateCaching(ctx context.Context, apiKey string) {
|
||||
fmt.Println("Part 2: Caching Support")
|
||||
fmt.Println(strings.Repeat("-", 40))
|
||||
fmt.Println("Note: Google AI caching requires pre-creating cached content")
|
||||
fmt.Println()
|
||||
|
||||
helper := setupCachingHelper(ctx, apiKey)
|
||||
cached := createCachedContent(ctx, helper)
|
||||
runCachedRequests(ctx, apiKey, cached.Name)
|
||||
}
|
||||
|
||||
func setupCachingHelper(ctx context.Context, apiKey string) *googleai.CachingHelper {
|
||||
helper, err := googleai.NewCachingHelper(ctx, googleai.WithAPIKey(apiKey))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create caching helper: %v", err)
|
||||
}
|
||||
return helper
|
||||
}
|
||||
|
||||
func createCachedContent(ctx context.Context, helper *googleai.CachingHelper) *genai.CachedContent {
|
||||
// Create a large context to cache
|
||||
largeContext := `You are an expert Go programming assistant.
|
||||
You have deep knowledge of Go best practices, performance optimization, and idiomatic code patterns.
|
||||
Always consider:
|
||||
- Error handling patterns
|
||||
- Goroutine safety and concurrency
|
||||
- Memory efficiency
|
||||
- Code readability and maintainability
|
||||
` + strings.Repeat("Always write clean, efficient, and well-documented code. ", 50)
|
||||
|
||||
fmt.Println("Creating cached content...")
|
||||
cached, err := helper.CreateCachedContent(ctx, "gemini-2.0-flash",
|
||||
[]llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeSystem,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart(largeContext),
|
||||
},
|
||||
},
|
||||
},
|
||||
5*time.Minute, // Cache for 5 minutes
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create cached content: %v", err)
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if err := helper.DeleteCachedContent(ctx, cached.Name); err != nil {
|
||||
log.Printf("Failed to delete cached content: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Printf("Cached content created: %s\n", cached.Name)
|
||||
fmt.Printf("Token count: %d\n", cached.UsageMetadata.TotalTokenCount)
|
||||
return cached
|
||||
}
|
||||
|
||||
func runCachedRequests(ctx context.Context, apiKey, cachedContentName string) {
|
||||
client, err := googleai.New(ctx, googleai.WithAPIKey(apiKey))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
questions := []string{
|
||||
"What's the best way to handle errors in Go?",
|
||||
"How do I avoid goroutine leaks?",
|
||||
}
|
||||
|
||||
for i, question := range questions {
|
||||
fmt.Printf("\n--- Request %d: %s ---\n", i+1, question)
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart(question),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.GenerateContent(ctx, messages,
|
||||
googleai.WithCachedContent(cachedContentName),
|
||||
llms.WithMaxTokens(200),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(resp.Choices) > 0 {
|
||||
// Show truncated response
|
||||
content := resp.Choices[0].Content
|
||||
if len(content) > 150 {
|
||||
content = content[:150] + "..."
|
||||
}
|
||||
fmt.Printf("Response: %s\n", content)
|
||||
|
||||
// Check for cached tokens
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
if cachedTokens, ok := genInfo["CachedTokens"].(int32); ok && cachedTokens > 0 {
|
||||
fmt.Printf("Cached tokens used: %d ✓\n", cachedTokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n✨ Google AI caching helps reduce costs by reusing pre-processed context!")
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
"github.com/tmc/langchaingo/llms/ollama"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("=== Ollama Reasoning & Caching Demo ===")
|
||||
fmt.Println()
|
||||
|
||||
// Get Ollama server URL from environment or use default
|
||||
serverURL := os.Getenv("OLLAMA_HOST")
|
||||
if serverURL == "" {
|
||||
serverURL = "http://localhost:11434"
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Part 1: Demonstrate reasoning support
|
||||
demonstrateReasoning(ctx, serverURL)
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
fmt.Println()
|
||||
|
||||
// Part 2: Demonstrate context caching
|
||||
demonstrateCaching(ctx, serverURL)
|
||||
}
|
||||
|
||||
func demonstrateReasoning(ctx context.Context, serverURL string) {
|
||||
fmt.Println("Part 1: Reasoning Support")
|
||||
fmt.Println(strings.Repeat("-", 40))
|
||||
fmt.Println("Note: Requires a reasoning model like deepseek-r1 or qwq")
|
||||
fmt.Println()
|
||||
|
||||
// Try to use a reasoning model
|
||||
modelName := "deepseek-r1:latest"
|
||||
fmt.Printf("Attempting to use model: %s\n", modelName)
|
||||
|
||||
llm, err := ollama.New(
|
||||
ollama.WithServerURL(serverURL),
|
||||
ollama.WithModel(modelName),
|
||||
ollama.WithPullModel(), // Auto-pull if not available
|
||||
ollama.WithPullTimeout(5*time.Minute), // Allow time for download
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create Ollama client: %v", err)
|
||||
fmt.Println("Falling back to a standard model for demo...")
|
||||
|
||||
// Fall back to a standard model
|
||||
modelName = "llama3:latest"
|
||||
llm, err = ollama.New(
|
||||
ollama.WithServerURL(serverURL),
|
||||
ollama.WithModel(modelName),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create fallback client: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if model supports reasoning
|
||||
if reasoner, ok := interface{}(llm).(llms.ReasoningModel); ok {
|
||||
fmt.Printf("Model supports reasoning: %v\n", reasoner.SupportsReasoning())
|
||||
}
|
||||
|
||||
// Complex reasoning problem
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart(`Alice has 3 apples. Bob has twice as many apples as Alice.
|
||||
Charlie has 2 fewer apples than Bob. How many apples do they have in total?
|
||||
Think through this step by step.`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Println("\nSending reasoning query...")
|
||||
resp, err := llm.GenerateContent(ctx, messages,
|
||||
llms.WithMaxTokens(300),
|
||||
llms.WithThinkingMode(llms.ThinkingModeMedium), // Enable thinking if supported
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Error generating content: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Choices) > 0 {
|
||||
fmt.Println("\nResponse:")
|
||||
content := resp.Choices[0].Content
|
||||
// Truncate very long responses
|
||||
if len(content) > 500 {
|
||||
content = content[:500] + "...\n[truncated]"
|
||||
}
|
||||
fmt.Println(content)
|
||||
|
||||
// Show token usage
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
if tokens, ok := genInfo["TotalTokens"].(int); ok {
|
||||
fmt.Printf("\nTotal tokens used: %d\n", tokens)
|
||||
}
|
||||
if enabled, ok := genInfo["ThinkingEnabled"].(bool); ok && enabled {
|
||||
fmt.Println("✓ Thinking mode was enabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func demonstrateCaching(ctx context.Context, serverURL string) {
|
||||
fmt.Println("Part 2: Context Caching")
|
||||
fmt.Println(strings.Repeat("-", 40))
|
||||
fmt.Println("Using in-memory context cache to reduce redundant processing")
|
||||
fmt.Println()
|
||||
|
||||
llm, cache := setupCachingClient(serverURL)
|
||||
runCachingDemo(ctx, llm, cache)
|
||||
}
|
||||
|
||||
func setupCachingClient(serverURL string) (llms.Model, *ollama.ContextCache) {
|
||||
// Create Ollama client
|
||||
llm, err := ollama.New(
|
||||
ollama.WithServerURL(serverURL),
|
||||
ollama.WithModel("llama3:latest"), // Use any available model
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Ollama client: %v", err)
|
||||
}
|
||||
|
||||
// Create context cache
|
||||
cache := ollama.NewContextCache(10, 10*time.Minute)
|
||||
fmt.Println("Created context cache (10 entries, 10 min TTL)")
|
||||
|
||||
return llm, cache
|
||||
}
|
||||
|
||||
func runCachingDemo(ctx context.Context, llm llms.Model, cache *ollama.ContextCache) {
|
||||
// Large system context that benefits from caching
|
||||
systemContext := `You are an expert programming assistant specializing in Go.
|
||||
You have deep knowledge of Go idioms, best practices, and performance optimization.
|
||||
Always provide clear, concise, and idiomatic Go code examples.`
|
||||
|
||||
// Multiple questions using the same context
|
||||
questions := []string{
|
||||
"What's the best way to handle errors in Go?",
|
||||
"How do I create a goroutine-safe map?",
|
||||
"What are channels used for in Go?",
|
||||
}
|
||||
|
||||
for i, question := range questions {
|
||||
processQuestion(ctx, llm, cache, systemContext, question, i+1)
|
||||
}
|
||||
|
||||
// Show cache statistics
|
||||
entries, hits, avgSaved := cache.Stats()
|
||||
fmt.Printf("\n=== Cache Statistics ===\n")
|
||||
fmt.Printf("Entries: %d\n", entries)
|
||||
fmt.Printf("Total hits: %d\n", hits)
|
||||
fmt.Printf("Avg tokens saved per hit: %d\n", avgSaved)
|
||||
|
||||
if hits > 0 {
|
||||
fmt.Println("\n✨ Context caching reduced token processing and improved response times!")
|
||||
}
|
||||
}
|
||||
|
||||
func processQuestion(ctx context.Context, llm llms.Model, cache *ollama.ContextCache, systemContext, question string, requestNum int) {
|
||||
fmt.Printf("\n--- Request %d: %s ---\n", requestNum, question)
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeSystem,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart(systemContext),
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart(question),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
resp, err := llm.GenerateContent(ctx, messages,
|
||||
llms.WithMaxTokens(150),
|
||||
ollama.WithContextCache(cache), // Use context cache
|
||||
)
|
||||
elapsed := time.Since(startTime)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Choices) > 0 {
|
||||
// Show truncated response
|
||||
content := resp.Choices[0].Content
|
||||
if len(content) > 200 {
|
||||
content = content[:200] + "..."
|
||||
}
|
||||
fmt.Printf("Response: %s\n", content)
|
||||
|
||||
// Check cache status
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
if hit, ok := genInfo["CacheHit"].(bool); ok {
|
||||
if hit {
|
||||
fmt.Printf("✓ Cache HIT - ")
|
||||
if cached, ok := genInfo["CachedTokens"].(int); ok {
|
||||
fmt.Printf("reused %d tokens", cached)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("✗ Cache MISS - context stored for reuse")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
if tokens, ok := genInfo["PromptTokens"].(int); ok {
|
||||
fmt.Printf("Prompt tokens: %d, ", tokens)
|
||||
}
|
||||
fmt.Printf("Response time: %v\n", elapsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
module github.com/tmc/langchaingo/examples/prompt-caching
|
||||
|
||||
go 1.23.8
|
||||
|
||||
toolchain go1.24.6
|
||||
|
||||
require github.com/tmc/langchaingo v0.1.14-pre.3
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
||||
)
|
||||
|
||||
replace github.com/tmc/langchaingo => ../..
|
||||
@@ -0,0 +1,22 @@
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
@@ -0,0 +1,251 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
"github.com/tmc/langchaingo/llms/anthropic"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
fmt.Println("=== Prompt Caching Demo ===")
|
||||
fmt.Println("Demonstrating cost savings with Anthropic's prompt caching")
|
||||
fmt.Println()
|
||||
|
||||
if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey == "" {
|
||||
fmt.Println("Error: ANTHROPIC_API_KEY environment variable not set")
|
||||
fmt.Println("\nTo run this demo:")
|
||||
fmt.Println(" export ANTHROPIC_API_KEY='your-api-key'")
|
||||
fmt.Println(" go run main.go")
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize Anthropic client
|
||||
llm, err := anthropic.New(anthropic.WithModel("claude-3-5-sonnet-20241022"))
|
||||
if err != nil {
|
||||
fmt.Printf("Error initializing Anthropic: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Large context that will be cached (minimum 1024 tokens for caching)
|
||||
largeContext := `You are an expert software architect with deep knowledge of system design patterns.
|
||||
|
||||
## System Design Patterns Reference
|
||||
|
||||
### 1. Microservices Architecture
|
||||
- Service decomposition based on business capabilities
|
||||
- Independent deployment and scaling
|
||||
- Service discovery and registration
|
||||
- API Gateway pattern for unified entry point
|
||||
- Circuit breaker for fault tolerance
|
||||
- Event-driven communication via message queues
|
||||
- Database per service for data isolation
|
||||
- Saga pattern for distributed transactions
|
||||
|
||||
### 2. Event-Driven Architecture
|
||||
- Event sourcing for audit trails
|
||||
- CQRS (Command Query Responsibility Segregation)
|
||||
- Event streaming with Apache Kafka or similar
|
||||
- Event store for event persistence
|
||||
- Projections for read models
|
||||
- Eventual consistency considerations
|
||||
|
||||
### 3. Caching Strategies
|
||||
- Cache-aside (lazy loading)
|
||||
- Write-through caching
|
||||
- Write-behind caching
|
||||
- Distributed caching with Redis/Memcached
|
||||
- CDN for static content
|
||||
- Application-level caching
|
||||
- Database query result caching
|
||||
|
||||
### 4. Load Balancing
|
||||
- Round-robin distribution
|
||||
- Least connections algorithm
|
||||
- IP hash for session affinity
|
||||
- Weighted distribution
|
||||
- Health checks and failover
|
||||
- Geographic load balancing
|
||||
|
||||
### 5. Data Storage Patterns
|
||||
- SQL vs NoSQL selection criteria
|
||||
- Sharding for horizontal scaling
|
||||
- Read replicas for read-heavy workloads
|
||||
- Master-slave replication
|
||||
- Multi-master replication
|
||||
- Time-series databases for metrics
|
||||
- Object storage for large files
|
||||
|
||||
### 6. Security Patterns
|
||||
- Authentication vs Authorization
|
||||
- OAuth 2.0 and OpenID Connect
|
||||
- JWT tokens for stateless auth
|
||||
- API key management
|
||||
- Rate limiting and throttling
|
||||
- WAF (Web Application Firewall)
|
||||
- Encryption at rest and in transit
|
||||
|
||||
### 7. Monitoring and Observability
|
||||
- Distributed tracing (OpenTelemetry)
|
||||
- Centralized logging (ELK stack)
|
||||
- Metrics collection (Prometheus)
|
||||
- Alerting and incident management
|
||||
- Performance monitoring
|
||||
- Error tracking and reporting
|
||||
|
||||
### 8. Deployment Patterns
|
||||
- Blue-green deployments
|
||||
- Canary releases
|
||||
- Feature flags
|
||||
- Rolling updates
|
||||
- Immutable infrastructure
|
||||
- Infrastructure as Code (Terraform)
|
||||
- Container orchestration (Kubernetes)
|
||||
|
||||
When answering questions, consider these patterns and provide specific, actionable recommendations.`
|
||||
|
||||
fmt.Println("Context Size:", len(largeContext), "characters")
|
||||
fmt.Println("(Approximately", len(strings.Fields(largeContext)), "words)")
|
||||
fmt.Println()
|
||||
|
||||
// Series of questions using the same cached context
|
||||
questions := []string{
|
||||
"What caching strategy would you recommend for a read-heavy e-commerce product catalog?",
|
||||
"How should I implement authentication for a microservices architecture?",
|
||||
"What's the best approach for handling distributed transactions across services?",
|
||||
"How can I ensure high availability for a global application?",
|
||||
}
|
||||
|
||||
var totalCachedTokens, totalSavedTokens int
|
||||
|
||||
for i, question := range questions {
|
||||
fmt.Printf("%s\n", strings.Repeat("=", 60))
|
||||
fmt.Printf("Request %d: %s\n", i+1, question)
|
||||
fmt.Printf("%s\n", strings.Repeat("-", 60))
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeSystem,
|
||||
Parts: []llms.ContentPart{
|
||||
// Mark the large context for caching
|
||||
llms.WithCacheControl(llms.TextPart(largeContext), anthropic.EphemeralCache()),
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart(question),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := llm.GenerateContent(ctx, messages,
|
||||
llms.WithMaxTokens(200),
|
||||
anthropic.WithPromptCaching(), // Enable prompt caching beta feature
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Display response (truncated)
|
||||
content := resp.Choices[0].Content
|
||||
if len(content) > 250 {
|
||||
content = content[:250] + "..."
|
||||
}
|
||||
fmt.Printf("\nResponse: %s\n", content)
|
||||
|
||||
// Display caching metrics
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
// Extract cache token information manually from generation info
|
||||
usage := extractCacheUsage(genInfo)
|
||||
|
||||
fmt.Printf("\nToken Usage:\n")
|
||||
fmt.Printf(" Input Tokens: %d\n", usage.InputTokens)
|
||||
fmt.Printf(" Output Tokens: %d\n", usage.OutputTokens)
|
||||
|
||||
if usage.CacheCreationInputTokens > 0 {
|
||||
fmt.Printf(" Cache Creation: %d tokens (25%% premium for initial caching)\n",
|
||||
usage.CacheCreationInputTokens)
|
||||
}
|
||||
|
||||
if usage.CachedInputTokens > 0 {
|
||||
fmt.Printf(" Cached Tokens: %d (%.0f%% discount applied) ✓\n",
|
||||
usage.CachedInputTokens, usage.CacheDiscountPercent)
|
||||
|
||||
savedTokens := int(float64(usage.CachedInputTokens) * (usage.CacheDiscountPercent / 100.0))
|
||||
totalCachedTokens += usage.CachedInputTokens
|
||||
totalSavedTokens += savedTokens
|
||||
|
||||
fmt.Printf(" Token Savings: %d tokens\n", savedTokens)
|
||||
} else if i > 0 {
|
||||
fmt.Println(" Cache Status: MISS (context not cached)")
|
||||
} else {
|
||||
fmt.Println(" Cache Status: CREATING (first request)")
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Display summary
|
||||
fmt.Printf("%s\n", strings.Repeat("=", 60))
|
||||
fmt.Println("CACHING SUMMARY")
|
||||
fmt.Printf("%s\n", strings.Repeat("=", 60))
|
||||
fmt.Printf("Total Requests: %d\n", len(questions))
|
||||
fmt.Printf("Total Cached Tokens: %d\n", totalCachedTokens)
|
||||
fmt.Printf("Total Token Savings: %d\n", totalSavedTokens)
|
||||
if totalCachedTokens > 0 {
|
||||
fmt.Printf("Average Discount: 90%%\n")
|
||||
fmt.Printf("\nCost Reduction: ~%.0f%% on input tokens after first request\n",
|
||||
90.0) // Anthropic provides 90% discount on cached tokens
|
||||
}
|
||||
|
||||
fmt.Println("\nKey Benefits:")
|
||||
fmt.Println("✓ Significant cost reduction for repeated context")
|
||||
fmt.Println("✓ Faster response times (pre-processed context)")
|
||||
fmt.Println("✓ Consistent context across multiple queries")
|
||||
fmt.Println("✓ Ideal for chatbots, Q&A systems, and analysis tools")
|
||||
}
|
||||
|
||||
// CacheUsage represents token usage with caching information
|
||||
type CacheUsage struct {
|
||||
InputTokens int
|
||||
OutputTokens int
|
||||
CacheCreationInputTokens int
|
||||
CachedInputTokens int
|
||||
CacheDiscountPercent float64
|
||||
}
|
||||
|
||||
// extractCacheUsage extracts cache-related token information from generation info
|
||||
func extractCacheUsage(genInfo map[string]any) *CacheUsage {
|
||||
usage := &CacheUsage{}
|
||||
|
||||
// Standard token fields
|
||||
if v, ok := genInfo["InputTokens"].(int); ok {
|
||||
usage.InputTokens = v
|
||||
}
|
||||
if v, ok := genInfo["OutputTokens"].(int); ok {
|
||||
usage.OutputTokens = v
|
||||
}
|
||||
|
||||
// Cache-specific fields (Anthropic)
|
||||
if v, ok := genInfo["CacheCreationInputTokens"].(int); ok {
|
||||
usage.CacheCreationInputTokens = v
|
||||
}
|
||||
if v, ok := genInfo["CacheReadInputTokens"].(int); ok {
|
||||
usage.CachedInputTokens = v
|
||||
}
|
||||
|
||||
// Calculate discount (Anthropic provides 90% discount on cached tokens)
|
||||
if usage.CachedInputTokens > 0 {
|
||||
usage.CacheDiscountPercent = 90.0
|
||||
}
|
||||
|
||||
return usage
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
module github.com/tmc/langchaingo/examples/reasoning-tokens
|
||||
|
||||
go 1.23.8
|
||||
|
||||
toolchain go1.24.6
|
||||
|
||||
require github.com/tmc/langchaingo v0.1.14-pre.3
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
||||
)
|
||||
|
||||
replace github.com/tmc/langchaingo => ../..
|
||||
@@ -0,0 +1,22 @@
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
|
||||
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
@@ -0,0 +1,179 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
"github.com/tmc/langchaingo/llms/anthropic"
|
||||
"github.com/tmc/langchaingo/llms/openai"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
fmt.Println("=== Multi-Backend Reasoning/Thinking Tokens Demo ===")
|
||||
fmt.Println("Comparing reasoning capabilities across LLM providers")
|
||||
fmt.Println()
|
||||
|
||||
// Complex reasoning prompt that benefits from step-by-step thinking
|
||||
prompt := `A farmer needs to cross a river with a fox, a chicken, and a bag of grain.
|
||||
The boat can only carry the farmer and one item at a time.
|
||||
If left alone, the fox will eat the chicken, and the chicken will eat the grain.
|
||||
How can the farmer get everything across safely? Think through this step-by-step.`
|
||||
|
||||
// Test with OpenAI o1-mini (reasoning model)
|
||||
if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
fmt.Println("OpenAI o1-mini (Reasoning Model)")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
llm, err := openai.New(openai.WithModel("o1-mini"))
|
||||
if err != nil {
|
||||
fmt.Printf("Error initializing OpenAI: %v\n\n", err)
|
||||
} else {
|
||||
testReasoning(ctx, llm, "o1-mini", prompt, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Test with Anthropic Claude 3.7 (supports extended thinking)
|
||||
if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
fmt.Println("Anthropic Claude 3.7 Sonnet (Extended Thinking)")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
llm, err := anthropic.New(anthropic.WithModel("claude-3-7-sonnet-20250219"))
|
||||
if err != nil {
|
||||
fmt.Printf("Error initializing Anthropic: %v\n\n", err)
|
||||
} else {
|
||||
testReasoning(ctx, llm, "claude-3-7-sonnet-20250219", prompt, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Compare with standard models (no reasoning tokens)
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
fmt.Println("COMPARISON: Standard Models (No Reasoning)")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
|
||||
fmt.Println("\n--- OpenAI GPT-4 Turbo ---")
|
||||
llm, err := openai.New(openai.WithModel("gpt-4-turbo-preview"))
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
testReasoning(ctx, llm, "gpt-4-turbo-preview", prompt, false)
|
||||
}
|
||||
}
|
||||
|
||||
if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
|
||||
fmt.Println("\n--- Anthropic Claude 3 Sonnet ---")
|
||||
llm, err := anthropic.New(anthropic.WithModel("claude-3-sonnet-20240229"))
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
testReasoning(ctx, llm, "claude-3-sonnet-20240229", prompt, false)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println("Demo Complete!")
|
||||
fmt.Println("\nKey Observations:")
|
||||
fmt.Println("- Reasoning models use additional 'thinking' tokens for internal processing")
|
||||
fmt.Println("- These tokens improve response quality but aren't shown in the output")
|
||||
fmt.Println("- Standard models generate all tokens as visible output")
|
||||
}
|
||||
|
||||
func testReasoning(ctx context.Context, llm llms.Model, modelName string, prompt string, expectReasoning bool) {
|
||||
// Check if model supports reasoning
|
||||
supportsReasoning := false
|
||||
if reasoner, ok := llm.(llms.ReasoningModel); ok {
|
||||
supportsReasoning = reasoner.SupportsReasoning()
|
||||
}
|
||||
|
||||
fmt.Printf("Model: %s\n", modelName)
|
||||
fmt.Printf("Reasoning Support: %v\n", supportsReasoning)
|
||||
|
||||
if expectReasoning && !supportsReasoning {
|
||||
fmt.Println("Note: This model version may not support reasoning tokens yet")
|
||||
}
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{llms.TextPart(prompt)},
|
||||
},
|
||||
}
|
||||
|
||||
// Configure thinking mode based on model capabilities
|
||||
opts := []llms.CallOption{
|
||||
llms.WithMaxTokens(500),
|
||||
}
|
||||
|
||||
if supportsReasoning {
|
||||
// Use high thinking mode for complex reasoning
|
||||
opts = append(opts, llms.WithThinkingMode(llms.ThinkingModeHigh))
|
||||
fmt.Println("Thinking Mode: HIGH (maximum reasoning depth)")
|
||||
} else {
|
||||
fmt.Println("Thinking Mode: NONE (standard generation)")
|
||||
}
|
||||
|
||||
fmt.Print("\nGenerating response... ")
|
||||
resp, err := llm.GenerateContent(ctx, messages, opts...)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("done")
|
||||
|
||||
// Display response
|
||||
content := resp.Choices[0].Content
|
||||
fmt.Println("\n--- Response ---")
|
||||
if len(content) > 400 {
|
||||
fmt.Println(content[:400] + "...\n[truncated]")
|
||||
} else {
|
||||
fmt.Println(content)
|
||||
}
|
||||
|
||||
// Display detailed token metrics
|
||||
fmt.Println("\n--- Token Metrics ---")
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
// Extract standard token usage
|
||||
var inputTokens, outputTokens, totalTokens int
|
||||
if v, ok := genInfo["PromptTokens"].(int); ok {
|
||||
inputTokens = v
|
||||
}
|
||||
if v, ok := genInfo["CompletionTokens"].(int); ok {
|
||||
outputTokens = v
|
||||
}
|
||||
if v, ok := genInfo["TotalTokens"].(int); ok {
|
||||
totalTokens = v
|
||||
}
|
||||
|
||||
fmt.Printf("Input Tokens: %d\n", inputTokens)
|
||||
fmt.Printf("Output Tokens: %d\n", outputTokens)
|
||||
fmt.Printf("Total Tokens: %d\n", totalTokens)
|
||||
|
||||
// Extract thinking-specific tokens
|
||||
usage := llms.ExtractThinkingTokens(genInfo)
|
||||
if usage != nil && usage.ThinkingTokens > 0 {
|
||||
fmt.Printf("\nReasoning Breakdown:\n")
|
||||
fmt.Printf(" Thinking Tokens: %d\n", usage.ThinkingTokens)
|
||||
fmt.Printf(" Visible Output: %d\n", outputTokens-usage.ThinkingTokens)
|
||||
fmt.Printf(" Thinking Ratio: %.1f%% of output\n",
|
||||
float64(usage.ThinkingTokens)/float64(outputTokens)*100)
|
||||
|
||||
if usage.ThinkingBudgetAllocated > 0 {
|
||||
fmt.Printf(" Budget Allocated: %d\n", usage.ThinkingBudgetAllocated)
|
||||
fmt.Printf(" Budget Used: %d\n", usage.ThinkingBudgetUsed)
|
||||
fmt.Printf(" Budget Efficiency: %.1f%%\n",
|
||||
float64(usage.ThinkingBudgetUsed)/float64(usage.ThinkingBudgetAllocated)*100)
|
||||
}
|
||||
} else if supportsReasoning {
|
||||
fmt.Println("\nNote: Model supports reasoning but no thinking tokens were used.")
|
||||
fmt.Println(" This may happen for simpler prompts or certain model versions.")
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/tmc/langchaingo/callbacks"
|
||||
"github.com/tmc/langchaingo/httputil"
|
||||
@@ -32,9 +33,13 @@ const (
|
||||
type LLM struct {
|
||||
CallbacksHandler callbacks.Handler
|
||||
client *anthropicclient.Client
|
||||
model string // Track current model for reasoning detection
|
||||
}
|
||||
|
||||
var _ llms.Model = (*LLM)(nil)
|
||||
var (
|
||||
_ llms.Model = (*LLM)(nil)
|
||||
_ llms.ReasoningModel = (*LLM)(nil)
|
||||
)
|
||||
|
||||
// New returns a new Anthropic LLM.
|
||||
func New(opts ...Option) (*LLM, error) {
|
||||
@@ -44,6 +49,7 @@ func New(opts ...Option) (*LLM, error) {
|
||||
}
|
||||
return &LLM{
|
||||
client: c,
|
||||
model: c.Model, // Store the model for reasoning detection
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -136,6 +142,9 @@ func generateMessagesContent(ctx context.Context, o *LLM, messages []llms.Messag
|
||||
}
|
||||
|
||||
tools := toolsToTools(opts.Tools)
|
||||
|
||||
betaHeaders, thinking := extractThinkingOptions(o, opts)
|
||||
|
||||
result, err := o.client.CreateMessage(ctx, &anthropicclient.MessageRequest{
|
||||
Model: opts.Model,
|
||||
Messages: chatMessages,
|
||||
@@ -145,6 +154,8 @@ func generateMessagesContent(ctx context.Context, o *LLM, messages []llms.Messag
|
||||
Temperature: opts.Temperature,
|
||||
TopP: opts.TopP,
|
||||
Tools: tools,
|
||||
Thinking: thinking,
|
||||
BetaHeaders: betaHeaders,
|
||||
StreamingFunc: opts.StreamingFunc,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -153,6 +164,11 @@ func generateMessagesContent(ctx context.Context, o *LLM, messages []llms.Messag
|
||||
}
|
||||
return nil, fmt.Errorf("anthropic: failed to create message: %w", err)
|
||||
}
|
||||
return processAnthropicResponse(result)
|
||||
}
|
||||
|
||||
// processAnthropicResponse converts Anthropic API response to standard ContentResponse
|
||||
func processAnthropicResponse(result *anthropicclient.MessageResponsePayload) (*llms.ContentResponse, error) {
|
||||
if result == nil || len(result.Content) == 0 {
|
||||
return nil, ErrEmptyResponse
|
||||
}
|
||||
@@ -162,12 +178,20 @@ func generateMessagesContent(ctx context.Context, o *LLM, messages []llms.Messag
|
||||
switch content.GetType() {
|
||||
case "text":
|
||||
if textContent, ok := content.(*anthropicclient.TextContent); ok {
|
||||
// Extract thinking content from the response text
|
||||
thinkingContent, outputContent := extractThinkingFromText(textContent.Text)
|
||||
|
||||
choices[i] = &llms.ContentChoice{
|
||||
Content: textContent.Text,
|
||||
StopReason: result.StopReason,
|
||||
GenerationInfo: map[string]any{
|
||||
"InputTokens": result.Usage.InputTokens,
|
||||
"OutputTokens": result.Usage.OutputTokens,
|
||||
"InputTokens": result.Usage.InputTokens,
|
||||
"OutputTokens": result.Usage.OutputTokens,
|
||||
"CacheCreationInputTokens": result.Usage.CacheCreationInputTokens,
|
||||
"CacheReadInputTokens": result.Usage.CacheReadInputTokens,
|
||||
// Standardized fields for cross-provider compatibility
|
||||
"ThinkingContent": thinkingContent, // Standardized field
|
||||
"OutputContent": outputContent, // Standardized field
|
||||
},
|
||||
}
|
||||
} else {
|
||||
@@ -191,22 +215,40 @@ func generateMessagesContent(ctx context.Context, o *LLM, messages []llms.Messag
|
||||
},
|
||||
StopReason: result.StopReason,
|
||||
GenerationInfo: map[string]any{
|
||||
"InputTokens": result.Usage.InputTokens,
|
||||
"OutputTokens": result.Usage.OutputTokens,
|
||||
"InputTokens": result.Usage.InputTokens,
|
||||
"OutputTokens": result.Usage.OutputTokens,
|
||||
"CacheCreationInputTokens": result.Usage.CacheCreationInputTokens,
|
||||
"CacheReadInputTokens": result.Usage.CacheReadInputTokens,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("anthropic: %w for tool use message %T", ErrInvalidContentType, content)
|
||||
}
|
||||
case "thinking":
|
||||
if thinkingContent, ok := content.(*anthropicclient.ThinkingContent); ok {
|
||||
choices[i] = &llms.ContentChoice{
|
||||
Content: "", // Thinking content is not included in output
|
||||
StopReason: result.StopReason,
|
||||
GenerationInfo: map[string]any{
|
||||
"ThinkingContent": thinkingContent.Thinking,
|
||||
"ThinkingSignature": thinkingContent.Signature,
|
||||
"InputTokens": result.Usage.InputTokens,
|
||||
"OutputTokens": result.Usage.OutputTokens,
|
||||
"CacheCreationInputTokens": result.Usage.CacheCreationInputTokens,
|
||||
"CacheReadInputTokens": result.Usage.CacheReadInputTokens,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("anthropic: %w for thinking message %T", ErrInvalidContentType, content)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("anthropic: %w: %v", ErrUnsupportedContentType, content.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
resp := &llms.ContentResponse{
|
||||
return &llms.ContentResponse{
|
||||
Choices: choices,
|
||||
}
|
||||
return resp, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolsToTools(tools []llms.Tool) []anthropicclient.Tool {
|
||||
@@ -260,9 +302,19 @@ func processMessages(messages []llms.MessageContent) ([]anthropicclient.ChatMess
|
||||
}
|
||||
|
||||
func handleSystemMessage(msg llms.MessageContent) (string, error) {
|
||||
if textContent, ok := msg.Parts[0].(llms.TextContent); ok {
|
||||
// Handle both direct TextContent and CachedContent wrapper
|
||||
part := msg.Parts[0]
|
||||
|
||||
// If it's cached content, unwrap it
|
||||
if cached, ok := part.(llms.CachedContent); ok {
|
||||
part = cached.ContentPart
|
||||
}
|
||||
|
||||
// Extract text from the part
|
||||
if textContent, ok := part.(llms.TextContent); ok {
|
||||
return textContent.Text, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("anthropic: %w for system message", ErrInvalidContentType)
|
||||
}
|
||||
|
||||
@@ -271,6 +323,36 @@ func handleHumanMessage(msg llms.MessageContent) (anthropicclient.ChatMessage, e
|
||||
|
||||
for _, part := range msg.Parts {
|
||||
switch p := part.(type) {
|
||||
case llms.CachedContent:
|
||||
// Handle cached content with cache control
|
||||
var cacheControl *anthropicclient.CacheControl
|
||||
if p.CacheControl != nil {
|
||||
cacheControl = &anthropicclient.CacheControl{
|
||||
Type: p.CacheControl.Type,
|
||||
}
|
||||
}
|
||||
|
||||
// Process the wrapped content
|
||||
switch wrapped := p.ContentPart.(type) {
|
||||
case llms.TextContent:
|
||||
contents = append(contents, &anthropicclient.TextContent{
|
||||
Type: "text",
|
||||
Text: wrapped.Text,
|
||||
CacheControl: cacheControl,
|
||||
})
|
||||
case llms.BinaryContent:
|
||||
contents = append(contents, &anthropicclient.ImageContent{
|
||||
Type: "image",
|
||||
Source: anthropicclient.ImageSource{
|
||||
Type: "base64",
|
||||
MediaType: wrapped.MIMEType,
|
||||
Data: base64.StdEncoding.EncodeToString(wrapped.Data),
|
||||
},
|
||||
CacheControl: cacheControl,
|
||||
})
|
||||
default:
|
||||
return anthropicclient.ChatMessage{}, fmt.Errorf("anthropic: unsupported cached content part type: %T", wrapped)
|
||||
}
|
||||
case llms.TextContent:
|
||||
contents = append(contents, &anthropicclient.TextContent{
|
||||
Type: "text",
|
||||
@@ -352,3 +434,134 @@ func handleToolMessage(msg llms.MessageContent) (anthropicclient.ChatMessage, er
|
||||
}
|
||||
return anthropicclient.ChatMessage{}, fmt.Errorf("anthropic: %w for tool message", ErrInvalidContentType)
|
||||
}
|
||||
|
||||
// SupportsReasoning implements the ReasoningModel interface.
|
||||
// Returns true if the current model supports extended thinking capabilities.
|
||||
func (o *LLM) SupportsReasoning() bool {
|
||||
return supportsReasoningForModel(o.model)
|
||||
}
|
||||
|
||||
// supportsReasoningForModel checks if a specific model supports reasoning.
|
||||
// This is a separate function to avoid race conditions when checking capabilities.
|
||||
func supportsReasoningForModel(model string) bool {
|
||||
if model == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
modelLower := strings.ToLower(model)
|
||||
|
||||
// Claude 3.7+ supports extended thinking
|
||||
if strings.Contains(modelLower, "claude-3-7") ||
|
||||
strings.Contains(modelLower, "claude-3.7") ||
|
||||
strings.Contains(modelLower, "claude-3-7-sonnet") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Claude 4+ supports extended thinking with interleaving
|
||||
if strings.Contains(modelLower, "claude-4") ||
|
||||
strings.Contains(modelLower, "claude-opus-4") ||
|
||||
strings.Contains(modelLower, "claude-sonnet-4") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Future Claude 5+ expected to support reasoning
|
||||
if strings.Contains(modelLower, "claude-5") ||
|
||||
strings.Contains(modelLower, "claude-opus-5") ||
|
||||
strings.Contains(modelLower, "claude-sonnet-5") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// extractThinkingOptions extracts thinking configuration and beta headers from call options
|
||||
func extractThinkingOptions(o *LLM, opts *llms.CallOptions) ([]string, *anthropicclient.ThinkingConfig) {
|
||||
// Extract beta headers for prompt caching support
|
||||
var betaHeaders []string
|
||||
if opts.Metadata != nil {
|
||||
if headers, ok := opts.Metadata["anthropic:beta_headers"].([]string); ok {
|
||||
betaHeaders = headers
|
||||
}
|
||||
}
|
||||
|
||||
// Extract thinking configuration
|
||||
var budgetTokens int
|
||||
if opts.Metadata != nil {
|
||||
if config, ok := opts.Metadata["thinking_config"].(*llms.ThinkingConfig); ok {
|
||||
// Only set budget_tokens for models that support extended thinking
|
||||
// Claude 3.7+ and Claude 4+ support this feature
|
||||
currentModel := opts.Model
|
||||
if currentModel == "" {
|
||||
currentModel = o.model
|
||||
}
|
||||
if supportsReasoningForModel(currentModel) {
|
||||
if config.BudgetTokens > 0 {
|
||||
budgetTokens = config.BudgetTokens
|
||||
} else if config.Mode != llms.ThinkingModeNone {
|
||||
// Calculate budget based on mode
|
||||
budgetTokens = llms.CalculateThinkingBudget(config.Mode, opts.MaxTokens)
|
||||
}
|
||||
|
||||
// Ensure budget is within valid range for Claude 3.7+
|
||||
if budgetTokens > 0 {
|
||||
if budgetTokens < 1024 {
|
||||
budgetTokens = 1024 // Minimum for Claude
|
||||
} else if budgetTokens > 128000 {
|
||||
budgetTokens = 128000 // Maximum for Claude (128K)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add interleaved thinking header if requested (Claude 4+)
|
||||
if config.InterleaveThinking && supportsReasoningForModel(currentModel) {
|
||||
betaHeaders = append(betaHeaders, "interleaved-thinking-2025-05-14")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create thinking configuration if we have a budget
|
||||
var thinking *anthropicclient.ThinkingConfig
|
||||
if budgetTokens > 0 {
|
||||
thinking = &anthropicclient.ThinkingConfig{
|
||||
Type: "enabled",
|
||||
BudgetTokens: budgetTokens,
|
||||
}
|
||||
}
|
||||
|
||||
return betaHeaders, thinking
|
||||
}
|
||||
|
||||
// extractThinkingFromText extracts thinking content from Anthropic responses
|
||||
// Anthropic models often embed thinking in <thinking> tags
|
||||
func extractThinkingFromText(fullText string) (thinkingContent, outputContent string) {
|
||||
// Look for <thinking> tags in the text
|
||||
if strings.Contains(fullText, "<thinking>") {
|
||||
start := strings.Index(fullText, "<thinking>")
|
||||
end := strings.Index(fullText, "</thinking>")
|
||||
if start >= 0 && end > start {
|
||||
// Extract thinking content between tags
|
||||
thinkingContent = fullText[start+10 : end] // +10 for "<thinking>"
|
||||
|
||||
// Extract output content (everything before and after thinking tags)
|
||||
beforeThinking := strings.TrimSpace(fullText[:start])
|
||||
afterThinking := ""
|
||||
if end+12 < len(fullText) { // +12 for "</thinking>"
|
||||
afterThinking = strings.TrimSpace(fullText[end+12:])
|
||||
}
|
||||
|
||||
// Combine non-thinking content
|
||||
if beforeThinking != "" && afterThinking != "" {
|
||||
outputContent = beforeThinking + "\n\n" + afterThinking
|
||||
} else if beforeThinking != "" {
|
||||
outputContent = beforeThinking
|
||||
} else {
|
||||
outputContent = afterThinking
|
||||
}
|
||||
|
||||
return strings.TrimSpace(thinkingContent), strings.TrimSpace(outputContent)
|
||||
}
|
||||
}
|
||||
|
||||
// If no thinking tags found, treat entire text as output
|
||||
return "", fullText
|
||||
}
|
||||
|
||||
@@ -137,6 +137,11 @@ type MessageRequest struct {
|
||||
StopWords []string `json:"stop_sequences,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
|
||||
// Extended thinking parameters (Claude 3.7+)
|
||||
Thinking *ThinkingConfig `json:"thinking,omitempty"`
|
||||
|
||||
// BetaHeaders are additional beta feature headers to include
|
||||
BetaHeaders []string `json:"-"`
|
||||
StreamingFunc func(ctx context.Context, chunk []byte) error `json:"-"`
|
||||
}
|
||||
|
||||
@@ -152,27 +157,38 @@ func (c *Client) CreateMessage(ctx context.Context, r *MessageRequest) (*Message
|
||||
TopP: r.TopP,
|
||||
Tools: r.Tools,
|
||||
Stream: r.Stream,
|
||||
Thinking: r.Thinking,
|
||||
StreamingFunc: r.StreamingFunc,
|
||||
})
|
||||
}, r.BetaHeaders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) setHeaders(req *http.Request) {
|
||||
func (c *Client) setHeaders(req *http.Request, betaHeaders []string) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("x-api-key", c.token) //nolint:canonicalheader
|
||||
|
||||
// This is necessary as per https://docs.anthropic.com/en/api/versioning
|
||||
// If this changes frequently enough we should expose it as an option..
|
||||
req.Header.Set("anthropic-version", "2023-06-01") // nolint:canonicalheader
|
||||
if c.anthropicBetaHeader != "" {
|
||||
|
||||
// Set beta headers from request, falling back to client default
|
||||
if len(betaHeaders) > 0 {
|
||||
for _, header := range betaHeaders {
|
||||
req.Header.Add("anthropic-beta", header) // nolint:canonicalheader
|
||||
}
|
||||
} else if c.anthropicBetaHeader != "" {
|
||||
req.Header.Set("anthropic-beta", c.anthropicBetaHeader) // nolint:canonicalheader
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, path string, payloadBytes []byte) (*http.Response, error) {
|
||||
return c.doWithHeaders(ctx, path, payloadBytes, nil)
|
||||
}
|
||||
|
||||
func (c *Client) doWithHeaders(ctx context.Context, path string, payloadBytes []byte, betaHeaders []string) (*http.Response, error) {
|
||||
if c.baseURL == "" {
|
||||
c.baseURL = DefaultBaseURL
|
||||
}
|
||||
@@ -184,7 +200,7 @@ func (c *Client) do(ctx context.Context, path string, payloadBytes []byte) (*htt
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
|
||||
c.setHeaders(req)
|
||||
c.setHeaders(req, betaHeaders)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -44,9 +44,18 @@ type messagePayload struct {
|
||||
Tools []Tool `json:"tools,omitempty"`
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
|
||||
// Extended thinking parameters (Claude 3.7+)
|
||||
Thinking *ThinkingConfig `json:"thinking,omitempty"`
|
||||
|
||||
StreamingFunc func(ctx context.Context, chunk []byte) error `json:"-"`
|
||||
}
|
||||
|
||||
// ThinkingConfig represents the thinking configuration for Claude 3.7+
|
||||
type ThinkingConfig struct {
|
||||
Type string `json:"type"` // "enabled" or "disabled"
|
||||
BudgetTokens int `json:"budget_tokens,omitempty"`
|
||||
}
|
||||
|
||||
// Tool used for the request message payload.
|
||||
type Tool struct {
|
||||
Name string `json:"name"`
|
||||
@@ -54,14 +63,20 @@ type Tool struct {
|
||||
InputSchema any `json:"input_schema,omitempty"`
|
||||
}
|
||||
|
||||
// CacheControl represents Anthropic's prompt caching configuration.
|
||||
type CacheControl struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// Content can be TextContent or ToolUseContent depending on the type.
|
||||
type Content interface {
|
||||
GetType() string
|
||||
}
|
||||
|
||||
type TextContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
CacheControl *CacheControl `json:"cache_control,omitempty"`
|
||||
}
|
||||
|
||||
func (tc TextContent) GetType() string {
|
||||
@@ -69,8 +84,9 @@ func (tc TextContent) GetType() string {
|
||||
}
|
||||
|
||||
type ImageContent struct {
|
||||
Type string `json:"type"`
|
||||
Source ImageSource `json:"source"`
|
||||
Type string `json:"type"`
|
||||
Source ImageSource `json:"source"`
|
||||
CacheControl *CacheControl `json:"cache_control,omitempty"`
|
||||
}
|
||||
|
||||
func (ic ImageContent) GetType() string {
|
||||
@@ -84,10 +100,11 @@ type ImageSource struct {
|
||||
}
|
||||
|
||||
type ToolUseContent struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
CacheControl *CacheControl `json:"cache_control,omitempty"`
|
||||
|
||||
inputData string `json:"-"` // Used to gather input data when streaming
|
||||
}
|
||||
@@ -97,15 +114,27 @@ func (tuc ToolUseContent) GetType() string {
|
||||
}
|
||||
|
||||
type ToolResultContent struct {
|
||||
Type string `json:"type"`
|
||||
ToolUseID string `json:"tool_use_id"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
ToolUseID string `json:"tool_use_id"`
|
||||
Content string `json:"content"`
|
||||
CacheControl *CacheControl `json:"cache_control,omitempty"`
|
||||
}
|
||||
|
||||
func (trc ToolResultContent) GetType() string {
|
||||
return trc.Type
|
||||
}
|
||||
|
||||
// ThinkingContent represents Claude's thinking/reasoning content
|
||||
type ThinkingContent struct {
|
||||
Type string `json:"type"`
|
||||
Thinking string `json:"thinking"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
}
|
||||
|
||||
func (tc ThinkingContent) GetType() string {
|
||||
return tc.Type
|
||||
}
|
||||
|
||||
type MessageResponsePayload struct {
|
||||
Content []Content `json:"content"`
|
||||
ID string `json:"id"`
|
||||
@@ -115,8 +144,10 @@ type MessageResponsePayload struct {
|
||||
StopSequence string `json:"stop_sequence"`
|
||||
Type string `json:"type"`
|
||||
Usage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
CacheCreationInputTokens int `json:"cache_creation_input_tokens,omitempty"`
|
||||
CacheReadInputTokens int `json:"cache_read_input_tokens,omitempty"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
@@ -153,6 +184,12 @@ func (m *MessageResponsePayload) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
m.Content = append(m.Content, tuc)
|
||||
case "thinking":
|
||||
tc := &ThinkingContent{}
|
||||
if err := json.Unmarshal(raw, tc); err != nil {
|
||||
return err
|
||||
}
|
||||
m.Content = append(m.Content, tc)
|
||||
default:
|
||||
return fmt.Errorf("unknown content type: %s\n%v", typeStruct.Type, string(raw))
|
||||
}
|
||||
@@ -187,7 +224,7 @@ func (c *Client) setMessageDefaults(payload *messagePayload) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) createMessage(ctx context.Context, payload *messagePayload) (*MessageResponsePayload, error) {
|
||||
func (c *Client) createMessage(ctx context.Context, payload *messagePayload, betaHeaders []string) (*MessageResponsePayload, error) {
|
||||
c.setMessageDefaults(payload)
|
||||
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
@@ -195,7 +232,7 @@ func (c *Client) createMessage(ctx context.Context, payload *messagePayload) (*M
|
||||
return nil, fmt.Errorf("marshal payload: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.do(ctx, "/messages", payloadBytes)
|
||||
resp, err := c.doWithHeaders(ctx, "/messages", payloadBytes, betaHeaders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -317,6 +354,14 @@ func handleMessageStartEvent(event map[string]interface{}, response MessageRespo
|
||||
response.Type = getString(message, "type")
|
||||
response.Usage.InputTokens = int(inputTokens)
|
||||
|
||||
// Capture cache token information if present
|
||||
if cacheCreationTokens, err := getFloat64(usage, "cache_creation_input_tokens"); err == nil {
|
||||
response.Usage.CacheCreationInputTokens = int(cacheCreationTokens)
|
||||
}
|
||||
if cacheReadTokens, err := getFloat64(usage, "cache_read_input_tokens"); err == nil {
|
||||
response.Usage.CacheReadInputTokens = int(cacheReadTokens)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -357,6 +402,10 @@ func handleContentBlockStartEvent(event map[string]interface{}, response Message
|
||||
Name: getString(contentBlock, "name"),
|
||||
Input: input,
|
||||
})
|
||||
case "thinking":
|
||||
response.Content = append(response.Content, &ThinkingContent{
|
||||
Type: eventType,
|
||||
})
|
||||
default:
|
||||
return response, fmt.Errorf("%w: unknown content block type: %s", ErrInvalidDeltaField, eventType)
|
||||
}
|
||||
@@ -390,6 +439,8 @@ func handleContentBlockDeltaEvent(ctx context.Context, event map[string]interfac
|
||||
return handleTextDelta(ctx, delta, response, payload, index)
|
||||
case "input_json_delta":
|
||||
return handleJSONDelta(delta, response, index)
|
||||
case "thinking_delta":
|
||||
return handleThinkingDelta(delta, response, index)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
@@ -433,6 +484,20 @@ func handleJSONDelta(delta map[string]interface{}, response MessageResponsePaylo
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// handleThinkingDelta processes thinking delta events for content blocks.
|
||||
func handleThinkingDelta(delta map[string]interface{}, response MessageResponsePayload, index int) (MessageResponsePayload, error) {
|
||||
thinking, ok := delta["thinking"].(string)
|
||||
if !ok {
|
||||
return response, ErrInvalidDeltaTextField
|
||||
}
|
||||
thinkingContent, ok := response.Content[index].(*ThinkingContent)
|
||||
if !ok {
|
||||
return response, fmt.Errorf("failed to cast to ThinkingContent at index %d", index)
|
||||
}
|
||||
thinkingContent.Thinking += thinking
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func handleContentBlockStop(event map[string]interface{}, response MessageResponsePayload) (MessageResponsePayload, error) {
|
||||
indexValue, ok := event["index"].(float64)
|
||||
if !ok {
|
||||
@@ -473,6 +538,16 @@ func handleMessageDeltaEvent(event map[string]interface{}, response MessageRespo
|
||||
if outputTokens, ok := usage["output_tokens"].(float64); ok {
|
||||
response.Usage.OutputTokens = int(outputTokens)
|
||||
}
|
||||
// Also capture cache tokens in the final message_delta event
|
||||
if inputTokens, err := getFloat64(usage, "input_tokens"); err == nil {
|
||||
response.Usage.InputTokens = int(inputTokens)
|
||||
}
|
||||
if cacheCreationTokens, err := getFloat64(usage, "cache_creation_input_tokens"); err == nil {
|
||||
response.Usage.CacheCreationInputTokens = int(cacheCreationTokens)
|
||||
}
|
||||
if cacheReadTokens, err := getFloat64(usage, "cache_read_input_tokens"); err == nil {
|
||||
response.Usage.CacheReadInputTokens = int(cacheReadTokens)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package anthropic
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
if os.Getenv("ANTHROPIC_API_KEY") == "" {
|
||||
t.Skip("ANTHROPIC_API_KEY not set")
|
||||
}
|
||||
|
||||
llm, err := New(WithModel("claude-3-haiku-20240307"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Anthropic LLM: %v", err)
|
||||
}
|
||||
|
||||
// Test with automatic capability discovery
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package anthropic
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
)
|
||||
|
||||
// WithPromptCaching enables Anthropic's prompt caching feature.
|
||||
// This allows frequently-used prompts and system messages to be cached for improved
|
||||
// performance and reduced costs.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// llm.GenerateContent(ctx, messages,
|
||||
// anthropic.WithPromptCaching(),
|
||||
// )
|
||||
func WithPromptCaching() llms.CallOption {
|
||||
return func(opts *llms.CallOptions) {
|
||||
if opts.Metadata == nil {
|
||||
opts.Metadata = make(map[string]interface{})
|
||||
}
|
||||
opts.Metadata["anthropic:beta_headers"] = []string{"prompt-caching-2024-07-31"}
|
||||
}
|
||||
}
|
||||
|
||||
// WithExtendedOutput enables 128K token output for Claude 3.7+.
|
||||
// Standard models are limited to 8K tokens, but this beta feature allows
|
||||
// generating much longer responses.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// llm.GenerateContent(ctx, messages,
|
||||
// llms.WithMaxTokens(50000),
|
||||
// anthropic.WithExtendedOutput(),
|
||||
// )
|
||||
func WithExtendedOutput() llms.CallOption {
|
||||
return func(opts *llms.CallOptions) {
|
||||
if opts.Metadata == nil {
|
||||
opts.Metadata = make(map[string]interface{})
|
||||
}
|
||||
// Add to existing headers if present
|
||||
if existing, ok := opts.Metadata["anthropic:beta_headers"].([]string); ok {
|
||||
opts.Metadata["anthropic:beta_headers"] = append(existing, "output-128k-2025-02-19")
|
||||
} else {
|
||||
opts.Metadata["anthropic:beta_headers"] = []string{"output-128k-2025-02-19"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithInterleavedThinking enables thinking between tool calls for Claude 3.7+.
|
||||
// This allows the model to use reasoning tokens to plan tool usage and interpret results.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// llm.GenerateContent(ctx, messages,
|
||||
// llms.WithTools(tools),
|
||||
// llms.WithThinkingMode(llms.ThinkingModeMedium),
|
||||
// anthropic.WithInterleavedThinking(),
|
||||
// )
|
||||
func WithInterleavedThinking() llms.CallOption {
|
||||
return func(opts *llms.CallOptions) {
|
||||
if opts.Metadata == nil {
|
||||
opts.Metadata = make(map[string]interface{})
|
||||
}
|
||||
// Add to existing headers if present
|
||||
if existing, ok := opts.Metadata["anthropic:beta_headers"].([]string); ok {
|
||||
opts.Metadata["anthropic:beta_headers"] = append(existing, "interleaved-thinking-2025-05-14")
|
||||
} else {
|
||||
opts.Metadata["anthropic:beta_headers"] = []string{"interleaved-thinking-2025-05-14"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithBetaHeader adds a custom beta header for accessing Anthropic's experimental features.
|
||||
// This is useful for testing new features before dedicated support is added.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// llm.GenerateContent(ctx, messages,
|
||||
// anthropic.WithBetaHeader("new-feature-2025-01-01"),
|
||||
// )
|
||||
func WithBetaHeader(header string) llms.CallOption {
|
||||
return func(opts *llms.CallOptions) {
|
||||
if opts.Metadata == nil {
|
||||
opts.Metadata = make(map[string]interface{})
|
||||
}
|
||||
// Add to existing headers if present
|
||||
if existing, ok := opts.Metadata["anthropic:beta_headers"].([]string); ok {
|
||||
opts.Metadata["anthropic:beta_headers"] = append(existing, header)
|
||||
} else {
|
||||
opts.Metadata["anthropic:beta_headers"] = []string{header}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EphemeralCache creates a standard ephemeral cache control for Anthropic with 5-minute duration.
|
||||
func EphemeralCache() *llms.CacheControl {
|
||||
return &llms.CacheControl{
|
||||
Type: "ephemeral",
|
||||
Duration: 5 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
// EphemeralCacheOneHour creates a 1-hour ephemeral cache control for Anthropic.
|
||||
func EphemeralCacheOneHour() *llms.CacheControl {
|
||||
return &llms.CacheControl{
|
||||
Type: "ephemeral",
|
||||
Duration: time.Hour,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package anthropic_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
"github.com/tmc/langchaingo/llms/anthropic"
|
||||
)
|
||||
|
||||
func TestEphemeralCache(t *testing.T) {
|
||||
cache := anthropic.EphemeralCache()
|
||||
|
||||
if cache.Type != "ephemeral" {
|
||||
t.Errorf("expected type 'ephemeral', got %q", cache.Type)
|
||||
}
|
||||
|
||||
if cache.Duration != 5*time.Minute {
|
||||
t.Errorf("expected duration 5m, got %v", cache.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEphemeralCacheOneHour(t *testing.T) {
|
||||
cache := anthropic.EphemeralCacheOneHour()
|
||||
|
||||
if cache.Type != "ephemeral" {
|
||||
t.Errorf("expected type 'ephemeral', got %q", cache.Type)
|
||||
}
|
||||
|
||||
if cache.Duration != time.Hour {
|
||||
t.Errorf("expected duration 1h, got %v", cache.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithPromptCaching(t *testing.T) {
|
||||
option := anthropic.WithPromptCaching()
|
||||
|
||||
var opts llms.CallOptions
|
||||
option(&opts)
|
||||
|
||||
if opts.Metadata == nil {
|
||||
t.Fatal("metadata should be initialized")
|
||||
}
|
||||
|
||||
headers, ok := opts.Metadata["anthropic:beta_headers"].([]string)
|
||||
if !ok {
|
||||
t.Fatal("anthropic:beta_headers should be a []string")
|
||||
}
|
||||
|
||||
if len(headers) != 1 || headers[0] != "prompt-caching-2024-07-31" {
|
||||
t.Errorf("expected ['prompt-caching-2024-07-31'], got %v", headers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithExtendedOutput(t *testing.T) {
|
||||
option := anthropic.WithExtendedOutput()
|
||||
|
||||
var opts llms.CallOptions
|
||||
option(&opts)
|
||||
|
||||
if opts.Metadata == nil {
|
||||
t.Fatal("metadata should be initialized")
|
||||
}
|
||||
|
||||
headers, ok := opts.Metadata["anthropic:beta_headers"].([]string)
|
||||
if !ok {
|
||||
t.Fatal("anthropic:beta_headers should be a []string")
|
||||
}
|
||||
|
||||
if len(headers) != 1 || headers[0] != "output-128k-2025-02-19" {
|
||||
t.Errorf("expected ['output-128k-2025-02-19'], got %v", headers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithInterleavedThinking(t *testing.T) {
|
||||
option := anthropic.WithInterleavedThinking()
|
||||
|
||||
var opts llms.CallOptions
|
||||
option(&opts)
|
||||
|
||||
if opts.Metadata == nil {
|
||||
t.Fatal("metadata should be initialized")
|
||||
}
|
||||
|
||||
headers, ok := opts.Metadata["anthropic:beta_headers"].([]string)
|
||||
if !ok {
|
||||
t.Fatal("anthropic:beta_headers should be a []string")
|
||||
}
|
||||
|
||||
if len(headers) != 1 || headers[0] != "interleaved-thinking-2025-05-14" {
|
||||
t.Errorf("expected ['interleaved-thinking-2025-05-14'], got %v", headers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithBetaHeader(t *testing.T) {
|
||||
option := anthropic.WithBetaHeader("custom-feature-2025-01-01")
|
||||
|
||||
var opts llms.CallOptions
|
||||
option(&opts)
|
||||
|
||||
if opts.Metadata == nil {
|
||||
t.Fatal("metadata should be initialized")
|
||||
}
|
||||
|
||||
headers, ok := opts.Metadata["anthropic:beta_headers"].([]string)
|
||||
if !ok {
|
||||
t.Fatal("anthropic:beta_headers should be a []string")
|
||||
}
|
||||
|
||||
if len(headers) != 1 || headers[0] != "custom-feature-2025-01-01" {
|
||||
t.Errorf("expected ['custom-feature-2025-01-01'], got %v", headers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleBetaHeaders(t *testing.T) {
|
||||
var opts llms.CallOptions
|
||||
|
||||
// Apply multiple options
|
||||
anthropic.WithPromptCaching()(&opts)
|
||||
anthropic.WithExtendedOutput()(&opts)
|
||||
|
||||
headers, ok := opts.Metadata["anthropic:beta_headers"].([]string)
|
||||
if !ok {
|
||||
t.Fatal("anthropic:beta_headers should be a []string")
|
||||
}
|
||||
|
||||
if len(headers) != 2 {
|
||||
t.Errorf("expected 2 headers, got %d", len(headers))
|
||||
}
|
||||
|
||||
expectedHeaders := map[string]bool{
|
||||
"prompt-caching-2024-07-31": true,
|
||||
"output-128k-2025-02-19": true,
|
||||
}
|
||||
|
||||
for _, h := range headers {
|
||||
if !expectedHeaders[h] {
|
||||
t.Errorf("unexpected header: %s", h)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package anthropic_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
"github.com/tmc/langchaingo/llms/anthropic"
|
||||
)
|
||||
|
||||
func TestAnthropicPromptCaching(t *testing.T) {
|
||||
// This test requires ANTHROPIC_API_KEY environment variable
|
||||
if testing.Short() {
|
||||
t.Skip("skipping prompt caching test in short mode")
|
||||
}
|
||||
|
||||
llm, err := anthropic.New(anthropic.WithModel("claude-3-haiku-20240307"))
|
||||
if err != nil {
|
||||
t.Skip("failed to create Anthropic LLM, skipping prompt caching test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a message with cached content
|
||||
longContext := "You are an expert software engineer with deep knowledge of Go programming. " +
|
||||
"You have extensive experience building distributed systems, microservices, and CLI tools. " +
|
||||
"You understand best practices for error handling, testing, and code organization in Go. " +
|
||||
"You always write clean, idiomatic Go code following the standard library patterns. " +
|
||||
"Please analyze any code I provide and suggest improvements based on Go best practices."
|
||||
|
||||
cachedPart := llms.WithCacheControl(
|
||||
llms.TextPart(longContext),
|
||||
anthropic.EphemeralCache(),
|
||||
)
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeSystem,
|
||||
Parts: []llms.ContentPart{cachedPart},
|
||||
},
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("What are the main principles of good Go code?"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Call with Anthropic caching headers
|
||||
resp, err := llm.GenerateContent(ctx, messages,
|
||||
anthropic.WithPromptCaching(),
|
||||
llms.WithMaxTokens(100),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
// In tests, we expect this to work if API key is available
|
||||
// If not available, we skip rather than fail
|
||||
t.Skipf("failed to generate content with caching: %v", err)
|
||||
}
|
||||
|
||||
if resp == nil || len(resp.Choices) == 0 {
|
||||
t.Fatal("expected non-empty response")
|
||||
}
|
||||
|
||||
if resp.Choices[0].Content == "" {
|
||||
t.Error("expected non-empty content in response")
|
||||
}
|
||||
|
||||
// Test with 1-hour cache
|
||||
longCachePart := llms.WithCacheControl(
|
||||
llms.TextPart(longContext),
|
||||
anthropic.EphemeralCacheOneHour(),
|
||||
)
|
||||
|
||||
messages2 := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeSystem,
|
||||
Parts: []llms.ContentPart{longCachePart},
|
||||
},
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("Give me one Go best practice."),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp2, err := llm.GenerateContent(ctx, messages2,
|
||||
anthropic.WithPromptCaching(),
|
||||
llms.WithMaxTokens(50),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Skipf("failed to generate content with 1-hour caching: %v", err)
|
||||
}
|
||||
|
||||
if resp2 == nil || len(resp2.Choices) == 0 {
|
||||
t.Fatal("expected non-empty response for 1-hour cache test")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnthropicCacheControlInMessages(t *testing.T) {
|
||||
// Test that cache control is properly handled in message processing
|
||||
// This is a unit test that doesn't require API calls
|
||||
|
||||
cachedText := llms.WithCacheControl(
|
||||
llms.TextPart("This is cached content"),
|
||||
anthropic.EphemeralCache(),
|
||||
)
|
||||
|
||||
message := llms.MessageContent{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{cachedText},
|
||||
}
|
||||
|
||||
// This tests that the message can be created without errors
|
||||
// The actual processing is tested through integration tests
|
||||
messages := []llms.MessageContent{message}
|
||||
|
||||
if len(messages) != 1 {
|
||||
t.Error("expected single message")
|
||||
}
|
||||
|
||||
if len(messages[0].Parts) != 1 {
|
||||
t.Error("expected single part in message")
|
||||
}
|
||||
|
||||
cached, ok := messages[0].Parts[0].(llms.CachedContent)
|
||||
if !ok {
|
||||
t.Error("expected CachedContent part")
|
||||
}
|
||||
|
||||
if cached.CacheControl == nil {
|
||||
t.Error("expected cache control to be set")
|
||||
}
|
||||
|
||||
if cached.CacheControl.Type != "ephemeral" {
|
||||
t.Errorf("expected ephemeral cache type, got %q", cached.CacheControl.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnthropicBetaHeaders(t *testing.T) {
|
||||
// Test that beta headers option works correctly
|
||||
option := anthropic.WithPromptCaching()
|
||||
|
||||
var opts llms.CallOptions
|
||||
option(&opts)
|
||||
|
||||
if opts.Metadata == nil {
|
||||
t.Fatal("metadata should be initialized")
|
||||
}
|
||||
|
||||
headers, ok := opts.Metadata["anthropic:beta_headers"].([]string)
|
||||
if !ok {
|
||||
t.Fatal("anthropic:beta_headers should be a []string")
|
||||
}
|
||||
|
||||
expectedHeader := "prompt-caching-2024-07-31"
|
||||
if len(headers) != 1 || headers[0] != expectedHeader {
|
||||
t.Errorf("expected [%q], got %v", expectedHeader, headers)
|
||||
}
|
||||
}
|
||||
@@ -122,4 +122,4 @@ func TestGetProvider_EdgeCases(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,4 +102,4 @@ func TestAnthropicToolCallSupport(t *testing.T) {
|
||||
require.Equal(t, "It's 72°F and sunny in San Francisco", content.Content)
|
||||
require.Equal(t, "toolu_123", content.ToolUseID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package bedrock
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
if os.Getenv("AWS_REGION") == "" {
|
||||
t.Skip("AWS_REGION not set")
|
||||
}
|
||||
|
||||
llm, err := New(
|
||||
WithModel("anthropic.claude-3-haiku-20240307-v1:0"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Bedrock LLM: %v", err)
|
||||
}
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cloudflare
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
apiToken := os.Getenv("CLOUDFLARE_API_TOKEN")
|
||||
if apiToken == "" {
|
||||
t.Skip("CLOUDFLARE_API_TOKEN not set")
|
||||
}
|
||||
|
||||
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
|
||||
if accountID == "" {
|
||||
t.Skip("CLOUDFLARE_ACCOUNT_ID not set")
|
||||
}
|
||||
|
||||
llm, err := New(WithToken(apiToken), WithAccountID(accountID))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Cloudflare LLM: %v", err)
|
||||
}
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cohere
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
if os.Getenv("COHERE_API_KEY") == "" {
|
||||
t.Skip("COHERE_API_KEY not set")
|
||||
}
|
||||
|
||||
llm, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Cohere LLM: %v", err)
|
||||
}
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package ernie
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
apiKey := os.Getenv("ERNIE_API_KEY")
|
||||
if apiKey == "" {
|
||||
t.Skip("ERNIE_API_KEY not set")
|
||||
}
|
||||
|
||||
secretKey := os.Getenv("ERNIE_SECRET_KEY")
|
||||
if secretKey == "" {
|
||||
t.Skip("ERNIE_SECRET_KEY not set")
|
||||
}
|
||||
|
||||
llm, err := New(WithAKSK(apiKey, secretKey))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Ernie LLM: %v", err)
|
||||
}
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package fake
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
// Fake LLM doesn't need API keys
|
||||
llm := &LLM{}
|
||||
|
||||
// Test basic functionality only
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Package googleai provides caching support for Google AI models.
|
||||
package googleai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/generative-ai-go/genai"
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
)
|
||||
|
||||
// CachingHelper provides utilities for working with Google AI's cached content feature.
|
||||
// Unlike Anthropic which supports inline cache control, Google AI requires
|
||||
// pre-creating cached content through the API.
|
||||
type CachingHelper struct {
|
||||
client *genai.Client
|
||||
}
|
||||
|
||||
// NewCachingHelper creates a helper for managing cached content.
|
||||
func NewCachingHelper(ctx context.Context, opts ...Option) (*CachingHelper, error) {
|
||||
// Create a GoogleAI client to get access to the underlying genai client
|
||||
gai, err := New(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CachingHelper{
|
||||
client: gai.client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateCachedContent creates cached content that can be reused across multiple requests.
|
||||
// This is useful for caching large system prompts, context documents, or frequently used instructions.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// helper, _ := NewCachingHelper(ctx, WithAPIKey(apiKey))
|
||||
// cached, _ := helper.CreateCachedContent(ctx, "gemini-2.0-flash", []llms.MessageContent{
|
||||
// {
|
||||
// Role: llms.ChatMessageTypeSystem,
|
||||
// Parts: []llms.ContentPart{
|
||||
// llms.TextPart("You are an expert assistant with deep knowledge..."),
|
||||
// },
|
||||
// },
|
||||
// }, 1*time.Hour)
|
||||
//
|
||||
// // Use the cached content in requests
|
||||
// model, _ := New(ctx, WithAPIKey(apiKey))
|
||||
// resp, _ := model.GenerateContent(ctx, messages, WithCachedContent(cached.Name))
|
||||
func (ch *CachingHelper) CreateCachedContent(
|
||||
ctx context.Context,
|
||||
modelName string,
|
||||
messages []llms.MessageContent,
|
||||
ttl time.Duration,
|
||||
) (*genai.CachedContent, error) {
|
||||
// Convert langchain messages to genai content
|
||||
contents := make([]*genai.Content, 0, len(messages))
|
||||
var systemInstruction *genai.Content
|
||||
|
||||
for _, msg := range messages {
|
||||
parts := make([]genai.Part, 0, len(msg.Parts))
|
||||
for _, part := range msg.Parts {
|
||||
switch p := part.(type) {
|
||||
case llms.TextContent:
|
||||
parts = append(parts, genai.Text(p.Text))
|
||||
case llms.CachedContent:
|
||||
// Extract the underlying content if it's wrapped with cache control
|
||||
// (though Google AI doesn't use inline cache control like Anthropic)
|
||||
if textPart, ok := p.ContentPart.(llms.TextContent); ok {
|
||||
parts = append(parts, genai.Text(textPart.Text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content := &genai.Content{
|
||||
Parts: parts,
|
||||
}
|
||||
|
||||
// Set role
|
||||
switch msg.Role {
|
||||
case llms.ChatMessageTypeSystem:
|
||||
content.Role = "system"
|
||||
systemInstruction = content
|
||||
case llms.ChatMessageTypeHuman:
|
||||
content.Role = "user"
|
||||
contents = append(contents, content)
|
||||
case llms.ChatMessageTypeAI:
|
||||
content.Role = "model"
|
||||
contents = append(contents, content)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the cached content
|
||||
cc := &genai.CachedContent{
|
||||
Model: modelName,
|
||||
Contents: contents,
|
||||
SystemInstruction: systemInstruction,
|
||||
Expiration: genai.ExpireTimeOrTTL{
|
||||
TTL: ttl,
|
||||
},
|
||||
}
|
||||
|
||||
return ch.client.CreateCachedContent(ctx, cc)
|
||||
}
|
||||
|
||||
// GetCachedContent retrieves existing cached content by name.
|
||||
func (ch *CachingHelper) GetCachedContent(ctx context.Context, name string) (*genai.CachedContent, error) {
|
||||
return ch.client.GetCachedContent(ctx, name)
|
||||
}
|
||||
|
||||
// DeleteCachedContent removes cached content.
|
||||
func (ch *CachingHelper) DeleteCachedContent(ctx context.Context, name string) error {
|
||||
return ch.client.DeleteCachedContent(ctx, name)
|
||||
}
|
||||
|
||||
// ListCachedContents returns an iterator for all cached content.
|
||||
func (ch *CachingHelper) ListCachedContents(ctx context.Context) *genai.CachedContentIterator {
|
||||
return ch.client.ListCachedContents(ctx)
|
||||
}
|
||||
@@ -58,6 +58,12 @@ func (g *GoogleAI) GenerateContent(
|
||||
opt(&opts)
|
||||
}
|
||||
|
||||
// Update the tracked model if it was overridden
|
||||
effectiveModel := opts.Model
|
||||
if effectiveModel != "" && effectiveModel != g.model {
|
||||
g.model = effectiveModel
|
||||
}
|
||||
|
||||
model := g.client.GenerativeModel(opts.Model)
|
||||
model.SetCandidateCount(int32(opts.CandidateCount))
|
||||
model.SetMaxOutputTokens(int32(opts.MaxTokens))
|
||||
@@ -65,6 +71,12 @@ func (g *GoogleAI) GenerateContent(
|
||||
model.SetTopP(float32(opts.TopP))
|
||||
model.SetTopK(int32(opts.TopK))
|
||||
model.StopSequences = opts.StopWords
|
||||
|
||||
// Support for cached content (if provided through metadata)
|
||||
// Note: This requires the cached content to be pre-created using Client.CreateCachedContent
|
||||
if cachedContentName, ok := opts.Metadata["CachedContentName"].(string); ok && cachedContentName != "" {
|
||||
model.CachedContentName = cachedContentName
|
||||
}
|
||||
model.SafetySettings = []*genai.SafetySetting{
|
||||
{
|
||||
Category: genai.HarmCategoryDangerousContent,
|
||||
@@ -162,8 +174,27 @@ func convertCandidates(candidates []*genai.Candidate, usage *genai.UsageMetadata
|
||||
metadata["input_tokens"] = usage.PromptTokenCount
|
||||
metadata["output_tokens"] = usage.CandidatesTokenCount
|
||||
metadata["total_tokens"] = usage.TotalTokenCount
|
||||
// Standardized field names for cross-provider compatibility
|
||||
metadata["PromptTokens"] = usage.PromptTokenCount
|
||||
metadata["CompletionTokens"] = usage.CandidatesTokenCount
|
||||
metadata["TotalTokens"] = usage.TotalTokenCount
|
||||
|
||||
// Cache-related token information (if available)
|
||||
if usage.CachedContentTokenCount > 0 {
|
||||
metadata["CachedTokens"] = usage.CachedContentTokenCount
|
||||
metadata["CacheReadInputTokens"] = usage.CachedContentTokenCount // Anthropic compatibility
|
||||
// Google AI includes cached tokens in the prompt count, calculate non-cached
|
||||
metadata["NonCachedInputTokens"] = usage.PromptTokenCount - usage.CachedContentTokenCount
|
||||
}
|
||||
}
|
||||
|
||||
// Google AI doesn't separate thinking content like OpenAI o1, but we provide empty standardized fields
|
||||
metadata["ThinkingContent"] = "" // Google models don't separate thinking content
|
||||
metadata["ThinkingTokens"] = 0 // Google models don't track thinking tokens separately
|
||||
|
||||
// Note: Google AI's CachedContent requires pre-created cached content via API,
|
||||
// not inline cache control like Anthropic. Use Client.CreateCachedContent() for caching.
|
||||
|
||||
contentResponse.Choices = append(contentResponse.Choices,
|
||||
&llms.ContentChoice{
|
||||
Content: buf.String(),
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package googleai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
if os.Getenv("GOOGLE_API_KEY") == "" {
|
||||
t.Skip("GOOGLE_API_KEY not set")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
llm, err := New(ctx,
|
||||
WithAPIKey(os.Getenv("GOOGLE_API_KEY")),
|
||||
WithDefaultModel("gemini-1.5-flash"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Google AI LLM: %v", err)
|
||||
}
|
||||
defer llm.Close()
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
+35
-2
@@ -4,6 +4,7 @@ package googleai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/google/generative-ai-go/genai"
|
||||
"github.com/tmc/langchaingo/callbacks"
|
||||
@@ -15,9 +16,13 @@ type GoogleAI struct {
|
||||
CallbacksHandler callbacks.Handler
|
||||
client *genai.Client
|
||||
opts Options
|
||||
model string // Track current model for reasoning detection
|
||||
}
|
||||
|
||||
var _ llms.Model = &GoogleAI{}
|
||||
var (
|
||||
_ llms.Model = &GoogleAI{}
|
||||
_ llms.ReasoningModel = &GoogleAI{}
|
||||
)
|
||||
|
||||
// New creates a new GoogleAI client.
|
||||
func New(ctx context.Context, opts ...Option) (*GoogleAI, error) {
|
||||
@@ -28,7 +33,8 @@ func New(ctx context.Context, opts ...Option) (*GoogleAI, error) {
|
||||
clientOptions.EnsureAuthPresent()
|
||||
|
||||
gi := &GoogleAI{
|
||||
opts: clientOptions,
|
||||
opts: clientOptions,
|
||||
model: clientOptions.DefaultModel, // Store the default model
|
||||
}
|
||||
|
||||
client, err := genai.NewClient(ctx, clientOptions.ClientOptions...)
|
||||
@@ -49,3 +55,30 @@ func (g *GoogleAI) Close() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SupportsReasoning implements the ReasoningModel interface.
|
||||
// Returns true if the current model supports reasoning/thinking tokens.
|
||||
func (g *GoogleAI) SupportsReasoning() bool {
|
||||
// Check the current model (may have been overridden by WithModel option)
|
||||
model := g.model
|
||||
if model == "" {
|
||||
model = g.opts.DefaultModel
|
||||
}
|
||||
|
||||
// Gemini 2.0 models support reasoning/thinking capabilities
|
||||
if strings.Contains(model, "gemini-2.0") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Future Gemini 3+ models expected to support reasoning
|
||||
if strings.Contains(model, "gemini-3") || strings.Contains(model, "gemini-4") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Gemini Experimental models may have reasoning capabilities
|
||||
if strings.Contains(model, "gemini-exp") && strings.Contains(model, "thinking") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"reflect"
|
||||
|
||||
"cloud.google.com/go/vertexai/genai"
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
@@ -184,6 +185,18 @@ func WithHarmThreshold(ht HarmBlockThreshold) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithCachedContent enables the use of pre-created cached content.
|
||||
// The cached content must be created separately using Client.CreateCachedContent.
|
||||
// This is different from Anthropic's inline cache control.
|
||||
func WithCachedContent(name string) llms.CallOption {
|
||||
return func(o *llms.CallOptions) {
|
||||
if o.Metadata == nil {
|
||||
o.Metadata = make(map[string]interface{})
|
||||
}
|
||||
o.Metadata["CachedContentName"] = name
|
||||
}
|
||||
}
|
||||
|
||||
type HarmBlockThreshold int32
|
||||
|
||||
const (
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package googleai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
)
|
||||
|
||||
func TestGoogleAI_SupportsReasoning(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Gemini 2.0 Flash supports reasoning",
|
||||
model: "gemini-2.0-flash",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Gemini 2.0 Pro supports reasoning",
|
||||
model: "gemini-2.0-pro",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Gemini 3.0 (future) supports reasoning",
|
||||
model: "gemini-3.0-ultra",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Gemini experimental with thinking supports reasoning",
|
||||
model: "gemini-exp-thinking-1206",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Gemini 1.5 Flash does not support reasoning",
|
||||
model: "gemini-1.5-flash",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Gemini 1.0 Pro does not support reasoning",
|
||||
model: "gemini-1.0-pro",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Gemini experimental without thinking does not support reasoning",
|
||||
model: "gemini-exp-1206",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create client with test model
|
||||
client := &GoogleAI{
|
||||
model: tt.model,
|
||||
opts: DefaultOptions(),
|
||||
}
|
||||
|
||||
// Test SupportsReasoning
|
||||
got := client.SupportsReasoning()
|
||||
if got != tt.expected {
|
||||
t.Errorf("SupportsReasoning() for model %s = %v, want %v", tt.model, got, tt.expected)
|
||||
}
|
||||
|
||||
// Also test with model set via options
|
||||
client.model = ""
|
||||
client.opts.DefaultModel = tt.model
|
||||
got = client.SupportsReasoning()
|
||||
if got != tt.expected {
|
||||
t.Errorf("SupportsReasoning() with default model %s = %v, want %v", tt.model, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleAI_ReasoningIntegration(t *testing.T) {
|
||||
apiKey := os.Getenv("GOOGLE_API_KEY")
|
||||
if apiKey == "" {
|
||||
t.Skip("GOOGLE_API_KEY not set")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Gemini 2.0 Flash (reasoning model)
|
||||
client, err := New(ctx,
|
||||
WithAPIKey(apiKey),
|
||||
WithDefaultModel("gemini-2.0-flash"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Verify it implements ReasoningModel interface
|
||||
if _, ok := interface{}(client).(llms.ReasoningModel); !ok {
|
||||
t.Error("GoogleAI should implement ReasoningModel interface")
|
||||
}
|
||||
|
||||
// Verify it reports reasoning support for Gemini 2.0
|
||||
if !client.SupportsReasoning() {
|
||||
t.Error("Gemini 2.0 Flash should support reasoning")
|
||||
}
|
||||
|
||||
// Test reasoning with a complex problem
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("What is 25 * 17 + 13? Think step by step."),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.GenerateContent(ctx, messages,
|
||||
llms.WithMaxTokens(200),
|
||||
llms.WithThinkingMode(llms.ThinkingModeMedium), // Note: Google AI may not use this yet
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate content: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Choices) == 0 {
|
||||
t.Fatal("No response choices")
|
||||
}
|
||||
|
||||
content := resp.Choices[0].Content
|
||||
if !strings.Contains(content, "438") {
|
||||
t.Errorf("Expected answer 438 in response, got: %s", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleAI_CachingSupport(t *testing.T) {
|
||||
apiKey := os.Getenv("GOOGLE_API_KEY")
|
||||
if apiKey == "" {
|
||||
t.Skip("GOOGLE_API_KEY not set")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create caching helper
|
||||
helper, err := NewCachingHelper(ctx, WithAPIKey(apiKey))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create caching helper: %v", err)
|
||||
}
|
||||
|
||||
// Create cached content with a large system prompt
|
||||
longContext := `You are an expert code reviewer with deep knowledge of Go best practices.
|
||||
Always consider performance, security, and maintainability in your reviews.
|
||||
` + strings.Repeat("This is padding text to ensure we have enough tokens for caching. ", 100)
|
||||
|
||||
cached, err := helper.CreateCachedContent(ctx, "gemini-2.0-flash",
|
||||
[]llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeSystem,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart(longContext),
|
||||
},
|
||||
},
|
||||
},
|
||||
5*time.Minute,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create cached content: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := helper.DeleteCachedContent(ctx, cached.Name); err != nil {
|
||||
t.Logf("Failed to delete cached content: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Use the cached content in a request
|
||||
client, err := New(ctx, WithAPIKey(apiKey))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("What are the key things to look for in a Go code review?"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.GenerateContent(ctx, messages,
|
||||
WithCachedContent(cached.Name),
|
||||
llms.WithMaxTokens(200),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate with cached content: %v", err)
|
||||
}
|
||||
|
||||
// Check that cached tokens are reported in the response
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
if cachedTokens, ok := genInfo["CachedTokens"].(int32); ok && cachedTokens > 0 {
|
||||
t.Logf("Successfully used %d cached tokens", cachedTokens)
|
||||
} else {
|
||||
t.Log("No cached tokens reported (this may be normal if caching is not yet active)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package huggingface
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
if os.Getenv("HUGGINGFACEHUB_API_TOKEN") == "" {
|
||||
t.Skip("HUGGINGFACEHUB_API_TOKEN not set")
|
||||
}
|
||||
|
||||
llm, err := New(WithModel("gpt2"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HuggingFace LLM: %v", err)
|
||||
}
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package llamafile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
// Llamafile uses LLAMAFILE_HOST environment variable for server URL
|
||||
// If not set, it defaults to http://127.0.0.1:8080
|
||||
|
||||
llm, err := New()
|
||||
if err != nil {
|
||||
t.Skipf("Llamafile server not available: %v", err)
|
||||
}
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -28,6 +28,18 @@ type Model interface {
|
||||
Call(ctx context.Context, prompt string, options ...CallOption) (string, error)
|
||||
}
|
||||
|
||||
// ReasoningModel is an interface for models that support extended reasoning/thinking.
|
||||
// Models implementing this interface can generate internal reasoning tokens that are
|
||||
// used to improve response quality but may not be included in the final output.
|
||||
type ReasoningModel interface {
|
||||
Model
|
||||
|
||||
// SupportsReasoning returns true if the model supports reasoning/thinking tokens.
|
||||
// This capability allows models to "think" through problems internally before
|
||||
// generating a response, improving quality for complex tasks.
|
||||
SupportsReasoning() bool
|
||||
}
|
||||
|
||||
// GenerateFromSinglePrompt is a convenience function for calling an LLM with
|
||||
// a single string prompt, expecting a single string response. It's useful for
|
||||
// simple, string-only interactions and provides a slightly more ergonomic API
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
binPath := os.Getenv("LOCAL_LLM_BIN")
|
||||
if binPath == "" {
|
||||
t.Skip("LOCAL_LLM_BIN not set")
|
||||
}
|
||||
|
||||
llm, err := New(WithBin(binPath))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Local LLM: %v", err)
|
||||
}
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package maritaca
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
if os.Getenv("MARITACA_API_KEY") == "" {
|
||||
t.Skip("MARITACA_API_KEY not set")
|
||||
}
|
||||
|
||||
llm, err := New(
|
||||
WithToken(os.Getenv("MARITACA_API_KEY")),
|
||||
WithModel("sabia-3"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Maritaca LLM: %v", err)
|
||||
}
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package mistral
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
if os.Getenv("MISTRAL_API_KEY") == "" {
|
||||
t.Skip("MISTRAL_API_KEY not set")
|
||||
}
|
||||
|
||||
llm, err := New(
|
||||
WithAPIKey(os.Getenv("MISTRAL_API_KEY")),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Mistral LLM: %v", err)
|
||||
}
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func (m *Model) Call(ctx context.Context, prompt string, options ...llms.CallOpt
|
||||
Role: "user",
|
||||
Content: prompt,
|
||||
})
|
||||
res, err := m.client.Chat("", messages, &mistralChatParams)
|
||||
res, err := m.client.Chat(callOptions.Model, messages, &mistralChatParams)
|
||||
if err != nil {
|
||||
m.CallbacksHandler.HandleLLMError(ctx, err)
|
||||
return "", err
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package ollama
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
)
|
||||
|
||||
// ContextCache provides a simple in-memory cache for conversation contexts.
|
||||
// This helps reduce token usage by reusing processed context across requests.
|
||||
// Note: This is different from provider-native caching like Anthropic/Google AI.
|
||||
type ContextCache struct {
|
||||
mu sync.RWMutex
|
||||
entries map[string]*CacheEntry
|
||||
maxSize int // Maximum number of entries
|
||||
ttl time.Duration // Time to live for cache entries
|
||||
}
|
||||
|
||||
// CacheEntry represents a cached context entry.
|
||||
type CacheEntry struct {
|
||||
Messages []llms.MessageContent
|
||||
ContextTokens int
|
||||
CreatedAt time.Time
|
||||
LastAccessed time.Time
|
||||
AccessCount int
|
||||
}
|
||||
|
||||
// NewContextCache creates a new context cache with specified capacity and TTL.
|
||||
func NewContextCache(maxSize int, ttl time.Duration) *ContextCache {
|
||||
return &ContextCache{
|
||||
entries: make(map[string]*CacheEntry),
|
||||
maxSize: maxSize,
|
||||
ttl: ttl,
|
||||
}
|
||||
}
|
||||
|
||||
// generateCacheKey creates a unique key for a set of messages.
|
||||
func (c *ContextCache) generateCacheKey(messages []llms.MessageContent) string {
|
||||
h := sha256.New()
|
||||
for _, msg := range messages {
|
||||
h.Write([]byte(msg.Role))
|
||||
for _, part := range msg.Parts {
|
||||
switch p := part.(type) {
|
||||
case llms.TextContent:
|
||||
h.Write([]byte(p.Text))
|
||||
}
|
||||
}
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil))[:16] // Use first 16 chars for brevity
|
||||
}
|
||||
|
||||
// Get retrieves a cached context if available and not expired.
|
||||
func (c *ContextCache) Get(messages []llms.MessageContent) (*CacheEntry, bool) {
|
||||
key := c.generateCacheKey(messages)
|
||||
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
entry, exists := c.entries[key]
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Check if entry has expired
|
||||
if time.Since(entry.CreatedAt) > c.ttl {
|
||||
// Entry expired, don't return it
|
||||
// Note: We don't delete here to avoid lock upgrade
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Update access info
|
||||
entry.LastAccessed = time.Now()
|
||||
entry.AccessCount++
|
||||
|
||||
return entry, true
|
||||
}
|
||||
|
||||
// Put stores a context in the cache.
|
||||
func (c *ContextCache) Put(messages []llms.MessageContent, contextTokens int) {
|
||||
key := c.generateCacheKey(messages)
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Clean up expired entries if we're at capacity
|
||||
if len(c.entries) >= c.maxSize {
|
||||
c.evictExpiredOrOldest()
|
||||
}
|
||||
|
||||
c.entries[key] = &CacheEntry{
|
||||
Messages: messages,
|
||||
ContextTokens: contextTokens,
|
||||
CreatedAt: time.Now(),
|
||||
LastAccessed: time.Now(),
|
||||
AccessCount: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// evictExpiredOrOldest removes expired entries or the oldest entry if at capacity.
|
||||
func (c *ContextCache) evictExpiredOrOldest() {
|
||||
now := time.Now()
|
||||
|
||||
// First, remove expired entries
|
||||
for key, entry := range c.entries {
|
||||
if now.Sub(entry.CreatedAt) > c.ttl {
|
||||
delete(c.entries, key)
|
||||
}
|
||||
}
|
||||
|
||||
// If still at capacity, remove least recently accessed
|
||||
if len(c.entries) >= c.maxSize {
|
||||
var oldestKey string
|
||||
var oldestTime time.Time
|
||||
|
||||
for key, entry := range c.entries {
|
||||
if oldestKey == "" || entry.LastAccessed.Before(oldestTime) {
|
||||
oldestKey = key
|
||||
oldestTime = entry.LastAccessed
|
||||
}
|
||||
}
|
||||
|
||||
if oldestKey != "" {
|
||||
delete(c.entries, oldestKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear removes all entries from the cache.
|
||||
func (c *ContextCache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.entries = make(map[string]*CacheEntry)
|
||||
}
|
||||
|
||||
// Stats returns cache statistics.
|
||||
func (c *ContextCache) Stats() (entries int, totalHits int, avgTokensSaved int) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
entries = len(c.entries)
|
||||
totalTokens := 0
|
||||
|
||||
for _, entry := range c.entries {
|
||||
totalHits += entry.AccessCount - 1 // Subtract 1 for initial put
|
||||
if entry.AccessCount > 1 {
|
||||
totalTokens += entry.ContextTokens * (entry.AccessCount - 1)
|
||||
}
|
||||
}
|
||||
|
||||
if totalHits > 0 {
|
||||
avgTokensSaved = totalTokens / totalHits
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// WithContextCache creates a call option to use cached context.
|
||||
func WithContextCache(cache *ContextCache) llms.CallOption {
|
||||
return func(opts *llms.CallOptions) {
|
||||
if opts.Metadata == nil {
|
||||
opts.Metadata = make(map[string]interface{})
|
||||
}
|
||||
opts.Metadata["context_cache"] = cache
|
||||
}
|
||||
}
|
||||
@@ -327,7 +327,7 @@ func TestClient_GenerateChatWithThink(t *testing.T) {
|
||||
assert.NotNil(t, response.Message)
|
||||
assert.NotEmpty(t, response.Message.Content)
|
||||
assert.True(t, response.Done)
|
||||
|
||||
|
||||
// The think parameter should be included in the request
|
||||
// This test verifies that the parameter is properly serialized
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package ollama
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
serverURL := os.Getenv("OLLAMA_HOST")
|
||||
if serverURL == "" {
|
||||
serverURL = "http://localhost:11434"
|
||||
}
|
||||
|
||||
llm, err := New(
|
||||
WithServerURL(serverURL),
|
||||
WithModel("gpt-oss:20b"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Skipf("Ollama not available: %v", err)
|
||||
}
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tmc/langchaingo/callbacks"
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
@@ -24,7 +25,10 @@ type LLM struct {
|
||||
options options
|
||||
}
|
||||
|
||||
var _ llms.Model = (*LLM)(nil)
|
||||
var (
|
||||
_ llms.Model = (*LLM)(nil)
|
||||
_ llms.ReasoningModel = (*LLM)(nil)
|
||||
)
|
||||
|
||||
// New creates a new ollama LLM implementation.
|
||||
func New(opts ...Option) (*LLM, error) {
|
||||
@@ -41,6 +45,27 @@ func New(opts ...Option) (*LLM, error) {
|
||||
return &LLM{client: client, options: o}, nil
|
||||
}
|
||||
|
||||
// SupportsReasoning implements the ReasoningModel interface.
|
||||
// Returns true if the current model supports reasoning/thinking.
|
||||
func (o *LLM) SupportsReasoning() bool {
|
||||
// Check if the model supports reasoning based on model name patterns
|
||||
model := strings.ToLower(o.options.model)
|
||||
|
||||
// Ollama models that support reasoning/thinking:
|
||||
// - deepseek-r1 models (DeepSeek reasoning models)
|
||||
// - qwq models (Alibaba's QwQ reasoning models)
|
||||
// - Models with "reasoning" or "thinking" in the name
|
||||
if strings.Contains(model, "deepseek-r1") ||
|
||||
strings.Contains(model, "qwq") ||
|
||||
strings.Contains(model, "reasoning") ||
|
||||
strings.Contains(model, "thinking") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Future: could check model capabilities via Ollama API when available
|
||||
return false
|
||||
}
|
||||
|
||||
// Call Implement the call interface for LLM.
|
||||
func (o *LLM) Call(ctx context.Context, prompt string, options ...llms.CallOption) (string, error) {
|
||||
return llms.GenerateFromSinglePrompt(ctx, o, prompt, options...)
|
||||
@@ -57,6 +82,14 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten
|
||||
opt(&opts)
|
||||
}
|
||||
|
||||
// Check if context caching is enabled
|
||||
var contextCache *ContextCache
|
||||
if opts.Metadata != nil {
|
||||
if cache, ok := opts.Metadata["context_cache"].(*ContextCache); ok {
|
||||
contextCache = cache
|
||||
}
|
||||
}
|
||||
|
||||
// Override LLM model if set as llms.CallOption
|
||||
model := o.options.model
|
||||
if opts.Model != "" {
|
||||
@@ -112,6 +145,16 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten
|
||||
|
||||
// Get our ollamaOptions from llms.CallOptions
|
||||
ollamaOptions := makeOllamaOptionsFromOptions(o.options.ollamaOptions, opts)
|
||||
|
||||
// Handle thinking mode if specified via metadata
|
||||
if opts.Metadata != nil {
|
||||
if config, ok := opts.Metadata["thinking_config"].(*llms.ThinkingConfig); ok {
|
||||
if config.Mode != llms.ThinkingModeNone && o.SupportsReasoning() {
|
||||
// Enable thinking for models that support it
|
||||
ollamaOptions.Think = true
|
||||
}
|
||||
}
|
||||
}
|
||||
req := &ollamaclient.ChatRequest{
|
||||
Model: model,
|
||||
Format: format,
|
||||
@@ -162,14 +205,40 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten
|
||||
content = resp.Message.Content
|
||||
}
|
||||
|
||||
// Build generation info with standardized fields
|
||||
genInfo := map[string]any{
|
||||
"CompletionTokens": resp.EvalCount,
|
||||
"PromptTokens": resp.PromptEvalCount,
|
||||
"TotalTokens": resp.EvalCount + resp.PromptEvalCount,
|
||||
// Add empty thinking fields for cross-provider compatibility
|
||||
"ThinkingContent": "", // Ollama doesn't separate thinking content
|
||||
"ThinkingTokens": 0, // Ollama doesn't track thinking tokens separately
|
||||
}
|
||||
|
||||
// If context caching is enabled, track cache usage
|
||||
if contextCache != nil {
|
||||
if cacheEntry, hit := contextCache.Get(messages); hit {
|
||||
// Cache hit - we reused cached context
|
||||
genInfo["CachedTokens"] = cacheEntry.ContextTokens
|
||||
genInfo["CacheHit"] = true
|
||||
} else {
|
||||
// Cache miss - store for future use
|
||||
contextCache.Put(messages, resp.PromptEvalCount)
|
||||
genInfo["CachedTokens"] = 0
|
||||
genInfo["CacheHit"] = false
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Ollama may include thinking in the main content when Think mode is enabled
|
||||
// Future versions may provide separate thinking content
|
||||
if ollamaOptions.Think && o.SupportsReasoning() {
|
||||
genInfo["ThinkingEnabled"] = true
|
||||
}
|
||||
|
||||
choices := []*llms.ContentChoice{
|
||||
{
|
||||
Content: content,
|
||||
GenerationInfo: map[string]any{
|
||||
"CompletionTokens": resp.EvalCount,
|
||||
"PromptTokens": resp.PromptEvalCount,
|
||||
"TotalTokens": resp.EvalCount + resp.PromptEvalCount,
|
||||
},
|
||||
Content: content,
|
||||
GenerationInfo: genInfo,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -250,6 +319,16 @@ func makeOllamaOptionsFromOptions(ollamaOptions ollamaclient.Options, opts llms.
|
||||
ollamaOptions.FrequencyPenalty = float32(opts.FrequencyPenalty)
|
||||
ollamaOptions.PresencePenalty = float32(opts.PresencePenalty)
|
||||
|
||||
// Extract thinking configuration for models that support it
|
||||
if opts.Metadata != nil {
|
||||
if config, ok := opts.Metadata["thinking_config"].(*llms.ThinkingConfig); ok {
|
||||
// Enable thinking mode if not explicitly disabled
|
||||
if config.Mode != llms.ThinkingModeNone {
|
||||
ollamaOptions.Think = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ollamaOptions
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
package ollama
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
)
|
||||
|
||||
func TestOllama_SupportsReasoning(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "DeepSeek R1 supports reasoning",
|
||||
model: "deepseek-r1:latest",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "DeepSeek R1 32b supports reasoning",
|
||||
model: "deepseek-r1:32b",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "QwQ model supports reasoning",
|
||||
model: "qwq:32b",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Model with reasoning in name supports reasoning",
|
||||
model: "custom-reasoning:latest",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Model with thinking in name supports reasoning",
|
||||
model: "my-thinking-model:v1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Llama does not support reasoning",
|
||||
model: "llama3:latest",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Mistral does not support reasoning",
|
||||
model: "mistral:latest",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Phi does not support reasoning",
|
||||
model: "phi:latest",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
llm := &LLM{
|
||||
options: options{
|
||||
model: tt.model,
|
||||
},
|
||||
}
|
||||
|
||||
got := llm.SupportsReasoning()
|
||||
if got != tt.expected {
|
||||
t.Errorf("SupportsReasoning() for model %s = %v, want %v", tt.model, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOllama_ContextCache(t *testing.T) {
|
||||
// Create a context cache with 10 entries and 5 minute TTL
|
||||
cache := NewContextCache(10, 5*time.Minute)
|
||||
|
||||
// Test messages
|
||||
messages1 := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("What is the capital of France?"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
messages2 := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("What is the capital of Germany?"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test Put and Get
|
||||
cache.Put(messages1, 100)
|
||||
|
||||
entry, hit := cache.Get(messages1)
|
||||
if !hit {
|
||||
t.Error("Expected cache hit for messages1")
|
||||
}
|
||||
if entry == nil || entry.ContextTokens != 100 {
|
||||
t.Error("Invalid cache entry returned")
|
||||
}
|
||||
|
||||
// Test cache miss
|
||||
_, hit = cache.Get(messages2)
|
||||
if hit {
|
||||
t.Error("Expected cache miss for messages2")
|
||||
}
|
||||
|
||||
// Test multiple accesses
|
||||
cache.Get(messages1)
|
||||
cache.Get(messages1)
|
||||
|
||||
entries, totalHits, avgTokensSaved := cache.Stats()
|
||||
if entries != 1 {
|
||||
t.Errorf("Expected 1 entry, got %d", entries)
|
||||
}
|
||||
if totalHits != 3 { // 3 additional gets after initial put
|
||||
t.Errorf("Expected 3 total hits, got %d", totalHits)
|
||||
}
|
||||
if avgTokensSaved != 100 {
|
||||
t.Errorf("Expected 100 average tokens saved, got %d", avgTokensSaved)
|
||||
}
|
||||
|
||||
// Test Clear
|
||||
cache.Clear()
|
||||
entries, _, _ = cache.Stats()
|
||||
if entries != 0 {
|
||||
t.Errorf("Expected 0 entries after clear, got %d", entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOllama_ReasoningIntegration(t *testing.T) {
|
||||
// Skip if Ollama is not available
|
||||
serverURL := os.Getenv("OLLAMA_HOST")
|
||||
if serverURL == "" {
|
||||
serverURL = "http://localhost:11434"
|
||||
}
|
||||
|
||||
// Try to create client
|
||||
llm, err := New(
|
||||
WithServerURL(serverURL),
|
||||
WithModel("deepseek-r1:latest"), // Use a reasoning model if available
|
||||
)
|
||||
if err != nil {
|
||||
t.Skipf("Ollama not available: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Check if it implements ReasoningModel
|
||||
if _, ok := interface{}(llm).(llms.ReasoningModel); !ok {
|
||||
t.Error("Ollama LLM should implement ReasoningModel interface")
|
||||
}
|
||||
|
||||
// Test with thinking mode enabled
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("What is 15 + 27? Show your thinking."),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := llm.GenerateContent(ctx, messages,
|
||||
llms.WithMaxTokens(100),
|
||||
llms.WithThinkingMode(llms.ThinkingModeMedium),
|
||||
)
|
||||
if err != nil {
|
||||
// If the model isn't available, skip
|
||||
if strings.Contains(err.Error(), "model") || strings.Contains(err.Error(), "pull") {
|
||||
t.Skip("Reasoning model not available")
|
||||
}
|
||||
t.Fatalf("Failed to generate content: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Choices) == 0 {
|
||||
t.Fatal("No response choices")
|
||||
}
|
||||
|
||||
content := resp.Choices[0].Content
|
||||
if !strings.Contains(content, "42") {
|
||||
t.Logf("Response might not contain correct answer: %s", content)
|
||||
}
|
||||
|
||||
// Check that thinking was enabled
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
if enabled, ok := genInfo["ThinkingEnabled"].(bool); ok && enabled {
|
||||
t.Log("Thinking mode was enabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOllama_CachingIntegration(t *testing.T) {
|
||||
// Skip if Ollama is not available
|
||||
serverURL := os.Getenv("OLLAMA_HOST")
|
||||
if serverURL == "" {
|
||||
serverURL = "http://localhost:11434"
|
||||
}
|
||||
|
||||
llm, err := New(
|
||||
WithServerURL(serverURL),
|
||||
WithModel("llama3:latest"), // Use any available model
|
||||
)
|
||||
if err != nil {
|
||||
t.Skipf("Ollama not available: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create context cache
|
||||
cache := NewContextCache(5, 10*time.Minute)
|
||||
|
||||
// Test messages
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeSystem,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("You are a helpful assistant."),
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("What is 2+2?"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// First request (cache miss)
|
||||
resp1, err := llm.GenerateContent(ctx, messages,
|
||||
llms.WithMaxTokens(50),
|
||||
WithContextCache(cache),
|
||||
)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "model") || strings.Contains(err.Error(), "pull") {
|
||||
t.Skip("Model not available")
|
||||
}
|
||||
t.Fatalf("First request failed: %v", err)
|
||||
}
|
||||
|
||||
// Check cache miss
|
||||
if genInfo := resp1.Choices[0].GenerationInfo; genInfo != nil {
|
||||
if hit, ok := genInfo["CacheHit"].(bool); ok && hit {
|
||||
t.Error("Expected cache miss on first request")
|
||||
}
|
||||
}
|
||||
|
||||
// Second request with same messages (cache hit)
|
||||
resp2, err := llm.GenerateContent(ctx, messages,
|
||||
llms.WithMaxTokens(50),
|
||||
WithContextCache(cache),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Second request failed: %v", err)
|
||||
}
|
||||
|
||||
// Check cache hit
|
||||
if genInfo := resp2.Choices[0].GenerationInfo; genInfo != nil {
|
||||
if hit, ok := genInfo["CacheHit"].(bool); ok && !hit {
|
||||
t.Error("Expected cache hit on second request")
|
||||
}
|
||||
if cached, ok := genInfo["CachedTokens"].(int); ok && cached > 0 {
|
||||
t.Logf("Reused %d cached tokens", cached)
|
||||
}
|
||||
}
|
||||
|
||||
// Check cache stats
|
||||
entries, hits, avgSaved := cache.Stats()
|
||||
t.Logf("Cache stats: %d entries, %d hits, %d avg tokens saved", entries, hits, avgSaved)
|
||||
}
|
||||
@@ -64,6 +64,10 @@ type ChatRequest struct {
|
||||
// Options for streaming response. Only set this when you set stream: true.
|
||||
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
||||
|
||||
// ReasoningEffort controls thinking effort for reasoning models (o1, o3, GPT-5).
|
||||
// Valid values: "minimal" (GPT-5 only), "low", "medium", "high"
|
||||
ReasoningEffort string `json:"reasoning_effort,omitempty"`
|
||||
|
||||
// StreamingFunc is a function to be called for each chunk of a streaming response.
|
||||
// Return an error to stop streaming early.
|
||||
StreamingFunc func(ctx context.Context, chunk []byte) error `json:"-"`
|
||||
@@ -484,7 +488,7 @@ func (c *Client) createChat(ctx context.Context, payload *ChatRequest) (*ChatCom
|
||||
}
|
||||
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
|
||||
|
||||
// Restore original metadata
|
||||
payload.Metadata = originalMetadata
|
||||
if err != nil {
|
||||
|
||||
@@ -178,28 +178,28 @@ func TestIsReasoningModel(t *testing.T) {
|
||||
{"gpt-4-turbo", false},
|
||||
{"gpt-4o", false},
|
||||
{"text-davinci-003", false},
|
||||
|
||||
|
||||
// GPT-5 models - should be reasoning models
|
||||
{"gpt-5", true},
|
||||
{"gpt-5-preview", true},
|
||||
{"gpt-5-mini", true},
|
||||
{"gpt-5-turbo", true},
|
||||
|
||||
|
||||
// o1 models - should be reasoning models
|
||||
{"o1-preview", true},
|
||||
{"o1-mini", true},
|
||||
{"o1-large", true},
|
||||
|
||||
|
||||
// o3 models - should be reasoning models
|
||||
{"o3", true}, // Base o3 model
|
||||
{"o3-mini", true},
|
||||
{"o3-preview", true},
|
||||
{"o3-large", true},
|
||||
|
||||
|
||||
// Edge cases
|
||||
{"", false},
|
||||
{"o10-preview", false}, // Doesn't start with "o1-"
|
||||
{"o30-mini", false}, // Doesn't start with "o3-"
|
||||
{"o30-mini", false}, // Doesn't start with "o3-"
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -285,7 +285,7 @@ func TestInternalMetadataFiltering(t *testing.T) {
|
||||
return nil, err
|
||||
}
|
||||
capturedRequestBody = body
|
||||
|
||||
|
||||
// Return a minimal valid response to avoid errors
|
||||
responseBody := `{"choices":[{"message":{"content":"test"}}],"usage":{"total_tokens":10}}`
|
||||
return &http.Response{
|
||||
@@ -303,8 +303,8 @@ func TestInternalMetadataFiltering(t *testing.T) {
|
||||
{Role: "user", Content: "test"},
|
||||
},
|
||||
Metadata: map[string]any{
|
||||
"openai:use_legacy_max_tokens": true, // Should be filtered out
|
||||
"custom_field": "value", // Should be preserved
|
||||
"openai:use_legacy_max_tokens": true, // Should be filtered out
|
||||
"custom_field": "value", // Should be preserved
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
if os.Getenv("OPENAI_API_KEY") == "" {
|
||||
t.Skip("OPENAI_API_KEY not set")
|
||||
}
|
||||
|
||||
llm, err := New(WithModel("gpt-3.5-turbo"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create OpenAI LLM: %v", err)
|
||||
}
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
+200
-7
@@ -3,6 +3,8 @@ package openai
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/tmc/langchaingo/callbacks"
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
@@ -14,6 +16,7 @@ type ChatMessage = openaiclient.ChatMessage
|
||||
type LLM struct {
|
||||
CallbacksHandler callbacks.Handler
|
||||
client *openaiclient.Client
|
||||
model string // Track current model for reasoning detection
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -24,7 +27,60 @@ const (
|
||||
RoleTool = "tool"
|
||||
)
|
||||
|
||||
var _ llms.Model = (*LLM)(nil)
|
||||
// ModelCapability defines what a model supports
|
||||
type ModelCapability struct {
|
||||
Pattern string // Regex pattern to match model names
|
||||
SupportsSystem bool // If true, supports system messages
|
||||
SupportsThinking bool // If true, supports reasoning/thinking
|
||||
SupportsCaching bool // If true, supports prompt caching
|
||||
// Add more capabilities as needed
|
||||
}
|
||||
|
||||
// modelCapabilities defines capabilities for different model patterns
|
||||
var modelCapabilities = []ModelCapability{
|
||||
// OpenAI reasoning models (o1, o3 series) - no system message support
|
||||
{
|
||||
Pattern: `(?i)^o[13](-mini|-preview)?$`, // Matches o1, o1-mini, o1-preview, o3, o3-mini
|
||||
SupportsSystem: false, // O1 models don't support system messages
|
||||
SupportsThinking: true,
|
||||
SupportsCaching: false,
|
||||
},
|
||||
// GPT-4 models
|
||||
{
|
||||
Pattern: `(?i)^gpt-4`, // Matches gpt-4, gpt-4-turbo, etc.
|
||||
SupportsSystem: true,
|
||||
SupportsThinking: false,
|
||||
SupportsCaching: false, // OpenAI caching coming soon
|
||||
},
|
||||
// GPT-3.5 models
|
||||
{
|
||||
Pattern: `(?i)^gpt-3\.5`,
|
||||
SupportsSystem: true,
|
||||
SupportsThinking: false,
|
||||
SupportsCaching: false,
|
||||
},
|
||||
// Future models can be added here
|
||||
}
|
||||
|
||||
// getModelCapabilities returns the capabilities for a given model
|
||||
func getModelCapabilities(model string) ModelCapability {
|
||||
for _, cap := range modelCapabilities {
|
||||
if matched, _ := regexp.MatchString(cap.Pattern, model); matched {
|
||||
return cap
|
||||
}
|
||||
}
|
||||
// Default capabilities - assume standard model
|
||||
return ModelCapability{
|
||||
SupportsSystem: true,
|
||||
SupportsThinking: false,
|
||||
SupportsCaching: false,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ llms.Model = (*LLM)(nil)
|
||||
_ llms.ReasoningModel = (*LLM)(nil)
|
||||
)
|
||||
|
||||
// New returns a new OpenAI LLM.
|
||||
func New(opts ...Option) (*LLM, error) {
|
||||
@@ -35,6 +91,7 @@ func New(opts ...Option) (*LLM, error) {
|
||||
return &LLM{
|
||||
client: c,
|
||||
CallbacksHandler: opt.callbackHandler,
|
||||
model: c.Model, // Store the model for reasoning detection
|
||||
}, err
|
||||
}
|
||||
|
||||
@@ -54,8 +111,40 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten
|
||||
opt(&opts)
|
||||
}
|
||||
|
||||
// Determine the effective model for this request (don't mutate o.model to avoid races)
|
||||
effectiveModel := opts.Model
|
||||
if effectiveModel == "" {
|
||||
effectiveModel = o.model
|
||||
}
|
||||
|
||||
// Get capabilities for this model
|
||||
modelCaps := getModelCapabilities(effectiveModel)
|
||||
|
||||
// For models that don't support system messages, we need to merge them into user messages
|
||||
var systemContent string
|
||||
if !modelCaps.SupportsSystem {
|
||||
for _, mc := range messages {
|
||||
if mc.Role == llms.ChatMessageTypeSystem {
|
||||
// Extract system message content
|
||||
for _, part := range mc.Parts {
|
||||
if textPart, ok := part.(llms.TextContent); ok {
|
||||
if systemContent != "" {
|
||||
systemContent += "\n\n"
|
||||
}
|
||||
systemContent += textPart.Text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chatMsgs := make([]*ChatMessage, 0, len(messages))
|
||||
for _, mc := range messages {
|
||||
// Skip system messages for models that don't support them
|
||||
if mc.Role == llms.ChatMessageTypeSystem && !modelCaps.SupportsSystem {
|
||||
continue
|
||||
}
|
||||
|
||||
msg := &ChatMessage{MultiContent: mc.Parts}
|
||||
switch mc.Role {
|
||||
case llms.ChatMessageTypeSystem:
|
||||
@@ -64,6 +153,17 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten
|
||||
msg.Role = RoleAssistant
|
||||
case llms.ChatMessageTypeHuman:
|
||||
msg.Role = RoleUser
|
||||
// For models without system support, prepend system content to first user message
|
||||
if systemContent != "" && !modelCaps.SupportsSystem {
|
||||
// Prepend system content to the user message
|
||||
newParts := []llms.ContentPart{}
|
||||
if systemContent != "" {
|
||||
newParts = append(newParts, llms.TextContent{Text: systemContent + "\n\n"})
|
||||
}
|
||||
newParts = append(newParts, mc.Parts...)
|
||||
msg.MultiContent = newParts
|
||||
systemContent = "" // Clear after using
|
||||
}
|
||||
case llms.ChatMessageTypeGeneric:
|
||||
msg.Role = RoleUser
|
||||
case llms.ChatMessageTypeFunction:
|
||||
@@ -110,6 +210,57 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten
|
||||
}
|
||||
}
|
||||
|
||||
// Extract reasoning effort for thinking models
|
||||
// Note: OpenAI o1/o3 models have built-in reasoning and don't support reasoning_effort parameter
|
||||
// This is kept for future models that might support it (like GPT-5)
|
||||
var reasoningEffort string
|
||||
// Commented out for now since current o1 models don't support this parameter
|
||||
/*
|
||||
if opts.Metadata != nil {
|
||||
if config, ok := opts.Metadata["thinking_config"].(*llms.ThinkingConfig); ok {
|
||||
// Map thinking mode to reasoning effort
|
||||
switch config.Mode {
|
||||
case llms.ThinkingModeLow:
|
||||
reasoningEffort = "low"
|
||||
case llms.ThinkingModeMedium:
|
||||
reasoningEffort = "medium"
|
||||
case llms.ThinkingModeHigh:
|
||||
reasoningEffort = "high"
|
||||
}
|
||||
|
||||
// Handle streaming for thinking
|
||||
if config.StreamThinking && opts.StreamingReasoningFunc == nil && opts.StreamingFunc != nil {
|
||||
// Set up default reasoning streaming if requested but not provided
|
||||
// Wrap the single-param streaming func into a reasoning func
|
||||
opts.StreamingReasoningFunc = func(ctx context.Context, reasoningChunk []byte, chunk []byte) error {
|
||||
// For default behavior, we might want to stream both or just the main content
|
||||
// Here we'll just stream the main content chunk
|
||||
if len(chunk) > 0 {
|
||||
return opts.StreamingFunc(ctx, chunk)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Filter out internal metadata that shouldn't be sent to API
|
||||
apiMetadata := make(map[string]any)
|
||||
if opts.Metadata != nil {
|
||||
for k, v := range opts.Metadata {
|
||||
// Skip internal metadata keys
|
||||
if k == "thinking_config" || strings.HasPrefix(k, "openai:") {
|
||||
continue
|
||||
}
|
||||
apiMetadata[k] = v
|
||||
}
|
||||
}
|
||||
// Only include metadata if there are actual values to send
|
||||
if len(apiMetadata) == 0 {
|
||||
apiMetadata = nil
|
||||
}
|
||||
|
||||
req := &openaiclient.ChatRequest{
|
||||
Model: opts.Model,
|
||||
StopWords: opts.StopWords,
|
||||
@@ -120,6 +271,7 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten
|
||||
N: opts.N,
|
||||
FrequencyPenalty: opts.FrequencyPenalty,
|
||||
PresencePenalty: opts.PresencePenalty,
|
||||
ReasoningEffort: reasoningEffort,
|
||||
|
||||
// Token handling: check metadata flag for legacy behavior
|
||||
// By default use max_completion_tokens (modern field)
|
||||
@@ -140,7 +292,7 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten
|
||||
ToolChoice: opts.ToolChoice,
|
||||
FunctionCallBehavior: openaiclient.FunctionCallBehavior(opts.FunctionCallBehavior),
|
||||
Seed: opts.Seed,
|
||||
Metadata: opts.Metadata,
|
||||
Metadata: apiMetadata,
|
||||
}
|
||||
if opts.JSONMode {
|
||||
req.ResponseFormat = ResponseFormatJSON
|
||||
@@ -187,11 +339,14 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten
|
||||
ReasoningContent: c.Message.ReasoningContent,
|
||||
StopReason: fmt.Sprint(c.FinishReason),
|
||||
GenerationInfo: map[string]any{
|
||||
"CompletionTokens": result.Usage.CompletionTokens,
|
||||
"PromptTokens": result.Usage.PromptTokens,
|
||||
"TotalTokens": result.Usage.TotalTokens,
|
||||
"ReasoningTokens": result.Usage.CompletionTokensDetails.ReasoningTokens,
|
||||
"PromptAudioTokens": result.Usage.PromptTokensDetails.AudioTokens,
|
||||
"CompletionTokens": result.Usage.CompletionTokens,
|
||||
"PromptTokens": result.Usage.PromptTokens,
|
||||
"TotalTokens": result.Usage.TotalTokens,
|
||||
"ReasoningTokens": result.Usage.CompletionTokensDetails.ReasoningTokens,
|
||||
"PromptAudioTokens": result.Usage.PromptTokensDetails.AudioTokens,
|
||||
// Standardized fields for cross-provider compatibility
|
||||
"ThinkingContent": c.Message.ReasoningContent, // Standardized field
|
||||
"ThinkingTokens": result.Usage.CompletionTokensDetails.ReasoningTokens, // Standardized field
|
||||
"PromptCachedTokens": result.Usage.PromptTokensDetails.CachedTokens,
|
||||
"CompletionAudioTokens": result.Usage.CompletionTokensDetails.AudioTokens,
|
||||
"CompletionReasoningTokens": result.Usage.CompletionTokensDetails.ReasoningTokens,
|
||||
@@ -229,6 +384,44 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// SupportsReasoning implements the ReasoningModel interface.
|
||||
// Returns true if the current model supports reasoning/thinking tokens.
|
||||
func (o *LLM) SupportsReasoning() bool {
|
||||
// Check the current model (may have been overridden by WithModel option)
|
||||
model := o.model
|
||||
if model == "" {
|
||||
model = o.client.Model
|
||||
}
|
||||
|
||||
modelLower := strings.ToLower(model)
|
||||
|
||||
// OpenAI o1 series (reasoning models)
|
||||
if strings.HasPrefix(modelLower, "o1-") ||
|
||||
strings.Contains(modelLower, "o1-preview") ||
|
||||
strings.Contains(modelLower, "o1-mini") {
|
||||
return true
|
||||
}
|
||||
|
||||
// OpenAI o3 series
|
||||
if strings.HasPrefix(modelLower, "o3-") ||
|
||||
strings.Contains(modelLower, "o3-mini") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Future o4+ series
|
||||
if strings.HasPrefix(modelLower, "o4-") ||
|
||||
strings.HasPrefix(modelLower, "o5-") {
|
||||
return true
|
||||
}
|
||||
|
||||
// GPT-5 series (expected to have reasoning capabilities)
|
||||
if strings.HasPrefix(modelLower, "gpt-5") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CreateEmbedding creates embeddings for the given input texts.
|
||||
func (o *LLM) CreateEmbedding(ctx context.Context, inputTexts []string) ([][]float32, error) {
|
||||
embeddings, err := o.client.CreateEmbedding(ctx, &openaiclient.EmbeddingRequest{
|
||||
|
||||
+4
-3
@@ -87,7 +87,8 @@ type FunctionDefinition struct {
|
||||
Description string `json:"description"`
|
||||
// Parameters is a list of parameters for the function.
|
||||
Parameters any `json:"parameters,omitempty"`
|
||||
// Strict is a flag to indicate if the function should be called strictly. Only used for openai llm structured output.
|
||||
// Strict is a flag to indicate if the function should be called strictly.
|
||||
// Provider support varies - typically used for structured output guarantees.
|
||||
Strict bool `json:"strict,omitempty"`
|
||||
}
|
||||
|
||||
@@ -283,8 +284,8 @@ func WithMetadata(metadata map[string]interface{}) CallOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithResponseMIMEType will add an option to set the ResponseMIMEType
|
||||
// Currently only supported by googleai llms.
|
||||
// WithResponseMIMEType will add an option to set the ResponseMIMEType.
|
||||
// Provider support varies - check your provider's documentation.
|
||||
func WithResponseMIMEType(responseMIMEType string) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.ResponseMIMEType = responseMIMEType
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package llms
|
||||
|
||||
import "time"
|
||||
|
||||
// CacheControl represents prompt caching configuration for providers that support it.
|
||||
type CacheControl struct {
|
||||
// Type specifies the type of caching (provider-specific, e.g., "ephemeral").
|
||||
Type string `json:"type,omitempty"`
|
||||
|
||||
// Duration specifies cache lifetime (provider-specific limits apply).
|
||||
Duration time.Duration `json:"-"`
|
||||
}
|
||||
|
||||
// CachedContent represents content with caching instructions.
|
||||
type CachedContent struct {
|
||||
ContentPart
|
||||
CacheControl *CacheControl `json:"cache_control,omitempty"`
|
||||
}
|
||||
|
||||
func (cc CachedContent) isPart() {}
|
||||
|
||||
// WithCacheControl wraps content with cache control instructions.
|
||||
func WithCacheControl(content ContentPart, control *CacheControl) CachedContent {
|
||||
return CachedContent{
|
||||
ContentPart: content,
|
||||
CacheControl: control,
|
||||
}
|
||||
}
|
||||
|
||||
// WithPromptCaching adds cache control metadata to call options.
|
||||
// This is a generic option that can be used by any provider that supports caching.
|
||||
// Providers should check for this metadata and handle it appropriately.
|
||||
func WithPromptCaching(enabled bool) CallOption {
|
||||
return func(opts *CallOptions) {
|
||||
if opts.Metadata == nil {
|
||||
opts.Metadata = make(map[string]interface{})
|
||||
}
|
||||
opts.Metadata["prompt_caching"] = enabled
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package llms_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
)
|
||||
|
||||
func TestWithCacheControl(t *testing.T) {
|
||||
textContent := llms.TextPart("Hello world")
|
||||
cacheControl := &llms.CacheControl{
|
||||
Type: "test",
|
||||
Duration: 10 * time.Minute,
|
||||
}
|
||||
|
||||
cached := llms.WithCacheControl(textContent, cacheControl)
|
||||
|
||||
if cached.ContentPart != textContent {
|
||||
t.Error("cached content should wrap the original content")
|
||||
}
|
||||
|
||||
if cached.CacheControl != cacheControl {
|
||||
t.Error("cached content should have the provided cache control")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithPromptCaching(t *testing.T) {
|
||||
option := llms.WithPromptCaching(true)
|
||||
|
||||
var opts llms.CallOptions
|
||||
option(&opts)
|
||||
|
||||
if opts.Metadata == nil {
|
||||
t.Fatal("metadata should be initialized")
|
||||
}
|
||||
|
||||
enabled, ok := opts.Metadata["prompt_caching"].(bool)
|
||||
if !ok {
|
||||
t.Fatal("prompt_caching should be a bool")
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
t.Error("prompt_caching should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedContentImplementsContentPart(t *testing.T) {
|
||||
textContent := llms.TextPart("Hello world")
|
||||
cacheControl := &llms.CacheControl{
|
||||
Type: "test",
|
||||
Duration: 10 * time.Minute,
|
||||
}
|
||||
cached := llms.WithCacheControl(textContent, cacheControl)
|
||||
|
||||
// This should compile - CachedContent implements ContentPart
|
||||
var part llms.ContentPart = cached
|
||||
_ = part
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package llms
|
||||
|
||||
import "strings"
|
||||
|
||||
// ThinkingMode represents different thinking/reasoning modes for LLMs.
|
||||
type ThinkingMode string
|
||||
|
||||
const (
|
||||
// ThinkingModeNone disables thinking/reasoning.
|
||||
ThinkingModeNone ThinkingMode = "none"
|
||||
|
||||
// ThinkingModeLow allocates minimal tokens for thinking (~20% of max tokens).
|
||||
ThinkingModeLow ThinkingMode = "low"
|
||||
|
||||
// ThinkingModeMedium allocates moderate tokens for thinking (~50% of max tokens).
|
||||
ThinkingModeMedium ThinkingMode = "medium"
|
||||
|
||||
// ThinkingModeHigh allocates maximum tokens for thinking (~80% of max tokens).
|
||||
ThinkingModeHigh ThinkingMode = "high"
|
||||
|
||||
// ThinkingModeAuto lets the model decide how much thinking is needed.
|
||||
ThinkingModeAuto ThinkingMode = "auto"
|
||||
)
|
||||
|
||||
// ThinkingConfig configures thinking/reasoning behavior for models that support it.
|
||||
type ThinkingConfig struct {
|
||||
// Mode specifies the thinking mode (none, low, medium, high, auto).
|
||||
Mode ThinkingMode `json:"mode,omitempty"`
|
||||
|
||||
// BudgetTokens sets explicit token budget for thinking.
|
||||
// Providers may have different minimum and maximum limits.
|
||||
BudgetTokens int `json:"budget_tokens,omitempty"`
|
||||
|
||||
// ReturnThinking controls whether thinking/reasoning is included in response.
|
||||
// Provider support and behavior varies.
|
||||
ReturnThinking bool `json:"return_thinking,omitempty"`
|
||||
|
||||
// StreamThinking enables streaming of thinking tokens as they're generated.
|
||||
// Not all providers support this feature.
|
||||
StreamThinking bool `json:"stream_thinking,omitempty"`
|
||||
|
||||
// InterleaveThinking enables thinking between tool calls.
|
||||
// Provider support varies.
|
||||
InterleaveThinking bool `json:"interleave_thinking,omitempty"`
|
||||
}
|
||||
|
||||
// DefaultThinkingConfig returns a sensible default thinking configuration.
|
||||
func DefaultThinkingConfig() *ThinkingConfig {
|
||||
return &ThinkingConfig{
|
||||
Mode: ThinkingModeAuto,
|
||||
ReturnThinking: false,
|
||||
StreamThinking: false,
|
||||
}
|
||||
}
|
||||
|
||||
// WithThinking adds thinking configuration to call options.
|
||||
func WithThinking(config *ThinkingConfig) CallOption {
|
||||
return func(opts *CallOptions) {
|
||||
if opts.Metadata == nil {
|
||||
opts.Metadata = make(map[string]interface{})
|
||||
}
|
||||
opts.Metadata["thinking_config"] = config
|
||||
}
|
||||
}
|
||||
|
||||
// getOrCreateThinkingConfig is a helper to get or create thinking config from metadata.
|
||||
func getOrCreateThinkingConfig(opts *CallOptions) *ThinkingConfig {
|
||||
if opts.Metadata == nil {
|
||||
opts.Metadata = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if existing, ok := opts.Metadata["thinking_config"].(*ThinkingConfig); ok {
|
||||
return existing
|
||||
}
|
||||
|
||||
config := DefaultThinkingConfig()
|
||||
opts.Metadata["thinking_config"] = config
|
||||
return config
|
||||
}
|
||||
|
||||
// GetThinkingConfig safely retrieves thinking config from call options.
|
||||
// Returns nil if no thinking config is present.
|
||||
func GetThinkingConfig(opts *CallOptions) *ThinkingConfig {
|
||||
if opts == nil || opts.Metadata == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
config, _ := opts.Metadata["thinking_config"].(*ThinkingConfig)
|
||||
return config
|
||||
}
|
||||
|
||||
// WithThinkingMode sets the thinking mode for the request.
|
||||
func WithThinkingMode(mode ThinkingMode) CallOption {
|
||||
return func(opts *CallOptions) {
|
||||
config := getOrCreateThinkingConfig(opts)
|
||||
config.Mode = mode
|
||||
}
|
||||
}
|
||||
|
||||
// WithThinkingBudget sets explicit token budget for thinking.
|
||||
func WithThinkingBudget(tokens int) CallOption {
|
||||
return func(opts *CallOptions) {
|
||||
config := getOrCreateThinkingConfig(opts)
|
||||
config.BudgetTokens = tokens
|
||||
}
|
||||
}
|
||||
|
||||
// WithReturnThinking enables returning thinking/reasoning in the response.
|
||||
func WithReturnThinking(enabled bool) CallOption {
|
||||
return func(opts *CallOptions) {
|
||||
config := getOrCreateThinkingConfig(opts)
|
||||
config.ReturnThinking = enabled
|
||||
}
|
||||
}
|
||||
|
||||
// WithStreamThinking enables streaming of thinking tokens.
|
||||
func WithStreamThinking(enabled bool) CallOption {
|
||||
return func(opts *CallOptions) {
|
||||
config := getOrCreateThinkingConfig(opts)
|
||||
config.StreamThinking = enabled
|
||||
}
|
||||
}
|
||||
|
||||
// WithInterleaveThinking enables interleaved thinking between tool calls.
|
||||
// Provider support varies - check your provider's documentation.
|
||||
func WithInterleaveThinking(enabled bool) CallOption {
|
||||
return func(opts *CallOptions) {
|
||||
config := getOrCreateThinkingConfig(opts)
|
||||
config.InterleaveThinking = enabled
|
||||
}
|
||||
}
|
||||
|
||||
// Note: ReasoningModel interface is defined in llms.go
|
||||
|
||||
// IsReasoningModel returns true if the model is a reasoning/thinking model.
|
||||
// This includes OpenAI o1/o3/GPT-5 series, Anthropic Claude 3.7+, DeepSeek reasoner, etc.
|
||||
// For runtime checking of LLM instances, use SupportsReasoningModel instead.
|
||||
func IsReasoningModel(model string) bool {
|
||||
return DefaultIsReasoningModel(model)
|
||||
}
|
||||
|
||||
// SupportsReasoningModel checks if an LLM instance supports reasoning tokens.
|
||||
// This first checks if the LLM implements the ReasoningModel interface.
|
||||
// If not, it falls back to checking the model string if available.
|
||||
func SupportsReasoningModel(llm interface{}) bool {
|
||||
// Check if the LLM implements ReasoningModel
|
||||
if reasoner, ok := llm.(ReasoningModel); ok {
|
||||
return reasoner.SupportsReasoning()
|
||||
}
|
||||
|
||||
// Fallback: check if we can extract a model string somehow
|
||||
// This is a best-effort approach for backwards compatibility
|
||||
return false
|
||||
}
|
||||
|
||||
// DefaultIsReasoningModel provides the default reasoning model detection logic.
|
||||
// This can be used by LLM implementations that want to extend rather than replace
|
||||
// the default detection logic.
|
||||
func DefaultIsReasoningModel(model string) bool {
|
||||
modelLower := strings.ToLower(model)
|
||||
|
||||
// OpenAI reasoning models
|
||||
if strings.HasPrefix(modelLower, "gpt-5") ||
|
||||
strings.HasPrefix(modelLower, "o1-") ||
|
||||
strings.HasPrefix(modelLower, "o3-") ||
|
||||
strings.Contains(modelLower, "o1-preview") ||
|
||||
strings.Contains(modelLower, "o1-mini") ||
|
||||
strings.Contains(modelLower, "o3-mini") ||
|
||||
strings.Contains(modelLower, "o4-mini") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Anthropic extended thinking models
|
||||
if strings.Contains(modelLower, "claude-3-7") ||
|
||||
strings.Contains(modelLower, "claude-3.7") ||
|
||||
strings.Contains(modelLower, "claude-4") ||
|
||||
strings.Contains(modelLower, "claude-opus-4") ||
|
||||
strings.Contains(modelLower, "claude-sonnet-4") {
|
||||
return true
|
||||
}
|
||||
|
||||
// DeepSeek reasoner
|
||||
if strings.Contains(modelLower, "deepseek-reasoner") ||
|
||||
strings.Contains(modelLower, "deepseek-r1") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Grok reasoning models
|
||||
if strings.Contains(modelLower, "grok") && strings.Contains(modelLower, "reasoning") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CalculateThinkingBudget calculates the token budget based on mode and max tokens.
|
||||
func CalculateThinkingBudget(mode ThinkingMode, maxTokens int) int {
|
||||
switch mode {
|
||||
case ThinkingModeLow:
|
||||
return maxTokens * 20 / 100 // 20%
|
||||
case ThinkingModeMedium:
|
||||
return maxTokens * 50 / 100 // 50%
|
||||
case ThinkingModeHigh:
|
||||
return maxTokens * 80 / 100 // 80%
|
||||
case ThinkingModeAuto:
|
||||
// Let the model decide
|
||||
return 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// ThinkingTokenUsage represents token usage specific to thinking/reasoning.
|
||||
type ThinkingTokenUsage struct {
|
||||
// ThinkingTokens is the total number of thinking/reasoning tokens used.
|
||||
ThinkingTokens int `json:"thinking_tokens,omitempty"`
|
||||
|
||||
// ThinkingInputTokens is the number of input tokens used for thinking.
|
||||
ThinkingInputTokens int `json:"thinking_input_tokens,omitempty"`
|
||||
|
||||
// ThinkingOutputTokens is the number of output tokens from thinking.
|
||||
ThinkingOutputTokens int `json:"thinking_output_tokens,omitempty"`
|
||||
|
||||
// ThinkingCachedTokens is the number of cached thinking tokens (if applicable).
|
||||
ThinkingCachedTokens int `json:"thinking_cached_tokens,omitempty"`
|
||||
|
||||
// ThinkingBudgetUsed is the actual budget used vs allocated.
|
||||
ThinkingBudgetUsed int `json:"thinking_budget_used,omitempty"`
|
||||
|
||||
// ThinkingBudgetAllocated is the budget that was allocated.
|
||||
ThinkingBudgetAllocated int `json:"thinking_budget_allocated,omitempty"`
|
||||
}
|
||||
|
||||
// ExtractThinkingTokens extracts thinking token information from generation info.
|
||||
func ExtractThinkingTokens(generationInfo map[string]any) *ThinkingTokenUsage {
|
||||
if generationInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
usage := &ThinkingTokenUsage{}
|
||||
|
||||
// OpenAI-style reasoning tokens
|
||||
if v, ok := generationInfo["ReasoningTokens"].(int); ok {
|
||||
usage.ThinkingTokens = v
|
||||
}
|
||||
if v, ok := generationInfo["CompletionReasoningTokens"].(int); ok {
|
||||
usage.ThinkingOutputTokens = v
|
||||
}
|
||||
|
||||
// Anthropic-style thinking tokens (would be in extended thinking mode)
|
||||
if v, ok := generationInfo["ThinkingTokens"].(int); ok {
|
||||
usage.ThinkingTokens = v
|
||||
}
|
||||
if v, ok := generationInfo["ThinkingInputTokens"].(int); ok {
|
||||
usage.ThinkingInputTokens = v
|
||||
}
|
||||
if v, ok := generationInfo["ThinkingOutputTokens"].(int); ok {
|
||||
usage.ThinkingOutputTokens = v
|
||||
}
|
||||
|
||||
// Budget information
|
||||
if v, ok := generationInfo["ThinkingBudgetUsed"].(int); ok {
|
||||
usage.ThinkingBudgetUsed = v
|
||||
}
|
||||
if v, ok := generationInfo["ThinkingBudgetAllocated"].(int); ok {
|
||||
usage.ThinkingBudgetAllocated = v
|
||||
}
|
||||
|
||||
return usage
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
package llms_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
)
|
||||
|
||||
func TestThinkingModes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mode llms.ThinkingMode
|
||||
expected string
|
||||
}{
|
||||
{"None", llms.ThinkingModeNone, "none"},
|
||||
{"Low", llms.ThinkingModeLow, "low"},
|
||||
{"Medium", llms.ThinkingModeMedium, "medium"},
|
||||
{"High", llms.ThinkingModeHigh, "high"},
|
||||
{"Auto", llms.ThinkingModeAuto, "auto"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if string(tt.mode) != tt.expected {
|
||||
t.Errorf("ThinkingMode %s = %s, want %s", tt.name, tt.mode, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultThinkingConfig(t *testing.T) {
|
||||
config := llms.DefaultThinkingConfig()
|
||||
|
||||
if config.Mode != llms.ThinkingModeAuto {
|
||||
t.Errorf("Default mode = %s, want %s", config.Mode, llms.ThinkingModeAuto)
|
||||
}
|
||||
|
||||
if config.ReturnThinking {
|
||||
t.Error("Default ReturnThinking should be false")
|
||||
}
|
||||
|
||||
if config.StreamThinking {
|
||||
t.Error("Default StreamThinking should be false")
|
||||
}
|
||||
|
||||
if config.InterleaveThinking {
|
||||
t.Error("Default InterleaveThinking should be false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithThinking(t *testing.T) {
|
||||
config := &llms.ThinkingConfig{
|
||||
Mode: llms.ThinkingModeHigh,
|
||||
BudgetTokens: 4096,
|
||||
ReturnThinking: true,
|
||||
StreamThinking: true,
|
||||
}
|
||||
|
||||
option := llms.WithThinking(config)
|
||||
|
||||
var opts llms.CallOptions
|
||||
option(&opts)
|
||||
|
||||
if opts.Metadata == nil {
|
||||
t.Fatal("metadata should be initialized")
|
||||
}
|
||||
|
||||
storedConfig, ok := opts.Metadata["thinking_config"].(*llms.ThinkingConfig)
|
||||
if !ok {
|
||||
t.Fatal("thinking_config should be stored in metadata")
|
||||
}
|
||||
|
||||
if storedConfig != config {
|
||||
t.Error("stored config should match original")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithThinkingMode(t *testing.T) {
|
||||
option := llms.WithThinkingMode(llms.ThinkingModeMedium)
|
||||
|
||||
var opts llms.CallOptions
|
||||
option(&opts)
|
||||
|
||||
config, ok := opts.Metadata["thinking_config"].(*llms.ThinkingConfig)
|
||||
if !ok {
|
||||
t.Fatal("thinking_config should be stored in metadata")
|
||||
}
|
||||
|
||||
if config.Mode != llms.ThinkingModeMedium {
|
||||
t.Errorf("mode = %s, want %s", config.Mode, llms.ThinkingModeMedium)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithThinkingBudget(t *testing.T) {
|
||||
option := llms.WithThinkingBudget(8192)
|
||||
|
||||
var opts llms.CallOptions
|
||||
option(&opts)
|
||||
|
||||
config, ok := opts.Metadata["thinking_config"].(*llms.ThinkingConfig)
|
||||
if !ok {
|
||||
t.Fatal("thinking_config should be stored in metadata")
|
||||
}
|
||||
|
||||
if config.BudgetTokens != 8192 {
|
||||
t.Errorf("BudgetTokens = %d, want 8192", config.BudgetTokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithReturnThinking(t *testing.T) {
|
||||
option := llms.WithReturnThinking(true)
|
||||
|
||||
var opts llms.CallOptions
|
||||
option(&opts)
|
||||
|
||||
config, ok := opts.Metadata["thinking_config"].(*llms.ThinkingConfig)
|
||||
if !ok {
|
||||
t.Fatal("thinking_config should be stored in metadata")
|
||||
}
|
||||
|
||||
if !config.ReturnThinking {
|
||||
t.Error("ReturnThinking should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithStreamThinking(t *testing.T) {
|
||||
option := llms.WithStreamThinking(true)
|
||||
|
||||
var opts llms.CallOptions
|
||||
option(&opts)
|
||||
|
||||
config, ok := opts.Metadata["thinking_config"].(*llms.ThinkingConfig)
|
||||
if !ok {
|
||||
t.Fatal("thinking_config should be stored in metadata")
|
||||
}
|
||||
|
||||
if !config.StreamThinking {
|
||||
t.Error("StreamThinking should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithInterleaveThinking(t *testing.T) {
|
||||
option := llms.WithInterleaveThinking(true)
|
||||
|
||||
var opts llms.CallOptions
|
||||
option(&opts)
|
||||
|
||||
config, ok := opts.Metadata["thinking_config"].(*llms.ThinkingConfig)
|
||||
if !ok {
|
||||
t.Fatal("thinking_config should be stored in metadata")
|
||||
}
|
||||
|
||||
if !config.InterleaveThinking {
|
||||
t.Error("InterleaveThinking should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsReasoningModel(t *testing.T) {
|
||||
tests := []struct {
|
||||
model string
|
||||
expected bool
|
||||
}{
|
||||
// OpenAI reasoning models
|
||||
{"gpt-5-mini", true},
|
||||
{"gpt-5-preview", true},
|
||||
{"o1-preview", true},
|
||||
{"o1-mini", true},
|
||||
{"o3-mini", true},
|
||||
{"o3-2025-04-16", true},
|
||||
{"o4-mini", true},
|
||||
|
||||
// Anthropic extended thinking models
|
||||
{"claude-3-7-sonnet", true},
|
||||
{"claude-3.7-sonnet", true},
|
||||
{"claude-4", true},
|
||||
{"claude-opus-4", true},
|
||||
{"claude-sonnet-4", true},
|
||||
|
||||
// DeepSeek reasoner
|
||||
{"deepseek-reasoner", true},
|
||||
{"deepseek-r1", true},
|
||||
|
||||
// Grok reasoning
|
||||
{"grok-reasoning", true},
|
||||
|
||||
// Non-reasoning models
|
||||
{"gpt-4", false},
|
||||
{"gpt-3.5-turbo", false},
|
||||
{"claude-3-sonnet", false},
|
||||
{"claude-2", false},
|
||||
{"llama-2", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.model, func(t *testing.T) {
|
||||
result := llms.IsReasoningModel(tt.model)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsReasoningModel(%q) = %v, want %v", tt.model, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateThinkingBudget(t *testing.T) {
|
||||
tests := []struct {
|
||||
mode llms.ThinkingMode
|
||||
maxTokens int
|
||||
expected int
|
||||
}{
|
||||
{llms.ThinkingModeLow, 1000, 200}, // 20%
|
||||
{llms.ThinkingModeMedium, 1000, 500}, // 50%
|
||||
{llms.ThinkingModeHigh, 1000, 800}, // 80%
|
||||
{llms.ThinkingModeAuto, 1000, 0}, // Let model decide
|
||||
{llms.ThinkingModeNone, 1000, 0}, // No thinking
|
||||
{llms.ThinkingModeLow, 5000, 1000}, // 20% of 5000
|
||||
{llms.ThinkingModeMedium, 5000, 2500}, // 50% of 5000
|
||||
{llms.ThinkingModeHigh, 5000, 4000}, // 80% of 5000
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.mode), func(t *testing.T) {
|
||||
result := llms.CalculateThinkingBudget(tt.mode, tt.maxTokens)
|
||||
if result != tt.expected {
|
||||
t.Errorf("CalculateThinkingBudget(%s, %d) = %d, want %d",
|
||||
tt.mode, tt.maxTokens, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractThinkingTokens(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
generationInfo map[string]any
|
||||
expected *llms.ThinkingTokenUsage
|
||||
}{
|
||||
{
|
||||
name: "OpenAI reasoning tokens",
|
||||
generationInfo: map[string]any{
|
||||
"ReasoningTokens": 500,
|
||||
"CompletionReasoningTokens": 300,
|
||||
},
|
||||
expected: &llms.ThinkingTokenUsage{
|
||||
ThinkingTokens: 500,
|
||||
ThinkingOutputTokens: 300,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Anthropic thinking tokens",
|
||||
generationInfo: map[string]any{
|
||||
"ThinkingTokens": 1000,
|
||||
"ThinkingInputTokens": 200,
|
||||
"ThinkingOutputTokens": 800,
|
||||
"ThinkingBudgetUsed": 1000,
|
||||
"ThinkingBudgetAllocated": 2000,
|
||||
},
|
||||
expected: &llms.ThinkingTokenUsage{
|
||||
ThinkingTokens: 1000,
|
||||
ThinkingInputTokens: 200,
|
||||
ThinkingOutputTokens: 800,
|
||||
ThinkingBudgetUsed: 1000,
|
||||
ThinkingBudgetAllocated: 2000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil generationInfo",
|
||||
generationInfo: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "empty generationInfo",
|
||||
generationInfo: map[string]any{},
|
||||
expected: &llms.ThinkingTokenUsage{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := llms.ExtractThinkingTokens(tt.generationInfo)
|
||||
|
||||
if tt.expected == nil {
|
||||
if result != nil {
|
||||
t.Error("expected nil result")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("expected non-nil result")
|
||||
}
|
||||
|
||||
if result.ThinkingTokens != tt.expected.ThinkingTokens {
|
||||
t.Errorf("ThinkingTokens = %d, want %d",
|
||||
result.ThinkingTokens, tt.expected.ThinkingTokens)
|
||||
}
|
||||
|
||||
if result.ThinkingInputTokens != tt.expected.ThinkingInputTokens {
|
||||
t.Errorf("ThinkingInputTokens = %d, want %d",
|
||||
result.ThinkingInputTokens, tt.expected.ThinkingInputTokens)
|
||||
}
|
||||
|
||||
if result.ThinkingOutputTokens != tt.expected.ThinkingOutputTokens {
|
||||
t.Errorf("ThinkingOutputTokens = %d, want %d",
|
||||
result.ThinkingOutputTokens, tt.expected.ThinkingOutputTokens)
|
||||
}
|
||||
|
||||
if result.ThinkingBudgetUsed != tt.expected.ThinkingBudgetUsed {
|
||||
t.Errorf("ThinkingBudgetUsed = %d, want %d",
|
||||
result.ThinkingBudgetUsed, tt.expected.ThinkingBudgetUsed)
|
||||
}
|
||||
|
||||
if result.ThinkingBudgetAllocated != tt.expected.ThinkingBudgetAllocated {
|
||||
t.Errorf("ThinkingBudgetAllocated = %d, want %d",
|
||||
result.ThinkingBudgetAllocated, tt.expected.ThinkingBudgetAllocated)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MockLLMWithThinking is a mock LLM that supports thinking tokens
|
||||
type MockLLMWithThinking struct {
|
||||
thinkingConfig *llms.ThinkingConfig
|
||||
supportsThinking bool
|
||||
}
|
||||
|
||||
func (m *MockLLMWithThinking) Call(ctx context.Context, prompt string, options ...llms.CallOption) (string, error) {
|
||||
return "test response with thinking", nil
|
||||
}
|
||||
|
||||
func (m *MockLLMWithThinking) GenerateContent(ctx context.Context, messages []llms.MessageContent, options ...llms.CallOption) (*llms.ContentResponse, error) {
|
||||
var opts llms.CallOptions
|
||||
for _, opt := range options {
|
||||
opt(&opts)
|
||||
}
|
||||
|
||||
// Extract thinking config
|
||||
if opts.Metadata != nil {
|
||||
if config, ok := opts.Metadata["thinking_config"].(*llms.ThinkingConfig); ok {
|
||||
m.thinkingConfig = config
|
||||
}
|
||||
}
|
||||
|
||||
generationInfo := map[string]any{
|
||||
"CompletionTokens": 100,
|
||||
"PromptTokens": 200,
|
||||
"TotalTokens": 300,
|
||||
}
|
||||
|
||||
// Add thinking tokens if configured
|
||||
if m.thinkingConfig != nil && m.thinkingConfig.Mode != llms.ThinkingModeNone {
|
||||
budget := llms.CalculateThinkingBudget(m.thinkingConfig.Mode, 1000)
|
||||
generationInfo["ReasoningTokens"] = budget
|
||||
generationInfo["ThinkingBudgetAllocated"] = budget
|
||||
generationInfo["ThinkingBudgetUsed"] = budget * 80 / 100 // Use 80% of budget
|
||||
}
|
||||
|
||||
content := "test response"
|
||||
if m.thinkingConfig != nil && m.thinkingConfig.ReturnThinking {
|
||||
content = "<thinking>step by step reasoning</thinking>\n" + content
|
||||
}
|
||||
|
||||
return &llms.ContentResponse{
|
||||
Choices: []*llms.ContentChoice{
|
||||
{
|
||||
Content: content,
|
||||
GenerationInfo: generationInfo,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SupportsReasoning implements the ReasoningModel interface.
|
||||
func (m *MockLLMWithThinking) SupportsReasoning() bool {
|
||||
return m.supportsThinking
|
||||
}
|
||||
|
||||
func TestThinkingIntegration(t *testing.T) {
|
||||
llm := &MockLLMWithThinking{supportsThinking: true}
|
||||
ctx := context.Background()
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{llms.TextPart("test")},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("with thinking mode high", func(t *testing.T) {
|
||||
resp, err := llm.GenerateContent(ctx, messages,
|
||||
llms.WithThinkingMode(llms.ThinkingModeHigh),
|
||||
llms.WithReturnThinking(true),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if llm.thinkingConfig == nil {
|
||||
t.Fatal("thinking config should be set")
|
||||
}
|
||||
|
||||
if llm.thinkingConfig.Mode != llms.ThinkingModeHigh {
|
||||
t.Errorf("mode = %s, want %s", llm.thinkingConfig.Mode, llms.ThinkingModeHigh)
|
||||
}
|
||||
|
||||
if !llm.thinkingConfig.ReturnThinking {
|
||||
t.Error("ReturnThinking should be true")
|
||||
}
|
||||
|
||||
// Check response includes thinking
|
||||
if resp.Choices[0].Content == "" {
|
||||
t.Error("content should not be empty")
|
||||
}
|
||||
|
||||
// Extract thinking tokens
|
||||
usage := llms.ExtractThinkingTokens(resp.Choices[0].GenerationInfo)
|
||||
if usage == nil {
|
||||
t.Fatal("thinking token usage should be extracted")
|
||||
}
|
||||
|
||||
if usage.ThinkingTokens == 0 {
|
||||
t.Error("ThinkingTokens should be > 0")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with explicit budget", func(t *testing.T) {
|
||||
_, err := llm.GenerateContent(ctx, messages,
|
||||
llms.WithThinkingBudget(4096),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if llm.thinkingConfig == nil {
|
||||
t.Fatal("thinking config should be set")
|
||||
}
|
||||
|
||||
if llm.thinkingConfig.BudgetTokens != 4096 {
|
||||
t.Errorf("BudgetTokens = %d, want 4096", llm.thinkingConfig.BudgetTokens)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSupportsReasoningModel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
llm interface{}
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "LLM with reasoning support",
|
||||
llm: &MockLLMWithThinking{supportsThinking: true},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "LLM without reasoning support",
|
||||
llm: &MockLLMWithThinking{supportsThinking: false},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Non-reasoning LLM",
|
||||
llm: struct{}{},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := llms.SupportsReasoningModel(tt.llm)
|
||||
if result != tt.expected {
|
||||
t.Errorf("SupportsReasoningModel() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package llms_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
)
|
||||
|
||||
// MockLLMWithTokenUsage is a mock LLM that returns token usage information
|
||||
type MockLLMWithTokenUsage struct {
|
||||
includeCache bool
|
||||
}
|
||||
|
||||
func (m *MockLLMWithTokenUsage) Call(ctx context.Context, prompt string, options ...llms.CallOption) (string, error) {
|
||||
return "test response", nil
|
||||
}
|
||||
|
||||
func (m *MockLLMWithTokenUsage) GenerateContent(ctx context.Context, messages []llms.MessageContent, options ...llms.CallOption) (*llms.ContentResponse, error) {
|
||||
generationInfo := map[string]any{
|
||||
"CompletionTokens": 50,
|
||||
"PromptTokens": 100,
|
||||
"TotalTokens": 150,
|
||||
}
|
||||
|
||||
if m.includeCache {
|
||||
// OpenAI-style cache tokens
|
||||
generationInfo["PromptCachedTokens"] = 80
|
||||
|
||||
// Anthropic-style cache tokens
|
||||
generationInfo["CacheCreationInputTokens"] = 20
|
||||
generationInfo["CacheReadInputTokens"] = 80
|
||||
}
|
||||
|
||||
return &llms.ContentResponse{
|
||||
Choices: []*llms.ContentChoice{
|
||||
{
|
||||
Content: "test response",
|
||||
GenerationInfo: generationInfo,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestTokenUtilizationWithoutCache(t *testing.T) {
|
||||
llm := &MockLLMWithTokenUsage{includeCache: false}
|
||||
ctx := context.Background()
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{llms.TextPart("test")},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := llm.GenerateContent(ctx, messages)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Choices) == 0 {
|
||||
t.Fatal("expected at least one choice")
|
||||
}
|
||||
|
||||
info := resp.Choices[0].GenerationInfo
|
||||
|
||||
// Check basic token counts
|
||||
if ct, ok := info["CompletionTokens"].(int); !ok || ct != 50 {
|
||||
t.Errorf("expected CompletionTokens=50, got %v", info["CompletionTokens"])
|
||||
}
|
||||
|
||||
if pt, ok := info["PromptTokens"].(int); !ok || pt != 100 {
|
||||
t.Errorf("expected PromptTokens=100, got %v", info["PromptTokens"])
|
||||
}
|
||||
|
||||
if tt, ok := info["TotalTokens"].(int); !ok || tt != 150 {
|
||||
t.Errorf("expected TotalTokens=150, got %v", info["TotalTokens"])
|
||||
}
|
||||
|
||||
// Cache tokens should not be present
|
||||
if _, ok := info["PromptCachedTokens"]; ok {
|
||||
t.Error("PromptCachedTokens should not be present")
|
||||
}
|
||||
|
||||
if _, ok := info["CacheCreationInputTokens"]; ok {
|
||||
t.Error("CacheCreationInputTokens should not be present")
|
||||
}
|
||||
|
||||
if _, ok := info["CacheReadInputTokens"]; ok {
|
||||
t.Error("CacheReadInputTokens should not be present")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenUtilizationWithCache(t *testing.T) {
|
||||
llm := &MockLLMWithTokenUsage{includeCache: true}
|
||||
ctx := context.Background()
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{llms.TextPart("test")},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := llm.GenerateContent(ctx, messages)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Choices) == 0 {
|
||||
t.Fatal("expected at least one choice")
|
||||
}
|
||||
|
||||
info := resp.Choices[0].GenerationInfo
|
||||
|
||||
// Check basic token counts
|
||||
if ct, ok := info["CompletionTokens"].(int); !ok || ct != 50 {
|
||||
t.Errorf("expected CompletionTokens=50, got %v", info["CompletionTokens"])
|
||||
}
|
||||
|
||||
if pt, ok := info["PromptTokens"].(int); !ok || pt != 100 {
|
||||
t.Errorf("expected PromptTokens=100, got %v", info["PromptTokens"])
|
||||
}
|
||||
|
||||
if tt, ok := info["TotalTokens"].(int); !ok || tt != 150 {
|
||||
t.Errorf("expected TotalTokens=150, got %v", info["TotalTokens"])
|
||||
}
|
||||
|
||||
// OpenAI-style cache tokens
|
||||
if pct, ok := info["PromptCachedTokens"].(int); !ok || pct != 80 {
|
||||
t.Errorf("expected PromptCachedTokens=80, got %v", info["PromptCachedTokens"])
|
||||
}
|
||||
|
||||
// Anthropic-style cache tokens
|
||||
if ccit, ok := info["CacheCreationInputTokens"].(int); !ok || ccit != 20 {
|
||||
t.Errorf("expected CacheCreationInputTokens=20, got %v", info["CacheCreationInputTokens"])
|
||||
}
|
||||
|
||||
if crit, ok := info["CacheReadInputTokens"].(int); !ok || crit != 80 {
|
||||
t.Errorf("expected CacheReadInputTokens=80, got %v", info["CacheReadInputTokens"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateCostSavings(t *testing.T) {
|
||||
// Test function to calculate cost savings from cached tokens
|
||||
tests := []struct {
|
||||
name string
|
||||
promptTokens int
|
||||
cachedTokens int
|
||||
pricePerMToken float64
|
||||
expectedSavings float64
|
||||
}{
|
||||
{
|
||||
name: "OpenAI 50% discount",
|
||||
promptTokens: 1000,
|
||||
cachedTokens: 800,
|
||||
pricePerMToken: 5.0, // $5 per 1M tokens
|
||||
expectedSavings: 0.002, // 800 tokens * 50% discount * $5/1M
|
||||
},
|
||||
{
|
||||
name: "Anthropic 90% discount",
|
||||
promptTokens: 2000,
|
||||
cachedTokens: 1500,
|
||||
pricePerMToken: 15.0, // $15 per 1M tokens
|
||||
expectedSavings: 0.02025, // 1500 tokens * 90% discount * $15/1M
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Calculate savings
|
||||
var discountRate float64
|
||||
if tt.name == "OpenAI 50% discount" {
|
||||
discountRate = 0.5
|
||||
} else {
|
||||
discountRate = 0.9
|
||||
}
|
||||
|
||||
savings := float64(tt.cachedTokens) * discountRate * tt.pricePerMToken / 1_000_000
|
||||
|
||||
if savings != tt.expectedSavings {
|
||||
t.Errorf("expected savings=%f, got %f", tt.expectedSavings, savings)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package watsonx
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
wx "github.com/IBM/watsonx-go/pkg/models"
|
||||
"github.com/tmc/langchaingo/testing/llmtest"
|
||||
)
|
||||
|
||||
func TestLLM(t *testing.T) {
|
||||
if os.Getenv("WATSONX_API_KEY") == "" || os.Getenv("WATSONX_PROJECT_ID") == "" {
|
||||
t.Skip("WATSONX_API_KEY or WATSONX_PROJECT_ID not set")
|
||||
}
|
||||
|
||||
llm, err := New(
|
||||
"ibm/granite-13b-instruct-v2",
|
||||
wx.WithWatsonxAPIKey(wx.WatsonxAPIKey(os.Getenv("WATSONX_API_KEY"))),
|
||||
wx.WithWatsonxProjectID(wx.WatsonxProjectID(os.Getenv("WATSONX_PROJECT_ID"))),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create WatsonX LLM: %v", err)
|
||||
}
|
||||
|
||||
llmtest.TestLLM(t, llm)
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
Files importing testify/assert or testify/require:
|
||||
|
||||
./agents/conversational_test.go
|
||||
./agents/executor_test.go
|
||||
./agents/markl_test.go
|
||||
./callbacks/agent_final_stream_test.go
|
||||
./chains/chains_test.go
|
||||
./chains/constitution/constitutional_test.go
|
||||
./chains/constitutional_test.go
|
||||
./chains/conversation_test.go
|
||||
./chains/conversational_retrieval_qa_test.go
|
||||
./chains/llm_azure_test.go
|
||||
./chains/llm_math_test.go
|
||||
./chains/llm_test.go
|
||||
./chains/map_reduce_test.go
|
||||
./chains/map_rerank_documents_test.go
|
||||
./chains/question_answering_test.go
|
||||
./chains/retrieval_qa_test.go
|
||||
./chains/sequential_test.go
|
||||
./chains/sql_database_test.go
|
||||
./chains/stuff_documents_test.go
|
||||
./chains/summarization_test.go
|
||||
./chains/transform_test.go
|
||||
./documentloaders/assemblyai_test.go
|
||||
./documentloaders/csv_test.go
|
||||
./documentloaders/html_test.go
|
||||
./documentloaders/notion_test.go
|
||||
./documentloaders/pdf_test.go
|
||||
./documentloaders/text_test.go
|
||||
./embeddings/bedrock/bedrock_test.go
|
||||
./embeddings/cybertron/cybertron_test.go
|
||||
./embeddings/embedding_test.go
|
||||
./embeddings/huggingface/huggingface_test.go
|
||||
./embeddings/jina/jina_test.go
|
||||
./embeddings/openai_test.go
|
||||
./embeddings/vector_math_test.go
|
||||
./embeddings/vertexai_palm_test.go
|
||||
./embeddings/voyageai/voyageai_test.go
|
||||
./internal/imageutil/download_test.go
|
||||
./internal/sliceutil/slice_test.go
|
||||
./llms/anthropic/internal/anthropicclient/anthropicclient_test.go
|
||||
./llms/cache/cache_test.go
|
||||
./llms/cache/inmemory/inmemory_test.go
|
||||
./llms/cloudflare/internal/cloudflareclient/cloudflareclient_test.go
|
||||
./llms/cohere/internal/cohereclient/cohereclient_test.go
|
||||
./llms/count_tokens_test.go
|
||||
./llms/ernie/internal/ernieclient/ernieclient_test.go
|
||||
./llms/googleai/shared_test/shared_test.go
|
||||
./llms/huggingface/internal/huggingfaceclient/huggingfaceclient_test.go
|
||||
./llms/llamafile/internal/llamafileclient/llamafileclient_test.go
|
||||
./llms/llamafile/llamafilellm_test.go
|
||||
./llms/maritaca/internal/maritacaclient/maritacaclient_test.go
|
||||
./llms/maritaca/maritaca_test.go
|
||||
./llms/mistral/mistralembed_test.go
|
||||
./llms/ollama/internal/ollamaclient/ollamaclient_test.go
|
||||
./llms/ollama/ollama_test.go
|
||||
./llms/openai/internal/openaiclient/chat_test.go
|
||||
./llms/openai/internal/openaiclient/openaiclient_test.go
|
||||
./llms/openai/multicontent_test.go
|
||||
./llms/openai/structured_output_test.go
|
||||
./memory/buffer_test.go
|
||||
./memory/chat_test.go
|
||||
./memory/mongo/mongo_chat_history_test.go
|
||||
./memory/sqlite3/sqlite3_history_test.go
|
||||
./memory/token_buffer_test.go
|
||||
./memory/window_buffer_test.go
|
||||
./outputparser/comma_seperated_list_test.go
|
||||
./outputparser/structured_test.go
|
||||
./prompts/chat_prompt_template_test.go
|
||||
./prompts/prompt_test.go
|
||||
./prompts/templates_test.go
|
||||
./textsplitter/markdown_splitter_test.go
|
||||
./textsplitter/recursive_character_test.go
|
||||
./textsplitter/token_splitter_test.go
|
||||
./tools/duckduckgo/ddg_test.go
|
||||
./tools/perplexity/perplexity_test.go
|
||||
./tools/serpapi/serpapi_test.go
|
||||
./tools/sqldatabase/mysql/mysql_test.go
|
||||
./tools/sqldatabase/postgresql/postgresql_test.go
|
||||
./tools/sqldatabase/sqlite3/sqlite3_test.go
|
||||
./tools/wikipedia/wikipedia_test.go
|
||||
./tools/zapier/internal/client_test.go
|
||||
./tools/zapier/zapier_test.go
|
||||
./vectorstores/alloydb/vectorstore_container_test.go
|
||||
./vectorstores/alloydb/vectorstore_test.go
|
||||
./vectorstores/azureaisearch/azureaisearch_httprr_test.go
|
||||
./vectorstores/bedrockknowledgebases/bedrockknowledgebases_test.go
|
||||
./vectorstores/chroma/chroma_test.go
|
||||
./vectorstores/cloudsql/vectorstore_container_test.go
|
||||
./vectorstores/milvus/milvus_test.go
|
||||
./vectorstores/mongovector/mongovector_test.go
|
||||
./vectorstores/opensearch/opensearch_test.go
|
||||
./vectorstores/pgvector/pgvector_test.go
|
||||
./vectorstores/pinecone/pinecone_test.go
|
||||
./vectorstores/qdrant/qdrant_test.go
|
||||
./vectorstores/redisvector/index_schema_test.go
|
||||
./vectorstores/redisvector/redis_vector_test.go
|
||||
./vectorstores/weaviate/weaviate_httprr_test.go
|
||||
./vectorstores/weaviate/weaviate_test.go
|
||||
|
||||
Total: 97 files
|
||||
|
||||
Most commonly used testify functions:
|
||||
- require.NoError (729 occurrences)
|
||||
- assert.Equal (107 occurrences)
|
||||
- assert.NotEmpty (90 occurrences)
|
||||
- require.Equal (70 occurrences)
|
||||
- require.Contains (51 occurrences)
|
||||
- require.Len (50 occurrences)
|
||||
- require.True (44 occurrences)
|
||||
- assert.Len (44 occurrences)
|
||||
- assert.NotNil (34 occurrences)
|
||||
- assert.Regexp (25 occurrences)
|
||||
- require.Error (23 occurrences)
|
||||
- require.NotEmpty (16 occurrences)
|
||||
- assert.Contains (12 occurrences)
|
||||
- require.ErrorIs (9 occurrences)
|
||||
- require.NotContains (8 occurrences)
|
||||
- assert.True (8 occurrences)
|
||||
- assert.NotContains (6 occurrences)
|
||||
- require.NotNil (4 occurrences)
|
||||
- assert.NotZero (4 occurrences)
|
||||
|
||||
These would need to be replaced with standard library testing methods like:
|
||||
- require.NoError(t, err) → if err != nil { t.Fatal(err) }
|
||||
- assert.Equal(t, expected, actual) → if expected != actual { t.Errorf("expected %v, got %v", expected, actual) }
|
||||
- require.Equal(t, expected, actual) → if expected != actual { t.Fatalf("expected %v, got %v", expected, actual) }
|
||||
- etc.
|
||||
@@ -0,0 +1,62 @@
|
||||
// Package llmtest provides utilities for testing LLM implementations.
|
||||
//
|
||||
// Inspired by Go's testing/fstest package, llmtest offers a simple,
|
||||
// backend-independent way to verify that LLM implementations conform
|
||||
// to the expected interfaces and behaviors.
|
||||
//
|
||||
// # Design Philosophy
|
||||
//
|
||||
// Following the principles of testing/fstest:
|
||||
// - Minimal API surface - one main function (TestLLM)
|
||||
// - Automatic capability discovery - no configuration required
|
||||
// - Comprehensive by default - tests all detected capabilities
|
||||
// - Interface testing - works with any llms.Model implementation
|
||||
// - Simple usage pattern - just pass the model to test
|
||||
//
|
||||
// # Usage
|
||||
//
|
||||
// Testing an LLM implementation is straightforward:
|
||||
//
|
||||
// func TestMyLLM(t *testing.T) {
|
||||
// llm, err := mylllm.New()
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// llmtest.TestLLM(t, llm)
|
||||
// }
|
||||
//
|
||||
// # Automatic Capability Discovery
|
||||
//
|
||||
// The package automatically detects and tests supported capabilities:
|
||||
// - Basic operations (Call, GenerateContent)
|
||||
// - Streaming (if model implements streaming interface)
|
||||
// - Tool/Function calling (probed with test tool)
|
||||
// - Reasoning/Thinking mode (if supported)
|
||||
// - Token counting (if usage information provided)
|
||||
// - Context caching (if implemented)
|
||||
//
|
||||
// # Mock Implementation
|
||||
//
|
||||
// A MockLLM is provided for testing without making actual API calls:
|
||||
//
|
||||
// mock := &llmtest.MockLLM{
|
||||
// CallFunc: func(ctx context.Context, prompt string, options ...llms.CallOption) (string, error) {
|
||||
// return "mocked response", nil
|
||||
// },
|
||||
// }
|
||||
// llmtest.TestLLM(t, mock)
|
||||
//
|
||||
// # Parallel Testing
|
||||
//
|
||||
// All tests run in parallel by default for better performance:
|
||||
// - Core tests (Call, GenerateContent) run concurrently
|
||||
// - Capability tests run in parallel when detected
|
||||
// - Safe for concurrent execution with independent contexts
|
||||
//
|
||||
// # Provider Coverage
|
||||
//
|
||||
// The package is used to test all LangChain Go providers:
|
||||
// anthropic, bedrock, cloudflare, cohere, ernie, fake, googleai,
|
||||
// huggingface, llamafile, local, maritaca, mistral, ollama, openai,
|
||||
// watsonx, and more.
|
||||
package llmtest
|
||||
@@ -0,0 +1,740 @@
|
||||
// Package llmtest provides support for testing LLM implementations.
|
||||
//
|
||||
// Following the design of testing/fstest, this package provides a simple
|
||||
// TestLLM function that verifies an LLM implementation behaves correctly.
|
||||
package llmtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
)
|
||||
|
||||
// TestLLM tests an LLM implementation.
|
||||
// It performs basic operations and checks that the model behaves correctly.
|
||||
// It automatically discovers and tests capabilities by probing the model.
|
||||
//
|
||||
// If TestLLM finds any misbehaviors, it reports them via t.Error/t.Fatal.
|
||||
//
|
||||
// Typical usage inside a test:
|
||||
//
|
||||
// func TestLLM(t *testing.T) {
|
||||
// llm, err := mylllm.New(...)
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// llmtest.TestLLM(t, llm)
|
||||
// }
|
||||
func TestLLM(t *testing.T, model llms.Model) {
|
||||
t.Helper()
|
||||
t.Parallel()
|
||||
|
||||
// Run core tests as subtests - these should always work
|
||||
t.Run("Core", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Call", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCall(t, model)
|
||||
})
|
||||
|
||||
t.Run("GenerateContent", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testGenerateContent(t, model)
|
||||
})
|
||||
})
|
||||
|
||||
// Discover and test capabilities
|
||||
t.Run("Capabilities", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test streaming if supported
|
||||
if supportsStreaming(model) {
|
||||
t.Run("Streaming", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testStreaming(t, model)
|
||||
})
|
||||
}
|
||||
|
||||
// Test tool calls if supported
|
||||
if supportsTools(model) {
|
||||
t.Run("ToolCalls", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testToolCalls(t, model)
|
||||
})
|
||||
}
|
||||
|
||||
// Test reasoning if supported
|
||||
if supportsReasoning(model) {
|
||||
t.Run("Reasoning", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testReasoning(t, model)
|
||||
})
|
||||
}
|
||||
|
||||
// Test caching by trying it - if it works, great
|
||||
t.Run("Caching", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCaching(t, model)
|
||||
})
|
||||
|
||||
// Test token counting - always run but don't fail if not supported
|
||||
t.Run("TokenCounting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testTokenCounting(t, model)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Capability detection functions
|
||||
|
||||
// supportsStreaming checks if the model supports streaming
|
||||
func supportsStreaming(model llms.Model) bool {
|
||||
// Check if model implements the streaming interface
|
||||
_, ok := model.(interface {
|
||||
GenerateContentStream(context.Context, []llms.MessageContent, ...llms.CallOption) (<-chan llms.ContentResponse, error)
|
||||
})
|
||||
return ok
|
||||
}
|
||||
|
||||
// supportsTools probes if the model supports tool calls
|
||||
func supportsTools(model llms.Model) bool {
|
||||
// Try a simple tool call with a dummy tool
|
||||
ctx := context.Background()
|
||||
tools := []llms.Tool{
|
||||
{
|
||||
Type: "function",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "test_tool",
|
||||
Description: "Test tool",
|
||||
Parameters: map[string]any{"type": "object"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("test"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Try with tools - if it doesn't error out, it's supported
|
||||
_, err := model.GenerateContent(ctx, messages,
|
||||
llms.WithTools(tools),
|
||||
llms.WithMaxTokens(1),
|
||||
)
|
||||
|
||||
// If we get a specific "tools not supported" error, return false
|
||||
// Otherwise assume it's supported (even if other errors occur)
|
||||
if err != nil && strings.Contains(strings.ToLower(err.Error()), "not support") {
|
||||
return false
|
||||
}
|
||||
return err == nil || !strings.Contains(strings.ToLower(err.Error()), "tool")
|
||||
}
|
||||
|
||||
// supportsReasoning checks if the model supports reasoning/thinking
|
||||
func supportsReasoning(model llms.Model) bool {
|
||||
// Check if model implements reasoning interface
|
||||
if reasoner, ok := model.(interface {
|
||||
SupportsReasoning() bool
|
||||
}); ok {
|
||||
return reasoner.SupportsReasoning()
|
||||
}
|
||||
|
||||
// Try using thinking mode and see if it works
|
||||
ctx := context.Background()
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("test"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Try with thinking mode
|
||||
resp, err := model.GenerateContent(ctx, messages,
|
||||
llms.WithMaxTokens(10),
|
||||
llms.WithThinkingMode(llms.ThinkingModeLow),
|
||||
)
|
||||
|
||||
// Check if thinking tokens are reported
|
||||
if err == nil && resp != nil && len(resp.Choices) > 0 {
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
if _, ok := genInfo["ThinkingTokens"]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// TestLLMWithOptions tests an LLM with specific test options.
|
||||
func TestLLMWithOptions(t *testing.T, model llms.Model, opts TestOptions, expected ...string) {
|
||||
t.Helper()
|
||||
|
||||
// Store options for test functions to use
|
||||
testCtx := &testContext{
|
||||
model: model,
|
||||
options: opts,
|
||||
expected: expected,
|
||||
}
|
||||
|
||||
// Run tests with context
|
||||
runTestsWithContext(t, testCtx)
|
||||
}
|
||||
|
||||
// TestOptions configures test execution.
|
||||
type TestOptions struct {
|
||||
// Timeout for each test operation
|
||||
Timeout time.Duration
|
||||
|
||||
// Skip specific test categories
|
||||
SkipCall bool
|
||||
SkipGenerateContent bool
|
||||
SkipStreaming bool
|
||||
|
||||
// Custom test prompts
|
||||
TestPrompt string
|
||||
TestMessages []llms.MessageContent
|
||||
|
||||
// For providers that need special options
|
||||
CallOptions []llms.CallOption
|
||||
}
|
||||
|
||||
// Internal test context
|
||||
type testContext struct {
|
||||
model llms.Model
|
||||
options TestOptions
|
||||
expected []string
|
||||
}
|
||||
|
||||
func runTestsWithContext(t *testing.T, ctx *testContext) {
|
||||
behaviors := make(map[string]bool)
|
||||
for _, exp := range ctx.expected {
|
||||
behaviors[exp] = true
|
||||
}
|
||||
|
||||
if !ctx.options.SkipCall {
|
||||
t.Run("Call", func(t *testing.T) {
|
||||
testCallWithContext(t, ctx)
|
||||
})
|
||||
}
|
||||
|
||||
if !ctx.options.SkipGenerateContent {
|
||||
t.Run("GenerateContent", func(t *testing.T) {
|
||||
testGenerateContentWithContext(t, ctx)
|
||||
})
|
||||
}
|
||||
|
||||
if behaviors["supports-streaming"] && !ctx.options.SkipStreaming {
|
||||
t.Run("Streaming", func(t *testing.T) {
|
||||
testStreamingWithContext(t, ctx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Core test implementations
|
||||
|
||||
func testCall(t *testing.T, model llms.Model) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := llms.GenerateFromSinglePrompt(ctx, model, "Reply with 'OK' and nothing else", llms.WithMaxTokens(10))
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
t.Error("Call returned empty result")
|
||||
}
|
||||
}
|
||||
|
||||
func testCallWithContext(t *testing.T, tctx *testContext) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
if tctx.options.Timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, tctx.options.Timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
prompt := "Reply with 'OK' and nothing else"
|
||||
if tctx.options.TestPrompt != "" {
|
||||
prompt = tctx.options.TestPrompt
|
||||
}
|
||||
|
||||
opts := append([]llms.CallOption{llms.WithMaxTokens(10)}, tctx.options.CallOptions...)
|
||||
result, err := llms.GenerateFromSinglePrompt(ctx, tctx.model, prompt, opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
t.Error("Call returned empty result")
|
||||
}
|
||||
}
|
||||
|
||||
func testGenerateContent(t *testing.T, model llms.Model) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("Reply with 'Hello' and nothing else"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := model.GenerateContent(ctx, messages, llms.WithMaxTokens(10))
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateContent failed: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Choices) == 0 {
|
||||
t.Fatal("No choices in response")
|
||||
}
|
||||
|
||||
content := strings.ToLower(resp.Choices[0].Content)
|
||||
if !strings.Contains(content, "hello") {
|
||||
t.Errorf("Expected 'hello' in response, got: %s", resp.Choices[0].Content)
|
||||
}
|
||||
}
|
||||
|
||||
func testGenerateContentWithContext(t *testing.T, tctx *testContext) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
if tctx.options.Timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, tctx.options.Timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
messages := tctx.options.TestMessages
|
||||
if len(messages) == 0 {
|
||||
messages = []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("Reply with 'Hello' and nothing else"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
opts := append([]llms.CallOption{llms.WithMaxTokens(10)}, tctx.options.CallOptions...)
|
||||
resp, err := tctx.model.GenerateContent(ctx, messages, opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateContent failed: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Choices) == 0 {
|
||||
t.Fatal("No choices in response")
|
||||
}
|
||||
}
|
||||
|
||||
func testStreaming(t *testing.T, model llms.Model) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("Count from 1 to 3"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Skip if model doesn't support streaming
|
||||
streamer, ok := model.(interface {
|
||||
GenerateContentStream(context.Context, []llms.MessageContent, ...llms.CallOption) (<-chan llms.ContentResponse, error)
|
||||
})
|
||||
if !ok {
|
||||
t.Skip("Model doesn't support streaming")
|
||||
}
|
||||
|
||||
stream, err := streamer.GenerateContentStream(ctx, messages, llms.WithMaxTokens(50))
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateContentStream failed: %v", err)
|
||||
}
|
||||
|
||||
var chunks []string
|
||||
for chunk := range stream {
|
||||
if len(chunk.Choices) > 0 {
|
||||
chunks = append(chunks, chunk.Choices[0].Content)
|
||||
}
|
||||
}
|
||||
|
||||
if len(chunks) == 0 {
|
||||
t.Error("No chunks received from stream")
|
||||
}
|
||||
|
||||
fullContent := strings.Join(chunks, "")
|
||||
if fullContent == "" {
|
||||
t.Error("Stream produced no content")
|
||||
}
|
||||
}
|
||||
|
||||
func testStreamingWithContext(t *testing.T, tctx *testContext) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
if tctx.options.Timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, tctx.options.Timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
messages := tctx.options.TestMessages
|
||||
if len(messages) == 0 {
|
||||
messages = []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("Count from 1 to 3"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if model doesn't support streaming
|
||||
streamer, ok := tctx.model.(interface {
|
||||
GenerateContentStream(context.Context, []llms.MessageContent, ...llms.CallOption) (<-chan llms.ContentResponse, error)
|
||||
})
|
||||
if !ok {
|
||||
t.Skip("Model doesn't support streaming")
|
||||
}
|
||||
|
||||
opts := append([]llms.CallOption{llms.WithMaxTokens(50)}, tctx.options.CallOptions...)
|
||||
stream, err := streamer.GenerateContentStream(ctx, messages, opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateContentStream failed: %v", err)
|
||||
}
|
||||
|
||||
var chunks []string
|
||||
for chunk := range stream {
|
||||
if len(chunk.Choices) > 0 {
|
||||
chunks = append(chunks, chunk.Choices[0].Content)
|
||||
}
|
||||
}
|
||||
|
||||
if len(chunks) == 0 {
|
||||
t.Error("No chunks received from stream")
|
||||
}
|
||||
}
|
||||
|
||||
func testToolCalls(t *testing.T, model llms.Model) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
// Define a simple tool
|
||||
tools := []llms.Tool{
|
||||
{
|
||||
Type: "function",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "get_weather",
|
||||
Description: "Get the weather for a location",
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"location": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The city and country",
|
||||
},
|
||||
},
|
||||
"required": []string{"location"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("What's the weather in San Francisco?"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := model.GenerateContent(ctx, messages,
|
||||
llms.WithTools(tools),
|
||||
llms.WithMaxTokens(100),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateContent with tools failed: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Choices) == 0 {
|
||||
t.Fatal("No choices in response")
|
||||
}
|
||||
|
||||
// Check if tool was called
|
||||
choice := resp.Choices[0]
|
||||
if len(choice.ToolCalls) == 0 {
|
||||
t.Log("No tool calls in response (model may not support tools)")
|
||||
} else {
|
||||
toolCall := choice.ToolCalls[0]
|
||||
if toolCall.FunctionCall.Name != "get_weather" {
|
||||
t.Errorf("Expected get_weather tool call, got: %s", toolCall.FunctionCall.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testReasoning(t *testing.T, model llms.Model) {
|
||||
t.Helper()
|
||||
|
||||
// Check if model supports reasoning
|
||||
if reasoner, ok := model.(interface {
|
||||
SupportsReasoning() bool
|
||||
}); ok && !reasoner.SupportsReasoning() {
|
||||
t.Skip("Model doesn't support reasoning")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("What is 25 + 17? Think step by step."),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Try with thinking mode if available
|
||||
var opts []llms.CallOption
|
||||
opts = append(opts, llms.WithMaxTokens(200))
|
||||
|
||||
// Try to use thinking mode (may not be supported)
|
||||
if thinkingMode := llms.ThinkingModeMedium; true {
|
||||
opts = append(opts, llms.WithThinkingMode(thinkingMode))
|
||||
}
|
||||
|
||||
resp, err := model.GenerateContent(ctx, messages, opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateContent failed: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Choices) == 0 {
|
||||
t.Fatal("No choices in response")
|
||||
}
|
||||
|
||||
content := resp.Choices[0].Content
|
||||
if !strings.Contains(content, "42") {
|
||||
t.Log("Answer might be incorrect (expected 42)")
|
||||
}
|
||||
|
||||
// Check for reasoning tokens if available
|
||||
if genInfo := resp.Choices[0].GenerationInfo; genInfo != nil {
|
||||
if thinkingTokens, ok := genInfo["ThinkingTokens"].(int); ok {
|
||||
t.Logf("Used %d thinking tokens", thinkingTokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCaching(t *testing.T, model llms.Model) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
// Long context that benefits from caching
|
||||
longContext := strings.Repeat("This is cached context. ", 50)
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeSystem,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart(longContext),
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("Say 'OK'"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// First call (cache miss)
|
||||
_, err := model.GenerateContent(ctx, messages, llms.WithMaxTokens(10))
|
||||
if err != nil {
|
||||
t.Fatalf("First call failed: %v", err)
|
||||
}
|
||||
|
||||
// Second call (potential cache hit)
|
||||
resp2, err := model.GenerateContent(ctx, messages, llms.WithMaxTokens(10))
|
||||
if err != nil {
|
||||
t.Fatalf("Second call failed: %v", err)
|
||||
}
|
||||
|
||||
// Check if caching info is available
|
||||
if genInfo := resp2.Choices[0].GenerationInfo; genInfo != nil {
|
||||
if cached, ok := genInfo["CachedTokens"].(int); ok && cached > 0 {
|
||||
t.Logf("Cached %d tokens", cached)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testTokenCounting(t *testing.T, model llms.Model) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("Count to 5"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := model.GenerateContent(ctx, messages, llms.WithMaxTokens(50))
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateContent failed: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Choices) == 0 {
|
||||
t.Fatal("No choices in response")
|
||||
}
|
||||
|
||||
genInfo := resp.Choices[0].GenerationInfo
|
||||
if genInfo == nil {
|
||||
t.Skip("No generation info provided")
|
||||
}
|
||||
|
||||
// Check for token counts
|
||||
var hasTokenInfo bool
|
||||
for _, field := range []string{"TotalTokens", "PromptTokens", "CompletionTokens"} {
|
||||
if v, ok := genInfo[field].(int); ok && v > 0 {
|
||||
hasTokenInfo = true
|
||||
t.Logf("%s: %d", field, v)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasTokenInfo {
|
||||
t.Log("No token counting information provided")
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateLLM checks if a model satisfies basic requirements without running tests.
|
||||
// It returns an error describing what's wrong, or nil if the model is valid.
|
||||
func ValidateLLM(model llms.Model) error {
|
||||
if model == nil {
|
||||
return errors.New("model is nil")
|
||||
}
|
||||
|
||||
// Check if required methods are implemented
|
||||
ctx := context.Background()
|
||||
|
||||
// Try a simple call
|
||||
_, err := llms.GenerateFromSinglePrompt(ctx, model, "test", llms.WithMaxTokens(1))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Call method failed: %w", err)
|
||||
}
|
||||
|
||||
// Try GenerateContent
|
||||
messages := []llms.MessageContent{
|
||||
{
|
||||
Role: llms.ChatMessageTypeHuman,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.TextPart("test"),
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err = model.GenerateContent(ctx, messages, llms.WithMaxTokens(1))
|
||||
if err != nil {
|
||||
return fmt.Errorf("GenerateContent method failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MockLLM provides a simple mock implementation for testing.
|
||||
type MockLLM struct {
|
||||
// Response to return from Call
|
||||
CallResponse string
|
||||
CallError error
|
||||
|
||||
// Response to return from GenerateContent
|
||||
GenerateResponse *llms.ContentResponse
|
||||
GenerateError error
|
||||
|
||||
// Track calls for verification
|
||||
CallCount int
|
||||
GenerateCount int
|
||||
LastPrompt string
|
||||
LastMessages []llms.MessageContent
|
||||
}
|
||||
|
||||
// Call implements llms.Model
|
||||
func (m *MockLLM) Call(ctx context.Context, prompt string, options ...llms.CallOption) (string, error) {
|
||||
m.CallCount++
|
||||
m.LastPrompt = prompt
|
||||
return m.CallResponse, m.CallError
|
||||
}
|
||||
|
||||
// GenerateContent implements llms.Model
|
||||
func (m *MockLLM) GenerateContent(ctx context.Context, messages []llms.MessageContent, options ...llms.CallOption) (*llms.ContentResponse, error) {
|
||||
m.GenerateCount++
|
||||
m.LastMessages = messages
|
||||
|
||||
if m.GenerateResponse != nil {
|
||||
return m.GenerateResponse, m.GenerateError
|
||||
}
|
||||
|
||||
// Default response
|
||||
return &llms.ContentResponse{
|
||||
Choices: []*llms.ContentChoice{
|
||||
{
|
||||
Content: "mock response",
|
||||
},
|
||||
},
|
||||
}, m.GenerateError
|
||||
}
|
||||
|
||||
// GenerateContentStream implements streaming
|
||||
func (m *MockLLM) GenerateContentStream(ctx context.Context, messages []llms.MessageContent, options ...llms.CallOption) (<-chan llms.ContentResponse, error) {
|
||||
// Create a channel and send the mock response
|
||||
ch := make(chan llms.ContentResponse, 1)
|
||||
|
||||
// Send the response in chunks
|
||||
go func() {
|
||||
defer close(ch)
|
||||
|
||||
// Simulate streaming by sending the response in parts
|
||||
if m.GenerateResponse != nil {
|
||||
ch <- *m.GenerateResponse
|
||||
} else {
|
||||
// Default streaming response
|
||||
ch <- llms.ContentResponse{
|
||||
Choices: []*llms.ContentChoice{
|
||||
{
|
||||
Content: "mock",
|
||||
},
|
||||
},
|
||||
}
|
||||
ch <- llms.ContentResponse{
|
||||
Choices: []*llms.ContentChoice{
|
||||
{
|
||||
Content: " response",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// Verify MockLLM implements llms.Model
|
||||
var _ llms.Model = (*MockLLM)(nil)
|
||||
@@ -0,0 +1,77 @@
|
||||
package llmtest
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
)
|
||||
|
||||
// TestMockLLM tests the mock implementation.
|
||||
func TestMockLLM(t *testing.T) {
|
||||
mock := &MockLLM{
|
||||
CallResponse: "OK",
|
||||
GenerateResponse: &llms.ContentResponse{
|
||||
Choices: []*llms.ContentChoice{
|
||||
{
|
||||
Content: "Hello",
|
||||
GenerationInfo: map[string]interface{}{
|
||||
"TotalTokens": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
TestLLM(t, mock)
|
||||
}
|
||||
|
||||
// TestValidateLLM tests the validation function.
|
||||
func TestValidateLLM(t *testing.T) {
|
||||
// Test with nil model
|
||||
if err := ValidateLLM(nil); err == nil {
|
||||
t.Error("ValidateLLM should fail with nil model")
|
||||
}
|
||||
|
||||
// Test with valid mock
|
||||
mock := &MockLLM{
|
||||
CallResponse: "OK",
|
||||
GenerateResponse: &llms.ContentResponse{
|
||||
Choices: []*llms.ContentChoice{
|
||||
{
|
||||
Content: "response",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := ValidateLLM(mock); err != nil {
|
||||
t.Errorf("ValidateLLM failed with valid mock: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Integration tests with real providers (require API keys)
|
||||
|
||||
func TestAnthropicIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
if os.Getenv("ANTHROPIC_API_KEY") == "" {
|
||||
t.Skip("ANTHROPIC_API_KEY not set")
|
||||
}
|
||||
|
||||
// Import is handled in the actual test files for each provider
|
||||
}
|
||||
|
||||
func TestOpenAIIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test")
|
||||
}
|
||||
|
||||
if os.Getenv("OPENAI_API_KEY") == "" {
|
||||
t.Skip("OPENAI_API_KEY not set")
|
||||
}
|
||||
|
||||
// Import is handled in the actual test files for each provider
|
||||
}
|
||||
Reference in New Issue
Block a user