/** * Xiaomi MiMo search provider. * * Uses the `web_search` tool type in the Chat Completions API to perform * web searches. This is NOT standard OpenAI protocol — MiMo adds a * `tools: [{ type: 'web_search' }]` entry that other providers don't support. * * Supported models: mimo-v2.5, mimo-v2.5-pro, mimo-v2.5-pro-ultraspeed. * * @module @kamitobi/dsh-multi-search/providers/mimo */ 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 ──────────────────────────────────────────────────────────────── /** MiMo-specific configuration options. */ export interface MiMoProviderConfig extends BaseProviderConfig { /** Force web search even if the model thinks it's unnecessary. */ forceSearch?: boolean /** Maximum keywords per search round. */ maxKeyword?: number /** Maximum number of search results to return. */ maxResults?: number } const DEFAULTS: Required> = { model: 'mimo-v2.5', forceSearch: true, maxKeyword: 3, maxResults: 5, } const DEFAULT_BASE_URL = 'https://api.xiaomimimo.com/v1' // ─── Provider ────────────────────────────────────────────────────────────── /** Response annotation shape from MiMo. */ interface MiMoAnnotation { type?: string url?: string title?: string summary?: string site_name?: string publish_time?: string } export class MiMoSearchProvider implements WebSearchProvider { readonly id = 'multi-search' private readonly apiKey: string private readonly baseURL: string private readonly model: string private readonly forceSearch: boolean private readonly maxKeyword: number private readonly maxResults: number constructor(config: MiMoProviderConfig) { this.apiKey = config.apiKey || '' this.baseURL = config.baseURL || DEFAULT_BASE_URL this.model = config.model || DEFAULTS.model this.forceSearch = config.forceSearch ?? DEFAULTS.forceSearch this.maxKeyword = config.maxKeyword ?? DEFAULTS.maxKeyword 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: 'system', content: 'You are a helpful search assistant. Use web search to find information.', }, { role: 'user', content: request.query }, ], max_completion_tokens: 1024, stream: false, tools: [ { type: 'web_search', max_keyword: this.maxKeyword, force_search: this.forceSearch, limit: maxResults, }, ], tool_choice: 'auto', } 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 MiMoAnnotation[] | 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 }