diff --git a/llms/openai/internal/openaiclient/chat.go b/llms/openai/internal/openaiclient/chat.go index 991c236ab..07e410912 100644 --- a/llms/openai/internal/openaiclient/chat.go +++ b/llms/openai/internal/openaiclient/chat.go @@ -82,8 +82,13 @@ type ChatRequest struct { FunctionCallBehavior FunctionCallBehavior `json:"function_call,omitempty"` // Metadata allows you to specify additional information that will be passed to the model. + // Note: OpenAI requires store: true when using metadata. Metadata map[string]any `json:"metadata,omitempty"` + // Store controls whether the request is stored for later retrieval. + // This is required when using metadata per OpenAI's API. + Store bool `json:"store,omitempty"` + // WebSearchOptions configures web search behavior for search-enabled models // like gpt-4o-search-preview and gpt-4o-mini-search-preview. WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"` diff --git a/llms/openai/internal/openaiclient/marshal_test.go b/llms/openai/internal/openaiclient/marshal_test.go index cacb4efee..e04be2b26 100644 --- a/llms/openai/internal/openaiclient/marshal_test.go +++ b/llms/openai/internal/openaiclient/marshal_test.go @@ -321,3 +321,93 @@ func TestIsReasoningModel(t *testing.T) { }) } } + +func TestChatRequest_StoreFieldMarshalJSON(t *testing.T) { + tests := []struct { + name string + request ChatRequest + wantStore bool + wantMetadata bool + }{ + { + name: "no metadata - store should not be present", + request: ChatRequest{ + Model: "gpt-4", + }, + wantStore: false, + wantMetadata: false, + }, + { + name: "with metadata - store should be true", + request: ChatRequest{ + Model: "gpt-4", + Metadata: map[string]any{ + "feature": "support", + }, + Store: true, + }, + wantStore: true, + wantMetadata: true, + }, + { + name: "store explicitly set to false with metadata", + request: ChatRequest{ + Model: "gpt-4", + Metadata: map[string]any{ + "team": "ai", + }, + Store: false, + }, + wantStore: false, + wantMetadata: true, + }, + { + name: "store explicitly set to true without metadata", + request: ChatRequest{ + Model: "gpt-4", + Store: true, + }, + wantStore: true, + wantMetadata: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.request) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + + hasStore := result["store"] != nil + hasMetadata := result["metadata"] != nil + + if tt.wantStore && !hasStore { + t.Errorf("expected store to be present in JSON: %s", string(data)) + } + if !tt.wantStore && hasStore { + t.Errorf("expected store to NOT be present in JSON: %s", string(data)) + } + if tt.wantMetadata && !hasMetadata { + t.Errorf("expected metadata to be present in JSON: %s", string(data)) + } + if !tt.wantMetadata && hasMetadata { + t.Errorf("expected metadata to NOT be present in JSON: %s", string(data)) + } + + if hasStore { + storeVal, ok := result["store"].(bool) + if !ok { + t.Errorf("store is not a bool: %T", result["store"]) + } else if storeVal != tt.wantStore { + t.Errorf("store value: got %v, want %v", storeVal, tt.wantStore) + } + } + }) + } +} diff --git a/llms/openai/openaillm.go b/llms/openai/openaillm.go index 84690072a..5556e2956 100644 --- a/llms/openai/openaillm.go +++ b/llms/openai/openaillm.go @@ -293,6 +293,7 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten FunctionCallBehavior: openaiclient.FunctionCallBehavior(opts.FunctionCallBehavior), Seed: opts.Seed, Metadata: apiMetadata, + Store: len(apiMetadata) > 0, WebSearchOptions: webSearchOptionsFromCallOptions(opts.WebSearchOptions), } if opts.JSONMode { diff --git a/llms/openai/options_test.go b/llms/openai/options_test.go index 1908e75dc..14fde3a9b 100644 --- a/llms/openai/options_test.go +++ b/llms/openai/options_test.go @@ -178,3 +178,55 @@ func TestWebSearchOptionsConversion(t *testing.T) { t.Errorf("expected Country=GB, got %s", result2.UserLocation.Approximate.Country) } } + +func TestWithMetadataSetsStoreField(t *testing.T) { + // Test that using llms.WithMetadata results in the Store field being set + // This tests the integration in openaillm.go where apiMetadata is populated + // and Store is set based on whether metadata is present + + // Simulate the filtering logic from openaillm.go + opts := &llms.CallOptions{} + metadata := map[string]interface{}{ + "feature": "support", + "environment": "local", + "team": "ai", + } + llms.WithMetadata(metadata)(opts) + + // Verify metadata is set + if opts.Metadata == nil { + t.Fatal("expected Metadata to be set") + } + + // Simulate the filtering that happens in openaillm.go + apiMetadata := make(map[string]any) + for k, v := range opts.Metadata { + if k == "thinking_config" { // simulating the HasPrefix check + continue + } + apiMetadata[k] = v + } + + // Verify that when metadata is present, Store should be true + store := len(apiMetadata) > 0 + if !store { + t.Error("expected Store to be true when metadata is present") + } + + // Test with empty metadata + opts2 := &llms.CallOptions{} + llms.WithMetadata(map[string]interface{}{})(opts2) + + apiMetadata2 := make(map[string]any) + for k, v := range opts2.Metadata { + if k == "thinking_config" { + continue + } + apiMetadata2[k] = v + } + + store2 := len(apiMetadata2) > 0 + if store2 { + t.Error("expected Store to be false when metadata is empty") + } +}