Files
Bruno ClermontandBruno Clermont 89b83af931 fix(ollama): disable thinking mode by default (#1308)
Ollama models like qwq and deepseek-r1 have thinking enabled by default.
Disable it to prevent bloated response generation unless explicitly requested.

- Add isOllamaEndpoint() detection (providerType, name, apiBase)
- Always send think=false in Ollama requests unless OptThinkingLevel is set
- SupportsThinking() and Capabilities().Thinking return false for Ollama
- Add tests covering default-off, explicit-opt-in, and capability flags

Co-authored-by: Bruno Clermont <bruno.clermont@gmail.com>
2026-06-30 21:25:12 +07:00

97 lines
4.0 KiB
Go

package providers
import "strings"
// 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")
}
// isFireworksEndpoint returns true for Fireworks AI endpoints.
// Fireworks requires stream=true for max_tokens > 4096.
func (p *OpenAIProvider) isFireworksEndpoint() bool {
return strings.Contains(strings.ToLower(p.apiBase), "fireworks.ai")
}
// isTogetherEndpoint returns true for Together AI inference hosts.
// Together rejects some OpenAI extensions (e.g. stream_options, reasoning_effort) with HTTP 400.
// Uses URL, provider_type, and name so reverse-proxied Together endpoints are also detected.
func (p *OpenAIProvider) isTogetherEndpoint() bool {
b := strings.ToLower(p.apiBase)
if strings.Contains(b, "together.xyz") || strings.Contains(b, "together.ai") {
return true
}
if strings.Contains(strings.ToLower(strings.TrimSpace(p.providerType)), "together") {
return true
}
if strings.Contains(strings.ToLower(p.name), "together") {
return true
}
return false
}
// isDashScopeAPIBase returns true for Alibaba DashScope OpenAI-compatible endpoints.
func isDashScopeAPIBase(apiBase string) bool {
return strings.Contains(strings.ToLower(apiBase), "dashscope")
}
// isOllamaEndpoint returns true for local or self-hosted Ollama instances.
// Ollama models such as qwq and deepseek-r1 have thinking enabled by default;
// goclaw must send think=false to suppress it unless the user explicitly opts in.
// Detection uses providerType (DB), name, and apiBase so both ProviderOllama and
// ProviderOllamaCloud are covered, as well as self-hosted instances behind a proxy.
func (p *OpenAIProvider) isOllamaEndpoint() bool {
pt := strings.ToLower(strings.TrimSpace(p.providerType))
if pt == "ollama" || pt == "ollama_cloud" {
return true
}
if strings.Contains(strings.ToLower(p.name), "ollama") {
return true
}
b := strings.ToLower(p.apiBase)
return strings.Contains(b, "11434") || strings.Contains(b, "ollama")
}
// dashScopePassthroughKeys is true when enable_thinking / thinking_budget may be added to the JSON body.
// Uses the same DashScope/Bailian route detection as prompt-cache wrapping.
func (p *OpenAIProvider) dashScopePassthroughKeys() bool {
return p.isDashScope()
}
// ollamaNativeURL returns the full URL for Ollama's native /api/chat endpoint.
// Ollama's OpenAI-compat shim at /v1/chat/completions silently ignores options.num_ctx,
// while the native /api/chat endpoint honors it. The apiBase may include a /v1 suffix
// (e.g. "http://localhost:11434/v1") — it is stripped before appending /api/chat.
func (p *OpenAIProvider) ollamaNativeURL() string {
base := strings.TrimRight(strings.TrimSuffix(strings.TrimRight(p.apiBase, "/"), "/v1"), "/")
return base + "/api/chat"
}
// isDashScope returns true when this provider routes requests to DashScope/Bailian
// (supports cache_control:ephemeral wire format - verified live 2026-05-08).
// Uses 3-source detection (URL + providerType + name) to handle reverse-proxied
// DashScope endpoints. Includes "bailian" because live qwen-richard provider has
// provider_type=bailian.
//
// Used by buildRequestBody to wrap system content with Anthropic-style
// cache_control blocks for prompt caching (90% discount on cached prefix tokens).
func (p *OpenAIProvider) isDashScope() bool {
if isDashScopeAPIBase(p.apiBase) {
return true
}
pt := strings.ToLower(strings.TrimSpace(p.providerType))
if strings.Contains(pt, "dashscope") || strings.Contains(pt, "bailian") {
return true
}
name := strings.ToLower(p.name)
if strings.Contains(name, "dashscope") || strings.Contains(name, "bailian") {
return true
}
return false
}