@corsair-dev/studio 0.1.2 → 0.1.4
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/dist/server/index.js +624 -28
- package/dist/src/server/chat-store.d.ts +50 -0
- package/dist/src/server/chat-store.d.ts.map +1 -0
- package/dist/src/server/handlers/chat.d.ts +3 -0
- package/dist/src/server/handlers/chat.d.ts.map +1 -0
- package/dist/src/server/handlers/chats.d.ts +5 -0
- package/dist/src/server/handlers/chats.d.ts.map +1 -0
- package/dist/src/server/handlers/operations.d.ts.map +1 -1
- package/dist/src/server/model-pricing.d.ts +13 -0
- package/dist/src/server/model-pricing.d.ts.map +1 -0
- package/dist/src/server/router.d.ts.map +1 -1
- package/dist/web/assets/index-C1yAU4zE.js +55 -0
- package/dist/web/assets/index-C1yAU4zE.js.map +1 -0
- package/dist/web/assets/index-MIlMjQfJ.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +13 -3
- package/dist/web/assets/index-Ck3_0vOi.js +0 -54
- package/dist/web/assets/index-Ck3_0vOi.js.map +0 -1
- package/dist/web/assets/index-D8r6TgYt.css +0 -1
package/dist/server/index.js
CHANGED
|
@@ -341,6 +341,594 @@ var exchangeOAuth = async (ctx) => {
|
|
|
341
341
|
return { ok: true };
|
|
342
342
|
};
|
|
343
343
|
|
|
344
|
+
// src/server/handlers/chat.ts
|
|
345
|
+
import { buildCorsairToolDefs } from "@corsair-dev/mcp";
|
|
346
|
+
import { streamText, tool } from "ai";
|
|
347
|
+
import { z } from "zod";
|
|
348
|
+
|
|
349
|
+
// src/server/chat-store.ts
|
|
350
|
+
import Database from "better-sqlite3";
|
|
351
|
+
import { Kysely, SqliteDialect } from "kysely";
|
|
352
|
+
|
|
353
|
+
// src/server/model-pricing.ts
|
|
354
|
+
var MODEL_PRICES = {
|
|
355
|
+
"gpt-4o": { input: 2.5, output: 10 },
|
|
356
|
+
"gpt-4o-2024-11-20": { input: 2.5, output: 10 },
|
|
357
|
+
"gpt-4o-2024-08-06": { input: 2.5, output: 10 },
|
|
358
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
359
|
+
"gpt-4-turbo": { input: 10, output: 30 },
|
|
360
|
+
"gpt-4": { input: 30, output: 60 },
|
|
361
|
+
"gpt-3.5-turbo": { input: 0.5, output: 1.5 },
|
|
362
|
+
"gpt-4.1": { input: 2, output: 8 },
|
|
363
|
+
"gpt-4.1-mini": { input: 0.4, output: 1.6 },
|
|
364
|
+
"gpt-4.1-nano": { input: 0.1, output: 0.4 },
|
|
365
|
+
"gpt-5": { input: 1.25, output: 10 },
|
|
366
|
+
"gpt-5-mini": { input: 0.25, output: 2 },
|
|
367
|
+
"gpt-5-nano": { input: 0.05, output: 0.4 },
|
|
368
|
+
o1: { input: 15, output: 60 },
|
|
369
|
+
"o1-mini": { input: 3, output: 12 },
|
|
370
|
+
"o1-preview": { input: 15, output: 60 },
|
|
371
|
+
"o3-mini": { input: 1.1, output: 4.4 },
|
|
372
|
+
o3: { input: 2, output: 8 },
|
|
373
|
+
"o4-mini": { input: 1.1, output: 4.4 },
|
|
374
|
+
"claude-opus-4-5": { input: 15, output: 75 },
|
|
375
|
+
"claude-opus-4-7": { input: 15, output: 75 },
|
|
376
|
+
"claude-opus-4-1": { input: 15, output: 75 },
|
|
377
|
+
"claude-sonnet-4-5": { input: 3, output: 15 },
|
|
378
|
+
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
379
|
+
"claude-sonnet-4": { input: 3, output: 15 },
|
|
380
|
+
"claude-haiku-4-5": { input: 1, output: 5 },
|
|
381
|
+
"claude-3-7-sonnet-latest": { input: 3, output: 15 },
|
|
382
|
+
"claude-3-5-sonnet-latest": { input: 3, output: 15 },
|
|
383
|
+
"claude-3-5-sonnet-20241022": { input: 3, output: 15 },
|
|
384
|
+
"claude-3-5-haiku-latest": { input: 0.8, output: 4 },
|
|
385
|
+
"claude-3-5-haiku-20241022": { input: 0.8, output: 4 },
|
|
386
|
+
"claude-3-opus-latest": { input: 15, output: 75 },
|
|
387
|
+
"claude-3-haiku-20240307": { input: 0.25, output: 1.25 },
|
|
388
|
+
"gemini-2.5-pro": { input: 1.25, output: 10 },
|
|
389
|
+
"gemini-2.5-flash": { input: 0.3, output: 2.5 },
|
|
390
|
+
"gemini-2.5-flash-lite": { input: 0.1, output: 0.4 },
|
|
391
|
+
"gemini-2.0-flash": { input: 0.1, output: 0.4 },
|
|
392
|
+
"gemini-2.0-flash-001": { input: 0.1, output: 0.4 },
|
|
393
|
+
"gemini-2.0-flash-lite": { input: 0.075, output: 0.3 },
|
|
394
|
+
"gemini-2.0-pro-exp": { input: 1.25, output: 5 },
|
|
395
|
+
"gemini-1.5-pro": { input: 1.25, output: 5 },
|
|
396
|
+
"gemini-1.5-pro-latest": { input: 1.25, output: 5 },
|
|
397
|
+
"gemini-1.5-flash": { input: 0.075, output: 0.3 },
|
|
398
|
+
"gemini-1.5-flash-latest": { input: 0.075, output: 0.3 },
|
|
399
|
+
"gemini-1.5-flash-8b": { input: 0.0375, output: 0.15 },
|
|
400
|
+
"llama-3.3-70b-versatile": { input: 0.59, output: 0.79 },
|
|
401
|
+
"llama-3.1-70b-versatile": { input: 0.59, output: 0.79 },
|
|
402
|
+
"llama-3.1-8b-instant": { input: 0.05, output: 0.08 },
|
|
403
|
+
"llama-3.2-1b-preview": { input: 0.04, output: 0.04 },
|
|
404
|
+
"llama-3.2-3b-preview": { input: 0.06, output: 0.06 },
|
|
405
|
+
"llama-3.2-11b-vision-preview": { input: 0.18, output: 0.18 },
|
|
406
|
+
"llama-3.2-90b-vision-preview": { input: 0.9, output: 0.9 },
|
|
407
|
+
"mixtral-8x7b-32768": { input: 0.24, output: 0.24 },
|
|
408
|
+
"gemma2-9b-it": { input: 0.2, output: 0.2 },
|
|
409
|
+
"deepseek-r1-distill-llama-70b": { input: 0.75, output: 0.99 },
|
|
410
|
+
"deepseek-chat": { input: 0.27, output: 1.1 },
|
|
411
|
+
"deepseek-reasoner": { input: 0.55, output: 2.19 },
|
|
412
|
+
"mistral-large-latest": { input: 2, output: 6 },
|
|
413
|
+
"mistral-small-latest": { input: 0.2, output: 0.6 },
|
|
414
|
+
"codestral-latest": { input: 0.3, output: 0.9 },
|
|
415
|
+
"grok-2": { input: 2, output: 10 },
|
|
416
|
+
"grok-2-mini": { input: 0.3, output: 0.5 },
|
|
417
|
+
"grok-3": { input: 3, output: 15 },
|
|
418
|
+
"grok-3-mini": { input: 0.3, output: 0.5 },
|
|
419
|
+
"grok-4": { input: 5, output: 25 }
|
|
420
|
+
};
|
|
421
|
+
function normalizeId(id) {
|
|
422
|
+
return id.toLowerCase().replace(/^models\//, "").replace(/^anthropic\//, "").replace(/^openai\//, "").replace(/^google\//, "").replace(/^groq\//, "").trim();
|
|
423
|
+
}
|
|
424
|
+
function candidateIds(modelId) {
|
|
425
|
+
const norm = normalizeId(modelId);
|
|
426
|
+
const variants = /* @__PURE__ */ new Set([modelId, norm]);
|
|
427
|
+
const stripDate = norm.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
428
|
+
if (stripDate !== norm) variants.add(stripDate);
|
|
429
|
+
const stripVersion = norm.replace(/-(?:00\d|0?\d{2,3})$/, "");
|
|
430
|
+
if (stripVersion !== norm) variants.add(stripVersion);
|
|
431
|
+
const stripLatest = norm.replace(/-latest$/, "");
|
|
432
|
+
if (stripLatest !== norm) variants.add(stripLatest);
|
|
433
|
+
const stripPreview = norm.replace(/-preview$/, "").replace(/-exp$/, "");
|
|
434
|
+
if (stripPreview !== norm) variants.add(stripPreview);
|
|
435
|
+
return Array.from(variants);
|
|
436
|
+
}
|
|
437
|
+
var PRICES_NORMALIZED = (() => {
|
|
438
|
+
const map = {};
|
|
439
|
+
for (const [k, v] of Object.entries(MODEL_PRICES)) {
|
|
440
|
+
map[normalizeId(k)] = v;
|
|
441
|
+
}
|
|
442
|
+
return map;
|
|
443
|
+
})();
|
|
444
|
+
function getModelPrice(modelId) {
|
|
445
|
+
if (MODEL_PRICES[modelId]) return MODEL_PRICES[modelId];
|
|
446
|
+
for (const candidate of candidateIds(modelId)) {
|
|
447
|
+
const direct = PRICES_NORMALIZED[candidate];
|
|
448
|
+
if (direct) return direct;
|
|
449
|
+
}
|
|
450
|
+
const norm = normalizeId(modelId);
|
|
451
|
+
let bestMatch = null;
|
|
452
|
+
for (const [key, price] of Object.entries(PRICES_NORMALIZED)) {
|
|
453
|
+
if (norm.startsWith(key) || key.startsWith(norm)) {
|
|
454
|
+
if (!bestMatch || key.length > bestMatch.key.length) {
|
|
455
|
+
bestMatch = { key, price };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return bestMatch?.price ?? null;
|
|
460
|
+
}
|
|
461
|
+
function computeCost(modelId, usage) {
|
|
462
|
+
const price = getModelPrice(modelId);
|
|
463
|
+
if (!price) {
|
|
464
|
+
console.warn(`[corsair:pricing] no price entry for model "${modelId}"`);
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
const input = usage.inputTokens / 1e6 * price.input;
|
|
468
|
+
const output = usage.outputTokens / 1e6 * price.output;
|
|
469
|
+
return input + output;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/server/chat-store.ts
|
|
473
|
+
var sqlite = new Database(":memory:");
|
|
474
|
+
var db = new Kysely({
|
|
475
|
+
dialect: new SqliteDialect({ database: sqlite })
|
|
476
|
+
});
|
|
477
|
+
var schemaReady = initSchema();
|
|
478
|
+
async function initSchema() {
|
|
479
|
+
await db.schema.createTable("chats").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("title", "text", (col) => col.notNull()).addColumn("created_at", "integer", (col) => col.notNull()).execute();
|
|
480
|
+
await db.schema.createTable("chat_messages").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("chat_id", "text", (col) => col.notNull()).addColumn("role", "text", (col) => col.notNull()).addColumn("blocks", "text", (col) => col.notNull()).addColumn("error", "text").addColumn("seq", "integer", (col) => col.notNull()).addColumn("usage", "text").execute();
|
|
481
|
+
}
|
|
482
|
+
var _seq = 0;
|
|
483
|
+
function getTextFromBlocks(blocks) {
|
|
484
|
+
return blocks.filter(
|
|
485
|
+
(block) => block.type === "text"
|
|
486
|
+
).map((block) => block.content).join("");
|
|
487
|
+
}
|
|
488
|
+
function parseRawUsage(raw) {
|
|
489
|
+
if (!raw) return null;
|
|
490
|
+
try {
|
|
491
|
+
const parsed = JSON.parse(raw);
|
|
492
|
+
if (typeof parsed.model !== "string" || typeof parsed.inputTokens !== "number" || typeof parsed.outputTokens !== "number" || typeof parsed.totalTokens !== "number") {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
model: parsed.model,
|
|
497
|
+
inputTokens: parsed.inputTokens,
|
|
498
|
+
outputTokens: parsed.outputTokens,
|
|
499
|
+
totalTokens: parsed.totalTokens
|
|
500
|
+
};
|
|
501
|
+
} catch {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function withCost(raw) {
|
|
506
|
+
return {
|
|
507
|
+
...raw,
|
|
508
|
+
cost: computeCost(raw.model, raw)
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function aggregateUsage(rows) {
|
|
512
|
+
const total = {
|
|
513
|
+
inputTokens: 0,
|
|
514
|
+
outputTokens: 0,
|
|
515
|
+
totalTokens: 0,
|
|
516
|
+
cost: 0,
|
|
517
|
+
hasUnknownCost: false
|
|
518
|
+
};
|
|
519
|
+
for (const u of rows) {
|
|
520
|
+
if (!u) continue;
|
|
521
|
+
total.inputTokens += u.inputTokens;
|
|
522
|
+
total.outputTokens += u.outputTokens;
|
|
523
|
+
total.totalTokens += u.totalTokens;
|
|
524
|
+
if (u.cost === null) {
|
|
525
|
+
total.hasUnknownCost = true;
|
|
526
|
+
} else {
|
|
527
|
+
total.cost += u.cost;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return total;
|
|
531
|
+
}
|
|
532
|
+
async function createChat() {
|
|
533
|
+
await schemaReady;
|
|
534
|
+
const id = crypto.randomUUID();
|
|
535
|
+
const created_at = Date.now();
|
|
536
|
+
const title = "New chat";
|
|
537
|
+
await db.insertInto("chats").values({ id, title, created_at }).execute();
|
|
538
|
+
return {
|
|
539
|
+
id,
|
|
540
|
+
title,
|
|
541
|
+
created_at,
|
|
542
|
+
usage_total: aggregateUsage([])
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
async function listChats() {
|
|
546
|
+
await schemaReady;
|
|
547
|
+
const chats = await db.selectFrom("chats").select(["id", "title", "created_at"]).orderBy("created_at", "desc").execute();
|
|
548
|
+
const messageRows = await db.selectFrom("chat_messages").select(["chat_id", "usage"]).execute();
|
|
549
|
+
const usageByChat = /* @__PURE__ */ new Map();
|
|
550
|
+
for (const row of messageRows) {
|
|
551
|
+
const raw = parseRawUsage(row.usage);
|
|
552
|
+
if (!raw) continue;
|
|
553
|
+
const list = usageByChat.get(row.chat_id) ?? [];
|
|
554
|
+
list.push(withCost(raw));
|
|
555
|
+
usageByChat.set(row.chat_id, list);
|
|
556
|
+
}
|
|
557
|
+
return chats.map((chat) => ({
|
|
558
|
+
id: chat.id,
|
|
559
|
+
title: chat.title,
|
|
560
|
+
created_at: chat.created_at,
|
|
561
|
+
usage_total: aggregateUsage(usageByChat.get(chat.id) ?? [])
|
|
562
|
+
}));
|
|
563
|
+
}
|
|
564
|
+
async function chatExists(chatId) {
|
|
565
|
+
await schemaReady;
|
|
566
|
+
const row = await db.selectFrom("chats").select("id").where("id", "=", chatId).executeTakeFirst();
|
|
567
|
+
return !!row;
|
|
568
|
+
}
|
|
569
|
+
async function getMessages(chatId) {
|
|
570
|
+
await schemaReady;
|
|
571
|
+
const rows = await db.selectFrom("chat_messages").select(["id", "chat_id", "role", "blocks", "error", "usage"]).where("chat_id", "=", chatId).orderBy("seq", "asc").execute();
|
|
572
|
+
return rows.map((row) => {
|
|
573
|
+
const raw = parseRawUsage(row.usage);
|
|
574
|
+
return {
|
|
575
|
+
id: row.id,
|
|
576
|
+
chat_id: row.chat_id,
|
|
577
|
+
role: row.role,
|
|
578
|
+
blocks: JSON.parse(row.blocks),
|
|
579
|
+
error: row.error,
|
|
580
|
+
usage: raw ? withCost(raw) : null
|
|
581
|
+
};
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
async function appendMessage(chatId, id, role, blocks, options = {}) {
|
|
585
|
+
await schemaReady;
|
|
586
|
+
await db.insertInto("chat_messages").values({
|
|
587
|
+
id,
|
|
588
|
+
chat_id: chatId,
|
|
589
|
+
role,
|
|
590
|
+
blocks: JSON.stringify(blocks),
|
|
591
|
+
error: options.error ?? null,
|
|
592
|
+
seq: ++_seq,
|
|
593
|
+
usage: options.usage ? JSON.stringify(options.usage) : null
|
|
594
|
+
}).execute();
|
|
595
|
+
if (role === "user") {
|
|
596
|
+
const row = await db.selectFrom("chats").select("title").where("id", "=", chatId).executeTakeFirst();
|
|
597
|
+
if (row?.title === "New chat") {
|
|
598
|
+
const text = getTextFromBlocks(blocks);
|
|
599
|
+
const newTitle = text.slice(0, 60).trim();
|
|
600
|
+
if (newTitle) {
|
|
601
|
+
await db.updateTable("chats").set({ title: newTitle }).where("id", "=", chatId).execute();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/server/handlers/chat.ts
|
|
608
|
+
function isTextBlock(block) {
|
|
609
|
+
return block.type === "text";
|
|
610
|
+
}
|
|
611
|
+
function isCompletedToolBlock(block) {
|
|
612
|
+
return block.type === "tool" && block.result !== void 0;
|
|
613
|
+
}
|
|
614
|
+
function isPendingToolBlock(block) {
|
|
615
|
+
return block.type === "tool" && block.result === void 0;
|
|
616
|
+
}
|
|
617
|
+
function joinTextBlocks(blocks) {
|
|
618
|
+
return blocks.map((block) => block.content).join("");
|
|
619
|
+
}
|
|
620
|
+
function toToolArgs(args) {
|
|
621
|
+
if (args && typeof args === "object") {
|
|
622
|
+
const toolArgs = {};
|
|
623
|
+
for (const [key, value] of Object.entries(args)) {
|
|
624
|
+
toolArgs[key] = value;
|
|
625
|
+
}
|
|
626
|
+
return toolArgs;
|
|
627
|
+
}
|
|
628
|
+
return {};
|
|
629
|
+
}
|
|
630
|
+
function makeTextPart(block) {
|
|
631
|
+
return { type: "text", text: block.content };
|
|
632
|
+
}
|
|
633
|
+
function makeToolCallPart(toolCallId, block) {
|
|
634
|
+
return {
|
|
635
|
+
type: "tool-call",
|
|
636
|
+
toolCallId,
|
|
637
|
+
toolName: block.name,
|
|
638
|
+
args: toToolArgs(block.args)
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function makeToolResultPart(toolCallId, block) {
|
|
642
|
+
return {
|
|
643
|
+
type: "tool-result",
|
|
644
|
+
toolCallId,
|
|
645
|
+
toolName: block.name,
|
|
646
|
+
result: block.result
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function findPendingToolBlockIndex(blocks, toolName) {
|
|
650
|
+
return blocks.findLastIndex(
|
|
651
|
+
(block) => isPendingToolBlock(block) && block.name === toolName
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
function toAiMessages(stored) {
|
|
655
|
+
const result = [];
|
|
656
|
+
for (const msg of stored) {
|
|
657
|
+
const textBlocks = msg.blocks.filter(isTextBlock);
|
|
658
|
+
const text = joinTextBlocks(textBlocks);
|
|
659
|
+
if (msg.role === "user") {
|
|
660
|
+
result.push({ role: "user", content: text });
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const toolBlocks = msg.blocks.filter(isCompletedToolBlock);
|
|
664
|
+
if (toolBlocks.length === 0) {
|
|
665
|
+
result.push({ role: "assistant", content: text });
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
const toolCallIds = toolBlocks.map(() => crypto.randomUUID());
|
|
669
|
+
const assistantParts = textBlocks.map(makeTextPart);
|
|
670
|
+
const toolParts = [];
|
|
671
|
+
for (const [index, block] of toolBlocks.entries()) {
|
|
672
|
+
const toolCallId = toolCallIds[index];
|
|
673
|
+
if (!toolCallId) {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
assistantParts.push(makeToolCallPart(toolCallId, block));
|
|
677
|
+
toolParts.push(makeToolResultPart(toolCallId, block));
|
|
678
|
+
}
|
|
679
|
+
result.push({ role: "assistant", content: assistantParts });
|
|
680
|
+
result.push({ role: "tool", content: toolParts });
|
|
681
|
+
}
|
|
682
|
+
return result;
|
|
683
|
+
}
|
|
684
|
+
function buildAiTools(corsairClient) {
|
|
685
|
+
const defs = buildCorsairToolDefs({ corsair: corsairClient, setup: false });
|
|
686
|
+
const tools = {};
|
|
687
|
+
for (const def of defs) {
|
|
688
|
+
tools[def.name] = tool({
|
|
689
|
+
description: def.description,
|
|
690
|
+
parameters: z.object(def.shape),
|
|
691
|
+
execute: async (args) => {
|
|
692
|
+
const result = await def.handler(args);
|
|
693
|
+
const texts = result.content.filter((c) => c.type === "text");
|
|
694
|
+
if (result.isError) {
|
|
695
|
+
throw new Error(texts.map((c) => c.text).join("\n"));
|
|
696
|
+
}
|
|
697
|
+
const text = texts[0]?.text ?? "";
|
|
698
|
+
try {
|
|
699
|
+
return JSON.parse(text);
|
|
700
|
+
} catch {
|
|
701
|
+
return text;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
return tools;
|
|
707
|
+
}
|
|
708
|
+
async function resolveModel() {
|
|
709
|
+
const override = process.env.CORSAIR_CHAT_MODEL;
|
|
710
|
+
console.log(
|
|
711
|
+
"[corsair:chat] resolving model \u2014 OPENAI_API_KEY:",
|
|
712
|
+
!!process.env.OPENAI_API_KEY,
|
|
713
|
+
"| ANTHROPIC_API_KEY:",
|
|
714
|
+
!!process.env.ANTHROPIC_API_KEY,
|
|
715
|
+
"| CORSAIR_CHAT_MODEL:",
|
|
716
|
+
override ?? "(default)"
|
|
717
|
+
);
|
|
718
|
+
if (process.env.OPENAI_API_KEY) {
|
|
719
|
+
const { openai } = await import("@ai-sdk/openai");
|
|
720
|
+
const modelId = override ?? "gpt-4o-mini";
|
|
721
|
+
console.log("[corsair:chat] using openai:", modelId);
|
|
722
|
+
return { model: openai(modelId), modelId };
|
|
723
|
+
}
|
|
724
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
725
|
+
const { anthropic } = await import("@ai-sdk/anthropic");
|
|
726
|
+
const modelId = override ?? "claude-sonnet-4-6";
|
|
727
|
+
console.log("[corsair:chat] using anthropic:", modelId);
|
|
728
|
+
return { model: anthropic(modelId), modelId };
|
|
729
|
+
}
|
|
730
|
+
if (process.env.GOOGLE_GENERATIVE_AI_API_KEY) {
|
|
731
|
+
const { google } = await import("@ai-sdk/google");
|
|
732
|
+
const modelId = override ?? "gemini-2.0-flash";
|
|
733
|
+
return { model: google(modelId), modelId };
|
|
734
|
+
}
|
|
735
|
+
if (process.env.GROQ_API_KEY) {
|
|
736
|
+
const { createGroq } = await import("@ai-sdk/groq");
|
|
737
|
+
const modelId = override ?? "llama-3.3-70b-versatile";
|
|
738
|
+
return { model: createGroq()(modelId), modelId };
|
|
739
|
+
}
|
|
740
|
+
throw new Error(
|
|
741
|
+
"No AI provider configured. Set one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, or GROQ_API_KEY."
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
function errorMessage(err) {
|
|
745
|
+
return err instanceof Error ? err.message : String(err);
|
|
746
|
+
}
|
|
747
|
+
function isObject(value) {
|
|
748
|
+
return typeof value === "object" && value !== null;
|
|
749
|
+
}
|
|
750
|
+
function parseRawUsage2(raw) {
|
|
751
|
+
if (!isObject(raw)) return void 0;
|
|
752
|
+
const usage = {};
|
|
753
|
+
if (typeof raw.promptTokens === "number")
|
|
754
|
+
usage.promptTokens = raw.promptTokens;
|
|
755
|
+
if (typeof raw.completionTokens === "number")
|
|
756
|
+
usage.completionTokens = raw.completionTokens;
|
|
757
|
+
if (typeof raw.totalTokens === "number") usage.totalTokens = raw.totalTokens;
|
|
758
|
+
return usage;
|
|
759
|
+
}
|
|
760
|
+
function parseStreamPart(raw) {
|
|
761
|
+
if (!isObject(raw) || typeof raw.type !== "string") {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
const textDelta = typeof raw.textDelta === "string" ? raw.textDelta : void 0;
|
|
765
|
+
const toolName = typeof raw.toolName === "string" ? raw.toolName : void 0;
|
|
766
|
+
let responseModelId;
|
|
767
|
+
if (isObject(raw.response) && typeof raw.response.modelId === "string") {
|
|
768
|
+
responseModelId = raw.response.modelId;
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
type: raw.type,
|
|
772
|
+
textDelta,
|
|
773
|
+
toolName,
|
|
774
|
+
args: raw.args,
|
|
775
|
+
result: raw.result,
|
|
776
|
+
usage: parseRawUsage2(raw.usage),
|
|
777
|
+
responseModelId
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function buildRawMessageUsage(modelId, raw) {
|
|
781
|
+
if (!raw) return null;
|
|
782
|
+
const inputTokens = raw.promptTokens ?? 0;
|
|
783
|
+
const outputTokens = raw.completionTokens ?? 0;
|
|
784
|
+
const totalTokens = raw.totalTokens ?? inputTokens + outputTokens;
|
|
785
|
+
if (inputTokens === 0 && outputTokens === 0 && totalTokens === 0) return null;
|
|
786
|
+
return { model: modelId, inputTokens, outputTokens, totalTokens };
|
|
787
|
+
}
|
|
788
|
+
var chatHandler = async (ctx) => {
|
|
789
|
+
console.log("[corsair:chat] request received");
|
|
790
|
+
const body = await readJsonBody(ctx.req);
|
|
791
|
+
const tenant = body.tenant ? String(body.tenant) : void 0;
|
|
792
|
+
const chatId = body.chatId ? String(body.chatId) : void 0;
|
|
793
|
+
const newUserText = body.message ? String(body.message) : void 0;
|
|
794
|
+
const storedHistory = chatId && await chatExists(chatId) ? await getMessages(chatId) : [];
|
|
795
|
+
const messages = toAiMessages(storedHistory);
|
|
796
|
+
if (newUserText) {
|
|
797
|
+
messages.push({ role: "user", content: newUserText });
|
|
798
|
+
}
|
|
799
|
+
console.log(
|
|
800
|
+
"[corsair:chat] history:",
|
|
801
|
+
storedHistory.length,
|
|
802
|
+
"stored msgs \u2192",
|
|
803
|
+
messages.length,
|
|
804
|
+
"AI msgs | tenant:",
|
|
805
|
+
tenant ?? "(none)",
|
|
806
|
+
"| chatId:",
|
|
807
|
+
chatId ?? "(none)"
|
|
808
|
+
);
|
|
809
|
+
let resolved;
|
|
810
|
+
try {
|
|
811
|
+
resolved = await resolveModel();
|
|
812
|
+
} catch (err) {
|
|
813
|
+
const message = errorMessage(err);
|
|
814
|
+
console.error("[corsair:chat] model resolution failed:", message);
|
|
815
|
+
ctx.res.writeHead(400, { "content-type": "application/json" });
|
|
816
|
+
ctx.res.end(JSON.stringify({ error: message }));
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const handle = await ctx.getCorsair();
|
|
820
|
+
const client = handle.resolveClient(tenant);
|
|
821
|
+
const tools = buildAiTools(client);
|
|
822
|
+
const userMsgId = crypto.randomUUID();
|
|
823
|
+
if (chatId && await chatExists(chatId) && newUserText) {
|
|
824
|
+
try {
|
|
825
|
+
await appendMessage(chatId, userMsgId, "user", [
|
|
826
|
+
{ type: "text", content: newUserText }
|
|
827
|
+
]);
|
|
828
|
+
} catch (err) {
|
|
829
|
+
console.error("[corsair:chat] failed to save user message:", err);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
ctx.res.writeHead(200, {
|
|
833
|
+
"content-type": "text/event-stream",
|
|
834
|
+
"cache-control": "no-cache",
|
|
835
|
+
connection: "keep-alive"
|
|
836
|
+
});
|
|
837
|
+
const send = (data) => {
|
|
838
|
+
ctx.res.write(`data: ${JSON.stringify(data)}
|
|
839
|
+
|
|
840
|
+
`);
|
|
841
|
+
};
|
|
842
|
+
const assistantBlocks = [];
|
|
843
|
+
let finalRawUsage = null;
|
|
844
|
+
console.log("[corsair:chat] starting stream");
|
|
845
|
+
try {
|
|
846
|
+
const result = streamText({
|
|
847
|
+
model: resolved.model,
|
|
848
|
+
system: "You are a helpful assistant with access to Corsair tools. Use the tools to answer questions about the user's integrations and data. Use list_operations to get the exact endpoint names.",
|
|
849
|
+
messages,
|
|
850
|
+
tools,
|
|
851
|
+
maxSteps: 10
|
|
852
|
+
});
|
|
853
|
+
for await (const raw of result.fullStream) {
|
|
854
|
+
const part = parseStreamPart(raw);
|
|
855
|
+
if (!part) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (part.type === "text-delta") {
|
|
859
|
+
send({ type: "text", text: part.textDelta });
|
|
860
|
+
const last = assistantBlocks[assistantBlocks.length - 1];
|
|
861
|
+
if (last?.type === "text") {
|
|
862
|
+
last.content += part.textDelta ?? "";
|
|
863
|
+
} else {
|
|
864
|
+
assistantBlocks.push({ type: "text", content: part.textDelta ?? "" });
|
|
865
|
+
}
|
|
866
|
+
} else if (part.type === "tool-call") {
|
|
867
|
+
send({ type: "tool-start", name: part.toolName, args: part.args });
|
|
868
|
+
assistantBlocks.push({
|
|
869
|
+
type: "tool",
|
|
870
|
+
name: part.toolName ?? "",
|
|
871
|
+
args: part.args
|
|
872
|
+
});
|
|
873
|
+
} else if (part.type === "tool-result") {
|
|
874
|
+
send({ type: "tool-end", name: part.toolName, result: part.result });
|
|
875
|
+
const idx = findPendingToolBlockIndex(assistantBlocks, part.toolName);
|
|
876
|
+
const block = idx >= 0 ? assistantBlocks[idx] : void 0;
|
|
877
|
+
if (block && isPendingToolBlock(block)) {
|
|
878
|
+
block.result = part.result;
|
|
879
|
+
}
|
|
880
|
+
} else if (part.type === "finish") {
|
|
881
|
+
const effectiveModelId = part.responseModelId ?? resolved.modelId;
|
|
882
|
+
const raw2 = buildRawMessageUsage(effectiveModelId, part.usage);
|
|
883
|
+
console.log(
|
|
884
|
+
"[corsair:chat] finish \u2014 model:",
|
|
885
|
+
effectiveModelId,
|
|
886
|
+
"| usage:",
|
|
887
|
+
part.usage
|
|
888
|
+
);
|
|
889
|
+
if (raw2) {
|
|
890
|
+
finalRawUsage = raw2;
|
|
891
|
+
send({ type: "usage", usage: withCost(raw2) });
|
|
892
|
+
}
|
|
893
|
+
send({ type: "done" });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
} catch (err) {
|
|
897
|
+
const message = errorMessage(err);
|
|
898
|
+
console.error("[corsair:chat] stream error:", message);
|
|
899
|
+
send({ type: "error", message });
|
|
900
|
+
} finally {
|
|
901
|
+
if (chatId && await chatExists(chatId) && assistantBlocks.length > 0) {
|
|
902
|
+
try {
|
|
903
|
+
await appendMessage(
|
|
904
|
+
chatId,
|
|
905
|
+
crypto.randomUUID(),
|
|
906
|
+
"assistant",
|
|
907
|
+
assistantBlocks,
|
|
908
|
+
{ usage: finalRawUsage ?? void 0 }
|
|
909
|
+
);
|
|
910
|
+
} catch (err) {
|
|
911
|
+
console.error("[corsair:chat] failed to save assistant message:", err);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
console.log("[corsair:chat] done");
|
|
915
|
+
ctx.res.end();
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// src/server/handlers/chats.ts
|
|
920
|
+
var listChatsHandler = async () => {
|
|
921
|
+
return { chats: await listChats() };
|
|
922
|
+
};
|
|
923
|
+
var createChatHandler = async () => {
|
|
924
|
+
return { chat: await createChat() };
|
|
925
|
+
};
|
|
926
|
+
var getChatMessagesHandler = async (ctx) => {
|
|
927
|
+
const chatId = ctx.url.searchParams.get("chatId");
|
|
928
|
+
if (!chatId) throw new Error("chatId is required");
|
|
929
|
+
return { messages: await getMessages(chatId) };
|
|
930
|
+
};
|
|
931
|
+
|
|
344
932
|
// src/server/handlers/credentials-internal.ts
|
|
345
933
|
var BASE_FIELDS = {
|
|
346
934
|
oauth_2: {
|
|
@@ -533,9 +1121,9 @@ function getKyselyDb(database) {
|
|
|
533
1121
|
var listDbTables = async (ctx) => {
|
|
534
1122
|
const { internal } = await ctx.getCorsair();
|
|
535
1123
|
if (!internal.database) throw new Error("No database configured.");
|
|
536
|
-
const
|
|
537
|
-
if (!
|
|
538
|
-
const existing = await
|
|
1124
|
+
const db2 = getKyselyDb(internal.database);
|
|
1125
|
+
if (!db2) throw new Error("Could not access kysely db handle.");
|
|
1126
|
+
const existing = await db2.introspection.getTables();
|
|
539
1127
|
const existingNames = new Set(existing.map((t) => t.name));
|
|
540
1128
|
return {
|
|
541
1129
|
core: CORE_TABLES.filter((t) => existingNames.has(t)),
|
|
@@ -551,14 +1139,14 @@ var listDbRows = async (ctx) => {
|
|
|
551
1139
|
if (!table) throw new Error("Missing table.");
|
|
552
1140
|
const { internal } = await ctx.getCorsair();
|
|
553
1141
|
if (!internal.database) throw new Error("No database configured.");
|
|
554
|
-
const
|
|
555
|
-
if (!
|
|
1142
|
+
const db2 = getKyselyDb(internal.database);
|
|
1143
|
+
if (!db2) throw new Error("Could not access kysely db handle.");
|
|
556
1144
|
let rows;
|
|
557
1145
|
try {
|
|
558
|
-
const q =
|
|
1146
|
+
const q = db2.selectFrom(table).selectAll().limit(limit).offset(offset);
|
|
559
1147
|
rows = await q.orderBy("created_at", "desc").execute();
|
|
560
1148
|
} catch {
|
|
561
|
-
rows = await
|
|
1149
|
+
rows = await db2.selectFrom(table).selectAll().limit(limit).offset(offset).execute();
|
|
562
1150
|
}
|
|
563
1151
|
const safeRows = rows.map(redactSensitive);
|
|
564
1152
|
return { rows: safeRows, limit, offset };
|
|
@@ -580,9 +1168,9 @@ var queryEntityData = async (ctx) => {
|
|
|
580
1168
|
if (!entity) throw new Error("Missing entity.");
|
|
581
1169
|
const { internal } = await ctx.getCorsair();
|
|
582
1170
|
if (!internal.database) throw new Error("No database configured.");
|
|
583
|
-
const
|
|
584
|
-
if (!
|
|
585
|
-
const base =
|
|
1171
|
+
const db2 = getKyselyDb(internal.database);
|
|
1172
|
+
if (!db2) throw new Error("Could not access kysely db handle.");
|
|
1173
|
+
const base = db2.selectFrom("corsair_entities as e").innerJoin("corsair_accounts as a", "a.id", "e.account_id").innerJoin("corsair_integrations as i", "i.id", "a.integration_id").where("a.tenant_id", "=", tenant).where("i.name", "=", integration).where("e.entity_type", "=", entity);
|
|
586
1174
|
let rowsRaw = [];
|
|
587
1175
|
let hasMore = false;
|
|
588
1176
|
let total = 0;
|
|
@@ -682,14 +1270,14 @@ function collectPrimitiveValues(value) {
|
|
|
682
1270
|
var listPermissions = async (ctx) => {
|
|
683
1271
|
const { internal } = await ctx.getCorsair();
|
|
684
1272
|
if (!internal.database) throw new Error("No database configured.");
|
|
685
|
-
const
|
|
686
|
-
if (!
|
|
1273
|
+
const db2 = getKyselyDb(internal.database);
|
|
1274
|
+
if (!db2) throw new Error("Could not access kysely db handle.");
|
|
687
1275
|
const limit = Math.min(
|
|
688
1276
|
Math.max(Number(ctx.url.searchParams.get("limit") ?? 100), 1),
|
|
689
1277
|
500
|
|
690
1278
|
);
|
|
691
1279
|
try {
|
|
692
|
-
const rows = await
|
|
1280
|
+
const rows = await db2.selectFrom("corsair_permissions").selectAll().limit(limit).offset(0).orderBy("created_at", "desc").execute();
|
|
693
1281
|
return { rows };
|
|
694
1282
|
} catch (err) {
|
|
695
1283
|
return { rows: [], note: err.message };
|
|
@@ -809,19 +1397,19 @@ var setupPlugin = async (ctx) => {
|
|
|
809
1397
|
if (!hasAuthConfig(internal, pluginId)) {
|
|
810
1398
|
throw new Error(`Plugin '${pluginId}' has no credential setup.`);
|
|
811
1399
|
}
|
|
812
|
-
const
|
|
1400
|
+
const db2 = asDb(internal.database);
|
|
813
1401
|
const now = /* @__PURE__ */ new Date();
|
|
814
|
-
let integration = await
|
|
1402
|
+
let integration = await db2.selectFrom("corsair_integrations").selectAll().where("name", "=", pluginId).executeTakeFirst();
|
|
815
1403
|
if (!integration) {
|
|
816
1404
|
const id = randomUUID();
|
|
817
|
-
await
|
|
1405
|
+
await db2.insertInto("corsair_integrations").values({
|
|
818
1406
|
id,
|
|
819
1407
|
name: pluginId,
|
|
820
1408
|
config: {},
|
|
821
1409
|
created_at: now,
|
|
822
1410
|
updated_at: now
|
|
823
1411
|
}).execute();
|
|
824
|
-
integration = await
|
|
1412
|
+
integration = await db2.selectFrom("corsair_integrations").selectAll().where("id", "=", id).executeTakeFirst();
|
|
825
1413
|
}
|
|
826
1414
|
if (!integration) {
|
|
827
1415
|
throw new Error(`Failed to create integration for '${pluginId}'.`);
|
|
@@ -830,9 +1418,9 @@ var setupPlugin = async (ctx) => {
|
|
|
830
1418
|
if (typeof integrationId !== "string") {
|
|
831
1419
|
throw new Error(`Invalid integration row for '${pluginId}'.`);
|
|
832
1420
|
}
|
|
833
|
-
let account = await
|
|
1421
|
+
let account = await db2.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).where("integration_id", "=", integrationId).executeTakeFirst();
|
|
834
1422
|
if (!account) {
|
|
835
|
-
await
|
|
1423
|
+
await db2.insertInto("corsair_accounts").values({
|
|
836
1424
|
id: randomUUID(),
|
|
837
1425
|
tenant_id: tenantId,
|
|
838
1426
|
integration_id: integrationId,
|
|
@@ -840,7 +1428,7 @@ var setupPlugin = async (ctx) => {
|
|
|
840
1428
|
created_at: now,
|
|
841
1429
|
updated_at: now
|
|
842
1430
|
}).execute();
|
|
843
|
-
account = await
|
|
1431
|
+
account = await db2.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).where("integration_id", "=", integrationId).executeTakeFirst();
|
|
844
1432
|
}
|
|
845
1433
|
const rootKeys = instance.keys ?? null;
|
|
846
1434
|
const integrationNamespace = rootKeys?.[pluginId] ?? null;
|
|
@@ -948,8 +1536,8 @@ var listTenants = async (ctx) => {
|
|
|
948
1536
|
return { tenants: ["default"] };
|
|
949
1537
|
}
|
|
950
1538
|
if (!internal.database) throw new Error("No database configured.");
|
|
951
|
-
const
|
|
952
|
-
const rows = await
|
|
1539
|
+
const db2 = asDb2(internal.database);
|
|
1540
|
+
const rows = await db2.selectFrom("corsair_accounts").selectAll().execute();
|
|
953
1541
|
const ids = /* @__PURE__ */ new Set(["default"]);
|
|
954
1542
|
for (const row of rows) {
|
|
955
1543
|
const tenantId = row.tenant_id;
|
|
@@ -973,13 +1561,13 @@ var createTenant = async (ctx) => {
|
|
|
973
1561
|
throw new Error("Multi-tenancy is not enabled for this Corsair instance.");
|
|
974
1562
|
}
|
|
975
1563
|
if (!internal.database) throw new Error("No database configured.");
|
|
976
|
-
const
|
|
1564
|
+
const db2 = asDb2(internal.database);
|
|
977
1565
|
const now = /* @__PURE__ */ new Date();
|
|
978
1566
|
const authPluginIds = getAuthPluginIds(internal);
|
|
979
1567
|
if (authPluginIds.length === 0) {
|
|
980
1568
|
return { ok: true, created: false };
|
|
981
1569
|
}
|
|
982
|
-
const integrations = await
|
|
1570
|
+
const integrations = await db2.selectFrom("corsair_integrations").selectAll().execute();
|
|
983
1571
|
const integrationByName = /* @__PURE__ */ new Map();
|
|
984
1572
|
for (const row of integrations) {
|
|
985
1573
|
const name = row.name;
|
|
@@ -995,10 +1583,10 @@ var createTenant = async (ctx) => {
|
|
|
995
1583
|
created_at: now,
|
|
996
1584
|
updated_at: now
|
|
997
1585
|
};
|
|
998
|
-
await
|
|
1586
|
+
await db2.insertInto("corsair_integrations").values(row).execute();
|
|
999
1587
|
integrationByName.set(pluginId, row);
|
|
1000
1588
|
}
|
|
1001
|
-
const existingAccounts = await
|
|
1589
|
+
const existingAccounts = await db2.selectFrom("corsair_accounts").selectAll().where("tenant_id", "=", tenantId).execute();
|
|
1002
1590
|
const accountByIntegrationId = /* @__PURE__ */ new Map();
|
|
1003
1591
|
for (const row of existingAccounts) {
|
|
1004
1592
|
const integrationId = row.integration_id;
|
|
@@ -1016,7 +1604,7 @@ var createTenant = async (ctx) => {
|
|
|
1016
1604
|
if (!existing) {
|
|
1017
1605
|
createdAny = true;
|
|
1018
1606
|
pluginsMissingDek.add(pluginId);
|
|
1019
|
-
await
|
|
1607
|
+
await db2.insertInto("corsair_accounts").values({
|
|
1020
1608
|
id: randomUUID2(),
|
|
1021
1609
|
tenant_id: tenantId,
|
|
1022
1610
|
integration_id: integrationId,
|
|
@@ -1066,7 +1654,15 @@ var routes = [
|
|
|
1066
1654
|
{ method: "GET", path: "/api/db/tables", handler: listDbTables },
|
|
1067
1655
|
{ method: "POST", path: "/api/db/rows", handler: listDbRows },
|
|
1068
1656
|
{ method: "POST", path: "/api/db/entities/query", handler: queryEntityData },
|
|
1069
|
-
{ method: "GET", path: "/api/db/permissions", handler: listPermissions }
|
|
1657
|
+
{ method: "GET", path: "/api/db/permissions", handler: listPermissions },
|
|
1658
|
+
{ method: "GET", path: "/api/chats", handler: listChatsHandler },
|
|
1659
|
+
{ method: "POST", path: "/api/chats", handler: createChatHandler },
|
|
1660
|
+
{
|
|
1661
|
+
method: "GET",
|
|
1662
|
+
path: "/api/chats/messages",
|
|
1663
|
+
handler: getChatMessagesHandler
|
|
1664
|
+
},
|
|
1665
|
+
{ method: "POST", path: "/api/chat", handler: chatHandler }
|
|
1070
1666
|
];
|
|
1071
1667
|
async function handleApi(req, res, baseCtx) {
|
|
1072
1668
|
const url = new URL(req.url ?? "/", "http://localhost");
|