fix(openai): store 요청 필드를 호환 처리한다

This commit is contained in:
toki 2026-06-27 23:50:23 +09:00
parent a342d584c5
commit 9529af8dfc
4 changed files with 27 additions and 2 deletions

View file

@ -164,6 +164,7 @@ Workspace-bound route는 workspace가 없거나 상대 경로이면 OpenAI-compa
- `tool_choice`
- `parallel_tool_calls`
- `stream_options`
- `store`
`tools`가 있는 Chat Completions 요청에서 provider route(`openai_compat`, `vllm`, `ollama`, provider pool)는 forced tool 선택 객체와 `"none"` 같은 명시적 `tool_choice`를 backend에 전달한다. 단, `"auto"`는 OpenAI-compatible 기본값과 같으므로 provider request에서는 생략한다. 일부 vLLM 계열 backend는 explicit/default `"auto"``--enable-auto-tool-choice`/`--tool-call-parser` 없이 400으로 거부한다. 이 400이 발생하고 요청 tool이 정확히 1개이면 Node adapter는 해당 tool에 대한 forced `tool_choice`로 1회 재시도한다. forced tool도 `--tool-call-parser` 요구로 거부되거나 여러 tool이라 forced를 고를 수 없으면, Node adapter는 `tools`/`tool_choice`를 제거하고 text tool-call system instruction을 leading system message에 병합해 1회 재시도하며 완료 metadata에 `openai_text_tool_fallback: "true"`를 싣는다.
provider가 native OpenAI-compatible `tool_calls`를 반환하면 Node는 내부 `RunEvent.metadata["openai_tool_calls"]` JSON으로 보존하고, Edge는 이를 OpenAI-compatible `message.tool_calls` 또는 stream `delta.tool_calls`로 반환하며 `finish_reason: "tool_calls"`를 사용한다.
@ -173,7 +174,7 @@ CLI route(`adapter: "cli"`)는 native backend tool calling이 없으므로, 호
CLI fallback에서 텍스트 tool call을 구조화할 때만 Edge는 요청의 `tools[].function.parameters` schema를 기준으로 arguments를 정규화한다. 예를 들어 tool schema가 `commands: string[]`만 허용하면 command 객체 입력도 shell string 배열로 접고, `commands: {command,args}[]`를 허용하면 shell 문법이 없는 명령을 structured argv로 만든다.
CLI fallback에서 schema에 없는 UI 설명용 `description`이나 실행 위치 힌트용 `runInTerminal`은 command 객체와 최상위 args에서 제거한다. 단, `cd`, `command -v`, `&&`, pipe, redirect, quote 등 shell 해석이 필요한 명령은 schema가 허용할 때 `commands: ["cd /work && git status"]` 같은 shell string으로 유지한다.
CLI route에서는 backend auto tool-calling 요구 조건으로 요청이 실패하지 않도록 내부 실행 입력의 `tool_choice``"none"`으로 낮춘다.
`parallel_tool_calls``stream_options`는 클라이언트 호환성을 위해 수신하지만 현재 Edge 실행 의미에는 반영하지 않는다.
`parallel_tool_calls`, `stream_options`, `store`는 클라이언트 호환성을 위해 수신하지만 현재 Edge 실행 의미에는 반영하지 않는다.
금지:

View file

@ -105,7 +105,7 @@ func decodeChatCompletionRequest(dec *json.Decoder, req *chatCompletionRequest)
}
for key := range raw {
switch key {
case "model", "messages", "stream", "metadata", "max_tokens", "max_completion_tokens", "temperature", "top_p", "presence_penalty", "frequency_penalty", "seed", "stop", "response_format", "tools", "tool_choice", "parallel_tool_calls", "stream_options":
case "model", "messages", "stream", "metadata", "max_tokens", "max_completion_tokens", "temperature", "top_p", "presence_penalty", "frequency_penalty", "seed", "stop", "response_format", "tools", "tool_choice", "parallel_tool_calls", "stream_options", "store":
default:
return fmt.Errorf("%s is not supported for /v1/chat/completions", key)
}

View file

@ -232,6 +232,29 @@ func TestChatCompletionsPassesStandardOptions(t *testing.T) {
}
}
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"}

View file

@ -28,6 +28,7 @@ type chatCompletionRequest struct {
ToolChoice any `json:"tool_choice,omitempty"`
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
StreamOptions any `json:"stream_options,omitempty"`
Store any `json:"store,omitempty"`
}
type chatMessage struct {