diff --git a/frontend/apps/main/src/features/admin/ai/ProviderForm.vue b/frontend/apps/main/src/features/admin/ai/ProviderForm.vue index a64b1c6f..44fcec49 100644 --- a/frontend/apps/main/src/features/admin/ai/ProviderForm.vue +++ b/frontend/apps/main/src/features/admin/ai/ProviderForm.vue @@ -79,6 +79,17 @@ + + + {{ t('admin.ai.reasoningEffort') }} + + + + {{ t('admin.ai.reasoningEffortHint') }} + + + + {{ t('admin.ai.instructions') }} @@ -264,6 +275,7 @@ const form = useForm({ base_url: z.string().optional(), api_key: z.string().optional(), instructions: z.string().optional(), + reasoning_effort: z.string().optional(), temperature: numberField().refine((v) => isBlank(v) || inRange(v, 0, 2), { message: t('admin.ai.temperatureRange') }), @@ -281,6 +293,7 @@ const form = useForm({ base_url: '', api_key: '', instructions: '', + reasoning_effort: '', temperature: fieldDefaults.temperature, max_tokens: fieldDefaults.maxTokens, dimensions: fieldDefaults.dimensions, @@ -298,6 +311,7 @@ onMounted(async () => { base_url: data.base_url || '', api_key: data.api_key || '', instructions: data.instructions || '', + reasoning_effort: data.reasoning_effort || '', // Fall back to sensible defaults when the provider has nothing saved (0/absent). temperature: data.temperature != null ? String(data.temperature) : fieldDefaults.temperature, max_tokens: data.max_tokens ? String(data.max_tokens) : fieldDefaults.maxTokens, @@ -321,6 +335,7 @@ const onSubmit = form.handleSubmit(async (values) => { if (props.showCompletionFields) { payload.instructions = values.instructions || '' payload.vision = !!values.vision + payload.reasoning_effort = (values.reasoning_effort || '').trim() if (values.temperature !== '' && values.temperature != null) payload.temperature = Number(values.temperature) if (values.max_tokens !== '' && values.max_tokens != null) diff --git a/i18n/en-US.json b/i18n/en-US.json index 9913d534..12ab26bb 100644 --- a/i18n/en-US.json +++ b/i18n/en-US.json @@ -130,6 +130,8 @@ "admin.ai.temperatureRange": "Temperature must be a number between 0 and 2.", "admin.ai.positiveNumber": "Enter a whole number greater than 0.", "admin.ai.maxTokens": "Max tokens", + "admin.ai.reasoningEffort": "Reasoning effort", + "admin.ai.reasoningEffortHint": "Leave blank for most models. Reasoning models such as GPT-5.x require 'none' to use tools; other values are 'low', 'medium' or 'high'.", "admin.ai.instructions": "Custom instructions", "admin.ai.instructionsHint": "Added to every Copilot and reply-draft prompt. Use it to set tone and format, e.g. keep replies under 3 sentences, match the brand voice, always reply in the customer's language.", "admin.ai.vision": "Model supports image input", diff --git a/internal/ai/ai.go b/internal/ai/ai.go index 560d7385..6d671bba 100644 --- a/internal/ai/ai.go +++ b/internal/ai/ai.go @@ -218,15 +218,16 @@ func (m *Manager) UpdateProviderConfig(providerType string, in models.ProviderCo } cfg := models.ProviderConfig{ - Provider: "openai", - BaseURL: in.BaseURL, - APIKey: apiKey, - Model: in.Model, - Temperature: in.Temperature, - MaxTokens: in.MaxTokens, - Dimensions: in.Dimensions, - Instructions: in.Instructions, - Vision: in.Vision, + Provider: "openai", + BaseURL: in.BaseURL, + APIKey: apiKey, + Model: in.Model, + Temperature: in.Temperature, + MaxTokens: in.MaxTokens, + Dimensions: in.Dimensions, + Instructions: in.Instructions, + Vision: in.Vision, + ReasoningEffort: in.ReasoningEffort, } b, err := json.Marshal(cfg) if err != nil { diff --git a/internal/ai/models/models.go b/internal/ai/models/models.go index a98f63e5..d396a0e7 100644 --- a/internal/ai/models/models.go +++ b/internal/ai/models/models.go @@ -45,6 +45,9 @@ type ProviderConfig struct { MaxTokens int `json:"max_tokens"` Dimensions int `json:"dimensions"` Instructions string `json:"instructions"` + // ReasoningEffort, when set, is sent as-is. Reasoning models (e.g. GPT-5.x) need "none" to use + // tools on /chat/completions; leave blank for models that reject the param. + ReasoningEffort string `json:"reasoning_effort,omitempty"` // Vision marks the completion model as accepting image input; when false the harness drops image parts. Vision bool `json:"vision"` } diff --git a/internal/ai/openai.go b/internal/ai/openai.go index f6e39892..a6178f54 100644 --- a/internal/ai/openai.go +++ b/internal/ai/openai.go @@ -84,13 +84,19 @@ func (o *OpenAIClient) SendChatCompletion(payload models.ChatCompletionPayload) o.lo.Debug("chat completion request", "model", model, "messages", len(messages), "images", sentImages, "vision", o.cfg.Vision, "tools", len(payload.Tools)) body := map[string]any{ - "model": model, - "messages": messages, - "max_tokens": maxTokens, + "model": model, + "messages": messages, + "max_completion_tokens": maxTokens, } + // Only send optional params the admin set. Reasoning models (e.g. GPT-5.x) reject a non-default + // temperature and require reasoning_effort "none" to use tools on /chat/completions; leave + // temperature blank and set reasoning_effort accordingly for those. if o.cfg.Temperature != nil { body["temperature"] = *o.cfg.Temperature } + if o.cfg.ReasoningEffort != "" { + body["reasoning_effort"] = o.cfg.ReasoningEffort + } if len(payload.Tools) > 0 { body["tools"] = payload.Tools body["tool_choice"] = "auto"