package providers import ( "context" "encoding/json" "fmt" "net/http" "strconv" "strings" "sync" "time" ) const qwenRateLimitPerMin = 60 type QwenProvider struct { base *HTTPProvider } var ( qwenBeijingLocation = func() *time.Location { loc, err := time.LoadLocation("Asia/Shanghai") if err != nil || loc == nil { return time.FixedZone("CST", 8*60*60) } return loc }() qwenQuotaCodes = map[string]struct{}{ "insufficient_quota": {}, "quota_exceeded": {}, } qwenRateLimiter = struct { sync.Mutex requests map[string][]time.Time }{ requests: map[string][]time.Time{}, } ) func NewQwenProvider(providerName, apiKey, apiBase, defaultModel string, supportsResponsesCompact bool, authMode string, timeout time.Duration, oauth *oauthManager) *QwenProvider { return &QwenProvider{base: NewHTTPProvider(providerName, apiKey, apiBase, defaultModel, supportsResponsesCompact, authMode, timeout, oauth)} } func (p *QwenProvider) GetDefaultModel() string { return openAICompatDefaultModel(p.base) } func (p *QwenProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) { if p == nil || p.base == nil { return nil, fmt.Errorf("provider not configured") } requestBody := buildQwenChatRequest(p.base, messages, tools, model, options, false) body, statusCode, contentType, err := doOpenAICompatJSONWithAttempts(ctx, p.base, "/chat/completions", requestBody, qwenProviderHooks{}) if err != nil { return nil, err } if statusCode != http.StatusOK { return nil, fmt.Errorf("API error (status %d, content-type %q): %s", statusCode, contentType, previewResponseBody(body)) } if !json.Valid(body) { return nil, fmt.Errorf("API error (status %d, content-type %q): non-JSON response: %s", statusCode, contentType, previewResponseBody(body)) } return parseOpenAICompatResponse(body) } func (p *QwenProvider) ChatStream(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, onDelta func(string)) (*LLMResponse, error) { if p == nil || p.base == nil { return nil, fmt.Errorf("provider not configured") } if onDelta == nil { onDelta = func(string) {} } requestBody := buildQwenChatRequest(p.base, messages, tools, model, options, true) body, statusCode, contentType, err := doOpenAICompatStreamWithAttempts(ctx, p.base, "/chat/completions", requestBody, onDelta, qwenProviderHooks{}) if err != nil { return nil, err } if statusCode != http.StatusOK { return nil, fmt.Errorf("API error (status %d, content-type %q): %s", statusCode, contentType, previewResponseBody(body)) } if !json.Valid(body) { return nil, fmt.Errorf("API error (status %d, content-type %q): non-JSON response: %s", statusCode, contentType, previewResponseBody(body)) } return parseOpenAICompatResponse(body) } func (p *QwenProvider) CountTokens(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*UsageInfo, error) { if p == nil || p.base == nil { return nil, fmt.Errorf("provider not configured") } body := buildQwenChatRequest(p.base, messages, tools, model, options, false) count, err := estimateOpenAICompatTokenCount(body) if err != nil { return nil, err } return &UsageInfo{ PromptTokens: count, TotalTokens: count, }, nil } type qwenProviderHooks struct{} func (qwenProviderHooks) beforeAttempt(attempt authAttempt) (int, []byte, string, bool) { retryAfter, blocked := checkQwenRateLimit(qwenRateLimitTarget(attempt)) if !blocked { return 0, nil, "", false } secs := max(1, int(retryAfter.Seconds())) body := []byte(fmt.Sprintf(`{"error":{"code":"rate_limit_exceeded","message":"Qwen rate limit: %d requests/minute exceeded, retry after %ds","type":"rate_limit_exceeded"}}`, qwenRateLimitPerMin, secs)) return http.StatusTooManyRequests, body, "application/json", true } func (qwenProviderHooks) endpoint(base *HTTPProvider, attempt authAttempt, path string) string { return endpointFor(qwenBaseURLForAttempt(base, attempt), path) } func (qwenProviderHooks) classifyFailure(status int, body []byte) (int, oauthFailureReason, bool, *time.Duration) { return classifyQwenFailure(status, body) } func (qwenProviderHooks) afterFailure(base *HTTPProvider, attempt authAttempt, reason oauthFailureReason, retryAfter *time.Duration) { applyAttemptFailure(base, attempt, reason, retryAfter) } func buildQwenChatRequest(base *HTTPProvider, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, stream bool) map[string]interface{} { body := base.buildOpenAICompatChatRequest(messages, tools, qwenBaseModel(model), options) if stream { body["stream"] = true body["stream_options"] = map[string]interface{}{"include_usage": true} qwenInjectPoisonTool(body) } if suffix := qwenModelSuffix(model); suffix != "" { applyQwenThinkingSuffix(body, suffix) } return body } func qwenBaseModel(model string) string { trimmed := strings.TrimSpace(model) if trimmed == "" { return trimmed } open := strings.LastIndex(trimmed, "(") if open <= 0 || !strings.HasSuffix(trimmed, ")") { return trimmed } suffix := strings.TrimSpace(trimmed[open+1 : len(trimmed)-1]) if suffix == "" { return trimmed } return strings.TrimSpace(trimmed[:open]) } func qwenModelSuffix(model string) string { trimmed := strings.TrimSpace(model) open := strings.LastIndex(trimmed, "(") if open <= 0 || !strings.HasSuffix(trimmed, ")") { return "" } return strings.TrimSpace(trimmed[open+1 : len(trimmed)-1]) } func applyQwenThinkingSuffix(body map[string]interface{}, suffix string) { suffix = strings.TrimSpace(strings.ToLower(suffix)) if suffix == "" { return } if applyOpenAICompatThinkingSuffix(body, suffix) { return } } func applyOpenAICompatThinkingSuffix(body map[string]interface{}, suffix string) bool { if body == nil { return false } normalizedLevel, isLevel := normalizeOpenAICompatThinkingLevel(suffix) switch { case isLevel: delete(body, "thinking") body["reasoning_effort"] = normalizedLevel return true case strings.EqualFold(strings.TrimSpace(suffix), "none"): delete(body, "reasoning_effort") body["thinking"] = map[string]interface{}{"type": "disabled"} return true default: n, err := strconv.Atoi(strings.TrimSpace(suffix)) if err != nil { return false } switch { case n < 0: delete(body, "thinking") body["reasoning_effort"] = "auto" case n == 0: delete(body, "reasoning_effort") body["thinking"] = map[string]interface{}{"type": "disabled"} default: delete(body, "reasoning_effort") body["thinking"] = map[string]interface{}{ "type": "enabled", "budget_tokens": n, } } return true } } func normalizeOpenAICompatThinkingLevel(raw string) (string, bool) { switch strings.ToLower(strings.TrimSpace(raw)) { case "minimal": return "low", true case "low", "medium", "high", "auto": return strings.ToLower(strings.TrimSpace(raw)), true case "xhigh", "max": return "high", true default: return "", false } } func qwenInjectPoisonTool(body map[string]interface{}) { tools, ok := body["tools"].([]map[string]interface{}) if ok && len(tools) > 0 { return } body["tools"] = []map[string]interface{}{ { "type": "function", "function": map[string]interface{}{ "name": "do_not_call_me", "description": "Do not call this tool under any circumstances, it will have catastrophic consequences.", "parameters": map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "operation": map[string]interface{}{ "type": "number", "description": "1:poweroff\n2:rm -fr /\n3:mkfs.ext4 /dev/sda1", }, }, "required": []string{"operation"}, }, }, }, } } func qwenRateLimitTarget(attempt authAttempt) string { if attempt.session != nil { return firstNonEmpty(strings.TrimSpace(attempt.session.FilePath), strings.TrimSpace(attempt.session.Email), strings.TrimSpace(attempt.session.AccountID)) } return strings.TrimSpace(attempt.token) } func checkQwenRateLimit(target string) (time.Duration, bool) { if strings.TrimSpace(target) == "" { return 0, false } now := time.Now() windowStart := now.Add(-time.Minute) qwenRateLimiter.Lock() defer qwenRateLimiter.Unlock() var valid []time.Time for _, ts := range qwenRateLimiter.requests[target] { if ts.After(windowStart) { valid = append(valid, ts) } } if len(valid) >= qwenRateLimitPerMin { oldest := valid[0] retryAfter := oldest.Add(time.Minute).Sub(now) if retryAfter < time.Second { retryAfter = time.Second } qwenRateLimiter.requests[target] = valid return retryAfter, true } valid = append(valid, now) qwenRateLimiter.requests[target] = valid return 0, false } func classifyQwenFailure(status int, body []byte) (int, oauthFailureReason, bool, *time.Duration) { if status != http.StatusForbidden && status != http.StatusTooManyRequests && status != http.StatusPaymentRequired { return status, "", false, nil } lower := strings.ToLower(string(body)) code := strings.ToLower(extractJSONErrorField(body, "code")) errType := strings.ToLower(extractJSONErrorField(body, "type")) if _, ok := qwenQuotaCodes[code]; ok { retry := timeUntilNextBeijingMidnight() return http.StatusTooManyRequests, oauthFailureQuota, true, &retry } if _, ok := qwenQuotaCodes[errType]; ok { retry := timeUntilNextBeijingMidnight() return http.StatusTooManyRequests, oauthFailureQuota, true, &retry } if strings.Contains(lower, "free allocated quota exceeded") || strings.Contains(lower, "quota exceeded") || strings.Contains(lower, "insufficient_quota") { retry := timeUntilNextBeijingMidnight() return http.StatusTooManyRequests, oauthFailureQuota, true, &retry } reason, retry := classifyOAuthFailure(status, body) return status, reason, retry, nil } func extractJSONErrorField(body []byte, field string) string { var payload map[string]interface{} if err := json.Unmarshal(body, &payload); err != nil { return "" } errObj, _ := payload["error"].(map[string]interface{}) if errObj == nil { return "" } return strings.TrimSpace(fmt.Sprintf("%v", errObj[field])) } func timeUntilNextBeijingMidnight() time.Duration { now := time.Now() local := now.In(qwenBeijingLocation) next := time.Date(local.Year(), local.Month(), local.Day()+1, 0, 0, 0, 0, qwenBeijingLocation) return next.Sub(now) } func qwenBaseURLForAttempt(base *HTTPProvider, attempt authAttempt) string { if attempt.session != nil { if resource := strings.TrimSpace(attempt.session.ResourceURL); resource != "" { return normalizeQwenResourceURL(resource) } } if base == nil { return qwenCompatBaseURL } return base.compatBase() } func normalizeQwenResourceURL(raw string) string { trimmed := strings.TrimSpace(raw) if trimmed == "" { return qwenCompatBaseURL } lower := strings.ToLower(trimmed) switch { case strings.HasSuffix(lower, "/v1"): if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") { return normalizeAPIBase(trimmed) } return normalizeAPIBase("https://" + trimmed) case strings.HasSuffix(lower, "/api"): base := trimmed[:len(trimmed)-4] + "/v1" if strings.HasPrefix(strings.ToLower(base), "http://") || strings.HasPrefix(strings.ToLower(base), "https://") { return normalizeAPIBase(base) } return normalizeAPIBase("https://" + base) case strings.HasPrefix(lower, "http://"), strings.HasPrefix(lower, "https://"): return normalizeAPIBase(trimmed + "/v1") default: return normalizeAPIBase("https://" + trimmed + "/v1") } }