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