fix(providers): use developer role for native OpenAI endpoints (GPT-4o+)

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()
This commit is contained in:
viettranx committed 2026-03-30 23:56:51 +07:00
1 parent 7fd31c34aa
commit a47d7f9f4f
1 file changed
+23 -1
+23 -1
View File
@@ -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)