coc-bridge 1.0.1 → 1.0.2
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/CHANGELOG.md +10 -0
- package/bin/coc-bridge.js +9 -6
- package/package.json +3 -3
- package/scripts/restore-claude.js +149 -0
- package/scripts/setup-claude.js +70 -4
- package/src/server.js +316 -127
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.0.2] — 2026-10-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Claude Code Tool Control**: Claude Code now retains 100% control of tool execution (`View`, `Edit`, `Bash`, `Glob`, `Grep`, `Write`), showing interactive diffs, user permission confirmations, and spinners instead of OpenCode silently intercepting them
|
|
14
|
+
- **Anthropic `tool_use` Event Streaming**: Bridges model responses directly to Anthropic SSE tool use events (`content_block_start`, `input_json_delta`, `message_delta` with `stop_reason: "tool_use"`)
|
|
15
|
+
- **Accurate Real-Time Token Counts**: Extracted directly from model execution metrics rather than estimated token formulas
|
|
16
|
+
- **`coc-bridge restore` command**: One-command rollback to revert Claude Code & VS Code back to official Anthropic defaults with zero friction
|
|
17
|
+
- **Automatic VS Code Settings**: Configures Claude Code VS Code extension settings with robust JSONC comment-stripping support
|
|
18
|
+
- **`coc-bridge models` & `coc-bridge use <id>`**: Live discovery and switching of default models saved to `~/.coc-bridge/config.json`
|
|
19
|
+
|
|
10
20
|
## [1.0.1] — 2026-10-14
|
|
11
21
|
|
|
12
22
|
### Fixed
|
package/bin/coc-bridge.js
CHANGED
|
@@ -302,9 +302,10 @@ ${BOLD}COMMANDS${RESET}
|
|
|
302
302
|
${G}status${RESET} Show whether the bridge is running
|
|
303
303
|
${G}logs${RESET} Tail the live log file
|
|
304
304
|
${G}models${RESET} List all available OpenCode models
|
|
305
|
-
${G}use <id>${RESET}
|
|
306
|
-
${G}server${RESET} Run in
|
|
307
|
-
${G}setup${RESET} Wire Claude Code to
|
|
305
|
+
${G}use <id>${RESET} Switch default model (saves to ~/.coc-bridge/config.json)
|
|
306
|
+
${G}server${RESET} Run in foreground (useful for debugging)
|
|
307
|
+
${G}setup${RESET} Wire Claude Code & VS Code to use the bridge
|
|
308
|
+
${G}restore${RESET} Revert Claude Code & VS Code back to official defaults
|
|
308
309
|
${G}doctor${RESET} Check that Node, opencode, and claude are all wired up
|
|
309
310
|
|
|
310
311
|
${BOLD}ENVIRONMENT VARIABLES${RESET}
|
|
@@ -321,7 +322,8 @@ ${BOLD}EXAMPLES${RESET}
|
|
|
321
322
|
coc-bridge stop # Shut it down
|
|
322
323
|
coc-bridge status # Is it running?
|
|
323
324
|
coc-bridge logs # Watch live logs
|
|
324
|
-
coc-bridge setup #
|
|
325
|
+
coc-bridge setup # Wire Claude Code & VS Code
|
|
326
|
+
coc-bridge restore # Revert back to official Claude Code
|
|
325
327
|
BRIDGE_PORT=5000 coc-bridge # Custom port
|
|
326
328
|
`);
|
|
327
329
|
}
|
|
@@ -339,8 +341,9 @@ const cmd = process.argv[2] || 'start';
|
|
|
339
341
|
case 'server': cmdServer(); break;
|
|
340
342
|
case 'models': cmdModels(); break;
|
|
341
343
|
case 'use': cmdUse(process.argv[3]); break;
|
|
342
|
-
case 'setup': require(path.join(__dirname, '..', 'scripts', 'setup-claude.js'));
|
|
343
|
-
case '
|
|
344
|
+
case 'setup': require(path.join(__dirname, '..', 'scripts', 'setup-claude.js')); break;
|
|
345
|
+
case 'restore': require(path.join(__dirname, '..', 'scripts', 'restore-claude.js')); break;
|
|
346
|
+
case 'doctor': require(path.join(__dirname, '..', 'scripts', 'doctor.js')); break;
|
|
344
347
|
case '--version':
|
|
345
348
|
case '-v': {
|
|
346
349
|
const pkg = require('../package.json');
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coc-bridge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A zero-dependency local bridge that connects Claude Code to OpenCode's free hosted models — no API keys required.",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"coc-bridge": "
|
|
8
|
-
"coc-bridge-start": "
|
|
7
|
+
"coc-bridge": "bin/coc-bridge.js",
|
|
8
|
+
"coc-bridge-start": "bin/coc-bridge.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node src/server.js",
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* coc-bridge restore — Reverts Claude Code and VS Code back to normal official settings.
|
|
4
|
+
* Removes all coc-bridge proxies, wrappers, and custom settings with zero friction.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const GREEN = '\x1b[32m';
|
|
14
|
+
const YELLOW = '\x1b[33m';
|
|
15
|
+
const RED = '\x1b[31m';
|
|
16
|
+
const RESET = '\x1b[0m';
|
|
17
|
+
const BOLD = '\x1b[1m';
|
|
18
|
+
const DIM = '\x1b[2m';
|
|
19
|
+
|
|
20
|
+
function ok(msg) { console.log(`${GREEN} ✔${RESET} ${msg}`); }
|
|
21
|
+
function warn(msg) { console.log(`${YELLOW} ⚠${RESET} ${msg}`); }
|
|
22
|
+
function info(msg) { console.log(`${DIM} ℹ ${msg}${RESET}`); }
|
|
23
|
+
function step(msg) { console.log(`\n${BOLD}${msg}${RESET}`); }
|
|
24
|
+
|
|
25
|
+
console.log(`\n${BOLD}coc-bridge restore 🔄${RESET}`);
|
|
26
|
+
console.log(`${DIM}Reverting Claude Code & VS Code back to official Anthropic defaults...${RESET}`);
|
|
27
|
+
|
|
28
|
+
// ─────────────────────────────────────────────
|
|
29
|
+
// 1. Stop running bridge daemon
|
|
30
|
+
// ─────────────────────────────────────────────
|
|
31
|
+
step('1. Stopping coc-bridge daemon...');
|
|
32
|
+
try {
|
|
33
|
+
const pidFile = path.join(os.homedir(), '.coc-bridge', 'bridge.pid');
|
|
34
|
+
if (fs.existsSync(pidFile)) {
|
|
35
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
36
|
+
if (pid) {
|
|
37
|
+
process.kill(pid, 'SIGTERM');
|
|
38
|
+
ok(`Terminated bridge process (PID ${pid})`);
|
|
39
|
+
}
|
|
40
|
+
fs.unlinkSync(pidFile);
|
|
41
|
+
} else {
|
|
42
|
+
info('Bridge is not running.');
|
|
43
|
+
}
|
|
44
|
+
} catch (_) {
|
|
45
|
+
info('Bridge stopped or already inactive.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─────────────────────────────────────────────
|
|
49
|
+
// 2. Restore claude binary wrapper
|
|
50
|
+
// ─────────────────────────────────────────────
|
|
51
|
+
step('2. Restoring official claude binary...');
|
|
52
|
+
let claudeBin;
|
|
53
|
+
try {
|
|
54
|
+
claudeBin = execSync('which claude', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
55
|
+
} catch (_) {}
|
|
56
|
+
|
|
57
|
+
if (claudeBin) {
|
|
58
|
+
const claudeBinReal = claudeBin + '-bin';
|
|
59
|
+
if (fs.existsSync(claudeBinReal)) {
|
|
60
|
+
// Overwrite the wrapper with the original executable
|
|
61
|
+
fs.copyFileSync(claudeBinReal, claudeBin);
|
|
62
|
+
fs.chmodSync(claudeBin, 0o755);
|
|
63
|
+
fs.unlinkSync(claudeBinReal);
|
|
64
|
+
ok(`Restored original binary at ${claudeBin}`);
|
|
65
|
+
} else {
|
|
66
|
+
// Check if the current file is a wrapper without backup
|
|
67
|
+
const content = fs.readFileSync(claudeBin, 'utf8');
|
|
68
|
+
if (content.includes('coc-bridge') || content.includes('ANTHROPIC_BASE_URL')) {
|
|
69
|
+
warn(`Claude wrapper detected, but backup binary ${claudeBinReal} was missing.`);
|
|
70
|
+
info('You can reinstall Claude Code cleanly via: npm install -g @anthropic-ai/claude-code');
|
|
71
|
+
} else {
|
|
72
|
+
ok(`Official claude binary is already in place (${claudeBin})`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
info('claude binary not found in PATH.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─────────────────────────────────────────────
|
|
80
|
+
// 3. Clean VS Code settings
|
|
81
|
+
// ─────────────────────────────────────────────
|
|
82
|
+
step('3. Restoring VS Code settings...');
|
|
83
|
+
|
|
84
|
+
const vsCodeCandidates = [
|
|
85
|
+
path.join(os.homedir(), '.config', 'Code', 'User', 'settings.json'),
|
|
86
|
+
path.join(os.homedir(), '.var', 'app', 'com.visualstudio.code', 'config', 'Code', 'User', 'settings.json'),
|
|
87
|
+
path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
|
88
|
+
process.env.APPDATA ? path.join(process.env.APPDATA, 'Code', 'User', 'settings.json') : null
|
|
89
|
+
].filter(Boolean);
|
|
90
|
+
|
|
91
|
+
let vsCodeUpdated = false;
|
|
92
|
+
for (const settingsPath of vsCodeCandidates) {
|
|
93
|
+
if (fs.existsSync(settingsPath)) {
|
|
94
|
+
try {
|
|
95
|
+
let raw = fs.readFileSync(settingsPath, 'utf8');
|
|
96
|
+
let cleaned = false;
|
|
97
|
+
|
|
98
|
+
// Clean using stripJsonComments + JSON parse
|
|
99
|
+
try {
|
|
100
|
+
const stripJsonComments = str => str.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? '' : m);
|
|
101
|
+
const json = JSON.parse(stripJsonComments(raw));
|
|
102
|
+
if (json['claudeCode.claudeProcessWrapper']) {
|
|
103
|
+
delete json['claudeCode.claudeProcessWrapper'];
|
|
104
|
+
cleaned = true;
|
|
105
|
+
}
|
|
106
|
+
if (json['claudeCode.environmentVariables']) {
|
|
107
|
+
// Remove bridge-injected variables
|
|
108
|
+
json['claudeCode.environmentVariables'] = json['claudeCode.environmentVariables'].filter(item => {
|
|
109
|
+
return !item.name.startsWith('ANTHROPIC_') && !item.name.startsWith('CLAUDE_CODE_');
|
|
110
|
+
});
|
|
111
|
+
if (json['claudeCode.environmentVariables'].length === 0) {
|
|
112
|
+
delete json['claudeCode.environmentVariables'];
|
|
113
|
+
}
|
|
114
|
+
cleaned = true;
|
|
115
|
+
}
|
|
116
|
+
if (cleaned) {
|
|
117
|
+
fs.writeFileSync(settingsPath, JSON.stringify(json, null, 4));
|
|
118
|
+
ok(`Restored VS Code settings: ${settingsPath}`);
|
|
119
|
+
vsCodeUpdated = true;
|
|
120
|
+
}
|
|
121
|
+
} catch (jsonErr) {
|
|
122
|
+
// If file contains JSON comments, strip wrapper lines with regex
|
|
123
|
+
raw = raw.replace(/"claudeCode\.claudeProcessWrapper":\s*"[^"]*",?/g, '');
|
|
124
|
+
fs.writeFileSync(settingsPath, raw);
|
|
125
|
+
ok(`Cleaned wrapper lines in: ${settingsPath}`);
|
|
126
|
+
vsCodeUpdated = true;
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
warn(`Could not update ${settingsPath}: ${e.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!vsCodeUpdated) {
|
|
135
|
+
info('No VS Code bridge overrides were found.');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─────────────────────────────────────────────
|
|
139
|
+
// Done
|
|
140
|
+
// ─────────────────────────────────────────────
|
|
141
|
+
console.log(`
|
|
142
|
+
${GREEN}${BOLD}✔ Successfully restored!${RESET}
|
|
143
|
+
|
|
144
|
+
Claude Code and VS Code are now back to normal official Anthropic mode.
|
|
145
|
+
No proxies or custom wrappers are active.
|
|
146
|
+
|
|
147
|
+
${DIM}Want coc-bridge back anytime? Just run:${RESET}
|
|
148
|
+
${BOLD}coc-bridge setup${RESET}
|
|
149
|
+
`);
|
package/scripts/setup-claude.js
CHANGED
|
@@ -137,16 +137,82 @@ if (!fs.existsSync(logDir)) {
|
|
|
137
137
|
ok(`Log dir exists: ${logDir}`);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// ─────────────────────────────────────────────
|
|
141
|
+
// 5. Configure VS Code extension automatically
|
|
142
|
+
// ─────────────────────────────────────────────
|
|
143
|
+
step('5. Configuring VS Code Claude Code extension...');
|
|
144
|
+
|
|
145
|
+
const vsCodeCandidates = [
|
|
146
|
+
path.join(os.homedir(), '.config', 'Code', 'User', 'settings.json'),
|
|
147
|
+
path.join(os.homedir(), '.var', 'app', 'com.visualstudio.code', 'config', 'Code', 'User', 'settings.json'),
|
|
148
|
+
path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
|
149
|
+
process.env.APPDATA ? path.join(process.env.APPDATA, 'Code', 'User', 'settings.json') : null
|
|
150
|
+
].filter(Boolean);
|
|
151
|
+
|
|
152
|
+
let vsCodeConfigured = false;
|
|
153
|
+
for (const settingsPath of vsCodeCandidates) {
|
|
154
|
+
if (fs.existsSync(settingsPath)) {
|
|
155
|
+
try {
|
|
156
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
157
|
+
const stripJsonComments = str => str.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? '' : m);
|
|
158
|
+
const settings = JSON.parse(stripJsonComments(raw));
|
|
159
|
+
|
|
160
|
+
settings['claudeCode.claudeProcessWrapper'] = claudeBin;
|
|
161
|
+
settings['claudeCode.environmentVariables'] = [
|
|
162
|
+
{ name: 'ANTHROPIC_BASE_URL', value: 'http://127.0.0.1:4045' },
|
|
163
|
+
{ name: 'ANTHROPIC_AUTH_TOKEN', value: 'coc-local' },
|
|
164
|
+
{ name: 'ANTHROPIC_API_KEY', value: '' },
|
|
165
|
+
{ name: 'CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT', value: '1' },
|
|
166
|
+
{ name: 'CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY', value: '1' },
|
|
167
|
+
{ name: 'ANTHROPIC_MODEL', value: 'opencode/space-bunny-free' },
|
|
168
|
+
{ name: 'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME', value: 'Space Bunny' },
|
|
169
|
+
{ name: 'ANTHROPIC_DEFAULT_OPUS_MODEL', value: 'opencode/nemotron-3-ultra-free' },
|
|
170
|
+
{ name: 'ANTHROPIC_DEFAULT_OPUS_MODEL_NAME', value: 'Nemotron 3 Ultra' },
|
|
171
|
+
{ name: 'ANTHROPIC_DEFAULT_SONNET_MODEL', value: 'opencode/muse-spark-1.3-contributor-free' },
|
|
172
|
+
{ name: 'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME', value: 'Muse Spark 1.3' },
|
|
173
|
+
{ name: 'ANTHROPIC_DEFAULT_HAIKU_MODEL', value: 'opencode/cat-jam-free' },
|
|
174
|
+
{ name: 'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME', value: 'Cat Jam' },
|
|
175
|
+
{ name: 'ANTHROPIC_DEFAULT_FABLE_MODEL', value: 'opencode/ling-3.1-flash-free' },
|
|
176
|
+
{ name: 'ANTHROPIC_DEFAULT_FABLE_MODEL_NAME', value: 'Ling 3.1 Flash' }
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4));
|
|
180
|
+
ok(`Configured VS Code: ${settingsPath}`);
|
|
181
|
+
vsCodeConfigured = true;
|
|
182
|
+
} catch (e) {
|
|
183
|
+
warn(`Could not automatically update ${settingsPath}: ${e.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!vsCodeConfigured) {
|
|
189
|
+
warn('VS Code settings not found. If using VS Code, set "claudeCode.claudeProcessWrapper": "' + claudeBin + '"');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─────────────────────────────────────────────
|
|
193
|
+
// 6. Ensure ~/.local/bin symlink exists
|
|
194
|
+
// ─────────────────────────────────────────────
|
|
195
|
+
step('6. Ensuring CLI command availability...');
|
|
196
|
+
try {
|
|
197
|
+
const localBin = path.join(os.homedir(), '.local', 'bin');
|
|
198
|
+
if (!fs.existsSync(localBin)) fs.mkdirSync(localBin, { recursive: true });
|
|
199
|
+
const targetLink = path.join(localBin, 'coc-bridge');
|
|
200
|
+
if (bridgeBin && !fs.existsSync(targetLink)) {
|
|
201
|
+
fs.symlinkSync(bridgeBin, targetLink);
|
|
202
|
+
ok(`Linked coc-bridge to ${targetLink}`);
|
|
203
|
+
}
|
|
204
|
+
} catch (_) {}
|
|
205
|
+
|
|
140
206
|
// ─────────────────────────────────────────────
|
|
141
207
|
// Done
|
|
142
208
|
// ─────────────────────────────────────────────
|
|
143
209
|
console.log(`
|
|
144
210
|
${GREEN}${BOLD}✅ Setup complete!${RESET}
|
|
145
211
|
|
|
146
|
-
Run ${BOLD}claude${RESET} as normal — coc-bridge
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
212
|
+
• Terminal: Run ${BOLD}claude${RESET} as normal — coc-bridge auto-starts in background.
|
|
213
|
+
• VS Code : Open the Claude Code extension — models are connected!
|
|
214
|
+
• Switch models: ${BOLD}coc-bridge models${RESET} / ${BOLD}coc-bridge use <id>${RESET}
|
|
215
|
+
• Want normal Claude back? Run: ${BOLD}coc-bridge restore${RESET}
|
|
150
216
|
|
|
151
217
|
Logs: ${logDir}/bridge.log
|
|
152
218
|
`);
|
package/src/server.js
CHANGED
|
@@ -9,6 +9,8 @@ const fs = require('fs');
|
|
|
9
9
|
const PORT = parseInt(process.env.BRIDGE_PORT || process.env.PORT || '4045', 10);
|
|
10
10
|
const MAX_PAYLOAD_BYTES = 50 * 1024 * 1024; // 50MB
|
|
11
11
|
const CONFIG_FILE = path.join(os.homedir(), '.coc-bridge', 'config.json');
|
|
12
|
+
const SANDBOX_DIR = path.join(os.tmpdir(), 'coc-bridge-sandbox');
|
|
13
|
+
try { fs.mkdirSync(SANDBOX_DIR, { recursive: true }); } catch (_) {}
|
|
12
14
|
|
|
13
15
|
// ─── Config (persists user's chosen default model) ────────────────────────────
|
|
14
16
|
function readConfig() {
|
|
@@ -126,60 +128,174 @@ function resolveModel(requested) {
|
|
|
126
128
|
|
|
127
129
|
|
|
128
130
|
/**
|
|
129
|
-
*
|
|
131
|
+
* Extract tool calls from model output.
|
|
132
|
+
* Supports:
|
|
133
|
+
* 1. ```tool_call ... ``` or ```json ... ``` blocks
|
|
134
|
+
* 2. <tool_call> ... </tool_call> XML tags
|
|
135
|
+
* 3. Bare JSON object containing "name" and "input" matching an allowed tool
|
|
136
|
+
*/
|
|
137
|
+
function extractToolCalls(text, allowedTools = []) {
|
|
138
|
+
if (!text) return { text: '', toolCalls: [] };
|
|
139
|
+
const toolCalls = [];
|
|
140
|
+
|
|
141
|
+
// 1. Match markdown blocks
|
|
142
|
+
const blockRegex = /```(?:tool_call|json)?\s*\n?([\s\S]*?)\n?```/gi;
|
|
143
|
+
let match;
|
|
144
|
+
let textSegments = [];
|
|
145
|
+
let lastIndex = 0;
|
|
146
|
+
|
|
147
|
+
while ((match = blockRegex.exec(text)) !== null) {
|
|
148
|
+
let isTool = false;
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(match[1].trim());
|
|
151
|
+
if (parsed && typeof parsed.name === 'string' && parsed.input !== undefined) {
|
|
152
|
+
const matchedTool = allowedTools.length === 0
|
|
153
|
+
? parsed.name
|
|
154
|
+
: allowedTools.find(t => t.toLowerCase() === parsed.name.toLowerCase());
|
|
155
|
+
if (matchedTool) {
|
|
156
|
+
toolCalls.push({ name: matchedTool, input: parsed.input });
|
|
157
|
+
textSegments.push(text.slice(lastIndex, match.index));
|
|
158
|
+
lastIndex = blockRegex.lastIndex;
|
|
159
|
+
isTool = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (_) {}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (toolCalls.length > 0) {
|
|
166
|
+
textSegments.push(text.slice(lastIndex));
|
|
167
|
+
const cleanText = textSegments.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
168
|
+
return { text: cleanText, toolCalls };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. Match <tool_call> tags
|
|
172
|
+
const xmlRegex = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/gi;
|
|
173
|
+
lastIndex = 0;
|
|
174
|
+
textSegments = [];
|
|
175
|
+
while ((match = xmlRegex.exec(text)) !== null) {
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(match[1].trim());
|
|
178
|
+
if (parsed && typeof parsed.name === 'string' && parsed.input !== undefined) {
|
|
179
|
+
const matchedTool = allowedTools.length === 0
|
|
180
|
+
? parsed.name
|
|
181
|
+
: allowedTools.find(t => t.toLowerCase() === parsed.name.toLowerCase());
|
|
182
|
+
if (matchedTool) {
|
|
183
|
+
toolCalls.push({ name: matchedTool, input: parsed.input });
|
|
184
|
+
textSegments.push(text.slice(lastIndex, match.index));
|
|
185
|
+
lastIndex = xmlRegex.lastIndex;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} catch (_) {}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (toolCalls.length > 0) {
|
|
192
|
+
textSegments.push(text.slice(lastIndex));
|
|
193
|
+
const cleanText = textSegments.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
194
|
+
return { text: cleanText, toolCalls };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 3. Match bare JSON object with "name" and "input"
|
|
198
|
+
const bareMatch = text.match(/(\{\s*"name"\s*:\s*"[^"]+"\s*,\s*"input"\s*:\s*\{[\s\S]*?\}\s*\})/);
|
|
199
|
+
if (bareMatch) {
|
|
200
|
+
try {
|
|
201
|
+
const parsed = JSON.parse(bareMatch[1].trim());
|
|
202
|
+
if (parsed && typeof parsed.name === 'string' && parsed.input !== undefined) {
|
|
203
|
+
const matchedTool = allowedTools.length === 0
|
|
204
|
+
? parsed.name
|
|
205
|
+
: allowedTools.find(t => t.toLowerCase() === parsed.name.toLowerCase());
|
|
206
|
+
if (matchedTool) {
|
|
207
|
+
const pre = text.slice(0, bareMatch.index).trim();
|
|
208
|
+
const post = text.slice(bareMatch.index + bareMatch[1].length).trim();
|
|
209
|
+
return {
|
|
210
|
+
text: [pre, post].filter(Boolean).join('\n'),
|
|
211
|
+
toolCalls: [{ name: matchedTool, input: parsed.input }]
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch (_) {}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { text: text.trim(), toolCalls: [] };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Convert Anthropic message format into a clean prompt string for OpenCode CLI,
|
|
223
|
+
* with full tool schemas injected so Claude Code controls tool execution.
|
|
130
224
|
*/
|
|
131
225
|
function buildPrompt(body) {
|
|
132
226
|
let promptParts = [];
|
|
227
|
+
const allowedTools = [];
|
|
228
|
+
|
|
229
|
+
// 1. Tool schemas and instructions
|
|
230
|
+
if (Array.isArray(body.tools) && body.tools.length > 0) {
|
|
231
|
+
let toolsDesc = '## Available Tools & Instructions\n';
|
|
232
|
+
toolsDesc += 'You are acting as the AI model for Claude Code. You do NOT have any built-in tools or direct filesystem access.\n';
|
|
233
|
+
toolsDesc += 'Do NOT attempt to use any internal OpenCode tools (like read, glob, shell, or edit).\n';
|
|
234
|
+
toolsDesc += 'Instead, to view files, make edits, run terminal commands, or list directories, you MUST emit a tool call in this exact JSON block:\n\n';
|
|
235
|
+
toolsDesc += '```tool_call\n';
|
|
236
|
+
toolsDesc += '{"name": "<ToolName>", "input": { ...arguments according to input schema... }}\n';
|
|
237
|
+
toolsDesc += '```\n\n';
|
|
238
|
+
toolsDesc += 'Claude Code will execute the tool on the user\'s local machine and return the result to you in the next message.\n';
|
|
239
|
+
toolsDesc += 'When you have enough information to answer the user without tools, reply with your normal explanation and NO tool_call blocks.\n\n';
|
|
240
|
+
toolsDesc += 'Tools available to request:\n';
|
|
241
|
+
|
|
242
|
+
for (const tool of body.tools) {
|
|
243
|
+
if (tool && tool.name) {
|
|
244
|
+
allowedTools.push(tool.name);
|
|
245
|
+
toolsDesc += `\n### Tool: ${tool.name}\n`;
|
|
246
|
+
if (tool.description) toolsDesc += `Description: ${tool.description}\n`;
|
|
247
|
+
if (tool.input_schema) toolsDesc += `Input Schema: ${JSON.stringify(tool.input_schema)}\n`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
promptParts.push(`[System Instructions & Tools]\n${toolsDesc}\n`);
|
|
252
|
+
}
|
|
133
253
|
|
|
134
|
-
// Add system
|
|
254
|
+
// 2. Add system prompt from Claude Code
|
|
135
255
|
if (body.system) {
|
|
256
|
+
let sysText = '';
|
|
136
257
|
if (typeof body.system === 'string') {
|
|
137
|
-
|
|
258
|
+
sysText = body.system;
|
|
138
259
|
} else if (Array.isArray(body.system)) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
260
|
+
sysText = body.system.map(s => (typeof s === 'string' ? s : (s.text || ''))).join('\n');
|
|
261
|
+
}
|
|
262
|
+
if (sysText.trim()) {
|
|
263
|
+
promptParts.push(`[Claude Code Instructions]\n${sysText.trim()}\n`);
|
|
143
264
|
}
|
|
144
265
|
}
|
|
145
266
|
|
|
146
|
-
// Add
|
|
267
|
+
// 3. Add message history
|
|
147
268
|
if (Array.isArray(body.messages)) {
|
|
148
269
|
for (const msg of body.messages) {
|
|
149
270
|
const role = msg.role === 'assistant' ? 'Assistant' : 'User';
|
|
150
|
-
let content = '';
|
|
151
271
|
|
|
152
272
|
if (typeof msg.content === 'string') {
|
|
153
|
-
|
|
273
|
+
promptParts.push(`${role}: ${msg.content.trim()}`);
|
|
154
274
|
} else if (Array.isArray(msg.content)) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
.join('\n');
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (content.trim()) {
|
|
175
|
-
promptParts.push(`${role}: ${content}`);
|
|
275
|
+
let textParts = [];
|
|
276
|
+
for (const part of msg.content) {
|
|
277
|
+
if (part.type === 'text' && part.text) {
|
|
278
|
+
textParts.push(part.text);
|
|
279
|
+
} else if (part.type === 'tool_use') {
|
|
280
|
+
textParts.push(`\`\`\`tool_call\n${JSON.stringify({ name: part.name, input: part.input })}\n\`\`\``);
|
|
281
|
+
} else if (part.type === 'tool_result') {
|
|
282
|
+
const resContent = typeof part.content === 'string'
|
|
283
|
+
? part.content
|
|
284
|
+
: (Array.isArray(part.content)
|
|
285
|
+
? part.content.map(c => c.text || JSON.stringify(c)).join('\n')
|
|
286
|
+
: JSON.stringify(part.content || ''));
|
|
287
|
+
textParts.push(`[Tool Result for ${part.tool_use_id || 'tool'}]:\n${resContent}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (textParts.length > 0) {
|
|
291
|
+
promptParts.push(`${role}: ${textParts.join('\n\n')}`);
|
|
292
|
+
}
|
|
176
293
|
}
|
|
177
294
|
}
|
|
178
295
|
}
|
|
179
296
|
|
|
180
|
-
// If there's an agent prompt turn, make sure Assistant is ready to answer
|
|
181
297
|
promptParts.push('Assistant:');
|
|
182
|
-
return promptParts.join('\n\n');
|
|
298
|
+
return { prompt: promptParts.join('\n\n'), allowedTools };
|
|
183
299
|
}
|
|
184
300
|
|
|
185
301
|
/**
|
|
@@ -196,14 +312,15 @@ function handleMessagesRequest(req, res, reqBody) {
|
|
|
196
312
|
}
|
|
197
313
|
|
|
198
314
|
const modelName = resolveModel(body.model);
|
|
199
|
-
const prompt = buildPrompt(body);
|
|
315
|
+
const { prompt, allowedTools } = buildPrompt(body);
|
|
200
316
|
const isStreaming = Boolean(body.stream);
|
|
201
317
|
const messageId = `msg_${crypto.randomBytes(12).toString('hex')}`;
|
|
202
318
|
|
|
203
|
-
console.log(`[Bridge]
|
|
319
|
+
console.log(`[Bridge] Request: ${modelName} (tools: ${allowedTools.length}, streaming=${isStreaming})`);
|
|
204
320
|
|
|
205
|
-
//
|
|
206
|
-
const child = spawn(OPENCODE_BIN, ['run', '-m', modelName], {
|
|
321
|
+
// Run in empty sandbox dir so OpenCode cannot touch user files directly
|
|
322
|
+
const child = spawn(OPENCODE_BIN, ['run', '-m', modelName, '--format', 'json'], {
|
|
323
|
+
cwd: SANDBOX_DIR,
|
|
207
324
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
208
325
|
env: { ...process.env }
|
|
209
326
|
});
|
|
@@ -226,108 +343,185 @@ function handleMessagesRequest(req, res, reqBody) {
|
|
|
226
343
|
}
|
|
227
344
|
});
|
|
228
345
|
|
|
229
|
-
// Write prompt to stdin and close
|
|
230
346
|
child.stdin.write(prompt);
|
|
231
347
|
child.stdin.end();
|
|
232
348
|
|
|
233
|
-
let
|
|
349
|
+
let stdoutBuffer = '';
|
|
350
|
+
let accumulatedText = '';
|
|
234
351
|
let accumulatedError = '';
|
|
352
|
+
let reportedTokens = null;
|
|
353
|
+
|
|
354
|
+
child.stdout.on('data', chunk => {
|
|
355
|
+
stdoutBuffer += chunk.toString();
|
|
356
|
+
const lines = stdoutBuffer.split('\n');
|
|
357
|
+
stdoutBuffer = lines.pop(); // keep partial line
|
|
358
|
+
|
|
359
|
+
for (const line of lines) {
|
|
360
|
+
if (!line.trim()) continue;
|
|
361
|
+
try {
|
|
362
|
+
const ev = JSON.parse(line);
|
|
363
|
+
if (ev.type === 'text' && ev.part && typeof ev.part.text === 'string') {
|
|
364
|
+
accumulatedText += ev.part.text;
|
|
365
|
+
} else if (ev.type === 'step_finish' && ev.part && ev.part.tokens) {
|
|
366
|
+
reportedTokens = ev.part.tokens;
|
|
367
|
+
}
|
|
368
|
+
} catch (_) {
|
|
369
|
+
accumulatedText += line + '\n';
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
235
373
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
240
|
-
'Cache-Control': 'no-cache',
|
|
241
|
-
'Connection': 'keep-alive'
|
|
242
|
-
});
|
|
374
|
+
child.stderr.on('data', chunk => {
|
|
375
|
+
accumulatedError += chunk.toString();
|
|
376
|
+
});
|
|
243
377
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
type: 'message_start',
|
|
247
|
-
message: {
|
|
248
|
-
id: messageId,
|
|
249
|
-
type: 'message',
|
|
250
|
-
role: 'assistant',
|
|
251
|
-
model: modelName,
|
|
252
|
-
content: [],
|
|
253
|
-
stop_reason: null,
|
|
254
|
-
stop_sequence: null,
|
|
255
|
-
usage: { input_tokens: Math.ceil(prompt.length / 4), output_tokens: 1 }
|
|
256
|
-
}
|
|
257
|
-
})}\n\n`);
|
|
258
|
-
|
|
259
|
-
// 2. content_block_start
|
|
260
|
-
res.write(`event: content_block_start\ndata: ${JSON.stringify({
|
|
261
|
-
type: 'content_block_start',
|
|
262
|
-
index: 0,
|
|
263
|
-
content_block: { type: 'text', text: '' }
|
|
264
|
-
})}\n\n`);
|
|
265
|
-
|
|
266
|
-
// Stream chunks as they arrive from OpenCode stdout
|
|
267
|
-
child.stdout.on('data', chunk => {
|
|
268
|
-
const text = chunk.toString();
|
|
269
|
-
accumulatedOutput += text;
|
|
270
|
-
res.write(`event: content_block_delta\ndata: ${JSON.stringify({
|
|
271
|
-
type: 'content_block_delta',
|
|
272
|
-
index: 0,
|
|
273
|
-
delta: { type: 'text_delta', text: text }
|
|
274
|
-
})}\n\n`);
|
|
275
|
-
});
|
|
378
|
+
child.on('close', code => {
|
|
379
|
+
clearTimeout(timer);
|
|
276
380
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
381
|
+
// Process any remaining buffered stdout
|
|
382
|
+
if (stdoutBuffer.trim()) {
|
|
383
|
+
try {
|
|
384
|
+
const ev = JSON.parse(stdoutBuffer);
|
|
385
|
+
if (ev.type === 'text' && ev.part && typeof ev.part.text === 'string') {
|
|
386
|
+
accumulatedText += ev.part.text;
|
|
387
|
+
} else if (ev.type === 'step_finish' && ev.part && ev.part.tokens) {
|
|
388
|
+
reportedTokens = ev.part.tokens;
|
|
389
|
+
}
|
|
390
|
+
} catch (_) {
|
|
391
|
+
accumulatedText += stdoutBuffer;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
280
394
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
395
|
+
if (code !== 0 && !accumulatedText.trim()) {
|
|
396
|
+
console.error(`[Bridge] OpenCode exited with code ${code}: ${accumulatedError}`);
|
|
397
|
+
const errMsg = accumulatedError.trim() || `OpenCode process exited with code ${code}`;
|
|
398
|
+
if (isStreaming) {
|
|
399
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream; charset=utf-8' });
|
|
286
400
|
res.write(`event: error\ndata: ${JSON.stringify({ type: 'error', error: { message: errMsg } })}\n\n`);
|
|
287
401
|
res.end();
|
|
288
|
-
|
|
402
|
+
} else {
|
|
403
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
404
|
+
res.end(JSON.stringify({ error: { type: 'api_error', message: errMsg } }));
|
|
289
405
|
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
290
408
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
409
|
+
// Token accounting
|
|
410
|
+
const inputTokens = (reportedTokens && reportedTokens.input) || Math.ceil(prompt.length / 4);
|
|
411
|
+
const outputTokens = (reportedTokens && reportedTokens.output)
|
|
412
|
+
? (reportedTokens.output + (reportedTokens.reasoning || 0))
|
|
413
|
+
: Math.ceil(accumulatedText.length / 4);
|
|
414
|
+
|
|
415
|
+
// Extract tool calls
|
|
416
|
+
const { text: cleanText, toolCalls } = extractToolCalls(accumulatedText, allowedTools);
|
|
417
|
+
const hasToolCalls = toolCalls.length > 0;
|
|
418
|
+
const stopReason = hasToolCalls ? 'tool_use' : 'end_turn';
|
|
419
|
+
|
|
420
|
+
if (isStreaming) {
|
|
421
|
+
res.writeHead(200, {
|
|
422
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
423
|
+
'Cache-Control': 'no-cache',
|
|
424
|
+
'Connection': 'keep-alive'
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// 1. message_start
|
|
428
|
+
res.write(`event: message_start\ndata: ${JSON.stringify({
|
|
429
|
+
type: 'message_start',
|
|
430
|
+
message: {
|
|
431
|
+
id: messageId,
|
|
432
|
+
type: 'message',
|
|
433
|
+
role: 'assistant',
|
|
434
|
+
model: modelName,
|
|
435
|
+
content: [],
|
|
436
|
+
stop_reason: null,
|
|
437
|
+
stop_sequence: null,
|
|
438
|
+
usage: { input_tokens: inputTokens, output_tokens: 1 }
|
|
439
|
+
}
|
|
295
440
|
})}\n\n`);
|
|
296
441
|
|
|
442
|
+
let blockIndex = 0;
|
|
443
|
+
|
|
444
|
+
// 2. Text block (if clean text exists)
|
|
445
|
+
if (cleanText.length > 0) {
|
|
446
|
+
res.write(`event: content_block_start\ndata: ${JSON.stringify({
|
|
447
|
+
type: 'content_block_start',
|
|
448
|
+
index: blockIndex,
|
|
449
|
+
content_block: { type: 'text', text: '' }
|
|
450
|
+
})}\n\n`);
|
|
451
|
+
|
|
452
|
+
res.write(`event: content_block_delta\ndata: ${JSON.stringify({
|
|
453
|
+
type: 'content_block_delta',
|
|
454
|
+
index: blockIndex,
|
|
455
|
+
delta: { type: 'text_delta', text: cleanText }
|
|
456
|
+
})}\n\n`);
|
|
457
|
+
|
|
458
|
+
res.write(`event: content_block_stop\ndata: ${JSON.stringify({
|
|
459
|
+
type: 'content_block_stop',
|
|
460
|
+
index: blockIndex
|
|
461
|
+
})}\n\n`);
|
|
462
|
+
|
|
463
|
+
blockIndex++;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// 3. Tool use blocks
|
|
467
|
+
for (const tc of toolCalls) {
|
|
468
|
+
const toolUseId = `toolu_${crypto.randomBytes(12).toString('hex')}`;
|
|
469
|
+
|
|
470
|
+
res.write(`event: content_block_start\ndata: ${JSON.stringify({
|
|
471
|
+
type: 'content_block_start',
|
|
472
|
+
index: blockIndex,
|
|
473
|
+
content_block: {
|
|
474
|
+
type: 'tool_use',
|
|
475
|
+
id: toolUseId,
|
|
476
|
+
name: tc.name,
|
|
477
|
+
input: {}
|
|
478
|
+
}
|
|
479
|
+
})}\n\n`);
|
|
480
|
+
|
|
481
|
+
res.write(`event: content_block_delta\ndata: ${JSON.stringify({
|
|
482
|
+
type: 'content_block_delta',
|
|
483
|
+
index: blockIndex,
|
|
484
|
+
delta: {
|
|
485
|
+
type: 'input_json_delta',
|
|
486
|
+
partial_json: JSON.stringify(tc.input)
|
|
487
|
+
}
|
|
488
|
+
})}\n\n`);
|
|
489
|
+
|
|
490
|
+
res.write(`event: content_block_stop\ndata: ${JSON.stringify({
|
|
491
|
+
type: 'content_block_stop',
|
|
492
|
+
index: blockIndex
|
|
493
|
+
})}\n\n`);
|
|
494
|
+
|
|
495
|
+
blockIndex++;
|
|
496
|
+
}
|
|
497
|
+
|
|
297
498
|
// 4. message_delta
|
|
298
499
|
res.write(`event: message_delta\ndata: ${JSON.stringify({
|
|
299
500
|
type: 'message_delta',
|
|
300
|
-
delta: { stop_reason:
|
|
301
|
-
usage: { output_tokens:
|
|
501
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
502
|
+
usage: { output_tokens: outputTokens }
|
|
302
503
|
})}\n\n`);
|
|
303
504
|
|
|
304
505
|
// 5. message_stop
|
|
305
506
|
res.write(`event: message_stop\ndata: ${JSON.stringify({ type: 'message_stop' })}\n\n`);
|
|
306
507
|
res.end();
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
} else {
|
|
310
|
-
// Non-streaming response
|
|
311
|
-
child.stdout.on('data', chunk => {
|
|
312
|
-
accumulatedOutput += chunk.toString();
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
child.stderr.on('data', chunk => {
|
|
316
|
-
accumulatedError += chunk.toString();
|
|
317
|
-
});
|
|
318
508
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
509
|
+
} else {
|
|
510
|
+
// Non-streaming response
|
|
511
|
+
const content = [];
|
|
512
|
+
if (cleanText.length > 0) {
|
|
513
|
+
content.push({ type: 'text', text: cleanText });
|
|
514
|
+
}
|
|
515
|
+
for (const tc of toolCalls) {
|
|
516
|
+
content.push({
|
|
517
|
+
type: 'tool_use',
|
|
518
|
+
id: `toolu_${crypto.randomBytes(12).toString('hex')}`,
|
|
519
|
+
name: tc.name,
|
|
520
|
+
input: tc.input
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
if (content.length === 0) {
|
|
524
|
+
content.push({ type: 'text', text: accumulatedText.trim() });
|
|
331
525
|
}
|
|
332
526
|
|
|
333
527
|
const responsePayload = {
|
|
@@ -335,24 +529,19 @@ function handleMessagesRequest(req, res, reqBody) {
|
|
|
335
529
|
type: 'message',
|
|
336
530
|
role: 'assistant',
|
|
337
531
|
model: modelName,
|
|
338
|
-
content
|
|
339
|
-
|
|
340
|
-
type: 'text',
|
|
341
|
-
text: accumulatedOutput.trim()
|
|
342
|
-
}
|
|
343
|
-
],
|
|
344
|
-
stop_reason: 'end_turn',
|
|
532
|
+
content,
|
|
533
|
+
stop_reason: stopReason,
|
|
345
534
|
stop_sequence: null,
|
|
346
535
|
usage: {
|
|
347
|
-
input_tokens:
|
|
348
|
-
output_tokens:
|
|
536
|
+
input_tokens: inputTokens,
|
|
537
|
+
output_tokens: outputTokens
|
|
349
538
|
}
|
|
350
539
|
};
|
|
351
540
|
|
|
352
541
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
353
542
|
res.end(JSON.stringify(responsePayload));
|
|
354
|
-
}
|
|
355
|
-
}
|
|
543
|
+
}
|
|
544
|
+
});
|
|
356
545
|
}
|
|
357
546
|
|
|
358
547
|
// HTTP Server
|