@wangzhizhi/remi 0.0.1-alpha
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/README.md +9 -0
- package/dist/doctor.js +108 -0
- package/dist/git.js +41 -0
- package/dist/help.js +27 -0
- package/dist/i18n.js +422 -0
- package/dist/index.js +97 -0
- package/dist/initPrompt.js +17 -0
- package/dist/model.js +116 -0
- package/dist/modelSelection.js +34 -0
- package/dist/permissionDisplay.js +46 -0
- package/dist/permissions.js +206 -0
- package/dist/repl.js +346 -0
- package/dist/resume.js +3 -0
- package/dist/setup.js +62 -0
- package/dist/statusline.js +59 -0
- package/dist/style.js +48 -0
- package/dist/syntaxTheme.js +39 -0
- package/dist/tui/RemiApp.js +1756 -0
- package/dist/tui/commands.js +427 -0
- package/dist/tui/index.js +42 -0
- package/dist/tui/renderers/Header.js +28 -0
- package/dist/tui/renderers/MessageList.js +1176 -0
- package/dist/tui/renderers/PromptBox.js +118 -0
- package/dist/tui/renderers/StatusLine.js +124 -0
- package/dist/tui/renderers/WorkingIndicator.js +70 -0
- package/dist/tui/slashCommandHighlight.js +8 -0
- package/dist/tui/theme.js +13 -0
- package/dist/tui/types.js +1 -0
- package/dist/usage.js +66 -0
- package/dist/version.js +5 -0
- package/node_modules/@remi/compact/dist/index.js +389 -0
- package/node_modules/@remi/compact/package.json +8 -0
- package/node_modules/@remi/config/dist/index.js +426 -0
- package/node_modules/@remi/config/package.json +8 -0
- package/node_modules/@remi/core/dist/contextBuilder.js +344 -0
- package/node_modules/@remi/core/dist/directoryOverview.js +359 -0
- package/node_modules/@remi/core/dist/index.js +2843 -0
- package/node_modules/@remi/core/dist/projectInstructions.js +123 -0
- package/node_modules/@remi/core/dist/responseStyles.js +98 -0
- package/node_modules/@remi/core/package.json +8 -0
- package/node_modules/@remi/llm/dist/index.js +804 -0
- package/node_modules/@remi/llm/package.json +8 -0
- package/node_modules/@remi/memory/dist/index.js +312 -0
- package/node_modules/@remi/memory/package.json +8 -0
- package/node_modules/@remi/permissions/dist/index.js +90 -0
- package/node_modules/@remi/permissions/package.json +8 -0
- package/node_modules/@remi/sessions/dist/index.js +370 -0
- package/node_modules/@remi/sessions/package.json +8 -0
- package/node_modules/@remi/skills/dist/index.js +273 -0
- package/node_modules/@remi/skills/package.json +8 -0
- package/node_modules/@remi/terminal-markdown/dist/index.js +1412 -0
- package/node_modules/@remi/terminal-markdown/package.json +8 -0
- package/node_modules/@remi/tools/dist/index.js +3875 -0
- package/node_modules/@remi/tools/package.json +8 -0
- package/package.json +48 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
import { modelRoles, providerApiKeyStatus, resolveProviderApiKey } from '@remi/config';
|
|
2
|
+
export const llmPackageName = '@remi/llm';
|
|
3
|
+
export class ProviderRegistry {
|
|
4
|
+
providers;
|
|
5
|
+
env;
|
|
6
|
+
constructor(providers, env = process.env) {
|
|
7
|
+
this.providers = providers;
|
|
8
|
+
this.env = env;
|
|
9
|
+
}
|
|
10
|
+
list() {
|
|
11
|
+
return Object.entries(this.providers).map(([alias, provider]) => ({
|
|
12
|
+
alias,
|
|
13
|
+
type: provider.type,
|
|
14
|
+
baseURL: provider.baseURL,
|
|
15
|
+
apiKeyStatus: providerApiKeyStatus(provider, this.env),
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
get(alias) {
|
|
19
|
+
return this.providers[alias];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class ModelRegistry {
|
|
23
|
+
models;
|
|
24
|
+
profiles;
|
|
25
|
+
constructor(models, profiles) {
|
|
26
|
+
this.models = models;
|
|
27
|
+
this.profiles = profiles;
|
|
28
|
+
}
|
|
29
|
+
list() {
|
|
30
|
+
return Object.entries(this.models).map(([alias, model]) => ({
|
|
31
|
+
alias,
|
|
32
|
+
displayName: model.displayName ?? alias,
|
|
33
|
+
provider: model.provider,
|
|
34
|
+
model: model.model,
|
|
35
|
+
contextWindow: model.contextWindow,
|
|
36
|
+
supportsTools: model.supportsTools,
|
|
37
|
+
supportsReasoning: model.supportsReasoning,
|
|
38
|
+
...(model.tokenEstimator ? { tokenEstimator: model.tokenEstimator } : {}),
|
|
39
|
+
...(model.tokenEstimateProfile ? { tokenEstimateProfile: model.tokenEstimateProfile } : {}),
|
|
40
|
+
usedBy: this.findUsage(alias),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
get(alias) {
|
|
44
|
+
return this.models[alias];
|
|
45
|
+
}
|
|
46
|
+
findUsage(modelAlias) {
|
|
47
|
+
const usage = [];
|
|
48
|
+
for (const [profileName, profile] of Object.entries(this.profiles)) {
|
|
49
|
+
for (const role of modelRoles) {
|
|
50
|
+
if (profile.roles[role] === modelAlias) {
|
|
51
|
+
usage.push(`${profileName}:${role}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return usage;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export class ModelRouter {
|
|
59
|
+
config;
|
|
60
|
+
env;
|
|
61
|
+
activeProfile;
|
|
62
|
+
sessionRoleOverrides = new Map();
|
|
63
|
+
nextTurnRoleOverrides = new Map();
|
|
64
|
+
providers;
|
|
65
|
+
models;
|
|
66
|
+
constructor(config, env = process.env) {
|
|
67
|
+
this.config = config;
|
|
68
|
+
this.env = env;
|
|
69
|
+
this.activeProfile = config.activeProfile ?? 'daily';
|
|
70
|
+
this.providers = new ProviderRegistry(config.providers ?? {}, env);
|
|
71
|
+
this.models = new ModelRegistry(config.models ?? {}, config.profiles ?? {});
|
|
72
|
+
}
|
|
73
|
+
getActiveProfile() {
|
|
74
|
+
return this.activeProfile;
|
|
75
|
+
}
|
|
76
|
+
useProfile(profileName) {
|
|
77
|
+
if (!this.config.profiles?.[profileName]) {
|
|
78
|
+
throw new Error(`Unknown model profile: ${profileName}`);
|
|
79
|
+
}
|
|
80
|
+
this.activeProfile = profileName;
|
|
81
|
+
}
|
|
82
|
+
switchRoleModel(role, modelAlias) {
|
|
83
|
+
this.assertRole(role);
|
|
84
|
+
this.assertModel(modelAlias);
|
|
85
|
+
this.sessionRoleOverrides.set(role, modelAlias);
|
|
86
|
+
}
|
|
87
|
+
useModelForNextTurn(role, modelAlias) {
|
|
88
|
+
this.assertRole(role);
|
|
89
|
+
this.assertModel(modelAlias);
|
|
90
|
+
this.nextTurnRoleOverrides.set(role, modelAlias);
|
|
91
|
+
}
|
|
92
|
+
resolve(role, context = {}) {
|
|
93
|
+
this.assertRole(role);
|
|
94
|
+
const profileName = context.profile ?? this.activeProfile;
|
|
95
|
+
const profile = this.resolveProfile(profileName);
|
|
96
|
+
const alias = this.nextTurnRoleOverrides.get(role) ?? this.sessionRoleOverrides.get(role) ?? profile.roles[role];
|
|
97
|
+
if (!alias) {
|
|
98
|
+
throw new Error(`No model configured for role ${role} in profile ${profileName}`);
|
|
99
|
+
}
|
|
100
|
+
const model = this.config.models?.[alias];
|
|
101
|
+
if (!model) {
|
|
102
|
+
throw new Error(`Unknown model alias: ${alias}`);
|
|
103
|
+
}
|
|
104
|
+
const provider = this.config.providers?.[model.provider];
|
|
105
|
+
if (!provider) {
|
|
106
|
+
throw new Error(`Unknown provider alias: ${model.provider}`);
|
|
107
|
+
}
|
|
108
|
+
this.nextTurnRoleOverrides.delete(role);
|
|
109
|
+
const resolved = {
|
|
110
|
+
role,
|
|
111
|
+
profile: profileName,
|
|
112
|
+
alias,
|
|
113
|
+
displayName: model.displayName ?? alias,
|
|
114
|
+
providerAlias: model.provider,
|
|
115
|
+
providerType: provider.type,
|
|
116
|
+
baseURL: provider.baseURL,
|
|
117
|
+
model: model.model,
|
|
118
|
+
contextWindow: model.contextWindow,
|
|
119
|
+
supportsTools: model.supportsTools,
|
|
120
|
+
supportsReasoning: model.supportsReasoning,
|
|
121
|
+
apiKeyStatus: providerApiKeyStatus(provider, this.env),
|
|
122
|
+
};
|
|
123
|
+
if (profile.effort !== undefined)
|
|
124
|
+
resolved.effort = profile.effort;
|
|
125
|
+
if (model.maxOutputTokens !== undefined)
|
|
126
|
+
resolved.maxOutputTokens = model.maxOutputTokens;
|
|
127
|
+
if (model.tokenEstimator !== undefined)
|
|
128
|
+
resolved.tokenEstimator = model.tokenEstimator;
|
|
129
|
+
if (model.tokenEstimateProfile !== undefined)
|
|
130
|
+
resolved.tokenEstimateProfile = model.tokenEstimateProfile;
|
|
131
|
+
if (model.usageShape !== undefined)
|
|
132
|
+
resolved.usageShape = model.usageShape;
|
|
133
|
+
if (model.cacheUsageShape !== undefined)
|
|
134
|
+
resolved.cacheUsageShape = model.cacheUsageShape;
|
|
135
|
+
return resolved;
|
|
136
|
+
}
|
|
137
|
+
resolveProfile(profileName, seen = new Set()) {
|
|
138
|
+
const profile = this.config.profiles?.[profileName];
|
|
139
|
+
if (!profile) {
|
|
140
|
+
throw new Error(`Unknown model profile: ${profileName}`);
|
|
141
|
+
}
|
|
142
|
+
if (!profile.extends) {
|
|
143
|
+
return profile;
|
|
144
|
+
}
|
|
145
|
+
if (seen.has(profileName)) {
|
|
146
|
+
throw new Error(`Circular model profile inheritance: ${profileName}`);
|
|
147
|
+
}
|
|
148
|
+
seen.add(profileName);
|
|
149
|
+
const parent = this.resolveProfile(profile.extends, seen);
|
|
150
|
+
const resolved = {
|
|
151
|
+
roles: {
|
|
152
|
+
...parent.roles,
|
|
153
|
+
...profile.roles,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const effort = profile.effort ?? parent.effort;
|
|
157
|
+
if (effort !== undefined)
|
|
158
|
+
resolved.effort = effort;
|
|
159
|
+
return resolved;
|
|
160
|
+
}
|
|
161
|
+
assertRole(role) {
|
|
162
|
+
if (!modelRoles.includes(role)) {
|
|
163
|
+
throw new Error(`Unknown model role: ${role}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
assertModel(modelAlias) {
|
|
167
|
+
if (!this.config.models?.[modelAlias]) {
|
|
168
|
+
throw new Error(`Unknown model alias: ${modelAlias}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export function createModelRouter(config, env = process.env) {
|
|
173
|
+
return new ModelRouter(config, env);
|
|
174
|
+
}
|
|
175
|
+
export class TokenEstimatorRegistry {
|
|
176
|
+
estimators = new Map();
|
|
177
|
+
constructor(estimators = []) {
|
|
178
|
+
this.register(createHeuristicTokenEstimator());
|
|
179
|
+
for (const estimator of estimators) {
|
|
180
|
+
this.register(estimator);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
register(estimator) {
|
|
184
|
+
this.estimators.set(estimator.kind, estimator);
|
|
185
|
+
}
|
|
186
|
+
resolve(model) {
|
|
187
|
+
const requestedKind = model.tokenEstimator ?? 'heuristic';
|
|
188
|
+
const profile = model.tokenEstimateProfile ?? 'default';
|
|
189
|
+
const estimator = this.estimators.get(requestedKind);
|
|
190
|
+
if (estimator) {
|
|
191
|
+
return {
|
|
192
|
+
estimator,
|
|
193
|
+
requestedKind,
|
|
194
|
+
actualKind: estimator.kind,
|
|
195
|
+
profile,
|
|
196
|
+
fallback: false,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const fallback = this.estimators.get('heuristic') ?? createHeuristicTokenEstimator();
|
|
200
|
+
return {
|
|
201
|
+
estimator: fallback,
|
|
202
|
+
requestedKind,
|
|
203
|
+
actualKind: fallback.kind,
|
|
204
|
+
profile,
|
|
205
|
+
fallback: true,
|
|
206
|
+
reason: tokenEstimatorFallbackReason(requestedKind),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export function createTokenEstimatorRegistry(estimators) {
|
|
211
|
+
return new TokenEstimatorRegistry(estimators);
|
|
212
|
+
}
|
|
213
|
+
export function createHeuristicTokenEstimator() {
|
|
214
|
+
return {
|
|
215
|
+
kind: 'heuristic',
|
|
216
|
+
estimateText(input) {
|
|
217
|
+
const profile = input.profile ?? 'default';
|
|
218
|
+
return {
|
|
219
|
+
tokens: estimateTextTokensHeuristic(input.text, profile),
|
|
220
|
+
estimator: 'heuristic',
|
|
221
|
+
profile,
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
export function estimateTextTokensHeuristic(text, profile = 'default') {
|
|
227
|
+
if (text.length === 0) {
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
const weights = heuristicWeights(profile);
|
|
231
|
+
let asciiLike = 0;
|
|
232
|
+
let wideLike = 0;
|
|
233
|
+
let whitespace = 0;
|
|
234
|
+
for (const char of text) {
|
|
235
|
+
const code = char.codePointAt(0) ?? 0;
|
|
236
|
+
if (isCjkLikeCodePoint(code)) {
|
|
237
|
+
wideLike += 1;
|
|
238
|
+
}
|
|
239
|
+
else if (/\s/u.test(char)) {
|
|
240
|
+
whitespace += 1;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
asciiLike += 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return Math.max(1, Math.ceil(asciiLike / weights.asciiCharsPerToken + wideLike * weights.wideTokenWeight + whitespace / weights.whitespaceCharsPerToken));
|
|
247
|
+
}
|
|
248
|
+
export function estimateMessagesTokensWithEstimator(messages, resolution) {
|
|
249
|
+
return messages.reduce((sum, message) => {
|
|
250
|
+
const text = `${message.role}\n${message.content}`;
|
|
251
|
+
return sum + resolution.estimator.estimateText({ text, profile: resolution.profile }).tokens;
|
|
252
|
+
}, 0);
|
|
253
|
+
}
|
|
254
|
+
function tokenEstimatorFallbackReason(kind) {
|
|
255
|
+
if (kind === 'deepseek-local-tokenizer') {
|
|
256
|
+
return 'DeepSeek local tokenizer adapter is not registered in this runtime';
|
|
257
|
+
}
|
|
258
|
+
if (kind === 'ark-tokenization-api') {
|
|
259
|
+
return 'Ark Tokenization API adapter is not registered in this runtime';
|
|
260
|
+
}
|
|
261
|
+
if (kind === 'anthropic-count-tokens') {
|
|
262
|
+
return 'Anthropic countTokens adapter is not registered in this runtime';
|
|
263
|
+
}
|
|
264
|
+
if (kind === 'tiktoken') {
|
|
265
|
+
return 'tiktoken adapter is not registered in this runtime';
|
|
266
|
+
}
|
|
267
|
+
return 'Requested token estimator is unavailable';
|
|
268
|
+
}
|
|
269
|
+
function heuristicWeights(profile) {
|
|
270
|
+
if (profile === 'code-heavy') {
|
|
271
|
+
return { asciiCharsPerToken: 3.2, wideTokenWeight: 0.9, whitespaceCharsPerToken: 8 };
|
|
272
|
+
}
|
|
273
|
+
if (profile === 'json-heavy') {
|
|
274
|
+
return { asciiCharsPerToken: 2.8, wideTokenWeight: 0.9, whitespaceCharsPerToken: 10 };
|
|
275
|
+
}
|
|
276
|
+
if (profile === 'cjk-heavy') {
|
|
277
|
+
return { asciiCharsPerToken: 3.8, wideTokenWeight: 1, whitespaceCharsPerToken: 8 };
|
|
278
|
+
}
|
|
279
|
+
return { asciiCharsPerToken: 4, wideTokenWeight: 0.8, whitespaceCharsPerToken: 12 };
|
|
280
|
+
}
|
|
281
|
+
function isCjkLikeCodePoint(code) {
|
|
282
|
+
return ((code >= 0x3400 && code <= 0x9fff) ||
|
|
283
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
284
|
+
(code >= 0x3040 && code <= 0x30ff) ||
|
|
285
|
+
(code >= 0xac00 && code <= 0xd7af));
|
|
286
|
+
}
|
|
287
|
+
export class OpenAICompatibleProvider {
|
|
288
|
+
options;
|
|
289
|
+
env;
|
|
290
|
+
fetchImpl;
|
|
291
|
+
constructor(options) {
|
|
292
|
+
this.options = options;
|
|
293
|
+
this.env = options.env ?? process.env;
|
|
294
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
295
|
+
}
|
|
296
|
+
async *streamChat(messages, model, options = {}) {
|
|
297
|
+
const apiKey = resolveProviderApiKey(this.options.provider, this.env);
|
|
298
|
+
if (!apiKey) {
|
|
299
|
+
throw new Error(`Missing API key for provider ${model.providerAlias}`);
|
|
300
|
+
}
|
|
301
|
+
const controller = createTimeoutController(options.signal, options.timeoutMs);
|
|
302
|
+
const response = await this.fetchImpl(chatCompletionsUrl(this.options.provider.baseURL), {
|
|
303
|
+
method: 'POST',
|
|
304
|
+
headers: {
|
|
305
|
+
'Content-Type': 'application/json',
|
|
306
|
+
Authorization: `Bearer ${apiKey}`,
|
|
307
|
+
...(this.options.provider.headers ?? {}),
|
|
308
|
+
},
|
|
309
|
+
body: JSON.stringify({
|
|
310
|
+
model: model.model,
|
|
311
|
+
messages: messages.map(toOpenAIMessage),
|
|
312
|
+
stream: true,
|
|
313
|
+
stream_options: { include_usage: true },
|
|
314
|
+
...(options.tools && options.tools.length > 0 && model.supportsTools
|
|
315
|
+
? {
|
|
316
|
+
tools: options.tools.map(tool => ({
|
|
317
|
+
type: 'function',
|
|
318
|
+
function: {
|
|
319
|
+
name: tool.name,
|
|
320
|
+
description: tool.description,
|
|
321
|
+
parameters: tool.parameters,
|
|
322
|
+
},
|
|
323
|
+
})),
|
|
324
|
+
tool_choice: 'auto',
|
|
325
|
+
}
|
|
326
|
+
: {}),
|
|
327
|
+
}),
|
|
328
|
+
signal: controller.signal,
|
|
329
|
+
});
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
throw new Error(await formatHttpError(response, model.displayName));
|
|
332
|
+
}
|
|
333
|
+
if (!response.body) {
|
|
334
|
+
throw new Error('LLM request did not return a response body');
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
yield* parseOpenAIStream(response.body);
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
controller.dispose();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
export class AnthropicCompatibleProvider {
|
|
345
|
+
options;
|
|
346
|
+
env;
|
|
347
|
+
fetchImpl;
|
|
348
|
+
constructor(options) {
|
|
349
|
+
this.options = options;
|
|
350
|
+
this.env = options.env ?? process.env;
|
|
351
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
352
|
+
}
|
|
353
|
+
async *streamChat(messages, model, options = {}) {
|
|
354
|
+
const apiKey = resolveProviderApiKey(this.options.provider, this.env);
|
|
355
|
+
if (!apiKey) {
|
|
356
|
+
throw new Error(`Missing API key for provider ${model.providerAlias}`);
|
|
357
|
+
}
|
|
358
|
+
const controller = createTimeoutController(options.signal, options.timeoutMs);
|
|
359
|
+
const response = await this.fetchImpl(anthropicMessagesUrl(this.options.provider.baseURL), {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
headers: {
|
|
362
|
+
'Content-Type': 'application/json',
|
|
363
|
+
'anthropic-version': '2023-06-01',
|
|
364
|
+
'x-api-key': apiKey,
|
|
365
|
+
...(this.options.provider.headers ?? {}),
|
|
366
|
+
},
|
|
367
|
+
body: JSON.stringify(createAnthropicMessagesBody(messages, model)),
|
|
368
|
+
signal: controller.signal,
|
|
369
|
+
});
|
|
370
|
+
if (!response.ok) {
|
|
371
|
+
throw new Error(await formatHttpError(response, model.displayName));
|
|
372
|
+
}
|
|
373
|
+
if (!response.body) {
|
|
374
|
+
throw new Error('LLM request did not return a response body');
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
yield* parseAnthropicStream(response.body);
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
controller.dispose();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
export function createProviderClient(provider, env = process.env, fetchImpl = fetch) {
|
|
385
|
+
if (provider.type === 'openai-compatible') {
|
|
386
|
+
return new OpenAICompatibleProvider({ provider, env, fetch: fetchImpl });
|
|
387
|
+
}
|
|
388
|
+
if (provider.type === 'anthropic-compatible') {
|
|
389
|
+
return new AnthropicCompatibleProvider({ provider, env, fetch: fetchImpl });
|
|
390
|
+
}
|
|
391
|
+
throw new Error(`Provider type is not implemented yet: ${provider.type}`);
|
|
392
|
+
}
|
|
393
|
+
export async function* parseOpenAIStream(body) {
|
|
394
|
+
const reader = body.getReader();
|
|
395
|
+
const decoder = new TextDecoder();
|
|
396
|
+
let buffer = '';
|
|
397
|
+
const pendingToolCalls = new Map();
|
|
398
|
+
while (true) {
|
|
399
|
+
const { done, value } = await reader.read();
|
|
400
|
+
if (done) {
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
buffer += decoder.decode(value, { stream: true });
|
|
404
|
+
const lines = buffer.split(/\r?\n/);
|
|
405
|
+
buffer = lines.pop() ?? '';
|
|
406
|
+
for (const line of lines) {
|
|
407
|
+
const chunk = parseOpenAIStreamLine(line);
|
|
408
|
+
if (chunk !== undefined) {
|
|
409
|
+
if (isOpenAIToolCallDelta(chunk)) {
|
|
410
|
+
mergeToolCallDelta(pendingToolCalls, chunk);
|
|
411
|
+
}
|
|
412
|
+
else if (isToolCallsDone(chunk)) {
|
|
413
|
+
yield* flushPendingToolCalls(pendingToolCalls);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
yield chunk;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
buffer += decoder.decode();
|
|
422
|
+
const chunk = parseOpenAIStreamLine(buffer);
|
|
423
|
+
if (chunk !== undefined) {
|
|
424
|
+
if (isOpenAIToolCallDelta(chunk)) {
|
|
425
|
+
mergeToolCallDelta(pendingToolCalls, chunk);
|
|
426
|
+
}
|
|
427
|
+
else if (isToolCallsDone(chunk)) {
|
|
428
|
+
yield* flushPendingToolCalls(pendingToolCalls);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
yield chunk;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
yield* flushPendingToolCalls(pendingToolCalls);
|
|
435
|
+
}
|
|
436
|
+
export function parseOpenAIStreamLine(line) {
|
|
437
|
+
const trimmed = line.trim();
|
|
438
|
+
if (!trimmed.startsWith('data:')) {
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
const payload = trimmed.slice('data:'.length).trim();
|
|
442
|
+
if (!payload || payload === '[DONE]') {
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
const parsed = JSON.parse(payload);
|
|
446
|
+
const usage = parseOpenAIUsage(parsed.usage);
|
|
447
|
+
if (usage) {
|
|
448
|
+
return { type: 'usage', usage };
|
|
449
|
+
}
|
|
450
|
+
const choice = parsed.choices?.[0];
|
|
451
|
+
if (choice?.finish_reason === 'tool_calls') {
|
|
452
|
+
return { type: 'tool_calls_done' };
|
|
453
|
+
}
|
|
454
|
+
const toolCall = choice?.delta?.tool_calls?.[0];
|
|
455
|
+
if (toolCall) {
|
|
456
|
+
const index = typeof toolCall.index === 'number' ? toolCall.index : 0;
|
|
457
|
+
const id = typeof toolCall.id === 'string' ? toolCall.id : undefined;
|
|
458
|
+
const name = typeof toolCall.function?.name === 'string' ? toolCall.function.name : undefined;
|
|
459
|
+
const argumentsDelta = typeof toolCall.function?.arguments === 'string' ? toolCall.function.arguments : undefined;
|
|
460
|
+
return {
|
|
461
|
+
type: 'tool_call_delta',
|
|
462
|
+
index,
|
|
463
|
+
...(id ? { id } : {}),
|
|
464
|
+
...(name ? { name } : {}),
|
|
465
|
+
...(argumentsDelta ? { argumentsDelta } : {}),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const reasoningContent = choice?.delta?.reasoning_content;
|
|
469
|
+
if (typeof reasoningContent === 'string' && reasoningContent.length > 0) {
|
|
470
|
+
return { type: 'reasoning_delta', text: reasoningContent };
|
|
471
|
+
}
|
|
472
|
+
const content = choice?.delta?.content;
|
|
473
|
+
if (typeof content !== 'string' || content.length === 0) {
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
return content;
|
|
477
|
+
}
|
|
478
|
+
export async function* parseAnthropicStream(body) {
|
|
479
|
+
const reader = body.getReader();
|
|
480
|
+
const decoder = new TextDecoder();
|
|
481
|
+
let buffer = '';
|
|
482
|
+
let usage;
|
|
483
|
+
while (true) {
|
|
484
|
+
const { done, value } = await reader.read();
|
|
485
|
+
if (done) {
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
buffer += decoder.decode(value, { stream: true });
|
|
489
|
+
const lines = buffer.split(/\r?\n/);
|
|
490
|
+
buffer = lines.pop() ?? '';
|
|
491
|
+
for (const line of lines) {
|
|
492
|
+
const chunk = parseAnthropicStreamLine(line);
|
|
493
|
+
if (typeof chunk === 'string') {
|
|
494
|
+
yield chunk;
|
|
495
|
+
}
|
|
496
|
+
else if (isUsageChunk(chunk)) {
|
|
497
|
+
usage = mergeAnthropicUsage(usage, chunk.usage);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
buffer += decoder.decode();
|
|
502
|
+
const chunk = parseAnthropicStreamLine(buffer);
|
|
503
|
+
if (typeof chunk === 'string') {
|
|
504
|
+
yield chunk;
|
|
505
|
+
}
|
|
506
|
+
else if (isUsageChunk(chunk)) {
|
|
507
|
+
usage = mergeAnthropicUsage(usage, chunk.usage);
|
|
508
|
+
}
|
|
509
|
+
if (usage) {
|
|
510
|
+
yield { type: 'usage', usage };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
export function parseAnthropicStreamLine(line) {
|
|
514
|
+
const trimmed = line.trim();
|
|
515
|
+
if (!trimmed.startsWith('data:')) {
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
const payload = trimmed.slice('data:'.length).trim();
|
|
519
|
+
if (!payload || payload === '[DONE]') {
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
522
|
+
const parsed = JSON.parse(payload);
|
|
523
|
+
const delta = objectField(parsed, 'delta');
|
|
524
|
+
const text = delta?.text;
|
|
525
|
+
if (typeof text === 'string' && text.length > 0) {
|
|
526
|
+
return text;
|
|
527
|
+
}
|
|
528
|
+
const message = objectField(parsed, 'message');
|
|
529
|
+
const messageUsage = message ? objectField(message, 'usage') : undefined;
|
|
530
|
+
const usage = parseAnthropicUsage(objectField(parsed, 'usage') ?? messageUsage);
|
|
531
|
+
if (usage) {
|
|
532
|
+
return { type: 'usage', usage };
|
|
533
|
+
}
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
function parseOpenAIUsage(rawUsage) {
|
|
537
|
+
if (!rawUsage || typeof rawUsage !== 'object') {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
const usage = rawUsage;
|
|
541
|
+
const promptDetails = objectField(usage, 'prompt_tokens_details') ?? objectField(usage, 'input_tokens_details');
|
|
542
|
+
const completionDetails = objectField(usage, 'completion_tokens_details') ?? objectField(usage, 'output_tokens_details');
|
|
543
|
+
const promptCacheHitTokens = numberField(usage, 'prompt_cache_hit_tokens');
|
|
544
|
+
const promptCacheMissTokens = numberField(usage, 'prompt_cache_miss_tokens');
|
|
545
|
+
const fallbackInputTokens = promptCacheHitTokens !== undefined || promptCacheMissTokens !== undefined
|
|
546
|
+
? (promptCacheMissTokens ?? 0)
|
|
547
|
+
: 0;
|
|
548
|
+
const inputTokens = numberField(usage, 'prompt_tokens') ?? numberField(usage, 'input_tokens') ?? fallbackInputTokens;
|
|
549
|
+
const outputTokens = numberField(usage, 'completion_tokens') ?? numberField(usage, 'output_tokens') ?? 0;
|
|
550
|
+
const cachedInputTokens = firstNumberField([
|
|
551
|
+
numberField(promptDetails, 'cached_tokens'),
|
|
552
|
+
numberField(promptDetails, 'cached_input_tokens'),
|
|
553
|
+
numberField(promptDetails, 'cache_read_input_tokens'),
|
|
554
|
+
numberField(promptDetails, 'cache_read_tokens'),
|
|
555
|
+
numberField(usage, 'cached_tokens'),
|
|
556
|
+
numberField(usage, 'cached_input_tokens'),
|
|
557
|
+
numberField(usage, 'cache_read_input_tokens'),
|
|
558
|
+
numberField(usage, 'cache_read_tokens'),
|
|
559
|
+
promptCacheHitTokens,
|
|
560
|
+
]);
|
|
561
|
+
const totalTokens = normalizeTokenTotal(inputTokens, outputTokens, cachedInputTokens, numberField(usage, 'total_tokens'));
|
|
562
|
+
const reasoningOutputTokens = numberField(completionDetails, 'reasoning_tokens') ??
|
|
563
|
+
numberField(completionDetails, 'reasoning_output_tokens') ??
|
|
564
|
+
numberField(usage, 'reasoning_tokens') ??
|
|
565
|
+
numberField(usage, 'reasoning_output_tokens');
|
|
566
|
+
if (inputTokens === 0 && outputTokens === 0 && totalTokens === 0 && cachedInputTokens === undefined && reasoningOutputTokens === undefined) {
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
inputTokens,
|
|
571
|
+
outputTokens,
|
|
572
|
+
totalTokens,
|
|
573
|
+
...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
|
|
574
|
+
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
function parseAnthropicUsage(rawUsage) {
|
|
578
|
+
if (!rawUsage || typeof rawUsage !== 'object') {
|
|
579
|
+
return undefined;
|
|
580
|
+
}
|
|
581
|
+
const usage = rawUsage;
|
|
582
|
+
const inputTokens = (numberField(usage, 'input_tokens') ?? 0) + (numberField(usage, 'cache_creation_input_tokens') ?? 0);
|
|
583
|
+
const outputTokens = numberField(usage, 'output_tokens') ?? 0;
|
|
584
|
+
const cachedInputTokens = numberField(usage, 'cache_read_input_tokens') ?? numberField(usage, 'cached_input_tokens');
|
|
585
|
+
const reasoningOutputTokens = numberField(usage, 'reasoning_output_tokens') ?? numberField(usage, 'reasoning_tokens');
|
|
586
|
+
const totalTokens = normalizeTokenTotal(inputTokens, outputTokens, cachedInputTokens);
|
|
587
|
+
if (inputTokens === 0 && outputTokens === 0 && cachedInputTokens === undefined && reasoningOutputTokens === undefined) {
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
inputTokens,
|
|
592
|
+
outputTokens,
|
|
593
|
+
totalTokens,
|
|
594
|
+
...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
|
|
595
|
+
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function mergeAnthropicUsage(current, next) {
|
|
599
|
+
const inputTokens = Math.max(current?.inputTokens ?? 0, next.inputTokens);
|
|
600
|
+
const outputTokens = Math.max(current?.outputTokens ?? 0, next.outputTokens);
|
|
601
|
+
const cachedInputTokens = maxOptionalNumber(current?.cachedInputTokens, next.cachedInputTokens);
|
|
602
|
+
const reasoningOutputTokens = maxOptionalNumber(current?.reasoningOutputTokens, next.reasoningOutputTokens);
|
|
603
|
+
return {
|
|
604
|
+
inputTokens,
|
|
605
|
+
outputTokens,
|
|
606
|
+
totalTokens: normalizeTokenTotal(inputTokens, outputTokens, cachedInputTokens),
|
|
607
|
+
...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
|
|
608
|
+
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function normalizeTokenTotal(inputTokens, outputTokens, cachedInputTokens, reportedTotalTokens) {
|
|
612
|
+
return Math.max(reportedTotalTokens ?? 0, inputTokens + outputTokens + (cachedInputTokens ?? 0));
|
|
613
|
+
}
|
|
614
|
+
function maxOptionalNumber(left, right) {
|
|
615
|
+
if (left === undefined && right === undefined) {
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
return Math.max(left ?? 0, right ?? 0);
|
|
619
|
+
}
|
|
620
|
+
function createAnthropicMessagesBody(messages, model) {
|
|
621
|
+
const system = messages
|
|
622
|
+
.filter(message => message.role === 'system')
|
|
623
|
+
.map(message => message.content)
|
|
624
|
+
.filter(content => content.length > 0)
|
|
625
|
+
.join('\n\n');
|
|
626
|
+
const body = {
|
|
627
|
+
model: model.model,
|
|
628
|
+
max_tokens: model.maxOutputTokens ?? 4096,
|
|
629
|
+
messages: messages
|
|
630
|
+
.filter(message => message.role !== 'system')
|
|
631
|
+
.map(message => ({
|
|
632
|
+
role: message.role,
|
|
633
|
+
content: message.content,
|
|
634
|
+
})),
|
|
635
|
+
stream: true,
|
|
636
|
+
};
|
|
637
|
+
if (system.length > 0) {
|
|
638
|
+
body.system = system;
|
|
639
|
+
}
|
|
640
|
+
return body;
|
|
641
|
+
}
|
|
642
|
+
function toOpenAIMessage(message) {
|
|
643
|
+
if (message.role === 'tool') {
|
|
644
|
+
return {
|
|
645
|
+
role: 'tool',
|
|
646
|
+
tool_call_id: message.toolCallId,
|
|
647
|
+
content: message.content,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
|
|
651
|
+
return {
|
|
652
|
+
role: 'assistant',
|
|
653
|
+
content: message.content.length > 0 ? message.content : null,
|
|
654
|
+
...(message.reasoningContent && message.reasoningContent.length > 0
|
|
655
|
+
? { reasoning_content: message.reasoningContent }
|
|
656
|
+
: {}),
|
|
657
|
+
tool_calls: message.toolCalls.map(toolCall => ({
|
|
658
|
+
id: toolCall.id,
|
|
659
|
+
type: 'function',
|
|
660
|
+
function: {
|
|
661
|
+
name: toolCall.name,
|
|
662
|
+
arguments: toolCall.arguments,
|
|
663
|
+
},
|
|
664
|
+
})),
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
role: message.role,
|
|
669
|
+
content: message.content,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function isOpenAIToolCallDelta(chunk) {
|
|
673
|
+
return typeof chunk !== 'string' && chunk.type === 'tool_call_delta';
|
|
674
|
+
}
|
|
675
|
+
function isToolCallsDone(chunk) {
|
|
676
|
+
return typeof chunk !== 'string' && chunk.type === 'tool_calls_done';
|
|
677
|
+
}
|
|
678
|
+
function isUsageChunk(chunk) {
|
|
679
|
+
return typeof chunk === 'object' && chunk !== null && chunk.type === 'usage';
|
|
680
|
+
}
|
|
681
|
+
function mergeToolCallDelta(pendingToolCalls, delta) {
|
|
682
|
+
const current = pendingToolCalls.get(delta.index) ?? {};
|
|
683
|
+
pendingToolCalls.set(delta.index, {
|
|
684
|
+
...current,
|
|
685
|
+
...(delta.id ? { id: delta.id } : {}),
|
|
686
|
+
...(delta.name ? { name: delta.name } : {}),
|
|
687
|
+
arguments: `${current.arguments ?? ''}${delta.argumentsDelta ?? ''}`,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
function* flushPendingToolCalls(pendingToolCalls) {
|
|
691
|
+
const calls = [...pendingToolCalls.entries()].sort(([left], [right]) => left - right);
|
|
692
|
+
pendingToolCalls.clear();
|
|
693
|
+
for (const [index, call] of calls) {
|
|
694
|
+
if (!call.id || !call.name) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
yield {
|
|
698
|
+
type: 'tool_call',
|
|
699
|
+
toolCall: {
|
|
700
|
+
id: call.id,
|
|
701
|
+
name: call.name,
|
|
702
|
+
arguments: call.arguments ?? '{}',
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function objectField(record, key) {
|
|
708
|
+
const value = record[key];
|
|
709
|
+
return value && typeof value === 'object' ? value : undefined;
|
|
710
|
+
}
|
|
711
|
+
function numberField(record, key) {
|
|
712
|
+
const value = record?.[key];
|
|
713
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
714
|
+
}
|
|
715
|
+
function firstNumberField(values) {
|
|
716
|
+
return values.find(value => value !== undefined);
|
|
717
|
+
}
|
|
718
|
+
export async function formatHttpError(response, modelName) {
|
|
719
|
+
const modelText = modelName ? ` for model ${modelName}` : '';
|
|
720
|
+
const statusText = response.statusText ? ` ${response.statusText}` : '';
|
|
721
|
+
const reason = response.status === 429 ? 'rate limit or quota exceeded' : 'request failed';
|
|
722
|
+
const detail = await safeErrorDetail(response);
|
|
723
|
+
return `LLM request failed${modelText} with HTTP ${response.status}${statusText} (${reason})${detail ? `: ${detail}` : ''}`;
|
|
724
|
+
}
|
|
725
|
+
async function safeErrorDetail(response) {
|
|
726
|
+
let raw = '';
|
|
727
|
+
try {
|
|
728
|
+
raw = await response.text();
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
return undefined;
|
|
732
|
+
}
|
|
733
|
+
if (!raw.trim()) {
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
const parsedDetail = parseErrorDetail(raw);
|
|
737
|
+
return truncateDetail(parsedDetail ?? raw.replace(/\s+/g, ' ').trim());
|
|
738
|
+
}
|
|
739
|
+
function parseErrorDetail(raw) {
|
|
740
|
+
try {
|
|
741
|
+
const parsed = JSON.parse(raw);
|
|
742
|
+
return extractErrorDetail(parsed);
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
return undefined;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function extractErrorDetail(value) {
|
|
749
|
+
if (typeof value === 'string') {
|
|
750
|
+
return value;
|
|
751
|
+
}
|
|
752
|
+
if (!value || typeof value !== 'object') {
|
|
753
|
+
return undefined;
|
|
754
|
+
}
|
|
755
|
+
const record = value;
|
|
756
|
+
const error = record['error'];
|
|
757
|
+
const errorDetail = error && typeof error === 'object' ? extractErrorDetail(error) : undefined;
|
|
758
|
+
const candidates = [
|
|
759
|
+
errorDetail,
|
|
760
|
+
stringField(record, 'message'),
|
|
761
|
+
stringField(record, 'msg'),
|
|
762
|
+
stringField(record, 'code'),
|
|
763
|
+
stringField(record, 'type'),
|
|
764
|
+
].filter((item) => item !== undefined && item.length > 0);
|
|
765
|
+
return candidates.length > 0 ? candidates.join(' - ') : undefined;
|
|
766
|
+
}
|
|
767
|
+
function stringField(record, key) {
|
|
768
|
+
const value = record[key];
|
|
769
|
+
return typeof value === 'string' ? value : undefined;
|
|
770
|
+
}
|
|
771
|
+
function truncateDetail(detail) {
|
|
772
|
+
return detail.length <= 500 ? detail : `${detail.slice(0, 497)}...`;
|
|
773
|
+
}
|
|
774
|
+
function chatCompletionsUrl(baseURL) {
|
|
775
|
+
return `${baseURL.replace(/\/$/, '')}/chat/completions`;
|
|
776
|
+
}
|
|
777
|
+
function anthropicMessagesUrl(baseURL) {
|
|
778
|
+
const normalized = baseURL.replace(/\/$/, '');
|
|
779
|
+
return normalized.endsWith('/v1') ? `${normalized}/messages` : `${normalized}/v1/messages`;
|
|
780
|
+
}
|
|
781
|
+
function createTimeoutController(parentSignal, timeoutMs) {
|
|
782
|
+
const controller = new AbortController();
|
|
783
|
+
const onAbort = () => controller.abort(parentSignal?.reason);
|
|
784
|
+
let timer;
|
|
785
|
+
if (parentSignal) {
|
|
786
|
+
if (parentSignal.aborted) {
|
|
787
|
+
controller.abort(parentSignal.reason);
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
parentSignal.addEventListener('abort', onAbort, { once: true });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
794
|
+
timer = setTimeout(() => controller.abort(new Error('LLM request timed out')), timeoutMs);
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
signal: controller.signal,
|
|
798
|
+
dispose: () => {
|
|
799
|
+
if (timer)
|
|
800
|
+
clearTimeout(timer);
|
|
801
|
+
parentSignal?.removeEventListener('abort', onAbort);
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
}
|