pikiloom 0.4.30 → 0.4.32
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/dashboard/dist/assets/AgentTab-C9Z6BUng.js +1 -0
- package/dashboard/dist/assets/{ConnectionModal-BktbmACQ.js → ConnectionModal-DZEnnbRt.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-DPNjooga.js → DirBrowser-DNd06lse.js} +1 -1
- package/dashboard/dist/assets/ExtensionsTab-BXrPOYT9.js +1 -0
- package/dashboard/dist/assets/{IMAccessTab-1HeNr-tg.js → IMAccessTab-gGQ_84mB.js} +1 -1
- package/dashboard/dist/assets/{Modal-B4wc5184.js → Modal-DqsmC8u2.js} +1 -1
- package/dashboard/dist/assets/{Modals-C-2IImYO.js → Modals-CZuvmOU0.js} +1 -1
- package/dashboard/dist/assets/{Select-xEvJAcZi.js → Select-Cvo1Mvn_.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-DkvJ4WNu.js +1 -0
- package/dashboard/dist/assets/{SystemTab-DTggBzSg.js → SystemTab-BnxqGrbn.js} +1 -1
- package/dashboard/dist/assets/index-CBNgeLIy.js +3 -0
- package/dashboard/dist/assets/index-CH_4h4ZM.css +1 -0
- package/dashboard/dist/assets/{index-DVHryefY.js → index-CifA8H7w.js} +3 -3
- package/dashboard/dist/assets/{shared-CDeMhM6X.js → shared-BoB6k6CT.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/artifacts.js +11 -1
- package/dist/agent/index.js +1 -1
- package/dist/bot/bot.js +4 -1
- package/dist/catalog/mcp-servers.js +24 -0
- package/dist/core/utils.js +12 -6
- package/dist/dashboard/routes/agents.js +5 -0
- package/dist/dashboard/routes/local-models.js +117 -0
- package/dist/dashboard/routes/sessions.js +7 -2
- package/dist/model/anthropic-bridge.js +419 -0
- package/dist/model/injector.js +35 -3
- package/package.json +1 -1
- package/dashboard/dist/assets/AgentTab-B4EYK2m8.js +0 -1
- package/dashboard/dist/assets/ExtensionsTab-Bv_23Acs.js +0 -1
- package/dashboard/dist/assets/SessionPanel-DRrCVpih.js +0 -1
- package/dashboard/dist/assets/index-CJm8YsKX.js +0 -3
- package/dashboard/dist/assets/index-CXIN3nTr.css +0 -1
package/dist/agent/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import './drivers/gemini.js';
|
|
|
4
4
|
import './drivers/hermes.js';
|
|
5
5
|
export { IMAGE_EXTS } from './types.js';
|
|
6
6
|
export { attachAgentImage, attachInlineImage, materializeImage, rewriteAttachmentBlocksForTransport, attachmentUrl, resolveAllowedAttachmentPath, allowedAttachmentRoots, decodeAttachmentPathParam, sessionAttachmentsDir, codexHome, } from './images.js';
|
|
7
|
-
export { deliverArtifact, readDeliveredArtifacts, deliveredArtifactBlocks, mimeForArtifact, } from './artifacts.js';
|
|
7
|
+
export { deliverArtifact, readDeliveredArtifacts, deliveredArtifactBlocks, latestDeliveredTaskId, mimeForArtifact, } from './artifacts.js';
|
|
8
8
|
export { Q, agentLog, agentWarn, agentError, dedupeStrings, numberOrNull, normalizeStreamPreviewPlan, parseTodoWriteAsPlan, normalizeActivityLine, pushRecentActivity, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, firstNonEmptyLine, shortValue, normalizeErrorMessage, joinErrorMessages, appendSystemPrompt, mimeForExt, computeContext, buildStreamPreviewMeta, summarizeClaudeToolUse, summarizeClaudeToolResult, previewToolCallInput, previewToolCallResult, roundPercent, toIsoFromEpochSeconds, normalizeUsageStatus, labelFromWindowMinutes, usageWindowFromRateLimit, parseJsonTail, modelFamily, normalizeClaudeModelId, emptyUsage, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, stripClaudeAtMentionImages, isPendingSessionId, emitSessionIdUpdate, sessionListDisplayTitle, } from './utils.js';
|
|
9
9
|
export { updateSessionMeta, promoteSessionId, recordFork, resolveCanonicalSessionId, getSessionPromotions, listPikiloomSessions, findPikiloomSession, getSessionStoredConfig, ensureManagedSession, findManagedThreadSession, stageSessionFiles, mergeManagedAndNativeSessions, managedRecordToSessionInfo, getSessions, getSessionTail, getSessionMessages, applyTurnWindow, applyTurnFilter, classifySession, deriveUserStatus, exportSession, importSession, deleteAgentSession, isProcessAlive, isRunningSessionStale, reconcileOrphanedRunningSessions, } from './session.js';
|
|
10
10
|
export { detectAgentBin, listAgents, resolveDefaultAgent, run, doStream, listModels, resolveAgentModels, getUsage, getAgentBoundModelId, setAgentBoundModelId, } from './stream.js';
|
package/dist/bot/bot.js
CHANGED
|
@@ -418,7 +418,10 @@ export class Bot {
|
|
|
418
418
|
if (!sid || isPendingSessionId(sid))
|
|
419
419
|
return result;
|
|
420
420
|
const kind = sendOpts?.kind === 'photo' ? 'photo' : 'document';
|
|
421
|
-
const
|
|
421
|
+
const taskId = sessionKey
|
|
422
|
+
? this.streamSnapshots.get(this.resolveSessionKey(sessionKey))?.taskId
|
|
423
|
+
: undefined;
|
|
424
|
+
const record = deliverArtifact(agent, sid, filePath, { kind, caption: sendOpts?.caption, ...(taskId ? { taskId } : {}) });
|
|
422
425
|
if (record && sessionKey) {
|
|
423
426
|
this.emitStream(this.resolveSessionKey(sessionKey), {
|
|
424
427
|
type: 'artifact',
|
|
@@ -212,6 +212,30 @@ export const MCP_SERVERS = [
|
|
|
212
212
|
iconSlug: 'lark',
|
|
213
213
|
homepage: 'https://open.feishu.cn/document/mcp',
|
|
214
214
|
},
|
|
215
|
+
{
|
|
216
|
+
id: 'gmail',
|
|
217
|
+
name: 'Gmail',
|
|
218
|
+
description: 'Read, search, send, and manage Gmail messages, threads, and labels. One-time sign-in via `npx @gongrzhe/server-gmail-autoauth-mcp auth`.',
|
|
219
|
+
descriptionZh: '读取、搜索、发送和管理 Gmail 邮件、会话与标签。首次需运行 `npx @gongrzhe/server-gmail-autoauth-mcp auth` 完成 Google 授权。',
|
|
220
|
+
category: 'communication',
|
|
221
|
+
recommendedScope: 'global',
|
|
222
|
+
transport: { type: 'stdio', command: 'npx', args: ['-y', '@gongrzhe/server-gmail-autoauth-mcp'] },
|
|
223
|
+
auth: { type: 'none' },
|
|
224
|
+
iconSlug: 'gmail',
|
|
225
|
+
homepage: 'https://github.com/GongRzhe/Gmail-MCP-Server',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: 'outlook',
|
|
229
|
+
name: 'Outlook',
|
|
230
|
+
description: 'Microsoft 365 / Outlook email via Microsoft Graph — read, search, send, and organize mail. One-time device-code sign-in via `npx @softeria/ms-365-mcp-server --login`.',
|
|
231
|
+
descriptionZh: '通过 Microsoft Graph 访问 Microsoft 365 / Outlook 邮箱 — 读取、搜索、发送和整理邮件。首次需运行 `npx @softeria/ms-365-mcp-server --login` 完成设备码登录。',
|
|
232
|
+
category: 'communication',
|
|
233
|
+
recommendedScope: 'global',
|
|
234
|
+
transport: { type: 'stdio', command: 'npx', args: ['-y', '@softeria/ms-365-mcp-server', '--preset', 'mail'] },
|
|
235
|
+
auth: { type: 'none' },
|
|
236
|
+
iconSlug: 'outlook',
|
|
237
|
+
homepage: 'https://github.com/softeria/ms-365-mcp-server',
|
|
238
|
+
},
|
|
215
239
|
{
|
|
216
240
|
id: 'stripe',
|
|
217
241
|
name: 'Stripe',
|
package/dist/core/utils.js
CHANGED
|
@@ -16,13 +16,19 @@ export function ensureGitignore(dir) {
|
|
|
16
16
|
'.claude/skills/',
|
|
17
17
|
'.agents/skills/',
|
|
18
18
|
]);
|
|
19
|
-
const rawLines = fs.readFileSync(gi, 'utf8').split(/\r?\n/);
|
|
20
|
-
const normalized = rawLines.filter(line => {
|
|
21
|
-
const trimmed = line.trim();
|
|
22
|
-
return trimmed && !managedLines.includes(trimmed) && !legacyLines.has(trimmed);
|
|
23
|
-
});
|
|
24
|
-
const next = [...normalized, ...managedLines, ''].join('\n');
|
|
25
19
|
const current = fs.readFileSync(gi, 'utf8');
|
|
20
|
+
const rawLines = current.split(/\r?\n/);
|
|
21
|
+
const kept = rawLines.filter(line => !legacyLines.has(line.trim()));
|
|
22
|
+
const present = new Set(kept.map(line => line.trim()));
|
|
23
|
+
const missing = managedLines.filter(line => !present.has(line));
|
|
24
|
+
if (kept.length === rawLines.length && missing.length === 0)
|
|
25
|
+
return;
|
|
26
|
+
let next = kept.join('\n');
|
|
27
|
+
if (missing.length) {
|
|
28
|
+
if (next.length && !next.endsWith('\n'))
|
|
29
|
+
next += '\n';
|
|
30
|
+
next += missing.join('\n') + '\n';
|
|
31
|
+
}
|
|
26
32
|
if (current === next)
|
|
27
33
|
return;
|
|
28
34
|
fs.writeFileSync(gi, next);
|
|
@@ -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(/</g, '<')
|
|
221
|
+
.replace(/>/g, '>')
|
|
222
|
+
.replace(/"/g, '"')
|
|
223
|
+
.replace(/�?39;|'/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();
|
|
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
6
6
|
import { loadUserConfig } from '../../core/config/user-config.js';
|
|
7
|
-
import { listAgents, listSkills, decodeAttachmentPathParam, resolveAllowedAttachmentPath, rewriteAttachmentBlocksForTransport, deliveredArtifactBlocks, mimeForArtifact, } from '../../agent/index.js';
|
|
7
|
+
import { listAgents, listSkills, decodeAttachmentPathParam, resolveAllowedAttachmentPath, rewriteAttachmentBlocksForTransport, deliveredArtifactBlocks, latestDeliveredTaskId, mimeForArtifact, } from '../../agent/index.js';
|
|
8
8
|
import { getSessionStatusForBot } from '../../bot/session-status.js';
|
|
9
9
|
import { findPikiloomSession } from '../../agent/session.js';
|
|
10
10
|
import { readAwaitResume } from '../../agent/await-resume.js';
|
|
@@ -405,7 +405,12 @@ function prepareSessionMessagesForDashboard(result, agent, sessionId) {
|
|
|
405
405
|
}));
|
|
406
406
|
const includesTail = !result.window || !result.window.hasNewer;
|
|
407
407
|
if (includesTail) {
|
|
408
|
-
|
|
408
|
+
// Only surface the latest turn's delivered files. Artifacts are persisted in a
|
|
409
|
+
// session-wide manifest, so without this scope every file ever sent in the session
|
|
410
|
+
// would be re-appended onto the latest reply (cross-turn image bleed). Legacy
|
|
411
|
+
// artifacts predating taskId tagging fall back to the full set.
|
|
412
|
+
const latestTask = latestDeliveredTaskId(agent, sessionId);
|
|
413
|
+
const delivered = rewriteAttachmentBlocksForTransport(deliveredArtifactBlocks(agent, sessionId, latestTask ? (a => a.taskId === latestTask) : undefined), { agent, sessionId });
|
|
409
414
|
if (delivered.length) {
|
|
410
415
|
const text = deliveredSummaryText(delivered);
|
|
411
416
|
richMessages.push({ role: 'assistant', text, blocks: delivered });
|
|
@@ -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
|
+
}
|