From dd63401e2c0df6297059514ba9af559a6ab4bc70 Mon Sep 17 00:00:00 2001 From: yxia216 Date: Thu, 30 Oct 2025 00:50:42 -0400 Subject: [PATCH 1/9] init Signed-off-by: yxia216 --- internal/apischema/openai/openai.go | 7 +++++++ internal/translator/openai_gcpvertexai.go | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/internal/apischema/openai/openai.go b/internal/apischema/openai/openai.go index c2afcb7f06..b452fc06be 100644 --- a/internal/apischema/openai/openai.go +++ b/internal/apischema/openai/openai.go @@ -1314,6 +1314,9 @@ type ChatCompletionResponseChoiceMessage struct { // List of ratings for the safety of a response candidate. There is at most one rating per category. // https://cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1/GenerateContentResponse#SafetyRating SafetyRatings []*genai.SafetyRating `json:"safety_ratings,omitempty"` + + // GroundingMetadata specifies sources used to ground generated content. + GroundingMetadata *genai.GroundingMetadata `json:"grounding_metadata,omitempty"` } // URLCitation contains citation information for web search results. @@ -1639,6 +1642,10 @@ type GCPVertexAIVendorFields struct { // // https://cloud.google.com/vertex-ai/docs/reference/rest/v1/SafetySetting SafetySettings []*genai.SafetySetting `json:"safetySettings,omitzero"` + + //EnterpriseWebSearch controls whether to use Web Grounding for Enterprise + // https://docs.cloud.google.com/vertex-ai/generative-ai/docs/grounding/web-grounding-enterprise + EnterpriseWebSearch bool `json:"enterprise_search,omitzero"` } // GCPVertexAIGenerationConfig represents Gemini generation configuration options. diff --git a/internal/translator/openai_gcpvertexai.go b/internal/translator/openai_gcpvertexai.go index c42def23bb..0757fe5367 100644 --- a/internal/translator/openai_gcpvertexai.go +++ b/internal/translator/openai_gcpvertexai.go @@ -439,6 +439,11 @@ func (o *openAIToGCPVertexAITranslatorV1ChatCompletion) openAIMessageToGeminiMes if err != nil { return nil, fmt.Errorf("error converting tool choice: %w", err) } + if openAIReq.GCPVertexAIVendorFields.EnterpriseWebSearch { + tools = append(tools, genai.Tool{ + EnterpriseWebSearch: &genai.EnterpriseWebSearch{}, + }) + } // Convert generation config. generationConfig, responseMode, err := openAIReqToGeminiGenerationConfig(openAIReq, requestModel) From 441a464a66507f98b87bdf98f7a4c02fbf55a2f4 Mon Sep 17 00:00:00 2001 From: yxia216 Date: Mon, 10 Nov 2025 13:51:48 -0500 Subject: [PATCH 2/9] fix-test Signed-off-by: yxia216 --- internal/translator/openai_gcpvertexai.go | 2 +- .../translator/openai_gcpvertexai_test.go | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/internal/translator/openai_gcpvertexai.go b/internal/translator/openai_gcpvertexai.go index 0757fe5367..a90952f6a1 100644 --- a/internal/translator/openai_gcpvertexai.go +++ b/internal/translator/openai_gcpvertexai.go @@ -439,7 +439,7 @@ func (o *openAIToGCPVertexAITranslatorV1ChatCompletion) openAIMessageToGeminiMes if err != nil { return nil, fmt.Errorf("error converting tool choice: %w", err) } - if openAIReq.GCPVertexAIVendorFields.EnterpriseWebSearch { + if openAIReq.EnterpriseWebSearch { tools = append(tools, genai.Tool{ EnterpriseWebSearch: &genai.EnterpriseWebSearch{}, }) diff --git a/internal/translator/openai_gcpvertexai_test.go b/internal/translator/openai_gcpvertexai_test.go index 7a16024c6d..c0e84d1276 100644 --- a/internal/translator/openai_gcpvertexai_test.go +++ b/internal/translator/openai_gcpvertexai_test.go @@ -335,6 +335,28 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) } }`) + wantBdyWithEnterpriseWebSearch := []byte(`{ + "contents": [ + { + "parts": [ + { + "text": "Test with web grounding for enterprise" + } + ], + "role": "user" + } + ], + "tools": [ + { + "enterpriseWebSearch": {}, + } + ], + "generation_config": { + "maxOutputTokens": 1024, + "temperature": 0.7 + }, +}`) + tests := []struct { name string modelNameOverride internalapi.ModelNameOverride @@ -739,6 +761,48 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) }, wantBody: wantBdyWithGuidedRegex, }, + { + name: "Request with gcp web grounding for enterprise", + input: openai.ChatCompletionRequest{ + Model: "gemini-1.5-pro", + Temperature: ptr.To(0.7), + MaxTokens: ptr.To(int64(1024)), + Messages: []openai.ChatCompletionMessageParamUnion{ + { + OfUser: &openai.ChatCompletionUserMessageParam{ + Role: openai.ChatMessageRoleUser, + Content: openai.StringOrUserRoleContentUnion{Value: "Test with web grounding for enterprise"}, + }, + }, + }, + GCPVertexAIVendorFields: &openai.GCPVertexAIVendorFields{ + EnterpriseWebSearch: true, + }, + }, + onRetry: false, + wantError: false, + wantHeaderMut: &extprocv3.HeaderMutation{ + SetHeaders: []*corev3.HeaderValueOption{ + { + Header: &corev3.HeaderValue{ + Key: ":path", + RawValue: []byte("publishers/google/models/gemini-1.5-pro:generateContent"), + }, + }, + { + Header: &corev3.HeaderValue{ + Key: "Content-Length", + RawValue: []byte("395"), + }, + }, + }, + }, + wantBody: &extprocv3.BodyMutation{ + Mutation: &extprocv3.BodyMutation_Body{ + Body: wantBdyWithEnterpriseWebSearch, + }, + }, + }, } for _, tc := range tests { From a556bce8cc1740a301407b07bd86fe1bc3a92537 Mon Sep 17 00:00:00 2001 From: yxia216 Date: Mon, 10 Nov 2025 14:08:48 -0500 Subject: [PATCH 3/9] fix-tests Signed-off-by: yxia216 --- internal/translator/openai_gcpvertexai.go | 11 ++++++----- internal/translator/openai_gcpvertexai_test.go | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/internal/translator/openai_gcpvertexai.go b/internal/translator/openai_gcpvertexai.go index a90952f6a1..0f05a00c63 100644 --- a/internal/translator/openai_gcpvertexai.go +++ b/internal/translator/openai_gcpvertexai.go @@ -439,11 +439,6 @@ func (o *openAIToGCPVertexAITranslatorV1ChatCompletion) openAIMessageToGeminiMes if err != nil { return nil, fmt.Errorf("error converting tool choice: %w", err) } - if openAIReq.EnterpriseWebSearch { - tools = append(tools, genai.Tool{ - EnterpriseWebSearch: &genai.EnterpriseWebSearch{}, - }) - } // Convert generation config. generationConfig, responseMode, err := openAIReqToGeminiGenerationConfig(openAIReq, requestModel) @@ -492,6 +487,12 @@ func (o *openAIToGCPVertexAITranslatorV1ChatCompletion) applyVendorSpecificField if gcpVendorFields.SafetySettings != nil { gcr.SafetySettings = gcpVendorFields.SafetySettings } + + if gcpVendorFields.EnterpriseWebSearch { + gcr.Tools = append(gcr.Tools, genai.Tool{ + EnterpriseWebSearch: &genai.EnterpriseWebSearch{}, + }) + } } func (o *openAIToGCPVertexAITranslatorV1ChatCompletion) geminiResponseToOpenAIMessage(gcr genai.GenerateContentResponse, responseModel string) (*openai.ChatCompletionResponse, error) { diff --git a/internal/translator/openai_gcpvertexai_test.go b/internal/translator/openai_gcpvertexai_test.go index c0e84d1276..e996db19ef 100644 --- a/internal/translator/openai_gcpvertexai_test.go +++ b/internal/translator/openai_gcpvertexai_test.go @@ -348,13 +348,13 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) ], "tools": [ { - "enterpriseWebSearch": {}, + "enterpriseWebSearch": {} } ], "generation_config": { "maxOutputTokens": 1024, "temperature": 0.7 - }, + } }`) tests := []struct { @@ -792,7 +792,7 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) { Header: &corev3.HeaderValue{ Key: "Content-Length", - RawValue: []byte("395"), + RawValue: []byte("190"), }, }, }, From 8c5fa53208e19a8f58c9690b55540a084c8ce061 Mon Sep 17 00:00:00 2001 From: yxia216 Date: Thu, 20 Nov 2025 11:13:58 -0500 Subject: [PATCH 4/9] output Signed-off-by: yxia216 --- internal/translator/gemini_helper.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/translator/gemini_helper.go b/internal/translator/gemini_helper.go index 94482d29ae..c4a5f07b91 100644 --- a/internal/translator/gemini_helper.go +++ b/internal/translator/gemini_helper.go @@ -667,6 +667,14 @@ func geminiCandidatesToOpenAIChoices(candidates []*genai.Candidate, responseMode choice.Message.SafetyRatings = candidate.SafetyRatings } + if candidate.GroundingMetadata != nil { + if choice.Message.Role == "" { + choice.Message.Role = openai.ChatMessageRoleAssistant + } + + choice.Message.GroundingMetadata = candidate.GroundingMetadata + } + // Handle logprobs if available. if candidate.LogprobsResult != nil { choice.Logprobs = geminiLogprobsToOpenAILogprobs(*candidate.LogprobsResult) From 7db29b7c9e5cb7a4c92e9deddad3947d0cfa6309 Mon Sep 17 00:00:00 2001 From: yxia216 Date: Thu, 20 Nov 2025 11:48:24 -0500 Subject: [PATCH 5/9] fix-rebase Signed-off-by: yxia216 --- internal/apischema/openai/openai.go | 2 +- .../apischema/openai/vendor_fields_test.go | 153 ++++++++++++++++++ 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/internal/apischema/openai/openai.go b/internal/apischema/openai/openai.go index b452fc06be..7f1e789245 100644 --- a/internal/apischema/openai/openai.go +++ b/internal/apischema/openai/openai.go @@ -1643,7 +1643,7 @@ type GCPVertexAIVendorFields struct { // https://cloud.google.com/vertex-ai/docs/reference/rest/v1/SafetySetting SafetySettings []*genai.SafetySetting `json:"safetySettings,omitzero"` - //EnterpriseWebSearch controls whether to use Web Grounding for Enterprise + // EnterpriseWebSearch controls whether to use Web Grounding for Enterprise // https://docs.cloud.google.com/vertex-ai/generative-ai/docs/grounding/web-grounding-enterprise EnterpriseWebSearch bool `json:"enterprise_search,omitzero"` } diff --git a/internal/apischema/openai/vendor_fields_test.go b/internal/apischema/openai/vendor_fields_test.go index 2b5815abe6..33ea08a34e 100644 --- a/internal/apischema/openai/vendor_fields_test.go +++ b/internal/apischema/openai/vendor_fields_test.go @@ -16,6 +16,7 @@ import ( "github.com/openai/openai-go/v2/packages/param" "github.com/stretchr/testify/require" "google.golang.org/genai" + "k8s.io/utils/ptr" ) func TestChatCompletionRequest_VendorFieldsExtraction(t *testing.T) { @@ -60,6 +61,158 @@ func TestChatCompletionRequest_VendorFieldsExtraction(t *testing.T) { }, }, }, + { + name: "Request with GCP Vertex AI EnterpriseWebSearch enabled", + jsonData: []byte(`{ + "model": "gemini-1.5-pro", + "messages": [ + { + "role": "user", + "content": "Hello with enterprise search!" + } + ], + "enterprise_search": true + }`), + expected: &ChatCompletionRequest{ + Model: "gemini-1.5-pro", + Messages: []ChatCompletionMessageParamUnion{ + { + OfUser: &ChatCompletionUserMessageParam{ + Role: ChatMessageRoleUser, + Content: StringOrUserRoleContentUnion{Value: "Hello with enterprise search!"}, + }, + }, + }, + GCPVertexAIVendorFields: &GCPVertexAIVendorFields{ + EnterpriseWebSearch: true, + }, + }, + }, + { + name: "Request with GCP Vertex AI EnterpriseWebSearch disabled", + jsonData: []byte(`{ + "model": "gemini-1.5-pro", + "messages": [ + { + "role": "user", + "content": "Hello with enterprise search disabled!" + } + ], + "enterprise_search": false + }`), + expected: &ChatCompletionRequest{ + Model: "gemini-1.5-pro", + Messages: []ChatCompletionMessageParamUnion{ + { + OfUser: &ChatCompletionUserMessageParam{ + Role: ChatMessageRoleUser, + Content: StringOrUserRoleContentUnion{Value: "Hello with enterprise search disabled!"}, + }, + }, + }, + GCPVertexAIVendorFields: &GCPVertexAIVendorFields{ + EnterpriseWebSearch: false, + }, + }, + }, + { + name: "Request with combined GCP Vertex AI fields including EnterpriseWebSearch", + jsonData: []byte(`{ + "model": "gemini-1.5-pro", + "messages": [ + { + "role": "user", + "content": "Hello with all GCP fields!" + } + ], + "generationConfig": { + "thinkingConfig": { + "includeThoughts": true, + "thinkingBudget": 1000 + } + }, + "safetySettings": [{ + "category": "HARM_CATEGORY_HARASSMENT", + "threshold": "BLOCK_ONLY_HIGH" + }], + "enterprise_search": true + }`), + expected: &ChatCompletionRequest{ + Model: "gemini-1.5-pro", + Messages: []ChatCompletionMessageParamUnion{ + { + OfUser: &ChatCompletionUserMessageParam{ + Role: ChatMessageRoleUser, + Content: StringOrUserRoleContentUnion{Value: "Hello with all GCP fields!"}, + }, + }, + }, + GCPVertexAIVendorFields: &GCPVertexAIVendorFields{ + GenerationConfig: &GCPVertexAIGenerationConfig{ + ThinkingConfig: &genai.ThinkingConfig{ + IncludeThoughts: true, + ThinkingBudget: ptr.To(int32(1000)), + }, + }, + SafetySettings: []*genai.SafetySetting{ + { + Category: genai.HarmCategoryHarassment, + Threshold: genai.HarmBlockThresholdBlockOnlyHigh, + }, + }, + EnterpriseWebSearch: true, + }, + }, + }, + { + name: "Request with multiple vendor fields", + jsonData: []byte(`{ + "model": "claude-3", + "messages": [ + { + "role": "user", + "content": "Multiple vendors test" + } + ], + "generationConfig": { + "thinkingConfig": { + "includeThoughts": true, + "thinkingBudget": 1000 + } + }, + "thinking": { + "type": "enabled", + "budget_tokens": 1000 + } + }`), + expected: &ChatCompletionRequest{ + Model: "claude-3", + Messages: []ChatCompletionMessageParamUnion{ + { + OfUser: &ChatCompletionUserMessageParam{ + Role: ChatMessageRoleUser, + Content: StringOrUserRoleContentUnion{Value: "Multiple vendors test"}, + }, + }, + }, + AnthropicVendorFields: &AnthropicVendorFields{ + Thinking: &anthropic.ThinkingConfigParamUnion{ + OfEnabled: &anthropic.ThinkingConfigEnabledParam{ + BudgetTokens: 1000, + Type: "enabled", + }, + }, + }, + GCPVertexAIVendorFields: &GCPVertexAIVendorFields{ + GenerationConfig: &GCPVertexAIGenerationConfig{ + ThinkingConfig: &genai.ThinkingConfig{ + IncludeThoughts: true, + ThinkingBudget: ptr.To(int32(1000)), + }, + }, + }, + }, + }, { name: "Request without vendor fields", jsonData: []byte(`{ From 1be159e2259c5a2d4b60f0b9b0ccb915a9cf1f5b Mon Sep 17 00:00:00 2001 From: yxia216 Date: Thu, 20 Nov 2025 12:30:07 -0500 Subject: [PATCH 6/9] fix Signed-off-by: yxia216 --- .../translator/openai_gcpvertexai_test.go | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/internal/translator/openai_gcpvertexai_test.go b/internal/translator/openai_gcpvertexai_test.go index e996db19ef..ba55c5e1db 100644 --- a/internal/translator/openai_gcpvertexai_test.go +++ b/internal/translator/openai_gcpvertexai_test.go @@ -781,27 +781,11 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) }, onRetry: false, wantError: false, - wantHeaderMut: &extprocv3.HeaderMutation{ - SetHeaders: []*corev3.HeaderValueOption{ - { - Header: &corev3.HeaderValue{ - Key: ":path", - RawValue: []byte("publishers/google/models/gemini-1.5-pro:generateContent"), - }, - }, - { - Header: &corev3.HeaderValue{ - Key: "Content-Length", - RawValue: []byte("190"), - }, - }, - }, - }, - wantBody: &extprocv3.BodyMutation{ - Mutation: &extprocv3.BodyMutation_Body{ - Body: wantBdyWithEnterpriseWebSearch, - }, + wantHeaderMut: []internalapi.Header{ + {":path", "publishers/google/models/gemini-1.5-pro:generateContent"}, + {"content-length", "190"}, }, + wantBody: wantBdyWithEnterpriseWebSearch, }, } From a451f7dae2292427e15b19c4b41f3486f88b5a1b Mon Sep 17 00:00:00 2001 From: yxia216 Date: Sat, 29 Nov 2025 18:56:56 -0500 Subject: [PATCH 7/9] rebase Signed-off-by: yxia216 --- .../apischema/openai/vendor_fields_test.go | 62 ------------------- 1 file changed, 62 deletions(-) diff --git a/internal/apischema/openai/vendor_fields_test.go b/internal/apischema/openai/vendor_fields_test.go index 33ea08a34e..bcdf6c1b75 100644 --- a/internal/apischema/openai/vendor_fields_test.go +++ b/internal/apischema/openai/vendor_fields_test.go @@ -16,7 +16,6 @@ import ( "github.com/openai/openai-go/v2/packages/param" "github.com/stretchr/testify/require" "google.golang.org/genai" - "k8s.io/utils/ptr" ) func TestChatCompletionRequest_VendorFieldsExtraction(t *testing.T) { @@ -125,12 +124,6 @@ func TestChatCompletionRequest_VendorFieldsExtraction(t *testing.T) { "content": "Hello with all GCP fields!" } ], - "generationConfig": { - "thinkingConfig": { - "includeThoughts": true, - "thinkingBudget": 1000 - } - }, "safetySettings": [{ "category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_ONLY_HIGH" @@ -148,12 +141,6 @@ func TestChatCompletionRequest_VendorFieldsExtraction(t *testing.T) { }, }, GCPVertexAIVendorFields: &GCPVertexAIVendorFields{ - GenerationConfig: &GCPVertexAIGenerationConfig{ - ThinkingConfig: &genai.ThinkingConfig{ - IncludeThoughts: true, - ThinkingBudget: ptr.To(int32(1000)), - }, - }, SafetySettings: []*genai.SafetySetting{ { Category: genai.HarmCategoryHarassment, @@ -164,55 +151,6 @@ func TestChatCompletionRequest_VendorFieldsExtraction(t *testing.T) { }, }, }, - { - name: "Request with multiple vendor fields", - jsonData: []byte(`{ - "model": "claude-3", - "messages": [ - { - "role": "user", - "content": "Multiple vendors test" - } - ], - "generationConfig": { - "thinkingConfig": { - "includeThoughts": true, - "thinkingBudget": 1000 - } - }, - "thinking": { - "type": "enabled", - "budget_tokens": 1000 - } - }`), - expected: &ChatCompletionRequest{ - Model: "claude-3", - Messages: []ChatCompletionMessageParamUnion{ - { - OfUser: &ChatCompletionUserMessageParam{ - Role: ChatMessageRoleUser, - Content: StringOrUserRoleContentUnion{Value: "Multiple vendors test"}, - }, - }, - }, - AnthropicVendorFields: &AnthropicVendorFields{ - Thinking: &anthropic.ThinkingConfigParamUnion{ - OfEnabled: &anthropic.ThinkingConfigEnabledParam{ - BudgetTokens: 1000, - Type: "enabled", - }, - }, - }, - GCPVertexAIVendorFields: &GCPVertexAIVendorFields{ - GenerationConfig: &GCPVertexAIGenerationConfig{ - ThinkingConfig: &genai.ThinkingConfig{ - IncludeThoughts: true, - ThinkingBudget: ptr.To(int32(1000)), - }, - }, - }, - }, - }, { name: "Request without vendor fields", jsonData: []byte(`{ From 5adce1f12fdcffdd65d839f3c4534e242fa2c08e Mon Sep 17 00:00:00 2001 From: yxia216 Date: Sun, 30 Nov 2025 22:17:15 -0500 Subject: [PATCH 8/9] comments Signed-off-by: yxia216 --- internal/apischema/openai/openai.go | 9 +- internal/apischema/openai/openai_test.go | 127 ++++++++++++++++++ .../apischema/openai/vendor_fields_test.go | 91 ------------- internal/translator/gemini_helper.go | 20 ++- internal/translator/gemini_helper_test.go | 52 +++++++ internal/translator/openai_gcpvertexai.go | 6 - .../translator/openai_gcpvertexai_test.go | 6 +- 7 files changed, 204 insertions(+), 107 deletions(-) diff --git a/internal/apischema/openai/openai.go b/internal/apischema/openai/openai.go index 71749fcf7d..a42a271df0 100644 --- a/internal/apischema/openai/openai.go +++ b/internal/apischema/openai/openai.go @@ -1071,8 +1071,9 @@ type StreamOptions struct { type ToolType string const ( - ToolTypeFunction ToolType = "function" - ToolTypeImageGeneration ToolType = "image_generation" + ToolTypeFunction ToolType = "function" + ToolTypeImageGeneration ToolType = "image_generation" + ToolTypeEnterpriseWebSearch ToolType = "enterprise_search" ) type Tool struct { @@ -1642,10 +1643,6 @@ type GCPVertexAIVendorFields struct { // // https://cloud.google.com/vertex-ai/docs/reference/rest/v1/SafetySetting SafetySettings []*genai.SafetySetting `json:"safetySettings,omitzero"` - - // EnterpriseWebSearch controls whether to use Web Grounding for Enterprise - // https://docs.cloud.google.com/vertex-ai/generative-ai/docs/grounding/web-grounding-enterprise - EnterpriseWebSearch bool `json:"enterprise_search,omitzero"` } // GCPVertexAIGenerationConfig represents Gemini generation configuration options. diff --git a/internal/apischema/openai/openai_test.go b/internal/apischema/openai/openai_test.go index 9dfd0261c7..284cf0c1ab 100644 --- a/internal/apischema/openai/openai_test.go +++ b/internal/apischema/openai/openai_test.go @@ -1193,6 +1193,133 @@ func TestChatCompletionRequest(t *testing.T) { }, }, }, + { + name: "enterprise search tool", + jsonStr: `{ + "model": "gemini-1.5-pro", + "messages": [ + { + "role": "user", + "content": "Hello with enterprise search!" + } + ], + "tools": [ + { + "type": "enterprise_search" + } + ] + }`, + expected: &ChatCompletionRequest{ + Model: "gemini-1.5-pro", + Messages: []ChatCompletionMessageParamUnion{ + { + OfUser: &ChatCompletionUserMessageParam{ + Role: ChatMessageRoleUser, + Content: StringOrUserRoleContentUnion{Value: "Hello with enterprise search!"}, + }, + }, + }, + Tools: []Tool{ + { + Type: ToolTypeEnterpriseWebSearch, + }, + }, + }, + }, + { + name: "mixed function and enterprise search tools", + jsonStr: `{ + "model": "gemini-1.5-pro", + "messages": [ + { + "role": "user", + "content": "Mixed tools test" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get current weather" + } + }, + { + "type": "enterprise_search" + } + ] + }`, + expected: &ChatCompletionRequest{ + Model: "gemini-1.5-pro", + Messages: []ChatCompletionMessageParamUnion{ + { + OfUser: &ChatCompletionUserMessageParam{ + Role: ChatMessageRoleUser, + Content: StringOrUserRoleContentUnion{Value: "Mixed tools test"}, + }, + }, + }, + Tools: []Tool{ + { + Type: ToolTypeFunction, + Function: &FunctionDefinition{ + Name: "get_weather", + Description: "Get current weather", + }, + }, + { + Type: ToolTypeEnterpriseWebSearch, + }, + }, + }, + }, + { + name: "enterprise search with vendor fields", + jsonStr: `{ + "model": "gemini-1.5-pro", + "messages": [ + { + "role": "user", + "content": "Combined enterprise search and safety settings" + } + ], + "tools": [ + { + "type": "enterprise_search" + } + ], + "safetySettings": [ + { + "category": "HARM_CATEGORY_HARASSMENT", + "threshold": "BLOCK_ONLY_HIGH" + } + ] + }`, + expected: &ChatCompletionRequest{ + Model: "gemini-1.5-pro", + Messages: []ChatCompletionMessageParamUnion{ + { + OfUser: &ChatCompletionUserMessageParam{ + Role: ChatMessageRoleUser, + Content: StringOrUserRoleContentUnion{Value: "Combined enterprise search and safety settings"}, + }, + }, + }, + Tools: []Tool{ + { + Type: ToolTypeEnterpriseWebSearch, + }, + }, + GCPVertexAIVendorFields: &GCPVertexAIVendorFields{ + SafetySettings: []*genai.SafetySetting{ + { + Category: genai.HarmCategoryHarassment, + Threshold: genai.HarmBlockThresholdBlockOnlyHigh, + }, + }, + }, + }, + }, } for _, tc := range testCases { diff --git a/internal/apischema/openai/vendor_fields_test.go b/internal/apischema/openai/vendor_fields_test.go index bcdf6c1b75..2b5815abe6 100644 --- a/internal/apischema/openai/vendor_fields_test.go +++ b/internal/apischema/openai/vendor_fields_test.go @@ -60,97 +60,6 @@ func TestChatCompletionRequest_VendorFieldsExtraction(t *testing.T) { }, }, }, - { - name: "Request with GCP Vertex AI EnterpriseWebSearch enabled", - jsonData: []byte(`{ - "model": "gemini-1.5-pro", - "messages": [ - { - "role": "user", - "content": "Hello with enterprise search!" - } - ], - "enterprise_search": true - }`), - expected: &ChatCompletionRequest{ - Model: "gemini-1.5-pro", - Messages: []ChatCompletionMessageParamUnion{ - { - OfUser: &ChatCompletionUserMessageParam{ - Role: ChatMessageRoleUser, - Content: StringOrUserRoleContentUnion{Value: "Hello with enterprise search!"}, - }, - }, - }, - GCPVertexAIVendorFields: &GCPVertexAIVendorFields{ - EnterpriseWebSearch: true, - }, - }, - }, - { - name: "Request with GCP Vertex AI EnterpriseWebSearch disabled", - jsonData: []byte(`{ - "model": "gemini-1.5-pro", - "messages": [ - { - "role": "user", - "content": "Hello with enterprise search disabled!" - } - ], - "enterprise_search": false - }`), - expected: &ChatCompletionRequest{ - Model: "gemini-1.5-pro", - Messages: []ChatCompletionMessageParamUnion{ - { - OfUser: &ChatCompletionUserMessageParam{ - Role: ChatMessageRoleUser, - Content: StringOrUserRoleContentUnion{Value: "Hello with enterprise search disabled!"}, - }, - }, - }, - GCPVertexAIVendorFields: &GCPVertexAIVendorFields{ - EnterpriseWebSearch: false, - }, - }, - }, - { - name: "Request with combined GCP Vertex AI fields including EnterpriseWebSearch", - jsonData: []byte(`{ - "model": "gemini-1.5-pro", - "messages": [ - { - "role": "user", - "content": "Hello with all GCP fields!" - } - ], - "safetySettings": [{ - "category": "HARM_CATEGORY_HARASSMENT", - "threshold": "BLOCK_ONLY_HIGH" - }], - "enterprise_search": true - }`), - expected: &ChatCompletionRequest{ - Model: "gemini-1.5-pro", - Messages: []ChatCompletionMessageParamUnion{ - { - OfUser: &ChatCompletionUserMessageParam{ - Role: ChatMessageRoleUser, - Content: StringOrUserRoleContentUnion{Value: "Hello with all GCP fields!"}, - }, - }, - }, - GCPVertexAIVendorFields: &GCPVertexAIVendorFields{ - SafetySettings: []*genai.SafetySetting{ - { - Category: genai.HarmCategoryHarassment, - Threshold: genai.HarmBlockThresholdBlockOnlyHigh, - }, - }, - EnterpriseWebSearch: true, - }, - }, - }, { name: "Request without vendor fields", jsonData: []byte(`{ diff --git a/internal/translator/gemini_helper.go b/internal/translator/gemini_helper.go index 4f4863547c..840673624f 100644 --- a/internal/translator/gemini_helper.go +++ b/internal/translator/gemini_helper.go @@ -358,6 +358,9 @@ func openAIToolsToGeminiTools(openaiTools []openai.Tool, parametersJSONSchemaAva if len(openaiTools) == 0 { return nil, nil } + + var genaiTools []genai.Tool + var functionDecls []*genai.FunctionDeclaration for _, tool := range openaiTools { @@ -389,14 +392,27 @@ func openAIToolsToGeminiTools(openaiTools []openai.Tool, parametersJSONSchemaAva } case openai.ToolTypeImageGeneration: return nil, fmt.Errorf("tool-type image generation not supported yet when translating OpenAI req to Gemini") + case openai.ToolTypeEnterpriseWebSearch: + genaiTools = append(genaiTools, genai.Tool{ + EnterpriseWebSearch: &genai.EnterpriseWebSearch{}, + }) default: return nil, fmt.Errorf("unsupported tool type: %s", tool.Type) } } - if len(functionDecls) == 0 { + // Only return nil if there are no tools at all (neither function declarations nor other tools) + if len(functionDecls) == 0 && len(genaiTools) == 0 { return nil, nil } - return []genai.Tool{{FunctionDeclarations: functionDecls}}, nil + + // Only append function declarations if there are any + if len(functionDecls) > 0 { + genaiTools = append(genaiTools, genai.Tool{ + FunctionDeclarations: functionDecls, + }) + } + + return genaiTools, nil } // openAIToolChoiceToGeminiToolConfig converts OpenAI tool_choice to Gemini ToolConfig. diff --git a/internal/translator/gemini_helper_test.go b/internal/translator/gemini_helper_test.go index 58e410effd..5107859603 100644 --- a/internal/translator/gemini_helper_test.go +++ b/internal/translator/gemini_helper_test.go @@ -1420,6 +1420,58 @@ func TestOpenAIToolsToGeminiTools(t *testing.T) { parametersJSONSchemaAvailable: false, expected: nil, }, + { + name: "enterprise search tool only", + openaiTools: []openai.Tool{ + { + Type: openai.ToolTypeEnterpriseWebSearch, + }, + }, + parametersJSONSchemaAvailable: false, + expected: []genai.Tool{ + { + EnterpriseWebSearch: &genai.EnterpriseWebSearch{}, + }, + }, + }, + { + name: "mixed function and enterprise search tools", + openaiTools: []openai.Tool{ + { + Type: openai.ToolTypeFunction, + Function: &openai.FunctionDefinition{ + Name: "get_weather", + Description: "Get current weather", + Parameters: funcParams, + }, + }, + { + Type: openai.ToolTypeEnterpriseWebSearch, + }, + }, + parametersJSONSchemaAvailable: false, + expected: []genai.Tool{ + { + EnterpriseWebSearch: &genai.EnterpriseWebSearch{}, + }, + { + FunctionDeclarations: []*genai.FunctionDeclaration{ + { + Name: "get_weather", + Description: "Get current weather", + Parameters: &genai.Schema{ + Type: "object", + Properties: map[string]*genai.Schema{ + "a": {Type: "integer"}, + "b": {Type: "integer"}, + }, + Required: []string{"a", "b"}, + }, + }, + }, + }, + }, + }, } for _, tc := range tests { diff --git a/internal/translator/openai_gcpvertexai.go b/internal/translator/openai_gcpvertexai.go index 8b63bbf51c..3e766bb077 100644 --- a/internal/translator/openai_gcpvertexai.go +++ b/internal/translator/openai_gcpvertexai.go @@ -493,12 +493,6 @@ func (o *openAIToGCPVertexAITranslatorV1ChatCompletion) applyVendorSpecificField if gcpVendorFields.SafetySettings != nil { gcr.SafetySettings = gcpVendorFields.SafetySettings } - - if gcpVendorFields.EnterpriseWebSearch { - gcr.Tools = append(gcr.Tools, genai.Tool{ - EnterpriseWebSearch: &genai.EnterpriseWebSearch{}, - }) - } } func (o *openAIToGCPVertexAITranslatorV1ChatCompletion) geminiResponseToOpenAIMessage(gcr genai.GenerateContentResponse, responseModel string) (*openai.ChatCompletionResponse, error) { diff --git a/internal/translator/openai_gcpvertexai_test.go b/internal/translator/openai_gcpvertexai_test.go index a523a18f96..488cc39e25 100644 --- a/internal/translator/openai_gcpvertexai_test.go +++ b/internal/translator/openai_gcpvertexai_test.go @@ -775,8 +775,10 @@ func TestOpenAIToGCPVertexAITranslatorV1ChatCompletion_RequestBody(t *testing.T) }, }, }, - GCPVertexAIVendorFields: &openai.GCPVertexAIVendorFields{ - EnterpriseWebSearch: true, + Tools: []openai.Tool{ + { + Type: "enterprise_search", + }, }, }, onRetry: false, From 28fb3d11fd680c4e0abfc670f23530deb475305b Mon Sep 17 00:00:00 2001 From: yxia216 Date: Thu, 4 Dec 2025 21:41:23 -0500 Subject: [PATCH 9/9] fix-comments Signed-off-by: yxia216 --- internal/apischema/openai/openai.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/apischema/openai/openai.go b/internal/apischema/openai/openai.go index a42a271df0..2142a8f73c 100644 --- a/internal/apischema/openai/openai.go +++ b/internal/apischema/openai/openai.go @@ -1317,6 +1317,7 @@ type ChatCompletionResponseChoiceMessage struct { SafetyRatings []*genai.SafetyRating `json:"safety_ratings,omitempty"` // GroundingMetadata specifies sources used to ground generated content. + // https://docs.cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1beta1/GroundingMetadata GroundingMetadata *genai.GroundingMetadata `json:"grounding_metadata,omitempty"` }