iop/apps/edge/internal/openai/chat_policy.go
toki e5869a53ae fix(edge): provider 호출 정규화 계층을 추가한다
caller별 예외 대신 요청 의미와 protocol profile capability로 operation을 선택해 tools와 effort 조합을 보존한다. 지원하지 않는 effort는 가장 가까운 하위 등급으로만 내리고 상향 매핑은 거부한다.
2026-08-13 23:06:50 +09:00

148 lines
4.1 KiB
Go

package openai
import (
"encoding/json"
"fmt"
"iop/packages/go/config"
)
func validateThinkControl(req *chatCompletionRequest) error {
if req.ThinkingTokenBudget != nil && *req.ThinkingTokenBudget < 0 {
return fmt.Errorf("thinking_token_budget must be non-negative")
}
if req.ReasoningEffort != nil {
eff := *req.ReasoningEffort
if eff == "" {
return fmt.Errorf("reasoning_effort cannot be empty when present")
}
if eff != "none" && eff != "low" && eff != "medium" && eff != "high" && eff != "xhigh" && eff != "max" {
return fmt.Errorf("reasoning_effort must be one of none, low, medium, high, xhigh, or max")
}
}
if req.Think != nil && !*req.Think {
if req.ReasoningEffort != nil && *req.ReasoningEffort != "none" {
return fmt.Errorf("think=false conflicts with reasoning_effort=%s", *req.ReasoningEffort)
}
}
if req.ThinkingTokenBudget != nil {
if req.Think != nil && !*req.Think {
return fmt.Errorf("thinking_token_budget cannot be set when think=false")
}
if req.ReasoningEffort != nil && *req.ReasoningEffort == "none" {
return fmt.Errorf("thinking_token_budget cannot be set when reasoning_effort=none")
}
}
return nil
}
func applyModelCatalogGenerationPolicyToChat(req *chatCompletionRequest, entry config.ModelCatalogEntry, strictOutput bool, providerNativeThinking bool) {
if req == nil {
return
}
applyOutputTokenPolicy(&req.MaxTokens, req.MaxCompletionTokens, entry.DefaultMaxTokens, entry.MinMaxTokens)
if entry.DefaultThinkingTokenBudget > 0 && !providerNativeThinking && chatRequestAllowsDefaultThinkingBudget(*req) {
if req.ThinkingTokenBudget == nil {
req.ThinkingTokenBudget = intPtr(entry.DefaultThinkingTokenBudget)
}
if strictOutput && req.Think == nil {
req.Think = boolPtr(true)
}
}
}
func applyModelCatalogGenerationPolicyToResponses(req *responsesRequest, entry config.ModelCatalogEntry) {
if req == nil {
return
}
applyOutputTokenPolicy(&req.MaxOutputTokens, nil, entry.DefaultMaxTokens, entry.MinMaxTokens)
}
func applyOutputTokenPolicy(primary **int, fallback *int, defaultTokens, minTokens int) {
if primary == nil {
return
}
current := 0
if *primary != nil {
current = **primary
} else if fallback != nil {
current = *fallback
}
next := current
if next == 0 && defaultTokens > 0 {
next = defaultTokens
}
if minTokens > 0 && (next == 0 || next < minTokens) {
next = minTokens
}
if next > 0 && next != current {
*primary = intPtr(next)
}
}
func chatRequestAllowsDefaultThinkingBudget(req chatCompletionRequest) bool {
if req.Think != nil && !*req.Think {
return false
}
if req.ReasoningEffort != nil && *req.ReasoningEffort == "none" {
return false
}
return true
}
func chatRequestHasProviderNativeThinking(rawBody []byte) bool {
var raw map[string]json.RawMessage
if err := json.Unmarshal(rawBody, &raw); err != nil {
return false
}
ctkRaw, ok := raw["chat_template_kwargs"]
if !ok {
return false
}
var ctk map[string]json.RawMessage
if err := json.Unmarshal(ctkRaw, &ctk); err != nil {
return false
}
_, hasEnableThinking := ctk["enable_thinking"]
_, hasBudget := ctk["thinking_token_budget"]
return hasEnableThinking || hasBudget
}
func intPtr(v int) *int {
return &v
}
func boolPtr(v bool) *bool {
return &v
}
func chatRunMetadata(runMeta map[string]string, req chatCompletionRequest, outputPolicy strictOutputPolicy) map[string]string {
if runMeta == nil {
runMeta = make(map[string]string)
}
runMeta["openai_model"] = req.Model
runMeta["openai_stream"] = fmt.Sprintf("%t", req.Stream)
runMeta["strict_output"] = fmt.Sprintf("%t", outputPolicy.Strict)
return runMeta
}
func (s *Server) resolveStrictOutput() bool {
return s.cfg.StrictOutput
}
func (s *Server) resolveStrictStreamBuffer() bool {
return s.cfg.StrictStreamBuffer
}
func (s *Server) resolveOutputPolicy(prompt string) strictOutputPolicy {
policy := strictOutputPolicy{
Strict: s.resolveStrictOutput(),
StreamBuffer: s.resolveStrictStreamBuffer(),
}
if !policy.Strict {
return policy
}
policy.XMLCompletionTool, policy.XMLResultTag = inferXMLCompletionContract(prompt)
policy.ContractInstruction = policy.XMLCompletionTool != ""
return policy
}