/** * DashScope (阿里云百炼) search provider. * * Uses the `enable_search` extension in the Chat Completions API to perform * web searches. This is NOT standard OpenAI protocol — DashScope adds * `enable_search` and `search_options` fields that other providers don't support. * * Supported models: qwen-plus, qwen-max, qwen-flash, qwen-turbo, qwen3-max, etc. * * @module @kamitobi/dsh-multi-search/providers/dashscope */ import type { WebSearchProvider, WebSearchRequest, WebSearchResult, WebSearchSource, } from '@deepseek-ai/dsh-web' import type { BaseProviderConfig } from '../types.js' import { callChatCompletions, type ChatCompletionResponse } from '../api.js' // ─── Config ──────────────────────────────────────────────────────────────── /** DashScope-specific configuration options. */ export interface DashScopeProviderConfig extends BaseProviderConfig { /** Force web search even if the model thinks it's unnecessary. */ forceSearch?: boolean /** Search strategy: 'turbo' (fastest), 'max' (most thorough), 'agent' (with extraction). */ searchStrategy?: 'turbo' | 'max' | 'agent' /** Maximum number of search results to return. */ maxResults?: number } const DEFAULTS: Required> = { model: 'qwen3.7flash', forceSearch: false, searchStrategy: 'max', maxResults: 5, } const DEFAULT_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1' // ─── Provider ────────────────────────────────────────────────────────────── /** Response annotation shape from DashScope. */ interface DashScopeAnnotation { type?: string url?: string title?: string summary?: string site_name?: string publish_time?: string } export class DashScopeSearchProvider implements WebSearchProvider { readonly id = 'multi-search' private readonly apiKey: string private readonly baseURL: string private readonly model: string private readonly forceSearch: boolean private readonly searchStrategy: string private readonly maxResults: number constructor(config: DashScopeProviderConfig) { this.apiKey = config.apiKey || '' this.baseURL = config.baseURL || DEFAULT_BASE_URL this.model = config.model || DEFAULTS.model this.forceSearch = config.forceSearch ?? DEFAULTS.forceSearch this.searchStrategy = config.searchStrategy || DEFAULTS.searchStrategy this.maxResults = config.maxResults ?? DEFAULTS.maxResults } available(): boolean { return Boolean(this.apiKey) } async search(request: WebSearchRequest, signal?: AbortSignal): Promise { const maxResults = request.maxResults ?? this.maxResults const body: Record = { model: this.model, messages: [{ role: 'user', content: request.query }], max_tokens: 2048, stream: false, enable_search: true, search_options: { forced_search: this.forceSearch, search_strategy: this.searchStrategy, enable_source: true, }, } const data = await callChatCompletions(this.baseURL, this.apiKey, body, signal) const sources = extractAnnotations(data) const content = data.choices?.[0]?.message?.content return { content, sources, truncated: sources.length > maxResults, } } } // ─── Annotation extraction ───────────────────────────────────────────────── function extractAnnotations(data: ChatCompletionResponse): WebSearchSource[] { const sources: WebSearchSource[] = [] const seen = new Set() const annotations = data.choices?.[0]?.message?.annotations as DashScopeAnnotation[] | undefined if (annotations) { for (const a of annotations) { if (a.url && !seen.has(a.url)) { seen.add(a.url) sources.push({ url: a.url, title: a.title || a.site_name, snippet: a.summary, publishedAt: a.publish_time, }) } } } return sources }