1package providertests
2
3import (
4 "encoding/json"
5 "testing"
6
7 "charm.land/fantasy"
8 "charm.land/fantasy/providers/openai"
9 "github.com/stretchr/testify/require"
10)
11
12func TestProviderRegistry_Serialization_OpenAIOptions(t *testing.T) {
13 msg := fantasy.Message{
14 Role: fantasy.MessageRoleUser,
15 Content: []fantasy.MessagePart{
16 fantasy.TextPart{Text: "hi"},
17 },
18 ProviderOptions: fantasy.ProviderOptions{
19 openai.Name: &openai.ProviderOptions{User: fantasy.Opt("tester")},
20 },
21 }
22
23 data, err := json.Marshal(msg)
24 require.NoError(t, err)
25
26 var raw struct {
27 ProviderOptions map[string]map[string]any `json:"provider_options"`
28 }
29 require.NoError(t, json.Unmarshal(data, &raw))
30
31 po, ok := raw.ProviderOptions[openai.Name]
32 require.True(t, ok)
33 require.Equal(t, openai.TypeProviderOptions, po["type"]) // no magic strings
34 // ensure inner data has the field we set
35 inner, ok := po["data"].(map[string]any)
36 require.True(t, ok)
37 require.Equal(t, "tester", inner["user"])
38
39 var decoded fantasy.Message
40 require.NoError(t, json.Unmarshal(data, &decoded))
41
42 got, ok := decoded.ProviderOptions[openai.Name]
43 require.True(t, ok)
44 opt, ok := got.(*openai.ProviderOptions)
45 require.True(t, ok)
46 require.NotNil(t, opt.User)
47 require.Equal(t, "tester", *opt.User)
48}
49
50func TestProviderRegistry_Serialization_OpenAIResponses(t *testing.T) {
51 // Use ResponsesProviderOptions in provider options
52 msg := fantasy.Message{
53 Role: fantasy.MessageRoleUser,
54 Content: []fantasy.MessagePart{
55 fantasy.TextPart{Text: "hello"},
56 },
57 ProviderOptions: fantasy.ProviderOptions{
58 openai.Name: &openai.ResponsesProviderOptions{
59 PromptCacheKey: fantasy.Opt("cache-key-1"),
60 ParallelToolCalls: fantasy.Opt(true),
61 },
62 },
63 }
64
65 data, err := json.Marshal(msg)
66 require.NoError(t, err)
67
68 // JSON should include the typed wrapper with constant TypeResponsesProviderOptions
69 var raw struct {
70 ProviderOptions map[string]map[string]any `json:"provider_options"`
71 }
72 require.NoError(t, json.Unmarshal(data, &raw))
73
74 po := raw.ProviderOptions[openai.Name]
75 require.Equal(t, openai.TypeResponsesProviderOptions, po["type"]) // no magic strings
76 inner, ok := po["data"].(map[string]any)
77 require.True(t, ok)
78 require.Equal(t, "cache-key-1", inner["prompt_cache_key"])
79 require.Equal(t, true, inner["parallel_tool_calls"])
80
81 // Unmarshal back and assert concrete type
82 var decoded fantasy.Message
83 require.NoError(t, json.Unmarshal(data, &decoded))
84 got := decoded.ProviderOptions[openai.Name]
85 reqOpts, ok := got.(*openai.ResponsesProviderOptions)
86 require.True(t, ok)
87 require.NotNil(t, reqOpts.PromptCacheKey)
88 require.Equal(t, "cache-key-1", *reqOpts.PromptCacheKey)
89 require.NotNil(t, reqOpts.ParallelToolCalls)
90 require.Equal(t, true, *reqOpts.ParallelToolCalls)
91}
92
93func TestProviderRegistry_Serialization_OpenAIResponsesReasoningMetadata(t *testing.T) {
94 resp := fantasy.Response{
95 Content: []fantasy.Content{
96 fantasy.TextContent{
97 Text: "",
98 ProviderMetadata: fantasy.ProviderMetadata{
99 openai.Name: &openai.ResponsesReasoningMetadata{
100 ItemID: "item-123",
101 Summary: []string{"part1", "part2"},
102 },
103 },
104 },
105 },
106 }
107
108 data, err := json.Marshal(resp)
109 require.NoError(t, err)
110
111 // Ensure the provider metadata is wrapped with type using constant
112 var raw struct {
113 Content []struct {
114 Type string `json:"type"`
115 Data map[string]any `json:"data"`
116 } `json:"content"`
117 }
118 require.NoError(t, json.Unmarshal(data, &raw))
119 require.Greater(t, len(raw.Content), 0)
120 tc := raw.Content[0]
121 pm, ok := tc.Data["provider_metadata"].(map[string]any)
122 require.True(t, ok)
123 om, ok := pm[openai.Name].(map[string]any)
124 require.True(t, ok)
125 require.Equal(t, openai.TypeResponsesReasoningMetadata, om["type"]) // no magic strings
126 inner, ok := om["data"].(map[string]any)
127 require.True(t, ok)
128 require.Equal(t, "item-123", inner["item_id"])
129
130 // Unmarshal back
131 var decoded fantasy.Response
132 require.NoError(t, json.Unmarshal(data, &decoded))
133 pmDecoded := decoded.Content[0].(fantasy.TextContent).ProviderMetadata
134 val, ok := pmDecoded[openai.Name]
135 require.True(t, ok)
136 meta, ok := val.(*openai.ResponsesReasoningMetadata)
137 require.True(t, ok)
138 require.Equal(t, "item-123", meta.ItemID)
139 require.Equal(t, []string{"part1", "part2"}, meta.Summary)
140}