From a47d7f9f4f1427dae642fa112ef16a77dfe995dd Mon Sep 17 00:00:00 2001 From: viettranx Date: Mon, 30 Mar 2026 23:56:51 +0700 Subject: [PATCH] fix(providers): use developer role for native OpenAI endpoints (GPT-4o+) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GPT-4o+ models prioritize "developer" messages over "system" for instruction adherence. GoClaw was sending "system" for all providers, causing GPT models to poorly follow SOUL/system prompts. Map "system" → "developer" only for native OpenAI endpoints (api.openai.com). Non-OpenAI backends keep "system" role unchanged. Ported from OpenClaw TS: model-compat.ts → isOpenAINativeEndpoint() --- internal/providers/openai.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/internal/providers/openai.go b/internal/providers/openai.go index 366fdf48..4ee77f81 100644 --- a/internal/providers/openai.go +++ b/internal/providers/openai.go @@ -28,6 +28,16 @@ type OpenAIProvider struct { retryConfig RetryConfig } +// isOpenAINativeEndpoint returns true for endpoints confirmed to be native OpenAI +// infrastructure that accepts the "developer" message role. +// Azure OpenAI, proxies, and other OpenAI-compatible backends only support "system". +// Matching OpenClaw TS: model-compat.ts → isOpenAINativeEndpoint(). +func isOpenAINativeEndpoint(apiBase string) bool { + // Extract hostname from the API base URL. + lower := strings.ToLower(apiBase) + return strings.Contains(lower, "api.openai.com") +} + func NewOpenAIProvider(name, apiKey, apiBase, defaultModel string) *OpenAIProvider { if apiBase == "" { apiBase = "https://api.openai.com/v1" @@ -269,14 +279,26 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream inputMessages = collapseToolCallsWithoutSig(inputMessages) } + // Detect native OpenAI endpoint to enable developer role. + // GPT-4o+ models prioritize "developer" messages over "system" for instruction + // adherence. Non-OpenAI backends (proxies, Qwen, DeepSeek, etc.) reject "developer". + // Matching OpenClaw TS: model-compat.ts → isOpenAINativeEndpoint(). + useDevRole := isOpenAINativeEndpoint(p.apiBase) + // Convert messages to proper OpenAI wire format. // This is necessary because our internal Message/ToolCall structs don't match // the OpenAI API format (tool_calls need type+function wrapper, arguments as JSON string). // Also omits empty content on assistant messages with tool_calls (Gemini compatibility). msgs := make([]map[string]any, 0, len(inputMessages)) for _, m := range inputMessages { + role := m.Role + // Map "system" → "developer" for native OpenAI endpoints (GPT-4o+). + // The developer role has higher instruction priority than system role. + if useDevRole && role == "system" { + role = "developer" + } msg := map[string]any{ - "role": m.Role, + "role": role, } // Echo reasoning_content for assistant messages (required by Kimi, DeepSeek when thinking is enabled)