@quantish/agent 0.1.15 → 0.1.17

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.
Files changed (4) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +140 -152
  3. package/dist/index.js +1157 -353
  4. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -18,6 +18,9 @@ var schema = {
18
18
  anthropicApiKey: {
19
19
  type: "string"
20
20
  },
21
+ openrouterApiKey: {
22
+ type: "string"
23
+ },
21
24
  quantishApiKey: {
22
25
  type: "string"
23
26
  },
@@ -28,6 +31,10 @@ var schema = {
28
31
  model: {
29
32
  type: "string",
30
33
  default: "claude-sonnet-4-5-20250929"
34
+ },
35
+ provider: {
36
+ type: "string",
37
+ default: "anthropic"
31
38
  }
32
39
  };
33
40
  var ConfigManager = class {
@@ -54,6 +61,20 @@ var ConfigManager = class {
54
61
  setAnthropicApiKey(key) {
55
62
  this.conf.set("anthropicApiKey", key);
56
63
  }
64
+ /**
65
+ * Get the OpenRouter API key
66
+ */
67
+ getOpenRouterApiKey() {
68
+ const envKey = process.env.OPENROUTER_API_KEY;
69
+ if (envKey) return envKey;
70
+ return this.conf.get("openrouterApiKey");
71
+ }
72
+ /**
73
+ * Set the OpenRouter API key
74
+ */
75
+ setOpenRouterApiKey(key) {
76
+ this.conf.set("openrouterApiKey", key);
77
+ }
57
78
  /**
58
79
  * Get the Quantish API key
59
80
  */
@@ -68,13 +89,22 @@ var ConfigManager = class {
68
89
  setQuantishApiKey(key) {
69
90
  this.conf.set("quantishApiKey", key);
70
91
  }
92
+ /**
93
+ * Get the current LLM provider
94
+ */
95
+ getProvider() {
96
+ return this.conf.get("provider") ?? "anthropic";
97
+ }
98
+ /**
99
+ * Set the LLM provider
100
+ */
101
+ setProvider(provider) {
102
+ this.conf.set("provider", provider);
103
+ }
71
104
  /**
72
105
  * Get the Trading MCP server URL (user's wallet/orders)
73
- * Priority: MCP_SERVER_URL env var > config file > default
74
106
  */
75
107
  getMcpServerUrl() {
76
- const envUrl = process.env.MCP_SERVER_URL;
77
- if (envUrl) return envUrl;
78
108
  return this.conf.get("mcpServerUrl") ?? DEFAULT_MCP_URL;
79
109
  }
80
110
  /**
@@ -101,12 +131,6 @@ var ConfigManager = class {
101
131
  setMcpServerUrl(url) {
102
132
  this.conf.set("mcpServerUrl", url);
103
133
  }
104
- /**
105
- * Generic setter for any config key
106
- */
107
- set(key, value) {
108
- this.conf.set(key, value);
109
- }
110
134
  /**
111
135
  * Get the model to use
112
136
  */
@@ -120,13 +144,27 @@ var ConfigManager = class {
120
144
  this.conf.set("model", model);
121
145
  }
122
146
  /**
123
- * Check if the CLI is configured (has at least Anthropic key)
147
+ * Check if the CLI is configured (has required LLM API key)
124
148
  * Discovery MCP works without any user key (embedded public key)
125
149
  * Trading MCP requires a user key
126
150
  */
127
151
  isConfigured() {
152
+ const provider = this.getProvider();
153
+ if (provider === "openrouter") {
154
+ return !!this.getOpenRouterApiKey();
155
+ }
128
156
  return !!this.getAnthropicApiKey();
129
157
  }
158
+ /**
159
+ * Get the appropriate LLM API key based on current provider
160
+ */
161
+ getLLMApiKey() {
162
+ const provider = this.getProvider();
163
+ if (provider === "openrouter") {
164
+ return this.getOpenRouterApiKey();
165
+ }
166
+ return this.getAnthropicApiKey();
167
+ }
130
168
  /**
131
169
  * Check if trading is enabled (has Quantish API key)
132
170
  */
@@ -139,9 +177,11 @@ var ConfigManager = class {
139
177
  getAll() {
140
178
  return {
141
179
  anthropicApiKey: this.getAnthropicApiKey(),
180
+ openrouterApiKey: this.getOpenRouterApiKey(),
142
181
  quantishApiKey: this.getQuantishApiKey(),
143
182
  mcpServerUrl: this.getMcpServerUrl(),
144
- model: this.getModel()
183
+ model: this.getModel(),
184
+ provider: this.getProvider()
145
185
  };
146
186
  }
147
187
  /**
@@ -527,27 +567,61 @@ async function runSetup() {
527
567
  return false;
528
568
  }
529
569
  console.log();
530
- console.log(chalk.bold("Step 1: Anthropic API Key"));
531
- console.log(chalk.dim("Powers the AI agent. Get yours at https://console.anthropic.com/"));
532
- let anthropicKey = config.getAnthropicApiKey();
533
- if (anthropicKey) {
534
- console.log(chalk.dim(`Current: ${anthropicKey.slice(0, 10)}...`));
535
- const newKey = await prompt("Enter new key (or press Enter to keep current): ", true);
536
- if (newKey) {
537
- anthropicKey = newKey;
570
+ console.log(chalk.bold("Step 1: Choose your LLM Provider"));
571
+ console.log(chalk.dim("The AI that powers the agent.\n"));
572
+ console.log(" 1. " + chalk.cyan("Anthropic") + chalk.dim(" (Claude models - Opus, Sonnet, Haiku)"));
573
+ console.log(" 2. " + chalk.green("OpenRouter") + chalk.dim(" (Access 100+ models - MiniMax, DeepSeek, etc.)\n"));
574
+ const providerChoice = await prompt("Choose (1 or 2): ");
575
+ const useOpenRouter = providerChoice === "2";
576
+ if (useOpenRouter) {
577
+ config.setProvider("openrouter");
578
+ console.log();
579
+ console.log(chalk.bold("OpenRouter API Key"));
580
+ console.log(chalk.dim("Get yours at https://openrouter.ai/keys\n"));
581
+ let openrouterKey = config.getOpenRouterApiKey();
582
+ if (openrouterKey) {
583
+ console.log(chalk.dim(`Current: ${openrouterKey.slice(0, 10)}...`));
584
+ const newKey = await prompt("Enter new key (or press Enter to keep current): ", true);
585
+ if (newKey) {
586
+ openrouterKey = newKey;
587
+ }
588
+ } else {
589
+ openrouterKey = await prompt("Enter your OpenRouter API key: ", true);
538
590
  }
591
+ if (!openrouterKey) {
592
+ console.log(chalk.red("OpenRouter API key is required."));
593
+ return false;
594
+ }
595
+ if (!openrouterKey.startsWith("sk-or-")) {
596
+ console.log(chalk.yellow("Warning: Key doesn't look like an OpenRouter key (should start with sk-or-)"));
597
+ }
598
+ config.setOpenRouterApiKey(openrouterKey);
599
+ console.log(chalk.green("\u2713 OpenRouter API key saved\n"));
539
600
  } else {
540
- anthropicKey = await prompt("Enter your Anthropic API key: ", true);
541
- }
542
- if (!anthropicKey) {
543
- console.log(chalk.red("Anthropic API key is required."));
544
- return false;
545
- }
546
- if (!anthropicKey.startsWith("sk-ant-")) {
547
- console.log(chalk.yellow("Warning: Key doesn't look like an Anthropic key (should start with sk-ant-)"));
601
+ config.setProvider("anthropic");
602
+ console.log();
603
+ console.log(chalk.bold("Anthropic API Key"));
604
+ console.log(chalk.dim("Get yours at https://console.anthropic.com/\n"));
605
+ let anthropicKey = config.getAnthropicApiKey();
606
+ if (anthropicKey) {
607
+ console.log(chalk.dim(`Current: ${anthropicKey.slice(0, 10)}...`));
608
+ const newKey = await prompt("Enter new key (or press Enter to keep current): ", true);
609
+ if (newKey) {
610
+ anthropicKey = newKey;
611
+ }
612
+ } else {
613
+ anthropicKey = await prompt("Enter your Anthropic API key: ", true);
614
+ }
615
+ if (!anthropicKey) {
616
+ console.log(chalk.red("Anthropic API key is required."));
617
+ return false;
618
+ }
619
+ if (!anthropicKey.startsWith("sk-ant-")) {
620
+ console.log(chalk.yellow("Warning: Key doesn't look like an Anthropic key (should start with sk-ant-)"));
621
+ }
622
+ config.setAnthropicApiKey(anthropicKey);
623
+ console.log(chalk.green("\u2713 Anthropic API key saved\n"));
548
624
  }
549
- config.setAnthropicApiKey(anthropicKey);
550
- console.log(chalk.green("\u2713 Anthropic API key saved\n"));
551
625
  console.log(chalk.bold("Step 2: Polymarket Trading (Optional)"));
552
626
  console.log(chalk.dim("Enable trading on Polymarket with your own managed wallet."));
553
627
  console.log(chalk.dim("Skip this if you only want to search/discover markets.\n"));
@@ -681,15 +755,14 @@ async function runSetup() {
681
755
  async function ensureConfigured() {
682
756
  const config = getConfigManager();
683
757
  if (!config.isConfigured()) {
684
- console.log(chalk.yellow("Quantish CLI is not configured yet."));
685
- console.log("Run " + chalk.yellow("quantish init") + " to set up.\n");
686
- return false;
758
+ console.log(chalk.yellow("Quantish CLI is not configured yet.\n"));
759
+ return await runSetup();
687
760
  }
688
761
  return true;
689
762
  }
690
763
 
691
764
  // src/agent/loop.ts
692
- import Anthropic from "@anthropic-ai/sdk";
765
+ import Anthropic2 from "@anthropic-ai/sdk";
693
766
 
694
767
  // src/tools/filesystem.ts
695
768
  import * as fs from "fs/promises";
@@ -780,41 +853,6 @@ async function fileExists(filePath) {
780
853
  return { success: false, error: `Failed to check file: ${error2 instanceof Error ? error2.message : String(error2)}` };
781
854
  }
782
855
  }
783
- async function editLines(filePath, startLine, endLine, newContent) {
784
- try {
785
- const resolvedPath = path.resolve(filePath);
786
- if (!existsSync(resolvedPath)) {
787
- return { success: false, error: `File not found: ${filePath}` };
788
- }
789
- const content = await fs.readFile(resolvedPath, "utf-8");
790
- const lines = content.split("\n");
791
- if (startLine < 1 || endLine < startLine || startLine > lines.length) {
792
- return {
793
- success: false,
794
- error: `Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines.`
795
- };
796
- }
797
- const startIdx = startLine - 1;
798
- const endIdx = Math.min(endLine, lines.length);
799
- const newLines = newContent.split("\n");
800
- const beforeLines = lines.slice(0, startIdx);
801
- const afterLines = lines.slice(endIdx);
802
- const resultLines = [...beforeLines, ...newLines, ...afterLines];
803
- const newFileContent = resultLines.join("\n");
804
- await fs.writeFile(resolvedPath, newFileContent, "utf-8");
805
- return {
806
- success: true,
807
- data: {
808
- path: resolvedPath,
809
- linesReplaced: endIdx - startIdx,
810
- newLinesInserted: newLines.length,
811
- totalLines: resultLines.length
812
- }
813
- };
814
- } catch (error2) {
815
- return { success: false, error: `Failed to edit lines: ${error2 instanceof Error ? error2.message : String(error2)}` };
816
- }
817
- }
818
856
  async function editFile(filePath, oldString, newString, options) {
819
857
  try {
820
858
  const resolvedPath = path.resolve(filePath);
@@ -932,35 +970,9 @@ var filesystemTools = [
932
970
  required: ["path"]
933
971
  }
934
972
  },
935
- {
936
- name: "edit_lines",
937
- description: "Edit specific lines in a file by line number. MORE EFFICIENT than edit_file - use this when you know the line numbers from read_file. Only sends line numbers + new content, not full old content.",
938
- input_schema: {
939
- type: "object",
940
- properties: {
941
- path: {
942
- type: "string",
943
- description: "The path to the file to edit"
944
- },
945
- start_line: {
946
- type: "number",
947
- description: "The first line number to replace (1-based, inclusive)"
948
- },
949
- end_line: {
950
- type: "number",
951
- description: "The last line number to replace (1-based, inclusive)"
952
- },
953
- new_content: {
954
- type: "string",
955
- description: "The new content to insert (replaces lines start_line through end_line)"
956
- }
957
- },
958
- required: ["path", "start_line", "end_line", "new_content"]
959
- }
960
- },
961
973
  {
962
974
  name: "edit_file",
963
- description: "Edit a file by replacing a specific string with new content. Use edit_lines instead when you know line numbers - it uses fewer tokens.",
975
+ description: "Edit a file by replacing a specific string with new content. Safer than write_file as it only modifies the targeted section. The old_string must match exactly (including whitespace).",
964
976
  input_schema: {
965
977
  type: "object",
966
978
  properties: {
@@ -983,111 +995,8 @@ var filesystemTools = [
983
995
  },
984
996
  required: ["path", "old_string", "new_string"]
985
997
  }
986
- },
987
- {
988
- name: "setup_env",
989
- description: "Setup or update environment variables in a .env file for an application. Creates .env if it doesn't exist. Optionally creates a .env.example template. Use this when building any application that needs API keys or configuration.",
990
- input_schema: {
991
- type: "object",
992
- properties: {
993
- path: {
994
- type: "string",
995
- description: 'Path to the .env file (default: ".env" in current directory)'
996
- },
997
- variables: {
998
- type: "object",
999
- description: 'Object with environment variable names as keys and values. Example: { "QUANTISH_API_KEY": "abc123", "TOKEN_ID": "xyz" }',
1000
- additionalProperties: { type: "string" }
1001
- },
1002
- overwrite: {
1003
- type: "boolean",
1004
- description: "If true, overwrite existing variables. Default false (skip existing)."
1005
- },
1006
- create_example: {
1007
- type: "boolean",
1008
- description: "If true, also create a .env.example template file with placeholder values."
1009
- }
1010
- },
1011
- required: ["variables"]
1012
- }
1013
998
  }
1014
999
  ];
1015
- async function setupEnv(envPath = ".env", variables, options) {
1016
- try {
1017
- const resolvedPath = path.resolve(envPath);
1018
- let content = "";
1019
- const existingVars = {};
1020
- if (existsSync(resolvedPath)) {
1021
- content = await fs.readFile(resolvedPath, "utf-8");
1022
- for (const line of content.split("\n")) {
1023
- const trimmed = line.trim();
1024
- if (trimmed && !trimmed.startsWith("#")) {
1025
- const eqIndex = trimmed.indexOf("=");
1026
- if (eqIndex > 0) {
1027
- const key = trimmed.slice(0, eqIndex);
1028
- const value = trimmed.slice(eqIndex + 1);
1029
- existingVars[key] = value;
1030
- }
1031
- }
1032
- }
1033
- }
1034
- const updatedVars = [];
1035
- const addedVars = [];
1036
- const skippedVars = [];
1037
- for (const [key, value] of Object.entries(variables)) {
1038
- if (existingVars[key] !== void 0) {
1039
- if (options?.overwrite) {
1040
- const regex = new RegExp(`^${key}=.*$`, "m");
1041
- content = content.replace(regex, `${key}=${value}`);
1042
- updatedVars.push(key);
1043
- } else {
1044
- skippedVars.push(key);
1045
- }
1046
- } else {
1047
- if (content && !content.endsWith("\n")) {
1048
- content += "\n";
1049
- }
1050
- content += `${key}=${value}
1051
- `;
1052
- addedVars.push(key);
1053
- }
1054
- }
1055
- await fs.writeFile(resolvedPath, content, "utf-8");
1056
- if (options?.createExample) {
1057
- const examplePath = resolvedPath.replace(/\.env$/, ".env.example");
1058
- let exampleContent = "# Environment variables for this application\n";
1059
- exampleContent += "# Copy this file to .env and fill in your values\n\n";
1060
- for (const key of Object.keys({ ...existingVars, ...variables })) {
1061
- if (key === "QUANTISH_API_KEY") {
1062
- exampleContent += `# Get your API key at https://quantish.live
1063
- `;
1064
- exampleContent += `${key}=your_api_key_here
1065
-
1066
- `;
1067
- } else {
1068
- exampleContent += `${key}=
1069
- `;
1070
- }
1071
- }
1072
- await fs.writeFile(examplePath, exampleContent, "utf-8");
1073
- }
1074
- return {
1075
- success: true,
1076
- data: {
1077
- path: resolvedPath,
1078
- added: addedVars,
1079
- updated: updatedVars,
1080
- skipped: skippedVars,
1081
- exampleCreated: options?.createExample || false
1082
- }
1083
- };
1084
- } catch (error2) {
1085
- return {
1086
- success: false,
1087
- error: `Failed to setup env: ${error2 instanceof Error ? error2.message : String(error2)}`
1088
- };
1089
- }
1090
- }
1091
1000
  async function executeFilesystemTool(name, args) {
1092
1001
  switch (name) {
1093
1002
  case "read_file":
@@ -1103,13 +1012,6 @@ async function executeFilesystemTool(name, args) {
1103
1012
  return deleteFile(args.path);
1104
1013
  case "file_exists":
1105
1014
  return fileExists(args.path);
1106
- case "edit_lines":
1107
- return editLines(
1108
- args.path,
1109
- args.start_line,
1110
- args.end_line,
1111
- args.new_content
1112
- );
1113
1015
  case "edit_file":
1114
1016
  return editFile(
1115
1017
  args.path,
@@ -1117,15 +1019,6 @@ async function executeFilesystemTool(name, args) {
1117
1019
  args.new_string,
1118
1020
  { replaceAll: args.replace_all }
1119
1021
  );
1120
- case "setup_env":
1121
- return setupEnv(
1122
- args.path || ".env",
1123
- args.variables,
1124
- {
1125
- overwrite: args.overwrite,
1126
- createExample: args.create_example
1127
- }
1128
- );
1129
1022
  default:
1130
1023
  return { success: false, error: `Unknown filesystem tool: ${name}` };
1131
1024
  }
@@ -2425,16 +2318,16 @@ async function compactConversation(anthropic, history, model, systemPrompt, tool
2425
2318
 
2426
2319
  // src/agent/pricing.ts
2427
2320
  var MODELS = {
2428
- "claude-opus-4-5-20251101": {
2429
- id: "claude-opus-4-5-20251101",
2321
+ "claude-opus-4-5-20250929": {
2322
+ id: "claude-opus-4-5-20250929",
2430
2323
  name: "opus-4.5",
2431
2324
  displayName: "Claude Opus 4.5",
2432
2325
  pricing: {
2433
- inputPerMTok: 15,
2434
- outputPerMTok: 75,
2435
- cacheWritePerMTok: 18.75,
2326
+ inputPerMTok: 5,
2327
+ outputPerMTok: 25,
2328
+ cacheWritePerMTok: 6.25,
2436
2329
  // 1.25x input
2437
- cacheReadPerMTok: 1.5
2330
+ cacheReadPerMTok: 0.5
2438
2331
  // 0.1x input
2439
2332
  },
2440
2333
  contextWindow: 2e5,
@@ -2455,8 +2348,8 @@ var MODELS = {
2455
2348
  contextWindow: 2e5,
2456
2349
  description: "Balanced performance and cost. Great for most coding and trading tasks."
2457
2350
  },
2458
- "claude-haiku-4-5-20251001": {
2459
- id: "claude-haiku-4-5-20251001",
2351
+ "claude-haiku-4-5-20250929": {
2352
+ id: "claude-haiku-4-5-20250929",
2460
2353
  name: "haiku-4.5",
2461
2354
  displayName: "Claude Haiku 4.5",
2462
2355
  pricing: {
@@ -2473,12 +2366,12 @@ var MODELS = {
2473
2366
  };
2474
2367
  var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
2475
2368
  var MODEL_ALIASES = {
2476
- "opus": "claude-opus-4-5-20251101",
2477
- "opus-4.5": "claude-opus-4-5-20251101",
2369
+ "opus": "claude-opus-4-5-20250929",
2370
+ "opus-4.5": "claude-opus-4-5-20250929",
2478
2371
  "sonnet": "claude-sonnet-4-5-20250929",
2479
2372
  "sonnet-4.5": "claude-sonnet-4-5-20250929",
2480
- "haiku": "claude-haiku-4-5-20251001",
2481
- "haiku-4.5": "claude-haiku-4-5-20251001"
2373
+ "haiku": "claude-haiku-4-5-20250929",
2374
+ "haiku-4.5": "claude-haiku-4-5-20250929"
2482
2375
  };
2483
2376
  function resolveModelId(nameOrAlias) {
2484
2377
  const lower = nameOrAlias.toLowerCase();
@@ -2545,6 +2438,744 @@ function listModels() {
2545
2438
  return Object.values(MODELS);
2546
2439
  }
2547
2440
 
2441
+ // src/agent/provider.ts
2442
+ import Anthropic from "@anthropic-ai/sdk";
2443
+
2444
+ // src/agent/openrouter.ts
2445
+ var OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
2446
+ var OPENROUTER_MODELS = {
2447
+ // MiniMax models - very cost effective
2448
+ "minimax/minimax-m2.1": {
2449
+ id: "minimax/minimax-m2.1",
2450
+ name: "minimax-m2.1",
2451
+ displayName: "MiniMax M2.1",
2452
+ provider: "MiniMax",
2453
+ pricing: {
2454
+ inputPerMTok: 0.3,
2455
+ // $0.0000003 * 1M
2456
+ outputPerMTok: 1.2,
2457
+ // $0.0000012 * 1M
2458
+ cacheReadPerMTok: 0.03,
2459
+ cacheWritePerMTok: 0.375
2460
+ },
2461
+ contextWindow: 204800,
2462
+ maxOutputTokens: 131072,
2463
+ supportsTools: true,
2464
+ supportsReasoning: true,
2465
+ description: "10B active params, state-of-the-art for coding and agentic workflows. Very cost efficient."
2466
+ },
2467
+ "minimax/minimax-m2": {
2468
+ id: "minimax/minimax-m2",
2469
+ name: "minimax-m2",
2470
+ displayName: "MiniMax M2",
2471
+ provider: "MiniMax",
2472
+ pricing: {
2473
+ inputPerMTok: 0.2,
2474
+ outputPerMTok: 1,
2475
+ cacheReadPerMTok: 0.03
2476
+ },
2477
+ contextWindow: 196608,
2478
+ maxOutputTokens: 131072,
2479
+ supportsTools: true,
2480
+ supportsReasoning: true,
2481
+ description: "Compact model optimized for end-to-end coding and agentic workflows."
2482
+ },
2483
+ // DeepSeek models - very cheap
2484
+ "deepseek/deepseek-v3.2": {
2485
+ id: "deepseek/deepseek-v3.2",
2486
+ name: "deepseek-v3.2",
2487
+ displayName: "DeepSeek V3.2",
2488
+ provider: "DeepSeek",
2489
+ pricing: {
2490
+ inputPerMTok: 0.224,
2491
+ outputPerMTok: 0.32
2492
+ },
2493
+ contextWindow: 163840,
2494
+ supportsTools: true,
2495
+ supportsReasoning: true,
2496
+ description: "High efficiency with strong reasoning. GPT-5 class performance."
2497
+ },
2498
+ // Mistral models
2499
+ "mistralai/devstral-2512": {
2500
+ id: "mistralai/devstral-2512",
2501
+ name: "devstral-2512",
2502
+ displayName: "Devstral 2 2512",
2503
+ provider: "Mistral",
2504
+ pricing: {
2505
+ inputPerMTok: 0.05,
2506
+ outputPerMTok: 0.22
2507
+ },
2508
+ contextWindow: 262144,
2509
+ supportsTools: true,
2510
+ description: "State-of-the-art open model for agentic coding. 123B params."
2511
+ },
2512
+ "mistralai/mistral-large-2512": {
2513
+ id: "mistralai/mistral-large-2512",
2514
+ name: "mistral-large-2512",
2515
+ displayName: "Mistral Large 3",
2516
+ provider: "Mistral",
2517
+ pricing: {
2518
+ inputPerMTok: 0.5,
2519
+ outputPerMTok: 1.5
2520
+ },
2521
+ contextWindow: 262144,
2522
+ supportsTools: true,
2523
+ description: "Most capable Mistral model. 675B total params (41B active)."
2524
+ },
2525
+ // Google Gemini
2526
+ "google/gemini-3-flash-preview": {
2527
+ id: "google/gemini-3-flash-preview",
2528
+ name: "gemini-3-flash",
2529
+ displayName: "Gemini 3 Flash Preview",
2530
+ provider: "Google",
2531
+ pricing: {
2532
+ inputPerMTok: 0.5,
2533
+ outputPerMTok: 3,
2534
+ cacheReadPerMTok: 0.05
2535
+ },
2536
+ contextWindow: 1048576,
2537
+ supportsTools: true,
2538
+ supportsReasoning: true,
2539
+ description: "High speed thinking model for agentic workflows. 1M context."
2540
+ },
2541
+ "google/gemini-3-pro-preview": {
2542
+ id: "google/gemini-3-pro-preview",
2543
+ name: "gemini-3-pro",
2544
+ displayName: "Gemini 3 Pro Preview",
2545
+ provider: "Google",
2546
+ pricing: {
2547
+ inputPerMTok: 2,
2548
+ outputPerMTok: 12,
2549
+ cacheReadPerMTok: 0.2,
2550
+ cacheWritePerMTok: 2.375
2551
+ },
2552
+ contextWindow: 1048576,
2553
+ supportsTools: true,
2554
+ supportsReasoning: true,
2555
+ description: "Flagship frontier model for high-precision multimodal reasoning."
2556
+ },
2557
+ // xAI Grok
2558
+ "x-ai/grok-4.1-fast": {
2559
+ id: "x-ai/grok-4.1-fast",
2560
+ name: "grok-4.1-fast",
2561
+ displayName: "Grok 4.1 Fast",
2562
+ provider: "xAI",
2563
+ pricing: {
2564
+ inputPerMTok: 0.2,
2565
+ outputPerMTok: 0.5,
2566
+ cacheReadPerMTok: 0.05
2567
+ },
2568
+ contextWindow: 2e6,
2569
+ maxOutputTokens: 3e4,
2570
+ supportsTools: true,
2571
+ supportsReasoning: true,
2572
+ description: "Best agentic tool calling model. 2M context window."
2573
+ },
2574
+ // Anthropic via OpenRouter (for fallback/comparison)
2575
+ "anthropic/claude-opus-4.5": {
2576
+ id: "anthropic/claude-opus-4.5",
2577
+ name: "claude-opus-4.5-or",
2578
+ displayName: "Claude Opus 4.5 (OR)",
2579
+ provider: "Anthropic",
2580
+ pricing: {
2581
+ inputPerMTok: 5,
2582
+ outputPerMTok: 25,
2583
+ cacheReadPerMTok: 0.5,
2584
+ cacheWritePerMTok: 6.25
2585
+ },
2586
+ contextWindow: 2e5,
2587
+ maxOutputTokens: 32e3,
2588
+ supportsTools: true,
2589
+ supportsReasoning: true,
2590
+ description: "Anthropic Opus 4.5 via OpenRouter."
2591
+ },
2592
+ "anthropic/claude-haiku-4.5": {
2593
+ id: "anthropic/claude-haiku-4.5",
2594
+ name: "claude-haiku-4.5-or",
2595
+ displayName: "Claude Haiku 4.5 (OR)",
2596
+ provider: "Anthropic",
2597
+ pricing: {
2598
+ inputPerMTok: 1,
2599
+ outputPerMTok: 5,
2600
+ cacheReadPerMTok: 0.1,
2601
+ cacheWritePerMTok: 1.25
2602
+ },
2603
+ contextWindow: 2e5,
2604
+ maxOutputTokens: 64e3,
2605
+ supportsTools: true,
2606
+ supportsReasoning: true,
2607
+ description: "Anthropic Haiku 4.5 via OpenRouter. Fast and efficient."
2608
+ },
2609
+ // Free models (for testing/experimentation)
2610
+ "mistralai/devstral-2512:free": {
2611
+ id: "mistralai/devstral-2512:free",
2612
+ name: "devstral-free",
2613
+ displayName: "Devstral 2 (Free)",
2614
+ provider: "Mistral",
2615
+ pricing: {
2616
+ inputPerMTok: 0,
2617
+ outputPerMTok: 0
2618
+ },
2619
+ contextWindow: 262144,
2620
+ supportsTools: true,
2621
+ description: "Free tier Devstral for testing. Limited capacity."
2622
+ },
2623
+ "xiaomi/mimo-v2-flash:free": {
2624
+ id: "xiaomi/mimo-v2-flash:free",
2625
+ name: "mimo-v2-flash-free",
2626
+ displayName: "MiMo V2 Flash (Free)",
2627
+ provider: "Xiaomi",
2628
+ pricing: {
2629
+ inputPerMTok: 0,
2630
+ outputPerMTok: 0
2631
+ },
2632
+ contextWindow: 262144,
2633
+ supportsTools: true,
2634
+ supportsReasoning: true,
2635
+ description: "Free MoE model. Top open-source on SWE-bench."
2636
+ }
2637
+ };
2638
+ var OPENROUTER_ALIASES = {
2639
+ // MiniMax
2640
+ "minimax": "minimax/minimax-m2.1",
2641
+ "m2": "minimax/minimax-m2",
2642
+ "m2.1": "minimax/minimax-m2.1",
2643
+ // DeepSeek
2644
+ "deepseek": "deepseek/deepseek-v3.2",
2645
+ "ds": "deepseek/deepseek-v3.2",
2646
+ // Mistral
2647
+ "devstral": "mistralai/devstral-2512",
2648
+ "mistral": "mistralai/mistral-large-2512",
2649
+ "mistral-large": "mistralai/mistral-large-2512",
2650
+ // Google
2651
+ "gemini": "google/gemini-3-flash-preview",
2652
+ "gemini-flash": "google/gemini-3-flash-preview",
2653
+ "gemini-pro": "google/gemini-3-pro-preview",
2654
+ // xAI
2655
+ "grok": "x-ai/grok-4.1-fast",
2656
+ // Anthropic via OR
2657
+ "opus-or": "anthropic/claude-opus-4.5",
2658
+ "haiku-or": "anthropic/claude-haiku-4.5",
2659
+ // Free
2660
+ "free": "mistralai/devstral-2512:free",
2661
+ "mimo": "xiaomi/mimo-v2-flash:free"
2662
+ };
2663
+ function resolveOpenRouterModelId(nameOrAlias) {
2664
+ const lower = nameOrAlias.toLowerCase();
2665
+ if (OPENROUTER_MODELS[lower]) {
2666
+ return lower;
2667
+ }
2668
+ if (OPENROUTER_ALIASES[lower]) {
2669
+ return OPENROUTER_ALIASES[lower];
2670
+ }
2671
+ for (const [id, config] of Object.entries(OPENROUTER_MODELS)) {
2672
+ if (config.name.toLowerCase() === lower) {
2673
+ return id;
2674
+ }
2675
+ }
2676
+ if (nameOrAlias.includes("/")) {
2677
+ return nameOrAlias;
2678
+ }
2679
+ return null;
2680
+ }
2681
+ function getOpenRouterModelConfig(modelId) {
2682
+ return OPENROUTER_MODELS[modelId] ?? null;
2683
+ }
2684
+ function convertToOpenAITools(anthropicTools) {
2685
+ return anthropicTools.map((tool) => ({
2686
+ type: "function",
2687
+ function: {
2688
+ name: tool.name,
2689
+ description: tool.description ?? "",
2690
+ parameters: tool.input_schema
2691
+ }
2692
+ }));
2693
+ }
2694
+ var OpenRouterClient = class {
2695
+ apiKey;
2696
+ baseUrl;
2697
+ appName;
2698
+ appUrl;
2699
+ constructor(config) {
2700
+ this.apiKey = config.apiKey;
2701
+ this.baseUrl = config.baseUrl ?? OPENROUTER_BASE_URL;
2702
+ this.appName = config.appName ?? "Quantish Agent";
2703
+ this.appUrl = config.appUrl ?? "https://quantish.ai";
2704
+ }
2705
+ /**
2706
+ * Create a chat completion (non-streaming)
2707
+ */
2708
+ async createChatCompletion(options) {
2709
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
2710
+ method: "POST",
2711
+ headers: {
2712
+ "Authorization": `Bearer ${this.apiKey}`,
2713
+ "Content-Type": "application/json",
2714
+ "HTTP-Referer": this.appUrl,
2715
+ "X-Title": this.appName
2716
+ },
2717
+ body: JSON.stringify({
2718
+ model: options.model,
2719
+ messages: options.messages,
2720
+ tools: options.tools,
2721
+ tool_choice: options.tool_choice ?? (options.tools ? "auto" : void 0),
2722
+ max_tokens: options.max_tokens,
2723
+ temperature: options.temperature,
2724
+ top_p: options.top_p,
2725
+ stream: false
2726
+ })
2727
+ });
2728
+ if (!response.ok) {
2729
+ const errorText = await response.text();
2730
+ throw new Error(`OpenRouter API error (${response.status}): ${errorText}`);
2731
+ }
2732
+ return response.json();
2733
+ }
2734
+ /**
2735
+ * Create a streaming chat completion
2736
+ */
2737
+ async *createStreamingChatCompletion(options) {
2738
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
2739
+ method: "POST",
2740
+ headers: {
2741
+ "Authorization": `Bearer ${this.apiKey}`,
2742
+ "Content-Type": "application/json",
2743
+ "HTTP-Referer": this.appUrl,
2744
+ "X-Title": this.appName
2745
+ },
2746
+ body: JSON.stringify({
2747
+ model: options.model,
2748
+ messages: options.messages,
2749
+ tools: options.tools,
2750
+ tool_choice: options.tool_choice ?? (options.tools ? "auto" : void 0),
2751
+ max_tokens: options.max_tokens,
2752
+ temperature: options.temperature,
2753
+ top_p: options.top_p,
2754
+ stream: true
2755
+ })
2756
+ });
2757
+ if (!response.ok) {
2758
+ const errorText = await response.text();
2759
+ throw new Error(`OpenRouter API error (${response.status}): ${errorText}`);
2760
+ }
2761
+ if (!response.body) {
2762
+ throw new Error("No response body for streaming request");
2763
+ }
2764
+ const reader = response.body.getReader();
2765
+ const decoder = new TextDecoder();
2766
+ let buffer = "";
2767
+ try {
2768
+ while (true) {
2769
+ const { done, value } = await reader.read();
2770
+ if (done) break;
2771
+ buffer += decoder.decode(value, { stream: true });
2772
+ const lines = buffer.split("\n");
2773
+ buffer = lines.pop() ?? "";
2774
+ for (const line of lines) {
2775
+ const trimmed = line.trim();
2776
+ if (!trimmed || trimmed === "data: [DONE]") continue;
2777
+ if (!trimmed.startsWith("data: ")) continue;
2778
+ try {
2779
+ const json = JSON.parse(trimmed.slice(6));
2780
+ yield json;
2781
+ } catch {
2782
+ }
2783
+ }
2784
+ }
2785
+ } finally {
2786
+ reader.releaseLock();
2787
+ }
2788
+ }
2789
+ /**
2790
+ * Get generation details including exact cost
2791
+ */
2792
+ async getGenerationDetails(generationId) {
2793
+ const response = await fetch(`${this.baseUrl}/generation?id=${generationId}`, {
2794
+ method: "GET",
2795
+ headers: {
2796
+ "Authorization": `Bearer ${this.apiKey}`
2797
+ }
2798
+ });
2799
+ if (!response.ok) {
2800
+ const errorText = await response.text();
2801
+ throw new Error(`OpenRouter API error (${response.status}): ${errorText}`);
2802
+ }
2803
+ return response.json();
2804
+ }
2805
+ /**
2806
+ * List available models
2807
+ */
2808
+ async listModels() {
2809
+ const response = await fetch(`${this.baseUrl}/models`, {
2810
+ method: "GET",
2811
+ headers: {
2812
+ "Authorization": `Bearer ${this.apiKey}`
2813
+ }
2814
+ });
2815
+ if (!response.ok) {
2816
+ const errorText = await response.text();
2817
+ throw new Error(`OpenRouter API error (${response.status}): ${errorText}`);
2818
+ }
2819
+ return response.json();
2820
+ }
2821
+ };
2822
+ function calculateOpenRouterCost(modelId, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
2823
+ const config = getOpenRouterModelConfig(modelId);
2824
+ const pricing = config?.pricing ?? {
2825
+ inputPerMTok: 1,
2826
+ outputPerMTok: 3,
2827
+ cacheReadPerMTok: 0.1,
2828
+ cacheWritePerMTok: 1.25
2829
+ };
2830
+ const inputCost = inputTokens / 1e6 * pricing.inputPerMTok;
2831
+ const outputCost = outputTokens / 1e6 * pricing.outputPerMTok;
2832
+ const cacheReadCost = cacheReadTokens / 1e6 * (pricing.cacheReadPerMTok ?? pricing.inputPerMTok * 0.1);
2833
+ const cacheWriteCost = cacheWriteTokens / 1e6 * (pricing.cacheWritePerMTok ?? pricing.inputPerMTok * 1.25);
2834
+ return {
2835
+ inputCost,
2836
+ outputCost,
2837
+ cacheReadCost,
2838
+ cacheWriteCost,
2839
+ totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost
2840
+ };
2841
+ }
2842
+ function listOpenRouterModels() {
2843
+ return Object.values(OPENROUTER_MODELS);
2844
+ }
2845
+
2846
+ // src/agent/provider.ts
2847
+ var AnthropicProvider = class {
2848
+ client;
2849
+ config;
2850
+ constructor(config) {
2851
+ this.config = config;
2852
+ const headers = {};
2853
+ if (config.contextEditing && config.contextEditing.length > 0) {
2854
+ headers["anthropic-beta"] = "context-management-2025-06-27";
2855
+ }
2856
+ this.client = new Anthropic({
2857
+ apiKey: config.apiKey,
2858
+ defaultHeaders: Object.keys(headers).length > 0 ? headers : void 0
2859
+ });
2860
+ }
2861
+ getModel() {
2862
+ return this.config.model;
2863
+ }
2864
+ async countTokens(messages) {
2865
+ try {
2866
+ const response = await this.client.messages.countTokens({
2867
+ model: this.config.model,
2868
+ system: this.config.systemPrompt,
2869
+ tools: this.config.tools,
2870
+ messages
2871
+ });
2872
+ return response.input_tokens;
2873
+ } catch {
2874
+ return 0;
2875
+ }
2876
+ }
2877
+ async chat(messages) {
2878
+ const systemWithCache = [
2879
+ {
2880
+ type: "text",
2881
+ text: this.config.systemPrompt,
2882
+ cache_control: { type: "ephemeral" }
2883
+ }
2884
+ ];
2885
+ const response = await this.client.messages.create({
2886
+ model: this.config.model,
2887
+ max_tokens: this.config.maxTokens,
2888
+ system: systemWithCache,
2889
+ tools: this.config.tools,
2890
+ messages
2891
+ });
2892
+ const usage = response.usage;
2893
+ const cost = calculateCost(
2894
+ this.config.model,
2895
+ usage.input_tokens,
2896
+ usage.output_tokens,
2897
+ usage.cache_creation_input_tokens ?? 0,
2898
+ usage.cache_read_input_tokens ?? 0
2899
+ );
2900
+ const textBlocks = response.content.filter(
2901
+ (block) => block.type === "text"
2902
+ );
2903
+ const toolUses = response.content.filter(
2904
+ (block) => block.type === "tool_use"
2905
+ );
2906
+ return {
2907
+ text: textBlocks.map((b) => b.text).join(""),
2908
+ toolCalls: toolUses.map((t) => ({
2909
+ id: t.id,
2910
+ name: t.name,
2911
+ input: t.input
2912
+ })),
2913
+ usage: {
2914
+ inputTokens: usage.input_tokens,
2915
+ outputTokens: usage.output_tokens,
2916
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
2917
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0
2918
+ },
2919
+ cost,
2920
+ stopReason: response.stop_reason === "tool_use" ? "tool_use" : "end_turn",
2921
+ rawResponse: response
2922
+ };
2923
+ }
2924
+ async streamChat(messages, callbacks) {
2925
+ const systemWithCache = [
2926
+ {
2927
+ type: "text",
2928
+ text: this.config.systemPrompt,
2929
+ cache_control: { type: "ephemeral" }
2930
+ }
2931
+ ];
2932
+ const stream = this.client.messages.stream({
2933
+ model: this.config.model,
2934
+ max_tokens: this.config.maxTokens,
2935
+ system: systemWithCache,
2936
+ tools: this.config.tools,
2937
+ messages
2938
+ });
2939
+ let fullText = "";
2940
+ for await (const event of stream) {
2941
+ if (event.type === "content_block_delta") {
2942
+ const delta = event.delta;
2943
+ if (delta.type === "text_delta" && delta.text) {
2944
+ fullText += delta.text;
2945
+ callbacks.onText?.(delta.text);
2946
+ } else if (delta.type === "thinking_delta" && delta.thinking) {
2947
+ callbacks.onThinking?.(delta.thinking);
2948
+ }
2949
+ }
2950
+ }
2951
+ const response = await stream.finalMessage();
2952
+ const usage = response.usage;
2953
+ const cost = calculateCost(
2954
+ this.config.model,
2955
+ usage.input_tokens,
2956
+ usage.output_tokens,
2957
+ usage.cache_creation_input_tokens ?? 0,
2958
+ usage.cache_read_input_tokens ?? 0
2959
+ );
2960
+ const toolUses = response.content.filter(
2961
+ (block) => block.type === "tool_use"
2962
+ );
2963
+ for (const tool of toolUses) {
2964
+ callbacks.onToolCall?.(tool.id, tool.name, tool.input);
2965
+ }
2966
+ return {
2967
+ text: fullText,
2968
+ toolCalls: toolUses.map((t) => ({
2969
+ id: t.id,
2970
+ name: t.name,
2971
+ input: t.input
2972
+ })),
2973
+ usage: {
2974
+ inputTokens: usage.input_tokens,
2975
+ outputTokens: usage.output_tokens,
2976
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
2977
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0
2978
+ },
2979
+ cost,
2980
+ stopReason: response.stop_reason === "tool_use" ? "tool_use" : "end_turn",
2981
+ rawResponse: response
2982
+ };
2983
+ }
2984
+ };
2985
+ var OpenRouterProvider = class {
2986
+ client;
2987
+ config;
2988
+ openaiTools;
2989
+ constructor(config) {
2990
+ this.config = config;
2991
+ this.client = new OpenRouterClient({
2992
+ apiKey: config.apiKey
2993
+ });
2994
+ this.openaiTools = convertToOpenAITools(config.tools);
2995
+ }
2996
+ getModel() {
2997
+ return this.config.model;
2998
+ }
2999
+ async countTokens(_messages) {
3000
+ const text = JSON.stringify(_messages);
3001
+ return Math.ceil(text.length / 4);
3002
+ }
3003
+ /**
3004
+ * Convert Anthropic message format to OpenAI format
3005
+ */
3006
+ convertMessages(messages) {
3007
+ const result = [];
3008
+ result.push({
3009
+ role: "system",
3010
+ content: this.config.systemPrompt
3011
+ });
3012
+ for (const msg of messages) {
3013
+ if (msg.role === "user") {
3014
+ if (typeof msg.content === "string") {
3015
+ result.push({ role: "user", content: msg.content });
3016
+ } else if (Array.isArray(msg.content)) {
3017
+ const toolResults = msg.content.filter(
3018
+ (block) => block.type === "tool_result"
3019
+ );
3020
+ if (toolResults.length > 0) {
3021
+ for (const tr of toolResults) {
3022
+ const toolResult = tr;
3023
+ result.push({
3024
+ role: "tool",
3025
+ tool_call_id: toolResult.tool_use_id,
3026
+ content: typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content)
3027
+ });
3028
+ }
3029
+ } else {
3030
+ const textContent = msg.content.filter((block) => block.type === "text").map((block) => block.text).join("");
3031
+ if (textContent) {
3032
+ result.push({ role: "user", content: textContent });
3033
+ }
3034
+ }
3035
+ }
3036
+ } else if (msg.role === "assistant") {
3037
+ if (typeof msg.content === "string") {
3038
+ result.push({ role: "assistant", content: msg.content });
3039
+ } else if (Array.isArray(msg.content)) {
3040
+ const textBlocks = msg.content.filter(
3041
+ (block) => block.type === "text"
3042
+ );
3043
+ const toolUses = msg.content.filter(
3044
+ (block) => block.type === "tool_use"
3045
+ );
3046
+ const textContent = textBlocks.map((b) => b.text).join("");
3047
+ if (toolUses.length > 0) {
3048
+ result.push({
3049
+ role: "assistant",
3050
+ content: textContent || null,
3051
+ tool_calls: toolUses.map((t) => ({
3052
+ id: t.id,
3053
+ type: "function",
3054
+ function: {
3055
+ name: t.name,
3056
+ arguments: JSON.stringify(t.input)
3057
+ }
3058
+ }))
3059
+ });
3060
+ } else {
3061
+ result.push({ role: "assistant", content: textContent });
3062
+ }
3063
+ }
3064
+ }
3065
+ }
3066
+ return result;
3067
+ }
3068
+ async chat(messages) {
3069
+ const openaiMessages = this.convertMessages(messages);
3070
+ const response = await this.client.createChatCompletion({
3071
+ model: this.config.model,
3072
+ messages: openaiMessages,
3073
+ tools: this.openaiTools.length > 0 ? this.openaiTools : void 0,
3074
+ max_tokens: this.config.maxTokens
3075
+ });
3076
+ const choice = response.choices[0];
3077
+ const usage = response.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
3078
+ const cost = calculateOpenRouterCost(
3079
+ this.config.model,
3080
+ usage.prompt_tokens,
3081
+ usage.completion_tokens
3082
+ );
3083
+ const toolCalls = choice.message.tool_calls ?? [];
3084
+ return {
3085
+ text: choice.message.content ?? "",
3086
+ toolCalls: toolCalls.map((tc) => ({
3087
+ id: tc.id,
3088
+ name: tc.function.name,
3089
+ input: JSON.parse(tc.function.arguments)
3090
+ })),
3091
+ usage: {
3092
+ inputTokens: usage.prompt_tokens,
3093
+ outputTokens: usage.completion_tokens,
3094
+ cacheCreationTokens: 0,
3095
+ cacheReadTokens: 0
3096
+ },
3097
+ cost,
3098
+ stopReason: choice.finish_reason === "tool_calls" ? "tool_use" : "end_turn",
3099
+ rawResponse: response
3100
+ };
3101
+ }
3102
+ async streamChat(messages, callbacks) {
3103
+ const openaiMessages = this.convertMessages(messages);
3104
+ let fullText = "";
3105
+ const toolCallsInProgress = /* @__PURE__ */ new Map();
3106
+ let finishReason = null;
3107
+ let usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
3108
+ const stream = this.client.createStreamingChatCompletion({
3109
+ model: this.config.model,
3110
+ messages: openaiMessages,
3111
+ tools: this.openaiTools.length > 0 ? this.openaiTools : void 0,
3112
+ max_tokens: this.config.maxTokens
3113
+ });
3114
+ for await (const chunk of stream) {
3115
+ const choice = chunk.choices[0];
3116
+ if (!choice) continue;
3117
+ if (choice.delta.content) {
3118
+ fullText += choice.delta.content;
3119
+ callbacks.onText?.(choice.delta.content);
3120
+ }
3121
+ if (choice.delta.tool_calls) {
3122
+ for (const tcDelta of choice.delta.tool_calls) {
3123
+ const existing = toolCallsInProgress.get(tcDelta.index);
3124
+ if (!existing) {
3125
+ toolCallsInProgress.set(tcDelta.index, {
3126
+ id: tcDelta.id ?? "",
3127
+ name: tcDelta.function?.name ?? "",
3128
+ arguments: tcDelta.function?.arguments ?? ""
3129
+ });
3130
+ } else {
3131
+ if (tcDelta.id) existing.id = tcDelta.id;
3132
+ if (tcDelta.function?.name) existing.name = tcDelta.function.name;
3133
+ if (tcDelta.function?.arguments) existing.arguments += tcDelta.function.arguments;
3134
+ }
3135
+ }
3136
+ }
3137
+ if (choice.finish_reason) {
3138
+ finishReason = choice.finish_reason;
3139
+ }
3140
+ if (chunk.usage) {
3141
+ usage = chunk.usage;
3142
+ }
3143
+ }
3144
+ const toolCalls = [];
3145
+ for (const [, tc] of toolCallsInProgress) {
3146
+ try {
3147
+ const input = JSON.parse(tc.arguments || "{}");
3148
+ toolCalls.push({ id: tc.id, name: tc.name, input });
3149
+ callbacks.onToolCall?.(tc.id, tc.name, input);
3150
+ } catch {
3151
+ }
3152
+ }
3153
+ const cost = calculateOpenRouterCost(
3154
+ this.config.model,
3155
+ usage.prompt_tokens,
3156
+ usage.completion_tokens
3157
+ );
3158
+ return {
3159
+ text: fullText,
3160
+ toolCalls,
3161
+ usage: {
3162
+ inputTokens: usage.prompt_tokens,
3163
+ outputTokens: usage.completion_tokens,
3164
+ cacheCreationTokens: 0,
3165
+ cacheReadTokens: 0
3166
+ },
3167
+ cost,
3168
+ stopReason: finishReason === "tool_calls" ? "tool_use" : "end_turn"
3169
+ };
3170
+ }
3171
+ };
3172
+ function createLLMProvider(config) {
3173
+ if (config.provider === "openrouter") {
3174
+ return new OpenRouterProvider(config);
3175
+ }
3176
+ return new AnthropicProvider(config);
3177
+ }
3178
+
2548
3179
  // src/agent/loop.ts
2549
3180
  var MAX_TOOL_RESULT_CHARS = 8e3;
2550
3181
  function truncateToolResult(result, toolName) {
@@ -2729,82 +3360,36 @@ function extractTokenInfo(token) {
2729
3360
  price: token.price ?? token.probability
2730
3361
  };
2731
3362
  }
2732
- var DEFAULT_SYSTEM_PROMPT = `You are Quantish, an AI coding and trading agent. Be concise.
2733
-
2734
- ## APIs
2735
-
2736
- TRADING (requires QUANTISH_API_KEY):
2737
- - URL: https://quantish-sdk-production.up.railway.app/mcp/execute
2738
- - Format: JSON-RPC 2.0 { jsonrpc: '2.0', method: 'tools/call', params: { name, arguments }, id }
2739
- - Tools: get_balances, get_positions, place_order, cancel_order, get_orders, get_orderbook, get_price
3363
+ var DEFAULT_SYSTEM_PROMPT = `You are Quantish, an AI coding and trading agent.
2740
3364
 
2741
- DISCOVERY (free):
2742
- - URL: https://quantish.live/mcp/execute
2743
- - Format: { name, arguments }
2744
- - Key: qm_ueQeqrmvZyHtR1zuVbLYkhx0fKyVAuV8
2745
- - Tools: search_markets, get_market_details, get_trending_markets
3365
+ You have two sets of capabilities:
2746
3366
 
2747
- ## Response Structures (IMPORTANT - use these field paths)
2748
-
2749
- search_markets / get_trending_markets returns:
2750
- {
2751
- "found": N,
2752
- "markets": [{ "platform", "id", "title", "markets": [{ "marketId", "question", "outcomes": [{ "name", "price" }], "clobTokenIds": "[json_array]", "conditionId" }] }]
2753
- }
2754
-
2755
- get_market_details returns:
2756
- {
2757
- "platform": "polymarket",
2758
- "id": "12345",
2759
- "conditionId": "0x...",
2760
- "title": "Market Title",
2761
- "clobTokenIds": "["TOKEN_YES","TOKEN_NO"]",
2762
- "markets": [{
2763
- "marketId": "67890",
2764
- "question": "Question?",
2765
- "outcomes": [{ "name": "Yes", "price": 0.55 }, { "name": "No", "price": 0.45 }],
2766
- "clobTokenIds": "["TOKEN_YES","TOKEN_NO"]"
2767
- }]
2768
- }
3367
+ ## Trading Tools (via MCP)
3368
+ You can interact with Polymarket prediction markets:
3369
+ - Check wallet balances and positions
3370
+ - Place, cancel, and manage orders
3371
+ - Transfer funds and claim winnings
3372
+ - Get market prices and orderbook data
2769
3373
 
2770
- KEY FIELDS:
2771
- - market.id = top-level ID for get_market_details
2772
- - market.markets[0].marketId = sub-market ID
2773
- - market.markets[0].outcomes[].name = "Yes"/"No" or outcome name
2774
- - market.markets[0].outcomes[].price = decimal 0-1
2775
- - JSON.parse(market.clobTokenIds || market.markets[0].clobTokenIds) = token IDs array
2776
- - market.conditionId = condition ID for trading
3374
+ ## Coding Tools (local)
3375
+ You can work with the local filesystem:
3376
+ - Read and write files
3377
+ - List directories and search with grep
3378
+ - Run shell commands
3379
+ - Use git for version control
2777
3380
 
2778
- ## Standalone App Code
3381
+ ## Guidelines
3382
+ - Be concise and helpful
3383
+ - When making trades, always confirm details before proceeding
3384
+ - Prices on Polymarket are between 0.01 and 0.99 (probabilities)
3385
+ - Minimum order value is $1
3386
+ - When writing code, follow existing patterns and conventions
3387
+ - For dangerous operations (rm, sudo), explain what you're doing
2779
3388
 
2780
- Trading helper:
2781
- async function callTradingTool(name, args = {}) {
2782
- const res = await fetch('https://quantish-sdk-production.up.railway.app/mcp/execute', {
2783
- method: 'POST',
2784
- headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.QUANTISH_API_KEY },
2785
- body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/call', params: { name, arguments: args }, id: Date.now() })
2786
- });
2787
- return JSON.parse((await res.json()).result.content[0].text);
2788
- }
2789
-
2790
- Discovery helper:
2791
- async function callDiscoveryTool(name, args = {}) {
2792
- const res = await fetch('https://quantish.live/mcp/execute', {
2793
- method: 'POST',
2794
- headers: { 'Content-Type': 'application/json', 'X-API-Key': 'qm_ueQeqrmvZyHtR1zuVbLYkhx0fKyVAuV8' },
2795
- body: JSON.stringify({ name, arguments: args })
2796
- });
2797
- return JSON.parse((await res.json()).result.content[0].text);
2798
- }
2799
-
2800
- ## Rules
2801
- 1. Never use @modelcontextprotocol/sdk - use fetch()
2802
- 2. Always create .env.example and use dotenv
2803
- 3. Never hardcode/mock data - always fetch real data
2804
- 4. Check logs before restarting servers
2805
- 5. PREFER edit_lines over edit_file - uses line numbers, saves tokens`;
3389
+ You help users build trading bots and agents by combining coding skills with trading capabilities.`;
2806
3390
  var Agent = class {
2807
3391
  anthropic;
3392
+ llmProvider;
2808
3393
  mcpClient;
2809
3394
  mcpClientManager;
2810
3395
  config;
@@ -2825,6 +3410,8 @@ var Agent = class {
2825
3410
  this.config = {
2826
3411
  enableLocalTools: true,
2827
3412
  enableMCPTools: true,
3413
+ provider: "anthropic",
3414
+ // Default to Anthropic
2828
3415
  // Default context editing: clear old tool uses when context exceeds 100k tokens
2829
3416
  contextEditing: config.contextEditing || [
2830
3417
  {
@@ -2839,14 +3426,176 @@ var Agent = class {
2839
3426
  if (this.config.contextEditing && this.config.contextEditing.length > 0) {
2840
3427
  headers["anthropic-beta"] = "context-management-2025-06-27";
2841
3428
  }
2842
- this.anthropic = new Anthropic({
2843
- apiKey: config.anthropicApiKey,
3429
+ const anthropicKey = config.anthropicApiKey || "placeholder";
3430
+ this.anthropic = new Anthropic2({
3431
+ apiKey: anthropicKey,
2844
3432
  defaultHeaders: Object.keys(headers).length > 0 ? headers : void 0
2845
3433
  });
2846
3434
  this.mcpClient = config.mcpClient;
2847
3435
  this.mcpClientManager = config.mcpClientManager;
2848
3436
  this.workingDirectory = config.workingDirectory || process.cwd();
2849
3437
  }
3438
+ /**
3439
+ * Get the API key for the current provider
3440
+ */
3441
+ getApiKey() {
3442
+ if (this.config.provider === "openrouter") {
3443
+ return this.config.openrouterApiKey || "";
3444
+ }
3445
+ return this.config.anthropicApiKey || "";
3446
+ }
3447
+ /**
3448
+ * Check if using OpenRouter provider
3449
+ */
3450
+ isOpenRouter() {
3451
+ return this.config.provider === "openrouter";
3452
+ }
3453
+ /**
3454
+ * Get the current provider name
3455
+ */
3456
+ getProvider() {
3457
+ return this.config.provider || "anthropic";
3458
+ }
3459
+ /**
3460
+ * Set the LLM provider
3461
+ */
3462
+ setProvider(provider) {
3463
+ this.config.provider = provider;
3464
+ this.llmProvider = void 0;
3465
+ }
3466
+ /**
3467
+ * Get or create the LLM provider instance
3468
+ */
3469
+ async getOrCreateProvider() {
3470
+ if (this.llmProvider) {
3471
+ return this.llmProvider;
3472
+ }
3473
+ const allTools = await this.getAllTools();
3474
+ const systemPrompt = this.config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
3475
+ const model = this.config.model ?? DEFAULT_MODEL;
3476
+ const maxTokens = this.config.maxTokens ?? 8192;
3477
+ this.llmProvider = createLLMProvider({
3478
+ provider: this.config.provider || "anthropic",
3479
+ apiKey: this.getApiKey(),
3480
+ model,
3481
+ maxTokens,
3482
+ systemPrompt,
3483
+ tools: allTools,
3484
+ contextEditing: this.config.contextEditing
3485
+ });
3486
+ return this.llmProvider;
3487
+ }
3488
+ /**
3489
+ * Run the agent using the provider abstraction (for OpenRouter and future providers)
3490
+ */
3491
+ async runWithProvider(userMessage) {
3492
+ const maxIterations = this.config.maxIterations ?? 200;
3493
+ const useStreaming = this.config.streaming ?? true;
3494
+ const provider = await this.getOrCreateProvider();
3495
+ const contextMessage = `[Working directory: ${this.workingDirectory}]
3496
+
3497
+ ${userMessage}`;
3498
+ this.conversationHistory.push({
3499
+ role: "user",
3500
+ content: contextMessage
3501
+ });
3502
+ const toolCalls = [];
3503
+ let iterations = 0;
3504
+ let finalText = "";
3505
+ while (iterations < maxIterations) {
3506
+ iterations++;
3507
+ this.config.onStreamStart?.();
3508
+ let response;
3509
+ if (useStreaming) {
3510
+ response = await provider.streamChat(this.conversationHistory, {
3511
+ onText: (text) => {
3512
+ finalText += text;
3513
+ this.config.onText?.(text, false);
3514
+ },
3515
+ onThinking: (text) => {
3516
+ this.config.onThinking?.(text);
3517
+ },
3518
+ onToolCall: (id, name, input) => {
3519
+ this.config.onToolCall?.(name, input);
3520
+ }
3521
+ });
3522
+ if (response.text) {
3523
+ this.config.onText?.("", true);
3524
+ }
3525
+ } else {
3526
+ response = await provider.chat(this.conversationHistory);
3527
+ if (response.text) {
3528
+ finalText += response.text;
3529
+ this.config.onText?.(response.text, true);
3530
+ }
3531
+ }
3532
+ this.config.onStreamEnd?.();
3533
+ this.updateTokenUsage({
3534
+ input_tokens: response.usage.inputTokens,
3535
+ output_tokens: response.usage.outputTokens,
3536
+ cache_creation_input_tokens: response.usage.cacheCreationTokens,
3537
+ cache_read_input_tokens: response.usage.cacheReadTokens
3538
+ });
3539
+ const responseContent = [];
3540
+ if (response.text) {
3541
+ responseContent.push({ type: "text", text: response.text });
3542
+ }
3543
+ for (const tc of response.toolCalls) {
3544
+ responseContent.push({
3545
+ type: "tool_use",
3546
+ id: tc.id,
3547
+ name: tc.name,
3548
+ input: tc.input
3549
+ });
3550
+ }
3551
+ if (response.toolCalls.length === 0) {
3552
+ this.conversationHistory.push({
3553
+ role: "assistant",
3554
+ content: responseContent
3555
+ });
3556
+ break;
3557
+ }
3558
+ const toolResults = [];
3559
+ for (const toolCall2 of response.toolCalls) {
3560
+ await new Promise((resolve2) => setImmediate(resolve2));
3561
+ const { result, source } = await this.executeTool(
3562
+ toolCall2.name,
3563
+ toolCall2.input
3564
+ );
3565
+ const success2 = !(result && typeof result === "object" && "error" in result);
3566
+ this.config.onToolResult?.(toolCall2.name, result, success2);
3567
+ toolCalls.push({
3568
+ name: toolCall2.name,
3569
+ input: toolCall2.input,
3570
+ result,
3571
+ source
3572
+ });
3573
+ toolResults.push({
3574
+ type: "tool_result",
3575
+ tool_use_id: toolCall2.id,
3576
+ content: JSON.stringify(result)
3577
+ });
3578
+ }
3579
+ this.conversationHistory.push({
3580
+ role: "assistant",
3581
+ content: responseContent
3582
+ });
3583
+ this.conversationHistory.push({
3584
+ role: "user",
3585
+ content: toolResults
3586
+ });
3587
+ this.truncateLastToolResults();
3588
+ if (response.stopReason === "end_turn" && response.toolCalls.length === 0) {
3589
+ break;
3590
+ }
3591
+ }
3592
+ return {
3593
+ text: finalText,
3594
+ toolCalls,
3595
+ iterations,
3596
+ tokenUsage: { ...this.cumulativeTokenUsage }
3597
+ };
3598
+ }
2850
3599
  /**
2851
3600
  * Get all available tools
2852
3601
  */
@@ -2899,16 +3648,16 @@ var Agent = class {
2899
3648
  }
2900
3649
  /**
2901
3650
  * Run the agent with a user message (supports streaming)
2902
- * @param userMessage - The user's input message
2903
- * @param options - Optional configuration including abort signal
2904
3651
  */
2905
- async run(userMessage, options) {
2906
- const maxIterations = this.config.maxIterations ?? 200;
3652
+ async run(userMessage) {
3653
+ if (this.config.provider === "openrouter") {
3654
+ return this.runWithProvider(userMessage);
3655
+ }
3656
+ const maxIterations = this.config.maxIterations ?? 15;
2907
3657
  const model = this.config.model ?? "claude-sonnet-4-5-20250929";
2908
3658
  const maxTokens = this.config.maxTokens ?? 8192;
2909
3659
  const systemPrompt = this.config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
2910
3660
  const useStreaming = this.config.streaming ?? true;
2911
- const signal = options?.signal;
2912
3661
  const allTools = await this.getAllTools();
2913
3662
  const contextManagement = this.config.contextEditing && this.config.contextEditing.length > 0 ? { edits: this.config.contextEditing } : void 0;
2914
3663
  const contextMessage = `[Working directory: ${this.workingDirectory}]
@@ -2922,9 +3671,6 @@ ${userMessage}`;
2922
3671
  let iterations = 0;
2923
3672
  let finalText = "";
2924
3673
  while (iterations < maxIterations) {
2925
- if (signal?.aborted) {
2926
- throw new Error("Operation aborted by user");
2927
- }
2928
3674
  iterations++;
2929
3675
  this.config.onStreamStart?.();
2930
3676
  let response;
@@ -2949,12 +3695,8 @@ ${userMessage}`;
2949
3695
  if (contextManagement) {
2950
3696
  streamOptions.context_management = contextManagement;
2951
3697
  }
2952
- const stream = this.anthropic.messages.stream(streamOptions, { signal });
3698
+ const stream = this.anthropic.messages.stream(streamOptions);
2953
3699
  for await (const event of stream) {
2954
- if (signal?.aborted) {
2955
- stream.controller.abort();
2956
- throw new Error("Operation aborted by user");
2957
- }
2958
3700
  if (event.type === "content_block_delta") {
2959
3701
  const delta = event.delta;
2960
3702
  if (delta.type === "text_delta" && delta.text) {
@@ -3019,11 +3761,7 @@ ${userMessage}`;
3019
3761
  }
3020
3762
  const toolResults = [];
3021
3763
  for (const toolUse of toolUses) {
3022
- if (signal?.aborted) {
3023
- throw new Error("Operation aborted by user");
3024
- }
3025
3764
  this.config.onToolCall?.(toolUse.name, toolUse.input);
3026
- await new Promise((resolve2) => setImmediate(resolve2));
3027
3765
  const { result, source } = await this.executeTool(
3028
3766
  toolUse.name,
3029
3767
  toolUse.input
@@ -3204,19 +3942,34 @@ ${userMessage}`;
3204
3942
  * Set the model to use for future requests
3205
3943
  */
3206
3944
  setModel(modelIdOrAlias) {
3207
- const resolvedId = resolveModelId(modelIdOrAlias);
3945
+ let resolvedId = resolveModelId(modelIdOrAlias);
3946
+ let displayName;
3947
+ if (resolvedId) {
3948
+ const modelConfig = getModelConfig(resolvedId);
3949
+ displayName = modelConfig?.displayName;
3950
+ } else {
3951
+ resolvedId = resolveOpenRouterModelId(modelIdOrAlias);
3952
+ if (resolvedId) {
3953
+ const orConfig = getOpenRouterModelConfig(resolvedId);
3954
+ displayName = orConfig?.displayName ?? resolvedId;
3955
+ if (!this.isOpenRouter() && resolvedId.includes("/")) {
3956
+ this.config.provider = "openrouter";
3957
+ }
3958
+ }
3959
+ }
3208
3960
  if (!resolvedId) {
3209
- const availableModels = Object.values(MODELS).map((m) => m.name).join(", ");
3961
+ const anthropicModels = Object.values(MODELS).map((m) => m.name).join(", ");
3962
+ const orModels = Object.values(OPENROUTER_MODELS).slice(0, 5).map((m) => m.name).join(", ");
3210
3963
  return {
3211
3964
  success: false,
3212
- error: `Unknown model: "${modelIdOrAlias}". Available: ${availableModels}`
3965
+ error: `Unknown model: "${modelIdOrAlias}". Anthropic: ${anthropicModels}. OpenRouter: ${orModels}, ...`
3213
3966
  };
3214
3967
  }
3215
3968
  this.config.model = resolvedId;
3216
- const modelConfig = getModelConfig(resolvedId);
3969
+ this.llmProvider = void 0;
3217
3970
  return {
3218
3971
  success: true,
3219
- model: modelConfig?.displayName ?? resolvedId
3972
+ model: displayName ?? resolvedId
3220
3973
  };
3221
3974
  }
3222
3975
  /**
@@ -3342,7 +4095,7 @@ import { useState, useCallback, useRef, useEffect } from "react";
3342
4095
  import { Box, Text, useApp, useInput } from "ink";
3343
4096
  import TextInput from "ink-text-input";
3344
4097
  import Spinner from "ink-spinner";
3345
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4098
+ import { jsx, jsxs } from "react/jsx-runtime";
3346
4099
  function formatTokenCount(count) {
3347
4100
  if (count < 1e3) return String(count);
3348
4101
  if (count < 1e5) return `${(count / 1e3).toFixed(1)}k`;
@@ -3357,7 +4110,8 @@ var SLASH_COMMANDS = [
3357
4110
  { cmd: "/help", desc: "Show available commands" },
3358
4111
  { cmd: "/clear", desc: "Clear conversation history" },
3359
4112
  { cmd: "/compact", desc: "Summarize conversation to save tokens" },
3360
- { cmd: "/model", desc: "Switch model (opus, sonnet, haiku)" },
4113
+ { cmd: "/model", desc: "Switch model (opus, sonnet, haiku, minimax, etc.)" },
4114
+ { cmd: "/provider", desc: "Switch LLM provider (anthropic, openrouter)" },
3361
4115
  { cmd: "/cost", desc: "Show session cost breakdown" },
3362
4116
  { cmd: "/tools", desc: "List available tools" },
3363
4117
  { cmd: "/config", desc: "Show configuration info" },
@@ -3423,7 +4177,8 @@ function App({ agent, onExit }) {
3423
4177
  content: `\u{1F4DA} Available Commands:
3424
4178
  /clear - Clear conversation history
3425
4179
  /compact - Summarize conversation (keeps context, saves tokens)
3426
- /model - Switch model (opus, sonnet, haiku)
4180
+ /model - Switch model (opus, sonnet, haiku, minimax, deepseek, etc.)
4181
+ /provider - Switch LLM provider (anthropic, openrouter)
3427
4182
  /cost - Show session cost breakdown
3428
4183
  /help - Show this help message
3429
4184
  /tools - List available tools
@@ -3576,30 +4331,46 @@ Use /stop <id> to stop a process.`
3576
4331
  case "model":
3577
4332
  if (!args) {
3578
4333
  const currentModel = agent.getModel();
4334
+ const currentProvider = agent.getProvider();
3579
4335
  const modelConfig = getModelConfig(currentModel);
3580
- const models = listModels();
3581
- const modelList = models.map((m) => {
4336
+ const orModelConfig = getOpenRouterModelConfig(currentModel);
4337
+ const displayName = modelConfig?.displayName || orModelConfig?.displayName || currentModel;
4338
+ const anthropicModels = listModels();
4339
+ const anthropicList = anthropicModels.map((m) => {
3582
4340
  const isCurrent = m.id === currentModel ? " (current)" : "";
3583
4341
  return ` ${m.name}${isCurrent} - ${m.description}`;
3584
4342
  }).join("\n");
4343
+ const orModels = listOpenRouterModels().slice(0, 8);
4344
+ const orList = orModels.map((m) => {
4345
+ const isCurrent = m.id === currentModel ? " (current)" : "";
4346
+ return ` ${m.name}${isCurrent} - ${m.description.slice(0, 50)}...`;
4347
+ }).join("\n");
3585
4348
  setMessages((prev) => [...prev, {
3586
4349
  role: "system",
3587
- content: `\u{1F916} Current model: ${modelConfig?.displayName || currentModel}
4350
+ content: `\u{1F916} Current: ${displayName} (${currentProvider})
4351
+
4352
+ Anthropic Models:
4353
+ ${anthropicList}
3588
4354
 
3589
- Available models:
3590
- ${modelList}
4355
+ OpenRouter Models (selection):
4356
+ ${orList}
4357
+ ... and many more! Use any OpenRouter model ID like 'minimax/minimax-m2.1'
3591
4358
 
3592
- Usage: /model <name> (e.g., /model haiku, /model opus)`
4359
+ Usage: /model <name> (e.g., /model haiku, /model minimax)
4360
+ Using an OpenRouter model auto-switches to OpenRouter provider.`
3593
4361
  }]);
3594
4362
  return true;
3595
4363
  }
3596
4364
  const result = agent.setModel(args);
3597
4365
  if (result.success) {
3598
- const newConfig = getModelConfig(agent.getModel());
4366
+ const anthropicConfig = getModelConfig(agent.getModel());
4367
+ const orConfig = getOpenRouterModelConfig(agent.getModel());
4368
+ const description = anthropicConfig?.description || orConfig?.description || "";
4369
+ const providerInfo = agent.isOpenRouter() ? " (OpenRouter)" : " (Anthropic)";
3599
4370
  setMessages((prev) => [...prev, {
3600
4371
  role: "system",
3601
- content: `\u2705 Switched to ${result.model}
3602
- ${newConfig?.description || ""}`
4372
+ content: `\u2705 Switched to ${result.model}${providerInfo}
4373
+ ${description}`
3603
4374
  }]);
3604
4375
  } else {
3605
4376
  setMessages((prev) => [...prev, {
@@ -3608,6 +4379,43 @@ Usage: /model <name> (e.g., /model haiku, /model opus)`
3608
4379
  }]);
3609
4380
  }
3610
4381
  return true;
4382
+ case "provider":
4383
+ if (!args) {
4384
+ const currentProvider = agent.getProvider();
4385
+ setMessages((prev) => [...prev, {
4386
+ role: "system",
4387
+ content: `\u{1F527} LLM Provider
4388
+
4389
+ Current: ${currentProvider}
4390
+
4391
+ Available providers:
4392
+ anthropic - Claude models (Opus, Sonnet, Haiku)
4393
+ openrouter - Multi-provider access (MiniMax, DeepSeek, Gemini, etc.)
4394
+
4395
+ Usage: /provider <name> (e.g., /provider openrouter)
4396
+
4397
+ Note: When switching to OpenRouter, make sure OPENROUTER_API_KEY is set.
4398
+ You can also just use /model with an OpenRouter model name.`
4399
+ }]);
4400
+ return true;
4401
+ }
4402
+ const providerArg = args.toLowerCase();
4403
+ if (providerArg !== "anthropic" && providerArg !== "openrouter") {
4404
+ setMessages((prev) => [...prev, {
4405
+ role: "system",
4406
+ content: `\u274C Unknown provider: "${args}". Use: anthropic, openrouter`
4407
+ }]);
4408
+ return true;
4409
+ }
4410
+ agent.setProvider(providerArg);
4411
+ const providerModels = providerArg === "openrouter" ? "minimax, deepseek, gemini, grok, devstral" : "opus, sonnet, haiku";
4412
+ setMessages((prev) => [...prev, {
4413
+ role: "system",
4414
+ content: `\u2705 Switched to ${providerArg} provider
4415
+ Available models: ${providerModels}
4416
+ Use /model to select a model.`
4417
+ }]);
4418
+ return true;
3611
4419
  case "cost":
3612
4420
  const usage = agent.getTokenUsage();
3613
4421
  const sessionCost = agent.getSessionCost();
@@ -3680,7 +4488,7 @@ Last API Call Cost:
3680
4488
  completedToolCalls.current = [];
3681
4489
  abortController.current = new AbortController();
3682
4490
  try {
3683
- const result = await agent.run(trimmed, { signal: abortController.current?.signal });
4491
+ const result = await agent.run(trimmed);
3684
4492
  if (isInterrupted) {
3685
4493
  setMessages((prev) => [...prev, {
3686
4494
  role: "system",
@@ -3819,22 +4627,18 @@ Stopped ${count} background process${count > 1 ? "es" : ""}.`);
3819
4627
  msg.role === "system" && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "gray", italic: true, children: msg.content }) })
3820
4628
  ] }, i)) }),
3821
4629
  currentToolCalls.length > 0 && /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, marginLeft: 2, children: currentToolCalls.map((tc, i) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
3822
- /* @__PURE__ */ jsx(Box, { children: tc.pending ? /* @__PURE__ */ jsxs(Fragment, { children: [
3823
- /* @__PURE__ */ jsx(Text, { color: "yellow", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
3824
- /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
4630
+ /* @__PURE__ */ jsxs(Box, { children: [
4631
+ tc.pending ? /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
4632
+ /* @__PURE__ */ jsx(Spinner, { type: "dots" }),
3825
4633
  " ",
3826
4634
  tc.name
3827
- ] }),
3828
- /* @__PURE__ */ jsx(Text, { color: "gray", children: formatArgs(tc.args) }),
3829
- /* @__PURE__ */ jsx(Text, { color: "yellow", dimColor: true, children: " Running..." })
3830
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3831
- /* @__PURE__ */ jsxs(Text, { color: tc.success ? "green" : "red", children: [
4635
+ ] }) : /* @__PURE__ */ jsxs(Text, { color: tc.success ? "blue" : "red", children: [
3832
4636
  tc.success ? "\u2713" : "\u2717",
3833
4637
  " ",
3834
4638
  tc.name
3835
4639
  ] }),
3836
4640
  /* @__PURE__ */ jsx(Text, { color: "gray", children: formatArgs(tc.args) })
3837
- ] }) }),
4641
+ ] }),
3838
4642
  !tc.pending && tc.result && /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
3839
4643
  "\u2192 ",
3840
4644
  formatResult(tc.result, 100)
@@ -3936,19 +4740,8 @@ program.name("quantish").description("AI coding & trading agent for Polymarket")
3936
4740
  program.command("init").description("Configure Quantish CLI with your API keys").action(async () => {
3937
4741
  await runSetup();
3938
4742
  });
3939
- program.command("config").description("View or edit configuration").option("-s, --show", "Show current configuration").option("-c, --clear", "Clear all configuration").option("--path", "Show config file path").option("--export", "Export configuration as .env format").option("--show-keys", "Show full API keys (use with caution)").option("--server <url>", "Set custom Trading MCP server URL").action(async (options) => {
4743
+ program.command("config").description("View or edit configuration").option("-s, --show", "Show current configuration").option("-c, --clear", "Clear all configuration").option("--path", "Show config file path").option("--export", "Export configuration as .env format").option("--show-keys", "Show full API keys (use with caution)").action(async (options) => {
3940
4744
  const config = getConfigManager();
3941
- if (options.server) {
3942
- try {
3943
- new URL(options.server);
3944
- } catch {
3945
- error("Invalid URL format. Please provide a valid URL (e.g., https://your-server.com/mcp)");
3946
- return;
3947
- }
3948
- config.set("mcpServerUrl", options.server);
3949
- success(`Trading MCP server URL set to: ${options.server}`);
3950
- return;
3951
- }
3952
4745
  if (options.path) {
3953
4746
  console.log(config.getConfigPath());
3954
4747
  return;
@@ -3967,11 +4760,15 @@ program.command("config").description("View or edit configuration").option("-s,
3967
4760
  if (all2.anthropicApiKey) {
3968
4761
  console.log(`ANTHROPIC_API_KEY=${all2.anthropicApiKey}`);
3969
4762
  }
4763
+ if (all2.openrouterApiKey) {
4764
+ console.log(`OPENROUTER_API_KEY=${all2.openrouterApiKey}`);
4765
+ }
3970
4766
  if (all2.quantishApiKey) {
3971
4767
  console.log(`QUANTISH_API_KEY=${all2.quantishApiKey}`);
3972
4768
  }
3973
4769
  console.log(`QUANTISH_MCP_URL=${all2.mcpServerUrl}`);
3974
4770
  console.log(`QUANTISH_MODEL=${all2.model || "claude-sonnet-4-5-20250929"}`);
4771
+ console.log(`QUANTISH_PROVIDER=${all2.provider || "anthropic"}`);
3975
4772
  console.log();
3976
4773
  console.log(chalk3.dim("# Discovery MCP (public, read-only market data)"));
3977
4774
  console.log(`QUANTISH_DISCOVERY_URL=https://quantish.live/mcp`);
@@ -3986,11 +4783,14 @@ program.command("config").description("View or edit configuration").option("-s,
3986
4783
  printDivider();
3987
4784
  if (options.showKeys) {
3988
4785
  tableRow("Anthropic API Key", all.anthropicApiKey || chalk3.dim("Not set"));
4786
+ tableRow("OpenRouter API Key", all.openrouterApiKey || chalk3.dim("Not set"));
3989
4787
  tableRow("Quantish API Key", all.quantishApiKey || chalk3.dim("Not set"));
3990
4788
  } else {
3991
4789
  tableRow("Anthropic API Key", all.anthropicApiKey ? `${all.anthropicApiKey.slice(0, 10)}...` : chalk3.dim("Not set"));
4790
+ tableRow("OpenRouter API Key", all.openrouterApiKey ? `${all.openrouterApiKey.slice(0, 10)}...` : chalk3.dim("Not set"));
3992
4791
  tableRow("Quantish API Key", all.quantishApiKey ? `${all.quantishApiKey.slice(0, 12)}...` : chalk3.dim("Not set"));
3993
4792
  }
4793
+ tableRow("Provider", all.provider || "anthropic");
3994
4794
  tableRow("MCP Server URL", all.mcpServerUrl);
3995
4795
  tableRow("Model", all.model || "claude-sonnet-4-5-20250929");
3996
4796
  printDivider();
@@ -4123,7 +4923,9 @@ async function runInteractiveChat(options = {}) {
4123
4923
  const config = getConfigManager();
4124
4924
  const mcpClientManager = createMCPManager(options);
4125
4925
  const agent = createAgent({
4926
+ provider: config.getProvider(),
4126
4927
  anthropicApiKey: config.getAnthropicApiKey(),
4928
+ openrouterApiKey: config.getOpenRouterApiKey(),
4127
4929
  mcpClientManager,
4128
4930
  model: config.getModel(),
4129
4931
  enableLocalTools: options.enableLocal !== false,
@@ -4256,7 +5058,9 @@ async function runOneShotPrompt(message, options = {}) {
4256
5058
  const config = getConfigManager();
4257
5059
  const mcpClientManager = createMCPManager(options);
4258
5060
  const agent = createAgent({
5061
+ provider: config.getProvider(),
4259
5062
  anthropicApiKey: config.getAnthropicApiKey(),
5063
+ openrouterApiKey: config.getOpenRouterApiKey(),
4260
5064
  mcpClientManager,
4261
5065
  model: config.getModel(),
4262
5066
  enableLocalTools: options.enableLocal !== false,