- agent-readable-repository-refactor 완료: 기존 테스트 파일 아카이브 이동 - 새 테스트 파일 추가 (edge, node, client, config, readability) - readability_audit 스크립트 및 baseline 추가 - roadmap/SDD 문서 갱신 - agent-client/pi/extensions/openai-sampling-parameters 추가
493 lines
15 KiB
Go
493 lines
15 KiB
Go
package openai_compat
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"iop/apps/node/internal/runtime"
|
|
"iop/packages/go/config"
|
|
)
|
|
|
|
func TestOpenAICompatExecuteMapsThinkFalseForVLLMChatTemplateKwargs(t *testing.T) {
|
|
var body map[string]any
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
_, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"ok"}}]}`)
|
|
_, _ = fmt.Fprintf(w, "data: [DONE]\n\n")
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: "vllm",
|
|
Endpoint: server.URL,
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-vllm",
|
|
Target: "qwen-model",
|
|
Input: map[string]any{
|
|
"prompt": "hello",
|
|
"think": false,
|
|
},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
ctk, ok := body["chat_template_kwargs"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected chat_template_kwargs to be a map, got %T", body["chat_template_kwargs"])
|
|
}
|
|
if ctk["enable_thinking"] != false {
|
|
t.Fatalf("expected enable_thinking to be false, got %v", ctk["enable_thinking"])
|
|
}
|
|
}
|
|
|
|
func TestOpenAICompatExecuteVLLMMLXUnsupportedThinkDisable(t *testing.T) {
|
|
// The vLLM-MLX runtime cannot produce a reasoning-free streaming response,
|
|
// so think=false / reasoning_effort=none must fail loudly instead of being
|
|
// silently accepted as a reasoning stream.
|
|
for _, disable := range []map[string]any{
|
|
{"prompt": "hello", "think": false},
|
|
{"prompt": "hello", "reasoning_effort": "none"},
|
|
} {
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: "vllm-mlx",
|
|
Endpoint: "http://localhost:8000",
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-vllm-mlx-disable",
|
|
Target: "qwen-model",
|
|
Input: disable,
|
|
}, sink)
|
|
if err == nil {
|
|
t.Fatalf("expected error for vllm-mlx think disable %v, got nil", disable)
|
|
}
|
|
if !strings.Contains(err.Error(), "unsupported think control") {
|
|
t.Fatalf("expected error message to contain 'unsupported think control' for %v, got: %v", disable, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpenAICompatExecuteMapsThinkFalseForLemonadeChatTemplateKwargs(t *testing.T) {
|
|
var body map[string]any
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
_, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"ok"}}]}`)
|
|
_, _ = fmt.Fprintf(w, "data: [DONE]\n\n")
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: "lemonade",
|
|
Endpoint: server.URL,
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-lemonade",
|
|
Target: "lemonade-model",
|
|
Input: map[string]any{
|
|
"prompt": "hello",
|
|
"think": false,
|
|
},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
if _, ok := body["think"]; ok {
|
|
t.Fatalf("expected top-level think to be omitted for lemonade, got %v", body["think"])
|
|
}
|
|
ctk, ok := body["chat_template_kwargs"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected chat_template_kwargs to be a map, got %T", body["chat_template_kwargs"])
|
|
}
|
|
if ctk["enable_thinking"] != false {
|
|
t.Fatalf("expected enable_thinking to be false, got %v", ctk["enable_thinking"])
|
|
}
|
|
}
|
|
|
|
func TestOpenAICompatExecuteOmitsThinkControlWhenRequestOmitsIt(t *testing.T) {
|
|
for _, provider := range []string{"vllm", "vllm-mlx", "lemonade", ""} {
|
|
t.Run(provider, func(t *testing.T) {
|
|
var body map[string]any
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
_, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"ok"}}]}`)
|
|
_, _ = fmt.Fprintf(w, "data: [DONE]\n\n")
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: provider,
|
|
Endpoint: server.URL,
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-omit",
|
|
Target: "qwen-model",
|
|
Input: map[string]any{
|
|
"prompt": "hello",
|
|
},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
for _, key := range []string{"think", "reasoning_effort", "thinking_token_budget", "chat_template_kwargs"} {
|
|
if _, ok := body[key]; ok {
|
|
t.Fatalf("expected key %q to be omitted for provider %q", key, provider)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOpenAICompatExecuteVLLMUnsupportedReasoningEffort(t *testing.T) {
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: "vllm",
|
|
Endpoint: "http://localhost:8000",
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-unsupported",
|
|
Target: "qwen-model",
|
|
Input: map[string]any{
|
|
"prompt": "hello",
|
|
"reasoning_effort": "low",
|
|
},
|
|
}, sink)
|
|
if err == nil {
|
|
t.Fatalf("expected error for unsupported reasoning_effort, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "unsupported think control") {
|
|
t.Fatalf("expected error message to contain 'unsupported think control', got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOpenAICompatExecuteVLLMUnsupportedNegativeBudget(t *testing.T) {
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: "vllm",
|
|
Endpoint: "http://localhost:8000",
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-unsupported",
|
|
Target: "qwen-model",
|
|
Input: map[string]any{
|
|
"prompt": "hello",
|
|
"thinking_token_budget": -10,
|
|
},
|
|
}, sink)
|
|
if err == nil {
|
|
t.Fatalf("expected error for negative budget, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "unsupported think control") {
|
|
t.Fatalf("expected error message to contain 'unsupported think control', got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOpenAICompatExecuteMapsReasoningEffortNone(t *testing.T) {
|
|
t.Run("vllm", func(t *testing.T) {
|
|
var body map[string]any
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
_, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"ok"}}]}`)
|
|
_, _ = fmt.Fprintf(w, "data: [DONE]\n\n")
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: "vllm",
|
|
Endpoint: server.URL,
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-vllm-none",
|
|
Target: "qwen-model",
|
|
Input: map[string]any{
|
|
"prompt": "hello",
|
|
"reasoning_effort": "none",
|
|
},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
ctk, ok := body["chat_template_kwargs"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected chat_template_kwargs to be a map, got %T", body["chat_template_kwargs"])
|
|
}
|
|
if ctk["enable_thinking"] != false {
|
|
t.Fatalf("expected enable_thinking to be false, got %v", ctk["enable_thinking"])
|
|
}
|
|
})
|
|
|
|
t.Run("lemonade", func(t *testing.T) {
|
|
var body map[string]any
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
_, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"ok"}}]}`)
|
|
_, _ = fmt.Fprintf(w, "data: [DONE]\n\n")
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: "lemonade",
|
|
Endpoint: server.URL,
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-lemonade-none",
|
|
Target: "lemonade-model",
|
|
Input: map[string]any{
|
|
"prompt": "hello",
|
|
"reasoning_effort": "none",
|
|
},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
if _, ok := body["reasoning_effort"]; ok {
|
|
t.Fatalf("expected reasoning_effort to be omitted, got %v", body["reasoning_effort"])
|
|
}
|
|
ctk, ok := body["chat_template_kwargs"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected chat_template_kwargs to be a map, got %T", body["chat_template_kwargs"])
|
|
}
|
|
if ctk["enable_thinking"] != false {
|
|
t.Fatalf("expected enable_thinking to be false, got %v", ctk["enable_thinking"])
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOpenAICompatExecuteMapsThinkTrueForVLLMChatTemplateKwargs(t *testing.T) {
|
|
var body map[string]any
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
_, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"ok"}}]}`)
|
|
_, _ = fmt.Fprintf(w, "data: [DONE]\n\n")
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: "vllm",
|
|
Endpoint: server.URL,
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-vllm",
|
|
Target: "qwen-model",
|
|
Input: map[string]any{
|
|
"prompt": "hello",
|
|
"think": true,
|
|
},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
ctk, ok := body["chat_template_kwargs"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected chat_template_kwargs to be a map, got %T", body["chat_template_kwargs"])
|
|
}
|
|
if ctk["enable_thinking"] != true {
|
|
t.Fatalf("expected enable_thinking to be true, got %v", ctk["enable_thinking"])
|
|
}
|
|
}
|
|
|
|
func TestOpenAICompatExecuteMapsThinkTrueForVLLMMLXChatTemplateKwargs(t *testing.T) {
|
|
var body map[string]any
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
_, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"ok"}}]}`)
|
|
_, _ = fmt.Fprintf(w, "data: [DONE]\n\n")
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: "vllm-mlx",
|
|
Endpoint: server.URL,
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-vllm-mlx",
|
|
Target: "qwen-model",
|
|
Input: map[string]any{
|
|
"prompt": "hello",
|
|
"think": true,
|
|
},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
ctk, ok := body["chat_template_kwargs"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected chat_template_kwargs to be a map, got %T", body["chat_template_kwargs"])
|
|
}
|
|
if ctk["enable_thinking"] != true {
|
|
t.Fatalf("expected enable_thinking to be true, got %v", ctk["enable_thinking"])
|
|
}
|
|
}
|
|
|
|
func TestOpenAICompatExecuteMapsThinkTrueForLemonadeChatTemplateKwargs(t *testing.T) {
|
|
var body map[string]any
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
_, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"ok"}}]}`)
|
|
_, _ = fmt.Fprintf(w, "data: [DONE]\n\n")
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: "lemonade",
|
|
Endpoint: server.URL,
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-lemonade",
|
|
Target: "lemonade-model",
|
|
Input: map[string]any{
|
|
"prompt": "hello",
|
|
"think": true,
|
|
"thinking_token_budget": 1024,
|
|
},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
if _, ok := body["think"]; ok {
|
|
t.Fatalf("expected top-level think to be omitted for lemonade, got %v", body["think"])
|
|
}
|
|
if _, ok := body["thinking_token_budget"]; ok {
|
|
t.Fatalf("expected top-level thinking_token_budget to be omitted for lemonade, got %v", body["thinking_token_budget"])
|
|
}
|
|
ctk, ok := body["chat_template_kwargs"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected chat_template_kwargs to be a map, got %T", body["chat_template_kwargs"])
|
|
}
|
|
if ctk["enable_thinking"] != true {
|
|
t.Fatalf("expected enable_thinking to be true, got %v", ctk["enable_thinking"])
|
|
}
|
|
if ctk["thinking_token_budget"] != float64(1024) {
|
|
t.Fatalf("expected thinking_token_budget to be float64 1024, got %v", ctk["thinking_token_budget"])
|
|
}
|
|
}
|
|
|
|
func TestOpenAICompatExecuteMapsThinkingTokenBudgetForVLLMChatTemplateKwargs(t *testing.T) {
|
|
var body map[string]any
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
_, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"ok"}}]}`)
|
|
_, _ = fmt.Fprintf(w, "data: [DONE]\n\n")
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: "vllm",
|
|
Endpoint: server.URL,
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-vllm-budget",
|
|
Target: "qwen-model",
|
|
Input: map[string]any{
|
|
"prompt": "hello",
|
|
"thinking_token_budget": 2048,
|
|
},
|
|
}, sink)
|
|
if err != nil {
|
|
t.Fatalf("Execute failed: %v", err)
|
|
}
|
|
|
|
if _, ok := body["thinking_token_budget"]; ok {
|
|
t.Fatalf("expected top-level thinking_token_budget to be omitted for vllm, got %v", body["thinking_token_budget"])
|
|
}
|
|
|
|
ctk, ok := body["chat_template_kwargs"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected chat_template_kwargs to be a map, got %T", body["chat_template_kwargs"])
|
|
}
|
|
if ctk["enable_thinking"] != true {
|
|
t.Fatalf("expected enable_thinking to be true for budget-only request, got %v", ctk["enable_thinking"])
|
|
}
|
|
if ctk["thinking_token_budget"] != float64(2048) {
|
|
t.Fatalf("expected thinking_token_budget to be 2048, got %v", ctk["thinking_token_budget"])
|
|
}
|
|
}
|
|
|
|
func TestOpenAICompatExecuteVLLMUnsupportedInvalidBudget(t *testing.T) {
|
|
adapter := New(config.OpenAICompatConf{
|
|
Provider: "vllm",
|
|
Endpoint: "http://localhost:8000",
|
|
}, zap.NewNop())
|
|
|
|
sink := &fakeSink{}
|
|
invalidBudgets := []any{"invalid", -5, 12.34}
|
|
for _, val := range invalidBudgets {
|
|
err := adapter.Execute(context.Background(), runtime.ExecutionSpec{
|
|
RunID: "run-vllm-invalid-budget",
|
|
Target: "qwen-model",
|
|
Input: map[string]any{
|
|
"prompt": "hello",
|
|
"thinking_token_budget": val,
|
|
},
|
|
}, sink)
|
|
if err == nil {
|
|
t.Fatalf("expected error for budget val %v, got nil", val)
|
|
}
|
|
if !strings.Contains(err.Error(), "unsupported think control") {
|
|
t.Fatalf("expected error message to contain 'unsupported think control' for val %v, got: %v", val, err)
|
|
}
|
|
}
|
|
}
|