iris-chatbot 5.3.1 → 5.5.0
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/package.json +1 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/src/app/api/chat/route.ts +204 -15
- package/template/src/app/api/chat/uploads/route.ts +220 -0
- package/template/src/app/globals.css +174 -3
- package/template/src/components/ChatView.tsx +349 -12
- package/template/src/components/Composer.tsx +93 -3
- package/template/src/components/MessageCard.tsx +204 -49
- package/template/src/lib/attachments.ts +222 -0
- package/template/src/lib/data.ts +6 -2
- package/template/src/lib/types.ts +11 -0
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iris",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "iris",
|
|
9
|
-
"version": "5.
|
|
9
|
+
"version": "5.5.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.72.1",
|
|
12
12
|
"clsx": "^2.1.1",
|
package/template/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
2
|
import Anthropic from "@anthropic-ai/sdk";
|
|
3
|
+
import type { BetaContentBlockParam, BetaMessageParam } from "@anthropic-ai/sdk/resources/beta";
|
|
3
4
|
import type {
|
|
5
|
+
ChatAttachment,
|
|
4
6
|
ChatConnectionPayload,
|
|
5
7
|
ChatMessageInput,
|
|
6
8
|
MemoryContextPayload,
|
|
@@ -11,6 +13,10 @@ import type {
|
|
|
11
13
|
} from "../../../lib/types";
|
|
12
14
|
import { DEFAULT_LOCAL_TOOLS_SETTINGS } from "../../../lib/types";
|
|
13
15
|
import { GEMINI_OPENAI_BASE_URL, supportsToolsByDefault } from "../../../lib/connections";
|
|
16
|
+
import {
|
|
17
|
+
getAttachmentProviderForConnection,
|
|
18
|
+
normalizeChatAttachments,
|
|
19
|
+
} from "../../../lib/attachments";
|
|
14
20
|
import { createApprovalRequest } from "../../../lib/tooling/approvals";
|
|
15
21
|
import {
|
|
16
22
|
createToolRegistry,
|
|
@@ -514,17 +520,59 @@ function sanitizeChatMessages(messages: ChatMessageInput[]): ChatMessageInput[]
|
|
|
514
520
|
continue;
|
|
515
521
|
}
|
|
516
522
|
const normalizedContent = message.content.replace(/\r\n/g, "\n");
|
|
517
|
-
|
|
523
|
+
const normalizedAttachments =
|
|
524
|
+
role === "user" ? normalizeChatAttachments(message.attachments) : [];
|
|
525
|
+
if (!normalizedContent.trim() && normalizedAttachments.length === 0) {
|
|
518
526
|
continue;
|
|
519
527
|
}
|
|
520
528
|
sanitized.push({
|
|
521
529
|
role,
|
|
522
530
|
content: normalizedContent,
|
|
531
|
+
...(normalizedAttachments.length > 0
|
|
532
|
+
? { attachments: normalizedAttachments }
|
|
533
|
+
: {}),
|
|
523
534
|
});
|
|
524
535
|
}
|
|
525
536
|
return sanitized;
|
|
526
537
|
}
|
|
527
538
|
|
|
539
|
+
function messageHasAttachments(message: ChatMessageInput): boolean {
|
|
540
|
+
return Array.isArray(message.attachments) && message.attachments.length > 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function messagesContainAttachments(messages: ChatMessageInput[]): boolean {
|
|
544
|
+
return messages.some((message) => messageHasAttachments(message));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function validateAttachmentProviderCompatibility(params: {
|
|
548
|
+
connection: RuntimeConnection;
|
|
549
|
+
messages: ChatMessageInput[];
|
|
550
|
+
}): string | null {
|
|
551
|
+
const providerFromConnection = getAttachmentProviderForConnection(params.connection);
|
|
552
|
+
const providers = new Set<ChatAttachment["provider"]>();
|
|
553
|
+
for (const message of params.messages) {
|
|
554
|
+
for (const attachment of message.attachments ?? []) {
|
|
555
|
+
providers.add(attachment.provider);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (providers.size === 0) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
if (providers.size > 1) {
|
|
563
|
+
return "Attachments from multiple providers cannot be mixed in one request.";
|
|
564
|
+
}
|
|
565
|
+
if (!providerFromConnection) {
|
|
566
|
+
return "This connection does not support attachments.";
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const providerFromAttachment = [...providers][0];
|
|
570
|
+
if (providerFromAttachment !== providerFromConnection) {
|
|
571
|
+
return `Attachments were uploaded for ${providerFromAttachment}, but the active connection is ${providerFromConnection}.`;
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
528
576
|
function sanitizeMemoryContext(
|
|
529
577
|
memory: ChatRequest["memory"],
|
|
530
578
|
): MemoryContextPayload | null {
|
|
@@ -2574,6 +2622,90 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
2574
2622
|
return true;
|
|
2575
2623
|
}
|
|
2576
2624
|
|
|
2625
|
+
function toOpenAIMessageContent(
|
|
2626
|
+
message: ChatMessageInput,
|
|
2627
|
+
): string | OpenAI.Responses.ResponseInputMessageContentList {
|
|
2628
|
+
if (message.role !== "user" || !messageHasAttachments(message)) {
|
|
2629
|
+
return message.content;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
const content: OpenAI.Responses.ResponseInputMessageContentList = [];
|
|
2633
|
+
if (message.content.trim()) {
|
|
2634
|
+
content.push({
|
|
2635
|
+
type: "input_text",
|
|
2636
|
+
text: message.content,
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
for (const attachment of message.attachments ?? []) {
|
|
2641
|
+
if (attachment.kind === "image") {
|
|
2642
|
+
content.push({
|
|
2643
|
+
type: "input_image",
|
|
2644
|
+
detail: "auto",
|
|
2645
|
+
file_id: attachment.providerFileId,
|
|
2646
|
+
});
|
|
2647
|
+
continue;
|
|
2648
|
+
}
|
|
2649
|
+
content.push({
|
|
2650
|
+
type: "input_file",
|
|
2651
|
+
file_id: attachment.providerFileId,
|
|
2652
|
+
filename: attachment.name,
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
if (content.length === 0) {
|
|
2657
|
+
content.push({
|
|
2658
|
+
type: "input_text",
|
|
2659
|
+
text: "",
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
return content;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
function toAnthropicUserContent(message: ChatMessageInput): string | BetaContentBlockParam[] {
|
|
2666
|
+
if (!messageHasAttachments(message)) {
|
|
2667
|
+
return message.content;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
const content: BetaContentBlockParam[] = [];
|
|
2671
|
+
if (message.content.trim()) {
|
|
2672
|
+
content.push({
|
|
2673
|
+
type: "text",
|
|
2674
|
+
text: message.content,
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
for (const attachment of message.attachments ?? []) {
|
|
2679
|
+
if (attachment.kind === "image") {
|
|
2680
|
+
content.push({
|
|
2681
|
+
type: "image",
|
|
2682
|
+
source: {
|
|
2683
|
+
type: "file",
|
|
2684
|
+
file_id: attachment.providerFileId,
|
|
2685
|
+
},
|
|
2686
|
+
});
|
|
2687
|
+
continue;
|
|
2688
|
+
}
|
|
2689
|
+
content.push({
|
|
2690
|
+
type: "document",
|
|
2691
|
+
title: attachment.name,
|
|
2692
|
+
citations: { enabled: true },
|
|
2693
|
+
source: {
|
|
2694
|
+
type: "file",
|
|
2695
|
+
file_id: attachment.providerFileId,
|
|
2696
|
+
},
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
if (content.length === 0) {
|
|
2701
|
+
content.push({
|
|
2702
|
+
type: "text",
|
|
2703
|
+
text: "",
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
return content;
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2577
2709
|
async function streamPlainChat(params: {
|
|
2578
2710
|
connection: RuntimeConnection;
|
|
2579
2711
|
model: string;
|
|
@@ -2600,19 +2732,65 @@ async function streamPlainChat(params: {
|
|
|
2600
2732
|
throw new Error("Missing Anthropic API key for this connection.");
|
|
2601
2733
|
}
|
|
2602
2734
|
const client = new Anthropic({ apiKey: params.connection.apiKey });
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2735
|
+
if (messagesContainAttachments(params.messages)) {
|
|
2736
|
+
const systemBlocks = [
|
|
2737
|
+
params.system?.trim() || "",
|
|
2738
|
+
...params.messages
|
|
2739
|
+
.filter((message) => message.role === "system")
|
|
2740
|
+
.map((message) => message.content.trim())
|
|
2741
|
+
.filter(Boolean),
|
|
2742
|
+
].filter(Boolean);
|
|
2743
|
+
const anthropicMessages: BetaMessageParam[] = params.messages
|
|
2744
|
+
.filter((message) => message.role !== "system")
|
|
2745
|
+
.map((message) => ({
|
|
2746
|
+
role: message.role === "assistant" ? "assistant" : "user",
|
|
2747
|
+
content:
|
|
2748
|
+
message.role === "assistant"
|
|
2749
|
+
? message.content
|
|
2750
|
+
: toAnthropicUserContent(message),
|
|
2751
|
+
}));
|
|
2752
|
+
|
|
2753
|
+
const response = await client.beta.messages.create({
|
|
2754
|
+
model: params.model,
|
|
2755
|
+
max_tokens: 1024,
|
|
2756
|
+
system: systemBlocks.length > 0 ? systemBlocks.join("\n\n") : undefined,
|
|
2757
|
+
messages: anthropicMessages,
|
|
2758
|
+
stream: true,
|
|
2759
|
+
betas: ["files-api-2025-04-14", "pdfs-2024-09-25"],
|
|
2760
|
+
});
|
|
2612
2761
|
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2762
|
+
for await (const event of response) {
|
|
2763
|
+
const eventAny = event as {
|
|
2764
|
+
type?: string;
|
|
2765
|
+
delta?: { type?: string; text?: string };
|
|
2766
|
+
error?: { message?: string };
|
|
2767
|
+
};
|
|
2768
|
+
if (eventAny.type === "content_block_delta" && eventAny.delta?.type === "text_delta") {
|
|
2769
|
+
emitTokenText(params.send, eventAny.delta.text ?? "");
|
|
2770
|
+
continue;
|
|
2771
|
+
}
|
|
2772
|
+
if (eventAny.type === "error") {
|
|
2773
|
+
params.send({
|
|
2774
|
+
type: "error",
|
|
2775
|
+
error: eventAny.error?.message || "Anthropic response error",
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
} else {
|
|
2780
|
+
const response = await client.messages.stream({
|
|
2781
|
+
model: params.model,
|
|
2782
|
+
max_tokens: 1024,
|
|
2783
|
+
system: params.system,
|
|
2784
|
+
messages: params.messages.map((message) => ({
|
|
2785
|
+
role: message.role === "assistant" ? "assistant" : "user",
|
|
2786
|
+
content: message.content,
|
|
2787
|
+
})),
|
|
2788
|
+
});
|
|
2789
|
+
|
|
2790
|
+
for await (const event of response) {
|
|
2791
|
+
if (event.type === "content_block_delta" && event.delta?.type === "text_delta") {
|
|
2792
|
+
emitTokenText(params.send, event.delta.text);
|
|
2793
|
+
}
|
|
2616
2794
|
}
|
|
2617
2795
|
}
|
|
2618
2796
|
return;
|
|
@@ -2644,7 +2822,7 @@ async function streamPlainChat(params: {
|
|
|
2644
2822
|
input: params.messages.map((message) => ({
|
|
2645
2823
|
type: "message",
|
|
2646
2824
|
role: message.role,
|
|
2647
|
-
content: message
|
|
2825
|
+
content: toOpenAIMessageContent(message),
|
|
2648
2826
|
})),
|
|
2649
2827
|
tools: webSearchMode !== "off" ? [{ type: "web_search" }] : undefined,
|
|
2650
2828
|
tool_choice:
|
|
@@ -3105,6 +3283,17 @@ export async function POST(request: Request) {
|
|
|
3105
3283
|
headers: { "Content-Type": "application/json" },
|
|
3106
3284
|
});
|
|
3107
3285
|
}
|
|
3286
|
+
const hasAttachmentsInMessages = messagesContainAttachments(sanitizedMessages);
|
|
3287
|
+
const attachmentCompatibilityError = validateAttachmentProviderCompatibility({
|
|
3288
|
+
connection,
|
|
3289
|
+
messages: sanitizedMessages,
|
|
3290
|
+
});
|
|
3291
|
+
if (attachmentCompatibilityError) {
|
|
3292
|
+
return new Response(JSON.stringify({ error: attachmentCompatibilityError }), {
|
|
3293
|
+
status: 400,
|
|
3294
|
+
headers: { "Content-Type": "application/json" },
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3108
3297
|
|
|
3109
3298
|
const webSourcesEnabled = normalizeWebSourcesEnabled(body.enableWebSources);
|
|
3110
3299
|
const webSearchMode = decideWebSearchMode({
|
|
@@ -3154,7 +3343,7 @@ export async function POST(request: Request) {
|
|
|
3154
3343
|
});
|
|
3155
3344
|
}
|
|
3156
3345
|
|
|
3157
|
-
if (localTools.enabled) {
|
|
3346
|
+
if (localTools.enabled && !hasAttachmentsInMessages) {
|
|
3158
3347
|
const lastUserText =
|
|
3159
3348
|
[...sanitizedMessages].reverse().find((message) => message.role === "user")?.content ?? "";
|
|
3160
3349
|
const communicationIntentType = classifyDraftCommunicationIntent(lastUserText);
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
3
|
+
import type { ChatAttachment, ChatConnectionPayload } from "../../../../lib/types";
|
|
4
|
+
import {
|
|
5
|
+
getAttachmentProviderForConnection,
|
|
6
|
+
MAX_ATTACHMENTS_PER_MESSAGE,
|
|
7
|
+
validateAttachmentCandidate,
|
|
8
|
+
} from "../../../../lib/attachments";
|
|
9
|
+
|
|
10
|
+
export const runtime = "nodejs";
|
|
11
|
+
export const dynamic = "force-dynamic";
|
|
12
|
+
|
|
13
|
+
type RuntimeConnection = {
|
|
14
|
+
kind: "builtin" | "openai_compatible" | "ollama";
|
|
15
|
+
provider?: "openai" | "anthropic" | "google";
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
headers: Array<{ key: string; value: string }>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function headersArrayToRecord(
|
|
22
|
+
headers: RuntimeConnection["headers"],
|
|
23
|
+
): Record<string, string> | undefined {
|
|
24
|
+
if (!headers.length) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const out: Record<string, string> = {};
|
|
28
|
+
for (const header of headers) {
|
|
29
|
+
const key = header.key.trim();
|
|
30
|
+
if (!key) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
out[key] = header.value;
|
|
34
|
+
}
|
|
35
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeConnection(rawConnection: unknown): RuntimeConnection | null {
|
|
39
|
+
if (!rawConnection || typeof rawConnection !== "object") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const connection = rawConnection as Partial<ChatConnectionPayload>;
|
|
43
|
+
const kind =
|
|
44
|
+
connection.kind === "builtin" ||
|
|
45
|
+
connection.kind === "openai_compatible" ||
|
|
46
|
+
connection.kind === "ollama"
|
|
47
|
+
? connection.kind
|
|
48
|
+
: null;
|
|
49
|
+
if (!kind) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const provider =
|
|
53
|
+
kind === "builtin" &&
|
|
54
|
+
(connection.provider === "openai" ||
|
|
55
|
+
connection.provider === "anthropic" ||
|
|
56
|
+
connection.provider === "google")
|
|
57
|
+
? connection.provider
|
|
58
|
+
: undefined;
|
|
59
|
+
const baseUrl =
|
|
60
|
+
typeof connection.baseUrl === "string" && connection.baseUrl.trim().length > 0
|
|
61
|
+
? connection.baseUrl.trim()
|
|
62
|
+
: undefined;
|
|
63
|
+
const apiKey =
|
|
64
|
+
typeof connection.apiKey === "string" && connection.apiKey.trim().length > 0
|
|
65
|
+
? connection.apiKey.trim()
|
|
66
|
+
: undefined;
|
|
67
|
+
const headers = Array.isArray(connection.headers)
|
|
68
|
+
? connection.headers
|
|
69
|
+
.filter(
|
|
70
|
+
(header): header is { key: string; value: string } =>
|
|
71
|
+
Boolean(header) &&
|
|
72
|
+
typeof header.key === "string" &&
|
|
73
|
+
typeof header.value === "string",
|
|
74
|
+
)
|
|
75
|
+
.map((header) => ({
|
|
76
|
+
key: header.key,
|
|
77
|
+
value: header.value,
|
|
78
|
+
}))
|
|
79
|
+
: [];
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
kind,
|
|
83
|
+
provider,
|
|
84
|
+
baseUrl,
|
|
85
|
+
apiKey,
|
|
86
|
+
headers,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function badRequest(message: string) {
|
|
91
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
92
|
+
status: 400,
|
|
93
|
+
headers: {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function POST(request: Request) {
|
|
100
|
+
let formData: FormData;
|
|
101
|
+
try {
|
|
102
|
+
formData = await request.formData();
|
|
103
|
+
} catch {
|
|
104
|
+
return badRequest("Invalid multipart form payload.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const rawConnection = formData.get("connection");
|
|
108
|
+
if (typeof rawConnection !== "string" || !rawConnection.trim()) {
|
|
109
|
+
return badRequest("Missing connection payload.");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let parsedConnectionValue: unknown;
|
|
113
|
+
try {
|
|
114
|
+
parsedConnectionValue = JSON.parse(rawConnection);
|
|
115
|
+
} catch {
|
|
116
|
+
return badRequest("Invalid connection payload.");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const connection = normalizeConnection(parsedConnectionValue);
|
|
120
|
+
if (!connection) {
|
|
121
|
+
return badRequest("Invalid connection payload.");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const providerFamily = getAttachmentProviderForConnection(connection);
|
|
125
|
+
if (!providerFamily) {
|
|
126
|
+
return badRequest("This connection does not support chat attachments.");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const files = formData.getAll("files").filter((entry): entry is File => entry instanceof File);
|
|
130
|
+
if (files.length === 0) {
|
|
131
|
+
return badRequest("No files were uploaded.");
|
|
132
|
+
}
|
|
133
|
+
if (files.length > MAX_ATTACHMENTS_PER_MESSAGE) {
|
|
134
|
+
return badRequest(`You can upload up to ${MAX_ATTACHMENTS_PER_MESSAGE} files per message.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const attachments: ChatAttachment[] = [];
|
|
138
|
+
|
|
139
|
+
if (providerFamily === "anthropic") {
|
|
140
|
+
if (!connection.apiKey) {
|
|
141
|
+
return badRequest("Missing Anthropic API key.");
|
|
142
|
+
}
|
|
143
|
+
const client = new Anthropic({
|
|
144
|
+
apiKey: connection.apiKey,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
const validation = validateAttachmentCandidate({
|
|
149
|
+
name: file.name,
|
|
150
|
+
mimeType: file.type,
|
|
151
|
+
sizeBytes: file.size,
|
|
152
|
+
});
|
|
153
|
+
if (!validation.ok) {
|
|
154
|
+
return badRequest(validation.error);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const uploadable = new File([await file.arrayBuffer()], validation.name, {
|
|
158
|
+
type: validation.mimeType,
|
|
159
|
+
});
|
|
160
|
+
const uploaded = await client.beta.files.upload({
|
|
161
|
+
file: uploadable,
|
|
162
|
+
betas: ["files-api-2025-04-14"],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
attachments.push({
|
|
166
|
+
provider: "anthropic",
|
|
167
|
+
providerFileId: uploaded.id,
|
|
168
|
+
kind: validation.kind,
|
|
169
|
+
name: validation.name,
|
|
170
|
+
mimeType: validation.mimeType,
|
|
171
|
+
sizeBytes: validation.sizeBytes,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
if (connection.kind === "openai_compatible" && !connection.baseUrl) {
|
|
176
|
+
return badRequest("OpenAI-compatible connection is missing base URL.");
|
|
177
|
+
}
|
|
178
|
+
if (connection.kind === "builtin" && connection.provider === "openai" && !connection.apiKey) {
|
|
179
|
+
return badRequest("Missing OpenAI API key.");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const client = new OpenAI({
|
|
183
|
+
apiKey: connection.apiKey ?? "no-key-required",
|
|
184
|
+
baseURL: connection.kind === "openai_compatible" ? connection.baseUrl : undefined,
|
|
185
|
+
defaultHeaders: headersArrayToRecord(connection.headers),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
for (const file of files) {
|
|
189
|
+
const validation = validateAttachmentCandidate({
|
|
190
|
+
name: file.name,
|
|
191
|
+
mimeType: file.type,
|
|
192
|
+
sizeBytes: file.size,
|
|
193
|
+
});
|
|
194
|
+
if (!validation.ok) {
|
|
195
|
+
return badRequest(validation.error);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const uploadable = await OpenAI.toFile(
|
|
199
|
+
await file.arrayBuffer(),
|
|
200
|
+
validation.name,
|
|
201
|
+
{ type: validation.mimeType },
|
|
202
|
+
);
|
|
203
|
+
const uploaded = await client.files.create({
|
|
204
|
+
file: uploadable,
|
|
205
|
+
purpose: "user_data",
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
attachments.push({
|
|
209
|
+
provider: "openai",
|
|
210
|
+
providerFileId: uploaded.id,
|
|
211
|
+
kind: validation.kind,
|
|
212
|
+
name: validation.name,
|
|
213
|
+
mimeType: validation.mimeType,
|
|
214
|
+
sizeBytes: validation.sizeBytes,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return Response.json({ attachments });
|
|
220
|
+
}
|