feat: add extended thinking support for Anthropic models
Support configurable thinking levels (off/low/medium/high/xhigh/adaptive) via `agents.defaults.thinking_level` config field. - "adaptive": uses Anthropic's adaptive thinking API (Claude 4.6+) - "low/medium/high/xhigh": uses budget_tokens (all thinking-capable models) - "off": disables thinking (default) API constraints handled: - Temperature cleared when thinking is enabled - budget_tokens clamped to max_tokens-1 - Thinking response blocks parsed into Reasoning field Relates to #645, #966
This commit is contained in:
parent
c8178f4ad4
commit
1dc7ff753f
7 changed files with 313 additions and 16 deletions
|
|
@ -26,6 +26,7 @@ type AgentInstance struct {
|
||||||
MaxIterations int
|
MaxIterations int
|
||||||
MaxTokens int
|
MaxTokens int
|
||||||
Temperature float64
|
Temperature float64
|
||||||
|
ThinkingLevel ThinkingLevel
|
||||||
ContextWindow int
|
ContextWindow int
|
||||||
SummarizeMessageThreshold int
|
SummarizeMessageThreshold int
|
||||||
SummarizeTokenPercent int
|
SummarizeTokenPercent int
|
||||||
|
|
@ -103,6 +104,8 @@ func NewAgentInstance(
|
||||||
temperature = *defaults.Temperature
|
temperature = *defaults.Temperature
|
||||||
}
|
}
|
||||||
|
|
||||||
|
thinkingLevel := parseThinkingLevel(defaults.ThinkingLevel)
|
||||||
|
|
||||||
summarizeMessageThreshold := defaults.SummarizeMessageThreshold
|
summarizeMessageThreshold := defaults.SummarizeMessageThreshold
|
||||||
if summarizeMessageThreshold == 0 {
|
if summarizeMessageThreshold == 0 {
|
||||||
summarizeMessageThreshold = 20
|
summarizeMessageThreshold = 20
|
||||||
|
|
@ -169,6 +172,7 @@ func NewAgentInstance(
|
||||||
MaxIterations: maxIter,
|
MaxIterations: maxIter,
|
||||||
MaxTokens: maxTokens,
|
MaxTokens: maxTokens,
|
||||||
Temperature: temperature,
|
Temperature: temperature,
|
||||||
|
ThinkingLevel: thinkingLevel,
|
||||||
ContextWindow: maxTokens,
|
ContextWindow: maxTokens,
|
||||||
SummarizeMessageThreshold: summarizeMessageThreshold,
|
SummarizeMessageThreshold: summarizeMessageThreshold,
|
||||||
SummarizeTokenPercent: summarizeTokenPercent,
|
SummarizeTokenPercent: summarizeTokenPercent,
|
||||||
|
|
|
||||||
|
|
@ -771,23 +771,24 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
var response *providers.LLMResponse
|
var response *providers.LLMResponse
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
llmOpts := map[string]any{
|
||||||
|
"max_tokens": agent.MaxTokens,
|
||||||
|
"temperature": agent.Temperature,
|
||||||
|
"prompt_cache_key": agent.ID,
|
||||||
|
}
|
||||||
|
// parseThinkingLevel guarantees ThinkingOff for empty/unknown values,
|
||||||
|
// so checking != ThinkingOff is sufficient.
|
||||||
|
if agent.ThinkingLevel != ThinkingOff {
|
||||||
|
llmOpts["thinking_level"] = string(agent.ThinkingLevel)
|
||||||
|
}
|
||||||
|
|
||||||
callLLM := func() (*providers.LLMResponse, error) {
|
callLLM := func() (*providers.LLMResponse, error) {
|
||||||
if len(agent.Candidates) > 1 && al.fallback != nil {
|
if len(agent.Candidates) > 1 && al.fallback != nil {
|
||||||
fbResult, fbErr := al.fallback.Execute(
|
fbResult, fbErr := al.fallback.Execute(
|
||||||
ctx,
|
ctx,
|
||||||
agent.Candidates,
|
agent.Candidates,
|
||||||
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
|
func(ctx context.Context, provider, model string) (*providers.LLMResponse, error) {
|
||||||
return agent.Provider.Chat(
|
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, llmOpts)
|
||||||
ctx,
|
|
||||||
messages,
|
|
||||||
providerToolDefs,
|
|
||||||
model,
|
|
||||||
map[string]any{
|
|
||||||
"max_tokens": agent.MaxTokens,
|
|
||||||
"temperature": agent.Temperature,
|
|
||||||
"prompt_cache_key": agent.ID,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if fbErr != nil {
|
if fbErr != nil {
|
||||||
|
|
@ -803,11 +804,7 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
}
|
}
|
||||||
return fbResult.Response, nil
|
return fbResult.Response, nil
|
||||||
}
|
}
|
||||||
return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, map[string]any{
|
return agent.Provider.Chat(ctx, messages, providerToolDefs, agent.Model, llmOpts)
|
||||||
"max_tokens": agent.MaxTokens,
|
|
||||||
"temperature": agent.Temperature,
|
|
||||||
"prompt_cache_key": agent.ID,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry loop for context/token errors
|
// Retry loop for context/token errors
|
||||||
|
|
|
||||||
39
pkg/agent/thinking.go
Normal file
39
pkg/agent/thinking.go
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// ThinkingLevel controls how the provider sends thinking parameters.
|
||||||
|
//
|
||||||
|
// - "adaptive": sends {thinking: {type: "adaptive"}} + output_config.effort (Claude 4.6+)
|
||||||
|
// - "low"/"medium"/"high"/"xhigh": sends {thinking: {type: "enabled", budget_tokens: N}} (all models)
|
||||||
|
// - "off": disables thinking
|
||||||
|
type ThinkingLevel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ThinkingOff ThinkingLevel = "off"
|
||||||
|
ThinkingLow ThinkingLevel = "low"
|
||||||
|
ThinkingMedium ThinkingLevel = "medium"
|
||||||
|
ThinkingHigh ThinkingLevel = "high"
|
||||||
|
ThinkingXHigh ThinkingLevel = "xhigh"
|
||||||
|
ThinkingAdaptive ThinkingLevel = "adaptive"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseThinkingLevel normalizes a config string to a ThinkingLevel.
|
||||||
|
// Case-insensitive and whitespace-tolerant for user-facing config values.
|
||||||
|
// Returns ThinkingOff for unknown or empty values.
|
||||||
|
func parseThinkingLevel(level string) ThinkingLevel {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(level)) {
|
||||||
|
case "adaptive":
|
||||||
|
return ThinkingAdaptive
|
||||||
|
case "low":
|
||||||
|
return ThinkingLow
|
||||||
|
case "medium":
|
||||||
|
return ThinkingMedium
|
||||||
|
case "high":
|
||||||
|
return ThinkingHigh
|
||||||
|
case "xhigh":
|
||||||
|
return ThinkingXHigh
|
||||||
|
default:
|
||||||
|
return ThinkingOff
|
||||||
|
}
|
||||||
|
}
|
||||||
35
pkg/agent/thinking_test.go
Normal file
35
pkg/agent/thinking_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseThinkingLevel(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want ThinkingLevel
|
||||||
|
}{
|
||||||
|
{"off", "off", ThinkingOff},
|
||||||
|
{"empty", "", ThinkingOff},
|
||||||
|
{"low", "low", ThinkingLow},
|
||||||
|
{"medium", "medium", ThinkingMedium},
|
||||||
|
{"high", "high", ThinkingHigh},
|
||||||
|
{"xhigh", "xhigh", ThinkingXHigh},
|
||||||
|
{"adaptive", "adaptive", ThinkingAdaptive},
|
||||||
|
{"unknown", "unknown", ThinkingOff},
|
||||||
|
// Case-insensitive and whitespace-tolerant
|
||||||
|
{"upper_Medium", "Medium", ThinkingMedium},
|
||||||
|
{"upper_HIGH", "HIGH", ThinkingHigh},
|
||||||
|
{"mixed_Adaptive", "Adaptive", ThinkingAdaptive},
|
||||||
|
{"leading_space", " high", ThinkingHigh},
|
||||||
|
{"trailing_space", "low ", ThinkingLow},
|
||||||
|
{"both_spaces", " medium ", ThinkingMedium},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := parseThinkingLevel(tt.input); got != tt.want {
|
||||||
|
t.Errorf("parseThinkingLevel(%q) = %q, want %q", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -183,6 +183,7 @@ type AgentDefaults struct {
|
||||||
SummarizeMessageThreshold int `json:"summarize_message_threshold" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_MESSAGE_THRESHOLD"`
|
SummarizeMessageThreshold int `json:"summarize_message_threshold" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_MESSAGE_THRESHOLD"`
|
||||||
SummarizeTokenPercent int `json:"summarize_token_percent" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_TOKEN_PERCENT"`
|
SummarizeTokenPercent int `json:"summarize_token_percent" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_TOKEN_PERCENT"`
|
||||||
MaxMediaSize int `json:"max_media_size,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_MEDIA_SIZE"`
|
MaxMediaSize int `json:"max_media_size,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_MEDIA_SIZE"`
|
||||||
|
ThinkingLevel string `json:"thinking_level,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_THINKING_LEVEL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultMaxMediaSize = 20 * 1024 * 1024 // 20 MB
|
const DefaultMaxMediaSize = 20 * 1024 * 1024 // 20 MB
|
||||||
|
|
|
||||||
|
|
@ -182,9 +182,65 @@ func buildParams(
|
||||||
params.Tools = translateTools(tools)
|
params.Tools = translateTools(tools)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extended Thinking / Adaptive Thinking
|
||||||
|
// The thinking_level value directly determines the API parameter format:
|
||||||
|
// "adaptive" → {thinking: {type: "adaptive"}} + output_config.effort
|
||||||
|
// "low/medium/high/xhigh" → {thinking: {type: "enabled", budget_tokens: N}}
|
||||||
|
if level, ok := options["thinking_level"].(string); ok && level != "" && level != "off" {
|
||||||
|
applyThinkingConfig(¶ms, level)
|
||||||
|
}
|
||||||
|
|
||||||
return params, nil
|
return params, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applyThinkingConfig sets thinking parameters based on the level value.
|
||||||
|
// "adaptive" uses the adaptive thinking API (Claude 4.6+).
|
||||||
|
// All other levels use budget_tokens which is universally supported.
|
||||||
|
//
|
||||||
|
// Anthropic API constraint: temperature must not be set when thinking is enabled.
|
||||||
|
// budget_tokens must be strictly less than max_tokens.
|
||||||
|
func applyThinkingConfig(params *anthropic.MessageNewParams, level string) {
|
||||||
|
// Anthropic API rejects requests with temperature set alongside thinking.
|
||||||
|
// Reset to zero value (omitted from JSON serialization).
|
||||||
|
params.Temperature = anthropic.MessageNewParams{}.Temperature
|
||||||
|
|
||||||
|
if level == "adaptive" {
|
||||||
|
adaptive := anthropic.NewThinkingConfigAdaptiveParam()
|
||||||
|
params.Thinking = anthropic.ThinkingConfigParamUnion{OfAdaptive: &adaptive}
|
||||||
|
params.OutputConfig = anthropic.OutputConfigParam{
|
||||||
|
Effort: anthropic.OutputConfigEffortHigh,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
budget := int64(levelToBudget(level))
|
||||||
|
if budget <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// budget_tokens must be < max_tokens; clamp to respect user's max_tokens setting.
|
||||||
|
if budget >= params.MaxTokens {
|
||||||
|
budget = params.MaxTokens - 1
|
||||||
|
}
|
||||||
|
params.Thinking = anthropic.ThinkingConfigParamOfEnabled(budget)
|
||||||
|
}
|
||||||
|
|
||||||
|
// levelToBudget maps a thinking level string to budget_tokens for legacy models.
|
||||||
|
func levelToBudget(level string) int {
|
||||||
|
switch level {
|
||||||
|
case "low":
|
||||||
|
return 4096
|
||||||
|
case "medium":
|
||||||
|
return 10000
|
||||||
|
case "high":
|
||||||
|
return 32000
|
||||||
|
case "xhigh":
|
||||||
|
return 128000
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam {
|
func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam {
|
||||||
result := make([]anthropic.ToolUnionParam, 0, len(tools))
|
result := make([]anthropic.ToolUnionParam, 0, len(tools))
|
||||||
for _, t := range tools {
|
for _, t := range tools {
|
||||||
|
|
@ -213,10 +269,14 @@ func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam {
|
||||||
|
|
||||||
func parseResponse(resp *anthropic.Message) *LLMResponse {
|
func parseResponse(resp *anthropic.Message) *LLMResponse {
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
|
var reasoning strings.Builder
|
||||||
var toolCalls []ToolCall
|
var toolCalls []ToolCall
|
||||||
|
|
||||||
for _, block := range resp.Content {
|
for _, block := range resp.Content {
|
||||||
switch block.Type {
|
switch block.Type {
|
||||||
|
case "thinking":
|
||||||
|
tb := block.AsThinking()
|
||||||
|
reasoning.WriteString(tb.Thinking)
|
||||||
case "text":
|
case "text":
|
||||||
tb := block.AsText()
|
tb := block.AsText()
|
||||||
content.WriteString(tb.Text)
|
content.WriteString(tb.Text)
|
||||||
|
|
@ -247,6 +307,7 @@ func parseResponse(resp *anthropic.Message) *LLMResponse {
|
||||||
|
|
||||||
return &LLMResponse{
|
return &LLMResponse{
|
||||||
Content: content.String(),
|
Content: content.String(),
|
||||||
|
Reasoning: reasoning.String(),
|
||||||
ToolCalls: toolCalls,
|
ToolCalls: toolCalls,
|
||||||
FinishReason: finishReason,
|
FinishReason: finishReason,
|
||||||
Usage: &UsageInfo{
|
Usage: &UsageInfo{
|
||||||
|
|
|
||||||
160
pkg/providers/anthropic/thinking_test.go
Normal file
160
pkg/providers/anthropic/thinking_test.go
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
package anthropicprovider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anthropics/anthropic-sdk-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApplyThinkingConfig_Adaptive(t *testing.T) {
|
||||||
|
params := anthropic.MessageNewParams{
|
||||||
|
MaxTokens: 16000,
|
||||||
|
Temperature: anthropic.Float(0.7),
|
||||||
|
}
|
||||||
|
applyThinkingConfig(¶ms, "adaptive")
|
||||||
|
|
||||||
|
if params.Thinking.OfAdaptive == nil {
|
||||||
|
t.Fatal("expected adaptive thinking")
|
||||||
|
}
|
||||||
|
if params.Thinking.OfEnabled != nil {
|
||||||
|
t.Error("should not set enabled thinking in adaptive mode")
|
||||||
|
}
|
||||||
|
if params.OutputConfig.Effort != anthropic.OutputConfigEffortHigh {
|
||||||
|
t.Errorf("effort = %q, want %q", params.OutputConfig.Effort, anthropic.OutputConfigEffortHigh)
|
||||||
|
}
|
||||||
|
if params.Temperature.Valid() {
|
||||||
|
t.Error("temperature should be cleared when thinking is enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyThinkingConfig_BudgetLevels(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
level string
|
||||||
|
wantBudget int64
|
||||||
|
}{
|
||||||
|
{"low", 4096},
|
||||||
|
{"medium", 10000},
|
||||||
|
{"high", 32000},
|
||||||
|
{"xhigh", 128000},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.level, func(t *testing.T) {
|
||||||
|
params := anthropic.MessageNewParams{
|
||||||
|
MaxTokens: 200000,
|
||||||
|
Temperature: anthropic.Float(0.5),
|
||||||
|
}
|
||||||
|
applyThinkingConfig(¶ms, tt.level)
|
||||||
|
|
||||||
|
if params.Thinking.OfEnabled == nil {
|
||||||
|
t.Fatal("expected enabled thinking")
|
||||||
|
}
|
||||||
|
if params.Thinking.OfAdaptive != nil {
|
||||||
|
t.Error("should not set adaptive thinking")
|
||||||
|
}
|
||||||
|
if params.Thinking.OfEnabled.BudgetTokens != tt.wantBudget {
|
||||||
|
t.Errorf("budget_tokens = %d, want %d", params.Thinking.OfEnabled.BudgetTokens, tt.wantBudget)
|
||||||
|
}
|
||||||
|
if params.OutputConfig.Effort != "" {
|
||||||
|
t.Errorf("effort = %q, want empty", params.OutputConfig.Effort)
|
||||||
|
}
|
||||||
|
if params.Temperature.Valid() {
|
||||||
|
t.Error("temperature should be cleared when thinking is enabled")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyThinkingConfig_BudgetClamp(t *testing.T) {
|
||||||
|
// budget_tokens must be < max_tokens; clamp budget down to respect user's max_tokens.
|
||||||
|
params := anthropic.MessageNewParams{MaxTokens: 4096}
|
||||||
|
applyThinkingConfig(¶ms, "high") // budget=32000 > maxTokens=4096
|
||||||
|
|
||||||
|
if params.Thinking.OfEnabled == nil {
|
||||||
|
t.Fatal("expected enabled thinking")
|
||||||
|
}
|
||||||
|
if params.Thinking.OfEnabled.BudgetTokens != 4095 {
|
||||||
|
t.Errorf("budget_tokens = %d, want 4095 (maxTokens-1)", params.Thinking.OfEnabled.BudgetTokens)
|
||||||
|
}
|
||||||
|
if params.MaxTokens != 4096 {
|
||||||
|
t.Errorf("max_tokens should not be modified, got %d", params.MaxTokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyThinkingConfig_UnknownLevel(t *testing.T) {
|
||||||
|
params := anthropic.MessageNewParams{MaxTokens: 16000}
|
||||||
|
applyThinkingConfig(¶ms, "unknown")
|
||||||
|
|
||||||
|
if params.Thinking.OfEnabled != nil {
|
||||||
|
t.Error("should not set enabled thinking for unknown level")
|
||||||
|
}
|
||||||
|
if params.Thinking.OfAdaptive != nil {
|
||||||
|
t.Error("should not set adaptive thinking for unknown level")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLevelToBudget(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
level string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"low", "low", 4096},
|
||||||
|
{"medium", "medium", 10000},
|
||||||
|
{"high", "high", 32000},
|
||||||
|
{"xhigh", "xhigh", 128000},
|
||||||
|
{"off", "off", 0},
|
||||||
|
{"empty", "", 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := levelToBudget(tt.level); got != tt.want {
|
||||||
|
t.Errorf("levelToBudget(%q) = %d, want %d", tt.level, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildParams_ThinkingClearsTemperature(t *testing.T) {
|
||||||
|
msgs := []Message{{Role: "user", Content: "hello"}}
|
||||||
|
opts := map[string]any{
|
||||||
|
"max_tokens": 200000,
|
||||||
|
"temperature": 0.8,
|
||||||
|
"thinking_level": "medium",
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err := buildParams(msgs, nil, "claude-sonnet-4-6", opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Temperature.Valid() {
|
||||||
|
t.Error("temperature should be cleared when thinking_level is set")
|
||||||
|
}
|
||||||
|
if params.Thinking.OfEnabled == nil {
|
||||||
|
t.Fatal("expected enabled thinking")
|
||||||
|
}
|
||||||
|
if params.Thinking.OfEnabled.BudgetTokens != 10000 {
|
||||||
|
t.Errorf("budget_tokens = %d, want 10000", params.Thinking.OfEnabled.BudgetTokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildParams_NoThinkingKeepsTemperature(t *testing.T) {
|
||||||
|
msgs := []Message{{Role: "user", Content: "hello"}}
|
||||||
|
opts := map[string]any{
|
||||||
|
"temperature": 0.8,
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err := buildParams(msgs, nil, "claude-sonnet-4-6", opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !params.Temperature.Valid() {
|
||||||
|
t.Error("temperature should be preserved when thinking is not set")
|
||||||
|
}
|
||||||
|
if params.Temperature.Value != 0.8 {
|
||||||
|
t.Errorf("temperature = %f, want 0.8", params.Temperature.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue