@gricha/perry 0.1.7 → 0.1.8
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/dist/agent/router.js +59 -0
- package/dist/agent/run.js +4 -0
- package/dist/agent/web/assets/index-CaFOQOgc.css +1 -0
- package/dist/agent/web/assets/index-DQmM39Em.js +104 -0
- package/dist/agent/web/index.html +2 -2
- package/dist/chat/base-chat-websocket.js +5 -2
- package/dist/chat/handler.js +16 -0
- package/dist/chat/host-opencode-handler.js +22 -0
- package/dist/chat/opencode-handler.js +22 -0
- package/dist/chat/opencode-server.js +98 -65
- package/dist/chat/opencode-websocket.js +11 -3
- package/dist/chat/websocket.js +4 -4
- package/dist/models/cache.js +70 -0
- package/dist/models/discovery.js +108 -0
- package/package.json +1 -1
- package/dist/agent/web/assets/index-BTiTEcB0.js +0 -104
- package/dist/agent/web/assets/index-D2_-UqVf.css +0 -1
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Perry</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DQmM39Em.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CaFOQOgc.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
|
@@ -41,13 +41,16 @@ export class BaseChatWebSocketServer extends BaseWebSocketServer {
|
|
|
41
41
|
};
|
|
42
42
|
if (!connection.session) {
|
|
43
43
|
if (isHostMode) {
|
|
44
|
-
connection.session = this.createHostSession(message.sessionId, onMessage);
|
|
44
|
+
connection.session = this.createHostSession(message.sessionId, onMessage, message.model);
|
|
45
45
|
}
|
|
46
46
|
else {
|
|
47
47
|
const containerName = getContainerName(workspaceName);
|
|
48
|
-
connection.session = this.createContainerSession(containerName, message.sessionId, onMessage);
|
|
48
|
+
connection.session = this.createContainerSession(containerName, message.sessionId, onMessage, message.model);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
else if (message.model && connection.session.setModel) {
|
|
52
|
+
connection.session.setModel(message.model);
|
|
53
|
+
}
|
|
51
54
|
await connection.session.sendMessage(message.content);
|
|
52
55
|
}
|
|
53
56
|
}
|
package/dist/chat/handler.js
CHANGED
|
@@ -4,6 +4,7 @@ export class ChatSession {
|
|
|
4
4
|
workDir;
|
|
5
5
|
sessionId;
|
|
6
6
|
model;
|
|
7
|
+
sessionModel;
|
|
7
8
|
onMessage;
|
|
8
9
|
buffer = '';
|
|
9
10
|
constructor(options, onMessage) {
|
|
@@ -11,6 +12,7 @@ export class ChatSession {
|
|
|
11
12
|
this.workDir = options.workDir || '/home/workspace';
|
|
12
13
|
this.sessionId = options.sessionId;
|
|
13
14
|
this.model = options.model || 'sonnet';
|
|
15
|
+
this.sessionModel = this.model;
|
|
14
16
|
this.onMessage = onMessage;
|
|
15
17
|
}
|
|
16
18
|
async sendMessage(userMessage) {
|
|
@@ -121,6 +123,7 @@ export class ChatSession {
|
|
|
121
123
|
const timestamp = new Date().toISOString();
|
|
122
124
|
if (msg.type === 'system' && msg.subtype === 'init') {
|
|
123
125
|
this.sessionId = msg.session_id;
|
|
126
|
+
this.sessionModel = this.model;
|
|
124
127
|
this.onMessage({
|
|
125
128
|
type: 'system',
|
|
126
129
|
content: `Session started: ${msg.session_id?.slice(0, 8)}...`,
|
|
@@ -165,6 +168,19 @@ export class ChatSession {
|
|
|
165
168
|
});
|
|
166
169
|
}
|
|
167
170
|
}
|
|
171
|
+
setModel(model) {
|
|
172
|
+
if (this.model !== model) {
|
|
173
|
+
this.model = model;
|
|
174
|
+
if (this.sessionModel !== model) {
|
|
175
|
+
this.sessionId = undefined;
|
|
176
|
+
this.onMessage({
|
|
177
|
+
type: 'system',
|
|
178
|
+
content: `Switching to model: ${model}`,
|
|
179
|
+
timestamp: new Date().toISOString(),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
168
184
|
getSessionId() {
|
|
169
185
|
return this.sessionId;
|
|
170
186
|
}
|
|
@@ -5,12 +5,16 @@ export class HostOpencodeSession {
|
|
|
5
5
|
process = null;
|
|
6
6
|
workDir;
|
|
7
7
|
sessionId;
|
|
8
|
+
model;
|
|
9
|
+
sessionModel;
|
|
8
10
|
onMessage;
|
|
9
11
|
buffer = '';
|
|
10
12
|
historyLoaded = false;
|
|
11
13
|
constructor(options, onMessage) {
|
|
12
14
|
this.workDir = options.workDir || homedir();
|
|
13
15
|
this.sessionId = options.sessionId;
|
|
16
|
+
this.model = options.model;
|
|
17
|
+
this.sessionModel = options.model;
|
|
14
18
|
this.onMessage = onMessage;
|
|
15
19
|
}
|
|
16
20
|
async loadHistory() {
|
|
@@ -126,6 +130,9 @@ export class HostOpencodeSession {
|
|
|
126
130
|
if (this.sessionId) {
|
|
127
131
|
args.push('--session', this.sessionId);
|
|
128
132
|
}
|
|
133
|
+
if (this.model) {
|
|
134
|
+
args.push('--model', this.model);
|
|
135
|
+
}
|
|
129
136
|
args.push(userMessage);
|
|
130
137
|
console.log('[host-opencode] Running: stdbuf', args.join(' '));
|
|
131
138
|
this.onMessage({
|
|
@@ -218,6 +225,7 @@ export class HostOpencodeSession {
|
|
|
218
225
|
if (event.type === 'step_start' && event.sessionID) {
|
|
219
226
|
if (!this.sessionId) {
|
|
220
227
|
this.sessionId = event.sessionID;
|
|
228
|
+
this.sessionModel = this.model;
|
|
221
229
|
this.historyLoaded = true;
|
|
222
230
|
this.onMessage({
|
|
223
231
|
type: 'system',
|
|
@@ -272,6 +280,20 @@ export class HostOpencodeSession {
|
|
|
272
280
|
});
|
|
273
281
|
}
|
|
274
282
|
}
|
|
283
|
+
setModel(model) {
|
|
284
|
+
if (this.model !== model) {
|
|
285
|
+
this.model = model;
|
|
286
|
+
if (this.sessionModel !== model) {
|
|
287
|
+
this.sessionId = undefined;
|
|
288
|
+
this.historyLoaded = false;
|
|
289
|
+
this.onMessage({
|
|
290
|
+
type: 'system',
|
|
291
|
+
content: `Switching to model: ${model}`,
|
|
292
|
+
timestamp: new Date().toISOString(),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
275
297
|
getSessionId() {
|
|
276
298
|
return this.sessionId;
|
|
277
299
|
}
|
|
@@ -4,6 +4,8 @@ export class OpencodeSession {
|
|
|
4
4
|
containerName;
|
|
5
5
|
workDir;
|
|
6
6
|
sessionId;
|
|
7
|
+
model;
|
|
8
|
+
sessionModel;
|
|
7
9
|
onMessage;
|
|
8
10
|
buffer = '';
|
|
9
11
|
historyLoaded = false;
|
|
@@ -11,6 +13,8 @@ export class OpencodeSession {
|
|
|
11
13
|
this.containerName = options.containerName;
|
|
12
14
|
this.workDir = options.workDir || '/home/workspace';
|
|
13
15
|
this.sessionId = options.sessionId;
|
|
16
|
+
this.model = options.model;
|
|
17
|
+
this.sessionModel = options.model;
|
|
14
18
|
this.onMessage = onMessage;
|
|
15
19
|
}
|
|
16
20
|
async loadHistory() {
|
|
@@ -86,6 +90,9 @@ export class OpencodeSession {
|
|
|
86
90
|
if (this.sessionId) {
|
|
87
91
|
args.push('--session', this.sessionId);
|
|
88
92
|
}
|
|
93
|
+
if (this.model) {
|
|
94
|
+
args.push('--model', this.model);
|
|
95
|
+
}
|
|
89
96
|
args.push(userMessage);
|
|
90
97
|
console.log('[opencode] Running:', 'docker', args.join(' '));
|
|
91
98
|
this.onMessage({
|
|
@@ -174,6 +181,7 @@ export class OpencodeSession {
|
|
|
174
181
|
if (event.type === 'step_start' && event.sessionID) {
|
|
175
182
|
if (!this.sessionId) {
|
|
176
183
|
this.sessionId = event.sessionID;
|
|
184
|
+
this.sessionModel = this.model;
|
|
177
185
|
this.historyLoaded = true;
|
|
178
186
|
this.onMessage({
|
|
179
187
|
type: 'system',
|
|
@@ -228,6 +236,20 @@ export class OpencodeSession {
|
|
|
228
236
|
});
|
|
229
237
|
}
|
|
230
238
|
}
|
|
239
|
+
setModel(model) {
|
|
240
|
+
if (this.model !== model) {
|
|
241
|
+
this.model = model;
|
|
242
|
+
if (this.sessionModel !== model) {
|
|
243
|
+
this.sessionId = undefined;
|
|
244
|
+
this.historyLoaded = false;
|
|
245
|
+
this.onMessage({
|
|
246
|
+
type: 'system',
|
|
247
|
+
content: `Switching to model: ${model}`,
|
|
248
|
+
timestamp: new Date().toISOString(),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
231
253
|
getSessionId() {
|
|
232
254
|
return this.sessionId;
|
|
233
255
|
}
|
|
@@ -53,6 +53,8 @@ export class OpenCodeServerSession {
|
|
|
53
53
|
containerName;
|
|
54
54
|
workDir;
|
|
55
55
|
sessionId;
|
|
56
|
+
model;
|
|
57
|
+
sessionModel;
|
|
56
58
|
onMessage;
|
|
57
59
|
sseProcess = null;
|
|
58
60
|
responseComplete = false;
|
|
@@ -62,6 +64,8 @@ export class OpenCodeServerSession {
|
|
|
62
64
|
this.containerName = options.containerName;
|
|
63
65
|
this.workDir = options.workDir || '/home/workspace';
|
|
64
66
|
this.sessionId = options.sessionId;
|
|
67
|
+
this.model = options.model;
|
|
68
|
+
this.sessionModel = options.model;
|
|
65
69
|
this.onMessage = onMessage;
|
|
66
70
|
}
|
|
67
71
|
async sendMessage(userMessage) {
|
|
@@ -74,6 +78,7 @@ export class OpenCodeServerSession {
|
|
|
74
78
|
});
|
|
75
79
|
try {
|
|
76
80
|
if (!this.sessionId) {
|
|
81
|
+
const sessionPayload = this.model ? JSON.stringify({ model: this.model }) : '{}';
|
|
77
82
|
const createResult = await execInContainer(this.containerName, [
|
|
78
83
|
'curl',
|
|
79
84
|
'-s',
|
|
@@ -83,10 +88,11 @@ export class OpenCodeServerSession {
|
|
|
83
88
|
'-H',
|
|
84
89
|
'Content-Type: application/json',
|
|
85
90
|
'-d',
|
|
86
|
-
|
|
91
|
+
sessionPayload,
|
|
87
92
|
], { user: 'workspace' });
|
|
88
93
|
const session = JSON.parse(createResult.stdout);
|
|
89
94
|
this.sessionId = session.id;
|
|
95
|
+
this.sessionModel = this.model;
|
|
90
96
|
this.onMessage({
|
|
91
97
|
type: 'system',
|
|
92
98
|
content: `Session started ${this.sessionId}`,
|
|
@@ -96,8 +102,8 @@ export class OpenCodeServerSession {
|
|
|
96
102
|
this.responseComplete = false;
|
|
97
103
|
this.seenToolUse.clear();
|
|
98
104
|
this.seenToolResult.clear();
|
|
99
|
-
const
|
|
100
|
-
await
|
|
105
|
+
const { ready, done } = await this.startSSEStream(port);
|
|
106
|
+
await ready;
|
|
101
107
|
const messagePayload = JSON.stringify({
|
|
102
108
|
parts: [{ type: 'text', text: userMessage }],
|
|
103
109
|
});
|
|
@@ -114,7 +120,7 @@ export class OpenCodeServerSession {
|
|
|
114
120
|
], { user: 'workspace' }).catch((err) => {
|
|
115
121
|
console.error('[opencode-server] Send error:', err);
|
|
116
122
|
});
|
|
117
|
-
await
|
|
123
|
+
await done;
|
|
118
124
|
this.onMessage({
|
|
119
125
|
type: 'done',
|
|
120
126
|
content: 'Response complete',
|
|
@@ -131,70 +137,84 @@ export class OpenCodeServerSession {
|
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
139
|
async startSSEStream(port) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
140
|
+
let resolveReady;
|
|
141
|
+
let resolveDone;
|
|
142
|
+
const ready = new Promise((r) => (resolveReady = r));
|
|
143
|
+
const done = new Promise((r) => (resolveDone = r));
|
|
144
|
+
const proc = Bun.spawn([
|
|
145
|
+
'docker',
|
|
146
|
+
'exec',
|
|
147
|
+
'-i',
|
|
148
|
+
this.containerName,
|
|
149
|
+
'curl',
|
|
150
|
+
'-s',
|
|
151
|
+
'-N',
|
|
152
|
+
'--max-time',
|
|
153
|
+
'120',
|
|
154
|
+
`http://localhost:${port}/event`,
|
|
155
|
+
], {
|
|
156
|
+
stdin: 'ignore',
|
|
157
|
+
stdout: 'pipe',
|
|
158
|
+
stderr: 'pipe',
|
|
159
|
+
});
|
|
160
|
+
this.sseProcess = proc;
|
|
161
|
+
const decoder = new TextDecoder();
|
|
162
|
+
let buffer = '';
|
|
163
|
+
let hasReceivedData = false;
|
|
164
|
+
const processChunk = (chunk) => {
|
|
165
|
+
buffer += decoder.decode(chunk);
|
|
166
|
+
if (!hasReceivedData) {
|
|
167
|
+
hasReceivedData = true;
|
|
168
|
+
resolveReady();
|
|
169
|
+
}
|
|
170
|
+
const lines = buffer.split('\n');
|
|
171
|
+
buffer = lines.pop() || '';
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
if (!line.startsWith('data: '))
|
|
174
|
+
continue;
|
|
175
|
+
const data = line.slice(6).trim();
|
|
176
|
+
if (!data)
|
|
177
|
+
continue;
|
|
178
|
+
try {
|
|
179
|
+
const event = JSON.parse(data);
|
|
180
|
+
this.handleEvent(event);
|
|
181
|
+
if (event.type === 'session.idle') {
|
|
182
|
+
this.responseComplete = true;
|
|
183
|
+
proc.kill();
|
|
184
|
+
resolveDone();
|
|
185
|
+
return;
|
|
176
186
|
}
|
|
177
187
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (!proc.stdout) {
|
|
181
|
-
resolve();
|
|
182
|
-
return;
|
|
188
|
+
catch {
|
|
189
|
+
continue;
|
|
183
190
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
(async () => {
|
|
194
|
+
if (!proc.stdout) {
|
|
195
|
+
resolveReady();
|
|
196
|
+
resolveDone();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
for await (const chunk of proc.stdout) {
|
|
200
|
+
processChunk(chunk);
|
|
201
|
+
if (this.responseComplete)
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
resolveDone();
|
|
205
|
+
})();
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
if (!hasReceivedData) {
|
|
208
|
+
resolveReady();
|
|
209
|
+
}
|
|
210
|
+
}, 500);
|
|
211
|
+
setTimeout(() => {
|
|
212
|
+
if (!this.responseComplete) {
|
|
213
|
+
proc.kill();
|
|
214
|
+
resolveDone();
|
|
215
|
+
}
|
|
216
|
+
}, 120000);
|
|
217
|
+
return { ready, done };
|
|
198
218
|
}
|
|
199
219
|
handleEvent(event) {
|
|
200
220
|
const timestamp = new Date().toISOString();
|
|
@@ -243,6 +263,19 @@ export class OpenCodeServerSession {
|
|
|
243
263
|
});
|
|
244
264
|
}
|
|
245
265
|
}
|
|
266
|
+
setModel(model) {
|
|
267
|
+
if (this.model !== model) {
|
|
268
|
+
this.model = model;
|
|
269
|
+
if (this.sessionModel !== model) {
|
|
270
|
+
this.sessionId = undefined;
|
|
271
|
+
this.onMessage({
|
|
272
|
+
type: 'system',
|
|
273
|
+
content: `Switching to model: ${model}`,
|
|
274
|
+
timestamp: new Date().toISOString(),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
246
279
|
getSessionId() {
|
|
247
280
|
return this.sessionId;
|
|
248
281
|
}
|
|
@@ -3,6 +3,11 @@ import { createHostOpencodeSession } from './host-opencode-handler';
|
|
|
3
3
|
import { createOpenCodeServerSession } from './opencode-server';
|
|
4
4
|
export class OpencodeWebSocketServer extends BaseChatWebSocketServer {
|
|
5
5
|
agentType = 'opencode';
|
|
6
|
+
getConfig;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
super(options);
|
|
9
|
+
this.getConfig = options.getConfig;
|
|
10
|
+
}
|
|
6
11
|
createConnection(ws, workspaceName) {
|
|
7
12
|
return {
|
|
8
13
|
ws,
|
|
@@ -10,14 +15,17 @@ export class OpencodeWebSocketServer extends BaseChatWebSocketServer {
|
|
|
10
15
|
workspaceName,
|
|
11
16
|
};
|
|
12
17
|
}
|
|
13
|
-
createHostSession(sessionId, onMessage) {
|
|
14
|
-
|
|
18
|
+
createHostSession(sessionId, onMessage, messageModel) {
|
|
19
|
+
const model = messageModel || this.getConfig?.()?.agents?.opencode?.model;
|
|
20
|
+
return createHostOpencodeSession({ sessionId, model }, onMessage);
|
|
15
21
|
}
|
|
16
|
-
createContainerSession(containerName, sessionId, onMessage) {
|
|
22
|
+
createContainerSession(containerName, sessionId, onMessage, messageModel) {
|
|
23
|
+
const model = messageModel || this.getConfig?.()?.agents?.opencode?.model;
|
|
17
24
|
return createOpenCodeServerSession({
|
|
18
25
|
containerName,
|
|
19
26
|
workDir: '/home/workspace',
|
|
20
27
|
sessionId,
|
|
28
|
+
model,
|
|
21
29
|
}, onMessage);
|
|
22
30
|
}
|
|
23
31
|
}
|
package/dist/chat/websocket.js
CHANGED
|
@@ -15,14 +15,14 @@ export class ChatWebSocketServer extends BaseChatWebSocketServer {
|
|
|
15
15
|
workspaceName,
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
createHostSession(sessionId, onMessage) {
|
|
18
|
+
createHostSession(sessionId, onMessage, messageModel) {
|
|
19
19
|
const config = this.getConfig();
|
|
20
|
-
const model = config.agents?.claude_code?.model;
|
|
20
|
+
const model = messageModel || config.agents?.claude_code?.model;
|
|
21
21
|
return createHostChatSession({ sessionId, model }, onMessage);
|
|
22
22
|
}
|
|
23
|
-
createContainerSession(containerName, sessionId, onMessage) {
|
|
23
|
+
createContainerSession(containerName, sessionId, onMessage, messageModel) {
|
|
24
24
|
const config = this.getConfig();
|
|
25
|
-
const model = config.agents?.claude_code?.model;
|
|
25
|
+
const model = messageModel || config.agents?.claude_code?.model;
|
|
26
26
|
return createChatSession({
|
|
27
27
|
containerName,
|
|
28
28
|
workDir: '/home/workspace',
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const CACHE_FILE = 'model-cache.json';
|
|
4
|
+
const TTL_MS = 60 * 60 * 1000;
|
|
5
|
+
export class ModelCacheManager {
|
|
6
|
+
cachePath;
|
|
7
|
+
cache = null;
|
|
8
|
+
constructor(configDir) {
|
|
9
|
+
this.cachePath = path.join(configDir, CACHE_FILE);
|
|
10
|
+
}
|
|
11
|
+
async load() {
|
|
12
|
+
if (this.cache) {
|
|
13
|
+
return this.cache;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const content = await fs.readFile(this.cachePath, 'utf-8');
|
|
17
|
+
this.cache = JSON.parse(content);
|
|
18
|
+
return this.cache;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
this.cache = {};
|
|
22
|
+
return this.cache;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async save() {
|
|
26
|
+
if (!this.cache)
|
|
27
|
+
return;
|
|
28
|
+
await fs.mkdir(path.dirname(this.cachePath), { recursive: true });
|
|
29
|
+
await fs.writeFile(this.cachePath, JSON.stringify(this.cache, null, 2), 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
isExpired(entry) {
|
|
32
|
+
if (!entry)
|
|
33
|
+
return true;
|
|
34
|
+
return Date.now() - entry.cachedAt > TTL_MS;
|
|
35
|
+
}
|
|
36
|
+
async getClaudeCodeModels() {
|
|
37
|
+
const cache = await this.load();
|
|
38
|
+
if (this.isExpired(cache.claudeCode)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return cache.claudeCode.models;
|
|
42
|
+
}
|
|
43
|
+
async setClaudeCodeModels(models) {
|
|
44
|
+
const cache = await this.load();
|
|
45
|
+
cache.claudeCode = {
|
|
46
|
+
models,
|
|
47
|
+
cachedAt: Date.now(),
|
|
48
|
+
};
|
|
49
|
+
await this.save();
|
|
50
|
+
}
|
|
51
|
+
async getOpencodeModels() {
|
|
52
|
+
const cache = await this.load();
|
|
53
|
+
if (this.isExpired(cache.opencode)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return cache.opencode.models;
|
|
57
|
+
}
|
|
58
|
+
async setOpencodeModels(models) {
|
|
59
|
+
const cache = await this.load();
|
|
60
|
+
cache.opencode = {
|
|
61
|
+
models,
|
|
62
|
+
cachedAt: Date.now(),
|
|
63
|
+
};
|
|
64
|
+
await this.save();
|
|
65
|
+
}
|
|
66
|
+
async clearCache() {
|
|
67
|
+
this.cache = {};
|
|
68
|
+
await this.save();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/models';
|
|
3
|
+
const ANTHROPIC_API_VERSION = '2023-06-01';
|
|
4
|
+
const FALLBACK_CLAUDE_MODELS = [
|
|
5
|
+
{ id: 'sonnet', name: 'Sonnet', description: 'Fast and capable' },
|
|
6
|
+
{ id: 'opus', name: 'Opus', description: 'Most capable' },
|
|
7
|
+
{ id: 'haiku', name: 'Haiku', description: 'Fastest' },
|
|
8
|
+
];
|
|
9
|
+
export async function discoverClaudeCodeModels(config) {
|
|
10
|
+
const oauthToken = config.agents?.claude_code?.oauth_token;
|
|
11
|
+
if (!oauthToken) {
|
|
12
|
+
return FALLBACK_CLAUDE_MODELS;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(ANTHROPIC_API_URL, {
|
|
16
|
+
method: 'GET',
|
|
17
|
+
headers: {
|
|
18
|
+
'x-api-key': oauthToken,
|
|
19
|
+
'anthropic-version': ANTHROPIC_API_VERSION,
|
|
20
|
+
},
|
|
21
|
+
signal: AbortSignal.timeout(5000),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
return FALLBACK_CLAUDE_MODELS;
|
|
25
|
+
}
|
|
26
|
+
const data = (await response.json());
|
|
27
|
+
const models = data.data
|
|
28
|
+
.filter((m) => m.type === 'model')
|
|
29
|
+
.map((m) => ({
|
|
30
|
+
id: m.id,
|
|
31
|
+
name: m.display_name || m.id,
|
|
32
|
+
}));
|
|
33
|
+
if (models.length === 0) {
|
|
34
|
+
return FALLBACK_CLAUDE_MODELS;
|
|
35
|
+
}
|
|
36
|
+
return models;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return FALLBACK_CLAUDE_MODELS;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function runCommand(command, args) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const child = spawn(command, args, {
|
|
45
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
|
+
});
|
|
47
|
+
let stdout = '';
|
|
48
|
+
let stderr = '';
|
|
49
|
+
child.stdout.on('data', (chunk) => {
|
|
50
|
+
stdout += chunk;
|
|
51
|
+
});
|
|
52
|
+
child.stderr.on('data', (chunk) => {
|
|
53
|
+
stderr += chunk;
|
|
54
|
+
});
|
|
55
|
+
child.on('error', reject);
|
|
56
|
+
child.on('close', (code) => {
|
|
57
|
+
if (code === 0) {
|
|
58
|
+
resolve(stdout.trim());
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
reject(new Error(`Command failed: ${stderr}`));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function parseOpencodeModels(output) {
|
|
67
|
+
const models = [];
|
|
68
|
+
const lines = output.split('\n').filter((line) => line.trim());
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (!trimmed || trimmed.startsWith('{'))
|
|
72
|
+
continue;
|
|
73
|
+
const parts = trimmed.split('/');
|
|
74
|
+
const id = trimmed;
|
|
75
|
+
const name = parts.length > 1 ? parts[1] : parts[0];
|
|
76
|
+
const displayName = name
|
|
77
|
+
.split('-')
|
|
78
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
79
|
+
.join(' ');
|
|
80
|
+
models.push({
|
|
81
|
+
id,
|
|
82
|
+
name: displayName,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return models;
|
|
86
|
+
}
|
|
87
|
+
export async function discoverHostOpencodeModels() {
|
|
88
|
+
try {
|
|
89
|
+
const output = await runCommand('opencode', ['models']);
|
|
90
|
+
return parseOpencodeModels(output);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function discoverContainerOpencodeModels(containerName, execInContainer) {
|
|
97
|
+
try {
|
|
98
|
+
const result = await execInContainer(containerName, ['opencode', 'models']);
|
|
99
|
+
if (result.exitCode !== 0) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return parseOpencodeModels(result.stdout);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export { FALLBACK_CLAUDE_MODELS };
|