krasavacode 0.3.6 → 0.4.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/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 { isGeminiConfigured } from './setup-gemini.js';
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(homedir(), '.krasavacode', 'state.json');
9
+ const STATE_FILE = join(ROOT, 'state.json');
9
10
 
10
11
  const KRASAVACODE_MARKER = 'krasavacode/managed';
11
12
 
12
- function pollinationsProvider() {
13
- return {
14
- name: 'pollinations',
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({ withGemini }) {
33
- const Providers = withGemini
34
- ? [geminiProvider(), pollinationsProvider()]
35
- : [pollinationsProvider()];
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 Router = withGemini
38
- ? {
39
- default: 'gemini,gemini-2.5-flash',
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
- return {
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
- async function readState() {
65
- try { return JSON.parse(await readFile(STATE_FILE, 'utf8')); }
66
- catch { return {}; }
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
- * - If user has run setup-gemini Gemini first, Pollinations as fallback Provider
74
- * - Else → Pollinations only
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 withGemini = await isGeminiConfigured();
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 — сохранил резервную копию: ${backupPath}`);
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 { withGemini };
78
+ return { configured: await configuredProviders() };
102
79
  }
103
80
 
104
81
  export const CCR_PORT = 3456;
@@ -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
+ }