libre-webui 0.14.3 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -33
- package/backend/dist/db.d.ts.map +1 -1
- package/backend/dist/db.js +84 -0
- package/backend/dist/db.js.map +1 -1
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +99 -35
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/middleware/auth.d.ts.map +1 -1
- package/backend/dist/middleware/auth.js +27 -2
- package/backend/dist/middleware/auth.js.map +1 -1
- package/backend/dist/routes/plugins.d.ts.map +1 -1
- package/backend/dist/routes/plugins.js +1 -1
- package/backend/dist/routes/plugins.js.map +1 -1
- package/backend/dist/routes/users.d.ts.map +1 -1
- package/backend/dist/routes/users.js +40 -6
- package/backend/dist/routes/users.js.map +1 -1
- package/backend/dist/routes/work.d.ts +3 -0
- package/backend/dist/routes/work.d.ts.map +1 -0
- package/backend/dist/routes/work.js +604 -0
- package/backend/dist/routes/work.js.map +1 -0
- package/backend/dist/services/ollamaService.d.ts +7 -2
- package/backend/dist/services/ollamaService.d.ts.map +1 -1
- package/backend/dist/services/ollamaService.js +8 -8
- package/backend/dist/services/ollamaService.js.map +1 -1
- package/backend/dist/services/pluginService.d.ts.map +1 -1
- package/backend/dist/services/pluginService.js +15 -15
- package/backend/dist/services/pluginService.js.map +1 -1
- package/backend/dist/services/preferencesService.d.ts.map +1 -1
- package/backend/dist/services/preferencesService.js +1 -0
- package/backend/dist/services/preferencesService.js.map +1 -1
- package/backend/dist/services/workAgentGuidance.d.ts +24 -0
- package/backend/dist/services/workAgentGuidance.d.ts.map +1 -0
- package/backend/dist/services/workAgentGuidance.js +131 -0
- package/backend/dist/services/workAgentGuidance.js.map +1 -0
- package/backend/dist/services/workAgentService.d.ts +33 -0
- package/backend/dist/services/workAgentService.d.ts.map +1 -0
- package/backend/dist/services/workAgentService.js +935 -0
- package/backend/dist/services/workAgentService.js.map +1 -0
- package/backend/dist/services/workEventService.d.ts +40 -0
- package/backend/dist/services/workEventService.d.ts.map +1 -0
- package/backend/dist/services/workEventService.js +324 -0
- package/backend/dist/services/workEventService.js.map +1 -0
- package/backend/dist/services/workModelProviderService.d.ts +51 -0
- package/backend/dist/services/workModelProviderService.d.ts.map +1 -0
- package/backend/dist/services/workModelProviderService.js +952 -0
- package/backend/dist/services/workModelProviderService.js.map +1 -0
- package/backend/dist/services/workRuntimeService.d.ts +143 -0
- package/backend/dist/services/workRuntimeService.d.ts.map +1 -0
- package/backend/dist/services/workRuntimeService.js +1860 -0
- package/backend/dist/services/workRuntimeService.js.map +1 -0
- package/backend/dist/services/workTaskService.d.ts +96 -0
- package/backend/dist/services/workTaskService.d.ts.map +1 -0
- package/backend/dist/services/workTaskService.js +718 -0
- package/backend/dist/services/workTaskService.js.map +1 -0
- package/backend/dist/types/index.d.ts +3 -0
- package/backend/dist/types/index.d.ts.map +1 -1
- package/backend/dist/types/index.js.map +1 -1
- package/backend/dist/types/work.d.ts +209 -0
- package/backend/dist/types/work.d.ts.map +1 -0
- package/backend/dist/types/work.js +18 -0
- package/backend/dist/types/work.js.map +1 -0
- package/backend/dist/utils/ollamaLibrary.d.ts.map +1 -1
- package/backend/dist/utils/ollamaLibrary.js +81 -8
- package/backend/dist/utils/ollamaLibrary.js.map +1 -1
- package/backend/dist/utils/pluginChatAdapter.d.ts +9 -0
- package/backend/dist/utils/pluginChatAdapter.d.ts.map +1 -1
- package/backend/dist/utils/pluginChatAdapter.js +32 -6
- package/backend/dist/utils/pluginChatAdapter.js.map +1 -1
- package/backend/dist/utils/pluginStreamAdapter.d.ts +12 -0
- package/backend/dist/utils/pluginStreamAdapter.d.ts.map +1 -1
- package/backend/dist/utils/pluginStreamAdapter.js +168 -19
- package/backend/dist/utils/pluginStreamAdapter.js.map +1 -1
- package/backend/dist/utils/pluginStreaming.d.ts +10 -0
- package/backend/dist/utils/pluginStreaming.d.ts.map +1 -1
- package/backend/dist/utils/pluginStreaming.js.map +1 -1
- package/backend/dist/utils/pluginValidation.d.ts +1 -0
- package/backend/dist/utils/pluginValidation.d.ts.map +1 -1
- package/backend/dist/utils/pluginValidation.js +3 -0
- package/backend/dist/utils/pluginValidation.js.map +1 -1
- package/backend/package.json +1 -1
- package/electron/main.js +39 -16
- package/frontend/dist/assets/index-DkShhNBf.js +5 -0
- package/frontend/dist/assets/index-bdSWBsAM.css +1 -0
- package/frontend/dist/index.html +5 -4
- package/frontend/dist/js/{ArtifactContainer-CuvIIoHC.js → ArtifactContainer-BHSBrlQB.js} +1 -1
- package/frontend/dist/js/{ArtifactDemoPage-DI0QzAee.js → ArtifactDemoPage-BHGJmJAT.js} +1 -1
- package/frontend/dist/js/ArtifactSlideOutPanel-s8vcE5Ea.js +1 -0
- package/frontend/dist/js/ChatPage-B6Hm_pdu.js +16 -0
- package/frontend/dist/js/FirstTimeSetup-BYWAVAdE.js +1 -0
- package/frontend/dist/js/GalleryPage-CH4f65q8.js +1 -0
- package/frontend/dist/js/LibreClawPage-CcH7M3mJ.js +1 -0
- package/frontend/dist/js/{ModelManagerModals-DfgizeSq.js → ModelManagerModals-DltRprHU.js} +1 -1
- package/frontend/dist/js/ModelSelector-T1KZtIde.js +1 -0
- package/frontend/dist/js/ModelsPage-C1bM_Yoc.js +2 -0
- package/frontend/dist/js/PersonasPage-T_PrhnPG.js +13 -0
- package/frontend/dist/js/RichMessageContent-BdU5vXz9.js +3 -0
- package/frontend/dist/js/SettingsModal-BVj-D1L4.js +1 -0
- package/frontend/dist/js/UserManagementPage-Pix71oRW.js +1 -0
- package/frontend/dist/js/WorkPage-DdOVGyj_.js +2 -0
- package/frontend/dist/js/api-Do6u0tMM.js +19 -0
- package/frontend/dist/js/{ar-UnnZB1Qy.js → ar-KceI0PfT.js} +2 -2
- package/frontend/dist/js/babel-BnLuavNv.js +16 -0
- package/frontend/dist/js/{bn-uZXaDyeV.js → bn-DsKo2dzS.js} +2 -2
- package/frontend/dist/js/{cs-CCXlnUEB.js → cs-6UKhKj0A.js} +2 -2
- package/frontend/dist/js/{da--BpVVGPp.js → da-CJgZ3YuL.js} +2 -2
- package/frontend/dist/js/{de-DZd9lsBc.js → de-BZAmOuCT.js} +2 -2
- package/frontend/dist/js/{es-Bm968oUX.js → es-CD4u7no9.js} +2 -2
- package/frontend/dist/js/estree-Bu_-Wi6O.js +44 -0
- package/frontend/dist/js/fr-Cou2jOS3.js +2 -0
- package/frontend/dist/js/{hi-C6kwkL7d.js → hi-ZxkbNCyB.js} +2 -2
- package/frontend/dist/js/html-Ca764Rus.js +25 -0
- package/frontend/dist/js/{id-CDfqnnXS.js → id-DT9CqaWQ.js} +2 -2
- package/frontend/dist/js/{is-Dnd_7Jqv.js → is-BWSbAIoq.js} +2 -2
- package/frontend/dist/js/{it-BM0j6_Sz.js → it-CdNoTXm3.js} +2 -2
- package/frontend/dist/js/{ja-xGblKDDf.js → ja-CIuQg0xv.js} +2 -2
- package/frontend/dist/js/{ko-CQ2d_Oj0.js → ko-BWRUjDOH.js} +2 -2
- package/frontend/dist/js/markdown-DaiQGPEs.js +70 -0
- package/frontend/dist/js/{ms-CFpRuv_a.js → ms-gJstIj2a.js} +2 -2
- package/frontend/dist/js/{nl-D6oTztmP.js → nl-CydjVNUw.js} +2 -2
- package/frontend/dist/js/{pl-ByuvABul.js → pl-DPN9mAa5.js} +2 -2
- package/frontend/dist/js/postcss-DSDkwgFE.js +62 -0
- package/frontend/dist/js/{pt-CNFV21T7.js → pt-MRXGqIjQ.js} +2 -2
- package/frontend/dist/js/router-vendor-BjDI9mRe.js +1 -0
- package/frontend/dist/js/{ru-CRsSOntz.js → ru-CvQ9GkJF.js} +2 -2
- package/frontend/dist/js/standalone-DWWnsbIr.js +29 -0
- package/frontend/dist/js/{sv-BRkwjyS5.js → sv-BxS2fvhU.js} +2 -2
- package/frontend/dist/js/{th-DigqsQEB.js → th-DMxyn6e4.js} +2 -2
- package/frontend/dist/js/{tr-B5aMUMJE.js → tr-BTPldUaA.js} +2 -2
- package/frontend/dist/js/typescript-CatjJeHm.js +22 -0
- package/frontend/dist/js/ui-vendor-CAFEDSaO.js +177 -0
- package/frontend/dist/js/{uk-BUYi3llT.js → uk-BGxZOKgp.js} +2 -2
- package/frontend/dist/js/useBaseQuery-CVNuhuID.js +1 -0
- package/frontend/dist/js/useQuery-CYhC_da4.js +1 -0
- package/frontend/dist/js/{vi-hQIcXi3m.js → vi-82JkwFsQ.js} +2 -2
- package/frontend/dist/js/yaml-mLy9chOK.js +154 -0
- package/frontend/dist/js/{zh-CUDZ3JDv.js → zh-Bxxfwci3.js} +2 -2
- package/package.json +4 -2
- package/plugins/kimi-code.json +6 -37
- package/plugins/mlx-lm.json +55 -0
- package/frontend/dist/assets/index-BKzwVaAM.js +0 -16
- package/frontend/dist/assets/index-BPvF-fpc.css +0 -1
- package/frontend/dist/js/ArtifactSlideOutPanel-BzBemzHy.js +0 -1
- package/frontend/dist/js/ChatPage-DqpXctC2.js +0 -16
- package/frontend/dist/js/FirstTimeSetup-DRWeCsVI.js +0 -1
- package/frontend/dist/js/GalleryPage-CgD8fVx0.js +0 -1
- package/frontend/dist/js/LibreClawPage-B_Yon2fQ.js +0 -1
- package/frontend/dist/js/ModelsPage-CAKQ7J-b.js +0 -2
- package/frontend/dist/js/PersonasPage-Dj6Bka4I.js +0 -13
- package/frontend/dist/js/RichMessageContent-DP7XoxIa.js +0 -3
- package/frontend/dist/js/SettingsModal-BFyYhZYE.js +0 -1
- package/frontend/dist/js/UserManagementPage-CGMnhZr1.js +0 -1
- package/frontend/dist/js/fr-RlC0rD8I.js +0 -2
- package/frontend/dist/js/router-vendor-Bbvzv1ZC.js +0 -1
- package/frontend/dist/js/ui-vendor-CurEAeCe.js +0 -177
- package/frontend/dist/js/useBaseQuery-B1on3xcz.js +0 -1
- package/frontend/dist/js/useQuery-CXvo2RBT.js +0 -1
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Libre WebUI
|
|
3
|
+
* Copyright (C) 2025 Kroonen AI, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import axios from 'axios';
|
|
18
|
+
import { getOpenAICompatibleSamplingParameters, resolvePluginChatParameters, } from '../utils/pluginChatAdapter.js';
|
|
19
|
+
import { streamAnthropicResponse, streamOpenAICompatibleResponse, } from '../utils/pluginStreamAdapter.js';
|
|
20
|
+
import { applyModelEndpointTemplate, assertSafePluginEndpoint, buildPluginAuthHeaders, pluginRequiresApiKey, resolvePluginEndpoint, validatePluginModel, } from '../utils/pluginValidation.js';
|
|
21
|
+
import ollamaService from './ollamaService.js';
|
|
22
|
+
import pluginService from './pluginService.js';
|
|
23
|
+
export const WORK_TOOL_ARGUMENTS_ERROR_METADATA_KEY = 'libreToolArgumentsError';
|
|
24
|
+
export const WORK_TOOL_ARGUMENTS_ERROR_MESSAGE = 'The provider returned incomplete or invalid JSON for this tool call, likely because its output-token limit was reached. Retry with a smaller payload; split large write_file content into focused files.';
|
|
25
|
+
export class WorkModelProviderError extends Error {
|
|
26
|
+
constructor(message, status, code) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'WorkModelProviderError';
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.code = code;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class WorkModelProviderService {
|
|
34
|
+
constructor(dependencies = {
|
|
35
|
+
ollama: ollamaService,
|
|
36
|
+
plugins: pluginService,
|
|
37
|
+
post: axios.post.bind(axios),
|
|
38
|
+
}) {
|
|
39
|
+
this.dependencies = dependencies;
|
|
40
|
+
}
|
|
41
|
+
async availability(userId) {
|
|
42
|
+
const [ollamaAvailable, pluginAvailable] = await Promise.all([
|
|
43
|
+
this.dependencies.ollama.isHealthy(),
|
|
44
|
+
Promise.resolve(this.hasConfiguredPlugin(userId)),
|
|
45
|
+
]);
|
|
46
|
+
return { ollamaAvailable, pluginAvailable };
|
|
47
|
+
}
|
|
48
|
+
async assertModelSupportsTools(model, provider, userId) {
|
|
49
|
+
const cleaned = model.trim();
|
|
50
|
+
if (!cleaned) {
|
|
51
|
+
throw new WorkModelProviderError('A Work model is required.', 422, 'WORK_MODEL_TOOLS_UNSUPPORTED');
|
|
52
|
+
}
|
|
53
|
+
if (provider.providerType === 'plugin') {
|
|
54
|
+
this.requireExactPlugin(provider.providerId, cleaned, userId);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
assertOllamaProvider(provider);
|
|
58
|
+
let details;
|
|
59
|
+
try {
|
|
60
|
+
details = await this.dependencies.ollama.showModel(cleaned, false);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new WorkModelProviderError(error instanceof Error ? error.message : 'Could not inspect model.', 503, 'WORK_MODEL_UNAVAILABLE');
|
|
64
|
+
}
|
|
65
|
+
const capabilities = Array.isArray(details.capabilities)
|
|
66
|
+
? details.capabilities.map(value => String(value).toLowerCase())
|
|
67
|
+
: [];
|
|
68
|
+
if (!capabilities.includes('tools')) {
|
|
69
|
+
throw new WorkModelProviderError(`Model "${cleaned}" does not advertise tool support.`, 422, 'WORK_MODEL_TOOLS_UNSUPPORTED');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async generateChatResponse(request, provider, userId, signal) {
|
|
73
|
+
if (provider.providerType === 'ollama') {
|
|
74
|
+
assertOllamaProvider(provider);
|
|
75
|
+
return this.dependencies.ollama.generateChatResponse(request, signal);
|
|
76
|
+
}
|
|
77
|
+
const plugin = this.requireExactPlugin(provider.providerId, request.model, userId);
|
|
78
|
+
return this.generatePluginResponse(plugin, request, userId, signal);
|
|
79
|
+
}
|
|
80
|
+
async generateChatStreamResponse(request, provider, userId, observer, signal) {
|
|
81
|
+
const streamRequest = { ...request, stream: true };
|
|
82
|
+
if (provider.providerType === 'ollama') {
|
|
83
|
+
assertOllamaProvider(provider);
|
|
84
|
+
return this.generateOllamaStream(streamRequest, observer, signal);
|
|
85
|
+
}
|
|
86
|
+
const plugin = this.requireExactPlugin(provider.providerId, request.model, userId);
|
|
87
|
+
return this.generatePluginStream(plugin, streamRequest, userId, observer, signal);
|
|
88
|
+
}
|
|
89
|
+
hasConfiguredPlugin(userId) {
|
|
90
|
+
return this.dependencies.plugins
|
|
91
|
+
.getActivePlugins()
|
|
92
|
+
.filter(isWorkPlugin)
|
|
93
|
+
.some(plugin => plugin.model_map.length > 0 &&
|
|
94
|
+
(!pluginRequiresApiKey(plugin) ||
|
|
95
|
+
Boolean(this.dependencies.plugins.getApiKey(plugin, userId))));
|
|
96
|
+
}
|
|
97
|
+
requireExactPlugin(providerId, model, userId) {
|
|
98
|
+
const cleanedProviderId = providerId?.trim();
|
|
99
|
+
if (!cleanedProviderId) {
|
|
100
|
+
throw new WorkModelProviderError('A plugin provider ID is required.', 400, 'WORK_PLUGIN_ID_REQUIRED');
|
|
101
|
+
}
|
|
102
|
+
const plugin = this.dependencies.plugins.getPlugin(cleanedProviderId);
|
|
103
|
+
if (!plugin || !plugin.active) {
|
|
104
|
+
throw new WorkModelProviderError(`Plugin "${cleanedProviderId}" is not active.`, 422, 'WORK_PLUGIN_UNAVAILABLE');
|
|
105
|
+
}
|
|
106
|
+
assertWorkPluginType(plugin);
|
|
107
|
+
if (!plugin.model_map.includes(model)) {
|
|
108
|
+
throw new WorkModelProviderError(`Model "${model}" is not configured for plugin "${plugin.id}".`, 422, 'WORK_PLUGIN_MODEL_UNAVAILABLE');
|
|
109
|
+
}
|
|
110
|
+
if (pluginRequiresApiKey(plugin) &&
|
|
111
|
+
!this.dependencies.plugins.getApiKey(plugin, userId)) {
|
|
112
|
+
throw new WorkModelProviderError(`API key not found for plugin ${plugin.id}.`, 422, 'WORK_PLUGIN_CREDENTIALS_MISSING');
|
|
113
|
+
}
|
|
114
|
+
return plugin;
|
|
115
|
+
}
|
|
116
|
+
async generatePluginResponse(plugin, request, userId, signal) {
|
|
117
|
+
validatePluginModel(request.model);
|
|
118
|
+
const apiKey = this.dependencies.plugins.getApiKey(plugin, userId);
|
|
119
|
+
if (pluginRequiresApiKey(plugin) && !apiKey) {
|
|
120
|
+
throw new WorkModelProviderError(`API key not found for plugin ${plugin.id}.`, 422, 'WORK_PLUGIN_CREDENTIALS_MISSING');
|
|
121
|
+
}
|
|
122
|
+
const variables = this.dependencies.plugins.getPluginVariables(plugin, userId);
|
|
123
|
+
const endpoint = applyModelEndpointTemplate(resolvePluginEndpoint(plugin.endpoint, variables.endpoint), request.model);
|
|
124
|
+
assertSafePluginEndpoint(endpoint, 'Work model endpoint');
|
|
125
|
+
const headers = buildPluginAuthHeaders(plugin, apiKey);
|
|
126
|
+
const { payload, extraHeaders } = buildPluginWorkPayload(plugin, request, variables);
|
|
127
|
+
Object.assign(headers, extraHeaders);
|
|
128
|
+
try {
|
|
129
|
+
const response = await this.dependencies.post(endpoint, payload, {
|
|
130
|
+
headers,
|
|
131
|
+
signal,
|
|
132
|
+
timeout: 300000,
|
|
133
|
+
});
|
|
134
|
+
return normalizePluginWorkResponse(plugin, response.data, request.model);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (signal?.aborted)
|
|
138
|
+
throw error;
|
|
139
|
+
const message = axios.isAxiosError(error) && error.response
|
|
140
|
+
? `Plugin API error: ${error.response.status} - ${providerErrorMessage(error.response.data)}`
|
|
141
|
+
: error instanceof Error
|
|
142
|
+
? error.message
|
|
143
|
+
: 'Plugin request failed.';
|
|
144
|
+
throw new WorkModelProviderError(message, 502, 'WORK_PLUGIN_REQUEST_FAILED');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async generateOllamaStream(request, observer, signal) {
|
|
148
|
+
const stream = this.dependencies.ollama.generateChatStreamResponse;
|
|
149
|
+
if (!stream) {
|
|
150
|
+
const response = await this.dependencies.ollama.generateChatResponse({ ...request, stream: false }, signal);
|
|
151
|
+
if (response.message?.thinking) {
|
|
152
|
+
observer.onReasoning?.(response.message.thinking);
|
|
153
|
+
}
|
|
154
|
+
if (response.message?.content) {
|
|
155
|
+
observer.onContent?.(response.message.content);
|
|
156
|
+
}
|
|
157
|
+
observer.onUsage?.(ollamaUsage(response));
|
|
158
|
+
return response;
|
|
159
|
+
}
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
let settled = false;
|
|
162
|
+
let content = '';
|
|
163
|
+
let reasoning = '';
|
|
164
|
+
let latest;
|
|
165
|
+
const toolCalls = [];
|
|
166
|
+
const finish = (error) => {
|
|
167
|
+
if (settled)
|
|
168
|
+
return;
|
|
169
|
+
settled = true;
|
|
170
|
+
if (error) {
|
|
171
|
+
reject(error);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const response = {
|
|
175
|
+
...(latest || {
|
|
176
|
+
model: request.model,
|
|
177
|
+
created_at: new Date().toISOString(),
|
|
178
|
+
done: true,
|
|
179
|
+
}),
|
|
180
|
+
message: {
|
|
181
|
+
role: 'assistant',
|
|
182
|
+
content,
|
|
183
|
+
...(reasoning ? { thinking: reasoning } : {}),
|
|
184
|
+
...(toolCalls.length ? { tool_calls: toolCalls } : {}),
|
|
185
|
+
},
|
|
186
|
+
done: true,
|
|
187
|
+
};
|
|
188
|
+
observer.onUsage?.(ollamaUsage(response));
|
|
189
|
+
resolve(response);
|
|
190
|
+
};
|
|
191
|
+
void stream
|
|
192
|
+
.call(this.dependencies.ollama, request, chunk => {
|
|
193
|
+
latest = chunk;
|
|
194
|
+
const contentDelta = chunk.message?.content || '';
|
|
195
|
+
const reasoningDelta = chunk.message?.thinking || '';
|
|
196
|
+
if (contentDelta) {
|
|
197
|
+
content += contentDelta;
|
|
198
|
+
observer.onContent?.(contentDelta);
|
|
199
|
+
}
|
|
200
|
+
if (reasoningDelta) {
|
|
201
|
+
reasoning += reasoningDelta;
|
|
202
|
+
observer.onReasoning?.(reasoningDelta);
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(chunk.message?.tool_calls)) {
|
|
205
|
+
for (const call of chunk.message.tool_calls) {
|
|
206
|
+
toolCalls.push(call);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (chunk.done)
|
|
210
|
+
finish();
|
|
211
|
+
}, error => finish(error), () => finish(), signal)
|
|
212
|
+
.catch(error => finish(error instanceof Error ? error : new Error(String(error))));
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async generatePluginStream(plugin, request, userId, observer, signal) {
|
|
216
|
+
validatePluginModel(request.model);
|
|
217
|
+
const apiKey = this.dependencies.plugins.getApiKey(plugin, userId);
|
|
218
|
+
if (pluginRequiresApiKey(plugin) && !apiKey) {
|
|
219
|
+
throw new WorkModelProviderError(`API key not found for plugin ${plugin.id}.`, 422, 'WORK_PLUGIN_CREDENTIALS_MISSING');
|
|
220
|
+
}
|
|
221
|
+
const variables = this.dependencies.plugins.getPluginVariables(plugin, userId);
|
|
222
|
+
let endpoint = applyModelEndpointTemplate(resolvePluginEndpoint(plugin.endpoint, variables.endpoint), request.model);
|
|
223
|
+
if (plugin.id === 'gemini') {
|
|
224
|
+
endpoint = geminiStreamingEndpoint(endpoint);
|
|
225
|
+
}
|
|
226
|
+
assertSafePluginEndpoint(endpoint, 'Work model endpoint');
|
|
227
|
+
const headers = buildPluginAuthHeaders(plugin, apiKey);
|
|
228
|
+
const { payload, extraHeaders } = buildPluginWorkPayload(plugin, { ...request, stream: true }, variables);
|
|
229
|
+
Object.assign(headers, extraHeaders);
|
|
230
|
+
const timeoutSignal = AbortSignal.timeout(300000);
|
|
231
|
+
const requestSignal = signal
|
|
232
|
+
? AbortSignal.any([signal, timeoutSignal])
|
|
233
|
+
: timeoutSignal;
|
|
234
|
+
try {
|
|
235
|
+
const response = await fetch(endpoint, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers,
|
|
238
|
+
body: JSON.stringify(payload),
|
|
239
|
+
signal: requestSignal,
|
|
240
|
+
});
|
|
241
|
+
const contentType = response.headers.get('content-type') || '';
|
|
242
|
+
if (response.ok &&
|
|
243
|
+
!contentType.includes('text/event-stream') &&
|
|
244
|
+
!contentType.includes('application/x-ndjson')) {
|
|
245
|
+
const data = (await response.json());
|
|
246
|
+
const normalized = normalizePluginWorkResponse(plugin, data, request.model);
|
|
247
|
+
if (normalized.message.thinking) {
|
|
248
|
+
observer.onReasoning?.(normalized.message.thinking);
|
|
249
|
+
}
|
|
250
|
+
if (normalized.message.content) {
|
|
251
|
+
observer.onContent?.(normalized.message.content);
|
|
252
|
+
}
|
|
253
|
+
return normalized;
|
|
254
|
+
}
|
|
255
|
+
const chunks = plugin.id === 'anthropic'
|
|
256
|
+
? streamAnthropicResponse(response)
|
|
257
|
+
: plugin.id === 'gemini'
|
|
258
|
+
? streamGeminiWorkResponse(response)
|
|
259
|
+
: streamOpenAICompatibleResponse(response);
|
|
260
|
+
return await collectPluginWorkStream(chunks, request.model, observer);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (signal?.aborted)
|
|
264
|
+
throw error;
|
|
265
|
+
if (timeoutSignal.aborted) {
|
|
266
|
+
throw new WorkModelProviderError('Plugin request timed out after 300 seconds.', 504, 'WORK_PLUGIN_REQUEST_TIMEOUT');
|
|
267
|
+
}
|
|
268
|
+
if (error instanceof WorkModelProviderError)
|
|
269
|
+
throw error;
|
|
270
|
+
throw new WorkModelProviderError(error instanceof Error ? error.message : 'Plugin request failed.', 502, 'WORK_PLUGIN_REQUEST_FAILED');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
export function buildPluginWorkPayload(plugin, request, variables = {}) {
|
|
275
|
+
const options = (request.options || {});
|
|
276
|
+
const params = resolvePluginChatParameters(options, variables);
|
|
277
|
+
if (plugin.id === 'anthropic') {
|
|
278
|
+
return {
|
|
279
|
+
payload: buildAnthropicWorkPayload(request.model, request.messages, request.tools || [], params.maxTokens, Boolean(request.stream)),
|
|
280
|
+
extraHeaders: { 'anthropic-version': '2023-06-01' },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (plugin.id === 'gemini') {
|
|
284
|
+
return {
|
|
285
|
+
payload: buildGeminiWorkPayload(request.messages, request.tools || [], params),
|
|
286
|
+
extraHeaders: {},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
payload: {
|
|
291
|
+
model: request.model,
|
|
292
|
+
messages: toOpenAIWorkMessages(request.messages),
|
|
293
|
+
tools: request.tools,
|
|
294
|
+
tool_choice: request.tools?.length ? 'auto' : undefined,
|
|
295
|
+
...getOpenAICompatibleSamplingParameters(plugin, params),
|
|
296
|
+
max_tokens: params.maxTokens,
|
|
297
|
+
stream: Boolean(request.stream),
|
|
298
|
+
},
|
|
299
|
+
extraHeaders: {},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
export function normalizePluginWorkResponse(plugin, response, model) {
|
|
303
|
+
if (plugin.id === 'anthropic') {
|
|
304
|
+
return normalizeAnthropicWorkResponse(response, model);
|
|
305
|
+
}
|
|
306
|
+
if (plugin.id === 'gemini') {
|
|
307
|
+
return normalizeGeminiWorkResponse(response, model);
|
|
308
|
+
}
|
|
309
|
+
return normalizeOpenAIWorkResponse(response, model);
|
|
310
|
+
}
|
|
311
|
+
export function toOpenAIWorkMessages(messages) {
|
|
312
|
+
let pendingCalls = [];
|
|
313
|
+
return messages.map((message, messageIndex) => {
|
|
314
|
+
if (message.role === 'assistant') {
|
|
315
|
+
const toolCalls = normalizeOutboundToolCalls(message.tool_calls);
|
|
316
|
+
const reasoningContent = toolCalls
|
|
317
|
+
.map(call => asObject(call.providerMetadata)?.openAIReasoningContent)
|
|
318
|
+
.find(value => typeof value === 'string');
|
|
319
|
+
pendingCalls = toolCalls.map(call => ({
|
|
320
|
+
id: String(call.id),
|
|
321
|
+
name: String(call.function.name),
|
|
322
|
+
}));
|
|
323
|
+
return {
|
|
324
|
+
role: 'assistant',
|
|
325
|
+
content: message.content || null,
|
|
326
|
+
...(typeof reasoningContent === 'string'
|
|
327
|
+
? { reasoning_content: reasoningContent }
|
|
328
|
+
: {}),
|
|
329
|
+
...(toolCalls.length
|
|
330
|
+
? {
|
|
331
|
+
tool_calls: toolCalls.map(call => ({
|
|
332
|
+
id: call.id,
|
|
333
|
+
type: 'function',
|
|
334
|
+
function: call.function,
|
|
335
|
+
})),
|
|
336
|
+
}
|
|
337
|
+
: {}),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (message.role === 'tool') {
|
|
341
|
+
const matchingIndex = pendingCalls.findIndex(call => !message.tool_name || call.name === message.tool_name);
|
|
342
|
+
const matching = matchingIndex >= 0
|
|
343
|
+
? pendingCalls.splice(matchingIndex, 1)[0]
|
|
344
|
+
: undefined;
|
|
345
|
+
return {
|
|
346
|
+
role: 'tool',
|
|
347
|
+
content: message.content,
|
|
348
|
+
name: message.tool_name,
|
|
349
|
+
tool_call_id: matching?.id || `work-tool-${messageIndex}`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return { role: message.role, content: message.content };
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function buildAnthropicWorkPayload(model, messages, tools, maxTokens, stream = false) {
|
|
356
|
+
const system = messages
|
|
357
|
+
.filter(message => message.role === 'system')
|
|
358
|
+
.map(message => message.content)
|
|
359
|
+
.join('\n');
|
|
360
|
+
const providerMessages = [];
|
|
361
|
+
let pendingCalls = [];
|
|
362
|
+
let appendToolResult = false;
|
|
363
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
364
|
+
if (message.role === 'system')
|
|
365
|
+
continue;
|
|
366
|
+
if (message.role === 'assistant') {
|
|
367
|
+
const blocks = [];
|
|
368
|
+
const toolCalls = normalizeOutboundToolCalls(message.tool_calls);
|
|
369
|
+
const anthropicThinkingBlocks = toolCalls.flatMap(call => {
|
|
370
|
+
const metadata = asObject(call.providerMetadata);
|
|
371
|
+
return Array.isArray(metadata?.anthropicThinkingBlocks)
|
|
372
|
+
? metadata.anthropicThinkingBlocks.flatMap(block => asObject(block) ? [block] : [])
|
|
373
|
+
: [];
|
|
374
|
+
});
|
|
375
|
+
blocks.push(...anthropicThinkingBlocks);
|
|
376
|
+
if (message.content)
|
|
377
|
+
blocks.push({ type: 'text', text: message.content });
|
|
378
|
+
pendingCalls = toolCalls.map(call => ({
|
|
379
|
+
id: String(call.id),
|
|
380
|
+
name: String(call.function.name),
|
|
381
|
+
}));
|
|
382
|
+
for (const call of toolCalls) {
|
|
383
|
+
const fn = call.function;
|
|
384
|
+
blocks.push({
|
|
385
|
+
type: 'tool_use',
|
|
386
|
+
id: call.id,
|
|
387
|
+
name: fn.name,
|
|
388
|
+
input: parseToolArguments(fn.arguments),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
providerMessages.push({ role: 'assistant', content: blocks });
|
|
392
|
+
appendToolResult = false;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (message.role === 'tool') {
|
|
396
|
+
const matchIndex = pendingCalls.findIndex(call => !message.tool_name || call.name === message.tool_name);
|
|
397
|
+
const matching = matchIndex >= 0 ? pendingCalls.splice(matchIndex, 1)[0] : undefined;
|
|
398
|
+
const block = {
|
|
399
|
+
type: 'tool_result',
|
|
400
|
+
tool_use_id: matching?.id || `work-tool-${messageIndex}`,
|
|
401
|
+
content: message.content,
|
|
402
|
+
};
|
|
403
|
+
const previous = providerMessages[providerMessages.length - 1];
|
|
404
|
+
if (appendToolResult && previous?.role === 'user') {
|
|
405
|
+
previous.content.push(block);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
providerMessages.push({ role: 'user', content: [block] });
|
|
409
|
+
}
|
|
410
|
+
appendToolResult = true;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
providerMessages.push({
|
|
414
|
+
role: 'user',
|
|
415
|
+
content: [{ type: 'text', text: message.content }],
|
|
416
|
+
});
|
|
417
|
+
appendToolResult = false;
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
model,
|
|
421
|
+
system: system || undefined,
|
|
422
|
+
messages: providerMessages,
|
|
423
|
+
tools: toAnthropicTools(tools),
|
|
424
|
+
max_tokens: maxTokens ?? 4096,
|
|
425
|
+
stream,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function buildGeminiWorkPayload(messages, tools, params) {
|
|
429
|
+
const system = messages
|
|
430
|
+
.filter(message => message.role === 'system')
|
|
431
|
+
.map(message => message.content)
|
|
432
|
+
.join('\n');
|
|
433
|
+
const contents = [];
|
|
434
|
+
let pendingCalls = [];
|
|
435
|
+
const append = (role, part) => {
|
|
436
|
+
const previous = contents[contents.length - 1];
|
|
437
|
+
if (previous?.role === role)
|
|
438
|
+
previous.parts.push(part);
|
|
439
|
+
else
|
|
440
|
+
contents.push({ role, parts: [part] });
|
|
441
|
+
};
|
|
442
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
443
|
+
if (message.role === 'system')
|
|
444
|
+
continue;
|
|
445
|
+
if (message.role === 'assistant') {
|
|
446
|
+
if (message.content)
|
|
447
|
+
append('model', { text: message.content });
|
|
448
|
+
const calls = normalizeOutboundToolCalls(message.tool_calls);
|
|
449
|
+
pendingCalls = calls.map(call => ({
|
|
450
|
+
id: String(call.id),
|
|
451
|
+
name: String(call.function.name),
|
|
452
|
+
}));
|
|
453
|
+
for (const call of calls) {
|
|
454
|
+
const fn = call.function;
|
|
455
|
+
append('model', {
|
|
456
|
+
functionCall: {
|
|
457
|
+
id: call.id,
|
|
458
|
+
name: fn.name,
|
|
459
|
+
args: parseToolArguments(fn.arguments),
|
|
460
|
+
},
|
|
461
|
+
...(typeof call.thoughtSignature === 'string'
|
|
462
|
+
? { thoughtSignature: call.thoughtSignature }
|
|
463
|
+
: {}),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (message.role === 'tool') {
|
|
469
|
+
const matchIndex = pendingCalls.findIndex(call => !message.tool_name || call.name === message.tool_name);
|
|
470
|
+
const matching = matchIndex >= 0 ? pendingCalls.splice(matchIndex, 1)[0] : undefined;
|
|
471
|
+
append('user', {
|
|
472
|
+
functionResponse: {
|
|
473
|
+
id: matching?.id || `work-tool-${messageIndex}`,
|
|
474
|
+
name: message.tool_name || matching?.name || 'work_tool',
|
|
475
|
+
response: { result: message.content },
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
append('user', { text: message.content });
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
systemInstruction: system ? { parts: [{ text: system }] } : undefined,
|
|
484
|
+
contents,
|
|
485
|
+
...(tools.length
|
|
486
|
+
? {
|
|
487
|
+
tools: [
|
|
488
|
+
{
|
|
489
|
+
functionDeclarations: tools.flatMap(tool => {
|
|
490
|
+
const fn = asObject(tool.function);
|
|
491
|
+
return fn
|
|
492
|
+
? [
|
|
493
|
+
{
|
|
494
|
+
name: fn.name,
|
|
495
|
+
description: fn.description,
|
|
496
|
+
parameters: fn.parameters,
|
|
497
|
+
},
|
|
498
|
+
]
|
|
499
|
+
: [];
|
|
500
|
+
}),
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
}
|
|
504
|
+
: {}),
|
|
505
|
+
generationConfig: {
|
|
506
|
+
temperature: params.temperature,
|
|
507
|
+
maxOutputTokens: params.maxTokens ?? 4096,
|
|
508
|
+
topP: params.topP,
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function normalizeOpenAIWorkResponse(response, model) {
|
|
513
|
+
const choices = Array.isArray(response.choices) ? response.choices : [];
|
|
514
|
+
const choice = asObject(choices[0]);
|
|
515
|
+
const message = asObject(choice?.message);
|
|
516
|
+
if (!message) {
|
|
517
|
+
throw new WorkModelProviderError('Plugin returned an invalid chat response.', 502, 'WORK_PLUGIN_INVALID_RESPONSE');
|
|
518
|
+
}
|
|
519
|
+
const toolCalls = normalizeInboundToolCalls(message.tool_calls);
|
|
520
|
+
if (typeof message.reasoning_content === 'string' && toolCalls[0]) {
|
|
521
|
+
toolCalls[0].providerMetadata = {
|
|
522
|
+
...asObject(toolCalls[0].providerMetadata),
|
|
523
|
+
openAIReasoningContent: message.reasoning_content,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
return workResponse(model, contentText(message.content), toolCalls, typeof message.reasoning_content === 'string'
|
|
527
|
+
? message.reasoning_content
|
|
528
|
+
: '');
|
|
529
|
+
}
|
|
530
|
+
function normalizeAnthropicWorkResponse(response, model) {
|
|
531
|
+
const blocks = Array.isArray(response.content) ? response.content : [];
|
|
532
|
+
const text = [];
|
|
533
|
+
const calls = [];
|
|
534
|
+
const thinkingBlocks = [];
|
|
535
|
+
const reasoning = [];
|
|
536
|
+
for (const [index, value] of blocks.entries()) {
|
|
537
|
+
const block = asObject(value);
|
|
538
|
+
if (block?.type === 'thinking' || block?.type === 'redacted_thinking') {
|
|
539
|
+
thinkingBlocks.push(block);
|
|
540
|
+
if (typeof block.thinking === 'string')
|
|
541
|
+
reasoning.push(block.thinking);
|
|
542
|
+
}
|
|
543
|
+
if (block?.type === 'text' && typeof block.text === 'string') {
|
|
544
|
+
text.push(block.text);
|
|
545
|
+
}
|
|
546
|
+
if (block?.type === 'tool_use' && typeof block.name === 'string') {
|
|
547
|
+
calls.push({
|
|
548
|
+
id: typeof block.id === 'string' ? block.id : `work-anthropic-${index}`,
|
|
549
|
+
function: {
|
|
550
|
+
name: block.name,
|
|
551
|
+
arguments: asObject(block.input) || {},
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (thinkingBlocks.length > 0 && calls[0]) {
|
|
557
|
+
calls[0].providerMetadata = {
|
|
558
|
+
anthropicThinkingBlocks: thinkingBlocks,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return workResponse(model, text.join(''), calls, reasoning.join(''));
|
|
562
|
+
}
|
|
563
|
+
function normalizeGeminiWorkResponse(response, model) {
|
|
564
|
+
const candidates = Array.isArray(response.candidates)
|
|
565
|
+
? response.candidates
|
|
566
|
+
: [];
|
|
567
|
+
const candidate = asObject(candidates[0]);
|
|
568
|
+
const content = asObject(candidate?.content);
|
|
569
|
+
const parts = Array.isArray(content?.parts) ? content.parts : [];
|
|
570
|
+
const text = [];
|
|
571
|
+
const reasoning = [];
|
|
572
|
+
const calls = [];
|
|
573
|
+
for (const [index, value] of parts.entries()) {
|
|
574
|
+
const part = asObject(value);
|
|
575
|
+
if (typeof part?.text === 'string') {
|
|
576
|
+
if (part.thought === true)
|
|
577
|
+
reasoning.push(part.text);
|
|
578
|
+
else
|
|
579
|
+
text.push(part.text);
|
|
580
|
+
}
|
|
581
|
+
const call = asObject(part?.functionCall);
|
|
582
|
+
if (call && typeof call.name === 'string') {
|
|
583
|
+
calls.push({
|
|
584
|
+
id: typeof call.id === 'string' ? call.id : `work-gemini-${index}`,
|
|
585
|
+
...(typeof part?.thoughtSignature === 'string'
|
|
586
|
+
? { thoughtSignature: part.thoughtSignature }
|
|
587
|
+
: {}),
|
|
588
|
+
function: {
|
|
589
|
+
name: call.name,
|
|
590
|
+
arguments: asObject(call.args) || {},
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return workResponse(model, text.join(''), calls, reasoning.join(''));
|
|
596
|
+
}
|
|
597
|
+
function workResponse(model, content, toolCalls, reasoning = '') {
|
|
598
|
+
return {
|
|
599
|
+
model,
|
|
600
|
+
created_at: new Date().toISOString(),
|
|
601
|
+
message: {
|
|
602
|
+
role: 'assistant',
|
|
603
|
+
content,
|
|
604
|
+
...(reasoning ? { thinking: reasoning } : {}),
|
|
605
|
+
tool_calls: toolCalls,
|
|
606
|
+
},
|
|
607
|
+
done: true,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
async function collectPluginWorkStream(chunks, model, observer) {
|
|
611
|
+
let content = '';
|
|
612
|
+
let reasoning = '';
|
|
613
|
+
let usage = {};
|
|
614
|
+
const toolCalls = [];
|
|
615
|
+
for await (const chunk of chunks) {
|
|
616
|
+
if (chunk.type === 'content') {
|
|
617
|
+
content += chunk.content;
|
|
618
|
+
observer.onContent?.(chunk.content);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (chunk.type === 'reasoning') {
|
|
622
|
+
reasoning += chunk.content;
|
|
623
|
+
observer.onReasoning?.(chunk.content);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (chunk.type === 'usage') {
|
|
627
|
+
usage = { ...usage, ...chunk.usage };
|
|
628
|
+
observer.onUsage?.(usage);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (chunk.type === 'tool_call') {
|
|
632
|
+
const parsedArguments = parseToolArgumentsWithStatus(chunk.toolCall.arguments);
|
|
633
|
+
const metadata = {
|
|
634
|
+
...chunk.toolCall.providerMetadata,
|
|
635
|
+
...(parsedArguments.error
|
|
636
|
+
? {
|
|
637
|
+
[WORK_TOOL_ARGUMENTS_ERROR_METADATA_KEY]: parsedArguments.error,
|
|
638
|
+
}
|
|
639
|
+
: {}),
|
|
640
|
+
};
|
|
641
|
+
toolCalls.push({
|
|
642
|
+
id: chunk.toolCall.id || `work-plugin-${toolCalls.length}`,
|
|
643
|
+
...(typeof metadata?.geminiThoughtSignature === 'string'
|
|
644
|
+
? { thoughtSignature: metadata.geminiThoughtSignature }
|
|
645
|
+
: {}),
|
|
646
|
+
...(metadata &&
|
|
647
|
+
Object.keys(metadata).some(key => key !== 'geminiThoughtSignature')
|
|
648
|
+
? {
|
|
649
|
+
providerMetadata: Object.fromEntries(Object.entries(metadata).filter(([key]) => key !== 'geminiThoughtSignature')),
|
|
650
|
+
}
|
|
651
|
+
: {}),
|
|
652
|
+
function: {
|
|
653
|
+
name: chunk.toolCall.name,
|
|
654
|
+
arguments: parsedArguments.arguments,
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (reasoning && toolCalls[0]) {
|
|
660
|
+
const firstMetadata = asObject(toolCalls[0].providerMetadata) || {};
|
|
661
|
+
if (!firstMetadata.openAIReasoningContent &&
|
|
662
|
+
!firstMetadata.anthropicThinkingBlocks) {
|
|
663
|
+
toolCalls[0].providerMetadata = {
|
|
664
|
+
...firstMetadata,
|
|
665
|
+
openAIReasoningContent: reasoning,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
...workResponse(model, content, toolCalls, reasoning),
|
|
671
|
+
prompt_eval_count: usage.promptTokens,
|
|
672
|
+
eval_count: usage.completionTokens,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function ollamaUsage(response) {
|
|
676
|
+
return {
|
|
677
|
+
promptTokens: response.prompt_eval_count,
|
|
678
|
+
completionTokens: response.eval_count,
|
|
679
|
+
totalTokens: response.prompt_eval_count !== undefined ||
|
|
680
|
+
response.eval_count !== undefined
|
|
681
|
+
? (response.prompt_eval_count || 0) + (response.eval_count || 0)
|
|
682
|
+
: undefined,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function geminiStreamingEndpoint(endpoint) {
|
|
686
|
+
const url = new URL(endpoint);
|
|
687
|
+
url.pathname = url.pathname.replace(/:generateContent$/, ':streamGenerateContent');
|
|
688
|
+
url.searchParams.set('alt', 'sse');
|
|
689
|
+
return url.toString();
|
|
690
|
+
}
|
|
691
|
+
async function* streamGeminiWorkResponse(response) {
|
|
692
|
+
if (!response.ok) {
|
|
693
|
+
const errorText = await response.text();
|
|
694
|
+
throw new Error(`Plugin API error: ${response.status} - ${errorText.slice(0, 200)}`);
|
|
695
|
+
}
|
|
696
|
+
if (!response.body) {
|
|
697
|
+
throw new Error('No response body for streaming');
|
|
698
|
+
}
|
|
699
|
+
const reader = response.body.getReader();
|
|
700
|
+
const decoder = new TextDecoder();
|
|
701
|
+
let buffer = '';
|
|
702
|
+
let callIndex = 0;
|
|
703
|
+
try {
|
|
704
|
+
while (true) {
|
|
705
|
+
const { done, value } = await reader.read();
|
|
706
|
+
if (done)
|
|
707
|
+
break;
|
|
708
|
+
buffer += decoder.decode(value, { stream: true });
|
|
709
|
+
const lines = buffer.split('\n');
|
|
710
|
+
buffer = lines.pop() || '';
|
|
711
|
+
for (const line of lines) {
|
|
712
|
+
const trimmed = line.trim();
|
|
713
|
+
if (!trimmed.startsWith('data:'))
|
|
714
|
+
continue;
|
|
715
|
+
let payload;
|
|
716
|
+
try {
|
|
717
|
+
payload = JSON.parse(trimmed.slice(5).trim());
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const streamError = providerStreamErrorMessage(payload);
|
|
723
|
+
if (streamError) {
|
|
724
|
+
throw new Error(`Plugin API error: ${streamError}`);
|
|
725
|
+
}
|
|
726
|
+
const candidates = Array.isArray(payload.candidates)
|
|
727
|
+
? payload.candidates
|
|
728
|
+
: [];
|
|
729
|
+
const candidate = asObject(candidates[0]);
|
|
730
|
+
const candidateContent = asObject(candidate?.content);
|
|
731
|
+
const parts = Array.isArray(candidateContent?.parts)
|
|
732
|
+
? candidateContent.parts
|
|
733
|
+
: [];
|
|
734
|
+
for (const rawPart of parts) {
|
|
735
|
+
const part = asObject(rawPart);
|
|
736
|
+
if (typeof part?.text === 'string' && part.text) {
|
|
737
|
+
yield part.thought === true
|
|
738
|
+
? { type: 'reasoning', content: part.text }
|
|
739
|
+
: { type: 'content', content: part.text };
|
|
740
|
+
}
|
|
741
|
+
const call = asObject(part?.functionCall);
|
|
742
|
+
if (call && typeof call.name === 'string') {
|
|
743
|
+
yield {
|
|
744
|
+
type: 'tool_call',
|
|
745
|
+
toolCall: {
|
|
746
|
+
id: typeof call.id === 'string'
|
|
747
|
+
? call.id
|
|
748
|
+
: `work-gemini-${callIndex++}`,
|
|
749
|
+
name: call.name,
|
|
750
|
+
arguments: JSON.stringify(asObject(call.args) || {}),
|
|
751
|
+
...(typeof part?.thoughtSignature === 'string'
|
|
752
|
+
? {
|
|
753
|
+
providerMetadata: {
|
|
754
|
+
geminiThoughtSignature: part.thoughtSignature,
|
|
755
|
+
},
|
|
756
|
+
}
|
|
757
|
+
: {}),
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const usageMetadata = asObject(payload.usageMetadata);
|
|
763
|
+
if (usageMetadata) {
|
|
764
|
+
const promptTokens = typeof usageMetadata.promptTokenCount === 'number'
|
|
765
|
+
? usageMetadata.promptTokenCount
|
|
766
|
+
: undefined;
|
|
767
|
+
const completionTokens = typeof usageMetadata.candidatesTokenCount === 'number'
|
|
768
|
+
? usageMetadata.candidatesTokenCount
|
|
769
|
+
: undefined;
|
|
770
|
+
const totalTokens = typeof usageMetadata.totalTokenCount === 'number'
|
|
771
|
+
? usageMetadata.totalTokenCount
|
|
772
|
+
: promptTokens !== undefined || completionTokens !== undefined
|
|
773
|
+
? (promptTokens || 0) + (completionTokens || 0)
|
|
774
|
+
: undefined;
|
|
775
|
+
yield {
|
|
776
|
+
type: 'usage',
|
|
777
|
+
usage: { promptTokens, completionTokens, totalTokens },
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
yield { type: 'done' };
|
|
783
|
+
}
|
|
784
|
+
finally {
|
|
785
|
+
reader.releaseLock();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function normalizeOutboundToolCalls(value) {
|
|
789
|
+
if (!Array.isArray(value))
|
|
790
|
+
return [];
|
|
791
|
+
return value.flatMap((raw, index) => {
|
|
792
|
+
const call = asObject(raw);
|
|
793
|
+
const fn = asObject(call?.function);
|
|
794
|
+
if (!fn || typeof fn.name !== 'string')
|
|
795
|
+
return [];
|
|
796
|
+
const providerMetadata = asObject(call?.providerMetadata);
|
|
797
|
+
return [
|
|
798
|
+
{
|
|
799
|
+
id: typeof call?.id === 'string' ? call.id : `work-tool-call-${index}`,
|
|
800
|
+
...(typeof call?.thoughtSignature === 'string'
|
|
801
|
+
? { thoughtSignature: call.thoughtSignature }
|
|
802
|
+
: {}),
|
|
803
|
+
...(providerMetadata ? { providerMetadata } : {}),
|
|
804
|
+
type: 'function',
|
|
805
|
+
function: {
|
|
806
|
+
name: fn.name,
|
|
807
|
+
arguments: typeof fn.arguments === 'string'
|
|
808
|
+
? fn.arguments
|
|
809
|
+
: JSON.stringify(asObject(fn.arguments) || {}),
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
];
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
function normalizeInboundToolCalls(value) {
|
|
816
|
+
if (!Array.isArray(value))
|
|
817
|
+
return [];
|
|
818
|
+
return value.flatMap((raw, index) => {
|
|
819
|
+
const call = asObject(raw);
|
|
820
|
+
const fn = asObject(call?.function);
|
|
821
|
+
if (!fn || typeof fn.name !== 'string')
|
|
822
|
+
return [];
|
|
823
|
+
const parsedArguments = parseToolArgumentsWithStatus(fn.arguments);
|
|
824
|
+
const providerMetadata = {
|
|
825
|
+
...asObject(call?.providerMetadata),
|
|
826
|
+
...(parsedArguments.error
|
|
827
|
+
? {
|
|
828
|
+
[WORK_TOOL_ARGUMENTS_ERROR_METADATA_KEY]: parsedArguments.error,
|
|
829
|
+
}
|
|
830
|
+
: {}),
|
|
831
|
+
};
|
|
832
|
+
return [
|
|
833
|
+
{
|
|
834
|
+
id: typeof call?.id === 'string' ? call.id : `work-plugin-${index}`,
|
|
835
|
+
...(typeof call?.thoughtSignature === 'string'
|
|
836
|
+
? { thoughtSignature: call.thoughtSignature }
|
|
837
|
+
: {}),
|
|
838
|
+
...(Object.keys(providerMetadata).length > 0
|
|
839
|
+
? { providerMetadata }
|
|
840
|
+
: {}),
|
|
841
|
+
function: {
|
|
842
|
+
name: fn.name,
|
|
843
|
+
arguments: typeof fn.arguments === 'string' && !parsedArguments.error
|
|
844
|
+
? fn.arguments
|
|
845
|
+
: parsedArguments.arguments,
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
];
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
function toAnthropicTools(tools) {
|
|
852
|
+
return tools.flatMap(tool => {
|
|
853
|
+
const fn = asObject(tool.function);
|
|
854
|
+
if (!fn || typeof fn.name !== 'string')
|
|
855
|
+
return [];
|
|
856
|
+
return [
|
|
857
|
+
{
|
|
858
|
+
name: fn.name,
|
|
859
|
+
description: fn.description,
|
|
860
|
+
input_schema: fn.parameters || {
|
|
861
|
+
type: 'object',
|
|
862
|
+
properties: {},
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
];
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
function parseToolArguments(value) {
|
|
869
|
+
return parseToolArgumentsWithStatus(value).arguments;
|
|
870
|
+
}
|
|
871
|
+
function parseToolArgumentsWithStatus(value) {
|
|
872
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
873
|
+
return { arguments: value };
|
|
874
|
+
}
|
|
875
|
+
if (typeof value === 'string') {
|
|
876
|
+
if (!value.trim())
|
|
877
|
+
return { arguments: {} };
|
|
878
|
+
try {
|
|
879
|
+
const parsed = asObject(JSON.parse(value));
|
|
880
|
+
return parsed
|
|
881
|
+
? { arguments: parsed }
|
|
882
|
+
: {
|
|
883
|
+
arguments: {},
|
|
884
|
+
error: WORK_TOOL_ARGUMENTS_ERROR_MESSAGE,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
return {
|
|
889
|
+
arguments: {},
|
|
890
|
+
error: WORK_TOOL_ARGUMENTS_ERROR_MESSAGE,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return { arguments: {} };
|
|
895
|
+
}
|
|
896
|
+
function contentText(value) {
|
|
897
|
+
if (typeof value === 'string')
|
|
898
|
+
return value;
|
|
899
|
+
if (!Array.isArray(value))
|
|
900
|
+
return '';
|
|
901
|
+
return value
|
|
902
|
+
.flatMap(item => {
|
|
903
|
+
const block = asObject(item);
|
|
904
|
+
return typeof block?.text === 'string' ? [block.text] : [];
|
|
905
|
+
})
|
|
906
|
+
.join('');
|
|
907
|
+
}
|
|
908
|
+
function providerErrorMessage(value) {
|
|
909
|
+
const payload = asObject(value);
|
|
910
|
+
const error = asObject(payload?.error);
|
|
911
|
+
if (typeof error?.message === 'string')
|
|
912
|
+
return error.message;
|
|
913
|
+
if (typeof payload?.message === 'string')
|
|
914
|
+
return payload.message;
|
|
915
|
+
return 'Request failed.';
|
|
916
|
+
}
|
|
917
|
+
function providerStreamErrorMessage(value) {
|
|
918
|
+
const payload = asObject(value);
|
|
919
|
+
if (!payload)
|
|
920
|
+
return undefined;
|
|
921
|
+
const error = asObject(payload.error);
|
|
922
|
+
const message = typeof error?.message === 'string'
|
|
923
|
+
? error.message
|
|
924
|
+
: typeof payload.error === 'string'
|
|
925
|
+
? payload.error
|
|
926
|
+
: typeof payload.message === 'string' &&
|
|
927
|
+
(payload.type === 'error' || payload.status === 'error')
|
|
928
|
+
? payload.message
|
|
929
|
+
: undefined;
|
|
930
|
+
return message?.slice(0, 500);
|
|
931
|
+
}
|
|
932
|
+
function asObject(value) {
|
|
933
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
934
|
+
? value
|
|
935
|
+
: undefined;
|
|
936
|
+
}
|
|
937
|
+
function isWorkPlugin(plugin) {
|
|
938
|
+
return plugin.type === 'completion' || plugin.type === 'chat';
|
|
939
|
+
}
|
|
940
|
+
function assertWorkPluginType(plugin) {
|
|
941
|
+
if (!isWorkPlugin(plugin)) {
|
|
942
|
+
throw new WorkModelProviderError(`Plugin "${plugin.name}" does not provide chat completions.`, 422, 'WORK_PLUGIN_TOOLS_UNSUPPORTED');
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function assertOllamaProvider(provider) {
|
|
946
|
+
if (provider.providerType !== 'ollama' || provider.providerId) {
|
|
947
|
+
throw new WorkModelProviderError('Invalid Ollama provider selection.', 400, 'WORK_PROVIDER_INVALID');
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
export const workModelProviderService = new WorkModelProviderService();
|
|
951
|
+
export default workModelProviderService;
|
|
952
|
+
//# sourceMappingURL=workModelProviderService.js.map
|