@radaros/transport 0.1.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/dist/index.d.ts +82 -0
- package/dist/index.js +728 -0
- package/package.json +47 -0
- package/src/express/file-upload.ts +88 -0
- package/src/express/middleware.ts +15 -0
- package/src/express/router-factory.ts +208 -0
- package/src/express/swagger.ts +424 -0
- package/src/express/types.ts +32 -0
- package/src/index.ts +9 -0
- package/src/socketio/gateway.ts +75 -0
- package/src/socketio/handlers.ts +1 -0
- package/src/socketio/types.ts +9 -0
- package/tsconfig.json +11 -0
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@radaros/transport",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
15
|
+
"dev": "tsup src/index.ts --format esm --dts --watch"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^25.3.1",
|
|
19
|
+
"tsup": "^8.0.0",
|
|
20
|
+
"typescript": "^5.6.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@radaros/core": "^0.1.0",
|
|
24
|
+
"@types/express": "^4.0.0 || ^5.0.0",
|
|
25
|
+
"express": "^4.0.0 || ^5.0.0",
|
|
26
|
+
"multer": ">=1.4.0",
|
|
27
|
+
"socket.io": "^4.0.0",
|
|
28
|
+
"swagger-ui-express": "^5.0.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"express": {
|
|
32
|
+
"optional": true
|
|
33
|
+
},
|
|
34
|
+
"socket.io": {
|
|
35
|
+
"optional": true
|
|
36
|
+
},
|
|
37
|
+
"@types/express": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"multer": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"swagger-ui-express": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const _require = createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
const MIME_TO_PART_TYPE: Record<string, "image" | "audio" | "file"> = {
|
|
6
|
+
"image/png": "image",
|
|
7
|
+
"image/jpeg": "image",
|
|
8
|
+
"image/jpg": "image",
|
|
9
|
+
"image/gif": "image",
|
|
10
|
+
"image/webp": "image",
|
|
11
|
+
"audio/mpeg": "audio",
|
|
12
|
+
"audio/mp3": "audio",
|
|
13
|
+
"audio/wav": "audio",
|
|
14
|
+
"audio/ogg": "audio",
|
|
15
|
+
"audio/webm": "audio",
|
|
16
|
+
"audio/flac": "audio",
|
|
17
|
+
"audio/aac": "audio",
|
|
18
|
+
"audio/mp4": "audio",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getPartType(mimeType: string): "image" | "audio" | "file" {
|
|
22
|
+
return MIME_TO_PART_TYPE[mimeType] ?? "file";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FileUploadOptions {
|
|
26
|
+
maxFileSize?: number;
|
|
27
|
+
maxFiles?: number;
|
|
28
|
+
allowedMimeTypes?: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createFileUploadMiddleware(opts: FileUploadOptions = {}) {
|
|
32
|
+
let multer: any;
|
|
33
|
+
try {
|
|
34
|
+
multer = _require("multer");
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"multer is required for file uploads. Install it: npm install multer"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const storage = multer.memoryStorage();
|
|
42
|
+
const upload = multer({
|
|
43
|
+
storage,
|
|
44
|
+
limits: {
|
|
45
|
+
fileSize: opts.maxFileSize ?? 50 * 1024 * 1024,
|
|
46
|
+
files: opts.maxFiles ?? 10,
|
|
47
|
+
},
|
|
48
|
+
fileFilter: opts.allowedMimeTypes
|
|
49
|
+
? (_req: any, file: any, cb: any) => {
|
|
50
|
+
if (opts.allowedMimeTypes!.includes(file.mimetype)) {
|
|
51
|
+
cb(null, true);
|
|
52
|
+
} else {
|
|
53
|
+
cb(new Error(`File type ${file.mimetype} is not allowed`));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
: undefined,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return upload.array("files", opts.maxFiles ?? 10);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function filesToContentParts(files: any[]): any[] {
|
|
63
|
+
return files.map((file) => {
|
|
64
|
+
const base64 = file.buffer.toString("base64");
|
|
65
|
+
const partType = getPartType(file.mimetype);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
type: partType,
|
|
69
|
+
data: base64,
|
|
70
|
+
mimeType: file.mimetype,
|
|
71
|
+
...(partType === "file" ? { fileName: file.originalname } : {}),
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildMultiModalInput(body: any, files?: any[]): string | any[] {
|
|
77
|
+
const textInput = body?.input;
|
|
78
|
+
if (!files || files.length === 0) {
|
|
79
|
+
return textInput;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parts: any[] = [];
|
|
83
|
+
if (textInput) {
|
|
84
|
+
parts.push({ type: "text", text: textInput });
|
|
85
|
+
}
|
|
86
|
+
parts.push(...filesToContentParts(files));
|
|
87
|
+
return parts;
|
|
88
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function errorHandler() {
|
|
2
|
+
return (err: any, _req: any, res: any, _next: any) => {
|
|
3
|
+
console.error("[radaros:transport] Error:", err.message);
|
|
4
|
+
res.status(err.statusCode ?? 500).json({
|
|
5
|
+
error: err.message ?? "Internal server error",
|
|
6
|
+
});
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function requestLogger() {
|
|
11
|
+
return (req: any, _res: any, next: any) => {
|
|
12
|
+
console.log(`[radaros:transport] ${req.method} ${req.path}`);
|
|
13
|
+
next();
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import type { RouterOptions } from "./types.js";
|
|
3
|
+
import { generateOpenAPISpec, serveSwaggerUI } from "./swagger.js";
|
|
4
|
+
import { createFileUploadMiddleware, buildMultiModalInput } from "./file-upload.js";
|
|
5
|
+
|
|
6
|
+
const _require = createRequire(import.meta.url);
|
|
7
|
+
|
|
8
|
+
const API_KEY_HEADERS: Record<string, string> = {
|
|
9
|
+
"x-openai-api-key": "openai",
|
|
10
|
+
"x-google-api-key": "google",
|
|
11
|
+
"x-anthropic-api-key": "anthropic",
|
|
12
|
+
"x-api-key": "_generic",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function extractApiKey(req: any, agent: any): string | undefined {
|
|
16
|
+
for (const [header, provider] of Object.entries(API_KEY_HEADERS)) {
|
|
17
|
+
const value = req.headers[header];
|
|
18
|
+
if (value && (provider === "_generic" || provider === agent.providerId)) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return req.body?.apiKey ?? undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createAgentRouter(opts: RouterOptions) {
|
|
26
|
+
let express: any;
|
|
27
|
+
try {
|
|
28
|
+
express = _require("express");
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(
|
|
31
|
+
"express is required for createAgentRouter. Install it: npm install express"
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const router = express.Router();
|
|
36
|
+
|
|
37
|
+
if (opts.middleware) {
|
|
38
|
+
for (const mw of opts.middleware) {
|
|
39
|
+
router.use(mw);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── File upload middleware (lazy-initialized) ───────────────────────────
|
|
44
|
+
let uploadMiddleware: any = null;
|
|
45
|
+
if (opts.fileUpload) {
|
|
46
|
+
const uploadOpts = typeof opts.fileUpload === "object" ? opts.fileUpload : {};
|
|
47
|
+
uploadMiddleware = createFileUploadMiddleware(uploadOpts);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function withUpload(handler: (req: any, res: any) => Promise<void>) {
|
|
51
|
+
if (!uploadMiddleware) return handler;
|
|
52
|
+
return (req: any, res: any, next: any) => {
|
|
53
|
+
uploadMiddleware(req, res, (err: any) => {
|
|
54
|
+
if (err) {
|
|
55
|
+
return res.status(400).json({ error: err.message });
|
|
56
|
+
}
|
|
57
|
+
handler(req, res).catch(next);
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Swagger UI ──────────────────────────────────────────────────────────
|
|
63
|
+
if (opts.swagger?.enabled) {
|
|
64
|
+
const spec = generateOpenAPISpec(opts, opts.swagger);
|
|
65
|
+
const docsPath = opts.swagger.docsPath ?? "/docs";
|
|
66
|
+
const specPath = opts.swagger.specPath ?? "/docs/spec.json";
|
|
67
|
+
|
|
68
|
+
router.get(specPath, (_req: any, res: any) => {
|
|
69
|
+
res.json(spec);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const { serve, setup } = serveSwaggerUI(spec);
|
|
74
|
+
router.use(docsPath, serve, setup);
|
|
75
|
+
} catch (e: any) {
|
|
76
|
+
console.warn(`[radaros:transport] Swagger UI disabled: ${e.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Agent endpoints ─────────────────────────────────────────────────────
|
|
81
|
+
if (opts.agents) {
|
|
82
|
+
for (const [name, agent] of Object.entries(opts.agents)) {
|
|
83
|
+
router.post(
|
|
84
|
+
`/agents/${name}/run`,
|
|
85
|
+
withUpload(async (req: any, res: any) => {
|
|
86
|
+
try {
|
|
87
|
+
const input = buildMultiModalInput(req.body, req.files);
|
|
88
|
+
if (!input) {
|
|
89
|
+
return res.status(400).json({ error: "input is required" });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { sessionId, userId } = req.body ?? {};
|
|
93
|
+
const apiKey = extractApiKey(req, agent);
|
|
94
|
+
const result = await agent.run(input, { sessionId, userId, apiKey });
|
|
95
|
+
res.json(result);
|
|
96
|
+
} catch (error: any) {
|
|
97
|
+
res.status(500).json({ error: error.message });
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
router.post(`/agents/${name}/stream`, async (req: any, res: any) => {
|
|
103
|
+
try {
|
|
104
|
+
const { input, sessionId, userId } = req.body ?? {};
|
|
105
|
+
if (!input) {
|
|
106
|
+
return res.status(400).json({ error: "input is required" });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const apiKey = extractApiKey(req, agent);
|
|
110
|
+
|
|
111
|
+
res.writeHead(200, {
|
|
112
|
+
"Content-Type": "text/event-stream",
|
|
113
|
+
"Cache-Control": "no-cache",
|
|
114
|
+
Connection: "keep-alive",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const stream = agent.stream(input, { sessionId, userId, apiKey });
|
|
118
|
+
for await (const chunk of stream) {
|
|
119
|
+
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
res.write("data: [DONE]\n\n");
|
|
123
|
+
res.end();
|
|
124
|
+
} catch (error: any) {
|
|
125
|
+
if (!res.headersSent) {
|
|
126
|
+
res.status(500).json({ error: error.message });
|
|
127
|
+
} else {
|
|
128
|
+
res.write(
|
|
129
|
+
`data: ${JSON.stringify({ type: "error", error: error.message })}\n\n`
|
|
130
|
+
);
|
|
131
|
+
res.end();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Team endpoints ──────────────────────────────────────────────────────
|
|
139
|
+
if (opts.teams) {
|
|
140
|
+
for (const [name, team] of Object.entries(opts.teams)) {
|
|
141
|
+
router.post(`/teams/${name}/run`, async (req: any, res: any) => {
|
|
142
|
+
try {
|
|
143
|
+
const { input, sessionId, userId } = req.body ?? {};
|
|
144
|
+
if (!input) {
|
|
145
|
+
return res.status(400).json({ error: "input is required" });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const apiKey = req.headers["x-api-key"] ?? req.body?.apiKey;
|
|
149
|
+
const result = await team.run(input, { sessionId, userId, apiKey });
|
|
150
|
+
res.json(result);
|
|
151
|
+
} catch (error: any) {
|
|
152
|
+
res.status(500).json({ error: error.message });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
router.post(`/teams/${name}/stream`, async (req: any, res: any) => {
|
|
157
|
+
try {
|
|
158
|
+
const { input, sessionId, userId } = req.body ?? {};
|
|
159
|
+
if (!input) {
|
|
160
|
+
return res.status(400).json({ error: "input is required" });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const apiKey = req.headers["x-api-key"] ?? req.body?.apiKey;
|
|
164
|
+
|
|
165
|
+
res.writeHead(200, {
|
|
166
|
+
"Content-Type": "text/event-stream",
|
|
167
|
+
"Cache-Control": "no-cache",
|
|
168
|
+
Connection: "keep-alive",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const stream = team.stream(input, { sessionId, userId, apiKey });
|
|
172
|
+
for await (const chunk of stream) {
|
|
173
|
+
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
res.write("data: [DONE]\n\n");
|
|
177
|
+
res.end();
|
|
178
|
+
} catch (error: any) {
|
|
179
|
+
if (!res.headersSent) {
|
|
180
|
+
res.status(500).json({ error: error.message });
|
|
181
|
+
} else {
|
|
182
|
+
res.write(
|
|
183
|
+
`data: ${JSON.stringify({ type: "error", error: error.message })}\n\n`
|
|
184
|
+
);
|
|
185
|
+
res.end();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Workflow endpoints ──────────────────────────────────────────────────
|
|
193
|
+
if (opts.workflows) {
|
|
194
|
+
for (const [name, workflow] of Object.entries(opts.workflows)) {
|
|
195
|
+
router.post(`/workflows/${name}/run`, async (req: any, res: any) => {
|
|
196
|
+
try {
|
|
197
|
+
const { sessionId, userId } = req.body ?? {};
|
|
198
|
+
const result = await workflow.run({ sessionId, userId });
|
|
199
|
+
res.json(result);
|
|
200
|
+
} catch (error: any) {
|
|
201
|
+
res.status(500).json({ error: error.message });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return router;
|
|
208
|
+
}
|