provider_registry_test.go

  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}