pikiloom 0.4.31 → 0.4.33

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.
Files changed (32) hide show
  1. package/dashboard/dist/assets/AgentTab-C9Z6BUng.js +1 -0
  2. package/dashboard/dist/assets/{ConnectionModal-CbWtylsE.js → ConnectionModal-DZEnnbRt.js} +1 -1
  3. package/dashboard/dist/assets/{DirBrowser-BsHjGC5A.js → DirBrowser-DNd06lse.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-BYoLkIki.js → ExtensionsTab-BXrPOYT9.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-B-SxDXft.js → IMAccessTab-gGQ_84mB.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-CzQbFS5c.js → Modal-DqsmC8u2.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-V9ykx8c4.js → Modals-CZuvmOU0.js} +1 -1
  8. package/dashboard/dist/assets/{Select-DE6cseW_.js → Select-Cvo1Mvn_.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-DkvJ4WNu.js +1 -0
  10. package/dashboard/dist/assets/{SystemTab-Cx6DyiFf.js → SystemTab-BnxqGrbn.js} +1 -1
  11. package/dashboard/dist/assets/index-CBNgeLIy.js +3 -0
  12. package/dashboard/dist/assets/index-CH_4h4ZM.css +1 -0
  13. package/dashboard/dist/assets/{index-Cjaly0tB.js → index-CifA8H7w.js} +3 -3
  14. package/dashboard/dist/assets/{shared-DQrcGYOc.js → shared-BoB6k6CT.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/drivers/codex.js +2 -0
  17. package/dist/agent/stream.js +12 -2
  18. package/dist/agent/utils.js +2 -0
  19. package/dist/bot/bot.js +4 -1
  20. package/dist/bot/render-shared.js +3 -2
  21. package/dist/channels/feishu/render.js +2 -0
  22. package/dist/channels/telegram/render.js +2 -0
  23. package/dist/dashboard/routes/agents.js +5 -0
  24. package/dist/dashboard/routes/local-models.js +117 -0
  25. package/dist/model/anthropic-bridge.js +419 -0
  26. package/dist/model/injector.js +33 -3
  27. package/dist/model/responses-bridge.js +29 -4
  28. package/package.json +1 -1
  29. package/dashboard/dist/assets/AgentTab-CYoIkNyL.js +0 -1
  30. package/dashboard/dist/assets/SessionPanel-CMu962uE.js +0 -1
  31. package/dashboard/dist/assets/index-CXIN3nTr.css +0 -1
  32. package/dashboard/dist/assets/index-Dp_KlJM9.js +0 -3
@@ -167,6 +167,7 @@ export async function run(cmd, opts, parseLine, parseStderrLine) {
167
167
  contextWindow: byokWindow,
168
168
  byokContextWindow: byokWindow,
169
169
  byokProviderName: byokProvider,
170
+ byokProfileName: (opts.byokProfileName || null),
170
171
  contextUsedTokens: null,
171
172
  codexCumulative: null,
172
173
  stopReason: null, activity: '',
@@ -473,8 +474,13 @@ export async function doStream(opts) {
473
474
  agentLog(`[byok] ${injection.detail}`);
474
475
  }
475
476
  const activeProfile = getActiveProfile(prepared.agent);
476
- if (activeProfile?.effort) {
477
- prepared.thinkingEffort = activeProfile.effort;
477
+ if (activeProfile) {
478
+ if (activeProfile.effort)
479
+ prepared.thinkingEffort = activeProfile.effort;
480
+ const profileLabel = activeProfile.name?.trim();
481
+ if (profileLabel && profileLabel !== activeProfile.modelId) {
482
+ prepared.byokProfileName = profileLabel;
483
+ }
478
484
  }
479
485
  }
480
486
  catch (e) {
@@ -500,6 +506,8 @@ export async function doStream(opts) {
500
506
  await awaitAgentUpdateIdle(prepared.agent, AGENT_UPDATE_TIMEOUTS.spawnWait);
501
507
  const result = await driver.doStream(prepared);
502
508
  const finalized = finalizeStreamResult(result, opts.workdir, opts.prompt, session, opts.claudeWorkflowEnabled);
509
+ finalized.byokProviderName = prepared.byokProviderName ?? null;
510
+ finalized.byokProfileName = prepared.byokProfileName ?? null;
503
511
  if (opts.forkOf && finalized.sessionId) {
504
512
  try {
505
513
  recordFork(opts.workdir, {
@@ -537,6 +545,8 @@ export async function doStream(opts) {
537
545
  incomplete: true,
538
546
  activity: null,
539
547
  plan: null,
548
+ byokProviderName: prepared.byokProviderName ?? null,
549
+ byokProfileName: prepared.byokProfileName ?? null,
540
550
  };
541
551
  const failureDisplayPrompt = collapseSkillPrompt(opts.prompt) ?? opts.prompt;
542
552
  session.record.lastQuestion = shortValue(failureDisplayPrompt, 500);
@@ -223,6 +223,8 @@ export function buildStreamPreviewMeta(s) {
223
223
  meta.turnOutputTokens = turnOutput;
224
224
  if (s.byokProviderName)
225
225
  meta.providerName = s.byokProviderName;
226
+ if (s.byokProfileName)
227
+ meta.profileName = s.byokProfileName;
226
228
  if (s.subAgents && s.subAgents.size > 0)
227
229
  meta.subAgents = Array.from(s.subAgents.values());
228
230
  if (s.generatingImages && s.generatingImages > 0)
package/dist/bot/bot.js CHANGED
@@ -4,7 +4,7 @@ import { execSync, spawn } from 'node:child_process';
4
4
  import { getActiveUserConfig, loadWorkspaces, onUserConfigChange, resolveUserWorkdir, setUserWorkdir, updateUserConfig } from '../core/config/user-config.js';
5
5
  import { doStream, ensureManagedSession, findManagedThreadSession, getSessionStoredConfig, getUsage, initializeProjectSkills, listAgents, resolveAgentModels, resolveDefaultAgent, listSkills, stageSessionFiles, reconcileOrphanedRunningSessions, getAgentBoundModelId, setAgentBoundModelId, collapseSkillPrompt, readGoal, accountTurn, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, bumpContinuationCount, pauseGoal, resumeGoal, setGoal as setGoalState, clearGoal as clearGoalState, setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, getClaudeNativeGoal, buildClaudeSetGoalPrompt, buildClaudeClearGoalPrompt, deliverArtifact, attachmentUrl, isPendingSessionId, } from '../agent/index.js';
6
6
  import { compactForHandover, describeHandoverRef } from '../agent/handover.js';
7
- import { getActiveProfileId, setActiveProfile } from '../model/index.js';
7
+ import { getActiveProfileId, getActiveProfile, setActiveProfile } from '../model/index.js';
8
8
  import { querySessions, querySessionTail, updateSession, } from './session-hub.js';
9
9
  import { getDriver, hasDriver, allDriverIds, getDriverCapabilities } from '../agent/driver.js';
10
10
  import { resolveGuiIntegrationConfig } from '../agent/mcp/bridge.js';
@@ -1689,6 +1689,9 @@ export class Bot {
1689
1689
  this.log(`workflow ${enabled ? 'enabled' : 'disabled'} for ${cs.agent} chat=${chatId}`);
1690
1690
  }
1691
1691
  modelForAgent(agent) {
1692
+ const profile = getActiveProfile(agent);
1693
+ if (profile?.modelId)
1694
+ return profile.modelId;
1692
1695
  if (agent === 'hermes') {
1693
1696
  const bound = getAgentBoundModelId('hermes');
1694
1697
  if (bound)
@@ -48,8 +48,9 @@ function compactModelLabel(model) {
48
48
  }
49
49
  export function formatFooterParts(agent, elapsedMs, meta, contextPercent, decorations) {
50
50
  const identityParts = [agent];
51
- if (decorations?.model)
52
- identityParts.push(compactModelLabel(decorations.model));
51
+ const modelLabel = decorations?.profileName ?? meta?.profileName ?? decorations?.model ?? null;
52
+ if (modelLabel)
53
+ identityParts.push(compactModelLabel(modelLabel));
53
54
  const runtimeParts = [];
54
55
  if (decorations?.effort)
55
56
  runtimeParts.push(decorations.effort);
@@ -232,6 +232,8 @@ export function buildFinalReplyRender(agent, result) {
232
232
  const footerText = `\n\n${formatFinalFooter(data.footerStatus, agent, data.elapsedMs, result.contextPercent ?? null, {
233
233
  model: result.model,
234
234
  effort: result.thinkingEffort,
235
+ provider: result.byokProviderName,
236
+ profileName: result.byokProfileName,
235
237
  })}`;
236
238
  let activityText = '';
237
239
  let activityNoteText = '';
@@ -372,6 +372,8 @@ export function buildFinalReplyRender(agent, result) {
372
372
  const footerHtml = `\n\n${formatFinalFooterHtml(data.footerStatus, agent, data.elapsedMs, result.contextPercent ?? null, {
373
373
  model: result.model,
374
374
  effort: result.thinkingEffort,
375
+ provider: result.byokProviderName,
376
+ profileName: result.byokProfileName,
375
377
  })}`;
376
378
  let activityHtml = '';
377
379
  let activityNoteHtml = '';
@@ -172,6 +172,10 @@ async function buildAgentStatusResponse(config = loadUserConfig(), agentOptions
172
172
  const activeProfile = getActiveProfile(agentId);
173
173
  const byokProvider = activeProfile ? getProvider(activeProfile.providerId) : null;
174
174
  const byokProviderName = byokProvider?.name || null;
175
+ const byokProfileName = activeProfile
176
+ && activeProfile.name.trim().toLowerCase() !== activeProfile.modelId.trim().toLowerCase()
177
+ ? activeProfile.name
178
+ : null;
175
179
  const nativeSelectedModel = runtimeSelectedModel || nativeConfig?.model || null;
176
180
  const nativeSelectedEffort = runtimeSelectedEffort || nativeConfig?.effort || null;
177
181
  const selectedModel = activeProfile?.modelId || nativeSelectedModel;
@@ -202,6 +206,7 @@ async function buildAgentStatusResponse(config = loadUserConfig(), agentOptions
202
206
  nativeConfig,
203
207
  byokProviderName,
204
208
  byokModels,
209
+ byokProfileName,
205
210
  capabilities: getDriverCapabilities(agentId),
206
211
  latestVersion: updateState?.latestVersion || null,
207
212
  updateAvailable: updateState?.updateAvailable || false,
@@ -190,6 +190,123 @@ async function ensureProviderForBackend(spec) {
190
190
  return null;
191
191
  }
192
192
  }
193
+ const OLLAMA_QUANT_FACTOR = 0.7;
194
+ export function parseOllamaSizeToParamsB(token) {
195
+ const t = token.trim().toLowerCase();
196
+ let m = t.match(/^(\d+(?:\.\d+)?)x(\d+(?:\.\d+)?)b$/);
197
+ if (m)
198
+ return parseFloat(m[1]) * parseFloat(m[2]);
199
+ m = t.match(/^e(\d+(?:\.\d+)?)b$/);
200
+ if (m)
201
+ return parseFloat(m[1]);
202
+ m = t.match(/^(\d+(?:\.\d+)?)m$/);
203
+ if (m)
204
+ return parseFloat(m[1]) / 1000;
205
+ m = t.match(/^(\d+(?:\.\d+)?)b$/);
206
+ if (m)
207
+ return parseFloat(m[1]);
208
+ return null;
209
+ }
210
+ export function estimateOllamaDiskGb(paramsB) {
211
+ return Math.round(paramsB * OLLAMA_QUANT_FACTOR * 10) / 10;
212
+ }
213
+ export function estimateOllamaMinRamGb(paramsB) {
214
+ return Math.max(4, Math.round(paramsB * OLLAMA_QUANT_FACTOR * 1.5 + 4));
215
+ }
216
+ function decodeEntities(s) {
217
+ return s
218
+ .replace(/ /g, ' ')
219
+ .replace(/&/g, '&')
220
+ .replace(/&lt;/g, '<')
221
+ .replace(/&gt;/g, '>')
222
+ .replace(/&quot;/g, '"')
223
+ .replace(/&#0?39;|&#x27;/gi, "'")
224
+ .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)));
225
+ }
226
+ function allGroups(src, re) {
227
+ const out = [];
228
+ let m;
229
+ while ((m = re.exec(src)))
230
+ out.push(m[1]);
231
+ return out;
232
+ }
233
+ export function parseOllamaLibrary(html) {
234
+ const blocks = html.split('<li x-test-model').slice(1);
235
+ const out = [];
236
+ for (const b of blocks) {
237
+ const name = b.match(/title="([^"]+)"/)?.[1]?.trim();
238
+ if (!name)
239
+ continue;
240
+ const descRaw = b.match(/<p[^>]*max-w-lg[^>]*>([\s\S]*?)<\/p>/)?.[1] ?? '';
241
+ const description = decodeEntities(descRaw.replace(/<[^>]+>/g, '')).trim();
242
+ const capabilities = allGroups(b, /x-test-capability[^>]*>([^<]+)</g).map(s => s.trim());
243
+ const sizes = allGroups(b, /x-test-size[^>]*>([^<]+)</g).map(raw => {
244
+ const tag = raw.trim();
245
+ const paramsB = parseOllamaSizeToParamsB(tag);
246
+ if (paramsB == null)
247
+ return { tag, paramsB: 0, diskGb: 0, minRamGb: 0 };
248
+ return { tag, paramsB, diskGb: estimateOllamaDiskGb(paramsB), minRamGb: estimateOllamaMinRamGb(paramsB) };
249
+ });
250
+ const pulls = (b.match(/x-test-pull-count[^>]*>([^<]+)</)?.[1] ?? '').trim();
251
+ const updated = (b.match(/x-test-updated[^>]*>([^<]+)</)?.[1] ?? '').trim();
252
+ out.push({ name, description, capabilities, sizes, pulls, updated, url: `https://ollama.com/library/${name}` });
253
+ }
254
+ return out;
255
+ }
256
+ const OLLAMA_LIBRARY_URL = 'https://ollama.com/library?sort=popular';
257
+ const OLLAMA_LIBRARY_TTL_MS = 6 * 60 * 60 * 1000;
258
+ const OLLAMA_LIBRARY_TIMEOUT_MS = 12000;
259
+ let libraryCache = null;
260
+ let libraryInflight = null;
261
+ async function fetchOllamaLibrary() {
262
+ const controller = new AbortController();
263
+ const timer = setTimeout(() => controller.abort(), OLLAMA_LIBRARY_TIMEOUT_MS);
264
+ try {
265
+ const res = await fetch(OLLAMA_LIBRARY_URL, {
266
+ signal: controller.signal,
267
+ headers: { 'user-agent': 'Mozilla/5.0 (pikiloom local-models)' },
268
+ });
269
+ if (!res.ok)
270
+ throw new Error(`HTTP ${res.status}`);
271
+ const models = parseOllamaLibrary(await res.text());
272
+ if (!models.length)
273
+ throw new Error('no models parsed from Ollama library');
274
+ return models;
275
+ }
276
+ finally {
277
+ clearTimeout(timer);
278
+ }
279
+ }
280
+ async function getOllamaLibrary(force) {
281
+ const fresh = libraryCache && Date.now() - libraryCache.at < OLLAMA_LIBRARY_TTL_MS;
282
+ if (libraryCache && fresh && !force) {
283
+ return { models: libraryCache.models, fetchedAt: libraryCache.at, stale: false };
284
+ }
285
+ if (!libraryInflight) {
286
+ libraryInflight = fetchOllamaLibrary()
287
+ .then(models => { libraryCache = { at: Date.now(), models }; return models; })
288
+ .finally(() => { libraryInflight = null; });
289
+ }
290
+ try {
291
+ const models = await libraryInflight;
292
+ return { models, fetchedAt: libraryCache.at, stale: false };
293
+ }
294
+ catch (e) {
295
+ if (libraryCache)
296
+ return { models: libraryCache.models, fetchedAt: libraryCache.at, stale: true };
297
+ throw e;
298
+ }
299
+ }
300
+ router.get('/api/local-models/ollama-library', async (c) => {
301
+ try {
302
+ const force = c.req.query('refresh') === '1';
303
+ const { models, fetchedAt, stale } = await getOllamaLibrary(force);
304
+ return c.json({ ok: true, models, fetchedAt, stale });
305
+ }
306
+ catch (e) {
307
+ return c.json({ ok: false, error: e?.message || String(e) }, 502);
308
+ }
309
+ });
193
310
  router.get('/api/local-models/probe', async (c) => {
194
311
  try {
195
312
  const initialProviders = listProviders();
@@ -0,0 +1,419 @@
1
+ import http from 'node:http';
2
+ import { writeScopedLog } from '../core/logging.js';
3
+ const SCOPE = 'anthropic-bridge';
4
+ const log = (m) => { writeScopedLog(SCOPE, m); };
5
+ const warn = (m) => { writeScopedLog(SCOPE, m, { level: 'warn', stream: 'stderr' }); };
6
+ let server = null;
7
+ let listenPort = 0;
8
+ let starting = null;
9
+ let idCounter = 0;
10
+ function genId(prefix) {
11
+ idCounter += 1;
12
+ return `${prefix}_${Date.now().toString(36)}${idCounter.toString(36)}`;
13
+ }
14
+ function num(v) { return typeof v === 'number' && Number.isFinite(v) ? v : 0; }
15
+ function decodeUpstream(token) {
16
+ try {
17
+ return Buffer.from(token, 'base64url').toString('utf8') || null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function chatCompletionsUrl(base) {
24
+ const b = base.replace(/\/+$/, '');
25
+ return b.endsWith('/chat/completions') ? b : `${b}/chat/completions`;
26
+ }
27
+ export async function ensureAnthropicBridge() {
28
+ if (server && listenPort)
29
+ return listenPort;
30
+ if (starting)
31
+ return starting;
32
+ starting = new Promise((resolve, reject) => {
33
+ const srv = http.createServer(handleRequest);
34
+ srv.on('error', err => { warn(`server error: ${err?.message || err}`); reject(err); });
35
+ srv.listen(0, '127.0.0.1', () => {
36
+ server = srv;
37
+ const addr = srv.address();
38
+ listenPort = typeof addr === 'object' && addr ? addr.port : 0;
39
+ log(`listening on 127.0.0.1:${listenPort}`);
40
+ resolve(listenPort);
41
+ });
42
+ });
43
+ try {
44
+ return await starting;
45
+ }
46
+ finally {
47
+ starting = null;
48
+ }
49
+ }
50
+ export function shutdownAnthropicBridge() {
51
+ try {
52
+ server?.close();
53
+ }
54
+ catch { }
55
+ server = null;
56
+ listenPort = 0;
57
+ }
58
+ function readBearer(req) {
59
+ const xkey = req.headers['x-api-key'];
60
+ if (typeof xkey === 'string' && xkey)
61
+ return xkey;
62
+ const auth = req.headers['authorization'];
63
+ const a = Array.isArray(auth) ? auth[0] : auth;
64
+ if (typeof a === 'string')
65
+ return a.replace(/^Bearer\s+/i, '');
66
+ return '';
67
+ }
68
+ function handleRequest(req, res) {
69
+ const url = new URL(req.url || '/', 'http://127.0.0.1');
70
+ const m = url.pathname.match(/^\/u\/([^/]+)\/v1\/(messages|messages\/count_tokens|models)$/);
71
+ if (!m) {
72
+ res.writeHead(404).end('not found');
73
+ return;
74
+ }
75
+ const upstreamBase = decodeUpstream(m[1]);
76
+ if (!upstreamBase) {
77
+ res.writeHead(400).end('bad upstream token');
78
+ return;
79
+ }
80
+ const route = m[2];
81
+ if (route === 'models') {
82
+ res.writeHead(200, { 'content-type': 'application/json' });
83
+ res.end(JSON.stringify({ data: [], has_more: false }));
84
+ return;
85
+ }
86
+ if (req.method !== 'POST') {
87
+ res.writeHead(405).end('method not allowed');
88
+ return;
89
+ }
90
+ const chunks = [];
91
+ req.on('data', c => chunks.push(c));
92
+ req.on('end', () => {
93
+ let body = {};
94
+ try {
95
+ body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
96
+ }
97
+ catch {
98
+ body = {};
99
+ }
100
+ if (route === 'messages/count_tokens') {
101
+ res.writeHead(200, { 'content-type': 'application/json' });
102
+ res.end(JSON.stringify({ input_tokens: estimateTokens(body) }));
103
+ return;
104
+ }
105
+ handleMessages(req, res, upstreamBase, body).catch(err => {
106
+ warn(`handler error: ${err?.message || err}`);
107
+ sendError(res, `bridge error: ${err?.message || err}`);
108
+ });
109
+ });
110
+ }
111
+ async function handleMessages(req, res, upstreamBase, body) {
112
+ const wantStream = body.stream === true;
113
+ const chatReq = anthropicToChatRequest(body, wantStream);
114
+ const key = readBearer(req);
115
+ const upstreamUrl = chatCompletionsUrl(upstreamBase);
116
+ log(`-> ${upstreamUrl} model=${chatReq.model} msgs=${chatReq.messages.length} tools=${chatReq.tools?.length ?? 0} stream=${wantStream}`);
117
+ let upstreamResp;
118
+ try {
119
+ upstreamResp = await fetch(upstreamUrl, {
120
+ method: 'POST',
121
+ headers: { 'content-type': 'application/json', ...(key ? { authorization: `Bearer ${key}` } : {}) },
122
+ body: JSON.stringify(chatReq),
123
+ });
124
+ }
125
+ catch (e) {
126
+ sendError(res, `upstream fetch failed: ${e?.message || e}`);
127
+ return;
128
+ }
129
+ if (!upstreamResp.ok || !upstreamResp.body) {
130
+ const raw = await upstreamResp.text().catch(() => '');
131
+ warn(`upstream ${upstreamResp.status}: ${raw.slice(0, 300)}`);
132
+ sendError(res, `upstream ${upstreamResp.status}: ${raw.slice(0, 500)}`, upstreamResp.status >= 400 ? upstreamResp.status : 502);
133
+ return;
134
+ }
135
+ if (!wantStream) {
136
+ const data = await upstreamResp.json().catch(() => null);
137
+ res.writeHead(200, { 'content-type': 'application/json' });
138
+ res.end(JSON.stringify(chatCompletionToAnthropic(data, chatReq.model)));
139
+ return;
140
+ }
141
+ res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
142
+ res.flushHeaders?.();
143
+ const emit = (type, data) => {
144
+ res.write(`event: ${type}\n`);
145
+ res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
146
+ };
147
+ const msgId = genId('msg');
148
+ emit('message_start', {
149
+ message: {
150
+ id: msgId, type: 'message', role: 'assistant', model: chatReq.model,
151
+ content: [], stop_reason: null, stop_sequence: null,
152
+ usage: { input_tokens: 0, output_tokens: 0 },
153
+ },
154
+ });
155
+ let textOpen = false;
156
+ let textEver = false;
157
+ const tools = new Map();
158
+ let usage = null;
159
+ let finish = null;
160
+ const reader = upstreamResp.body.getReader();
161
+ const dec = new TextDecoder();
162
+ let buf = '';
163
+ try {
164
+ for (;;) {
165
+ const { value, done } = await reader.read();
166
+ if (done)
167
+ break;
168
+ buf += dec.decode(value, { stream: true });
169
+ let nl;
170
+ while ((nl = buf.indexOf('\n')) >= 0) {
171
+ const line = buf.slice(0, nl).trim();
172
+ buf = buf.slice(nl + 1);
173
+ if (!line.startsWith('data:'))
174
+ continue;
175
+ const dataStr = line.slice(5).trim();
176
+ if (dataStr === '[DONE]') {
177
+ buf = '';
178
+ break;
179
+ }
180
+ let chunk;
181
+ try {
182
+ chunk = JSON.parse(dataStr);
183
+ }
184
+ catch {
185
+ continue;
186
+ }
187
+ if (chunk.usage)
188
+ usage = chunk.usage;
189
+ const choice = chunk.choices?.[0];
190
+ if (!choice)
191
+ continue;
192
+ if (choice.finish_reason)
193
+ finish = choice.finish_reason;
194
+ const delta = choice.delta || {};
195
+ if (typeof delta.content === 'string' && delta.content) {
196
+ if (!textOpen) {
197
+ emit('content_block_start', { index: 0, content_block: { type: 'text', text: '' } });
198
+ textOpen = true;
199
+ textEver = true;
200
+ }
201
+ emit('content_block_delta', { index: 0, delta: { type: 'text_delta', text: delta.content } });
202
+ }
203
+ if (Array.isArray(delta.tool_calls)) {
204
+ for (const tc of delta.tool_calls) {
205
+ const idx = typeof tc.index === 'number' ? tc.index : 0;
206
+ let st = tools.get(idx);
207
+ if (!st) {
208
+ st = { id: tc.id || genId('toolu'), name: '', args: '' };
209
+ tools.set(idx, st);
210
+ }
211
+ if (tc.id)
212
+ st.id = tc.id;
213
+ if (tc.function?.name)
214
+ st.name += tc.function.name;
215
+ if (typeof tc.function?.arguments === 'string')
216
+ st.args += tc.function.arguments;
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+ catch (e) {
223
+ warn(`stream read error: ${e?.message || e}`);
224
+ }
225
+ if (textOpen)
226
+ emit('content_block_stop', { index: 0 });
227
+ if (!textEver && tools.size === 0) {
228
+ emit('content_block_start', { index: 0, content_block: { type: 'text', text: '' } });
229
+ emit('content_block_stop', { index: 0 });
230
+ textEver = true;
231
+ }
232
+ let idx = textEver ? 1 : 0;
233
+ for (const st of tools.values()) {
234
+ emit('content_block_start', { index: idx, content_block: { type: 'tool_use', id: st.id, name: st.name, input: {} } });
235
+ emit('content_block_delta', { index: idx, delta: { type: 'input_json_delta', partial_json: st.args.trim() ? st.args : '{}' } });
236
+ emit('content_block_stop', { index: idx });
237
+ idx += 1;
238
+ }
239
+ const stopReason = tools.size ? 'tool_use' : mapStop(finish);
240
+ emit('message_delta', {
241
+ delta: { stop_reason: stopReason, stop_sequence: null },
242
+ usage: { input_tokens: num(usage?.prompt_tokens), output_tokens: num(usage?.completion_tokens) },
243
+ });
244
+ emit('message_stop', {});
245
+ res.end();
246
+ }
247
+ function sendError(res, message, status = 502) {
248
+ if (res.headersSent) {
249
+ try {
250
+ res.write(`event: error\n`);
251
+ res.write(`data: ${JSON.stringify({ type: 'error', error: { type: 'api_error', message } })}\n\n`);
252
+ res.end();
253
+ }
254
+ catch { }
255
+ return;
256
+ }
257
+ res.writeHead(status, { 'content-type': 'application/json' });
258
+ res.end(JSON.stringify({ type: 'error', error: { type: 'api_error', message } }));
259
+ }
260
+ function blockText(content) {
261
+ if (typeof content === 'string')
262
+ return content;
263
+ if (Array.isArray(content)) {
264
+ return content
265
+ .map((b) => (typeof b === 'string' ? b : (b?.type === 'text' && typeof b.text === 'string' ? b.text : '')))
266
+ .join('');
267
+ }
268
+ return '';
269
+ }
270
+ function anthropicToChatRequest(body, wantStream) {
271
+ const messages = [];
272
+ if (body.system) {
273
+ const sys = typeof body.system === 'string' ? body.system : blockText(body.system);
274
+ if (sys.trim())
275
+ messages.push({ role: 'system', content: sys });
276
+ }
277
+ for (const m of (Array.isArray(body.messages) ? body.messages : [])) {
278
+ const role = m?.role;
279
+ const content = m?.content;
280
+ if (typeof content === 'string') {
281
+ messages.push({ role: role === 'assistant' ? 'assistant' : 'user', content });
282
+ continue;
283
+ }
284
+ if (!Array.isArray(content))
285
+ continue;
286
+ if (role === 'assistant') {
287
+ let text = '';
288
+ const toolCalls = [];
289
+ for (const b of content) {
290
+ if (b?.type === 'text')
291
+ text += b.text || '';
292
+ else if (b?.type === 'tool_use') {
293
+ toolCalls.push({
294
+ id: b.id,
295
+ type: 'function',
296
+ function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) },
297
+ });
298
+ }
299
+ }
300
+ const msg = { role: 'assistant', content: text || null };
301
+ if (toolCalls.length)
302
+ msg.tool_calls = toolCalls;
303
+ messages.push(msg);
304
+ }
305
+ else {
306
+ const toolMsgs = [];
307
+ const parts = [];
308
+ for (const b of content) {
309
+ if (b?.type === 'text')
310
+ parts.push({ type: 'text', text: b.text || '' });
311
+ else if (b?.type === 'image' && b.source) {
312
+ const src = b.source;
313
+ let url = '';
314
+ if (src.type === 'base64' && src.data)
315
+ url = `data:${src.media_type || 'image/png'};base64,${src.data}`;
316
+ else if (src.type === 'url' && src.url)
317
+ url = src.url;
318
+ if (url)
319
+ parts.push({ type: 'image_url', image_url: { url } });
320
+ }
321
+ else if (b?.type === 'tool_result') {
322
+ const trText = typeof b.content === 'string' ? b.content : blockText(b.content);
323
+ toolMsgs.push({ role: 'tool', tool_call_id: b.tool_use_id, content: trText });
324
+ }
325
+ }
326
+ for (const tm of toolMsgs)
327
+ messages.push(tm);
328
+ if (parts.length) {
329
+ const onlyText = parts.every(p => p.type === 'text');
330
+ messages.push({ role: 'user', content: onlyText ? parts.map(p => p.text).join('') : parts });
331
+ }
332
+ }
333
+ }
334
+ const tools = Array.isArray(body.tools) ? body.tools.map(anthropicToolToChat).filter(Boolean) : undefined;
335
+ const req = { model: body.model, messages, stream: wantStream };
336
+ if (wantStream)
337
+ req.stream_options = { include_usage: true };
338
+ if (tools && tools.length)
339
+ req.tools = tools;
340
+ if (body.tool_choice) {
341
+ const tc = anthropicToolChoiceToChat(body.tool_choice);
342
+ if (tc)
343
+ req.tool_choice = tc;
344
+ }
345
+ if (typeof body.max_tokens === 'number')
346
+ req.max_tokens = body.max_tokens;
347
+ if (typeof body.temperature === 'number')
348
+ req.temperature = body.temperature;
349
+ if (typeof body.top_p === 'number')
350
+ req.top_p = body.top_p;
351
+ return req;
352
+ }
353
+ function anthropicToolToChat(t) {
354
+ if (!t || typeof t.name !== 'string' || !t.name)
355
+ return null;
356
+ return {
357
+ type: 'function',
358
+ function: {
359
+ name: t.name,
360
+ description: typeof t.description === 'string' ? t.description : '',
361
+ parameters: t.input_schema && typeof t.input_schema === 'object' ? t.input_schema : { type: 'object', properties: {} },
362
+ },
363
+ };
364
+ }
365
+ function anthropicToolChoiceToChat(tc) {
366
+ if (!tc || typeof tc !== 'object')
367
+ return undefined;
368
+ if (tc.type === 'auto')
369
+ return 'auto';
370
+ if (tc.type === 'any')
371
+ return 'required';
372
+ if (tc.type === 'tool' && tc.name)
373
+ return { type: 'function', function: { name: tc.name } };
374
+ return 'auto';
375
+ }
376
+ function chatCompletionToAnthropic(data, model) {
377
+ const choice = data?.choices?.[0];
378
+ const msg = choice?.message || {};
379
+ const content = [];
380
+ if (typeof msg.content === 'string' && msg.content)
381
+ content.push({ type: 'text', text: msg.content });
382
+ if (Array.isArray(msg.tool_calls)) {
383
+ for (const tc of msg.tool_calls) {
384
+ let input = {};
385
+ try {
386
+ input = JSON.parse(tc.function?.arguments || '{}');
387
+ }
388
+ catch {
389
+ input = {};
390
+ }
391
+ content.push({ type: 'tool_use', id: tc.id || genId('toolu'), name: tc.function?.name || '', input });
392
+ }
393
+ }
394
+ if (!content.length)
395
+ content.push({ type: 'text', text: '' });
396
+ const stop = msg.tool_calls?.length ? 'tool_use' : mapStop(choice?.finish_reason || 'stop');
397
+ return {
398
+ id: genId('msg'), type: 'message', role: 'assistant', model,
399
+ content, stop_reason: stop, stop_sequence: null,
400
+ usage: { input_tokens: num(data?.usage?.prompt_tokens), output_tokens: num(data?.usage?.completion_tokens) },
401
+ };
402
+ }
403
+ function mapStop(finish) {
404
+ switch (finish) {
405
+ case 'length': return 'max_tokens';
406
+ case 'tool_calls': return 'tool_use';
407
+ case 'content_filter': return 'end_turn';
408
+ default: return 'end_turn';
409
+ }
410
+ }
411
+ function estimateTokens(body) {
412
+ let chars = 0;
413
+ if (body?.system)
414
+ chars += (typeof body.system === 'string' ? body.system : blockText(body.system)).length;
415
+ for (const m of (Array.isArray(body?.messages) ? body.messages : [])) {
416
+ chars += blockText(m?.content).length;
417
+ }
418
+ return Math.max(1, Math.ceil(chars / 4));
419
+ }