From 487eb9166cc87bbd9b58a46fca66abc08513ec52 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:37:53 -0500 Subject: [PATCH] feat(coding-agent): add kimi web search provider --- .../coding-agent/src/cli/web-search-cli.ts | 1 + .../src/config/settings-schema.ts | 2 +- .../src/modes/components/settings-defs.ts | 3 +- packages/coding-agent/src/web/search/index.ts | 8 +- .../coding-agent/src/web/search/provider.ts | 5 +- .../src/web/search/providers/kimi.ts | 197 ++++++++++++++++++ packages/coding-agent/src/web/search/types.ts | 1 + 7 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 packages/coding-agent/src/web/search/providers/kimi.ts diff --git a/packages/coding-agent/src/cli/web-search-cli.ts b/packages/coding-agent/src/cli/web-search-cli.ts index 9d3595013..3a2feda14 100644 --- a/packages/coding-agent/src/cli/web-search-cli.ts +++ b/packages/coding-agent/src/cli/web-search-cli.ts @@ -26,6 +26,7 @@ const PROVIDERS: Array = [ "exa", "brave", "jina", + "kimi", "zai", "gemini", "codex", diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index b3e29e2f7..482783891 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -653,7 +653,7 @@ export const SETTINGS_SCHEMA = { // ───────────────────────────────────────────────────────────────────────── "providers.webSearch": { type: "enum", - values: ["auto", "exa", "brave", "jina", "zai", "perplexity", "anthropic"] as const, + values: ["auto", "exa", "brave", "jina", "kimi", "zai", "perplexity", "anthropic"] as const, default: "auto", ui: { tab: "services", label: "Web search provider", description: "Provider for web search tool", submenu: true }, }, diff --git a/packages/coding-agent/src/modes/components/settings-defs.ts b/packages/coding-agent/src/modes/components/settings-defs.ts index 97e2ec6ae..d01bafe9d 100644 --- a/packages/coding-agent/src/modes/components/settings-defs.ts +++ b/packages/coding-agent/src/modes/components/settings-defs.ts @@ -164,11 +164,12 @@ const OPTION_PROVIDERS: Partial> = { { value: "auto", label: "Auto", - description: "Priority: Exa > Brave > Jina > Perplexity > Anthropic > Gemini > Codex > Z.AI > Synthetic", + description: "Priority: Exa > Brave > Jina > Kimi > Perplexity > Anthropic > Gemini > Codex > Z.AI > Synthetic", }, { value: "exa", label: "Exa", description: "Requires EXA_API_KEY" }, { value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" }, { value: "jina", label: "Jina", description: "Requires JINA_API_KEY" }, + { value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" }, { value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_API_KEY" }, { value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" }, { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" }, diff --git a/packages/coding-agent/src/web/search/index.ts b/packages/coding-agent/src/web/search/index.ts index 0f487ccd8..235bf4336 100644 --- a/packages/coding-agent/src/web/search/index.ts +++ b/packages/coding-agent/src/web/search/index.ts @@ -1,7 +1,7 @@ /** * Unified Web Search Tool * - * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Gemini, Codex, Z.AI, and Synthetic + * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Z.AI, and Synthetic * providers with provider-specific parameters exposed conditionally. * * When EXA_API_KEY is available, additional specialized tools are exposed: @@ -33,7 +33,7 @@ import { SearchProviderError } from "./types"; export const webSearchSchema = Type.Object({ query: Type.String({ description: "Search query" }), provider: Type.Optional( - StringEnum(["auto", "exa", "brave", "jina", "zai", "anthropic", "perplexity", "gemini", "codex", "synthetic"], { + StringEnum(["auto", "exa", "brave", "jina", "kimi", "zai", "anthropic", "perplexity", "gemini", "codex", "synthetic"], { description: "Search provider (default: auto)", }), ), @@ -47,7 +47,7 @@ export const webSearchSchema = Type.Object({ export type SearchParams = { query: string; - provider?: "auto" | "exa" | "brave" | "jina" | "zai" | "anthropic" | "perplexity" | "gemini" | "codex" | "synthetic"; + provider?: "auto" | "exa" | "brave" | "jina" | "kimi" | "zai" | "anthropic" | "perplexity" | "gemini" | "codex" | "synthetic"; recency?: "day" | "week" | "month" | "year"; limit?: number; /** Maximum output tokens. Defaults to 4096. */ @@ -236,7 +236,7 @@ export async function runSearchQuery( /** * Web search tool implementation. * - * Supports Anthropic, Perplexity, Exa, Brave, Jina, Gemini, Codex, Z.AI, and Synthetic providers with automatic fallback. + * Supports Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Z.AI, and Synthetic providers with automatic fallback. * Session is accepted for interface consistency but not used. */ export class SearchTool implements AgentTool { diff --git a/packages/coding-agent/src/web/search/provider.ts b/packages/coding-agent/src/web/search/provider.ts index cfb98a29d..a6c1407bd 100644 --- a/packages/coding-agent/src/web/search/provider.ts +++ b/packages/coding-agent/src/web/search/provider.ts @@ -5,6 +5,7 @@ import { CodexProvider } from "./providers/codex"; import { ExaProvider } from "./providers/exa"; import { GeminiProvider } from "./providers/gemini"; import { JinaProvider } from "./providers/jina"; +import { KimiProvider } from "./providers/kimi"; import { PerplexityProvider } from "./providers/perplexity"; import { SyntheticProvider } from "./providers/synthetic"; import { ZaiProvider } from "./providers/zai"; @@ -18,6 +19,7 @@ const SEARCH_PROVIDERS: Record = { brave: new BraveProvider(), jina: new JinaProvider(), perplexity: new PerplexityProvider(), + kimi: new KimiProvider(), zai: new ZaiProvider(), anthropic: new AnthropicProvider(), gemini: new GeminiProvider(), @@ -33,6 +35,7 @@ const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [ "anthropic", "gemini", "codex", + "kimi", "zai", "synthetic", ]; @@ -49,7 +52,7 @@ export function setPreferredSearchProvider(provider: SearchProviderId | "auto"): preferredProvId = provider; } -/** Determine which providers are configured (priority: Exa → Brave → Jina → Perplexity → Anthropic → Gemini → Codex → Z.AI → Synthetic) */ +/** Determine which providers are configured (priority: Exa → Brave → Jina → Kimi → Perplexity → Anthropic → Gemini → Codex → Z.AI → Synthetic) */ export async function resolveProviderChain( preferredProvider: SearchProviderId | "auto" = preferredProvId, ): Promise { diff --git a/packages/coding-agent/src/web/search/providers/kimi.ts b/packages/coding-agent/src/web/search/providers/kimi.ts new file mode 100644 index 000000000..001b79c05 --- /dev/null +++ b/packages/coding-agent/src/web/search/providers/kimi.ts @@ -0,0 +1,197 @@ +/** + * Kimi Web Search Provider + * + * Uses Moonshot Kimi Code search API to retrieve web results. + * Endpoint: POST https://api.kimi.com/coding/v1/search + */ +import { getEnvApiKey } from "@oh-my-pi/pi-ai"; +import { $env } from "@oh-my-pi/pi-utils"; +import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs"; +import { AgentStorage } from "../../../session/agent-storage"; +import type { SearchResponse, SearchSource } from "../../../web/search/types"; +import { SearchProviderError } from "../../../web/search/types"; +import type { SearchParams } from "./base"; +import { SearchProvider } from "./base"; +const KIMI_SEARCH_URL = "https://api.kimi.com/coding/v1/search"; + +const DEFAULT_NUM_RESULTS = 10; +const MAX_NUM_RESULTS = 20; +const DEFAULT_TIMEOUT_SECONDS = 30; + +export interface KimiSearchParams { + query: string; + num_results?: number; + include_content?: boolean; + signal?: AbortSignal; +} + +interface KimiSearchResult { + site_name?: string; + title?: string; + url?: string; + snippet?: string; + content?: string; + date?: string; + icon?: string; + mime?: string; +} + +interface KimiSearchResponse { + search_results?: KimiSearchResult[]; +} + +function asTrimmed(value: string | undefined): string | undefined { + if (!value) return undefined; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} + +function resolveBaseUrl(): string { + return asTrimmed($env.MOONSHOT_SEARCH_BASE_URL) ?? asTrimmed($env.KIMI_SEARCH_BASE_URL) ?? KIMI_SEARCH_URL; +} + + +function clampNumResults(value: number | undefined): number { + if (!value || Number.isNaN(value)) return DEFAULT_NUM_RESULTS; + return Math.min(MAX_NUM_RESULTS, Math.max(1, value)); +} + +function dateToAgeSeconds(dateStr: string | undefined): number | undefined { + if (!dateStr) return undefined; + try { + const date = new Date(dateStr); + if (Number.isNaN(date.getTime())) return undefined; + return Math.floor((Date.now() - date.getTime()) / 1000); + } catch { + return undefined; + } +} + +/** + * Find Kimi search credentials from environment or agent.db credentials. + * Priority: MOONSHOT_SEARCH_API_KEY / KIMI_SEARCH_API_KEY / MOONSHOT_API_KEY, then agent.db providers "moonshot" or "kimi-code". + */ +async function findApiKey(): Promise { + const envKey = + asTrimmed($env.MOONSHOT_SEARCH_API_KEY) ?? + asTrimmed($env.KIMI_SEARCH_API_KEY) ?? + getEnvApiKey("moonshot") ?? + null; + if (envKey) return envKey; + + try { + const storage = await AgentStorage.open(getAgentDbPath()); + for (const provider of ["moonshot", "kimi-code"] as const) { + const records = storage.listAuthCredentials(provider); + for (const record of records) { + const credential = record.credential; + if (credential.type === "api_key" && credential.key.trim().length > 0) { + return credential.key; + } + if (credential.type === "oauth" && credential.access.trim().length > 0) { + return credential.access; + } + } + } + } catch { + return null; + } + + return null; +} + + +async function callKimiSearch( + apiKey: string, + params: { query: string; limit: number; includeContent: boolean; signal?: AbortSignal }, +): Promise<{ response: KimiSearchResponse; requestId?: string }> { + const response = await fetch(resolveBaseUrl(), { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + text_query: params.query, + limit: params.limit, + enable_page_crawling: params.includeContent, + timeout_seconds: DEFAULT_TIMEOUT_SECONDS, + }), + signal: params.signal, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new SearchProviderError( + "kimi", + `Kimi search API error (${response.status}): ${errorText}`, + response.status, + ); + } + + const data = (await response.json()) as KimiSearchResponse; + const requestId = response.headers.get("x-request-id") ?? response.headers.get("x-msh-request-id") ?? undefined; + return { response: data, requestId }; +} + +/** Execute Kimi web search. */ +export async function searchKimi(params: KimiSearchParams): Promise { + const apiKey = await findApiKey(); + if (!apiKey) { + throw new Error( + "Kimi search credentials not found. Set MOONSHOT_SEARCH_API_KEY, KIMI_SEARCH_API_KEY, MOONSHOT_API_KEY, or login with 'omp /login moonshot'.", + ); + } + + const limit = clampNumResults(params.num_results); + const { response, requestId } = await callKimiSearch(apiKey, { + query: params.query, + limit, + includeContent: params.include_content ?? false, + signal: params.signal, + }); + const sources: SearchSource[] = []; + + for (const result of response.search_results ?? []) { + if (!result.url) continue; + const publishedDate = asTrimmed(result.date); + const snippet = asTrimmed(result.snippet) ?? asTrimmed(result.content); + sources.push({ + title: asTrimmed(result.title) ?? result.url, + url: result.url, + snippet, + publishedDate, + ageSeconds: dateToAgeSeconds(publishedDate), + author: asTrimmed(result.site_name), + }); + } + + return { + provider: "kimi", + sources: sources.slice(0, limit), + requestId, + }; +} + +/** Search provider for Kimi web search. */ +export class KimiProvider extends SearchProvider { + readonly id = "kimi"; + readonly label = "Kimi"; + + async isAvailable(): Promise { + try { + return !!(await findApiKey()); + } catch { + return false; + } + } + + search(params: SearchParams): Promise { + return searchKimi({ + query: params.query, + num_results: params.numSearchResults ?? params.limit, + signal: params.signal, + }); + } +} diff --git a/packages/coding-agent/src/web/search/types.ts b/packages/coding-agent/src/web/search/types.ts index 38181cc6b..b12d50f39 100644 --- a/packages/coding-agent/src/web/search/types.ts +++ b/packages/coding-agent/src/web/search/types.ts @@ -9,6 +9,7 @@ export type SearchProviderId = | "exa" | "brave" | "jina" + | "kimi" | "zai" | "anthropic" | "perplexity"