claude-connect 0.1.7 → 0.1.9
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 +26 -3
- package/package.json +1 -1
- package/src/data/catalog-store.js +80 -2
- package/src/gateway/server.js +63 -4
- package/src/lib/claude-settings.js +19 -3
- package/src/lib/model-budget.js +73 -0
- package/src/lib/profile.js +2 -1
- package/src/lib/provider-rate-limit.js +109 -0
- package/src/lib/terminal.js +26 -13
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Claude Connect
|
|
2
2
|
|
|
3
|
-
> Conecta `Claude Code` con `OpenCode Go`, `Zen`, `Kimi`, `DeepSeek`, `Ollama`, `OpenAI`, `Inception Labs`, `OpenRouter` y `Qwen` desde una interfaz de consola clara, rápida y reversible.
|
|
3
|
+
> Conecta `Claude Code` con `OpenCode Go`, `Zen`, `Kimi`, `DeepSeek`, `Z.AI`, `Ollama`, `OpenAI`, `Inception Labs`, `OpenRouter` y `Qwen` desde una interfaz de consola clara, rápida y reversible.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/claude-connect)
|
|
6
6
|
[](https://nodejs.org/)
|
|
7
7
|
[](./LICENSE)
|
|
8
|
-
[](https://www.npmjs.com/package/claude-connect)
|
|
8
|
+
[](https://www.npmjs.com/package/claude-connect)
|
|
9
9
|
|
|
10
10
|
## Why Claude Connect
|
|
11
11
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
### Highlights
|
|
15
15
|
|
|
16
|
-
- `OpenCode Go`, `Zen`, `Kimi`, `DeepSeek`, `Ollama`, `OpenAI`, `Inception Labs`, `OpenRouter` y `Qwen` listos desde el primer arranque
|
|
16
|
+
- `OpenCode Go`, `Zen`, `Kimi`, `DeepSeek`, `Z.AI`, `Ollama`, `OpenAI`, `Inception Labs`, `OpenRouter` y `Qwen` listos desde el primer arranque
|
|
17
17
|
- soporte para `Token` y `OAuth` cuando el proveedor lo permite
|
|
18
18
|
- API keys compartidas por proveedor para no repetir el mismo token en cada modelo
|
|
19
19
|
- activación reversible sobre la instalación real de `Claude Code`
|
|
@@ -78,11 +78,14 @@ Al activar:
|
|
|
78
78
|
- `Zen` usa conexión directa o gateway según el modelo elegido
|
|
79
79
|
- `Kimi` usa gateway local y reenvia al endpoint Anthropic de `https://api.kimi.com/coding/`
|
|
80
80
|
- `DeepSeek` apunta a `https://api.deepseek.com/anthropic`
|
|
81
|
+
- `Z.AI` apunta a `https://api.z.ai/api/anthropic`
|
|
81
82
|
- `Ollama` pide una URL local o remota, valida `/api/tags` y usa el gateway local sobre `.../api/chat`
|
|
82
83
|
- `OpenAI` usa el gateway local sobre `https://api.openai.com/v1/chat/completions`
|
|
83
84
|
- `Inception Labs` usa el gateway local sobre `https://api.inceptionlabs.ai/v1/chat/completions`
|
|
84
85
|
- `OpenRouter` usa `openrouter/free` por gateway sobre `https://openrouter.ai/api/v1`
|
|
85
86
|
- `Qwen` apunta al gateway local `http://127.0.0.1:4310/anthropic`
|
|
87
|
+
- para algunos modelos con limites conocidos, el gateway ahora ajusta `max_tokens` y bloquea prompts sobredimensionados antes de que el upstream devuelva errores opacos
|
|
88
|
+
- para `Inception Labs`, el gateway tambien respeta un presupuesto local de input tokens por minuto para reducir errores de `Rate limit reached`
|
|
86
89
|
|
|
87
90
|
## Providers
|
|
88
91
|
|
|
@@ -92,6 +95,7 @@ Al activar:
|
|
|
92
95
|
| `Zen` | `Claude*` de Zen + modelos `chat/completions` de Zen | `Token` | Mixta |
|
|
93
96
|
| `Kimi` | `kimi-for-coding` | `Token` | Gateway local |
|
|
94
97
|
| `DeepSeek` | `deepseek-chat`, `deepseek-reasoner` | `Token` | Directa |
|
|
98
|
+
| `Z.AI` | `glm-5.1`, `glm-4.7`, `glm-4.5-air` | `Token` | Directa |
|
|
95
99
|
| `Ollama` | modelos descubiertos desde tu servidor | `Servidor Ollama` | Gateway local |
|
|
96
100
|
| `OpenAI` | `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`, `gpt-5.2-codex`, `gpt-5.2`, `gpt-5.1-codex-max`, `gpt-5.1-codex-mini` | `Token` | Gateway local |
|
|
97
101
|
| `Inception Labs` | `mercury-2` | `Token` | Gateway local |
|
|
@@ -123,12 +127,31 @@ Nota sobre `OpenAI`:
|
|
|
123
127
|
Nota sobre `Inception Labs`:
|
|
124
128
|
|
|
125
129
|
- esta primera integracion expone solo `mercury-2`, que es el modelo chat-compatible oficial en `v1/chat/completions`
|
|
130
|
+
- `mercury-2` se trata como modelo solo texto en Claude Connect; si envias una imagen, la app ahora corta la peticion con un mensaje claro
|
|
131
|
+
- Claude Connect aplica presupuesto preventivo de contexto para `mercury-2` usando ventana `128K` y salida maxima `16,384`
|
|
132
|
+
- Claude Connect tambien aplica una ventana deslizante local de `400,000` input tokens por minuto para reducir rechazos del upstream por rate limit
|
|
126
133
|
- `Mercury Edit 2` no se publica todavia en Claude Connect porque usa endpoints `fim/edit` que no encajan con Claude Code en esta arquitectura
|
|
127
134
|
- autenticacion soportada: `API key`
|
|
128
135
|
- referencias oficiales:
|
|
129
136
|
- https://docs.inceptionlabs.ai/get-started/get-started
|
|
130
137
|
- https://docs.inceptionlabs.ai/get-started/authentication
|
|
131
138
|
- https://docs.inceptionlabs.ai/get-started/models
|
|
139
|
+
- https://docs.inceptionlabs.ai/get-started/rate-limits
|
|
140
|
+
|
|
141
|
+
Nota sobre `DeepSeek`:
|
|
142
|
+
|
|
143
|
+
- Claude Connect aplica presupuesto preventivo de contexto para `deepseek-chat` y `deepseek-reasoner`
|
|
144
|
+
- referencias oficiales:
|
|
145
|
+
- https://api-docs.deepseek.com/quick_start/pricing/
|
|
146
|
+
- https://api-docs.deepseek.com/guides/reasoning_model
|
|
147
|
+
|
|
148
|
+
Nota sobre `Z.AI`:
|
|
149
|
+
|
|
150
|
+
- usa el endpoint Anthropic-compatible oficial `https://api.z.ai/api/anthropic`
|
|
151
|
+
- Claude Connect fija `API_TIMEOUT_MS=3000000`
|
|
152
|
+
- al activar un perfil de `Z.AI`, tambien mapea `ANTHROPIC_DEFAULT_HAIKU_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL` y `ANTHROPIC_DEFAULT_OPUS_MODEL` al modelo elegido para que Claude Code use `GLM` de forma consistente
|
|
153
|
+
- referencias oficiales:
|
|
154
|
+
- https://docs.z.ai/devpack/tool/claude
|
|
132
155
|
|
|
133
156
|
Nota sobre `Ollama`:
|
|
134
157
|
|
package/package.json
CHANGED
|
@@ -39,6 +39,7 @@ CREATE TABLE IF NOT EXISTS models (
|
|
|
39
39
|
api_base_url TEXT,
|
|
40
40
|
api_path TEXT,
|
|
41
41
|
auth_env_mode TEXT NOT NULL DEFAULT 'auth_token',
|
|
42
|
+
supports_vision INTEGER NOT NULL DEFAULT 1,
|
|
42
43
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
43
44
|
is_default INTEGER NOT NULL DEFAULT 0
|
|
44
45
|
);
|
|
@@ -527,6 +528,75 @@ const seedProviders = [
|
|
|
527
528
|
}
|
|
528
529
|
]
|
|
529
530
|
},
|
|
531
|
+
{
|
|
532
|
+
id: 'zai',
|
|
533
|
+
name: 'Z.AI',
|
|
534
|
+
vendor: 'Zhipu AI',
|
|
535
|
+
description: 'GLM Coding Plan para Claude Code usando el endpoint Anthropic-compatible oficial de z.ai.',
|
|
536
|
+
docsUrl: 'https://docs.z.ai/devpack/tool/claude',
|
|
537
|
+
docsVerifiedAt: '2026-04-04',
|
|
538
|
+
baseUrl: 'https://api.z.ai/api/anthropic',
|
|
539
|
+
defaultModelId: 'glm-5.1',
|
|
540
|
+
defaultAuthMethodId: 'token',
|
|
541
|
+
defaultApiKeyEnvVar: 'ZAI_API_KEY',
|
|
542
|
+
models: [
|
|
543
|
+
{
|
|
544
|
+
id: 'glm-5.1',
|
|
545
|
+
name: 'GLM-5.1',
|
|
546
|
+
category: 'Coding',
|
|
547
|
+
contextWindow: 'Auto',
|
|
548
|
+
summary: 'Modelo recomendado de la documentacion oficial de z.ai para usuarios Max que quieren usar GLM-5.1 en Claude Code.',
|
|
549
|
+
upstreamModelId: 'glm-5.1',
|
|
550
|
+
transportMode: 'direct',
|
|
551
|
+
apiStyle: 'anthropic',
|
|
552
|
+
apiBaseUrl: 'https://api.z.ai/api/anthropic',
|
|
553
|
+
apiPath: '/v1/messages',
|
|
554
|
+
authEnvMode: 'auth_token',
|
|
555
|
+
sortOrder: 1,
|
|
556
|
+
isDefault: 1
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
id: 'glm-4.7',
|
|
560
|
+
name: 'GLM-4.7',
|
|
561
|
+
category: 'General',
|
|
562
|
+
contextWindow: 'Auto',
|
|
563
|
+
summary: 'Modelo default recomendado por z.ai para Opus y Sonnet dentro del GLM Coding Plan.',
|
|
564
|
+
upstreamModelId: 'glm-4.7',
|
|
565
|
+
transportMode: 'direct',
|
|
566
|
+
apiStyle: 'anthropic',
|
|
567
|
+
apiBaseUrl: 'https://api.z.ai/api/anthropic',
|
|
568
|
+
apiPath: '/v1/messages',
|
|
569
|
+
authEnvMode: 'auth_token',
|
|
570
|
+
sortOrder: 2,
|
|
571
|
+
isDefault: 0
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
id: 'glm-4.5-air',
|
|
575
|
+
name: 'GLM-4.5-Air',
|
|
576
|
+
category: 'Fast',
|
|
577
|
+
contextWindow: 'Auto',
|
|
578
|
+
summary: 'Modelo ligero recomendado por z.ai para la clase Haiku dentro del GLM Coding Plan.',
|
|
579
|
+
upstreamModelId: 'glm-4.5-air',
|
|
580
|
+
transportMode: 'direct',
|
|
581
|
+
apiStyle: 'anthropic',
|
|
582
|
+
apiBaseUrl: 'https://api.z.ai/api/anthropic',
|
|
583
|
+
apiPath: '/v1/messages',
|
|
584
|
+
authEnvMode: 'auth_token',
|
|
585
|
+
sortOrder: 3,
|
|
586
|
+
isDefault: 0
|
|
587
|
+
}
|
|
588
|
+
],
|
|
589
|
+
authMethods: [
|
|
590
|
+
{
|
|
591
|
+
id: 'token',
|
|
592
|
+
name: 'Token',
|
|
593
|
+
description: 'Conexion por API key contra el endpoint Anthropic-compatible oficial de z.ai.',
|
|
594
|
+
credentialKind: 'env_var',
|
|
595
|
+
sortOrder: 1,
|
|
596
|
+
isDefault: 1
|
|
597
|
+
}
|
|
598
|
+
]
|
|
599
|
+
},
|
|
530
600
|
{
|
|
531
601
|
id: 'ollama',
|
|
532
602
|
name: 'Ollama',
|
|
@@ -703,6 +773,7 @@ const seedProviders = [
|
|
|
703
773
|
apiBaseUrl: 'https://api.inceptionlabs.ai/v1',
|
|
704
774
|
apiPath: '/chat/completions',
|
|
705
775
|
authEnvMode: 'auth_token',
|
|
776
|
+
supportsVision: false,
|
|
706
777
|
sortOrder: 1,
|
|
707
778
|
isDefault: 1
|
|
708
779
|
}
|
|
@@ -838,10 +909,10 @@ function seedCatalog(db) {
|
|
|
838
909
|
INSERT INTO models (
|
|
839
910
|
id, provider_id, name, category, context_window, summary,
|
|
840
911
|
upstream_model_id,
|
|
841
|
-
transport_mode, api_style, api_base_url, api_path, auth_env_mode,
|
|
912
|
+
transport_mode, api_style, api_base_url, api_path, auth_env_mode, supports_vision,
|
|
842
913
|
sort_order, is_default
|
|
843
914
|
)
|
|
844
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
915
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
845
916
|
ON CONFLICT(id) DO UPDATE SET
|
|
846
917
|
provider_id = excluded.provider_id,
|
|
847
918
|
name = excluded.name,
|
|
@@ -854,6 +925,7 @@ function seedCatalog(db) {
|
|
|
854
925
|
api_base_url = excluded.api_base_url,
|
|
855
926
|
api_path = excluded.api_path,
|
|
856
927
|
auth_env_mode = excluded.auth_env_mode,
|
|
928
|
+
supports_vision = excluded.supports_vision,
|
|
857
929
|
sort_order = excluded.sort_order,
|
|
858
930
|
is_default = excluded.is_default
|
|
859
931
|
`);
|
|
@@ -924,6 +996,7 @@ function seedCatalog(db) {
|
|
|
924
996
|
model.apiBaseUrl ?? null,
|
|
925
997
|
model.apiPath ?? null,
|
|
926
998
|
model.authEnvMode ?? 'auth_token',
|
|
999
|
+
model.supportsVision === false ? 0 : 1,
|
|
927
1000
|
model.sortOrder,
|
|
928
1001
|
model.isDefault
|
|
929
1002
|
);
|
|
@@ -995,6 +1068,10 @@ function ensureSchemaMigrations(db) {
|
|
|
995
1068
|
alterStatements.push(`ALTER TABLE models ADD COLUMN auth_env_mode TEXT NOT NULL DEFAULT 'auth_token'`);
|
|
996
1069
|
}
|
|
997
1070
|
|
|
1071
|
+
if (!modelColumns.has('supports_vision')) {
|
|
1072
|
+
alterStatements.push(`ALTER TABLE models ADD COLUMN supports_vision INTEGER NOT NULL DEFAULT 1`);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
998
1075
|
if (!modelColumns.has('upstream_model_id')) {
|
|
999
1076
|
alterStatements.push(`ALTER TABLE models ADD COLUMN upstream_model_id TEXT`);
|
|
1000
1077
|
}
|
|
@@ -1037,6 +1114,7 @@ function mapModelRow(row) {
|
|
|
1037
1114
|
apiBaseUrl: row.api_base_url,
|
|
1038
1115
|
apiPath: row.api_path,
|
|
1039
1116
|
authEnvMode: row.auth_env_mode,
|
|
1117
|
+
supportsVision: Boolean(row.supports_vision),
|
|
1040
1118
|
sortOrder: Number(row.sort_order),
|
|
1041
1119
|
isDefault: Boolean(row.is_default)
|
|
1042
1120
|
};
|
package/src/gateway/server.js
CHANGED
|
@@ -24,8 +24,10 @@ import {
|
|
|
24
24
|
} from './state.js';
|
|
25
25
|
import { resolveClaudeConnectPaths } from '../lib/app-paths.js';
|
|
26
26
|
import { readSwitchState } from '../lib/claude-settings.js';
|
|
27
|
+
import { enforceModelTokenBudget } from '../lib/model-budget.js';
|
|
27
28
|
import { readOAuthToken, refreshOAuthToken } from '../lib/oauth.js';
|
|
28
29
|
import { readProfileFile } from '../lib/profile.js';
|
|
30
|
+
import { reserveProviderInputTokens } from '../lib/provider-rate-limit.js';
|
|
29
31
|
import { readManagedProviderTokenSecret, readManagedTokenSecret } from '../lib/secrets.js';
|
|
30
32
|
|
|
31
33
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
@@ -161,6 +163,48 @@ function getUpstreamModelId(profile) {
|
|
|
161
163
|
return profile?.model?.upstreamModelId ?? profile?.model?.id ?? 'unknown';
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
function requestContainsImageInput(body) {
|
|
167
|
+
return Array.isArray(body?.messages)
|
|
168
|
+
&& body.messages.some((messageItem) => Array.isArray(messageItem?.content)
|
|
169
|
+
&& messageItem.content.some((part) => part?.type === 'image'));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function profileSupportsImageInput(profile) {
|
|
173
|
+
if (typeof profile?.model?.supportsVision === 'boolean') {
|
|
174
|
+
return profile.model.supportsVision;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (profile?.provider?.id === 'inception') {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function stringifyUpstreamMessage(value) {
|
|
185
|
+
if (typeof value === 'string') {
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (value == null) {
|
|
190
|
+
return '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (typeof value === 'object') {
|
|
194
|
+
if ('message' in value && typeof value.message === 'string') {
|
|
195
|
+
return value.message;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
return JSON.stringify(value);
|
|
200
|
+
} catch (_error) {
|
|
201
|
+
return String(value);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return String(value);
|
|
206
|
+
}
|
|
207
|
+
|
|
164
208
|
function resolveGatewayUpstreamConfig(profile) {
|
|
165
209
|
if (profile?.provider?.id === 'ollama') {
|
|
166
210
|
return {
|
|
@@ -335,9 +379,9 @@ async function forwardUpstreamRequest({ targetUrl, headers, payload, context, re
|
|
|
335
379
|
});
|
|
336
380
|
}
|
|
337
381
|
|
|
338
|
-
const message = responsePayload?.error?.message
|
|
339
|
-
|| responsePayload?.message
|
|
340
|
-
|| responsePayload?.error
|
|
382
|
+
const message = stringifyUpstreamMessage(responsePayload?.error?.message)
|
|
383
|
+
|| stringifyUpstreamMessage(responsePayload?.message)
|
|
384
|
+
|| stringifyUpstreamMessage(responsePayload?.error)
|
|
341
385
|
|| `HTTP ${response.status}`;
|
|
342
386
|
const providerName = context?.profile?.provider?.name ?? context?.profile?.provider?.id ?? 'El proveedor';
|
|
343
387
|
const containsImageInput = Array.isArray(payload?.messages)
|
|
@@ -453,9 +497,24 @@ async function handleCountTokens(request, response) {
|
|
|
453
497
|
}
|
|
454
498
|
|
|
455
499
|
async function handleMessages(request, response) {
|
|
456
|
-
const
|
|
500
|
+
const rawBody = await readJsonBody(request);
|
|
457
501
|
const context = await resolveGatewayContext();
|
|
458
502
|
|
|
503
|
+
if (requestContainsImageInput(rawBody) && !profileSupportsImageInput(context.profile)) {
|
|
504
|
+
const providerName = context.profile.provider.name;
|
|
505
|
+
const modelName = context.profile.model.name;
|
|
506
|
+
throw new Error(`${providerName} no admite imagenes con el modelo ${modelName} en esta integracion. Usa un proveedor o modelo con soporte visual.`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const body = enforceModelTokenBudget({
|
|
510
|
+
body: rawBody,
|
|
511
|
+
profile: context.profile
|
|
512
|
+
});
|
|
513
|
+
await reserveProviderInputTokens({
|
|
514
|
+
profile: context.profile,
|
|
515
|
+
inputTokens: estimateTokenCountFromAnthropicRequest(body)
|
|
516
|
+
});
|
|
517
|
+
|
|
459
518
|
if (context.upstreamApiStyle === 'anthropic') {
|
|
460
519
|
const upstreamResponse = await forwardAnthropicMessage({
|
|
461
520
|
requestBody: body,
|
|
@@ -154,6 +154,13 @@ export async function resolveClaudeTransportForProfile({
|
|
|
154
154
|
extraEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = profile.model.id;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
if (profile.provider.id === 'zai') {
|
|
158
|
+
extraEnv.API_TIMEOUT_MS = '3000000';
|
|
159
|
+
extraEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = profile.model.id;
|
|
160
|
+
extraEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = profile.model.id;
|
|
161
|
+
extraEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = profile.model.id;
|
|
162
|
+
}
|
|
163
|
+
|
|
157
164
|
if (profile.provider.id === 'kimi') {
|
|
158
165
|
extraEnv.ENABLE_TOOL_SEARCH = 'false';
|
|
159
166
|
}
|
|
@@ -220,6 +227,8 @@ export function buildClaudeSettingsForProfile({
|
|
|
220
227
|
delete env.ENABLE_TOOL_SEARCH;
|
|
221
228
|
delete env.ANTHROPIC_MODEL;
|
|
222
229
|
delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
230
|
+
delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
231
|
+
delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
223
232
|
|
|
224
233
|
Object.assign(env, extraEnv);
|
|
225
234
|
|
|
@@ -259,9 +268,16 @@ export async function activateClaudeProfile({ profile, gatewayBaseUrl = 'http://
|
|
|
259
268
|
const currentAccount = await readClaudeAccount();
|
|
260
269
|
const currentCredentials = await readJsonIfExists(claudeCredentialsPath);
|
|
261
270
|
const currentState = await readSwitchState();
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
271
|
+
const canReuseActiveSnapshot = currentState?.active === true;
|
|
272
|
+
const originalSettings = canReuseActiveSnapshot
|
|
273
|
+
? currentState?.originalSettings ?? currentSettings
|
|
274
|
+
: currentSettings;
|
|
275
|
+
const originalAccount = canReuseActiveSnapshot
|
|
276
|
+
? currentState?.originalAccount ?? currentAccount
|
|
277
|
+
: currentAccount;
|
|
278
|
+
const originalCredentials = canReuseActiveSnapshot
|
|
279
|
+
&& currentState
|
|
280
|
+
&& Object.prototype.hasOwnProperty.call(currentState, 'originalCredentials')
|
|
265
281
|
? currentState.originalCredentials
|
|
266
282
|
: currentCredentials;
|
|
267
283
|
const transport = await resolveClaudeTransportForProfile({
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { estimateTokenCountFromAnthropicRequest } from '../gateway/messages.js';
|
|
2
|
+
|
|
3
|
+
function getModelIdentity(profile) {
|
|
4
|
+
return profile?.model?.upstreamModelId ?? profile?.model?.id ?? '';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getModelTokenLimits(profile) {
|
|
8
|
+
const providerId = profile?.provider?.id;
|
|
9
|
+
const modelId = getModelIdentity(profile);
|
|
10
|
+
|
|
11
|
+
if (providerId === 'inception' && modelId === 'mercury-2') {
|
|
12
|
+
return {
|
|
13
|
+
contextWindowTokens: 128_000,
|
|
14
|
+
defaultOutputTokens: 8_192,
|
|
15
|
+
maxOutputTokens: 16_384
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (providerId === 'deepseek' && modelId === 'deepseek-chat') {
|
|
20
|
+
return {
|
|
21
|
+
contextWindowTokens: 128_000,
|
|
22
|
+
defaultOutputTokens: 4_000,
|
|
23
|
+
maxOutputTokens: 8_000
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (providerId === 'deepseek' && modelId === 'deepseek-reasoner') {
|
|
28
|
+
return {
|
|
29
|
+
contextWindowTokens: 128_000,
|
|
30
|
+
defaultOutputTokens: 32_000,
|
|
31
|
+
maxOutputTokens: 64_000
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function enforceModelTokenBudget({ body, profile, safetyMarginTokens = 1024 }) {
|
|
39
|
+
const limits = getModelTokenLimits(profile);
|
|
40
|
+
|
|
41
|
+
if (!limits) {
|
|
42
|
+
return body;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const estimatedInputTokens = estimateTokenCountFromAnthropicRequest(body);
|
|
46
|
+
const availableForOutput = limits.contextWindowTokens - estimatedInputTokens - safetyMarginTokens;
|
|
47
|
+
|
|
48
|
+
if (availableForOutput <= 0) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`La conversacion actual excede el contexto aproximado de ${limits.contextWindowTokens.toLocaleString('en-US')} tokens para ${profile.model.name}. Usa /compact o /clear antes de continuar.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const requestedOutputTokens = typeof body?.max_tokens === 'number'
|
|
55
|
+
? body.max_tokens
|
|
56
|
+
: limits.defaultOutputTokens;
|
|
57
|
+
const clampedOutputTokens = Math.min(
|
|
58
|
+
requestedOutputTokens,
|
|
59
|
+
limits.maxOutputTokens,
|
|
60
|
+
availableForOutput
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (clampedOutputTokens < 256) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Queda muy poco margen de salida para ${profile.model.name} dentro de su contexto aproximado de ${limits.contextWindowTokens.toLocaleString('en-US')} tokens. Usa /compact o /clear antes de continuar.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...body,
|
|
71
|
+
max_tokens: clampedOutputTokens
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/lib/profile.js
CHANGED
|
@@ -32,7 +32,8 @@ export function buildProfile({ provider, model, authMethod, profileName, apiKeyE
|
|
|
32
32
|
apiStyle: model.apiStyle,
|
|
33
33
|
apiBaseUrl: model.apiBaseUrl,
|
|
34
34
|
apiPath: model.apiPath,
|
|
35
|
-
authEnvMode: model.authEnvMode
|
|
35
|
+
authEnvMode: model.authEnvMode,
|
|
36
|
+
supportsVision: model.supportsVision ?? true
|
|
36
37
|
},
|
|
37
38
|
auth: {
|
|
38
39
|
method: authMethod.id
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
function getProviderId(profile) {
|
|
2
|
+
return profile?.provider?.id ?? '';
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function getProviderInputTokensPerMinute(profile) {
|
|
6
|
+
const providerId = getProviderId(profile);
|
|
7
|
+
|
|
8
|
+
if (providerId === 'inception') {
|
|
9
|
+
return 400_000;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const recentReservations = [];
|
|
16
|
+
|
|
17
|
+
function pruneReservations(now) {
|
|
18
|
+
for (let index = recentReservations.length - 1; index >= 0; index -= 1) {
|
|
19
|
+
if (now - recentReservations[index].timestamp >= 60_000) {
|
|
20
|
+
recentReservations.splice(index, 1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sumReservedTokens(profile, now) {
|
|
26
|
+
const providerId = getProviderId(profile);
|
|
27
|
+
|
|
28
|
+
return recentReservations.reduce((total, entry) => {
|
|
29
|
+
if (entry.providerId !== providerId) {
|
|
30
|
+
return total;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (now - entry.timestamp >= 60_000) {
|
|
34
|
+
return total;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return total + entry.tokens;
|
|
38
|
+
}, 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function earliestExpiryForProvider(profile, now) {
|
|
42
|
+
const providerId = getProviderId(profile);
|
|
43
|
+
let minExpiry = null;
|
|
44
|
+
|
|
45
|
+
for (const entry of recentReservations) {
|
|
46
|
+
if (entry.providerId !== providerId) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const expiry = entry.timestamp + 60_000;
|
|
51
|
+
|
|
52
|
+
if (expiry <= now) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (minExpiry == null || expiry < minExpiry) {
|
|
57
|
+
minExpiry = expiry;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return minExpiry;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resetProviderRateLimitState() {
|
|
65
|
+
recentReservations.length = 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function reserveProviderInputTokens({
|
|
69
|
+
profile,
|
|
70
|
+
inputTokens,
|
|
71
|
+
now = () => Date.now(),
|
|
72
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
73
|
+
}) {
|
|
74
|
+
const tokensPerMinute = getProviderInputTokensPerMinute(profile);
|
|
75
|
+
|
|
76
|
+
if (!tokensPerMinute) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (inputTokens > tokensPerMinute) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${profile.provider.name} rechazo la solicitud porque la entrada estimada excede el limite de ${tokensPerMinute.toLocaleString('en-US')} tokens por minuto. Usa /compact o /clear antes de continuar.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
while (true) {
|
|
87
|
+
const currentTime = now();
|
|
88
|
+
pruneReservations(currentTime);
|
|
89
|
+
const usedTokens = sumReservedTokens(profile, currentTime);
|
|
90
|
+
|
|
91
|
+
if (usedTokens + inputTokens <= tokensPerMinute) {
|
|
92
|
+
recentReservations.push({
|
|
93
|
+
providerId: getProviderId(profile),
|
|
94
|
+
tokens: inputTokens,
|
|
95
|
+
timestamp: currentTime
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const nextExpiry = earliestExpiryForProvider(profile, currentTime);
|
|
101
|
+
|
|
102
|
+
if (nextExpiry == null) {
|
|
103
|
+
throw new Error(`No se pudo reservar presupuesto de tokens para ${profile.provider.name}.`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const waitMs = Math.max(250, nextExpiry - currentTime + 25);
|
|
107
|
+
await sleep(waitMs);
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/lib/terminal.js
CHANGED
|
@@ -19,7 +19,26 @@ export function openAppScreen() {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function closeAppScreen() {
|
|
22
|
-
process.stdout.write('\x1b[?25h\x1b[?1049l');
|
|
22
|
+
process.stdout.write('\x1b[?25h\x1b[?1049l\r\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function beginKeyboardCapture() {
|
|
26
|
+
readline.emitKeypressEvents(process.stdin);
|
|
27
|
+
process.stdin.resume();
|
|
28
|
+
process.stdin.setRawMode(true);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function restoreKeyboardState(onKeypress) {
|
|
32
|
+
if (typeof onKeypress === 'function') {
|
|
33
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (process.stdin.isTTY && process.stdin.isRaw) {
|
|
37
|
+
process.stdin.setRawMode(false);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.stdin.pause();
|
|
41
|
+
process.stdout.write('\x1b[?25h');
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
export function clearScreen() {
|
|
@@ -64,13 +83,11 @@ export function buildFrame({ eyebrow, title, subtitle, body = [], footer = [] })
|
|
|
64
83
|
|
|
65
84
|
export function waitForAnyKey(message = 'Presiona una tecla para continuar.') {
|
|
66
85
|
return new Promise((resolve, reject) => {
|
|
67
|
-
|
|
68
|
-
process.stdin.setRawMode(true);
|
|
86
|
+
beginKeyboardCapture();
|
|
69
87
|
let escapePending = false;
|
|
70
88
|
|
|
71
89
|
const cleanup = () => {
|
|
72
|
-
|
|
73
|
-
process.stdin.setRawMode(false);
|
|
90
|
+
restoreKeyboardState(onKeypress);
|
|
74
91
|
};
|
|
75
92
|
|
|
76
93
|
const onKeypress = (_input, key = {}) => {
|
|
@@ -124,12 +141,10 @@ export function selectFromList({
|
|
|
124
141
|
let selectedIndex = 0;
|
|
125
142
|
let escapePending = false;
|
|
126
143
|
|
|
127
|
-
|
|
128
|
-
process.stdin.setRawMode(true);
|
|
144
|
+
beginKeyboardCapture();
|
|
129
145
|
|
|
130
146
|
const cleanup = () => {
|
|
131
|
-
|
|
132
|
-
process.stdin.setRawMode(false);
|
|
147
|
+
restoreKeyboardState(onKeypress);
|
|
133
148
|
};
|
|
134
149
|
|
|
135
150
|
const render = () => {
|
|
@@ -247,12 +262,10 @@ export function promptText({
|
|
|
247
262
|
let value = '';
|
|
248
263
|
let escapePending = false;
|
|
249
264
|
|
|
250
|
-
|
|
251
|
-
process.stdin.setRawMode(true);
|
|
265
|
+
beginKeyboardCapture();
|
|
252
266
|
|
|
253
267
|
const cleanup = () => {
|
|
254
|
-
|
|
255
|
-
process.stdin.setRawMode(false);
|
|
268
|
+
restoreKeyboardState(onKeypress);
|
|
256
269
|
};
|
|
257
270
|
|
|
258
271
|
const render = () => {
|