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,844 @@
1
+ /**
2
+ * Módulo para interactuar con la API compatible de OpenAI, Claude, Gemini, Ollama y OpenRouter (ChatAPI).
3
+ * Actúa como orquestador común de transporte HTTP/SSE delegando las particularidades en los adaptadores (ChatProviders).
4
+ * Compatible con file://, http:// y Node.js.
5
+ */
6
+
7
+ (function (root, factory) {
8
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
9
+ module.exports = factory();
10
+ } else {
11
+ root.ChatAPI = factory();
12
+ }
13
+ })(typeof self !== 'undefined' ? self : this, function () {
14
+ 'use strict';
15
+
16
+ const Sandbox = typeof window !== 'undefined' ? (window.ChatSandbox || {}) : {};
17
+ const WebBrowser = typeof window !== 'undefined' ? (window.ChatWebBrowser || {}) : {};
18
+ const WebSearch = typeof window !== 'undefined' ? (window.ChatWebSearch || {}) : {};
19
+ const ProvidersModule = typeof window !== 'undefined' ? (window.ChatProviders || {}) : (typeof require !== 'undefined' ? (() => { try { return require('./providers.js'); } catch(e) { return {}; } })() : {});
20
+ const registry = ProvidersModule.registry || (ProvidersModule.ProviderRegistry ? new ProvidersModule.ProviderRegistry() : null);
21
+
22
+ const TOOL_NAME_MAP = Object.freeze({
23
+ // 1. Descarga y extracción de PDF
24
+ download_pdf: 'download_pdf',
25
+ fetch_pdf: 'download_pdf',
26
+ download_pdf_document: 'download_pdf',
27
+ fetch_pdf_document: 'download_pdf',
28
+ download_file: 'download_pdf',
29
+ downloadpdf: 'download_pdf',
30
+ fetchpdf: 'download_pdf',
31
+ downloadpdfdocument: 'download_pdf',
32
+ fetchpdfdocument: 'download_pdf',
33
+ downloadfile: 'download_pdf',
34
+ getpdf: 'download_pdf',
35
+ readpdf: 'download_pdf',
36
+
37
+ // 2. Navegación / Consulta de página web
38
+ fetch_web_page: 'fetch_web_page',
39
+ fetch_web: 'fetch_web_page',
40
+ fetch_url: 'fetch_web_page',
41
+ get_web_page: 'fetch_web_page',
42
+ read_web_page: 'fetch_web_page',
43
+ web_fetch: 'fetch_web_page',
44
+ browse_web: 'fetch_web_page',
45
+ fetchwebpage: 'fetch_web_page',
46
+ fetchweb: 'fetch_web_page',
47
+ fetchurl: 'fetch_web_page',
48
+ getwebpage: 'fetch_web_page',
49
+ readwebpage: 'fetch_web_page',
50
+ webpage: 'fetch_web_page',
51
+ browse: 'fetch_web_page',
52
+
53
+ // 3. Búsqueda en internet
54
+ search_web: 'search_web',
55
+ web_search: 'search_web',
56
+ duckduckgo_search: 'search_web',
57
+ duckduckgo: 'search_web',
58
+ search_internet: 'search_web',
59
+ internet_search: 'search_web',
60
+ searchweb: 'search_web',
61
+ websearch: 'search_web',
62
+ duckduckgosearch: 'search_web',
63
+ searchinternet: 'search_web',
64
+ internetsearch: 'search_web',
65
+ search: 'search_web',
66
+
67
+ // 4. Ejecución de JavaScript
68
+ execute_javascript: 'execute_javascript',
69
+ execute_js: 'execute_javascript',
70
+ run_javascript: 'execute_javascript',
71
+ run_js: 'execute_javascript',
72
+ executejavascript: 'execute_javascript',
73
+ executejs: 'execute_javascript',
74
+ runjavascript: 'execute_javascript',
75
+ runjs: 'execute_javascript',
76
+ javascript: 'execute_javascript',
77
+ evaljs: 'execute_javascript',
78
+ evaljavascript: 'execute_javascript',
79
+
80
+ // 5. Renderizado de Gráficos (render_chart)
81
+ render_chart: 'render_chart',
82
+ draw_chart: 'render_chart',
83
+ create_chart: 'render_chart',
84
+ plot_chart: 'render_chart',
85
+ generate_chart: 'render_chart',
86
+ show_chart: 'render_chart',
87
+ renderchart: 'render_chart',
88
+ drawchart: 'render_chart',
89
+ createchart: 'render_chart',
90
+ plotchart: 'render_chart',
91
+ chart: 'render_chart',
92
+ grafico: 'render_chart',
93
+
94
+ // 6. Base de Conocimiento RAG
95
+ read_knowledge_chunk: 'read_knowledge_chunk',
96
+ readknowledgechunk: 'read_knowledge_chunk',
97
+ list_documents: 'list_documents',
98
+ listdocuments: 'list_documents',
99
+ list_knowledge_base: 'list_documents',
100
+ listknowledgebase: 'list_documents',
101
+ list_docs: 'list_documents',
102
+ listdocs: 'list_documents',
103
+ get_documents: 'list_documents',
104
+ getdocuments: 'list_documents',
105
+ listar_documentos: 'list_documents',
106
+ listardocumentos: 'list_documents',
107
+ search_knowledge_base: 'search_knowledge_base',
108
+ searchknowledgebase: 'search_knowledge_base',
109
+ search_kb: 'search_knowledge_base',
110
+ searchkb: 'search_knowledge_base',
111
+ search_documents: 'search_knowledge_base',
112
+ searchdocuments: 'search_knowledge_base',
113
+ search_knowledge: 'search_knowledge_base',
114
+ searchknowledge: 'search_knowledge_base',
115
+ buscar_en_documentos: 'search_knowledge_base',
116
+ buscarendocumentos: 'search_knowledge_base'
117
+ });
118
+
119
+ function mirrorResponse(payload) {
120
+ const i18n = typeof window !== 'undefined' ? window.ChatI18n : require('./i18n.js');
121
+ const guidance = [
122
+ i18n.t('mirror_response_intro'),
123
+ i18n.t('mirror_response_about'),
124
+ i18n.t('mirror_response_profile'),
125
+ i18n.t('mirror_response_webllm'),
126
+ i18n.t('mirror_response_request')
127
+ ].join('\n\n');
128
+ return `${guidance}\n\n\`\`\`json\n${JSON.stringify(payload)}\n\`\`\``;
129
+ }
130
+
131
+ /**
132
+ * Normaliza los nombres de las herramientas admitiendo variaciones con y sin guiones bajos.
133
+ */
134
+ function normalizeToolName(rawName) {
135
+ if (!rawName) return '';
136
+ const clean = String(rawName).trim().toLowerCase().replace(/[^a-z0-9_]/g, '');
137
+ const noUnderscore = clean.replace(/_/g, '');
138
+ return TOOL_NAME_MAP[clean] || TOOL_NAME_MAP[noUnderscore] || clean;
139
+ }
140
+
141
+
142
+ /**
143
+ * Detecta el tipo de API adecuado según la URL o tipo explícito.
144
+ */
145
+ function detectApiType(rawUrl, explicitType) {
146
+ if (registry) {
147
+ return registry.detect(rawUrl, explicitType);
148
+ }
149
+ if (explicitType && explicitType !== 'auto') return explicitType;
150
+ const url = (rawUrl || '').toLowerCase().trim();
151
+ if (url.includes('11434') || url.includes('ollama')) return 'ollama';
152
+ if (url.includes('openrouter.ai')) return 'openrouter';
153
+ if (url.includes('anthropic.com')) return 'claude';
154
+ if (url.includes('googleapis.com') || url.includes('gemini')) return 'gemini';
155
+ return 'openai';
156
+ }
157
+
158
+ /**
159
+ * Normaliza la URL base al endpoint de chat del proveedor resuelto.
160
+ */
161
+ function normalizeApiUrl(rawUrl, explicitType) {
162
+ if (registry) {
163
+ const adapter = registry.resolve(rawUrl, explicitType);
164
+ return adapter.normalizeEndpoint(rawUrl);
165
+ }
166
+ let url = (rawUrl || 'http://localhost:1234/v1').trim();
167
+ if (url.endsWith('/')) url = url.slice(0, -1);
168
+ const type = detectApiType(url, explicitType);
169
+ if (type === 'ollama') {
170
+ if (url.endsWith('/api/chat') || url.endsWith('/chat/completions')) return url;
171
+ if (url.endsWith('/v1')) return `${url}/chat/completions`;
172
+ return `${url}/v1/chat/completions`;
173
+ }
174
+ if (type === 'claude') {
175
+ if (url.endsWith('/v1/messages') || url.endsWith('/messages') || url.endsWith('/chat/completions')) return url;
176
+ if (url.endsWith('/v1')) return `${url}/messages`;
177
+ return `${url}/v1/messages`;
178
+ }
179
+ if (url.endsWith('/chat/completions')) return url;
180
+ if (url.endsWith('/v1')) return `${url}/chat/completions`;
181
+ return `${url}/v1/chat/completions`;
182
+ }
183
+
184
+ /**
185
+ * Estimación aproximada de tokens para textos.
186
+ */
187
+ function estimateTokens(text, chunkCount) {
188
+ if (!text) return 0;
189
+ if (chunkCount && chunkCount > 0) {
190
+ return Math.max(chunkCount, Math.ceil(text.length / 3.8));
191
+ }
192
+ return Math.ceil(text.length / 3.8);
193
+ }
194
+
195
+ /**
196
+ * Modos de razonamiento estándar organizados por proveedor.
197
+ */
198
+ const STANDARD_REASONING_MODES = registry ? registry.getReasoningModes() : {
199
+ openai: { type: 'openai', label: 'OpenAI / LM Studio', levels: ['none', 'low', 'medium', 'high', 'xhigh'], description: 'Estándar OpenAI / LM Studio' },
200
+ ollama: { type: 'ollama', label: 'Ollama', levels: ['none', 'low', 'medium', 'high', 'xhigh'], description: 'Estándar Ollama' },
201
+ openrouter: { type: 'openrouter', label: 'OpenRouter', levels: ['none', 'low', 'medium', 'high', 'xhigh'], description: 'Estándar OpenRouter' },
202
+ claude: { type: 'claude', label: 'Anthropic Claude', levels: ['none', 'low', 'medium', 'high', 'xhigh'], description: 'Estándar Claude' },
203
+ gemini: { type: 'gemini', label: 'Google Gemini', levels: ['none', 'low', 'medium', 'high', 'xhigh'], description: 'Estándar Gemini' },
204
+ custom: { type: 'custom', label: 'Personalizado', levels: ['none', 'low', 'medium', 'high', 'xhigh'], description: 'Personalizado' }
205
+ };
206
+
207
+ function getStandardReasoningOptions(explicitType, rawUrl) {
208
+ if (registry) {
209
+ const adapter = registry.resolve(rawUrl, explicitType);
210
+ return adapter.getReasoningConfig();
211
+ }
212
+ const type = detectApiType(rawUrl, explicitType);
213
+ return STANDARD_REASONING_MODES[type] || STANDARD_REASONING_MODES.openai;
214
+ }
215
+
216
+ /**
217
+ * Consulta los modelos disponibles en el servidor delegando en el adaptador.
218
+ */
219
+ async function fetchServerModels(rawUrl, apiKey, explicitType) {
220
+ const resolvedApiKey = String(apiKey || '').trim();
221
+ let cleanUrl = (rawUrl || 'http://localhost:1234/v1').trim();
222
+ if (cleanUrl.endsWith('/')) cleanUrl = cleanUrl.slice(0, -1);
223
+ if (cleanUrl.endsWith('/chat/completions')) cleanUrl = cleanUrl.replace(/\/chat\/completions$/, '');
224
+
225
+ const adapter = registry ? registry.resolve(cleanUrl, explicitType) : null;
226
+ if (adapter) {
227
+ const caps = adapter.getCapabilities();
228
+ if (caps && caps.modelListing === false) {
229
+ return {
230
+ success: false,
231
+ error: 'El proveedor configurado no admite descubrimiento automático de modelos.'
232
+ };
233
+ }
234
+ }
235
+ if (adapter && typeof adapter.listModels === 'function') {
236
+ return adapter.listModels();
237
+ }
238
+ const candidateEndpoints = adapter ? adapter.getModelEndpoints(cleanUrl) : [`${cleanUrl}/v1/models`];
239
+
240
+ const headers = { ...adapter?.buildHeaders(resolvedApiKey), Accept: 'application/json' };
241
+
242
+ let lastError = null;
243
+
244
+ for (const endpoint of candidateEndpoints) {
245
+ let timeoutId = null;
246
+ try {
247
+ const controller = new AbortController();
248
+ timeoutId = setTimeout(() => controller.abort(), 8000);
249
+
250
+ const response = await fetch(endpoint, {
251
+ method: 'GET',
252
+ headers: headers,
253
+ signal: controller.signal
254
+ });
255
+
256
+ if (!response.ok) {
257
+ lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);
258
+ continue;
259
+ }
260
+
261
+ const data = await response.json();
262
+ const extractedModels = adapter ? adapter.parseModelsResponse(data) : (Array.isArray(data.data) ? data.data : []);
263
+
264
+ if (extractedModels && extractedModels.length > 0) {
265
+ extractedModels.sort((a, b) => a.id.localeCompare(b.id));
266
+ return {
267
+ success: true,
268
+ endpoint: endpoint,
269
+ models: extractedModels,
270
+ count: extractedModels.length,
271
+ raw: data
272
+ };
273
+ }
274
+ } catch (err) {
275
+ lastError = err;
276
+ } finally {
277
+ if (timeoutId) clearTimeout(timeoutId);
278
+ }
279
+ }
280
+
281
+ return {
282
+ success: false,
283
+ error: lastError ? (lastError.message || String(lastError)) : 'No se pudo obtener la lista de modelos desde los endpoints consultados.'
284
+ };
285
+ }
286
+
287
+ async function downloadLocalModel(model, onProgress, providerId = 'webllm') {
288
+ const adapter = registry?.get(providerId);
289
+ if (!adapter || typeof adapter.downloadModel !== 'function') {
290
+ throw new Error('The configured provider does not support local model downloads.');
291
+ }
292
+ return adapter.downloadModel(model, onProgress);
293
+ }
294
+
295
+ async function deleteLocalModel(model, providerId = 'webllm') {
296
+ const adapter = registry?.get(providerId);
297
+ if (!adapter || typeof adapter.deleteModel !== 'function') {
298
+ throw new Error('The configured provider does not support deleting local models.');
299
+ }
300
+ return adapter.deleteModel(model);
301
+ }
302
+
303
+ async function cancelLocalModelOperation(model, providerId = 'webllm') {
304
+ const adapter = registry?.get(providerId);
305
+ if (!adapter || typeof adapter.cancelModelOperation !== 'function') return;
306
+ return adapter.cancelModelOperation(model);
307
+ }
308
+
309
+ /**
310
+ * Orquestador común de streaming HTTP/SSE.
311
+ */
312
+ async function streamChatCompletion(params) {
313
+ const {
314
+ apiUrl,
315
+ apiType,
316
+ apiKey,
317
+ model,
318
+ messages,
319
+ temperature = 0.7,
320
+ reasoningEffort = 'medium',
321
+ reasoningTransport = 'auto',
322
+ enableTools = false,
323
+ toolChoice = 'auto',
324
+ enableAgentJs = false,
325
+ enableAgentWeb = false,
326
+ enableAgentSearch = false,
327
+ enableAgentChart = false,
328
+ activeRagBranchId = '',
329
+ signal,
330
+ onBeforeRequest,
331
+ onChunk,
332
+ onReasoningChunk,
333
+ onGenerationStatus,
334
+ onToolCallDelta,
335
+ onLog,
336
+ onDone,
337
+ onError
338
+ } = params;
339
+
340
+ const effectiveApiKey = String(apiKey || '').trim();
341
+
342
+ const adapter = registry ? registry.resolve(apiUrl, apiType) : null;
343
+ const endpoint = adapter ? adapter.normalizeEndpoint(apiUrl) : normalizeApiUrl(apiUrl, apiType);
344
+ const detectedType = adapter ? adapter.id : detectApiType(apiUrl, apiType);
345
+ const headers = adapter ? adapter.buildHeaders(effectiveApiKey) : { 'Content-Type': 'application/json' };
346
+
347
+ // Inyectar herramientas agénticas activadas si están disponibles
348
+ let toolsList = [];
349
+ if (Array.isArray(params.tools)) {
350
+ toolsList = params.tools;
351
+ } else if (enableTools || (Boolean(activeRagBranchId) && enableTools !== false)) {
352
+ const AgentCore = typeof window !== 'undefined' ? window.ChatAgentCore : (typeof require !== 'undefined' ? (() => { try { return require('./agent-core.js'); } catch(e){ return null; } })() : null);
353
+ if (AgentCore && AgentCore.registry && typeof AgentCore.registry.getActiveDefinitions === 'function') {
354
+ toolsList = AgentCore.registry.getActiveDefinitions(params);
355
+ }
356
+ }
357
+
358
+ // Construcción del payload mediante el adaptador
359
+ const payload = adapter ? adapter.buildPayload({
360
+ model,
361
+ messages,
362
+ temperature,
363
+ reasoningEffort,
364
+ reasoningTransport,
365
+ toolsList,
366
+ toolChoice: toolChoice || 'auto'
367
+ }) : {
368
+ model: (model || '').trim(),
369
+ messages,
370
+ stream: true,
371
+ temperature: parseFloat(temperature) || 0.7
372
+ };
373
+
374
+ if (onBeforeRequest) {
375
+ try {
376
+ const debugResult = await onBeforeRequest({ endpoint, headers, payload });
377
+ if (debugResult && debugResult.cancel) {
378
+ if (onLog) {
379
+ onLog({
380
+ type: 'info',
381
+ text: '🛑 Envío de petición cancelado por el usuario en el depurador de mensajes.'
382
+ });
383
+ }
384
+ if (onError) {
385
+ onError('Envío cancelado en depurador de mensajes.');
386
+ }
387
+ return {
388
+ aborted: true,
389
+ cancelled: true,
390
+ text: '',
391
+ toolCalls: null,
392
+ stats: null
393
+ };
394
+ }
395
+ if (debugResult && debugResult.modifiedPayload) {
396
+ Object.keys(payload).forEach(k => delete payload[k]);
397
+ Object.assign(payload, debugResult.modifiedPayload);
398
+ }
399
+ } catch (err) {
400
+ console.warn('ChatAPI: Error en callback onBeforeRequest:', err);
401
+ }
402
+ }
403
+
404
+ if (signal?.aborted) {
405
+ return { aborted: true, cancelled: true, accumulatedText: '', toolCalls: null, stats: null };
406
+ }
407
+
408
+ if (detectedType === 'mirror') {
409
+ const accumulatedText = mirrorResponse(payload);
410
+ const stats = {
411
+ ttftSec: '0.00', generationSec: '0.00', totalSec: '0.00',
412
+ tokens: estimateTokens(accumulatedText, 1), tokensPerSec: '0.0',
413
+ cachedTokens: 0, cacheCreationTokens: 0, promptTokens: 0,
414
+ completionTokens: 0, totalTokens: 0, reasoningTokens: 0, isEstimated: true
415
+ };
416
+ if (onLog) {
417
+ onLog({ type: 'network', text: `MIRROR POST ${endpoint} [OPENAI] | La petición no se ha enviado.` });
418
+ onLog({ type: 'raw', subtype: 'outgoing', text: `>>> MIRROR POST ${endpoint}\n${accumulatedText}` });
419
+ }
420
+ if (onChunk) onChunk(accumulatedText, accumulatedText, stats);
421
+ if (onDone) await onDone(accumulatedText, stats, null, '');
422
+ return { accumulatedText, accumulatedReasoning: '', stats, toolCalls: null };
423
+ }
424
+
425
+ if (onLog) {
426
+ onLog({
427
+ type: 'network',
428
+ text: `POST ${endpoint} [${detectedType.toUpperCase()}] | Modelo: ${model || '(no especificado)'} | Razonamiento: ${reasoningEffort || 'off'} | Temp: ${payload.temperature}`
429
+ });
430
+ onLog({
431
+ type: 'raw',
432
+ subtype: 'outgoing',
433
+ text: `>>> OUTGOING POST ${endpoint}\n${JSON.stringify(payload, null, 2)}`
434
+ });
435
+ }
436
+
437
+ let accumulatedText = '';
438
+ let accumulatedReasoning = '';
439
+ let accumulatedToolCalls = {};
440
+ let chunkCount = 0;
441
+ let activeReasoningTag = null;
442
+ let serverCachedTokens = 0;
443
+ let serverCacheCreationTokens = 0;
444
+ let serverPromptTokens = 0;
445
+ let serverCompletionTokens = 0;
446
+ let serverTotalTokens = 0;
447
+ let serverReasoningTokens = 0;
448
+ const requestStartTime = performance.now();
449
+ let firstTokenTime = null;
450
+ let generationStatus = { phase: '', percent: null, detail: '' };
451
+ function publishGenerationStatus(update = {}) {
452
+ const next = { ...generationStatus, ...update };
453
+ if (next.phase === generationStatus.phase && next.percent === generationStatus.percent && next.detail === generationStatus.detail) return;
454
+ generationStatus = next;
455
+ onGenerationStatus?.(next);
456
+ }
457
+ publishGenerationStatus({ phase: 'connecting' });
458
+
459
+ function getStats() {
460
+ const now = performance.now();
461
+ const totalElapsedSec = ((now - requestStartTime) / 1000).toFixed(2);
462
+ const isEstimated = serverCompletionTokens <= 0;
463
+ const tokens = serverCompletionTokens > 0
464
+ ? serverCompletionTokens
465
+ : estimateTokens(accumulatedText + accumulatedReasoning, chunkCount);
466
+
467
+ if (firstTokenTime) {
468
+ const ttftSec = ((firstTokenTime - requestStartTime) / 1000).toFixed(2);
469
+ const generationElapsedMs = Math.max(now - firstTokenTime, 10);
470
+ const generationElapsedSec = generationElapsedMs / 1000;
471
+ const tokensPerSec = tokens > 0 ? (tokens / generationElapsedSec).toFixed(1) : '0.0';
472
+
473
+ return {
474
+ ttftSec: ttftSec,
475
+ generationSec: generationElapsedSec.toFixed(2),
476
+ totalSec: totalElapsedSec,
477
+ tokens: tokens,
478
+ tokensPerSec: tokensPerSec,
479
+ cachedTokens: serverCachedTokens,
480
+ cacheCreationTokens: serverCacheCreationTokens,
481
+ promptTokens: serverPromptTokens,
482
+ completionTokens: serverCompletionTokens,
483
+ totalTokens: serverTotalTokens,
484
+ reasoningTokens: serverReasoningTokens,
485
+ isEstimated: isEstimated
486
+ };
487
+ } else {
488
+ return {
489
+ ttftSec: totalElapsedSec,
490
+ generationSec: '0.00',
491
+ totalSec: totalElapsedSec,
492
+ tokens: tokens,
493
+ tokensPerSec: '0.0',
494
+ cachedTokens: serverCachedTokens,
495
+ cacheCreationTokens: serverCacheCreationTokens,
496
+ promptTokens: serverPromptTokens,
497
+ completionTokens: serverCompletionTokens,
498
+ totalTokens: serverTotalTokens,
499
+ reasoningTokens: serverReasoningTokens,
500
+ isEstimated: isEstimated
501
+ };
502
+ }
503
+ }
504
+
505
+ function getFinalToolCalls() {
506
+ const callIndices = Object.keys(accumulatedToolCalls);
507
+ if (callIndices.length > 0) {
508
+ return callIndices.map(idx => {
509
+ const item = accumulatedToolCalls[idx];
510
+ if (item?.function?.name) {
511
+ item.function.name = normalizeToolName(item.function.name) || item.function.name;
512
+ }
513
+ return item;
514
+ });
515
+ }
516
+ return null;
517
+ }
518
+
519
+ try {
520
+ let response = adapter?.createStreamResponse
521
+ ? await adapter.createStreamResponse({
522
+ payload,
523
+ signal,
524
+ onProgress: progress => {
525
+ publishGenerationStatus(progress);
526
+ onLog?.({ type: 'info', text: `${adapter.label || adapter.id}: ${String(progress?.detail || progress?.phase || 'preparing')}` });
527
+ }
528
+ })
529
+ : await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(payload), signal });
530
+
531
+ if (!response.ok) {
532
+ let serverErrorMsg = '';
533
+ try {
534
+ const rawError = await response.text();
535
+ try {
536
+ const errorJson = JSON.parse(rawError);
537
+ serverErrorMsg = (errorJson && (errorJson.error?.message || errorJson.message || errorJson.error)) || rawError;
538
+ } catch (jsonErr) {
539
+ serverErrorMsg = rawError;
540
+ }
541
+ } catch (readErr) {
542
+ serverErrorMsg = response.statusText;
543
+ }
544
+
545
+ // Auto-recuperación delegando en el adaptador
546
+ if (adapter) {
547
+ const recovery = adapter.handleHttpError(response.status, serverErrorMsg, payload);
548
+ if (recovery && recovery.retry) {
549
+ if (onLog) {
550
+ onLog({
551
+ type: 'error',
552
+ text: `HTTP 400: Servidor rechazó parámetro (${recovery.reason || 'parámetro incompatible'}). Reintentando sin él...`
553
+ });
554
+ }
555
+ response = await fetch(endpoint, {
556
+ method: 'POST',
557
+ headers: headers,
558
+ body: JSON.stringify(payload),
559
+ signal: signal
560
+ });
561
+ }
562
+ }
563
+
564
+ if (!response.ok) {
565
+ let errorMessage = serverErrorMsg || `Error HTTP ${response.status}: ${response.statusText}`;
566
+ if (response.status === 401) {
567
+ errorMessage = 'Clave de API inválida o no autorizada (401). Verifica tu API Key en la Configuración.';
568
+ } else if (response.status === 404) {
569
+ errorMessage = `Endpoint no encontrado (404). Verifica la URL del servidor: ${endpoint}`;
570
+ }
571
+ if (onLog) {
572
+ onLog({ type: 'error', text: errorMessage });
573
+ onLog({ type: 'raw', subtype: 'incoming', text: `<<< INCOMING HTTP ${response.status} ${response.statusText}\n${serverErrorMsg || errorMessage}` });
574
+ }
575
+ throw new Error(errorMessage);
576
+ }
577
+ }
578
+
579
+ if (!response.body) {
580
+ throw new Error('La respuesta del servidor no soporta streaming.');
581
+ }
582
+
583
+ const reader = response.body.getReader();
584
+ const decoder = new TextDecoder('utf-8');
585
+ let buffer = '';
586
+
587
+ while (true) {
588
+ const { done, value } = await reader.read();
589
+ if (done) break;
590
+
591
+ buffer += decoder.decode(value, { stream: true });
592
+ const lines = buffer.split('\n');
593
+ buffer = lines.pop() || '';
594
+
595
+ for (let i = 0; i < lines.length; i++) {
596
+ const rawLine = lines[i];
597
+ const trimmedLine = rawLine.trim();
598
+ if (!trimmedLine) continue;
599
+
600
+ if (onLog) {
601
+ onLog({
602
+ type: 'raw',
603
+ subtype: 'incoming',
604
+ text: rawLine
605
+ });
606
+ }
607
+
608
+ if (trimmedLine.startsWith(':')) continue;
609
+
610
+ if (trimmedLine.startsWith('data: ')) {
611
+ const dataStr = trimmedLine.substring(6).trim();
612
+
613
+ if (dataStr === '[DONE]') {
614
+ const stats = getStats();
615
+ const toolCalls = getFinalToolCalls();
616
+ if (onLog) {
617
+ const cacheInfo = stats.cachedTokens > 0 ? ` | ⚡ Cache: ${stats.cachedTokens} tok` : '';
618
+ onLog({
619
+ type: 'stats',
620
+ text: `Streaming finalizado [DONE] | Total: ${stats.tokens} tokens | ${stats.tokensPerSec} t/s | TTFT: ${stats.ttftSec}s${cacheInfo}`
621
+ });
622
+ }
623
+ if (onDone) await onDone(accumulatedText, stats, toolCalls, accumulatedReasoning);
624
+ return { accumulatedText, accumulatedReasoning, stats, toolCalls };
625
+ }
626
+
627
+ try {
628
+ const parsed = JSON.parse(dataStr);
629
+ const chunkData = adapter ? adapter.parseStreamChunk(parsed, { firstTokenTime, accumulatedReasoning, accumulatedText }) : { textChunk: parsed.choices?.[0]?.delta?.content || '' };
630
+
631
+ if (chunkData.cachedTokens > 0) serverCachedTokens = chunkData.cachedTokens;
632
+ if (chunkData.cacheCreationTokens > 0) serverCacheCreationTokens = chunkData.cacheCreationTokens;
633
+
634
+ // Tokens de razonamiento específicos
635
+ if (chunkData.reasoningChunk) {
636
+ publishGenerationStatus({ phase: 'thinking', percent: null, detail: '' });
637
+ if (!firstTokenTime) firstTokenTime = performance.now();
638
+ accumulatedReasoning += chunkData.reasoningChunk;
639
+ if (onReasoningChunk) {
640
+ onReasoningChunk(chunkData.reasoningChunk, accumulatedReasoning);
641
+ } else if (onLog) {
642
+ onLog({ type: 'thinking', text: chunkData.reasoningChunk });
643
+ }
644
+ }
645
+
646
+ // Contenido textual con soporte de etiquetas <think>, <thought>, <reasoning>
647
+ const textChunk = chunkData.textChunk;
648
+ if (textChunk) {
649
+ if (!firstTokenTime) firstTokenTime = performance.now();
650
+
651
+ let remaining = textChunk;
652
+ while (remaining.length > 0) {
653
+ if (!activeReasoningTag) {
654
+ const openMatch = remaining.match(/<(think|thought|reasoning)>/i);
655
+ if (openMatch) {
656
+ const preText = remaining.slice(0, openMatch.index);
657
+ if (preText) {
658
+ publishGenerationStatus({ phase: 'generating', percent: null, detail: '' });
659
+ accumulatedText += preText;
660
+ chunkCount++;
661
+ if (onChunk) onChunk(accumulatedText, preText, getStats());
662
+ }
663
+ activeReasoningTag = openMatch[1].toLowerCase();
664
+ remaining = remaining.slice(openMatch.index + openMatch[0].length);
665
+ publishGenerationStatus({ phase: 'thinking', percent: null, detail: '' });
666
+ } else {
667
+ publishGenerationStatus({ phase: 'generating', percent: null, detail: '' });
668
+ accumulatedText += remaining;
669
+ chunkCount++;
670
+ if (onChunk) onChunk(accumulatedText, remaining, getStats());
671
+ remaining = '';
672
+ }
673
+ } else {
674
+ const closeRegex = new RegExp(`<\/${activeReasoningTag}>`, 'i');
675
+ const closeMatch = remaining.match(closeRegex);
676
+ if (closeMatch) {
677
+ const rText = remaining.slice(0, closeMatch.index);
678
+ if (rText) {
679
+ publishGenerationStatus({ phase: 'thinking', percent: null, detail: '' });
680
+ accumulatedReasoning += rText;
681
+ if (onReasoningChunk) {
682
+ onReasoningChunk(rText, accumulatedReasoning);
683
+ } else if (onLog) {
684
+ onLog({ type: 'thinking', text: rText });
685
+ }
686
+ }
687
+ activeReasoningTag = null;
688
+ remaining = remaining.slice(closeMatch.index + closeMatch[0].length);
689
+ } else {
690
+ publishGenerationStatus({ phase: 'thinking', percent: null, detail: '' });
691
+ accumulatedReasoning += remaining;
692
+ if (onReasoningChunk) {
693
+ onReasoningChunk(remaining, accumulatedReasoning);
694
+ } else if (onLog) {
695
+ onLog({ type: 'thinking', text: remaining });
696
+ }
697
+ remaining = '';
698
+ }
699
+ }
700
+ }
701
+ }
702
+
703
+ // Tool Call Deltas
704
+ if (chunkData.toolCallDeltas && chunkData.toolCallDeltas.length > 0) {
705
+ if (!firstTokenTime) firstTokenTime = performance.now();
706
+ chunkData.toolCallDeltas.forEach(tc => {
707
+ const idx = tc.index ?? 0;
708
+ if (!accumulatedToolCalls[idx]) {
709
+ accumulatedToolCalls[idx] = {
710
+ id: tc.id || `call_${Date.now()}_${idx}`,
711
+ type: 'function',
712
+ function: {
713
+ name: tc.function?.name || '',
714
+ arguments: ''
715
+ }
716
+ };
717
+ }
718
+ if (tc.id) accumulatedToolCalls[idx].id = tc.id;
719
+ if (tc.function?.name) accumulatedToolCalls[idx].function.name = tc.function.name;
720
+ if (tc.function?.arguments) accumulatedToolCalls[idx].function.arguments += tc.function.arguments;
721
+ if (tc.thought_signature) accumulatedToolCalls[idx].thought_signature = tc.thought_signature;
722
+ if (tc.extra_content) accumulatedToolCalls[idx].extra_content = tc.extra_content;
723
+ if (tc.provider_specific_fields) accumulatedToolCalls[idx].provider_specific_fields = tc.provider_specific_fields;
724
+ if (chunkData.thoughtSignature && !accumulatedToolCalls[idx].thought_signature) {
725
+ accumulatedToolCalls[idx].thought_signature = chunkData.thoughtSignature;
726
+ }
727
+
728
+ if (onToolCallDelta) onToolCallDelta(accumulatedToolCalls[idx]);
729
+ });
730
+ }
731
+
732
+ // Usage logging si viene en el chunk (OpenAI prompt_tokens / Anthropic input_tokens / Ollama)
733
+ const streamUsage = chunkData.usage || parsed.usage;
734
+ if (streamUsage) {
735
+ const pTokens = streamUsage.prompt_tokens ?? streamUsage.input_tokens ?? streamUsage.prompt_eval_count ?? 0;
736
+ const cTokens = streamUsage.completion_tokens ?? streamUsage.output_tokens ?? streamUsage.eval_count ?? 0;
737
+ const rTokens = streamUsage.completion_tokens_details?.reasoning_tokens ?? streamUsage.reasoning_tokens ?? 0;
738
+
739
+ if (pTokens > 0) serverPromptTokens = pTokens;
740
+ if (cTokens > 0) serverCompletionTokens = cTokens;
741
+ if (rTokens > 0) serverReasoningTokens = rTokens;
742
+
743
+ if (streamUsage.total_tokens && streamUsage.total_tokens > 0) {
744
+ serverTotalTokens = Math.max(streamUsage.total_tokens, (serverPromptTokens + serverCompletionTokens));
745
+ } else if (serverPromptTokens > 0 || serverCompletionTokens > 0) {
746
+ serverTotalTokens = serverPromptTokens + serverCompletionTokens;
747
+ }
748
+
749
+ if (onLog) {
750
+ const cacheLog = serverCachedTokens > 0 ? ` (⚡ Cache: ${serverCachedTokens} tok)` : '';
751
+ onLog({
752
+ type: 'stats',
753
+ text: `Uso de tokens: Prompt=${serverPromptTokens}, Respuesta=${serverCompletionTokens}, Total=${serverTotalTokens}${serverReasoningTokens ? ` (Razonamiento: ${serverReasoningTokens})` : ''}${cacheLog}`
754
+ });
755
+ }
756
+ }
757
+
758
+ } catch (jsonErr) {}
759
+ }
760
+ }
761
+ }
762
+
763
+ // Vaciar buffer residual de TextDecoder si quedaron bytes en streaming
764
+ try {
765
+ const flushText = decoder.decode();
766
+ if (flushText && onLog) {
767
+ onLog({ type: 'raw', subtype: 'incoming', text: flushText });
768
+ }
769
+ } catch (e) {}
770
+
771
+ const finalStats = getStats();
772
+ const finalToolCalls = getFinalToolCalls();
773
+ if (onDone) await onDone(accumulatedText, finalStats, finalToolCalls, accumulatedReasoning);
774
+ return { accumulatedText, accumulatedReasoning, stats: finalStats, toolCalls: finalToolCalls };
775
+
776
+ } catch (err) {
777
+ if (err.name === 'AbortError') {
778
+ console.log('Petición cancelada por el usuario.');
779
+ const finalStats = getStats();
780
+ const finalToolCalls = getFinalToolCalls();
781
+ if (onDone) await onDone(accumulatedText || '(Generación detenida)', finalStats, finalToolCalls);
782
+ return;
783
+ }
784
+
785
+ console.error('Error en streamChatCompletion:', err);
786
+ if (onError) onError(err);
787
+ }
788
+ }
789
+
790
+ /**
791
+ * Obtiene las capacidades del proveedor resuelto para una URL o tipo de API.
792
+ */
793
+ function getProviderCapabilities(rawUrl, explicitType, model) {
794
+ if (registry) {
795
+ return registry.getCapabilities(rawUrl, model, explicitType);
796
+ }
797
+ return {
798
+ streaming: true,
799
+ vision: true,
800
+ tools: true,
801
+ reasoning: true,
802
+ jsonMode: true,
803
+ promptCaching: true,
804
+ embeddings: true,
805
+ modelListing: true
806
+ };
807
+ }
808
+
809
+ /**
810
+ * Diagnostica el endpoint del proveedor y analiza sus capacidades declaradas, inferidas y comprobadas.
811
+ */
812
+ async function inspectProvider(config = {}, options = {}) {
813
+ const { apiUrl, apiType, apiKey, model } = config;
814
+ const effectiveApiKey = String(apiKey || '').trim();
815
+ if (registry && registry.inspect) {
816
+ return registry.inspect(apiUrl, effectiveApiKey, model, apiType, options);
817
+ }
818
+ const adapter = registry ? registry.resolve(apiUrl, apiType) : null;
819
+ if (adapter && adapter.inspect) {
820
+ return adapter.inspect({ ...options, apiUrl, apiKey: effectiveApiKey, model });
821
+ }
822
+ return {
823
+ success: false,
824
+ error: 'No se pudo inicializar el inspector de proveedores.'
825
+ };
826
+ }
827
+
828
+ return {
829
+ detectApiType,
830
+ normalizeApiUrl,
831
+ fetchServerModels,
832
+ downloadLocalModel,
833
+ deleteLocalModel,
834
+ cancelLocalModelOperation,
835
+ getStandardReasoningOptions,
836
+ STANDARD_REASONING_MODES,
837
+ streamChatCompletion,
838
+ estimateTokens,
839
+ normalizeToolName,
840
+ getProviderCapabilities,
841
+ inspectProvider,
842
+ registry
843
+ };
844
+ });