kimi-vercel-ai-sdk-provider 0.2.0
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.
- package/LICENSE +198 -0
- package/README.md +871 -0
- package/dist/index.d.mts +1317 -0
- package/dist/index.d.ts +1317 -0
- package/dist/index.js +2764 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2734 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +70 -0
- package/src/__tests__/caching.test.ts +97 -0
- package/src/__tests__/chat.test.ts +386 -0
- package/src/__tests__/code-integration.test.ts +562 -0
- package/src/__tests__/code-provider.test.ts +289 -0
- package/src/__tests__/code.test.ts +427 -0
- package/src/__tests__/core.test.ts +172 -0
- package/src/__tests__/files.test.ts +185 -0
- package/src/__tests__/integration.test.ts +457 -0
- package/src/__tests__/provider.test.ts +188 -0
- package/src/__tests__/tools.test.ts +519 -0
- package/src/chat/index.ts +42 -0
- package/src/chat/kimi-chat-language-model.ts +829 -0
- package/src/chat/kimi-chat-messages.ts +297 -0
- package/src/chat/kimi-chat-response.ts +84 -0
- package/src/chat/kimi-chat-settings.ts +216 -0
- package/src/code/index.ts +66 -0
- package/src/code/kimi-code-language-model.ts +669 -0
- package/src/code/kimi-code-messages.ts +303 -0
- package/src/code/kimi-code-provider.ts +239 -0
- package/src/code/kimi-code-settings.ts +193 -0
- package/src/code/kimi-code-types.ts +354 -0
- package/src/core/errors.ts +140 -0
- package/src/core/index.ts +36 -0
- package/src/core/types.ts +148 -0
- package/src/core/utils.ts +210 -0
- package/src/files/attachment-processor.ts +276 -0
- package/src/files/file-utils.ts +257 -0
- package/src/files/index.ts +24 -0
- package/src/files/kimi-file-client.ts +292 -0
- package/src/index.ts +122 -0
- package/src/kimi-provider.ts +263 -0
- package/src/tools/builtin-tools.ts +273 -0
- package/src/tools/index.ts +33 -0
- package/src/tools/prepare-tools.ts +306 -0
- package/src/version.ts +4 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core module exports.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type { KimiErrorData } from './errors';
|
|
7
|
+
// Types
|
|
8
|
+
export type {
|
|
9
|
+
KimiChatConfig,
|
|
10
|
+
KimiChatModelId,
|
|
11
|
+
KimiModelCapabilities,
|
|
12
|
+
KimiResponseMetadata,
|
|
13
|
+
KimiTokenUsage
|
|
14
|
+
} from './types';
|
|
15
|
+
// Utilities
|
|
16
|
+
export type { KimiExtendedUsage } from './utils';
|
|
17
|
+
// Errors
|
|
18
|
+
export {
|
|
19
|
+
KimiAuthenticationError,
|
|
20
|
+
KimiContentFilterError,
|
|
21
|
+
KimiContextLengthError,
|
|
22
|
+
KimiError,
|
|
23
|
+
KimiModelNotFoundError,
|
|
24
|
+
KimiRateLimitError,
|
|
25
|
+
KimiValidationError,
|
|
26
|
+
kimiErrorSchema,
|
|
27
|
+
kimiFailedResponseHandler
|
|
28
|
+
} from './errors';
|
|
29
|
+
export { inferModelCapabilities } from './types';
|
|
30
|
+
export {
|
|
31
|
+
convertKimiUsage,
|
|
32
|
+
extractMessageContent,
|
|
33
|
+
getKimiRequestId,
|
|
34
|
+
getResponseMetadata,
|
|
35
|
+
mapKimiFinishReason
|
|
36
|
+
} from './utils';
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for the Kimi provider.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LanguageModelV3 } from '@ai-sdk/provider';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Model IDs
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Available Kimi chat model IDs.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* - `kimi-k2.5` - Latest flagship model with multimodal support
|
|
17
|
+
* - `kimi-k2.5-thinking` - K2.5 with always-on deep reasoning
|
|
18
|
+
* - `kimi-k2-turbo` - Fast, cost-effective model
|
|
19
|
+
* - `kimi-k2-thinking` - K2 with always-on deep reasoning
|
|
20
|
+
*/
|
|
21
|
+
export type KimiChatModelId = 'kimi-k2.5' | 'kimi-k2.5-thinking' | 'kimi-k2-turbo' | 'kimi-k2-thinking' | (string & {});
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Model Capabilities
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Capabilities that can be detected from model ID patterns or explicitly set.
|
|
29
|
+
*/
|
|
30
|
+
export interface KimiModelCapabilities {
|
|
31
|
+
/**
|
|
32
|
+
* Whether the model supports thinking/reasoning mode.
|
|
33
|
+
* Models with `-thinking` suffix have this enabled by default.
|
|
34
|
+
*/
|
|
35
|
+
thinking?: boolean;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether the model always uses thinking mode (cannot be disabled).
|
|
39
|
+
* Thinking models like `kimi-k2.5-thinking` have this set to true.
|
|
40
|
+
*/
|
|
41
|
+
alwaysThinking?: boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether the model supports image inputs.
|
|
45
|
+
*/
|
|
46
|
+
imageInput?: boolean;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether the model supports video inputs.
|
|
50
|
+
* Currently only kimi-k2.5 models support video.
|
|
51
|
+
*/
|
|
52
|
+
videoInput?: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Maximum context window size in tokens.
|
|
56
|
+
*/
|
|
57
|
+
maxContextSize?: number;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether the model supports tool/function calling.
|
|
61
|
+
*/
|
|
62
|
+
toolCalling?: boolean;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether the model supports JSON mode.
|
|
66
|
+
*/
|
|
67
|
+
jsonMode?: boolean;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether the model supports structured outputs.
|
|
71
|
+
*/
|
|
72
|
+
structuredOutputs?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Infer model capabilities from the model ID.
|
|
77
|
+
*
|
|
78
|
+
* @param modelId - The model identifier
|
|
79
|
+
* @returns Inferred capabilities based on model name patterns
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* const caps = inferModelCapabilities('kimi-k2.5-thinking');
|
|
84
|
+
* // { thinking: true, alwaysThinking: true, videoInput: true, ... }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function inferModelCapabilities(modelId: string): KimiModelCapabilities {
|
|
88
|
+
const isThinkingModel = modelId.includes('-thinking');
|
|
89
|
+
const isK25Model = modelId.includes('k2.5') || modelId.includes('k2-5');
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
thinking: isThinkingModel,
|
|
93
|
+
alwaysThinking: isThinkingModel,
|
|
94
|
+
imageInput: true, // All Kimi models support images
|
|
95
|
+
videoInput: isK25Model, // Only K2.5 models support video
|
|
96
|
+
maxContextSize: 256_000, // 256k context window
|
|
97
|
+
toolCalling: true,
|
|
98
|
+
jsonMode: true,
|
|
99
|
+
structuredOutputs: true
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Provider Configuration
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Configuration for the chat language model.
|
|
109
|
+
* @internal
|
|
110
|
+
*/
|
|
111
|
+
export interface KimiChatConfig {
|
|
112
|
+
provider: string;
|
|
113
|
+
baseURL: string;
|
|
114
|
+
headers: () => Record<string, string | undefined>;
|
|
115
|
+
fetch?: typeof globalThis.fetch;
|
|
116
|
+
generateId?: () => string;
|
|
117
|
+
supportsStructuredOutputs?: boolean;
|
|
118
|
+
includeUsageInStream?: boolean;
|
|
119
|
+
supportedUrls?: LanguageModelV3['supportedUrls'];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// API Response Types
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Token usage information from Kimi API.
|
|
128
|
+
*/
|
|
129
|
+
export interface KimiTokenUsage {
|
|
130
|
+
prompt_tokens?: number | null;
|
|
131
|
+
completion_tokens?: number | null;
|
|
132
|
+
total_tokens?: number | null;
|
|
133
|
+
prompt_tokens_details?: {
|
|
134
|
+
cached_tokens?: number | null;
|
|
135
|
+
} | null;
|
|
136
|
+
completion_tokens_details?: {
|
|
137
|
+
reasoning_tokens?: number | null;
|
|
138
|
+
} | null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Response metadata from Kimi API.
|
|
143
|
+
*/
|
|
144
|
+
export interface KimiResponseMetadata {
|
|
145
|
+
id?: string | null;
|
|
146
|
+
model?: string | null;
|
|
147
|
+
created?: number | null;
|
|
148
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for the Kimi provider.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { JSONObject, LanguageModelV3FinishReason, LanguageModelV3Usage } from '@ai-sdk/provider';
|
|
7
|
+
import type { KimiResponseMetadata, KimiTokenUsage } from './types';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Finish Reason Mapping
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Map Kimi finish reasons to standard AI SDK finish reasons.
|
|
15
|
+
*
|
|
16
|
+
* @param finishReason - The raw finish reason from Kimi API
|
|
17
|
+
* @returns The mapped unified finish reason
|
|
18
|
+
*/
|
|
19
|
+
export function mapKimiFinishReason(finishReason: string | null | undefined): LanguageModelV3FinishReason['unified'] {
|
|
20
|
+
switch (finishReason) {
|
|
21
|
+
case 'stop':
|
|
22
|
+
return 'stop';
|
|
23
|
+
case 'length':
|
|
24
|
+
return 'length';
|
|
25
|
+
case 'content_filter':
|
|
26
|
+
return 'content-filter';
|
|
27
|
+
case 'tool_calls':
|
|
28
|
+
case 'function_call':
|
|
29
|
+
return 'tool-calls';
|
|
30
|
+
default:
|
|
31
|
+
return 'other';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Usage Conversion
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extended usage information including web search and code interpreter tokens.
|
|
41
|
+
*/
|
|
42
|
+
export interface KimiExtendedUsage extends LanguageModelV3Usage {
|
|
43
|
+
/**
|
|
44
|
+
* Tokens used by the built-in web search tool.
|
|
45
|
+
*/
|
|
46
|
+
webSearchTokens?: number;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Tokens used by the built-in code interpreter.
|
|
50
|
+
*/
|
|
51
|
+
codeInterpreterTokens?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert Kimi usage data to standard AI SDK usage format.
|
|
56
|
+
*
|
|
57
|
+
* @param usage - The raw usage data from Kimi API
|
|
58
|
+
* @param webSearchTokens - Optional web search token count
|
|
59
|
+
* @param codeInterpreterTokens - Optional code interpreter token count
|
|
60
|
+
* @returns Standardized usage object
|
|
61
|
+
*/
|
|
62
|
+
export function convertKimiUsage(
|
|
63
|
+
usage: KimiTokenUsage | null | undefined,
|
|
64
|
+
webSearchTokens?: number,
|
|
65
|
+
codeInterpreterTokens?: number
|
|
66
|
+
): KimiExtendedUsage {
|
|
67
|
+
if (usage == null) {
|
|
68
|
+
return {
|
|
69
|
+
inputTokens: {
|
|
70
|
+
total: undefined,
|
|
71
|
+
noCache: undefined,
|
|
72
|
+
cacheRead: undefined,
|
|
73
|
+
cacheWrite: undefined
|
|
74
|
+
},
|
|
75
|
+
outputTokens: {
|
|
76
|
+
total: undefined,
|
|
77
|
+
text: undefined,
|
|
78
|
+
reasoning: undefined
|
|
79
|
+
},
|
|
80
|
+
raw: undefined,
|
|
81
|
+
...(webSearchTokens != null ? { webSearchTokens } : {}),
|
|
82
|
+
...(codeInterpreterTokens != null ? { codeInterpreterTokens } : {})
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const promptTokens = usage.prompt_tokens ?? 0;
|
|
87
|
+
const completionTokens = usage.completion_tokens ?? 0;
|
|
88
|
+
const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
89
|
+
const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? 0;
|
|
90
|
+
|
|
91
|
+
// Convert to JSONObject for the raw field
|
|
92
|
+
const rawUsage: JSONObject = {
|
|
93
|
+
prompt_tokens: usage.prompt_tokens ?? undefined,
|
|
94
|
+
completion_tokens: usage.completion_tokens ?? undefined,
|
|
95
|
+
total_tokens: usage.total_tokens ?? undefined,
|
|
96
|
+
...(usage.prompt_tokens_details
|
|
97
|
+
? {
|
|
98
|
+
prompt_tokens_details: {
|
|
99
|
+
cached_tokens: usage.prompt_tokens_details.cached_tokens ?? undefined
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
: {}),
|
|
103
|
+
...(usage.completion_tokens_details
|
|
104
|
+
? {
|
|
105
|
+
completion_tokens_details: {
|
|
106
|
+
reasoning_tokens: usage.completion_tokens_details.reasoning_tokens ?? undefined
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
: {})
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
inputTokens: {
|
|
114
|
+
total: promptTokens,
|
|
115
|
+
noCache: promptTokens - cacheReadTokens,
|
|
116
|
+
cacheRead: cacheReadTokens,
|
|
117
|
+
cacheWrite: undefined
|
|
118
|
+
},
|
|
119
|
+
outputTokens: {
|
|
120
|
+
total: completionTokens,
|
|
121
|
+
text: completionTokens - reasoningTokens,
|
|
122
|
+
reasoning: reasoningTokens
|
|
123
|
+
},
|
|
124
|
+
raw: rawUsage,
|
|
125
|
+
...(webSearchTokens != null ? { webSearchTokens } : {}),
|
|
126
|
+
...(codeInterpreterTokens != null ? { codeInterpreterTokens } : {})
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Response Metadata
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Extract response metadata from Kimi API response.
|
|
136
|
+
*
|
|
137
|
+
* @param response - The raw response metadata
|
|
138
|
+
* @returns Formatted response metadata
|
|
139
|
+
*/
|
|
140
|
+
export function getResponseMetadata(response: KimiResponseMetadata) {
|
|
141
|
+
return {
|
|
142
|
+
id: response.id ?? undefined,
|
|
143
|
+
modelId: response.model ?? undefined,
|
|
144
|
+
timestamp: response.created != null ? new Date(response.created * 1000) : undefined
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Extract request ID from response headers.
|
|
150
|
+
*
|
|
151
|
+
* @param headers - Response headers
|
|
152
|
+
* @returns The request ID if found
|
|
153
|
+
*/
|
|
154
|
+
export function getKimiRequestId(headers?: Record<string, string>): string | undefined {
|
|
155
|
+
if (!headers) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const lowerHeaders = Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
|
|
160
|
+
|
|
161
|
+
return lowerHeaders['x-request-id'] || lowerHeaders['x-trace-id'] || lowerHeaders['x-moonshot-request-id'];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// Message Content Extraction
|
|
166
|
+
// ============================================================================
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Extract text and reasoning content from a message.
|
|
170
|
+
*
|
|
171
|
+
* @param message - The message object from API response
|
|
172
|
+
* @returns Extracted text and reasoning content
|
|
173
|
+
*/
|
|
174
|
+
export function extractMessageContent(message: {
|
|
175
|
+
content?: unknown;
|
|
176
|
+
reasoning_content?: string | null;
|
|
177
|
+
reasoning?: string | null;
|
|
178
|
+
}): { text: string; reasoning: string } {
|
|
179
|
+
let text = '';
|
|
180
|
+
let reasoning = '';
|
|
181
|
+
|
|
182
|
+
if (typeof message.content === 'string') {
|
|
183
|
+
text = message.content;
|
|
184
|
+
} else if (Array.isArray(message.content)) {
|
|
185
|
+
for (const part of message.content) {
|
|
186
|
+
if (part && typeof part === 'object') {
|
|
187
|
+
const candidate = part as Record<string, unknown>;
|
|
188
|
+
if (candidate.type === 'text' && typeof candidate.text === 'string') {
|
|
189
|
+
text += candidate.text;
|
|
190
|
+
}
|
|
191
|
+
if (candidate.type === 'thinking' && typeof candidate.thinking === 'string') {
|
|
192
|
+
reasoning += candidate.thinking;
|
|
193
|
+
}
|
|
194
|
+
if (candidate.type === 'reasoning' && typeof candidate.text === 'string') {
|
|
195
|
+
reasoning += candidate.text;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (typeof message.reasoning_content === 'string') {
|
|
202
|
+
reasoning += message.reasoning_content;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (typeof message.reasoning === 'string') {
|
|
206
|
+
reasoning += message.reasoning;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { text, reasoning };
|
|
210
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment processor for experimental_attachments support.
|
|
3
|
+
* Automatically uploads files to Kimi and injects content into prompts.
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getExtensionFromPath,
|
|
9
|
+
getMediaTypeFromExtension,
|
|
10
|
+
isDocumentMediaType,
|
|
11
|
+
isFileExtractMediaType,
|
|
12
|
+
isImageMediaType,
|
|
13
|
+
isVideoMediaType
|
|
14
|
+
} from './file-utils';
|
|
15
|
+
import { KimiFileClient, type KimiFileClientConfig } from './kimi-file-client';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* An attachment from experimental_attachments.
|
|
23
|
+
*/
|
|
24
|
+
export interface Attachment {
|
|
25
|
+
/** URL of the attachment */
|
|
26
|
+
url?: string;
|
|
27
|
+
/** Name of the attachment */
|
|
28
|
+
name?: string;
|
|
29
|
+
/** MIME type */
|
|
30
|
+
contentType?: string;
|
|
31
|
+
/** Raw content data */
|
|
32
|
+
content?: Uint8Array | string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Processed attachment result.
|
|
37
|
+
*/
|
|
38
|
+
export interface ProcessedAttachment {
|
|
39
|
+
/** Original attachment */
|
|
40
|
+
original: Attachment;
|
|
41
|
+
/** Processing type */
|
|
42
|
+
type: 'text-inject' | 'image-url' | 'video-url' | 'skip';
|
|
43
|
+
/** Extracted text content (for documents) */
|
|
44
|
+
textContent?: string;
|
|
45
|
+
/** URL to use in message (for images/videos) */
|
|
46
|
+
mediaUrl?: string;
|
|
47
|
+
/** Kimi file ID (if uploaded) */
|
|
48
|
+
fileId?: string;
|
|
49
|
+
/** Error if processing failed */
|
|
50
|
+
error?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Options for processing attachments.
|
|
55
|
+
*/
|
|
56
|
+
export interface ProcessAttachmentsOptions {
|
|
57
|
+
/** Attachments to process */
|
|
58
|
+
attachments: Attachment[];
|
|
59
|
+
/** File client configuration */
|
|
60
|
+
clientConfig: KimiFileClientConfig;
|
|
61
|
+
/** Whether to auto-upload documents for extraction */
|
|
62
|
+
autoUploadDocuments?: boolean;
|
|
63
|
+
/** Whether to upload images to Kimi's file API */
|
|
64
|
+
uploadImages?: boolean;
|
|
65
|
+
/** Whether to delete files after extraction (cleanup) */
|
|
66
|
+
cleanupAfterExtract?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Main Function
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Process experimental_attachments for Kimi.
|
|
75
|
+
*
|
|
76
|
+
* This function handles different attachment types:
|
|
77
|
+
* - Documents (PDF, DOC, etc.): Uploads to Kimi, extracts content, returns text to inject
|
|
78
|
+
* - Images: Returns URL for vision input
|
|
79
|
+
* - Videos: Returns URL for video input
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* const processed = await processAttachments({
|
|
84
|
+
* attachments: message.experimental_attachments ?? [],
|
|
85
|
+
* clientConfig: {
|
|
86
|
+
* baseURL: 'https://api.moonshot.ai/v1',
|
|
87
|
+
* headers: () => ({ Authorization: `Bearer ${apiKey}` }),
|
|
88
|
+
* },
|
|
89
|
+
* });
|
|
90
|
+
*
|
|
91
|
+
* // Inject document content into system messages
|
|
92
|
+
* const documentContent = processed
|
|
93
|
+
* .filter(p => p.type === 'text-inject' && p.textContent)
|
|
94
|
+
* .map(p => p.textContent)
|
|
95
|
+
* .join('\n');
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export async function processAttachments(options: ProcessAttachmentsOptions): Promise<ProcessedAttachment[]> {
|
|
99
|
+
const {
|
|
100
|
+
attachments,
|
|
101
|
+
clientConfig,
|
|
102
|
+
autoUploadDocuments = true,
|
|
103
|
+
uploadImages = false,
|
|
104
|
+
cleanupAfterExtract = false
|
|
105
|
+
} = options;
|
|
106
|
+
|
|
107
|
+
const results: ProcessedAttachment[] = [];
|
|
108
|
+
const client = new KimiFileClient(clientConfig);
|
|
109
|
+
|
|
110
|
+
for (const attachment of attachments) {
|
|
111
|
+
try {
|
|
112
|
+
const processed = await processAttachment(attachment, client, {
|
|
113
|
+
autoUploadDocuments,
|
|
114
|
+
uploadImages,
|
|
115
|
+
cleanupAfterExtract
|
|
116
|
+
});
|
|
117
|
+
results.push(processed);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
results.push({
|
|
120
|
+
original: attachment,
|
|
121
|
+
type: 'skip',
|
|
122
|
+
error: error instanceof Error ? error.message : String(error)
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Helper Functions
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
async function processAttachment(
|
|
135
|
+
attachment: Attachment,
|
|
136
|
+
client: KimiFileClient,
|
|
137
|
+
options: { autoUploadDocuments: boolean; uploadImages: boolean; cleanupAfterExtract: boolean }
|
|
138
|
+
): Promise<ProcessedAttachment> {
|
|
139
|
+
// Determine content type
|
|
140
|
+
const contentType = resolveContentType(attachment);
|
|
141
|
+
|
|
142
|
+
// Handle images - just return URL for vision
|
|
143
|
+
if (isImageMediaType(contentType)) {
|
|
144
|
+
if (options.uploadImages && attachment.content) {
|
|
145
|
+
// Upload image if content is provided
|
|
146
|
+
const result = await client.upload({
|
|
147
|
+
data: attachment.content,
|
|
148
|
+
filename: attachment.name ?? 'image.jpg',
|
|
149
|
+
mediaType: contentType,
|
|
150
|
+
purpose: 'image'
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
original: attachment,
|
|
155
|
+
type: 'image-url',
|
|
156
|
+
fileId: result.id,
|
|
157
|
+
mediaUrl: attachment.url
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
original: attachment,
|
|
163
|
+
type: 'image-url',
|
|
164
|
+
mediaUrl: attachment.url
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Handle videos - just return URL for video understanding
|
|
169
|
+
if (isVideoMediaType(contentType)) {
|
|
170
|
+
return {
|
|
171
|
+
original: attachment,
|
|
172
|
+
type: 'video-url',
|
|
173
|
+
mediaUrl: attachment.url
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Handle documents that need extraction
|
|
178
|
+
if (options.autoUploadDocuments && (isDocumentMediaType(contentType) || isFileExtractMediaType(contentType))) {
|
|
179
|
+
// Need to fetch content if only URL is provided
|
|
180
|
+
let data: Uint8Array | string;
|
|
181
|
+
|
|
182
|
+
if (attachment.content) {
|
|
183
|
+
data = attachment.content;
|
|
184
|
+
} else if (attachment.url) {
|
|
185
|
+
// Fetch the file from URL
|
|
186
|
+
const response = await fetch(attachment.url);
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
throw new Error(`Failed to fetch attachment: ${response.status}`);
|
|
189
|
+
}
|
|
190
|
+
data = new Uint8Array(await response.arrayBuffer());
|
|
191
|
+
} else {
|
|
192
|
+
return {
|
|
193
|
+
original: attachment,
|
|
194
|
+
type: 'skip',
|
|
195
|
+
error: 'No content or URL provided for document attachment'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Upload and extract content
|
|
200
|
+
const result = await client.uploadAndExtract({
|
|
201
|
+
data,
|
|
202
|
+
filename: attachment.name ?? guessFilename(attachment, contentType),
|
|
203
|
+
mediaType: contentType,
|
|
204
|
+
purpose: 'file-extract'
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Cleanup if requested
|
|
208
|
+
if (options.cleanupAfterExtract && result.file.id) {
|
|
209
|
+
try {
|
|
210
|
+
await client.deleteFile(result.file.id);
|
|
211
|
+
} catch {
|
|
212
|
+
// Ignore cleanup errors
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
original: attachment,
|
|
218
|
+
type: 'text-inject',
|
|
219
|
+
textContent: result.content,
|
|
220
|
+
fileId: result.file.id
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Skip unsupported types
|
|
225
|
+
return {
|
|
226
|
+
original: attachment,
|
|
227
|
+
type: 'skip',
|
|
228
|
+
error: `Unsupported content type: ${contentType}`
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function resolveContentType(attachment: Attachment): string {
|
|
233
|
+
// Use explicit content type if provided
|
|
234
|
+
if (attachment.contentType) {
|
|
235
|
+
return attachment.contentType;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Try to infer from filename or URL
|
|
239
|
+
const path = attachment.name ?? attachment.url;
|
|
240
|
+
if (path) {
|
|
241
|
+
const ext = getExtensionFromPath(path);
|
|
242
|
+
if (ext) {
|
|
243
|
+
return getMediaTypeFromExtension(ext);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Default to octet-stream
|
|
248
|
+
return 'application/octet-stream';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function guessFilename(attachment: Attachment, contentType: string): string {
|
|
252
|
+
if (attachment.name) {
|
|
253
|
+
return attachment.name;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (attachment.url) {
|
|
257
|
+
const urlPath = attachment.url.split('?')[0];
|
|
258
|
+
const segments = urlPath.split('/');
|
|
259
|
+
const lastSegment = segments[segments.length - 1];
|
|
260
|
+
if (lastSegment && lastSegment.includes('.')) {
|
|
261
|
+
return lastSegment;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Generate filename from content type
|
|
266
|
+
const extensionMap: Record<string, string> = {
|
|
267
|
+
'application/pdf': 'document.pdf',
|
|
268
|
+
'text/plain': 'document.txt',
|
|
269
|
+
'application/msword': 'document.doc',
|
|
270
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'document.docx',
|
|
271
|
+
'image/jpeg': 'image.jpg',
|
|
272
|
+
'image/png': 'image.png'
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
return extensionMap[contentType] ?? 'file.bin';
|
|
276
|
+
}
|