iop/apps/edge/internal/openai/server_test.go

7092 lines
270 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"
"go.uber.org/zap/zaptest/observer"
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
type fakeRunService struct {
req edgeservice.SubmitRunRequest
reqs []edgeservice.SubmitRunRequest
ollamaReq edgeservice.OllamaAPIRequest
ollamaResp edgeservice.OllamaAPIView
events chan *iop.RunEvent
eventRuns []chan *iop.RunEvent
runIDs []string
submitMu sync.Mutex
submitErr error
submitErrAfter int // zero means no error; >=1 means error starts from this req index (1-based)
cancelMu sync.Mutex
cancelCalls []edgeservice.CancelRunRequest
cancelErr error
// tunnelProviderURL, when set, makes SubmitProviderTunnel relay the tunnel
// request to this httptest provider fixture and stream the raw provider
// response back as ordered frames (emulating the Node relay).
tunnelProviderURL string
// tunnelServedTarget emulates provider-pool admission: BuildBody is called
// with this target when non-empty.
tunnelServedTarget string
// tunnelFrames, when set, is handed to the tunnel handle as-is instead of
// contacting tunnelProviderURL.
tunnelFrames chan *iop.ProviderTunnelFrame
tunnelErr error
tunnelReqs []edgeservice.SubmitProviderTunnelRequest
tunnelBodies [][]byte
// tunnelWaitTimeout overrides the tunnel handle wait timeout for
// timeout-path fixtures; zero keeps the 5s default.
tunnelWaitTimeout time.Duration
}
type fakeTunnelHandle struct {
dispatch edgeservice.RunDispatch
frames chan *iop.ProviderTunnelFrame
waitTimeout time.Duration
}
func (h *fakeTunnelHandle) Dispatch() edgeservice.RunDispatch { return h.dispatch }
func (h *fakeTunnelHandle) Stream() edgeservice.ProviderTunnelStream {
return edgeservice.ProviderTunnelStream{Frames: h.frames}
}
func (h *fakeTunnelHandle) Close() {}
func (h *fakeTunnelHandle) WaitTimeout() time.Duration {
if h.waitTimeout > 0 {
return h.waitTimeout
}
return 5 * time.Second
}
func (s *fakeRunService) SubmitProviderTunnel(_ context.Context, req edgeservice.SubmitProviderTunnelRequest) (edgeservice.ProviderTunnelResult, error) {
s.submitMu.Lock()
s.tunnelReqs = append(s.tunnelReqs, req)
s.submitMu.Unlock()
if s.tunnelErr != nil {
return nil, s.tunnelErr
}
target := req.Target
if s.tunnelServedTarget != "" {
target = s.tunnelServedTarget
}
body := req.Body
if req.BuildBody != nil {
built, err := req.BuildBody(target)
if err != nil {
return nil, err
}
body = built
}
s.submitMu.Lock()
s.tunnelBodies = append(s.tunnelBodies, body)
s.submitMu.Unlock()
frames := s.tunnelFrames
if frames == nil && s.tunnelProviderURL != "" {
relayed, err := relayProviderFixtureFrames(s.tunnelProviderURL, req.Method, req.Path, body)
if err != nil {
return nil, err
}
frames = relayed
}
return &fakeTunnelHandle{
dispatch: edgeservice.RunDispatch{
RunID: "run-tunnel",
NodeID: "node-1",
ModelGroupKey: req.ModelGroupKey,
Adapter: req.Adapter,
Target: target,
SessionID: req.SessionID,
TimeoutSec: 5,
},
frames: frames,
waitTimeout: s.tunnelWaitTimeout,
}, nil
}
func (s *fakeRunService) tunnelReqsSnapshot() []edgeservice.SubmitProviderTunnelRequest {
s.submitMu.Lock()
defer s.submitMu.Unlock()
return append([]edgeservice.SubmitProviderTunnelRequest(nil), s.tunnelReqs...)
}
func (s *fakeRunService) tunnelBodiesSnapshot() [][]byte {
s.submitMu.Lock()
defer s.submitMu.Unlock()
return append([][]byte(nil), s.tunnelBodies...)
}
// relayProviderFixtureFrames performs the provider HTTP request the way the
// Node relay does and converts status/header/body bytes into ordered tunnel
// frames, chunking the body at odd boundaries to exercise ordered writes.
func relayProviderFixtureFrames(providerURL, method, path string, body []byte) (chan *iop.ProviderTunnelFrame, error) {
httpReq, err := http.NewRequest(method, providerURL+path, bytes.NewReader(body))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
headers := make(map[string]string)
for k, values := range resp.Header {
if len(values) > 0 {
headers[k] = values[0]
}
}
const chunkSize = 7
frames := make(chan *iop.ProviderTunnelFrame, len(respBody)/chunkSize+3)
seq := int64(0)
frames <- &iop.ProviderTunnelFrame{
RunId: "run-tunnel",
TunnelId: "tunnel-1",
Sequence: seq,
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START,
StatusCode: int32(resp.StatusCode),
Headers: headers,
}
seq++
for off := 0; off < len(respBody); off += chunkSize {
end := off + chunkSize
if end > len(respBody) {
end = len(respBody)
}
frames <- &iop.ProviderTunnelFrame{
RunId: "run-tunnel",
TunnelId: "tunnel-1",
Sequence: seq,
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
Body: append([]byte(nil), respBody[off:end]...),
}
seq++
}
frames <- &iop.ProviderTunnelFrame{
RunId: "run-tunnel",
TunnelId: "tunnel-1",
Sequence: seq,
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END,
End: true,
}
close(frames)
return frames, nil
}
func staticProviderTunnelFrames(body string) chan *iop.ProviderTunnelFrame {
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(body)}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
return frames
}
func (s *fakeRunService) CancelRun(_ context.Context, req edgeservice.CancelRunRequest) (edgeservice.CommandResult, error) {
s.cancelMu.Lock()
s.cancelCalls = append(s.cancelCalls, req)
s.cancelMu.Unlock()
if s.cancelErr != nil {
return edgeservice.CommandResult{}, s.cancelErr
}
return edgeservice.CommandResult{NodeID: req.NodeRef, SessionID: req.SessionID}, nil
}
func (s *fakeRunService) cancelCallsSnapshot() []edgeservice.CancelRunRequest {
s.cancelMu.Lock()
defer s.cancelMu.Unlock()
return append([]edgeservice.CancelRunRequest(nil), s.cancelCalls...)
}
func (s *fakeRunService) reqsSnapshot() []edgeservice.SubmitRunRequest {
s.submitMu.Lock()
defer s.submitMu.Unlock()
return append([]edgeservice.SubmitRunRequest(nil), s.reqs...)
}
func TestRoutesRequireBearerTokenWhenConfigured(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{
BearerToken: "secret-token",
Models: []string{"model-a"},
}, &fakeRunService{}, nil)
for _, tc := range []struct {
name string
auth string
wantStatus int
}{
{name: "missing", wantStatus: http.StatusUnauthorized},
{name: "wrong", auth: "Bearer wrong", wantStatus: http.StatusUnauthorized},
{name: "valid", auth: "Bearer secret-token", wantStatus: http.StatusOK},
} {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
if tc.auth != "" {
req.Header.Set("Authorization", tc.auth)
}
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != tc.wantStatus {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
})
}
}
func TestHealthzDoesNotRequireBearerToken(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{BearerToken: "secret-token"}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
}
func (s *fakeRunService) SubmitRun(_ context.Context, req edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) {
s.submitMu.Lock()
s.req = req
s.reqs = append(s.reqs, req)
if s.submitErr != nil {
err := s.submitErr
s.submitMu.Unlock()
return nil, err
}
attempt := len(s.reqs)
if s.submitErrAfter > 0 && attempt >= s.submitErrAfter {
s.submitMu.Unlock()
return nil, fmt.Errorf("inject error after %d attempts", attempt-1)
}
events := s.events
if attempt-1 < len(s.eventRuns) {
events = s.eventRuns[attempt-1]
}
runID := "run-test"
if attempt-1 < len(s.runIDs) {
runID = s.runIDs[attempt-1]
} else if attempt > 1 {
runID = fmt.Sprintf("run-test-%d", attempt)
}
s.submitMu.Unlock()
return &edgeservice.RunHandle{
RunDispatch: edgeservice.RunDispatch{
RunID: runID,
ModelGroupKey: req.ModelGroupKey,
Adapter: req.Adapter,
Target: req.Target,
SessionID: req.SessionID,
TimeoutSec: 5,
},
RunStream: edgeservice.RunStream{
Events: events,
NodeEvents: make(chan *iop.EdgeNodeEvent),
},
}, nil
}
func (s *fakeRunService) OllamaAPI(_ context.Context, req edgeservice.OllamaAPIRequest) (edgeservice.OllamaAPIView, error) {
s.ollamaReq = req
if s.ollamaResp.StatusCode == 0 {
s.ollamaResp.StatusCode = http.StatusOK
}
return s.ollamaResp, nil
}
func bufferedRunEvents(events ...*iop.RunEvent) chan *iop.RunEvent {
ch := make(chan *iop.RunEvent, len(events))
for _, event := range events {
ch <- event
}
return ch
}
func TestChatCompletionsDispatchesConfiguredOllamaTarget(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: " world"}
fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 2, OutputTokens: 2}}
srv := NewServer(config.EdgeOpenAIConf{
Adapter: "ollama",
Target: "llama-fixed",
SessionID: "cline",
TimeoutSec: 15,
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"client-model",
"messages":[{"role":"system","content":"brief"},{"role":"user","content":"say 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 fake.req.Adapter != "ollama" || fake.req.Target != "llama-fixed" {
t.Fatalf("dispatch target mismatch: %+v", fake.req)
}
if fake.req.SessionID != "cline" || fake.req.TimeoutSec != 15 {
t.Fatalf("execution config mismatch: %+v", fake.req)
}
if fake.req.ModelGroupKey != "client-model" {
t.Fatalf("model group key: got %q, want client-model", fake.req.ModelGroupKey)
}
if !strings.Contains(fake.req.Prompt, "system: brief") || !strings.Contains(fake.req.Prompt, "user: say hello") {
t.Fatalf("prompt did not include messages: %q", fake.req.Prompt)
}
var resp chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.Choices[0].Message.Content != "hello world" {
t.Fatalf("content: got %q", resp.Choices[0].Message.Content)
}
if resp.Usage == nil || resp.Usage.TotalTokens != 4 {
t.Fatalf("usage: %+v", resp.Usage)
}
}
func TestChatCompletionsUsesRequestModelWhenNoConfiguredTarget(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"from-request",
"messages":[{"role":"user","content":"hi"}]
}`))
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 fake.req.Target != "from-request" {
t.Fatalf("target: got %q", fake.req.Target)
}
}
func TestChatCompletionsPreservesProviderFinishReason(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "truncated"}
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{"finish_reason": "length"}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"hi"}]
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.Choices[0].FinishReason != "length" {
t.Fatalf("finish_reason: got %q, want length", resp.Choices[0].FinishReason)
}
}
func TestChatCompletionsPassesStandardOptions(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"from-request",
"messages":[{"role":"user","content":"hi"}],
"max_tokens":12,
"temperature":0.1,
"top_p":0.9,
"stop":["END"],
"response_format":{"type":"json_object"},
"tools":[{"type":"function","function":{"name":"lookup"}}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
options, ok := fake.req.Input["options"].(map[string]any)
if !ok {
t.Fatalf("options not passed: %+v", fake.req.Input)
}
if options["temperature"].(float64) != 0.1 || options["top_p"].(float64) != 0.9 {
t.Fatalf("unexpected options: %+v", options)
}
if options["max_tokens"].(int) != 12 {
t.Fatalf("max_tokens not passed: %+v", options)
}
if _, ok := options["stop"]; !ok {
t.Fatalf("stop not passed: %+v", options)
}
if _, ok := options["response_format"]; !ok {
t.Fatalf("response_format not passed: %+v", options)
}
if tools, ok := fake.req.Input["tools"].([]any); !ok || len(tools) != 1 {
t.Fatalf("tools not passed: %+v", fake.req.Input["tools"])
}
if _, ok := fake.req.Input["tool_choice"]; ok {
t.Fatalf("tool_choice must be omitted when the client omits it: %+v", fake.req.Input["tool_choice"])
}
}
func TestChatCompletionsAcceptsStoreNoOp(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"from-request",
"messages":[{"role":"user","content":"hi"}],
"store":false
}`))
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 _, ok := fake.req.Input["store"]; ok {
t.Fatalf("store must be accepted as a client compatibility no-op: %+v", fake.req.Input)
}
}
func TestChatCompletionsOmitsProviderToolChoiceAuto(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"hi"}],
"tools":[{"type":"function","function":{"name":"lookup"}}],
"tool_choice":"auto",
"parallel_tool_calls":true,
"stream_options":{"include_usage":true}
}`))
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 _, ok := fake.req.Input["tool_choice"]; ok {
t.Fatalf("tool_choice auto should be omitted for provider defaults: %+v", fake.req.Input["tool_choice"])
}
if _, ok := fake.req.Input["parallel_tool_calls"]; ok {
t.Fatalf("parallel_tool_calls must not be forwarded: %+v", fake.req.Input)
}
if _, ok := fake.req.Input["stream_options"]; ok {
t.Fatalf("stream_options must not be forwarded: %+v", fake.req.Input)
}
}
func TestChatCompletionsPassesProviderForcedToolChoice(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"hi"}],
"tools":[{"type":"function","function":{"name":"lookup"}}],
"tool_choice":{"type":"function","function":{"name":"lookup"}}
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
choice, ok := fake.req.Input["tool_choice"].(map[string]any)
if !ok {
t.Fatalf("tool_choice not forwarded as object: %+v", fake.req.Input["tool_choice"])
}
if choice["type"] != "function" {
t.Fatalf("tool_choice type: %+v", choice)
}
}
func TestChatCompletionsDowngradesCLIToolChoiceAutoToNone(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"codex-cli",
"messages":[{"role":"user","content":"hi"}],
"tools":[{"type":"function","function":{"name":"lookup"}}],
"tool_choice":"auto"
}`))
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 fake.req.Input["tool_choice"] != "none" {
t.Fatalf("tool_choice: got %+v, want none", fake.req.Input["tool_choice"])
}
}
func TestChatCompletionsSynthesizesToolCallsFromClineTextBlock(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "먼저 현재 git 상태를 확인하겠습니다.\n\n<tool_call>\n<function=run_commands>\n<parameter=commands>\n[\"cd /config/workspace/iop && git status\"]\n</parameter>\n</function>\n</tool_call>"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object"}}}],
"tool_choice":"auto"
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
choice := resp.Choices[0]
if choice.FinishReason != "tool_calls" {
t.Fatalf("finish_reason: got %q, want tool_calls", choice.FinishReason)
}
if strings.Contains(choice.Message.Content, "<tool_call>") {
t.Fatalf("content still contains raw tool_call block: %q", choice.Message.Content)
}
if choice.Message.Content != "먼저 현재 git 상태를 확인하겠습니다." {
t.Fatalf("content: got %q", choice.Message.Content)
}
if len(choice.Message.ToolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(choice.Message.ToolCalls))
}
call, ok := choice.Message.ToolCalls[0].(map[string]any)
if !ok {
t.Fatalf("tool call shape: %+v", choice.Message.ToolCalls[0])
}
if call["id"] != "call_iop_run-test_1" {
t.Fatalf("tool call id: got %+v", call["id"])
}
fn, ok := call["function"].(map[string]any)
if !ok {
t.Fatalf("function shape: %+v", call["function"])
}
if fn["name"] != "run_commands" {
t.Fatalf("function name: got %+v", fn["name"])
}
var args map[string][]string
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
if len(args["commands"]) != 1 || args["commands"][0] != "cd /config/workspace/iop && git status" {
t.Fatalf("commands args: %+v", args["commands"])
}
}
func TestChatCompletionsSynthesizesToolCallsFromClineTemplateCall(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "사용자가 변경 내용을 푸시해달라고 요청하셨습니다. 먼저 현재 git 상태를 확인하여 변경된 파일과 커밋 가능한 내용을 파악하겠습니다.\n\n{{run_commands(commands=[{'command': 'git status', 'description': '현재 git 상태 확인', 'runInTerminal': True}]})}}"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"push changes"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object"}}}],
"tool_choice":"auto"
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
choice := resp.Choices[0]
if choice.FinishReason != "tool_calls" {
t.Fatalf("finish_reason: got %q, want tool_calls", choice.FinishReason)
}
if strings.Contains(choice.Message.Content, "{{run_commands") {
t.Fatalf("content still contains raw template tool call: %q", choice.Message.Content)
}
if len(choice.Message.ToolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(choice.Message.ToolCalls))
}
call, ok := choice.Message.ToolCalls[0].(map[string]any)
if !ok {
t.Fatalf("tool call shape: %+v", choice.Message.ToolCalls[0])
}
fn, ok := call["function"].(map[string]any)
if !ok {
t.Fatalf("function shape: %+v", call["function"])
}
if fn["name"] != "run_commands" {
t.Fatalf("function name: got %+v", fn["name"])
}
var args map[string][]map[string]any
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
if len(args["commands"]) != 1 || args["commands"][0]["command"] != "git" {
t.Fatalf("commands args: %+v", args["commands"])
}
commandArgs := args["commands"][0]["args"].([]any)
if len(commandArgs) != 1 || commandArgs[0] != "status" {
t.Fatalf("command args: %+v", commandArgs)
}
}
func TestChatCompletionsSynthesizesTextToolCallsForProviderRoute(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "먼저 현재 git 상태를 확인하겠습니다.\n\n<tool_call>\n<function=run_commands>\n<parameter=commands>\n[\"git status\"]\n</parameter>\n</function>\n</tool_call>"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object"}}}],
"tool_choice":"auto"
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
choice := resp.Choices[0]
if choice.FinishReason != "tool_calls" || len(choice.Message.ToolCalls) == 0 {
t.Fatalf("provider text should be synthesized: %+v", choice)
}
if strings.Contains(choice.Message.Content, "<tool_call>") {
t.Fatalf("provider raw content should not be preserved: %q", choice.Message.Content)
}
}
func TestChatCompletionsSynthesizesEditToolCallWithMarkdownAndAngleBracketPlaceholder(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "두 줄을 `18083` -> `18081`로 수정하고 검증을 진행한다.\n\n" + editToolCallFixture()}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"edit"}],
"tools":[{"type":"function","function":{"name":"edit","parameters":{"type":"object","properties":{"path":{"type":"string"},"edits":{"type":"array","items":{"type":"object","properties":{"oldText":{"type":"string"},"newText":{"type":"string"}},"required":["newText"],"additionalProperties":false}}},"required":["path","edits"],"additionalProperties":false}}}],
"tool_choice":"auto"
}`))
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, "<tool_call") || strings.Contains(body, `\u003ctool_call`) {
t.Fatalf("raw tool_call leaked in response: %s", body)
}
var resp chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
choice := resp.Choices[0]
if choice.FinishReason != "tool_calls" || len(choice.Message.ToolCalls) != 1 {
t.Fatalf("expected edit tool call, got %+v", choice)
}
call := choice.Message.ToolCalls[0].(map[string]any)
fn := call["function"].(map[string]any)
if fn["name"] != "edit" {
t.Fatalf("function name: got %+v", fn["name"])
}
args := decodeEditToolArguments(t, fn["arguments"].(string))
if args["path"] != "apps/client/test/client_bootstrap_test.dart" {
t.Fatalf("path argument: %+v", args["path"])
}
edits := args["edits"].([]any)
edit := edits[0].(map[string]any)
if _, ok := edit["path"]; ok {
t.Fatalf("edit item must not contain path: %+v", edit)
}
if !strings.Contains(edit["newText"].(string), "http://<edge-host>:18081/v1/chat/completions") {
t.Fatalf("newText lost angle-bracket placeholder: %+v", edit["newText"])
}
}
func TestChatCompletionsStreamSynthesizesEditToolCallWithMarkdownAndAngleBracketPlaceholder(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "수정합니다.\n\n" + editToolCallFixture()}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"stream":true,
"messages":[{"role":"user","content":"edit"}],
"tools":[{"type":"function","function":{"name":"edit","parameters":{"type":"object","properties":{"path":{"type":"string"},"edits":{"type":"array","items":{"type":"object","properties":{"oldText":{"type":"string"},"newText":{"type":"string"}},"required":["newText"],"additionalProperties":false}}},"required":["path","edits"],"additionalProperties":false}}}],
"tool_choice":"auto"
}`))
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, "<tool_call") || strings.Contains(body, `\u003ctool_call`) {
t.Fatalf("raw tool_call leaked in stream: %s", body)
}
if !strings.Contains(body, `"tool_calls"`) || !strings.Contains(body, `"finish_reason":"tool_calls"`) || !strings.Contains(body, "data: [DONE]") {
t.Fatalf("expected streamed edit tool call, got:\n%s", body)
}
}
func editToolCallFixture() string {
edits, _ := json.Marshal([]map[string]string{{
"oldText": "final baseUrl = 'http://127.0.0.1:18083/v1/chat/completions';",
"newText": "```bash\ncurl -fsS http://<edge-host>:18081/v1/chat/completions \\\n -H \"Content-Type: application/json\"\n```",
}})
return "<tool_call>\n" +
"<function=edit>\n" +
"<parameter=path>\napps/client/test/client_bootstrap_test.dart\n</parameter>\n" +
"<parameter=edits>\n" + string(edits) + "\n</parameter>\n" +
"</function>\n" +
"</tool_call>"
}
func decodeEditToolArguments(t *testing.T, raw string) map[string]any {
t.Helper()
var args map[string]any
if err := json.Unmarshal([]byte(raw), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
edits, ok := args["edits"].([]any)
if !ok || len(edits) != 1 {
t.Fatalf("edits argument: %+v", args["edits"])
}
if _, ok := edits[0].(map[string]any); !ok {
t.Fatalf("edit item: %+v", edits[0])
}
return args
}
func TestChatCompletionsSynthesizesProviderTextToolCallsWhenFallbackMarked(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "먼저 현재 git 상태를 확인하겠습니다.\n\n<tool_call>\n<function=run_commands>\n<parameter=commands>\n[\"cd /config/workspace/iop && git status\"]\n</parameter>\n</function>\n</tool_call>"}
fake.events <- &iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
runtimeMetadataOpenAITextToolFallback: "true",
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"]}}}],
"tool_choice":"auto"
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
choice := resp.Choices[0]
if choice.FinishReason != "tool_calls" {
t.Fatalf("finish_reason: got %q, want tool_calls", choice.FinishReason)
}
if strings.Contains(choice.Message.Content, "<tool_call>") {
t.Fatalf("content still contains raw tool_call block: %q", choice.Message.Content)
}
if len(choice.Message.ToolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(choice.Message.ToolCalls))
}
call := choice.Message.ToolCalls[0].(map[string]any)
fn := call["function"].(map[string]any)
var args map[string][]string
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
if len(args["commands"]) != 1 || args["commands"][0] != "cd /config/workspace/iop && git status" {
t.Fatalf("commands args: %+v", args["commands"])
}
}
func TestChatCompletionsSanitizesKnownSentinelToken(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "thinking <|mask_end|>"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer <|mask_end|>"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"hi"}]
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if strings.Contains(resp.Choices[0].Message.Content, "<|mask_end|>") || strings.Contains(resp.Choices[0].Message.ReasoningContent, "<|mask_end|>") {
t.Fatalf("sentinel leaked in response: %+v", resp.Choices[0].Message)
}
if resp.Choices[0].Message.Content != "answer" || resp.Choices[0].Message.ReasoningContent != "thinking" {
t.Fatalf("unexpected sanitized output: %+v", resp.Choices[0].Message)
}
}
func TestChatCompletionsSynthesizesToolCallAndDropsTrailingSentinel(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call>\n<function=run_commands>\n<parameter=commands>[\"git status\"]</parameter>\n</function>\n</tool_call><|mask_end|>"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"]}}}]
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if strings.Contains(w.Body.String(), "<|mask_end|>") {
t.Fatalf("sentinel leaked in response body: %s", w.Body.String())
}
if resp.Choices[0].FinishReason != "tool_calls" || len(resp.Choices[0].Message.ToolCalls) != 1 {
t.Fatalf("expected structured tool call, got %+v", resp.Choices[0])
}
if resp.Choices[0].Message.Content != "" {
t.Fatalf("trailing sentinel should not become content: %q", resp.Choices[0].Message.Content)
}
}
func TestChatCompletionsPassesThroughNativeToolCallsFromProviderRoute(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 1)}
fake.events <- &iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_1","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}],
"tool_choice":"auto"
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
choice := resp.Choices[0]
if choice.FinishReason != "tool_calls" {
t.Fatalf("finish_reason: got %q, want tool_calls", choice.FinishReason)
}
if len(choice.Message.ToolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(choice.Message.ToolCalls))
}
call := choice.Message.ToolCalls[0].(map[string]any)
if call["id"] != "call_1" {
t.Fatalf("tool call id: got %+v", call["id"])
}
}
func TestChatCompletionsNativeToolCallPreservesLeadingProse(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "선행 prose\n\n<tool_call><function=run_commands><parameter=commands>[\"git status\"]</parameter></function></tool_call>"}
fake.events <- &iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_1","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}],
"tool_choice":"auto"
}`))
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, "<tool_call") || strings.Contains(body, `\u003ctool_call`) {
t.Fatalf("raw tool_call leaked in response: %s", body)
}
var resp chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
choice := resp.Choices[0]
if choice.Message.Content != "선행 prose" {
t.Fatalf("content: got %q", choice.Message.Content)
}
if choice.FinishReason != "tool_calls" || len(choice.Message.ToolCalls) != 1 {
t.Fatalf("expected native tool call: %+v", choice)
}
call := choice.Message.ToolCalls[0].(map[string]any)
if call["id"] != "call_1" {
t.Fatalf("tool call id: got %+v", call["id"])
}
}
func TestChatCompletionsNormalizesNativeToolCallArgumentsForProviderRoute(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 1)}
fake.events <- &iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_1","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":\"[{\\\"command\\\":\\\"cd /config/workspace/iop && git status\\\"}]\"}"}}]`,
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"]}}}],
"tool_choice":"auto"
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
call := resp.Choices[0].Message.ToolCalls[0].(map[string]any)
fn := call["function"].(map[string]any)
var args map[string][]string
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
if len(args["commands"]) != 1 || args["commands"][0] != "cd /config/workspace/iop && git status" {
t.Fatalf("commands args: %+v", args["commands"])
}
}
func TestChatCompletionsRetriesMalformedToolCallBeforeResponse(t *testing.T) {
fake := &fakeRunService{
eventRuns: []chan *iop.RunEvent{
bufferedRunEvents(&iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_bad","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":\"git status\"}"}}]`,
},
}),
bufferedRunEvents(&iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_good","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
},
}),
},
runIDs: []string{"run-bad", "run-good"},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"],"additionalProperties":false}}}],
"tool_choice":"auto"
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
reqs := fake.reqsSnapshot()
if len(reqs) != 2 {
t.Fatalf("submit attempts: got %d want 2", len(reqs))
}
if reqs[0].Metadata[runtimeMetadataToolValidationAttempt] != "1" {
t.Fatalf("first attempt metadata: %+v", reqs[0].Metadata)
}
if reqs[1].Metadata[runtimeMetadataToolValidationAttempt] != "2" || reqs[1].Metadata[runtimeMetadataToolValidationRetryOf] != "run-bad" {
t.Fatalf("retry metadata: %+v", reqs[1].Metadata)
}
if !strings.Contains(reqs[1].Metadata[runtimeMetadataToolValidationFailure], "$.arguments.commands type string") {
t.Fatalf("retry failure reason: %+v", reqs[1].Metadata)
}
var resp chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.ID != "chatcmpl-run-good" {
t.Fatalf("response id: got %q", resp.ID)
}
call := resp.Choices[0].Message.ToolCalls[0].(map[string]any)
if call["id"] != "call_good" {
t.Fatalf("tool call id: got %+v", call["id"])
}
}
func TestChatCompletionsFailsMalformedToolCallAfterRetryLimit(t *testing.T) {
invalidComplete := func(id string) *iop.RunEvent {
return &iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: fmt.Sprintf(`[{"id":%q,"type":"function","function":{"name":"run_commands","arguments":"{\"unexpected\":true}"}}]`, id),
},
}
}
fake := &fakeRunService{
eventRuns: []chan *iop.RunEvent{
bufferedRunEvents(invalidComplete("call_bad_1")),
bufferedRunEvents(invalidComplete("call_bad_2")),
},
runIDs: []string{"run-bad-1", "run-bad-2"},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"],"additionalProperties":false}}}],
"tool_choice":{"type":"function","function":{"name":"run_commands"}}
}`))
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(), `"type":"tool_validation_error"`) || !strings.Contains(w.Body.String(), "$.arguments.commands is required") {
t.Fatalf("expected tool validation error body, got %s", w.Body.String())
}
if reqs := fake.reqsSnapshot(); len(reqs) != 2 {
t.Fatalf("submit attempts: got %d want 2", len(reqs))
}
}
func TestChatCompletionsToolChoiceNoneSkipsRuntimeToolValidation(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 1)}
fake.events <- &iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_1","type":"function","function":{"name":"unknown_tool","arguments":"not-json"}}]`,
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"]}}}],
"tool_choice":"none"
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
reqs := fake.reqsSnapshot()
if len(reqs) != 1 {
t.Fatalf("submit attempts: got %d want 1", len(reqs))
}
if _, ok := reqs[0].Metadata[runtimeMetadataToolValidationAttempt]; ok {
t.Fatalf("tool_choice none should not enable validation metadata: %+v", reqs[0].Metadata)
}
}
func TestChatCompletionsToolsStreamRetriesMalformedToolCallBeforeChunk(t *testing.T) {
fake := &fakeRunService{
eventRuns: []chan *iop.RunEvent{
bufferedRunEvents(&iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_bad","type":"function","function":{"name":"edit","arguments":"{\"edits\":[{\"path\":\"apps/client/test/client_bootstrap_test.dart\",\"newText\":\"ok\"}]}"}}]`,
},
}),
bufferedRunEvents(&iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_good","type":"function","function":{"name":"edit","arguments":"{\"path\":\"apps/client/test/client_bootstrap_test.dart\",\"edits\":[{\"newText\":\"ok\"}]}"}}]`,
},
}),
},
runIDs: []string{"run-bad", "run-good"},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"stream":true,
"messages":[{"role":"user","content":"edit"}],
"tools":[{"type":"function","function":{"name":"edit","parameters":{"type":"object","properties":{"path":{"type":"string"},"edits":{"type":"array","items":{"type":"object","properties":{"newText":{"type":"string"}},"required":["newText"],"additionalProperties":false}}},"required":["path","edits"],"additionalProperties":false}}}],
"tool_choice":"auto"
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
reqs := fake.reqsSnapshot()
if len(reqs) != 2 {
t.Fatalf("submit attempts: got %d want 2", len(reqs))
}
if reqs[0].Metadata[runtimeMetadataToolValidationAttempt] != "1" {
t.Fatalf("first attempt metadata: %+v", reqs[0].Metadata)
}
if reqs[1].Metadata[runtimeMetadataToolValidationAttempt] != "2" || reqs[1].Metadata[runtimeMetadataToolValidationRetryOf] != "run-bad" {
t.Fatalf("retry metadata: %+v", reqs[1].Metadata)
}
if !strings.Contains(reqs[1].Metadata[runtimeMetadataToolValidationFailure], "$.arguments.path is required") {
t.Fatalf("retry failure reason: %+v", reqs[1].Metadata)
}
body := w.Body.String()
if strings.Contains(body, "call_bad") {
t.Fatalf("buffered stream leaked invalid tool call before retry:\n%s", body)
}
if !strings.Contains(body, "call_good") || !strings.Contains(body, `"finish_reason":"tool_calls"`) || !strings.Contains(body, "data: [DONE]") {
t.Fatalf("buffered stream did not emit validated tool call:\n%s", body)
}
}
func TestChatCompletionsStrictBufferedStreamRetriesMalformedToolCallBeforeChunk(t *testing.T) {
fake := &fakeRunService{
eventRuns: []chan *iop.RunEvent{
bufferedRunEvents(&iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_bad","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":\"git status\"}"}}]`,
},
}),
bufferedRunEvents(&iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_good","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
},
}),
},
runIDs: []string{"run-bad", "run-good"},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true, StrictStreamBuffer: true}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"stream":true,
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"],"additionalProperties":false}}}],
"tool_choice":"auto"
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
reqs := fake.reqsSnapshot()
if len(reqs) != 2 {
t.Fatalf("submit attempts: got %d want 2", len(reqs))
}
if reqs[0].Metadata[runtimeMetadataToolValidationAttempt] != "1" {
t.Fatalf("first attempt metadata: %+v", reqs[0].Metadata)
}
if reqs[1].Metadata[runtimeMetadataToolValidationAttempt] != "2" || reqs[1].Metadata[runtimeMetadataToolValidationRetryOf] != "run-bad" {
t.Fatalf("retry metadata: %+v", reqs[1].Metadata)
}
body := w.Body.String()
if strings.Contains(body, "call_bad") {
t.Fatalf("buffered stream leaked invalid tool call before retry:\n%s", body)
}
if !strings.Contains(body, "call_good") || !strings.Contains(body, `"finish_reason":"tool_calls"`) || !strings.Contains(body, "data: [DONE]") {
t.Fatalf("buffered stream did not emit validated tool call:\n%s", body)
}
}
func TestChatCompletionsStrictBufferedStreamFailsMalformedToolCallAfterRetryLimit(t *testing.T) {
invalidComplete := func(id string) *iop.RunEvent {
return &iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: fmt.Sprintf(`[{"id":%q,"type":"function","function":{"name":"run_commands","arguments":"{\"unexpected\":true}"}}]`, id),
},
}
}
fake := &fakeRunService{
eventRuns: []chan *iop.RunEvent{
bufferedRunEvents(invalidComplete("call_bad_1")),
bufferedRunEvents(invalidComplete("call_bad_2")),
},
runIDs: []string{"run-bad-1", "run-bad-2"},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true, StrictStreamBuffer: true}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"stream":true,
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"],"additionalProperties":false}}}],
"tool_choice":{"type":"function","function":{"name":"run_commands"}}
}`))
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, `"type":"tool_validation_error"`) || !strings.Contains(body, "$.arguments.commands is required") {
t.Fatalf("expected tool_validation_error SSE body, got:\n%s", body)
}
if strings.Contains(body, "call_bad") {
t.Fatalf("buffered stream leaked invalid tool call chunk after retry limit:\n%s", body)
}
if !strings.Contains(body, "data: [DONE]") {
t.Fatalf("buffered stream error should terminate with [DONE]:\n%s", body)
}
if reqs := fake.reqsSnapshot(); len(reqs) != 2 {
t.Fatalf("submit attempts: got %d want 2", len(reqs))
}
}
func TestValidateJSONSchemaSubsetCoversObjectArrayEnumAdditionalProperties(t *testing.T) {
schema := map[string]any{
"type": "object",
"properties": map[string]any{
"mode": map[string]any{
"type": "string",
"enum": []any{"read", "write"},
},
"commands": map[string]any{
"type": "array",
"items": map[string]any{"type": "string"},
},
"options": map[string]any{
"type": "object",
"properties": map[string]any{
"dry_run": map[string]any{"type": "boolean"},
},
"additionalProperties": false,
},
},
"required": []any{"mode", "commands"},
"additionalProperties": false,
}
valid := map[string]any{
"mode": "read",
"commands": []any{"git status"},
"options": map[string]any{"dry_run": true},
}
if err := validateJSONSchemaSubset(valid, schema, "$.arguments"); err != nil {
t.Fatalf("valid schema rejected: %v", err)
}
cases := []struct {
name string
args map[string]any
want string
}{
{
name: "enum",
args: map[string]any{"mode": "delete", "commands": []any{"git status"}},
want: "$.arguments.mode value",
},
{
name: "array item type",
args: map[string]any{"mode": "read", "commands": []any{1}},
want: "$.arguments.commands[0] type integer does not match schema type string",
},
{
name: "additional properties",
args: map[string]any{"mode": "read", "commands": []any{"git status"}, "extra": true},
want: "$.arguments.extra is not allowed",
},
{
name: "nested additional properties",
args: map[string]any{"mode": "read", "commands": []any{"git status"}, "options": map[string]any{"dry_run": true, "trace": true}},
want: "$.arguments.options.trace is not allowed",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := validateJSONSchemaSubset(tc.args, schema, "$.arguments")
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error got %v, want substring %q", err, tc.want)
}
})
}
}
func TestSynthesizesTemplateToolCallConvertsCommandObjectsToShellStrings(t *testing.T) {
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'command -v git && echo \"found\" || echo \"not found\"', 'runInTerminal': True}])}}"
tools := []any{map[string]any{
"type": "function",
"function": map[string]any{
"name": "run_commands",
},
}}
cleaned, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
if cleaned != "확인합니다." {
t.Fatalf("content: got %q", cleaned)
}
if len(toolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(toolCalls))
}
call := toolCalls[0].(map[string]any)
fn := call["function"].(map[string]any)
var args map[string]any
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
commands := args["commands"].([]any)
if commands[0] != `command -v git && echo "found" || echo "not found"` {
t.Fatalf("commands args: %+v", commands)
}
if _, ok := args["runInTerminal"]; ok {
t.Fatalf("runInTerminal should not be emitted: %+v", args)
}
}
func TestSynthesizesTemplateToolCallStripsTopLevelExecutionHints(t *testing.T) {
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'git status'}], runInTerminal=True, description='status')}}"
tools := []any{map[string]any{
"type": "function",
"function": map[string]any{
"name": "run_commands",
},
}}
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
if len(toolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(toolCalls))
}
call := toolCalls[0].(map[string]any)
fn := call["function"].(map[string]any)
var args map[string]any
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
commands := args["commands"].([]any)
command := commands[0].(map[string]any)
if command["command"] != "git" {
t.Fatalf("commands args: %+v", commands)
}
commandArgs := command["args"].([]any)
if len(commandArgs) != 1 || commandArgs[0] != "status" {
t.Fatalf("command args: %+v", commandArgs)
}
if _, ok := args["runInTerminal"]; ok {
t.Fatalf("runInTerminal should not be emitted: %+v", args)
}
if _, ok := args["description"]; ok {
t.Fatalf("description should not be emitted: %+v", args)
}
}
func TestSynthesizesTemplateToolCallKeepsStructuredCommandArgs(t *testing.T) {
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'git', 'args': ['status', '--short'], 'description': 'status'}])}}"
tools := []any{map[string]any{
"type": "function",
"function": map[string]any{
"name": "run_commands",
},
}}
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
if len(toolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(toolCalls))
}
call := toolCalls[0].(map[string]any)
fn := call["function"].(map[string]any)
var args map[string]any
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
commands := args["commands"].([]any)
command := commands[0].(map[string]any)
if command["command"] != "git" {
t.Fatalf("command: %+v", command)
}
commandArgs := command["args"].([]any)
if len(commandArgs) != 2 || commandArgs[0] != "status" || commandArgs[1] != "--short" {
t.Fatalf("command args: %+v", commandArgs)
}
if _, ok := command["description"]; ok {
t.Fatalf("description should not be emitted: %+v", command)
}
}
func TestSynthesizesTemplateToolCallFollowsClineUnionSchemaWithShellStrings(t *testing.T) {
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'git', 'args': ['-C', '/config/workspace/iop', 'status']}, {'command': 'pwd && ls -la /config/workspace/'}])}}"
tools := []any{map[string]any{
"type": "function",
"function": map[string]any{
"name": "run_commands",
"parameters": map[string]any{
"type": "object",
"properties": map[string]any{
"commands": map[string]any{
"type": "array",
"items": map[string]any{
"anyOf": []any{
map[string]any{
"type": "object",
"properties": map[string]any{
"command": map[string]any{"type": "string"},
"args": map[string]any{
"type": "array",
"items": map[string]any{"type": "string"},
},
},
},
map[string]any{"type": "string"},
},
},
},
},
},
},
}}
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
if len(toolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(toolCalls))
}
call := toolCalls[0].(map[string]any)
fn := call["function"].(map[string]any)
var args map[string][]string
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
if len(args["commands"]) != 2 {
t.Fatalf("commands len: %+v", args["commands"])
}
if args["commands"][0] != "git -C /config/workspace/iop status" {
t.Fatalf("first command: %+v", args["commands"][0])
}
if args["commands"][1] != "pwd && ls -la /config/workspace/" {
t.Fatalf("second command: %+v", args["commands"][1])
}
}
func TestSynthesizesTemplateToolCallConvertsStructuredShellOperatorsToString(t *testing.T) {
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'cd', 'args': ['/config/workspace/iop', '&&', 'git', 'status', '&&', 'git', 'diff', '--stat']}, {'command': 'ls', 'args': ['-la', '/config/workspace/iop', '&&', 'pwd']}])}}"
tools := []any{map[string]any{
"type": "function",
"function": map[string]any{
"name": "run_commands",
},
}}
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
if len(toolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(toolCalls))
}
call := toolCalls[0].(map[string]any)
fn := call["function"].(map[string]any)
var args map[string]any
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
commands := args["commands"].([]any)
if commands[0] != "cd /config/workspace/iop && git status && git diff --stat" {
t.Fatalf("first command: %+v", commands[0])
}
if commands[1] != "ls -la /config/workspace/iop && pwd" {
t.Fatalf("second command: %+v", commands[1])
}
}
func TestSynthesizesTemplateToolCallFollowsStringArrayCommandSchema(t *testing.T) {
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'git status', 'description': 'status'}])}}"
tools := []any{map[string]any{
"type": "function",
"function": map[string]any{
"name": "run_commands",
"parameters": map[string]any{
"type": "object",
"properties": map[string]any{
"commands": map[string]any{
"type": "array",
"items": map[string]any{
"type": "string",
},
},
},
},
},
}}
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
if len(toolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(toolCalls))
}
call := toolCalls[0].(map[string]any)
fn := call["function"].(map[string]any)
var args map[string][]string
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
if len(args["commands"]) != 1 || args["commands"][0] != "git status" {
t.Fatalf("commands args: %+v", args["commands"])
}
}
func TestSynthesizesTemplateToolCallFollowsSingleCommandSchema(t *testing.T) {
content := "확인합니다.\n\n{{bash(commands=[{'command': 'cd', 'args': ['/config/workspace/iop', '&&', 'git', 'status']}], runInTerminal=True)}}"
tools := []any{map[string]any{
"type": "function",
"function": map[string]any{
"name": "bash",
"parameters": map[string]any{
"type": "object",
"properties": map[string]any{
"command": map[string]any{
"type": "string",
},
},
},
},
}}
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
if len(toolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(toolCalls))
}
call := toolCalls[0].(map[string]any)
fn := call["function"].(map[string]any)
var args map[string]string
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
if args["command"] != "cd /config/workspace/iop && git status" {
t.Fatalf("command arg: %+v", args)
}
if _, ok := args["commands"]; ok {
t.Fatalf("commands should not be emitted: %+v", args)
}
if _, ok := args["runInTerminal"]; ok {
t.Fatalf("runInTerminal should not be emitted: %+v", args)
}
}
func TestSynthesizesShellCommandStringWithoutExtraExecutionHints(t *testing.T) {
content := "확인합니다.\n\n{{run_commands(commands=['command -v git && echo \"found\" || echo \"not found\"'])}}"
tools := []any{map[string]any{
"type": "function",
"function": map[string]any{
"name": "run_commands",
},
}}
cleaned, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
if cleaned != "확인합니다." {
t.Fatalf("content: got %q", cleaned)
}
if len(toolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(toolCalls))
}
call := toolCalls[0].(map[string]any)
fn := call["function"].(map[string]any)
var args map[string]any
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
commands := args["commands"].([]any)
if commands[0] != `command -v git && echo "found" || echo "not found"` {
t.Fatalf("commands args: %+v", commands)
}
if _, ok := args["runInTerminal"]; ok {
t.Fatalf("runInTerminal should not be emitted: %+v", args)
}
}
func TestSynthesizesTemplateToolCallWithClosingMarkerInString(t *testing.T) {
content := "확인합니다.\n\n{{run_commands(commands=[{'command': \"printf ')}}'\", 'description': 'marker test', 'runInTerminal': False}])}}"
tools := []any{map[string]any{
"type": "function",
"function": map[string]any{
"name": "run_commands",
},
}}
cleaned, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
if cleaned != "확인합니다." {
t.Fatalf("content: got %q", cleaned)
}
if len(toolCalls) != 1 {
t.Fatalf("tool_calls len: got %d", len(toolCalls))
}
call, ok := toolCalls[0].(map[string]any)
if !ok {
t.Fatalf("tool call shape: %+v", toolCalls[0])
}
fn, ok := call["function"].(map[string]any)
if !ok {
t.Fatalf("function shape: %+v", call["function"])
}
var args map[string][]string
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
t.Fatalf("arguments JSON: %v", err)
}
if len(args["commands"]) != 1 || args["commands"][0] != "printf ')}}'" {
t.Fatalf("commands args: %+v", args["commands"])
}
}
func TestChatCompletionsLeavesUnknownTextToolCallAsContent(t *testing.T) {
run1Events := make(chan *iop.RunEvent, 2)
run1Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=delete_everything><parameter=confirm>true</parameter></function></tool_call>"}
run1Events <- &iop.RunEvent{Type: "complete"}
run2Events := make(chan *iop.RunEvent, 2)
run2Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=delete_everything><parameter=confirm>true</parameter></function></tool_call>"}
run2Events <- &iop.RunEvent{Type: "complete"}
fake := &fakeRunService{eventRuns: []chan *iop.RunEvent{run1Events, run2Events}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusBadGateway {
t.Fatalf("status: got %d, want 502; body=%s", w.Code, w.Body.String())
}
var resp errorResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.Error.Type != "tool_validation_error" {
t.Fatalf("unexpected error type: got %q, want tool_validation_error", resp.Error.Type)
}
if !strings.Contains(resp.Error.Message, "delete_everything") {
t.Fatalf("unexpected error message: %q", resp.Error.Message)
}
}
func TestChatCompletionsLeavesUnknownTemplateToolCallAsContent(t *testing.T) {
run1Events := make(chan *iop.RunEvent, 2)
run1Events <- &iop.RunEvent{Type: "delta", Delta: "{{delete_everything(confirm=True)}}"}
run1Events <- &iop.RunEvent{Type: "complete"}
run2Events := make(chan *iop.RunEvent, 2)
run2Events <- &iop.RunEvent{Type: "delta", Delta: "{{delete_everything(confirm=True)}}"}
run2Events <- &iop.RunEvent{Type: "complete"}
fake := &fakeRunService{eventRuns: []chan *iop.RunEvent{run1Events, run2Events}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusBadGateway {
t.Fatalf("status: got %d, want 502; body=%s", w.Code, w.Body.String())
}
var resp errorResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.Error.Type != "tool_validation_error" {
t.Fatalf("unexpected error type: got %q, want tool_validation_error", resp.Error.Type)
}
if !strings.Contains(resp.Error.Message, "delete_everything") {
t.Fatalf("unexpected error message: %q", resp.Error.Message)
}
}
func TestChatCompletionsMapsMaxCompletionTokens(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"from-request",
"messages":[{"role":"user","content":"hi"}],
"max_completion_tokens":7
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
options, ok := fake.req.Input["options"].(map[string]any)
if !ok {
t.Fatalf("options not passed: %+v", fake.req.Input)
}
if options["max_tokens"].(int) != 7 {
t.Fatalf("max_completion_tokens not mapped to provider max_tokens: %+v", options)
}
}
func TestChatCompletionsStreamsSSE(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hi"}
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{"finish_reason": "length"}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"hi"}]
}`))
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, `"content":"hi"`) || !strings.Contains(body, "data: [DONE]") {
t.Fatalf("unexpected SSE body:\n%s", body)
}
if !strings.Contains(body, `"finish_reason":"length"`) {
t.Fatalf("streaming finish_reason did not preserve provider value:\n%s", body)
}
}
func TestChatCompletionsStreamSynthesizesToolCallsFromClineTextBlock(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "먼저 현재 git 상태를 확인하겠습니다.\n\n"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call>\n<function=run_commands>\n<parameter=commands>\n[\"cd /config/workspace/iop && git status\"]\n</parameter>\n</function>\n</tool_call>"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}]
}`))
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, `"tool_calls"`) || !strings.Contains(body, `"finish_reason":"tool_calls"`) {
t.Fatalf("stream did not synthesize tool_calls:\n%s", body)
}
if strings.Contains(body, "<tool_call>") || strings.Contains(body, `\u003ctool_call`) {
t.Fatalf("stream leaked raw tool_call block:\n%s", body)
}
if !strings.Contains(body, `"index":0`) {
t.Fatalf("stream tool_call delta should include index:\n%s", body)
}
if !strings.Contains(body, `"content":"먼저 현재 git 상태를 확인하겠습니다."`) {
t.Fatalf("stream should preserve text before tool call:\n%s", body)
}
}
func TestChatCompletionsStreamWithToolsBuffersTextUntilComplete(t *testing.T) {
fake := &fakeRunService{events: bufferedRunEvents(
&iop.RunEvent{Type: "delta", Delta: "streaming before complete"},
&iop.RunEvent{Type: "complete"},
)}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"explain sudo"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}],
"tool_choice":"auto"
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
reqs := fake.reqsSnapshot()
if len(reqs) != 1 {
t.Fatalf("submit attempts: got %d want 1", len(reqs))
}
if reqs[0].Metadata[runtimeMetadataToolValidationAttempt] != "1" {
t.Fatalf("tool stream should enable buffered validation metadata: %+v", reqs[0].Metadata)
}
body := w.Body.String()
if !strings.Contains(body, `"content":"streaming before complete"`) ||
!strings.Contains(body, `"finish_reason":"stop"`) ||
!strings.Contains(body, "data: [DONE]") {
t.Fatalf("buffered stream did not emit final content:\n%s", body)
}
}
func TestChatCompletionsStreamSynthesizesToolCallsFromClineTemplateCall(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "먼저 현재 git 상태를 확인하겠습니다.\n\n"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "{{run_commands(commands=[{'command': 'git status', 'description': '현재 git 상태 확인', 'runInTerminal': True}]})}}"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}]
}`))
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, `"tool_calls"`) || !strings.Contains(body, `"finish_reason":"tool_calls"`) {
t.Fatalf("stream did not synthesize tool_calls:\n%s", body)
}
if strings.Contains(body, "{{run_commands") {
t.Fatalf("stream leaked raw template tool call:\n%s", body)
}
if !strings.Contains(body, `"index":0`) {
t.Fatalf("stream tool_call delta should include index:\n%s", body)
}
if !strings.Contains(body, `\"commands\":[{\"args\":[\"status\"],\"command\":\"git\"}]`) &&
!strings.Contains(body, `\"commands\":[{\"command\":\"git\",\"args\":[\"status\"]}]`) {
t.Fatalf("stream tool_call arguments should include command:\n%s", body)
}
if strings.Contains(body, "runInTerminal") || strings.Contains(body, "description") {
t.Fatalf("stream tool_call arguments should strip execution metadata:\n%s", body)
}
if !strings.Contains(body, `"content":"먼저 현재 git 상태를 확인하겠습니다."`) {
t.Fatalf("stream should preserve text before tool call:\n%s", body)
}
}
func TestChatCompletionsStreamPassesThroughNativeToolCallsFromProviderRoute(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "checking"}
fake.events <- &iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_1","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}],
"tool_choice":"auto"
}`))
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, `"tool_calls"`) || !strings.Contains(body, `"call_1"`) {
t.Fatalf("stream did not pass through native tool_calls:\n%s", body)
}
if !strings.Contains(body, `"finish_reason":"tool_calls"`) {
t.Fatalf("stream finish_reason:\n%s", body)
}
}
func TestChatCompletionsStreamDropsRawTextWhenNativeToolCallsArrive(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: `<tool_call><function=run_commands><parameter=commands>["git status"]</parameter></function></tool_call>`}
fake.events <- &iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_1","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}],
"tool_choice":"auto"
}`))
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, `"tool_calls"`) || !strings.Contains(body, `"call_1"`) {
t.Fatalf("stream did not pass through native tool_calls:\n%s", body)
}
if strings.Contains(body, "<tool_call") || strings.Contains(body, `\u003ctool_call`) {
t.Fatalf("stream leaked raw text tool call:\n%s", body)
}
}
func TestChatCompletionsToolsStreamNativeToolCallPreservesLeadingProse(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "선행 prose\n\n<tool_call><function=run_commands><parameter=commands>[\"git status\"]</parameter></function></tool_call>"}
fake.events <- &iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_1","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}],
"tool_choice":"auto"
}`))
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, `"content":"선행 prose"`) {
t.Fatalf("stream dropped leading prose:\n%s", body)
}
if !strings.Contains(body, `"tool_calls"`) || !strings.Contains(body, `"call_1"`) {
t.Fatalf("stream did not pass through native tool_calls:\n%s", body)
}
if strings.Contains(body, "<tool_call") || strings.Contains(body, `\u003ctool_call`) {
t.Fatalf("stream leaked raw text tool call:\n%s", body)
}
}
func TestChatCompletionsStreamDropsPartialTextToolCallSuffixWhenNativeToolCallsArrive(t *testing.T) {
cases := []struct {
name string
delta string
wantContent string
leakMarkers []string
}{
{
name: "xml partial candidate suffix",
delta: "checking status <tool_ca",
wantContent: `"content":"checking status"`,
leakMarkers: []string{`\u003ctool_ca`},
},
{
name: "mustache partial candidate suffix",
delta: "checking status {",
wantContent: `"content":"checking status"`,
leakMarkers: []string{`"content":"{"`},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: tc.delta}
fake.events <- &iop.RunEvent{
Type: "complete",
Metadata: map[string]string{
"finish_reason": "tool_calls",
runtimeMetadataOpenAIToolCalls: `[{"id":"call_1","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}],
"tool_choice":"auto"
}`))
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, `"tool_calls"`) || !strings.Contains(body, `"call_1"`) {
t.Fatalf("stream did not pass through native tool_calls:\n%s", body)
}
if !strings.Contains(body, `"finish_reason":"tool_calls"`) {
t.Fatalf("stream finish_reason:\n%s", body)
}
if !strings.Contains(body, tc.wantContent) {
t.Fatalf("stream dropped safe prefix content %s:\n%s", tc.wantContent, body)
}
for _, marker := range tc.leakMarkers {
if strings.Contains(body, marker) {
t.Fatalf("stream leaked partial text tool call marker %q:\n%s", marker, body)
}
}
})
}
}
func TestChatCompletionsStreamingReportsClosedRunStream(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent)}
close(fake.events)
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"hi"}]
}`))
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, `"message":"run stream closed"`) || !strings.Contains(body, "data: [DONE]") {
t.Fatalf("unexpected SSE error body:\n%s", body)
}
}
func TestChatCompletionsReturnsReasoningContentSeparately(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 5)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "thinking "}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "more"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer"}
fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 2, OutputTokens: 2}}
srv := NewServer(config.EdgeOpenAIConf{
Adapter: "ollama",
Target: "llama-fixed",
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"client-model",
"messages":[{"role":"user","content":"hi"}]
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.Choices[0].Message.Content != "answer" {
t.Fatalf("content: got %q, expected \"answer\"", resp.Choices[0].Message.Content)
}
if resp.Choices[0].Message.ReasoningContent != "thinking more" {
t.Fatalf("reasoning_content: got %q, expected \"thinking more\"", resp.Choices[0].Message.ReasoningContent)
}
}
func TestChatCompletionsFallsBackToReasoningWhenContentEmpty(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "thinking "}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "only"}
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{"finish_reason": "length"}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"client-model",
"messages":[{"role":"user","content":"hi"}]
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
content := resp.Choices[0].Message.Content
if !strings.Contains(content, "thinking only") || !strings.Contains(content, "finish_reason=length") {
t.Fatalf("content fallback did not preserve reasoning and finish reason: %q", content)
}
if resp.Choices[0].Message.ReasoningContent != "thinking only" {
t.Fatalf("reasoning_content: got %q, expected \"thinking only\"", resp.Choices[0].Message.ReasoningContent)
}
}
func TestChatCompletionsSuppressesReasoningContentWhenExcluded(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 5)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "thinking "}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "more"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer"}
fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 2, OutputTokens: 2}}
srv := NewServer(config.EdgeOpenAIConf{
Adapter: "ollama",
Target: "llama-fixed",
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"client-model",
"messages":[{"role":"user","content":"hi"}],
"include_reasoning": false
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.Choices[0].Message.Content != "answer" {
t.Fatalf("content: got %q, expected \"answer\"", resp.Choices[0].Message.Content)
}
if resp.Choices[0].Message.ReasoningContent != "" {
t.Fatalf("reasoning_content: got %q, expected empty string due to include_reasoning=false", resp.Choices[0].Message.ReasoningContent)
}
}
func TestChatCompletionsReportsHiddenReasoningWhenExcludedAndContentEmpty(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "thinking "}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "only"}
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{"finish_reason": "length"}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"client-model",
"messages":[{"role":"user","content":"hi"}],
"include_reasoning": false
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
content := resp.Choices[0].Message.Content
if content == "" {
t.Fatalf("content should report hidden reasoning instead of returning empty success")
}
if strings.Contains(content, "thinking only") {
t.Fatalf("hidden reasoning fallback leaked reasoning content: %q", content)
}
if !strings.Contains(content, "reasoning was hidden by include_reasoning=false") || !strings.Contains(content, "finish_reason=length") {
t.Fatalf("hidden reasoning fallback did not report cause and finish reason: %q", content)
}
if resp.Choices[0].Message.ReasoningContent != "" {
t.Fatalf("reasoning_content: got %q, expected empty string due to include_reasoning=false", resp.Choices[0].Message.ReasoningContent)
}
}
func TestChatCompletionsStreamsReasoningSSE(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hi"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"hi"}]
}`))
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, `"reasoning_content":"think"`) || !strings.Contains(body, `"content":"hi"`) || !strings.Contains(body, "data: [DONE]") {
t.Fatalf("unexpected SSE body:\n%s", body)
}
}
func TestChatCompletionsStrictOutputStreamsExplicitReasoningSSE(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<attempt_completion><result>hi</result></attempt_completion>"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"hi"}],
"include_reasoning": true
}`))
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, `"reasoning_content":"think"`) ||
!strings.Contains(body, `"content":"\u003cattempt_completion\u003e\u003cresult\u003ehi\u003c/result\u003e\u003c/attempt_completion\u003e"`) ||
!strings.Contains(body, "data: [DONE]") {
t.Fatalf("unexpected SSE body:\n%s", body)
}
}
func TestChatCompletionsStreamFallsBackToReasoningWhenContentEmpty(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think only"}
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{"finish_reason": "length"}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"hi"}]
}`))
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, `"reasoning_content":"think only"`) ||
!strings.Contains(body, `"content":"think only`) ||
!strings.Contains(body, "finish_reason=length") ||
!strings.Contains(body, "data: [DONE]") {
t.Fatalf("unexpected SSE body:\n%s", body)
}
}
func TestChatCompletionsStreamSuppressesReasoningWhenExcluded(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hi"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"hi"}],
"include_reasoning": false
}`))
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, "reasoning_content") || strings.Contains(body, "think") {
t.Fatalf("unexpected reasoning_content in SSE body when excluded:\n%s", body)
}
if !strings.Contains(body, `"content":"hi"`) || !strings.Contains(body, "data: [DONE]") {
t.Fatalf("unexpected SSE body, missing content or DONE:\n%s", body)
}
}
func TestChatCompletionsStreamSanitizesKnownSentinelTokenAcrossDeltas(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 5)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think <|mask"}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "_end|>"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello <|mask"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "_end|> world"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"hi"}]
}`))
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, "<|mask") || strings.Contains(body, "mask_end") {
t.Fatalf("stream leaked sentinel token:\n%s", body)
}
if !strings.Contains(body, `"content":"hello "`) || !strings.Contains(body, `"content":"world"`) {
t.Fatalf("stream did not preserve sanitized content chunks:\n%s", body)
}
if !strings.Contains(body, `"reasoning_content":"think "`) || !strings.Contains(body, "data: [DONE]") {
t.Fatalf("stream did not preserve sanitized reasoning or done marker:\n%s", body)
}
}
func TestChatCompletionsStreamReportsHiddenReasoningWhenExcludedAndContentEmpty(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think only"}
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{"finish_reason": "length"}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"stream-model",
"stream":true,
"messages":[{"role":"user","content":"hi"}],
"include_reasoning": false
}`))
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, "reasoning_content") || strings.Contains(body, "think only") {
t.Fatalf("unexpected reasoning fallback in SSE body when excluded:\n%s", body)
}
if !strings.Contains(body, "reasoning was hidden by include_reasoning=false") ||
!strings.Contains(body, "finish_reason=length") ||
!strings.Contains(body, "data: [DONE]") {
t.Fatalf("unexpected SSE body, missing hidden reasoning report or DONE:\n%s", body)
}
}
func TestChatCompletionsStrictOutputNormalizesXMLStyleAgentResponse(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "---\n<thinking>hidden</thinking>\n\n<attempt_completion>\n<result>ok</result>\n</attempt completion>\n```\ntrailing noise"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"agent-model",
"messages":[{"role":"user","content":"finish"}]
}`))
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 fake.req.Input["think"] != false {
t.Fatalf("strict output should disable thinking by default: %+v", fake.req.Input)
}
var resp chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
want := "<attempt_completion>\n<result>ok</result>\n</attempt_completion>"
if resp.Choices[0].Message.Content != want {
t.Fatalf("content:\ngot %q\nwant %q", resp.Choices[0].Message.Content, want)
}
if resp.Choices[0].Message.ReasoningContent != "" {
t.Fatalf("strict output should drop reasoning content: %q", resp.Choices[0].Message.ReasoningContent)
}
}
func TestChatCompletionsStrictOutputPreservesExplicitReasoningContent(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden thought"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<attempt_completion>\n<result>ok</result>\n</attempt_completion>"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"agent-model",
"messages":[{"role":"user","content":"finish"}],
"include_reasoning": true
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.Choices[0].Message.Content != "<attempt_completion>\n<result>ok</result>\n</attempt_completion>" {
t.Fatalf("content: %q", resp.Choices[0].Message.Content)
}
if resp.Choices[0].Message.ReasoningContent != "hidden thought" {
t.Fatalf("reasoning_content: got %q, want hidden thought", resp.Choices[0].Message.ReasoningContent)
}
}
func TestChatCompletionsStrictOutputWrapsPlainTextWhenPromptDeclaresCompletionTool(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "plain answer"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"agent-model",
"messages":[
{"role":"system","content":"Once you've completed the user's task, you must use the attempt_completion tool to present the result.\n\n<attempt_completion>\n<result>done</result>\n</attempt_completion>"},
{"role":"user","content":"finish"}
]
}`))
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 !strings.Contains(fake.req.Prompt, "output exactly one <attempt_completion> block") || !strings.Contains(fake.req.Prompt, "actual user-facing response") {
t.Fatalf("strict contract instruction not injected:\n%s", fake.req.Prompt)
}
var resp chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
want := "<attempt_completion>\n<result>plain answer</result>\n</attempt_completion>"
if resp.Choices[0].Message.Content != want {
t.Fatalf("content:\ngot %q\nwant %q", resp.Choices[0].Message.Content, want)
}
}
func TestChatCompletionsStrictOutputBuffersStreamingResponse(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "---\n<think>noise</think><read_file><path>a.go</path></read file>"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true, StrictStreamBuffer: true}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"agent-model",
"stream":true,
"messages":[{"role":"user","content":"read"}]
}`))
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, "reasoning_content") || strings.Contains(body, "<think>") || strings.Contains(body, "</read file>") {
t.Fatalf("strict stream leaked unstable content:\n%s", body)
}
if !strings.Contains(body, "read_file") || !strings.Contains(body, "a.go") || !strings.Contains(body, "data: [DONE]") {
t.Fatalf("unexpected strict SSE body:\n%s", body)
}
}
func TestChatCompletionsStrictOutputStreamsLiveContent(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 5)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<attempt_completion>\n"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<result>ok</result>\n</attempt_completion>"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"agent-model",
"stream":true,
"messages":[{"role":"user","content":"finish"}]
}`))
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, "reasoning_content") || strings.Contains(body, "hidden") {
t.Fatalf("strict stream leaked reasoning content:\n%s", body)
}
if strings.Count(body, `"content":`) != 2 || !strings.Contains(body, "attempt_completion") || !strings.Contains(body, "data: [DONE]") {
t.Fatalf("strict stream should pass live content deltas:\n%s", body)
}
}
func TestModelsUsesConfiguredModelsOrTarget(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Target: "fallback-model"}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
w := httptest.NewRecorder()
srv.handleModels(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d", w.Code)
}
if !strings.Contains(w.Body.String(), "fallback-model") {
t.Fatalf("expected target model, got %s", w.Body.String())
}
srv = NewServer(config.EdgeOpenAIConf{Models: []string{"a", "b"}}, &fakeRunService{}, nil)
w = httptest.NewRecorder()
srv.handleModels(w, req)
if !strings.Contains(w.Body.String(), `"id":"a"`) || !strings.Contains(w.Body.String(), `"id":"b"`) {
t.Fatalf("expected configured models, got %s", w.Body.String())
}
}
// TestOpenAIModelsUsesRefreshedCatalog verifies that a provider-pool catalog
// applied via SetModelCatalog (config refresh apply) is reflected in /v1/models
// on the next request, replacing the previous catalog snapshot.
func TestOpenAIModelsUsesRefreshedCatalog(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{}, &fakeRunService{}, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "model-old"}})
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
w := httptest.NewRecorder()
srv.handleModels(w, req)
if body := w.Body.String(); !strings.Contains(body, `"id":"model-old"`) {
t.Fatalf("expected initial catalog model-old, got %s", body)
}
// Refresh the catalog: the next request must show the new model and not the old.
srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "model-new"}})
w = httptest.NewRecorder()
srv.handleModels(w, req)
body := w.Body.String()
if !strings.Contains(body, `"id":"model-new"`) {
t.Fatalf("expected refreshed catalog model-new, got %s", body)
}
if strings.Contains(body, `"id":"model-old"`) {
t.Fatalf("stale catalog model-old still present after refresh, got %s", body)
}
}
// TestOpenAIProviderPoolRouteUsesRefreshedCatalog verifies that provider-pool
// route resolution reads the refreshed catalog snapshot, so a model added by a
// config refresh is routed via the provider pool.
func TestOpenAIProviderPoolRouteUsesRefreshedCatalog(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{}, &fakeRunService{}, nil)
if srv.findProviderPoolEntry("model-new") != nil {
t.Fatal("expected no provider-pool entry before catalog is set")
}
srv.SetModelCatalog([]config.ModelCatalogEntry{{
ID: "model-new",
Providers: map[string]string{"prov-a": "served-a"},
}})
dispatch, ok := srv.resolveRouteDispatch("model-new")
if !ok || !dispatch.ProviderPool {
t.Fatalf("expected provider-pool dispatch for refreshed model, got ok=%v dispatch=%+v", ok, dispatch)
}
}
func TestOllamaAPIPassthrough(t *testing.T) {
fake := &fakeRunService{
ollamaResp: edgeservice.OllamaAPIView{
StatusCode: http.StatusAccepted,
ContentType: "application/json",
Body: `{"ok":true}`,
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", TimeoutSec: 15}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/api/show", strings.NewReader(`{"model":"gemma4:26b"}`))
w := httptest.NewRecorder()
srv.handleOllamaAPI(w, req)
if w.Code != http.StatusAccepted {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.ollamaReq.Adapter != "ollama" || fake.ollamaReq.Method != http.MethodPost || fake.ollamaReq.Path != "/api/show" {
t.Fatalf("passthrough req mismatch: %+v", fake.ollamaReq)
}
if fake.ollamaReq.Body != `{"model":"gemma4:26b"}` || fake.ollamaReq.TimeoutSec != 15 {
t.Fatalf("passthrough body/timeout mismatch: %+v", fake.ollamaReq)
}
if w.Body.String() != `{"ok":true}` {
t.Fatalf("body: got %s", w.Body.String())
}
}
func TestOllamaAPIPassthroughPreservesConfiguredTarget(t *testing.T) {
fake := &fakeRunService{
ollamaResp: edgeservice.OllamaAPIView{
StatusCode: http.StatusOK,
ContentType: "application/json",
Body: `{"models":[]}`,
},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "gemma4:26b", TimeoutSec: 15}, fake, nil)
req := httptest.NewRequest(http.MethodGet, "/api/tags", nil)
w := httptest.NewRecorder()
srv.handleOllamaAPI(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.ollamaReq.Target != "gemma4:26b" {
t.Fatalf("passthrough target: got %q, want gemma4:26b", fake.ollamaReq.Target)
}
}
func TestResponsesDispatchesNonStreamingRequest(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: " world"}
fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 1, OutputTokens: 1}}
srv := NewServer(config.EdgeOpenAIConf{
Adapter: "ollama",
Target: "llama-fixed",
SessionID: "configured-session",
TimeoutSec: 42,
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"say hello",
"instructions":"Use short answers.",
"max_output_tokens":16,
"temperature":0.2,
"top_p":0.9,
"background":false
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Target != "llama-fixed" {
t.Fatalf("target: got %q, want llama-fixed", fake.req.Target)
}
if fake.req.Prompt != "Use short answers.\n\nsay hello" {
t.Fatalf("prompt: got %q", fake.req.Prompt)
}
options, ok := fake.req.Input["options"].(map[string]any)
if !ok {
t.Fatalf("options not passed: %+v", fake.req.Input)
}
if options["max_tokens"].(int) != 16 || options["temperature"].(float64) != 0.2 || options["top_p"].(float64) != 0.9 {
t.Fatalf("unexpected response options: %+v", options)
}
if fake.req.TimeoutSec != 42 {
t.Fatalf("timeout: got %d, want 42", fake.req.TimeoutSec)
}
if fake.req.SessionID != "configured-session" {
t.Fatalf("session_id: got %q, want configured-session", fake.req.SessionID)
}
if fake.req.Background {
t.Fatal("background should remain false for responses")
}
var resp responsesResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.OutputText != "hello world" {
t.Fatalf("output_text: got %q", resp.OutputText)
}
if len(resp.Output) == 0 || len(resp.Output[0].Content) == 0 || resp.Output[0].Content[0].Text != "hello world" {
t.Fatalf("output[0].content[0].text wrong: %+v", resp.Output)
}
}
func TestResponsesRejectsUnsupportedRequests(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
cases := []struct {
name string
method string
body string
want int
}{
{"wrong method", http.MethodGet, "", http.StatusMethodNotAllowed},
{"stream=true", http.MethodPost, `{"model":"m","input":"hi","stream":true}`, http.StatusBadRequest},
{"empty input", http.MethodPost, `{"model":"m","input":""}`, http.StatusBadRequest},
{"non-string input", http.MethodPost, `{"model":"m","input":["a","b"]}`, http.StatusBadRequest},
{"options wrapper unsupported", http.MethodPost, `{"model":"m","input":"hi","options":{"max_output_tokens":16}}`, http.StatusBadRequest},
{"background unsupported", http.MethodPost, `{"model":"m","input":"hi","background":true}`, http.StatusBadRequest},
{"bad max_output_tokens", http.MethodPost, `{"model":"m","input":"hi","max_output_tokens":0}`, http.StatusBadRequest},
{"bad top_p", http.MethodPost, `{"model":"m","input":"hi","top_p":1.5}`, http.StatusBadRequest},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(tc.method, "/v1/responses", strings.NewReader(tc.body))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != tc.want {
t.Fatalf("got %d want %d body=%s", w.Code, tc.want, w.Body.String())
}
})
}
}
func TestResponsesGenericMetadataContract(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"test",
"metadata":{
"request_id":"req-001",
"workspace":"/config/workspace/iop",
"task_id":"task-123",
"custom":"value"
}
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Target != "client-model" {
t.Fatalf("target: got %q, want client-model", fake.req.Target)
}
if fake.req.Workspace != "/config/workspace/iop" {
t.Fatalf("workspace: got %q", fake.req.Workspace)
}
if fake.req.Metadata["request_id"] != "req-001" {
t.Fatalf("request_id: got %q", fake.req.Metadata["request_id"])
}
if _, ok := fake.req.Metadata["workspace"]; ok {
t.Fatal("workspace should not be copied into run metadata")
}
if fake.req.Metadata["task_id"] != "task-123" {
t.Fatalf("task_id: got %q", fake.req.Metadata["task_id"])
}
if fake.req.Metadata["custom"] != "value" {
t.Fatalf("custom: got %q", fake.req.Metadata["custom"])
}
if fake.req.Metadata["openai_model"] != "client-model" {
t.Fatalf("openai_model: got %q", fake.req.Metadata["openai_model"])
}
if fake.req.ModelGroupKey != "client-model" {
t.Fatalf("model group key: got %q, want client-model", fake.req.ModelGroupKey)
}
if fake.req.Metadata["openai_stream"] != "false" {
t.Fatalf("openai_stream: got %q", fake.req.Metadata["openai_stream"])
}
if fake.req.Metadata["strict_output"] != "false" {
t.Fatalf("strict_output: got %q", fake.req.Metadata["strict_output"])
}
}
func TestChatCompletionsMetadataContractAndWorkspace(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"client-model",
"messages":[{"role":"user","content":"hi"}],
"metadata":{
"request_id":"req-chat-001",
"workspace":"/config/workspace/iop",
"task_id":"task-123"
}
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Target != "client-model" {
t.Fatalf("target: got %q, want client-model", fake.req.Target)
}
if fake.req.Workspace != "/config/workspace/iop" {
t.Fatalf("workspace: got %q", fake.req.Workspace)
}
if fake.req.Metadata["request_id"] != "req-chat-001" {
t.Fatalf("request_id: got %q", fake.req.Metadata["request_id"])
}
if fake.req.Metadata["task_id"] != "task-123" {
t.Fatalf("task_id: got %q", fake.req.Metadata["task_id"])
}
if fake.req.ModelGroupKey != "client-model" {
t.Fatalf("model group key: got %q, want client-model", fake.req.ModelGroupKey)
}
if _, ok := fake.req.Metadata["workspace"]; ok {
t.Fatal("workspace should not be copied into run metadata")
}
}
func TestChatCompletionsRejectsObjectMetadata(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"m",
"messages":[{"role":"user","content":"hi"}],
"metadata":{"cli":{"flag":"x"}}
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("got %d want 400, body=%s", w.Code, w.Body.String())
}
}
func TestChatCompletionsRejectsUnsupportedFields(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
cases := []struct {
name string
body string
}{
{"options wrapper", `{"model":"m","messages":[{"role":"user","content":"hi"}],"options":{"num_ctx":8192}}`},
{"format", `{"model":"m","messages":[{"role":"user","content":"hi"}],"format":"json"}`},
{"keep_alive", `{"model":"m","messages":[{"role":"user","content":"hi"}],"keep_alive":"10m"}`},
{"bad max_tokens", `{"model":"m","messages":[{"role":"user","content":"hi"}],"max_tokens":0}`},
{"bad top_p", `{"model":"m","messages":[{"role":"user","content":"hi"}],"top_p":2}`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(tc.body))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("got %d want 400, body=%s", w.Code, w.Body.String())
}
})
}
}
func TestChatCompletionsAcceptsThinkControlFields(t *testing.T) {
fake := &fakeRunService{
events: make(chan *iop.RunEvent, 1),
}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
// Case 1: All valid think-control fields provided
reqBody := `{
"model":"client-model",
"messages":[{"role":"user","content":"hi"}],
"think": true,
"reasoning_effort": "medium",
"thinking_token_budget": 1024,
"include_reasoning": false
}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(reqBody))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Input["think"] != true {
t.Fatalf("think: got %v, want true", fake.req.Input["think"])
}
if fake.req.Input["reasoning_effort"] != "medium" {
t.Fatalf("reasoning_effort: got %v, want 'medium'", fake.req.Input["reasoning_effort"])
}
budgetVal := fake.req.Input["thinking_token_budget"]
switch v := budgetVal.(type) {
case float64:
if v != 1024 {
t.Fatalf("thinking_token_budget: got %f, want 1024", v)
}
case int:
if v != 1024 {
t.Fatalf("thinking_token_budget: got %d, want 1024", v)
}
default:
t.Fatalf("thinking_token_budget: got %T, want number type", budgetVal)
}
if fake.req.Input["include_reasoning"] != false {
t.Fatalf("include_reasoning: got %v, want false", fake.req.Input["include_reasoning"])
}
// Case 2: Omitted think-control fields should NOT add keys to runInput
fake2 := &fakeRunService{
events: make(chan *iop.RunEvent, 1),
}
fake2.events <- &iop.RunEvent{Type: "complete"}
srv2 := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake2, nil)
reqBody2 := `{
"model":"client-model",
"messages":[{"role":"user","content":"hi"}]
}`
req2 := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(reqBody2))
w2 := httptest.NewRecorder()
srv2.routes().ServeHTTP(w2, req2)
if w2.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w2.Code, w2.Body.String())
}
for _, key := range []string{"think", "reasoning_effort", "thinking_token_budget", "include_reasoning"} {
if _, ok := fake2.req.Input[key]; ok {
t.Fatalf("expected key %q to be omitted from runInput, but it was present", key)
}
}
}
func TestChatCompletionsRejectsInvalidThinkControlFields(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
cases := []struct {
name string
body string
}{
{"negative budget", `{"model":"m","messages":[{"role":"user","content":"hi"}],"thinking_token_budget":-1}`},
{"bad effort", `{"model":"m","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"super_high"}`},
{"empty effort", `{"model":"m","messages":[{"role":"user","content":"hi"}],"reasoning_effort":""}`},
{"think=false and reasoning_effort other than none", `{"model":"m","messages":[{"role":"user","content":"hi"}],"think":false,"reasoning_effort":"high"}`},
{"thinking_token_budget conflicts with think=false", `{"model":"m","messages":[{"role":"user","content":"hi"}],"think":false,"thinking_token_budget":100}`},
{"thinking_token_budget conflicts with reasoning_effort=none", `{"model":"m","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"none","thinking_token_budget":100}`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(tc.body))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("got %d want 400, body=%s", w.Code, w.Body.String())
}
})
}
}
func TestChatCompletionsRejectsSourceMetadata(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"m",
"messages":[{"role":"user","content":"hi"}],
"metadata":{"source":"manual"}
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("got %d want 400, body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "metadata.source is not supported") {
t.Fatalf("expected source unsupported error, got %s", w.Body.String())
}
}
func TestChatCompletionsRejectsNonStringMetadataValue(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"m",
"messages":[{"role":"user","content":"hi"}],
"metadata":{"attempt":2}
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("got %d want 400, body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "metadata.attempt must be a string") {
t.Fatalf("expected string metadata error, got %s", w.Body.String())
}
}
func TestChatCompletionsRejectsObjectWorkspaceMetadata(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"m",
"messages":[{"role":"user","content":"hi"}],
"metadata":{
"workspace": {
"path": "/home/user/workspace",
"source_branch": "develop"
}
}
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("got %d want 400, body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "metadata.workspace must be a string") {
t.Fatalf("expected workspace string error, got %s", w.Body.String())
}
}
func TestResponsesPreservesGenericTaskMetadata(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"test",
"metadata":{
"request_id":"req-flat-001",
"task_id":"task-flat"
}
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Metadata["task_id"] != "task-flat" {
t.Fatalf("task_id: got %q", fake.req.Metadata["task_id"])
}
}
func TestResponsesConfiguredTargetWinsOverRequestModel(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "config-target"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"test"
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Target != "config-target" {
t.Fatalf("target: got %q, want config-target", fake.req.Target)
}
var resp responsesResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if resp.Model != "client-model" {
t.Fatalf("model: got %q, want client-model", resp.Model)
}
}
func TestResponsesRejectsNonStringMetadata(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
cases := []struct {
name string
body string
}{
{"cli only", `{"model":"m","input":"hi","metadata":{"cli":{"flag":"x"}}}`},
{"inference target", `{"model":"m","input":"hi","metadata":{"inference":{"target":"t"}}}`},
{"nomadcode metadata", `{"model":"m","input":"hi","metadata":{"nomadcode":{"task_id":"t"}}}`},
{"source metadata", `{"model":"m","input":"hi","metadata":{"source":"manual"}}`},
{"number metadata", `{"model":"m","input":"hi","metadata":{"attempt":2}}`},
{"boolean metadata", `{"model":"m","input":"hi","metadata":{"urgent":true}}`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(tc.body))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("got %d want 400, body=%s", w.Code, w.Body.String())
}
})
}
}
func TestResponsesRejectsObjectWorkspaceMetadata(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"m",
"input":"hi",
"metadata":{
"workspace": {
"path": "/home/user/workspace",
"source_branch": "develop"
}
}
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("got %d want 400, body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "metadata.workspace must be a string") {
t.Fatalf("expected workspace string error, got %s", w.Body.String())
}
}
func TestResponsesReturnsOpenAICompatibleShape(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer"}
fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 1, OutputTokens: 1}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"say hello"
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
var resp responsesResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if !strings.HasPrefix(resp.ID, "resp-") {
t.Fatalf("id: got %q, want resp- prefix", resp.ID)
}
if resp.Model != "client-model" {
t.Fatalf("model: got %q", resp.Model)
}
if resp.OutputText != "answer" {
t.Fatalf("output_text: got %q", resp.OutputText)
}
if len(resp.Output) == 0 || len(resp.Output[0].Content) == 0 || resp.Output[0].Content[0].Text != "answer" {
t.Fatalf("output[0].content[0].text wrong: %+v", resp.Output)
}
if resp.Usage.TotalTokens != 2 {
t.Fatalf("usage.total_tokens: got %d", resp.Usage.TotalTokens)
}
}
func TestResponsesSanitizesKnownSentinelToken(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden <|mask_end|>"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer <|mask_end|>"}
fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 1, OutputTokens: 1}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"say hello"
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
var resp responsesResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if strings.Contains(resp.OutputText, "<|mask_end|>") || strings.Contains(w.Body.String(), "<|mask_end|>") {
t.Fatalf("sentinel leaked in responses output: %s", w.Body.String())
}
if resp.OutputText != "answer" {
t.Fatalf("output_text: got %q, want answer", resp.OutputText)
}
}
func TestResponsesUsageDefaultsToZeroObject(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"m",
"input":"hi"
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
var body map[string]json.RawMessage
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatalf("decode: %v", err)
}
if _, ok := body["usage"]; !ok {
t.Fatal("usage field missing from response")
}
var usage openAIUsage
if err := json.Unmarshal(body["usage"], &usage); err != nil {
t.Fatalf("usage is not an object: %s", body["usage"])
}
}
func TestResponsesStrictOutputNormalizesAgentResponse(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "plain answer"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama", StrictOutput: true}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"Once you've completed the user's task, you must use the attempt_completion tool to present the result.\n\n<attempt_completion>\n<result>done</result>\n</attempt_completion>\n\nfinish"
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Metadata["strict_output"] != "true" {
t.Fatalf("strict_output metadata: got %q", fake.req.Metadata["strict_output"])
}
if fake.req.Input["think"] != false {
t.Fatalf("strict output should disable thinking by default: %+v", fake.req.Input)
}
if !strings.Contains(fake.req.Prompt, "output exactly one <attempt_completion> block") {
t.Fatalf("strict contract instruction not injected:\n%s", fake.req.Prompt)
}
var resp responsesResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
want := "<attempt_completion>\n<result>plain answer</result>\n</attempt_completion>"
if resp.OutputText != want {
t.Fatalf("output_text:\ngot %q\nwant %q", resp.OutputText, want)
}
if len(resp.Output) == 0 || len(resp.Output[0].Content) == 0 || resp.Output[0].Content[0].Text != want {
t.Fatalf("output[0].content[0].text wrong: %+v", resp.Output)
}
}
func TestCollectRunResultTimesOut(t *testing.T) {
handle := &edgeservice.RunHandle{
RunDispatch: edgeservice.RunDispatch{TimeoutSec: 1},
RunStream: edgeservice.RunStream{
Events: make(chan *iop.RunEvent),
NodeEvents: make(chan *iop.EdgeNodeEvent),
},
}
ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond)
defer cancel()
_, _, _, _, _, _, err := collectRunResult(ctx, handle.Stream(), handle.WaitTimeout())
if err == nil {
t.Fatal("expected timeout error")
}
}
// fakeRunResultWithTimeout implements edgeservice.RunResult with a directly
// configurable WaitTimeout, so tests can exercise the run-timeout cancel path
// without waiting out the real DefaultTimeoutSec-derived duration.
type fakeRunResultWithTimeout struct {
dispatch edgeservice.RunDispatch
stream edgeservice.RunStream
waitTimeout time.Duration
}
func (f *fakeRunResultWithTimeout) Dispatch() edgeservice.RunDispatch { return f.dispatch }
func (f *fakeRunResultWithTimeout) Stream() edgeservice.RunStream { return f.stream }
func (f *fakeRunResultWithTimeout) Close() {}
func (f *fakeRunResultWithTimeout) WaitTimeout() time.Duration { return f.waitTimeout }
func TestChatCompletionContextCancelSendsCancelRun(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent)}
srv := NewServer(config.EdgeOpenAIConf{
Adapter: "ollama",
Target: "llama-fixed",
SessionID: "cline",
TimeoutSec: 15,
}, fake, nil)
ctx, cancel := context.WithCancel(context.Background())
cancel()
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"client-model",
"messages":[{"role":"user","content":"hi"}]
}`)).WithContext(ctx)
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusRequestTimeout {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
calls := fake.cancelCallsSnapshot()
if len(calls) != 1 {
t.Fatalf("expected 1 CancelRun call, got %d: %+v", len(calls), calls)
}
if calls[0].RunID != "run-test" || calls[0].Adapter != "ollama" || calls[0].Target != "llama-fixed" || calls[0].SessionID != "cline" {
t.Fatalf("unexpected cancel request: %+v", calls[0])
}
}
func TestResponsesContextCancelSendsCancelRun(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent)}
srv := NewServer(config.EdgeOpenAIConf{
Adapter: "ollama",
Target: "llama-fixed",
SessionID: "cline",
TimeoutSec: 15,
}, fake, nil)
ctx, cancel := context.WithCancel(context.Background())
cancel()
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"say hello"
}`)).WithContext(ctx)
w := httptest.NewRecorder()
srv.handleResponses(w, req)
if w.Code != http.StatusRequestTimeout {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
calls := fake.cancelCallsSnapshot()
if len(calls) != 1 {
t.Fatalf("expected 1 CancelRun call, got %d: %+v", len(calls), calls)
}
if calls[0].RunID != "run-test" || calls[0].Adapter != "ollama" || calls[0].Target != "llama-fixed" || calls[0].SessionID != "cline" {
t.Fatalf("unexpected cancel request: %+v", calls[0])
}
}
func TestStreamChatCompletionContextCancelSendsCancelRun(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent)}
srv := NewServer(config.EdgeOpenAIConf{
Adapter: "ollama",
Target: "llama-fixed",
SessionID: "cline",
TimeoutSec: 15,
}, fake, nil)
ctx, cancel := context.WithCancel(context.Background())
cancel()
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"client-model",
"stream":true,
"messages":[{"role":"user","content":"hi"}]
}`)).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-test" || calls[0].Adapter != "ollama" || calls[0].Target != "llama-fixed" || calls[0].SessionID != "cline" {
t.Fatalf("unexpected cancel request: %+v", calls[0])
}
}
func TestStreamChatCompletionTimeoutSendsCancelRun(t *testing.T) {
fake := &fakeRunService{}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
handle := &fakeRunResultWithTimeout{
dispatch: edgeservice.RunDispatch{
RunID: "run-timeout",
NodeID: "node-1",
Adapter: "ollama",
Target: "llama3",
SessionID: "cline",
},
stream: edgeservice.RunStream{
Events: make(chan *iop.RunEvent),
NodeEvents: make(chan *iop.EdgeNodeEvent),
},
waitTimeout: 50 * time.Millisecond,
}
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
w := httptest.NewRecorder()
srv.streamChatCompletion(w, req, chatCompletionRequest{Model: "llama3"}, edgeservice.SubmitRunRequest{}, handle, strictOutputPolicy{}, toolValidationContract{})
calls := fake.cancelCallsSnapshot()
if len(calls) != 1 {
t.Fatalf("expected 1 CancelRun call, got %d: %+v", len(calls), calls)
}
if calls[0].RunID != "run-timeout" || calls[0].NodeRef != "node-1" {
t.Fatalf("unexpected cancel request: %+v", calls[0])
}
if !strings.Contains(w.Body.String(), "run timed out") {
t.Fatalf("expected timeout SSE error, got %s", w.Body.String())
}
}
func TestChatCompletionCompleteDoesNotSendCancelRun(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello"}
fake.events <- &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":"client-model",
"messages":[{"role":"user","content":"hi"}]
}`))
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 calls := fake.cancelCallsSnapshot(); len(calls) != 0 {
t.Fatalf("expected no CancelRun calls for a normal completion, got %d: %+v", len(calls), calls)
}
}
func TestModelsExposesCatalogRouteModels(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "model-a", Adapter: "ollama", Target: "llama3"},
{Model: "model-b", Adapter: "vllm", Target: "qwen"},
},
}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
w := httptest.NewRecorder()
srv.handleModels(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d", w.Code)
}
body := w.Body.String()
if !strings.Contains(body, `"id":"model-a"`) || !strings.Contains(body, `"id":"model-b"`) {
t.Fatalf("expected catalog model IDs, got %s", body)
}
}
func TestModelsSkipsRoutesWithEmptyTarget(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "model-with-target", Adapter: "ollama", Target: "llama3"},
{Model: "model-no-target", Adapter: "ollama", Target: ""},
},
}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
w := httptest.NewRecorder()
srv.handleModels(w, req)
body := w.Body.String()
if !strings.Contains(body, `"id":"model-with-target"`) {
t.Fatalf("expected model-with-target in response, got %s", body)
}
if strings.Contains(body, "model-no-target") {
t.Fatalf("model-no-target should be skipped (no target), got %s", body)
}
}
func TestModelsRouteCatalogWinsOverModelsField(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{
Models: []string{"legacy-model"},
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "catalog-model", Adapter: "ollama", Target: "llama3"},
},
}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
w := httptest.NewRecorder()
srv.handleModels(w, req)
body := w.Body.String()
if !strings.Contains(body, `"id":"catalog-model"`) {
t.Fatalf("expected catalog model in response, got %s", body)
}
if strings.Contains(body, "legacy-model") {
t.Fatalf("legacy model should be hidden when catalog is set, got %s", body)
}
}
func TestChatCompletionsRouteCatalogDispatches(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "model-a", Adapter: "ollama", Target: "llama3"},
{Model: "model-b", Adapter: "vllm", Target: "qwen"},
},
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"model-a",
"messages":[{"role":"user","content":"hi"}]
}`))
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 fake.req.Adapter != "ollama" || fake.req.Target != "llama3" {
t.Fatalf("dispatch mismatch: adapter=%q target=%q", fake.req.Adapter, fake.req.Target)
}
}
func TestChatCompletionsRouteCatalogDispatchesModelB(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "model-a", Adapter: "ollama", Target: "llama3"},
{Model: "model-b", Adapter: "ollama", Target: "qwen", MaxQueue: 5, QueueTimeoutMS: 2000},
},
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"model-b",
"messages":[{"role":"user","content":"hi"}]
}`))
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 fake.req.Adapter != "ollama" || fake.req.Target != "qwen" {
t.Fatalf("dispatch mismatch: adapter=%q target=%q", fake.req.Adapter, fake.req.Target)
}
if fake.req.ModelGroupKey != "model-b" {
t.Fatalf("model group key: got %q, want model-b", fake.req.ModelGroupKey)
}
if fake.req.MaxQueue != 5 || fake.req.QueueTimeoutMS != 2000 {
t.Fatalf("queue policy mismatch: MaxQueue=%d, QueueTimeoutMS=%d", fake.req.MaxQueue, fake.req.QueueTimeoutMS)
}
}
func TestChatCompletionsCatalogMissFallsToLegacyTarget(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{
Adapter: "ollama",
Target: "legacy-target",
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "model-a", Adapter: "ollama", Target: "llama3"},
},
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"unknown-model",
"messages":[{"role":"user","content":"hi"}]
}`))
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 fake.req.Target != "legacy-target" {
t.Fatalf("expected legacy-target fallback, got %q", fake.req.Target)
}
}
func TestChatCompletionsEmptyModelReturns400(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"",
"messages":[{"role":"user","content":"hi"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400 for empty model, got %d body=%s", w.Code, w.Body.String())
}
}
func TestResponsesRouteCatalogDispatchesRoute(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "model-a", Adapter: "ollama", Target: "qwen"},
},
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"model-a",
"input":"say hello"
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Adapter != "ollama" || fake.req.Target != "qwen" {
t.Fatalf("dispatch mismatch: adapter=%q target=%q", fake.req.Adapter, fake.req.Target)
}
if fake.req.ModelGroupKey != "model-a" {
t.Fatalf("model group key: got %q, want model-a", fake.req.ModelGroupKey)
}
}
func TestResponsesRouteCatalogDispatchesQueuePolicy(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "model-a", Adapter: "ollama", Target: "route-target", MaxQueue: 3, QueueTimeoutMS: 1500},
},
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"model-a",
"input":"test"
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Target != "route-target" {
t.Fatalf("expected route-target, got %q", fake.req.Target)
}
if fake.req.ModelGroupKey != "model-a" {
t.Fatalf("model group key: got %q, want model-a", fake.req.ModelGroupKey)
}
if fake.req.MaxQueue != 3 || fake.req.QueueTimeoutMS != 1500 {
t.Fatalf("queue policy mismatch: MaxQueue=%d, QueueTimeoutMS=%d", fake.req.MaxQueue, fake.req.QueueTimeoutMS)
}
}
func TestChatCompletionsRejectsNumCtxOptionsWrapper(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"from-request",
"messages":[{"role":"user","content":"hi"}],
"options":{"num_ctx":8192}
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("got %d want 400, body=%s", w.Code, w.Body.String())
}
}
func TestResolveRouteDispatchPreservesWorkspaceRequired(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "codex", Adapter: "cli", Target: "codex", WorkspaceRequired: true},
{Model: "llama3", Adapter: "ollama", Target: "llama3:8b"},
},
}, &fakeRunService{}, nil)
dispatch, ok := srv.resolveRouteDispatch("codex")
if !ok {
t.Fatal("expected dispatch to succeed for codex route")
}
if !dispatch.WorkspaceRequired {
t.Fatalf("expected workspace_required=true for codex route, got false")
}
dispatch, ok = srv.resolveRouteDispatch("llama3")
if !ok {
t.Fatal("expected dispatch to succeed for llama3 route")
}
if dispatch.WorkspaceRequired {
t.Fatalf("expected workspace_required=false for llama3 route, got true")
}
}
func TestResolveRouteDispatchFallbackWorkspaceRequiredFalse(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{
Adapter: "ollama",
Target: "llama3",
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "codex", Adapter: "cli", Target: "codex", WorkspaceRequired: true},
},
}, &fakeRunService{}, nil)
dispatch, ok := srv.resolveRouteDispatch("unknown-model")
if !ok {
t.Fatal("expected fallback dispatch to succeed")
}
if dispatch.WorkspaceRequired {
t.Fatalf("expected workspace_required=false for fallback route, got true")
}
}
func TestCollectRunResultFailsWhenEventStreamCloses(t *testing.T) {
events := make(chan *iop.RunEvent)
close(events)
handle := &edgeservice.RunHandle{
RunDispatch: edgeservice.RunDispatch{TimeoutSec: 60},
RunStream: edgeservice.RunStream{
Events: events,
NodeEvents: make(chan *iop.EdgeNodeEvent),
},
}
_, _, _, _, _, _, err := collectRunResult(context.Background(), handle.Stream(), handle.WaitTimeout())
if err == nil {
t.Fatal("expected closed stream error")
}
if !strings.Contains(err.Error(), "run stream closed") {
t.Fatalf("expected run stream closed error, got %v", err)
}
}
// workspaceBoundCfg returns a server config with a workspace-required codex route and a
// non-required llama3 route for workspace validation tests.
func workspaceBoundCfg() config.EdgeOpenAIConf {
return config.EdgeOpenAIConf{
Adapter: "ollama",
Target: "llama3",
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "codex", Adapter: "cli", Target: "codex", WorkspaceRequired: true},
{Model: "llama3", Adapter: "ollama", Target: "llama3"},
},
}
}
func TestResponsesWorkspaceRequiredRouteMissingWorkspace400(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent)}
srv := NewServer(workspaceBoundCfg(), fake, nil)
body := `{"model":"codex","input":"hello"}`
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
w := httptest.NewRecorder()
srv.handleResponses(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("missing workspace: want 400, got %d body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "workspace is required") {
t.Fatalf("expected workspace error, got %s", w.Body.String())
}
}
func TestResponsesWorkspaceRequiredRouteRelativeWorkspace400(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent)}
srv := NewServer(workspaceBoundCfg(), fake, nil)
body := `{"model":"codex","input":"hello","metadata":{"workspace":"relative/path"}}`
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
w := httptest.NewRecorder()
srv.handleResponses(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("relative workspace: want 400, got %d body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "absolute path") {
t.Fatalf("expected absolute path error, got %s", w.Body.String())
}
}
func TestResponsesWorkspaceRequiredRouteAbsoluteWorkspaceOK(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(workspaceBoundCfg(), fake, nil)
body := `{"model":"codex","input":"hello","metadata":{"workspace":"/abs/path"}}`
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
w := httptest.NewRecorder()
srv.handleResponses(w, req)
if w.Code != http.StatusOK {
t.Fatalf("absolute workspace: want 200, got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Workspace != "/abs/path" {
t.Fatalf("workspace not preserved: got %q", fake.req.Workspace)
}
}
func TestResponsesNonRequiredRouteNoWorkspaceOK(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(workspaceBoundCfg(), fake, nil)
body := `{"model":"llama3","input":"hello"}`
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
w := httptest.NewRecorder()
srv.handleResponses(w, req)
if w.Code != http.StatusOK {
t.Fatalf("non-required route no workspace: want 200, got %d body=%s", w.Code, w.Body.String())
}
}
func TestResponsesSurfacesDistinctRunFailures(t *testing.T) {
cases := []struct {
name string
runError string
want []string
forbidAny []string
}{
{
name: "missing workspace path from node",
runError: "cli adapter: workspace not found: /abs/missing",
want: []string{`"type":"run_error"`, "cli adapter: workspace not found: /abs/missing"},
},
{
name: "inaccessible workspace path from node",
runError: "cli adapter: workspace inaccessible: /abs/private: permission denied",
want: []string{`"type":"run_error"`, "cli adapter: workspace inaccessible: /abs/private"},
},
{
name: "agent process exit failure",
runError: "command failed: exit status 7",
want: []string{`"type":"run_error"`, "command failed: exit status 7"},
forbidAny: []string{"workspace not found", "workspace inaccessible", "workspace is not a directory"},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 1)}
fake.events <- &iop.RunEvent{Type: "error", Error: tc.runError}
srv := NewServer(workspaceBoundCfg(), fake, nil)
body := `{"model":"codex","input":"hello","metadata":{"workspace":"/abs/workspace"}}`
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
w := httptest.NewRecorder()
srv.handleResponses(w, req)
if w.Code != http.StatusBadGateway {
t.Fatalf("run failure: want 502, got %d body=%s", w.Code, w.Body.String())
}
for _, want := range tc.want {
if !strings.Contains(w.Body.String(), want) {
t.Fatalf("expected body to contain %q, got %s", want, w.Body.String())
}
}
for _, forbidden := range tc.forbidAny {
if strings.Contains(w.Body.String(), forbidden) {
t.Fatalf("body should not contain %q, got %s", forbidden, w.Body.String())
}
}
})
}
}
func TestChatCompletionsWorkspaceRequiredRouteMissingWorkspace400(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent)}
srv := NewServer(workspaceBoundCfg(), fake, nil)
body := `{"model":"codex","messages":[{"role":"user","content":"hi"}]}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("missing workspace: want 400, got %d body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "workspace is required") {
t.Fatalf("expected workspace error, got %s", w.Body.String())
}
}
func TestChatCompletionsWorkspaceRequiredRouteRelativeWorkspace400(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent)}
srv := NewServer(workspaceBoundCfg(), fake, nil)
body := `{"model":"codex","messages":[{"role":"user","content":"hi"}],"metadata":{"workspace":"some/relative"}}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("relative workspace: want 400, got %d body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "absolute path") {
t.Fatalf("expected absolute path error, got %s", w.Body.String())
}
}
func TestChatCompletionsWorkspaceRequiredRouteAbsoluteWorkspaceOK(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(workspaceBoundCfg(), fake, nil)
body := `{"model":"codex","messages":[{"role":"user","content":"hi"}],"metadata":{"workspace":"/abs/workspace"}}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("absolute workspace: want 200, got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Workspace != "/abs/workspace" {
t.Fatalf("workspace not preserved: got %q", fake.req.Workspace)
}
}
func TestChatCompletionsNonRequiredRouteNoWorkspaceOK(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(workspaceBoundCfg(), fake, nil)
body := `{"model":"llama3","messages":[{"role":"user","content":"hi"}]}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("non-required route no workspace: want 200, got %d body=%s", w.Code, w.Body.String())
}
}
// TestHandleModelsProviderPoolCatalog verifies that /v1/models returns the
// provider-pool catalog IDs when a catalog is set, ignoring legacy model_routes.
func TestHandleModelsProviderPoolCatalog(t *testing.T) {
catalog := []config.ModelCatalogEntry{
{ID: "qwen3.6:35b", Providers: map[string]string{"prov-1": "Qwen3-35B-A22B"}},
{ID: "llama3.3:70b", Providers: map[string]string{"prov-2": "llama-3.3-70b"}},
}
srv := NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "legacy-model", Target: "legacy-target"},
},
}, &fakeRunService{}, nil)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
w := httptest.NewRecorder()
srv.handleModels(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, "qwen3.6:35b") || !strings.Contains(body, "llama3.3:70b") {
t.Fatalf("catalog models not listed: %s", body)
}
if strings.Contains(body, "legacy-model") {
t.Fatalf("legacy model_routes should be suppressed when catalog is set: %s", body)
}
}
// TestChatCompletionsProviderPoolDispatch verifies that when a request model
// matches the provider-pool catalog with omitted response mode, the request is
// dispatched over the provider tunnel with ProviderPool=true and Adapter/Target
// left empty for service-layer resolution (pure passthrough default, SDD D02).
func TestChatCompletionsProviderPoolDispatch(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 := &fakeRunService{tunnelFrames: frames}
catalog := []config.ModelCatalogEntry{
{ID: "qwen3.6:35b", Providers: map[string]string{"prov-vllm": "Qwen3-35B-A22B"}},
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"qwen3.6:35b",
"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 1 tunnel dispatch, got %d (SubmitRun calls: %d)", len(tunnelReqs), len(fake.reqsSnapshot()))
}
treq := tunnelReqs[0]
if !treq.ProviderPool {
t.Error("ProviderPool should be true for catalog-matched model")
}
if treq.ModelGroupKey != "qwen3.6:35b" {
t.Errorf("ModelGroupKey: got %q, want qwen3.6:35b", treq.ModelGroupKey)
}
if treq.Adapter != "" || treq.Target != "" {
t.Errorf("Adapter/Target should be empty for provider-pool dispatch, got %q/%q", treq.Adapter, treq.Target)
}
if treq.Method != http.MethodPost || treq.Path != "/v1/chat/completions" {
t.Errorf("tunnel method/path: got %q %q", treq.Method, treq.Path)
}
if len(fake.reqsSnapshot()) != 0 {
t.Errorf("normalized SubmitRun must not be called for omitted-mode provider route")
}
}
func TestChatCompletionsStrictOutputProviderPoolDefaultKeepsPassthroughPath(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 := &fakeRunService{tunnelFrames: frames}
catalog := []config.ModelCatalogEntry{
{ID: "qwen3.6:35b", Providers: map[string]string{"prov-vllm": "Qwen3-35B-A22B"}},
}
srv := NewServer(config.EdgeOpenAIConf{StrictOutput: true}, fake, nil)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"hello"}],
"include_reasoning": true
}`))
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()) != 1 {
t.Fatalf("strict output default must keep provider routes on passthrough, got %d tunnel dispatches", len(fake.tunnelReqsSnapshot()))
}
if len(fake.reqsSnapshot()) != 0 {
t.Fatalf("strict output provider route must not fall back to normalized SubmitRun, got %d calls", len(fake.reqsSnapshot()))
}
if got := w.Header().Get(responseModeHeaderName); got != "" {
t.Fatalf("pure passthrough must not carry response mode header, got %q", got)
}
}
func TestChatCompletionsResponseModeRouting(t *testing.T) {
cases := []struct {
name string
metadata string
wantStatus int
wantTunnel bool
wantRun bool
wantMessage string
}{
{
name: "explicit passthrough uses tunnel",
metadata: `"metadata":{"iop_response_mode":"passthrough"}`,
wantStatus: http.StatusOK,
wantTunnel: true,
},
{
name: "sideband mode uses tunnel with IOP extension surface",
metadata: `"metadata":{"iop_response_mode":"passthrough+sideband"}`,
wantStatus: http.StatusOK,
wantTunnel: true,
},
{
name: "transformed mode is rejected for provider route",
metadata: `"metadata":{"iop_response_mode":"transformed"}`,
wantStatus: http.StatusBadRequest,
wantMessage: "metadata.iop_response_mode=transformed is not supported for OpenAI-compatible provider model groups",
},
{
name: "unknown mode fails fast",
metadata: `"metadata":{"iop_response_mode":"rawish"}`,
wantStatus: http.StatusBadRequest,
wantMessage: "metadata.iop_response_mode must be one of passthrough, passthrough+sideband, or transformed",
},
}
for _, tc := range cases {
t.Run(tc.name, func(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 := &fakeRunService{
events: bufferedRunEvents(
&iop.RunEvent{Type: "delta", Delta: "ok"},
&iop.RunEvent{Type: "complete"},
),
tunnelFrames: frames,
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
})
body := `{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}],
` + tc.metadata + `
}`
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(body))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != tc.wantStatus {
t.Fatalf("status: got %d want %d body=%s", w.Code, tc.wantStatus, w.Body.String())
}
if tc.wantMessage != "" && !strings.Contains(w.Body.String(), tc.wantMessage) {
t.Fatalf("expected %q in body, got %s", tc.wantMessage, w.Body.String())
}
if got := len(fake.tunnelReqsSnapshot()) > 0; got != tc.wantTunnel {
t.Fatalf("tunnel dispatch: got %v want %v", got, tc.wantTunnel)
}
if got := len(fake.reqsSnapshot()) > 0; got != tc.wantRun {
t.Fatalf("normalized dispatch: got %v want %v", got, tc.wantRun)
}
})
}
}
// chatSidebandServer returns an openai.Server backed by the given tunnel
// frames for explicit passthrough+sideband fixtures.
func chatSidebandServer(frames chan *iop.ProviderTunnelFrame) (*Server, *fakeRunService) {
fake := &fakeRunService{tunnelFrames: frames}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
})
return srv, fake
}
// TestChatCompletionsPassthroughSidebandStreamingExposesIOPExtension verifies
// SDD S05 for the streaming path: explicit passthrough+sideband preserves the
// provider SSE events contiguously and exposes IOP route/usage/assembled
// observations as explicit `event: iop.sideband` extension events, injected
// only at provider event boundaries.
func TestChatCompletionsPassthroughSidebandStreamingExposesIOPExtension(t *testing.T) {
roleEvent := "data: {\"choices\":[{\"delta\":{\"role\":\"assistant\"}}]}\n\n"
contentEvent := "data: {\"choices\":[{\"delta\":{\"content\":\"hello\"}}]}\n\n"
doneEvent := "data: [DONE]\n\n"
frames := make(chan *iop.ProviderTunnelFrame, 7)
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(roleEvent)}
// Split the content event so the usage frame arrives while the provider
// event is incomplete; the sideband writer must not inject there.
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(contentEvent[:17])}
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_BODY, Body: []byte(contentEvent[17:])}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(doneEvent)}
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"}],
"metadata":{"iop_response_mode":"passthrough+sideband"}
}`))
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.Header().Get("X-IOP-Response-Mode"); got != "passthrough+sideband" {
t.Errorf("sideband response mode header: got %q", got)
}
body := w.Body.String()
// Provider events reach the caller contiguously: sideband injection never
// splits a provider SSE event.
for _, event := range []string{roleEvent, contentEvent, doneEvent} {
if !strings.Contains(body, event) {
t.Fatalf("provider event not preserved contiguously: %q in %q", event, body)
}
}
if !strings.HasPrefix(body, "event: iop.sideband\n") {
t.Fatalf("stream must open with the IOP route sideband event, got %q", body)
}
routeIdx := strings.Index(body, `"kind":"route"`)
if routeIdx < 0 || routeIdx > strings.Index(body, roleEvent) {
t.Fatalf("route observation must precede provider events: %q", body)
}
for _, marker := range []string{
`"object":"iop.sideband"`,
`"run_id":"run-tunnel"`,
`"node_id":"node-1"`,
`"response_mode":"passthrough+sideband"`,
} {
if !strings.Contains(body, marker) {
t.Errorf("route observation missing %q: %q", marker, body)
}
}
usageIdx := strings.Index(body, `"kind":"usage"`)
contentEnd := strings.Index(body, contentEvent) + len(contentEvent)
if usageIdx < contentEnd {
t.Errorf("usage observation must wait for the provider event boundary: usage at %d, content event ends at %d", usageIdx, contentEnd)
}
if !strings.Contains(body, `"input_tokens":3`) || !strings.Contains(body, `"output_tokens":5`) {
t.Errorf("usage observation missing token counts: %q", body)
}
assembledIdx := strings.Index(body, `"kind":"assembled"`)
if assembledIdx < 0 || assembledIdx < strings.Index(body, doneEvent) {
t.Errorf("assembled observation must close the stream: %q", body)
}
if !strings.Contains(body, `"content":"hello"`) {
t.Errorf("assembled observation missing assembled content: %q", body)
}
tunnelReqs := fake.tunnelReqsSnapshot()
if len(tunnelReqs) != 1 {
t.Fatalf("expected 1 tunnel dispatch, got %d", len(tunnelReqs))
}
if got := tunnelReqs[0].Metadata["iop_response_mode"]; got != "passthrough+sideband" {
t.Errorf("tunnel metadata response mode: got %q", got)
}
if len(fake.reqsSnapshot()) != 0 {
t.Error("sideband mode must not use the normalized SubmitRun path")
}
}
// TestChatCompletionsPassthroughSidebandNonStreamingWrapsProviderBody verifies
// SDD S05 for the non-streaming path: the provider JSON body is carried
// verbatim inside the iop.chat.passthrough_sideband envelope together with
// route/usage/assembled observations, and is not presented as byte identity.
func TestChatCompletionsPassthroughSidebandNonStreamingWrapsProviderBody(t *testing.T) {
providerBody := `{"id":"cmpl-1","object":"chat.completion","choices":[{"index":0,"message":{"role":"assistant","content":"hi","reasoning_content":"because"},"finish_reason":"stop"}]}`
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": "application/json"},
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(providerBody[:23])}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(providerBody[23:])}
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_USAGE,
Usage: &iop.Usage{InputTokens: 7, OutputTokens: 11},
}
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",
"messages":[{"role":"user","content":"hello"}],
"metadata":{"iop_response_mode":"passthrough+sideband"}
}`))
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.Header().Get("X-IOP-Response-Mode"); got != "passthrough+sideband" {
t.Errorf("sideband response mode header: got %q", got)
}
if !strings.Contains(w.Body.String(), providerBody) {
t.Fatalf("provider body must be embedded verbatim: %s", w.Body.String())
}
var envelope struct {
Object string `json:"object"`
Sideband struct {
Route struct {
RunID string `json:"run_id"`
NodeID string `json:"node_id"`
ResponseMode string `json:"response_mode"`
ProviderStatusCode int `json:"provider_status_code"`
} `json:"route"`
Usage *struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
Assembled *struct {
Content string `json:"content"`
Reasoning string `json:"reasoning"`
BodyBytes int `json:"body_bytes"`
} `json:"assembled"`
} `json:"iop_sideband"`
ProviderStatusCode int `json:"provider_status_code"`
ProviderResponse json.RawMessage `json:"provider_response"`
}
if err := json.Unmarshal(w.Body.Bytes(), &envelope); err != nil {
t.Fatalf("envelope not valid JSON: %v body=%s", err, w.Body.String())
}
if envelope.Object != "iop.chat.passthrough_sideband" {
t.Errorf("envelope object: got %q", envelope.Object)
}
if envelope.Sideband.Route.RunID != "run-tunnel" || envelope.Sideband.Route.NodeID != "node-1" {
t.Errorf("route observation: %+v", envelope.Sideband.Route)
}
if envelope.Sideband.Route.ResponseMode != "passthrough+sideband" || envelope.Sideband.Route.ProviderStatusCode != http.StatusOK {
t.Errorf("route observation mode/status: %+v", envelope.Sideband.Route)
}
if envelope.Sideband.Usage == nil || envelope.Sideband.Usage.InputTokens != 7 || envelope.Sideband.Usage.OutputTokens != 11 {
t.Errorf("usage observation: %+v", envelope.Sideband.Usage)
}
if envelope.Sideband.Assembled == nil ||
envelope.Sideband.Assembled.Content != "hi" ||
envelope.Sideband.Assembled.Reasoning != "because" ||
envelope.Sideband.Assembled.BodyBytes != len(providerBody) {
t.Errorf("assembled observation: %+v", envelope.Sideband.Assembled)
}
if envelope.ProviderStatusCode != http.StatusOK {
t.Errorf("provider status: got %d", envelope.ProviderStatusCode)
}
if string(envelope.ProviderResponse) != providerBody {
t.Errorf("provider_response not verbatim:\n got: %s\nwant: %s", envelope.ProviderResponse, providerBody)
}
if len(fake.reqsSnapshot()) != 0 {
t.Error("sideband mode must not use the normalized SubmitRun path")
}
}
// TestChatCompletionsPassthroughDoesNotExposeSideband verifies SDD S06: pure
// passthrough (omitted mode) does not expose usage frames, IOP sideband marker,
// event, or label to the caller.
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())
}
if got := w.Body.String(); got != providerBody {
t.Fatalf("pure passthrough body not byte-identical:\n got: %q\nwant: %q", got, providerBody)
}
for _, marker := range []string{"iop.sideband", "iop_sideband", "iop_response_mode"} {
if strings.Contains(w.Body.String(), marker) {
t.Errorf("pure passthrough body must not contain %q", marker)
}
}
if got := w.Header().Get("X-IOP-Response-Mode"); got != "" {
t.Errorf("pure passthrough must not carry the IOP response mode header, got %q", got)
}
}
// chatProviderAuthServer builds a provider-pool tunnel server with the given
// provider auth forwarding config, mirroring chatSidebandServer's catalog so
// pool-model routes over the provider tunnel path.
func chatProviderAuthServer(frames chan *iop.ProviderTunnelFrame, auth config.EdgeOpenAIProviderAuthConf) (*Server, *fakeRunService) {
fake := &fakeRunService{tunnelFrames: frames}
srv := NewServer(config.EdgeOpenAIConf{ProviderAuth: auth}, fake, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{
{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
})
return srv, fake
}
func staticOKTunnelFrames(body string) chan *iop.ProviderTunnelFrame {
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(body)}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
return frames
}
// TestChatProviderTunnelForwardsProviderAuthHeader verifies SDD S03: a raw user
// token supplied in the configured provider auth request header is forwarded to
// the provider tunnel request as the configured target header, formatted with
// the configured scheme, while already-prefixed values are preserved.
func TestChatProviderTunnelForwardsProviderAuthHeader(t *testing.T) {
auth := config.EdgeOpenAIProviderAuthConf{
Enabled: true,
FromHeader: "X-IOP-Provider-Authorization",
TargetHeader: "Authorization",
Scheme: "Bearer",
Required: true,
}
for _, tc := range []struct {
name string
callerAuth string
wantHeader string
}{
{name: "raw token gets scheme", callerAuth: "user-token", wantHeader: "Bearer user-token"},
{name: "already prefixed preserved", callerAuth: "Bearer user-token", wantHeader: "Bearer user-token"},
} {
t.Run(tc.name, func(t *testing.T) {
srv, fake := chatProviderAuthServer(staticOKTunnelFrames(`{"ok":true}`), auth)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}]
}`))
req.Header.Set("X-IOP-Provider-Authorization", tc.callerAuth)
w := httptest.NewRecorder()
srv.handleChatCompletions(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 got := reqs[0].Headers["Authorization"]; got != tc.wantHeader {
t.Fatalf("provider Authorization header: got %q, want %q", got, tc.wantHeader)
}
})
}
}
// TestChatProviderTunnelProviderAuthRequired verifies SDD S03: a missing
// required provider auth header is rejected before dispatch, so no tunnel
// request is submitted.
func TestChatProviderTunnelProviderAuthRequired(t *testing.T) {
auth := config.EdgeOpenAIProviderAuthConf{
Enabled: true,
FromHeader: "X-IOP-Provider-Authorization",
TargetHeader: "Authorization",
Scheme: "Bearer",
Required: true,
}
srv, fake := chatProviderAuthServer(staticOKTunnelFrames(`{"ok":true}`), auth)
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.StatusBadRequest {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if strings.Contains(w.Body.String(), "user-token") {
t.Fatalf("error body must not echo a raw token: %s", w.Body.String())
}
if got := len(fake.tunnelReqsSnapshot()); got != 0 {
t.Fatalf("missing required provider auth must not dispatch: got %d tunnel requests", got)
}
}
// TestChatProviderTunnelProviderAuthDoesNotReplaceInboundAuth verifies the
// provider token is sourced only from the configured provider auth header, not
// from the inbound IOP Authorization header, keeping IOP inbound auth and
// outbound provider auth separate.
func TestChatProviderTunnelProviderAuthDoesNotReplaceInboundAuth(t *testing.T) {
auth := config.EdgeOpenAIProviderAuthConf{
Enabled: true,
FromHeader: "X-IOP-Provider-Authorization",
TargetHeader: "Authorization",
Scheme: "Bearer",
Required: true,
}
srv, fake := chatProviderAuthServer(staticOKTunnelFrames(`{"ok":true}`), auth)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}]
}`))
// Inbound IOP auth and the configured provider auth header carry different
// tokens; the provider header must come from the provider auth header only.
req.Header.Set("Authorization", "Bearer iop-inbound-token")
req.Header.Set("X-IOP-Provider-Authorization", "provider-token")
w := httptest.NewRecorder()
srv.handleChatCompletions(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 got := reqs[0].Headers["Authorization"]; got != "Bearer provider-token" {
t.Fatalf("provider Authorization must derive from provider auth header, got %q", got)
}
}
// TestChatCompletionsTransformedModeLabelsIOPOutput verifies SDD S07: explicit
// transformed output uses the normalized path and is labeled as IOP
// transformed output in both the response header and the run metadata.
func TestChatCompletionsTransformedModeLabelsIOPOutput(t *testing.T) {
fake := &fakeRunService{events: bufferedRunEvents(
&iop.RunEvent{Type: "delta", Delta: "ok"},
&iop.RunEvent{Type: "complete"},
)}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "backend-model"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"client-model",
"messages":[{"role":"user","content":"hello"}],
"metadata":{"iop_response_mode":"transformed"}
}`))
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.Header().Get("X-IOP-Response-Mode"); got != "transformed" {
t.Errorf("transformed label header: got %q", got)
}
if !strings.Contains(w.Body.String(), `"object":"chat.completion"`) {
t.Errorf("transformed output must be the normalized IOP response: %s", w.Body.String())
}
if len(fake.tunnelReqsSnapshot()) != 0 {
t.Error("transformed mode must not use the tunnel")
}
reqs := fake.reqsSnapshot()
if len(reqs) != 1 {
t.Fatalf("expected 1 normalized SubmitRun dispatch, got %d", len(reqs))
}
if got := reqs[0].Metadata["iop_response_mode"]; got != "transformed" {
t.Errorf("run metadata response mode: got %q", got)
}
}
// TestChatCompletionsPassthroughDoesNotLabelTransformed verifies pure
// passthrough responses carry no IOP transformed label.
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())
}
if got := w.Body.String(); got != providerBody {
t.Fatalf("pure passthrough body not byte-identical: %q", got)
}
if got := w.Header().Get("X-IOP-Response-Mode"); got != "" {
t.Errorf("pure passthrough must not carry the IOP response mode header, got %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, *fakeRunService) {
fake := &fakeRunService{
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)
}
}
// TestChatCompletionsPassthroughProviderPoolGenerationPolicy verifies that
// provider-pool omitted-mode passthrough requests apply the catalog entry's
// generation policy (default_max_tokens, min_max_tokens, default_thinking_token_budget)
// before dispatching to the provider. The fixture omits a top-level model echo,
// so the response body remains byte-identical.
func TestChatCompletionsPassthroughProviderPoolGenerationPolicy(t *testing.T) {
providerBody := `{"id":"cmpl-provider-1","object":"chat.completion","choices":[{"index":0,"message":{"role":"assistant","content":"hi"},"finish_reason":"stop"}]}`
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()
fake := &fakeRunService{
tunnelProviderURL: provider.URL,
tunnelServedTarget: "served-model",
}
// Enable strict output to trigger think=true from policy
srv := NewServer(config.EdgeOpenAIConf{StrictOutput: true}, fake, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{
{
ID: "pool-model",
Providers: map[string]string{"prov-1": "served-model"},
DefaultMaxTokens: 100,
MinMaxTokens: 50,
DefaultThinkingTokenBudget: 30,
},
})
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}],
"max_completion_tokens":20,
"metadata":{"iop_response_mode":"passthrough"}
}`))
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)
}
var parsedReq map[string]interface{}
if err := json.Unmarshal(gotProviderReq, &parsedReq); err != nil {
t.Fatalf("failed to parse provider request: %v", err)
}
if model, ok := parsedReq["model"].(string); !ok || model != "served-model" {
t.Errorf("expected model served-model, got %v", parsedReq["model"])
}
// MinMaxTokens (50) should reach max_tokens (overriding max_completion_tokens 20)
if maxTokens, ok := parsedReq["max_tokens"].(float64); !ok || maxTokens != 50 {
t.Errorf("expected max_tokens 50, got %v", parsedReq["max_tokens"])
}
if _, ok := parsedReq["max_completion_tokens"]; ok {
t.Errorf("max_completion_tokens should be deleted")
}
// DefaultThinkingTokenBudget (30) and Think (true) should reach provider
if budget, ok := parsedReq["thinking_token_budget"].(float64); !ok || budget != 30 {
t.Errorf("expected thinking_token_budget 30, got %v", parsedReq["thinking_token_budget"])
}
if think, ok := parsedReq["think"].(bool); !ok || !think {
t.Errorf("expected think true, got %v", parsedReq["think"])
}
}
// 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 := &fakeRunService{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)
}
}
// TestChatCompletionsLegacyProviderRouteRejectsTransformedMode verifies that
// explicit transformed mode cannot send a legacy provider route back through
// the normalized RunEvent path.
func TestChatCompletionsLegacyProviderRouteRejectsTransformedMode(t *testing.T) {
fake := &fakeRunService{events: bufferedRunEvents(
&iop.RunEvent{Type: "delta", Delta: "ok"},
&iop.RunEvent{Type: "complete"},
)}
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"}],
"metadata":{"iop_response_mode":"transformed"}
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "metadata.iop_response_mode=transformed is not supported for OpenAI-compatible provider model groups") {
t.Fatalf("expected transformed rejection, got %s", w.Body.String())
}
if len(fake.tunnelReqsSnapshot()) != 0 {
t.Error("rejected transformed mode must not use the tunnel")
}
if len(fake.reqsSnapshot()) != 0 {
t.Fatalf("rejected transformed mode must not use SubmitRun, got %d calls", len(fake.reqsSnapshot()))
}
}
// TestChatCompletionsPassthroughNonProviderRouteKeepsNormalizedPath verifies
// that CLI/ollama legacy routes are unaffected by the passthrough default.
func TestChatCompletionsPassthroughNonProviderRouteKeepsNormalizedPath(t *testing.T) {
fake := &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 := &fakeRunService{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 := &fakeRunService{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 := &fakeRunService{
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)
}
}
// TestChatCompletionsPassthroughSidebandStreamWriteFailureSendsCancelRunOnce
// verifies the sideband streaming writer propagates exactly one CancelRun when
// a provider body write fails mid-stream and stops relaying afterwards.
func TestChatCompletionsPassthroughSidebandStreamWriteFailureSendsCancelRunOnce(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_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"}],
"metadata":{"iop_response_mode":"passthrough+sideband"}
}`))
// Write 1 is the route sideband event, write 2 the first provider chunk;
// the second provider chunk fails.
w := newFlakyResponseWriter(2)
srv.handleChatCompletions(w, req)
status, body, failedWrites := w.snapshot()
if status != http.StatusOK {
t.Fatalf("status: got %d body=%s", status, body)
}
if failedWrites == 0 {
t.Fatal("fixture did not exercise the sideband write failure path")
}
if !strings.Contains(body, "data: chunk-1\n\n") || strings.Contains(body, "chunk-2") {
t.Fatalf("sideband write failure must truncate after the ordered prefix: %q", body)
}
calls := fake.cancelCallsSnapshot()
if len(calls) != 1 {
t.Fatalf("expected exactly 1 CancelRun call on sideband 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])
}
}
// TestChatCompletionsPassthroughSidebandStreamContextCancelSendsCancelRun
// verifies caller disconnect on the sideband streaming surface propagates
// exactly one CancelRun while the tunnel is still in flight.
func TestChatCompletionsPassthroughSidebandStreamContextCancelSendsCancelRun(t *testing.T) {
fake := &fakeRunService{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",
"stream":true,
"messages":[{"role":"user","content":"hello"}],
"metadata":{"iop_response_mode":"passthrough+sideband"}
}`)).WithContext(ctx)
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
calls := fake.cancelCallsSnapshot()
if len(calls) != 1 {
t.Fatalf("expected exactly 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])
}
}
// TestChatCompletionsPassthroughSidebandNonStreamingContextCancelSendsCancelRun
// verifies caller disconnect on the buffered sideband surface propagates
// exactly one CancelRun while the tunnel is still in flight.
func TestChatCompletionsPassthroughSidebandNonStreamingContextCancelSendsCancelRun(t *testing.T) {
fake := &fakeRunService{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"}],
"metadata":{"iop_response_mode":"passthrough+sideband"}
}`)).WithContext(ctx)
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
calls := fake.cancelCallsSnapshot()
if len(calls) != 1 {
t.Fatalf("expected exactly 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])
}
}
// TestChatCompletionsPassthroughSidebandNonStreamingTimeoutSendsCancelRun
// verifies the buffered sideband surface converts a tunnel wait timeout into a
// 502 and exactly one CancelRun.
func TestChatCompletionsPassthroughSidebandNonStreamingTimeoutSendsCancelRun(t *testing.T) {
fake := &fakeRunService{
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"}],
"metadata":{"iop_response_mode":"passthrough+sideband"}
}`))
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 sideband timeout, got %d: %+v", len(calls), calls)
}
}
func TestChatCompletionsProviderPoolAppliesGenerationPolicy(t *testing.T) {
fake := &fakeRunService{
tunnelFrames: staticProviderTunnelFrames(`{"ok":true}`),
tunnelServedTarget: "Ornith-1.0-35B",
}
catalog := []config.ModelCatalogEntry{{
ID: "ornith:35b",
DefaultMaxTokens: 32768,
MinMaxTokens: 32768,
DefaultThinkingTokenBudget: 8192,
Providers: map[string]string{"prov-vllm": "Ornith-1.0-35B"},
}}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"ornith:35b",
"messages":[{"role":"user","content":"hello"}],
"max_tokens":4096
}`))
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.reqsSnapshot()) != 0 {
t.Fatalf("provider-pool generation policy must stay on tunnel path, got %d SubmitRun calls", len(fake.reqsSnapshot()))
}
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"] != "Ornith-1.0-35B" {
t.Fatalf("served model rewrite not applied: %+v", providerReq["model"])
}
if providerReq["max_tokens"].(float64) != 32768 {
t.Fatalf("max_tokens policy not applied: %+v", providerReq)
}
if providerReq["thinking_token_budget"].(float64) != 8192 {
t.Fatalf("thinking_token_budget policy not applied: %+v", providerReq)
}
}
func TestChatCompletionsProviderPoolThinkingPolicyOverridesStrictOutputDisable(t *testing.T) {
fake := &fakeRunService{
tunnelFrames: staticProviderTunnelFrames(`{"ok":true}`),
tunnelServedTarget: "Ornith-1.0-35B",
}
catalog := []config.ModelCatalogEntry{{
ID: "ornith:35b",
DefaultThinkingTokenBudget: 8192,
Providers: map[string]string{"prov-vllm": "Ornith-1.0-35B"},
}}
srv := NewServer(config.EdgeOpenAIConf{StrictOutput: true}, fake, nil)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"ornith:35b",
"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.reqsSnapshot()) != 0 {
t.Fatalf("provider-pool thinking policy must stay on tunnel path, got %d SubmitRun calls", len(fake.reqsSnapshot()))
}
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["think"] != true {
t.Fatalf("provider-pool thinking policy should keep thinking enabled under strict output: %+v", providerReq)
}
if providerReq["thinking_token_budget"].(float64) != 8192 {
t.Fatalf("thinking_token_budget policy not applied: %+v", providerReq)
}
}
func TestChatCompletionsProviderPoolPreservesLargerGenerationPolicyValues(t *testing.T) {
fake := &fakeRunService{
tunnelFrames: staticProviderTunnelFrames(`{"ok":true}`),
tunnelServedTarget: "Ornith-1.0-35B",
}
catalog := []config.ModelCatalogEntry{{
ID: "ornith:35b",
DefaultMaxTokens: 32768,
MinMaxTokens: 32768,
DefaultThinkingTokenBudget: 8192,
Providers: map[string]string{"prov-vllm": "Ornith-1.0-35B"},
}}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"ornith:35b",
"messages":[{"role":"user","content":"hello"}],
"max_tokens":40000,
"thinking_token_budget":2048
}`))
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.reqsSnapshot()) != 0 {
t.Fatalf("provider-pool generation policy must stay on tunnel path, got %d SubmitRun calls", len(fake.reqsSnapshot()))
}
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["max_tokens"].(float64) != 40000 {
t.Fatalf("larger max_tokens should be preserved: %+v", providerReq)
}
if providerReq["thinking_token_budget"].(float64) != 2048 {
t.Fatalf("explicit thinking_token_budget should be preserved: %+v", providerReq)
}
}
// TestChatCompletionsLegacyRouteSetsProviderPoolFalse verifies that when the
// server is configured with Adapter + Target (no catalog), the dispatched
// SubmitRunRequest has ProviderPool=false so the service uses direct dispatch
// (not the queue admission gate that requires ProviderPool=true).
func TestChatCompletionsLegacyRouteSetsProviderPoolFalse(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hi"}
fake.events <- &iop.RunEvent{Type: "complete"}
// Server with adapter+target only, no catalog — classic legacy route.
srv := NewServer(config.EdgeOpenAIConf{
Adapter: "cli",
Target: "codex",
TimeoutSec: 10,
}, 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 fake.req.ProviderPool {
t.Error("ProviderPool must be false for a legacy adapter+target route with no catalog")
}
if fake.req.Adapter != "cli" || fake.req.Target != "codex" {
t.Errorf("Adapter/Target: got %q/%q, want cli/codex", fake.req.Adapter, fake.req.Target)
}
}
// 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, *fakeRunService) {
fake := &fakeRunService{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
}
// TestResponsesProviderPoolDispatch verifies that /v1/responses sends
// provider-pool models through the raw provider tunnel (POST /v1/responses)
// instead of the normalized RunEvent path.
func TestResponsesProviderPoolDispatch(t *testing.T) {
fake := &fakeRunService{
tunnelFrames: staticProviderTunnelFrames(`{"id":"resp-1","object":"response"}`),
tunnelServedTarget: "model-a",
}
catalog := []config.ModelCatalogEntry{
{ID: "prov-vllm:model-a", Providers: map[string]string{"prov-vllm": "model-a"}},
}
srv := NewServer(config.EdgeOpenAIConf{TimeoutSec: 5}, fake, nil)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"prov-vllm:model-a",
"input":"hello"
}`))
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("provider-pool /v1/responses must not call SubmitRun, got %d calls", 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)
}
}
// TestResponsesProviderPoolPreservesRawOutputTokens verifies that raw
// passthrough preserves the caller's Responses-shaped max_output_tokens and does
// not inject the Chat-shaped max_tokens or generation policy.
func TestResponsesProviderPoolPreservesRawOutputTokens(t *testing.T) {
fake := &fakeRunService{
tunnelFrames: staticProviderTunnelFrames(`{"ok":true}`),
tunnelServedTarget: "Ornith-1.0-35B",
}
catalog := []config.ModelCatalogEntry{{
ID: "ornith:35b",
DefaultMaxTokens: 32768,
MinMaxTokens: 32768,
DefaultThinkingTokenBudget: 8192,
Providers: map[string]string{"prov-vllm": "Ornith-1.0-35B"},
}}
srv := NewServer(config.EdgeOpenAIConf{TimeoutSec: 5}, fake, nil)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"ornith:35b",
"input":"hello",
"max_output_tokens":4096
}`))
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("provider-pool /v1/responses must not call SubmitRun, got %d calls", len(fake.reqsSnapshot()))
}
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"] != "Ornith-1.0-35B" {
t.Fatalf("served model rewrite not applied: %+v", providerReq["model"])
}
if providerReq["max_output_tokens"].(float64) != 4096 {
t.Fatalf("caller max_output_tokens must be preserved: %+v", providerReq)
}
if _, ok := providerReq["max_tokens"]; ok {
t.Fatalf("passthrough must not inject Chat-shaped max_tokens: %+v", providerReq)
}
if _, ok := providerReq["thinking_token_budget"]; ok {
t.Fatalf("passthrough must not inject thinking_token_budget: %+v", providerReq)
}
}
// TestResponsesProviderPoolPassthroughOmitsThinkingPolicy verifies that raw
// passthrough forwards the caller body as-is under strict output: the
// normalized-path thinking policy is not injected into the provider body.
func TestResponsesProviderPoolPassthroughOmitsThinkingPolicy(t *testing.T) {
fake := &fakeRunService{
tunnelFrames: staticProviderTunnelFrames(`{"ok":true}`),
tunnelServedTarget: "Ornith-1.0-35B",
}
catalog := []config.ModelCatalogEntry{{
ID: "ornith:35b",
DefaultThinkingTokenBudget: 8192,
Providers: map[string]string{"prov-vllm": "Ornith-1.0-35B"},
}}
srv := NewServer(config.EdgeOpenAIConf{TimeoutSec: 5, StrictOutput: true}, fake, nil)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"ornith:35b",
"input":"hello"
}`))
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("provider-pool /v1/responses must not call SubmitRun, got %d calls", len(fake.reqsSnapshot()))
}
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["think"]; ok {
t.Fatalf("passthrough must not inject think: %+v", providerReq)
}
if _, ok := providerReq["thinking_token_budget"]; ok {
t.Fatalf("passthrough must not inject thinking_token_budget: %+v", providerReq)
}
}
// 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)
}
}
// TestResponsesProviderTunnelForwardsProviderAuthHeader verifies the provider
// auth helper is applied to the Responses tunnel: a raw request-header token is
// forwarded as the configured target header, and a missing required token is
// rejected before dispatch.
func TestResponsesProviderTunnelForwardsProviderAuthHeader(t *testing.T) {
auth := config.EdgeOpenAIProviderAuthConf{
Enabled: true,
FromHeader: "X-IOP-Provider-Authorization",
TargetHeader: "Authorization",
Scheme: "Bearer",
Required: true,
}
t.Run("forwards raw token with scheme", func(t *testing.T) {
srv, fake := chatProviderAuthServer(staticOKTunnelFrames(`{"ok":true}`), auth)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"pool-model","input":"hi"}`))
req.Header.Set("X-IOP-Provider-Authorization", "user-token")
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 got := reqs[0].Headers["Authorization"]; got != "Bearer user-token" {
t.Fatalf("provider Authorization header: got %q want %q", got, "Bearer user-token")
}
})
t.Run("missing required token rejected", func(t *testing.T) {
srv, fake := chatProviderAuthServer(staticOKTunnelFrames(`{"ok":true}`), auth)
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.StatusBadRequest {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if got := len(fake.tunnelReqsSnapshot()); got != 0 {
t.Fatalf("missing required provider auth must not dispatch: got %d tunnel requests", got)
}
})
}
// 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)
}
}
// TestResponsesProviderTunnelResponseModeRouting documents the Responses
// provider boundary: passthrough and passthrough+sideband use the raw provider
// tunnel, transformed is rejected, and unknown modes fail fast.
func TestResponsesProviderTunnelResponseModeRouting(t *testing.T) {
cases := []struct {
name string
metadata string
wantStatus int
wantTunnel bool
wantMessage string
}{
{
name: "explicit passthrough uses responses tunnel",
metadata: `"metadata":{"iop_response_mode":"passthrough"}`,
wantStatus: http.StatusOK,
wantTunnel: true,
},
{
name: "sideband mode uses responses tunnel",
metadata: `"metadata":{"iop_response_mode":"passthrough+sideband"}`,
wantStatus: http.StatusOK,
wantTunnel: true,
},
{
name: "transformed mode is rejected",
metadata: `"metadata":{"iop_response_mode":"transformed"}`,
wantStatus: http.StatusBadRequest,
wantMessage: "metadata.iop_response_mode=transformed is not supported for /v1/responses provider routes",
},
{
name: "unknown mode fails fast",
metadata: `"metadata":{"iop_response_mode":"rawish"}`,
wantStatus: http.StatusBadRequest,
wantMessage: "metadata.iop_response_mode must be one of passthrough, passthrough+sideband, or transformed",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
srv, fake := responsesProviderTunnelServer(staticProviderTunnelFrames(`{"ok":true}`), "served-model")
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(fmt.Sprintf(`{
"model":"pool-model",
"input":"hi",
%s
}`, tc.metadata)))
w := httptest.NewRecorder()
srv.handleResponses(w, req)
if w.Code != tc.wantStatus {
t.Fatalf("status: got %d want %d body=%s", w.Code, tc.wantStatus, w.Body.String())
}
if tc.wantMessage != "" && !strings.Contains(w.Body.String(), tc.wantMessage) {
t.Fatalf("body must contain %q, got %s", tc.wantMessage, w.Body.String())
}
gotTunnel := len(fake.tunnelReqsSnapshot()) > 0
if gotTunnel != tc.wantTunnel {
t.Fatalf("tunnel dispatch: got %v want %v", gotTunnel, tc.wantTunnel)
}
if len(fake.reqsSnapshot()) != 0 {
t.Fatalf("responses provider route must not call SubmitRun, got %d", len(fake.reqsSnapshot()))
}
})
}
}
func TestResponsesProviderTunnelSidebandInjectsMetadata(t *testing.T) {
frames := staticProviderTunnelFrames(`{"id":"resp-1","object":"response","output_text":"hi","metadata":{"request_id":"req-1"}}`)
srv, fake := responsesProviderTunnelServer(frames, "served-model")
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"pool-model",
"input":"hi",
"metadata":{"iop_response_mode":"passthrough+sideband"}
}`))
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 got := reqs[0].Metadata[responseModeMetadataKey]; got != responseModePassthroughSideband {
t.Fatalf("tunnel response mode metadata: got %q want %q", got, responseModePassthroughSideband)
}
var body map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatalf("response JSON: %v body=%s", err, w.Body.String())
}
if body["id"] != "resp-1" || body["output_text"] != "hi" {
t.Fatalf("provider response fields must be preserved: %+v", body)
}
metadata, ok := body["metadata"].(map[string]any)
if !ok {
t.Fatalf("metadata must be an object: %+v", body["metadata"])
}
if metadata["request_id"] != "req-1" {
t.Fatalf("provider metadata must be preserved: %+v", metadata)
}
if metadata[responseModeMetadataKey] != responseModePassthroughSideband {
t.Fatalf("sideband response mode must be injected into metadata: %+v", metadata)
}
}
func TestResponsesProviderTunnelSidebandStreamingInjectsEvent(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,
"metadata":{"iop_response_mode":"passthrough+sideband"}
}`))
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 || !reqs[0].Stream {
t.Fatalf("expected one streaming tunnel dispatch, got %#v", reqs)
}
body := w.Body.String()
for _, marker := range []string{
"event: iop.sideband",
`"object":"iop.responses.sideband"`,
`"iop_response_mode":"passthrough+sideband"`,
"response.output_text.delta",
"data: [DONE]",
} {
if !strings.Contains(body, marker) {
t.Fatalf("streaming sideband body must contain %q, got %q", marker, body)
}
}
}
// TestChatCompletionsProviderPoolFallsBackToLegacyRoute verifies that when the
// request model does not match the catalog, the legacy model_routes path is used.
func TestChatCompletionsProviderPoolFallsBackToLegacyRoute(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
catalog := []config.ModelCatalogEntry{
{ID: "qwen3.6:35b", Providers: map[string]string{"prov-vllm": "Qwen3-35B-A22B"}},
}
srv := NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "ollama-model", Adapter: "ollama", Target: "llama3"},
},
}, fake, nil)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"ollama-model",
"messages":[{"role":"user","content":"hi"}]
}`))
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 fake.req.ProviderPool {
t.Error("ProviderPool should be false for non-catalog model")
}
if fake.req.Target != "llama3" {
t.Errorf("Target: got %q, want llama3", fake.req.Target)
}
}
func TestChatCompletionsRetriesUnknownTextToolCallBeforeResponse(t *testing.T) {
// Attempt 1: unknown tool call (triggers retry)
run1Events := make(chan *iop.RunEvent, 2)
run1Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=delete_everything><parameter=confirm>true</parameter></function></tool_call>"}
run1Events <- &iop.RunEvent{Type: "complete"}
// Attempt 2: valid tool call (succeeds)
run2Events := make(chan *iop.RunEvent, 2)
run2Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=run_commands><parameter=commands>[\"ls\"]\n</parameter></function></tool_call>"}
run2Events <- &iop.RunEvent{Type: "complete"}
fake := &fakeRunService{eventRuns: []chan *iop.RunEvent{run1Events, run2Events}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}}}}}]
}`))
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 chatCompletionResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if len(resp.Choices) == 0 {
t.Fatal("empty choices")
}
choice := resp.Choices[0]
if choice.FinishReason != "tool_calls" || len(choice.Message.ToolCalls) == 0 {
t.Fatalf("expected synthesized tool calls: %+v", choice)
}
}
func TestChatCompletionsFailsUnknownTextToolCallAfterRetryLimit(t *testing.T) {
run1Events := make(chan *iop.RunEvent, 2)
run1Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=unknown><parameter=arg>val</parameter></function></tool_call>"}
run1Events <- &iop.RunEvent{Type: "complete"}
run2Events := make(chan *iop.RunEvent, 2)
run2Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=unknown><parameter=arg>val</parameter></function></tool_call>"}
run2Events <- &iop.RunEvent{Type: "complete"}
fake := &fakeRunService{eventRuns: []chan *iop.RunEvent{run1Events, run2Events}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusBadGateway {
t.Fatalf("status: got %d, want 502; body=%s", w.Code, w.Body.String())
}
var resp errorResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.Error.Type != "tool_validation_error" {
t.Fatalf("unexpected error type: got %q, want tool_validation_error", resp.Error.Type)
}
}
func TestChatCompletionsProviderStreamSynthesizesTextToolCalls(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "thought text "}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=run_commands><parameter=commands>[\"echo 1\"]</parameter></function></tool_call>"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object"}}}],
"stream": true
}`))
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, `"tool_calls"`) {
t.Fatalf("expected stream to contain tool_calls: %s", body)
}
if strings.Contains(body, `"<tool_call>"`) {
t.Fatalf("stream should not contain raw tool_call block: %s", body)
}
}
func TestChatCompletionsProviderStreamBlocksUnknownTextToolCall(t *testing.T) {
invalidRun := func() chan *iop.RunEvent {
return bufferedRunEvents(
&iop.RunEvent{Type: "delta", Delta: "thought text "},
&iop.RunEvent{Type: "delta", Delta: "<tool_call><function=unknown_tool><parameter=arg>val</parameter></function></tool_call>"},
&iop.RunEvent{Type: "complete"},
)
}
fake := &fakeRunService{
eventRuns: []chan *iop.RunEvent{
invalidRun(),
invalidRun(),
},
runIDs: []string{"run-bad-1", "run-bad-2"},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}],
"stream": true
}`))
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, "tool_validation_error") {
t.Fatalf("expected stream to end with tool_validation_error: %s", body)
}
if strings.Contains(body, `\u003ctool_call\u003e`) {
t.Fatalf("unknown tool call leaked as content: %s", body)
}
if reqs := fake.reqsSnapshot(); len(reqs) != 2 {
t.Fatalf("submit attempts: got %d want 2", len(reqs))
}
}
func TestChatCompletionsStrictBufferedStreamFailsUnknownTextToolCallAfterRetryLimit(t *testing.T) {
run1Events := make(chan *iop.RunEvent, 2)
run1Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=unknown><parameter=arg>val</parameter></function></tool_call>"}
run1Events <- &iop.RunEvent{Type: "complete"}
run2Events := make(chan *iop.RunEvent, 2)
run2Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=unknown><parameter=arg>val</parameter></function></tool_call>"}
run2Events <- &iop.RunEvent{Type: "complete"}
fake := &fakeRunService{eventRuns: []chan *iop.RunEvent{run1Events, run2Events}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex", StrictOutput: true, StrictStreamBuffer: true}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands"}}],
"stream": true
}`))
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, "tool_validation_error") {
t.Fatalf("expected stream to end with tool_validation_error: %s", body)
}
}
func TestChatCompletionsFailsMalformedTextToolCallAfterRetryLimit(t *testing.T) {
invalidComplete := func() chan *iop.RunEvent {
ch := make(chan *iop.RunEvent, 2)
ch <- &iop.RunEvent{Type: "delta", Delta: "여기 툴 콜입니다. <tool_call><function=run_commands><parameter=commands>"}
ch <- &iop.RunEvent{Type: "complete"}
return ch
}
fake := &fakeRunService{
eventRuns: []chan *iop.RunEvent{
invalidComplete(),
invalidComplete(),
},
runIDs: []string{"run-bad-1", "run-bad-2"},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object"}}}],
"tool_choice":"auto"
}`))
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(), `"type":"tool_validation_error"`) || !strings.Contains(w.Body.String(), "malformed tool call: unclosed tool_call block") {
t.Fatalf("expected tool validation error body, got %s", w.Body.String())
}
if reqs := fake.reqsSnapshot(); len(reqs) != 2 {
t.Fatalf("submit attempts: got %d want 2", len(reqs))
}
}
func TestChatCompletionsProviderStreamBlocksMalformedTextToolCall(t *testing.T) {
invalidRun := func() chan *iop.RunEvent {
return bufferedRunEvents(
&iop.RunEvent{Type: "delta", Delta: "이것은 정상 텍스트 "},
&iop.RunEvent{Type: "delta", Delta: "<tool_call><function=run_commands>"},
&iop.RunEvent{Type: "complete"},
)
}
fake := &fakeRunService{
eventRuns: []chan *iop.RunEvent{
invalidRun(),
invalidRun(),
},
runIDs: []string{"run-bad-1", "run-bad-2"},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object"}}}],
"tool_choice":"auto",
"stream": true
}`))
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, "<tool_call") {
t.Fatalf("body should not contain tool_call leak, got: %s", body)
}
if !strings.Contains(body, "tool_validation_error") {
t.Fatalf("expected tool_validation_error, got: %s", body)
}
if reqs := fake.reqsSnapshot(); len(reqs) != 2 {
t.Fatalf("submit attempts: got %d want 2", len(reqs))
}
}
func TestValidateToolCallResponseValidatesEveryToolCall(t *testing.T) {
contract := toolValidationContract{
enabled: true,
specs: map[string]textToolSpec{
"run_commands": {
parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"commands": map[string]any{
"type": "array",
"items": map[string]any{
"type": "string",
},
},
},
"required": []any{"commands"},
},
},
"read_file": {
parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"path": map[string]any{
"type": "string",
},
},
"required": []any{"path"},
},
},
},
}
invalidFirst := []any{
map[string]any{
"id": "call_1",
"type": "object",
"function": map[string]any{
"name": "run_commands",
"arguments": `{"not_commands":[]}`,
},
},
map[string]any{
"id": "call_2",
"type": "object",
"function": map[string]any{
"name": "read_file",
"arguments": `{"path":"/a/b"}`,
},
},
}
err := validateToolCallResponse(contract, invalidFirst, "native")
if err == nil {
t.Fatal("expected validation error because first tool call is invalid")
}
if !strings.Contains(err.Error(), "tool call 0") || !strings.Contains(err.Error(), "commands is required") {
t.Fatalf("unexpected error message: %v", err)
}
allValid := []any{
map[string]any{
"id": "call_1",
"type": "object",
"function": map[string]any{
"name": "run_commands",
"arguments": `{"commands":["ls"]}`,
},
},
map[string]any{
"id": "call_2",
"type": "object",
"function": map[string]any{
"name": "read_file",
"arguments": `{"path":"/a/b"}`,
},
},
}
if err := validateToolCallResponse(contract, allValid, "native"); err != nil {
t.Fatalf("expected no validation error, got: %v", err)
}
}
func TestChatCompletionsFailsWhenAnySynthesizedTextToolCallViolatesSchema(t *testing.T) {
invalidComplete := func() chan *iop.RunEvent {
ch := make(chan *iop.RunEvent, 2)
ch <- &iop.RunEvent{Type: "delta", Delta: `<tool_call><function=run_commands><parameter=not_commands>["ls"]</parameter></function></tool_call><tool_call><function=read_file><parameter=path>"/a/b"</parameter></function></tool_call>`}
ch <- &iop.RunEvent{Type: "complete"}
return ch
}
fake := &fakeRunService{
eventRuns: []chan *iop.RunEvent{
invalidComplete(),
invalidComplete(),
},
runIDs: []string{"run-bad-1", "run-bad-2"},
}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"metadata":{"iop_response_mode":"transformed"},
"model":"qwen3.6:35b",
"messages":[{"role":"user","content":"status"}],
"tools":[
{"type":"function","function":{"name":"run_commands","parameters":{"type":"object","properties":{"commands":{"type":"array","items":{"type":"string"}}},"required":["commands"]}}},
{"type":"function","function":{"name":"read_file","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}}
],
"tool_choice":"auto"
}`))
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(), `"type":"tool_validation_error"`) {
t.Fatalf("expected tool validation error body, got %s", w.Body.String())
}
}
func TestResponsesMetadataIncludesTypedEstimateAndClassification(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"test",
"metadata":{"request_id":"req-001"}
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
estStr := fake.req.Metadata["estimated_input_tokens"]
if estStr == "" {
t.Fatalf("estimated_input_tokens should be present")
}
ctxClass := fake.req.Metadata["context_class"]
if ctxClass != "normal" {
t.Fatalf("context_class: got %q, want normal", ctxClass)
}
est := fake.req.EstimatedInputTokens
if est <= 0 {
t.Fatalf("EstimatedInputTokens: got %d", est)
}
if fake.req.ContextClass != "normal" {
t.Fatalf("ContextClass: got %q, want normal", fake.req.ContextClass)
}
}
func TestResponsesMetadataIncludesTypedEstimateAndClassification_LargePayload(t *testing.T) {
largeInput := make([]byte, 400000)
for i := range largeInput {
largeInput[i] = 'x'
}
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
body := `{"model":"client-model","input":"` + string(largeInput) + `"}`
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.ContextClass != "long" {
t.Fatalf("context_class: got %q, want long", fake.req.ContextClass)
}
if fake.req.EstimatedInputTokens < 100000 {
t.Fatalf("EstimatedInputTokens: got %d, want >= 100000", fake.req.EstimatedInputTokens)
}
}
func TestChatCompletionsMetadataIncludesTypedEstimateAndClassification(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"client-model",
"messages":[{"role":"user","content":"hi"}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
estStr := fake.req.Metadata["estimated_input_tokens"]
if estStr == "" {
t.Fatalf("estimated_input_tokens should be present")
}
ctxClass := fake.req.Metadata["context_class"]
if ctxClass != "normal" {
t.Fatalf("context_class: got %q, want normal", ctxClass)
}
est := fake.req.EstimatedInputTokens
if est <= 0 {
t.Fatalf("EstimatedInputTokens: got %d", est)
}
if fake.req.ContextClass != "normal" {
t.Fatalf("ContextClass: got %q, want normal", fake.req.ContextClass)
}
}
func TestChatCompletionsAssembledLogs(t *testing.T) {
core, observed := observer.New(zap.DebugLevel)
logger := zap.New(core)
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello"}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "reasoning..."}
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{
"openai_tool_calls": `[{"id":"call_001","type":"function","function":{"name":"get_weather","arguments":"{}"}}]`,
}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, logger)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"client-model",
"messages":[{"role":"user","content":"hi"}],
"tools":[{"type":"function","function":{"name":"get_weather"}}]
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
found := false
for _, entry := range observed.All() {
if entry.Message == "openai chat completion output" {
found = true
m := entry.ContextMap()
if m["response_mode"] != "normalized" {
t.Errorf("expected response_mode=normalized, got %v", m["response_mode"])
}
if m["assembled_content"] != "hello" {
t.Errorf("expected assembled_content=hello, got %v", m["assembled_content"])
}
if m["assembled_reasoning"] != "reasoning..." {
t.Errorf("expected assembled_reasoning=reasoning..., got %v", m["assembled_reasoning"])
}
tc, ok := m["assembled_tool_calls"].([]interface{})
if !ok || len(tc) != 1 || tc[0] != "get_weather" {
t.Errorf("expected assembled_tool_calls=[get_weather], got %v", m["assembled_tool_calls"])
}
if count, ok := m["assembled_tool_call_count"].(int64); !ok || count != 1 {
t.Errorf("expected assembled_tool_call_count=1, got %v", m["assembled_tool_call_count"])
}
}
}
if !found {
t.Error("expected log message 'openai chat completion output' not found")
}
}
func TestChatCompletionsAssembledLogsTransformed(t *testing.T) {
core, observed := observer.New(zap.DebugLevel)
logger := zap.New(core)
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello transformed"}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "reasoning transformed..."}
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{
"openai_tool_calls": `[{"id":"call_002","type":"function","function":{"name":"run_code","arguments":"{}"}}]`,
}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, logger)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"client-model",
"messages":[{"role":"user","content":"hi"}],
"tools":[{"type":"function","function":{"name":"run_code"}}],
"metadata":{"iop_response_mode":"transformed"}
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
found := false
for _, entry := range observed.All() {
if entry.Message == "openai chat completion output" {
found = true
m := entry.ContextMap()
if m["response_mode"] != "transformed" {
t.Errorf("expected response_mode=transformed, got %v", m["response_mode"])
}
if m["assembled_content"] != "hello transformed" {
t.Errorf("expected assembled_content=hello transformed, got %v", m["assembled_content"])
}
if m["assembled_reasoning"] != "reasoning transformed..." {
t.Errorf("expected assembled_reasoning=reasoning transformed..., got %v", m["assembled_reasoning"])
}
tc, ok := m["assembled_tool_calls"].([]interface{})
if !ok || len(tc) != 1 || tc[0] != "run_code" {
t.Errorf("expected assembled_tool_calls=[run_code], got %v", m["assembled_tool_calls"])
}
if count, ok := m["assembled_tool_call_count"].(int64); !ok || count != 1 {
t.Errorf("expected assembled_tool_call_count=1, got %v", m["assembled_tool_call_count"])
}
}
}
if !found {
t.Error("expected log message 'openai chat completion output' not found")
}
}
func TestChatCompletionsAssembledLogsSidebandNonStreaming(t *testing.T) {
core, observed := observer.New(zap.DebugLevel)
logger := zap.New(core)
providerBody := `{"choices":[{"message":{"content":"non-stream content","reasoning_content":"non-stream reasoning","tool_calls":[{"function":{"name":"call_non_stream"}}]}}]}`
frames := make(chan *iop.ProviderTunnelFrame, 4)
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_USAGE,
Usage: &iop.Usage{
InputTokens: 10,
OutputTokens: 20,
},
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
fake := &fakeRunService{tunnelFrames: frames}
catalog := []config.ModelCatalogEntry{
{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, logger)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}],
"metadata":{"iop_response_mode":"passthrough+sideband"}
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d body=%s", w.Code, w.Body.String())
}
// response body check
var envelope struct {
Object string `json:"object"`
Sideband struct {
Route *struct {
ResponseMode string `json:"response_mode"`
} `json:"route"`
Usage *struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
Assembled *struct {
Content string `json:"content"`
Reasoning string `json:"reasoning"`
ToolCallNames []string `json:"tool_call_names"`
BodyBytes int `json:"body_bytes"`
} `json:"assembled"`
} `json:"iop_sideband"`
}
if err := json.Unmarshal(w.Body.Bytes(), &envelope); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if envelope.Sideband.Route == nil {
t.Fatalf("expected sideband.route to be non-nil")
}
if envelope.Sideband.Route.ResponseMode != "passthrough+sideband" {
t.Errorf("expected route response_mode 'passthrough+sideband', got %q", envelope.Sideband.Route.ResponseMode)
}
if envelope.Sideband.Usage == nil {
t.Fatalf("expected sideband.usage to be non-nil")
}
if envelope.Sideband.Usage.InputTokens != 10 || envelope.Sideband.Usage.OutputTokens != 20 {
t.Errorf("expected usage input=10 output=20, got input=%d output=%d", envelope.Sideband.Usage.InputTokens, envelope.Sideband.Usage.OutputTokens)
}
if envelope.Sideband.Assembled == nil {
t.Fatalf("expected sideband.assembled to be non-nil")
}
if envelope.Sideband.Assembled.Content != "non-stream content" {
t.Errorf("expected sideband content 'non-stream content', got %q", envelope.Sideband.Assembled.Content)
}
if envelope.Sideband.Assembled.Reasoning != "non-stream reasoning" {
t.Errorf("expected sideband reasoning 'non-stream reasoning', got %q", envelope.Sideband.Assembled.Reasoning)
}
if len(envelope.Sideband.Assembled.ToolCallNames) != 1 || envelope.Sideband.Assembled.ToolCallNames[0] != "call_non_stream" {
t.Errorf("expected sideband tool_call_names [call_non_stream], got %v", envelope.Sideband.Assembled.ToolCallNames)
}
found := false
for _, entry := range observed.All() {
if entry.Message == "openai chat completion sideband response" {
found = true
m := entry.ContextMap()
if m["assembled_content"] != "non-stream content" {
t.Errorf("expected assembled_content='non-stream content', got %v", m["assembled_content"])
}
if m["assembled_reasoning"] != "non-stream reasoning" {
t.Errorf("expected assembled_reasoning='non-stream reasoning', got %v", m["assembled_reasoning"])
}
tc, ok := m["assembled_tool_calls"].([]interface{})
if !ok || len(tc) != 1 || tc[0] != "call_non_stream" {
t.Errorf("expected assembled_tool_calls=[call_non_stream], got %v", m["assembled_tool_calls"])
}
if count, ok := m["assembled_tool_call_count"].(int64); !ok || count != 1 {
t.Errorf("expected assembled_tool_call_count=1, got %v", m["assembled_tool_call_count"])
}
}
}
if !found {
t.Error("expected log message 'openai chat completion sideband response' not found")
}
}
func TestChatCompletionsAssembledLogsPassthrough(t *testing.T) {
core, observed := observer.New(zap.DebugLevel)
logger := zap.New(core)
providerBody := `{"choices":[{"message":{"content":"passthrough content","reasoning_content":"passthrough reasoning","tool_calls":[{"function":{"name":"call_passthrough"}}]}}]}`
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)
fake := &fakeRunService{tunnelFrames: frames}
catalog := []config.ModelCatalogEntry{
{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, logger)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"messages":[{"role":"user","content":"hello"}],
"metadata":{"iop_response_mode":"passthrough"}
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
found := false
for _, entry := range observed.All() {
if entry.Message == "openai chat completion passthrough closed" {
found = true
m := entry.ContextMap()
if m["assembled_content"] != "passthrough content" {
t.Errorf("expected assembled_content='passthrough content', got %v", m["assembled_content"])
}
if m["assembled_reasoning"] != "passthrough reasoning" {
t.Errorf("expected assembled_reasoning='passthrough reasoning', got %v", m["assembled_reasoning"])
}
tc, ok := m["assembled_tool_calls"].([]interface{})
if !ok || len(tc) != 1 || tc[0] != "call_passthrough" {
t.Errorf("expected assembled_tool_calls=[call_passthrough], got %v", m["assembled_tool_calls"])
}
if count, ok := m["assembled_tool_call_count"].(int64); !ok || count != 1 {
t.Errorf("expected assembled_tool_call_count=1, got %v", m["assembled_tool_call_count"])
}
}
}
if !found {
t.Error("expected log message 'openai chat completion passthrough closed' not found")
}
}
func TestChatCompletionsAssembledLogsSidebandStream(t *testing.T) {
core, observed := observer.New(zap.DebugLevel)
logger := zap.New(core)
roleEvent := "data: {\"choices\":[{\"delta\":{\"role\":\"assistant\"}}]}\n\n"
contentEvent := "data: {\"choices\":[{\"delta\":{\"content\":\"hello\"}}]}\n\n"
reasoningEvent := "data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"stream reasoning\"}}]}\n\n"
toolCallEvent := "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"name\":\"call_stream\"}}]}}]}\n\n"
doneEvent := "data: [DONE]\n\n"
frames := make(chan *iop.ProviderTunnelFrame, 8)
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(roleEvent)}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(contentEvent)}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(reasoningEvent)}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(toolCallEvent)}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(doneEvent)}
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_USAGE,
Usage: &iop.Usage{
InputTokens: 15,
OutputTokens: 25,
},
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
fake := &fakeRunService{tunnelFrames: frames}
catalog := []config.ModelCatalogEntry{
{ID: "pool-model", Providers: map[string]string{"prov-1": "served-model"}},
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, logger)
srv.SetModelCatalog(catalog)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
"model":"pool-model",
"stream":true,
"messages":[{"role":"user","content":"hello"}],
"metadata":{"iop_response_mode":"passthrough+sideband"}
}`))
w := httptest.NewRecorder()
srv.handleChatCompletions(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d body=%s", w.Code, w.Body.String())
}
// Verify SSE observations from response surface
bodyStr := w.Body.String()
lines := strings.Split(bodyStr, "\n")
var parsedObs []sidebandObservation
for i := 0; i < len(lines); i++ {
if strings.HasPrefix(lines[i], "event: iop.sideband") {
if i+1 < len(lines) && strings.HasPrefix(lines[i+1], "data: ") {
dataJSON := strings.TrimPrefix(lines[i+1], "data: ")
var obs sidebandObservation
if err := json.Unmarshal([]byte(dataJSON), &obs); err == nil {
parsedObs = append(parsedObs, obs)
}
}
}
}
// We expect 3 sideband events: route, usage, assembled
if len(parsedObs) != 3 {
t.Errorf("expected 3 sideband events, got %d", len(parsedObs))
} else {
// Verify Route
if parsedObs[0].Kind != "route" || parsedObs[0].Route == nil {
t.Errorf("expected first sideband event to be route, got %+v", parsedObs[0])
} else if parsedObs[0].Route.ResponseMode != "passthrough+sideband" {
t.Errorf("expected route response_mode 'passthrough+sideband', got %q", parsedObs[0].Route.ResponseMode)
}
// Verify Usage
if parsedObs[1].Kind != "usage" || parsedObs[1].Usage == nil {
t.Errorf("expected second sideband event to be usage, got %+v", parsedObs[1])
} else if parsedObs[1].Usage.InputTokens != 15 || parsedObs[1].Usage.OutputTokens != 25 {
t.Errorf("expected usage input=15, output=25, got input=%d, output=%d", parsedObs[1].Usage.InputTokens, parsedObs[1].Usage.OutputTokens)
}
// Verify Assembled
if parsedObs[2].Kind != "assembled" || parsedObs[2].Assembled == nil {
t.Errorf("expected third sideband event to be assembled, got %+v", parsedObs[2])
} else {
if parsedObs[2].Assembled.Content != "hello" {
t.Errorf("expected assembled content 'hello', got %q", parsedObs[2].Assembled.Content)
}
if parsedObs[2].Assembled.Reasoning != "stream reasoning" {
t.Errorf("expected assembled reasoning 'stream reasoning', got %q", parsedObs[2].Assembled.Reasoning)
}
if len(parsedObs[2].Assembled.ToolCallNames) != 1 || parsedObs[2].Assembled.ToolCallNames[0] != "call_stream" {
t.Errorf("expected assembled tool call names [call_stream], got %v", parsedObs[2].Assembled.ToolCallNames)
}
}
}
found := false
for _, entry := range observed.All() {
if entry.Message == "openai chat completion sideband stream closed" {
found = true
m := entry.ContextMap()
if m["assembled_content"] != "hello" {
t.Errorf("expected assembled_content=hello, got %v", m["assembled_content"])
}
if m["assembled_reasoning"] != "stream reasoning" {
t.Errorf("expected assembled_reasoning='stream reasoning', got %v", m["assembled_reasoning"])
}
tc, ok := m["assembled_tool_calls"].([]interface{})
if !ok || len(tc) != 1 || tc[0] != "call_stream" {
t.Errorf("expected assembled_tool_calls=[call_stream], got %v", m["assembled_tool_calls"])
}
if count, ok := m["assembled_tool_call_count"].(int64); !ok || count != 1 {
t.Errorf("expected assembled_tool_call_count=1, got %v", m["assembled_tool_call_count"])
}
}
}
if !found {
t.Error("expected log message 'openai chat completion sideband stream closed' not found")
}
}