shmakk 1.2.0 → 1.2.1
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/README.md +28 -2
- package/package.json +2 -2
- package/scripts/demo/record.py +196 -0
- package/scripts/demo/scenes.html +913 -0
- package/skills/media-video-compose.md +320 -0
- package/skills/media-video-script.md +204 -0
- package/skills/media-video-voice.md +184 -0
- package/src/agent-overview.js +320 -0
- package/src/agent-roster.js +53 -0
- package/src/agent.js +178 -18
- package/src/cli.js +193 -86
- package/src/completions.js +3 -1
- package/src/correction.js +11 -4
- package/src/endpoints.js +94 -31
- package/src/guard.js +101 -0
- package/src/index.js +19 -5
- package/src/llm.js +462 -52
- package/src/markdown.js +217 -0
- package/src/notify.js +34 -0
- package/src/pty.js +1 -1
- package/src/review.js +8 -1
- package/src/self-commands.js +108 -2
- package/src/session.js +58 -2
- package/src/subagent.js +12 -1
- package/src/taskClassifier.js +2 -2
- package/src/team.js +22 -0
- package/src/tools.js +408 -1
- package/src/workflows.js +32 -0
package/src/llm.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
let OpenAI;
|
|
2
2
|
try { OpenAI = require('openai'); } catch { OpenAI = null; }
|
|
3
3
|
|
|
4
|
-
const http = require('http');
|
|
5
|
-
const { spawn } = require('child_process');
|
|
6
4
|
const path = require('path');
|
|
7
5
|
const os = require('os');
|
|
8
|
-
const
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const { getCurrentEndpoint, getCurrentEndpointName, getModelRegistry } = require('./endpoints');
|
|
9
8
|
|
|
10
9
|
function parseHeaders(s) {
|
|
11
10
|
const out = {};
|
|
@@ -33,6 +32,8 @@ function envForProvider() {
|
|
|
33
32
|
const activeEndpoint = getCurrentEndpoint();
|
|
34
33
|
if (activeEndpoint) {
|
|
35
34
|
return {
|
|
35
|
+
name: getCurrentEndpointName() || activeEndpoint.name || null,
|
|
36
|
+
provider: activeEndpoint.provider || 'openai-compatible',
|
|
36
37
|
baseURL: activeEndpoint.base_url,
|
|
37
38
|
apiKey: activeEndpoint.api_key,
|
|
38
39
|
headers: activeEndpoint.headers,
|
|
@@ -42,6 +43,8 @@ function envForProvider() {
|
|
|
42
43
|
}
|
|
43
44
|
// Fall back to env vars for backwards compatibility
|
|
44
45
|
return {
|
|
46
|
+
name: null,
|
|
47
|
+
provider: process.env.SHMAKK_PROVIDER || 'openai-compatible',
|
|
45
48
|
baseURL: process.env.SHMAKK_BASE_URL,
|
|
46
49
|
apiKey: process.env.SHMAKK_API_KEY,
|
|
47
50
|
headers: process.env.SHMAKK_HEADERS,
|
|
@@ -52,78 +55,485 @@ function envForProvider() {
|
|
|
52
55
|
|
|
53
56
|
function isConfigured() {
|
|
54
57
|
const cfg = envForProvider();
|
|
55
|
-
return
|
|
58
|
+
if (recommendationMode()) return Object.keys(getModelRegistry().models).length > 0;
|
|
59
|
+
if (cfg.provider === 'anthropic') return !!cfg.apiKey;
|
|
60
|
+
if (cfg.provider === 'codex') return true; // codex-proxy handles auth via OAuth
|
|
61
|
+
return (!!cfg.baseURL || cfg.provider === 'openai') && !!OpenAI;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
function
|
|
64
|
+
function makeOpenAIClient(cfg) {
|
|
59
65
|
if (!OpenAI) throw new Error('openai sdk not installed');
|
|
60
|
-
const
|
|
66
|
+
const baseURL = cfg.baseURL || (cfg.provider === 'openai' ? 'https://local:8095/v1' : undefined);
|
|
67
|
+
if (!baseURL) throw new Error('SHMAKK_BASE_URL is required for OpenAI-compatible providers');
|
|
61
68
|
return new OpenAI({
|
|
62
|
-
baseURL
|
|
63
|
-
apiKey: cfg.apiKey || 'not-needed',
|
|
69
|
+
baseURL,
|
|
70
|
+
apiKey: cfg.apiKey || process.env.OPENAI_API_KEY || 'not-needed',
|
|
64
71
|
defaultHeaders: buildHeaders(cfg.headers, cfg.registry),
|
|
65
72
|
});
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
function makeProviderClient(cfg) {
|
|
76
|
+
if (cfg.provider === 'anthropic') return makeAnthropicCompatClient(cfg);
|
|
77
|
+
if (cfg.provider === 'codex') return makeCodexCompatClient(cfg);
|
|
78
|
+
return makeOpenAIClient(cfg);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function makeClient() {
|
|
82
|
+
const cfg = envForProvider();
|
|
83
|
+
if (recommendationMode()) return makeRoutingClient(cfg);
|
|
84
|
+
return makeProviderClient(cfg);
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
function modelFor() {
|
|
69
|
-
return process.env.
|
|
88
|
+
if (recommendationMode()) return process.env._SHMAKK_LAST_MODEL || 'model-recommendation';
|
|
89
|
+
const activeEndpoint = getCurrentEndpoint();
|
|
90
|
+
return activeEndpoint?.model || process.env.SHMAKK_MODEL || 'gpt-4o-mini';
|
|
70
91
|
}
|
|
71
92
|
|
|
72
|
-
|
|
73
|
-
return
|
|
93
|
+
function recommendationMode() {
|
|
94
|
+
return process.env.SHMAKK_MODEL_RECOMMENDATION === '1';
|
|
74
95
|
}
|
|
75
96
|
|
|
76
|
-
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
87
|
-
});
|
|
97
|
+
function configFromModelEntry(name, cfg) {
|
|
98
|
+
return {
|
|
99
|
+
name,
|
|
100
|
+
provider: cfg.provider || 'openai-compatible',
|
|
101
|
+
baseURL: cfg.base_url,
|
|
102
|
+
apiKey: cfg.api_key,
|
|
103
|
+
headers: cfg.headers,
|
|
104
|
+
registry: cfg.registry,
|
|
105
|
+
model: cfg.model || name,
|
|
106
|
+
};
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
|
|
109
|
+
function skillPathCandidates() {
|
|
110
|
+
const root = path.join(os.homedir(), '.config', 'shmakk', 'skills');
|
|
111
|
+
return [
|
|
112
|
+
path.join(root, 'model-recommendation.md'),
|
|
113
|
+
path.join(root, 'model-recommendation', 'SKILL.md'),
|
|
114
|
+
path.join(root, 'general-model-recommendation.md'),
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function loadRecommendationSkill() {
|
|
119
|
+
for (const p of skillPathCandidates()) {
|
|
120
|
+
try {
|
|
121
|
+
if (fs.existsSync(p)) return fs.readFileSync(p, 'utf8').slice(0, 12000);
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
return 'Choose the least expensive model that can reliably complete the task. Prefer strongest models for architecture, debugging, security, tool-heavy edits, and multi-agent planning. Prefer faster models for simple read-only, summarization, or mechanical edits.';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function summarizeCall(params) {
|
|
128
|
+
const messages = Array.isArray(params.messages) ? params.messages : [];
|
|
129
|
+
const text = messages.slice(-6).map((m) => {
|
|
130
|
+
const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '');
|
|
131
|
+
return `${m.role}: ${content.slice(0, 1200)}`;
|
|
132
|
+
}).join('\n\n');
|
|
133
|
+
const tools = Array.isArray(params.tools) ? params.tools.map((t) => t.function?.name || t.name).filter(Boolean) : [];
|
|
134
|
+
return { text, tools, stream: !!params.stream, toolChoice: params.tool_choice || null };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function fallbackRecommendation(registry, params) {
|
|
138
|
+
const entries = Object.entries(registry.models);
|
|
139
|
+
if (!entries.length) return null;
|
|
140
|
+
const summary = summarizeCall(params);
|
|
141
|
+
const s = `${summary.text}\n${summary.tools.join(' ')}`.toLowerCase();
|
|
142
|
+
const needsStrong = /architecture|security|debug|refactor|multi.?agent|team|design|review|risk|tool|edit|write|implement/.test(s);
|
|
143
|
+
const anthropic = entries.find(([, cfg]) => cfg.provider === 'anthropic');
|
|
144
|
+
const codex = entries.find(([, cfg]) => cfg.provider === 'codex' || /codex|gpt-5/i.test(cfg.model || ''));
|
|
145
|
+
if (needsStrong && codex) return codex[0];
|
|
146
|
+
if (needsStrong && anthropic) return anthropic[0];
|
|
147
|
+
return registry.main || entries[0][0];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function recommendModel(registry, params, signal) {
|
|
151
|
+
const available = Object.fromEntries(Object.entries(registry.models).map(([name, cfg]) => [name, {
|
|
152
|
+
provider: cfg.provider,
|
|
153
|
+
model: cfg.model,
|
|
154
|
+
main: name === registry.main || cfg.main,
|
|
155
|
+
}]));
|
|
156
|
+
|
|
157
|
+
const mainName = registry.main || Object.keys(registry.models)[0];
|
|
158
|
+
const main = mainName ? registry.models[mainName] : null;
|
|
159
|
+
if (!main) return fallbackRecommendation(registry, params);
|
|
160
|
+
|
|
91
161
|
try {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
162
|
+
const client = makeProviderClient(configFromModelEntry(mainName, main));
|
|
163
|
+
const resp = await client.chat.completions.create({
|
|
164
|
+
model: main.model,
|
|
165
|
+
temperature: 0,
|
|
166
|
+
stream: false,
|
|
167
|
+
tool_choice: 'none',
|
|
168
|
+
messages: [
|
|
169
|
+
{
|
|
170
|
+
role: 'system',
|
|
171
|
+
content: `${loadRecommendationSkill()}\n\nReturn only JSON: {"model":"<one available key>","reason":"<short reason>"}.`,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
role: 'user',
|
|
175
|
+
content: `Available models:\n${JSON.stringify(available, null, 2)}\n\nCall summary:\n${JSON.stringify(summarizeCall(params), null, 2)}`,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
}, { signal });
|
|
179
|
+
const raw = String(resp.choices?.[0]?.message?.content || '');
|
|
180
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
181
|
+
const parsed = match ? JSON.parse(match[0]) : null;
|
|
182
|
+
if (parsed && registry.models[parsed.model]) return parsed.model;
|
|
183
|
+
} catch {}
|
|
184
|
+
|
|
185
|
+
return fallbackRecommendation(registry, params);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function makeRoutingClient() {
|
|
189
|
+
return {
|
|
190
|
+
chat: {
|
|
191
|
+
completions: {
|
|
192
|
+
create: async (params, options = {}) => {
|
|
193
|
+
const registry = getModelRegistry();
|
|
194
|
+
const selected = await recommendModel(registry, params, options.signal);
|
|
195
|
+
const cfg = selected && registry.models[selected]
|
|
196
|
+
? configFromModelEntry(selected, registry.models[selected])
|
|
197
|
+
: envForProvider();
|
|
198
|
+
process.env._SHMAKK_LAST_MODEL = `${selected || cfg.name || cfg.provider}:${cfg.model || params.model}`;
|
|
199
|
+
const client = makeProviderClient(cfg);
|
|
200
|
+
return client.chat.completions.create({ ...params, model: cfg.model || params.model }, options);
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function ensureModelRuntime() {}
|
|
208
|
+
|
|
209
|
+
// ── Codex (Responses API) compat client ────────────────────────────────────
|
|
210
|
+
// Translates OpenAI chat.completions format to/from the Codex Responses API
|
|
211
|
+
// via the codex-proxy (mitmdump on :8095 -> chatgpt.com/backend-api/codex/responses).
|
|
212
|
+
|
|
213
|
+
function splitCodexSystem(messages) {
|
|
214
|
+
let instructions = '';
|
|
215
|
+
const input = [];
|
|
216
|
+
for (const m of messages || []) {
|
|
217
|
+
if (m.role === 'system') {
|
|
218
|
+
instructions += (instructions ? '\n\n' : '') +
|
|
219
|
+
(typeof m.content === 'string' ? m.content : JSON.stringify(m.content || ''));
|
|
220
|
+
} else if (m.role === 'tool') {
|
|
221
|
+
input.push({
|
|
222
|
+
type: 'function_call_output',
|
|
223
|
+
call_id: m.tool_call_id,
|
|
224
|
+
output: typeof m.content === 'string' ? m.content : JSON.stringify(m.content || ''),
|
|
225
|
+
});
|
|
226
|
+
} else if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
227
|
+
if (m.content) {
|
|
228
|
+
input.push({ role: 'assistant', content: String(m.content) });
|
|
229
|
+
}
|
|
230
|
+
for (const tc of m.tool_calls) {
|
|
231
|
+
input.push({
|
|
232
|
+
type: 'function_call',
|
|
233
|
+
call_id: tc.id,
|
|
234
|
+
name: tc.function?.name,
|
|
235
|
+
arguments: tc.function?.arguments || '{}',
|
|
236
|
+
});
|
|
103
237
|
}
|
|
104
|
-
|
|
238
|
+
} else {
|
|
239
|
+
input.push({
|
|
240
|
+
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
241
|
+
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content || ''),
|
|
242
|
+
});
|
|
105
243
|
}
|
|
106
|
-
return false;
|
|
107
|
-
} catch {
|
|
108
|
-
return false;
|
|
109
244
|
}
|
|
245
|
+
return { instructions: instructions || 'Be helpful.', input };
|
|
110
246
|
}
|
|
111
247
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
248
|
+
function codexTools(tools) {
|
|
249
|
+
return (tools || []).map((tool) => {
|
|
250
|
+
const fn = tool.function || tool;
|
|
251
|
+
return {
|
|
252
|
+
type: 'function',
|
|
253
|
+
name: fn.name,
|
|
254
|
+
description: fn.description || '',
|
|
255
|
+
parameters: fn.parameters || { type: 'object', properties: {} },
|
|
256
|
+
};
|
|
257
|
+
}).filter((t) => t.name);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function codexToolChoice(choice) {
|
|
261
|
+
if (!choice || choice === 'auto') return 'auto';
|
|
262
|
+
if (choice === 'required') return 'required';
|
|
263
|
+
if (choice === 'none') return 'none';
|
|
264
|
+
if (choice.function?.name) return choice.function.name;
|
|
265
|
+
return 'auto';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function fromCodexResponse(model, data) {
|
|
269
|
+
const message = { role: 'assistant', content: '', tool_calls: undefined };
|
|
270
|
+
const calls = [];
|
|
271
|
+
for (const item of data.output || []) {
|
|
272
|
+
if (item.type === 'message') {
|
|
273
|
+
const content = item.content || [];
|
|
274
|
+
if (typeof content === 'string') {
|
|
275
|
+
message.content += content;
|
|
276
|
+
} else if (Array.isArray(content)) {
|
|
277
|
+
for (const part of content) {
|
|
278
|
+
if (part.type === 'output_text') message.content += part.text || '';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (item.type === 'function_call') {
|
|
283
|
+
calls.push({
|
|
284
|
+
id: item.call_id,
|
|
285
|
+
type: 'function',
|
|
286
|
+
function: { name: item.name, arguments: item.arguments || '{}' },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (calls.length) message.tool_calls = calls;
|
|
291
|
+
return {
|
|
292
|
+
id: data.id,
|
|
293
|
+
object: 'chat.completion',
|
|
294
|
+
model,
|
|
295
|
+
choices: [{ index: 0, message, finish_reason: 'stop' }],
|
|
296
|
+
usage: data.usage,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function makeCodexCompatClient(cfg) {
|
|
301
|
+
return {
|
|
302
|
+
chat: {
|
|
303
|
+
completions: {
|
|
304
|
+
create: async (params, options = {}) => {
|
|
305
|
+
const { instructions, input } = splitCodexSystem(params.messages || []);
|
|
306
|
+
const tools = params.tool_choice === 'none' ? [] : codexTools(params.tools);
|
|
307
|
+
const body = {
|
|
308
|
+
model: params.model || cfg.model,
|
|
309
|
+
instructions,
|
|
310
|
+
input,
|
|
311
|
+
store: false,
|
|
312
|
+
stream: false, // always collect, then fake-stream if caller wants it
|
|
313
|
+
max_output_tokens: params.max_tokens || 4096,
|
|
314
|
+
};
|
|
315
|
+
if (params.temperature != null) body.temperature = params.temperature;
|
|
316
|
+
if (params.top_p != null) body.top_p = params.top_p;
|
|
317
|
+
if (tools.length) {
|
|
318
|
+
body.tools = tools;
|
|
319
|
+
const tc = codexToolChoice(params.tool_choice);
|
|
320
|
+
if (tc) body.tool_choice = tc;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const base = (cfg.baseURL || 'https://local:8095').replace(/\/+$/, '');
|
|
324
|
+
const res = await fetch(`${base}/backend-api/codex/responses`, {
|
|
325
|
+
method: 'POST',
|
|
326
|
+
signal: options.signal,
|
|
327
|
+
headers: {
|
|
328
|
+
'content-type': 'application/json',
|
|
329
|
+
...(cfg.apiKey ? { authorization: `Bearer ${cfg.apiKey}` } : {}),
|
|
330
|
+
...buildHeaders(cfg.headers, cfg.registry),
|
|
331
|
+
},
|
|
332
|
+
body: JSON.stringify(body),
|
|
333
|
+
});
|
|
334
|
+
if (!res.ok) throw new Error(`Codex API ${res.status}: ${await res.text().slice(0, 500)}`);
|
|
335
|
+
const completion = fromCodexResponse(body.model, await res.json());
|
|
336
|
+
if (params.stream) return fakeOpenAIStreamFromCompletion(completion);
|
|
337
|
+
return completion;
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function splitAnthropicSystem(messages) {
|
|
345
|
+
const system = [];
|
|
346
|
+
const converted = [];
|
|
347
|
+
for (const m of messages || []) {
|
|
348
|
+
if (m.role === 'system') {
|
|
349
|
+
system.push(typeof m.content === 'string' ? m.content : JSON.stringify(m.content || ''));
|
|
350
|
+
} else if (m.role === 'tool') {
|
|
351
|
+
converted.push({
|
|
352
|
+
role: 'user',
|
|
353
|
+
content: [{
|
|
354
|
+
type: 'tool_result',
|
|
355
|
+
tool_use_id: m.tool_call_id,
|
|
356
|
+
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content || ''),
|
|
357
|
+
}],
|
|
358
|
+
});
|
|
359
|
+
} else if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
360
|
+
const content = [];
|
|
361
|
+
if (m.content) content.push({ type: 'text', text: String(m.content) });
|
|
362
|
+
for (const tc of m.tool_calls) {
|
|
363
|
+
content.push({
|
|
364
|
+
type: 'tool_use',
|
|
365
|
+
id: tc.id,
|
|
366
|
+
name: tc.function?.name,
|
|
367
|
+
input: safeJson(tc.function?.arguments || '{}'),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
converted.push({ role: 'assistant', content });
|
|
123
371
|
} else {
|
|
124
|
-
|
|
372
|
+
converted.push({
|
|
373
|
+
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
374
|
+
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content || ''),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return { system: system.join('\n\n'), messages: converted };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function safeJson(s) {
|
|
382
|
+
try { return JSON.parse(s); } catch { return {}; }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function anthropicTools(tools) {
|
|
386
|
+
return (tools || []).map((tool) => {
|
|
387
|
+
const fn = tool.function || tool;
|
|
388
|
+
return {
|
|
389
|
+
name: fn.name,
|
|
390
|
+
description: fn.description || '',
|
|
391
|
+
input_schema: fn.parameters || { type: 'object', properties: {} },
|
|
392
|
+
};
|
|
393
|
+
}).filter((t) => t.name);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function anthropicToolChoice(choice) {
|
|
397
|
+
if (!choice || choice === 'auto') return { type: 'auto' };
|
|
398
|
+
if (choice === 'required') return { type: 'any' };
|
|
399
|
+
if (choice === 'none') return undefined;
|
|
400
|
+
if (choice.function?.name) return { type: 'tool', name: choice.function.name };
|
|
401
|
+
return { type: 'auto' };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function toOpenAICompletion(model, data) {
|
|
405
|
+
const message = { role: 'assistant', content: '', tool_calls: undefined };
|
|
406
|
+
const calls = [];
|
|
407
|
+
for (const block of data.content || []) {
|
|
408
|
+
if (block.type === 'text') message.content += block.text || '';
|
|
409
|
+
if (block.type === 'tool_use') {
|
|
410
|
+
calls.push({
|
|
411
|
+
id: block.id,
|
|
412
|
+
type: 'function',
|
|
413
|
+
function: { name: block.name, arguments: JSON.stringify(block.input || {}) },
|
|
414
|
+
});
|
|
125
415
|
}
|
|
126
416
|
}
|
|
417
|
+
if (calls.length) message.tool_calls = calls;
|
|
418
|
+
return { id: data.id, object: 'chat.completion', model, choices: [{ index: 0, message, finish_reason: data.stop_reason || 'stop' }] };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function* fakeOpenAIStreamFromCompletion(completion) {
|
|
422
|
+
const message = completion.choices?.[0]?.message || {};
|
|
423
|
+
if (message.content) {
|
|
424
|
+
yield { choices: [{ index: 0, delta: { content: message.content }, finish_reason: null }] };
|
|
425
|
+
}
|
|
426
|
+
for (let i = 0; i < (message.tool_calls || []).length; i++) {
|
|
427
|
+
const tc = message.tool_calls[i];
|
|
428
|
+
yield {
|
|
429
|
+
choices: [{
|
|
430
|
+
index: 0,
|
|
431
|
+
delta: {
|
|
432
|
+
tool_calls: [{
|
|
433
|
+
index: i,
|
|
434
|
+
id: tc.id,
|
|
435
|
+
type: 'function',
|
|
436
|
+
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
437
|
+
}],
|
|
438
|
+
},
|
|
439
|
+
finish_reason: null,
|
|
440
|
+
}],
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
yield { choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function makeAnthropicCompatClient(cfg) {
|
|
447
|
+
return {
|
|
448
|
+
chat: {
|
|
449
|
+
completions: {
|
|
450
|
+
create: async (params, options = {}) => {
|
|
451
|
+
if (!cfg.apiKey) throw new Error('Anthropic api_key is required');
|
|
452
|
+
const { system, messages } = splitAnthropicSystem(params.messages || []);
|
|
453
|
+
const tools = params.tool_choice === 'none' ? [] : anthropicTools(params.tools);
|
|
454
|
+
const body = {
|
|
455
|
+
model: params.model || cfg.model,
|
|
456
|
+
max_tokens: params.max_tokens || 4096,
|
|
457
|
+
temperature: params.temperature ?? 0,
|
|
458
|
+
messages,
|
|
459
|
+
};
|
|
460
|
+
if (system) body.system = system;
|
|
461
|
+
if (tools.length) {
|
|
462
|
+
body.tools = tools;
|
|
463
|
+
const toolChoice = anthropicToolChoice(params.tool_choice);
|
|
464
|
+
if (toolChoice) body.tool_choice = toolChoice;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const base = (cfg.baseURL || 'https://local:8083').replace(/\/+$/, '');
|
|
468
|
+
const res = await fetch(`${base}/v1/messages`, {
|
|
469
|
+
method: 'POST',
|
|
470
|
+
signal: options.signal,
|
|
471
|
+
headers: {
|
|
472
|
+
'content-type': 'application/json',
|
|
473
|
+
'x-api-key': cfg.apiKey,
|
|
474
|
+
'anthropic-version': '2023-06-01',
|
|
475
|
+
...buildHeaders(cfg.headers, cfg.registry),
|
|
476
|
+
},
|
|
477
|
+
body: JSON.stringify(body),
|
|
478
|
+
});
|
|
479
|
+
if (!res.ok) throw new Error(`Anthropic API ${res.status}: ${await res.text()}`);
|
|
480
|
+
const completion = toOpenAICompletion(body.model, await res.json());
|
|
481
|
+
if (params.stream) return fakeOpenAIStreamFromCompletion(completion);
|
|
482
|
+
return completion;
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ── DeepSeek settings ──────────────────────────────────────────────────────
|
|
490
|
+
// DeepSeek thinking / reasoning_effort increases protocol complexity because
|
|
491
|
+
// the runtime must distinguish visible content, internal reasoning_content,
|
|
492
|
+
// and structured tool_calls. That makes rare DSML leaks more likely in
|
|
493
|
+
// streaming/tool-heavy flows. Disable thinking for mutation/tool-loop turns.
|
|
494
|
+
|
|
495
|
+
function isDeepSeekProvider() {
|
|
496
|
+
const cfg = envForProvider();
|
|
497
|
+
const base = (cfg.baseURL || process.env.SHMAKK_BASE_URL || '').toLowerCase();
|
|
498
|
+
return base.includes('deepseek');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function getDeepSeekOptions(taskType) {
|
|
502
|
+
if (!isDeepSeekProvider()) return {};
|
|
503
|
+
|
|
504
|
+
// Respect runtime override (set after a DSML leak).
|
|
505
|
+
const forceNoThinking = process.env._SHMAKK_FORCE_NO_THINKING === '1';
|
|
506
|
+
|
|
507
|
+
if (forceNoThinking) {
|
|
508
|
+
return {
|
|
509
|
+
extra_body: {
|
|
510
|
+
thinking: { type: 'disabled' },
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const toolOrMutationTurn =
|
|
516
|
+
taskType === 'edit_file' ||
|
|
517
|
+
taskType === 'run_command' ||
|
|
518
|
+
taskType === 'apply_patch' ||
|
|
519
|
+
taskType === 'tool_loop';
|
|
520
|
+
|
|
521
|
+
if (toolOrMutationTurn) {
|
|
522
|
+
return {
|
|
523
|
+
extra_body: {
|
|
524
|
+
thinking: { type: 'disabled' },
|
|
525
|
+
},
|
|
526
|
+
// Do NOT send reasoning_effort here.
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Non-mutation / planning turns: reasoning is fine.
|
|
531
|
+
return {
|
|
532
|
+
reasoning_effort: 'high',
|
|
533
|
+
extra_body: {
|
|
534
|
+
thinking: { type: 'enabled' },
|
|
535
|
+
},
|
|
536
|
+
};
|
|
127
537
|
}
|
|
128
538
|
|
|
129
|
-
module.exports = { makeClient, modelFor, isConfigured,
|
|
539
|
+
module.exports = { makeClient, modelFor, isConfigured, ensureModelRuntime, getDeepSeekOptions, isDeepSeekProvider };
|