159 lines
5.8 KiB
Go
159 lines
5.8 KiB
Go
package bifrost
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"gpt-load/internal/channel"
|
|
"gpt-load/internal/execution"
|
|
"gpt-load/internal/protocol"
|
|
"gpt-load/internal/provideradapter"
|
|
)
|
|
|
|
func TestCLIProxyAPICountTokensUsesNativeGatewayWire(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
clientProtocol protocol.Protocol
|
|
path string
|
|
body string
|
|
wantPath string
|
|
credentialHeader string
|
|
response string
|
|
}{
|
|
{
|
|
name: "anthropic", clientProtocol: protocol.Anthropic,
|
|
path: "/v1/messages/count_tokens",
|
|
body: `{"model":"client-model","messages":[{"role":"user","content":"hello"}],` +
|
|
`"provider":"client","api_key":"client-body"}`,
|
|
wantPath: "/team-a/v1/messages/count_tokens", credentialHeader: "X-Api-Key",
|
|
response: `{"input_tokens":7}`,
|
|
},
|
|
{
|
|
name: "gemini", clientProtocol: protocol.Gemini,
|
|
path: "/v1beta/models/client-model:countTokens",
|
|
body: `{"contents":[{"role":"user","parts":[{"text":"hello"}]}],` +
|
|
`"provider":"client","api_key":"client-body"}`,
|
|
wantPath: "/team-a/v1beta/models/upstream-model:countTokens", credentialHeader: "X-Goog-Api-Key",
|
|
response: `{"totalTokens":7}`,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
if request.Method != http.MethodPost || request.URL.Path != test.wantPath ||
|
|
request.URL.Query().Get("trace") != "kept" || request.URL.Query().Get("key") != "" {
|
|
t.Errorf("upstream target = %s %s?%s", request.Method, request.URL.Path, request.URL.RawQuery)
|
|
}
|
|
if request.Header.Get(test.credentialHeader) != testAPIKey {
|
|
t.Errorf("credential %s = %q", test.credentialHeader, request.Header.Get(test.credentialHeader))
|
|
}
|
|
for _, name := range []string{"Authorization", "X-Api-Key", "X-Goog-Api-Key"} {
|
|
if name != test.credentialHeader && request.Header.Get(name) != "" {
|
|
t.Errorf("unexpected credential header %s = %q", name, request.Header.Get(name))
|
|
}
|
|
}
|
|
body, _ := io.ReadAll(request.Body)
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
t.Fatalf("decode upstream request: %v", err)
|
|
}
|
|
if payload["provider"] != nil || payload["api_key"] != nil {
|
|
t.Errorf("control fields reached upstream: %#v", payload)
|
|
}
|
|
if test.clientProtocol == protocol.Anthropic && payload["model"] != "upstream-model" {
|
|
t.Errorf("upstream model = %#v", payload["model"])
|
|
}
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(writer, test.response)
|
|
}))
|
|
defer server.Close()
|
|
|
|
manager, resolved := newCLIProxyAPIManagerForTest(t, server.URL+"/team-a")
|
|
spec := freezeTestAttempt(execution.AttemptSpec{
|
|
RequestID: "cpa-count-tokens", AttemptID: "cpa-count-tokens-attempt", Sequence: 1,
|
|
ChannelID: string(channel.CLIProxyAPI), ClientProtocol: test.clientProtocol,
|
|
Operation: execution.OperationCountTokens,
|
|
ClientModel: "client-model", UpstreamModel: "upstream-model",
|
|
Method: http.MethodPost, Path: test.path,
|
|
RawQuery: "trace=kept&key=client-query",
|
|
Header: http.Header{
|
|
"Authorization": {"Bearer client-auth"},
|
|
"X-Api-Key": {"client-auth"},
|
|
"X-Goog-Api-Key": {"client-auth"},
|
|
"Content-Type": {"application/json"},
|
|
},
|
|
Body: []byte(test.body), TargetConfig: resolved.TargetConfig,
|
|
Credential: execution.NewCredentialSnapshot(
|
|
17, 1, 1, []byte(`{"api_key":"`+testAPIKey+`"}`),
|
|
),
|
|
})
|
|
result := manager.Execute(context.Background(), spec)
|
|
if err := result.Validate(); err != nil || result.Error != nil || result.StatusCode != http.StatusOK ||
|
|
result.UpstreamProtocol != test.clientProtocol || !bytes.Equal(result.Body, []byte(test.response)) {
|
|
t.Fatalf("result = %+v, validation=%v, body=%s", result, err, result.Body)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCLIProxyAPIModelDiscoveryUsesOpenAIEndpoint(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
if request.Method != http.MethodGet || request.URL.Path != "/team-a/v1/models" {
|
|
t.Errorf("upstream target = %s %s", request.Method, request.URL.Path)
|
|
}
|
|
if request.Header.Get("Authorization") != "Bearer "+testAPIKey {
|
|
t.Errorf("Authorization = %q", request.Header.Get("Authorization"))
|
|
}
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(writer, `{"object":"list","data":[{"id":"cpa-model"}]}`)
|
|
}))
|
|
defer server.Close()
|
|
|
|
manager, resolved := newCLIProxyAPIManagerForTest(t, server.URL+"/team-a")
|
|
spec := utilitySpec(channel.CLIProxyAPI, protocol.OpenAICompletions, execution.OperationListModels, http.MethodGet, "/v1/models", nil)
|
|
spec.TargetConfig = resolved.TargetConfig
|
|
spec = freezeTestAttempt(spec)
|
|
result := manager.Execute(context.Background(), spec)
|
|
if err := result.Validate(); err != nil || result.Error != nil || result.StatusCode != http.StatusOK ||
|
|
!bytes.Contains(result.Body, []byte(`"id":"cpa-model"`)) {
|
|
t.Fatalf("result = %+v, validation=%v, body=%s", result, err, result.Body)
|
|
}
|
|
}
|
|
|
|
func newCLIProxyAPIManagerForTest(t *testing.T, baseURL string) (*RuntimeManager, channel.ResolvedTarget) {
|
|
t.Helper()
|
|
|
|
registry := channel.NewRegistry()
|
|
resolved, err := registry.Resolve(
|
|
channel.CLIProxyAPI,
|
|
json.RawMessage(`{"base_url":"`+baseURL+`"}`),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manager, err := newRuntimeManager(runtimeOptions{allowPrivateNetwork: true}, registry)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := manager.Start(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(manager.Shutdown)
|
|
if err := manager.Reconcile([]provideradapter.RuntimeTarget{{Target: resolved}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return manager, resolved
|
|
}
|