zerochat 7.3.0__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.
Files changed (130) hide show
  1. zerochat-7.3.0.dist-info/METADATA +70 -0
  2. zerochat-7.3.0.dist-info/RECORD +130 -0
  3. zerochat-7.3.0.dist-info/WHEEL +5 -0
  4. zerochat-7.3.0.dist-info/entry_points.txt +2 -0
  5. zerochat-7.3.0.dist-info/licenses/LICENSE +34 -0
  6. zerochat-7.3.0.dist-info/top_level.txt +2 -0
  7. zerochat.py +2004 -0
  8. zerochat_runtime/__init__.py +1 -0
  9. zerochat_runtime/assets/css/base.css +199 -0
  10. zerochat_runtime/assets/css/components/composer.css +1258 -0
  11. zerochat_runtime/assets/css/components/debug.css +777 -0
  12. zerochat_runtime/assets/css/components/header.css +97 -0
  13. zerochat_runtime/assets/css/components/icons.css +39 -0
  14. zerochat_runtime/assets/css/components/markdown.css +685 -0
  15. zerochat_runtime/assets/css/components/messages.css +473 -0
  16. zerochat_runtime/assets/css/components/modals.css +1319 -0
  17. zerochat_runtime/assets/css/components/sidebar.css +622 -0
  18. zerochat_runtime/assets/css/components/tools.css +1223 -0
  19. zerochat_runtime/assets/css/layout.css +171 -0
  20. zerochat_runtime/assets/css/print.css +120 -0
  21. zerochat_runtime/assets/css/styles.css +18 -0
  22. zerochat_runtime/assets/css/theme-overrides.css +555 -0
  23. zerochat_runtime/assets/css/tokens.css +165 -0
  24. zerochat_runtime/assets/help/architecture.html +247 -0
  25. zerochat_runtime/assets/help/composer.html +134 -0
  26. zerochat_runtime/assets/help/conversations.html +105 -0
  27. zerochat_runtime/assets/help/debug.html +108 -0
  28. zerochat_runtime/assets/help/en/architecture.html +247 -0
  29. zerochat_runtime/assets/help/en/composer.html +134 -0
  30. zerochat_runtime/assets/help/en/conversations.html +104 -0
  31. zerochat_runtime/assets/help/en/debug.html +105 -0
  32. zerochat_runtime/assets/help/en/gemini-free.html +39 -0
  33. zerochat_runtime/assets/help/en/index.html +191 -0
  34. zerochat_runtime/assets/help/en/learning.html +71 -0
  35. zerochat_runtime/assets/help/en/mcp.html +143 -0
  36. zerochat_runtime/assets/help/en/openrouter-free.html +39 -0
  37. zerochat_runtime/assets/help/en/profiles.html +173 -0
  38. zerochat_runtime/assets/help/en/rag.html +130 -0
  39. zerochat_runtime/assets/help/en/reasoning-telemetry.html +108 -0
  40. zerochat_runtime/assets/help/en/tools-agent.html +133 -0
  41. zerochat_runtime/assets/help/en/webllm.html +355 -0
  42. zerochat_runtime/assets/help/gemini-free.html +39 -0
  43. zerochat_runtime/assets/help/help.css +604 -0
  44. zerochat_runtime/assets/help/help.js +51 -0
  45. zerochat_runtime/assets/help/index.html +194 -0
  46. zerochat_runtime/assets/help/learning.html +71 -0
  47. zerochat_runtime/assets/help/mcp.html +143 -0
  48. zerochat_runtime/assets/help/openrouter-free.html +39 -0
  49. zerochat_runtime/assets/help/profiles.html +174 -0
  50. zerochat_runtime/assets/help/rag.html +130 -0
  51. zerochat_runtime/assets/help/reasoning-telemetry.html +111 -0
  52. zerochat_runtime/assets/help/tools-agent.html +133 -0
  53. zerochat_runtime/assets/help/webllm.html +359 -0
  54. zerochat_runtime/assets/js/agent-core.js +1553 -0
  55. zerochat_runtime/assets/js/api.js +844 -0
  56. zerochat_runtime/assets/js/app.js +2558 -0
  57. zerochat_runtime/assets/js/attachments.js +220 -0
  58. zerochat_runtime/assets/js/charts.js +338 -0
  59. zerochat_runtime/assets/js/chat-engine.js +566 -0
  60. zerochat_runtime/assets/js/config-store.js +207 -0
  61. zerochat_runtime/assets/js/context-manager.js +584 -0
  62. zerochat_runtime/assets/js/conversation-service.js +484 -0
  63. zerochat_runtime/assets/js/cookies.js +688 -0
  64. zerochat_runtime/assets/js/data-reset-service.js +136 -0
  65. zerochat_runtime/assets/js/debug.js +508 -0
  66. zerochat_runtime/assets/js/defaults.js +16 -0
  67. zerochat_runtime/assets/js/export.js +179 -0
  68. zerochat_runtime/assets/js/file-parser.js +2088 -0
  69. zerochat_runtime/assets/js/generation-controller.js +417 -0
  70. zerochat_runtime/assets/js/i18n.js +1483 -0
  71. zerochat_runtime/assets/js/icons.js +156 -0
  72. zerochat_runtime/assets/js/ingestionEngine.js +436 -0
  73. zerochat_runtime/assets/js/markdown.js +588 -0
  74. zerochat_runtime/assets/js/mcp.js +1271 -0
  75. zerochat_runtime/assets/js/message-turns.js +62 -0
  76. zerochat_runtime/assets/js/profile-backup.js +118 -0
  77. zerochat_runtime/assets/js/profile-export-bundle.js +481 -0
  78. zerochat_runtime/assets/js/profile-repository.js +213 -0
  79. zerochat_runtime/assets/js/providers-webllm.js +486 -0
  80. zerochat_runtime/assets/js/providers.js +1560 -0
  81. zerochat_runtime/assets/js/rag-index.js +295 -0
  82. zerochat_runtime/assets/js/rag-service.js +530 -0
  83. zerochat_runtime/assets/js/rag-ui.js +997 -0
  84. zerochat_runtime/assets/js/ragStorage.js +676 -0
  85. zerochat_runtime/assets/js/sandbox.js +449 -0
  86. zerochat_runtime/assets/js/state.js +704 -0
  87. zerochat_runtime/assets/js/storage-db.js +144 -0
  88. zerochat_runtime/assets/js/tool-cards.js +330 -0
  89. zerochat_runtime/assets/js/tool-security.js +653 -0
  90. zerochat_runtime/assets/js/tools/README.md +50 -0
  91. zerochat_runtime/assets/js/tools/builtin/agent-checkpoint.tool.js +212 -0
  92. zerochat_runtime/assets/js/tools/builtin/download-pdf.tool.js +111 -0
  93. zerochat_runtime/assets/js/tools/builtin/execute-javascript.tool.js +219 -0
  94. zerochat_runtime/assets/js/tools/builtin/fetch-web-page.tool.js +112 -0
  95. zerochat_runtime/assets/js/tools/builtin/list-documents.tool.js +117 -0
  96. zerochat_runtime/assets/js/tools/builtin/read-knowledge-chunk.tool.js +129 -0
  97. zerochat_runtime/assets/js/tools/builtin/read-knowledge-image.tool.js +95 -0
  98. zerochat_runtime/assets/js/tools/builtin/render-chart.tool.js +90 -0
  99. zerochat_runtime/assets/js/tools/builtin/search-knowledge-base.tool.js +124 -0
  100. zerochat_runtime/assets/js/tools/builtin/search-web.tool.js +125 -0
  101. zerochat_runtime/assets/js/tools/tool-manifest.js +53 -0
  102. zerochat_runtime/assets/js/tools/tool-runtime.js +77 -0
  103. zerochat_runtime/assets/js/ui-composer.js +319 -0
  104. zerochat_runtime/assets/js/ui-conversation.js +655 -0
  105. zerochat_runtime/assets/js/ui-dialogs.js +132 -0
  106. zerochat_runtime/assets/js/ui-generation-status.js +119 -0
  107. zerochat_runtime/assets/js/ui-inspector.js +804 -0
  108. zerochat_runtime/assets/js/ui-mcp.js +604 -0
  109. zerochat_runtime/assets/js/ui-profiles.js +824 -0
  110. zerochat_runtime/assets/js/ui-reasoning.js +146 -0
  111. zerochat_runtime/assets/js/ui-settings.js +825 -0
  112. zerochat_runtime/assets/js/ui-shell.js +215 -0
  113. zerochat_runtime/assets/js/ui-sidebar.js +429 -0
  114. zerochat_runtime/assets/js/ui-telemetry.js +404 -0
  115. zerochat_runtime/assets/js/ui-transfer.js +262 -0
  116. zerochat_runtime/assets/js/utils.js +216 -0
  117. zerochat_runtime/assets/js/vendor/orama.browser.js +11 -0
  118. zerochat_runtime/assets/js/web-browser.js +569 -0
  119. zerochat_runtime/assets/js/web-search.js +519 -0
  120. zerochat_runtime/assets/manifest.webmanifest +20 -0
  121. zerochat_runtime/assets/services/dummy_mcp/dummy_mcp_server.py +47 -0
  122. zerochat_runtime/assets/services/dummy_mcp/service.json +22 -0
  123. zerochat_runtime/assets/services/lsp/installer.json +9 -0
  124. zerochat_runtime/assets/services/lsp/service.json +22 -0
  125. zerochat_runtime/assets/services/memory/installer.json +9 -0
  126. zerochat_runtime/assets/services/memory/service.json +24 -0
  127. zerochat_runtime/assets/services/playwright/installer.json +10 -0
  128. zerochat_runtime/assets/services/playwright/service.json +39 -0
  129. zerochat_runtime/assets/sw.js +164 -0
  130. zerochat_runtime/assets/zerochat.html +671 -0
@@ -0,0 +1,1560 @@
1
+ /**
2
+ * Módulo de Adaptadores y Capacidades de Proveedores LLM (ChatProviders).
3
+ * Separa la lógica específica de cada proveedor (OpenAI, Claude, Gemini, Ollama, OpenRouter, Custom)
4
+ * de la capa común de transporte HTTP/SSE mediante un sistema declarativo de capacidades (Capabilities).
5
+ * Compatible con file://, http:// y Node.js.
6
+ */
7
+
8
+ (function (root, factory) {
9
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
10
+ module.exports = factory();
11
+ } else {
12
+ root.ChatProviders = factory();
13
+ }
14
+ })(typeof self !== 'undefined' ? self : this, function () {
15
+ 'use strict';
16
+
17
+ /**
18
+ * Esquema estándar de capacidades soportadas por adaptadores de modelos LLM.
19
+ */
20
+ const DEFAULT_CAPABILITIES = {
21
+ streaming: true, // Soporte para streaming de respuestas vía SSE
22
+ vision: true, // Soporte para procesamiento de imágenes multimodales
23
+ tools: true, // Soporte para Function / Tool Calling
24
+ reasoning: true, // Soporte para control de razonamiento (thinking / reasoning_effort)
25
+ jsonMode: true, // Soporte para structured outputs / response_format: { type: "json_object" }
26
+ promptCaching: true, // Soporte para Context / Prompt Caching efímero o persistente
27
+ embeddings: true, // Soporte para endpoints de generación de embeddings
28
+ modelListing: true // Soporte para descubrimiento automático de modelos (/models, /api/tags)
29
+ };
30
+
31
+ function serializeContent(content) {
32
+ if (typeof content === 'string') return content;
33
+ if (content === undefined) return '';
34
+ if (typeof content === 'object') return JSON.stringify(content);
35
+ return String(content);
36
+ }
37
+
38
+ /**
39
+ * Adaptador Base genérico (compatible con OpenAI, LM Studio, vLLM, LocalAI, DeepSeek).
40
+ */
41
+ class BaseProviderAdapter {
42
+ constructor(options = {}) {
43
+ this.id = options.id || 'openai';
44
+ this.label = options.label || 'OpenAI / LM Studio';
45
+ this.description = options.description || 'Estándar OpenAI / LM Studio (reasoning_effort: none, low, medium, high, xhigh)';
46
+ this.reasoningLevels = options.reasoningLevels || ['none', 'low', 'medium', 'high', 'xhigh'];
47
+ this.connection = {
48
+ endpoint: options.connection?.endpoint || 'http://localhost:1234/v1',
49
+ knownEndpoints: Array.isArray(options.connection?.knownEndpoints) ? [...options.connection.knownEndpoints] : [],
50
+ endpointReadOnly: options.connection?.endpointReadOnly === true,
51
+ credentials: options.connection?.credentials !== false,
52
+ localModelManagement: options.connection?.localModelManagement === true
53
+ };
54
+ this.capabilities = {
55
+ ...DEFAULT_CAPABILITIES,
56
+ ...(options.capabilities || {})
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Obtiene el conjunto de capacidades del adaptador, permitiendo ajustes según el modelo.
62
+ */
63
+ getCapabilities(model) {
64
+ return { ...this.capabilities };
65
+ }
66
+
67
+ getConnectionConfig() {
68
+ return { ...this.connection, knownEndpoints: [...this.connection.knownEndpoints] };
69
+ }
70
+
71
+ async deactivate() {}
72
+
73
+ /**
74
+ * Normaliza la URL base al endpoint de chat del proveedor.
75
+ */
76
+ normalizeEndpoint(rawUrl) {
77
+ let url = (rawUrl || 'http://localhost:1234/v1').trim();
78
+ if (url.endsWith('/')) url = url.slice(0, -1);
79
+ if (url.endsWith('/chat/completions')) return url;
80
+ if (url.endsWith('/v1')) return `${url}/chat/completions`;
81
+ return `${url}/v1/chat/completions`;
82
+ }
83
+
84
+ /**
85
+ * Construye las cabeceras HTTP necesarias para la petición.
86
+ */
87
+ buildHeaders(apiKey) {
88
+ const headers = { 'Content-Type': 'application/json' };
89
+ if (apiKey && apiKey.trim() !== '') {
90
+ headers['Authorization'] = `Bearer ${apiKey.trim()}`;
91
+ }
92
+ return headers;
93
+ }
94
+
95
+ /**
96
+ * Adapta y formatea la lista de mensajes (filtrando imágenes si vision está deshabilitado).
97
+ */
98
+ formatMessages(messages, capabilities) {
99
+ const caps = capabilities || this.getCapabilities();
100
+ if (caps.vision) {
101
+ return messages;
102
+ }
103
+ // Si el proveedor no soporta visión, degradar imágenes a texto plano
104
+ return messages.map(m => {
105
+ if (Array.isArray(m.content)) {
106
+ const textOnly = m.content
107
+ .filter(part => part.type === 'text')
108
+ .map(part => part.text)
109
+ .join('\n');
110
+ return { ...m, content: textOnly };
111
+ }
112
+ return m;
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Aplica la configuración de razonamiento / pensamiento al payload.
118
+ */
119
+ applyReasoning(payload, effortLevel) {
120
+ let effort = String(effortLevel || 'none').toLowerCase().trim();
121
+ if (effort === 'off') effort = 'none';
122
+ payload.reasoning_effort = effort;
123
+ }
124
+
125
+ supportsUsageStatistics() {
126
+ const id = String(this.id || '').toLowerCase();
127
+ return id !== 'ollama' && id !== 'gemini' && id !== 'claude';
128
+ }
129
+
130
+ /**
131
+ * Aplica la solicitud de métricas detalladas de tokens si se trata de streaming.
132
+ */
133
+ requestUsageStatistics(payload, stream = true) {
134
+ if (stream !== false && this.supportsUsageStatistics()) {
135
+ payload.stream_options = { include_usage: true };
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Aplica opciones específicas de caché de contexto (Prompt / KV Caching).
141
+ * En OpenAI/v1 compatible, la caché es gestionada automáticamente por el servidor según el prefijo,
142
+ * y las métricas se obtienen a través de requestUsageStatistics.
143
+ */
144
+ applyContextCache(payload, options = {}) {
145
+ this.requestUsageStatistics(payload, options.stream !== false);
146
+ }
147
+
148
+ /**
149
+ * Aplica el modo de respuesta estructurada en formato JSON.
150
+ */
151
+ applyJsonMode(payload) {
152
+ payload.response_format = { type: 'json_object' };
153
+ }
154
+
155
+ /**
156
+ * Inyecta las definiciones de herramientas agénticas.
157
+ */
158
+ applyTools(payload, toolsList, toolChoice = 'auto') {
159
+ if (toolsList && toolsList.length > 0) {
160
+ payload.tools = toolsList;
161
+ payload.tool_choice = toolChoice;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Construye el payload completo para la petición HTTP de Chat Completion.
167
+ */
168
+ buildPayload(params) {
169
+ const {
170
+ model = '',
171
+ messages = [],
172
+ temperature = 0.7,
173
+ reasoningEffort = 'medium',
174
+ reasoningTransport = 'auto',
175
+ toolsList = [],
176
+ toolChoice = 'auto',
177
+ stream = true,
178
+ jsonMode = false
179
+ } = params;
180
+
181
+ const capabilities = this.getCapabilities(model);
182
+ const formattedMessages = this.formatMessages(messages, capabilities);
183
+
184
+ const payload = {
185
+ model: (model || '').trim(),
186
+ messages: formattedMessages,
187
+ temperature: parseFloat(temperature) || 0.7
188
+ };
189
+
190
+ if (capabilities.streaming && stream !== false) {
191
+ payload.stream = true;
192
+ if (this.supportsUsageStatistics()) {
193
+ this.requestUsageStatistics(payload, true);
194
+ }
195
+ }
196
+
197
+ if (capabilities.reasoning && !['none', 'off', ''].includes(String(reasoningEffort).toLowerCase().trim())) {
198
+ this.applyReasoning(payload, reasoningEffort);
199
+ }
200
+
201
+ if (capabilities.tools && toolsList && toolsList.length > 0) {
202
+ this.applyTools(payload, toolsList, toolChoice);
203
+ }
204
+
205
+ if (capabilities.promptCaching) {
206
+ this.applyContextCache(payload, { toolsList, messages: formattedMessages, stream: stream !== false });
207
+ }
208
+
209
+ if (capabilities.jsonMode && jsonMode) {
210
+ this.applyJsonMode(payload);
211
+ }
212
+
213
+ return payload;
214
+ }
215
+
216
+ /**
217
+ * Parsea un evento SSE (data JSON) para extraer deltas de texto, pensamiento, tools y usage.
218
+ */
219
+ parseStreamChunk(parsed, state) {
220
+ const result = {
221
+ textChunk: '',
222
+ reasoningChunk: '',
223
+ toolCallDeltas: [],
224
+ usage: null,
225
+ cachedTokens: 0,
226
+ cacheCreationTokens: 0
227
+ };
228
+
229
+ const choice = parsed.choices?.[0];
230
+ const delta = choice?.delta;
231
+
232
+ if (delta) {
233
+ // Tokens de razonamiento específicos
234
+ const rChunk = delta.reasoning_content || delta.reasoning || delta.thinking || delta.thought || '';
235
+ if (rChunk) {
236
+ result.reasoningChunk = rChunk;
237
+ }
238
+
239
+ // Contenido textual normal
240
+ const text = delta.content || delta.text || '';
241
+ if (text) {
242
+ result.textChunk = text;
243
+ }
244
+
245
+ // Llamadas a herramientas
246
+ if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
247
+ result.toolCallDeltas = delta.tool_calls;
248
+ }
249
+
250
+ // Thought signature de Gemini / Google
251
+ const sig = delta.thought_signature || delta.extra_content?.google?.thought_signature || choice?.thought_signature || parsed.thought_signature;
252
+ if (sig) {
253
+ result.thoughtSignature = sig;
254
+ }
255
+ }
256
+
257
+ // Usage / Context Caching (OpenAI, Anthropic, Ollama)
258
+ let usage = parsed.usage || parsed.message?.usage;
259
+ if (!usage && (parsed.prompt_eval_count !== undefined || parsed.eval_count !== undefined)) {
260
+ const pEval = parsed.prompt_eval_count ?? 0;
261
+ const cEval = parsed.eval_count ?? 0;
262
+ usage = {
263
+ prompt_tokens: pEval,
264
+ completion_tokens: cEval,
265
+ total_tokens: pEval + cEval
266
+ };
267
+ }
268
+
269
+ if (usage) {
270
+ result.usage = usage;
271
+ if (usage.prompt_tokens_details?.cached_tokens) {
272
+ result.cachedTokens = usage.prompt_tokens_details.cached_tokens;
273
+ }
274
+ if (usage.cache_read_input_tokens) {
275
+ result.cachedTokens = usage.cache_read_input_tokens;
276
+ }
277
+ if (usage.cache_creation_input_tokens) {
278
+ result.cacheCreationTokens = usage.cache_creation_input_tokens;
279
+ }
280
+ }
281
+
282
+ return result;
283
+ }
284
+
285
+ /**
286
+ * Evalúa si un error HTTP 400 permite auto-recuperación (reintento sin params rechazados).
287
+ */
288
+ handleHttpError(status, serverErrorMsg, payload) {
289
+ const errLower = (serverErrorMsg || '').toLowerCase();
290
+
291
+ if (status === 400 && (payload.reasoning_effort || payload.thinking || payload.reasoning)) {
292
+ if (errLower.includes('reasoning') || errLower.includes('thinking') || errLower.includes('unexpected') || errLower.includes('unrecognized') || errLower.includes('extra') || errLower.includes('unknown')) {
293
+ delete payload.reasoning_effort;
294
+ delete payload.thinking;
295
+ delete payload.reasoning;
296
+ return { retry: true, reason: 'reasoning_rejected' };
297
+ }
298
+ }
299
+
300
+ if (status === 400 && payload.tools) {
301
+ if (errLower.includes('tool') || errLower.includes('function') || errLower.includes('unexpected') || errLower.includes('unrecognized')) {
302
+ delete payload.tools;
303
+ delete payload.tool_choice;
304
+ return { retry: true, reason: 'tools_rejected' };
305
+ }
306
+ }
307
+
308
+ return { retry: false };
309
+ }
310
+
311
+ /**
312
+ * Devuelve endpoints candidatos para consultar modelos disponibles.
313
+ */
314
+ getModelEndpoints(cleanUrl) {
315
+ const v1Url = cleanUrl.endsWith('/v1') ? cleanUrl : `${cleanUrl}/v1`;
316
+ const baseWithoutV1 = cleanUrl.replace(/\/v1$/, '');
317
+ // LM Studio exposes the active context length only through /api/v0/models.
318
+ return [`${baseWithoutV1}/api/v0/models`, `${v1Url}/models`];
319
+ }
320
+
321
+ /**
322
+ * Parsea la respuesta del endpoint de modelos.
323
+ */
324
+ parseModelsResponse(data) {
325
+ if (data && Array.isArray(data.data)) {
326
+ return data.data.map(item => {
327
+ if (typeof item === 'string') return { id: item, name: item };
328
+ return {
329
+ id: item.id || item.name || '',
330
+ name: item.id || item.name || '',
331
+ owned_by: item.owned_by,
332
+ details: item
333
+ };
334
+ }).filter(m => !!m.id);
335
+ }
336
+ if (Array.isArray(data)) {
337
+ return data.map(item => {
338
+ if (typeof item === 'string') return { id: item, name: item };
339
+ return {
340
+ id: item.id || item.name || '',
341
+ name: item.id || item.name || '',
342
+ details: item
343
+ };
344
+ }).filter(m => !!m.id);
345
+ }
346
+ return [];
347
+ }
348
+
349
+ /**
350
+ * Devuelve metadatos para la configuración de razonamiento.
351
+ */
352
+ getReasoningConfig() {
353
+ return {
354
+ type: this.id,
355
+ label: this.label,
356
+ levels: this.reasoningLevels,
357
+ transportOptions: this.capabilities.reasoning ? [] : ['omit', 'send-none'],
358
+ description: this.description
359
+ };
360
+ }
361
+
362
+ /**
363
+ * Inspecciona y diagnostica el endpoint del proveedor para determinar sus capacidades reales.
364
+ * Distingue entre:
365
+ * - 'declared': Declarada por el ProviderAdapter.
366
+ * - 'inferred': Inferida por heurística de nombres de modelos / endpoints.
367
+ * - 'confirmed': Comprobada mediante una prueba HTTP/SSE activa ultra-ligera (max_tokens: 1).
368
+ * - 'unsupported': Rechazada explícitamente o no soportada.
369
+ * - 'unknown': No determinada.
370
+ */
371
+ async inspect(params = {}) {
372
+ const {
373
+ apiUrl = '',
374
+ apiKey = '',
375
+ model = '',
376
+ runProbes = true,
377
+ timeoutMs = 6000
378
+ } = params;
379
+
380
+ const startTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
381
+ const normalizedEndpoint = this.normalizeEndpoint(apiUrl);
382
+ let cleanBase = (apiUrl || '').trim().replace(/\/+$/, '');
383
+ if (cleanBase.endsWith('/chat/completions')) cleanBase = cleanBase.replace(/\/chat\/completions$/, '');
384
+
385
+ const adapterCaps = this.getCapabilities(model);
386
+
387
+ // 1. Inicializar matriz de capacidades con el estado 'declared' o 'unsupported' según el adaptador
388
+ const capabilities = {
389
+ streaming: {
390
+ status: adapterCaps.streaming ? 'declared' : 'unsupported',
391
+ detail: adapterCaps.streaming ? 'Declarada en el adaptador' : 'No soportada según adaptador',
392
+ source: 'adapter'
393
+ },
394
+ tools: {
395
+ status: adapterCaps.tools ? 'declared' : 'unsupported',
396
+ detail: adapterCaps.tools ? 'Declarada en el adaptador' : 'No soportada según adaptador',
397
+ source: 'adapter'
398
+ },
399
+ vision: {
400
+ status: adapterCaps.vision ? 'declared' : 'unsupported',
401
+ detail: adapterCaps.vision ? 'Declarada en el adaptador' : 'No soportada según adaptador',
402
+ source: 'adapter'
403
+ },
404
+ reasoning: {
405
+ status: adapterCaps.reasoning ? 'declared' : 'unsupported',
406
+ detail: adapterCaps.reasoning ? 'Declarada en el adaptador' : 'No soportada según adaptador',
407
+ source: 'adapter'
408
+ },
409
+ jsonMode: {
410
+ status: adapterCaps.jsonMode ? 'declared' : 'unsupported',
411
+ detail: adapterCaps.jsonMode ? 'Declarada en el adaptador' : 'No soportada según adaptador',
412
+ source: 'adapter'
413
+ },
414
+ promptCaching: {
415
+ status: adapterCaps.promptCaching ? 'declared' : 'unknown',
416
+ detail: adapterCaps.promptCaching ? 'Declarada en el adaptador' : 'Soporte no determinado',
417
+ source: 'adapter'
418
+ },
419
+ embeddings: {
420
+ status: adapterCaps.embeddings ? 'declared' : 'unknown',
421
+ detail: adapterCaps.embeddings ? 'Declarada en el adaptador' : 'Soporte no determinado',
422
+ source: 'adapter'
423
+ },
424
+ modelListing: {
425
+ status: adapterCaps.modelListing ? 'declared' : 'unsupported',
426
+ detail: adapterCaps.modelListing ? 'Declarada en el adaptador' : 'No soportada según adaptador',
427
+ source: 'adapter'
428
+ }
429
+ };
430
+
431
+ let discoveredModels = [];
432
+ let modelListSuccess = false;
433
+ let connectionAttempted = false;
434
+ let connectionSuccess = false;
435
+ let authError = null;
436
+ let lastNetworkError = null;
437
+ let lastHttpStatus = null;
438
+
439
+ // 2. Comprobar listado de modelos y realizar inferencias
440
+ const modelEndpoints = this.getModelEndpoints(cleanBase);
441
+ const headers = this.buildHeaders(apiKey);
442
+
443
+ if (runProbes && typeof fetch === 'function') {
444
+ for (const endpoint of modelEndpoints) {
445
+ connectionAttempted = true;
446
+ let timer = null;
447
+ try {
448
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
449
+ if (controller) {
450
+ timer = setTimeout(() => controller.abort(), timeoutMs);
451
+ if (typeof timer?.unref === 'function') timer.unref();
452
+ }
453
+ const res = await fetch(endpoint, {
454
+ method: 'GET',
455
+ headers: { Accept: 'application/json', ...headers },
456
+ signal: controller ? controller.signal : undefined
457
+ });
458
+
459
+ lastHttpStatus = res.status;
460
+ if (res.ok) {
461
+ connectionSuccess = true;
462
+ const data = await res.json();
463
+ const parsed = this.parseModelsResponse(data);
464
+ if (Array.isArray(parsed) && parsed.length > 0) {
465
+ discoveredModels = parsed;
466
+ modelListSuccess = true;
467
+ capabilities.modelListing = {
468
+ status: 'confirmed',
469
+ detail: `${parsed.length} modelo(s) descubierto(s) en ${endpoint}`,
470
+ source: 'probe'
471
+ };
472
+ break;
473
+ }
474
+ } else if (res.status === 401 || res.status === 403) {
475
+ authError = `Error de autenticación (HTTP ${res.status}): Clave de API no válida o acceso denegado.`;
476
+ }
477
+ } catch (e) {
478
+ lastNetworkError = e;
479
+ } finally {
480
+ if (timer) clearTimeout(timer);
481
+ }
482
+ }
483
+ }
484
+
485
+ if (!modelListSuccess && adapterCaps.modelListing) {
486
+ capabilities.modelListing = {
487
+ status: 'unknown',
488
+ detail: 'No se pudo consultar el listado de modelos en los endpoints estándar',
489
+ source: 'probe'
490
+ };
491
+ }
492
+
493
+ // Inferencias por nombre de modelo seleccionado o lista de modelos descubiertos
494
+ const targetModel = (model || (discoveredModels[0] && discoveredModels[0].id) || '').toLowerCase();
495
+ const allModelNames = discoveredModels.map(m => (m.id || m.name || '').toLowerCase()).join(' ');
496
+ const combinedModelContext = (targetModel + ' ' + allModelNames).trim();
497
+
498
+ if (combinedModelContext) {
499
+ // Inferencia de Visión
500
+ if (/(?:-vl|-vision|vision|4o|sonnet|opus|flash|pixtral|llava|cogvlm|gemini|qwen.*vl)/i.test(combinedModelContext)) {
501
+ capabilities.vision = {
502
+ status: 'inferred',
503
+ detail: 'Inferida por identificador multimodal detectado en los modelos',
504
+ source: 'model_name'
505
+ };
506
+ }
507
+
508
+ // Inferencia de Razonamiento
509
+ if (/(?:-r1|r1|qwq|o1|o3|reasoning|thinking|gemma-4|deepseek)/i.test(combinedModelContext)) {
510
+ capabilities.reasoning = {
511
+ status: 'inferred',
512
+ detail: 'Inferida por identificador de modelo con razonamiento/pensamiento',
513
+ source: 'model_name'
514
+ };
515
+ }
516
+
517
+ // Inferencia de Embeddings
518
+ if (/(?:embedding|embed|bge|nomic|e5|text-embedding)/i.test(combinedModelContext)) {
519
+ capabilities.embeddings = {
520
+ status: 'inferred',
521
+ detail: 'Inferida por modelos de embeddings presentes en el catálogo',
522
+ source: 'model_name'
523
+ };
524
+ }
525
+ }
526
+
527
+ // 3. Pruebas activas controladas (Micro-sondas seguras con max_tokens: 1)
528
+ const probeModel = model || (discoveredModels[0] && discoveredModels[0].id) || '';
529
+ let probesRun = false;
530
+
531
+ if (runProbes && typeof fetch === 'function') {
532
+ probesRun = true;
533
+
534
+ // Micro-sonda A: Streaming & Chat básico
535
+ connectionAttempted = true;
536
+ let timerA = null;
537
+ try {
538
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
539
+ if (controller) {
540
+ timerA = setTimeout(() => controller.abort(), timeoutMs);
541
+ if (typeof timerA?.unref === 'function') timerA.unref();
542
+ }
543
+
544
+ const probePayload = this.buildPayload({
545
+ model: probeModel,
546
+ messages: [{ role: 'user', content: 'hi' }],
547
+ stream: true,
548
+ temperature: 0.1,
549
+ reasoningEffort: 'none'
550
+ });
551
+ probePayload.max_tokens = 1;
552
+
553
+ const res = await fetch(normalizedEndpoint, {
554
+ method: 'POST',
555
+ headers: { 'Content-Type': 'application/json', ...headers },
556
+ body: JSON.stringify(probePayload),
557
+ signal: controller ? controller.signal : undefined
558
+ });
559
+
560
+ lastHttpStatus = res.status;
561
+ if (res.ok) {
562
+ connectionSuccess = true;
563
+ const contentType = res.headers && res.headers.get ? (res.headers.get('content-type') || '') : '';
564
+ if (contentType.includes('event-stream') || res.body) {
565
+ capabilities.streaming = {
566
+ status: 'confirmed',
567
+ detail: 'Respuesta de streaming SSE (HTTP 200) verificada exitosamente',
568
+ source: 'probe'
569
+ };
570
+ }
571
+ } else if (res.status === 401 || res.status === 403) {
572
+ authError = `Error de autenticación (HTTP ${res.status}): Clave de API no válida o acceso denegado.`;
573
+ } else if (res.status === 400 || res.status === 422) {
574
+ connectionSuccess = true;
575
+ const errText = await res.text().catch(() => '');
576
+ if (errText.toLowerCase().includes('stream')) {
577
+ capabilities.streaming = {
578
+ status: 'unsupported',
579
+ detail: `Servidor rechazó streaming: ${errText.substring(0, 80)}`,
580
+ source: 'probe'
581
+ };
582
+ }
583
+ }
584
+ } catch (probeErr) {
585
+ lastNetworkError = probeErr;
586
+ } finally {
587
+ if (timerA) clearTimeout(timerA);
588
+ }
589
+
590
+ // Micro-sonda B: Tools / Function Calling
591
+ connectionAttempted = true;
592
+ let timerB = null;
593
+ try {
594
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
595
+ if (controller) {
596
+ timerB = setTimeout(() => controller.abort(), timeoutMs);
597
+ if (typeof timerB?.unref === 'function') timerB.unref();
598
+ }
599
+
600
+ const toolPayload = this.buildPayload({
601
+ model: probeModel,
602
+ messages: [{ role: 'user', content: 'hi' }],
603
+ stream: false,
604
+ toolsList: [{
605
+ type: 'function',
606
+ function: {
607
+ name: 'ping_test',
608
+ description: 'Inspector ping test',
609
+ parameters: { type: 'object', properties: {} }
610
+ }
611
+ }]
612
+ });
613
+ toolPayload.max_tokens = 1;
614
+ toolPayload.tool_choice = 'none';
615
+
616
+ const res = await fetch(normalizedEndpoint, {
617
+ method: 'POST',
618
+ headers: { 'Content-Type': 'application/json', ...headers },
619
+ body: JSON.stringify(toolPayload),
620
+ signal: controller ? controller.signal : undefined
621
+ });
622
+
623
+ lastHttpStatus = res.status;
624
+ if (res.ok) {
625
+ connectionSuccess = true;
626
+ capabilities.tools = {
627
+ status: 'confirmed',
628
+ detail: 'El servidor aceptó el esquema de tools/functions (HTTP 200)',
629
+ source: 'probe'
630
+ };
631
+ } else if (res.status === 401 || res.status === 403) {
632
+ authError = `Error de autenticación (HTTP ${res.status}): Clave de API no válida o acceso denegado.`;
633
+ } else if (res.status === 400 || res.status === 422) {
634
+ connectionSuccess = true;
635
+ const errText = await res.text().catch(() => '');
636
+ if (errText.toLowerCase().includes('tool') || errText.toLowerCase().includes('function') || errText.toLowerCase().includes('schema')) {
637
+ capabilities.tools = {
638
+ status: 'unsupported',
639
+ detail: `Servidor rechazó tools: ${errText.substring(0, 80)}`,
640
+ source: 'probe'
641
+ };
642
+ }
643
+ }
644
+ } catch (e) {
645
+ lastNetworkError = e;
646
+ } finally {
647
+ if (timerB) clearTimeout(timerB);
648
+ }
649
+
650
+ // Micro-sonda C: JSON Mode
651
+ connectionAttempted = true;
652
+ let timerC = null;
653
+ try {
654
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
655
+ if (controller) {
656
+ timerC = setTimeout(() => controller.abort(), timeoutMs);
657
+ if (typeof timerC?.unref === 'function') timerC.unref();
658
+ }
659
+
660
+ const jsonPayload = this.buildPayload({
661
+ model: probeModel,
662
+ messages: [{ role: 'user', content: 'hi' }],
663
+ stream: false,
664
+ jsonMode: true
665
+ });
666
+ jsonPayload.max_tokens = 1;
667
+
668
+ const res = await fetch(normalizedEndpoint, {
669
+ method: 'POST',
670
+ headers: { 'Content-Type': 'application/json', ...headers },
671
+ body: JSON.stringify(jsonPayload),
672
+ signal: controller ? controller.signal : undefined
673
+ });
674
+
675
+ lastHttpStatus = res.status;
676
+ if (res.ok) {
677
+ connectionSuccess = true;
678
+ capabilities.jsonMode = {
679
+ status: 'confirmed',
680
+ detail: 'El servidor aceptó response_format: json_object (HTTP 200)',
681
+ source: 'probe'
682
+ };
683
+ } else if (res.status === 401 || res.status === 403) {
684
+ authError = `Error de autenticación (HTTP ${res.status}): Clave de API no válida o acceso denegado.`;
685
+ } else if (res.status === 400 || res.status === 422) {
686
+ connectionSuccess = true;
687
+ const errText = await res.text().catch(() => '');
688
+ if (errText.toLowerCase().includes('response_format') || errText.toLowerCase().includes('json')) {
689
+ capabilities.jsonMode = {
690
+ status: 'unsupported',
691
+ detail: `Servidor rechazó json_object: ${errText.substring(0, 80)}`,
692
+ source: 'probe'
693
+ };
694
+ }
695
+ }
696
+ } catch (e) {
697
+ lastNetworkError = e;
698
+ } finally {
699
+ if (timerC) clearTimeout(timerC);
700
+ }
701
+
702
+ // Micro-sonda D: Embeddings endpoint check
703
+ let timerD = null;
704
+ try {
705
+ const embUrl = cleanBase.endsWith('/v1') ? `${cleanBase}/embeddings` : `${cleanBase}/v1/embeddings`;
706
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
707
+ if (controller) {
708
+ timerD = setTimeout(() => controller.abort(), timeoutMs);
709
+ if (typeof timerD?.unref === 'function') timerD.unref();
710
+ }
711
+
712
+ const res = await fetch(embUrl, {
713
+ method: 'POST',
714
+ headers: { 'Content-Type': 'application/json', ...headers },
715
+ body: JSON.stringify({ input: 'ping', model: probeModel }),
716
+ signal: controller ? controller.signal : undefined
717
+ });
718
+
719
+ if (res.ok) {
720
+ connectionSuccess = true;
721
+ capabilities.embeddings = {
722
+ status: 'confirmed',
723
+ detail: `Endpoint ${embUrl} responde correctamente (HTTP 200)`,
724
+ source: 'probe'
725
+ };
726
+ } else if (res.status === 404) {
727
+ capabilities.embeddings = {
728
+ status: 'unsupported',
729
+ detail: `Endpoint ${embUrl} no encontrado (404)`,
730
+ source: 'probe'
731
+ };
732
+ } else if (res.status === 401 || res.status === 403) {
733
+ authError = `Error de autenticación (HTTP ${res.status}): Clave de API no válida o acceso denegado.`;
734
+ } else if (res.status === 400 || res.status === 422) {
735
+ connectionSuccess = true;
736
+ }
737
+ } catch (e) {} finally {
738
+ if (timerD) clearTimeout(timerD);
739
+ }
740
+ }
741
+
742
+ const endTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
743
+
744
+ // Si se intentó conectar mediante sondas y ninguna tuvo éxito, informar fallo de conexión
745
+ if (runProbes && connectionAttempted && !connectionSuccess) {
746
+ let errorMsg;
747
+ if (authError) {
748
+ errorMsg = authError;
749
+ } else if (lastNetworkError) {
750
+ const netMsg = lastNetworkError.message || String(lastNetworkError);
751
+ errorMsg = `Error de conexión: No se pudo conectar con el servidor en ${normalizedEndpoint} (${netMsg}). Verifica que el servidor esté activo y la URL sea correcta.`;
752
+ } else if (lastHttpStatus) {
753
+ errorMsg = `Error de conexión: El servidor respondió con error HTTP ${lastHttpStatus} en ${normalizedEndpoint}. Verifica el endpoint y la configuración.`;
754
+ } else {
755
+ errorMsg = `Error de conexión: No se pudo establecer comunicación con el servidor en ${normalizedEndpoint}.`;
756
+ }
757
+
758
+ Object.keys(capabilities).forEach(k => {
759
+ capabilities[k] = {
760
+ status: 'unknown',
761
+ detail: 'No comprobada: fallo de conexión con el servidor',
762
+ source: 'probe'
763
+ };
764
+ });
765
+
766
+ return {
767
+ success: false,
768
+ connected: false,
769
+ error: errorMsg,
770
+ provider: {
771
+ id: this.id,
772
+ label: this.label,
773
+ description: this.description
774
+ },
775
+ endpoint: {
776
+ raw: apiUrl,
777
+ normalized: normalizedEndpoint,
778
+ base: cleanBase
779
+ },
780
+ model: {
781
+ selected: probeModel,
782
+ totalDiscovered: 0,
783
+ discovered: []
784
+ },
785
+ capabilities: capabilities,
786
+ probesRun: true,
787
+ inspectionTimeMs: Math.round(endTime - startTime)
788
+ };
789
+ }
790
+
791
+ // Devolver resultado estructurado de la inspección SIN apiKey
792
+ return {
793
+ success: true,
794
+ connected: runProbes ? true : null,
795
+ provider: {
796
+ id: this.id,
797
+ label: this.label,
798
+ description: this.description
799
+ },
800
+ endpoint: {
801
+ raw: apiUrl,
802
+ normalized: normalizedEndpoint,
803
+ base: cleanBase
804
+ },
805
+ model: {
806
+ selected: probeModel,
807
+ totalDiscovered: discoveredModels.length,
808
+ discovered: discoveredModels
809
+ },
810
+ capabilities: capabilities,
811
+ probesRun: probesRun,
812
+ inspectionTimeMs: Math.round(endTime - startTime)
813
+ };
814
+ }
815
+ }
816
+
817
+ /**
818
+ * Adaptador para Anthropic Claude (/v1/messages).
819
+ */
820
+ class ClaudeProviderAdapter extends BaseProviderAdapter {
821
+ constructor() {
822
+ super({
823
+ id: 'claude',
824
+ label: 'Anthropic Claude',
825
+ connection: { endpoint: 'https://api.anthropic.com/v1' },
826
+ description: 'Estándar Claude (thinking budget: disabled, 1k, 2k, 4k, 8k tokens)',
827
+ reasoningLevels: ['none', 'low', 'medium', 'high', 'xhigh'],
828
+ capabilities: {
829
+ streaming: true,
830
+ vision: true,
831
+ tools: true,
832
+ reasoning: true,
833
+ jsonMode: false,
834
+ promptCaching: true,
835
+ embeddings: false,
836
+ modelListing: true
837
+ }
838
+ });
839
+ }
840
+
841
+ normalizeEndpoint(rawUrl) {
842
+ let url = (rawUrl || 'https://api.anthropic.com/v1').trim();
843
+ if (url.endsWith('/')) url = url.slice(0, -1);
844
+ if (url.endsWith('/v1/messages') || url.endsWith('/messages') || url.endsWith('/chat/completions')) return url;
845
+ if (url.endsWith('/v1')) return `${url}/messages`;
846
+ return `${url}/v1/messages`;
847
+ }
848
+
849
+ /**
850
+ * Construye las cabeceras HTTP necesarias para Anthropic directo o proxies.
851
+ */
852
+ buildHeaders(apiKey) {
853
+ const headers = super.buildHeaders(apiKey);
854
+ // Cabecera obligatoria de versión para API directa de Anthropic (https://api.anthropic.com/v1)
855
+ headers['anthropic-version'] = '2023-06-01';
856
+ if (apiKey && apiKey.trim() !== '') {
857
+ headers['x-api-key'] = apiKey.trim();
858
+ }
859
+ return headers;
860
+ }
861
+
862
+ formatMessages(messages, capabilities) {
863
+ const caps = capabilities || this.getCapabilities();
864
+ return messages.map(m => {
865
+ if (Array.isArray(m.content)) {
866
+ const claudeParts = m.content.map(part => {
867
+ if (caps.vision && part.type === 'image_url' && part.image_url && part.image_url.url) {
868
+ const match = part.image_url.url.match(/^data:([^;]+);base64,(.+)$/);
869
+ if (match) {
870
+ return {
871
+ type: 'image',
872
+ source: {
873
+ type: 'base64',
874
+ media_type: match[1],
875
+ data: match[2]
876
+ }
877
+ };
878
+ }
879
+ }
880
+ return part;
881
+ }).filter(part => caps.vision || part.type !== 'image');
882
+ return { ...m, content: claudeParts };
883
+ }
884
+ return m;
885
+ });
886
+ }
887
+
888
+ buildPayload(params) {
889
+ const {
890
+ model = '',
891
+ messages = [],
892
+ temperature = 0.7,
893
+ reasoningEffort = 'medium',
894
+ toolsList = [],
895
+ toolChoice = 'auto',
896
+ stream = true
897
+ } = params;
898
+
899
+ const capabilities = this.getCapabilities(model);
900
+ const formattedMessages = this.formatMessages(messages, capabilities);
901
+
902
+ // Separar system prompt (requerido a nivel raíz en Claude Messages API)
903
+ const systemMessages = formattedMessages.filter(m => m.role === 'system');
904
+ const nonSystemMessages = formattedMessages.filter(m => m.role !== 'system');
905
+
906
+ let systemContent = '';
907
+ if (systemMessages.length > 0) {
908
+ systemContent = systemMessages.map(m => typeof m.content === 'string' ? m.content : JSON.stringify(m.content)).join('\n\n');
909
+ }
910
+
911
+ const payload = {
912
+ model: (model || '').trim(),
913
+ messages: nonSystemMessages,
914
+ temperature: parseFloat(temperature) || 0.7,
915
+ max_tokens: 4096
916
+ };
917
+
918
+ if (systemContent) {
919
+ if (capabilities.promptCaching) {
920
+ payload.system = [{ type: 'text', text: systemContent, cache_control: { type: 'ephemeral' } }];
921
+ } else {
922
+ payload.system = systemContent;
923
+ }
924
+ }
925
+
926
+ if (capabilities.streaming && stream !== false) {
927
+ payload.stream = true;
928
+ }
929
+
930
+ if (capabilities.reasoning && !['none', 'off', ''].includes(String(reasoningEffort).toLowerCase().trim())) {
931
+ this.applyReasoning(payload, reasoningEffort);
932
+ }
933
+
934
+ if (capabilities.tools && toolsList && toolsList.length > 0) {
935
+ payload.tools = toolsList.map(t => {
936
+ if (t.type === 'function' && t.function) {
937
+ return {
938
+ name: t.function.name,
939
+ description: t.function.description || '',
940
+ input_schema: t.function.parameters || { type: 'object', properties: {} }
941
+ };
942
+ }
943
+ return t;
944
+ });
945
+ if (capabilities.promptCaching && payload.tools.length > 0) {
946
+ payload.tools[payload.tools.length - 1].cache_control = { type: 'ephemeral' };
947
+ }
948
+ }
949
+
950
+ if (capabilities.promptCaching) {
951
+ this.applyContextCache(payload, { toolsList, messages: nonSystemMessages });
952
+ }
953
+
954
+ return payload;
955
+ }
956
+
957
+ applyReasoning(payload, effortLevel) {
958
+ let effort = String(effortLevel || 'none').toLowerCase().trim();
959
+ if (effort === 'off') effort = 'none';
960
+
961
+ if (effort !== 'none') {
962
+ let budget = 2048;
963
+ if (effort === 'low' || effort === 'minimal') budget = 1024;
964
+ else if (effort === 'medium') budget = 2048;
965
+ else if (effort === 'high') budget = 4096;
966
+ else if (effort === 'xhigh') budget = 8192;
967
+
968
+ payload.thinking = {
969
+ type: 'enabled',
970
+ budget_tokens: budget
971
+ };
972
+ payload.temperature = 1.0;
973
+ payload.max_tokens = Math.max(4096, budget + 1024);
974
+ } else {
975
+ payload.thinking = { type: 'disabled' };
976
+ }
977
+ }
978
+
979
+ applyContextCache(payload, options = {}) {
980
+ const { toolsList = [], messages = [] } = options;
981
+
982
+ if (toolsList.length > 0 && !toolsList[toolsList.length - 1].cache_control) {
983
+ toolsList[toolsList.length - 1].cache_control = { type: 'ephemeral' };
984
+ }
985
+
986
+ // Localizar el último turno procesable (user o tool) para anclar el punto dinámico de caché
987
+ let lastProcessableIdx = -1;
988
+ for (let i = messages.length - 1; i >= 0; i--) {
989
+ if (messages[i].role === 'user' || messages[i].role === 'tool') {
990
+ lastProcessableIdx = i;
991
+ break;
992
+ }
993
+ }
994
+
995
+ payload.messages = messages.map((m, idx) => {
996
+ if (m.role === 'system') {
997
+ if (typeof m.content === 'string') {
998
+ return {
999
+ ...m,
1000
+ content: [{ type: 'text', text: m.content, cache_control: { type: 'ephemeral' } }]
1001
+ };
1002
+ } else if (Array.isArray(m.content) && m.content.length > 0) {
1003
+ const updated = [...m.content];
1004
+ updated[updated.length - 1] = {
1005
+ ...updated[updated.length - 1],
1006
+ cache_control: { type: 'ephemeral' }
1007
+ };
1008
+ return { ...m, content: updated };
1009
+ }
1010
+ }
1011
+
1012
+ if (idx === lastProcessableIdx) {
1013
+ if (typeof m.content === 'string') {
1014
+ return {
1015
+ ...m,
1016
+ content: [{ type: 'text', text: m.content, cache_control: { type: 'ephemeral' } }]
1017
+ };
1018
+ } else if (Array.isArray(m.content) && m.content.length > 0) {
1019
+ const updated = [...m.content];
1020
+ updated[updated.length - 1] = {
1021
+ ...updated[updated.length - 1],
1022
+ cache_control: { type: 'ephemeral' }
1023
+ };
1024
+ return { ...m, content: updated };
1025
+ }
1026
+ }
1027
+
1028
+ return m;
1029
+ });
1030
+ }
1031
+
1032
+ parseStreamChunk(parsed, state) {
1033
+ const result = {
1034
+ textChunk: '',
1035
+ reasoningChunk: '',
1036
+ toolCallDeltas: [],
1037
+ usage: null,
1038
+ cachedTokens: 0,
1039
+ cacheCreationTokens: 0
1040
+ };
1041
+
1042
+ if (parsed.type === 'message_start' && parsed.message?.usage) {
1043
+ result.usage = parsed.message.usage;
1044
+ if (parsed.message.usage.cache_read_input_tokens) {
1045
+ result.cachedTokens = parsed.message.usage.cache_read_input_tokens;
1046
+ }
1047
+ if (parsed.message.usage.cache_creation_input_tokens) {
1048
+ result.cacheCreationTokens = parsed.message.usage.cache_creation_input_tokens;
1049
+ }
1050
+ }
1051
+
1052
+ if (parsed.type === 'message_delta' && parsed.usage) {
1053
+ result.usage = parsed.usage;
1054
+ }
1055
+
1056
+ if (parsed.type === 'content_block_delta') {
1057
+ if (parsed.delta?.type === 'thinking_delta' && parsed.delta?.thinking) {
1058
+ result.reasoningChunk = parsed.delta.thinking;
1059
+ } else if (parsed.delta?.type === 'text_delta' && parsed.delta?.text) {
1060
+ result.textChunk = parsed.delta.text;
1061
+ }
1062
+ }
1063
+
1064
+ // Fallback a formato choices por si OpenRouter sirve Claude en formato OpenAI
1065
+ if (parsed.choices?.[0] || parsed.usage) {
1066
+ const baseRes = super.parseStreamChunk(parsed, state);
1067
+ if (baseRes.textChunk) result.textChunk = baseRes.textChunk;
1068
+ if (baseRes.reasoningChunk) result.reasoningChunk = baseRes.reasoningChunk;
1069
+ if (baseRes.toolCallDeltas.length > 0) result.toolCallDeltas = baseRes.toolCallDeltas;
1070
+ if (baseRes.cachedTokens > 0) result.cachedTokens = baseRes.cachedTokens;
1071
+ if (baseRes.cacheCreationTokens > 0) result.cacheCreationTokens = baseRes.cacheCreationTokens;
1072
+ if (baseRes.usage && !result.usage) result.usage = baseRes.usage;
1073
+ }
1074
+
1075
+ return result;
1076
+ }
1077
+
1078
+ getModelEndpoints(cleanUrl) {
1079
+ const v1Url = cleanUrl.endsWith('/v1') ? cleanUrl : `${cleanUrl}/v1`;
1080
+ return [`${v1Url}/models`];
1081
+ }
1082
+ }
1083
+
1084
+ /**
1085
+ * Adaptador para Google Gemini (Endpoint OpenAI compatible).
1086
+ */
1087
+ class GeminiProviderAdapter extends BaseProviderAdapter {
1088
+ constructor() {
1089
+ super({
1090
+ id: 'gemini',
1091
+ label: 'Google Gemini',
1092
+ connection: { endpoint: 'https://generativelanguage.googleapis.com/v1beta/openai' },
1093
+ description: 'Google Gemini (OpenAI compatible endpoint)',
1094
+ reasoningLevels: ['none'],
1095
+ capabilities: {
1096
+ streaming: true,
1097
+ vision: true,
1098
+ tools: true,
1099
+ reasoning: false,
1100
+ jsonMode: true,
1101
+ promptCaching: true,
1102
+ embeddings: true,
1103
+ modelListing: true
1104
+ }
1105
+ });
1106
+ }
1107
+
1108
+ /**
1109
+ * En el endpoint OpenAI de Gemini (/v1beta/openai), la caché es implícita en servidor.
1110
+ * No se inyecta stream_options para no causar rechazos con stream: false o endpoints estrictos.
1111
+ */
1112
+ applyContextCache(payload, options = {}) {
1113
+ // Gemini maneja context caching automáticamente a nivel de servidor.
1114
+ }
1115
+
1116
+ normalizeEndpoint(rawUrl) {
1117
+ let url = (rawUrl || 'https://generativelanguage.googleapis.com/v1beta/openai').trim();
1118
+ if (url.endsWith('/')) url = url.slice(0, -1);
1119
+ if (url.endsWith('/chat/completions')) return url;
1120
+ if (url.endsWith('/v1')) return `${url}/chat/completions`;
1121
+ return `${url}/chat/completions`;
1122
+ }
1123
+
1124
+ getModelEndpoints(cleanUrl) {
1125
+ const v1Url = cleanUrl.endsWith('/v1') ? cleanUrl : `${cleanUrl}/v1`;
1126
+ return [`${v1Url}/models`, `${cleanUrl}/models`];
1127
+ }
1128
+
1129
+ parseModelsResponse(data) {
1130
+ if (!data) return [];
1131
+ let list = [];
1132
+ if (Array.isArray(data.data)) {
1133
+ list = data.data;
1134
+ } else if (Array.isArray(data.models)) {
1135
+ list = data.models;
1136
+ }
1137
+
1138
+ return list
1139
+ .map(m => {
1140
+ let id = m.id || m.name || '';
1141
+ if (id.startsWith('models/')) id = id.substring(7);
1142
+ return {
1143
+ id: id,
1144
+ name: m.displayName || id,
1145
+ description: m.description || ''
1146
+ };
1147
+ })
1148
+ .filter(m => m.id && (m.id.includes('gemini') || m.id.includes('gemma')));
1149
+ }
1150
+
1151
+ formatMessages(messages, capabilities) {
1152
+ const formatted = [];
1153
+ (messages || []).forEach(m => {
1154
+ if (!m || !m.role) return;
1155
+
1156
+ if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
1157
+ const cleanToolCalls = m.tool_calls.map(tc => {
1158
+ const rawArgs = typeof tc.function?.arguments === 'string'
1159
+ ? tc.function.arguments
1160
+ : JSON.stringify(tc.function?.arguments || {});
1161
+
1162
+ let safeArgs = rawArgs;
1163
+ try {
1164
+ JSON.parse(rawArgs);
1165
+ } catch (e) {
1166
+ const matches = rawArgs.match(/\{[^{}]*"[a-zA-Z0-9_]+"[^{}]*\}/g);
1167
+ if (matches && matches.length > 0) {
1168
+ try {
1169
+ JSON.parse(matches[0]);
1170
+ safeArgs = matches[0];
1171
+ } catch (e2) {
1172
+ safeArgs = '{}';
1173
+ }
1174
+ } else {
1175
+ safeArgs = '{}';
1176
+ }
1177
+ }
1178
+
1179
+ const out = {
1180
+ id: tc.id || `call_${Date.now()}`,
1181
+ type: 'function',
1182
+ function: {
1183
+ name: tc.function?.name || tc.name || '',
1184
+ arguments: safeArgs
1185
+ }
1186
+ };
1187
+
1188
+ const sig = tc.thought_signature || tc.extra_content?.google?.thought_signature;
1189
+ if (sig) {
1190
+ out.extra_content = { google: { thought_signature: sig } };
1191
+ } else if (tc.extra_content) {
1192
+ out.extra_content = tc.extra_content;
1193
+ }
1194
+
1195
+ return out;
1196
+ });
1197
+
1198
+ // Regla Gemini: Un turno assistant con tool_calls NUNCA puede ir inmediatamente después de system
1199
+ const prevMsg = formatted.length > 0 ? formatted[formatted.length - 1] : null;
1200
+ if (!prevMsg || prevMsg.role === 'system') {
1201
+ formatted.push({ role: 'user', content: 'Continue' });
1202
+ }
1203
+
1204
+ formatted.push({
1205
+ role: 'assistant',
1206
+ content: m.content || null,
1207
+ tool_calls: cleanToolCalls
1208
+ });
1209
+ } else if (m.role === 'tool') {
1210
+ const toolCallId = m.tool_call_id || `call_${Date.now()}`;
1211
+ const toolName = m.name || 'tool';
1212
+ const toolContent = serializeContent(m.content);
1213
+
1214
+ // Validar que el mensaje previo sea un assistant con la llamada correspondiente
1215
+ let prevMsg = formatted.length > 0 ? formatted[formatted.length - 1] : null;
1216
+ const hasMatchingToolCall = prevMsg && prevMsg.role === 'assistant' && Array.isArray(prevMsg.tool_calls) &&
1217
+ prevMsg.tool_calls.some(tc => tc.id === toolCallId || (tc.function && tc.function.name === toolName));
1218
+
1219
+ if (!hasMatchingToolCall) {
1220
+ // Si el mensaje anterior a este asistente autogenerado es system, insertar user primero
1221
+ if (!prevMsg || prevMsg.role === 'system') {
1222
+ formatted.push({ role: 'user', content: 'Continue' });
1223
+ }
1224
+ formatted.push({
1225
+ role: 'assistant',
1226
+ content: null,
1227
+ tool_calls: [{
1228
+ id: toolCallId,
1229
+ type: 'function',
1230
+ function: {
1231
+ name: toolName,
1232
+ arguments: '{}'
1233
+ }
1234
+ }]
1235
+ });
1236
+ }
1237
+
1238
+ formatted.push({
1239
+ role: 'tool',
1240
+ tool_call_id: toolCallId,
1241
+ name: toolName,
1242
+ content: toolContent
1243
+ });
1244
+ } else if (m.role === 'assistant') {
1245
+ // Si el mensaje es assistant (texto) y va inmediatamente después de system, insertar user antes
1246
+ const prevMsg = formatted.length > 0 ? formatted[formatted.length - 1] : null;
1247
+ if (!prevMsg || prevMsg.role === 'system') {
1248
+ formatted.push({ role: 'user', content: 'Continue' });
1249
+ }
1250
+ } else if (m.role === 'user') {
1251
+ // Regla Gemini: Un turno 'user' NUNCA puede ir inmediatamente después de un turno 'tool'
1252
+ const prevMsg = formatted.length > 0 ? formatted[formatted.length - 1] : null;
1253
+ if (prevMsg && prevMsg.role === 'tool') {
1254
+ formatted.push({ role: 'assistant', content: 'Tool information received.' });
1255
+ }
1256
+ formatted.push(m);
1257
+ } else {
1258
+ formatted.push(m);
1259
+ }
1260
+ });
1261
+ return formatted;
1262
+ }
1263
+
1264
+ buildPayload(params) {
1265
+ const payload = super.buildPayload(params);
1266
+ // El endpoint OpenAI de Gemini requiere el nombre del modelo sin el prefijo "models/"
1267
+ if (payload.model && payload.model.startsWith('models/')) {
1268
+ payload.model = payload.model.substring(7);
1269
+ }
1270
+ return payload;
1271
+ }
1272
+ }
1273
+
1274
+ /**
1275
+ * Adaptador para Ollama (/api/chat, /api/tags).
1276
+ */
1277
+ class OllamaProviderAdapter extends BaseProviderAdapter {
1278
+ constructor() {
1279
+ super({
1280
+ id: 'ollama',
1281
+ label: 'Ollama',
1282
+ connection: { endpoint: 'http://localhost:11434' },
1283
+ description: 'Estándar Ollama (reasoning_effort: none, low, medium, high, xhigh)',
1284
+ reasoningLevels: ['none', 'low', 'medium', 'high', 'xhigh'],
1285
+ capabilities: {
1286
+ streaming: true,
1287
+ vision: true,
1288
+ tools: true,
1289
+ reasoning: true,
1290
+ jsonMode: true,
1291
+ promptCaching: false,
1292
+ embeddings: true,
1293
+ modelListing: true
1294
+ }
1295
+ });
1296
+ }
1297
+
1298
+ normalizeEndpoint(rawUrl) {
1299
+ let url = (rawUrl || 'http://localhost:11434').trim();
1300
+ if (url.endsWith('/')) url = url.slice(0, -1);
1301
+ if (url.endsWith('/api/chat') || url.endsWith('/chat/completions')) return url;
1302
+ if (url.endsWith('/v1')) return `${url}/chat/completions`;
1303
+ return `${url}/v1/chat/completions`;
1304
+ }
1305
+
1306
+ getModelEndpoints(cleanUrl) {
1307
+ const baseWithoutV1 = cleanUrl.replace(/\/v1$/, '');
1308
+ const v1Url = cleanUrl.endsWith('/v1') ? cleanUrl : `${cleanUrl}/v1`;
1309
+ return [`${baseWithoutV1}/api/tags`, `${v1Url}/models`];
1310
+ }
1311
+
1312
+ parseModelsResponse(data) {
1313
+ if (data && Array.isArray(data.models)) {
1314
+ return data.models.map(item => {
1315
+ if (typeof item === 'string') return { id: item, name: item };
1316
+ return {
1317
+ id: item.name || item.model || item.id || '',
1318
+ name: item.name || item.model || item.id || '',
1319
+ details: item
1320
+ };
1321
+ }).filter(m => !!m.id);
1322
+ }
1323
+ return super.parseModelsResponse(data);
1324
+ }
1325
+ }
1326
+
1327
+ /**
1328
+ * Adaptador para OpenRouter (https://openrouter.ai/api/v1).
1329
+ */
1330
+ class OpenRouterProviderAdapter extends BaseProviderAdapter {
1331
+ constructor() {
1332
+ super({
1333
+ id: 'openrouter',
1334
+ label: 'OpenRouter',
1335
+ connection: { endpoint: 'https://openrouter.ai/api/v1' },
1336
+ description: 'Estándar OpenRouter (reasoning.effort: none, low, medium, high, xhigh)',
1337
+ reasoningLevels: ['none', 'low', 'medium', 'high', 'xhigh'],
1338
+ capabilities: {
1339
+ streaming: true,
1340
+ vision: true,
1341
+ tools: true,
1342
+ reasoning: true,
1343
+ jsonMode: true,
1344
+ promptCaching: true,
1345
+ embeddings: false,
1346
+ modelListing: true
1347
+ }
1348
+ });
1349
+ }
1350
+
1351
+ normalizeEndpoint(rawUrl) {
1352
+ let url = (rawUrl || 'https://openrouter.ai/api/v1').trim();
1353
+ if (url.endsWith('/')) url = url.slice(0, -1);
1354
+ if (url.endsWith('/chat/completions')) return url;
1355
+ if (url.endsWith('/v1')) return `${url}/chat/completions`;
1356
+ return `${url}/v1/chat/completions`;
1357
+ }
1358
+
1359
+ applyReasoning(payload, effortLevel) {
1360
+ let effort = String(effortLevel || 'none').toLowerCase().trim();
1361
+ if (effort === 'off') effort = 'none';
1362
+ payload.reasoning = { effort: effort };
1363
+ payload.reasoning_effort = effort;
1364
+ }
1365
+
1366
+ applyContextCache(payload, options = {}) {
1367
+ super.applyContextCache(payload, options);
1368
+ // Inyectar cache_control con límite estricto de máximo 4 puntos efímeros (política OpenRouter / Anthropic)
1369
+ const { toolsList = [], messages = [] } = options;
1370
+ let usedBreakpoints = 0;
1371
+ const MAX_BREAKPOINTS = 4;
1372
+
1373
+ // 1. Marcar la última herramienta si hay tools (1 punto)
1374
+ if (toolsList.length > 0 && usedBreakpoints < MAX_BREAKPOINTS) {
1375
+ if (!toolsList[toolsList.length - 1].cache_control) {
1376
+ toolsList[toolsList.length - 1].cache_control = { type: 'ephemeral' };
1377
+ }
1378
+ usedBreakpoints++;
1379
+ }
1380
+
1381
+ // 2. Identificar el último system prompt consolidado (1 punto)
1382
+ let lastSystemIdx = -1;
1383
+ for (let i = messages.length - 1; i >= 0; i--) {
1384
+ if (messages[i].role === 'system') {
1385
+ lastSystemIdx = i;
1386
+ break;
1387
+ }
1388
+ }
1389
+
1390
+ // 3. Identificar el último turno procesable (user o tool) (1 punto)
1391
+ let lastProcessableIdx = -1;
1392
+ for (let i = messages.length - 1; i >= 0; i--) {
1393
+ if (messages[i].role === 'user' || messages[i].role === 'tool') {
1394
+ lastProcessableIdx = i;
1395
+ break;
1396
+ }
1397
+ }
1398
+
1399
+ function attachCacheControl(content) {
1400
+ if (typeof content === 'string') {
1401
+ return [{ type: 'text', text: content, cache_control: { type: 'ephemeral' } }];
1402
+ } else if (Array.isArray(content) && content.length > 0) {
1403
+ const updated = [...content];
1404
+ const lastItem = updated[updated.length - 1];
1405
+ updated[updated.length - 1] = {
1406
+ ...lastItem,
1407
+ cache_control: { type: 'ephemeral' }
1408
+ };
1409
+ return updated;
1410
+ }
1411
+ return content;
1412
+ }
1413
+
1414
+ payload.messages = messages.map((m, idx) => {
1415
+ if (idx === lastSystemIdx && usedBreakpoints < MAX_BREAKPOINTS) {
1416
+ usedBreakpoints++;
1417
+ return { ...m, content: attachCacheControl(m.content) };
1418
+ }
1419
+
1420
+ if (idx === lastProcessableIdx && usedBreakpoints < MAX_BREAKPOINTS) {
1421
+ usedBreakpoints++;
1422
+ return { ...m, content: attachCacheControl(m.content) };
1423
+ }
1424
+
1425
+ return m;
1426
+ });
1427
+ }
1428
+
1429
+ getModelEndpoints(cleanUrl) {
1430
+ const v1Url = cleanUrl.endsWith('/v1') ? cleanUrl : `${cleanUrl}/v1`;
1431
+ return [`${v1Url}/models`];
1432
+ }
1433
+ }
1434
+
1435
+ /**
1436
+ * Registro central de adaptadores de proveedor (ProviderRegistry).
1437
+ */
1438
+ class MirrorProviderAdapter extends BaseProviderAdapter {
1439
+ constructor() {
1440
+ super({ id: 'mirror', label: 'Espejo', connection: { endpoint: 'mirror://local', endpointReadOnly: true, credentials: false }, capabilities: { modelListing: false, embeddings: false } });
1441
+ }
1442
+
1443
+ normalizeEndpoint() { return 'mirror://local'; }
1444
+
1445
+ async inspect() {
1446
+ return { success: false, error: 'Mirror is a local request preview and has no server to inspect.' };
1447
+ }
1448
+ }
1449
+
1450
+ class ProviderRegistry {
1451
+ constructor() {
1452
+ this.adapters = new Map();
1453
+ this.defaultAdapter = new BaseProviderAdapter();
1454
+
1455
+ // Registro inicial de adaptadores oficiales
1456
+ this.register(new BaseProviderAdapter({ id: 'openai', label: 'OpenAI / LM Studio', connection: { endpoint: 'http://localhost:1234/v1', knownEndpoints: ['https://api.openai.com/v1'] } }));
1457
+ this.register(new ClaudeProviderAdapter());
1458
+ this.register(new GeminiProviderAdapter());
1459
+ this.register(new OllamaProviderAdapter());
1460
+ this.register(new OpenRouterProviderAdapter());
1461
+ this.register(new BaseProviderAdapter({ id: 'custom', label: 'Personalizado' }));
1462
+ this.register(new MirrorProviderAdapter());
1463
+ }
1464
+
1465
+ /**
1466
+ * Registra un adaptador de proveedor.
1467
+ */
1468
+ register(adapter) {
1469
+ if (adapter && adapter.id) {
1470
+ this.adapters.set(adapter.id.toLowerCase(), adapter);
1471
+ }
1472
+ }
1473
+
1474
+ /**
1475
+ * Obtiene un adaptador por su ID.
1476
+ */
1477
+ get(id) {
1478
+ if (!id) return this.defaultAdapter;
1479
+ return this.adapters.get(String(id).toLowerCase()) || this.defaultAdapter;
1480
+ }
1481
+
1482
+ /**
1483
+ * Detecta el proveedor adecuado a partir de la URL del servidor o tipo explícito.
1484
+ */
1485
+ detect(rawUrl, explicitType) {
1486
+ if (explicitType && explicitType !== 'auto') {
1487
+ return String(explicitType).toLowerCase();
1488
+ }
1489
+ const url = (rawUrl || '').toLowerCase().trim();
1490
+ if (url.includes('11434') || url.includes('ollama')) return 'ollama';
1491
+ if (url.includes('openrouter.ai')) return 'openrouter';
1492
+ if (url.includes('anthropic.com')) return 'claude';
1493
+ if (url.includes('googleapis.com') || url.includes('gemini')) return 'gemini';
1494
+ return 'openai';
1495
+ }
1496
+
1497
+ /**
1498
+ * Resuelve y devuelve la instancia del adaptador para una URL y tipo dados.
1499
+ */
1500
+ resolve(rawUrl, explicitType) {
1501
+ const type = this.detect(rawUrl, explicitType);
1502
+ return this.get(type);
1503
+ }
1504
+
1505
+ /**
1506
+ * Obtiene las capacidades de un proveedor o modelo dado.
1507
+ */
1508
+ getCapabilities(rawUrlOrType, model, explicitType) {
1509
+ const adapter = this.resolve(rawUrlOrType, explicitType);
1510
+ return adapter.getCapabilities(model);
1511
+ }
1512
+
1513
+ /**
1514
+ * Obtiene todos los modos de razonamiento registrados para la UI.
1515
+ */
1516
+ getReasoningModes() {
1517
+ const modes = {};
1518
+ for (const [id, adapter] of this.adapters.entries()) {
1519
+ modes[id] = adapter.getReasoningConfig();
1520
+ }
1521
+ return modes;
1522
+ }
1523
+
1524
+ getConnectionEndpoints() {
1525
+ return Array.from(this.adapters.values())
1526
+ .flatMap(adapter => {
1527
+ const connection = adapter.getConnectionConfig?.() || {};
1528
+ return [connection.endpoint, ...(connection.knownEndpoints || [])];
1529
+ })
1530
+ .filter(Boolean);
1531
+ }
1532
+
1533
+ /**
1534
+ * Inspecciona y diagnostica el endpoint del proveedor delegando en el adaptador correspondiente.
1535
+ */
1536
+ async inspect(rawUrl, apiKey, model, explicitType, options = {}) {
1537
+ const adapter = this.resolve(rawUrl, explicitType);
1538
+ return adapter.inspect({
1539
+ ...options,
1540
+ apiUrl: rawUrl,
1541
+ apiKey: apiKey,
1542
+ model: model
1543
+ });
1544
+ }
1545
+ }
1546
+
1547
+ const registry = new ProviderRegistry();
1548
+
1549
+ return {
1550
+ DEFAULT_CAPABILITIES,
1551
+ BaseProviderAdapter,
1552
+ MirrorProviderAdapter,
1553
+ ClaudeProviderAdapter,
1554
+ GeminiProviderAdapter,
1555
+ OllamaProviderAdapter,
1556
+ OpenRouterProviderAdapter,
1557
+ ProviderRegistry,
1558
+ registry
1559
+ };
1560
+ });