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 }