iop/apps/edge/internal/openai/provider_tunnel_test.go
toki 01dc2ef78b refactor: readability baseline 및 테스트 구조 개선
- agent-readable-repository-refactor 완료: 기존 테스트 파일 아카이브 이동
- 새 테스트 파일 추가 (edge, node, client, config, readability)
- readability_audit 스크립트 및 baseline 추가
- roadmap/SDD 문서 갱신
- agent-client/pi/extensions/openai-sampling-parameters 추가
2026-07-17 16:02:12 +09:00

879 lines
35 KiB
Go

package openai
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"go.uber.org/zap"
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
// chatSidebandServer returns a direct OpenAI-compatible provider route backed by
// the given tunnel frames. It routes via the legacy openai_compat model route
// (not provider-pool) so the raw provider tunnel passthrough path is exercised.
func chatSidebandServer(frames chan *iop.ProviderTunnelFrame) (*Server, *providerFakeRunService) {
fake := &providerFakeRunService{
tunnelFrames: frames,
}
srv := chatProviderRouteServer(fake, nil)
return srv, fake
}
func chatProviderRouteServer(fake *providerFakeRunService, logger *zap.Logger) *Server {
return NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "pool-model", Adapter: "openai_compat", Target: "served-model"},
},
}, fake, logger)
}
func TestChatCompletionsPassthroughDoesNotExposeSideband(t *testing.T) {
providerBody := "data: {\"choices\":[{\"delta\":{\"content\":\"pure\"}}]}\n\ndata: [DONE]\n\n"
frames := make(chan *iop.ProviderTunnelFrame, 4)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START,
StatusCode: 200,
Headers: map[string]string{"Content-Type": "text/event-stream"},
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(providerBody)}
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_USAGE,
Usage: &iop.Usage{InputTokens: 3, OutputTokens: 5},
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
srv, _ := chatSidebandServer(frames)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"stream":true,
"messages":[{"role":"user","content":"hello"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
// Byte-identity with the provider body proves no IOP extension markers or
// response-mode labels are injected into the pure passthrough response.
if got := w.Body.String(); got != providerBody {
t.Fatalf("pure passthrough body not byte-identical:\n got: %q\nwant: %q", got, providerBody)
}
}
func TestChatCompletionsPassthroughDoesNotLabelTransformed(t *testing.T) {
providerBody := `{"ok":true}`
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(providerBody)}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
srv, _ := chatSidebandServer(frames)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
// Byte-identity with the provider body proves the pure passthrough response
// carries no IOP response-mode label.
if got := w.Body.String(); got != providerBody {
t.Fatalf("pure passthrough body not byte-identical: %q", got)
}
}
// chatPassthroughServer returns an openai.Server whose fake service relays the
// tunnel to the given provider fixture, emulating the Node relay.
func chatPassthroughServer(providerURL string) (*Server, *providerFakeRunService) {
fake := &providerFakeRunService{
poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
tunnelProviderURL: providerURL,
tunnelServedTarget: "served-model",
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
})
return srv, fake
}
// TestChatCompletionsPassthroughNonStreamingByteIdentity verifies SDD S04 for
// the non-streaming path: when the provider omits a top-level model echo, the
// caller receives the provider JSON body byte-identically, along with provider
// status and headers.
func TestChatCompletionsPassthroughNonStreamingByteIdentity(t *testing.T) {
// Provider-original body with fields the normalized path would rewrite or
// drop (reasoning_content, provider-specific key order and spacing).
providerBody := "{\"id\":\"cmpl-provider-1\",\"object\":\"chat.completion\", \"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"hi\",\"reasoning_content\":\"because\"},\"finish_reason\":\"stop\"}],\"provider_extra\":{\"a\":1}}"
var gotProviderReq []byte
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotProviderReq, _ = io.ReadAll(r.Body)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Provider-Marker", "prov-42")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(providerBody))
}))
defer provider.Close()
srv, fake := chatPassthroughServer(provider.URL)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if got := w.Body.String(); got != providerBody {
t.Fatalf("body not byte-identical to provider:\n got: %q\nwant: %q", got, providerBody)
}
if got := w.Header().Get("X-Provider-Marker"); got != "prov-42" {
t.Errorf("provider header not relayed: %q", got)
}
if got := w.Header().Get("Content-Type"); got != "application/json" {
t.Errorf("content type not relayed: %q", got)
}
// The provider request body carries the served model (model rewrite only).
if !strings.Contains(string(gotProviderReq), `"model":"served-model"`) {
t.Errorf("provider request body missing served model: %s", gotProviderReq)
}
if !strings.Contains(string(gotProviderReq), `"content":"hello"`) {
t.Errorf("provider request body lost caller messages: %s", gotProviderReq)
}
if len(fake.reqsSnapshot()) != 0 {
t.Error("normalized SubmitRun must not be called on passthrough")
}
}
func TestChatCompletionsPassthroughNonStreamingRewritesModelEcho(t *testing.T) {
providerBody := `{"id":"cmpl-provider-1","object":"chat.completion","created":123,"model":"served-model","choices":[{"index":0,"message":{"role":"assistant","content":"hi","reasoning_content":"because"},"finish_reason":"stop"}],"provider_extra":{"a":1}}`
var gotProviderReq []byte
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotProviderReq, _ = io.ReadAll(r.Body)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(providerBody))
}))
defer provider.Close()
srv, _ := chatPassthroughServer(provider.URL)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
var resp struct {
Model string `json:"model"`
Choices []struct {
Message struct {
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content"`
} `json:"message"`
} `json:"choices"`
ProviderExtra map[string]int `json:"provider_extra"`
}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v body=%s", err, w.Body.String())
}
if resp.Model != "pool-model" {
t.Fatalf("response model: got %q, want pool-model; body=%s", resp.Model, w.Body.String())
}
if resp.Choices[0].Message.Content != "hi" || resp.Choices[0].Message.ReasoningContent != "because" {
t.Fatalf("provider content/reasoning not preserved: %+v", resp.Choices[0].Message)
}
if resp.ProviderExtra["a"] != 1 {
t.Fatalf("provider_extra not preserved: %+v", resp.ProviderExtra)
}
if !strings.Contains(string(gotProviderReq), `"model":"served-model"`) {
t.Fatalf("provider request body missing served model: %s", gotProviderReq)
}
}
// TestChatCompletionsPassthroughStreamingByteIdentity verifies SDD S04 for the
// streaming path: provider SSE bytes without top-level model echoes, including
// provider-specific fields like reasoning_content and native tool_calls chunks,
// reach the caller byte-identically.
func TestChatCompletionsPassthroughStreamingByteIdentity(t *testing.T) {
providerBody := "data: {\"choices\":[{\"delta\":{\"role\":\"assistant\"}}]}\n\n" +
"data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"thinking...\"}}]}\n\n" +
"data: {\"choices\":[{\"delta\":{\"content\":\"<think>not rewritten</think>\"}}]}\n\n" +
"data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"name\":\"f\",\"arguments\":\"{}\"}}]}}]}\n\n" +
"data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":3}}\n\n" +
"data: [DONE]\n\n"
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(providerBody))
}))
defer provider.Close()
srv, _ := chatPassthroughServer(provider.URL)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"stream":true,
"messages":[{"role":"user","content":"hello"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if got := w.Body.String(); got != providerBody {
t.Fatalf("SSE stream not byte-identical to provider:\n got: %q\nwant: %q", got, providerBody)
}
if got := w.Header().Get("Content-Type"); got != "text/event-stream" {
t.Errorf("content type not relayed: %q", got)
}
}
func TestChatCompletionsPassthroughStreamingRewritesModelEcho(t *testing.T) {
providerBody := "data: {\"id\":\"chunk-1\",\"object\":\"chat.completion.chunk\",\"created\":123,\"model\":\"served-model\",\"choices\":[{\"delta\":{\"role\":\"assistant\"}}]}\n\n" +
"data: {\"id\":\"chunk-1\",\"object\":\"chat.completion.chunk\",\"created\":123,\"model\":\"served-model\",\"choices\":[{\"delta\":{\"reasoning\":\"thinking...\"}}]}\n\n" +
"data: {\"id\":\"chunk-1\",\"object\":\"chat.completion.chunk\",\"created\":123,\"model\":\"served-model\",\"choices\":[{\"delta\":{\"content\":\"hi\"}}]}\n\n" +
"data: [DONE]\n\n"
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(providerBody))
}))
defer provider.Close()
srv, _ := chatPassthroughServer(provider.URL)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"stream":true,
"messages":[{"role":"user","content":"hello"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
body := w.Body.String()
if strings.Contains(body, `"model":"served-model"`) {
t.Fatalf("response leaked provider-served model: %s", body)
}
if strings.Count(body, `"model":"pool-model"`) != 3 {
t.Fatalf("response model aliases not rewritten in every chunk: %s", body)
}
if !strings.Contains(body, `"reasoning":"thinking..."`) || !strings.Contains(body, `"content":"hi"`) {
t.Fatalf("provider reasoning/content not preserved: %s", body)
}
if !strings.Contains(body, "data: [DONE]") {
t.Fatalf("done marker not preserved: %s", body)
}
if got := w.Header().Get("Content-Type"); got != "text/event-stream" {
t.Errorf("content type not relayed: %q", got)
}
}
// TestChatCompletionsPassthroughProviderErrorStatusRelayed verifies a provider
// error status/body is passed to the caller unmodified instead of being
// converted into an IOP error envelope.
func TestChatCompletionsPassthroughProviderErrorStatusRelayed(t *testing.T) {
providerBody := `{"error":{"message":"rate limited by provider","type":"rate_limit"}}`
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
_, _ = w.Write([]byte(providerBody))
}))
defer provider.Close()
srv, _ := chatPassthroughServer(provider.URL)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusTooManyRequests {
t.Fatalf("provider status not relayed: got %d", w.Code)
}
if got := w.Body.String(); got != providerBody {
t.Fatalf("provider error body not byte-identical: %q", got)
}
}
// TestChatCompletionsPassthroughLegacyProviderRouteUsesTunnel verifies that a
// legacy model_routes entry with an openai_compat adapter also defaults to the
// tunnel passthrough path (direct route, no provider pool).
func TestChatCompletionsPassthroughLegacyProviderRouteUsesTunnel(t *testing.T) {
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(`{"ok":true}`)}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
fake := &providerFakeRunService{
tunnelFrames: frames,
}
srv := NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "compat-model", Adapter: "openai_compat", Target: "backend-model"},
},
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"compat-model",
"messages":[{"role":"user","content":"hello"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
tunnelReqs := fake.tunnelReqsSnapshot()
if len(tunnelReqs) != 1 {
t.Fatalf("expected tunnel dispatch for openai_compat route, got %d", len(tunnelReqs))
}
if tunnelReqs[0].Adapter != "openai_compat" || tunnelReqs[0].Target != "backend-model" {
t.Errorf("tunnel adapter/target: got %q/%q", tunnelReqs[0].Adapter, tunnelReqs[0].Target)
}
if tunnelReqs[0].ProviderPool {
t.Error("legacy route must not set ProviderPool")
}
bodies := fake.tunnelBodiesSnapshot()
if len(bodies) != 1 || !strings.Contains(string(bodies[0]), `"model":"backend-model"`) {
t.Errorf("legacy route body must carry route target: %s", bodies)
}
}
func TestChatCompletionsPassthroughNonProviderRouteKeepsNormalizedPath(t *testing.T) {
fake := &providerFakeRunService{
fakeRunService: fakeRunService{events: bufferedRunEvents(
&iop.RunEvent{Type: "delta", Delta: "ok"},
&iop.RunEvent{Type: "complete"},
)},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama-fixed"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"any-model",
"messages":[{"role":"user","content":"hello"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if len(fake.tunnelReqsSnapshot()) != 0 {
t.Error("ollama route must not use the tunnel")
}
if len(fake.reqsSnapshot()) != 1 {
t.Fatalf("expected normalized SubmitRun dispatch, got %d", len(fake.reqsSnapshot()))
}
}
// TestChatCompletionsPassthroughTunnelErrorBeforeStartReturns502 verifies an
// ERROR frame before response-start becomes a JSON error response.
func TestChatCompletionsPassthroughTunnelErrorBeforeStartReturns502(t *testing.T) {
frames := make(chan *iop.ProviderTunnelFrame, 1)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_ERROR,
Error: "provider connect refused",
}
close(frames)
fake := &providerFakeRunService{
tunnelFrames: frames,
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
})
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusBadGateway {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "provider connect refused") {
t.Fatalf("error message not surfaced: %s", w.Body.String())
}
}
// TestChatCompletionsPassthroughContextCancelSendsCancelRun verifies caller
// disconnect propagates to the Node cancel path for tunnel dispatches.
func TestChatCompletionsPassthroughContextCancelSendsCancelRun(t *testing.T) {
// Frames channel stays open and empty: the tunnel is in-flight when the
// caller context is cancelled.
fake := &providerFakeRunService{
tunnelFrames: make(chan *iop.ProviderTunnelFrame),
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
})
ctx, cancel := context.WithCancel(context.Background())
cancel()
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}]
}`)).WithContext(ctx)
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
calls := fake.cancelCallsSnapshot()
if len(calls) != 1 {
t.Fatalf("expected 1 CancelRun call, got %d: %+v", len(calls), calls)
}
if calls[0].RunID != "run-tunnel" || calls[0].NodeRef != "node-1" {
t.Fatalf("unexpected cancel request: %+v", calls[0])
}
}
// flakyResponseWriter is a tunnel-path ResponseWriter double: it accepts
// failAfterWrites successful Write calls, then fails every subsequent Write,
// optionally sleeping writeDelay per call to emulate a slow client.
type flakyResponseWriter struct {
mu sync.Mutex
header http.Header
status int
body bytes.Buffer
failAfterWrites int // -1: never fail
writeDelay time.Duration
writes int
failedWrites int
}
func newFlakyResponseWriter(failAfterWrites int) *flakyResponseWriter {
return &flakyResponseWriter{header: make(http.Header), failAfterWrites: failAfterWrites}
}
func (w *flakyResponseWriter) Header() http.Header { return w.header }
func (w *flakyResponseWriter) WriteHeader(code int) {
w.mu.Lock()
defer w.mu.Unlock()
if w.status == 0 {
w.status = code
}
}
func (w *flakyResponseWriter) Write(p []byte) (int, error) {
if w.writeDelay > 0 {
time.Sleep(w.writeDelay)
}
w.mu.Lock()
defer w.mu.Unlock()
if w.status == 0 {
w.status = http.StatusOK
}
if w.failAfterWrites >= 0 && w.writes >= w.failAfterWrites {
w.failedWrites++
return 0, errors.New("client connection write failed")
}
w.writes++
w.body.Write(p)
return len(p), nil
}
func (w *flakyResponseWriter) Flush() {}
func (w *flakyResponseWriter) snapshot() (status int, body string, failedWrites int) {
w.mu.Lock()
defer w.mu.Unlock()
return w.status, w.body.String(), w.failedWrites
}
// TestChatCompletionsPassthroughWriteFailureSendsCancelRunOnce verifies S09
// for a caller write failure after response start: exactly one CancelRun is
// propagated to the Node, the ordered prefix written before the failure is
// preserved, and no tunnel bytes are written after the failed write even
// though more frames (including END) are available.
func TestChatCompletionsPassthroughWriteFailureSendsCancelRunOnce(t *testing.T) {
frames := make(chan *iop.ProviderTunnelFrame, 5)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START,
StatusCode: 200,
Headers: map[string]string{"Content-Type": "text/event-stream"},
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte("data: chunk-1\n\n")}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte("data: chunk-2\n\n")}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte("data: chunk-3\n\n")}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
srv, fake := chatSidebandServer(frames)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"stream":true,
"messages":[{"role":"user","content":"hello"}]
}`))
w := newFlakyResponseWriter(1) // first body write succeeds, second fails
srv.handleChatCompletions(w, req)
status, body, failedWrites := w.snapshot()
if status != http.StatusOK {
t.Fatalf("status: got %d body=%s", status, body)
}
if body != "data: chunk-1\n\n" {
t.Fatalf("body after write failure must stay the ordered prefix:\n got: %q\nwant: %q", body, "data: chunk-1\n\n")
}
if failedWrites == 0 {
t.Fatal("fixture did not exercise the write failure path")
}
calls := fake.cancelCallsSnapshot()
if len(calls) != 1 {
t.Fatalf("expected exactly 1 CancelRun call on write failure, got %d: %+v", len(calls), calls)
}
if calls[0].RunID != "run-tunnel" || calls[0].NodeRef != "node-1" {
t.Fatalf("unexpected cancel request: %+v", calls[0])
}
}
// TestChatCompletionsPassthroughWaitTimeoutSendsCancelRun verifies the tunnel
// wait timeout before response start returns a 502 and propagates exactly one
// CancelRun to the Node.
func TestChatCompletionsPassthroughWaitTimeoutSendsCancelRun(t *testing.T) {
fake := &providerFakeRunService{
tunnelFrames: make(chan *iop.ProviderTunnelFrame),
tunnelWaitTimeout: 50 * time.Millisecond,
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
})
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusBadGateway {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "run timed out") {
t.Fatalf("timeout error not surfaced: %s", w.Body.String())
}
calls := fake.cancelCallsSnapshot()
if len(calls) != 1 {
t.Fatalf("expected exactly 1 CancelRun call on wait timeout, got %d: %+v", len(calls), calls)
}
if calls[0].RunID != "run-tunnel" {
t.Fatalf("unexpected cancel request: %+v", calls[0])
}
}
// TestChatCompletionsPassthroughProviderErrorAfterStartStopsBodyWrites
// verifies an ERROR frame after response start truncates the stream: bytes
// written before the error are preserved in order, later frames are never
// written, and no spurious CancelRun is sent for a provider-terminal failure.
func TestChatCompletionsPassthroughProviderErrorAfterStartStopsBodyWrites(t *testing.T) {
frames := make(chan *iop.ProviderTunnelFrame, 4)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START,
StatusCode: 200,
Headers: map[string]string{"Content-Type": "text/event-stream"},
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte("data: before-error\n\n")}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_ERROR, Error: "provider stream failed"}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte("data: after-error-must-not-write\n\n")}
close(frames)
srv, fake := chatSidebandServer(frames)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"stream":true,
"messages":[{"role":"user","content":"hello"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status already committed before the error must stay 200, got %d", w.Code)
}
if got := w.Body.String(); got != "data: before-error\n\n" {
t.Fatalf("provider error must truncate after the ordered prefix:\n got: %q\nwant: %q", got, "data: before-error\n\n")
}
if calls := fake.cancelCallsSnapshot(); len(calls) != 0 {
t.Fatalf("provider-terminal error must not trigger CancelRun, got %+v", calls)
}
}
// TestChatCompletionsPassthroughSlowClientByteIdentity verifies a slow caller
// applies backpressure without corrupting opaque relay data: every body frame
// reaches the caller exactly once, in order, and a slow-but-alive client never
// triggers CancelRun.
func TestChatCompletionsPassthroughSlowClientByteIdentity(t *testing.T) {
const chunkCount = 40
var want strings.Builder
frames := make(chan *iop.ProviderTunnelFrame, chunkCount+2)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START,
StatusCode: 200,
Headers: map[string]string{"Content-Type": "text/event-stream"},
}
for i := 0; i < chunkCount; i++ {
chunk := fmt.Sprintf("data: chunk-%02d\n\n", i)
want.WriteString(chunk)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
Sequence: int64(i + 1),
Body: []byte(chunk),
}
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
srv, fake := chatSidebandServer(frames)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"stream":true,
"messages":[{"role":"user","content":"hello"}]
}`))
w := newFlakyResponseWriter(-1)
w.writeDelay = 2 * time.Millisecond
srv.handleChatCompletions(w, req)
status, body, _ := w.snapshot()
if status != http.StatusOK {
t.Fatalf("status: got %d", status)
}
if body != want.String() {
t.Fatalf("slow client stream not byte-identical/ordered:\n got: %q\nwant: %q", body, want.String())
}
if calls := fake.cancelCallsSnapshot(); len(calls) != 0 {
t.Fatalf("slow but alive client must not trigger CancelRun, got %+v", calls)
}
}
// responsesProviderTunnelServer builds a server whose catalog routes
// "pool-model" through the OpenAI-compatible provider tunnel, wired to the given
// provider frames and served target.
func responsesProviderTunnelServer(frames chan *iop.ProviderTunnelFrame, servedTarget string) (*Server, *providerFakeRunService) {
fake := &providerFakeRunService{
poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
tunnelFrames: frames,
tunnelServedTarget: servedTarget,
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
})
return srv, fake
}
// TestResponsesProviderTunnelAllowsUnknownFields verifies that Codex/Responses
// unknown fields (tools, parallel_tool_calls, store) are accepted on the
// provider passthrough route and preserved in the forwarded tunnel body.
func TestResponsesProviderTunnelAllowsUnknownFields(t *testing.T) {
srv, fake := responsesProviderTunnelServer(staticProviderTunnelFrames(`{"ok":true}`), "served-model")
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"pool-model",
"input":"hi",
"tools":[{"type":"web_search"}],
"parallel_tool_calls":true,
"store":false
}`))
w := httptest.NewRecorder()
srv.handleResponses(w, req)
if w.Code != http.StatusOK {
t.Fatalf("provider passthrough must accept unknown fields: 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 body=%s", err, bodies[0])
}
if _, ok := providerReq["tools"]; !ok {
t.Fatalf("tools must be preserved on passthrough: %+v", providerReq)
}
if providerReq["parallel_tool_calls"] != true {
t.Fatalf("parallel_tool_calls must be preserved: %+v", providerReq)
}
if providerReq["store"] != false {
t.Fatalf("store must be preserved: %+v", providerReq)
}
}
// TestResponsesProviderTunnelSubmitsResponsesPath verifies the passthrough
// dispatches a provider tunnel POST to /v1/responses and never calls SubmitRun.
func TestResponsesProviderTunnelSubmitsResponsesPath(t *testing.T) {
srv, fake := responsesProviderTunnelServer(staticProviderTunnelFrames(`{"ok":true}`), "served-model")
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"pool-model","input":"hi"}`))
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("responses passthrough must not call SubmitRun, got %d", len(fake.reqsSnapshot()))
}
reqs := fake.tunnelReqsSnapshot()
if len(reqs) != 1 {
t.Fatalf("expected 1 tunnel dispatch, got %d", len(reqs))
}
if reqs[0].Path != "/v1/responses" {
t.Fatalf("tunnel path: got %q want /v1/responses", reqs[0].Path)
}
if reqs[0].Method != http.MethodPost {
t.Fatalf("tunnel method: got %q want POST", reqs[0].Method)
}
}
// TestResponsesProviderTunnelRewritesOnlyModel verifies the caller model alias
// is rewritten to the provider-served model while max_output_tokens, tools, and
// arbitrary fields are preserved verbatim.
func TestResponsesProviderTunnelRewritesOnlyModel(t *testing.T) {
srv, fake := responsesProviderTunnelServer(staticProviderTunnelFrames(`{"ok":true}`), "served-model")
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"pool-model",
"input":"hi",
"max_output_tokens":123,
"tools":[{"type":"web_search"}],
"custom_field":"keep-me"
}`))
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 body=%s", err, bodies[0])
}
if providerReq["model"] != "served-model" {
t.Fatalf("model must be rewritten to served target: %+v", providerReq["model"])
}
if providerReq["max_output_tokens"].(float64) != 123 {
t.Fatalf("max_output_tokens must be preserved: %+v", providerReq)
}
if _, ok := providerReq["tools"]; !ok {
t.Fatalf("tools must be preserved: %+v", providerReq)
}
if providerReq["custom_field"] != "keep-me" {
t.Fatalf("arbitrary fields must be preserved: %+v", providerReq)
}
}
// TestResponsesProviderTunnelStreaming verifies a stream:true Responses request
// sets Stream=true on the tunnel dispatch and relays raw SSE bytes verbatim.
func TestResponsesProviderTunnelStreaming(t *testing.T) {
frames := make(chan *iop.ProviderTunnelFrame, 4)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START,
StatusCode: 200,
Headers: map[string]string{"Content-Type": "text/event-stream"},
}
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
Body: []byte("data: {\"type\":\"response.output_text.delta\",\"delta\":\"hi\"}\n\n"),
}
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
Body: []byte("data: [DONE]\n\n"),
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
srv, fake := responsesProviderTunnelServer(frames, "served-model")
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"pool-model","input":"hi","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.Fatalf("tunnel request Stream must be true for a stream:true responses request")
}
body := w.Body.String()
if !strings.Contains(body, "response.output_text.delta") || !strings.Contains(body, "[DONE]") {
t.Fatalf("SSE body must be relayed verbatim, got %q", body)
}
}
func TestChatCompletionsProviderPoolTunnelRelaysProviderErrorBody(t *testing.T) {
providerError := `{"error":{"message":"unknown field custom_provider_options","type":"invalid_request_error","param":"custom_provider_options"}}`
frames := make(chan *iop.ProviderTunnelFrame, 3)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START,
StatusCode: http.StatusUnprocessableEntity,
Headers: map[string]string{"Content-Type": "application/json"},
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(providerError)}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
fake := &providerFakeRunService{
tunnelFrames: frames,
tunnelServedTarget: "served-model",
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "pool-model", Providers: map[string]string{"prov-vllm": "served-model"}},
})
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}],
"custom_provider_options":{"reject":true}
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusUnprocessableEntity {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if w.Body.String() != providerError {
t.Fatalf("provider error body not relayed byte-identically:\n got: %q\nwant: %q", w.Body.String(), providerError)
}
if got := w.Header().Get("Content-Type"); !strings.Contains(got, "application/json") {
t.Fatalf("Content-Type header not relayed: %q", got)
}
if len(fake.reqsSnapshot()) != 0 {
t.Fatalf("provider error must not fall back to normalized SubmitRun, got %d calls", len(fake.reqsSnapshot()))
}
}