delegate-team 2.0.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.
@@ -0,0 +1,545 @@
1
+ // src/proxy/server.ts
2
+ import http from "http";
3
+ import https from "https";
4
+ import { readFileSync, existsSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join as join2 } from "path";
7
+ import { execSync } from "child_process";
8
+
9
+ // src/config/index.ts
10
+ import { join, resolve, dirname } from "path";
11
+ import { fileURLToPath } from "url";
12
+ var __filename = fileURLToPath(import.meta.url);
13
+ var __dirname = dirname(__filename);
14
+ var WORKSPACE_ROOT = resolve(__dirname, "..");
15
+ var DELEGATE_TEAM_PATH = join(WORKSPACE_ROOT, "delegate-team");
16
+ var VERTEX_CODER_PATH = join(WORKSPACE_ROOT, "vertex-coder");
17
+ var RELAY_SCRIPT = join(DELEGATE_TEAM_PATH, "scripts", "relay.mjs");
18
+ var ROUTER_SCRIPT = join(DELEGATE_TEAM_PATH, "scripts", "opencode-router.mjs");
19
+ var VERTEX_VENV_PYTHON = join(VERTEX_CODER_PATH, ".venv", "bin", "python3");
20
+ var VERTEX_DIRECT_SCRIPT = join(VERTEX_CODER_PATH, "vertex_direct_coder.py");
21
+ var VERTEX_INTERACTIVE_SCRIPT = join(VERTEX_CODER_PATH, "vertex_interactive_agent.py");
22
+
23
+ // src/proxy/server.ts
24
+ var FALLBACK_RING = {
25
+ gemini: ["openrouter", "openai"],
26
+ openrouter: ["gemini", "openai"],
27
+ openai: ["gemini", "openrouter"]
28
+ };
29
+ function redact(s) {
30
+ if (!s) return "";
31
+ return String(s).replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [REDACTED]").replace(/(api[_-]?key["']?\s*[:=]\s*["']?)[^"',\s]+/gi, "$1[REDACTED]");
32
+ }
33
+ function readEnvFile(filePath) {
34
+ if (!existsSync(filePath)) return {};
35
+ const content = readFileSync(filePath, "utf8");
36
+ const env = {};
37
+ content.split("\n").forEach((line) => {
38
+ const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
39
+ if (match) {
40
+ let key = match[1];
41
+ let value = match[2] || "";
42
+ if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
43
+ else if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
44
+ env[key] = value;
45
+ }
46
+ });
47
+ return env;
48
+ }
49
+ var geminiEnv = readEnvFile(join2(homedir(), ".gemini", ".env"));
50
+ var openrouterEnv = readEnvFile(join2(homedir(), ".openrouter", ".env"));
51
+ var workspaceEnv = readEnvFile(join2(WORKSPACE_ROOT, ".env"));
52
+ var cachedConfig = null;
53
+ function getLocalConfig() {
54
+ if (cachedConfig) return cachedConfig;
55
+ const configPath = join2(homedir(), ".config", "dt", "config.json");
56
+ if (existsSync(configPath)) {
57
+ try {
58
+ cachedConfig = JSON.parse(readFileSync(configPath, "utf8"));
59
+ return cachedConfig;
60
+ } catch (err) {
61
+ }
62
+ }
63
+ cachedConfig = {};
64
+ return cachedConfig;
65
+ }
66
+ var cachedGoogleProject = null;
67
+ function getGoogleProject() {
68
+ if (cachedGoogleProject) return cachedGoogleProject;
69
+ try {
70
+ const proj = execSync("gcloud config get-value project", { encoding: "utf8" }).trim();
71
+ if (proj && proj !== "(unset)") {
72
+ cachedGoogleProject = proj;
73
+ return proj;
74
+ }
75
+ } catch (err) {
76
+ }
77
+ const config = getLocalConfig();
78
+ if (config.project_id) {
79
+ cachedGoogleProject = config.project_id;
80
+ return cachedGoogleProject;
81
+ }
82
+ cachedGoogleProject = process.env.GOOGLE_CLOUD_PROJECT || "";
83
+ return cachedGoogleProject;
84
+ }
85
+ var keys = {
86
+ gemini: process.env.GEMINI_API_KEY || workspaceEnv.GEMINI_API_KEY || geminiEnv.GEMINI_API_KEY,
87
+ openrouter: process.env.OPENROUTER_API_KEY || workspaceEnv.OPENROUTER_API_KEY || openrouterEnv.OPENROUTER_API_KEY,
88
+ openai: process.env.OPENAI_API_KEY || workspaceEnv.OPENAI_API_KEY
89
+ };
90
+ var cachedGcloudToken = null;
91
+ var cachedTokenExpiry = 0;
92
+ function getGcloudToken() {
93
+ const now = Date.now();
94
+ if (cachedGcloudToken && now < cachedTokenExpiry) {
95
+ return cachedGcloudToken;
96
+ }
97
+ try {
98
+ const token = execSync("gcloud auth print-access-token", { encoding: "utf8" }).trim();
99
+ cachedGcloudToken = token;
100
+ cachedTokenExpiry = now + 5 * 60 * 1e3;
101
+ return token;
102
+ } catch (err) {
103
+ return null;
104
+ }
105
+ }
106
+ function makeRequestStream(urlStr, options, bodyData) {
107
+ return new Promise((resolve2, reject) => {
108
+ const req = https.request(urlStr, options, (res) => {
109
+ if (res.statusCode && res.statusCode >= 400) {
110
+ let errData = "";
111
+ res.on("data", (chunk) => errData += chunk);
112
+ res.on("end", () => {
113
+ reject({ status: res.statusCode, data: errData });
114
+ });
115
+ return;
116
+ }
117
+ resolve2({ status: res.statusCode, headers: res.headers, stream: res });
118
+ });
119
+ req.on("error", reject);
120
+ if (bodyData) req.write(bodyData);
121
+ req.end();
122
+ });
123
+ }
124
+ function anthropicToOpenAI(payload) {
125
+ const newPayload = { ...payload };
126
+ if (newPayload.system) {
127
+ newPayload.messages = [{ role: "system", content: newPayload.system }, ...newPayload.messages || []];
128
+ delete newPayload.system;
129
+ }
130
+ if (newPayload.tools) {
131
+ newPayload.tools = newPayload.tools.map((t) => ({
132
+ type: "function",
133
+ function: {
134
+ name: t.name,
135
+ description: t.description,
136
+ parameters: t.input_schema
137
+ }
138
+ }));
139
+ }
140
+ if (Array.isArray(newPayload.messages)) {
141
+ newPayload.messages = newPayload.messages.map((msg) => {
142
+ let content = msg.content;
143
+ if (Array.isArray(content)) {
144
+ let textContent = "";
145
+ let tool_calls = [];
146
+ for (const block of content) {
147
+ if (block.type === "text") textContent += block.text;
148
+ else if (block.type === "tool_use") {
149
+ tool_calls.push({
150
+ id: block.id,
151
+ type: "function",
152
+ function: {
153
+ name: block.name,
154
+ arguments: typeof block.input === "string" ? block.input : JSON.stringify(block.input)
155
+ }
156
+ });
157
+ } else if (block.type === "tool_result") {
158
+ return {
159
+ role: "tool",
160
+ tool_call_id: block.tool_use_id,
161
+ content: typeof block.content === "string" ? block.content : JSON.stringify(block.content)
162
+ };
163
+ }
164
+ }
165
+ if (msg.role === "assistant" && tool_calls.length > 0) {
166
+ return { role: msg.role, content: textContent || null, tool_calls };
167
+ }
168
+ content = textContent;
169
+ }
170
+ return { role: msg.role, content };
171
+ });
172
+ }
173
+ return newPayload;
174
+ }
175
+ function openAIToAnthropic(responseStr) {
176
+ try {
177
+ const data = JSON.parse(responseStr);
178
+ if (!data.choices) return responseStr;
179
+ let content = [];
180
+ const msg = data.choices[0]?.message;
181
+ if (msg?.content) {
182
+ content.push({ type: "text", text: msg.content });
183
+ }
184
+ if (msg?.tool_calls) {
185
+ for (const tc of msg.tool_calls) {
186
+ if (tc.type === "function") {
187
+ content.push({
188
+ type: "tool_use",
189
+ id: tc.id,
190
+ name: tc.function.name,
191
+ input: JSON.parse(tc.function.arguments || "{}")
192
+ });
193
+ }
194
+ }
195
+ }
196
+ let stop_reason = "end_turn";
197
+ const fr = data.choices[0]?.finish_reason;
198
+ if (fr === "tool_calls") stop_reason = "tool_use";
199
+ else if (fr === "length") stop_reason = "max_tokens";
200
+ else if (fr === "stop") stop_reason = "end_turn";
201
+ else if (fr) stop_reason = fr;
202
+ return JSON.stringify({
203
+ id: data.id || `msg_${Date.now()}`,
204
+ type: "message",
205
+ role: "assistant",
206
+ content,
207
+ model: data.model || "unknown",
208
+ stop_reason,
209
+ usage: {
210
+ input_tokens: data.usage?.prompt_tokens || 0,
211
+ output_tokens: data.usage?.completion_tokens || 0
212
+ }
213
+ });
214
+ } catch (e) {
215
+ return responseStr;
216
+ }
217
+ }
218
+ function mapModel(backend, originalModel, useVertex = false) {
219
+ const config = getLocalConfig();
220
+ const mapping = config.model_mapping || {};
221
+ if (!originalModel) {
222
+ return mapping.default || (backend === "gemini" ? useVertex ? "google/gemini-3.5-flash" : "gemini-3.5-flash" : "gpt-4o");
223
+ }
224
+ const modelLower = originalModel.toLowerCase();
225
+ if (mapping[modelLower]) {
226
+ return mapping[modelLower];
227
+ }
228
+ if (backend === "gemini") {
229
+ if (useVertex) {
230
+ if (modelLower.includes("pro") || modelLower.includes("sonnet") || modelLower.includes("opus")) {
231
+ return "google/gemini-3.1-pro-custom-tools";
232
+ }
233
+ return "google/gemini-3.5-flash";
234
+ } else {
235
+ if (modelLower.includes("pro") || modelLower.includes("sonnet") || modelLower.includes("opus")) {
236
+ return "gemini-3.1-pro-custom-tools";
237
+ }
238
+ return "gemini-3.5-flash";
239
+ }
240
+ }
241
+ if (backend === "openrouter") {
242
+ if (modelLower.includes("sonnet") || modelLower.includes("claude-3-5") || modelLower.includes("claude-3.5")) {
243
+ return "anthropic/claude-3.5-sonnet";
244
+ }
245
+ if (modelLower.includes("opus")) {
246
+ return "anthropic/claude-3-opus";
247
+ }
248
+ if (modelLower.includes("haiku")) {
249
+ return "anthropic/claude-3-5-haiku";
250
+ }
251
+ if (modelLower.includes("gemini") && modelLower.includes("pro")) {
252
+ return "google/gemini-3.1-pro-custom-tools";
253
+ }
254
+ if (modelLower.includes("gemini") && (modelLower.includes("flash") || modelLower.includes("lite"))) {
255
+ return "google/gemini-3.5-flash";
256
+ }
257
+ if (modelLower.includes("gpt-4") || modelLower.includes("gpt-4o")) {
258
+ return "openai/gpt-4o";
259
+ }
260
+ if (originalModel.includes("/")) return originalModel;
261
+ console.warn(`[PROXY] Warning: OpenRouter model "${originalModel}" missing provider slash. Keeping as-is, but it may fail.`);
262
+ return originalModel;
263
+ }
264
+ if (backend === "openai") {
265
+ if (modelLower.includes("gpt-4") || modelLower.includes("gpt-4o")) {
266
+ return "gpt-4o";
267
+ }
268
+ return originalModel;
269
+ }
270
+ return originalModel;
271
+ }
272
+ async function handleRequest(req, res) {
273
+ const origin = req.headers.origin || "";
274
+ const allowedOrigins = /* @__PURE__ */ new Set([
275
+ "http://localhost:3210",
276
+ "http://127.0.0.1:3210",
277
+ "http://localhost:3000",
278
+ "http://127.0.0.1:3000"
279
+ ]);
280
+ if (origin && allowedOrigins.has(origin)) {
281
+ res.setHeader("Access-Control-Allow-Origin", origin);
282
+ } else if (!origin) {
283
+ } else {
284
+ }
285
+ res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
286
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key");
287
+ if (req.method === "OPTIONS") {
288
+ res.writeHead(200);
289
+ return res.end();
290
+ }
291
+ const config = getLocalConfig();
292
+ const requiredToken = config.proxy_token || process.env.PROXY_TOKEN;
293
+ if (!requiredToken) {
294
+ res.writeHead(500);
295
+ return res.end("Proxy requires a configured proxy_token in ~/.config/dt/config.json or PROXY_TOKEN env var");
296
+ }
297
+ const authHeader = req.headers["authorization"]?.replace(/^Bearer\s+/i, "") || req.headers["x-api-key"];
298
+ if (authHeader !== requiredToken) {
299
+ res.writeHead(401);
300
+ return res.end("Unauthorized");
301
+ }
302
+ if (req.method !== "POST") {
303
+ res.writeHead(405);
304
+ return res.end("Method Not Allowed");
305
+ }
306
+ let body = "";
307
+ const MAX_BODY_BYTES = 2 * 1024 * 1024;
308
+ let size = 0;
309
+ for await (const chunk of req) {
310
+ size += chunk.length;
311
+ if (size > MAX_BODY_BYTES) {
312
+ res.writeHead(413);
313
+ return res.end("Payload Too Large");
314
+ }
315
+ body += chunk;
316
+ }
317
+ let payload;
318
+ try {
319
+ payload = JSON.parse(body);
320
+ } catch (e) {
321
+ res.writeHead(400);
322
+ return res.end("Bad Request");
323
+ }
324
+ const isAnthropic = req.url?.includes("anthropic") || req.headers["x-api-key"] || payload.system !== void 0;
325
+ const isStream = !!payload.stream;
326
+ let openAIPayload = { ...payload };
327
+ if (isAnthropic) {
328
+ openAIPayload = anthropicToOpenAI(payload);
329
+ }
330
+ if (!openAIPayload.max_tokens && !openAIPayload.max_completion_tokens) {
331
+ openAIPayload.max_tokens = 1024;
332
+ }
333
+ const clientKey = authHeader;
334
+ let primaryBackend = "gemini";
335
+ const hasGcloud = !!getGcloudToken();
336
+ const hasGeminiKey = !!keys.gemini;
337
+ if (!hasGeminiKey && !hasGcloud && keys.openrouter) primaryBackend = "openrouter";
338
+ else if (!hasGeminiKey && !hasGcloud && !keys.openrouter && keys.openai) primaryBackend = "openai";
339
+ const chain = [primaryBackend, ...FALLBACK_RING[primaryBackend] || []];
340
+ for (const backend of chain) {
341
+ let activeKey = null;
342
+ let useVertex = false;
343
+ if (backend === "gemini") {
344
+ if (keys.gemini) {
345
+ activeKey = keys.gemini;
346
+ useVertex = false;
347
+ } else {
348
+ const token = getGcloudToken();
349
+ if (token) {
350
+ activeKey = token;
351
+ useVertex = true;
352
+ }
353
+ }
354
+ } else {
355
+ activeKey = keys[backend] || clientKey;
356
+ }
357
+ if (!activeKey) continue;
358
+ try {
359
+ let urlStr, headers;
360
+ const resolvedModel = mapModel(backend, openAIPayload.model, useVertex);
361
+ const backendPayload = { ...openAIPayload, model: resolvedModel };
362
+ if (backend === "gemini") {
363
+ if (useVertex) {
364
+ urlStr = `https://aiplatform.googleapis.com/v1/projects/${getGoogleProject()}/locations/global/endpoints/openapi/chat/completions`;
365
+ headers = {
366
+ "Content-Type": "application/json",
367
+ "Authorization": `Bearer ${activeKey}`
368
+ };
369
+ } else {
370
+ urlStr = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions";
371
+ headers = {
372
+ "Content-Type": "application/json",
373
+ "Authorization": `Bearer ${activeKey}`
374
+ };
375
+ }
376
+ } else if (backend === "openrouter") {
377
+ urlStr = "https://openrouter.ai/api/v1/chat/completions";
378
+ headers = {
379
+ "Content-Type": "application/json",
380
+ "Authorization": `Bearer ${activeKey}`
381
+ };
382
+ } else if (backend === "openai") {
383
+ urlStr = "https://api.openai.com/v1/chat/completions";
384
+ headers = {
385
+ "Content-Type": "application/json",
386
+ "Authorization": `Bearer ${activeKey}`
387
+ };
388
+ } else {
389
+ continue;
390
+ }
391
+ console.log(`[PROXY] Attempting backend [${backend.toUpperCase()}] with model [${resolvedModel}] (Streaming: ${isStream}, Vertex: ${useVertex})`);
392
+ const response = await makeRequestStream(urlStr, { method: "POST", headers }, JSON.stringify(backendPayload));
393
+ if (isStream) {
394
+ res.writeHead(200, {
395
+ "Content-Type": "text/event-stream",
396
+ "Cache-Control": "no-cache",
397
+ "Connection": "keep-alive"
398
+ });
399
+ if (isAnthropic) {
400
+ res.write(`event: message_start
401
+ data: ${JSON.stringify({
402
+ type: "message_start",
403
+ message: {
404
+ id: `msg_${Date.now()}`,
405
+ type: "message",
406
+ role: "assistant",
407
+ content: [],
408
+ model: resolvedModel,
409
+ stop_reason: null,
410
+ stop_sequence: null,
411
+ usage: { input_tokens: 0, output_tokens: 0 }
412
+ }
413
+ })}
414
+
415
+ `);
416
+ res.write(`event: content_block_start
417
+ data: ${JSON.stringify({
418
+ type: "content_block_start",
419
+ index: 0,
420
+ content_block: { type: "text", text: "" }
421
+ })}
422
+
423
+ `);
424
+ }
425
+ let sseBuffer = "";
426
+ for await (const chunk of response.stream) {
427
+ sseBuffer += chunk.toString("utf8");
428
+ let lineEnd = sseBuffer.indexOf("\n");
429
+ while (lineEnd !== -1) {
430
+ const line = sseBuffer.slice(0, lineEnd).trim();
431
+ sseBuffer = sseBuffer.slice(lineEnd + 1);
432
+ if (line.startsWith("data: ")) {
433
+ const dataContent = line.slice(6).trim();
434
+ if (dataContent === "[DONE]") {
435
+ if (isAnthropic) {
436
+ res.write(`event: content_block_stop
437
+ data: ${JSON.stringify({ type: "content_block_stop", index: 0 })}
438
+
439
+ `);
440
+ res.write(`event: message_delta
441
+ data: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: 0 } })}
442
+
443
+ `);
444
+ res.write(`event: message_stop
445
+ data: ${JSON.stringify({ type: "message_stop" })}
446
+
447
+ `);
448
+ } else {
449
+ res.write("data: [DONE]\n\n");
450
+ }
451
+ } else {
452
+ try {
453
+ const parsed = JSON.parse(dataContent);
454
+ if (isAnthropic) {
455
+ const delta = parsed.choices?.[0]?.delta || {};
456
+ if (delta.content) {
457
+ res.write(`event: content_block_delta
458
+ data: ${JSON.stringify({
459
+ type: "content_block_delta",
460
+ index: 0,
461
+ delta: { type: "text_delta", text: delta.content }
462
+ })}
463
+
464
+ `);
465
+ }
466
+ if (delta.tool_calls && delta.tool_calls.length > 0) {
467
+ for (const tc of delta.tool_calls) {
468
+ const tcIndex = (tc.index || 0) + 1;
469
+ if (tc.function?.name) {
470
+ res.write(`event: content_block_start
471
+ data: ${JSON.stringify({
472
+ type: "content_block_start",
473
+ index: tcIndex,
474
+ content_block: { type: "tool_use", id: tc.id || "call_" + Date.now(), name: tc.function.name, input: {} }
475
+ })}
476
+
477
+ `);
478
+ }
479
+ if (tc.function?.arguments) {
480
+ res.write(`event: content_block_delta
481
+ data: ${JSON.stringify({
482
+ type: "content_block_delta",
483
+ index: tcIndex,
484
+ delta: { type: "input_json_delta", partial_json: tc.function.arguments }
485
+ })}
486
+
487
+ `);
488
+ }
489
+ }
490
+ }
491
+ } else {
492
+ res.write(`data: ${JSON.stringify(parsed)}
493
+
494
+ `);
495
+ }
496
+ } catch (e) {
497
+ if (!isAnthropic) res.write(`data: ${dataContent}
498
+
499
+ `);
500
+ }
501
+ }
502
+ }
503
+ lineEnd = sseBuffer.indexOf("\n");
504
+ }
505
+ }
506
+ res.end();
507
+ return;
508
+ } else {
509
+ let fullData = "";
510
+ for await (const chunk of response.stream) {
511
+ fullData += chunk.toString("utf8");
512
+ }
513
+ let responseData = fullData;
514
+ if (isAnthropic) {
515
+ responseData = openAIToAnthropic(responseData);
516
+ }
517
+ res.writeHead(response.status, { "Content-Type": "application/json" });
518
+ return res.end(responseData);
519
+ }
520
+ } catch (err) {
521
+ const errStr = typeof err === "object" ? JSON.stringify(err) : String(err);
522
+ console.error(`[PROXY] Backend [${backend.toUpperCase()}] failed or returned error:`, redact(err.status || err.message || errStr), redact(err.data || ""));
523
+ }
524
+ }
525
+ res.writeHead(502, { "Content-Type": "application/json" });
526
+ res.end(JSON.stringify({ error: "All backends failed in Fallback Ring" }));
527
+ }
528
+ function runServe(port) {
529
+ const server = http.createServer(handleRequest);
530
+ server.listen(port, "127.0.0.1", () => {
531
+ console.log(`
532
+ \u{1F680} Starting LLM Gateway Proxy Server on http://127.0.0.1:${port}...`);
533
+ });
534
+ }
535
+
536
+ export {
537
+ DELEGATE_TEAM_PATH,
538
+ VERTEX_CODER_PATH,
539
+ RELAY_SCRIPT,
540
+ ROUTER_SCRIPT,
541
+ VERTEX_VENV_PYTHON,
542
+ VERTEX_DIRECT_SCRIPT,
543
+ VERTEX_INTERACTIVE_SCRIPT,
544
+ runServe
545
+ };