@pikoloo/codex-proxy 1.0.6
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 +21 -0
- package/README.md +199 -0
- package/bin/cli.js +118 -0
- package/docs/ACCOUNTS.md +202 -0
- package/docs/API.md +289 -0
- package/docs/ARCHITECTURE.md +129 -0
- package/docs/CLAUDE_INTEGRATION.md +163 -0
- package/docs/OAUTH.md +85 -0
- package/docs/OPENCLAW.md +34 -0
- package/docs/legal.md +11 -0
- package/images/dashboard-screenshot.png +0 -0
- package/images/demo-screenshot.png +0 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- package/package.json +61 -0
- package/public/css/style.css +1502 -0
- package/public/index.html +827 -0
- package/public/js/app.js +601 -0
- package/src/account-manager.js +528 -0
- package/src/account-rotation/index.js +93 -0
- package/src/account-rotation/rate-limits.js +293 -0
- package/src/account-rotation/strategies/base-strategy.js +48 -0
- package/src/account-rotation/strategies/index.js +31 -0
- package/src/account-rotation/strategies/round-robin-strategy.js +42 -0
- package/src/account-rotation/strategies/sticky-strategy.js +97 -0
- package/src/claude-config.js +153 -0
- package/src/cli/accounts.js +557 -0
- package/src/direct-api.js +164 -0
- package/src/format-converter.js +420 -0
- package/src/index.js +46 -0
- package/src/kilo-api.js +68 -0
- package/src/kilo-format-converter.js +285 -0
- package/src/kilo-models.js +103 -0
- package/src/kilo-streamer.js +243 -0
- package/src/middleware/credentials.js +116 -0
- package/src/middleware/sse.js +96 -0
- package/src/model-api.js +189 -0
- package/src/model-mapper.js +157 -0
- package/src/oauth.js +666 -0
- package/src/response-streamer.js +409 -0
- package/src/routes/accounts-route.js +332 -0
- package/src/routes/api-routes.js +98 -0
- package/src/routes/chat-route.js +229 -0
- package/src/routes/claude-config-route.js +121 -0
- package/src/routes/logs-route.js +43 -0
- package/src/routes/messages-route.js +203 -0
- package/src/routes/models-route.js +119 -0
- package/src/routes/settings-route.js +143 -0
- package/src/security.js +142 -0
- package/src/server-settings.js +56 -0
- package/src/server.js +58 -0
- package/src/signature-cache.js +106 -0
- package/src/thinking-utils.js +312 -0
- package/src/utils/logger.js +156 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kilo Format Converter
|
|
3
|
+
* Converts between Anthropic Messages API and OpenAI Chat Completions format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { cleanCacheControl } from './thinking-utils.js';
|
|
7
|
+
import { toAnthropicToolId, toOpenAIToolId } from './format-converter.js';
|
|
8
|
+
|
|
9
|
+
function extractSystemPrompt(system) {
|
|
10
|
+
if (!system) return [];
|
|
11
|
+
if (typeof system === 'string') return [{ role: 'system', content: system }];
|
|
12
|
+
if (Array.isArray(system)) {
|
|
13
|
+
const text = system
|
|
14
|
+
.filter(block => block.type === 'text')
|
|
15
|
+
.map(block => block.text)
|
|
16
|
+
.join('\n\n');
|
|
17
|
+
return text ? [{ role: 'system', content: text }] : [];
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sanitizeSchema(schema) {
|
|
23
|
+
if (typeof schema !== 'object' || schema === null) {
|
|
24
|
+
return { type: 'object' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = {};
|
|
28
|
+
|
|
29
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
30
|
+
if (key === 'const') {
|
|
31
|
+
result.enum = [value];
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if ([
|
|
36
|
+
'$schema', '$id', '$ref', '$defs', '$comment',
|
|
37
|
+
'additionalItems', 'definitions', 'examples',
|
|
38
|
+
'minLength', 'maxLength', 'pattern', 'format',
|
|
39
|
+
'minItems', 'maxItems', 'minimum', 'maximum',
|
|
40
|
+
'exclusiveMinimum', 'exclusiveMaximum',
|
|
41
|
+
'allOf', 'anyOf', 'oneOf', 'not'
|
|
42
|
+
].includes(key)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (key === 'additionalProperties' && typeof value === 'boolean') {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (key === 'type' && Array.isArray(value)) {
|
|
51
|
+
const nonNullTypes = value.filter(t => t !== 'null');
|
|
52
|
+
result.type = nonNullTypes.length > 0 ? nonNullTypes[0] : 'string';
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (key === 'properties' && value && typeof value === 'object') {
|
|
57
|
+
result.properties = {};
|
|
58
|
+
for (const [propKey, propValue] of Object.entries(value)) {
|
|
59
|
+
result.properties[propKey] = sanitizeSchema(propValue);
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (key === 'items') {
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
result.items = value.map(item => sanitizeSchema(item));
|
|
67
|
+
} else if (typeof value === 'object') {
|
|
68
|
+
result.items = sanitizeSchema(value);
|
|
69
|
+
} else {
|
|
70
|
+
result.items = value;
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (key === 'required' && Array.isArray(value)) {
|
|
76
|
+
result.required = value;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (key === 'enum' && Array.isArray(value)) {
|
|
81
|
+
result.enum = value;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (['type', 'description', 'title'].includes(key)) {
|
|
86
|
+
result[key] = value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!result.type) {
|
|
91
|
+
result.type = 'object';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (result.type === 'object' && !result.properties) {
|
|
95
|
+
result.properties = {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function convertTools(tools) {
|
|
102
|
+
if (!Array.isArray(tools)) return undefined;
|
|
103
|
+
return tools.map(tool => ({
|
|
104
|
+
type: 'function',
|
|
105
|
+
function: {
|
|
106
|
+
name: tool.name,
|
|
107
|
+
description: tool.description || '',
|
|
108
|
+
parameters: sanitizeSchema(tool.input_schema || { type: 'object' })
|
|
109
|
+
}
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function convertToolChoice(toolChoice) {
|
|
114
|
+
if (!toolChoice) return undefined;
|
|
115
|
+
if (typeof toolChoice === 'string') return toolChoice;
|
|
116
|
+
if (toolChoice.type === 'tool' && toolChoice.name) {
|
|
117
|
+
return { type: 'function', function: { name: toolChoice.name } };
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeTextBlocks(content) {
|
|
123
|
+
if (typeof content === 'string') return [content];
|
|
124
|
+
if (!Array.isArray(content)) return [];
|
|
125
|
+
return content.filter(block => block.type === 'text').map(block => block.text);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeToolResultContent(block) {
|
|
129
|
+
if (typeof block.content === 'string') return block.content;
|
|
130
|
+
if (Array.isArray(block.content)) {
|
|
131
|
+
return block.content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
|
132
|
+
}
|
|
133
|
+
if (block.content && typeof block.content === 'object') {
|
|
134
|
+
return JSON.stringify(block.content);
|
|
135
|
+
}
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function convertMessages(messages = []) {
|
|
140
|
+
// Clean cache_control from messages first
|
|
141
|
+
const cleanedMessages = cleanCacheControl(messages);
|
|
142
|
+
|
|
143
|
+
const output = [];
|
|
144
|
+
|
|
145
|
+
for (const msg of cleanedMessages) {
|
|
146
|
+
if (msg.role === 'user') {
|
|
147
|
+
const textParts = normalizeTextBlocks(msg.content);
|
|
148
|
+
if (textParts.length > 0) {
|
|
149
|
+
output.push({ role: 'user', content: textParts.join('\n\n') });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (Array.isArray(msg.content)) {
|
|
153
|
+
for (const block of msg.content) {
|
|
154
|
+
if (block.type === 'tool_result') {
|
|
155
|
+
output.push({
|
|
156
|
+
role: 'tool',
|
|
157
|
+
tool_call_id: toOpenAIToolId(block.tool_use_id),
|
|
158
|
+
content: normalizeToolResultContent(block)
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (msg.role === 'assistant') {
|
|
166
|
+
const textParts = normalizeTextBlocks(msg.content);
|
|
167
|
+
const toolCalls = [];
|
|
168
|
+
|
|
169
|
+
if (Array.isArray(msg.content)) {
|
|
170
|
+
for (const block of msg.content) {
|
|
171
|
+
if (block.type === 'tool_use') {
|
|
172
|
+
const openAIId = toOpenAIToolId(block.id);
|
|
173
|
+
toolCalls.push({
|
|
174
|
+
id: openAIId,
|
|
175
|
+
type: 'function',
|
|
176
|
+
function: {
|
|
177
|
+
name: block.name,
|
|
178
|
+
arguments: typeof block.input === 'string'
|
|
179
|
+
? block.input
|
|
180
|
+
: JSON.stringify(block.input || {})
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (textParts.length > 0 || toolCalls.length > 0) {
|
|
188
|
+
const message = {
|
|
189
|
+
role: 'assistant',
|
|
190
|
+
content: textParts.length > 0 ? textParts.join('\n\n') : null
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (toolCalls.length > 0) {
|
|
194
|
+
message.tool_calls = toolCalls;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
output.push(message);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return output;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function convertAnthropicToOpenAIChat(anthropicRequest, targetModel) {
|
|
206
|
+
const { system, messages, tools, tool_choice, max_tokens, temperature, top_p, stop_sequences, stream } = anthropicRequest;
|
|
207
|
+
|
|
208
|
+
const convertedMessages = [
|
|
209
|
+
...extractSystemPrompt(system),
|
|
210
|
+
...convertMessages(messages || [])
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const request = {
|
|
214
|
+
model: targetModel,
|
|
215
|
+
messages: convertedMessages,
|
|
216
|
+
stream: stream !== false
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (typeof max_tokens === 'number') request.max_tokens = max_tokens;
|
|
220
|
+
if (typeof temperature === 'number') request.temperature = temperature;
|
|
221
|
+
if (typeof top_p === 'number') request.top_p = top_p;
|
|
222
|
+
if (Array.isArray(stop_sequences) && stop_sequences.length > 0) request.stop = stop_sequences;
|
|
223
|
+
|
|
224
|
+
const convertedTools = convertTools(tools);
|
|
225
|
+
if (convertedTools?.length) request.tools = convertedTools;
|
|
226
|
+
|
|
227
|
+
const convertedToolChoice = convertToolChoice(tool_choice);
|
|
228
|
+
if (convertedToolChoice) request.tool_choice = convertedToolChoice;
|
|
229
|
+
|
|
230
|
+
return request;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function convertOpenAIChatToAnthropic(openAiResponse) {
|
|
234
|
+
const message = openAiResponse?.choices?.[0]?.message || {};
|
|
235
|
+
const content = [];
|
|
236
|
+
|
|
237
|
+
if (message.reasoning || message.reasoning_content) {
|
|
238
|
+
content.push({
|
|
239
|
+
type: 'thinking',
|
|
240
|
+
thinking: message.reasoning || message.reasoning_content,
|
|
241
|
+
signature: 'kilo-reasoning' // Placeholder signature
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (message.content) {
|
|
246
|
+
content.push({ type: 'text', text: message.content });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (Array.isArray(message.tool_calls)) {
|
|
250
|
+
for (const call of message.tool_calls) {
|
|
251
|
+
let input = {};
|
|
252
|
+
try {
|
|
253
|
+
input = typeof call.function?.arguments === 'string'
|
|
254
|
+
? JSON.parse(call.function.arguments)
|
|
255
|
+
: call.function?.arguments || {};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
input = {};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
content.push({
|
|
261
|
+
type: 'tool_use',
|
|
262
|
+
id: toAnthropicToolId(call.id),
|
|
263
|
+
name: call.function?.name || 'unknown',
|
|
264
|
+
input
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const finishReason = openAiResponse?.choices?.[0]?.finish_reason;
|
|
270
|
+
const stopReason = finishReason === 'tool_calls' ? 'tool_use' : 'end_turn';
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
content: content.length > 0 ? content : [{ type: 'text', text: '' }],
|
|
274
|
+
stopReason,
|
|
275
|
+
usage: {
|
|
276
|
+
input_tokens: openAiResponse?.usage?.prompt_tokens || 0,
|
|
277
|
+
output_tokens: openAiResponse?.usage?.completion_tokens || 0
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export default {
|
|
283
|
+
convertAnthropicToOpenAIChat,
|
|
284
|
+
convertOpenAIChatToAnthropic
|
|
285
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kilo Models
|
|
3
|
+
* Fetches and caches available free models from the Kilo API.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const KILO_MODELS_URL = 'https://api.kilo.ai/api/openrouter/models';
|
|
7
|
+
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
8
|
+
|
|
9
|
+
let cachedModels = null;
|
|
10
|
+
let cacheTimestamp = 0;
|
|
11
|
+
|
|
12
|
+
const KILO_HEADERS = {
|
|
13
|
+
Authorization: 'Bearer anonymous',
|
|
14
|
+
'HTTP-Referer': 'https://kilo.ai'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fetch all models from Kilo API and filter to free ones with tool support.
|
|
19
|
+
* Results are cached for 10 minutes.
|
|
20
|
+
* @returns {Promise<Array<{id: string, name: string, context_length: number}>>}
|
|
21
|
+
*/
|
|
22
|
+
export async function fetchFreeModels() {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
if (cachedModels && (now - cacheTimestamp) < CACHE_TTL_MS) {
|
|
25
|
+
return cachedModels;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(KILO_MODELS_URL, {
|
|
30
|
+
headers: KILO_HEADERS
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
console.warn(`[KiloModels] Failed to fetch models: ${response.status}`);
|
|
35
|
+
return cachedModels || getHardcodedFallback();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
const allModels = data?.data || [];
|
|
40
|
+
|
|
41
|
+
const freeModels = allModels
|
|
42
|
+
.filter(m => m.isFree === true)
|
|
43
|
+
.filter(m => !m.id.includes('deprecated') && m.id !== 'kilo/auto-free')
|
|
44
|
+
.map(m => ({
|
|
45
|
+
id: m.id,
|
|
46
|
+
name: m.name || m.id,
|
|
47
|
+
context_length: m.context_length || 0,
|
|
48
|
+
supportsTools: (m.supported_parameters || []).includes('tools')
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
cachedModels = freeModels;
|
|
52
|
+
cacheTimestamp = now;
|
|
53
|
+
console.log(`[KiloModels] Fetched ${freeModels.length} free models from API`);
|
|
54
|
+
return freeModels;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.warn(`[KiloModels] Error fetching models: ${error.message}`);
|
|
57
|
+
return cachedModels || getHardcodedFallback();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the list of free model IDs (just the id strings).
|
|
63
|
+
* @returns {Promise<string[]>}
|
|
64
|
+
*/
|
|
65
|
+
export async function getFreeModelIds() {
|
|
66
|
+
const models = await fetchFreeModels();
|
|
67
|
+
return models.map(m => m.id);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if a given model ID is currently free.
|
|
72
|
+
* @param {string} modelId
|
|
73
|
+
* @returns {Promise<boolean>}
|
|
74
|
+
*/
|
|
75
|
+
export async function isModelFree(modelId) {
|
|
76
|
+
const models = await fetchFreeModels();
|
|
77
|
+
return models.some(m => m.id === modelId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Invalidate the cache (e.g. after a failed request).
|
|
82
|
+
*/
|
|
83
|
+
export function invalidateCache() {
|
|
84
|
+
cachedModels = null;
|
|
85
|
+
cacheTimestamp = 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Hardcoded fallback in case the API is unreachable.
|
|
90
|
+
*/
|
|
91
|
+
function getHardcodedFallback() {
|
|
92
|
+
return [
|
|
93
|
+
{ id: 'minimax/minimax-m2.5:free', name: 'MiniMax M2.5', context_length: 204800, supportsTools: true },
|
|
94
|
+
{ id: 'kilo-auto/free', name: 'Kilo Auto Free', context_length: 204800, supportsTools: true }
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default {
|
|
99
|
+
fetchFreeModels,
|
|
100
|
+
getFreeModelIds,
|
|
101
|
+
isModelFree,
|
|
102
|
+
invalidateCache
|
|
103
|
+
};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kilo Streamer
|
|
3
|
+
* Streams OpenAI Chat Completions SSE and converts to Anthropic SSE events
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { generateMessageId, toAnthropicToolId } from './format-converter.js';
|
|
7
|
+
|
|
8
|
+
export async function* streamOpenAIChat(response, model) {
|
|
9
|
+
const messageId = generateMessageId();
|
|
10
|
+
let hasEmittedStart = false;
|
|
11
|
+
let blockIndex = 0;
|
|
12
|
+
let currentBlockType = null;
|
|
13
|
+
let currentToolCallId = null;
|
|
14
|
+
let currentToolName = null;
|
|
15
|
+
let pendingToolArgs = new Map();
|
|
16
|
+
let stopReason = 'end_turn';
|
|
17
|
+
let usage = { input_tokens: 0, output_tokens: 0 };
|
|
18
|
+
|
|
19
|
+
const reader = response.body.getReader();
|
|
20
|
+
const decoder = new TextDecoder();
|
|
21
|
+
let buffer = '';
|
|
22
|
+
|
|
23
|
+
const emitMessageStart = () => ({
|
|
24
|
+
event: 'message_start',
|
|
25
|
+
data: {
|
|
26
|
+
type: 'message_start',
|
|
27
|
+
message: {
|
|
28
|
+
id: messageId,
|
|
29
|
+
type: 'message',
|
|
30
|
+
role: 'assistant',
|
|
31
|
+
model,
|
|
32
|
+
content: [],
|
|
33
|
+
stop_reason: null,
|
|
34
|
+
stop_sequence: null,
|
|
35
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const emitContentBlockStart = (contentBlock) => ({
|
|
41
|
+
event: 'content_block_start',
|
|
42
|
+
data: {
|
|
43
|
+
type: 'content_block_start',
|
|
44
|
+
index: blockIndex,
|
|
45
|
+
content_block: contentBlock
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const emitContentBlockDelta = (delta) => ({
|
|
50
|
+
event: 'content_block_delta',
|
|
51
|
+
data: {
|
|
52
|
+
type: 'content_block_delta',
|
|
53
|
+
index: blockIndex,
|
|
54
|
+
delta
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const emitContentBlockStop = () => ({
|
|
59
|
+
event: 'content_block_stop',
|
|
60
|
+
data: { type: 'content_block_stop', index: blockIndex }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const startTextBlock = () => {
|
|
64
|
+
currentBlockType = 'text';
|
|
65
|
+
currentToolCallId = null;
|
|
66
|
+
currentToolName = null;
|
|
67
|
+
return emitContentBlockStart({ type: 'text', text: '' });
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const startThinkingBlock = () => {
|
|
71
|
+
currentBlockType = 'thinking';
|
|
72
|
+
currentToolCallId = null;
|
|
73
|
+
currentToolName = null;
|
|
74
|
+
return emitContentBlockStart({ type: 'thinking', thinking: '' });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const startToolBlock = (toolCall) => {
|
|
78
|
+
currentBlockType = 'tool_use';
|
|
79
|
+
const rawId = toolCall.id || `call_${Math.random().toString(36).slice(2)}`;
|
|
80
|
+
currentToolCallId = toAnthropicToolId(rawId);
|
|
81
|
+
currentToolName = toolCall.function?.name || 'tool';
|
|
82
|
+
stopReason = 'tool_use';
|
|
83
|
+
return emitContentBlockStart({
|
|
84
|
+
type: 'tool_use',
|
|
85
|
+
id: currentToolCallId,
|
|
86
|
+
name: currentToolName,
|
|
87
|
+
input: {}
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleDelta = (delta) => {
|
|
92
|
+
const events = [];
|
|
93
|
+
|
|
94
|
+
// Handle reasoning/thinking content (from models like MiniMax M2.5)
|
|
95
|
+
const reasoningContent = delta.reasoning || delta.reasoning_content;
|
|
96
|
+
if (reasoningContent) {
|
|
97
|
+
if (!hasEmittedStart) {
|
|
98
|
+
hasEmittedStart = true;
|
|
99
|
+
events.push(emitMessageStart());
|
|
100
|
+
events.push(startThinkingBlock());
|
|
101
|
+
} else if (currentBlockType !== 'thinking') {
|
|
102
|
+
if (currentBlockType === 'thinking') {
|
|
103
|
+
events.push(emitContentBlockDelta({ type: 'signature_delta', signature: 'kilo-reasoning' }));
|
|
104
|
+
}
|
|
105
|
+
events.push(emitContentBlockStop());
|
|
106
|
+
blockIndex++;
|
|
107
|
+
events.push(startThinkingBlock());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
events.push(emitContentBlockDelta({ type: 'thinking_delta', thinking: reasoningContent }));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const shouldStartText = (delta.content !== undefined && delta.content !== null) && (
|
|
114
|
+
delta.content.length > 0 || (!hasEmittedStart && !reasoningContent)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (shouldStartText) {
|
|
118
|
+
if (!hasEmittedStart) {
|
|
119
|
+
hasEmittedStart = true;
|
|
120
|
+
events.push(emitMessageStart());
|
|
121
|
+
events.push(startTextBlock());
|
|
122
|
+
} else if (currentBlockType !== 'text') {
|
|
123
|
+
if (currentBlockType === 'thinking') {
|
|
124
|
+
events.push(emitContentBlockDelta({ type: 'signature_delta', signature: 'kilo-reasoning' }));
|
|
125
|
+
}
|
|
126
|
+
events.push(emitContentBlockStop());
|
|
127
|
+
blockIndex++;
|
|
128
|
+
events.push(startTextBlock());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (delta.content.length > 0) {
|
|
132
|
+
events.push(emitContentBlockDelta({ type: 'text_delta', text: delta.content }));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
137
|
+
for (const toolCall of delta.tool_calls) {
|
|
138
|
+
if (!hasEmittedStart) {
|
|
139
|
+
hasEmittedStart = true;
|
|
140
|
+
events.push(emitMessageStart());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const toolId = toolCall.id ? toAnthropicToolId(toolCall.id) : currentToolCallId;
|
|
144
|
+
|
|
145
|
+
if (currentBlockType !== 'tool_use' || currentToolCallId !== toolId) {
|
|
146
|
+
if (currentBlockType) {
|
|
147
|
+
if (currentBlockType === 'thinking') {
|
|
148
|
+
events.push(emitContentBlockDelta({ type: 'signature_delta', signature: 'kilo-reasoning' }));
|
|
149
|
+
}
|
|
150
|
+
events.push(emitContentBlockStop());
|
|
151
|
+
blockIndex++;
|
|
152
|
+
}
|
|
153
|
+
events.push(startToolBlock(toolCall));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const argsDelta = toolCall.function?.arguments || '';
|
|
157
|
+
if (argsDelta) {
|
|
158
|
+
const callIdForArgs = toolCall.id || currentToolCallId;
|
|
159
|
+
const prev = pendingToolArgs.get(callIdForArgs) || '';
|
|
160
|
+
pendingToolArgs.set(callIdForArgs, prev + argsDelta);
|
|
161
|
+
events.push(emitContentBlockDelta({
|
|
162
|
+
type: 'input_json_delta',
|
|
163
|
+
partial_json: argsDelta
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return events;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
while (true) {
|
|
173
|
+
const { done, value } = await reader.read();
|
|
174
|
+
if (done) break;
|
|
175
|
+
|
|
176
|
+
buffer += decoder.decode(value, { stream: true });
|
|
177
|
+
const lines = buffer.split('\n');
|
|
178
|
+
buffer = lines.pop() || '';
|
|
179
|
+
|
|
180
|
+
for (const line of lines) {
|
|
181
|
+
if (!line.startsWith('data:')) continue;
|
|
182
|
+
const jsonText = line.slice(5).trim();
|
|
183
|
+
if (!jsonText) continue;
|
|
184
|
+
if (jsonText === '[DONE]') continue;
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const chunk = JSON.parse(jsonText);
|
|
188
|
+
|
|
189
|
+
if (chunk.usage) {
|
|
190
|
+
usage = {
|
|
191
|
+
input_tokens: chunk.usage.prompt_tokens || 0,
|
|
192
|
+
output_tokens: chunk.usage.completion_tokens || 0
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const choice = chunk.choices?.[0];
|
|
197
|
+
if (!choice) continue;
|
|
198
|
+
|
|
199
|
+
const events = handleDelta(choice.delta || {});
|
|
200
|
+
for (const evt of events) {
|
|
201
|
+
yield evt;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (choice.finish_reason) {
|
|
205
|
+
stopReason = choice.finish_reason === 'tool_calls' ? 'tool_use' : 'end_turn';
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
// ignore malformed chunks
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!hasEmittedStart) {
|
|
214
|
+
hasEmittedStart = true;
|
|
215
|
+
yield emitMessageStart();
|
|
216
|
+
yield emitContentBlockStart({ type: 'text', text: '' });
|
|
217
|
+
yield emitContentBlockDelta({ type: 'text_delta', text: '' });
|
|
218
|
+
yield emitContentBlockStop();
|
|
219
|
+
} else if (currentBlockType) {
|
|
220
|
+
if (currentBlockType === 'thinking') {
|
|
221
|
+
yield emitContentBlockDelta({ type: 'signature_delta', signature: 'kilo-reasoning' });
|
|
222
|
+
}
|
|
223
|
+
yield emitContentBlockStop();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
yield {
|
|
227
|
+
event: 'message_delta',
|
|
228
|
+
data: {
|
|
229
|
+
type: 'message_delta',
|
|
230
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
231
|
+
usage
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
yield {
|
|
236
|
+
event: 'message_stop',
|
|
237
|
+
data: { type: 'message_stop' }
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default {
|
|
242
|
+
streamOpenAIChat
|
|
243
|
+
};
|