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"