qwenproxy-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +14 -0
- package/README.md +907 -0
- package/bin/qwenproxy.js +141 -0
- package/package.json +78 -0
- package/src/api/error-classifier.ts +159 -0
- package/src/api/error-helpers.ts +118 -0
- package/src/api/models.ts +261 -0
- package/src/api/server.ts +859 -0
- package/src/cache/memory-cache.ts +385 -0
- package/src/clean-cache.ts +204 -0
- package/src/core/account-concurrency.ts +671 -0
- package/src/core/account-manager.ts +297 -0
- package/src/core/account-priority.ts +163 -0
- package/src/core/accounts.ts +186 -0
- package/src/core/config.ts +383 -0
- package/src/core/crypto-utils.ts +79 -0
- package/src/core/database.ts +276 -0
- package/src/core/errors.ts +118 -0
- package/src/core/logger.ts +269 -0
- package/src/core/memory-usage.ts +84 -0
- package/src/core/metrics.ts +291 -0
- package/src/core/model-alias.ts +77 -0
- package/src/core/model-registry.ts +544 -0
- package/src/core/mutex.ts +119 -0
- package/src/core/paths.ts +199 -0
- package/src/core/prompt-limits.ts +214 -0
- package/src/core/reasoning-effort.ts +102 -0
- package/src/core/stream-registry.ts +96 -0
- package/src/core/waf-isolation.ts +117 -0
- package/src/core/watchdog.ts +195 -0
- package/src/delete-chats.ts +23 -0
- package/src/index.ts +64 -0
- package/src/login.ts +147 -0
- package/src/reset-cooldowns.ts +11 -0
- package/src/routes/anthropic/index.ts +355 -0
- package/src/routes/anthropic/translate.ts +522 -0
- package/src/routes/anthropic/types.ts +154 -0
- package/src/routes/anthropic/validation.ts +144 -0
- package/src/routes/chat/account.ts +1817 -0
- package/src/routes/chat/context.ts +241 -0
- package/src/routes/chat/errors.ts +85 -0
- package/src/routes/chat/helpers.ts +268 -0
- package/src/routes/chat/index.ts +618 -0
- package/src/routes/chat/media.ts +285 -0
- package/src/routes/chat/retry-policy.ts +754 -0
- package/src/routes/chat/stop.ts +98 -0
- package/src/routes/chat/streaming.ts +2710 -0
- package/src/routes/chat/validation.ts +526 -0
- package/src/routes/chat.ts +2 -0
- package/src/routes/completions.ts +290 -0
- package/src/routes/images.ts +139 -0
- package/src/routes/responses/adapter.ts +503 -0
- package/src/routes/responses/index.ts +405 -0
- package/src/routes/responses/state.ts +230 -0
- package/src/routes/responses/streaming.ts +528 -0
- package/src/routes/responses/types.ts +285 -0
- package/src/routes/responses/validation.ts +202 -0
- package/src/routes/upload.ts +731 -0
- package/src/routes/videos.ts +214 -0
- package/src/services/auth-playwright.ts +173 -0
- package/src/services/captcha-coordinator.ts +161 -0
- package/src/services/captcha-solver.ts +553 -0
- package/src/services/chat-cleanup.ts +80 -0
- package/src/services/context-meter.ts +317 -0
- package/src/services/fingerprint.ts +242 -0
- package/src/services/human-behavior.ts +173 -0
- package/src/services/media-generation.ts +1748 -0
- package/src/services/playwright.ts +2800 -0
- package/src/services/qwen-chat-pool.ts +345 -0
- package/src/services/qwen-errors.ts +133 -0
- package/src/services/qwen-headers.ts +79 -0
- package/src/services/qwen-thread-state.ts +393 -0
- package/src/services/qwen-url.ts +19 -0
- package/src/services/qwen.ts +3126 -0
- package/src/services/session-keeper.ts +88 -0
- package/src/services/token-estimation-metrics.ts +118 -0
- package/src/sync/claude-code.ts +75 -0
- package/src/sync/codex.ts +123 -0
- package/src/sync/index.ts +362 -0
- package/src/sync/omp.ts +105 -0
- package/src/sync/opencode.ts +214 -0
- package/src/sync/types.ts +53 -0
- package/src/sync/utils.ts +27 -0
- package/src/sync-clients.ts +189 -0
- package/src/tools/instructions.ts +137 -0
- package/src/tools/manifest.ts +81 -0
- package/src/tools/parser.ts +2989 -0
- package/src/tools/toolcall-tags.ts +142 -0
- package/src/tools/types.ts +53 -0
- package/src/tui/app.ts +264 -0
- package/src/tui/index.ts +61 -0
- package/src/tui/markdown.ts +258 -0
- package/src/tui/proxy-client.ts +326 -0
- package/src/tui/screen.ts +278 -0
- package/src/tui/server-manager.ts +270 -0
- package/src/tui/theme.ts +432 -0
- package/src/tui/types.ts +33 -0
- package/src/tui/views/accounts-view.ts +656 -0
- package/src/tui/views/chat-view.ts +823 -0
- package/src/tui/views/logs-view.ts +413 -0
- package/src/tui/views/status-view.ts +204 -0
- package/src/tui/views/storage-view.ts +291 -0
- package/src/tui/views/sync-view.ts +409 -0
- package/src/types/ali-oss.d.ts +32 -0
- package/src/utils/context-truncation.ts +84 -0
- package/src/utils/json.ts +380 -0
- package/src/utils/session-id.ts +37 -0
- package/src/utils/tool-call-guard.ts +85 -0
- package/src/utils/types.ts +109 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface ClientSyncResult {
|
|
2
|
+
client: "claude-code" | "codex" | "opencode" | "omp";
|
|
3
|
+
filePath: string;
|
|
4
|
+
backupPath?: string;
|
|
5
|
+
success: boolean;
|
|
6
|
+
action: "updated" | "created" | "skipped" | "restored" | "failed";
|
|
7
|
+
message?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SyncOptions {
|
|
12
|
+
filePath: string;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
setActive?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SyncAllOptions {
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
port?: number;
|
|
22
|
+
host?: string;
|
|
23
|
+
setActive?: boolean;
|
|
24
|
+
stateFilePath?: string;
|
|
25
|
+
targets?: ("claude-code" | "codex" | "opencode" | "omp")[];
|
|
26
|
+
customPaths?: {
|
|
27
|
+
claudeCode?: string;
|
|
28
|
+
codex?: string;
|
|
29
|
+
openCode?: string;
|
|
30
|
+
omp?: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SyncRecord {
|
|
35
|
+
filePath: string;
|
|
36
|
+
backupPath: string;
|
|
37
|
+
existedBefore: boolean;
|
|
38
|
+
syncedAt: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SyncStateFile {
|
|
42
|
+
version: number;
|
|
43
|
+
updatedAt: string;
|
|
44
|
+
apiKey: string;
|
|
45
|
+
port: number;
|
|
46
|
+
host: string;
|
|
47
|
+
clients: {
|
|
48
|
+
claudeCode?: SyncRecord;
|
|
49
|
+
codex?: SyncRecord;
|
|
50
|
+
openCode?: SyncRecord;
|
|
51
|
+
omp?: SyncRecord;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function createTimestampBackup(filePath: string): string {
|
|
5
|
+
const dir = path.dirname(filePath);
|
|
6
|
+
const ext = path.extname(filePath);
|
|
7
|
+
const base = path.basename(filePath, ext);
|
|
8
|
+
const now = new Date();
|
|
9
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
10
|
+
const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
11
|
+
const backupPath = path.join(dir, `${base}.qwenproxy.${timestamp}${ext}.bak`);
|
|
12
|
+
fs.copyFileSync(filePath, backupPath);
|
|
13
|
+
return backupPath;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function restoreFromBackup(filePath: string, backupPath?: string): boolean {
|
|
17
|
+
if (!backupPath || !fs.existsSync(backupPath)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
fs.copyFileSync(backupPath, filePath);
|
|
21
|
+
try {
|
|
22
|
+
fs.unlinkSync(backupPath);
|
|
23
|
+
} catch {
|
|
24
|
+
// Ignore cleanup error
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import {
|
|
3
|
+
syncAllClients,
|
|
4
|
+
restoreAllClients,
|
|
5
|
+
normalizeClientName,
|
|
6
|
+
getDefaultPaths,
|
|
7
|
+
inspectClientSyncStatus,
|
|
8
|
+
} from "./sync/index.ts";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
|
|
11
|
+
function parseArgs() {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const options: {
|
|
14
|
+
restore: boolean;
|
|
15
|
+
list: boolean;
|
|
16
|
+
help: boolean;
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
port?: number;
|
|
19
|
+
host?: string;
|
|
20
|
+
setActive: boolean;
|
|
21
|
+
targets: ("claude-code" | "codex" | "opencode" | "omp")[];
|
|
22
|
+
} = {
|
|
23
|
+
restore: false,
|
|
24
|
+
list: false,
|
|
25
|
+
help: false,
|
|
26
|
+
setActive: true,
|
|
27
|
+
targets: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
const arg = args[i];
|
|
32
|
+
if (arg === "--restore" || arg === "restore") {
|
|
33
|
+
options.restore = true;
|
|
34
|
+
} else if (arg === "--list" || arg === "list") {
|
|
35
|
+
options.list = true;
|
|
36
|
+
} else if (arg === "--help" || arg === "-h" || arg === "help") {
|
|
37
|
+
options.help = true;
|
|
38
|
+
} else if (arg === "--api-key" && args[i + 1]) {
|
|
39
|
+
options.apiKey = args[++i];
|
|
40
|
+
} else if (arg === "--port" && args[i + 1]) {
|
|
41
|
+
options.port = parseInt(args[++i], 10);
|
|
42
|
+
} else if (arg === "--host" && args[i + 1]) {
|
|
43
|
+
options.host = args[++i];
|
|
44
|
+
} else if (arg === "--no-active") {
|
|
45
|
+
options.setActive = false;
|
|
46
|
+
} else if (arg === "--client" && args[i + 1]) {
|
|
47
|
+
const normalized = normalizeClientName(args[++i]);
|
|
48
|
+
if (normalized) options.targets.push(normalized);
|
|
49
|
+
} else if (!arg.startsWith("-")) {
|
|
50
|
+
const normalized = normalizeClientName(arg);
|
|
51
|
+
if (normalized) options.targets.push(normalized);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return options;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printHelp() {
|
|
59
|
+
console.log(`
|
|
60
|
+
Uso:
|
|
61
|
+
npm run sync [clientes...] [opções]
|
|
62
|
+
|
|
63
|
+
Exemplos:
|
|
64
|
+
npm run sync # Sincroniza todos os clientes detectados
|
|
65
|
+
npm run sync claude # Sincroniza apenas o Claude Code
|
|
66
|
+
npm run sync codex # Sincroniza apenas o Codex CLI
|
|
67
|
+
npm run sync opencode # Sincroniza apenas o OpenCode
|
|
68
|
+
npm run sync omp # Sincroniza apenas o OMP (Oh My Pi)
|
|
69
|
+
npm run sync claude codex # Sincroniza Claude Code e Codex
|
|
70
|
+
npm run sync -- --list # Lista os arquivos dos clientes detectados
|
|
71
|
+
npm run sync -- --restore # Restaura as configurações originais (rollback)
|
|
72
|
+
Opções:
|
|
73
|
+
--client <nome> Nome do cliente (claude, codex, opencode, omp)
|
|
74
|
+
--api-key <chave> Sobrescrever chave de API (padrão: lê do .env ou usa sk-qwenproxy-local)
|
|
75
|
+
--port <porta> Sobrescrever porta do servidor (padrão: lê do .env ou usa 7936)
|
|
76
|
+
--host <host> Sobrescrever host do servidor (padrão: 127.0.0.1)
|
|
77
|
+
--no-active Não definir o modelo ativo no Codex (apenas adiciona o provider)
|
|
78
|
+
--restore Desfaz alterações restaurando backups
|
|
79
|
+
--list Mostra status de detecção dos arquivos de configuração
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function main() {
|
|
84
|
+
const options = parseArgs();
|
|
85
|
+
|
|
86
|
+
console.log("==================================================");
|
|
87
|
+
console.log(" 🚀 QwenProxy - Client Configuration Sync");
|
|
88
|
+
console.log("==================================================");
|
|
89
|
+
|
|
90
|
+
if (options.help) {
|
|
91
|
+
printHelp();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (options.list) {
|
|
96
|
+
console.log("\n📁 Status de detecção dos clientes no seu computador:\n");
|
|
97
|
+
const defaultPaths = getDefaultPaths();
|
|
98
|
+
const clients = [
|
|
99
|
+
{ id: "claude-code" as const, name: "Claude Code", path: defaultPaths.claudeCode },
|
|
100
|
+
{ id: "codex" as const, name: "Codex CLI", path: defaultPaths.codex },
|
|
101
|
+
{ id: "opencode" as const, name: "OpenCode", path: defaultPaths.openCode },
|
|
102
|
+
{ id: "omp" as const, name: "OMP (Oh My Pi)", path: defaultPaths.omp },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
for (const c of clients) {
|
|
106
|
+
const status = inspectClientSyncStatus(c.id, c.path);
|
|
107
|
+
let badge = "";
|
|
108
|
+
let icon = "⚪";
|
|
109
|
+
if (status.synced) {
|
|
110
|
+
icon = "✅";
|
|
111
|
+
badge = status.model ? `[Sincronizado: ${status.model}]` : "[Sincronizado]";
|
|
112
|
+
} else if (status.installed) {
|
|
113
|
+
icon = "⚠️ ";
|
|
114
|
+
badge = "[Detectado - Outro provedor]";
|
|
115
|
+
} else {
|
|
116
|
+
icon = "⚪";
|
|
117
|
+
badge = "[Não instalado]";
|
|
118
|
+
}
|
|
119
|
+
console.log(` ${icon} ${c.name.padEnd(16)} ${badge}`);
|
|
120
|
+
console.log(` ${c.path}`);
|
|
121
|
+
}
|
|
122
|
+
console.log("\nPara sincronizar um ou todos, execute:");
|
|
123
|
+
console.log(" npm run sync");
|
|
124
|
+
console.log(" npm run sync claude\n");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (options.restore) {
|
|
129
|
+
console.log("\n🔄 Restaurando configurações originais a partir dos backups...\n");
|
|
130
|
+
const result = restoreAllClients();
|
|
131
|
+
if (result.restoredCount === 0) {
|
|
132
|
+
console.log("ℹ️ Nenhum backup ou arquivo de estado anterior encontrado para restaurar.");
|
|
133
|
+
} else {
|
|
134
|
+
for (const item of result.details) {
|
|
135
|
+
console.log(` ✓ Restaurado [${item.client}]: ${item.filePath}`);
|
|
136
|
+
}
|
|
137
|
+
console.log(`\n✅ ${result.restoredCount} configuração(ões) restaurada(s) com sucesso!`);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const targetNames = options.targets.length > 0
|
|
143
|
+
? options.targets.join(", ")
|
|
144
|
+
: "todos os detectados";
|
|
145
|
+
|
|
146
|
+
console.log(`\n📡 Sincronizando clientes: [${targetNames}]...\n`);
|
|
147
|
+
|
|
148
|
+
const result = syncAllClients({
|
|
149
|
+
apiKey: options.apiKey,
|
|
150
|
+
port: options.port,
|
|
151
|
+
host: options.host,
|
|
152
|
+
setActive: options.setActive,
|
|
153
|
+
targets: options.targets.length > 0 ? options.targets : undefined,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
console.log(`🔑 Chave API: ${result.apiKey}`);
|
|
157
|
+
console.log(`🌐 Porta: ${result.port}`);
|
|
158
|
+
console.log(`🏠 Host: ${result.host}\n`);
|
|
159
|
+
|
|
160
|
+
let count = 0;
|
|
161
|
+
for (const client of Object.values(result.clients)) {
|
|
162
|
+
if (!client) continue;
|
|
163
|
+
count++;
|
|
164
|
+
const icon = client.success ? "✅" : "❌";
|
|
165
|
+
console.log(`${icon} [${client.client}]`);
|
|
166
|
+
console.log(` Arquivo: ${client.filePath}`);
|
|
167
|
+
if (client.backupPath) {
|
|
168
|
+
console.log(` Backup: ${client.backupPath}`);
|
|
169
|
+
}
|
|
170
|
+
if (client.message) {
|
|
171
|
+
console.log(` Status: ${client.message}`);
|
|
172
|
+
}
|
|
173
|
+
if (client.error) {
|
|
174
|
+
console.log(` Erro: ${client.error}`);
|
|
175
|
+
}
|
|
176
|
+
console.log("");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log("--------------------------------------------------");
|
|
180
|
+
console.log(`✨ ${count} cliente(s) sincronizado(s) com zero perda de outras configs/provedores!`);
|
|
181
|
+
console.log("💡 Para desfazer e restaurar a qualquer momento:");
|
|
182
|
+
console.log(" npm run sync -- --restore");
|
|
183
|
+
console.log("==================================================\n");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
main().catch((err) => {
|
|
187
|
+
console.error("❌ Erro fatal durante a sincronização:", err);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { TOOL_CALL_OPEN, TOOL_CALL_CLOSE } from "./toolcall-tags.js";
|
|
2
|
+
import { buildCompactToolManifest } from "./manifest.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* LRU-style cache for tool instructions to avoid rebuilding on every request.
|
|
6
|
+
* Key format: toolsJson + "##" + toolChoice
|
|
7
|
+
* Upstream: cb518e0
|
|
8
|
+
*/
|
|
9
|
+
const toolInstructionsCache = new Map<string, string>();
|
|
10
|
+
const TOOL_CACHE_MAX_ENTRIES = 64;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Formats available tools into a compact TypeScript-like manifest if possible,
|
|
14
|
+
* falling back to the raw string if parsing fails.
|
|
15
|
+
*/
|
|
16
|
+
function formatToolsRepresentation(toolsInput: string | unknown[]): string {
|
|
17
|
+
if (typeof toolsInput === "string") {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(toolsInput);
|
|
20
|
+
if (Array.isArray(parsed)) {
|
|
21
|
+
const compact = buildCompactToolManifest(parsed);
|
|
22
|
+
if (compact.trim().length > 0) return compact;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
return toolsInput;
|
|
26
|
+
}
|
|
27
|
+
return toolsInput;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(toolsInput)) {
|
|
31
|
+
const compact = buildCompactToolManifest(toolsInput);
|
|
32
|
+
if (compact.trim().length > 0) return compact;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return String(toolsInput);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds tool calling instructions for the system prompt.
|
|
40
|
+
*
|
|
41
|
+
* @param toolsJson - Stringified JSON array of available tools (or tools array).
|
|
42
|
+
* @param toolChoice - Optional tool choice configuration.
|
|
43
|
+
* @returns Formatted instruction string.
|
|
44
|
+
*/
|
|
45
|
+
export function buildToolInstructions(
|
|
46
|
+
toolsJson: string | unknown[],
|
|
47
|
+
toolChoice?: unknown,
|
|
48
|
+
): string {
|
|
49
|
+
const toolsString = typeof toolsJson === "string" ? toolsJson : JSON.stringify(toolsJson);
|
|
50
|
+
|
|
51
|
+
// Check cache first
|
|
52
|
+
const cacheKey = `${toolsString}##${JSON.stringify(toolChoice ?? null)}`;
|
|
53
|
+
const cached = toolInstructionsCache.get(cacheKey);
|
|
54
|
+
if (cached !== undefined) {
|
|
55
|
+
return cached;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const manifest = formatToolsRepresentation(toolsJson);
|
|
59
|
+
|
|
60
|
+
let toolList: any[] = [];
|
|
61
|
+
if (typeof toolsJson === "string") {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(toolsJson);
|
|
64
|
+
if (Array.isArray(parsed)) toolList = parsed;
|
|
65
|
+
} catch {}
|
|
66
|
+
} else if (Array.isArray(toolsJson)) {
|
|
67
|
+
toolList = toolsJson;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const firstTool = toolList[0];
|
|
71
|
+
const sampleToolName =
|
|
72
|
+
(typeof firstTool?.function?.name === "string" && firstTool.function.name) ||
|
|
73
|
+
(typeof firstTool?.name === "string" && firstTool.name) ||
|
|
74
|
+
"tool_name";
|
|
75
|
+
|
|
76
|
+
const properties =
|
|
77
|
+
firstTool?.function?.parameters?.properties ||
|
|
78
|
+
firstTool?.parameters?.properties;
|
|
79
|
+
const firstPropKey =
|
|
80
|
+
properties && typeof properties === "object"
|
|
81
|
+
? Object.keys(properties)[0]
|
|
82
|
+
: null;
|
|
83
|
+
|
|
84
|
+
let sampleArgs1 = "{}";
|
|
85
|
+
let sampleArgs2 = "{}";
|
|
86
|
+
if (firstPropKey) {
|
|
87
|
+
sampleArgs1 = JSON.stringify({ [firstPropKey]: "value1" });
|
|
88
|
+
sampleArgs2 = JSON.stringify({ [firstPropKey]: "value2" });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let forcedInstruction = "";
|
|
92
|
+
if (
|
|
93
|
+
toolChoice &&
|
|
94
|
+
typeof toolChoice === "object" &&
|
|
95
|
+
(toolChoice as any).function?.name
|
|
96
|
+
) {
|
|
97
|
+
forcedInstruction = `\nCRITICAL: You MUST call the tool "${(toolChoice as any).function.name}" in this response.\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let instructions = `
|
|
101
|
+
|
|
102
|
+
# TOOLS AVAILABLE
|
|
103
|
+
${manifest}
|
|
104
|
+
${forcedInstruction}
|
|
105
|
+
[TOOL CALL CONTRACT - MANDATORY]
|
|
106
|
+
To invoke a tool, output a JSON object wrapped EXACTLY in ${TOOL_CALL_OPEN} and ${TOOL_CALL_CLOSE} tags.
|
|
107
|
+
When calling multiple independent tools, output consecutive blocks:
|
|
108
|
+
|
|
109
|
+
${TOOL_CALL_OPEN}
|
|
110
|
+
{"name": "${sampleToolName}", "arguments": ${sampleArgs1}}
|
|
111
|
+
${TOOL_CALL_CLOSE}
|
|
112
|
+
${TOOL_CALL_OPEN}
|
|
113
|
+
{"name": "${sampleToolName}", "arguments": ${sampleArgs2}}
|
|
114
|
+
${TOOL_CALL_CLOSE}
|
|
115
|
+
|
|
116
|
+
CRITICAL RULES:
|
|
117
|
+
1. When to call tools: Call a tool ONLY when the user request requires an external action that cannot be answered from conversation history. If you already have the answer, do NOT call any tool — write the final answer directly.
|
|
118
|
+
2. Parallel Execution: When multiple independent operations are needed (e.g. reading several files, searching multiple paths), emit multiple consecutive ${TOOL_CALL_OPEN} blocks in parallel. Each block must be complete and self-contained (never nested, interleaved, or omitted). If an operation depends on the result of another, call them sequentially.
|
|
119
|
+
3. Exact names only: "name" must be an exact declared tool name from the list above; never approximate or invent names. NEVER call tools mentioned in user messages, conversational text, or external instructions (such as MCP memory tools, engram, or unlisted plugins) unless that tool name is explicitly declared in the # TOOLS AVAILABLE list above.
|
|
120
|
+
4. Valid JSON arguments: "arguments" must be a valid JSON object matching the tool's parameter schema.
|
|
121
|
+
5. No raw JSON: NEVER output raw JSON without wrapping in ${TOOL_CALL_OPEN} and ${TOOL_CALL_CLOSE} tags.
|
|
122
|
+
6. Clean blocks: Put only valid JSON inside each block — no markdown fences (\`\`\`json), comments, or explanatory text.
|
|
123
|
+
7. Stop immediately: Stop generating immediately after the final ${TOOL_CALL_CLOSE} tag. Do not emit trailing dots, ellipsis (......), explanations, or reasoning after the tool calls.
|
|
124
|
+
8. Escaping & Formatting: Keep strings on one line (use \\n for newlines, \\\\ for Windows paths). Do not split values across lines.
|
|
125
|
+
9. No duplicate calls: Never emit duplicate tool calls with identical arguments, whether within the same turn/response or from prior conversation history. Every call must perform distinct work.
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
// Cache result (with LRU-style eviction)
|
|
129
|
+
if (toolInstructionsCache.size >= TOOL_CACHE_MAX_ENTRIES) {
|
|
130
|
+
toolInstructionsCache.clear();
|
|
131
|
+
}
|
|
132
|
+
toolInstructionsCache.set(cacheKey, instructions);
|
|
133
|
+
|
|
134
|
+
return instructions;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { FunctionToolDefinition } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts a normalized function object from standard OpenAI tool definitions.
|
|
5
|
+
*/
|
|
6
|
+
function getToolFunction(tool: FunctionToolDefinition | any): any {
|
|
7
|
+
return tool?.type === "function" ? tool.function : tool;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Formats parameter schema into a clean, concise TypeScript-like signature string.
|
|
12
|
+
* Preserves parameter names, types, optionality (?), and enums without structural JSON overhead.
|
|
13
|
+
*/
|
|
14
|
+
function formatParameterSignature(name: string, schema: any, isRequired: boolean): string {
|
|
15
|
+
const optional = isRequired ? "" : "?";
|
|
16
|
+
if (!schema || typeof schema !== "object") {
|
|
17
|
+
return `${name}${optional}: any`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Handle enums
|
|
21
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
22
|
+
const enumValues = schema.enum
|
|
23
|
+
.map((val: any) => (typeof val === "string" ? `"${val}"` : String(val)))
|
|
24
|
+
.join(" | ");
|
|
25
|
+
return `${name}${optional}: ${enumValues}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Handle arrays with item types
|
|
29
|
+
if (schema.type === "array") {
|
|
30
|
+
const itemType = schema.items?.type || "any";
|
|
31
|
+
return `${name}${optional}: ${itemType}[]`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle standard primitive types
|
|
35
|
+
const type = schema.type || "any";
|
|
36
|
+
return `${name}${optional}: ${type}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Builds a compact, human/LLM-readable tool manifest in TypeScript signature format.
|
|
41
|
+
* Reduces token consumption by 50-70% while keeping all critical details intact.
|
|
42
|
+
*
|
|
43
|
+
* Example output:
|
|
44
|
+
* read_file(path: string, start_line?: number, end_line?: number) - Read contents of a file.
|
|
45
|
+
* list_directory(path: string) - List directory files.
|
|
46
|
+
*/
|
|
47
|
+
export function buildCompactToolManifest(tools: FunctionToolDefinition[] | unknown): string {
|
|
48
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const lines: string[] = [];
|
|
53
|
+
|
|
54
|
+
for (const tool of tools) {
|
|
55
|
+
const fn = getToolFunction(tool);
|
|
56
|
+
const name = fn?.name;
|
|
57
|
+
if (!name || typeof name !== "string") continue;
|
|
58
|
+
|
|
59
|
+
const description = (fn.description || "").replace(/\s+/g, " ").trim();
|
|
60
|
+
const properties = fn.parameters?.properties || {};
|
|
61
|
+
const requiredSet = new Set<string>(
|
|
62
|
+
Array.isArray(fn.parameters?.required) ? fn.parameters.required : [],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const paramSignatures: string[] = [];
|
|
66
|
+
for (const [paramName, schema] of Object.entries(properties)) {
|
|
67
|
+
paramSignatures.push(
|
|
68
|
+
formatParameterSignature(paramName, schema, requiredSet.has(paramName)),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const signature = `${name}(${paramSignatures.join(", ")})`;
|
|
73
|
+
if (description) {
|
|
74
|
+
lines.push(`${signature} - ${description}`);
|
|
75
|
+
} else {
|
|
76
|
+
lines.push(signature);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|