krasavacode 0.3.6 → 0.4.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 +23 -14
- package/bin/krasavacode.js +4 -4
- package/package.json +1 -1
- package/src/cooldowns.js +54 -0
- package/src/doctor.js +19 -0
- package/src/hub.js +7 -5
- package/src/launch.js +19 -22
- package/src/metrics-proxy.js +164 -122
- package/src/preset.js +35 -58
- package/src/providers.js +234 -0
- package/src/setup.js +409 -0
- package/src/setup-gemini.js +0 -427
package/src/preset.js
CHANGED
|
@@ -1,85 +1,62 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile, copyFile, access } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { PROVIDERS, configuredProviders, pollinationsProvider } from './providers.js';
|
|
5
5
|
|
|
6
|
+
const ROOT = join(homedir(), '.krasavacode');
|
|
6
7
|
const CCR_DIR = join(homedir(), '.claude-code-router');
|
|
7
8
|
const CCR_CONFIG = join(CCR_DIR, 'config.json');
|
|
8
|
-
const STATE_FILE = join(
|
|
9
|
+
const STATE_FILE = join(ROOT, 'state.json');
|
|
9
10
|
|
|
10
11
|
const KRASAVACODE_MARKER = 'krasavacode/managed';
|
|
11
12
|
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
api_base_url: 'https://text.pollinations.ai/openai/chat/completions',
|
|
16
|
-
api_key: 'public',
|
|
17
|
-
models: ['openai', 'openai-fast', 'gpt-oss-20b'],
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function geminiProvider() {
|
|
22
|
-
// gemini-2.5-pro free tier = 0 requests; only flash is actually free.
|
|
23
|
-
return {
|
|
24
|
-
name: 'gemini',
|
|
25
|
-
api_base_url: 'https://generativelanguage.googleapis.com/v1beta/models/',
|
|
26
|
-
api_key: '$GEMINI_API_KEY',
|
|
27
|
-
models: ['gemini-2.5-flash', 'gemini-flash-latest'],
|
|
28
|
-
transformer: { use: ['gemini'] },
|
|
29
|
-
};
|
|
30
|
-
}
|
|
13
|
+
async function readState() { try { return JSON.parse(await readFile(STATE_FILE, 'utf8')); } catch { return {}; } }
|
|
14
|
+
async function writeState(s) { await writeFile(STATE_FILE, JSON.stringify(s, null, 2)); }
|
|
15
|
+
async function exists(p) { return access(p).then(() => true).catch(() => false); }
|
|
31
16
|
|
|
32
|
-
function buildConfig(
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
17
|
+
async function buildConfig() {
|
|
18
|
+
const configured = await configuredProviders();
|
|
19
|
+
const providers = configured.map(id => PROVIDERS[id].ccrProvider());
|
|
20
|
+
// Pollinations always last — final no-key fallback
|
|
21
|
+
providers.push(pollinationsProvider());
|
|
36
22
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
background: 'gemini,gemini-2.5-flash',
|
|
41
|
-
think: 'gemini,gemini-2.5-flash',
|
|
42
|
-
longContext: 'gemini,gemini-2.5-flash',
|
|
43
|
-
longContextThreshold: 60000,
|
|
44
|
-
}
|
|
45
|
-
: {
|
|
46
|
-
default: 'pollinations,openai',
|
|
47
|
-
background: 'pollinations,openai-fast',
|
|
48
|
-
think: 'pollinations,openai',
|
|
49
|
-
longContext: 'pollinations,openai',
|
|
50
|
-
longContextThreshold: 60000,
|
|
51
|
-
};
|
|
23
|
+
const firstId = configured[0];
|
|
24
|
+
const firstModel = firstId ? PROVIDERS[firstId].defaultModel : 'openai';
|
|
25
|
+
const firstProv = firstId ? firstId : 'pollinations';
|
|
52
26
|
|
|
53
|
-
|
|
27
|
+
const config = {
|
|
54
28
|
HOST: '127.0.0.1',
|
|
55
29
|
PORT: 3456,
|
|
56
30
|
LOG: false,
|
|
57
31
|
API_TIMEOUT_MS: 600000,
|
|
58
|
-
Providers,
|
|
59
|
-
Router
|
|
32
|
+
Providers: providers,
|
|
33
|
+
Router: {
|
|
34
|
+
// Static fallback if custom-router returns null
|
|
35
|
+
default: `${firstProv},${firstModel}`,
|
|
36
|
+
background: `${firstProv},${firstModel}`,
|
|
37
|
+
think: `${firstProv},${firstModel}`,
|
|
38
|
+
longContext: `${firstProv},${firstModel}`,
|
|
39
|
+
longContextThreshold: 60000,
|
|
40
|
+
},
|
|
60
41
|
_krasavacode: KRASAVACODE_MARKER,
|
|
61
42
|
};
|
|
62
|
-
}
|
|
63
43
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
44
|
+
// No custom router: provider selection is done at the metrics-proxy layer,
|
|
45
|
+
// which rewrites body.model = "provider,name" so ccr forwards directly.
|
|
46
|
+
return config;
|
|
67
47
|
}
|
|
68
|
-
async function writeState(s) { await writeFile(STATE_FILE, JSON.stringify(s, null, 2)); }
|
|
69
|
-
async function exists(p) { return access(p).then(() => true).catch(() => false); }
|
|
70
48
|
|
|
71
49
|
/**
|
|
72
|
-
* Generates ~/.claude-code-router/config.json
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* Returns { withGemini: boolean }.
|
|
50
|
+
* Generates ~/.claude-code-router/config.json AND the custom router file
|
|
51
|
+
* ~/.krasavacode/router.js. Backs up any pre-existing user config that
|
|
52
|
+
* isn't ours.
|
|
77
53
|
*/
|
|
78
54
|
export async function ensurePreset() {
|
|
55
|
+
await mkdir(ROOT, { recursive: true });
|
|
79
56
|
await mkdir(CCR_DIR, { recursive: true });
|
|
57
|
+
|
|
80
58
|
const state = await readState();
|
|
81
|
-
const
|
|
82
|
-
const config = buildConfig({ withGemini });
|
|
59
|
+
const config = await buildConfig();
|
|
83
60
|
|
|
84
61
|
if (await exists(CCR_CONFIG)) {
|
|
85
62
|
let existing;
|
|
@@ -93,12 +70,12 @@ export async function ensurePreset() {
|
|
|
93
70
|
await copyFile(CCR_CONFIG, backupPath);
|
|
94
71
|
state.userConfigBackedUp = backupPath;
|
|
95
72
|
await writeState(state);
|
|
96
|
-
console.log(`💾 Найден свой config.json у claude-code-router —
|
|
73
|
+
console.log(`💾 Найден свой config.json у claude-code-router — резервная копия: ${backupPath}`);
|
|
97
74
|
}
|
|
98
75
|
}
|
|
99
76
|
|
|
100
77
|
await writeFile(CCR_CONFIG, JSON.stringify(config, null, 2));
|
|
101
|
-
return {
|
|
78
|
+
return { configured: await configuredProviders() };
|
|
102
79
|
}
|
|
103
80
|
|
|
104
81
|
export const CCR_PORT = 3456;
|
package/src/providers.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider registry: единый источник правды для всех бесплатных провайдеров.
|
|
3
|
+
*
|
|
4
|
+
* Приоритет в chain (по убыванию щедрости free tier):
|
|
5
|
+
* 1. cerebras — 1M токенов/день, Llama 3.3 70B / Qwen3 235B
|
|
6
|
+
* 2. groq — 1000 RPD, Kimi K2 / DeepSeek-R1
|
|
7
|
+
* 3. gemini — 250 RPD, Gemini 2.5 Flash (фолбэк)
|
|
8
|
+
* 4. pollinations — без квоты, gpt-oss-20b (последний резерв)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { readFile, access } from 'node:fs/promises';
|
|
14
|
+
|
|
15
|
+
const ROOT = join(homedir(), '.krasavacode');
|
|
16
|
+
export const KEYS_DIR = join(ROOT, 'keys');
|
|
17
|
+
|
|
18
|
+
// Keys live in ~/.krasavacode/keys/<provider>.env as: PROVIDER_API_KEY=...
|
|
19
|
+
const ENV_VAR_NAMES = {
|
|
20
|
+
cerebras: 'CEREBRAS_API_KEY',
|
|
21
|
+
groq: 'GROQ_API_KEY',
|
|
22
|
+
gemini: 'GEMINI_API_KEY',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const PROVIDERS = {
|
|
26
|
+
cerebras: {
|
|
27
|
+
id: 'cerebras',
|
|
28
|
+
name: 'Cerebras',
|
|
29
|
+
tagline: '1M токенов/день, скорость 2600 ток/сек',
|
|
30
|
+
consoleUrl: 'https://cloud.cerebras.ai/?utm_source=krasavacode',
|
|
31
|
+
keyPattern: /^csk-[A-Za-z0-9]{20,}$/,
|
|
32
|
+
keyExample: 'csk-…',
|
|
33
|
+
keyHowto: [
|
|
34
|
+
'Зарегистрируйся (Sign up) — бесплатно, без карты',
|
|
35
|
+
'В дашборде нажми «API Keys» в левом меню',
|
|
36
|
+
'Нажми «Create API Key» → введи любое название',
|
|
37
|
+
'Скопируй ключ (начинается с csk-)',
|
|
38
|
+
],
|
|
39
|
+
quota: '~1 000 000 токенов в день, 30 запросов/мин',
|
|
40
|
+
bestModel: 'Qwen 3 235B (Cerebras)',
|
|
41
|
+
rpd: null, // unlimited by RPD, only TPD-bound
|
|
42
|
+
tpd: 1_000_000,
|
|
43
|
+
rpm: 30,
|
|
44
|
+
contextLimit: 8_000,
|
|
45
|
+
// OpenAI-compatible API
|
|
46
|
+
verify: async (key) => {
|
|
47
|
+
const res = await fetch('https://api.cerebras.ai/v1/chat/completions', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'content-type': 'application/json',
|
|
51
|
+
'authorization': `Bearer ${key}`,
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
model: 'llama-3.3-70b',
|
|
55
|
+
messages: [{ role: 'user', content: 'Reply: ok' }],
|
|
56
|
+
max_tokens: 5,
|
|
57
|
+
}),
|
|
58
|
+
signal: AbortSignal.timeout(15000),
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
let msg = `HTTP ${res.status}`;
|
|
62
|
+
try { msg = (await res.json())?.error?.message || msg; } catch {}
|
|
63
|
+
return { ok: false, error: msg };
|
|
64
|
+
}
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
return { ok: true, text: data.choices?.[0]?.message?.content?.trim() || 'ok' };
|
|
67
|
+
},
|
|
68
|
+
ccrProvider: () => ({
|
|
69
|
+
name: 'cerebras',
|
|
70
|
+
api_base_url: 'https://api.cerebras.ai/v1/chat/completions',
|
|
71
|
+
api_key: '$CEREBRAS_API_KEY',
|
|
72
|
+
models: ['qwen-3-235b-a22b-instruct-2507', 'llama-3.3-70b', 'gpt-oss-120b'],
|
|
73
|
+
}),
|
|
74
|
+
defaultModel: 'qwen-3-235b-a22b-instruct-2507',
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
groq: {
|
|
78
|
+
id: 'groq',
|
|
79
|
+
name: 'Groq',
|
|
80
|
+
tagline: '1000 запросов/день, Kimi K2 + DeepSeek-R1',
|
|
81
|
+
consoleUrl: 'https://console.groq.com/keys',
|
|
82
|
+
keyPattern: /^gsk_[A-Za-z0-9]{40,}$/,
|
|
83
|
+
keyExample: 'gsk_…',
|
|
84
|
+
keyHowto: [
|
|
85
|
+
'Войди через Google или GitHub — без карты',
|
|
86
|
+
'Перейди в раздел «API Keys» (страница уже открыта)',
|
|
87
|
+
'Нажми «Create API Key» → введи название',
|
|
88
|
+
'Скопируй ключ (начинается с gsk_)',
|
|
89
|
+
],
|
|
90
|
+
quota: '~1 000 запросов в день, 30 запросов/мин',
|
|
91
|
+
bestModel: 'Kimi K2 (через Groq)',
|
|
92
|
+
rpd: 1000,
|
|
93
|
+
tpd: null,
|
|
94
|
+
rpm: 30,
|
|
95
|
+
contextLimit: 128_000,
|
|
96
|
+
verify: async (key) => {
|
|
97
|
+
const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'content-type': 'application/json',
|
|
101
|
+
'authorization': `Bearer ${key}`,
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
model: 'llama-3.3-70b-versatile',
|
|
105
|
+
messages: [{ role: 'user', content: 'Reply: ok' }],
|
|
106
|
+
max_tokens: 5,
|
|
107
|
+
}),
|
|
108
|
+
signal: AbortSignal.timeout(15000),
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
let msg = `HTTP ${res.status}`;
|
|
112
|
+
try { msg = (await res.json())?.error?.message || msg; } catch {}
|
|
113
|
+
return { ok: false, error: msg };
|
|
114
|
+
}
|
|
115
|
+
const data = await res.json();
|
|
116
|
+
return { ok: true, text: data.choices?.[0]?.message?.content?.trim() || 'ok' };
|
|
117
|
+
},
|
|
118
|
+
ccrProvider: () => ({
|
|
119
|
+
name: 'groq',
|
|
120
|
+
api_base_url: 'https://api.groq.com/openai/v1/chat/completions',
|
|
121
|
+
api_key: '$GROQ_API_KEY',
|
|
122
|
+
models: [
|
|
123
|
+
'moonshotai/kimi-k2-instruct',
|
|
124
|
+
'deepseek-r1-distill-llama-70b',
|
|
125
|
+
'llama-3.3-70b-versatile',
|
|
126
|
+
'qwen/qwen3-32b',
|
|
127
|
+
],
|
|
128
|
+
}),
|
|
129
|
+
defaultModel: 'moonshotai/kimi-k2-instruct',
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
gemini: {
|
|
133
|
+
id: 'gemini',
|
|
134
|
+
name: 'Google Gemini',
|
|
135
|
+
tagline: '250 запросов/день, Gemini 2.5 Flash',
|
|
136
|
+
consoleUrl: 'https://aistudio.google.com/apikey',
|
|
137
|
+
keyPattern: /^AIza[A-Za-z0-9_-]{35}$/,
|
|
138
|
+
keyExample: 'AIzaSy…',
|
|
139
|
+
keyHowto: [
|
|
140
|
+
'Войди через свой Google-аккаунт (Gmail/YouTube подойдут)',
|
|
141
|
+
'Нажми «Create API key» наверху страницы',
|
|
142
|
+
'Если попросит выбрать проект — оставь предложенный',
|
|
143
|
+
'Скопируй ключ (начинается с AIza)',
|
|
144
|
+
],
|
|
145
|
+
quota: '~250 запросов в день, 10 запросов/мин',
|
|
146
|
+
bestModel: 'Gemini 2.5 Flash',
|
|
147
|
+
rpd: 250,
|
|
148
|
+
tpd: null,
|
|
149
|
+
rpm: 10,
|
|
150
|
+
contextLimit: 1_000_000,
|
|
151
|
+
verify: async (key) => {
|
|
152
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${encodeURIComponent(key)}`;
|
|
153
|
+
const res = await fetch(url, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: { 'content-type': 'application/json' },
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
contents: [{ parts: [{ text: 'Reply: ok' }] }],
|
|
158
|
+
generationConfig: { maxOutputTokens: 20 },
|
|
159
|
+
}),
|
|
160
|
+
signal: AbortSignal.timeout(15000),
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
let msg = `HTTP ${res.status}`;
|
|
164
|
+
try { msg = (await res.json())?.error?.message || msg; } catch {}
|
|
165
|
+
return { ok: false, error: msg };
|
|
166
|
+
}
|
|
167
|
+
const data = await res.json();
|
|
168
|
+
return { ok: true, text: (data.candidates?.[0]?.content?.parts?.[0]?.text || 'ok').trim() };
|
|
169
|
+
},
|
|
170
|
+
ccrProvider: () => ({
|
|
171
|
+
name: 'gemini',
|
|
172
|
+
api_base_url: 'https://generativelanguage.googleapis.com/v1beta/models/',
|
|
173
|
+
api_key: '$GEMINI_API_KEY',
|
|
174
|
+
models: ['gemini-2.5-flash', 'gemini-flash-latest'],
|
|
175
|
+
transformer: { use: ['gemini'] },
|
|
176
|
+
}),
|
|
177
|
+
defaultModel: 'gemini-2.5-flash',
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const PROVIDER_PRIORITY = ['cerebras', 'groq', 'gemini'];
|
|
182
|
+
|
|
183
|
+
export function pollinationsProvider() {
|
|
184
|
+
return {
|
|
185
|
+
name: 'pollinations',
|
|
186
|
+
api_base_url: 'https://text.pollinations.ai/openai/chat/completions',
|
|
187
|
+
api_key: 'public',
|
|
188
|
+
models: ['openai', 'openai-fast'],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function envFile(providerId) {
|
|
193
|
+
return join(KEYS_DIR, `${providerId}.env`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function exists(p) { return access(p).then(() => true).catch(() => false); }
|
|
197
|
+
|
|
198
|
+
export async function loadProviderKey(providerId) {
|
|
199
|
+
// New layout: ~/.krasavacode/keys/<id>.env
|
|
200
|
+
const newPath = envFile(providerId);
|
|
201
|
+
// Legacy layout (gemini only): ~/.krasavacode/gemini.env
|
|
202
|
+
const legacyPath = join(ROOT, `${providerId}.env`);
|
|
203
|
+
|
|
204
|
+
for (const p of [newPath, legacyPath]) {
|
|
205
|
+
try {
|
|
206
|
+
const content = await readFile(p, 'utf8');
|
|
207
|
+
const varName = ENV_VAR_NAMES[providerId];
|
|
208
|
+
const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
|
|
209
|
+
if (m) return m[1].trim();
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function isProviderConfigured(providerId) {
|
|
216
|
+
return (await loadProviderKey(providerId)) != null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Returns ids of all configured providers, in priority order. */
|
|
220
|
+
export async function configuredProviders() {
|
|
221
|
+
const result = [];
|
|
222
|
+
for (const id of PROVIDER_PRIORITY) {
|
|
223
|
+
if (await isProviderConfigured(id)) result.push(id);
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function getProviderEnvVarName(providerId) {
|
|
229
|
+
return ENV_VAR_NAMES[providerId];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function providerEnvFile(providerId) {
|
|
233
|
+
return envFile(providerId);
|
|
234
|
+
}
|