- agent-readable-repository-refactor 완료: 기존 테스트 파일 아카이브 이동 - 새 테스트 파일 추가 (edge, node, client, config, readability) - readability_audit 스크립트 및 baseline 추가 - roadmap/SDD 문서 갱신 - agent-client/pi/extensions/openai-sampling-parameters 추가
850 lines
30 KiB
Go
850 lines
30 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"
|
|
)
|
|
|
|
// TestResponsesProviderPoolTunnelSelectionUsesPassthrough verifies that when a
|
|
// provider-pool catalog match is resolved and the selected provider is an
|
|
// OpenAI-compatible type (tunnel executionPath), the Responses handler
|
|
// dispatches via tunnel passthrough with the raw body preserved (model rewrite
|
|
// only). This mirrors TestChatCompletionsProviderPoolTunnelSelectionUsesPassthrough
|
|
// for /v1/responses.
|
|
func TestResponsesProviderPoolTunnelSelectionUsesPassthrough(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{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
|
|
tunnelFrames: frames,
|
|
}
|
|
catalog := []config.ModelCatalogEntry{
|
|
{ID: "vllm-resp-model", Providers: map[string]string{"prov-vllm": "Qwen3-35B"}},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"vllm-resp-model",
|
|
"input":"hello world"
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
// Tunnel passthrough must use SubmitProviderPool tunnel path.
|
|
tunnelReqs := fake.tunnelReqsSnapshot()
|
|
reqs := fake.reqsSnapshot()
|
|
if len(tunnelReqs) != 1 {
|
|
t.Fatalf("expected 1 tunnel dispatch for vLLM path, got %d", len(tunnelReqs))
|
|
}
|
|
if len(reqs) != 0 {
|
|
t.Fatalf("expected 0 normalized SubmitRun for vLLM tunnel path, got %d", len(reqs))
|
|
}
|
|
if !tunnelReqs[0].ProviderPool {
|
|
t.Error("tunnel dispatch must have ProviderPool=true")
|
|
}
|
|
if tunnelReqs[0].Path != "/v1/responses" {
|
|
t.Fatalf("tunnel path: got %q want /v1/responses", tunnelReqs[0].Path)
|
|
}
|
|
}
|
|
|
|
// TestResponsesProviderPoolOllamaSelectionUsesNormalizedRun verifies that when a
|
|
// provider-pool catalog match is resolved and the selected provider is an
|
|
// Ollama type (normalized executionPath), the Responses handler dispatches via
|
|
// normalized RunEvent path with the decoded prompt/input, not via tunnel. This
|
|
// mirrors TestChatCompletionsProviderPoolOllamaSelectionUsesNormalizedRun for
|
|
// /v1/responses.
|
|
func TestResponsesProviderPoolOllamaSelectionUsesNormalizedRun(t *testing.T) {
|
|
fake := &providerFakeRunService{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathNormalized),
|
|
}
|
|
catalog := []config.ModelCatalogEntry{
|
|
{ID: "ollama-resp-model", Providers: map[string]string{"prov-ollama": "qwen3.6:35b"}},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"ollama-resp-model",
|
|
"input":"hello world"
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
// Normalized Ollama path must use SubmitProviderPool normalized path.
|
|
reqs := fake.reqsSnapshot()
|
|
tunnelReqs := fake.tunnelReqsSnapshot()
|
|
if len(reqs) != 1 {
|
|
t.Fatalf("expected 1 normalized SubmitRun for Ollama path, got %d", len(reqs))
|
|
}
|
|
if len(tunnelReqs) != 0 {
|
|
t.Fatalf("expected 0 tunnel dispatch for Ollama normalized path, got %d", len(tunnelReqs))
|
|
}
|
|
if !reqs[0].ProviderPool {
|
|
t.Error("normalized dispatch must have ProviderPool=true")
|
|
}
|
|
if reqs[0].ModelGroupKey != "ollama-resp-model" {
|
|
t.Fatalf("expected model group key %q, got %q", "ollama-resp-model", reqs[0].ModelGroupKey)
|
|
}
|
|
}
|
|
|
|
// TestResponsesMixedProviderPoolTunnelSelection verifies S05: a mixed
|
|
// provider-pool model group that selects an OpenAI-compatible (vLLM) provider
|
|
// dispatches via tunnel passthrough for /v1/responses.
|
|
func TestResponsesMixedProviderPoolTunnelSelection(t *testing.T) {
|
|
tunnelFrames := make(chan *iop.ProviderTunnelFrame, 3)
|
|
tunnelFrames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: 200}
|
|
tunnelFrames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(`{"id":"resp-mixed","object":"response"}`)}
|
|
tunnelFrames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
|
|
close(tunnelFrames)
|
|
|
|
fake := &providerFakeRunService{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
|
|
tunnelFrames: tunnelFrames,
|
|
}
|
|
catalog := []config.ModelCatalogEntry{
|
|
{
|
|
ID: "mixed-resp-model",
|
|
Providers: map[string]string{
|
|
"prov-vllm": "served-vllm",
|
|
"prov-ollama": "served-ollama",
|
|
},
|
|
},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"mixed-resp-model",
|
|
"input":"hello mixed"
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(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 1 tunnel dispatch, got %d", len(tunnelReqs))
|
|
}
|
|
if !tunnelReqs[0].ProviderPool {
|
|
t.Error("tunnel request must have ProviderPool=true")
|
|
}
|
|
if tunnelReqs[0].Path != "/v1/responses" {
|
|
t.Fatalf("tunnel path: got %q want /v1/responses", tunnelReqs[0].Path)
|
|
}
|
|
|
|
runReqs := fake.reqsSnapshot()
|
|
if len(runReqs) != 0 {
|
|
t.Errorf("expected 0 normalized SubmitRun calls, got %d", len(runReqs))
|
|
}
|
|
}
|
|
|
|
// TestResponsesMixedProviderPoolOllamaSelection verifies S05: when the mixed
|
|
// model group selects an Ollama provider, the request is dispatched over the
|
|
// normalized path (SubmitRun), and no tunnel dispatch is issued for /v1/responses.
|
|
func TestResponsesMixedProviderPoolOllamaSelection(t *testing.T) {
|
|
events := bufferedRunEvents(
|
|
&iop.RunEvent{Type: "delta", Delta: "ollama-resp"},
|
|
&iop.RunEvent{Type: "complete"},
|
|
)
|
|
|
|
fake := &providerFakeRunService{
|
|
fakeRunService: fakeRunService{
|
|
events: events,
|
|
},
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathNormalized),
|
|
}
|
|
catalog := []config.ModelCatalogEntry{
|
|
{
|
|
ID: "mixed-resp-model",
|
|
Providers: map[string]string{
|
|
"prov-vllm": "served-vllm",
|
|
"prov-ollama": "served-ollama",
|
|
},
|
|
},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"mixed-resp-model",
|
|
"input":"hello mixed"
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleResponses(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) != 0 {
|
|
t.Fatalf("expected 0 tunnel dispatches for Ollama selection, got %d", len(tunnelReqs))
|
|
}
|
|
|
|
runReqs := fake.reqsSnapshot()
|
|
if len(runReqs) != 1 {
|
|
t.Fatalf("expected 1 normalized SubmitRun, got %d", len(runReqs))
|
|
}
|
|
if runReqs[0].ModelGroupKey != "mixed-resp-model" {
|
|
t.Errorf("normalized ModelGroupKey: got %q, want mixed-resp-model", runReqs[0].ModelGroupKey)
|
|
}
|
|
if !runReqs[0].ProviderPool {
|
|
t.Error("normalized run must have ProviderPool=true")
|
|
}
|
|
}
|
|
|
|
func TestChatCompletionsProviderPoolOllamaSelectionUsesNormalizedRun(t *testing.T) {
|
|
runEvents := make(chan *iop.RunEvent, 2)
|
|
runEvents <- &iop.RunEvent{Type: "complete"}
|
|
close(runEvents)
|
|
|
|
fake := &providerFakeRunService{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathNormalized),
|
|
poolRunFrames: runEvents,
|
|
}
|
|
catalog := []config.ModelCatalogEntry{
|
|
{ID: "ollama-model", Providers: map[string]string{"prov-ollama": "qwen3.6:35b"}},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
|
"model":"ollama-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())
|
|
}
|
|
|
|
// Ollama-selected provider-pool should use normalized path (SubmitRun),
|
|
// NOT tunnel passthrough.
|
|
tunnelReqs := fake.tunnelReqsSnapshot()
|
|
reqs := fake.reqsSnapshot()
|
|
if len(tunnelReqs) != 0 {
|
|
t.Fatalf("expected 0 tunnel dispatches for Ollama-normalized path, got %d", len(tunnelReqs))
|
|
}
|
|
if len(reqs) != 1 {
|
|
t.Fatalf("expected exactly 1 normalized SubmitRun for Ollama path, got %d", len(reqs))
|
|
}
|
|
}
|
|
|
|
// TestChatCompletionsProviderPoolTunnelSelectionUsesPassthrough verifies that
|
|
// when a provider-pool catalog match is resolved and the selected provider is
|
|
// an OpenAI-compatible type (tunnel executionPath), the Chat handler dispatches
|
|
// via tunnel passthrough.
|
|
func TestChatCompletionsProviderPoolTunnelSelectionUsesPassthrough(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{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
|
|
tunnelFrames: frames,
|
|
}
|
|
catalog := []config.ModelCatalogEntry{
|
|
{ID: "vllm-model", Providers: map[string]string{"prov-vllm": "Qwen3-35B"}},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
|
"model":"vllm-model",
|
|
"messages":[{"role":"user","content":"hello"}]
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
srv.handleChatCompletions(w, req)
|
|
|
|
// OpenAI-compatible provider-pool should use tunnel passthrough.
|
|
tunnelReqs := fake.tunnelReqsSnapshot()
|
|
reqs := fake.reqsSnapshot()
|
|
if len(tunnelReqs) != 1 {
|
|
t.Fatalf("expected 1 tunnel dispatch for vLLM path, got %d", len(tunnelReqs))
|
|
}
|
|
if !tunnelReqs[0].ProviderPool {
|
|
t.Error("tunnel dispatch must have ProviderPool=true")
|
|
}
|
|
if len(reqs) != 0 {
|
|
t.Fatalf("expected 0 normalized SubmitRun for vLLM tunnel path, got %d", len(reqs))
|
|
}
|
|
}
|
|
|
|
// TestChatCompletionsMixedProviderPoolTunnelSelection verifies S05: a mixed
|
|
// model group containing both OpenAI-compatible and Ollama providers dispatches
|
|
// the selected tunnel-capable provider over the provider tunnel (passthrough)
|
|
// path. Only one tunnel request is sent, no normalized SubmitRun is called.
|
|
func TestChatCompletionsMixedProviderPoolTunnelSelection(t *testing.T) {
|
|
// Tunnel frames for the tunnel path.
|
|
tunnelFrames := make(chan *iop.ProviderTunnelFrame, 3)
|
|
tunnelFrames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: 200}
|
|
tunnelFrames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(`{"id":"cmpl-mixed","choices":[{"index":0,"message":{"role":"assistant","content":"tunnel"}}],"finish_reason":"stop"}`)}
|
|
tunnelFrames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
|
|
close(tunnelFrames)
|
|
|
|
// Fake service selects tunnel path (simulating vLLM provider selection).
|
|
fake := &providerFakeRunService{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
|
|
tunnelFrames: tunnelFrames,
|
|
}
|
|
|
|
// Mixed catalog: OpenAI-compatible (vLLM) + Ollama in same group.
|
|
catalog := []config.ModelCatalogEntry{
|
|
{
|
|
ID: "mixed-model",
|
|
Providers: map[string]string{
|
|
"prov-vllm": "served-vllm",
|
|
"prov-ollama": "served-ollama",
|
|
},
|
|
},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
|
"model":"mixed-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())
|
|
}
|
|
|
|
// Tunnel path: exactly 1 tunnel dispatch, 0 normalized SubmitRun.
|
|
tunnelReqs := fake.tunnelReqsSnapshot()
|
|
if len(tunnelReqs) != 1 {
|
|
t.Fatalf("expected 1 tunnel dispatch, got %d", len(tunnelReqs))
|
|
}
|
|
if !tunnelReqs[0].ProviderPool {
|
|
t.Error("tunnel request must have ProviderPool=true")
|
|
}
|
|
if tunnelReqs[0].ModelGroupKey != "mixed-model" {
|
|
t.Errorf("tunnel ModelGroupKey: got %q, want mixed-model", tunnelReqs[0].ModelGroupKey)
|
|
}
|
|
|
|
runReqs := fake.reqsSnapshot()
|
|
if len(runReqs) != 0 {
|
|
t.Errorf("expected 0 normalized SubmitRun calls, got %d", len(runReqs))
|
|
}
|
|
}
|
|
|
|
// TestChatCompletionsMixedProviderPoolOllamaSelection verifies S05: when the
|
|
// mixed model group selects an Ollama provider, the request is dispatched over
|
|
// the normalized path (SubmitRun), and no tunnel dispatch is issued.
|
|
func TestChatCompletionsMixedProviderPoolOllamaSelection(t *testing.T) {
|
|
// Normalized path events.
|
|
events := bufferedRunEvents(
|
|
&iop.RunEvent{Type: "delta", Delta: "ollama"},
|
|
&iop.RunEvent{Type: "complete"},
|
|
)
|
|
|
|
// Fake service selects normalized path (simulating Ollama provider selection).
|
|
fake := &providerFakeRunService{
|
|
fakeRunService: fakeRunService{
|
|
events: events,
|
|
},
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathNormalized),
|
|
}
|
|
|
|
// Same mixed catalog: vLLM + Ollama.
|
|
catalog := []config.ModelCatalogEntry{
|
|
{
|
|
ID: "mixed-model",
|
|
Providers: map[string]string{
|
|
"prov-vllm": "served-vllm",
|
|
"prov-ollama": "served-ollama",
|
|
},
|
|
},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
|
"model":"mixed-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())
|
|
}
|
|
|
|
// Normalized path: 0 tunnel dispatches, 1 normalized SubmitRun.
|
|
tunnelReqs := fake.tunnelReqsSnapshot()
|
|
if len(tunnelReqs) != 0 {
|
|
t.Fatalf("expected 0 tunnel dispatches for Ollama selection, got %d", len(tunnelReqs))
|
|
}
|
|
|
|
runReqs := fake.reqsSnapshot()
|
|
if len(runReqs) != 1 {
|
|
t.Fatalf("expected 1 normalized SubmitRun, got %d", len(runReqs))
|
|
}
|
|
// Adapter/Target are rewritten by the service's admitted candidate; the
|
|
// fake here records the pre-admission request so the handler-level path
|
|
// selection (tunnel vs normalized) is the assertion surface.
|
|
if runReqs[0].ModelGroupKey != "mixed-model" {
|
|
t.Errorf("normalized ModelGroupKey: got %q, want mixed-model", runReqs[0].ModelGroupKey)
|
|
}
|
|
if !runReqs[0].ProviderPool {
|
|
t.Error("normalized run must have ProviderPool=true")
|
|
}
|
|
}
|
|
|
|
// TestChatCompletionsOllamaOnlyProviderPoolUsesNormalizedPath verifies S05:
|
|
// a model group containing only Ollama providers always dispatches over the
|
|
// normalized path. No tunnel request is ever issued.
|
|
func TestChatCompletionsOllamaOnlyProviderPoolUsesNormalizedPath(t *testing.T) {
|
|
events := bufferedRunEvents(
|
|
&iop.RunEvent{Type: "delta", Delta: "ollama-only"},
|
|
&iop.RunEvent{Type: "complete"},
|
|
)
|
|
|
|
// Fake service selects normalized path.
|
|
fake := &providerFakeRunService{
|
|
fakeRunService: fakeRunService{
|
|
events: events,
|
|
},
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathNormalized),
|
|
}
|
|
|
|
// Ollama-only catalog.
|
|
catalog := []config.ModelCatalogEntry{
|
|
{
|
|
ID: "ollama-only-model",
|
|
Providers: map[string]string{
|
|
"prov-ollama": "qwen3.6:35b",
|
|
},
|
|
},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
|
"model":"ollama-only-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())
|
|
}
|
|
|
|
// Must not dispatch tunnel.
|
|
tunnelReqs := fake.tunnelReqsSnapshot()
|
|
if len(tunnelReqs) != 0 {
|
|
t.Fatalf("ollama-only must not dispatch tunnel, got %d", len(tunnelReqs))
|
|
}
|
|
|
|
// Must dispatch exactly one normalized run.
|
|
runReqs := fake.reqsSnapshot()
|
|
if len(runReqs) != 1 {
|
|
t.Fatalf("expected 1 normalized SubmitRun, got %d", len(runReqs))
|
|
}
|
|
if runReqs[0].ModelGroupKey != "ollama-only-model" {
|
|
t.Errorf("ollama-only ModelGroupKey: got %q, want ollama-only-model", runReqs[0].ModelGroupKey)
|
|
}
|
|
if !runReqs[0].ProviderPool {
|
|
t.Error("ollama-only normalized run must have ProviderPool=true")
|
|
}
|
|
}
|
|
|
|
// TestChatCompletionsProviderPoolTunnelPreservesUnknownFields verifies that
|
|
// when the provider-pool dispatch selects the tunnel path, the caller's raw
|
|
// body is forwarded with model rewrite only: unknown top-level fields
|
|
// (store, codex_trace, custom_provider_options, nested unknown object) and
|
|
// Codex/provider-specific fields survive into the tunnel request body. No
|
|
// IOP sideband key is injected into the pure response payload (SURFACE_FIELD).
|
|
func TestChatCompletionsProviderPoolTunnelPreservesUnknownFields(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,
|
|
tunnelServedTarget: "served-model",
|
|
}
|
|
|
|
catalog := []config.ModelCatalogEntry{
|
|
{ID: "pool-model", Providers: map[string]string{"prov-vllm": "served-model"}},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
reqBody := `{
|
|
"model":"pool-model",
|
|
"messages":[{"role":"user","content":"hello"}],
|
|
"store":true,
|
|
"parallel_tool_calls":true,
|
|
"codex_trace":{"enabled":true},
|
|
"custom_provider_options":{"timeout_ms":5000},
|
|
"nested":{"unknown":{"deep":{"value":42}}},
|
|
"thinking_token_budget":1024
|
|
}`
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(reqBody))
|
|
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 1 tunnel dispatch, got %d", len(tunnelReqs))
|
|
}
|
|
|
|
// Parse the tunnel body as a JSON map and verify model rewrite + unknown preservation.
|
|
tunnelBodies := fake.tunnelBodiesSnapshot()
|
|
if len(tunnelBodies) != 1 {
|
|
t.Fatalf("expected 1 tunnel body, got %d", len(tunnelBodies))
|
|
}
|
|
var bodyMap map[string]json.RawMessage
|
|
if err := json.Unmarshal(tunnelBodies[0], &bodyMap); err != nil {
|
|
t.Fatalf("decode tunnel body: %v body=%s", err, string(tunnelBodies[0]))
|
|
}
|
|
|
|
// Model must be rewritten to the served target.
|
|
var bodyModel string
|
|
if err := json.Unmarshal(bodyMap["model"], &bodyModel); err != nil {
|
|
t.Fatalf("decode model field: %v", err)
|
|
}
|
|
if bodyModel != "served-model" {
|
|
t.Errorf("model rewrite: got %q, want served-model", bodyModel)
|
|
}
|
|
|
|
// Known fields must survive.
|
|
if _, ok := bodyMap["messages"]; !ok {
|
|
t.Error("messages must be preserved in tunnel body")
|
|
}
|
|
if _, ok := bodyMap["store"]; !ok {
|
|
t.Error("store field must be preserved in tunnel body")
|
|
}
|
|
if _, ok := bodyMap["parallel_tool_calls"]; !ok {
|
|
t.Error("parallel_tool_calls field must be preserved in tunnel body")
|
|
}
|
|
if _, ok := bodyMap["codex_trace"]; !ok {
|
|
t.Error("codex_trace field must be preserved in tunnel body")
|
|
}
|
|
if _, ok := bodyMap["custom_provider_options"]; !ok {
|
|
t.Error("custom_provider_options field must be preserved in tunnel body")
|
|
}
|
|
if _, ok := bodyMap["nested"]; !ok {
|
|
t.Error("nested unknown object must be preserved in tunnel body")
|
|
}
|
|
if _, ok := bodyMap["thinking_token_budget"]; !ok {
|
|
t.Error("thinking_token_budget must be preserved in tunnel body")
|
|
}
|
|
|
|
// No IOP-internal metadata keys should leak into the pure passthrough body.
|
|
for _, key := range []string{"estimated_input_tokens", "context_class"} {
|
|
if _, ok := bodyMap[key]; ok {
|
|
t.Errorf("IOP internal key %q must not be in tunnel body", key)
|
|
}
|
|
}
|
|
|
|
if len(fake.reqsSnapshot()) != 0 {
|
|
t.Errorf("normalized SubmitRun must not be called for tunnel path, got %d", len(fake.reqsSnapshot()))
|
|
}
|
|
}
|
|
|
|
// TestChatCompletionsProviderPoolNormalizedIgnoresUnknownFields verifies that
|
|
// when the provider-pool dispatch selects the normalized path, the lenient
|
|
// ingress accepts unknown/provider-specific fields without rejecting the
|
|
// request, but the normalized RunRequest.Input only reflects standard supported
|
|
// fields. Unknown fields must never be invented or propagated into the adapter
|
|
// native request (SURFACE_FIELD).
|
|
func TestChatCompletionsProviderPoolNormalizedIgnoresUnknownFields(t *testing.T) {
|
|
completeEvent := &iop.RunEvent{
|
|
Type: "delta", Delta: "ok",
|
|
}
|
|
completeEvent2 := &iop.RunEvent{Type: "complete"}
|
|
|
|
fake := &providerFakeRunService{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathNormalized),
|
|
poolRunFrames: bufferedRunEvents(completeEvent, completeEvent2),
|
|
}
|
|
|
|
catalog := []config.ModelCatalogEntry{
|
|
{ID: "ollama-pool-model", Providers: map[string]string{"prov-ollama": "qwen3.6:35b"}},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
reqBody := `{
|
|
"model":"ollama-pool-model",
|
|
"messages":[{"role":"user","content":"hello"}],
|
|
"temperature":0.7,
|
|
"store":true,
|
|
"codex_trace":{"enabled":true},
|
|
"custom_provider_options":{"timeout_ms":5000},
|
|
"nested_unknown":{"deep":true},
|
|
"reasoning_effort":"high"
|
|
}`
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(reqBody))
|
|
w := httptest.NewRecorder()
|
|
srv.handleChatCompletions(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// The lenient ingress must NOT reject the request due to unknown fields.
|
|
runReqs := fake.reqsSnapshot()
|
|
if len(runReqs) != 1 {
|
|
t.Fatalf("expected 1 normalized SubmitRun, got %d", len(runReqs))
|
|
}
|
|
|
|
runReq := runReqs[0]
|
|
if !runReq.ProviderPool {
|
|
t.Error("normalized pool run must have ProviderPool=true")
|
|
}
|
|
|
|
// Standard supported fields must be present in Input.
|
|
input := runReq.Input
|
|
if _, ok := input["prompt"]; !ok {
|
|
t.Error("prompt must be in normalized Input")
|
|
}
|
|
if _, ok := input["messages"]; !ok {
|
|
t.Error("messages must be in normalized Input")
|
|
}
|
|
options, ok := input["options"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("options must be in normalized Input, got %T", input["options"])
|
|
}
|
|
if options["temperature"] != 0.7 {
|
|
t.Errorf("temperature must be forwarded to normalized options, got %+v", options["temperature"])
|
|
}
|
|
|
|
// Unknown/provider-specific fields must NOT be in the normalized Input.
|
|
for _, key := range []string{"store", "codex_trace", "custom_provider_options", "nested_unknown"} {
|
|
if _, ok := input[key]; ok {
|
|
t.Errorf("unknown field %q must not be in normalized Input", key)
|
|
}
|
|
if _, ok := options[key]; ok {
|
|
t.Errorf("unknown field %q must not be in normalized Input options", key)
|
|
}
|
|
}
|
|
|
|
// The tunnel path must not be taken.
|
|
tunnelReqs := fake.tunnelReqsSnapshot()
|
|
if len(tunnelReqs) != 0 {
|
|
t.Errorf("tunnel must not be dispatched for normalized path, got %d", len(tunnelReqs))
|
|
}
|
|
}
|
|
|
|
// TestResponsesProviderPoolTunnelUnknownFieldsUsesPoolSelection asserts that
|
|
// when the selected provider is tunnel-passthrough (OpenAI-compatible),
|
|
// unknown fields (tools, parallel_tool_calls, store, custom field) are
|
|
// preserved verbatim in the tunnel body, and that the Run sent to
|
|
// SubmitProviderPool carries a non-empty ModelGroupKey.
|
|
func TestResponsesProviderPoolTunnelUnknownFieldsUsesPoolSelection(t *testing.T) {
|
|
fake := &providerFakeRunService{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
|
|
}
|
|
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog([]config.ModelCatalogEntry{
|
|
{ID: "pool-model", Providers: map[string]string{"prov-tunnel": "served-tunnel"}},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"pool-model",
|
|
"input":"hello world",
|
|
"tools":[{"type":"function","function":{"name":"lookup"}}],
|
|
"parallel_tool_calls":true,
|
|
"store":false,
|
|
"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())
|
|
}
|
|
|
|
// SubmitProviderPool must have been called with a populated Run.
|
|
lastRun := fake.poolLastRunSnapshot()
|
|
if lastRun.ModelGroupKey == "" {
|
|
t.Error("SubmitProviderPool was called with empty Run.ModelGroupKey")
|
|
}
|
|
|
|
// The tunnel body must preserve unknown fields verbatim.
|
|
if len(fake.tunnelBodiesSnapshot()) != 1 {
|
|
t.Fatalf("expected exactly one tunnel body, got %d", len(fake.tunnelBodiesSnapshot()))
|
|
}
|
|
body := fake.tunnelBodiesSnapshot()[0]
|
|
if !strings.Contains(string(body), "\"tools\"") {
|
|
t.Error("tunnel body must preserve tools field")
|
|
}
|
|
if !strings.Contains(string(body), "\"parallel_tool_calls\"") {
|
|
t.Error("tunnel body must preserve parallel_tool_calls field")
|
|
}
|
|
if !strings.Contains(string(body), "\"store\"") {
|
|
t.Error("tunnel body must preserve store field")
|
|
}
|
|
if !strings.Contains(string(body), "\"custom_field\"") {
|
|
t.Error("tunnel body must preserve custom_field")
|
|
}
|
|
}
|
|
|
|
// TestResponsesProviderPoolOllamaSelectionRejectsUnknownFieldsWithoutTunnel
|
|
// asserts that when the selected provider is normalized (Ollama/CLI), an
|
|
// unknown-field request returns HTTP 400 and no tunnel or run dispatch occurs.
|
|
func TestResponsesProviderPoolOllamaSelectionRejectsUnknownFieldsWithoutTunnel(t *testing.T) {
|
|
fake := &providerFakeRunService{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathNormalized),
|
|
}
|
|
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog([]config.ModelCatalogEntry{
|
|
{ID: "ollama-model", Providers: map[string]string{"prov-ollama": "served-ollama"}},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"ollama-model",
|
|
"input":"hello",
|
|
"unknown_field":"reject-me"
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("status: got %d body=%s, want 400", w.Code, w.Body.String())
|
|
}
|
|
|
|
// No tunnel or run dispatch should have occurred.
|
|
if len(fake.tunnelReqsSnapshot()) != 0 {
|
|
t.Error("normalized path must not dispatch tunnel for unknown fields")
|
|
}
|
|
if len(fake.reqsSnapshot()) != 0 {
|
|
t.Error("normalized path must not dispatch run for unknown fields")
|
|
}
|
|
|
|
// Verify pool dispatch was made.
|
|
if fake.poolSubmitCountSnapshot() != 1 {
|
|
t.Fatalf("pool submit count: got %d, want 1", fake.poolSubmitCountSnapshot())
|
|
}
|
|
|
|
// Verify PrepareRun was called on the normalized path.
|
|
prepareRunCalled := fake.poolPrepareRunCalledSnapshot()
|
|
if !prepareRunCalled {
|
|
t.Errorf("PrepareRun must be called on normalized path (poolSubmitCount=%d, poolLastRun.ModelGroupKey=%q)", fake.poolSubmitCountSnapshot(), fake.poolLastRunSnapshot().ModelGroupKey)
|
|
}
|
|
}
|
|
|
|
// TestResponsesProviderPoolOllamaSelectionRejectsStreamingWithoutTunnel asserts
|
|
// that when the selected provider is normalized, stream:true returns HTTP 400
|
|
// and no tunnel dispatch occurs.
|
|
func TestResponsesProviderPoolOllamaSelectionRejectsStreamingWithoutTunnel(t *testing.T) {
|
|
fake := &providerFakeRunService{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathNormalized),
|
|
}
|
|
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog([]config.ModelCatalogEntry{
|
|
{ID: "ollama-model", Providers: map[string]string{"prov-ollama": "served-ollama"}},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"ollama-model",
|
|
"input":"hello",
|
|
"stream":true
|
|
}`))
|
|
w := httptest.NewRecorder()
|
|
|
|
srv.handleResponses(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("status: got %d body=%s, want 400", w.Code, w.Body.String())
|
|
}
|
|
|
|
// No tunnel dispatch for normalized path with stream:true.
|
|
if len(fake.tunnelReqsSnapshot()) != 0 {
|
|
t.Error("normalized path must not dispatch tunnel for stream:true")
|
|
}
|
|
if len(fake.reqsSnapshot()) != 0 {
|
|
t.Error("normalized path must not dispatch run for stream:true")
|
|
}
|
|
|
|
if !fake.poolPrepareRunCalledSnapshot() {
|
|
t.Error("PrepareRun must be called on normalized path")
|
|
}
|
|
}
|
|
|
|
// TestResponsesProviderPoolTunnelStreamingUsesPoolSelection asserts that when
|
|
// the selected provider is tunnel-passthrough, stream:true is relayed through
|
|
// SubmitProviderPool with the raw SSE body preserved.
|
|
func TestResponsesProviderPoolTunnelStreamingUsesPoolSelection(t *testing.T) {
|
|
fake := &providerFakeRunService{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
|
|
tunnelFrames: staticProviderTunnelFrames(`{"id":"resp-1","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hello"}]}],"stream":true}\n`),
|
|
}
|
|
|
|
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
|
|
srv.SetModelCatalog([]config.ModelCatalogEntry{
|
|
{ID: "pool-model", Providers: map[string]string{"prov-tunnel": "served-tunnel"}},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"pool-model",
|
|
"input":"hello",
|
|
"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())
|
|
}
|
|
|
|
// SubmitProviderPool must have been called.
|
|
if fake.poolSubmitCountSnapshot() != 1 {
|
|
t.Fatalf("SubmitProviderPool call count: got %d, want 1", fake.poolSubmitCountSnapshot())
|
|
}
|
|
|
|
// The tunnel body must preserve the raw SSE payload.
|
|
if len(fake.tunnelBodiesSnapshot()) != 1 {
|
|
t.Fatalf("expected one tunnel body, got %d", len(fake.tunnelBodiesSnapshot()))
|
|
}
|
|
body := fake.tunnelBodiesSnapshot()[0]
|
|
if !strings.Contains(string(body), "\"stream\":true") {
|
|
t.Error("tunnel body must preserve stream:true")
|
|
}
|
|
|
|
// PrepareRun must NOT be called on the tunnel path.
|
|
if fake.poolPrepareRunCalledSnapshot() {
|
|
t.Error("PrepareRun must NOT be called on tunnel path")
|
|
}
|
|
}
|