claude-connect 0.1.7 → 0.1.8
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 +10 -0
- package/package.json +1 -1
- package/src/data/catalog-store.js +11 -2
- package/src/gateway/server.js +58 -4
- package/src/lib/model-budget.js +73 -0
- package/src/lib/profile.js +2 -1
- package/src/lib/terminal.js +26 -13
package/README.md
CHANGED
|
@@ -83,6 +83,7 @@ Al activar:
|
|
|
83
83
|
- `Inception Labs` usa el gateway local sobre `https://api.inceptionlabs.ai/v1/chat/completions`
|
|
84
84
|
- `OpenRouter` usa `openrouter/free` por gateway sobre `https://openrouter.ai/api/v1`
|
|
85
85
|
- `Qwen` apunta al gateway local `http://127.0.0.1:4310/anthropic`
|
|
86
|
+
- para algunos modelos con limites conocidos, el gateway ahora ajusta `max_tokens` y bloquea prompts sobredimensionados antes de que el upstream devuelva errores opacos
|
|
86
87
|
|
|
87
88
|
## Providers
|
|
88
89
|
|
|
@@ -123,6 +124,8 @@ Nota sobre `OpenAI`:
|
|
|
123
124
|
Nota sobre `Inception Labs`:
|
|
124
125
|
|
|
125
126
|
- esta primera integracion expone solo `mercury-2`, que es el modelo chat-compatible oficial en `v1/chat/completions`
|
|
127
|
+
- `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
|
|
128
|
+
- Claude Connect aplica presupuesto preventivo de contexto para `mercury-2` usando ventana `128K` y salida maxima `16,384`
|
|
126
129
|
- `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
130
|
- autenticacion soportada: `API key`
|
|
128
131
|
- referencias oficiales:
|
|
@@ -130,6 +133,13 @@ Nota sobre `Inception Labs`:
|
|
|
130
133
|
- https://docs.inceptionlabs.ai/get-started/authentication
|
|
131
134
|
- https://docs.inceptionlabs.ai/get-started/models
|
|
132
135
|
|
|
136
|
+
Nota sobre `DeepSeek`:
|
|
137
|
+
|
|
138
|
+
- Claude Connect aplica presupuesto preventivo de contexto para `deepseek-chat` y `deepseek-reasoner`
|
|
139
|
+
- referencias oficiales:
|
|
140
|
+
- https://api-docs.deepseek.com/quick_start/pricing/
|
|
141
|
+
- https://api-docs.deepseek.com/guides/reasoning_model
|
|
142
|
+
|
|
133
143
|
Nota sobre `Ollama`:
|
|
134
144
|
|
|
135
145
|
- la URL del servidor se define al crear la conexión
|
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
|
);
|
|
@@ -703,6 +704,7 @@ const seedProviders = [
|
|
|
703
704
|
apiBaseUrl: 'https://api.inceptionlabs.ai/v1',
|
|
704
705
|
apiPath: '/chat/completions',
|
|
705
706
|
authEnvMode: 'auth_token',
|
|
707
|
+
supportsVision: false,
|
|
706
708
|
sortOrder: 1,
|
|
707
709
|
isDefault: 1
|
|
708
710
|
}
|
|
@@ -838,10 +840,10 @@ function seedCatalog(db) {
|
|
|
838
840
|
INSERT INTO models (
|
|
839
841
|
id, provider_id, name, category, context_window, summary,
|
|
840
842
|
upstream_model_id,
|
|
841
|
-
transport_mode, api_style, api_base_url, api_path, auth_env_mode,
|
|
843
|
+
transport_mode, api_style, api_base_url, api_path, auth_env_mode, supports_vision,
|
|
842
844
|
sort_order, is_default
|
|
843
845
|
)
|
|
844
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
846
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
845
847
|
ON CONFLICT(id) DO UPDATE SET
|
|
846
848
|
provider_id = excluded.provider_id,
|
|
847
849
|
name = excluded.name,
|
|
@@ -854,6 +856,7 @@ function seedCatalog(db) {
|
|
|
854
856
|
api_base_url = excluded.api_base_url,
|
|
855
857
|
api_path = excluded.api_path,
|
|
856
858
|
auth_env_mode = excluded.auth_env_mode,
|
|
859
|
+
supports_vision = excluded.supports_vision,
|
|
857
860
|
sort_order = excluded.sort_order,
|
|
858
861
|
is_default = excluded.is_default
|
|
859
862
|
`);
|
|
@@ -924,6 +927,7 @@ function seedCatalog(db) {
|
|
|
924
927
|
model.apiBaseUrl ?? null,
|
|
925
928
|
model.apiPath ?? null,
|
|
926
929
|
model.authEnvMode ?? 'auth_token',
|
|
930
|
+
model.supportsVision === false ? 0 : 1,
|
|
927
931
|
model.sortOrder,
|
|
928
932
|
model.isDefault
|
|
929
933
|
);
|
|
@@ -995,6 +999,10 @@ function ensureSchemaMigrations(db) {
|
|
|
995
999
|
alterStatements.push(`ALTER TABLE models ADD COLUMN auth_env_mode TEXT NOT NULL DEFAULT 'auth_token'`);
|
|
996
1000
|
}
|
|
997
1001
|
|
|
1002
|
+
if (!modelColumns.has('supports_vision')) {
|
|
1003
|
+
alterStatements.push(`ALTER TABLE models ADD COLUMN supports_vision INTEGER NOT NULL DEFAULT 1`);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
998
1006
|
if (!modelColumns.has('upstream_model_id')) {
|
|
999
1007
|
alterStatements.push(`ALTER TABLE models ADD COLUMN upstream_model_id TEXT`);
|
|
1000
1008
|
}
|
|
@@ -1037,6 +1045,7 @@ function mapModelRow(row) {
|
|
|
1037
1045
|
apiBaseUrl: row.api_base_url,
|
|
1038
1046
|
apiPath: row.api_path,
|
|
1039
1047
|
authEnvMode: row.auth_env_mode,
|
|
1048
|
+
supportsVision: Boolean(row.supports_vision),
|
|
1040
1049
|
sortOrder: Number(row.sort_order),
|
|
1041
1050
|
isDefault: Boolean(row.is_default)
|
|
1042
1051
|
};
|
package/src/gateway/server.js
CHANGED
|
@@ -24,6 +24,7 @@ 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';
|
|
29
30
|
import { readManagedProviderTokenSecret, readManagedTokenSecret } from '../lib/secrets.js';
|
|
@@ -161,6 +162,48 @@ function getUpstreamModelId(profile) {
|
|
|
161
162
|
return profile?.model?.upstreamModelId ?? profile?.model?.id ?? 'unknown';
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
function requestContainsImageInput(body) {
|
|
166
|
+
return Array.isArray(body?.messages)
|
|
167
|
+
&& body.messages.some((messageItem) => Array.isArray(messageItem?.content)
|
|
168
|
+
&& messageItem.content.some((part) => part?.type === 'image'));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function profileSupportsImageInput(profile) {
|
|
172
|
+
if (typeof profile?.model?.supportsVision === 'boolean') {
|
|
173
|
+
return profile.model.supportsVision;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (profile?.provider?.id === 'inception') {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function stringifyUpstreamMessage(value) {
|
|
184
|
+
if (typeof value === 'string') {
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (value == null) {
|
|
189
|
+
return '';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (typeof value === 'object') {
|
|
193
|
+
if ('message' in value && typeof value.message === 'string') {
|
|
194
|
+
return value.message;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
return JSON.stringify(value);
|
|
199
|
+
} catch (_error) {
|
|
200
|
+
return String(value);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return String(value);
|
|
205
|
+
}
|
|
206
|
+
|
|
164
207
|
function resolveGatewayUpstreamConfig(profile) {
|
|
165
208
|
if (profile?.provider?.id === 'ollama') {
|
|
166
209
|
return {
|
|
@@ -335,9 +378,9 @@ async function forwardUpstreamRequest({ targetUrl, headers, payload, context, re
|
|
|
335
378
|
});
|
|
336
379
|
}
|
|
337
380
|
|
|
338
|
-
const message = responsePayload?.error?.message
|
|
339
|
-
|| responsePayload?.message
|
|
340
|
-
|| responsePayload?.error
|
|
381
|
+
const message = stringifyUpstreamMessage(responsePayload?.error?.message)
|
|
382
|
+
|| stringifyUpstreamMessage(responsePayload?.message)
|
|
383
|
+
|| stringifyUpstreamMessage(responsePayload?.error)
|
|
341
384
|
|| `HTTP ${response.status}`;
|
|
342
385
|
const providerName = context?.profile?.provider?.name ?? context?.profile?.provider?.id ?? 'El proveedor';
|
|
343
386
|
const containsImageInput = Array.isArray(payload?.messages)
|
|
@@ -453,9 +496,20 @@ async function handleCountTokens(request, response) {
|
|
|
453
496
|
}
|
|
454
497
|
|
|
455
498
|
async function handleMessages(request, response) {
|
|
456
|
-
const
|
|
499
|
+
const rawBody = await readJsonBody(request);
|
|
457
500
|
const context = await resolveGatewayContext();
|
|
458
501
|
|
|
502
|
+
if (requestContainsImageInput(rawBody) && !profileSupportsImageInput(context.profile)) {
|
|
503
|
+
const providerName = context.profile.provider.name;
|
|
504
|
+
const modelName = context.profile.model.name;
|
|
505
|
+
throw new Error(`${providerName} no admite imagenes con el modelo ${modelName} en esta integracion. Usa un proveedor o modelo con soporte visual.`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const body = enforceModelTokenBudget({
|
|
509
|
+
body: rawBody,
|
|
510
|
+
profile: context.profile
|
|
511
|
+
});
|
|
512
|
+
|
|
459
513
|
if (context.upstreamApiStyle === 'anthropic') {
|
|
460
514
|
const upstreamResponse = await forwardAnthropicMessage({
|
|
461
515
|
requestBody: body,
|
|
@@ -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
|
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 = () => {
|