- 모델 그룹별 큐 기반 디스패팅 로직 추가 - ModelQueue 관리자로 모델 인스턴스 큐 처리 - OpenAPI chat 및 responses 핸들러 업데이트 - edge 서비스 테스트 코드 개선
191 lines
5.5 KiB
Go
191 lines
5.5 KiB
Go
package openai
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
edgeservice "iop/apps/edge/internal/service"
|
|
)
|
|
|
|
func (s *Server) handleResponses(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
|
return
|
|
}
|
|
defer r.Body.Close()
|
|
|
|
var req responsesRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request_error", "invalid JSON request")
|
|
return
|
|
}
|
|
|
|
if req.Stream {
|
|
writeError(w, http.StatusBadRequest, "invalid_request_error", "streaming is not supported for /v1/responses")
|
|
return
|
|
}
|
|
|
|
inputStr, err := parseResponsesInput(req.Input)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
|
|
return
|
|
}
|
|
|
|
prompt := buildResponsesPrompt(req.Instructions, inputStr)
|
|
outputPolicy := s.resolveOutputPolicy(prompt)
|
|
if instruction := strictOutputContractInstruction(outputPolicy); instruction != "" {
|
|
prompt = instruction + "\n" + prompt
|
|
}
|
|
|
|
runMeta, inferenceTarget, workspace, err := parseOpenAIMetadata(req.Metadata)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
|
|
return
|
|
}
|
|
|
|
dispatch, ok := s.resolveRouteDispatch(req.Model, inferenceTarget)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid_request_error", "model is required")
|
|
return
|
|
}
|
|
if err := validateWorkspaceForRoute(dispatch, workspace); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
|
|
return
|
|
}
|
|
|
|
runMeta["source"] = "openai-responses"
|
|
runMeta["openai_model"] = req.Model
|
|
runMeta["openai_stream"] = fmt.Sprintf("%t", req.Stream)
|
|
runMeta["strict_output"] = fmt.Sprintf("%t", outputPolicy.Strict)
|
|
input := map[string]any{"prompt": prompt}
|
|
if outputPolicy.Strict {
|
|
input["think"] = false
|
|
}
|
|
|
|
s.logger.Info("openai responses input",
|
|
zap.String("model", req.Model),
|
|
zap.String("target", dispatch.Target),
|
|
zap.String("adapter", dispatch.Adapter),
|
|
zap.Bool("strict_output", outputPolicy.Strict),
|
|
zap.String("xml_completion_tool", outputPolicy.XMLCompletionTool),
|
|
zap.Bool("contract_instruction", outputPolicy.ContractInstruction),
|
|
zap.Int("prompt_len", len(prompt)),
|
|
zap.String("prompt_preview", previewString(prompt, 1000)),
|
|
)
|
|
|
|
handle, err := s.service.SubmitRun(r.Context(), edgeservice.SubmitRunRequest{
|
|
NodeRef: dispatch.NodeRef,
|
|
ModelGroupKey: strings.TrimSpace(req.Model),
|
|
Adapter: dispatch.Adapter,
|
|
Target: dispatch.Target,
|
|
SessionID: dispatch.SessionID,
|
|
Workspace: workspace,
|
|
Prompt: prompt,
|
|
Input: input,
|
|
TimeoutSec: dispatch.TimeoutSec,
|
|
Metadata: runMeta,
|
|
})
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, "node_dispatch_error", err.Error())
|
|
return
|
|
}
|
|
defer handle.Close()
|
|
|
|
s.completeResponse(w, r, req, handle, outputPolicy)
|
|
}
|
|
|
|
func (s *Server) completeResponse(w http.ResponseWriter, r *http.Request, req responsesRequest, handle edgeservice.RunResult, outputPolicy strictOutputPolicy) {
|
|
text, reasoning, usage, err := collectRunResult(r.Context(), handle.Stream(), handle.WaitTimeout())
|
|
if err != nil {
|
|
writeError(w, httpStatusForRunError(err), "run_error", err.Error())
|
|
return
|
|
}
|
|
text, reasoning, normalized := normalizeCompletionOutput(outputPolicy, text, reasoning)
|
|
s.logger.Info("openai responses output",
|
|
zap.String("run_id", handle.Dispatch().RunID),
|
|
zap.Bool("strict_output", outputPolicy.Strict),
|
|
zap.String("xml_completion_tool", outputPolicy.XMLCompletionTool),
|
|
zap.Bool("normalized", normalized),
|
|
zap.Int("content_len", len(text)),
|
|
zap.Int("reasoning_len", len(reasoning)),
|
|
zap.String("content_preview", previewString(text, 1000)),
|
|
)
|
|
|
|
var u openAIUsage
|
|
if usage != nil {
|
|
u = *usage
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, responsesResponse{
|
|
ID: "resp-" + handle.Dispatch().RunID,
|
|
Object: "response",
|
|
CreatedAt: time.Now().Unix(),
|
|
Model: responseModel(req.Model, handle.Dispatch().Target),
|
|
OutputText: text,
|
|
Output: []responsesOutputItem{{
|
|
Type: "message",
|
|
Role: "assistant",
|
|
Content: []responsesContentItem{{
|
|
Type: "output_text",
|
|
Text: text,
|
|
}},
|
|
}},
|
|
Usage: u,
|
|
})
|
|
}
|
|
|
|
func parseResponsesInput(raw json.RawMessage) (string, error) {
|
|
if len(raw) == 0 {
|
|
return "", fmt.Errorf("input is required")
|
|
}
|
|
var s string
|
|
if err := json.Unmarshal(raw, &s); err != nil {
|
|
return "", fmt.Errorf("input must be a string")
|
|
}
|
|
if strings.TrimSpace(s) == "" {
|
|
return "", fmt.Errorf("input is required")
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func buildResponsesPrompt(instructions, input string) string {
|
|
instructions = strings.TrimSpace(instructions)
|
|
input = strings.TrimSpace(input)
|
|
if instructions == "" {
|
|
return input
|
|
}
|
|
return instructions + "\n" + input
|
|
}
|
|
|
|
func parseOpenAIMetadata(raw json.RawMessage) (map[string]string, string, string, error) {
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return make(map[string]string), "", "", nil
|
|
}
|
|
|
|
var rawMap map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &rawMap); err != nil {
|
|
return nil, "", "", fmt.Errorf("metadata must be an object")
|
|
}
|
|
|
|
if _, hasCLI := rawMap["cli"]; hasCLI {
|
|
return nil, "", "", fmt.Errorf("metadata.cli is not supported")
|
|
}
|
|
|
|
var meta responsesMetadata
|
|
if err := json.Unmarshal(raw, &meta); err != nil {
|
|
return nil, "", "", fmt.Errorf("invalid metadata format")
|
|
}
|
|
|
|
flat := meta.metadataForRun()
|
|
inferenceTarget := ""
|
|
if meta.Inference != nil {
|
|
inferenceTarget = meta.Inference.Target
|
|
}
|
|
|
|
return flat, inferenceTarget, strings.TrimSpace(meta.Workspace), nil
|
|
}
|