표준 Responses 요청이 선택된 provider profile을 통해 손실 없이 실행되고 Gemini의 휴대 가능한 reasoning 등급만 전달되도록 한다.
751 lines
32 KiB
Go
751 lines
32 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) {
|
|
// A Chat profile without a bridgeable native operation is rejected.
|
|
anthropicProfile, err := config.ResolveProtocolProfile("anthropic", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatalf("ResolveProtocolProfile anthropic: %v", err)
|
|
}
|
|
delete(anthropicProfile.Operations, string(config.OperationMessages))
|
|
|
|
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"] != openAIStreamGateCandidateRejectedMessage {
|
|
t.Fatalf("error.message = %v, want %q", errObj["message"], openAIStreamGateCandidateRejectedMessage)
|
|
}
|
|
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 TestResponsesProtocolProfileBridgesToAnthropicMessages(t *testing.T) {
|
|
profile, err := config.ResolveProtocolProfile("anthropic", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := selectProviderOperation(profile, config.OperationResponses, providerRequestRequirements{HasTools: true}); err != nil {
|
|
t.Fatalf("select Messages bridge: %v", err)
|
|
}
|
|
fake := &providerFakeRunService{
|
|
tunnelFrames: staticProviderTunnelFrames(
|
|
`data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":"bridged answer"}}` + "\n\n" +
|
|
`data: {"type":"message_stop"}` + "\n\n"),
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{ProviderID: "claude", ActualModel: "claude-served", 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: "claude-route", Providers: map[string]string{"claude": "claude-served"}, DefaultMaxTokens: 256}})
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"claude-route","instructions":"be concise","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}],"tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}]}`)))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
requests := fake.tunnelReqsSnapshot()
|
|
if len(requests) != 1 {
|
|
t.Fatalf("dispatches=%d", len(requests))
|
|
}
|
|
if requests[0].Operation != string(config.OperationMessages) || requests[0].Path != "/v1/messages" {
|
|
t.Fatalf("operation/path=%s %s", requests[0].Operation, requests[0].Path)
|
|
}
|
|
var providerBody map[string]any
|
|
if err := json.Unmarshal(fake.tunnelBodiesSnapshot()[0], &providerBody); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if providerBody["max_tokens"] != float64(256) {
|
|
t.Fatalf("Messages max_tokens=%v, want catalog default 256", providerBody["max_tokens"])
|
|
}
|
|
if !strings.Contains(w.Body.String(), `"output_text":"bridged answer"`) {
|
|
t.Fatalf("Responses response was not restored: %s", w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestResponsesProtocolProfileAnthropicBridgeResponse(t *testing.T) {
|
|
profile, err := config.ResolveProtocolProfile("anthropic", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Run("catalog token policy and canonical usage", func(t *testing.T) {
|
|
fake := &providerFakeRunService{
|
|
tunnelFrames: staticProviderTunnelFrames(`{"id":"msg-1","type":"message","role":"assistant","content":[{"type":"text","text":"answer"}],"usage":{"input_tokens":17,"output_tokens":9}}`),
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{ProviderID: "claude", ActualModel: "claude-served", 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: "claude-route", Providers: map[string]string{"claude": "claude-served"}, DefaultMaxTokens: 128, MinMaxTokens: 64}})
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"claude-route","input":"hello"}`)))
|
|
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)
|
|
}
|
|
if upstream["max_tokens"] != float64(128) {
|
|
t.Fatalf("max_tokens=%v, want 128", upstream["max_tokens"])
|
|
}
|
|
if body := w.Body.String(); !strings.Contains(body, `"input_tokens":17`) || !strings.Contains(body, `"output_tokens":9`) || !strings.Contains(body, `"total_tokens":26`) {
|
|
t.Fatalf("canonical usage missing: %s", body)
|
|
}
|
|
})
|
|
t.Run("missing effective limit rejects before dispatch", func(t *testing.T) {
|
|
fake := &providerFakeRunService{poolSelectedCandidate: edgeservice.ProviderPoolCandidate{ProviderID: "claude", ProtocolProfile: &profile}}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "claude-route", Providers: map[string]string{"claude": "claude-served"}}})
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"claude-route","input":"hello"}`)))
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if len(fake.tunnelReqsSnapshot()) != 0 {
|
|
t.Fatalf("invalid bridge dispatched %d requests", len(fake.tunnelReqsSnapshot()))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestResponsesProtocolProfileBridgesToGeminiChat(t *testing.T) {
|
|
profile, err := config.ResolveProtocolProfile("gemini", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := selectProviderOperation(profile, config.OperationResponses, providerRequestRequirements{HasTools: true, Effort: "high"}); err != nil {
|
|
t.Fatalf("select Chat bridge: %v", err)
|
|
}
|
|
fake := &providerFakeRunService{
|
|
tunnelFrames: staticProviderTunnelFrames(
|
|
`data: {"id":"chat-1","choices":[{"delta":{"content":"gemini answer","reasoning_content":"brief reasoning","tool_calls":[{"index":0,"id":"call-1","function":{"name":"lookup","arguments":"{}"},"extra_content":{"google":{"thought_signature":"sig-1"}}}]}}]}` + "\n\n" +
|
|
`data: [DONE]` + "\n\n"),
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{ProviderID: "gemini", ActualModel: "gemini-served", 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: "gemini-route", Providers: map[string]string{"gemini": "gemini-served"}}})
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"gemini-route","input":"hello","reasoning":{"effort":"high"},"tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}]}`)))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
requests := fake.tunnelReqsSnapshot()
|
|
if len(requests) != 1 || requests[0].Operation != string(config.OperationChatCompletions) || requests[0].Path != "/v1/chat/completions" {
|
|
t.Fatalf("requests=%+v", requests)
|
|
}
|
|
var provider map[string]any
|
|
if err := json.Unmarshal(fake.tunnelBodiesSnapshot()[0], &provider); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if provider["reasoning_effort"] != "high" {
|
|
t.Fatalf("Chat bridge body=%v", provider)
|
|
}
|
|
callID := encodeGeminiThoughtSignatureToolID("call-1", "sig-1")
|
|
if !strings.Contains(w.Body.String(), callID) || !strings.Contains(w.Body.String(), `"output_text":"gemini answer"`) {
|
|
t.Fatalf("Gemini response/signature not restored: %s", w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestResponsesProtocolProfileGeminiBridgeResponseAndSignature(t *testing.T) {
|
|
profile, err := config.ResolveProtocolProfile("gemini", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fake := &providerFakeRunService{
|
|
tunnelFrames: staticProviderTunnelFrames(`data: {"id":"chat-1","choices":[{"delta":{"content":"answer","tool_calls":[{"index":0,"id":"call-1","type":"function","function":{"name":"lookup","arguments":"{}"},"extra_content":{"google":{"thought_signature":"secret-signature"}}}]}}],"usage":{"prompt_tokens":5,"completion_tokens":3}}` + "\n\n" + `data: [DONE]` + "\n\n"),
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{ProviderID: "gemini", ActualModel: "gemini-served", 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: "gemini-route", Providers: map[string]string{"gemini": "gemini-served"}}})
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"gemini-route","input":"hello","tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}]}`)))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
wantCallID := encodeGeminiThoughtSignatureToolID("call-1", "secret-signature")
|
|
if !strings.Contains(w.Body.String(), wantCallID) || strings.Contains(w.Body.String(), "secret-signature") {
|
|
t.Fatalf("opaque signature call_id missing or raw signature leaked: %s", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestResponsesProtocolProfileGeminiEffortFallsBackToHigh verifies that a
|
|
// Responses request with effort=max routed through the Gemini Chat bridge
|
|
// selects the Chat operation and sends reasoning_effort=high to the provider.
|
|
// No thinking_level or thinking_budget is synthesized.
|
|
func TestResponsesProtocolProfileGeminiEffortFallsBackToHigh(t *testing.T) {
|
|
profile, err := config.ResolveProtocolProfile("gemini", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := selectProviderOperation(profile, config.OperationResponses, providerRequestRequirements{HasTools: true, Effort: "max"}); err != nil {
|
|
t.Fatalf("select Chat bridge with max effort: %v", err)
|
|
}
|
|
fake := &providerFakeRunService{
|
|
tunnelFrames: staticProviderTunnelFrames(
|
|
`data: {"id":"chat-eff","choices":[{"delta":{"content":"fallback answer"}}]}` + "\n\n" +
|
|
`data: [DONE]` + "\n\n"),
|
|
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{ProviderID: "gemini-eff", ActualModel: "gemini-served", 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: "gemini-eff", Providers: map[string]string{"gemini-eff": "gemini-served"}}})
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"gemini-eff","input":"hello","reasoning":{"effort":"max"},"tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}]}`)))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
requests := fake.tunnelReqsSnapshot()
|
|
if len(requests) != 1 {
|
|
t.Fatalf("expected 1 tunnel request, got %d", len(requests))
|
|
}
|
|
if requests[0].Operation != string(config.OperationChatCompletions) {
|
|
t.Fatalf("operation=%q, want chat_completions", requests[0].Operation)
|
|
}
|
|
var provider map[string]any
|
|
if err := json.Unmarshal(fake.tunnelBodiesSnapshot()[0], &provider); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if provider["reasoning_effort"] != "high" {
|
|
t.Fatalf("reasoning_effort=%v, want high", provider["reasoning_effort"])
|
|
}
|
|
if _, ok := provider["thinking_level"]; ok {
|
|
t.Errorf("thinking_level must not be synthesized: %+v", provider)
|
|
}
|
|
if _, ok := provider["thinking_budget"]; ok {
|
|
t.Errorf("thinking_budget must not be synthesized: %+v", provider)
|
|
}
|
|
if _, ok := provider["thinking"]; ok {
|
|
t.Errorf("thinking must not be synthesized: %+v", provider)
|
|
}
|
|
}
|
|
|
|
func TestResponsesProtocolProfileBridgeRejectsUnrepresentableControls(t *testing.T) {
|
|
profile, err := config.ResolveProtocolProfile("anthropic", "", config.BuiltInProtocolProfileCatalog())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fake := &providerFakeRunService{poolSelectedCandidate: edgeservice.ProviderPoolCandidate{ProviderID: "claude", 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: "claude-route", Providers: map[string]string{"claude": "claude-served"}}})
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"claude-route","input":"hello","store":true}`)))
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
|
}
|
|
if len(fake.tunnelReqsSnapshot()) != 0 {
|
|
t.Fatal("unrepresentable request dispatched")
|
|
}
|
|
}
|
|
|
|
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 bridges representable Responses operation",
|
|
profile: &anthropicProfile,
|
|
profileID: "anthropic",
|
|
expectedStatus: http.StatusOK,
|
|
expectTunnelCall: true,
|
|
},
|
|
{
|
|
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"},
|
|
DefaultMaxTokens: 256,
|
|
}}
|
|
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))
|
|
}
|
|
})
|
|
}
|
|
}
|