Use resolved targets for client routing
This commit is contained in:
@@ -123,26 +123,19 @@ func (p *recordingProvider) lastRequest(t *testing.T) recordedProviderRequest {
|
||||
return p.requests[len(p.requests)-1]
|
||||
}
|
||||
|
||||
func newProviderClient(t *testing.T, provider *recordingProvider, config OpenAICompatibleConfig) *OpenAICompatibleClient {
|
||||
t.Helper()
|
||||
|
||||
if config.BaseURL == "" {
|
||||
config.BaseURL = provider.endpoint("/v1")
|
||||
}
|
||||
client, err := NewOpenAICompatibleClient(config)
|
||||
if err != nil {
|
||||
t.Fatalf("construct client: %v", err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func ordinaryGenerateRequest() domain.GenerateRequest {
|
||||
return domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Model: "model"},
|
||||
Target: domain.ExecutionTarget{Endpoint: "https://provider.example/v1", Model: "model"},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *recordingProvider) generateRequest() domain.GenerateRequest {
|
||||
req := ordinaryGenerateRequest()
|
||||
req.Target.Endpoint = p.endpoint("/v1")
|
||||
return req
|
||||
}
|
||||
|
||||
type deadlineCapturingTransport struct {
|
||||
deadline time.Time
|
||||
hasDeadline bool
|
||||
@@ -253,35 +246,13 @@ func assertDeadlineNear(t *testing.T, deadline, before, after time.Time, duratio
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOpenAICompatibleClientRejectsInvalidBaseURL(t *testing.T) {
|
||||
for _, endpoint := range []string{
|
||||
"://invalid",
|
||||
"/v1",
|
||||
"https:///v1",
|
||||
"ftp://provider.example/v1",
|
||||
"https://user@provider.example/v1",
|
||||
"https://provider.example/v1?mode=chat",
|
||||
"https://provider.example/v1#chat",
|
||||
} {
|
||||
t.Run(endpoint, func(t *testing.T) {
|
||||
_, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{BaseURL: endpoint})
|
||||
if !errors.Is(err, ErrInvalidConfig) {
|
||||
t.Fatalf("expected ErrInvalidConfig, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOpenAICompatibleClientDoesNotMutateSuppliedZeroTimeoutClient(t *testing.T) {
|
||||
transport := http.DefaultTransport
|
||||
supplied := &http.Client{Transport: transport}
|
||||
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: supplied,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected constructor error: %v", err)
|
||||
}
|
||||
|
||||
if supplied.Timeout != 0 {
|
||||
t.Fatalf("expected supplied client timeout to remain zero, got %v", supplied.Timeout)
|
||||
@@ -305,13 +276,10 @@ func TestNewOpenAICompatibleClientDoesNotMutateSuppliedNonzeroTimeoutClient(t *t
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
Timeout: 2 * time.Second,
|
||||
HTTPClient: supplied,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected constructor error: %v", err)
|
||||
}
|
||||
|
||||
if supplied.Timeout != suppliedTimeout {
|
||||
t.Fatalf("expected supplied client timeout to remain %v, got %v", suppliedTimeout, supplied.Timeout)
|
||||
@@ -335,13 +303,10 @@ func TestNewOpenAICompatibleClientTreatsSuppliedNegativeTimeoutAsUnset(t *testin
|
||||
}
|
||||
configuredTimeout := 23 * time.Second
|
||||
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
Timeout: configuredTimeout,
|
||||
HTTPClient: supplied,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected constructor error: %v", err)
|
||||
}
|
||||
|
||||
if supplied.Timeout != -time.Second {
|
||||
t.Fatalf("expected supplied client timeout to remain negative, got %v", supplied.Timeout)
|
||||
@@ -370,7 +335,6 @@ func TestOpenAICompatibleClientRequestMapping(t *testing.T) {
|
||||
{name: "optional response fields", run: checkOptionalResponseFieldOmission},
|
||||
{name: "reasoning and extra parameters", run: checkReasoningAndExtraParameterMapping},
|
||||
{name: "optional request field presence", run: checkRequestFieldPresence},
|
||||
{name: "configured model fallback", run: checkConfiguredModelFallback},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@@ -380,14 +344,14 @@ func TestOpenAICompatibleClientRequestMapping(t *testing.T) {
|
||||
|
||||
func checkCanonicalRoleAndExactContentMapping(t *testing.T) {
|
||||
provider := newRecordingProvider(t)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{})
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
const content = " exact content\nwith whitespace "
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{
|
||||
Role: domain.RoleDeveloper,
|
||||
Content: content,
|
||||
}}},
|
||||
Target: domain.ExecutionTarget{Model: "model"},
|
||||
Target: domain.ExecutionTarget{Endpoint: provider.endpoint("/v1"), Model: "model"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error = %v", err)
|
||||
@@ -413,7 +377,7 @@ func checkCompleteRequestAndResponseMapping(t *testing.T) {
|
||||
"usage": {"prompt_tokens": 11, "completion_tokens": 22, "total_tokens": 33}
|
||||
}`)
|
||||
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
Timeout: 2 * time.Second,
|
||||
})
|
||||
t.Setenv("PROMPTKIT_TEST_API_KEY", "secret-key")
|
||||
@@ -424,6 +388,7 @@ func checkCompleteRequestAndResponseMapping(t *testing.T) {
|
||||
{Role: "user", Content: "Say hello"},
|
||||
}},
|
||||
Target: domain.ExecutionTarget{
|
||||
Endpoint: provider.endpoint("/v1"),
|
||||
Model: "gpt-test",
|
||||
Temperature: 0.4,
|
||||
MaxTokens: 123,
|
||||
@@ -594,9 +559,11 @@ func TestOpenAICompatibleClientAuthentication(t *testing.T) {
|
||||
tc.configureEnv(t)
|
||||
}
|
||||
provider := newRecordingProvider(t)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{Model: "model"})
|
||||
request := ordinaryGenerateRequest()
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
request := provider.generateRequest()
|
||||
request.Target = tc.target
|
||||
request.Target.Endpoint = provider.endpoint("/v1")
|
||||
request.Target.Model = "model"
|
||||
|
||||
_, err := client.Generate(context.Background(), request)
|
||||
if tc.wantError != nil {
|
||||
@@ -625,7 +592,7 @@ func TestOpenAICompatibleClientAuthentication(t *testing.T) {
|
||||
|
||||
func checkCacheControlledMessageMapping(t *testing.T) {
|
||||
provider := newRecordingProvider(t)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{})
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{
|
||||
@@ -639,7 +606,7 @@ func checkCacheControlledMessageMapping(t *testing.T) {
|
||||
},
|
||||
{Role: "user", Content: "Dynamic request."},
|
||||
}},
|
||||
Target: domain.ExecutionTarget{Model: "model"},
|
||||
Target: domain.ExecutionTarget{Endpoint: provider.endpoint("/v1"), Model: "model"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
@@ -691,7 +658,7 @@ func checkCacheControlledMessageMapping(t *testing.T) {
|
||||
|
||||
func checkEmptyCacheControlTTLOmission(t *testing.T) {
|
||||
provider := newRecordingProvider(t)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{})
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{
|
||||
@@ -703,7 +670,7 @@ func checkEmptyCacheControlTTLOmission(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}},
|
||||
Target: domain.ExecutionTarget{Model: "model"},
|
||||
Target: domain.ExecutionTarget{Endpoint: provider.endpoint("/v1"), Model: "model"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
@@ -745,8 +712,8 @@ func checkSessionIDMapping(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
provider := newRecordingProvider(t)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{})
|
||||
request := ordinaryGenerateRequest()
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
request := provider.generateRequest()
|
||||
request.Prompt.SessionID = tc.sessionID
|
||||
|
||||
if _, err := client.Generate(context.Background(), request); err != nil {
|
||||
@@ -766,19 +733,14 @@ func checkSessionIDMapping(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleClientRejectsTooLongSessionID(t *testing.T) {
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "http://example.com/v1",
|
||||
Model: "model",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
|
||||
_, err = client.Generate(context.Background(), domain.GenerateRequest{
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{
|
||||
SessionID: strings.Repeat("x", domain.SessionIDMaxLength+1),
|
||||
Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}},
|
||||
},
|
||||
Target: domain.ExecutionTarget{Endpoint: "http://example.com/v1", Model: "model"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid request error")
|
||||
@@ -828,9 +790,9 @@ func checkContentPresence(t *testing.T) {
|
||||
"cache_write_tokens": 5
|
||||
}
|
||||
}`)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{Model: "model"})
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
|
||||
response, err := client.Generate(context.Background(), ordinaryGenerateRequest())
|
||||
response, err := client.Generate(context.Background(), provider.generateRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("generate: %v", err)
|
||||
}
|
||||
@@ -865,10 +827,11 @@ func checkCacheUsageMapping(t *testing.T) {
|
||||
"cache_write_tokens": 60
|
||||
}
|
||||
}`)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{Model: "model"})
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
|
||||
resp, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Endpoint: provider.endpoint("/v1"), Model: "model"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
@@ -901,8 +864,8 @@ func TestOpenAICompatibleUsagePresence(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
provider := newRecordingProvider(t)
|
||||
provider.respond(http.StatusOK, `{`+tc.usage+`"choices":[{"message":{"content":"ok"}}]}`)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{Model: "model"})
|
||||
resp, err := client.Generate(context.Background(), ordinaryGenerateRequest())
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
resp, err := client.Generate(context.Background(), provider.generateRequest())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -913,17 +876,17 @@ func TestOpenAICompatibleUsagePresence(t *testing.T) {
|
||||
}
|
||||
provider := newRecordingProvider(t)
|
||||
provider.respond(http.StatusOK, `{"usage":{"prompt_tokens":"seven"},"choices":[{"message":{"content":"ok"}}]}`)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{Model: "model"})
|
||||
if resp, err := client.Generate(context.Background(), ordinaryGenerateRequest()); resp != nil || !errors.Is(err, ErrMalformedResponse) {
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
if resp, err := client.Generate(context.Background(), provider.generateRequest()); resp != nil || !errors.Is(err, ErrMalformedResponse) {
|
||||
t.Fatalf("invalid numeric usage response=%+v, error=%v", resp, err)
|
||||
}
|
||||
}
|
||||
|
||||
func checkOptionalResponseFieldOmission(t *testing.T) {
|
||||
provider := newRecordingProvider(t)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{})
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
|
||||
_, err := client.Generate(context.Background(), ordinaryGenerateRequest())
|
||||
_, err := client.Generate(context.Background(), provider.generateRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -938,11 +901,12 @@ func checkOptionalResponseFieldOmission(t *testing.T) {
|
||||
|
||||
func checkReasoningAndExtraParameterMapping(t *testing.T) {
|
||||
provider := newRecordingProvider(t)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{})
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{
|
||||
Endpoint: provider.endpoint("/v1"),
|
||||
Model: "model",
|
||||
ReasoningEffort: "high",
|
||||
ExtraParams: map[string]any{
|
||||
@@ -1020,8 +984,8 @@ func checkRequestFieldPresence(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
provider := newRecordingProvider(t)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{})
|
||||
request := ordinaryGenerateRequest()
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
request := provider.generateRequest()
|
||||
request.TargetPresence = tc.presence
|
||||
|
||||
if _, err := client.Generate(context.Background(), request); err != nil {
|
||||
@@ -1073,21 +1037,17 @@ func checkOmittedGenerationTimeout(t *testing.T) {
|
||||
transport := &deadlineCapturingTransport{}
|
||||
clientTimeout := 5 * time.Second
|
||||
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "http://example.com/v1",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
Timeout: clientTimeout,
|
||||
HTTPClient: &http.Client{
|
||||
Transport: transport,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
before := time.Now()
|
||||
_, err = client.Generate(context.Background(), domain.GenerateRequest{
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Model: "model", TimeoutSeconds: 0},
|
||||
Target: domain.ExecutionTarget{Endpoint: "http://example.com/v1", Model: "model", TimeoutSeconds: 0},
|
||||
})
|
||||
after := time.Now()
|
||||
if err == nil {
|
||||
@@ -1146,11 +1106,11 @@ func TestOpenAICompatibleClientRejectsInvalidExtraParamsBeforeProviderCall(t *te
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
provider := newRecordingProvider(t)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{})
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Model: "model", ExtraParams: tc.extraParams},
|
||||
Target: domain.ExecutionTarget{Endpoint: provider.endpoint("/v1"), Model: "model", ExtraParams: tc.extraParams},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid request error")
|
||||
@@ -1192,8 +1152,8 @@ func TestOpenAICompatibleClientClassifiesSchemaEncodingFailures(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
provider := newRecordingProvider(t)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{})
|
||||
request := ordinaryGenerateRequest()
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
request := provider.generateRequest()
|
||||
request.Target.ExtraParams = tc.extraParams
|
||||
request.StructuredOutput = &domain.StructuredOutputSpec{
|
||||
Type: domain.StructuredOutputJSONSchema,
|
||||
@@ -1218,30 +1178,12 @@ func TestOpenAICompatibleClientClassifiesSchemaEncodingFailures(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func checkConfiguredModelFallback(t *testing.T) {
|
||||
provider := newRecordingProvider(t)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{Model: "default-model"})
|
||||
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
var model string
|
||||
if !provider.lastRequest(t).decodeField(t, "model", &model) || model != "default-model" {
|
||||
t.Fatalf("model = %q, want default-model", model)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleClientEndpointComposition(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
run func(*testing.T)
|
||||
}{
|
||||
{name: "request endpoint overrides configured endpoint", run: checkEndpointOverride},
|
||||
{name: "request endpoint works without configured endpoint", run: checkEmptyConfiguredEndpoint},
|
||||
{name: "request endpoint", run: checkSelectedEndpoint},
|
||||
{name: "completion URL preserves valid base paths", run: checkCompletionURLComposition},
|
||||
{name: "invalid selected endpoint fails before transport", run: checkInvalidSelectedEndpointRejection},
|
||||
{name: "an endpoint is required", run: checkRequiredEndpoint},
|
||||
@@ -1252,35 +1194,6 @@ func TestOpenAICompatibleClientEndpointComposition(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func checkEndpointOverride(t *testing.T) {
|
||||
configuredProvider := newRecordingProvider(t)
|
||||
configuredProvider.respond(http.StatusOK, `{"choices":[{"message":{"content":"default"}}]}`)
|
||||
selectedProvider := newRecordingProvider(t)
|
||||
selectedProvider.respond(http.StatusOK, `{"choices":[{"message":{"content":"override"}}]}`)
|
||||
|
||||
client := newProviderClient(t, configuredProvider, OpenAICompatibleConfig{Model: "m"})
|
||||
|
||||
resp, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Endpoint: selectedProvider.endpoint("/v1")},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if resp.Content != "override" {
|
||||
t.Fatalf("expected override response, got %q", resp.Content)
|
||||
}
|
||||
if got := configuredProvider.requestCount(); got != 0 {
|
||||
t.Fatalf("configured provider calls = %d, want 0", got)
|
||||
}
|
||||
if got := selectedProvider.requestCount(); got != 1 {
|
||||
t.Fatalf("selected provider calls = %d, want 1", got)
|
||||
}
|
||||
if got := selectedProvider.lastRequest(t).url; got != "/v1/chat/completions" {
|
||||
t.Fatalf("selected URL = %q, want /v1/chat/completions", got)
|
||||
}
|
||||
}
|
||||
|
||||
func checkCommonResponseFailures(t *testing.T) {
|
||||
const sensitiveBody = `provider-secret-fragment request_payload_details`
|
||||
tests := []struct {
|
||||
@@ -1310,9 +1223,9 @@ func checkCommonResponseFailures(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
provider := newRecordingProvider(t)
|
||||
provider.respond(tc.statusCode, tc.body)
|
||||
client := newProviderClient(t, provider, OpenAICompatibleConfig{Model: "m"})
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
|
||||
response, err := client.Generate(context.Background(), ordinaryGenerateRequest())
|
||||
response, err := client.Generate(context.Background(), provider.generateRequest())
|
||||
if response != nil {
|
||||
t.Fatalf("expected no partial response, got %+v", response)
|
||||
}
|
||||
@@ -1357,9 +1270,7 @@ func checkSuccessfulResponseByteBoundary(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
body := sizedSuccessResponseBody(tc.size)
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "https://provider.example/v1",
|
||||
Model: "m",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
@@ -1370,12 +1281,10 @@ func checkSuccessfulResponseByteBoundary(t *testing.T) {
|
||||
}, nil
|
||||
})},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("construct client: %v", err)
|
||||
}
|
||||
|
||||
response, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Endpoint: "https://provider.example/v1", Model: "m"},
|
||||
})
|
||||
if tc.wantErr {
|
||||
if response != nil {
|
||||
@@ -1425,9 +1334,7 @@ func checkContinuingOversizedResponse(t *testing.T) {
|
||||
strings.NewReader(prefix),
|
||||
continuation,
|
||||
)}
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "https://provider.example/v1",
|
||||
Model: "m",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
@@ -1438,9 +1345,6 @@ func checkContinuingOversizedResponse(t *testing.T) {
|
||||
}, nil
|
||||
})},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("construct client: %v", err)
|
||||
}
|
||||
|
||||
type outcome struct {
|
||||
response *domain.GenerateResponse
|
||||
@@ -1450,6 +1354,7 @@ func checkContinuingOversizedResponse(t *testing.T) {
|
||||
go func() {
|
||||
response, generateErr := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Endpoint: "https://provider.example/v1", Model: "m"},
|
||||
})
|
||||
done <- outcome{response: response, err: generateErr}
|
||||
}()
|
||||
@@ -1497,9 +1402,7 @@ func checkSingleResponseDocument(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
body := &countingReadCloser{reader: strings.NewReader(tc.body)}
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "https://provider.example/v1",
|
||||
Model: "m",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
@@ -1510,12 +1413,10 @@ func checkSingleResponseDocument(t *testing.T) {
|
||||
}, nil
|
||||
})},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("construct client: %v", err)
|
||||
}
|
||||
|
||||
response, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Endpoint: "https://provider.example/v1", Model: "m"},
|
||||
})
|
||||
if tc.wantErr {
|
||||
if response != nil {
|
||||
@@ -1541,22 +1442,19 @@ func checkGenerationTimeoutPrecedence(t *testing.T) {
|
||||
transport := &deadlineCapturingTransport{err: context.DeadlineExceeded}
|
||||
generationTimeout := 2 * time.Second
|
||||
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "http://example.com/v1",
|
||||
Model: "m",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: transport,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
before := time.Now()
|
||||
_, err = client.Generate(context.Background(), domain.GenerateRequest{
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{
|
||||
Endpoint: "http://example.com/v1",
|
||||
Model: "m",
|
||||
TimeoutSeconds: int(generationTimeout / time.Second),
|
||||
},
|
||||
})
|
||||
@@ -1578,25 +1476,22 @@ func checkGenerationTimeoutPrecedence(t *testing.T) {
|
||||
|
||||
func checkCallerDeadlinePrecedence(t *testing.T) {
|
||||
transport := &deadlineCapturingTransport{err: context.DeadlineExceeded}
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "http://example.com/v1",
|
||||
Model: "m",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: transport,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
callerDeadline := time.Now().Add(time.Second)
|
||||
ctx, cancel := context.WithDeadline(context.Background(), callerDeadline)
|
||||
defer cancel()
|
||||
|
||||
_, err = client.Generate(ctx, domain.GenerateRequest{
|
||||
_, err := client.Generate(ctx, domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{
|
||||
Endpoint: "http://example.com/v1",
|
||||
Model: "m",
|
||||
TimeoutSeconds: 2,
|
||||
},
|
||||
})
|
||||
@@ -1618,22 +1513,18 @@ func checkCallerDeadlinePrecedence(t *testing.T) {
|
||||
}
|
||||
|
||||
func checkCallerCancellationIdentity(t *testing.T) {
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "http://example.com/v1",
|
||||
Model: "m",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{
|
||||
Transport: contextErrorTransport{},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
_, err = client.Generate(ctx, domain.GenerateRequest{
|
||||
_, err := client.Generate(ctx, domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Endpoint: "http://example.com/v1", Model: "m"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected cancellation error")
|
||||
@@ -1647,21 +1538,17 @@ func checkCallerCancellationIdentity(t *testing.T) {
|
||||
}
|
||||
|
||||
func checkExpiredCallerDeadlineIdentity(t *testing.T) {
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "http://example.com/v1",
|
||||
Model: "m",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{
|
||||
Transport: contextErrorTransport{},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Unix(1, 0))
|
||||
defer cancel()
|
||||
_, err = client.Generate(ctx, domain.GenerateRequest{
|
||||
_, err := client.Generate(ctx, domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Endpoint: "http://example.com/v1", Model: "m"},
|
||||
})
|
||||
if !errors.Is(err, ErrRequestFailed) || !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("expected request failure and caller deadline identities, got %v", err)
|
||||
@@ -1669,20 +1556,16 @@ func checkExpiredCallerDeadlineIdentity(t *testing.T) {
|
||||
}
|
||||
|
||||
func checkWholeRequestTimeoutIdentity(t *testing.T) {
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "http://example.com/v1",
|
||||
Model: "m",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: time.Millisecond,
|
||||
Transport: waitingContextTransport{},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = client.Generate(context.Background(), domain.GenerateRequest{
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Endpoint: "http://example.com/v1", Model: "m"},
|
||||
})
|
||||
if !errors.Is(err, ErrRequestFailed) || !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("expected request failure and client timeout identities, got %v", err)
|
||||
@@ -1696,20 +1579,15 @@ func checkTransportFailureIdentityAndRedaction(t *testing.T) {
|
||||
apiKey = "sensitive-api-key"
|
||||
content = "sensitive prompt content"
|
||||
)
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: endpoint,
|
||||
Model: "m",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
return nil, transportCause
|
||||
})},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = client.Generate(context.Background(), domain.GenerateRequest{
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: content}}},
|
||||
Target: domain.ExecutionTarget{APIKey: apiKey},
|
||||
Target: domain.ExecutionTarget{Endpoint: endpoint, Model: "m", APIKey: apiKey},
|
||||
})
|
||||
if !errors.Is(err, ErrRequestFailed) || !errors.Is(err, transportCause) {
|
||||
t.Fatalf("expected request failure and transport cause identities, got %v", err)
|
||||
@@ -1726,13 +1604,7 @@ func checkTransportFailureIdentityAndRedaction(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleClientRejectsInvalidExecutionSettings(t *testing.T) {
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "http://example.com/v1",
|
||||
Model: "m",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
@@ -1765,11 +1637,9 @@ func TestOpenAICompatibleClientRejectsInvalidExecutionSettings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func checkEmptyConfiguredEndpoint(t *testing.T) {
|
||||
func checkSelectedEndpoint(t *testing.T) {
|
||||
var selectedURL string
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "",
|
||||
Model: "m",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
selectedURL = req.URL.String()
|
||||
return &http.Response{
|
||||
@@ -1782,13 +1652,10 @@ func checkEmptyConfiguredEndpoint(t *testing.T) {
|
||||
}, nil
|
||||
})},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected empty configured base URL to be allowed, got %v", err)
|
||||
}
|
||||
|
||||
response, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Endpoint: "http://request-endpoint.example/v1"},
|
||||
Target: domain.ExecutionTarget{Endpoint: "http://request-endpoint.example/v1", Model: "m"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("generate with request endpoint: %v", err)
|
||||
@@ -1817,9 +1684,7 @@ func checkCompletionURLComposition(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var selectedURL string
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: tc.baseURL,
|
||||
Model: "m",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
selectedURL = req.URL.String()
|
||||
return &http.Response{
|
||||
@@ -1830,12 +1695,10 @@ func checkCompletionURLComposition(t *testing.T) {
|
||||
}, nil
|
||||
})},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("construct client: %v", err)
|
||||
}
|
||||
|
||||
_, err = client.Generate(context.Background(), domain.GenerateRequest{
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Endpoint: tc.baseURL, Model: "m"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("generate: %v", err)
|
||||
@@ -1860,21 +1723,16 @@ func checkInvalidSelectedEndpointRejection(t *testing.T) {
|
||||
for _, endpoint := range invalidEndpoints {
|
||||
t.Run(endpoint, func(t *testing.T) {
|
||||
transportCalls := 0
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "https://configured.example/v1",
|
||||
Model: "m",
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
HTTPClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
transportCalls++
|
||||
return nil, errors.New("transport must not be called")
|
||||
})},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("construct client: %v", err)
|
||||
}
|
||||
|
||||
_, err = client.Generate(context.Background(), domain.GenerateRequest{
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Endpoint: endpoint},
|
||||
Target: domain.ExecutionTarget{Endpoint: endpoint, Model: "m"},
|
||||
})
|
||||
if !errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
||||
@@ -1890,17 +1748,11 @@ func checkInvalidSelectedEndpointRejection(t *testing.T) {
|
||||
}
|
||||
|
||||
func checkRequiredEndpoint(t *testing.T) {
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "",
|
||||
Model: "m",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
|
||||
_, err = client.Generate(context.Background(), domain.GenerateRequest{
|
||||
_, err := client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{},
|
||||
Target: domain.ExecutionTarget{Model: "m"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected endpoint-required error")
|
||||
@@ -1909,3 +1761,13 @@ func checkRequiredEndpoint(t *testing.T) {
|
||||
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleClientRequiresModel(t *testing.T) {
|
||||
client := NewOpenAICompatibleClient(OpenAICompatibleConfig{})
|
||||
_, err := client.PrepareGeneration(domain.GenerateRequest{
|
||||
Target: domain.ExecutionTarget{Endpoint: "https://provider.example/v1"},
|
||||
})
|
||||
if !errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("missing model error = %v, want invalid request", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user