Merge pull request #877 from TonyGeez/feature/add-model-selector-command

Feature/add model selector command
This commit is contained in:
musi
2025-10-09 20:45:22 +08:00
committed by GitHub
8 changed files with 863 additions and 7 deletions

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@ log.txt
.idea
dist
.DS_Store
.vscode
.vscode
tsconfig.tsbuildinfo

View File

@@ -21,9 +21,11 @@
"dependencies": {
"@fastify/static": "^8.2.0",
"@musistudio/llms": "^1.0.36",
"@inquirer/prompts": "^5.0.0",
"dotenv": "^16.4.7",
"find-process": "^2.0.0",
"json5": "^2.2.3",
"lru-cache": "^11.2.2",
"minimist": "^1.2.8",
"openurl": "^1.1.1",
"rotating-file-stream": "^3.2.7",

244
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@fastify/static':
specifier: ^8.2.0
version: 8.2.0
'@inquirer/prompts':
specifier: ^5.0.0
version: 5.5.0
'@musistudio/llms':
specifier: ^1.0.36
version: 1.0.36(ws@8.18.3)
@@ -23,6 +26,9 @@ importers:
json5:
specifier: ^2.2.3
version: 2.2.3
lru-cache:
specifier: ^11.2.2
version: 11.2.2
minimist:
specifier: ^1.2.8
version: 1.2.8
@@ -259,6 +265,66 @@ packages:
'@modelcontextprotocol/sdk':
optional: true
'@inquirer/checkbox@2.5.0':
resolution: {integrity: sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==}
engines: {node: '>=18'}
'@inquirer/confirm@3.2.0':
resolution: {integrity: sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==}
engines: {node: '>=18'}
'@inquirer/core@9.2.1':
resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==}
engines: {node: '>=18'}
'@inquirer/editor@2.2.0':
resolution: {integrity: sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==}
engines: {node: '>=18'}
'@inquirer/expand@2.3.0':
resolution: {integrity: sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==}
engines: {node: '>=18'}
'@inquirer/figures@1.0.13':
resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==}
engines: {node: '>=18'}
'@inquirer/input@2.3.0':
resolution: {integrity: sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==}
engines: {node: '>=18'}
'@inquirer/number@1.1.0':
resolution: {integrity: sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==}
engines: {node: '>=18'}
'@inquirer/password@2.2.0':
resolution: {integrity: sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==}
engines: {node: '>=18'}
'@inquirer/prompts@5.5.0':
resolution: {integrity: sha512-BHDeL0catgHdcHbSFFUddNzvx/imzJMft+tWDPwTm3hfu8/tApk1HrooNngB2Mb4qY+KaRWF+iZqoVUPeslEog==}
engines: {node: '>=18'}
'@inquirer/rawlist@2.3.0':
resolution: {integrity: sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==}
engines: {node: '>=18'}
'@inquirer/search@1.1.0':
resolution: {integrity: sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==}
engines: {node: '>=18'}
'@inquirer/select@2.5.0':
resolution: {integrity: sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==}
engines: {node: '>=18'}
'@inquirer/type@1.5.5':
resolution: {integrity: sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==}
engines: {node: '>=18'}
'@inquirer/type@2.0.0':
resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==}
engines: {node: '>=18'}
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
@@ -293,6 +359,9 @@ packages:
'@types/node@24.7.0':
resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==}
'@types/wrap-ansi@3.0.0':
resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==}
abstract-logging@2.0.1:
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
@@ -311,6 +380,10 @@ packages:
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@@ -351,6 +424,13 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
cli-width@4.1.0:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -433,6 +513,10 @@ packages:
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
external-editor@3.1.0:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'}
fast-decode-uri-component@1.0.1:
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
@@ -557,6 +641,10 @@ packages:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@@ -665,6 +753,10 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mute-stream@1.0.0:
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
nice-try@1.0.5:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
@@ -712,6 +804,10 @@ packages:
openurl@1.1.1:
resolution: {integrity: sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==}
os-tmpdir@1.0.2:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
p-finally@1.0.0:
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
engines: {node: '>=4'}
@@ -911,6 +1007,10 @@ packages:
tiktoken@1.0.22:
resolution: {integrity: sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA==}
tmp@0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -965,6 +1065,10 @@ packages:
engines: {node: '>= 8'}
hasBin: true
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@@ -988,6 +1092,10 @@ packages:
utf-8-validate:
optional: true
yoctocolors-cjs@2.1.3:
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
engines: {node: '>=18'}
snapshots:
'@anthropic-ai/sdk@0.54.0': {}
@@ -1127,6 +1235,106 @@ snapshots:
- supports-color
- utf-8-validate
'@inquirer/checkbox@2.5.0':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/figures': 1.0.13
'@inquirer/type': 1.5.5
ansi-escapes: 4.3.2
yoctocolors-cjs: 2.1.3
'@inquirer/confirm@3.2.0':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 1.5.5
'@inquirer/core@9.2.1':
dependencies:
'@inquirer/figures': 1.0.13
'@inquirer/type': 2.0.0
'@types/mute-stream': 0.0.4
'@types/node': 22.18.8
'@types/wrap-ansi': 3.0.0
ansi-escapes: 4.3.2
cli-width: 4.1.0
mute-stream: 1.0.0
signal-exit: 4.1.0
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
yoctocolors-cjs: 2.1.3
'@inquirer/editor@2.2.0':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 1.5.5
external-editor: 3.1.0
'@inquirer/expand@2.3.0':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 1.5.5
yoctocolors-cjs: 2.1.3
'@inquirer/figures@1.0.13': {}
'@inquirer/input@2.3.0':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 1.5.5
'@inquirer/number@1.1.0':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 1.5.5
'@inquirer/password@2.2.0':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 1.5.5
ansi-escapes: 4.3.2
'@inquirer/prompts@5.5.0':
dependencies:
'@inquirer/checkbox': 2.5.0
'@inquirer/confirm': 3.2.0
'@inquirer/editor': 2.2.0
'@inquirer/expand': 2.3.0
'@inquirer/input': 2.3.0
'@inquirer/number': 1.1.0
'@inquirer/password': 2.2.0
'@inquirer/rawlist': 2.3.0
'@inquirer/search': 1.1.0
'@inquirer/select': 2.5.0
'@inquirer/rawlist@2.3.0':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 1.5.5
yoctocolors-cjs: 2.1.3
'@inquirer/search@1.1.0':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/figures': 1.0.13
'@inquirer/type': 1.5.5
yoctocolors-cjs: 2.1.3
'@inquirer/select@2.5.0':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/figures': 1.0.13
'@inquirer/type': 1.5.5
ansi-escapes: 4.3.2
yoctocolors-cjs: 2.1.3
'@inquirer/type@1.5.5':
dependencies:
mute-stream: 1.0.0
'@inquirer/type@2.0.0':
dependencies:
mute-stream: 1.0.0
'@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0':
@@ -1182,6 +1390,8 @@ snapshots:
dependencies:
undici-types: 7.14.0
'@types/wrap-ansi@3.0.0': {}
abstract-logging@2.0.1: {}
agent-base@7.1.4: {}
@@ -1197,6 +1407,10 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-escapes@4.3.2:
dependencies:
type-fest: 0.21.3
ansi-regex@5.0.1: {}
ansi-regex@6.2.2: {}
@@ -1229,6 +1443,10 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
chardet@0.7.0: {}
cli-width@4.1.0: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -1326,6 +1544,12 @@ snapshots:
extend@3.0.2: {}
external-editor@3.1.0:
dependencies:
chardet: 0.7.0
iconv-lite: 0.4.24
tmp: 0.0.33
fast-decode-uri-component@1.0.1: {}
fast-deep-equal@3.1.3: {}
@@ -1526,6 +1750,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
inherits@2.0.4: {}
interpret@1.4.0: {}
@@ -1610,6 +1838,8 @@ snapshots:
ms@2.1.3: {}
mute-stream@1.0.0: {}
nice-try@1.0.5: {}
node-domexception@1.0.0: {}
@@ -1640,6 +1870,8 @@ snapshots:
openurl@1.1.1: {}
os-tmpdir@1.0.2: {}
p-finally@1.0.0: {}
package-json-from-dist@1.0.1: {}
@@ -1808,6 +2040,10 @@ snapshots:
tiktoken@1.0.22: {}
tmp@0.0.33:
dependencies:
os-tmpdir: 1.0.2
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -1845,6 +2081,12 @@ snapshots:
dependencies:
isexe: 2.0.0
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -1860,3 +2102,5 @@ snapshots:
wrappy@1.0.2: {}
ws@8.18.3: {}
yoctocolors-cjs@2.1.3: {}

View File

@@ -1,6 +1,6 @@
import {IAgent, ITool} from "./type";
import { createHash } from 'crypto';
import { LRUCache } from 'lru-cache';
import * as LRU from 'lru-cache';
interface ImageCacheEntry {
source: any;
@@ -8,12 +8,13 @@ interface ImageCacheEntry {
}
class ImageCache {
private cache: LRUCache<string, ImageCacheEntry>;
private cache: any;
constructor(maxSize = 100) {
this.cache = new LRUCache({
const CacheClass: any = (LRU as any).LRUCache || (LRU as any);
this.cache = new CacheClass({
max: maxSize,
ttl: 5 * 60 * 1000,
ttl: 5 * 60 * 1000, // 5 minutes
});
}

View File

@@ -8,6 +8,7 @@ import {
isServiceRunning,
getServiceInfo,
} from "./utils/processCheck";
import { runModelSelector } from "./utils/modelSelector"; // ADD THIS LINE
import { version } from "../package.json";
import { spawn, exec } from "child_process";
import { PID_FILE, REFERENCE_COUNT_FILE } from "./constants";
@@ -26,6 +27,7 @@ Commands:
status Show server status
statusline Integrated statusline
code Execute claude command
model Interactive model selection and configuration
ui Open the web UI in browser
-v, version Show version information
-h, help Show help information
@@ -33,6 +35,7 @@ Commands:
Example:
ccr start
ccr code "Write a Hello World"
ccr model
ccr ui
`;
@@ -109,6 +112,10 @@ async function main() {
}
});
break;
// ADD THIS CASE
case "model":
await runModelSelector();
break;
case "code":
if (!isRunning) {
console.log("Service not running, starting service...");
@@ -321,4 +328,4 @@ async function main() {
}
}
main().catch(console.error);
main().catch(console.error);

462
src/utils/modelSelector.ts Normal file
View File

@@ -0,0 +1,462 @@
import * as fs from 'fs';
import * as path from 'path';
import { select, input, confirm } from '@inquirer/prompts';
// ANSI color codes
const RESET = "\x1B[0m";
const DIM = "\x1B[2m";
const BOLDGREEN = "\x1B[1m\x1B[32m";
const CYAN = "\x1B[36m";
const BOLDCYAN = "\x1B[1m\x1B[36m";
const GREEN = "\x1B[32m";
const YELLOW = "\x1B[33m";
const BOLDYELLOW = "\x1B[1m\x1B[33m";
interface TransformerConfig {
use: Array<string | [string, any]>;
[key: string]: any;
}
interface Provider {
name: string;
api_base_url: string;
api_key: string;
models: string[];
transformer?: TransformerConfig;
}
interface RouterConfig {
default: string;
background?: string;
think?: string;
longContext?: string;
longContextThreshold?: number;
webSearch?: string;
image?: string;
[key: string]: string | number | undefined;
}
interface Config {
Providers: Provider[];
Router: RouterConfig;
[key: string]: any;
}
interface ModelResult {
providerName: string;
modelName: string;
modelType: string;
}
const AVAILABLE_TRANSFORMERS = [
'anthropic',
'deepseek',
'gemini',
'openrouter',
'groq',
'maxtoken',
'tooluse',
'gemini-cli',
'reasoning',
'sampling',
'enhancetool',
'cleancache',
'vertex-gemini',
'chutes-glm',
'qwen-cli',
'rovo-cli'
];
function getConfigPath(): string {
const configDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude-code-router');
const configPath = path.join(configDir, 'config.json');
if (!fs.existsSync(configPath)) {
throw new Error(`config.json not found at ${configPath}`);
}
return configPath;
}
function loadConfig(): Config {
const configPath = getConfigPath();
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
function saveConfig(config: Config): void {
const configPath = getConfigPath();
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
console.log(`${GREEN}✓ config.json updated successfully${RESET}\n`);
}
function getAllModels(config: Config) {
const models: any[] = [];
for (const provider of config.Providers) {
for (const model of provider.models) {
models.push({
name: `${BOLDCYAN}${provider.name}${RESET}${CYAN} ${model}`,
value: `${provider.name},${model}`,
description: `\n${BOLDCYAN}Provider:${RESET} ${provider.name}`,
provider: provider.name,
model: model
});
}
}
return models;
}
function displayCurrentConfig(config: Config): void {
console.log(`\n${BOLDCYAN}═══════════════════════════════════════════════${RESET}`);
console.log(`${BOLDCYAN} Current Configuration${RESET}`);
console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\n`);
const formatModel = (routerValue?: string | number) => {
if (!routerValue || typeof routerValue === 'number') {
return `${DIM}Not configured${RESET}`;
}
const [provider, model] = routerValue.split(',');
return `${YELLOW}${provider}${RESET} | ${model}\n ${DIM}- ${routerValue}${RESET}`;
};
console.log(`${BOLDCYAN}Default Model:${RESET}`);
console.log(` ${formatModel(config.Router.default)}\n`);
if (config.Router.background) {
console.log(`${BOLDCYAN}Background Model:${RESET}`);
console.log(` ${formatModel(config.Router.background)}\n`);
}
if (config.Router.think) {
console.log(`${BOLDCYAN}Think Model:${RESET}`);
console.log(` ${formatModel(config.Router.think)}\n`);
}
if (config.Router.longContext) {
console.log(`${BOLDCYAN}Long Context Model:${RESET}`);
console.log(` ${formatModel(config.Router.longContext)}\n`);
}
if (config.Router.webSearch) {
console.log(`${BOLDCYAN}Web Search Model:${RESET}`);
console.log(` ${formatModel(config.Router.webSearch)}\n`);
}
if (config.Router.image) {
console.log(`${BOLDCYAN}Image Model:${RESET}`);
console.log(` ${formatModel(config.Router.image)}\n`);
}
console.log(`\n${BOLDCYAN}═══════════════════════════════════════════════${RESET}`);
console.log(`${BOLDCYAN} Add/Update Model${RESET}`);
console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\n`);
}
async function selectModelType() {
return await select({
message: `${BOLDYELLOW}Which model configuration do you want to update?${RESET}`,
choices: [
{ name: 'Default Model', value: 'default' },
{ name: 'Background Model', value: 'background' },
{ name: 'Think Model', value: 'think' },
{ name: 'Long Context Model', value: 'longContext' },
{ name: 'Web Search Model', value: 'webSearch' },
{ name: 'Image Model', value: 'image' },
{ name: `${BOLDGREEN}+ Add New Model${RESET}`, value: 'addModel' }
]
});
}
async function selectModel(config: Config, modelType: string) {
const models = getAllModels(config);
return await select({
message: `\n${BOLDYELLOW}Select a model for ${modelType}:${RESET}`,
choices: models,
pageSize: 15
});
}
async function configureTransformers(): Promise<TransformerConfig | undefined> {
const useTransformers = await confirm({
message: `\n${BOLDYELLOW}Add transformer configuration?${RESET}`,
default: false
});
if (!useTransformers) {
return undefined;
}
const transformers: Array<string | [string, any]> = [];
let addMore = true;
while (addMore) {
const transformer = await select({
message: `\n${BOLDYELLOW}Select a transformer:${RESET}`,
choices: AVAILABLE_TRANSFORMERS.map(t => ({ name: t, value: t })),
pageSize: 15
}) as string;
// Check if transformer needs options
if (transformer === 'maxtoken') {
const maxTokens = await input({
message: `\n${BOLDYELLOW}Max tokens:${RESET}`,
default: '30000',
validate: (value: string) => {
const num = parseInt(value);
if (isNaN(num) || num <= 0) {
return 'Please enter a valid positive number';
}
return true;
}
});
transformers.push(['maxtoken', { max_tokens: parseInt(maxTokens) }]);
} else if (transformer === 'openrouter') {
const addProvider = await confirm({
message: `\n${BOLDYELLOW}Add provider routing options?${RESET}`,
default: false
});
if (addProvider) {
const providerInput = await input({
message: 'Provider (e.g., moonshotai/fp8):',
validate: (value: string) => value.trim() !== '' || 'Provider cannot be empty'
});
transformers.push(['openrouter', { provider: { only: [providerInput] } }]);
} else {
transformers.push(transformer);
}
} else {
transformers.push(transformer);
}
addMore = await confirm({
message: `\n${BOLDYELLOW}Add another transformer?${RESET}`,
default: false
});
}
return { use: transformers };
}
async function addNewModel(config: Config): Promise<ModelResult | null> {
const providerChoices = config.Providers.map(p => ({
name: p.name,
value: p.name
}));
providerChoices.push({ name: `${BOLDGREEN}+ Add New Provider${RESET}`, value: '__new__' });
const selectedProvider = await select({
message: `\n${BOLDYELLOW}Select provider for the new model:${RESET}`,
choices: providerChoices
}) as string;
if (selectedProvider === '__new__') {
return await addNewProvider(config);
} else {
return await addModelToExistingProvider(config, selectedProvider);
}
}
async function addModelToExistingProvider(config: Config, providerName: string): Promise<ModelResult | null> {
const modelName = await input({
message: `\n${BOLDYELLOW}Enter the model name:${RESET}`,
validate: (value: string) => {
if (!value.trim()) {
return 'Model name cannot be empty';
}
return true;
}
});
const provider = config.Providers.find(p => p.name === providerName);
if (!provider) {
console.log(`${YELLOW}Provider not found${RESET}`);
return null;
}
if (provider.models.includes(modelName)) {
console.log(`${YELLOW}Model already exists in provider${RESET}`);
return null;
}
provider.models.push(modelName);
// Ask about model-specific transformers
const addModelTransformer = await confirm({
message: `\n${BOLDYELLOW}Add model-specific transformer configuration?${RESET}`,
default: false
});
if (addModelTransformer) {
const transformerConfig = await configureTransformers();
if (transformerConfig && provider.transformer) {
provider.transformer[modelName] = transformerConfig;
}
}
saveConfig(config);
console.log(`${GREEN}✓ Model "${modelName}" added to provider "${providerName}"${RESET}`);
const setAsDefault = await confirm({
message: `\n${BOLDYELLOW}Do you want to set this model in router configuration?${RESET}`,
default: false
});
if (setAsDefault) {
const modelType = await select({
message: `\n${BOLDYELLOW}Select configuration type:${RESET}`,
choices: [
{ name: 'Default Model', value: 'default' },
{ name: 'Background Model', value: 'background' },
{ name: 'Think Model', value: 'think' },
{ name: 'Long Context Model', value: 'longContext' },
{ name: 'Web Search Model', value: 'webSearch' },
{ name: 'Image Model', value: 'image' }
]
}) as string;
return { providerName, modelName, modelType };
}
return null;
}
async function addNewProvider(config: Config): Promise<ModelResult | null> {
console.log(`\n${BOLDCYAN}Adding New Provider${RESET}\n`);
const providerName = await input({
message: `${BOLDYELLOW}Provider name:${RESET}`,
validate: (value: string) => {
if (!value.trim()) {
return 'Provider name cannot be empty';
}
if (config.Providers.some(p => p.name === value)) {
return 'Provider already exists';
}
return true;
}
});
const apiBaseUrl = await input({
message: `\n${BOLDYELLOW}API base URL:${RESET}`,
validate: (value: string) => {
if (!value.trim()) {
return 'API base URL cannot be empty';
}
try {
new URL(value);
return true;
} catch {
return 'Please enter a valid URL';
}
}
});
const apiKey = await input({
message: `\n${BOLDYELLOW}API key:${RESET}`,
validate: (value: string) => {
if (!value.trim()) {
return 'API key cannot be empty';
}
return true;
}
});
const modelsInput = await input({
message: `\n${BOLDYELLOW}Model names (comma-separated):${RESET}`,
validate: (value: string) => {
if (!value.trim()) {
return 'At least one model name is required';
}
return true;
}
});
const models = modelsInput.split(',').map(m => m.trim()).filter(m => m);
const newProvider: Provider = {
name: providerName,
api_base_url: apiBaseUrl,
api_key: apiKey,
models: models
};
// Global transformer configuration
const transformerConfig = await configureTransformers();
if (transformerConfig) {
newProvider.transformer = transformerConfig;
}
config.Providers.push(newProvider);
saveConfig(config);
console.log(`${GREEN}\n✓ Provider "${providerName}" added successfully${RESET}`);
const setAsDefault = await confirm({
message: `\n${BOLDYELLOW}Do you want to set one of these models in router configuration?${RESET}`,
default: false
});
if (setAsDefault && models.length > 0) {
let selectedModel = models[0];
if (models.length > 1) {
selectedModel = await select({
message: `\n${BOLDYELLOW}Select which model to configure:${RESET}`,
choices: models.map(m => ({ name: m, value: m }))
}) as string;
}
const modelType = await select({
message: `\n${BOLDYELLOW}Select configuration type:${RESET}`,
choices: [
{ name: 'Default Model', value: 'default' },
{ name: 'Background Model', value: 'background' },
{ name: 'Think Model', value: 'think' },
{ name: 'Long Context Model', value: 'longContext' },
{ name: 'Web Search Model', value: 'webSearch' },
{ name: 'Image Model', value: 'image' }
]
}) as string;
return { providerName, modelName: selectedModel, modelType };
}
return null;
}
export async function runModelSelector(): Promise<void> {
console.clear();
try {
let config = loadConfig();
displayCurrentConfig(config);
const action = await selectModelType() as string;
if (action === 'addModel') {
const result = await addNewModel(config);
if (result) {
config = loadConfig();
config.Router[result.modelType] = `${result.providerName},${result.modelName}`;
saveConfig(config);
console.log(`${GREEN}${result.modelType} set to ${result.providerName},${result.modelName}${RESET}`);
}
} else {
const selectedModel = await selectModel(config, action) as string;
config.Router[action] = selectedModel;
saveConfig(config);
console.log(`${GREEN}${action} model updated to: ${selectedModel}${RESET}`);
}
displayCurrentConfig(config);
} catch (error: any) {
console.error(`${YELLOW}Error:${RESET}`, error.message);
process.exit(1);
}
}

139
ui/package-lock.json generated
View File

@@ -14,6 +14,7 @@
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11",
"class-variance-authority": "^0.7.1",
@@ -1181,6 +1182,32 @@
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@@ -1247,6 +1274,21 @@
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
@@ -1495,6 +1537,43 @@
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
@@ -1542,6 +1621,66 @@
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.11",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",

View File

@@ -1 +1 @@
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/debugpage.tsx","./src/components/jsoneditor.tsx","./src/components/logviewer.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/requesthistorydrawer.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/db.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"}
{"root":["./src/App.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/ConfigProvider.tsx","./src/components/DebugPage.tsx","./src/components/JsonEditor.tsx","./src/components/LogViewer.tsx","./src/components/Login.tsx","./src/components/ProtectedRoute.tsx","./src/components/ProviderList.tsx","./src/components/Providers.tsx","./src/components/PublicRoute.tsx","./src/components/RequestHistoryDrawer.tsx","./src/components/Router.tsx","./src/components/SettingsDialog.tsx","./src/components/StatusLineConfigDialog.tsx","./src/components/StatusLineImportExport.tsx","./src/components/TransformerList.tsx","./src/components/Transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/db.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"}