Merge pull request #76 from ekoeppen/gcp-global-endpoint
Fix Google Vertex global endpoint
2 files · +60 -2
M packages/provider/google_vertex.go +10 -2
| @@ -311,7 +311,11 @@ func (t *vertexTransport) RoundTrip(req *http.Request) (*http.Response, error) { | |||
| 311 | 311 | newPath := fmt.Sprintf("/v1/projects/%s/locations/%s/publishers/google/models/%s%s", | |
| 312 | 312 | t.cfg.project, t.cfg.location, modelID, verb) | |
| 313 | 313 | clone := req.Clone(req.Context()) | |
| 314 | - | clone.URL.Host = t.cfg.location + "-aiplatform.googleapis.com" | |
| 314 | + | if t.cfg.location == "global" { | |
| 315 | + | clone.URL.Host = "aiplatform.googleapis.com" | |
| 316 | + | } else { | |
| 317 | + | clone.URL.Host = t.cfg.location + "-aiplatform.googleapis.com" | |
| 318 | + | } | |
| 315 | 319 | clone.URL.Path = newPath | |
| 316 | 320 | // Drop existing auth header and pick the right one. | |
| 317 | 321 | clone.Header.Del("x-goog-api-key") | |
| @@ -336,9 +340,13 @@ func NewVertex(_ string, _ string) Client { | |||
| 336 | 340 | if err != nil { | |
| 337 | 341 | return &unimplementedClient{name: "google-vertex", hint: err.Error()} | |
| 338 | 342 | } | |
| 343 | + | baseHost := "aiplatform.googleapis.com" | |
| 344 | + | if cfg.location != "global" { | |
| 345 | + | baseHost = cfg.location + "-aiplatform.googleapis.com" | |
| 346 | + | } | |
| 339 | 347 | inner := &geminiClient{ | |
| 340 | 348 | apiKey: "vertex-placeholder", // overwritten by transport | |
| 341 | - | baseURL: "https://" + cfg.location + "-aiplatform.googleapis.com", | |
| 349 | + | baseURL: "https://" + baseHost, | |
| 342 | 350 | http: &http.Client{ | |
| 343 | 351 | Transport: &vertexTransport{inner: http.DefaultTransport, cfg: cfg}, | |
| 344 | 352 | Timeout: 0, | |
| @@ -311,7 +311,11 @@ func (t *vertexTransport) RoundTrip(req *http.Request) (*http.Response, error) { | |||||
| 311 | newPath := fmt.Sprintf("/v1/projects/%s/locations/%s/publishers/google/models/%s%s", | 311 | newPath := fmt.Sprintf("/v1/projects/%s/locations/%s/publishers/google/models/%s%s", | ||
| 312 | t.cfg.project, t.cfg.location, modelID, verb) | 312 | t.cfg.project, t.cfg.location, modelID, verb) | ||
| 313 | clone := req.Clone(req.Context()) | 313 | clone := req.Clone(req.Context()) | ||
| 314 | - | clone.URL.Host = t.cfg.location + "-aiplatform.googleapis.com" | 314 | + | if t.cfg.location == "global" { |
| 315 | + | clone.URL.Host = "aiplatform.googleapis.com" | |||
| 316 | + | } else { | |||
| 317 | + | clone.URL.Host = t.cfg.location + "-aiplatform.googleapis.com" | |||
| 318 | + | } | |||
| 315 | clone.URL.Path = newPath | 319 | clone.URL.Path = newPath | ||
| 316 | // Drop existing auth header and pick the right one. | 320 | // Drop existing auth header and pick the right one. | ||
| 317 | clone.Header.Del("x-goog-api-key") | 321 | clone.Header.Del("x-goog-api-key") | ||
| @@ -336,9 +340,13 @@ func NewVertex(_ string, _ string) Client { | |||||
| 336 | if err != nil { | 340 | if err != nil { | ||
| 337 | return &unimplementedClient{name: "google-vertex", hint: err.Error()} | 341 | return &unimplementedClient{name: "google-vertex", hint: err.Error()} | ||
| 338 | } | 342 | } | ||
| 343 | + | baseHost := "aiplatform.googleapis.com" | |||
| 344 | + | if cfg.location != "global" { | |||
| 345 | + | baseHost = cfg.location + "-aiplatform.googleapis.com" | |||
| 346 | + | } | |||
| 339 | inner := &geminiClient{ | 347 | inner := &geminiClient{ | ||
| 340 | apiKey: "vertex-placeholder", // overwritten by transport | 348 | apiKey: "vertex-placeholder", // overwritten by transport | ||
| 341 | - | baseURL: "https://" + cfg.location + "-aiplatform.googleapis.com", | 349 | + | baseURL: "https://" + baseHost, |
| 342 | http: &http.Client{ | 350 | http: &http.Client{ | ||
| 343 | Transport: &vertexTransport{inner: http.DefaultTransport, cfg: cfg}, | 351 | Transport: &vertexTransport{inner: http.DefaultTransport, cfg: cfg}, | ||
| 344 | Timeout: 0, | 352 | Timeout: 0, | ||
M packages/provider/google_vertex_test.go +50 -0
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package provider | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "net/http" | |
| 4 | 5 | "os" | |
| 5 | 6 | "path/filepath" | |
| 6 | 7 | "testing" | |
| @@ -40,6 +41,55 @@ func TestVertexConfigParsesAuthorizedUser(t *testing.T) { | |||
| 40 | 41 | } | |
| 41 | 42 | } | |
| 42 | 43 | ||
| 44 | + | type vertexRoundTripFunc func(*http.Request) (*http.Response, error) | |
| 45 | + | ||
| 46 | + | func (f vertexRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { | |
| 47 | + | return f(req) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func TestVertexTransportEndpoint(t *testing.T) { | |
| 51 | + | tests := []struct { | |
| 52 | + | name string | |
| 53 | + | location string | |
| 54 | + | wantHost string | |
| 55 | + | }{ | |
| 56 | + | {name: "global", location: "global", wantHost: "aiplatform.googleapis.com"}, | |
| 57 | + | {name: "regional", location: "us-central1", wantHost: "us-central1-aiplatform.googleapis.com"}, | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | for _, tt := range tests { | |
| 61 | + | t.Run(tt.name, func(t *testing.T) { | |
| 62 | + | var got *http.Request | |
| 63 | + | transport := &vertexTransport{ | |
| 64 | + | cfg: &vertexConfig{ | |
| 65 | + | project: "test-project", | |
| 66 | + | location: tt.location, | |
| 67 | + | apiKey: "test-key", | |
| 68 | + | }, | |
| 69 | + | inner: vertexRoundTripFunc(func(req *http.Request) (*http.Response, error) { | |
| 70 | + | got = req | |
| 71 | + | return &http.Response{StatusCode: http.StatusOK}, nil | |
| 72 | + | }), | |
| 73 | + | } | |
| 74 | + | req, err := http.NewRequest(http.MethodPost, "https://placeholder.example/v1beta/models/gemini-test:streamGenerateContent?alt=sse", nil) | |
| 75 | + | if err != nil { | |
| 76 | + | t.Fatal(err) | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | if _, err := transport.RoundTrip(req); err != nil { | |
| 80 | + | t.Fatalf("RoundTrip: %v", err) | |
| 81 | + | } | |
| 82 | + | if got.URL.Host != tt.wantHost { | |
| 83 | + | t.Errorf("host = %q, want %q", got.URL.Host, tt.wantHost) | |
| 84 | + | } | |
| 85 | + | wantPath := "/v1/projects/test-project/locations/" + tt.location + "/publishers/google/models/gemini-test:streamGenerateContent" | |
| 86 | + | if got.URL.Path != wantPath { | |
| 87 | + | t.Errorf("path = %q, want %q", got.URL.Path, wantPath) | |
| 88 | + | } | |
| 89 | + | }) | |
| 90 | + | } | |
| 91 | + | } | |
| 92 | + | ||
| 43 | 93 | func TestVertexConfigRejectsBadType(t *testing.T) { | |
| 44 | 94 | tmp := t.TempDir() | |
| 45 | 95 | path := filepath.Join(tmp, "adc.json") | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package provider | 1 | package provider | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "net/http" | |||
| 4 | "os" | 5 | "os" | ||
| 5 | "path/filepath" | 6 | "path/filepath" | ||
| 6 | "testing" | 7 | "testing" | ||
| @@ -40,6 +41,55 @@ func TestVertexConfigParsesAuthorizedUser(t *testing.T) { | |||||
| 40 | } | 41 | } | ||
| 41 | } | 42 | } | ||
| 42 | 43 | ||||
| 44 | + | type vertexRoundTripFunc func(*http.Request) (*http.Response, error) | |||
| 45 | + | ||||
| 46 | + | func (f vertexRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { | |||
| 47 | + | return f(req) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func TestVertexTransportEndpoint(t *testing.T) { | |||
| 51 | + | tests := []struct { | |||
| 52 | + | name string | |||
| 53 | + | location string | |||
| 54 | + | wantHost string | |||
| 55 | + | }{ | |||
| 56 | + | {name: "global", location: "global", wantHost: "aiplatform.googleapis.com"}, | |||
| 57 | + | {name: "regional", location: "us-central1", wantHost: "us-central1-aiplatform.googleapis.com"}, | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | for _, tt := range tests { | |||
| 61 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 62 | + | var got *http.Request | |||
| 63 | + | transport := &vertexTransport{ | |||
| 64 | + | cfg: &vertexConfig{ | |||
| 65 | + | project: "test-project", | |||
| 66 | + | location: tt.location, | |||
| 67 | + | apiKey: "test-key", | |||
| 68 | + | }, | |||
| 69 | + | inner: vertexRoundTripFunc(func(req *http.Request) (*http.Response, error) { | |||
| 70 | + | got = req | |||
| 71 | + | return &http.Response{StatusCode: http.StatusOK}, nil | |||
| 72 | + | }), | |||
| 73 | + | } | |||
| 74 | + | req, err := http.NewRequest(http.MethodPost, "https://placeholder.example/v1beta/models/gemini-test:streamGenerateContent?alt=sse", nil) | |||
| 75 | + | if err != nil { | |||
| 76 | + | t.Fatal(err) | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | if _, err := transport.RoundTrip(req); err != nil { | |||
| 80 | + | t.Fatalf("RoundTrip: %v", err) | |||
| 81 | + | } | |||
| 82 | + | if got.URL.Host != tt.wantHost { | |||
| 83 | + | t.Errorf("host = %q, want %q", got.URL.Host, tt.wantHost) | |||
| 84 | + | } | |||
| 85 | + | wantPath := "/v1/projects/test-project/locations/" + tt.location + "/publishers/google/models/gemini-test:streamGenerateContent" | |||
| 86 | + | if got.URL.Path != wantPath { | |||
| 87 | + | t.Errorf("path = %q, want %q", got.URL.Path, wantPath) | |||
| 88 | + | } | |||
| 89 | + | }) | |||
| 90 | + | } | |||
| 91 | + | } | |||
| 92 | + | ||||
| 43 | func TestVertexConfigRejectsBadType(t *testing.T) { | 93 | func TestVertexConfigRejectsBadType(t *testing.T) { | ||
| 44 | tmp := t.TempDir() | 94 | tmp := t.TempDir() | ||
| 45 | path := filepath.Join(tmp, "adc.json") | 95 | path := filepath.Join(tmp, "adc.json") | ||