caller별 예외 대신 요청 의미와 protocol profile capability로 operation을 선택해 tools와 effort 조합을 보존한다. 지원하지 않는 effort는 가장 가까운 하위 등급으로만 내리고 상향 매핑은 거부한다.
540 lines
20 KiB
Go
540 lines
20 KiB
Go
package openai
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
edgeservice "iop/apps/edge/internal/service"
|
|
"iop/packages/go/config"
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
// TestResponsesProtocolProfileOperationPassthrough verifies that an explicit
|
|
// openai_responses profile admits the responses operation through the
|
|
// provider-pool tunnel and that the tunnel request carries the canonical
|
|
// operation identifier.
|
|
func TestResponsesProtocolProfileOperationPassthrough(t *testing.T) {
|
|
profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatalf("ResolveProtocolProfile: %v", err)
|
|
}
|
|
|
|
fake := &providerFakeRunService{
|
|
tunnelFrames: staticProviderTunnelFrames(`{"id":"resp-1","object":"response","output":[]}`),
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
|
|
ProviderID: "prov-openai",
|
|
ProfileID: profile.ID,
|
|
ProfileDriver: string(profile.Driver),
|
|
ProfileCapabilities: append([]string(nil), profile.Capabilities...),
|
|
ProtocolProfile: &profile,
|
|
},
|
|
}
|
|
|
|
catalog := []config.ModelCatalogEntry{{
|
|
ID: "responses-model",
|
|
Providers: map[string]string{"prov-openai": "gpt-4-responses"},
|
|
}}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"responses-model",
|
|
"input":"say hello via responses"
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if len(fake.reqsSnapshot()) != 0 {
|
|
t.Fatalf("profile-backed responses must not fall back to normalized SubmitRun, got %d calls", len(fake.reqsSnapshot()))
|
|
}
|
|
reqs := fake.tunnelReqsSnapshot()
|
|
if len(reqs) != 1 {
|
|
t.Fatalf("expected 1 tunnel dispatch, got %d", len(reqs))
|
|
}
|
|
got := reqs[0]
|
|
if got.Operation != string(config.OperationResponses) {
|
|
t.Fatalf("tunnel operation: got %q want %q", got.Operation, config.OperationResponses)
|
|
}
|
|
if got.Path != "/v1/responses" {
|
|
t.Fatalf("tunnel path: got %q want /v1/responses", got.Path)
|
|
}
|
|
if got.Method != http.MethodPost {
|
|
t.Fatalf("tunnel method: got %q want POST", got.Method)
|
|
}
|
|
if got.ProviderID != "prov-openai" {
|
|
t.Fatalf("selected provider: got %q want prov-openai", got.ProviderID)
|
|
}
|
|
}
|
|
|
|
// TestResponsesProtocolProfileOperationPassthroughNonStream verifies that the
|
|
// responses profile operation admits non-streaming passthrough and that the
|
|
// provider body preserves the caller's Responses-shaped fields verbatim.
|
|
func TestResponsesProtocolProfileOperationPassthroughNonStream(t *testing.T) {
|
|
profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatalf("ResolveProtocolProfile: %v", err)
|
|
}
|
|
|
|
fake := &providerFakeRunService{
|
|
tunnelFrames: staticProviderTunnelFrames(`{"id":"resp-ns","object":"response","output":[]}`),
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
|
|
ProviderID: "prov-openai-ns",
|
|
ProfileID: profile.ID,
|
|
ProfileDriver: string(profile.Driver),
|
|
ProfileCapabilities: append([]string(nil), profile.Capabilities...),
|
|
ProtocolProfile: &profile,
|
|
},
|
|
}
|
|
|
|
catalog := []config.ModelCatalogEntry{{
|
|
ID: "responses-ns",
|
|
Providers: map[string]string{"prov-openai-ns": "gpt-4-ns"},
|
|
}}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"responses-ns",
|
|
"input":"non-stream input",
|
|
"max_output_tokens":128
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
bodies := fake.tunnelBodiesSnapshot()
|
|
if len(bodies) != 1 {
|
|
t.Fatalf("expected 1 tunnel body, got %d", len(bodies))
|
|
}
|
|
var providerReq map[string]any
|
|
if err := json.Unmarshal(bodies[0], &providerReq); err != nil {
|
|
t.Fatalf("provider body JSON: %v", err)
|
|
}
|
|
if providerReq["max_output_tokens"].(float64) != 128 {
|
|
t.Fatalf("max_output_tokens must be preserved: %+v", providerReq)
|
|
}
|
|
if _, ok := providerReq["stream"]; ok {
|
|
t.Fatalf("stream must not be injected on passthrough: %+v", providerReq)
|
|
}
|
|
}
|
|
|
|
func TestResponsesProtocolProfileEffortFallsBackToNearestLowerGrade(t *testing.T) {
|
|
profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatalf("ResolveProtocolProfile: %v", err)
|
|
}
|
|
mapping := profile.Normalization.Effort[string(config.OperationResponses)]
|
|
delete(mapping.Levels, "max")
|
|
profile.Normalization.Effort[string(config.OperationResponses)] = mapping
|
|
|
|
fake := &providerFakeRunService{
|
|
tunnelFrames: staticProviderTunnelFrames(`{"id":"resp-effort","object":"response","output":[]}`),
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
|
|
ActualModel: "gpt-served",
|
|
ProviderID: "prov-openai-effort",
|
|
ExecutionPath: string(edgeservice.ProviderPoolPathTunnel),
|
|
ProfileID: profile.ID,
|
|
ProfileDriver: string(profile.Driver),
|
|
ProfileCapabilities: append([]string(nil), profile.Capabilities...),
|
|
ProtocolProfile: &profile,
|
|
},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "responses-effort", Providers: map[string]string{"prov-openai-effort": "gpt-served"}}})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"responses-effort",
|
|
"input":"use the tool",
|
|
"reasoning":{"effort":"max","summary":"auto"},
|
|
"tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}]
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
var upstream map[string]any
|
|
if err := json.Unmarshal(fake.tunnelBodiesSnapshot()[0], &upstream); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reasoning, _ := upstream["reasoning"].(map[string]any)
|
|
if reasoning["effort"] != "xhigh" || reasoning["summary"] != "auto" {
|
|
t.Fatalf("reasoning=%v, want effort fallback with preserved summary", reasoning)
|
|
}
|
|
if upstream["model"] != "gpt-served" {
|
|
t.Fatalf("model=%v, want served target", upstream["model"])
|
|
}
|
|
}
|
|
|
|
// TestResponsesProtocolProfileOperationPassthroughStream verifies that the
|
|
// responses profile operation admits stream=true passthrough and that the
|
|
// provider tunnel carries the stream flag to the upstream provider.
|
|
func TestResponsesProtocolProfileOperationPassthroughStream(t *testing.T) {
|
|
profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatalf("ResolveProtocolProfile: %v", err)
|
|
}
|
|
|
|
frames := make(chan *iop.ProviderTunnelFrame, 3)
|
|
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: 200}
|
|
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(`{"id":"resp-stream","object":"response","output":[],"delta":"chunk-1"}`)}
|
|
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
|
|
close(frames)
|
|
|
|
fake := &providerFakeRunService{
|
|
tunnelFrames: frames,
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
|
|
ProviderID: "prov-openai-stream",
|
|
ProfileID: profile.ID,
|
|
ProfileDriver: string(profile.Driver),
|
|
ProfileCapabilities: append([]string(nil), profile.Capabilities...),
|
|
ProtocolProfile: &profile,
|
|
},
|
|
}
|
|
|
|
catalog := []config.ModelCatalogEntry{{
|
|
ID: "responses-stream",
|
|
Providers: map[string]string{"prov-openai-stream": "gpt-4-stream"},
|
|
}}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"responses-stream",
|
|
"input":"stream this",
|
|
"stream":true
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
reqs := fake.tunnelReqsSnapshot()
|
|
if len(reqs) != 1 {
|
|
t.Fatalf("expected 1 tunnel dispatch, got %d", len(reqs))
|
|
}
|
|
if !reqs[0].Stream {
|
|
t.Fatal("stream tunnel request must carry stream=true")
|
|
}
|
|
}
|
|
|
|
// TestResponsesProtocolProfileUnsupportedRejectsBeforeDispatch verifies that an
|
|
// explicit profile which does not declare the responses operation is rejected
|
|
// before any candidate selection or dispatch occurs. The handler maps the typed
|
|
// operation admission failure to HTTP 400 with invalid_request_error and the
|
|
// sanitized message "no provider profile supports the requested Responses operation".
|
|
func TestResponsesProtocolProfileUnsupportedRejectsBeforeDispatch(t *testing.T) {
|
|
// Anthropic profile does not declare the responses operation.
|
|
anthropicProfile, err := config.ResolveProtocolProfile("anthropic", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatalf("ResolveProtocolProfile anthropic: %v", err)
|
|
}
|
|
|
|
fake := &providerFakeRunService{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
|
|
ProviderID: "prov-anthropic",
|
|
ProfileID: anthropicProfile.ID,
|
|
ProfileDriver: string(anthropicProfile.Driver),
|
|
ProfileCapabilities: append([]string(nil), anthropicProfile.Capabilities...),
|
|
ProtocolProfile: &anthropicProfile,
|
|
},
|
|
}
|
|
|
|
catalog := []config.ModelCatalogEntry{{
|
|
ID: "responses-model",
|
|
Providers: map[string]string{"prov-anthropic": "claude-3"},
|
|
}}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"responses-model",
|
|
"input":"should be rejected"
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected HTTP 400 for unsupported operation, got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal error response: %v", err)
|
|
}
|
|
errObj, ok := resp["error"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("error object missing: %v", resp)
|
|
}
|
|
if errObj["type"] != "invalid_request_error" {
|
|
t.Fatalf("error.type = %v, want invalid_request_error", errObj["type"])
|
|
}
|
|
if errObj["message"] != "no provider profile supports the requested Responses operation" {
|
|
t.Fatalf("error.message = %v, want 'no provider profile supports the requested Responses operation'", errObj["message"])
|
|
}
|
|
if len(fake.tunnelReqsSnapshot()) != 0 {
|
|
t.Fatalf("unsupported profile must not dispatch any tunnel request, got %d", len(fake.tunnelReqsSnapshot()))
|
|
}
|
|
if len(fake.reqsSnapshot()) != 0 {
|
|
t.Fatalf("unsupported profile must not dispatch any normalized run, got %d", len(fake.reqsSnapshot()))
|
|
}
|
|
}
|
|
|
|
// TestResponsesLegacyProfileFallback verifies that a legacy candidate without a
|
|
// profile still falls back to the path-based dispatch and carries the
|
|
// /v1/responses path for tunnel passthrough.
|
|
func TestResponsesLegacyProfileFallback(t *testing.T) {
|
|
fake := &providerFakeRunService{
|
|
tunnelFrames: staticProviderTunnelFrames(`{"id":"resp-legacy","object":"response","output":[]}`),
|
|
tunnelServedTarget: "legacy-served",
|
|
}
|
|
|
|
catalog := []config.ModelCatalogEntry{{
|
|
ID: "legacy-model",
|
|
Providers: map[string]string{"prov-legacy": "legacy-served"},
|
|
}}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"legacy-model",
|
|
"input":"legacy input"
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if len(fake.reqsSnapshot()) != 0 {
|
|
t.Fatalf("legacy fallback must not fall back to normalized SubmitRun, got %d calls", len(fake.reqsSnapshot()))
|
|
}
|
|
reqs := fake.tunnelReqsSnapshot()
|
|
if len(reqs) != 1 {
|
|
t.Fatalf("expected 1 tunnel dispatch, got %d", len(reqs))
|
|
}
|
|
got := reqs[0]
|
|
if got.Path != "/v1/responses" {
|
|
t.Fatalf("legacy tunnel path: got %q want /v1/responses", got.Path)
|
|
}
|
|
if got.Operation != string(config.OperationResponses) {
|
|
t.Fatalf("legacy tunnel operation: got %q want %q", got.Operation, config.OperationResponses)
|
|
}
|
|
}
|
|
|
|
// TestResponsesProtocolProfileProviderAuthInjection verifies that provider auth
|
|
// headers configured via the request header are injected into the tunnel
|
|
// request when the selected candidate carries an explicit profile.
|
|
func TestResponsesProtocolProfileProviderAuthInjection(t *testing.T) {
|
|
profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatalf("ResolveProtocolProfile: %v", err)
|
|
}
|
|
|
|
fake := &providerFakeRunService{
|
|
tunnelFrames: staticProviderTunnelFrames(`{"id":"resp-auth","object":"response","output":[]}`),
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
|
|
ProviderID: "prov-openai-auth",
|
|
ProfileID: profile.ID,
|
|
ProfileDriver: string(profile.Driver),
|
|
ProfileCapabilities: append([]string(nil), profile.Capabilities...),
|
|
ProtocolProfile: &profile,
|
|
},
|
|
}
|
|
|
|
catalog := []config.ModelCatalogEntry{{
|
|
ID: "responses-auth",
|
|
Providers: map[string]string{"prov-openai-auth": "gpt-4-auth"},
|
|
}}
|
|
srv := NewServer(config.EdgeOpenAIConf{ProviderAuth: config.EdgeOpenAIProviderAuthConf{
|
|
Enabled: true,
|
|
FromHeader: "X-IOP-Provider-Authorization",
|
|
TargetHeader: "Authorization",
|
|
Scheme: "Bearer",
|
|
Required: true,
|
|
}}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"responses-auth",
|
|
"input":"auth input"
|
|
}`))
|
|
req.Header.Set("X-IOP-Provider-Authorization", "request-provider-secret")
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
reqs := fake.tunnelReqsSnapshot()
|
|
if len(reqs) != 1 {
|
|
t.Fatalf("expected 1 tunnel dispatch, got %d", len(reqs))
|
|
}
|
|
got := reqs[0]
|
|
if got.Headers["Authorization"] != "Bearer request-provider-secret" {
|
|
t.Fatalf("provider auth header: got %q, want %q", got.Headers["Authorization"], "Bearer request-provider-secret")
|
|
}
|
|
}
|
|
|
|
// TestResponsesProtocolProfileProviderAuthRequiredMissing verifies that a
|
|
// required provider auth header missing from the request is rejected with a
|
|
// 400 before any tunnel dispatch is sent.
|
|
func TestResponsesProtocolProfileProviderAuthRequiredMissing(t *testing.T) {
|
|
profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatalf("ResolveProtocolProfile: %v", err)
|
|
}
|
|
|
|
fake := &providerFakeRunService{
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
|
|
ProviderID: "prov-openai-auth-req",
|
|
ProfileID: profile.ID,
|
|
ProfileDriver: string(profile.Driver),
|
|
ProfileCapabilities: append([]string(nil), profile.Capabilities...),
|
|
ProtocolProfile: &profile,
|
|
},
|
|
}
|
|
|
|
catalog := []config.ModelCatalogEntry{{
|
|
ID: "responses-auth-req",
|
|
Providers: map[string]string{"prov-openai-auth-req": "gpt-4-auth-req"},
|
|
}}
|
|
srv := NewServer(config.EdgeOpenAIConf{ProviderAuth: config.EdgeOpenAIProviderAuthConf{
|
|
Enabled: true,
|
|
FromHeader: "X-IOP-Provider-Authorization",
|
|
TargetHeader: "Authorization",
|
|
Scheme: "Bearer",
|
|
Required: true,
|
|
}}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"responses-auth-req",
|
|
"input":"missing auth"
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code == http.StatusOK {
|
|
t.Fatalf("expected 400 for missing required provider auth, got %d", w.Code)
|
|
}
|
|
if len(fake.tunnelReqsSnapshot()) != 0 {
|
|
t.Fatalf("missing auth must not dispatch any tunnel request, got %d", len(fake.tunnelReqsSnapshot()))
|
|
}
|
|
}
|
|
|
|
// TestResponsesProtocolProfileOperationAdmissionTable verifies the operation
|
|
// admission behavior across supported/unsupported/nil-profile scenarios by
|
|
// driving each case through the handler and asserting the dispatch outcome.
|
|
func TestResponsesProtocolProfileOperationAdmissionTable(t *testing.T) {
|
|
openaiProfile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatalf("ResolveProtocolProfile openai: %v", err)
|
|
}
|
|
anthropicProfile, err := config.ResolveProtocolProfile("anthropic", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatalf("ResolveProtocolProfile anthropic: %v", err)
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
profile *config.ConcreteProtocolProfile
|
|
profileID string
|
|
expectedStatus int
|
|
expectedErrType string
|
|
expectedErrMsg string
|
|
expectTunnelCall bool
|
|
}{
|
|
{
|
|
name: "openai profile admits responses operation",
|
|
profile: &openaiProfile,
|
|
profileID: "openai",
|
|
expectedStatus: http.StatusOK,
|
|
expectTunnelCall: true,
|
|
},
|
|
{
|
|
name: "anthropic profile rejects responses operation",
|
|
profile: &anthropicProfile,
|
|
profileID: "anthropic",
|
|
expectedStatus: http.StatusBadRequest,
|
|
expectedErrType: "invalid_request_error",
|
|
expectedErrMsg: "no provider profile supports the requested Responses operation",
|
|
expectTunnelCall: false,
|
|
},
|
|
{
|
|
name: "nil profile (legacy) admits responses via path fallback",
|
|
profile: nil,
|
|
profileID: "",
|
|
expectedStatus: http.StatusOK,
|
|
expectTunnelCall: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
fake := &providerFakeRunService{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
|
|
tunnelFrames: staticProviderTunnelFrames(`{"id":"resp-table","object":"response","output":[]}`),
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
|
|
ProviderID: "prov-table",
|
|
ProfileID: tc.profileID,
|
|
ProfileDriver: "",
|
|
ProtocolProfile: tc.profile,
|
|
},
|
|
}
|
|
|
|
catalog := []config.ModelCatalogEntry{{
|
|
ID: "responses-table",
|
|
Providers: map[string]string{"prov-table": "served-table"},
|
|
}}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"responses-table",
|
|
"input":"table input"
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code != tc.expectedStatus {
|
|
t.Fatalf("status: got %d, want %d (body=%s)", w.Code, tc.expectedStatus, w.Body.String())
|
|
}
|
|
|
|
if tc.expectedErrType != "" {
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal error response: %v", err)
|
|
}
|
|
errObj, ok := resp["error"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("error object missing: %v", resp)
|
|
}
|
|
if errObj["type"] != tc.expectedErrType {
|
|
t.Fatalf("error.type = %v, want %s", errObj["type"], tc.expectedErrType)
|
|
}
|
|
if errObj["message"] != tc.expectedErrMsg {
|
|
t.Fatalf("error.message = %v, want %s", errObj["message"], tc.expectedErrMsg)
|
|
}
|
|
}
|
|
|
|
tunnelReqs := fake.tunnelReqsSnapshot()
|
|
if tc.expectTunnelCall && len(tunnelReqs) != 1 {
|
|
t.Fatalf("expected 1 tunnel call, got %d", len(tunnelReqs))
|
|
}
|
|
if !tc.expectTunnelCall && len(tunnelReqs) != 0 {
|
|
t.Fatalf("expected 0 tunnel calls, got %d", len(tunnelReqs))
|
|
}
|
|
})
|
|
}
|
|
}
|