github.com/anthropics/anthropic-sdk-go@v1.70.1/paramunmarshal_test.go (about) 1 package anthropic_test 2 3 // Union variant selection for request params: the variant whose 4 // constants match wins; otherwise the closest structural fit. 5 6 import ( 7 "encoding/json" 8 "reflect" 9 "strings" 10 "testing" 11 12 "github.com/anthropics/anthropic-sdk-go" 13 ) 14 15 func TestParamUnmarshalNullAnyFieldDecodesToNil(t *testing.T) { 16 raw := `{"model": "m", "max_tokens": 1, 17 "messages": [{"role": "assistant", "content": [{"type": "tool_use", "id": "t1", "name": "f", "input": null}]}]}` 18 var p anthropic.MessageNewParams 19 if err := json.Unmarshal([]byte(raw), &p); err != nil { 20 t.Fatalf("unexpected error: %v", err) 21 } 22 tu := p.Messages[0].Content[0].OfToolUse 23 if tu == nil { 24 t.Fatalf("tool_use variant not selected, got %#v", p.Messages[0].Content[0]) 25 } 26 if tu.Input != nil { 27 t.Errorf("input must decode to nil as encoding/json does, got %#v", tu.Input) 28 } 29 } 30 31 // Unknown keys are dropped and never touch SetExtraFields. 32 func TestParamUnmarshalUnknownFieldsIgnored(t *testing.T) { 33 raw := `{"model": "m", "max_tokens": 1, "some_future_field": {"a": 1}, 34 "messages": [{"role": "user", "content": [{"type": "text", "text": "hi", "x_meta": 1}]}]}` 35 var p anthropic.MessageNewParams 36 if err := json.Unmarshal([]byte(raw), &p); err != nil { 37 t.Fatalf("unexpected error: %v", err) 38 } 39 if p.Messages[0].Content[0].OfText == nil || p.Messages[0].Content[0].OfText.Text != "hi" { 40 t.Errorf("text block not decoded alongside unknown field, got %#v", p.Messages[0].Content[0]) 41 } 42 if len(p.ExtraFields()) != 0 || len(p.Messages[0].Content[0].OfText.ExtraFields()) != 0 { 43 t.Errorf("unknown keys must not populate the SetExtraFields escape hatch") 44 } 45 out, err := json.Marshal(p) 46 if err != nil { 47 t.Fatalf("marshal error: %v", err) 48 } 49 for _, dropped := range []string{"x_meta", "some_future_field"} { 50 if strings.Contains(string(out), dropped) { 51 t.Errorf("unknown key %s must be dropped on re-marshal, got %s", dropped, out) 52 } 53 } 54 } 55 56 func TestParamUnmarshalToolUnionVariantIdentity(t *testing.T) { 57 cases := map[string]struct { 58 raw string 59 picked func(u anthropic.ToolUnionParam) bool 60 }{ 61 "web_search_20250305": { 62 `{"type": "web_search_20250305", "name": "web_search", "max_uses": 3}`, 63 func(u anthropic.ToolUnionParam) bool { 64 return u.OfWebSearchTool20250305 != nil && u.OfWebSearchTool20250305.MaxUses.Value == 3 65 }, 66 }, 67 "text_editor_20250429": { 68 `{"type": "text_editor_20250429", "name": "str_replace_based_edit_tool"}`, 69 func(u anthropic.ToolUnionParam) bool { return u.OfTextEditor20250429 != nil }, 70 }, 71 "code_execution_20260120": { 72 `{"type": "code_execution_20260120", "name": "code_execution"}`, 73 func(u anthropic.ToolUnionParam) bool { return u.OfCodeExecutionTool20260120 != nil }, 74 }, 75 "web_fetch_20250910": { 76 `{"type": "web_fetch_20250910", "name": "web_fetch"}`, 77 func(u anthropic.ToolUnionParam) bool { return u.OfWebFetchTool20250910 != nil }, 78 }, 79 } 80 for name, tc := range cases { 81 t.Run(name, func(t *testing.T) { 82 var u anthropic.ToolUnionParam 83 if err := json.Unmarshal([]byte(tc.raw), &u); err != nil { 84 t.Fatalf("unexpected error: %v", err) 85 } 86 if !tc.picked(u) { 87 t.Fatalf("wrong variant selected for %s: %#v", tc.raw, u) 88 } 89 }) 90 } 91 } 92 93 // An unknown newer version lands in the closest sibling, so its 94 // config survives the round trip. 95 func TestParamUnmarshalUnknownToolVersionPreservesConfig(t *testing.T) { 96 raw := `{"type":"web_search_20990101","name":"web_search","max_uses":5,"allowed_domains":["example.com"]}` 97 var u anthropic.ToolUnionParam 98 if err := json.Unmarshal([]byte(raw), &u); err != nil { 99 t.Fatalf("unexpected error: %v", err) 100 } 101 if u.OfTool != nil { 102 t.Fatalf("must not fall through to the custom-tool variant (drops config): %#v", u) 103 } 104 out, err := json.Marshal(u) 105 if err != nil { 106 t.Fatalf("marshal error: %v", err) 107 } 108 assertJSONEqual(t, raw, string(out)) 109 } 110 111 func TestParamUnmarshalCustomToolPicksOfTool(t *testing.T) { 112 for _, raw := range []string{ 113 `{"name":"my_tool","input_schema":{"type":"object"}}`, 114 `{"type":"custom","name":"my_tool","input_schema":{"type":"object"}}`, 115 } { 116 var u anthropic.ToolUnionParam 117 if err := json.Unmarshal([]byte(raw), &u); err != nil { 118 t.Fatalf("unexpected error: %v", err) 119 } 120 if u.OfTool == nil { 121 t.Fatalf("expected the custom-tool variant for %s, got %#v", raw, u) 122 } 123 } 124 } 125 126 func assertJSONEqual(t *testing.T, want, got string) { 127 t.Helper() 128 var a, b any 129 if err := json.Unmarshal([]byte(want), &a); err != nil { 130 t.Fatalf("bad want json: %v", err) 131 } 132 if err := json.Unmarshal([]byte(got), &b); err != nil { 133 t.Fatalf("bad got json: %v", err) 134 } 135 if !reflect.DeepEqual(a, b) { 136 t.Fatalf("round trip lost data\n want %s\n got %s", want, got) 137 } 138 } 139 140 // Bare-string content promotes to a one-element text-block slice. 141 func TestParamUnmarshalStringPromotion(t *testing.T) { 142 var p anthropic.MessageNewParams 143 if err := json.Unmarshal([]byte(`{"model":"m","max_tokens":1,"system":"be brief"}`), &p); err != nil { 144 t.Fatalf("unexpected error: %v", err) 145 } 146 if len(p.System) != 1 || p.System[0].Text != "be brief" { 147 t.Errorf("system string not promoted, got %#v", p.System) 148 } 149 var bm anthropic.BetaMessageParam 150 if err := json.Unmarshal([]byte(`{"role":"user","content":"hello"}`), &bm); err != nil { 151 t.Fatalf("unexpected error: %v", err) 152 } 153 if len(bm.Content) != 1 || bm.Content[0].OfText == nil || bm.Content[0].OfText.Text != "hello" { 154 t.Errorf("beta content string not promoted, got %#v", bm.Content) 155 } 156 } 157