krims-code-cli 1.5.2__py3-none-any.whl
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.
- krims_code_cli-1.5.2.dist-info/METADATA +336 -0
- krims_code_cli-1.5.2.dist-info/RECORD +43 -0
- krims_code_cli-1.5.2.dist-info/WHEEL +5 -0
- krims_code_cli-1.5.2.dist-info/entry_points.txt +2 -0
- krims_code_cli-1.5.2.dist-info/licenses/LICENSE +21 -0
- krims_code_cli-1.5.2.dist-info/top_level.txt +1 -0
- krims_pip/__init__.py +1 -0
- krims_pip/cli.py +50 -0
- krims_pip/node_project/bin/krims-code.js +10 -0
- krims_pip/node_project/package-lock.json +793 -0
- krims_pip/node_project/package.json +46 -0
- krims_pip/node_project/src/agent.js +415 -0
- krims_pip/node_project/src/ai/fallback.js +239 -0
- krims_pip/node_project/src/ai/google.js +87 -0
- krims_pip/node_project/src/ai/providers.js +203 -0
- krims_pip/node_project/src/ai/router.js +146 -0
- krims_pip/node_project/src/ai/search.js +94 -0
- krims_pip/node_project/src/ai/telemetry.js +125 -0
- krims_pip/node_project/src/ai/tokens.js +101 -0
- krims_pip/node_project/src/ai/universal.js +507 -0
- krims_pip/node_project/src/ai/xai.js +50 -0
- krims_pip/node_project/src/chat.js +2543 -0
- krims_pip/node_project/src/cli.js +632 -0
- krims_pip/node_project/src/commands/commit.js +157 -0
- krims_pip/node_project/src/commands/dashboard.js +59 -0
- krims_pip/node_project/src/commands/github.js +349 -0
- krims_pip/node_project/src/commands/index.js +67 -0
- krims_pip/node_project/src/commands/status.js +66 -0
- krims_pip/node_project/src/commands/teamwork-preview.js +326 -0
- krims_pip/node_project/src/commands/theme.js +42 -0
- krims_pip/node_project/src/commands/themes.js +30 -0
- krims_pip/node_project/src/config.js +400 -0
- krims_pip/node_project/src/dashboard.js +112 -0
- krims_pip/node_project/src/file-parser.js +164 -0
- krims_pip/node_project/src/git.js +41 -0
- krims_pip/node_project/src/mic.js +220 -0
- krims_pip/node_project/src/modes.js +92 -0
- krims_pip/node_project/src/telemetry-server.js +855 -0
- krims_pip/node_project/src/ui/banner.js +204 -0
- krims_pip/node_project/src/ui/dashboard.html +834 -0
- krims_pip/node_project/src/ui/spinner.js +43 -0
- krims_pip/node_project/src/ui/theme.js +624 -0
- krims_pip/node_project/src/updater.js +196 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// Krims Code AI CLI — Real-time Token & Telemetry Tracker
|
|
3
|
+
// Tracks input/output token counts and session usage metrics.
|
|
4
|
+
// ═══════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
// Session token accumulator
|
|
7
|
+
let sessionTokens = {
|
|
8
|
+
prompt: 0,
|
|
9
|
+
completion: 0,
|
|
10
|
+
total: 0,
|
|
11
|
+
exchanges: 0,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Model-by-model breakdown
|
|
15
|
+
const modelBreakdown = {};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Heuristically estimates the token count of a string based on character length.
|
|
19
|
+
* Standard rule of thumb: 1 token ≈ 4 characters.
|
|
20
|
+
* @param {string} text
|
|
21
|
+
* @returns {number}
|
|
22
|
+
*/
|
|
23
|
+
export function estimateTokens(text) {
|
|
24
|
+
if (!text) return 0;
|
|
25
|
+
return Math.ceil(text.length / 4);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns a copy of the current session token stats.
|
|
30
|
+
* @returns {{prompt: number, completion: number, total: number, exchanges: number}}
|
|
31
|
+
*/
|
|
32
|
+
export function getSessionTokenStats() {
|
|
33
|
+
return { ...sessionTokens };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resets all accumulated session token statistics and breakdowns.
|
|
38
|
+
*/
|
|
39
|
+
export function resetSessionTokenStats() {
|
|
40
|
+
sessionTokens = {
|
|
41
|
+
prompt: 0,
|
|
42
|
+
completion: 0,
|
|
43
|
+
total: 0,
|
|
44
|
+
exchanges: 0,
|
|
45
|
+
};
|
|
46
|
+
// Clear object properties safely without losing reference
|
|
47
|
+
for (const key of Object.keys(modelBreakdown)) {
|
|
48
|
+
delete modelBreakdown[key];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Records token usage for a query.
|
|
54
|
+
* @param {string} model - The model name
|
|
55
|
+
* @param {number} promptTokens - Input prompt tokens count
|
|
56
|
+
* @param {number} completionTokens - Output completion tokens count
|
|
57
|
+
* @returns {{promptTokens: number, completionTokens: number, totalTokens: number}}
|
|
58
|
+
*/
|
|
59
|
+
export function recordTokenUsage(model, promptTokens, completionTokens) {
|
|
60
|
+
const modelName = model || "unknown-model";
|
|
61
|
+
const totalTokens = promptTokens + completionTokens;
|
|
62
|
+
|
|
63
|
+
// Update session totals
|
|
64
|
+
sessionTokens.prompt += promptTokens;
|
|
65
|
+
sessionTokens.completion += completionTokens;
|
|
66
|
+
sessionTokens.total += totalTokens;
|
|
67
|
+
sessionTokens.exchanges += 1;
|
|
68
|
+
|
|
69
|
+
// Update model breakdown
|
|
70
|
+
if (!modelBreakdown[modelName]) {
|
|
71
|
+
modelBreakdown[modelName] = {
|
|
72
|
+
prompt: 0,
|
|
73
|
+
completion: 0,
|
|
74
|
+
total: 0,
|
|
75
|
+
exchanges: 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
modelBreakdown[modelName].prompt += promptTokens;
|
|
79
|
+
modelBreakdown[modelName].completion += completionTokens;
|
|
80
|
+
modelBreakdown[modelName].total += totalTokens;
|
|
81
|
+
modelBreakdown[modelName].exchanges += 1;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
promptTokens,
|
|
85
|
+
completionTokens,
|
|
86
|
+
totalTokens,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns the model-by-model token usage breakdown.
|
|
92
|
+
* @returns {object}
|
|
93
|
+
*/
|
|
94
|
+
export function getBreakdownByModel() {
|
|
95
|
+
// Deep clone modelBreakdown
|
|
96
|
+
const clone = {};
|
|
97
|
+
for (const [key, value] of Object.entries(modelBreakdown)) {
|
|
98
|
+
clone[key] = { ...value };
|
|
99
|
+
}
|
|
100
|
+
return clone;
|
|
101
|
+
}
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// Krims Code AI CLI — Universal API Caller
|
|
3
|
+
// Handles OpenAI-compatible, Google, Anthropic, and Cohere APIs
|
|
4
|
+
// ═══════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calls any OpenAI-compatible API (OpenAI, Groq, Together, Mistral, xAI, etc.)
|
|
8
|
+
* @param {string} prompt - User message
|
|
9
|
+
* @param {string} systemPrompt - System prompt
|
|
10
|
+
* @param {string} apiKey - API key
|
|
11
|
+
* @param {string} baseUrl - Full endpoint URL
|
|
12
|
+
* @param {string} model - Model identifier
|
|
13
|
+
* @param {string} providerName - For error messages
|
|
14
|
+
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
15
|
+
*/
|
|
16
|
+
export async function callOpenAICompatible(prompt, systemPrompt, apiKey, baseUrl, model, providerName, onToken, history = []) {
|
|
17
|
+
const isStreaming = typeof onToken === "function";
|
|
18
|
+
const formattedHistory = history.map(h => ({
|
|
19
|
+
role: h.role === "assistant" ? "assistant" : "user",
|
|
20
|
+
content: h.content
|
|
21
|
+
}));
|
|
22
|
+
const body = {
|
|
23
|
+
model,
|
|
24
|
+
messages: [
|
|
25
|
+
{ role: "system", content: systemPrompt },
|
|
26
|
+
...formattedHistory,
|
|
27
|
+
{ role: "user", content: prompt },
|
|
28
|
+
],
|
|
29
|
+
temperature: 0.7,
|
|
30
|
+
max_tokens: 4096,
|
|
31
|
+
...(isStreaming ? { stream: true } : {}),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const headers = {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
Authorization: `Bearer ${apiKey}`,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// OpenRouter requires extra headers
|
|
40
|
+
if (baseUrl.includes("openrouter.ai")) {
|
|
41
|
+
headers["HTTP-Referer"] = "https://github.com/Krylo-60/krims-code-cli";
|
|
42
|
+
headers["X-Title"] = "Krims Code AI CLI";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const response = await fetch(baseUrl, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers,
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const errorBody = await response.text().catch(() => "");
|
|
53
|
+
throw new Error(`${providerName} API error (${response.status}): ${response.statusText}. ${errorBody}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isStreaming && response.body && typeof response.body.getReader === "function") {
|
|
57
|
+
const reader = response.body.getReader();
|
|
58
|
+
const decoder = new TextDecoder();
|
|
59
|
+
let buffer = "";
|
|
60
|
+
let fullText = "";
|
|
61
|
+
|
|
62
|
+
while (true) {
|
|
63
|
+
const { done, value } = await reader.read();
|
|
64
|
+
if (done) break;
|
|
65
|
+
buffer += decoder.decode(value, { stream: true });
|
|
66
|
+
|
|
67
|
+
const lines = buffer.split("\n");
|
|
68
|
+
buffer = lines.pop(); // Keep the partial line
|
|
69
|
+
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
const trimmed = line.trim();
|
|
72
|
+
if (!trimmed) continue;
|
|
73
|
+
if (trimmed === "data: [DONE]") continue;
|
|
74
|
+
if (trimmed.startsWith("data: ")) {
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
77
|
+
const content = data?.choices?.[0]?.delta?.content || "";
|
|
78
|
+
if (content) {
|
|
79
|
+
onToken(content);
|
|
80
|
+
fullText += content;
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// Ignore partial line errors
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Flush remaining buffer
|
|
89
|
+
if (buffer && buffer.startsWith("data: ")) {
|
|
90
|
+
try {
|
|
91
|
+
const data = JSON.parse(buffer.slice(6));
|
|
92
|
+
const content = data?.choices?.[0]?.delta?.content || "";
|
|
93
|
+
if (content) {
|
|
94
|
+
onToken(content);
|
|
95
|
+
fullText += content;
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!fullText) {
|
|
101
|
+
throw new Error(`${providerName} returned empty response`);
|
|
102
|
+
}
|
|
103
|
+
return { text: fullText, provider: providerName.toLowerCase(), model };
|
|
104
|
+
} else {
|
|
105
|
+
const data = await response.json();
|
|
106
|
+
const text = data?.choices?.[0]?.message?.content;
|
|
107
|
+
|
|
108
|
+
if (!text) {
|
|
109
|
+
throw new Error(`${providerName} returned empty response`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { text, provider: providerName.toLowerCase(), model };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Calls the Google Gemini API (non-OpenAI format).
|
|
118
|
+
* @param {string} prompt - User message
|
|
119
|
+
* @param {string} systemPrompt - System prompt
|
|
120
|
+
* @param {string} apiKey - Google API key
|
|
121
|
+
* @param {string} model - Model name
|
|
122
|
+
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
123
|
+
*/
|
|
124
|
+
export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "gemini-2.5-flash", onToken, history = []) {
|
|
125
|
+
const BASE = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
126
|
+
const isStreaming = typeof onToken === "function";
|
|
127
|
+
let currentHistory = [...history];
|
|
128
|
+
|
|
129
|
+
if (isStreaming) {
|
|
130
|
+
let fullText = "";
|
|
131
|
+
let currentPrompt = prompt;
|
|
132
|
+
let continuations = 0;
|
|
133
|
+
const MAX = 3;
|
|
134
|
+
|
|
135
|
+
while (continuations <= MAX) {
|
|
136
|
+
const url = `${BASE}/${model}:streamGenerateContent?key=${apiKey}`;
|
|
137
|
+
const formattedHistory = currentHistory.map(h => ({
|
|
138
|
+
role: h.role === "assistant" ? "model" : "user",
|
|
139
|
+
parts: [{ text: h.content }]
|
|
140
|
+
}));
|
|
141
|
+
const body = {
|
|
142
|
+
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
143
|
+
contents: [
|
|
144
|
+
...formattedHistory,
|
|
145
|
+
{ role: "user", parts: [{ text: currentPrompt }] }
|
|
146
|
+
],
|
|
147
|
+
generationConfig: { temperature: 0.7, maxOutputTokens: 8192 },
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const response = await fetch(url, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "Content-Type": "application/json" },
|
|
153
|
+
body: JSON.stringify(body),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
const errorBody = await response.text().catch(() => "");
|
|
158
|
+
throw new Error(`Gemini API error (${response.status}): ${response.statusText}. ${errorBody}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let streamedTextInThisTurn = "";
|
|
162
|
+
|
|
163
|
+
if (!response.body || typeof response.body.getReader !== "function") {
|
|
164
|
+
// Fallback to non-streaming if response body is not streamable (e.g. in unit tests)
|
|
165
|
+
const data = await response.json();
|
|
166
|
+
let chunkText = "";
|
|
167
|
+
let finishReason = "";
|
|
168
|
+
if (Array.isArray(data)) {
|
|
169
|
+
for (const chunk of data) {
|
|
170
|
+
const partText = chunk.candidates?.[0]?.content?.parts?.map((p) => p.text).filter(Boolean).join("") || "";
|
|
171
|
+
chunkText += partText;
|
|
172
|
+
finishReason = chunk.candidates?.[0]?.finishReason || finishReason;
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
chunkText = data.candidates?.[0]?.content?.parts?.map((p) => p.text).filter(Boolean).join("") || "";
|
|
176
|
+
finishReason = data.candidates?.[0]?.finishReason || finishReason;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (chunkText) {
|
|
180
|
+
onToken(chunkText);
|
|
181
|
+
fullText += chunkText;
|
|
182
|
+
streamedTextInThisTurn += chunkText;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (finishReason === "MAX_TOKENS" && continuations < MAX) {
|
|
186
|
+
currentHistory.push({ role: "user", content: currentPrompt });
|
|
187
|
+
currentHistory.push({ role: "assistant", content: streamedTextInThisTurn });
|
|
188
|
+
continuations++;
|
|
189
|
+
currentPrompt = "Continue your previous response from exactly where you left off.";
|
|
190
|
+
} else {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
const reader = response.body.getReader();
|
|
195
|
+
const decoder = new TextDecoder();
|
|
196
|
+
let buffer = "";
|
|
197
|
+
let finishReason = "";
|
|
198
|
+
|
|
199
|
+
while (true) {
|
|
200
|
+
const { done, value } = await reader.read();
|
|
201
|
+
if (done) break;
|
|
202
|
+
buffer += decoder.decode(value, { stream: true });
|
|
203
|
+
|
|
204
|
+
let braceCount = 0;
|
|
205
|
+
let jsonStart = -1;
|
|
206
|
+
let inString = false;
|
|
207
|
+
let escape = false;
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
210
|
+
const char = buffer[i];
|
|
211
|
+
if (inString) {
|
|
212
|
+
if (escape) {
|
|
213
|
+
escape = false;
|
|
214
|
+
} else if (char === "\\") {
|
|
215
|
+
escape = true;
|
|
216
|
+
} else if (char === '"') {
|
|
217
|
+
inString = false;
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
if (char === '"') {
|
|
221
|
+
inString = true;
|
|
222
|
+
} else if (char === "{") {
|
|
223
|
+
if (braceCount === 0) {
|
|
224
|
+
jsonStart = i;
|
|
225
|
+
}
|
|
226
|
+
braceCount++;
|
|
227
|
+
} else if (char === "}") {
|
|
228
|
+
braceCount--;
|
|
229
|
+
if (braceCount === 0 && jsonStart !== -1) {
|
|
230
|
+
const jsonStr = buffer.slice(jsonStart, i + 1);
|
|
231
|
+
try {
|
|
232
|
+
const obj = JSON.parse(jsonStr);
|
|
233
|
+
const text = obj.candidates?.[0]?.content?.parts?.map((p) => p.text).filter(Boolean).join("") || "";
|
|
234
|
+
finishReason = obj.candidates?.[0]?.finishReason || finishReason;
|
|
235
|
+
if (text) {
|
|
236
|
+
onToken(text);
|
|
237
|
+
fullText += text;
|
|
238
|
+
streamedTextInThisTurn += text;
|
|
239
|
+
}
|
|
240
|
+
} catch (e) {
|
|
241
|
+
// Ignore parse errors
|
|
242
|
+
}
|
|
243
|
+
buffer = buffer.slice(i + 1);
|
|
244
|
+
i = -1;
|
|
245
|
+
jsonStart = -1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (finishReason === "MAX_TOKENS" && continuations < MAX) {
|
|
253
|
+
currentHistory.push({ role: "user", content: currentPrompt });
|
|
254
|
+
currentHistory.push({ role: "assistant", content: streamedTextInThisTurn });
|
|
255
|
+
continuations++;
|
|
256
|
+
currentPrompt = "Continue your previous response from exactly where you left off.";
|
|
257
|
+
} else {
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!fullText.trim()) throw new Error("Gemini returned empty response");
|
|
264
|
+
return { text: fullText, provider: "google", model };
|
|
265
|
+
} else {
|
|
266
|
+
let fullText = "";
|
|
267
|
+
let currentPrompt = prompt;
|
|
268
|
+
let continuations = 0;
|
|
269
|
+
const MAX = 3;
|
|
270
|
+
|
|
271
|
+
while (continuations <= MAX) {
|
|
272
|
+
const url = `${BASE}/${model}:generateContent?key=${apiKey}`;
|
|
273
|
+
const formattedHistory = currentHistory.map(h => ({
|
|
274
|
+
role: h.role === "assistant" ? "model" : "user",
|
|
275
|
+
parts: [{ text: h.content }]
|
|
276
|
+
}));
|
|
277
|
+
const body = {
|
|
278
|
+
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
279
|
+
contents: [
|
|
280
|
+
...formattedHistory,
|
|
281
|
+
{ role: "user", parts: [{ text: currentPrompt }] }
|
|
282
|
+
],
|
|
283
|
+
generationConfig: { temperature: 0.7, maxOutputTokens: 8192 },
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const response = await fetch(url, {
|
|
287
|
+
method: "POST",
|
|
288
|
+
headers: { "Content-Type": "application/json" },
|
|
289
|
+
body: JSON.stringify(body),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
const errorBody = await response.text().catch(() => "");
|
|
294
|
+
throw new Error(`Gemini API error (${response.status}): ${response.statusText}. ${errorBody}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const data = await response.json();
|
|
298
|
+
if (data.promptFeedback?.blockReason) {
|
|
299
|
+
throw new Error(`Content blocked: ${data.promptFeedback.blockReason}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const candidate = data.candidates?.[0];
|
|
303
|
+
if (!candidate) throw new Error("Gemini returned no candidates");
|
|
304
|
+
|
|
305
|
+
const chunkText = candidate.content?.parts?.map((p) => p.text).filter(Boolean).join("") || "";
|
|
306
|
+
fullText += chunkText;
|
|
307
|
+
|
|
308
|
+
if (candidate.finishReason === "MAX_TOKENS" && continuations < MAX) {
|
|
309
|
+
currentHistory.push({ role: "user", content: currentPrompt });
|
|
310
|
+
currentHistory.push({ role: "assistant", content: chunkText });
|
|
311
|
+
continuations++;
|
|
312
|
+
currentPrompt = "Continue your previous response from exactly where you left off.";
|
|
313
|
+
} else {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!fullText.trim()) throw new Error("Gemini returned empty response");
|
|
319
|
+
return { text: fullText, provider: "google", model };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Calls the Anthropic Claude API.
|
|
325
|
+
* @param {string} prompt - User message
|
|
326
|
+
* @param {string} systemPrompt - System prompt
|
|
327
|
+
* @param {string} apiKey - Anthropic API key
|
|
328
|
+
* @param {string} model - Model name
|
|
329
|
+
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
330
|
+
*/
|
|
331
|
+
export async function callAnthropic(prompt, systemPrompt, apiKey, model = "claude-sonnet-4-20250514", onToken, history = []) {
|
|
332
|
+
const url = "https://api.anthropic.com/v1/messages";
|
|
333
|
+
const isStreaming = typeof onToken === "function";
|
|
334
|
+
const formattedHistory = history.map(h => ({
|
|
335
|
+
role: h.role === "assistant" ? "assistant" : "user",
|
|
336
|
+
content: h.content
|
|
337
|
+
}));
|
|
338
|
+
const body = {
|
|
339
|
+
model,
|
|
340
|
+
max_tokens: 4096,
|
|
341
|
+
system: systemPrompt,
|
|
342
|
+
messages: [
|
|
343
|
+
...formattedHistory,
|
|
344
|
+
{ role: "user", content: prompt }
|
|
345
|
+
],
|
|
346
|
+
...(isStreaming ? { stream: true } : {}),
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const response = await fetch(url, {
|
|
350
|
+
method: "POST",
|
|
351
|
+
headers: {
|
|
352
|
+
"Content-Type": "application/json",
|
|
353
|
+
"x-api-key": apiKey,
|
|
354
|
+
"anthropic-version": "2023-06-01",
|
|
355
|
+
},
|
|
356
|
+
body: JSON.stringify(body),
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
const errorBody = await response.text().catch(() => "");
|
|
361
|
+
throw new Error(`Anthropic API error (${response.status}): ${response.statusText}. ${errorBody}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (isStreaming && response.body && typeof response.body.getReader === "function") {
|
|
365
|
+
const reader = response.body.getReader();
|
|
366
|
+
const decoder = new TextDecoder();
|
|
367
|
+
let buffer = "";
|
|
368
|
+
let fullText = "";
|
|
369
|
+
|
|
370
|
+
while (true) {
|
|
371
|
+
const { done, value } = await reader.read();
|
|
372
|
+
if (done) break;
|
|
373
|
+
buffer += decoder.decode(value, { stream: true });
|
|
374
|
+
|
|
375
|
+
const lines = buffer.split("\n");
|
|
376
|
+
buffer = lines.pop();
|
|
377
|
+
|
|
378
|
+
for (const line of lines) {
|
|
379
|
+
const trimmed = line.trim();
|
|
380
|
+
if (!trimmed) continue;
|
|
381
|
+
if (trimmed.startsWith("data: ")) {
|
|
382
|
+
try {
|
|
383
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
384
|
+
if (data.type === "content_block_delta" && data.delta?.text) {
|
|
385
|
+
onToken(data.delta.text);
|
|
386
|
+
fullText += data.delta.text;
|
|
387
|
+
}
|
|
388
|
+
} catch (e) {
|
|
389
|
+
// Ignore partial JSON
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Flush remaining
|
|
395
|
+
if (buffer && buffer.startsWith("data: ")) {
|
|
396
|
+
try {
|
|
397
|
+
const data = JSON.parse(buffer.slice(6));
|
|
398
|
+
if (data.type === "content_block_delta" && data.delta?.text) {
|
|
399
|
+
onToken(data.delta.text);
|
|
400
|
+
fullText += data.delta.text;
|
|
401
|
+
}
|
|
402
|
+
} catch (e) {}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!fullText) throw new Error("Anthropic returned empty response");
|
|
406
|
+
return { text: fullText, provider: "anthropic", model };
|
|
407
|
+
} else {
|
|
408
|
+
const data = await response.json();
|
|
409
|
+
const text = data?.content?.[0]?.text;
|
|
410
|
+
|
|
411
|
+
if (!text) throw new Error("Anthropic returned empty response");
|
|
412
|
+
return { text, provider: "anthropic", model };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Calls the Cohere API (v2 chat format).
|
|
418
|
+
* @param {string} prompt - User message
|
|
419
|
+
* @param {string} systemPrompt - System prompt
|
|
420
|
+
* @param {string} apiKey - Cohere API key
|
|
421
|
+
* @param {string} model - Model name
|
|
422
|
+
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
423
|
+
*/
|
|
424
|
+
export async function callCohere(prompt, systemPrompt, apiKey, model = "command-r-plus", onToken, history = []) {
|
|
425
|
+
const url = "https://api.cohere.com/v2/chat";
|
|
426
|
+
const isStreaming = typeof onToken === "function";
|
|
427
|
+
const formattedHistory = history.map(h => ({
|
|
428
|
+
role: h.role === "assistant" ? "assistant" : "user",
|
|
429
|
+
content: h.content
|
|
430
|
+
}));
|
|
431
|
+
const body = {
|
|
432
|
+
model,
|
|
433
|
+
messages: [
|
|
434
|
+
{ role: "system", content: systemPrompt },
|
|
435
|
+
...formattedHistory,
|
|
436
|
+
{ role: "user", content: prompt },
|
|
437
|
+
],
|
|
438
|
+
...(isStreaming ? { stream: true } : {}),
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const response = await fetch(url, {
|
|
442
|
+
method: "POST",
|
|
443
|
+
headers: {
|
|
444
|
+
"Content-Type": "application/json",
|
|
445
|
+
Authorization: `Bearer ${apiKey}`,
|
|
446
|
+
},
|
|
447
|
+
body: JSON.stringify(body),
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (!response.ok) {
|
|
451
|
+
const errorBody = await response.text().catch(() => "");
|
|
452
|
+
throw new Error(`Cohere API error (${response.status}): ${response.statusText}. ${errorBody}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (isStreaming && response.body && typeof response.body.getReader === "function") {
|
|
456
|
+
const reader = response.body.getReader();
|
|
457
|
+
const decoder = new TextDecoder();
|
|
458
|
+
let buffer = "";
|
|
459
|
+
let fullText = "";
|
|
460
|
+
|
|
461
|
+
while (true) {
|
|
462
|
+
const { done, value } = await reader.read();
|
|
463
|
+
if (done) break;
|
|
464
|
+
buffer += decoder.decode(value, { stream: true });
|
|
465
|
+
|
|
466
|
+
const lines = buffer.split("\n");
|
|
467
|
+
buffer = lines.pop();
|
|
468
|
+
|
|
469
|
+
for (const line of lines) {
|
|
470
|
+
const trimmed = line.trim();
|
|
471
|
+
if (!trimmed) continue;
|
|
472
|
+
if (trimmed.startsWith("data: ")) {
|
|
473
|
+
try {
|
|
474
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
475
|
+
const content = data?.delta?.message?.content?.text || "";
|
|
476
|
+
if (content) {
|
|
477
|
+
onToken(content);
|
|
478
|
+
fullText += content;
|
|
479
|
+
}
|
|
480
|
+
} catch (e) {
|
|
481
|
+
// Ignore partial JSON
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Flush remaining
|
|
487
|
+
if (buffer && buffer.startsWith("data: ")) {
|
|
488
|
+
try {
|
|
489
|
+
const data = JSON.parse(buffer.slice(6));
|
|
490
|
+
const content = data?.delta?.message?.content?.text || "";
|
|
491
|
+
if (content) {
|
|
492
|
+
onToken(content);
|
|
493
|
+
fullText += content;
|
|
494
|
+
}
|
|
495
|
+
} catch (e) {}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!fullText) throw new Error("Cohere returned empty response");
|
|
499
|
+
return { text: fullText, provider: "cohere", model };
|
|
500
|
+
} else {
|
|
501
|
+
const data = await response.json();
|
|
502
|
+
const text = data?.message?.content?.[0]?.text;
|
|
503
|
+
|
|
504
|
+
if (!text) throw new Error("Cohere returned empty response");
|
|
505
|
+
return { text, provider: "cohere", model };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// Krims Code AI CLI — xAI/Grok API Provider
|
|
3
|
+
// ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sends a prompt to the xAI (Grok) API.
|
|
7
|
+
* @param {string} prompt - The user message
|
|
8
|
+
* @param {string} systemPrompt - System prompt for the mode
|
|
9
|
+
* @param {string} apiKey - xAI API key
|
|
10
|
+
* @param {string} [model='grok-2'] - Model name
|
|
11
|
+
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
12
|
+
*/
|
|
13
|
+
export async function callXai(prompt, systemPrompt, apiKey, model = "grok-2") {
|
|
14
|
+
const url = "https://api.x.ai/v1/chat/completions";
|
|
15
|
+
|
|
16
|
+
const body = {
|
|
17
|
+
model,
|
|
18
|
+
messages: [
|
|
19
|
+
{ role: "system", content: systemPrompt },
|
|
20
|
+
{ role: "user", content: prompt },
|
|
21
|
+
],
|
|
22
|
+
temperature: 0.7,
|
|
23
|
+
max_tokens: 4096,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const response = await fetch(url, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
Authorization: `Bearer ${apiKey}`,
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(body),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const errorBody = await response.text().catch(() => "");
|
|
37
|
+
throw new Error(
|
|
38
|
+
`xAI API error (${response.status}): ${response.statusText}. ${errorBody}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
const text = data?.choices?.[0]?.message?.content;
|
|
44
|
+
|
|
45
|
+
if (!text) {
|
|
46
|
+
throw new Error("xAI API returned empty response");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { text, provider: "xai", model };
|
|
50
|
+
}
|