98 lines
3.2 KiB
Go
98 lines
3.2 KiB
Go
package processing
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
// openAIChatUsesMaxCompletionTokens reports models that reject legacy max_tokens
|
||
|
|
// on Chat Completions (o-series + GPT-5 family including GPT-5.6).
|
||
|
|
// See https://developers.openai.com/api/reference/resources/chat
|
||
|
|
func openAIChatUsesMaxCompletionTokens(model string) bool {
|
||
|
|
return openAIReasoningChatModel(model)
|
||
|
|
}
|
||
|
|
|
||
|
|
// openAIChatOmitsCustomTemperature reports models that reject non-default
|
||
|
|
// temperature (only the API default of 1 is accepted, if temperature is sent).
|
||
|
|
func openAIChatOmitsCustomTemperature(model string) bool {
|
||
|
|
return openAIReasoningChatModel(model)
|
||
|
|
}
|
||
|
|
|
||
|
|
// openAIChatSupportsReasoningEffort reports Chat Completions models that accept
|
||
|
|
// top-level reasoning_effort (Responses API uses reasoning.effort instead).
|
||
|
|
func openAIChatSupportsReasoningEffort(model string) bool {
|
||
|
|
return openAIReasoningChatModel(model)
|
||
|
|
}
|
||
|
|
|
||
|
|
// openAIReasoningChatModel matches OpenAI reasoning / GPT-5(+).5/5.6 chat models
|
||
|
|
// and common snapshot aliases (e.g. gpt-5.6-luna-2026-…).
|
||
|
|
func openAIReasoningChatModel(model string) bool {
|
||
|
|
m := strings.ToLower(strings.TrimSpace(model))
|
||
|
|
if m == "" {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
// Strip provider prefixes used by OpenRouter-style gateways.
|
||
|
|
if i := strings.LastIndex(m, "/"); i >= 0 {
|
||
|
|
m = m[i+1:]
|
||
|
|
}
|
||
|
|
switch {
|
||
|
|
case strings.HasPrefix(m, "o1"),
|
||
|
|
strings.HasPrefix(m, "o3"),
|
||
|
|
strings.HasPrefix(m, "o4"),
|
||
|
|
strings.HasPrefix(m, "gpt-5"),
|
||
|
|
strings.HasPrefix(m, "chatgpt-o"):
|
||
|
|
return true
|
||
|
|
default:
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// defaultReasoningEffort picks Chat Completions reasoning_effort when unset.
|
||
|
|
// Long formula-HTML enhance uses "low" so max_completion_tokens prefers visible
|
||
|
|
// HTML over deep hidden reasoning. Tiny probes use "none" for speed/cost.
|
||
|
|
func defaultReasoningEffort(model string, maxTokens int, explicit string) string {
|
||
|
|
explicit = strings.ToLower(strings.TrimSpace(explicit))
|
||
|
|
if explicit != "" {
|
||
|
|
return explicit
|
||
|
|
}
|
||
|
|
if !openAIChatSupportsReasoningEffort(model) {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
if maxTokens <= 0 || maxTokens < 512 {
|
||
|
|
return "none"
|
||
|
|
}
|
||
|
|
return "low"
|
||
|
|
}
|
||
|
|
|
||
|
|
// chatCompletionBody is the Chat Completions JSON body. Temperature is a pointer
|
||
|
|
// so reasoning models can omit it entirely (omitempty).
|
||
|
|
type chatCompletionBody struct {
|
||
|
|
Model string `json:"model"`
|
||
|
|
Messages []chatMessage `json:"messages"`
|
||
|
|
Temperature *float64 `json:"temperature,omitempty"`
|
||
|
|
MaxTokens int `json:"max_tokens,omitempty"`
|
||
|
|
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"`
|
||
|
|
ReasoningEffort string `json:"reasoning_effort,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// buildChatCompletionBody maps CompleteOptions onto the correct Chat Completions
|
||
|
|
// fields for classic vs GPT-5 / o-series models.
|
||
|
|
func buildChatCompletionBody(model string, messages []chatMessage, temperature float64, maxTokens int, reasoningEffort string) chatCompletionBody {
|
||
|
|
body := chatCompletionBody{
|
||
|
|
Model: model,
|
||
|
|
Messages: messages,
|
||
|
|
}
|
||
|
|
if openAIChatUsesMaxCompletionTokens(model) {
|
||
|
|
if maxTokens > 0 {
|
||
|
|
body.MaxCompletionTokens = maxTokens
|
||
|
|
}
|
||
|
|
} else if maxTokens > 0 {
|
||
|
|
body.MaxTokens = maxTokens
|
||
|
|
}
|
||
|
|
if !openAIChatOmitsCustomTemperature(model) {
|
||
|
|
t := temperature
|
||
|
|
body.Temperature = &t
|
||
|
|
}
|
||
|
|
body.ReasoningEffort = defaultReasoningEffort(model, maxTokens, reasoningEffort)
|
||
|
|
return body
|
||
|
|
}
|