pi-provider-kimi-code 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,239 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Type, type Static } from "@earendil-works/pi-ai";
3
+
4
+ import {
5
+ type BuildKimiToolOptions,
6
+ buildHeaders,
7
+ getKimiDatasourceUrl,
8
+ buildKimiToolDeps,
9
+ buildTimeoutSignal,
10
+ errorResult,
11
+ fetchWithAuthRetry,
12
+ firstText,
13
+ readErrorBody,
14
+ shouldCollapse,
15
+ textComponent,
16
+ } from "./common.ts";
17
+
18
+ const DATASOURCE_DEFINITIONS = [
19
+ {
20
+ name: "stock_finance_data",
21
+ description: "A/HK/US stocks, prices, financial reports, indicators, shareholders.",
22
+ },
23
+ {
24
+ name: "yahoo_finance",
25
+ description: "Global finance: quotes, analyst ratings, options, indexes, historical prices.",
26
+ },
27
+ {
28
+ name: "world_bank_open_data",
29
+ description: "World Bank macro data: GDP, inflation, population, country comparisons.",
30
+ },
31
+ {
32
+ name: "tianyancha",
33
+ description: "Chinese enterprise registry: shareholders, legal representative, risks, patents.",
34
+ },
35
+ { name: "arxiv", description: "arXiv preprints: paper search, abstracts, arXiv IDs." },
36
+ {
37
+ name: "scholar",
38
+ description: "Google Scholar: academic papers, authors, citations, high-citation surveys.",
39
+ },
40
+ {
41
+ name: "yuandian_law",
42
+ description: "Chinese laws, regulations, judicial cases; labor contracts and legal clauses.",
43
+ },
44
+ ] as const;
45
+
46
+ const DATASOURCE_NAMES = DATASOURCE_DEFINITIONS.map((source) => source.name);
47
+
48
+ export const kimiDatasourceSchema = Type.Object({
49
+ data_source_name: Type.Union(
50
+ DATASOURCE_NAMES.map((name) => Type.Literal(name)),
51
+ {
52
+ description:
53
+ "Source: stock_finance_data stocks/reports; yahoo_finance quotes/options; world_bank_open_data GDP/population; tianyancha Chinese company registry/legal rep; arxiv preprints; scholar citations; yuandian_law Chinese laws/cases/contracts.",
54
+ },
55
+ ),
56
+ api_name: Type.Optional(
57
+ Type.String({
58
+ description: "API name from the datasource docs. Omit to get docs first.",
59
+ }),
60
+ ),
61
+ params: Type.Optional(
62
+ Type.Record(Type.String(), Type.Unknown(), {
63
+ description: "Parameters matching the selected datasource API docs.",
64
+ }),
65
+ ),
66
+ });
67
+
68
+ export type KimiDatasourceParams = Static<typeof kimiDatasourceSchema>;
69
+
70
+ interface DatasourceToolResponse {
71
+ is_success?: boolean;
72
+ error?: unknown;
73
+ result?: {
74
+ assistant?: unknown;
75
+ user?: unknown;
76
+ };
77
+ }
78
+
79
+ function extractTextFromResult(result: unknown): string {
80
+ if (!Array.isArray(result)) return "";
81
+ const texts: string[] = [];
82
+ for (const item of result) {
83
+ if (typeof item === "string") {
84
+ texts.push(item);
85
+ } else if (typeof item === "object" && item !== null) {
86
+ const record = item as Record<string, unknown>;
87
+ if (typeof record.text === "string") {
88
+ texts.push(record.text);
89
+ }
90
+ }
91
+ }
92
+ return texts.join("\n");
93
+ }
94
+
95
+ function extractResponseText(data: DatasourceToolResponse): string {
96
+ if (data.result?.assistant) {
97
+ const text = extractTextFromResult(data.result.assistant);
98
+ if (text) return text;
99
+ }
100
+ if (data.result?.user) {
101
+ const text = extractTextFromResult(data.result.user);
102
+ if (text) return text;
103
+ }
104
+ return JSON.stringify(data, null, 2);
105
+ }
106
+
107
+ function extractResponseFiles(data: DatasourceToolResponse): string[] {
108
+ const raw = data as Record<string, unknown>;
109
+ if (!Array.isArray(raw.files)) return [];
110
+ const names: string[] = [];
111
+ for (const file of raw.files) {
112
+ if (
113
+ typeof file === "object" &&
114
+ file !== null &&
115
+ typeof (file as Record<string, unknown>).name === "string"
116
+ ) {
117
+ names.push((file as Record<string, unknown>).name as string);
118
+ }
119
+ }
120
+ return names;
121
+ }
122
+
123
+ const REQUEST_ID_HEADERS = [
124
+ "x-request-id",
125
+ "x-trace-id",
126
+ "x-msh-request-id",
127
+ "x-msh-trace-id",
128
+ "request-id",
129
+ ];
130
+
131
+ function extractRequestId(headers: Headers): string | undefined {
132
+ for (const key of REQUEST_ID_HEADERS) {
133
+ const value = headers.get(key);
134
+ if (typeof value === "string" && value.trim()) return value.trim();
135
+ }
136
+ return undefined;
137
+ }
138
+
139
+ function buildTraceLine(toolCallId: string, requestId: string | undefined): string {
140
+ const parts: string[] = [];
141
+ if (requestId) parts.push(`request-id: ${requestId}`);
142
+ parts.push(`tool-call-id: ${toolCallId}`);
143
+ return `\n\n[kimi-datasource] ${parts.join(" · ")}`;
144
+ }
145
+
146
+ export function buildKimiDatasourceTool(options: BuildKimiToolOptions = {}) {
147
+ const deps = buildKimiToolDeps(options);
148
+ const defaultCollapsed = options.defaultCollapsed ?? true;
149
+
150
+ return defineTool({
151
+ name: "kimi_datasource",
152
+ label: "Kimi Datasource",
153
+ description:
154
+ "Call Kimi datasource APIs. Covers stocks/finance, macro data, Chinese company registry, papers, and Chinese law/cases/contracts. First omit api_name to get docs, then call api_name with documented params.",
155
+ promptSnippet: "Query Kimi datasource APIs; get docs first, then call documented api_name",
156
+ parameters: kimiDatasourceSchema,
157
+
158
+ async execute(toolCallId, params, signal) {
159
+ const accessToken = deps.getAccessToken();
160
+ if (!accessToken) {
161
+ return errorResult<string>(
162
+ "Missing Kimi Code OAuth credentials. Run /login kimi-coding first.",
163
+ );
164
+ }
165
+
166
+ const timeout = buildTimeoutSignal(signal);
167
+ try {
168
+ const apiName = params.api_name?.trim();
169
+ const body = apiName
170
+ ? {
171
+ method: "call_data_source_tool",
172
+ params: {
173
+ data_source_name: params.data_source_name,
174
+ api_name: apiName,
175
+ params: params.params ?? {},
176
+ },
177
+ }
178
+ : {
179
+ method: "get_data_source_desc",
180
+ params: { name: params.data_source_name },
181
+ };
182
+
183
+ const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
184
+ deps.fetch(getKimiDatasourceUrl(), {
185
+ method: "POST",
186
+ headers: buildHeaders(token, toolCallId),
187
+ body: JSON.stringify(body),
188
+ signal: timeout.signal,
189
+ }),
190
+ );
191
+
192
+ const requestId = extractRequestId(response.headers);
193
+ const trace = buildTraceLine(toolCallId, requestId);
194
+
195
+ if (!response.ok) {
196
+ const bodyText = await readErrorBody(response);
197
+ return errorResult<string>(
198
+ `kimi_datasource failed: ${response.status}${bodyText ? ` ${bodyText}` : ""}${trace}`,
199
+ );
200
+ }
201
+
202
+ const data = (await response.json()) as DatasourceToolResponse;
203
+ if (data.is_success === false) {
204
+ const errorText =
205
+ typeof data.error === "string"
206
+ ? data.error
207
+ : JSON.stringify(data.error ?? data, null, 2);
208
+ return errorResult<string>(`kimi_datasource error: ${errorText}${trace}`);
209
+ }
210
+
211
+ let text = extractResponseText(data);
212
+ const files = extractResponseFiles(data);
213
+ if (files.length > 0) {
214
+ text += `\n\nNote: the backend returned ${files.length} file(s) that cannot be written in this context: ${files.join(", ")}`;
215
+ }
216
+ text += trace;
217
+
218
+ return {
219
+ content: [{ type: "text", text }],
220
+ details: text,
221
+ };
222
+ } catch (error) {
223
+ const message = error instanceof Error ? error.message : String(error);
224
+ return errorResult<string>(`kimi_datasource failed: ${message}`);
225
+ } finally {
226
+ timeout.cleanup();
227
+ }
228
+ },
229
+
230
+ renderResult(result, renderOptions) {
231
+ const text = firstText(result);
232
+ if (!shouldCollapse(defaultCollapsed, renderOptions.expanded)) {
233
+ return textComponent(text);
234
+ }
235
+ const preview = text.length > 200 ? `${text.slice(0, 200)}…` : text;
236
+ return textComponent(`kimi_datasource: ${text.length} chars\n${preview}`);
237
+ },
238
+ });
239
+ }
@@ -1,12 +1,19 @@
1
- import { AuthStorage, defineTool, type AgentToolResult } from "@earendil-works/pi-coding-agent";
1
+ import { defineTool, type AgentToolResult } from "@earendil-works/pi-coding-agent";
2
2
  import { Type, type Static } from "@earendil-works/pi-ai";
3
3
 
4
- import { PROVIDER_ID } from "../constants.ts";
5
- import { getCommonHeaders } from "../device.ts";
6
- import { refreshKimiAuthToken } from "../oauth.ts";
7
-
8
- const MOONSHOT_BASE_V1 = "https://api.kimi.com/coding/v1";
9
- const MOONSHOT_TIMEOUT_MS = 180_000;
4
+ import {
5
+ type BuildKimiToolOptions,
6
+ buildHeaders,
7
+ getKimiBaseV1,
8
+ buildKimiToolDeps,
9
+ buildTimeoutSignal,
10
+ errorResult,
11
+ fetchWithAuthRetry,
12
+ firstText,
13
+ readErrorBody,
14
+ shouldCollapse,
15
+ textComponent,
16
+ } from "./common.ts";
10
17
 
11
18
  export const moonshotSearchSchema = Type.Object({
12
19
  query: Type.String({ description: "The query text to search for." }),
@@ -45,76 +52,13 @@ export interface MoonshotFetchResult {
45
52
  content: string;
46
53
  }
47
54
 
48
- interface MoonshotToolDeps {
49
- fetch: typeof fetch;
50
- getAccessToken: () => string | null;
51
- refreshAccessToken: (currentToken: string) => Promise<string | null>;
52
- }
53
-
54
- export interface BuildMoonshotToolOptions {
55
- deps?: Partial<MoonshotToolDeps>;
56
- defaultCollapsed?: boolean;
57
- }
58
-
59
- interface MoonshotSearchResponse {
60
- search_results?: unknown;
61
- }
62
-
63
- function defaultGetAccessToken(): string | null {
64
- const credential = AuthStorage.create().get(PROVIDER_ID);
65
- if (!credential || credential.type !== "oauth") return null;
66
- return credential.access;
67
- }
68
-
69
- function buildDeps(options: BuildMoonshotToolOptions = {}): MoonshotToolDeps {
70
- return {
71
- fetch: options.deps?.fetch ?? fetch,
72
- getAccessToken: options.deps?.getAccessToken ?? defaultGetAccessToken,
73
- refreshAccessToken: options.deps?.refreshAccessToken ?? refreshKimiAuthToken,
74
- };
75
- }
76
-
77
55
  function clampLimit(value: number | undefined): number {
78
56
  if (typeof value !== "number" || !Number.isFinite(value)) return 5;
79
57
  return Math.max(1, Math.min(20, Math.trunc(value)));
80
58
  }
81
59
 
82
- function errorResult<T>(message: string): AgentToolResult<T> {
83
- return {
84
- content: [{ type: "text", text: message }],
85
- details: undefined as T,
86
- };
87
- }
88
-
89
- function buildTimeoutSignal(signal: AbortSignal | undefined): {
90
- signal: AbortSignal;
91
- cleanup: () => void;
92
- } {
93
- const controller = new AbortController();
94
- const timeout = setTimeout(() => controller.abort(), MOONSHOT_TIMEOUT_MS);
95
- const abort = () => controller.abort();
96
- if (signal?.aborted) {
97
- controller.abort();
98
- } else {
99
- signal?.addEventListener("abort", abort, { once: true });
100
- }
101
-
102
- return {
103
- signal: controller.signal,
104
- cleanup: () => {
105
- clearTimeout(timeout);
106
- signal?.removeEventListener("abort", abort);
107
- },
108
- };
109
- }
110
-
111
- function buildHeaders(accessToken: string, toolCallId: string): Record<string, string> {
112
- return {
113
- ...getCommonHeaders(),
114
- Authorization: `Bearer ${accessToken}`,
115
- "Content-Type": "application/json",
116
- "X-Msh-Tool-Call-Id": toolCallId,
117
- };
60
+ interface MoonshotSearchResponse {
61
+ search_results?: unknown;
118
62
  }
119
63
 
120
64
  function stringField(record: Record<string, unknown>, key: string): string {
@@ -144,22 +88,6 @@ function searchResultsText(results: MoonshotSearchResult[]): string {
144
88
  return JSON.stringify(results, null, 2);
145
89
  }
146
90
 
147
- function textComponent(text: string) {
148
- return {
149
- render: () => text.split("\n"),
150
- invalidate: () => {},
151
- };
152
- }
153
-
154
- function firstText(result: AgentToolResult<unknown>): string {
155
- const first = result.content[0];
156
- return first?.type === "text" ? first.text : "";
157
- }
158
-
159
- function shouldCollapse(defaultCollapsed: boolean, expanded: boolean | undefined): boolean {
160
- return defaultCollapsed && expanded !== true;
161
- }
162
-
163
91
  function renderSearchResult(
164
92
  result: AgentToolResult<MoonshotSearchResult[]>,
165
93
  expanded: boolean | undefined,
@@ -195,25 +123,8 @@ function renderFetchResult(
195
123
  );
196
124
  }
197
125
 
198
- async function readErrorBody(response: Response): Promise<string> {
199
- return response.text().catch(() => "");
200
- }
201
-
202
- async function fetchWithAuthRetry(
203
- deps: MoonshotToolDeps,
204
- accessToken: string,
205
- request: (token: string) => Promise<Response>,
206
- ): Promise<Response> {
207
- const response = await request(accessToken);
208
- if (response.status !== 401) return response;
209
-
210
- const refreshed = await deps.refreshAccessToken(accessToken);
211
- if (!refreshed || refreshed === accessToken) return response;
212
- return request(refreshed);
213
- }
214
-
215
- export function buildMoonshotSearchTool(options: BuildMoonshotToolOptions = {}) {
216
- const deps = buildDeps(options);
126
+ export function buildMoonshotSearchTool(options: BuildKimiToolOptions = {}) {
127
+ const deps = buildKimiToolDeps(options);
217
128
  const defaultCollapsed = options.defaultCollapsed ?? true;
218
129
 
219
130
  return defineTool({
@@ -236,7 +147,7 @@ export function buildMoonshotSearchTool(options: BuildMoonshotToolOptions = {})
236
147
  const timeout = buildTimeoutSignal(signal);
237
148
  try {
238
149
  const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
239
- deps.fetch(`${MOONSHOT_BASE_V1}/search`, {
150
+ deps.fetch(`${getKimiBaseV1()}/search`, {
240
151
  method: "POST",
241
152
  headers: buildHeaders(token, toolCallId),
242
153
  body: JSON.stringify({
@@ -276,8 +187,8 @@ export function buildMoonshotSearchTool(options: BuildMoonshotToolOptions = {})
276
187
  });
277
188
  }
278
189
 
279
- export function buildMoonshotFetchTool(options: BuildMoonshotToolOptions = {}) {
280
- const deps = buildDeps(options);
190
+ export function buildMoonshotFetchTool(options: BuildKimiToolOptions = {}) {
191
+ const deps = buildKimiToolDeps(options);
281
192
  const defaultCollapsed = options.defaultCollapsed ?? true;
282
193
 
283
194
  return defineTool({
@@ -298,7 +209,7 @@ export function buildMoonshotFetchTool(options: BuildMoonshotToolOptions = {}) {
298
209
  const timeout = buildTimeoutSignal(signal);
299
210
  try {
300
211
  const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
301
- deps.fetch(`${MOONSHOT_BASE_V1}/fetch`, {
212
+ deps.fetch(`${getKimiBaseV1()}/fetch`, {
302
213
  method: "POST",
303
214
  headers: {
304
215
  ...buildHeaders(token, toolCallId),