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.
- zerochat-7.3.0.dist-info/METADATA +70 -0
- zerochat-7.3.0.dist-info/RECORD +130 -0
- zerochat-7.3.0.dist-info/WHEEL +5 -0
- zerochat-7.3.0.dist-info/entry_points.txt +2 -0
- zerochat-7.3.0.dist-info/licenses/LICENSE +34 -0
- zerochat-7.3.0.dist-info/top_level.txt +2 -0
- zerochat.py +2004 -0
- zerochat_runtime/__init__.py +1 -0
- zerochat_runtime/assets/css/base.css +199 -0
- zerochat_runtime/assets/css/components/composer.css +1258 -0
- zerochat_runtime/assets/css/components/debug.css +777 -0
- zerochat_runtime/assets/css/components/header.css +97 -0
- zerochat_runtime/assets/css/components/icons.css +39 -0
- zerochat_runtime/assets/css/components/markdown.css +685 -0
- zerochat_runtime/assets/css/components/messages.css +473 -0
- zerochat_runtime/assets/css/components/modals.css +1319 -0
- zerochat_runtime/assets/css/components/sidebar.css +622 -0
- zerochat_runtime/assets/css/components/tools.css +1223 -0
- zerochat_runtime/assets/css/layout.css +171 -0
- zerochat_runtime/assets/css/print.css +120 -0
- zerochat_runtime/assets/css/styles.css +18 -0
- zerochat_runtime/assets/css/theme-overrides.css +555 -0
- zerochat_runtime/assets/css/tokens.css +165 -0
- zerochat_runtime/assets/help/architecture.html +247 -0
- zerochat_runtime/assets/help/composer.html +134 -0
- zerochat_runtime/assets/help/conversations.html +105 -0
- zerochat_runtime/assets/help/debug.html +108 -0
- zerochat_runtime/assets/help/en/architecture.html +247 -0
- zerochat_runtime/assets/help/en/composer.html +134 -0
- zerochat_runtime/assets/help/en/conversations.html +104 -0
- zerochat_runtime/assets/help/en/debug.html +105 -0
- zerochat_runtime/assets/help/en/gemini-free.html +39 -0
- zerochat_runtime/assets/help/en/index.html +191 -0
- zerochat_runtime/assets/help/en/learning.html +71 -0
- zerochat_runtime/assets/help/en/mcp.html +143 -0
- zerochat_runtime/assets/help/en/openrouter-free.html +39 -0
- zerochat_runtime/assets/help/en/profiles.html +173 -0
- zerochat_runtime/assets/help/en/rag.html +130 -0
- zerochat_runtime/assets/help/en/reasoning-telemetry.html +108 -0
- zerochat_runtime/assets/help/en/tools-agent.html +133 -0
- zerochat_runtime/assets/help/en/webllm.html +355 -0
- zerochat_runtime/assets/help/gemini-free.html +39 -0
- zerochat_runtime/assets/help/help.css +604 -0
- zerochat_runtime/assets/help/help.js +51 -0
- zerochat_runtime/assets/help/index.html +194 -0
- zerochat_runtime/assets/help/learning.html +71 -0
- zerochat_runtime/assets/help/mcp.html +143 -0
- zerochat_runtime/assets/help/openrouter-free.html +39 -0
- zerochat_runtime/assets/help/profiles.html +174 -0
- zerochat_runtime/assets/help/rag.html +130 -0
- zerochat_runtime/assets/help/reasoning-telemetry.html +111 -0
- zerochat_runtime/assets/help/tools-agent.html +133 -0
- zerochat_runtime/assets/help/webllm.html +359 -0
- zerochat_runtime/assets/js/agent-core.js +1553 -0
- zerochat_runtime/assets/js/api.js +844 -0
- zerochat_runtime/assets/js/app.js +2558 -0
- zerochat_runtime/assets/js/attachments.js +220 -0
- zerochat_runtime/assets/js/charts.js +338 -0
- zerochat_runtime/assets/js/chat-engine.js +566 -0
- zerochat_runtime/assets/js/config-store.js +207 -0
- zerochat_runtime/assets/js/context-manager.js +584 -0
- zerochat_runtime/assets/js/conversation-service.js +484 -0
- zerochat_runtime/assets/js/cookies.js +688 -0
- zerochat_runtime/assets/js/data-reset-service.js +136 -0
- zerochat_runtime/assets/js/debug.js +508 -0
- zerochat_runtime/assets/js/defaults.js +16 -0
- zerochat_runtime/assets/js/export.js +179 -0
- zerochat_runtime/assets/js/file-parser.js +2088 -0
- zerochat_runtime/assets/js/generation-controller.js +417 -0
- zerochat_runtime/assets/js/i18n.js +1483 -0
- zerochat_runtime/assets/js/icons.js +156 -0
- zerochat_runtime/assets/js/ingestionEngine.js +436 -0
- zerochat_runtime/assets/js/markdown.js +588 -0
- zerochat_runtime/assets/js/mcp.js +1271 -0
- zerochat_runtime/assets/js/message-turns.js +62 -0
- zerochat_runtime/assets/js/profile-backup.js +118 -0
- zerochat_runtime/assets/js/profile-export-bundle.js +481 -0
- zerochat_runtime/assets/js/profile-repository.js +213 -0
- zerochat_runtime/assets/js/providers-webllm.js +486 -0
- zerochat_runtime/assets/js/providers.js +1560 -0
- zerochat_runtime/assets/js/rag-index.js +295 -0
- zerochat_runtime/assets/js/rag-service.js +530 -0
- zerochat_runtime/assets/js/rag-ui.js +997 -0
- zerochat_runtime/assets/js/ragStorage.js +676 -0
- zerochat_runtime/assets/js/sandbox.js +449 -0
- zerochat_runtime/assets/js/state.js +704 -0
- zerochat_runtime/assets/js/storage-db.js +144 -0
- zerochat_runtime/assets/js/tool-cards.js +330 -0
- zerochat_runtime/assets/js/tool-security.js +653 -0
- zerochat_runtime/assets/js/tools/README.md +50 -0
- zerochat_runtime/assets/js/tools/builtin/agent-checkpoint.tool.js +212 -0
- zerochat_runtime/assets/js/tools/builtin/download-pdf.tool.js +111 -0
- zerochat_runtime/assets/js/tools/builtin/execute-javascript.tool.js +219 -0
- zerochat_runtime/assets/js/tools/builtin/fetch-web-page.tool.js +112 -0
- zerochat_runtime/assets/js/tools/builtin/list-documents.tool.js +117 -0
- zerochat_runtime/assets/js/tools/builtin/read-knowledge-chunk.tool.js +129 -0
- zerochat_runtime/assets/js/tools/builtin/read-knowledge-image.tool.js +95 -0
- zerochat_runtime/assets/js/tools/builtin/render-chart.tool.js +90 -0
- zerochat_runtime/assets/js/tools/builtin/search-knowledge-base.tool.js +124 -0
- zerochat_runtime/assets/js/tools/builtin/search-web.tool.js +125 -0
- zerochat_runtime/assets/js/tools/tool-manifest.js +53 -0
- zerochat_runtime/assets/js/tools/tool-runtime.js +77 -0
- zerochat_runtime/assets/js/ui-composer.js +319 -0
- zerochat_runtime/assets/js/ui-conversation.js +655 -0
- zerochat_runtime/assets/js/ui-dialogs.js +132 -0
- zerochat_runtime/assets/js/ui-generation-status.js +119 -0
- zerochat_runtime/assets/js/ui-inspector.js +804 -0
- zerochat_runtime/assets/js/ui-mcp.js +604 -0
- zerochat_runtime/assets/js/ui-profiles.js +824 -0
- zerochat_runtime/assets/js/ui-reasoning.js +146 -0
- zerochat_runtime/assets/js/ui-settings.js +825 -0
- zerochat_runtime/assets/js/ui-shell.js +215 -0
- zerochat_runtime/assets/js/ui-sidebar.js +429 -0
- zerochat_runtime/assets/js/ui-telemetry.js +404 -0
- zerochat_runtime/assets/js/ui-transfer.js +262 -0
- zerochat_runtime/assets/js/utils.js +216 -0
- zerochat_runtime/assets/js/vendor/orama.browser.js +11 -0
- zerochat_runtime/assets/js/web-browser.js +569 -0
- zerochat_runtime/assets/js/web-search.js +519 -0
- zerochat_runtime/assets/manifest.webmanifest +20 -0
- zerochat_runtime/assets/services/dummy_mcp/dummy_mcp_server.py +47 -0
- zerochat_runtime/assets/services/dummy_mcp/service.json +22 -0
- zerochat_runtime/assets/services/lsp/installer.json +9 -0
- zerochat_runtime/assets/services/lsp/service.json +22 -0
- zerochat_runtime/assets/services/memory/installer.json +9 -0
- zerochat_runtime/assets/services/memory/service.json +24 -0
- zerochat_runtime/assets/services/playwright/installer.json +10 -0
- zerochat_runtime/assets/services/playwright/service.json +39 -0
- zerochat_runtime/assets/sw.js +164 -0
- 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
|
+
});
|