Skip to content

Commit 412c9b8

Browse files
Harden replay matching and Gemini models auth handling
1 parent 61ca107 commit 412c9b8

4 files changed

Lines changed: 14 additions & 15 deletions

File tree

internal/providers/gemini/gemini.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,8 @@ func (p *Provider) ListModels(ctx context.Context) (*core.ModelsResponse, error)
165165
modelsCfg.BaseURL = p.modelsURL
166166
modelsCfg.Hooks = p.hooks
167167
headers := func(req *http.Request) {
168-
// Add API key as query parameter.
169-
// NOTE: Passing the API key in the URL query parameter is required by Google's native Gemini API for the models endpoint.
170-
// This may be a security concern, as the API key can be logged in server access logs, proxy logs, and browser history.
171-
// See: https://cloud.google.com/vertex-ai/docs/generative-ai/model-parameters#api-key
172-
q := req.URL.Query()
173-
q.Set("key", p.apiKey)
174-
req.URL.RawQuery = q.Encode()
168+
// Use header-based API key auth for models requests.
169+
req.Header.Set("x-goog-api-key", p.apiKey)
175170

176171
// Preserve request tracing across list-models requests.
177172
requestID := req.Header.Get("X-Request-Id")

internal/providers/gemini/gemini_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,9 +333,9 @@ func TestListModels(t *testing.T) {
333333
t.Errorf("Path = %q, want %q", r.URL.Path, "/models")
334334
}
335335

336-
apiKey := r.URL.Query().Get("key")
336+
apiKey := r.Header.Get("x-goog-api-key")
337337
if apiKey == "" {
338-
t.Error("API key should be in query parameter 'key'")
338+
t.Error("API key should be in x-goog-api-key header")
339339
}
340340

341341
w.WriteHeader(tt.statusCode)

tests/contract/gemini_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ func TestGeminiReplayListModels(t *testing.T) {
8181
}
8282

8383
func TestGeminiReplayResponses(t *testing.T) {
84+
if !goldenFileExists(t, "golden/gemini/responses.golden.json") {
85+
t.Fatalf("missing golden file golden/gemini/responses.golden.json; run `make record-api` then `RECORD=1 go test -v -tags=contract -timeout=5m ./tests/contract/...`")
86+
}
87+
8488
provider := newGeminiReplayProvider(t, map[string]replayRoute{
8589
replayKey(http.MethodPost, "/chat/completions"): jsonFixtureRoute(t, "gemini/chat_completion.json"),
8690
})

tests/contract/replay_harness_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,6 @@ func (rt *replayTransport) RoundTrip(req *http.Request) (*http.Response, error)
3535

3636
key := replayKey(req.Method, req.URL.RequestURI())
3737
route, ok := rt.routes[key]
38-
if !ok && req.URL.RawQuery != "" {
39-
// Allow path-only route keys to match requests with query params.
40-
key = replayKey(req.Method, req.URL.Path)
41-
route, ok = rt.routes[key]
42-
}
4338
if !ok {
4439
notFoundBody := []byte(fmt.Sprintf(`{"error":{"message":"missing replay route: %s"}}`, key))
4540
return &http.Response{
@@ -140,7 +135,12 @@ func parseSSEEvents(t *testing.T, raw []byte) []sseEvent {
140135
case strings.HasPrefix(line, "event:"):
141136
currentName = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
142137
case strings.HasPrefix(line, "data:"):
143-
dataLines = append(dataLines, strings.TrimSpace(strings.TrimPrefix(line, "data:")))
138+
data := strings.TrimPrefix(line, "data:")
139+
if strings.HasPrefix(data, " ") {
140+
// Per SSE format, a single optional space may follow the colon.
141+
data = data[1:]
142+
}
143+
dataLines = append(dataLines, data)
144144
}
145145
}
146146
require.NoError(t, scanner.Err())

0 commit comments

Comments
 (0)