mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-10 08:11:23 +00:00
128 lines
4.1 KiB
Go
128 lines
4.1 KiB
Go
package providers
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"maps"
|
|
)
|
|
|
|
const (
|
|
dashscopeDefaultBase = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
|
dashscopeDefaultModel = "qwen3-max"
|
|
)
|
|
|
|
// dashscopeThinkingModels lists DashScope models that accept the
|
|
// enable_thinking / thinking_budget parameters (Qwen3 open-weight and Qwen3.5 series).
|
|
// Models NOT in this set (e.g. qwen3-plus, qwen3-turbo) will silently
|
|
// skip thinking injection to avoid API "model not supported" errors.
|
|
var dashscopeThinkingModels = map[string]bool{
|
|
// Qwen3.5 series — thinking + vision
|
|
"qwen3.5-plus": true,
|
|
"qwen3.5-turbo": true,
|
|
// Qwen3 hosted
|
|
"qwen3-max": true,
|
|
// Qwen3 open-weight (available as hosted inference)
|
|
"qwen3-235b-a22b": true,
|
|
"qwen3-32b": true,
|
|
"qwen3-14b": true,
|
|
"qwen3-8b": true,
|
|
}
|
|
|
|
// DashScopeProvider wraps OpenAIProvider to handle DashScope-specific behaviors.
|
|
// Critical: DashScope does NOT support tools + streaming simultaneously.
|
|
// When tools are present, ChatStream falls back to non-streaming Chat().
|
|
type DashScopeProvider struct {
|
|
*OpenAIProvider
|
|
}
|
|
|
|
func NewDashScopeProvider(name, apiKey, apiBase, defaultModel string) *DashScopeProvider {
|
|
if apiBase == "" {
|
|
apiBase = dashscopeDefaultBase
|
|
}
|
|
if defaultModel == "" {
|
|
defaultModel = dashscopeDefaultModel
|
|
}
|
|
return &DashScopeProvider{
|
|
OpenAIProvider: NewOpenAIProvider(name, apiKey, apiBase, defaultModel),
|
|
}
|
|
}
|
|
|
|
// Name is inherited from the embedded OpenAIProvider (returns the user-specified name).
|
|
func (p *DashScopeProvider) SupportsThinking() bool { return true }
|
|
|
|
// ModelSupportsThinking implements ModelThinkingCapable.
|
|
// Returns true only for models that accept enable_thinking / thinking_budget.
|
|
func (p *DashScopeProvider) ModelSupportsThinking(model string) bool {
|
|
return dashscopeThinkingModels[p.resolveModel(model)]
|
|
}
|
|
|
|
// applyThinkingGuard maps thinking_level to DashScope-specific params
|
|
// (enable_thinking / thinking_budget) only when the model supports it.
|
|
// Returns the (possibly mutated) request. Shared by Chat and ChatStream.
|
|
func (p *DashScopeProvider) applyThinkingGuard(req ChatRequest) ChatRequest {
|
|
level, ok := req.Options[OptThinkingLevel].(string)
|
|
if !ok || level == "" || level == "off" {
|
|
return req
|
|
}
|
|
|
|
if p.ModelSupportsThinking(req.Model) {
|
|
// Clone Options to avoid mutating caller's map
|
|
opts := make(map[string]any, len(req.Options)+2)
|
|
maps.Copy(opts, req.Options)
|
|
opts[OptEnableThinking] = true
|
|
opts[OptThinkingBudget] = dashscopeThinkingBudget(level)
|
|
delete(opts, OptThinkingLevel) // don't pass generic key to OpenAI buildRequestBody
|
|
req.Options = opts
|
|
} else {
|
|
slog.Debug("dashscope: model does not support thinking, skipping enable_thinking",
|
|
"model", p.resolveModel(req.Model), "requested_level", level)
|
|
}
|
|
|
|
return req
|
|
}
|
|
|
|
// Chat overrides OpenAIProvider.Chat to apply the per-model thinking guard.
|
|
func (p *DashScopeProvider) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
|
|
return p.OpenAIProvider.Chat(ctx, p.applyThinkingGuard(req))
|
|
}
|
|
|
|
// ChatStream handles DashScope's limitation: tools + streaming cannot coexist.
|
|
// When tools are present, falls back to non-streaming Chat() and synthesizes
|
|
// chunk callbacks for the caller.
|
|
func (p *DashScopeProvider) ChatStream(ctx context.Context, req ChatRequest, onChunk func(StreamChunk)) (*ChatResponse, error) {
|
|
req = p.applyThinkingGuard(req)
|
|
|
|
if len(req.Tools) > 0 {
|
|
slog.Debug("dashscope: tools present, falling back to non-streaming Chat")
|
|
resp, err := p.OpenAIProvider.Chat(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if onChunk != nil {
|
|
if resp.Thinking != "" {
|
|
onChunk(StreamChunk{Thinking: resp.Thinking})
|
|
}
|
|
if resp.Content != "" {
|
|
onChunk(StreamChunk{Content: resp.Content})
|
|
}
|
|
onChunk(StreamChunk{Done: true})
|
|
}
|
|
return resp, nil
|
|
}
|
|
return p.OpenAIProvider.ChatStream(ctx, req, onChunk)
|
|
}
|
|
|
|
// dashscopeThinkingBudget maps a thinking level to a DashScope thinking_budget value.
|
|
func dashscopeThinkingBudget(level string) int {
|
|
switch level {
|
|
case "low":
|
|
return 4096
|
|
case "medium":
|
|
return 16384
|
|
case "high":
|
|
return 32768
|
|
default:
|
|
return 16384
|
|
}
|
|
}
|