@hasna/terminal 2.3.2 → 3.1.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/dist/ai.js +78 -85
- package/dist/cache.js +3 -2
- package/dist/cli.js +1 -1
- package/dist/compression.js +8 -30
- package/dist/context-hints.js +20 -10
- package/dist/diff-cache.js +1 -1
- package/dist/discover.js +1 -1
- package/dist/economy.js +37 -5
- package/dist/expand-store.js +7 -1
- package/dist/mcp/server.js +44 -68
- package/dist/output-processor.js +10 -7
- package/dist/providers/anthropic.js +6 -2
- package/dist/providers/cerebras.js +6 -93
- package/dist/providers/groq.js +6 -93
- package/dist/providers/index.js +85 -36
- package/dist/providers/openai-compat.js +93 -0
- package/dist/providers/xai.js +6 -93
- package/dist/tokens.js +17 -0
- package/dist/tool-profiles.js +9 -2
- package/package.json +1 -1
- package/src/ai.ts +83 -94
- package/src/cache.ts +3 -2
- package/src/cli.tsx +1 -1
- package/src/compression.ts +8 -35
- package/src/context-hints.ts +20 -10
- package/src/diff-cache.ts +1 -1
- package/src/discover.ts +1 -1
- package/src/economy.ts +37 -5
- package/src/expand-store.ts +8 -1
- package/src/mcp/server.ts +45 -73
- package/src/output-processor.ts +11 -8
- package/src/providers/anthropic.ts +6 -2
- package/src/providers/base.ts +2 -0
- package/src/providers/cerebras.ts +6 -105
- package/src/providers/groq.ts +6 -105
- package/src/providers/index.ts +84 -33
- package/src/providers/openai-compat.ts +109 -0
- package/src/providers/xai.ts +6 -105
- package/src/tokens.ts +18 -0
- package/src/tool-profiles.ts +9 -2
- package/src/compression.test.ts +0 -49
- package/src/output-router.ts +0 -56
- package/src/parsers/base.ts +0 -72
- package/src/parsers/build.ts +0 -73
- package/src/parsers/errors.ts +0 -107
- package/src/parsers/files.ts +0 -91
- package/src/parsers/git.ts +0 -101
- package/src/parsers/index.ts +0 -66
- package/src/parsers/parsers.test.ts +0 -153
- package/src/parsers/tests.ts +0 -98
package/dist/mcp/server.js
CHANGED
|
@@ -5,17 +5,16 @@ import { z } from "zod";
|
|
|
5
5
|
import { spawn } from "child_process";
|
|
6
6
|
import { compress, stripAnsi } from "../compression.js";
|
|
7
7
|
import { stripNoise } from "../noise-filter.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { estimateTokens } from "../tokens.js";
|
|
9
|
+
import { processOutput } from "../output-processor.js";
|
|
10
10
|
import { searchFiles, searchContent, semanticSearch } from "../search/index.js";
|
|
11
11
|
import { listRecipes, listCollections, getRecipe, createRecipe } from "../recipes/storage.js";
|
|
12
12
|
import { substituteVariables } from "../recipes/model.js";
|
|
13
13
|
import { bgStart, bgStatus, bgStop, bgLogs, bgWaitPort } from "../supervisor.js";
|
|
14
14
|
import { diffOutput } from "../diff-cache.js";
|
|
15
|
-
import { processOutput } from "../output-processor.js";
|
|
16
15
|
import { listSessions, getSessionInteractions, getSessionStats } from "../sessions-db.js";
|
|
17
16
|
import { cachedRead } from "../file-cache.js";
|
|
18
|
-
import { getBootContext } from "../session-boot.js";
|
|
17
|
+
import { getBootContext, invalidateBootCache } from "../session-boot.js";
|
|
19
18
|
import { storeOutput, expandOutput } from "../expand-store.js";
|
|
20
19
|
import { rewriteCommand } from "../command-rewriter.js";
|
|
21
20
|
import { shouldBeLazy, toLazy } from "../lazy-executor.js";
|
|
@@ -46,6 +45,10 @@ function exec(command, cwd, timeout) {
|
|
|
46
45
|
// Strip noise before returning (npm fund, progress bars, etc.)
|
|
47
46
|
const cleanStdout = stripNoise(stdout).cleaned;
|
|
48
47
|
const cleanStderr = stripNoise(stderr).cleaned;
|
|
48
|
+
// Invalidate boot cache after state-changing git commands
|
|
49
|
+
if (/\bgit\s+(commit|checkout|branch|merge|reset|push|pull|rebase|stash)\b/.test(actualCommand)) {
|
|
50
|
+
invalidateBootCache();
|
|
51
|
+
}
|
|
49
52
|
resolve({ exitCode: code ?? 0, stdout: cleanStdout, stderr: cleanStderr, duration: Date.now() - start, rewritten: rw.changed ? rw.rewritten : undefined });
|
|
50
53
|
});
|
|
51
54
|
});
|
|
@@ -87,42 +90,20 @@ export function createServer() {
|
|
|
87
90
|
}) }],
|
|
88
91
|
};
|
|
89
92
|
}
|
|
90
|
-
// JSON
|
|
91
|
-
if (format === "json") {
|
|
92
|
-
const parsed = parseOutput(command, output);
|
|
93
|
-
if (parsed) {
|
|
94
|
-
const savings = tokenSavings(output, parsed.data);
|
|
95
|
-
if (savings.saved > 0) {
|
|
96
|
-
return {
|
|
97
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
98
|
-
exitCode: result.exitCode, parsed: parsed.data, parser: parsed.parser,
|
|
99
|
-
duration: result.duration, tokensSaved: savings.saved, savingsPercent: savings.percent,
|
|
100
|
-
}) }],
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
// JSON was larger — fall through to compression
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// Compressed mode (also fallback for json when no parser matches)
|
|
107
|
-
if (format === "compressed" || format === "json") {
|
|
108
|
-
const compressed = compress(command, output, { maxTokens, format: "json" });
|
|
109
|
-
return {
|
|
110
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
111
|
-
exitCode: result.exitCode, output: compressed.content, format: compressed.format,
|
|
112
|
-
duration: result.duration, tokensSaved: compressed.tokensSaved, savingsPercent: compressed.savingsPercent,
|
|
113
|
-
}) }],
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
// Summary mode — AI-powered
|
|
117
|
-
if (format === "summary") {
|
|
93
|
+
// JSON and Summary modes — both go through AI processing
|
|
94
|
+
if (format === "json" || format === "summary") {
|
|
118
95
|
try {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const summaryTokens = estimateTokens(summary);
|
|
96
|
+
const processed = await processOutput(command, output);
|
|
97
|
+
const detailKey = output.split("\n").length > 15 ? storeOutput(command, output) : undefined;
|
|
122
98
|
return {
|
|
123
99
|
content: [{ type: "text", text: JSON.stringify({
|
|
124
|
-
exitCode: result.exitCode,
|
|
125
|
-
|
|
100
|
+
exitCode: result.exitCode,
|
|
101
|
+
summary: processed.summary,
|
|
102
|
+
structured: processed.structured,
|
|
103
|
+
duration: result.duration,
|
|
104
|
+
tokensSaved: processed.tokensSaved,
|
|
105
|
+
aiProcessed: processed.aiProcessed,
|
|
106
|
+
...(detailKey ? { detailKey, expandable: true } : {}),
|
|
126
107
|
}) }],
|
|
127
108
|
};
|
|
128
109
|
}
|
|
@@ -136,6 +117,16 @@ export function createServer() {
|
|
|
136
117
|
};
|
|
137
118
|
}
|
|
138
119
|
}
|
|
120
|
+
// Compressed mode — fast non-AI: strip + dedup + truncate
|
|
121
|
+
if (format === "compressed") {
|
|
122
|
+
const compressed = compress(command, output, { maxTokens });
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
125
|
+
exitCode: result.exitCode, output: compressed.content, duration: result.duration,
|
|
126
|
+
tokensSaved: compressed.tokensSaved, savingsPercent: compressed.savingsPercent,
|
|
127
|
+
}) }],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
139
130
|
return { content: [{ type: "text", text: output }] };
|
|
140
131
|
});
|
|
141
132
|
// ── execute_smart: AI-powered output processing ────────────────────────────
|
|
@@ -192,28 +183,21 @@ export function createServer() {
|
|
|
192
183
|
command = includeHidden ? `ls -la "${target}"` : `ls -l "${target}"`;
|
|
193
184
|
}
|
|
194
185
|
const result = await exec(command);
|
|
195
|
-
const parsed = parseOutput(command, result.stdout);
|
|
196
|
-
if (parsed) {
|
|
197
|
-
return {
|
|
198
|
-
content: [{ type: "text", text: JSON.stringify({ cwd: target, ...parsed.data, parser: parsed.parser }) }],
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
186
|
const files = result.stdout.split("\n").filter(l => l.trim());
|
|
202
|
-
return { content: [{ type: "text", text: JSON.stringify({ cwd: target, files }) }] };
|
|
187
|
+
return { content: [{ type: "text", text: JSON.stringify({ cwd: target, files, count: files.length }) }] };
|
|
203
188
|
});
|
|
204
189
|
// ── explain_error: structured error diagnosis ─────────────────────────────
|
|
205
190
|
server.tool("explain_error", "Parse error output and return structured diagnosis with root cause and fix suggestion.", {
|
|
206
191
|
error: z.string().describe("Error output text"),
|
|
207
192
|
command: z.string().optional().describe("The command that produced the error"),
|
|
208
193
|
}, async ({ error, command }) => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const info = errorParser.parse(command ?? "", error);
|
|
212
|
-
return { content: [{ type: "text", text: JSON.stringify(info) }] };
|
|
213
|
-
}
|
|
194
|
+
// AI processes the error — no regex guessing
|
|
195
|
+
const processed = await processOutput(command ?? "unknown", error);
|
|
214
196
|
return {
|
|
215
197
|
content: [{ type: "text", text: JSON.stringify({
|
|
216
|
-
|
|
198
|
+
summary: processed.summary,
|
|
199
|
+
structured: processed.structured,
|
|
200
|
+
aiProcessed: processed.aiProcessed,
|
|
217
201
|
}) }],
|
|
218
202
|
};
|
|
219
203
|
});
|
|
@@ -221,9 +205,8 @@ export function createServer() {
|
|
|
221
205
|
server.tool("status", "Get open-terminal server status, capabilities, and available parsers.", async () => {
|
|
222
206
|
return {
|
|
223
207
|
content: [{ type: "text", text: JSON.stringify({
|
|
224
|
-
name: "open-terminal", version: "0.
|
|
225
|
-
|
|
226
|
-
features: ["structured-output", "token-compression", "ai-summary", "error-diagnosis"],
|
|
208
|
+
name: "open-terminal", version: "0.3.0", cwd: process.cwd(),
|
|
209
|
+
features: ["ai-output-processing", "token-compression", "noise-filtering", "diff-caching", "lazy-execution", "progressive-disclosure"],
|
|
227
210
|
}) }],
|
|
228
211
|
};
|
|
229
212
|
});
|
|
@@ -287,19 +270,12 @@ export function createServer() {
|
|
|
287
270
|
const command = variables ? substituteVariables(recipe.command, variables) : recipe.command;
|
|
288
271
|
const result = await exec(command, cwd, 30000);
|
|
289
272
|
const output = (result.stdout + result.stderr).trim();
|
|
290
|
-
if (format === "json") {
|
|
291
|
-
const
|
|
292
|
-
if (parsed) {
|
|
293
|
-
return { content: [{ type: "text", text: JSON.stringify({
|
|
294
|
-
recipe: name, exitCode: result.exitCode, parsed: parsed.data, duration: result.duration,
|
|
295
|
-
}) }] };
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (format === "compressed") {
|
|
299
|
-
const compressed = compress(command, output, { format: "json" });
|
|
273
|
+
if (format === "json" || format === "compressed") {
|
|
274
|
+
const processed = await processOutput(command, output);
|
|
300
275
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
301
|
-
recipe: name, exitCode: result.exitCode,
|
|
302
|
-
|
|
276
|
+
recipe: name, exitCode: result.exitCode, summary: processed.summary,
|
|
277
|
+
structured: processed.structured, duration: result.duration,
|
|
278
|
+
tokensSaved: processed.tokensSaved, aiProcessed: processed.aiProcessed,
|
|
303
279
|
}) }] };
|
|
304
280
|
}
|
|
305
281
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
@@ -384,10 +360,10 @@ export function createServer() {
|
|
|
384
360
|
duration: result.duration, tokensSaved: diff.tokensSaved,
|
|
385
361
|
}) }] };
|
|
386
362
|
}
|
|
387
|
-
// First run — return full output
|
|
388
|
-
const
|
|
363
|
+
// First run — return full output (ANSI stripped)
|
|
364
|
+
const clean = stripAnsi(output);
|
|
389
365
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
390
|
-
exitCode: result.exitCode, output:
|
|
366
|
+
exitCode: result.exitCode, output: clean,
|
|
391
367
|
diffSummary: "first run", duration: result.duration,
|
|
392
368
|
}) }] };
|
|
393
369
|
});
|
package/dist/output-processor.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// AI-powered output processor — uses cheap AI to intelligently summarize any output
|
|
2
2
|
// NOTHING is hardcoded. The AI decides what's important, what's noise, what to keep.
|
|
3
3
|
import { getProvider } from "./providers/index.js";
|
|
4
|
-
import { estimateTokens } from "./
|
|
4
|
+
import { estimateTokens } from "./tokens.js";
|
|
5
5
|
import { recordSaving } from "./economy.js";
|
|
6
6
|
import { discoverOutputHints } from "./context-hints.js";
|
|
7
7
|
import { formatProfileHints } from "./tool-profiles.js";
|
|
8
|
+
import { stripAnsi } from "./compression.js";
|
|
9
|
+
import { stripNoise } from "./noise-filter.js";
|
|
8
10
|
const MIN_LINES_TO_PROCESS = 15;
|
|
9
|
-
|
|
11
|
+
// Reserve ~2000 chars for system prompt + hints + profile + overhead
|
|
12
|
+
const PROMPT_OVERHEAD_CHARS = 2000;
|
|
13
|
+
const MAX_OUTPUT_FOR_AI = 6000; // chars of output to send to AI (leaves room for prompt overhead)
|
|
10
14
|
const SUMMARIZE_PROMPT = `You are an intelligent terminal assistant. Given a user's original question and the command output, ANSWER THE QUESTION directly.
|
|
11
15
|
|
|
12
16
|
RULES:
|
|
@@ -39,8 +43,9 @@ export async function processOutput(command, output, originalPrompt) {
|
|
|
39
43
|
netSavingsUsd: 0,
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
|
-
//
|
|
43
|
-
let toSummarize = output;
|
|
46
|
+
// Clean output before AI processing — strip ANSI codes and noise
|
|
47
|
+
let toSummarize = stripAnsi(output);
|
|
48
|
+
toSummarize = stripNoise(toSummarize).cleaned;
|
|
44
49
|
if (toSummarize.length > MAX_OUTPUT_FOR_AI) {
|
|
45
50
|
const headChars = Math.floor(MAX_OUTPUT_FOR_AI * 0.6);
|
|
46
51
|
const tailChars = Math.floor(MAX_OUTPUT_FOR_AI * 0.3);
|
|
@@ -61,13 +66,11 @@ export async function processOutput(command, output, originalPrompt) {
|
|
|
61
66
|
const summary = await provider.complete(`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}${hintsBlock}${profileHints}`, {
|
|
62
67
|
system: SUMMARIZE_PROMPT,
|
|
63
68
|
maxTokens: 300,
|
|
69
|
+
temperature: 0.2,
|
|
64
70
|
});
|
|
65
71
|
const originalTokens = estimateTokens(output);
|
|
66
72
|
const summaryTokens = estimateTokens(summary);
|
|
67
73
|
const saved = Math.max(0, originalTokens - summaryTokens);
|
|
68
|
-
if (saved > 0) {
|
|
69
|
-
recordSaving("compressed", saved);
|
|
70
|
-
}
|
|
71
74
|
// Try to extract structured JSON if the AI returned it
|
|
72
75
|
let structured;
|
|
73
76
|
try {
|
|
@@ -12,7 +12,9 @@ export class AnthropicProvider {
|
|
|
12
12
|
const message = await this.client.messages.create({
|
|
13
13
|
model: options.model ?? "claude-haiku-4-5-20251001",
|
|
14
14
|
max_tokens: options.maxTokens ?? 256,
|
|
15
|
-
|
|
15
|
+
temperature: options.temperature ?? 0,
|
|
16
|
+
...(options.stop ? { stop_sequences: options.stop } : {}),
|
|
17
|
+
system: [{ type: "text", text: options.system, cache_control: { type: "ephemeral" } }],
|
|
16
18
|
messages: [{ role: "user", content: prompt }],
|
|
17
19
|
});
|
|
18
20
|
const block = message.content[0];
|
|
@@ -25,7 +27,9 @@ export class AnthropicProvider {
|
|
|
25
27
|
const stream = await this.client.messages.stream({
|
|
26
28
|
model: options.model ?? "claude-haiku-4-5-20251001",
|
|
27
29
|
max_tokens: options.maxTokens ?? 256,
|
|
28
|
-
|
|
30
|
+
temperature: options.temperature ?? 0,
|
|
31
|
+
...(options.stop ? { stop_sequences: options.stop } : {}),
|
|
32
|
+
system: [{ type: "text", text: options.system, cache_control: { type: "ephemeral" } }],
|
|
29
33
|
messages: [{ role: "user", content: prompt }],
|
|
30
34
|
});
|
|
31
35
|
for await (const chunk of stream) {
|
|
@@ -1,95 +1,8 @@
|
|
|
1
|
-
// Cerebras provider —
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const DEFAULT_MODEL = "qwen-3-235b-a22b-instruct-2507";
|
|
5
|
-
export class CerebrasProvider {
|
|
1
|
+
// Cerebras provider — fast inference on Qwen/Llama models
|
|
2
|
+
import { OpenAICompatibleProvider } from "./openai-compat.js";
|
|
3
|
+
export class CerebrasProvider extends OpenAICompatibleProvider {
|
|
6
4
|
name = "cerebras";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
isAvailable() {
|
|
12
|
-
return !!process.env.CEREBRAS_API_KEY;
|
|
13
|
-
}
|
|
14
|
-
async complete(prompt, options) {
|
|
15
|
-
const model = options.model ?? DEFAULT_MODEL;
|
|
16
|
-
const res = await fetch(`${CEREBRAS_BASE_URL}/chat/completions`, {
|
|
17
|
-
method: "POST",
|
|
18
|
-
headers: {
|
|
19
|
-
"Content-Type": "application/json",
|
|
20
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
21
|
-
},
|
|
22
|
-
body: JSON.stringify({
|
|
23
|
-
model,
|
|
24
|
-
max_tokens: options.maxTokens ?? 256,
|
|
25
|
-
messages: [
|
|
26
|
-
{ role: "system", content: options.system },
|
|
27
|
-
{ role: "user", content: prompt },
|
|
28
|
-
],
|
|
29
|
-
}),
|
|
30
|
-
});
|
|
31
|
-
if (!res.ok) {
|
|
32
|
-
const text = await res.text();
|
|
33
|
-
throw new Error(`Cerebras API error ${res.status}: ${text}`);
|
|
34
|
-
}
|
|
35
|
-
const json = (await res.json());
|
|
36
|
-
return (json.choices?.[0]?.message?.content ?? "").trim();
|
|
37
|
-
}
|
|
38
|
-
async stream(prompt, options, callbacks) {
|
|
39
|
-
const model = options.model ?? DEFAULT_MODEL;
|
|
40
|
-
const res = await fetch(`${CEREBRAS_BASE_URL}/chat/completions`, {
|
|
41
|
-
method: "POST",
|
|
42
|
-
headers: {
|
|
43
|
-
"Content-Type": "application/json",
|
|
44
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
45
|
-
},
|
|
46
|
-
body: JSON.stringify({
|
|
47
|
-
model,
|
|
48
|
-
max_tokens: options.maxTokens ?? 256,
|
|
49
|
-
stream: true,
|
|
50
|
-
messages: [
|
|
51
|
-
{ role: "system", content: options.system },
|
|
52
|
-
{ role: "user", content: prompt },
|
|
53
|
-
],
|
|
54
|
-
}),
|
|
55
|
-
});
|
|
56
|
-
if (!res.ok) {
|
|
57
|
-
const text = await res.text();
|
|
58
|
-
throw new Error(`Cerebras API error ${res.status}: ${text}`);
|
|
59
|
-
}
|
|
60
|
-
let result = "";
|
|
61
|
-
const reader = res.body?.getReader();
|
|
62
|
-
if (!reader)
|
|
63
|
-
throw new Error("No response body");
|
|
64
|
-
const decoder = new TextDecoder();
|
|
65
|
-
let buffer = "";
|
|
66
|
-
while (true) {
|
|
67
|
-
const { done, value } = await reader.read();
|
|
68
|
-
if (done)
|
|
69
|
-
break;
|
|
70
|
-
buffer += decoder.decode(value, { stream: true });
|
|
71
|
-
const lines = buffer.split("\n");
|
|
72
|
-
buffer = lines.pop() ?? "";
|
|
73
|
-
for (const line of lines) {
|
|
74
|
-
const trimmed = line.trim();
|
|
75
|
-
if (!trimmed.startsWith("data: "))
|
|
76
|
-
continue;
|
|
77
|
-
const data = trimmed.slice(6);
|
|
78
|
-
if (data === "[DONE]")
|
|
79
|
-
break;
|
|
80
|
-
try {
|
|
81
|
-
const parsed = JSON.parse(data);
|
|
82
|
-
const delta = parsed.choices?.[0]?.delta?.content;
|
|
83
|
-
if (delta) {
|
|
84
|
-
result += delta;
|
|
85
|
-
callbacks.onToken(result.trim());
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
// skip malformed chunks
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return result.trim();
|
|
94
|
-
}
|
|
5
|
+
baseUrl = "https://api.cerebras.ai/v1";
|
|
6
|
+
defaultModel = "qwen-3-235b-a22b-instruct-2507";
|
|
7
|
+
apiKeyEnvVar = "CEREBRAS_API_KEY";
|
|
95
8
|
}
|
package/dist/providers/groq.js
CHANGED
|
@@ -1,95 +1,8 @@
|
|
|
1
|
-
// Groq provider —
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const DEFAULT_MODEL = "openai/gpt-oss-120b";
|
|
5
|
-
export class GroqProvider {
|
|
1
|
+
// Groq provider — ultra-fast inference
|
|
2
|
+
import { OpenAICompatibleProvider } from "./openai-compat.js";
|
|
3
|
+
export class GroqProvider extends OpenAICompatibleProvider {
|
|
6
4
|
name = "groq";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
isAvailable() {
|
|
12
|
-
return !!process.env.GROQ_API_KEY;
|
|
13
|
-
}
|
|
14
|
-
async complete(prompt, options) {
|
|
15
|
-
const model = options.model ?? DEFAULT_MODEL;
|
|
16
|
-
const res = await fetch(`${GROQ_BASE_URL}/chat/completions`, {
|
|
17
|
-
method: "POST",
|
|
18
|
-
headers: {
|
|
19
|
-
"Content-Type": "application/json",
|
|
20
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
21
|
-
},
|
|
22
|
-
body: JSON.stringify({
|
|
23
|
-
model,
|
|
24
|
-
max_tokens: options.maxTokens ?? 256,
|
|
25
|
-
messages: [
|
|
26
|
-
{ role: "system", content: options.system },
|
|
27
|
-
{ role: "user", content: prompt },
|
|
28
|
-
],
|
|
29
|
-
}),
|
|
30
|
-
});
|
|
31
|
-
if (!res.ok) {
|
|
32
|
-
const text = await res.text();
|
|
33
|
-
throw new Error(`Groq API error ${res.status}: ${text}`);
|
|
34
|
-
}
|
|
35
|
-
const json = (await res.json());
|
|
36
|
-
return (json.choices?.[0]?.message?.content ?? "").trim();
|
|
37
|
-
}
|
|
38
|
-
async stream(prompt, options, callbacks) {
|
|
39
|
-
const model = options.model ?? DEFAULT_MODEL;
|
|
40
|
-
const res = await fetch(`${GROQ_BASE_URL}/chat/completions`, {
|
|
41
|
-
method: "POST",
|
|
42
|
-
headers: {
|
|
43
|
-
"Content-Type": "application/json",
|
|
44
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
45
|
-
},
|
|
46
|
-
body: JSON.stringify({
|
|
47
|
-
model,
|
|
48
|
-
max_tokens: options.maxTokens ?? 256,
|
|
49
|
-
stream: true,
|
|
50
|
-
messages: [
|
|
51
|
-
{ role: "system", content: options.system },
|
|
52
|
-
{ role: "user", content: prompt },
|
|
53
|
-
],
|
|
54
|
-
}),
|
|
55
|
-
});
|
|
56
|
-
if (!res.ok) {
|
|
57
|
-
const text = await res.text();
|
|
58
|
-
throw new Error(`Groq API error ${res.status}: ${text}`);
|
|
59
|
-
}
|
|
60
|
-
let result = "";
|
|
61
|
-
const reader = res.body?.getReader();
|
|
62
|
-
if (!reader)
|
|
63
|
-
throw new Error("No response body");
|
|
64
|
-
const decoder = new TextDecoder();
|
|
65
|
-
let buffer = "";
|
|
66
|
-
while (true) {
|
|
67
|
-
const { done, value } = await reader.read();
|
|
68
|
-
if (done)
|
|
69
|
-
break;
|
|
70
|
-
buffer += decoder.decode(value, { stream: true });
|
|
71
|
-
const lines = buffer.split("\n");
|
|
72
|
-
buffer = lines.pop() ?? "";
|
|
73
|
-
for (const line of lines) {
|
|
74
|
-
const trimmed = line.trim();
|
|
75
|
-
if (!trimmed.startsWith("data: "))
|
|
76
|
-
continue;
|
|
77
|
-
const data = trimmed.slice(6);
|
|
78
|
-
if (data === "[DONE]")
|
|
79
|
-
break;
|
|
80
|
-
try {
|
|
81
|
-
const parsed = JSON.parse(data);
|
|
82
|
-
const delta = parsed.choices?.[0]?.delta?.content;
|
|
83
|
-
if (delta) {
|
|
84
|
-
result += delta;
|
|
85
|
-
callbacks.onToken(result.trim());
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
// skip malformed chunks
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return result.trim();
|
|
94
|
-
}
|
|
5
|
+
baseUrl = "https://api.groq.com/openai/v1";
|
|
6
|
+
defaultModel = "openai/gpt-oss-120b";
|
|
7
|
+
apiKeyEnvVar = "GROQ_API_KEY";
|
|
95
8
|
}
|
package/dist/providers/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Provider auto-detection and management
|
|
1
|
+
// Provider auto-detection and management — with fallback on failure
|
|
2
2
|
import { DEFAULT_PROVIDER_CONFIG } from "./base.js";
|
|
3
3
|
import { AnthropicProvider } from "./anthropic.js";
|
|
4
4
|
import { CerebrasProvider } from "./cerebras.js";
|
|
@@ -6,9 +6,10 @@ import { GroqProvider } from "./groq.js";
|
|
|
6
6
|
import { XaiProvider } from "./xai.js";
|
|
7
7
|
export { DEFAULT_PROVIDER_CONFIG } from "./base.js";
|
|
8
8
|
let _provider = null;
|
|
9
|
+
let _failedProviders = new Set();
|
|
9
10
|
/** Get the active LLM provider. Auto-detects based on available API keys. */
|
|
10
11
|
export function getProvider(config) {
|
|
11
|
-
if (_provider)
|
|
12
|
+
if (_provider && !_failedProviders.has(_provider.name))
|
|
12
13
|
return _provider;
|
|
13
14
|
const cfg = config ?? DEFAULT_PROVIDER_CONFIG;
|
|
14
15
|
_provider = resolveProvider(cfg);
|
|
@@ -17,51 +18,99 @@ export function getProvider(config) {
|
|
|
17
18
|
/** Reset the cached provider (useful when config changes). */
|
|
18
19
|
export function resetProvider() {
|
|
19
20
|
_provider = null;
|
|
21
|
+
_failedProviders.clear();
|
|
22
|
+
}
|
|
23
|
+
/** Get a fallback-wrapped provider that tries alternatives on failure */
|
|
24
|
+
export function getProviderWithFallback(config) {
|
|
25
|
+
const primary = getProvider(config);
|
|
26
|
+
return new FallbackProvider(primary);
|
|
20
27
|
}
|
|
21
28
|
function resolveProvider(config) {
|
|
22
|
-
if (config.provider
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
if (config.provider !== "auto") {
|
|
30
|
+
const providers = {
|
|
31
|
+
cerebras: () => new CerebrasProvider(),
|
|
32
|
+
anthropic: () => new AnthropicProvider(),
|
|
33
|
+
groq: () => new GroqProvider(),
|
|
34
|
+
xai: () => new XaiProvider(),
|
|
35
|
+
};
|
|
36
|
+
const factory = providers[config.provider];
|
|
37
|
+
if (factory) {
|
|
38
|
+
const p = factory();
|
|
39
|
+
if (!p.isAvailable())
|
|
40
|
+
throw new Error(`${config.provider.toUpperCase()}_API_KEY not set`);
|
|
41
|
+
return p;
|
|
42
|
+
}
|
|
33
43
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
// auto: prefer Cerebras, then xAI, then Groq, then Anthropic — skip failed
|
|
45
|
+
const candidates = [
|
|
46
|
+
new CerebrasProvider(),
|
|
47
|
+
new XaiProvider(),
|
|
48
|
+
new GroqProvider(),
|
|
49
|
+
new AnthropicProvider(),
|
|
50
|
+
];
|
|
51
|
+
for (const p of candidates) {
|
|
52
|
+
if (p.isAvailable() && !_failedProviders.has(p.name))
|
|
53
|
+
return p;
|
|
39
54
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
// If all failed, clear failures and try again
|
|
56
|
+
if (_failedProviders.size > 0) {
|
|
57
|
+
_failedProviders.clear();
|
|
58
|
+
for (const p of candidates) {
|
|
59
|
+
if (p.isAvailable())
|
|
60
|
+
return p;
|
|
61
|
+
}
|
|
45
62
|
}
|
|
46
|
-
// auto: prefer Cerebras (qwen-235b, fast + accurate), then xAI, then Groq, then Anthropic
|
|
47
|
-
const cerebras = new CerebrasProvider();
|
|
48
|
-
if (cerebras.isAvailable())
|
|
49
|
-
return cerebras;
|
|
50
|
-
const xai = new XaiProvider();
|
|
51
|
-
if (xai.isAvailable())
|
|
52
|
-
return xai;
|
|
53
|
-
const groq = new GroqProvider();
|
|
54
|
-
if (groq.isAvailable())
|
|
55
|
-
return groq;
|
|
56
|
-
const anthropic = new AnthropicProvider();
|
|
57
|
-
if (anthropic.isAvailable())
|
|
58
|
-
return anthropic;
|
|
59
63
|
throw new Error("No API key found. Set one of:\n" +
|
|
60
64
|
" export CEREBRAS_API_KEY=your-key (free, open-source)\n" +
|
|
61
65
|
" export GROQ_API_KEY=your-key (free, fast)\n" +
|
|
62
66
|
" export XAI_API_KEY=your-key (Grok, code-optimized)\n" +
|
|
63
67
|
" export ANTHROPIC_API_KEY=your-key (Claude)");
|
|
64
68
|
}
|
|
69
|
+
/** Provider wrapper that falls back to alternatives on API errors */
|
|
70
|
+
class FallbackProvider {
|
|
71
|
+
name;
|
|
72
|
+
primary;
|
|
73
|
+
constructor(primary) {
|
|
74
|
+
this.primary = primary;
|
|
75
|
+
this.name = primary.name;
|
|
76
|
+
}
|
|
77
|
+
isAvailable() {
|
|
78
|
+
return this.primary.isAvailable();
|
|
79
|
+
}
|
|
80
|
+
async complete(prompt, options) {
|
|
81
|
+
try {
|
|
82
|
+
return await this.primary.complete(prompt, options);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const fallback = this.getFallback();
|
|
86
|
+
if (fallback)
|
|
87
|
+
return fallback.complete(prompt, options);
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async stream(prompt, options, callbacks) {
|
|
92
|
+
try {
|
|
93
|
+
return await this.primary.stream(prompt, options, callbacks);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
const fallback = this.getFallback();
|
|
97
|
+
if (fallback)
|
|
98
|
+
return fallback.complete(prompt, options); // fallback doesn't stream
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
getFallback() {
|
|
103
|
+
_failedProviders.add(this.primary.name);
|
|
104
|
+
_provider = null; // force re-resolve
|
|
105
|
+
try {
|
|
106
|
+
const next = getProvider();
|
|
107
|
+
if (next.name !== this.primary.name)
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
65
114
|
/** List available providers (for onboarding UI). */
|
|
66
115
|
export function availableProviders() {
|
|
67
116
|
return [
|