create-langgraph-app 1.2.0 → 1.4.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/README.md +1 -1
- package/dist/create.js +270 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ npx create-langgraph-app
|
|
|
11
11
|
## What it does
|
|
12
12
|
|
|
13
13
|
1. Asks for your project name
|
|
14
|
-
2. Lets you pick an LLM provider (OpenAI, Anthropic, Google, Groq, Ollama)
|
|
14
|
+
2. Lets you pick an LLM provider (OpenAI, Anthropic, Google, Groq, DeepSeek, Ollama)
|
|
15
15
|
3. Lets you select which agent patterns to include:
|
|
16
16
|
- **Supervisor** — central coordinator + worker agents
|
|
17
17
|
- **Swarm** — peer-to-peer agent handoffs
|
package/dist/create.js
CHANGED
|
@@ -52,6 +52,7 @@ const PROVIDERS = {
|
|
|
52
52
|
google: { envKey: "GOOGLE_API_KEY", defaultModel: "gemini-2.0-flash" },
|
|
53
53
|
groq: { envKey: "GROQ_API_KEY", defaultModel: "llama-3.3-70b-versatile" },
|
|
54
54
|
ollama: { envKey: "", defaultModel: "llama3.2" },
|
|
55
|
+
deepseek: { envKey: "DEEPSEEK_API_KEY", defaultModel: "deepseek-v4-flash" },
|
|
55
56
|
};
|
|
56
57
|
function generateEnv(config) {
|
|
57
58
|
const prov = PROVIDERS[config.provider];
|
|
@@ -67,7 +68,9 @@ function generateEnv(config) {
|
|
|
67
68
|
else {
|
|
68
69
|
lines.push(`# No API key needed for ${config.provider}`);
|
|
69
70
|
}
|
|
70
|
-
lines.push(``, `# Model (optional — defaults to ${prov.defaultModel})`, `# LLM_MODEL=${prov.defaultModel}`, `LLM_TEMPERATURE=0`, ``,
|
|
71
|
+
lines.push(``, `# Model (optional — defaults to ${prov.defaultModel})`, `# LLM_MODEL=${prov.defaultModel}`, `LLM_TEMPERATURE=0`, ``, ...(config.provider === "deepseek"
|
|
72
|
+
? [`# DeepSeek reasoning mode (optional — "disabled" | "enabled")`, `# DEEPSEEK_THINKING=disabled`, ``]
|
|
73
|
+
: []), `PORT=3000`, ``, `# LangSmith tracing (optional)`, `# LANGCHAIN_TRACING_V2=true`, `# LANGSMITH_API_KEY=`, `# LANGSMITH_PROJECT=${config.name}`);
|
|
71
74
|
return lines.join("\n") + "\n";
|
|
72
75
|
}
|
|
73
76
|
function generatePackageJson(config) {
|
|
@@ -90,6 +93,7 @@ function generatePackageJson(config) {
|
|
|
90
93
|
google: "@langchain/google-genai",
|
|
91
94
|
groq: "@langchain/groq",
|
|
92
95
|
ollama: "@langchain/ollama",
|
|
96
|
+
deepseek: "@langchain/deepseek",
|
|
93
97
|
};
|
|
94
98
|
deps[provPkg[config.provider]] = "latest";
|
|
95
99
|
const pkg = {
|
|
@@ -101,6 +105,7 @@ function generatePackageJson(config) {
|
|
|
101
105
|
dev: "tsx src/index.ts",
|
|
102
106
|
"dev:http": "tsx src/server.ts",
|
|
103
107
|
test: "vitest run",
|
|
108
|
+
"test:watch": "vitest",
|
|
104
109
|
typecheck: "tsc --noEmit",
|
|
105
110
|
},
|
|
106
111
|
dependencies: Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b))),
|
|
@@ -126,13 +131,13 @@ function generateTsConfig() {
|
|
|
126
131
|
types: ["node"],
|
|
127
132
|
outDir: "dist",
|
|
128
133
|
},
|
|
129
|
-
include: ["src"],
|
|
134
|
+
include: ["src", "tests"],
|
|
130
135
|
}, null, 2) + "\n";
|
|
131
136
|
}
|
|
132
137
|
function generateEnvConfig(config) {
|
|
133
138
|
return `import "dotenv/config";
|
|
134
139
|
|
|
135
|
-
const VALID_PROVIDERS = ["openai", "anthropic", "google", "groq", "ollama"] as const;
|
|
140
|
+
const VALID_PROVIDERS = ["openai", "anthropic", "google", "groq", "ollama", "deepseek"] as const;
|
|
136
141
|
export type LlmProvider = (typeof VALID_PROVIDERS)[number];
|
|
137
142
|
|
|
138
143
|
function resolveProvider(): LlmProvider {
|
|
@@ -149,6 +154,7 @@ const API_KEY_MAP: Record<LlmProvider, string> = {
|
|
|
149
154
|
google: "GOOGLE_API_KEY",
|
|
150
155
|
groq: "GROQ_API_KEY",
|
|
151
156
|
ollama: "",
|
|
157
|
+
deepseek: "DEEPSEEK_API_KEY",
|
|
152
158
|
};
|
|
153
159
|
|
|
154
160
|
export const LLM_PROVIDER = resolveProvider();
|
|
@@ -161,6 +167,9 @@ if (requiredKey && !process.env[requiredKey]) {
|
|
|
161
167
|
|
|
162
168
|
export const LLM_MODEL = process.env.LLM_MODEL || undefined;
|
|
163
169
|
export const LLM_TEMPERATURE = Number(process.env.LLM_TEMPERATURE ?? 0);
|
|
170
|
+
// Uses || rather than ?? so an empty DEEPSEEK_THINKING= in .env falls back
|
|
171
|
+
// to "disabled" instead of sending an invalid thinking type to the API.
|
|
172
|
+
export const DEEPSEEK_THINKING = process.env.DEEPSEEK_THINKING || "disabled";
|
|
164
173
|
export const PORT = Number(process.env.PORT ?? 3000);
|
|
165
174
|
`;
|
|
166
175
|
}
|
|
@@ -172,11 +181,15 @@ const PROVIDER_LLM = {
|
|
|
172
181
|
google: { pkg: "@langchain/google-genai", className: "ChatGoogleGenerativeAI", modelArg: "model", defaultModel: "gemini-2.0-flash" },
|
|
173
182
|
groq: { pkg: "@langchain/groq", className: "ChatGroq", modelArg: "model", defaultModel: "llama-3.3-70b-versatile" },
|
|
174
183
|
ollama: { pkg: "@langchain/ollama", className: "ChatOllama", modelArg: "model", defaultModel: "llama3.2" },
|
|
184
|
+
deepseek: { pkg: "@langchain/deepseek", className: "ChatDeepSeek", modelArg: "model", defaultModel: "deepseek-v4-flash", extraArgs: 'modelKwargs: { thinking: { type: DEEPSEEK_THINKING } }' },
|
|
175
185
|
};
|
|
176
186
|
function generateLlmConfig(config) {
|
|
177
187
|
const p = PROVIDER_LLM[config.provider] ?? PROVIDER_LLM.openai;
|
|
188
|
+
const envImports = config.provider === "deepseek"
|
|
189
|
+
? "LLM_MODEL, LLM_TEMPERATURE, DEEPSEEK_THINKING"
|
|
190
|
+
: "LLM_MODEL, LLM_TEMPERATURE";
|
|
178
191
|
return `import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
179
|
-
import {
|
|
192
|
+
import { ${envImports} } from "./env";
|
|
180
193
|
|
|
181
194
|
const DEFAULT_MODEL = "${p.defaultModel}";
|
|
182
195
|
|
|
@@ -190,7 +203,7 @@ async function createLlm(opts: LlmOptions = {}): Promise<BaseChatModel> {
|
|
|
190
203
|
const temperature = opts.temperature ?? LLM_TEMPERATURE;
|
|
191
204
|
|
|
192
205
|
const { ${p.className} } = await import("${p.pkg}");
|
|
193
|
-
return new ${p.className}({ ${p.modelArg}: model, temperature });
|
|
206
|
+
return new ${p.className}({ ${p.modelArg}: model, temperature${p.extraArgs ? `, ${p.extraArgs}` : ""} });
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
let _default: Promise<BaseChatModel> | undefined;
|
|
@@ -335,6 +348,252 @@ export function makeSupervisor({
|
|
|
335
348
|
}
|
|
336
349
|
`;
|
|
337
350
|
}
|
|
351
|
+
// Maps a selected pattern to the app it generates, so the server and demos
|
|
352
|
+
// only reference files that actually exist.
|
|
353
|
+
const PATTERN_APPS = {
|
|
354
|
+
supervisor: { route: "supervisor", fn: "createSupervisorApp", path: "./apps/supervisor" },
|
|
355
|
+
swarm: { route: "swarm", fn: "createSwarmApp", path: "./apps/swarm" },
|
|
356
|
+
hitl: { route: "interrupt", fn: "createInterruptApp", path: "./apps/interrupt" },
|
|
357
|
+
structured: { route: "analyst", fn: "createAnalystApp", path: "./apps/analyst" },
|
|
358
|
+
rag: { route: "rag", fn: "createRagApp", path: "./apps/rag" },
|
|
359
|
+
};
|
|
360
|
+
function selectedApps(config) {
|
|
361
|
+
return config.patterns.map((p) => PATTERN_APPS[p]).filter(Boolean);
|
|
362
|
+
}
|
|
363
|
+
function generateServer(config) {
|
|
364
|
+
const apps = selectedApps(config);
|
|
365
|
+
const imports = apps.map((a) => `import { ${a.fn} } from "${a.path}";`).join("\n");
|
|
366
|
+
const entries = apps.map((a) => ` "${a.route}": asApp(await ${a.fn}()),`).join("\n");
|
|
367
|
+
return `import "./config/env";
|
|
368
|
+
import { fastify, type FastifyError, type FastifyReply, type FastifyRequest } from "fastify";
|
|
369
|
+
import { Command } from "@langchain/langgraph";
|
|
370
|
+
import { AIMessageChunk, type BaseMessage } from "@langchain/core/messages";
|
|
371
|
+
import { PORT } from "./config/env";
|
|
372
|
+
${imports}
|
|
373
|
+
|
|
374
|
+
// The apps are a mix of agents and graphs whose generic types don't unify;
|
|
375
|
+
// this is the structural slice the routes actually need.
|
|
376
|
+
interface AppGraph {
|
|
377
|
+
invoke(input: unknown, config?: unknown): Promise<{ messages: BaseMessage[] } & Record<string, unknown>>;
|
|
378
|
+
stream(input: unknown, config?: unknown): Promise<AsyncIterable<[unknown, { langgraph_node?: string } | undefined]>>;
|
|
379
|
+
getState(config: unknown): Promise<{ values?: unknown; next?: string[]; tasks?: unknown[] } | undefined>;
|
|
380
|
+
}
|
|
381
|
+
const asApp = (g: unknown) => g as AppGraph;
|
|
382
|
+
|
|
383
|
+
function httpError(status: number, message: string) {
|
|
384
|
+
return Object.assign(new Error(message), { statusCode: status });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Reject malformed bodies before they reach the agent. */
|
|
388
|
+
function validateMessages(raw: unknown): { role: string; content: string }[] {
|
|
389
|
+
if (raw === undefined) return [];
|
|
390
|
+
if (!Array.isArray(raw)) throw httpError(400, '"messages" must be an array');
|
|
391
|
+
return raw.map((m, i) => {
|
|
392
|
+
const { role, content } = (m ?? {}) as Record<string, unknown>;
|
|
393
|
+
if (typeof role !== "string" || typeof content !== "string") {
|
|
394
|
+
throw httpError(400, \`messages[\${i}] needs string "role" and "content"\`);
|
|
395
|
+
}
|
|
396
|
+
return { role, content };
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const server = fastify({ logger: false });
|
|
401
|
+
|
|
402
|
+
server.setErrorHandler(async (error: FastifyError, _req: FastifyRequest, reply: FastifyReply) => {
|
|
403
|
+
const status = error.statusCode ?? 500;
|
|
404
|
+
return reply.status(status).send({ error: error.message });
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
async function start() {
|
|
408
|
+
const apps: Record<string, AppGraph> = {
|
|
409
|
+
${entries}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const getApp = (name: string) => {
|
|
413
|
+
const app = apps[name];
|
|
414
|
+
if (!app) throw httpError(404, \`Unknown app: "\${name}". Available: \${Object.keys(apps).join(", ")}\`);
|
|
415
|
+
return app;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
server.post<{ Params: { app: string } }>("/:app/invoke", async (req, reply) => {
|
|
419
|
+
const app = getApp(req.params.app);
|
|
420
|
+
const body = (req.body ?? {}) as { messages?: unknown; thread_id?: string };
|
|
421
|
+
const messages = validateMessages(body.messages);
|
|
422
|
+
const thread_id = body.thread_id ?? "default";
|
|
423
|
+
|
|
424
|
+
const result = await app.invoke({ messages }, { configurable: { thread_id } });
|
|
425
|
+
const last = result.messages.at(-1);
|
|
426
|
+
return reply.send({
|
|
427
|
+
messages: result.messages,
|
|
428
|
+
structuredResponse: (result as Record<string, unknown>).structuredResponse ?? null,
|
|
429
|
+
lastMessage: typeof last?.content === "string" ? last.content : JSON.stringify(last?.content),
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
server.post<{ Params: { app: string } }>("/:app/stream", async (req, reply) => {
|
|
434
|
+
const app = getApp(req.params.app);
|
|
435
|
+
const body = (req.body ?? {}) as { messages?: unknown; thread_id?: string };
|
|
436
|
+
const messages = validateMessages(body.messages);
|
|
437
|
+
const thread_id = body.thread_id ?? "default";
|
|
438
|
+
|
|
439
|
+
reply.raw.writeHead(200, {
|
|
440
|
+
"Content-Type": "text/event-stream",
|
|
441
|
+
"Cache-Control": "no-cache",
|
|
442
|
+
Connection: "keep-alive",
|
|
443
|
+
});
|
|
444
|
+
const send = (e: unknown) => reply.raw.write(\`data: \${JSON.stringify(e)}\\n\\n\`);
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const stream = await app.stream({ messages }, { configurable: { thread_id }, streamMode: "messages" });
|
|
448
|
+
for await (const [chunk, metadata] of stream) {
|
|
449
|
+
if (chunk instanceof AIMessageChunk) {
|
|
450
|
+
send({ type: "token", content: chunk.content, node: metadata?.langgraph_node ?? "unknown" });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
send({ type: "done" });
|
|
454
|
+
} catch (err) {
|
|
455
|
+
send({ type: "error", content: err instanceof Error ? err.message : "Stream failed" });
|
|
456
|
+
} finally {
|
|
457
|
+
reply.raw.end();
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
server.post<{ Params: { app: string } }>("/:app/resume", async (req, reply) => {
|
|
462
|
+
const app = getApp(req.params.app);
|
|
463
|
+
const body = (req.body ?? {}) as { thread_id?: string; decision?: string };
|
|
464
|
+
if (body.decision === undefined) {
|
|
465
|
+
return reply.status(400).send({ error: '"decision" field is required' });
|
|
466
|
+
}
|
|
467
|
+
const result = await app.invoke(new Command({ resume: body.decision }), {
|
|
468
|
+
configurable: { thread_id: body.thread_id ?? "default" },
|
|
469
|
+
});
|
|
470
|
+
const last = result.messages.at(-1);
|
|
471
|
+
return reply.send({
|
|
472
|
+
messages: result.messages,
|
|
473
|
+
lastMessage: typeof last?.content === "string" ? last.content : JSON.stringify(last?.content),
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
server.get<{ Params: { app: string; threadId: string } }>("/:app/threads/:threadId", async (req, reply) => {
|
|
478
|
+
const app = getApp(req.params.app);
|
|
479
|
+
const state = await app.getState({ configurable: { thread_id: req.params.threadId } });
|
|
480
|
+
if (!state?.values) return reply.status(404).send({ error: "Thread not found" });
|
|
481
|
+
return reply.send({ values: state.values, next: state.next ?? [], tasks: state.tasks ?? [] });
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
server.get("/health", async () => ({ status: "ok", apps: Object.keys(apps) }));
|
|
485
|
+
|
|
486
|
+
// Container runtimes send SIGTERM on stop — drain instead of dying mid-request.
|
|
487
|
+
let shuttingDown = false;
|
|
488
|
+
for (const signal of ["SIGTERM", "SIGINT"] as const) {
|
|
489
|
+
process.on(signal, () => {
|
|
490
|
+
if (shuttingDown) return;
|
|
491
|
+
shuttingDown = true;
|
|
492
|
+
server.close().then(() => process.exit(0)).catch(() => process.exit(1));
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
await server.listen({ port: PORT, host: "0.0.0.0" });
|
|
497
|
+
console.log(\`Server running at http://localhost:\${PORT}\`);
|
|
498
|
+
console.log(\`Apps: \${Object.keys(apps).join(", ")}\`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
start().catch((err) => {
|
|
502
|
+
console.error("Failed to start server:", err);
|
|
503
|
+
process.exit(1);
|
|
504
|
+
});
|
|
505
|
+
`;
|
|
506
|
+
}
|
|
507
|
+
function generateScriptedModel() {
|
|
508
|
+
return `import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
509
|
+
import { AIMessage, type BaseMessage } from "@langchain/core/messages";
|
|
510
|
+
import type { ChatResult } from "@langchain/core/outputs";
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* A model that replays a queued list of responses, so tests can drive real
|
|
514
|
+
* graphs offline — no API key, no network, fully deterministic.
|
|
515
|
+
*/
|
|
516
|
+
export class ScriptedToolCallingModel extends BaseChatModel {
|
|
517
|
+
private queue: AIMessage[];
|
|
518
|
+
|
|
519
|
+
constructor(queue: AIMessage[]) {
|
|
520
|
+
super({});
|
|
521
|
+
this.queue = [...queue];
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
_llmType(): string {
|
|
525
|
+
return "scripted-tool-calling";
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
override bindTools(): this {
|
|
529
|
+
return this;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async _generate(_messages: BaseMessage[]): Promise<ChatResult> {
|
|
533
|
+
const message = this.queue.shift();
|
|
534
|
+
if (!message) throw new Error("Scripted model ran out of responses");
|
|
535
|
+
return { generations: [{ message, text: "" }] };
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
`;
|
|
539
|
+
}
|
|
540
|
+
function generateAgentTest(config) {
|
|
541
|
+
const hasSupervisor = config.patterns.includes("supervisor");
|
|
542
|
+
const supervisorTest = hasSupervisor
|
|
543
|
+
? `
|
|
544
|
+
it("delegates to a subagent and returns its answer", async () => {
|
|
545
|
+
const llm = new ScriptedToolCallingModel([
|
|
546
|
+
// supervisor delegates
|
|
547
|
+
new AIMessage({
|
|
548
|
+
content: "",
|
|
549
|
+
tool_calls: [{ id: "c1", name: "worker", args: { task: "do the thing" } }],
|
|
550
|
+
}),
|
|
551
|
+
// subagent answers
|
|
552
|
+
new AIMessage("worker done"),
|
|
553
|
+
// supervisor wraps up
|
|
554
|
+
new AIMessage("All finished."),
|
|
555
|
+
]);
|
|
556
|
+
|
|
557
|
+
const worker = makeAgent({ name: "worker", llm, tools: [] });
|
|
558
|
+
const app = await makeSupervisor({
|
|
559
|
+
subagents: [{ name: "worker", description: "Does the thing.", agent: worker }],
|
|
560
|
+
llm,
|
|
561
|
+
checkpointer: new MemorySaver(),
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const result = await app.invoke(
|
|
565
|
+
{ messages: [{ role: "user", content: "do the thing" }] },
|
|
566
|
+
{ configurable: { thread_id: "t1" } }
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
expect(result.messages.at(-1)?.content).toBe("All finished.");
|
|
570
|
+
// the supervisor sees the subagent's final answer, not its internals
|
|
571
|
+
const toolMessages = result.messages.filter((m) => m.getType() === "tool");
|
|
572
|
+
expect(toolMessages).toHaveLength(1);
|
|
573
|
+
expect(toolMessages[0].content).toBe("worker done");
|
|
574
|
+
});
|
|
575
|
+
`
|
|
576
|
+
: "";
|
|
577
|
+
const supervisorImports = hasSupervisor
|
|
578
|
+
? `import { makeSupervisor } from "../src/agents/supervisor";\nimport { MemorySaver } from "@langchain/langgraph";\n`
|
|
579
|
+
: "";
|
|
580
|
+
return `import { describe, expect, it } from "vitest";
|
|
581
|
+
import { AIMessage } from "@langchain/core/messages";
|
|
582
|
+
import { makeAgent } from "../src/agents/factory";
|
|
583
|
+
${supervisorImports}import { ScriptedToolCallingModel } from "./helpers/scripted-model";
|
|
584
|
+
|
|
585
|
+
describe("agents", () => {
|
|
586
|
+
it("runs a single agent to a final answer", async () => {
|
|
587
|
+
const llm = new ScriptedToolCallingModel([new AIMessage("Hello there.")]);
|
|
588
|
+
const agent = makeAgent({ name: "assistant", llm, tools: [], system: "Be brief." });
|
|
589
|
+
|
|
590
|
+
const result = await agent.invoke({ messages: [{ role: "user", content: "hi" }] });
|
|
591
|
+
|
|
592
|
+
expect(result.messages.at(-1)?.content).toBe("Hello there.");
|
|
593
|
+
});
|
|
594
|
+
${supervisorTest}});
|
|
595
|
+
`;
|
|
596
|
+
}
|
|
338
597
|
// Pattern-specific: only files for selected patterns are generated
|
|
339
598
|
function getPatternFiles(config) {
|
|
340
599
|
const files = [];
|
|
@@ -706,6 +965,11 @@ export async function createRagApp() {
|
|
|
706
965
|
);
|
|
707
966
|
console.log("Result:", rag.messages.at(-1)?.content);`);
|
|
708
967
|
}
|
|
968
|
+
// HTTP server — routes only the apps that were generated
|
|
969
|
+
files.push({ path: "src/server.ts", content: generateServer(config) });
|
|
970
|
+
// Tests — offline, no API key needed
|
|
971
|
+
files.push({ path: "tests/helpers/scripted-model.ts", content: generateScriptedModel() });
|
|
972
|
+
files.push({ path: "tests/agents.test.ts", content: generateAgentTest(config) });
|
|
709
973
|
// Generate index.ts
|
|
710
974
|
files.push({
|
|
711
975
|
path: "src/index.ts",
|
|
@@ -732,6 +996,7 @@ async function main() {
|
|
|
732
996
|
{ value: "anthropic", label: "Anthropic (Claude Sonnet)" },
|
|
733
997
|
{ value: "google", label: "Google (Gemini 2.0 Flash)" },
|
|
734
998
|
{ value: "groq", label: "Groq (Llama 3.3 70B)" },
|
|
999
|
+
{ value: "deepseek", label: "DeepSeek (deepseek-v4-flash)" },
|
|
735
1000
|
{ value: "ollama", label: "Ollama (local, no API key)" },
|
|
736
1001
|
]);
|
|
737
1002
|
const patterns = await choose(rl, "Which patterns? (select multiple)", [
|