- agent-readable-repository-refactor 완료: 기존 테스트 파일 아카이브 이동 - 새 테스트 파일 추가 (edge, node, client, config, readability) - readability_audit 스크립트 및 baseline 추가 - roadmap/SDD 문서 갱신 - agent-client/pi/extensions/openai-sampling-parameters 추가
488 lines
18 KiB
Go
488 lines
18 KiB
Go
package openai
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
edgeservice "iop/apps/edge/internal/service"
|
|
"iop/packages/go/config"
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
// 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 := &providerFakeRunService{
|
|
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
|
|
}`))
|
|
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"])
|
|
}
|
|
}
|
|
|
|
func TestChatCompletionsProviderPoolAppliesGenerationPolicy(t *testing.T) {
|
|
fake := &providerFakeRunService{
|
|
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 := &providerFakeRunService{
|
|
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 TestChatCompletionsProviderPoolProviderNativeThinkingDisablesDefaultBudgetInjection(t *testing.T) {
|
|
fake := &providerFakeRunService{
|
|
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"}],
|
|
"chat_template_kwargs":{"enable_thinking":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())
|
|
}
|
|
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])
|
|
}
|
|
ctk, ok := providerReq["chat_template_kwargs"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("chat_template_kwargs not preserved as object: %+v", providerReq)
|
|
}
|
|
if ctk["enable_thinking"] != false {
|
|
t.Fatalf("provider-native enable_thinking must stay false: %+v", ctk)
|
|
}
|
|
if _, ok := providerReq["think"]; ok {
|
|
t.Fatalf("catalog policy must not inject top-level think over provider-native thinking: %+v", providerReq)
|
|
}
|
|
if _, ok := providerReq["thinking_token_budget"]; ok {
|
|
t.Fatalf("catalog policy must not inject thinking_token_budget over provider-native thinking: %+v", providerReq)
|
|
}
|
|
}
|
|
|
|
func TestChatCompletionsProviderPoolPreservesLargerGenerationPolicyValues(t *testing.T) {
|
|
fake := &providerFakeRunService{
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 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 := &providerFakeRunService{
|
|
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)
|
|
}
|
|
}
|
|
|
|
// TestResponsesProviderPoolStrictOutputNormalizesAgentResponse verifies that the
|
|
// provider-pool normalized path preserves the strict-output contract from
|
|
// PrepareRun into completeResponse. A prompt containing an XML completion
|
|
// contract must have its response wrapped in the expected block (SDD D02).
|
|
func TestResponsesProviderPoolStrictOutputNormalizesAgentResponse(t *testing.T) {
|
|
fake := &providerFakeRunService{
|
|
poolDispatchPath: string(edgeservice.ProviderPoolPathNormalized),
|
|
}
|
|
fake.poolRunFrames = make(chan *iop.RunEvent, 3)
|
|
fake.poolRunFrames <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden"}
|
|
fake.poolRunFrames <- &iop.RunEvent{Type: "delta", Delta: "plain answer"}
|
|
fake.poolRunFrames <- &iop.RunEvent{Type: "complete", RunId: "run-pool-normalized"}
|
|
close(fake.poolRunFrames)
|
|
|
|
catalog := []config.ModelCatalogEntry{
|
|
{ID: "ollama-strict-model", Providers: map[string]string{"prov-ollama": "served-ollama"}},
|
|
}
|
|
srv := NewServer(config.EdgeOpenAIConf{StrictOutput: true}, fake, nil)
|
|
srv.SetModelCatalog(catalog)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
|
|
"model":"ollama-strict-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.handleResponses(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// PrepareRun must have been invoked (normalized path).
|
|
if !fake.poolPrepareRunCalledSnapshot() {
|
|
t.Fatal("PrepareRun was not called on normalized path")
|
|
}
|
|
|
|
// The prepared request must carry strict_output and the injected contract
|
|
// instruction.
|
|
if len(fake.reqsSnapshot()) != 1 {
|
|
t.Fatalf("expected one prepared req, got %d", len(fake.reqsSnapshot()))
|
|
}
|
|
reqPrep := fake.reqsSnapshot()[0]
|
|
if reqPrep.Metadata["strict_output"] != "true" {
|
|
t.Fatalf("strict_output metadata lost in prepared request: %+v", reqPrep.Metadata)
|
|
}
|
|
if !strings.Contains(reqPrep.Prompt, "output exactly one <attempt_completion> block") {
|
|
t.Fatalf("strict contract instruction not injected into prepared prompt:\n%s", reqPrep.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)
|
|
}
|
|
}
|
|
|
|
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 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())
|
|
}
|
|
}
|