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.
- package/dashboard/dist/assets/AgentTab-C9Z6BUng.js +1 -0
- package/dashboard/dist/assets/{ConnectionModal-CbWtylsE.js → ConnectionModal-DZEnnbRt.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-BsHjGC5A.js → DirBrowser-DNd06lse.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BYoLkIki.js → ExtensionsTab-BXrPOYT9.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-B-SxDXft.js → IMAccessTab-gGQ_84mB.js} +1 -1
- package/dashboard/dist/assets/{Modal-CzQbFS5c.js → Modal-DqsmC8u2.js} +1 -1
- package/dashboard/dist/assets/{Modals-V9ykx8c4.js → Modals-CZuvmOU0.js} +1 -1
- package/dashboard/dist/assets/{Select-DE6cseW_.js → Select-Cvo1Mvn_.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-DkvJ4WNu.js +1 -0
- package/dashboard/dist/assets/{SystemTab-Cx6DyiFf.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-Cjaly0tB.js → index-CifA8H7w.js} +3 -3
- package/dashboard/dist/assets/{shared-DQrcGYOc.js → shared-BoB6k6CT.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/drivers/codex.js +2 -0
- package/dist/agent/stream.js +12 -2
- package/dist/agent/utils.js +2 -0
- package/dist/bot/bot.js +4 -1
- package/dist/bot/render-shared.js +3 -2
- package/dist/channels/feishu/render.js +2 -0
- package/dist/channels/telegram/render.js +2 -0
- package/dist/dashboard/routes/agents.js +5 -0
- package/dist/dashboard/routes/local-models.js +117 -0
- package/dist/model/anthropic-bridge.js +419 -0
- package/dist/model/injector.js +33 -3
- package/dist/model/responses-bridge.js +29 -4
- package/package.json +1 -1
- package/dashboard/dist/assets/AgentTab-CYoIkNyL.js +0 -1
- package/dashboard/dist/assets/SessionPanel-CMu962uE.js +0 -1
- package/dashboard/dist/assets/index-CXIN3nTr.css +0 -1
- package/dashboard/dist/assets/index-Dp_KlJM9.js +0 -3
package/dist/agent/stream.js
CHANGED
|
@@ -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
|
|
477
|
-
|
|
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);
|
package/dist/agent/utils.js
CHANGED
|
@@ -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
|
-
|
|
52
|
-
|
|
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(/</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();
|
|
@@ -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
|
+
}
|