File size: 3,640 Bytes
2986042
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package builtin

import (
	"strings"

	"github.com/bytedance/sonic"
)

// buildOpenAISampling 从 OpenAI chat.completions 请求中抽取采样/上限参数。
// max_completion_tokens 优先于 max_tokens(OpenAI o-系列模型的新字段)。
// 全部字段为空时返回 nil,避免空结构污染 conversation。
func buildOpenAISampling(req openAIChatRequest) *samplingParams {
	sp := &samplingParams{
		Temperature:      req.Temperature,
		TopP:             req.TopP,
		TopK:             req.TopK,
		Seed:             req.Seed,
		FrequencyPenalty: req.FrequencyPenalty,
		PresencePenalty:  req.PresencePenalty,
		User:             strings.TrimSpace(req.User),
		ReasoningEffort:  strings.TrimSpace(strings.ToLower(req.ReasoningEffort)),
	}
	switch {
	case req.MaxCompletionTokens != nil:
		sp.MaxTokens = req.MaxCompletionTokens
	case req.MaxTokens != nil:
		sp.MaxTokens = req.MaxTokens
	}
	sp.Stop = parseStopSequences(req.Stop)
	if samplingParamsIsZero(sp) {
		return nil
	}
	return sp
}

// buildCodexSampling 从 Codex /v1/responses 请求中抽取采样/上限/推理参数。
// reasoning.effort → ReasoningEffort;max_output_tokens → MaxTokens;stop 同 OpenAI 接受 string/[]string。
func buildCodexSampling(req codexRequest) *samplingParams {
	sp := &samplingParams{
		Temperature:      req.Temperature,
		TopP:             req.TopP,
		TopK:             req.TopK,
		MaxTokens:        req.MaxOutputTokens,
		Seed:             req.Seed,
		FrequencyPenalty: req.FrequencyPenalty,
		PresencePenalty:  req.PresencePenalty,
		User:             strings.TrimSpace(req.User),
	}
	if req.Reasoning != nil {
		sp.ReasoningEffort = strings.ToLower(strings.TrimSpace(req.Reasoning.Effort))
	}
	sp.Stop = parseStopSequences(req.Stop)
	if samplingParamsIsZero(sp) {
		return nil
	}
	return sp
}

// parseStopSequences 接受 OpenAI stop 字段的两种形态:字符串或字符串数组。
// 其它类型静默丢弃,与 OpenAI 官方行为一致。
func parseStopSequences(raw []byte) []string {
	if len(raw) == 0 {
		return nil
	}
	var asSlice []string
	if err := sonic.Unmarshal(raw, &asSlice); err == nil {
		out := asSlice[:0]
		for _, s := range asSlice {
			if s = strings.TrimSpace(s); s != "" {
				out = append(out, s)
			}
		}
		if len(out) == 0 {
			return nil
		}
		return out
	}
	var asString string
	if err := sonic.Unmarshal(raw, &asString); err == nil {
		if s := strings.TrimSpace(asString); s != "" {
			return []string{s}
		}
	}
	return nil
}

// openAIReasoningEffortToThinking 把 OpenAI reasoning_effort 枚举映射成
// Anthropic 风格 thinking 结构,供 Anthropic/Codex/Gemini 编码器复用。
// 未指定或未识别值返回 nil,保留现有行为(不启用思考)。
func openAIReasoningEffortToThinking(effort string) *anthropicThinkingConfig {
	switch strings.ToLower(strings.TrimSpace(effort)) {
	case "":
		return nil
	case "none":
		return &anthropicThinkingConfig{Type: "disabled"}
	case "minimal", "low":
		return &anthropicThinkingConfig{Type: "enabled", BudgetTokens: 1024}
	case "medium", "auto":
		return &anthropicThinkingConfig{Type: "enabled", BudgetTokens: 4096}
	case "high":
		return &anthropicThinkingConfig{Type: "enabled", BudgetTokens: 16384}
	default:
		return &anthropicThinkingConfig{Type: "enabled", BudgetTokens: 4096}
	}
}

func samplingParamsIsZero(sp *samplingParams) bool {
	if sp == nil {
		return true
	}
	return sp.Temperature == nil && sp.TopP == nil && sp.TopK == nil &&
		sp.MaxTokens == nil && len(sp.Stop) == 0 && sp.ReasoningEffort == "" &&
		sp.Seed == nil && sp.FrequencyPenalty == nil && sp.PresencePenalty == nil &&
		sp.User == ""
}