package openai
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
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":{},
"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":{},
"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":{},
"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 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":{},
"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[\"git status\"]"}
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":{},
"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, "