@omnicross/core 0.1.0 → 0.1.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/dist/ApiConverter.cjs +799 -0
- package/dist/ApiConverter.d.cts +82 -0
- package/dist/ApiConverter.d.ts +82 -0
- package/dist/ApiConverter.js +763 -0
- package/dist/BuiltinToolExecutor-BluWyeob.d.ts +81 -0
- package/dist/BuiltinToolExecutor-CS2WpXhM.d.cts +81 -0
- package/dist/CompletionService-7fCmKAP3.d.ts +212 -0
- package/dist/CompletionService-DtOF_War.d.cts +212 -0
- package/dist/{ProviderProxy-f_8ziIhW.d.cts → ProviderProxy-C-xqrkKi.d.ts} +7 -2
- package/dist/{ProviderProxy-vjt8sQQk.d.ts → ProviderProxy-CnMQYN59.d.cts} +7 -2
- package/dist/completion/BuiltinToolExecutor.cjs +327 -0
- package/dist/completion/BuiltinToolExecutor.d.cts +4 -0
- package/dist/completion/BuiltinToolExecutor.d.ts +4 -0
- package/dist/completion/BuiltinToolExecutor.js +296 -0
- package/dist/completion/CompletionService.cjs +3487 -0
- package/dist/completion/CompletionService.d.cts +21 -0
- package/dist/completion/CompletionService.d.ts +21 -0
- package/dist/completion/CompletionService.js +3461 -0
- package/dist/completion/NativeSearchInjector.cjs +196 -0
- package/dist/completion/NativeSearchInjector.d.cts +42 -0
- package/dist/completion/NativeSearchInjector.d.ts +42 -0
- package/dist/completion/NativeSearchInjector.js +167 -0
- package/dist/completion/ProviderSearchInjector.cjs +87 -0
- package/dist/completion/ProviderSearchInjector.d.cts +47 -0
- package/dist/completion/ProviderSearchInjector.d.ts +47 -0
- package/dist/completion/ProviderSearchInjector.js +60 -0
- package/dist/completion/native-search-types.cjs +67 -0
- package/dist/completion/native-search-types.d.cts +3 -0
- package/dist/completion/native-search-types.d.ts +3 -0
- package/dist/completion/native-search-types.js +38 -0
- package/dist/completion/openrouter-headers.cjs +72 -0
- package/dist/completion/openrouter-headers.d.cts +44 -0
- package/dist/completion/openrouter-headers.d.ts +44 -0
- package/dist/completion/openrouter-headers.js +42 -0
- package/dist/completion/openrouter-models.cjs +86 -0
- package/dist/completion/openrouter-models.d.cts +27 -0
- package/dist/completion/openrouter-models.d.ts +27 -0
- package/dist/completion/openrouter-models.js +59 -0
- package/dist/completion/types.cjs +18 -0
- package/dist/completion/types.d.cts +3 -0
- package/dist/completion/types.d.ts +3 -0
- package/dist/completion/types.js +0 -0
- package/dist/completion/url-builder.cjs +138 -0
- package/dist/completion/url-builder.d.cts +87 -0
- package/dist/completion/url-builder.d.ts +87 -0
- package/dist/completion/url-builder.js +104 -0
- package/dist/completion.d.cts +148 -7
- package/dist/completion.d.ts +148 -7
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +27 -90
- package/dist/index.d.ts +27 -90
- package/dist/index.js +1 -0
- package/dist/outbound-api/routeResolver.cjs +221 -0
- package/dist/outbound-api/routeResolver.d.cts +18 -0
- package/dist/outbound-api/routeResolver.d.ts +18 -0
- package/dist/outbound-api/routeResolver.js +192 -0
- package/dist/outbound-api/subscriptionRegistryPort.d.cts +5 -2
- package/dist/outbound-api/subscriptionRegistryPort.d.ts +5 -2
- package/dist/outbound-api/types.cjs +18 -0
- package/dist/{types-CbCN2NQP.d.ts → outbound-api/types.d.cts} +17 -3
- package/dist/{types-CGGrKqC_.d.cts → outbound-api/types.d.ts} +17 -3
- package/dist/outbound-api/types.js +0 -0
- package/dist/outbound-api.cjs +1 -0
- package/dist/outbound-api.d.cts +14 -87
- package/dist/outbound-api.d.ts +14 -87
- package/dist/outbound-api.js +1 -0
- package/dist/pipeline/AuthSource.cjs +18 -0
- package/dist/pipeline/AuthSource.d.cts +101 -0
- package/dist/pipeline/AuthSource.d.ts +101 -0
- package/dist/pipeline/AuthSource.js +0 -0
- package/dist/pipeline/LlmConfigProviderAuth.cjs +169 -0
- package/dist/pipeline/LlmConfigProviderAuth.d.cts +86 -0
- package/dist/pipeline/LlmConfigProviderAuth.d.ts +86 -0
- package/dist/pipeline/LlmConfigProviderAuth.js +142 -0
- package/dist/pipeline/SubscriptionAuthSource.d.cts +165 -3
- package/dist/pipeline/SubscriptionAuthSource.d.ts +165 -3
- package/dist/pipeline/executeProviderCall.cjs +70 -0
- package/dist/pipeline/executeProviderCall.d.cts +149 -0
- package/dist/pipeline/executeProviderCall.d.ts +149 -0
- package/dist/pipeline/executeProviderCall.js +45 -0
- package/dist/pipeline/resolveProviderChain.cjs +47 -0
- package/dist/pipeline/resolveProviderChain.d.cts +58 -0
- package/dist/pipeline/resolveProviderChain.d.ts +58 -0
- package/dist/pipeline/resolveProviderChain.js +22 -0
- package/dist/pipeline/resolveSubscriptionChain.cjs +68 -0
- package/dist/pipeline/resolveSubscriptionChain.d.cts +68 -0
- package/dist/pipeline/resolveSubscriptionChain.d.ts +68 -0
- package/dist/pipeline/resolveSubscriptionChain.js +43 -0
- package/dist/ports/provider-config-source.cjs +18 -0
- package/dist/ports/provider-config-source.d.cts +51 -0
- package/dist/ports/provider-config-source.d.ts +51 -0
- package/dist/ports/provider-config-source.js +0 -0
- package/dist/ports/web-search-backend.cjs +18 -0
- package/dist/ports/web-search-backend.d.cts +29 -0
- package/dist/ports/web-search-backend.d.ts +29 -0
- package/dist/ports/web-search-backend.js +0 -0
- package/dist/ports.d.cts +10 -7
- package/dist/ports.d.ts +10 -7
- package/dist/provider-proxy/ProviderProxy.cjs +4643 -0
- package/dist/provider-proxy/ProviderProxy.d.cts +16 -0
- package/dist/provider-proxy/ProviderProxy.d.ts +16 -0
- package/dist/provider-proxy/ProviderProxy.js +4618 -0
- package/dist/provider-proxy/ingress/providerProxyShared.d.cts +5 -2
- package/dist/provider-proxy/ingress/providerProxyShared.d.ts +5 -2
- package/dist/provider-proxy/types.d.cts +406 -8
- package/dist/provider-proxy/types.d.ts +406 -8
- package/dist/provider-proxy.cjs +1 -0
- package/dist/provider-proxy.d.cts +8 -5
- package/dist/provider-proxy.d.ts +8 -5
- package/dist/provider-proxy.js +1 -0
- package/dist/routeResolver-BrbK6ja9.d.cts +88 -0
- package/dist/routeResolver-HE-ZO0fO.d.ts +88 -0
- package/dist/transformer/anthropicBetaInject.cjs +51 -0
- package/dist/transformer/anthropicBetaInject.d.cts +20 -0
- package/dist/transformer/anthropicBetaInject.d.ts +20 -0
- package/dist/transformer/anthropicBetaInject.js +25 -0
- package/dist/transformer/transformers/AnthropicTransformer.cjs +1017 -0
- package/dist/transformer/transformers/AnthropicTransformer.d.cts +148 -0
- package/dist/transformer/transformers/AnthropicTransformer.d.ts +148 -0
- package/dist/transformer/transformers/AnthropicTransformer.js +990 -0
- package/dist/transformer/transformers/ReasoningTransformer.cjs +273 -0
- package/dist/transformer/transformers/ReasoningTransformer.d.cts +47 -0
- package/dist/transformer/transformers/ReasoningTransformer.d.ts +47 -0
- package/dist/transformer/transformers/ReasoningTransformer.js +253 -0
- package/dist/transformer/transformers.cjs +3206 -0
- package/dist/transformer/transformers.d.cts +100 -0
- package/dist/transformer/transformers.d.ts +100 -0
- package/dist/transformer/transformers.js +3174 -0
- package/dist/transformer.d.cts +8 -31
- package/dist/transformer.d.ts +8 -31
- package/dist/types-BScIHmPr.d.cts +153 -0
- package/dist/types-BScIHmPr.d.ts +153 -0
- package/package.json +3 -3
- package/dist/SubscriptionAuthSource-Cr4fVEYY.d.cts +0 -264
- package/dist/SubscriptionAuthSource-D89zmiSS.d.ts +0 -264
- package/dist/index-BTSmc9Sm.d.ts +0 -645
- package/dist/index-DXazdTzZ.d.cts +0 -645
- package/dist/types-DCzHkhJt.d.ts +0 -467
- package/dist/types-DZIQbgp0.d.cts +0 -467
|
@@ -0,0 +1,3461 @@
|
|
|
1
|
+
// src/completion/url-builder.ts
|
|
2
|
+
import { resolveProviderEndpoint as resolveProviderEndpointShared } from "@omnicross/contracts/endpoint-resolver";
|
|
3
|
+
function resolveApiFormat(provider) {
|
|
4
|
+
if (provider.apiFormat) {
|
|
5
|
+
return provider.apiFormat;
|
|
6
|
+
}
|
|
7
|
+
if (provider.chatApiFormat) {
|
|
8
|
+
return provider.chatApiFormat;
|
|
9
|
+
}
|
|
10
|
+
if (provider.apiType === "claudecode" || provider.apiType === "anthropic") {
|
|
11
|
+
return "anthropic";
|
|
12
|
+
}
|
|
13
|
+
if (provider.apiType === "google") {
|
|
14
|
+
return "google";
|
|
15
|
+
}
|
|
16
|
+
return "openai";
|
|
17
|
+
}
|
|
18
|
+
function buildOpenAIApiUrl(baseUrl) {
|
|
19
|
+
const url = baseUrl.replace(/\/+$/, "");
|
|
20
|
+
if (url.endsWith("/chat/completions")) {
|
|
21
|
+
return url;
|
|
22
|
+
}
|
|
23
|
+
if (/\/v\d+$/.test(url)) {
|
|
24
|
+
return url + "/chat/completions";
|
|
25
|
+
}
|
|
26
|
+
if (url.includes("/chat/completions")) {
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
return url + "/v1/chat/completions";
|
|
30
|
+
}
|
|
31
|
+
function buildAnthropicApiUrl(baseUrl) {
|
|
32
|
+
const url = baseUrl.replace(/\/+$/, "");
|
|
33
|
+
if (url.endsWith("/messages")) {
|
|
34
|
+
return url;
|
|
35
|
+
}
|
|
36
|
+
if (url.includes("/messages")) {
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
if (/\/v\d+$/.test(url)) {
|
|
40
|
+
return url + "/messages";
|
|
41
|
+
}
|
|
42
|
+
return url + "/v1/messages";
|
|
43
|
+
}
|
|
44
|
+
function buildGeminiModelActionUrl(baseUrl, model, action) {
|
|
45
|
+
let url = baseUrl.replace(/\/+$/, "");
|
|
46
|
+
if (url.includes("/models/")) {
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
49
|
+
if (!/\/v\d+(?:beta)?(?:$|\/)/.test(url)) {
|
|
50
|
+
url += "/v1beta";
|
|
51
|
+
}
|
|
52
|
+
return `${url}/models/${model}:${action}`;
|
|
53
|
+
}
|
|
54
|
+
function buildGeminiApiUrl(baseUrl, model, stream) {
|
|
55
|
+
const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
|
|
56
|
+
return buildGeminiModelActionUrl(baseUrl, model, action);
|
|
57
|
+
}
|
|
58
|
+
function normalizeAzureEndpoint(endpoint) {
|
|
59
|
+
return endpoint.replace(/\/+$/, "").replace(/\/openai(\/v\d+)?$/, "");
|
|
60
|
+
}
|
|
61
|
+
function buildAzureOpenAIApiUrl(baseUrl, model, apiVersion) {
|
|
62
|
+
const endpoint = normalizeAzureEndpoint(baseUrl);
|
|
63
|
+
return `${endpoint}/openai/deployments/${model}/chat/completions?api-version=${apiVersion}`;
|
|
64
|
+
}
|
|
65
|
+
function buildOpenAIResponseApiUrl(baseUrl) {
|
|
66
|
+
const url = baseUrl.replace(/\/+$/, "");
|
|
67
|
+
if (url.endsWith("/responses")) {
|
|
68
|
+
return url;
|
|
69
|
+
}
|
|
70
|
+
if (/\/v\d+$/.test(url)) {
|
|
71
|
+
return url + "/responses";
|
|
72
|
+
}
|
|
73
|
+
return url + "/v1/responses";
|
|
74
|
+
}
|
|
75
|
+
var resolveProviderEndpoint = resolveProviderEndpointShared;
|
|
76
|
+
function buildProviderApiUrl(provider, options = {}) {
|
|
77
|
+
const format = resolveApiFormat(provider);
|
|
78
|
+
const { baseUrl } = resolveProviderEndpoint(provider);
|
|
79
|
+
switch (format) {
|
|
80
|
+
case "anthropic":
|
|
81
|
+
return buildAnthropicApiUrl(baseUrl);
|
|
82
|
+
case "google":
|
|
83
|
+
return buildGeminiApiUrl(baseUrl, options.model || "", options.stream || false);
|
|
84
|
+
case "azure-openai":
|
|
85
|
+
return buildAzureOpenAIApiUrl(baseUrl, options.model || "", provider.apiVersion || "2024-08-01-preview");
|
|
86
|
+
case "openai-response":
|
|
87
|
+
return buildOpenAIResponseApiUrl(baseUrl);
|
|
88
|
+
case "openai":
|
|
89
|
+
default:
|
|
90
|
+
return buildOpenAIApiUrl(baseUrl);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/openrouter.ts
|
|
95
|
+
function isOpenRouterProvider(provider) {
|
|
96
|
+
const baseUrl = (provider.api_base_url || "").toLowerCase();
|
|
97
|
+
return baseUrl.includes("openrouter.ai");
|
|
98
|
+
}
|
|
99
|
+
var OPENROUTER_APP_HEADERS = {
|
|
100
|
+
"HTTP-Referer": "https://omnicross.dev",
|
|
101
|
+
"X-Title": "omnicross"
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/completion/header-builder.ts
|
|
105
|
+
function getProviderHeaders(provider, apiKey) {
|
|
106
|
+
const format = resolveApiFormat(provider);
|
|
107
|
+
let headers;
|
|
108
|
+
switch (format) {
|
|
109
|
+
case "anthropic":
|
|
110
|
+
headers = {
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
"x-api-key": apiKey,
|
|
113
|
+
"anthropic-version": "2025-01-10"
|
|
114
|
+
// Required for extended thinking feature
|
|
115
|
+
};
|
|
116
|
+
break;
|
|
117
|
+
case "google":
|
|
118
|
+
headers = {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
"x-goog-api-key": apiKey
|
|
121
|
+
};
|
|
122
|
+
break;
|
|
123
|
+
case "azure-openai":
|
|
124
|
+
headers = {
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
"api-key": apiKey
|
|
127
|
+
};
|
|
128
|
+
break;
|
|
129
|
+
case "openai":
|
|
130
|
+
case "openai-response":
|
|
131
|
+
default:
|
|
132
|
+
headers = {
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
"Authorization": `Bearer ${apiKey}`
|
|
135
|
+
};
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
if (isOpenRouterProvider(provider)) {
|
|
139
|
+
return { ...headers, ...OPENROUTER_APP_HEADERS };
|
|
140
|
+
}
|
|
141
|
+
return headers;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/completion/message-converter.ts
|
|
145
|
+
var OPENROUTER_AUDIO_FORMAT_MAP = {
|
|
146
|
+
"audio/wav": "wav",
|
|
147
|
+
"audio/x-wav": "wav",
|
|
148
|
+
"audio/wave": "wav",
|
|
149
|
+
"audio/mpeg": "mp3",
|
|
150
|
+
"audio/mp3": "mp3",
|
|
151
|
+
"audio/aiff": "aiff",
|
|
152
|
+
"audio/x-aiff": "aiff",
|
|
153
|
+
"audio/aac": "aac",
|
|
154
|
+
"audio/ogg": "ogg",
|
|
155
|
+
"audio/flac": "flac",
|
|
156
|
+
"audio/x-flac": "flac",
|
|
157
|
+
"audio/mp4": "m4a",
|
|
158
|
+
"audio/m4a": "m4a",
|
|
159
|
+
"audio/x-m4a": "m4a",
|
|
160
|
+
"audio/L16": "pcm16",
|
|
161
|
+
"audio/L24": "pcm24"
|
|
162
|
+
};
|
|
163
|
+
var MAX_INLINE_VIDEO_BYTES = 25 * 1024 * 1024;
|
|
164
|
+
function decodeDataUrl(url) {
|
|
165
|
+
const match = url.match(/^data:([^;]+);base64,(.+)$/);
|
|
166
|
+
if (!match) return null;
|
|
167
|
+
return { mimeType: match[1], data: match[2] };
|
|
168
|
+
}
|
|
169
|
+
function looksLikeRemoteUrl(url) {
|
|
170
|
+
return /^https?:\/\//i.test(url);
|
|
171
|
+
}
|
|
172
|
+
function resolveAudioFormat(mimeType, sourceUrl) {
|
|
173
|
+
const candidate = (mimeType || "").toLowerCase();
|
|
174
|
+
const mapped = OPENROUTER_AUDIO_FORMAT_MAP[candidate];
|
|
175
|
+
if (mapped) return mapped;
|
|
176
|
+
const decoded = decodeDataUrl(sourceUrl);
|
|
177
|
+
if (decoded) {
|
|
178
|
+
const mappedFromUrl = OPENROUTER_AUDIO_FORMAT_MAP[decoded.mimeType.toLowerCase()];
|
|
179
|
+
if (mappedFromUrl) return mappedFromUrl;
|
|
180
|
+
}
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Unsupported audio format for OpenAI/OpenRouter chat input: ${mimeType || "unknown"}. Supported: wav, mp3, aiff, aac, ogg, flac, m4a, pcm16, pcm24.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
function convertMessageToOpenAI(msg) {
|
|
186
|
+
const role = msg.role;
|
|
187
|
+
const hasImages = msg.images && msg.images.length > 0;
|
|
188
|
+
const hasAudios = msg.audios && msg.audios.length > 0;
|
|
189
|
+
const hasVideos = msg.videos && msg.videos.length > 0;
|
|
190
|
+
if (!hasImages && !hasAudios && !hasVideos) {
|
|
191
|
+
return { role, content: msg.content };
|
|
192
|
+
}
|
|
193
|
+
const content = [];
|
|
194
|
+
if (msg.content) {
|
|
195
|
+
content.push({ type: "text", text: msg.content });
|
|
196
|
+
}
|
|
197
|
+
if (msg.images) {
|
|
198
|
+
for (const img of msg.images) {
|
|
199
|
+
content.push({
|
|
200
|
+
type: "image_url",
|
|
201
|
+
image_url: { url: img.url }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (msg.audios) {
|
|
206
|
+
for (const audio of msg.audios) {
|
|
207
|
+
const decoded = decodeDataUrl(audio.url);
|
|
208
|
+
if (decoded) {
|
|
209
|
+
const format = resolveAudioFormat(decoded.mimeType, audio.url);
|
|
210
|
+
content.push({ type: "input_audio", input_audio: { data: decoded.data, format } });
|
|
211
|
+
} else if (audio.mimeType) {
|
|
212
|
+
content.push({ type: "audio_url", audio_url: { url: audio.url, format: audio.mimeType } });
|
|
213
|
+
} else {
|
|
214
|
+
content.push({ type: "audio_url", audio_url: { url: audio.url } });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (msg.videos) {
|
|
219
|
+
for (const video of msg.videos) {
|
|
220
|
+
if (looksLikeRemoteUrl(video.url)) {
|
|
221
|
+
content.push({ type: "video_url", video_url: { url: video.url } });
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const decoded = decodeDataUrl(video.url);
|
|
225
|
+
if (decoded) {
|
|
226
|
+
const sizeBytes = Math.floor(decoded.data.length * 3 / 4);
|
|
227
|
+
if (sizeBytes > MAX_INLINE_VIDEO_BYTES) {
|
|
228
|
+
const sizeMb = (sizeBytes / 1024 / 1024).toFixed(1);
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Video too large for inline upload: ${sizeMb} MB exceeds the 25 MB cap. Use a publicly accessible HTTPS URL instead.`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
content.push({ type: "video_url", video_url: { url: video.url } });
|
|
234
|
+
} else {
|
|
235
|
+
content.push({ type: "video_url", video_url: { url: video.url } });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { role, content };
|
|
240
|
+
}
|
|
241
|
+
function convertMessageToAnthropic(msg) {
|
|
242
|
+
const role = msg.role === "system" ? "user" : msg.role;
|
|
243
|
+
const hasImages = msg.images && msg.images.length > 0;
|
|
244
|
+
const hasAudios = msg.audios && msg.audios.length > 0;
|
|
245
|
+
if (!hasImages && !hasAudios) {
|
|
246
|
+
return { role, content: msg.content };
|
|
247
|
+
}
|
|
248
|
+
const content = [];
|
|
249
|
+
if (msg.content) {
|
|
250
|
+
content.push({ type: "text", text: msg.content });
|
|
251
|
+
}
|
|
252
|
+
if (msg.images) {
|
|
253
|
+
for (const img of msg.images) {
|
|
254
|
+
let base64Data = img.url;
|
|
255
|
+
let mediaType = img.mimeType || "image/jpeg";
|
|
256
|
+
if (img.url.startsWith("data:")) {
|
|
257
|
+
const match = img.url.match(/^data:([^;]+);base64,(.+)$/);
|
|
258
|
+
if (match) {
|
|
259
|
+
mediaType = match[1];
|
|
260
|
+
base64Data = match[2];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
content.push({
|
|
264
|
+
type: "image",
|
|
265
|
+
source: { type: "base64", media_type: mediaType, data: base64Data }
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (msg.audios) {
|
|
270
|
+
for (const audio of msg.audios) {
|
|
271
|
+
let base64Data = audio.url;
|
|
272
|
+
let mediaType = audio.mimeType || "audio/wav";
|
|
273
|
+
if (audio.url.startsWith("data:")) {
|
|
274
|
+
const match = audio.url.match(/^data:([^;]+);base64,(.+)$/);
|
|
275
|
+
if (match) {
|
|
276
|
+
mediaType = match[1];
|
|
277
|
+
base64Data = match[2];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
content.push({
|
|
281
|
+
type: "audio",
|
|
282
|
+
source: { type: "base64", media_type: mediaType, data: base64Data }
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { role, content };
|
|
287
|
+
}
|
|
288
|
+
function convertMessageToGemini(msg) {
|
|
289
|
+
const role = msg.role === "assistant" ? "model" : msg.role;
|
|
290
|
+
const parts = [];
|
|
291
|
+
if (msg.content) {
|
|
292
|
+
parts.push({ text: msg.content });
|
|
293
|
+
}
|
|
294
|
+
if (msg.images && msg.images.length > 0) {
|
|
295
|
+
for (const img of msg.images) {
|
|
296
|
+
let base64Data = img.url;
|
|
297
|
+
let mimeType = img.mimeType || "image/jpeg";
|
|
298
|
+
if (img.url.startsWith("data:")) {
|
|
299
|
+
const match = img.url.match(/^data:([^;]+);base64,(.+)$/);
|
|
300
|
+
if (match) {
|
|
301
|
+
mimeType = match[1];
|
|
302
|
+
base64Data = match[2];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
parts.push({ inlineData: { mimeType, data: base64Data } });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (msg.audios && msg.audios.length > 0) {
|
|
309
|
+
for (const audio of msg.audios) {
|
|
310
|
+
let base64Data = audio.url;
|
|
311
|
+
let mimeType = audio.mimeType || "audio/wav";
|
|
312
|
+
if (audio.url.startsWith("data:")) {
|
|
313
|
+
const match = audio.url.match(/^data:([^;]+);base64,(.+)$/);
|
|
314
|
+
if (match) {
|
|
315
|
+
mimeType = match[1];
|
|
316
|
+
base64Data = match[2];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
parts.push({ inlineData: { mimeType, data: base64Data } });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { role, parts };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/completion/openrouter-utils.ts
|
|
326
|
+
function getOpenRouterProviderConfig(provider, modelId) {
|
|
327
|
+
if (!isOpenRouterProvider(provider)) {
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
const modelConfig = provider.modelConfigs?.find((m) => m.id === modelId);
|
|
331
|
+
return modelConfig?.openRouterProvider;
|
|
332
|
+
}
|
|
333
|
+
function addOpenRouterProviderToRequest(requestBody, provider, modelId) {
|
|
334
|
+
const providerRouting = getOpenRouterProviderConfig(provider, modelId);
|
|
335
|
+
if (!providerRouting) {
|
|
336
|
+
return requestBody;
|
|
337
|
+
}
|
|
338
|
+
const hasConfig = Object.values(providerRouting).some(
|
|
339
|
+
(v) => v !== void 0 && v !== null && (Array.isArray(v) ? v.length > 0 : true)
|
|
340
|
+
);
|
|
341
|
+
if (!hasConfig) {
|
|
342
|
+
return requestBody;
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
...requestBody,
|
|
346
|
+
provider: providerRouting
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/completion/native-search-types.ts
|
|
351
|
+
var NATIVE_SEARCH_TOOL_NAMES = [
|
|
352
|
+
"web_search",
|
|
353
|
+
"web_search_20250305",
|
|
354
|
+
"web_search_20260209",
|
|
355
|
+
"google_search",
|
|
356
|
+
"googleSearchRetrieval"
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
// src/completion/NativeSearchInjector.ts
|
|
360
|
+
function applyAugmentation(requestBody, augmentation) {
|
|
361
|
+
if (augmentation.additionalTools?.length) {
|
|
362
|
+
const existing = requestBody.tools ?? [];
|
|
363
|
+
requestBody.tools = [...existing, ...augmentation.additionalTools];
|
|
364
|
+
}
|
|
365
|
+
if (augmentation.bodyFields) {
|
|
366
|
+
Object.assign(requestBody, augmentation.bodyFields);
|
|
367
|
+
}
|
|
368
|
+
return requestBody;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/completion/DirectApiHandler.ts
|
|
372
|
+
import { getOpenAIReasoningEffort as getOpenAIReasoningEffort2 } from "@omnicross/contracts/thinking-config";
|
|
373
|
+
|
|
374
|
+
// src/sse-parser.ts
|
|
375
|
+
var DEBUG_SSE = true;
|
|
376
|
+
function debugSSE(prefix, ...args) {
|
|
377
|
+
if (DEBUG_SSE) {
|
|
378
|
+
console.log(`[SSE-Parser] ${prefix}`, ...args);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function createSSEParser(format, callbacks) {
|
|
382
|
+
const state = {
|
|
383
|
+
buffer: "",
|
|
384
|
+
content: "",
|
|
385
|
+
reasoning: "",
|
|
386
|
+
usage: void 0,
|
|
387
|
+
isDone: false,
|
|
388
|
+
streamStartTime: void 0,
|
|
389
|
+
firstTokenTime: void 0,
|
|
390
|
+
streamEndTime: void 0,
|
|
391
|
+
blocks: [],
|
|
392
|
+
currentBlockIndex: -1,
|
|
393
|
+
currentBlockType: "",
|
|
394
|
+
inputJsonBuffer: "",
|
|
395
|
+
audios: [],
|
|
396
|
+
videos: []
|
|
397
|
+
};
|
|
398
|
+
function parseOpenAIEvent(data) {
|
|
399
|
+
if (data === "[DONE]") {
|
|
400
|
+
debugSSE("OpenAI [DONE] received, content length:", state.content.length);
|
|
401
|
+
state.isDone = true;
|
|
402
|
+
callbacks.onDone?.();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const json = JSON.parse(data);
|
|
407
|
+
callbacks.onRawEvent?.({ data: json, raw: data });
|
|
408
|
+
const delta = json.choices?.[0]?.delta;
|
|
409
|
+
const finishReason = json.choices?.[0]?.finish_reason;
|
|
410
|
+
if (finishReason) {
|
|
411
|
+
debugSSE("OpenAI finish_reason:", finishReason, "content length so far:", state.content.length);
|
|
412
|
+
}
|
|
413
|
+
if (delta?.content) {
|
|
414
|
+
if (!state.firstTokenTime) {
|
|
415
|
+
state.firstTokenTime = Date.now();
|
|
416
|
+
}
|
|
417
|
+
state.content += delta.content;
|
|
418
|
+
callbacks.onDelta?.(delta.content);
|
|
419
|
+
}
|
|
420
|
+
if (delta?.reasoning_content) {
|
|
421
|
+
state.reasoning += delta.reasoning_content;
|
|
422
|
+
callbacks.onReasoning?.(delta.reasoning_content);
|
|
423
|
+
}
|
|
424
|
+
if (delta?.audio) {
|
|
425
|
+
const audioData = delta.audio;
|
|
426
|
+
if (audioData.data) {
|
|
427
|
+
const mimeType = audioData.format ? `audio/${audioData.format}` : "audio/wav";
|
|
428
|
+
const audio = {
|
|
429
|
+
url: audioData.data.startsWith("data:") ? audioData.data : `data:${mimeType};base64,${audioData.data}`,
|
|
430
|
+
mimeType
|
|
431
|
+
};
|
|
432
|
+
state.audios.push(audio);
|
|
433
|
+
callbacks.onAudio?.(audio);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (json.usage) {
|
|
437
|
+
debugSSE("OpenAI usage received:", JSON.stringify(json.usage));
|
|
438
|
+
debugSSE("OpenAI final chunk full data:", JSON.stringify(json));
|
|
439
|
+
state.usage = {
|
|
440
|
+
promptTokens: json.usage.prompt_tokens || 0,
|
|
441
|
+
completionTokens: json.usage.completion_tokens || 0,
|
|
442
|
+
totalTokens: json.usage.total_tokens || 0
|
|
443
|
+
};
|
|
444
|
+
callbacks.onUsage?.(state.usage);
|
|
445
|
+
}
|
|
446
|
+
if (finishReason === "stop" || finishReason === "end_turn" || finishReason === "length") {
|
|
447
|
+
debugSSE("OpenAI stream marked done due to finish_reason:", finishReason);
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function parseAnthropicEvent(data) {
|
|
453
|
+
try {
|
|
454
|
+
const json = JSON.parse(data);
|
|
455
|
+
callbacks.onRawEvent?.({ type: json.type, data: json, raw: data });
|
|
456
|
+
if (json.type === "content_block_start") {
|
|
457
|
+
const contentBlock = json.content_block;
|
|
458
|
+
const blockIndex = json.index ?? -1;
|
|
459
|
+
if (contentBlock?.type === "thinking") {
|
|
460
|
+
debugSSE("THINKING BLOCK STARTED!");
|
|
461
|
+
}
|
|
462
|
+
if (contentBlock?.type === "server_tool_use") {
|
|
463
|
+
state.currentBlockIndex = blockIndex;
|
|
464
|
+
state.currentBlockType = "server_tool_use";
|
|
465
|
+
state.inputJsonBuffer = "";
|
|
466
|
+
const toolUseBlock = {
|
|
467
|
+
id: contentBlock.id || `block_${Date.now()}`,
|
|
468
|
+
type: "tool_use",
|
|
469
|
+
toolId: contentBlock.id || "",
|
|
470
|
+
toolName: contentBlock.name || "web_search",
|
|
471
|
+
input: contentBlock.input || {},
|
|
472
|
+
status: "running"
|
|
473
|
+
};
|
|
474
|
+
state.blocks.push(toolUseBlock);
|
|
475
|
+
callbacks.onBlock?.(toolUseBlock);
|
|
476
|
+
}
|
|
477
|
+
if (contentBlock?.type === "web_search_tool_result") {
|
|
478
|
+
state.currentBlockIndex = blockIndex;
|
|
479
|
+
state.currentBlockType = "web_search_tool_result";
|
|
480
|
+
const searchResults = contentBlock.content;
|
|
481
|
+
let output = "";
|
|
482
|
+
if (Array.isArray(searchResults)) {
|
|
483
|
+
output = searchResults.filter((r) => r.type === "web_search_result").map(
|
|
484
|
+
(r) => `**${r.title || "Untitled"}**
|
|
485
|
+
${r.url || ""}
|
|
486
|
+
${r.encrypted_content ? "(encrypted)" : r.page_content || r.snippet || ""}`
|
|
487
|
+
).join("\n\n");
|
|
488
|
+
}
|
|
489
|
+
const lastToolUse = [...state.blocks].reverse().find(
|
|
490
|
+
(b) => b.type === "tool_use"
|
|
491
|
+
);
|
|
492
|
+
const toolResultBlock = {
|
|
493
|
+
id: `result_${Date.now()}`,
|
|
494
|
+
type: "tool_result",
|
|
495
|
+
toolId: lastToolUse?.toolId || "",
|
|
496
|
+
toolName: lastToolUse?.toolName || "web_search",
|
|
497
|
+
output: output || "(no results)"
|
|
498
|
+
};
|
|
499
|
+
state.blocks.push(toolResultBlock);
|
|
500
|
+
callbacks.onBlock?.(toolResultBlock);
|
|
501
|
+
if (lastToolUse) {
|
|
502
|
+
lastToolUse.status = "completed";
|
|
503
|
+
callbacks.onBlock?.(lastToolUse);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (json.type === "content_block_delta") {
|
|
508
|
+
const delta = json.delta;
|
|
509
|
+
if (delta?.type === "text_delta" && delta?.text) {
|
|
510
|
+
if (!state.firstTokenTime) {
|
|
511
|
+
state.firstTokenTime = Date.now();
|
|
512
|
+
}
|
|
513
|
+
state.content += delta.text;
|
|
514
|
+
callbacks.onDelta?.(delta.text);
|
|
515
|
+
}
|
|
516
|
+
if (delta?.type === "thinking_delta" && delta?.thinking) {
|
|
517
|
+
state.reasoning += delta.thinking;
|
|
518
|
+
callbacks.onReasoning?.(delta.thinking);
|
|
519
|
+
}
|
|
520
|
+
if (delta?.type === "input_json_delta" && delta?.partial_json) {
|
|
521
|
+
state.inputJsonBuffer += delta.partial_json;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (json.type === "content_block_stop") {
|
|
525
|
+
if (state.currentBlockType === "server_tool_use" && state.inputJsonBuffer) {
|
|
526
|
+
const lastToolUse = [...state.blocks].reverse().find(
|
|
527
|
+
(b) => b.type === "tool_use"
|
|
528
|
+
);
|
|
529
|
+
if (lastToolUse) {
|
|
530
|
+
try {
|
|
531
|
+
lastToolUse.input = JSON.parse(state.inputJsonBuffer);
|
|
532
|
+
} catch {
|
|
533
|
+
lastToolUse.input = { raw: state.inputJsonBuffer };
|
|
534
|
+
}
|
|
535
|
+
callbacks.onBlock?.(lastToolUse);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
state.currentBlockIndex = -1;
|
|
539
|
+
state.currentBlockType = "";
|
|
540
|
+
state.inputJsonBuffer = "";
|
|
541
|
+
}
|
|
542
|
+
if (json.type === "message_delta" && json.usage) {
|
|
543
|
+
state.usage = {
|
|
544
|
+
promptTokens: json.usage.input_tokens || 0,
|
|
545
|
+
completionTokens: json.usage.output_tokens || 0,
|
|
546
|
+
totalTokens: (json.usage.input_tokens || 0) + (json.usage.output_tokens || 0)
|
|
547
|
+
};
|
|
548
|
+
callbacks.onUsage?.(state.usage);
|
|
549
|
+
}
|
|
550
|
+
if (json.type === "message_stop") {
|
|
551
|
+
state.isDone = true;
|
|
552
|
+
callbacks.onDone?.();
|
|
553
|
+
}
|
|
554
|
+
if (json.type === "error") {
|
|
555
|
+
callbacks.onError?.(json.error?.message || "Unknown error");
|
|
556
|
+
}
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function parseGeminiEvent(data) {
|
|
561
|
+
try {
|
|
562
|
+
const json = JSON.parse(data);
|
|
563
|
+
callbacks.onRawEvent?.({ data: json, raw: data });
|
|
564
|
+
const candidates = json.candidates;
|
|
565
|
+
if (candidates && candidates.length > 0) {
|
|
566
|
+
const candidate = candidates[0];
|
|
567
|
+
const content = candidate.content;
|
|
568
|
+
if (content?.parts) {
|
|
569
|
+
for (const part of content.parts) {
|
|
570
|
+
if (part.thought === true && part.text) {
|
|
571
|
+
state.reasoning += part.text;
|
|
572
|
+
callbacks.onReasoning?.(part.text);
|
|
573
|
+
} else if (part.text) {
|
|
574
|
+
if (!state.firstTokenTime) {
|
|
575
|
+
state.firstTokenTime = Date.now();
|
|
576
|
+
}
|
|
577
|
+
state.content += part.text;
|
|
578
|
+
callbacks.onDelta?.(part.text);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const finishReason = candidate.finishReason;
|
|
583
|
+
if (finishReason === "STOP" || finishReason === "MAX_TOKENS" || finishReason === "SAFETY") {
|
|
584
|
+
debugSSE("Gemini finish reason:", finishReason);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const usage = json.usageMetadata;
|
|
588
|
+
if (usage) {
|
|
589
|
+
state.usage = {
|
|
590
|
+
promptTokens: usage.promptTokenCount || 0,
|
|
591
|
+
completionTokens: usage.candidatesTokenCount || 0,
|
|
592
|
+
totalTokens: usage.totalTokenCount || 0
|
|
593
|
+
};
|
|
594
|
+
callbacks.onUsage?.(state.usage);
|
|
595
|
+
debugSSE("Gemini usage:", state.usage);
|
|
596
|
+
}
|
|
597
|
+
if (json.error) {
|
|
598
|
+
callbacks.onError?.(json.error.message || "Gemini API error");
|
|
599
|
+
}
|
|
600
|
+
} catch {
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function parseOpenAIResponseEvent(data) {
|
|
604
|
+
try {
|
|
605
|
+
const json = JSON.parse(data);
|
|
606
|
+
callbacks.onRawEvent?.({ type: json.type, data: json, raw: data });
|
|
607
|
+
switch (json.type) {
|
|
608
|
+
case "response.output_text.delta":
|
|
609
|
+
if (json.delta) {
|
|
610
|
+
if (!state.firstTokenTime) {
|
|
611
|
+
state.firstTokenTime = Date.now();
|
|
612
|
+
}
|
|
613
|
+
state.content += json.delta;
|
|
614
|
+
callbacks.onDelta?.(json.delta);
|
|
615
|
+
}
|
|
616
|
+
break;
|
|
617
|
+
case "response.reasoning_summary_text.delta":
|
|
618
|
+
if (json.delta) {
|
|
619
|
+
state.reasoning += json.delta;
|
|
620
|
+
callbacks.onReasoning?.(json.delta);
|
|
621
|
+
}
|
|
622
|
+
break;
|
|
623
|
+
case "response.completed": {
|
|
624
|
+
const response = json.response;
|
|
625
|
+
if (response?.usage) {
|
|
626
|
+
state.usage = {
|
|
627
|
+
promptTokens: response.usage.input_tokens || 0,
|
|
628
|
+
completionTokens: response.usage.output_tokens || 0,
|
|
629
|
+
totalTokens: (response.usage.input_tokens || 0) + (response.usage.output_tokens || 0)
|
|
630
|
+
};
|
|
631
|
+
callbacks.onUsage?.(state.usage);
|
|
632
|
+
}
|
|
633
|
+
state.isDone = true;
|
|
634
|
+
callbacks.onDone?.();
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
case "error":
|
|
638
|
+
callbacks.onError?.(json.error?.message || json.message || "Unknown error");
|
|
639
|
+
break;
|
|
640
|
+
// Informational events — no-op
|
|
641
|
+
// response.created, response.in_progress, response.output_item.added,
|
|
642
|
+
// response.content_part.added, response.output_text.done,
|
|
643
|
+
// response.content_part.done, response.output_item.done, etc.
|
|
644
|
+
default:
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
} catch {
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function processEventBlock(eventBlock) {
|
|
651
|
+
const lines = eventBlock.split("\n");
|
|
652
|
+
const dataLines = [];
|
|
653
|
+
for (const line of lines) {
|
|
654
|
+
if (line.startsWith("data: ")) {
|
|
655
|
+
dataLines.push(line.slice(6));
|
|
656
|
+
} else if (line.trim() !== "" && !line.startsWith(":")) {
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (dataLines.length > 0) {
|
|
660
|
+
const data = dataLines.join("\n");
|
|
661
|
+
if (format === "openai") {
|
|
662
|
+
parseOpenAIEvent(data);
|
|
663
|
+
} else if (format === "gemini") {
|
|
664
|
+
parseGeminiEvent(data);
|
|
665
|
+
} else if (format === "openai-response") {
|
|
666
|
+
parseOpenAIResponseEvent(data);
|
|
667
|
+
} else {
|
|
668
|
+
parseAnthropicEvent(data);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
/**
|
|
674
|
+
* Push a chunk of data to the parser
|
|
675
|
+
* @param chunk - Raw SSE chunk from response stream
|
|
676
|
+
*/
|
|
677
|
+
push(chunk) {
|
|
678
|
+
if (state.isDone) return;
|
|
679
|
+
if (!state.streamStartTime) {
|
|
680
|
+
state.streamStartTime = Date.now();
|
|
681
|
+
}
|
|
682
|
+
state.buffer += chunk;
|
|
683
|
+
const events = state.buffer.split("\n\n");
|
|
684
|
+
state.buffer = events.pop() || "";
|
|
685
|
+
for (const eventBlock of events) {
|
|
686
|
+
processEventBlock(eventBlock);
|
|
687
|
+
if (state.isDone) break;
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
/**
|
|
691
|
+
* Flush any remaining data in the buffer
|
|
692
|
+
*/
|
|
693
|
+
flush() {
|
|
694
|
+
debugSSE("flush() called, buffer length:", state.buffer.length, "isDone:", state.isDone);
|
|
695
|
+
if (state.buffer.trim()) {
|
|
696
|
+
debugSSE("flush() processing remaining buffer:", state.buffer.substring(0, 200));
|
|
697
|
+
processEventBlock(state.buffer);
|
|
698
|
+
state.buffer = "";
|
|
699
|
+
}
|
|
700
|
+
state.streamEndTime = Date.now();
|
|
701
|
+
if (!state.isDone) {
|
|
702
|
+
debugSSE("flush() marking stream as done, final content length:", state.content.length);
|
|
703
|
+
state.isDone = true;
|
|
704
|
+
callbacks.onDone?.();
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
/**
|
|
708
|
+
* Get accumulated results
|
|
709
|
+
*/
|
|
710
|
+
getResult() {
|
|
711
|
+
let metrics;
|
|
712
|
+
if (state.usage && state.firstTokenTime && state.streamEndTime) {
|
|
713
|
+
const timeCompletionMs = state.streamEndTime - state.firstTokenTime;
|
|
714
|
+
const timeFirstTokenMs = state.streamStartTime ? state.firstTokenTime - state.streamStartTime : void 0;
|
|
715
|
+
metrics = {
|
|
716
|
+
completionTokens: state.usage.completionTokens,
|
|
717
|
+
timeCompletionMs: timeCompletionMs > 0 ? timeCompletionMs : 1,
|
|
718
|
+
// Ensure at least 1ms
|
|
719
|
+
timeFirstTokenMs
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
if (state.reasoning) {
|
|
723
|
+
const thinkingBlock = {
|
|
724
|
+
id: `thinking_${Date.now()}`,
|
|
725
|
+
type: "thinking",
|
|
726
|
+
content: state.reasoning
|
|
727
|
+
};
|
|
728
|
+
state.blocks.unshift(thinkingBlock);
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
content: state.content,
|
|
732
|
+
reasoning: state.reasoning,
|
|
733
|
+
usage: state.usage,
|
|
734
|
+
metrics,
|
|
735
|
+
blocks: state.blocks,
|
|
736
|
+
audios: state.audios,
|
|
737
|
+
videos: state.videos
|
|
738
|
+
};
|
|
739
|
+
},
|
|
740
|
+
/**
|
|
741
|
+
* Check if stream is complete
|
|
742
|
+
*/
|
|
743
|
+
isDone() {
|
|
744
|
+
return state.isDone;
|
|
745
|
+
},
|
|
746
|
+
/**
|
|
747
|
+
* Reset parser state for reuse
|
|
748
|
+
*/
|
|
749
|
+
reset() {
|
|
750
|
+
state.buffer = "";
|
|
751
|
+
state.content = "";
|
|
752
|
+
state.reasoning = "";
|
|
753
|
+
state.usage = void 0;
|
|
754
|
+
state.isDone = false;
|
|
755
|
+
state.streamStartTime = void 0;
|
|
756
|
+
state.firstTokenTime = void 0;
|
|
757
|
+
state.streamEndTime = void 0;
|
|
758
|
+
state.blocks = [];
|
|
759
|
+
state.currentBlockIndex = -1;
|
|
760
|
+
state.currentBlockType = "";
|
|
761
|
+
state.inputJsonBuffer = "";
|
|
762
|
+
state.audios = [];
|
|
763
|
+
state.videos = [];
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
async function streamSSEResponse(response, format, callbacks) {
|
|
768
|
+
debugSSE("streamSSEResponse started, format:", format);
|
|
769
|
+
const reader = response.body?.getReader();
|
|
770
|
+
if (!reader) {
|
|
771
|
+
callbacks.onError?.("No response body");
|
|
772
|
+
return { content: "", reasoning: "", blocks: [], audios: [], videos: [] };
|
|
773
|
+
}
|
|
774
|
+
const decoder = new TextDecoder();
|
|
775
|
+
const parser = createSSEParser(format, callbacks);
|
|
776
|
+
let chunkCount = 0;
|
|
777
|
+
try {
|
|
778
|
+
while (true) {
|
|
779
|
+
const { done, value } = await reader.read();
|
|
780
|
+
if (done) {
|
|
781
|
+
debugSSE("Reader done signal received after", chunkCount, "chunks");
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
785
|
+
chunkCount++;
|
|
786
|
+
parser.push(chunk);
|
|
787
|
+
if (parser.isDone()) {
|
|
788
|
+
debugSSE("Parser isDone after", chunkCount, "chunks");
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
parser.flush();
|
|
793
|
+
const result = parser.getResult();
|
|
794
|
+
debugSSE("streamSSEResponse complete, content length:", result.content.length, "completionTokens:", result.usage?.completionTokens, "usage:", result.usage);
|
|
795
|
+
return result;
|
|
796
|
+
} finally {
|
|
797
|
+
reader.releaseLock();
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/provider-proxy/ProviderProxy.ts
|
|
802
|
+
import http from "http";
|
|
803
|
+
|
|
804
|
+
// src/provider-proxy/providerProxyRouteMap.ts
|
|
805
|
+
import { randomBytes } from "crypto";
|
|
806
|
+
var DEFAULT_ROUTE_IDLE_MS = 10 * 60 * 1e3;
|
|
807
|
+
|
|
808
|
+
// src/pipeline/resolveProviderChain.ts
|
|
809
|
+
async function resolveProviderChain(llmConfig, providerId, model) {
|
|
810
|
+
const cachedChain = await llmConfig.resolveTransformerChain(providerId, model);
|
|
811
|
+
const mainTransformer = await llmConfig.getMainTransformer(providerId);
|
|
812
|
+
const chain = {
|
|
813
|
+
providerTransformers: [...cachedChain.providerTransformers],
|
|
814
|
+
modelTransformers: [...cachedChain.modelTransformers]
|
|
815
|
+
};
|
|
816
|
+
if (mainTransformer) {
|
|
817
|
+
const alreadyInChain = chain.providerTransformers.some(
|
|
818
|
+
(t) => t.name === mainTransformer.name
|
|
819
|
+
);
|
|
820
|
+
if (!alreadyInChain) {
|
|
821
|
+
chain.providerTransformers.unshift(mainTransformer);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
const hasTransformers = chain.providerTransformers.length > 0 || chain.modelTransformers.length > 0;
|
|
825
|
+
return { chain, mainTransformer, hasTransformers };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/transformer/anthropicBetaInject.ts
|
|
829
|
+
import { isExtendedContextCapable } from "@omnicross/contracts/extended-context";
|
|
830
|
+
var EXTENDED_CONTEXT_BETA = "context-1m-2025-08-07";
|
|
831
|
+
var ANTHROPIC_BETA_HEADER = "anthropic-beta";
|
|
832
|
+
function injectExtendedContextBeta(headers, model, useExtendedContext) {
|
|
833
|
+
if (!useExtendedContext) return;
|
|
834
|
+
if (!isExtendedContextCapable(model)) return;
|
|
835
|
+
let existingValue = "";
|
|
836
|
+
for (const key of Object.keys(headers)) {
|
|
837
|
+
if (key.toLowerCase() === ANTHROPIC_BETA_HEADER) {
|
|
838
|
+
const v = headers[key];
|
|
839
|
+
if (typeof v === "string") existingValue = v;
|
|
840
|
+
if (key !== ANTHROPIC_BETA_HEADER) delete headers[key];
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const parts = existingValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
844
|
+
if (!parts.includes(EXTENDED_CONTEXT_BETA)) {
|
|
845
|
+
parts.push(EXTENDED_CONTEXT_BETA);
|
|
846
|
+
}
|
|
847
|
+
headers[ANTHROPIC_BETA_HEADER] = parts.join(",");
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/pipeline/executeProviderCall.ts
|
|
851
|
+
async function executeProviderCall(ctx) {
|
|
852
|
+
const {
|
|
853
|
+
executor,
|
|
854
|
+
request,
|
|
855
|
+
provider,
|
|
856
|
+
chain,
|
|
857
|
+
endpointTransformer,
|
|
858
|
+
extendedContext,
|
|
859
|
+
resolveUrl,
|
|
860
|
+
buildHeaders,
|
|
861
|
+
prepareBody,
|
|
862
|
+
fetchFn,
|
|
863
|
+
runResponseChain = false,
|
|
864
|
+
responseChainRequest
|
|
865
|
+
} = ctx;
|
|
866
|
+
const { requestBody, config } = await executor.executeRequestChain(
|
|
867
|
+
request,
|
|
868
|
+
provider,
|
|
869
|
+
chain,
|
|
870
|
+
{ endpointTransformer, extendedContext }
|
|
871
|
+
);
|
|
872
|
+
const finalBody = prepareBody ? await prepareBody(requestBody, config) : requestBody;
|
|
873
|
+
const url = resolveUrl(config);
|
|
874
|
+
const headers = buildHeaders(config);
|
|
875
|
+
const fetched = await fetchFn(url, headers, finalBody);
|
|
876
|
+
let response = fetched;
|
|
877
|
+
if (runResponseChain) {
|
|
878
|
+
response = await executor.executeResponseChain(
|
|
879
|
+
// Preserve the EXACT first arg each site passed: proxy → requestBody,
|
|
880
|
+
// unified ingresses → their pre-transform request. The caller supplies
|
|
881
|
+
// it via `responseChainRequest`; fall back to `requestBody` (the proxy
|
|
882
|
+
// shape) so the contract is never silently `undefined`.
|
|
883
|
+
responseChainRequest ?? requestBody,
|
|
884
|
+
fetched,
|
|
885
|
+
provider,
|
|
886
|
+
chain,
|
|
887
|
+
{ endpointTransformer }
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
return { response, requestBody, finalBody, config, url, headers };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// src/transformer/TransformerChainExecutor.ts
|
|
894
|
+
var defaultLogger = {
|
|
895
|
+
debug: (msg, ...args) => console.debug(`[ChainExecutor] ${msg}`, ...args),
|
|
896
|
+
info: (msg, ...args) => console.info(`[ChainExecutor] ${msg}`, ...args),
|
|
897
|
+
warn: (msg, ...args) => console.warn(`[ChainExecutor] ${msg}`, ...args),
|
|
898
|
+
error: (msg, ...args) => console.error(`[ChainExecutor] ${msg}`, ...args)
|
|
899
|
+
};
|
|
900
|
+
var TransformerChainExecutor = class {
|
|
901
|
+
logger;
|
|
902
|
+
constructor(logger) {
|
|
903
|
+
this.logger = logger ?? defaultLogger;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Execute the request transformation chain
|
|
907
|
+
*
|
|
908
|
+
* @param request - Original request body
|
|
909
|
+
* @param provider - LLM provider configuration
|
|
910
|
+
* @param chain - Resolved transformer chain
|
|
911
|
+
* @param options - Execution options
|
|
912
|
+
* @returns Transformed request result
|
|
913
|
+
*/
|
|
914
|
+
async executeRequestChain(request, provider, chain, options = {}) {
|
|
915
|
+
const { endpointTransformer, headers, extendedContext } = options;
|
|
916
|
+
const context = {
|
|
917
|
+
logger: this.logger,
|
|
918
|
+
providerName: provider.name
|
|
919
|
+
};
|
|
920
|
+
let requestBody = request;
|
|
921
|
+
let config = {};
|
|
922
|
+
let bypass = false;
|
|
923
|
+
bypass = this.shouldBypassTransformers(
|
|
924
|
+
chain,
|
|
925
|
+
endpointTransformer,
|
|
926
|
+
requestBody
|
|
927
|
+
);
|
|
928
|
+
if (bypass) {
|
|
929
|
+
if (headers) {
|
|
930
|
+
const cleanHeaders = this.cleanHeaders(headers);
|
|
931
|
+
config.headers = cleanHeaders;
|
|
932
|
+
}
|
|
933
|
+
this.logger.debug("Bypass mode enabled - skipping transformations");
|
|
934
|
+
}
|
|
935
|
+
if (!bypass && endpointTransformer?.transformRequestOut) {
|
|
936
|
+
this.logger.debug("Executing transformRequestOut");
|
|
937
|
+
try {
|
|
938
|
+
const transformOut = await endpointTransformer.transformRequestOut(requestBody, context);
|
|
939
|
+
if (transformOut && typeof transformOut === "object") {
|
|
940
|
+
if ("body" in transformOut) {
|
|
941
|
+
requestBody = transformOut.body;
|
|
942
|
+
config = transformOut.config ?? {};
|
|
943
|
+
} else {
|
|
944
|
+
requestBody = transformOut;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
} catch (error) {
|
|
948
|
+
this.logger.error(`transformRequestOut error: ${this.getErrorMessage(error)}`);
|
|
949
|
+
throw error;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (!bypass && chain.providerTransformers.length > 0) {
|
|
953
|
+
this.logger.debug(`Executing ${chain.providerTransformers.length} provider transformers`);
|
|
954
|
+
for (const transformer of chain.providerTransformers) {
|
|
955
|
+
if (transformer.transformRequestIn) {
|
|
956
|
+
try {
|
|
957
|
+
const transformIn = await transformer.transformRequestIn(
|
|
958
|
+
requestBody,
|
|
959
|
+
provider,
|
|
960
|
+
context
|
|
961
|
+
);
|
|
962
|
+
if (transformIn && typeof transformIn === "object") {
|
|
963
|
+
if ("body" in transformIn) {
|
|
964
|
+
requestBody = transformIn.body;
|
|
965
|
+
config = { ...config, ...transformIn.config };
|
|
966
|
+
} else {
|
|
967
|
+
requestBody = transformIn;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
} catch (error) {
|
|
971
|
+
this.logger.error(
|
|
972
|
+
`Provider transformer ${transformer.name} error: ${this.getErrorMessage(error)}`
|
|
973
|
+
);
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (!bypass && chain.modelTransformers.length > 0) {
|
|
980
|
+
this.logger.debug(`Executing ${chain.modelTransformers.length} model transformers`);
|
|
981
|
+
for (const transformer of chain.modelTransformers) {
|
|
982
|
+
if (transformer.transformRequestIn) {
|
|
983
|
+
try {
|
|
984
|
+
const result = await transformer.transformRequestIn(
|
|
985
|
+
requestBody,
|
|
986
|
+
provider,
|
|
987
|
+
context
|
|
988
|
+
);
|
|
989
|
+
requestBody = result;
|
|
990
|
+
} catch (error) {
|
|
991
|
+
this.logger.error(
|
|
992
|
+
`Model transformer ${transformer.name} error: ${this.getErrorMessage(error)}`
|
|
993
|
+
);
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (extendedContext?.enabled) {
|
|
1000
|
+
if (!config.headers || typeof config.headers !== "object") {
|
|
1001
|
+
config.headers = {};
|
|
1002
|
+
}
|
|
1003
|
+
injectExtendedContextBeta(
|
|
1004
|
+
config.headers,
|
|
1005
|
+
extendedContext.model,
|
|
1006
|
+
true
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
return { requestBody, config, bypass };
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Execute the response transformation chain
|
|
1013
|
+
*
|
|
1014
|
+
* @param request - Original request (for context)
|
|
1015
|
+
* @param response - Response from provider
|
|
1016
|
+
* @param provider - LLM provider configuration
|
|
1017
|
+
* @param chain - Resolved transformer chain
|
|
1018
|
+
* @param options - Execution options
|
|
1019
|
+
* @returns Transformed response
|
|
1020
|
+
*/
|
|
1021
|
+
async executeResponseChain(request, response, provider, chain, options = {}) {
|
|
1022
|
+
const { endpointTransformer } = options;
|
|
1023
|
+
const context = {
|
|
1024
|
+
logger: this.logger,
|
|
1025
|
+
providerName: provider.name
|
|
1026
|
+
};
|
|
1027
|
+
let finalResponse = response;
|
|
1028
|
+
const bypass = this.shouldBypassTransformers(chain, endpointTransformer, request);
|
|
1029
|
+
if (bypass) {
|
|
1030
|
+
this.logger.debug("Bypass mode - skipping response transformations");
|
|
1031
|
+
return finalResponse;
|
|
1032
|
+
}
|
|
1033
|
+
if (chain.modelTransformers.length > 0) {
|
|
1034
|
+
const reversedModelTransformers = [...chain.modelTransformers].reverse();
|
|
1035
|
+
this.logger.debug(
|
|
1036
|
+
`Executing ${reversedModelTransformers.length} model response transformers (reversed)`
|
|
1037
|
+
);
|
|
1038
|
+
for (const transformer of reversedModelTransformers) {
|
|
1039
|
+
if (transformer.transformResponseOut) {
|
|
1040
|
+
try {
|
|
1041
|
+
finalResponse = await transformer.transformResponseOut(finalResponse, context);
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
this.logger.error(
|
|
1044
|
+
`Model transformer ${transformer.name} response error: ${this.getErrorMessage(error)}`
|
|
1045
|
+
);
|
|
1046
|
+
throw error;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (chain.providerTransformers.length > 0) {
|
|
1052
|
+
const reversedProviderTransformers = [...chain.providerTransformers].reverse();
|
|
1053
|
+
this.logger.debug(
|
|
1054
|
+
`Executing ${reversedProviderTransformers.length} provider response transformers (reversed)`
|
|
1055
|
+
);
|
|
1056
|
+
for (const transformer of reversedProviderTransformers) {
|
|
1057
|
+
if (transformer.transformResponseOut) {
|
|
1058
|
+
try {
|
|
1059
|
+
finalResponse = await transformer.transformResponseOut(finalResponse, context);
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
this.logger.error(
|
|
1062
|
+
`Provider transformer ${transformer.name} response error: ${this.getErrorMessage(error)}`
|
|
1063
|
+
);
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (endpointTransformer?.transformResponseIn) {
|
|
1070
|
+
this.logger.debug("Executing transformResponseIn");
|
|
1071
|
+
try {
|
|
1072
|
+
finalResponse = await endpointTransformer.transformResponseIn(finalResponse, context);
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
this.logger.error(`transformResponseIn error: ${this.getErrorMessage(error)}`);
|
|
1075
|
+
throw error;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return finalResponse;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Execute authentication handler if available
|
|
1082
|
+
*
|
|
1083
|
+
* @param request - Request body
|
|
1084
|
+
* @param provider - LLM provider
|
|
1085
|
+
* @param endpointTransformer - Endpoint transformer with auth handler
|
|
1086
|
+
* @param context - Transformer context
|
|
1087
|
+
* @returns Auth result with potentially modified request and config
|
|
1088
|
+
*/
|
|
1089
|
+
async executeAuth(request, provider, endpointTransformer, context) {
|
|
1090
|
+
let requestBody = request;
|
|
1091
|
+
let config = {};
|
|
1092
|
+
if (endpointTransformer?.auth) {
|
|
1093
|
+
this.logger.debug("Executing auth handler");
|
|
1094
|
+
try {
|
|
1095
|
+
const auth = await endpointTransformer.auth(requestBody, provider, context);
|
|
1096
|
+
if (auth && typeof auth === "object") {
|
|
1097
|
+
if ("body" in auth) {
|
|
1098
|
+
requestBody = auth.body;
|
|
1099
|
+
const authConfig = auth.config;
|
|
1100
|
+
if (authConfig) {
|
|
1101
|
+
const headers = { ...config.headers ?? {}, ...authConfig.headers ?? {} };
|
|
1102
|
+
delete headers["host"];
|
|
1103
|
+
config = { ...config, ...authConfig, headers };
|
|
1104
|
+
}
|
|
1105
|
+
} else {
|
|
1106
|
+
requestBody = auth;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
this.logger.error(`Auth handler error: ${this.getErrorMessage(error)}`);
|
|
1111
|
+
throw error;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return { requestBody, config };
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Check if transformers should be bypassed (optimization)
|
|
1118
|
+
*
|
|
1119
|
+
* Bypass is enabled when:
|
|
1120
|
+
* - Provider has only one transformer that matches the endpoint transformer
|
|
1121
|
+
* - Model has no specific transformers or only the same endpoint transformer
|
|
1122
|
+
*/
|
|
1123
|
+
shouldBypassTransformers(chain, endpointTransformer, _request) {
|
|
1124
|
+
if (!endpointTransformer?.name) {
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
const providerHasOnlyEndpoint = chain.providerTransformers.length === 1 && chain.providerTransformers[0]?.name === endpointTransformer.name;
|
|
1128
|
+
const modelHasNoTransformers = chain.modelTransformers.length === 0;
|
|
1129
|
+
const modelHasOnlyEndpoint = chain.modelTransformers.length === 1 && chain.modelTransformers[0]?.name === endpointTransformer.name;
|
|
1130
|
+
return providerHasOnlyEndpoint && (modelHasNoTransformers || modelHasOnlyEndpoint);
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Clean headers for pass-through
|
|
1134
|
+
*/
|
|
1135
|
+
cleanHeaders(headers) {
|
|
1136
|
+
const result = {};
|
|
1137
|
+
if (headers instanceof Headers) {
|
|
1138
|
+
headers.forEach((value, key) => {
|
|
1139
|
+
if (key.toLowerCase() !== "content-length") {
|
|
1140
|
+
result[key] = value;
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
} else {
|
|
1144
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1145
|
+
if (key.toLowerCase() !== "content-length") {
|
|
1146
|
+
result[key] = value;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return result;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Get error message from unknown error
|
|
1154
|
+
*/
|
|
1155
|
+
getErrorMessage(error) {
|
|
1156
|
+
if (error instanceof Error) {
|
|
1157
|
+
return error.message;
|
|
1158
|
+
}
|
|
1159
|
+
return String(error);
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
// src/transformer/transformers/ReasoningTransformer.ts
|
|
1164
|
+
import {
|
|
1165
|
+
buildAnthropicThinking,
|
|
1166
|
+
buildQwenThinkingConfig,
|
|
1167
|
+
calculateThinkingBudget,
|
|
1168
|
+
getOpenAIReasoningEffort
|
|
1169
|
+
} from "@omnicross/contracts/thinking-config";
|
|
1170
|
+
|
|
1171
|
+
// src/outbound-api/OutboundApiServer.ts
|
|
1172
|
+
import http2 from "http";
|
|
1173
|
+
import { networkInterfaces } from "os";
|
|
1174
|
+
|
|
1175
|
+
// src/outbound-api/outboundApiRouter.ts
|
|
1176
|
+
import { Readable } from "stream";
|
|
1177
|
+
|
|
1178
|
+
// src/outbound-api/outboundApiKeyAuth.ts
|
|
1179
|
+
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
1180
|
+
|
|
1181
|
+
// src/outbound-api/roleDetection.ts
|
|
1182
|
+
import { normalizeModelId } from "@omnicross/contracts/canonical-models";
|
|
1183
|
+
|
|
1184
|
+
// src/outbound-api/subscriptionSupport.ts
|
|
1185
|
+
var SUBSCRIPTION_PROVIDER_IDS = [
|
|
1186
|
+
"claude",
|
|
1187
|
+
"codex",
|
|
1188
|
+
"gemini",
|
|
1189
|
+
"opencodego"
|
|
1190
|
+
];
|
|
1191
|
+
var SUBSCRIPTION_ID_SET = new Set(SUBSCRIPTION_PROVIDER_IDS);
|
|
1192
|
+
|
|
1193
|
+
// src/api-converter/shared.ts
|
|
1194
|
+
function convertOpenAITool(tool) {
|
|
1195
|
+
return {
|
|
1196
|
+
name: tool.function.name,
|
|
1197
|
+
description: tool.function.description,
|
|
1198
|
+
input_schema: tool.function.parameters
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function mapAnthropicStopReason(stopReason) {
|
|
1202
|
+
switch (stopReason) {
|
|
1203
|
+
case "end_turn":
|
|
1204
|
+
return "stop";
|
|
1205
|
+
case "max_tokens":
|
|
1206
|
+
return "length";
|
|
1207
|
+
case "tool_use":
|
|
1208
|
+
return "tool_calls";
|
|
1209
|
+
case "stop_sequence":
|
|
1210
|
+
return "stop";
|
|
1211
|
+
default:
|
|
1212
|
+
return "stop";
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// src/api-converter/anthropic-to-openai.ts
|
|
1217
|
+
function convertAnthropicToOpenAI(response) {
|
|
1218
|
+
const content = [];
|
|
1219
|
+
const toolCalls = [];
|
|
1220
|
+
let reasoningContent = "";
|
|
1221
|
+
for (const block of response.content) {
|
|
1222
|
+
if (block.type === "text") {
|
|
1223
|
+
content.push(block.text);
|
|
1224
|
+
} else if (block.type === "tool_use") {
|
|
1225
|
+
toolCalls.push({
|
|
1226
|
+
id: block.id,
|
|
1227
|
+
type: "function",
|
|
1228
|
+
function: {
|
|
1229
|
+
name: block.name,
|
|
1230
|
+
arguments: JSON.stringify(block.input)
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
} else if (block.type === "thinking") {
|
|
1234
|
+
reasoningContent += block.thinking;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const finishReason = mapAnthropicStopReason(response.stop_reason);
|
|
1238
|
+
const result = {
|
|
1239
|
+
id: response.id,
|
|
1240
|
+
object: "chat.completion",
|
|
1241
|
+
created: Math.floor(Date.now() / 1e3),
|
|
1242
|
+
model: response.model,
|
|
1243
|
+
choices: [
|
|
1244
|
+
{
|
|
1245
|
+
index: 0,
|
|
1246
|
+
message: {
|
|
1247
|
+
role: "assistant",
|
|
1248
|
+
content: content.join("\n") || null,
|
|
1249
|
+
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
|
|
1250
|
+
},
|
|
1251
|
+
finish_reason: finishReason
|
|
1252
|
+
}
|
|
1253
|
+
],
|
|
1254
|
+
usage: {
|
|
1255
|
+
prompt_tokens: response.usage.input_tokens,
|
|
1256
|
+
completion_tokens: response.usage.output_tokens,
|
|
1257
|
+
total_tokens: response.usage.input_tokens + response.usage.output_tokens
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
if (reasoningContent) {
|
|
1261
|
+
result.reasoning_content = reasoningContent;
|
|
1262
|
+
}
|
|
1263
|
+
return result;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/api-converter/openai-to-anthropic.ts
|
|
1267
|
+
function convertOpenAIToAnthropic(request, config) {
|
|
1268
|
+
const messages = [];
|
|
1269
|
+
let system;
|
|
1270
|
+
for (const msg of request.messages) {
|
|
1271
|
+
if (msg.role === "system") {
|
|
1272
|
+
if (typeof msg.content === "string") {
|
|
1273
|
+
system = msg.content;
|
|
1274
|
+
} else if (Array.isArray(msg.content)) {
|
|
1275
|
+
system = msg.content.filter((p) => p.type === "text").map((p) => ({ type: "text", text: p.text || "" }));
|
|
1276
|
+
}
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
if (msg.role === "tool" && msg.tool_call_id) {
|
|
1280
|
+
const toolResult = {
|
|
1281
|
+
type: "tool_result",
|
|
1282
|
+
tool_use_id: msg.tool_call_id,
|
|
1283
|
+
content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
|
|
1284
|
+
};
|
|
1285
|
+
const lastMsg = messages[messages.length - 1];
|
|
1286
|
+
if (lastMsg && lastMsg.role === "user" && Array.isArray(lastMsg.content)) {
|
|
1287
|
+
lastMsg.content.push(toolResult);
|
|
1288
|
+
} else {
|
|
1289
|
+
messages.push({ role: "user", content: [toolResult] });
|
|
1290
|
+
}
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
|
|
1294
|
+
const content = [];
|
|
1295
|
+
if (msg.content) {
|
|
1296
|
+
if (typeof msg.content === "string") {
|
|
1297
|
+
content.push({ type: "text", text: msg.content });
|
|
1298
|
+
} else if (Array.isArray(msg.content)) {
|
|
1299
|
+
for (const part of msg.content) {
|
|
1300
|
+
if (part.type === "text" && part.text) {
|
|
1301
|
+
content.push({ type: "text", text: part.text });
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
for (const tc of msg.tool_calls) {
|
|
1307
|
+
content.push({
|
|
1308
|
+
type: "tool_use",
|
|
1309
|
+
id: tc.id,
|
|
1310
|
+
name: tc.function.name,
|
|
1311
|
+
input: JSON.parse(tc.function.arguments || "{}")
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
messages.push({ role: "assistant", content });
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
if (msg.role === "user" || msg.role === "assistant") {
|
|
1318
|
+
const content = convertOpenAIMessageContent(msg);
|
|
1319
|
+
messages.push({ role: msg.role, content });
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
let model = request.model;
|
|
1323
|
+
if (config.modelMapping && config.modelMapping[model]) {
|
|
1324
|
+
model = config.modelMapping[model];
|
|
1325
|
+
} else if (!model.startsWith("claude")) {
|
|
1326
|
+
model = config.defaultModel;
|
|
1327
|
+
}
|
|
1328
|
+
const result = {
|
|
1329
|
+
model,
|
|
1330
|
+
// Anthropic requires max_tokens; use high default (16384) if not explicitly set
|
|
1331
|
+
// This prevents output truncation for long responses
|
|
1332
|
+
max_tokens: request.max_tokens || 16384,
|
|
1333
|
+
messages,
|
|
1334
|
+
stream: request.stream
|
|
1335
|
+
};
|
|
1336
|
+
if (system) {
|
|
1337
|
+
result.system = system;
|
|
1338
|
+
}
|
|
1339
|
+
if (request.tools && request.tools.length > 0) {
|
|
1340
|
+
result.tools = request.tools.map(convertOpenAITool);
|
|
1341
|
+
if (request.tool_choice) {
|
|
1342
|
+
if (request.tool_choice === "auto") {
|
|
1343
|
+
result.tool_choice = { type: "auto" };
|
|
1344
|
+
} else if (request.tool_choice === "required") {
|
|
1345
|
+
result.tool_choice = { type: "any" };
|
|
1346
|
+
} else if (typeof request.tool_choice === "object") {
|
|
1347
|
+
result.tool_choice = {
|
|
1348
|
+
type: "tool",
|
|
1349
|
+
name: request.tool_choice.function.name
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return result;
|
|
1355
|
+
}
|
|
1356
|
+
function convertOpenAIMessageContent(msg) {
|
|
1357
|
+
if (typeof msg.content === "string") {
|
|
1358
|
+
return msg.content;
|
|
1359
|
+
}
|
|
1360
|
+
if (!msg.content || !Array.isArray(msg.content)) {
|
|
1361
|
+
return "";
|
|
1362
|
+
}
|
|
1363
|
+
const parts = [];
|
|
1364
|
+
for (const part of msg.content) {
|
|
1365
|
+
if (part.type === "text" && part.text) {
|
|
1366
|
+
parts.push({ type: "text", text: part.text });
|
|
1367
|
+
} else if (part.type === "image_url" && part.image_url) {
|
|
1368
|
+
const url = part.image_url.url;
|
|
1369
|
+
if (url.startsWith("data:")) {
|
|
1370
|
+
const match = url.match(/^data:([^;]+);base64,(.+)$/);
|
|
1371
|
+
if (match) {
|
|
1372
|
+
parts.push({
|
|
1373
|
+
type: "image",
|
|
1374
|
+
source: {
|
|
1375
|
+
type: "base64",
|
|
1376
|
+
media_type: match[1],
|
|
1377
|
+
data: match[2]
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
} else {
|
|
1382
|
+
parts.push({
|
|
1383
|
+
type: "image",
|
|
1384
|
+
source: { type: "url", url }
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return parts.length > 0 ? parts : "";
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// src/completion/DirectApiHandler.ts
|
|
1393
|
+
async function callOpenAICompletion(provider, apiKey, options, logger) {
|
|
1394
|
+
const request = {
|
|
1395
|
+
model: options.model,
|
|
1396
|
+
messages: options.messages.map((m) => convertMessageToOpenAI(m)),
|
|
1397
|
+
// Only set max_tokens if explicitly provided, otherwise let API use its default
|
|
1398
|
+
...options.maxTokens ? { max_tokens: options.maxTokens } : {},
|
|
1399
|
+
temperature: options.temperature,
|
|
1400
|
+
stream: false
|
|
1401
|
+
// For now, non-streaming only
|
|
1402
|
+
};
|
|
1403
|
+
if (options.thinkLevel && options.thinkLevel !== "none") {
|
|
1404
|
+
const effort = getOpenAIReasoningEffort2(options.thinkLevel);
|
|
1405
|
+
if (effort) {
|
|
1406
|
+
request.reasoning_effort = effort;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: false });
|
|
1410
|
+
const headers = getProviderHeaders(provider, apiKey);
|
|
1411
|
+
logger.info("Calling OpenAI completion API", { url: apiUrl, model: options.model });
|
|
1412
|
+
const response = await fetch(apiUrl, {
|
|
1413
|
+
method: "POST",
|
|
1414
|
+
headers,
|
|
1415
|
+
body: JSON.stringify(request)
|
|
1416
|
+
});
|
|
1417
|
+
if (!response.ok) {
|
|
1418
|
+
const errorText = await response.text();
|
|
1419
|
+
return {
|
|
1420
|
+
success: false,
|
|
1421
|
+
error: `API error (${response.status}): ${errorText}`
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
const data = await response.json();
|
|
1425
|
+
const choice = data.choices[0];
|
|
1426
|
+
return {
|
|
1427
|
+
success: true,
|
|
1428
|
+
message: {
|
|
1429
|
+
id: `msg_${Date.now()}`,
|
|
1430
|
+
role: "assistant",
|
|
1431
|
+
content: choice.message.content || "",
|
|
1432
|
+
timestamp: Date.now(),
|
|
1433
|
+
thinking: data.reasoning_content ? { content: data.reasoning_content } : void 0
|
|
1434
|
+
},
|
|
1435
|
+
usage: {
|
|
1436
|
+
promptTokens: data.usage.prompt_tokens,
|
|
1437
|
+
completionTokens: data.usage.completion_tokens,
|
|
1438
|
+
totalTokens: data.usage.total_tokens
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
async function callAnthropicCompletion(provider, apiKey, options, logger) {
|
|
1443
|
+
const hasImages = options.messages.some((m) => m.images && m.images.length > 0);
|
|
1444
|
+
if (hasImages) {
|
|
1445
|
+
const systemMessages = options.messages.filter((m) => m.role === "system");
|
|
1446
|
+
const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
|
|
1447
|
+
const anthropicRequest2 = {
|
|
1448
|
+
model: options.model,
|
|
1449
|
+
max_tokens: options.maxTokens || 16384,
|
|
1450
|
+
temperature: options.temperature,
|
|
1451
|
+
...systemMessages.length > 0 ? { system: systemMessages.map((m) => m.content).join("\n\n") } : {},
|
|
1452
|
+
messages: nonSystemMessages.map((m) => convertMessageToAnthropic(m)),
|
|
1453
|
+
stream: false
|
|
1454
|
+
};
|
|
1455
|
+
const apiUrl2 = buildProviderApiUrl(provider, { model: options.model, stream: false });
|
|
1456
|
+
const headers2 = getProviderHeaders(provider, apiKey);
|
|
1457
|
+
logger.info("Calling Anthropic completion API with images", { url: apiUrl2, model: options.model });
|
|
1458
|
+
const response2 = await fetch(apiUrl2, {
|
|
1459
|
+
method: "POST",
|
|
1460
|
+
headers: headers2,
|
|
1461
|
+
body: JSON.stringify(anthropicRequest2)
|
|
1462
|
+
});
|
|
1463
|
+
if (!response2.ok) {
|
|
1464
|
+
const errorText = await response2.text();
|
|
1465
|
+
return {
|
|
1466
|
+
success: false,
|
|
1467
|
+
error: `API error (${response2.status}): ${errorText}`
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
const anthropicResponse2 = await response2.json();
|
|
1471
|
+
const openaiResponse2 = convertAnthropicToOpenAI(anthropicResponse2);
|
|
1472
|
+
const choice2 = openaiResponse2.choices[0];
|
|
1473
|
+
return {
|
|
1474
|
+
success: true,
|
|
1475
|
+
message: {
|
|
1476
|
+
id: `assistant-${Date.now()}`,
|
|
1477
|
+
role: "assistant",
|
|
1478
|
+
content: choice2.message?.content || "",
|
|
1479
|
+
timestamp: Date.now()
|
|
1480
|
+
},
|
|
1481
|
+
usage: openaiResponse2.usage ? {
|
|
1482
|
+
promptTokens: openaiResponse2.usage.prompt_tokens,
|
|
1483
|
+
completionTokens: openaiResponse2.usage.completion_tokens,
|
|
1484
|
+
totalTokens: openaiResponse2.usage.total_tokens
|
|
1485
|
+
} : void 0
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
const openaiRequest = {
|
|
1489
|
+
model: options.model,
|
|
1490
|
+
messages: options.messages.map((m) => ({
|
|
1491
|
+
role: m.role,
|
|
1492
|
+
content: m.content
|
|
1493
|
+
})),
|
|
1494
|
+
// Anthropic requires max_tokens; use 16384 default if not explicitly set
|
|
1495
|
+
max_tokens: options.maxTokens || 16384,
|
|
1496
|
+
temperature: options.temperature,
|
|
1497
|
+
stream: false
|
|
1498
|
+
};
|
|
1499
|
+
const config = {
|
|
1500
|
+
defaultModel: options.model
|
|
1501
|
+
};
|
|
1502
|
+
const anthropicRequest = convertOpenAIToAnthropic(openaiRequest, config);
|
|
1503
|
+
const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: false });
|
|
1504
|
+
const headers = getProviderHeaders(provider, apiKey);
|
|
1505
|
+
logger.info("Calling Anthropic completion API", { url: apiUrl, model: options.model });
|
|
1506
|
+
const response = await fetch(apiUrl, {
|
|
1507
|
+
method: "POST",
|
|
1508
|
+
headers,
|
|
1509
|
+
body: JSON.stringify(anthropicRequest)
|
|
1510
|
+
});
|
|
1511
|
+
if (!response.ok) {
|
|
1512
|
+
const errorText = await response.text();
|
|
1513
|
+
return {
|
|
1514
|
+
success: false,
|
|
1515
|
+
error: `API error (${response.status}): ${errorText}`
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
const anthropicResponse = await response.json();
|
|
1519
|
+
const openaiResponse = convertAnthropicToOpenAI(anthropicResponse);
|
|
1520
|
+
const choice = openaiResponse.choices[0];
|
|
1521
|
+
return {
|
|
1522
|
+
success: true,
|
|
1523
|
+
message: {
|
|
1524
|
+
id: `msg_${Date.now()}`,
|
|
1525
|
+
role: "assistant",
|
|
1526
|
+
content: choice.message.content || "",
|
|
1527
|
+
timestamp: Date.now(),
|
|
1528
|
+
thinking: openaiResponse.reasoning_content ? { content: openaiResponse.reasoning_content } : void 0
|
|
1529
|
+
},
|
|
1530
|
+
usage: {
|
|
1531
|
+
promptTokens: openaiResponse.usage.prompt_tokens,
|
|
1532
|
+
completionTokens: openaiResponse.usage.completion_tokens,
|
|
1533
|
+
totalTokens: openaiResponse.usage.total_tokens
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
async function callGeminiCompletion(provider, apiKey, options, logger) {
|
|
1538
|
+
const contents = [];
|
|
1539
|
+
let systemInstruction;
|
|
1540
|
+
for (const msg of options.messages) {
|
|
1541
|
+
if (msg.role === "system") {
|
|
1542
|
+
systemInstruction = { parts: [{ text: msg.content }] };
|
|
1543
|
+
} else {
|
|
1544
|
+
contents.push(convertMessageToGemini(msg));
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
const request = {
|
|
1548
|
+
contents,
|
|
1549
|
+
generationConfig: {
|
|
1550
|
+
...options.maxTokens ? { maxOutputTokens: options.maxTokens } : {},
|
|
1551
|
+
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
if (systemInstruction) {
|
|
1555
|
+
request.systemInstruction = systemInstruction;
|
|
1556
|
+
}
|
|
1557
|
+
const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: false });
|
|
1558
|
+
const headers = getProviderHeaders(provider, apiKey);
|
|
1559
|
+
logger.info("Calling Gemini completion API", { url: apiUrl, model: options.model });
|
|
1560
|
+
const response = await fetch(apiUrl, {
|
|
1561
|
+
method: "POST",
|
|
1562
|
+
headers,
|
|
1563
|
+
body: JSON.stringify(request)
|
|
1564
|
+
});
|
|
1565
|
+
if (!response.ok) {
|
|
1566
|
+
const errorText = await response.text();
|
|
1567
|
+
return {
|
|
1568
|
+
success: false,
|
|
1569
|
+
error: `API error (${response.status}): ${errorText}`
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const data = await response.json();
|
|
1573
|
+
const candidates = data.candidates;
|
|
1574
|
+
if (!candidates || candidates.length === 0) {
|
|
1575
|
+
return {
|
|
1576
|
+
success: false,
|
|
1577
|
+
error: "No candidates in response"
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
const candidate = candidates[0];
|
|
1581
|
+
const content = candidate.content;
|
|
1582
|
+
let textContent = "";
|
|
1583
|
+
if (content?.parts) {
|
|
1584
|
+
for (const part of content.parts) {
|
|
1585
|
+
if (part.text) {
|
|
1586
|
+
textContent += part.text;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
const usage = data.usageMetadata;
|
|
1591
|
+
return {
|
|
1592
|
+
success: true,
|
|
1593
|
+
message: {
|
|
1594
|
+
id: `msg_${Date.now()}`,
|
|
1595
|
+
role: "assistant",
|
|
1596
|
+
content: textContent,
|
|
1597
|
+
timestamp: Date.now()
|
|
1598
|
+
},
|
|
1599
|
+
usage: usage ? {
|
|
1600
|
+
promptTokens: usage.promptTokenCount || 0,
|
|
1601
|
+
completionTokens: usage.candidatesTokenCount || 0,
|
|
1602
|
+
totalTokens: usage.totalTokenCount || 0
|
|
1603
|
+
} : void 0
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
async function callOpenAIResponseCompletion(provider, apiKey, options, logger) {
|
|
1607
|
+
const input = [];
|
|
1608
|
+
for (const msg of options.messages) {
|
|
1609
|
+
if (msg.role === "system") {
|
|
1610
|
+
input.push({ role: "developer", content: msg.content });
|
|
1611
|
+
} else {
|
|
1612
|
+
input.push({ role: msg.role, content: msg.content });
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
const request = {
|
|
1616
|
+
model: options.model,
|
|
1617
|
+
input,
|
|
1618
|
+
stream: false,
|
|
1619
|
+
...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
|
|
1620
|
+
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
1621
|
+
};
|
|
1622
|
+
if (options.thinkLevel && options.thinkLevel !== "none") {
|
|
1623
|
+
const effort = getOpenAIReasoningEffort2(options.thinkLevel);
|
|
1624
|
+
if (effort) {
|
|
1625
|
+
request.reasoning = { effort, summary: "auto" };
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: false });
|
|
1629
|
+
const headers = getProviderHeaders(provider, apiKey);
|
|
1630
|
+
logger.info("Calling OpenAI Response API", { url: apiUrl, model: options.model });
|
|
1631
|
+
const response = await fetch(apiUrl, {
|
|
1632
|
+
method: "POST",
|
|
1633
|
+
headers,
|
|
1634
|
+
body: JSON.stringify(request)
|
|
1635
|
+
});
|
|
1636
|
+
if (!response.ok) {
|
|
1637
|
+
const errorText = await response.text();
|
|
1638
|
+
return {
|
|
1639
|
+
success: false,
|
|
1640
|
+
error: `API error (${response.status}): ${errorText}`
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
const data = await response.json();
|
|
1644
|
+
let textContent = "";
|
|
1645
|
+
const output = data.output;
|
|
1646
|
+
if (output) {
|
|
1647
|
+
for (const item of output) {
|
|
1648
|
+
if (item.type === "message") {
|
|
1649
|
+
const content = item.content;
|
|
1650
|
+
if (content) {
|
|
1651
|
+
for (const part of content) {
|
|
1652
|
+
if (part.type === "output_text" && typeof part.text === "string") {
|
|
1653
|
+
textContent += part.text;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const usage = data.usage;
|
|
1661
|
+
return {
|
|
1662
|
+
success: true,
|
|
1663
|
+
message: {
|
|
1664
|
+
id: `msg_${Date.now()}`,
|
|
1665
|
+
role: "assistant",
|
|
1666
|
+
content: textContent,
|
|
1667
|
+
timestamp: Date.now()
|
|
1668
|
+
},
|
|
1669
|
+
usage: usage ? {
|
|
1670
|
+
promptTokens: usage.input_tokens || 0,
|
|
1671
|
+
completionTokens: usage.output_tokens || 0,
|
|
1672
|
+
totalTokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
|
|
1673
|
+
} : void 0
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// src/completion/StreamHandler.ts
|
|
1678
|
+
import { buildAnthropicThinking as buildAnthropicThinking2, getOpenAIReasoningEffort as getOpenAIReasoningEffort3 } from "@omnicross/contracts/thinking-config";
|
|
1679
|
+
async function streamOpenAICompletion(provider, apiKey, options, messageId, callbacks, logger) {
|
|
1680
|
+
const request = {
|
|
1681
|
+
model: options.model,
|
|
1682
|
+
messages: options.messages.map((m) => convertMessageToOpenAI(m)),
|
|
1683
|
+
// Only set max_tokens if explicitly provided, otherwise let API use its default
|
|
1684
|
+
...options.maxTokens ? { max_tokens: options.maxTokens } : {},
|
|
1685
|
+
temperature: options.temperature,
|
|
1686
|
+
stream: true
|
|
1687
|
+
};
|
|
1688
|
+
if (options.thinkLevel && options.thinkLevel !== "none") {
|
|
1689
|
+
const effort = getOpenAIReasoningEffort3(options.thinkLevel);
|
|
1690
|
+
if (effort) {
|
|
1691
|
+
request.reasoning_effort = effort;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: true });
|
|
1695
|
+
const headers = getProviderHeaders(provider, apiKey);
|
|
1696
|
+
if (options.nativeSearchAugmentation) {
|
|
1697
|
+
applyAugmentation(request, options.nativeSearchAugmentation);
|
|
1698
|
+
}
|
|
1699
|
+
logger.info("Streaming OpenAI completion request", {
|
|
1700
|
+
url: apiUrl,
|
|
1701
|
+
providerId: options.providerId,
|
|
1702
|
+
model: options.model,
|
|
1703
|
+
maxTokens: options.maxTokens,
|
|
1704
|
+
max_tokens_in_request: request.max_tokens,
|
|
1705
|
+
temperature: options.temperature,
|
|
1706
|
+
messagesCount: options.messages.length,
|
|
1707
|
+
hasNativeSearchAugmentation: !!options.nativeSearchAugmentation
|
|
1708
|
+
});
|
|
1709
|
+
const response = await fetch(apiUrl, {
|
|
1710
|
+
method: "POST",
|
|
1711
|
+
headers,
|
|
1712
|
+
body: JSON.stringify(request)
|
|
1713
|
+
});
|
|
1714
|
+
if (!response.ok) {
|
|
1715
|
+
const errorText = await response.text();
|
|
1716
|
+
callbacks.onError?.(`API error (${response.status}): ${errorText}`);
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
const result = await streamSSEResponse(response, "openai", {
|
|
1720
|
+
onDelta: callbacks.onDelta,
|
|
1721
|
+
onReasoning: callbacks.onReasoning,
|
|
1722
|
+
onAudio: callbacks.onAudio,
|
|
1723
|
+
onVideo: callbacks.onVideo,
|
|
1724
|
+
onError: callbacks.onError
|
|
1725
|
+
});
|
|
1726
|
+
callbacks.onDone?.(
|
|
1727
|
+
{
|
|
1728
|
+
id: messageId,
|
|
1729
|
+
role: "assistant",
|
|
1730
|
+
content: result.content,
|
|
1731
|
+
timestamp: Date.now(),
|
|
1732
|
+
thinking: result.reasoning ? { content: result.reasoning } : void 0,
|
|
1733
|
+
audios: result.audios.length > 0 ? result.audios : void 0,
|
|
1734
|
+
videos: result.videos.length > 0 ? result.videos : void 0
|
|
1735
|
+
},
|
|
1736
|
+
result.usage,
|
|
1737
|
+
result.metrics
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
async function streamAnthropicCompletion(provider, apiKey, options, messageId, callbacks, logger) {
|
|
1741
|
+
const hasImages = options.messages.some((m) => m.images && m.images.length > 0);
|
|
1742
|
+
const MAX_TOKENS_FOR_THINKING = 16384;
|
|
1743
|
+
let effectiveMaxTokens = options.maxTokens || 16384;
|
|
1744
|
+
const thinkingMaxTokens = options.thinkLevel && options.thinkLevel !== "none" ? Math.min(effectiveMaxTokens, MAX_TOKENS_FOR_THINKING) : effectiveMaxTokens;
|
|
1745
|
+
const thinkingConfig = options.thinkLevel && options.thinkLevel !== "none" ? buildAnthropicThinking2(options.model, options.thinkLevel, thinkingMaxTokens) : void 0;
|
|
1746
|
+
if (thinkingConfig) {
|
|
1747
|
+
effectiveMaxTokens = thinkingMaxTokens;
|
|
1748
|
+
}
|
|
1749
|
+
logger.debug("Anthropic thinking configuration", {
|
|
1750
|
+
thinkLevel: options.thinkLevel,
|
|
1751
|
+
thinkingConfig,
|
|
1752
|
+
effectiveMaxTokens
|
|
1753
|
+
});
|
|
1754
|
+
let anthropicRequest;
|
|
1755
|
+
if (hasImages) {
|
|
1756
|
+
const systemMessages = options.messages.filter((m) => m.role === "system");
|
|
1757
|
+
const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
|
|
1758
|
+
anthropicRequest = {
|
|
1759
|
+
model: options.model,
|
|
1760
|
+
max_tokens: effectiveMaxTokens,
|
|
1761
|
+
// Omit temperature when thinking is enabled (Anthropic will use default temperature=1)
|
|
1762
|
+
...thinkingConfig ? {} : { temperature: options.temperature },
|
|
1763
|
+
...systemMessages.length > 0 ? { system: systemMessages.map((m) => m.content).join("\n\n") } : {},
|
|
1764
|
+
messages: nonSystemMessages.map((m) => convertMessageToAnthropic(m)),
|
|
1765
|
+
stream: true,
|
|
1766
|
+
...thinkingConfig ? { thinking: thinkingConfig } : {}
|
|
1767
|
+
};
|
|
1768
|
+
} else {
|
|
1769
|
+
const config = {
|
|
1770
|
+
defaultModel: options.model
|
|
1771
|
+
};
|
|
1772
|
+
const openaiRequest = {
|
|
1773
|
+
model: options.model,
|
|
1774
|
+
messages: options.messages.map((m) => ({
|
|
1775
|
+
role: m.role,
|
|
1776
|
+
content: m.content
|
|
1777
|
+
})),
|
|
1778
|
+
// Anthropic requires max_tokens; use adjusted value
|
|
1779
|
+
max_tokens: effectiveMaxTokens,
|
|
1780
|
+
// Omit temperature when thinking is enabled (Anthropic will use default temperature=1)
|
|
1781
|
+
temperature: thinkingConfig ? void 0 : options.temperature,
|
|
1782
|
+
stream: true
|
|
1783
|
+
};
|
|
1784
|
+
anthropicRequest = convertOpenAIToAnthropic(openaiRequest, config);
|
|
1785
|
+
if (thinkingConfig) {
|
|
1786
|
+
anthropicRequest.thinking = thinkingConfig;
|
|
1787
|
+
delete anthropicRequest.temperature;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: true });
|
|
1791
|
+
const headers = getProviderHeaders(provider, apiKey);
|
|
1792
|
+
if (options.nativeSearchAugmentation) {
|
|
1793
|
+
applyAugmentation(anthropicRequest, options.nativeSearchAugmentation);
|
|
1794
|
+
}
|
|
1795
|
+
logger.info("Streaming Anthropic completion request", {
|
|
1796
|
+
url: apiUrl,
|
|
1797
|
+
model: anthropicRequest.model,
|
|
1798
|
+
messagesCount: anthropicRequest.messages?.length,
|
|
1799
|
+
max_tokens: anthropicRequest.max_tokens,
|
|
1800
|
+
temperature: anthropicRequest.temperature,
|
|
1801
|
+
stream: anthropicRequest.stream,
|
|
1802
|
+
hasImages,
|
|
1803
|
+
hasThinking: !!anthropicRequest.thinking,
|
|
1804
|
+
hasNativeSearchAugmentation: !!options.nativeSearchAugmentation
|
|
1805
|
+
});
|
|
1806
|
+
const response = await fetch(apiUrl, {
|
|
1807
|
+
method: "POST",
|
|
1808
|
+
headers,
|
|
1809
|
+
body: JSON.stringify(anthropicRequest)
|
|
1810
|
+
});
|
|
1811
|
+
logger.debug("Anthropic response status", { status: response.status });
|
|
1812
|
+
if (!response.ok) {
|
|
1813
|
+
const errorText = await response.text();
|
|
1814
|
+
logger.error("Anthropic API error", void 0, { status: response.status, errorText });
|
|
1815
|
+
callbacks.onError?.(`API error (${response.status}): ${errorText}`);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const result = await streamSSEResponse(response, "anthropic", {
|
|
1819
|
+
onDelta: callbacks.onDelta,
|
|
1820
|
+
onReasoning: callbacks.onReasoning,
|
|
1821
|
+
onError: callbacks.onError,
|
|
1822
|
+
onBlock: callbacks.onBlock
|
|
1823
|
+
});
|
|
1824
|
+
logger.info("Anthropic stream complete", {
|
|
1825
|
+
contentLength: result.content.length,
|
|
1826
|
+
reasoningLength: result.reasoning?.length || 0,
|
|
1827
|
+
blocksCount: result.blocks.length
|
|
1828
|
+
});
|
|
1829
|
+
callbacks.onDone?.(
|
|
1830
|
+
{
|
|
1831
|
+
id: messageId,
|
|
1832
|
+
role: "assistant",
|
|
1833
|
+
content: result.content,
|
|
1834
|
+
timestamp: Date.now(),
|
|
1835
|
+
thinking: result.reasoning ? { content: result.reasoning } : void 0,
|
|
1836
|
+
blocks: result.blocks.length > 0 ? result.blocks : void 0
|
|
1837
|
+
},
|
|
1838
|
+
result.usage,
|
|
1839
|
+
result.metrics
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
async function streamGeminiCompletion(provider, apiKey, options, messageId, callbacks, logger) {
|
|
1843
|
+
const contents = [];
|
|
1844
|
+
let systemInstruction;
|
|
1845
|
+
for (const msg of options.messages) {
|
|
1846
|
+
if (msg.role === "system") {
|
|
1847
|
+
systemInstruction = { parts: [{ text: msg.content }] };
|
|
1848
|
+
} else {
|
|
1849
|
+
contents.push(convertMessageToGemini(msg));
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
const request = {
|
|
1853
|
+
contents,
|
|
1854
|
+
generationConfig: {
|
|
1855
|
+
...options.maxTokens ? { maxOutputTokens: options.maxTokens } : {},
|
|
1856
|
+
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
if (systemInstruction) {
|
|
1860
|
+
request.systemInstruction = systemInstruction;
|
|
1861
|
+
}
|
|
1862
|
+
const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: true });
|
|
1863
|
+
const headers = getProviderHeaders(provider, apiKey);
|
|
1864
|
+
if (options.nativeSearchAugmentation) {
|
|
1865
|
+
applyAugmentation(request, options.nativeSearchAugmentation);
|
|
1866
|
+
}
|
|
1867
|
+
logger.info("Streaming Gemini completion request", {
|
|
1868
|
+
url: apiUrl,
|
|
1869
|
+
model: options.model,
|
|
1870
|
+
contentsCount: contents.length,
|
|
1871
|
+
hasSystemInstruction: !!systemInstruction,
|
|
1872
|
+
maxOutputTokens: options.maxTokens,
|
|
1873
|
+
temperature: options.temperature,
|
|
1874
|
+
hasNativeSearchAugmentation: !!options.nativeSearchAugmentation
|
|
1875
|
+
});
|
|
1876
|
+
const response = await fetch(apiUrl, {
|
|
1877
|
+
method: "POST",
|
|
1878
|
+
headers,
|
|
1879
|
+
body: JSON.stringify(request)
|
|
1880
|
+
});
|
|
1881
|
+
logger.debug("Gemini response status", { status: response.status });
|
|
1882
|
+
if (!response.ok) {
|
|
1883
|
+
const errorText = await response.text();
|
|
1884
|
+
logger.error("Gemini API error", void 0, { status: response.status, errorText });
|
|
1885
|
+
callbacks.onError?.(`API error (${response.status}): ${errorText}`);
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
const result = await streamSSEResponse(response, "gemini", {
|
|
1889
|
+
onDelta: callbacks.onDelta,
|
|
1890
|
+
onReasoning: callbacks.onReasoning,
|
|
1891
|
+
onError: callbacks.onError
|
|
1892
|
+
});
|
|
1893
|
+
logger.info("Gemini stream complete", { contentLength: result.content.length });
|
|
1894
|
+
callbacks.onDone?.(
|
|
1895
|
+
{
|
|
1896
|
+
id: messageId,
|
|
1897
|
+
role: "assistant",
|
|
1898
|
+
content: result.content,
|
|
1899
|
+
timestamp: Date.now(),
|
|
1900
|
+
thinking: result.reasoning ? { content: result.reasoning } : void 0
|
|
1901
|
+
},
|
|
1902
|
+
result.usage,
|
|
1903
|
+
result.metrics
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
async function streamOpenAIResponseCompletion(provider, apiKey, options, messageId, callbacks, logger) {
|
|
1907
|
+
const input = [];
|
|
1908
|
+
for (const msg of options.messages) {
|
|
1909
|
+
if (msg.role === "system") {
|
|
1910
|
+
input.push({ role: "developer", content: msg.content });
|
|
1911
|
+
} else {
|
|
1912
|
+
input.push({ role: msg.role, content: msg.content });
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
const request = {
|
|
1916
|
+
model: options.model,
|
|
1917
|
+
input,
|
|
1918
|
+
stream: true,
|
|
1919
|
+
...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
|
|
1920
|
+
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
1921
|
+
};
|
|
1922
|
+
if (options.thinkLevel && options.thinkLevel !== "none") {
|
|
1923
|
+
const effort = getOpenAIReasoningEffort3(options.thinkLevel);
|
|
1924
|
+
if (effort) {
|
|
1925
|
+
request.reasoning = { effort, summary: "auto" };
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: true });
|
|
1929
|
+
const headers = getProviderHeaders(provider, apiKey);
|
|
1930
|
+
logger.info("Streaming OpenAI Response API request", {
|
|
1931
|
+
url: apiUrl,
|
|
1932
|
+
providerId: options.providerId,
|
|
1933
|
+
model: options.model,
|
|
1934
|
+
maxOutputTokens: options.maxTokens,
|
|
1935
|
+
temperature: options.temperature,
|
|
1936
|
+
inputCount: input.length,
|
|
1937
|
+
reasoning: request.reasoning
|
|
1938
|
+
});
|
|
1939
|
+
const response = await fetch(apiUrl, {
|
|
1940
|
+
method: "POST",
|
|
1941
|
+
headers,
|
|
1942
|
+
body: JSON.stringify(request)
|
|
1943
|
+
});
|
|
1944
|
+
if (!response.ok) {
|
|
1945
|
+
const errorText = await response.text();
|
|
1946
|
+
callbacks.onError?.(`API error (${response.status}): ${errorText}`);
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
const result = await streamSSEResponse(response, "openai-response", {
|
|
1950
|
+
onDelta: callbacks.onDelta,
|
|
1951
|
+
onReasoning: callbacks.onReasoning,
|
|
1952
|
+
onError: callbacks.onError
|
|
1953
|
+
});
|
|
1954
|
+
logger.info("OpenAI Response API stream complete", { contentLength: result.content.length });
|
|
1955
|
+
callbacks.onDone?.(
|
|
1956
|
+
{
|
|
1957
|
+
id: messageId,
|
|
1958
|
+
role: "assistant",
|
|
1959
|
+
content: result.content,
|
|
1960
|
+
timestamp: Date.now(),
|
|
1961
|
+
thinking: result.reasoning ? { content: result.reasoning } : void 0
|
|
1962
|
+
},
|
|
1963
|
+
result.usage,
|
|
1964
|
+
result.metrics
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// src/completion/ThinkingResolver.ts
|
|
1969
|
+
import {
|
|
1970
|
+
buildAnthropicThinking as buildAnthropicThinking3,
|
|
1971
|
+
calculateThinkingBudget as calculateThinkingBudget2,
|
|
1972
|
+
DEFAULT_MAX_TOKENS,
|
|
1973
|
+
getClaudeMaxTokens,
|
|
1974
|
+
isReasoningModel
|
|
1975
|
+
} from "@omnicross/contracts/thinking-config";
|
|
1976
|
+
async function resolveEffectiveMaxTokens(llmConfig, getProvider, logger, providerId, modelId, sessionMaxTokens) {
|
|
1977
|
+
if (sessionMaxTokens !== void 0 && sessionMaxTokens > 0) {
|
|
1978
|
+
logger.debug("Using session maxTokens", { sessionMaxTokens });
|
|
1979
|
+
return sessionMaxTokens;
|
|
1980
|
+
}
|
|
1981
|
+
try {
|
|
1982
|
+
const globalParams = await llmConfig.getGlobalModelParameters();
|
|
1983
|
+
if (globalParams?.maxTokens?.enabled && globalParams.maxTokens.value > 0) {
|
|
1984
|
+
logger.debug("Using global maxTokens", { maxTokens: globalParams.maxTokens.value });
|
|
1985
|
+
return globalParams.maxTokens.value;
|
|
1986
|
+
}
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
logger.warn("Failed to get global params", err instanceof Error ? err : void 0);
|
|
1989
|
+
}
|
|
1990
|
+
const MAX_TOKENS_CAP = 131072;
|
|
1991
|
+
const provider = await getProvider(providerId);
|
|
1992
|
+
if (provider) {
|
|
1993
|
+
const modelConfig = provider.modelConfigs?.find((m) => m.id === modelId);
|
|
1994
|
+
if (modelConfig?.maxTokens && modelConfig.maxTokens > 0) {
|
|
1995
|
+
const cappedMaxTokens = Math.min(modelConfig.maxTokens, MAX_TOKENS_CAP);
|
|
1996
|
+
logger.debug("Using model config maxTokens", {
|
|
1997
|
+
maxTokens: modelConfig.maxTokens,
|
|
1998
|
+
cappedMaxTokens
|
|
1999
|
+
});
|
|
2000
|
+
return cappedMaxTokens;
|
|
2001
|
+
}
|
|
2002
|
+
if (provider.modelGroups) {
|
|
2003
|
+
for (const group of provider.modelGroups) {
|
|
2004
|
+
const model = group.models?.find((m) => m.id === modelId);
|
|
2005
|
+
if (model?.maxTokens && model.maxTokens > 0) {
|
|
2006
|
+
const cappedMaxTokens = Math.min(model.maxTokens, MAX_TOKENS_CAP);
|
|
2007
|
+
logger.debug("Using modelGroup model maxTokens", {
|
|
2008
|
+
maxTokens: model.maxTokens,
|
|
2009
|
+
cappedMaxTokens
|
|
2010
|
+
});
|
|
2011
|
+
return cappedMaxTokens;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
try {
|
|
2017
|
+
const discoveredMaxTokens = await llmConfig.getDiscoveredModelMaxTokens(providerId, modelId);
|
|
2018
|
+
if (discoveredMaxTokens && discoveredMaxTokens > 0) {
|
|
2019
|
+
const cappedMaxTokens = Math.min(discoveredMaxTokens, MAX_TOKENS_CAP);
|
|
2020
|
+
logger.debug("Using discovered model maxTokens", {
|
|
2021
|
+
discoveredMaxTokens,
|
|
2022
|
+
cappedMaxTokens
|
|
2023
|
+
});
|
|
2024
|
+
return cappedMaxTokens;
|
|
2025
|
+
}
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
logger.warn("Failed to get discovered model maxTokens", err instanceof Error ? err : void 0);
|
|
2028
|
+
}
|
|
2029
|
+
logger.debug("No maxTokens configured, returning undefined");
|
|
2030
|
+
return void 0;
|
|
2031
|
+
}
|
|
2032
|
+
async function getRequiredMaxTokens(llmConfig, getProvider, logger, providerId, modelId, sessionMaxTokens) {
|
|
2033
|
+
const resolved = await resolveEffectiveMaxTokens(llmConfig, getProvider, logger, providerId, modelId, sessionMaxTokens);
|
|
2034
|
+
if (resolved !== void 0) {
|
|
2035
|
+
return resolved;
|
|
2036
|
+
}
|
|
2037
|
+
logger.debug("Using default maxTokens", { defaultMaxTokens: DEFAULT_MAX_TOKENS });
|
|
2038
|
+
return DEFAULT_MAX_TOKENS;
|
|
2039
|
+
}
|
|
2040
|
+
async function resolveThinkingBudget(getProvider, logger, providerId, modelId, maxTokens, thinkLevel) {
|
|
2041
|
+
if (thinkLevel === "none" || !isReasoningModel(modelId)) {
|
|
2042
|
+
return {
|
|
2043
|
+
adjustedMaxTokens: maxTokens,
|
|
2044
|
+
thinkingBudget: void 0,
|
|
2045
|
+
thinkingConfig: void 0
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
const thinkingBudget = calculateThinkingBudget2(modelId, thinkLevel, maxTokens);
|
|
2049
|
+
const provider = await getProvider(providerId);
|
|
2050
|
+
const providerName = provider?.name?.toLowerCase() || "";
|
|
2051
|
+
const apiFormat = provider ? resolveApiFormat(provider) : "openai";
|
|
2052
|
+
if (apiFormat === "anthropic" || providerName === "anthropic" || providerName.includes("claude")) {
|
|
2053
|
+
const thinkingConfig = buildAnthropicThinking3(modelId, thinkLevel, maxTokens);
|
|
2054
|
+
const adjustedMaxTokens = getClaudeMaxTokens(maxTokens, thinkingBudget) || maxTokens;
|
|
2055
|
+
logger.debug("Claude model thinking budget", {
|
|
2056
|
+
thinkingBudget,
|
|
2057
|
+
adjustedMaxTokens
|
|
2058
|
+
});
|
|
2059
|
+
return {
|
|
2060
|
+
adjustedMaxTokens,
|
|
2061
|
+
thinkingBudget,
|
|
2062
|
+
thinkingConfig
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
logger.debug("Non-Claude model thinking budget", { thinkingBudget });
|
|
2066
|
+
return {
|
|
2067
|
+
adjustedMaxTokens: maxTokens,
|
|
2068
|
+
thinkingBudget,
|
|
2069
|
+
thinkingConfig: thinkingBudget ? { type: "enabled", budget_tokens: thinkingBudget } : void 0
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// src/completion/ToolExecutor.ts
|
|
2074
|
+
function logToolFormat(tools, logger) {
|
|
2075
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
2076
|
+
logger.warn("No tools provided");
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
const firstTool = tools[0];
|
|
2080
|
+
if ("function" in firstTool) {
|
|
2081
|
+
const openaiTools = tools;
|
|
2082
|
+
logger.info("Tools configured (OpenAI format)", {
|
|
2083
|
+
count: openaiTools.length,
|
|
2084
|
+
tools: openaiTools.map((t) => t.function.name),
|
|
2085
|
+
firstTool: {
|
|
2086
|
+
name: firstTool.function.name,
|
|
2087
|
+
description: firstTool.function.description?.slice(0, 100),
|
|
2088
|
+
parameters: firstTool.function.parameters
|
|
2089
|
+
}
|
|
2090
|
+
});
|
|
2091
|
+
} else if ("input_schema" in firstTool) {
|
|
2092
|
+
const anthropicTools = tools;
|
|
2093
|
+
logger.info("Tools configured (Anthropic format)", {
|
|
2094
|
+
count: anthropicTools.length,
|
|
2095
|
+
tools: anthropicTools.map((t) => t.name),
|
|
2096
|
+
firstTool: {
|
|
2097
|
+
name: firstTool.name,
|
|
2098
|
+
description: firstTool.description?.slice(0, 100),
|
|
2099
|
+
input_schema: firstTool.input_schema
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
} else if ("functionDeclarations" in firstTool) {
|
|
2103
|
+
const geminiTools = tools;
|
|
2104
|
+
const allDeclarations = geminiTools.flatMap((t) => t.functionDeclarations);
|
|
2105
|
+
logger.info("Tools configured (Gemini format)", {
|
|
2106
|
+
count: allDeclarations.length,
|
|
2107
|
+
tools: allDeclarations.map((t) => t.name),
|
|
2108
|
+
firstTool: {
|
|
2109
|
+
name: allDeclarations[0]?.name,
|
|
2110
|
+
description: allDeclarations[0]?.description?.slice(0, 100),
|
|
2111
|
+
parameters: allDeclarations[0]?.parameters
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
function buildToolRequest(apiFormat, conversationMessages, actualModel, options, provider) {
|
|
2117
|
+
let requestBody;
|
|
2118
|
+
let url;
|
|
2119
|
+
if (apiFormat === "google") {
|
|
2120
|
+
const contents = [];
|
|
2121
|
+
for (const msg of conversationMessages) {
|
|
2122
|
+
if (msg.role !== "system") {
|
|
2123
|
+
contents.push({
|
|
2124
|
+
role: msg.role === "assistant" ? "model" : "user",
|
|
2125
|
+
parts: [{ text: msg.content }]
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
requestBody = {
|
|
2130
|
+
contents,
|
|
2131
|
+
tools: options.tools
|
|
2132
|
+
};
|
|
2133
|
+
url = buildProviderApiUrl(provider, { model: actualModel, stream: true });
|
|
2134
|
+
} else if (apiFormat === "anthropic") {
|
|
2135
|
+
requestBody = {
|
|
2136
|
+
model: actualModel,
|
|
2137
|
+
messages: conversationMessages.filter((m) => m.role !== "system").map((m) => ({
|
|
2138
|
+
role: m.role,
|
|
2139
|
+
content: m.content
|
|
2140
|
+
})),
|
|
2141
|
+
max_tokens: options.maxTokens || 4096,
|
|
2142
|
+
temperature: options.temperature ?? 0.7,
|
|
2143
|
+
stream: true,
|
|
2144
|
+
tools: options.tools
|
|
2145
|
+
};
|
|
2146
|
+
const systemMsg = conversationMessages.find((m) => m.role === "system");
|
|
2147
|
+
if (systemMsg) {
|
|
2148
|
+
requestBody.system = systemMsg.content;
|
|
2149
|
+
}
|
|
2150
|
+
url = buildProviderApiUrl(provider, { model: actualModel, stream: true });
|
|
2151
|
+
} else {
|
|
2152
|
+
requestBody = {
|
|
2153
|
+
model: actualModel,
|
|
2154
|
+
messages: conversationMessages.map((m) => ({
|
|
2155
|
+
role: m.role,
|
|
2156
|
+
content: m.content
|
|
2157
|
+
})),
|
|
2158
|
+
max_tokens: options.maxTokens || 4096,
|
|
2159
|
+
temperature: options.temperature ?? 0.7,
|
|
2160
|
+
stream: true,
|
|
2161
|
+
tools: options.tools
|
|
2162
|
+
};
|
|
2163
|
+
url = buildProviderApiUrl(provider, { model: actualModel, stream: true });
|
|
2164
|
+
}
|
|
2165
|
+
return { requestBody, url };
|
|
2166
|
+
}
|
|
2167
|
+
function extractDeltaContent(rawJson, apiFormat) {
|
|
2168
|
+
const json = rawJson;
|
|
2169
|
+
if (apiFormat === "google") {
|
|
2170
|
+
const parts = json.candidates?.[0]?.content?.parts || [];
|
|
2171
|
+
let text = "";
|
|
2172
|
+
for (const part of parts) {
|
|
2173
|
+
if (part.thought === true) continue;
|
|
2174
|
+
if (part.text) text += part.text;
|
|
2175
|
+
}
|
|
2176
|
+
return text;
|
|
2177
|
+
} else if (apiFormat === "anthropic") {
|
|
2178
|
+
return json.delta?.text || "";
|
|
2179
|
+
} else {
|
|
2180
|
+
return json.choices?.[0]?.delta?.content || "";
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
function extractDeltaReasoning(rawJson, apiFormat) {
|
|
2184
|
+
const json = rawJson;
|
|
2185
|
+
if (apiFormat === "google") {
|
|
2186
|
+
const parts = json.candidates?.[0]?.content?.parts || [];
|
|
2187
|
+
let reasoning = "";
|
|
2188
|
+
for (const part of parts) {
|
|
2189
|
+
if (part.thought === true && part.text) reasoning += part.text;
|
|
2190
|
+
}
|
|
2191
|
+
return reasoning;
|
|
2192
|
+
} else if (apiFormat === "anthropic") {
|
|
2193
|
+
return "";
|
|
2194
|
+
} else {
|
|
2195
|
+
const delta = json.choices?.[0]?.delta;
|
|
2196
|
+
return (delta?.thinking?.content || "") + (delta?.reasoning_content || "");
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
function extractToolCalls(rawJson, apiFormat, toolCalls, callbacks, logger, pendingOpenAIToolCalls) {
|
|
2200
|
+
const json = rawJson;
|
|
2201
|
+
if (apiFormat === "google") {
|
|
2202
|
+
const parts = json.candidates?.[0]?.content?.parts || [];
|
|
2203
|
+
for (const part of parts) {
|
|
2204
|
+
if (part.functionCall) {
|
|
2205
|
+
logger.info("Function call detected", { functionCall: part.functionCall });
|
|
2206
|
+
const toolCall = {
|
|
2207
|
+
id: `call_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
2208
|
+
name: part.functionCall.name,
|
|
2209
|
+
args: part.functionCall.args || {}
|
|
2210
|
+
};
|
|
2211
|
+
toolCalls.push(toolCall);
|
|
2212
|
+
callbacks.onToolCall?.(toolCall);
|
|
2213
|
+
logger.info("Tool called", { toolName: toolCall.name });
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
} else if (apiFormat !== "anthropic") {
|
|
2217
|
+
const delta = json.choices?.[0]?.delta;
|
|
2218
|
+
if (delta?.tool_calls && pendingOpenAIToolCalls) {
|
|
2219
|
+
for (const tc of delta.tool_calls) {
|
|
2220
|
+
const index = tc.index ?? 0;
|
|
2221
|
+
let pending = pendingOpenAIToolCalls.get(index);
|
|
2222
|
+
if (!pending) {
|
|
2223
|
+
pending = { id: "", name: "", arguments: "" };
|
|
2224
|
+
pendingOpenAIToolCalls.set(index, pending);
|
|
2225
|
+
}
|
|
2226
|
+
if (tc.id) pending.id = tc.id;
|
|
2227
|
+
if (tc.function?.name) pending.name += tc.function.name;
|
|
2228
|
+
if (tc.function?.arguments) pending.arguments += tc.function.arguments;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
function finalizeOpenAIToolCalls(pendingOpenAIToolCalls, toolCalls, callbacks, logger) {
|
|
2234
|
+
for (const [, pending] of pendingOpenAIToolCalls) {
|
|
2235
|
+
if (pending.name) {
|
|
2236
|
+
try {
|
|
2237
|
+
const toolCall = {
|
|
2238
|
+
id: pending.id || `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
2239
|
+
name: pending.name,
|
|
2240
|
+
args: JSON.parse(pending.arguments || "{}")
|
|
2241
|
+
};
|
|
2242
|
+
toolCalls.push(toolCall);
|
|
2243
|
+
callbacks.onToolCall?.(toolCall);
|
|
2244
|
+
logger.info("Tool called (OpenAI)", { toolName: toolCall.name, args: toolCall.args });
|
|
2245
|
+
} catch (e) {
|
|
2246
|
+
logger.error("Failed to parse OpenAI tool call arguments", e instanceof Error ? e : void 0, {
|
|
2247
|
+
name: pending.name,
|
|
2248
|
+
arguments: pending.arguments
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
function parseStreamChunk(rawJson, apiFormat, callbacks, logger) {
|
|
2255
|
+
const json = rawJson;
|
|
2256
|
+
if (apiFormat === "google") {
|
|
2257
|
+
const candidate = json.candidates?.[0];
|
|
2258
|
+
if (!candidate) {
|
|
2259
|
+
logger.warn("No candidate found in Gemini response");
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
const parts = candidate.content?.parts || [];
|
|
2263
|
+
for (const part of parts) {
|
|
2264
|
+
if (part.thought === true && part.text) {
|
|
2265
|
+
callbacks.onReasoning?.(part.text);
|
|
2266
|
+
continue;
|
|
2267
|
+
} else if (part.thoughtSignature && !part.thought) {
|
|
2268
|
+
logger.debug("Gemini returned thoughtSignature without thought content");
|
|
2269
|
+
}
|
|
2270
|
+
if (part.text) {
|
|
2271
|
+
callbacks.onDelta?.(part.text);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
} else if (apiFormat === "anthropic") {
|
|
2275
|
+
const delta = json.delta;
|
|
2276
|
+
if (delta?.text) {
|
|
2277
|
+
callbacks.onDelta?.(delta.text);
|
|
2278
|
+
}
|
|
2279
|
+
} else {
|
|
2280
|
+
const delta = json.choices?.[0]?.delta;
|
|
2281
|
+
if (delta?.content) {
|
|
2282
|
+
callbacks.onDelta?.(delta.content);
|
|
2283
|
+
}
|
|
2284
|
+
if (delta?.thinking?.content) {
|
|
2285
|
+
callbacks.onReasoning?.(delta.thinking.content);
|
|
2286
|
+
}
|
|
2287
|
+
if (delta?.reasoning_content) {
|
|
2288
|
+
callbacks.onReasoning?.(delta.reasoning_content);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
function buildIterationBlocks(iteration, content, reasoning, blocks, callbacks) {
|
|
2293
|
+
const blockIdPrefix = `block_${iteration}_${Date.now()}`;
|
|
2294
|
+
if (reasoning && reasoning.trim()) {
|
|
2295
|
+
const thinkingBlock = {
|
|
2296
|
+
id: `${blockIdPrefix}_thinking`,
|
|
2297
|
+
type: "thinking",
|
|
2298
|
+
content: reasoning
|
|
2299
|
+
};
|
|
2300
|
+
blocks.push(thinkingBlock);
|
|
2301
|
+
callbacks.onBlock?.(thinkingBlock);
|
|
2302
|
+
}
|
|
2303
|
+
if (content && content.trim()) {
|
|
2304
|
+
const textBlock = {
|
|
2305
|
+
id: `${blockIdPrefix}_text`,
|
|
2306
|
+
type: "text",
|
|
2307
|
+
content
|
|
2308
|
+
};
|
|
2309
|
+
blocks.push(textBlock);
|
|
2310
|
+
callbacks.onBlock?.(textBlock);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
async function executeToolCalls(toolCalls, mcpTools, mcpService, blocks, callbacks, logger, builtinExecutor) {
|
|
2314
|
+
const toolResults = [];
|
|
2315
|
+
for (const toolCall of toolCalls) {
|
|
2316
|
+
const toolUseBlock = {
|
|
2317
|
+
id: `${toolCall.id}_use`,
|
|
2318
|
+
type: "tool_use",
|
|
2319
|
+
toolId: toolCall.id,
|
|
2320
|
+
toolName: toolCall.name,
|
|
2321
|
+
input: toolCall.args,
|
|
2322
|
+
status: "running"
|
|
2323
|
+
};
|
|
2324
|
+
blocks.push(toolUseBlock);
|
|
2325
|
+
callbacks.onBlock?.(toolUseBlock);
|
|
2326
|
+
try {
|
|
2327
|
+
logger.info("Executing tool", { toolName: toolCall.name });
|
|
2328
|
+
const mcpTool = mcpTools.find((t) => t.id === toolCall.name);
|
|
2329
|
+
if (!mcpTool) {
|
|
2330
|
+
logger.error("Tool not found", void 0, { toolName: toolCall.name });
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
logger.debug("Tool info", {
|
|
2334
|
+
serverId: mcpTool.serverId,
|
|
2335
|
+
toolName: mcpTool.name,
|
|
2336
|
+
args: toolCall.args
|
|
2337
|
+
});
|
|
2338
|
+
const mappedArgs = mapToolArguments(toolCall.args, mcpTool, logger);
|
|
2339
|
+
let result;
|
|
2340
|
+
if (mcpTool.serverId === "builtin" && builtinExecutor) {
|
|
2341
|
+
result = await builtinExecutor.execute(mcpTool.name, mappedArgs);
|
|
2342
|
+
} else if (mcpService) {
|
|
2343
|
+
result = await mcpService.callTool(mcpTool.serverId, mcpTool.name, mappedArgs, toolCall.id);
|
|
2344
|
+
} else {
|
|
2345
|
+
result = { isError: true, content: [{ type: "text", text: "MCP service not available" }] };
|
|
2346
|
+
}
|
|
2347
|
+
logger.info("Tool result received", { result });
|
|
2348
|
+
callbacks.onToolResult?.(toolCall.id, result);
|
|
2349
|
+
const resultText = result.content?.[0]?.text || JSON.stringify(result);
|
|
2350
|
+
const toolResultBlock = {
|
|
2351
|
+
id: `${toolCall.id}_result`,
|
|
2352
|
+
type: "tool_result",
|
|
2353
|
+
toolId: toolCall.id,
|
|
2354
|
+
toolName: toolCall.name,
|
|
2355
|
+
output: resultText,
|
|
2356
|
+
isError: result.isError
|
|
2357
|
+
};
|
|
2358
|
+
blocks.push(toolResultBlock);
|
|
2359
|
+
callbacks.onBlock?.(toolResultBlock);
|
|
2360
|
+
const toolUseBlockRef = blocks.find((b) => b.id === `${toolCall.id}_use`);
|
|
2361
|
+
if (toolUseBlockRef && toolUseBlockRef.type === "tool_use") {
|
|
2362
|
+
toolUseBlockRef.status = result.isError ? "error" : "completed";
|
|
2363
|
+
callbacks.onBlock?.({ ...toolUseBlockRef });
|
|
2364
|
+
}
|
|
2365
|
+
toolResults.push({ toolCall, result });
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
logger.error("Tool execution error", error instanceof Error ? error : void 0);
|
|
2368
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2369
|
+
const errorResult = {
|
|
2370
|
+
isError: true,
|
|
2371
|
+
content: [{ type: "text", text: `Error: ${errorMessage}` }]
|
|
2372
|
+
};
|
|
2373
|
+
callbacks.onToolResult?.(toolCall.id, errorResult);
|
|
2374
|
+
const errorToolResultBlock = {
|
|
2375
|
+
id: `${toolCall.id}_result`,
|
|
2376
|
+
type: "tool_result",
|
|
2377
|
+
toolId: toolCall.id,
|
|
2378
|
+
toolName: toolCall.name,
|
|
2379
|
+
error: errorMessage,
|
|
2380
|
+
isError: true
|
|
2381
|
+
};
|
|
2382
|
+
blocks.push(errorToolResultBlock);
|
|
2383
|
+
callbacks.onBlock?.(errorToolResultBlock);
|
|
2384
|
+
const errorToolUseBlockRef = blocks.find((b) => b.id === `${toolCall.id}_use`);
|
|
2385
|
+
if (errorToolUseBlockRef && errorToolUseBlockRef.type === "tool_use") {
|
|
2386
|
+
errorToolUseBlockRef.status = "error";
|
|
2387
|
+
callbacks.onBlock?.({ ...errorToolUseBlockRef });
|
|
2388
|
+
}
|
|
2389
|
+
toolResults.push({ toolCall, result: errorResult });
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
return toolResults;
|
|
2393
|
+
}
|
|
2394
|
+
function mapToolArguments(args, mcpTool, logger) {
|
|
2395
|
+
const mappedArgs = { ...args };
|
|
2396
|
+
const toolSchema = mcpTool.inputSchema;
|
|
2397
|
+
if (toolSchema?.properties) {
|
|
2398
|
+
const expectedParams = Object.keys(toolSchema.properties);
|
|
2399
|
+
const paramMappings = {
|
|
2400
|
+
"search_query": ["query", "q", "searchQuery", "search"],
|
|
2401
|
+
"content": ["text", "body", "message"],
|
|
2402
|
+
"file_path": ["path", "filePath", "file"],
|
|
2403
|
+
"url": ["link", "uri"]
|
|
2404
|
+
};
|
|
2405
|
+
for (const expectedParam of expectedParams) {
|
|
2406
|
+
if (!(expectedParam in mappedArgs)) {
|
|
2407
|
+
const aliases = paramMappings[expectedParam];
|
|
2408
|
+
if (aliases) {
|
|
2409
|
+
for (const alias of aliases) {
|
|
2410
|
+
if (alias in mappedArgs) {
|
|
2411
|
+
logger.info("Mapping parameter", { from: alias, to: expectedParam });
|
|
2412
|
+
mappedArgs[expectedParam] = mappedArgs[alias];
|
|
2413
|
+
delete mappedArgs[alias];
|
|
2414
|
+
break;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return mappedArgs;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
// src/completion/ToolHandler.ts
|
|
2425
|
+
async function streamWithTools(options, callbacks, mcpService, llmConfig, getProvider, resolveApiKey, logger, builtinExecutor) {
|
|
2426
|
+
try {
|
|
2427
|
+
const routedInfo = await llmConfig.resolveRoutedModel(
|
|
2428
|
+
options.providerId,
|
|
2429
|
+
options.model
|
|
2430
|
+
);
|
|
2431
|
+
const actualProviderId = routedInfo?.actualProviderId || options.providerId;
|
|
2432
|
+
const actualModel = routedInfo?.actualModelId || options.model;
|
|
2433
|
+
const provider = await getProvider(actualProviderId);
|
|
2434
|
+
if (!provider) {
|
|
2435
|
+
callbacks.onError?.(`Provider not found: ${actualProviderId}`);
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
if (!provider.enabled) {
|
|
2439
|
+
callbacks.onError?.(`Provider is disabled: ${provider.name}`);
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
const { apiKey: effectiveKey } = resolveProviderEndpoint(provider);
|
|
2443
|
+
const apiKey = resolveApiKey(effectiveKey);
|
|
2444
|
+
if (!apiKey) {
|
|
2445
|
+
callbacks.onError?.("API key not configured");
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
2449
|
+
callbacks.onStart?.(messageId);
|
|
2450
|
+
const globalParams = await llmConfig.getGlobalModelParameters();
|
|
2451
|
+
const MAX_ITERATIONS = globalParams.toolMaxTurns ?? 100;
|
|
2452
|
+
let iteration = 0;
|
|
2453
|
+
const conversationMessages = [...options.messages];
|
|
2454
|
+
let finalContent = "";
|
|
2455
|
+
let finalReasoning = "";
|
|
2456
|
+
let finalUsageTokens;
|
|
2457
|
+
const blocks = [];
|
|
2458
|
+
logger.info("Sending request to LLM with tools", {
|
|
2459
|
+
model: actualModel,
|
|
2460
|
+
providerId: actualProviderId,
|
|
2461
|
+
messagesCount: options.messages.length
|
|
2462
|
+
});
|
|
2463
|
+
logToolFormat(options.tools, logger);
|
|
2464
|
+
const apiFormat = resolveApiFormat(provider);
|
|
2465
|
+
logger.info("API format determined", { apiFormat });
|
|
2466
|
+
while (iteration < MAX_ITERATIONS) {
|
|
2467
|
+
iteration++;
|
|
2468
|
+
logger.info("Agentic loop iteration", {
|
|
2469
|
+
iteration,
|
|
2470
|
+
maxIterations: MAX_ITERATIONS,
|
|
2471
|
+
messagesCount: conversationMessages.length
|
|
2472
|
+
});
|
|
2473
|
+
const { requestBody, url } = buildToolRequest(
|
|
2474
|
+
apiFormat,
|
|
2475
|
+
conversationMessages,
|
|
2476
|
+
actualModel,
|
|
2477
|
+
options,
|
|
2478
|
+
provider
|
|
2479
|
+
);
|
|
2480
|
+
if (options.nativeSearchAugmentation) {
|
|
2481
|
+
applyAugmentation(requestBody, options.nativeSearchAugmentation);
|
|
2482
|
+
}
|
|
2483
|
+
const headers = getProviderHeaders(provider, apiKey);
|
|
2484
|
+
logger.info("Sending tool call request", {
|
|
2485
|
+
url,
|
|
2486
|
+
headers,
|
|
2487
|
+
requestBody
|
|
2488
|
+
});
|
|
2489
|
+
const response = await fetch(url, {
|
|
2490
|
+
method: "POST",
|
|
2491
|
+
headers,
|
|
2492
|
+
body: JSON.stringify(requestBody)
|
|
2493
|
+
});
|
|
2494
|
+
if (!response.ok) {
|
|
2495
|
+
const errorText = await response.text();
|
|
2496
|
+
callbacks.onError?.(`API error (${response.status}): ${errorText}`);
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
const reader = response.body?.getReader();
|
|
2500
|
+
if (!reader) {
|
|
2501
|
+
callbacks.onError?.("No response body");
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
const decoder = new TextDecoder();
|
|
2505
|
+
let content = "";
|
|
2506
|
+
let reasoning = "";
|
|
2507
|
+
const toolCalls = [];
|
|
2508
|
+
const pendingOpenAIToolCalls = /* @__PURE__ */ new Map();
|
|
2509
|
+
let buffer = "";
|
|
2510
|
+
try {
|
|
2511
|
+
while (true) {
|
|
2512
|
+
const { done, value } = await reader.read();
|
|
2513
|
+
if (done) break;
|
|
2514
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
2515
|
+
logger.debug("Received raw chunk", { chunkLength: chunk.length });
|
|
2516
|
+
buffer += chunk;
|
|
2517
|
+
const lines = buffer.split("\n");
|
|
2518
|
+
buffer = lines.pop() || "";
|
|
2519
|
+
for (const line of lines) {
|
|
2520
|
+
const trimmedLine = line.trim();
|
|
2521
|
+
if (!trimmedLine) continue;
|
|
2522
|
+
logger.debug("Processing line", { linePreview: trimmedLine.substring(0, 200) });
|
|
2523
|
+
if (trimmedLine.startsWith("data: ")) {
|
|
2524
|
+
const data = trimmedLine.slice(6);
|
|
2525
|
+
if (data === "[DONE]") {
|
|
2526
|
+
logger.debug("Stream done");
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
try {
|
|
2530
|
+
const json = JSON.parse(data);
|
|
2531
|
+
logger.debug("Parsed JSON", { json });
|
|
2532
|
+
if (json.error) {
|
|
2533
|
+
logger.error("API Error", void 0, { error: json.error });
|
|
2534
|
+
callbacks.onError?.(`API Error: ${json.error.message || JSON.stringify(json.error)}`);
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
parseStreamChunk(json, apiFormat, callbacks, logger);
|
|
2538
|
+
content = content + extractDeltaContent(json, apiFormat);
|
|
2539
|
+
reasoning = reasoning + extractDeltaReasoning(json, apiFormat);
|
|
2540
|
+
extractToolCalls(json, apiFormat, toolCalls, callbacks, logger, pendingOpenAIToolCalls);
|
|
2541
|
+
if (apiFormat !== "google" && apiFormat !== "anthropic" && json.usage) {
|
|
2542
|
+
finalUsageTokens = {
|
|
2543
|
+
promptTokens: json.usage.prompt_tokens || 0,
|
|
2544
|
+
completionTokens: json.usage.completion_tokens || 0,
|
|
2545
|
+
totalTokens: json.usage.total_tokens || 0
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
} catch (_e) {
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
if (apiFormat !== "google" && apiFormat !== "anthropic") {
|
|
2554
|
+
finalizeOpenAIToolCalls(pendingOpenAIToolCalls, toolCalls, callbacks, logger);
|
|
2555
|
+
pendingOpenAIToolCalls.clear();
|
|
2556
|
+
}
|
|
2557
|
+
finalContent = content;
|
|
2558
|
+
finalReasoning = reasoning;
|
|
2559
|
+
buildIterationBlocks(iteration, content, reasoning, blocks, callbacks);
|
|
2560
|
+
const executableToolCalls = toolCalls.filter(
|
|
2561
|
+
(tc) => !NATIVE_SEARCH_TOOL_NAMES.includes(tc.name)
|
|
2562
|
+
);
|
|
2563
|
+
logger.info("Tool execution check", {
|
|
2564
|
+
toolCallsLength: toolCalls.length,
|
|
2565
|
+
executableToolCallsLength: executableToolCalls.length,
|
|
2566
|
+
hasMcpTools: !!options.mcpTools,
|
|
2567
|
+
mcpToolsLength: options.mcpTools?.length,
|
|
2568
|
+
hasMcpService: !!mcpService
|
|
2569
|
+
});
|
|
2570
|
+
if (executableToolCalls.length > 0 && options.mcpTools && (mcpService || builtinExecutor)) {
|
|
2571
|
+
logger.info("Executing tool calls", { count: executableToolCalls.length });
|
|
2572
|
+
conversationMessages.push({
|
|
2573
|
+
id: `assistant_${iteration}`,
|
|
2574
|
+
role: "assistant",
|
|
2575
|
+
content: content || "Calling tools...",
|
|
2576
|
+
timestamp: Date.now()
|
|
2577
|
+
});
|
|
2578
|
+
const toolResults = await executeToolCalls(
|
|
2579
|
+
executableToolCalls,
|
|
2580
|
+
options.mcpTools,
|
|
2581
|
+
mcpService,
|
|
2582
|
+
blocks,
|
|
2583
|
+
callbacks,
|
|
2584
|
+
logger,
|
|
2585
|
+
builtinExecutor
|
|
2586
|
+
);
|
|
2587
|
+
const toolResultsText = toolResults.map(({ toolCall, result }) => {
|
|
2588
|
+
const resultText = typeof result === "string" ? result : result.content?.[0]?.text || JSON.stringify(result);
|
|
2589
|
+
return `Tool ${toolCall.name} result:
|
|
2590
|
+
${resultText}`;
|
|
2591
|
+
}).join("\n\n");
|
|
2592
|
+
conversationMessages.push({
|
|
2593
|
+
id: `tool_results_${iteration}`,
|
|
2594
|
+
role: "user",
|
|
2595
|
+
content: toolResultsText,
|
|
2596
|
+
timestamp: Date.now()
|
|
2597
|
+
});
|
|
2598
|
+
logger.info("Continuing to next iteration with tool results");
|
|
2599
|
+
continue;
|
|
2600
|
+
} else {
|
|
2601
|
+
logger.info("No tool calls, finishing");
|
|
2602
|
+
break;
|
|
2603
|
+
}
|
|
2604
|
+
} catch (streamError) {
|
|
2605
|
+
logger.error("Stream processing error", streamError instanceof Error ? streamError : void 0);
|
|
2606
|
+
callbacks.onError?.(`Stream error: ${streamError instanceof Error ? streamError.message : "Unknown error"}`);
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
logger.info("Sending final callback", { blocksCount: blocks.length });
|
|
2611
|
+
callbacks.onDone?.({
|
|
2612
|
+
id: messageId,
|
|
2613
|
+
role: "assistant",
|
|
2614
|
+
content: finalContent,
|
|
2615
|
+
timestamp: Date.now(),
|
|
2616
|
+
thinking: finalReasoning ? { content: finalReasoning } : void 0,
|
|
2617
|
+
blocks: blocks.length > 0 ? blocks : void 0
|
|
2618
|
+
}, finalUsageTokens);
|
|
2619
|
+
} catch (error) {
|
|
2620
|
+
logger.error("Error in streamWithTools", error instanceof Error ? error : void 0);
|
|
2621
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2622
|
+
callbacks.onError?.(message);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
// src/completion/TransformerHandler.ts
|
|
2627
|
+
function readUsageFromOpenAIResponse(usage) {
|
|
2628
|
+
if (!usage) return null;
|
|
2629
|
+
const promptTokens = Number(usage.prompt_tokens) || 0;
|
|
2630
|
+
const completionTokens = Number(usage.completion_tokens) || 0;
|
|
2631
|
+
const promptDetails = usage.prompt_tokens_details ?? {};
|
|
2632
|
+
const cachedTokens = Number(promptDetails.cached_tokens) || Number(usage.cache_read_input_tokens) || 0;
|
|
2633
|
+
const cacheCreation = Number(usage.cache_creation_input_tokens) || 0;
|
|
2634
|
+
const inputTokens = Math.max(0, promptTokens - cachedTokens - cacheCreation);
|
|
2635
|
+
const reasoningDetails = usage.completion_tokens_details ?? usage.output_tokens_details ?? {};
|
|
2636
|
+
const reasoningTokens = Number(reasoningDetails.reasoning_tokens) || 0;
|
|
2637
|
+
return {
|
|
2638
|
+
inputTokens,
|
|
2639
|
+
outputTokens: completionTokens,
|
|
2640
|
+
cacheReadTokens: cachedTokens,
|
|
2641
|
+
cacheCreationTokens: cacheCreation,
|
|
2642
|
+
reasoningTokens
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
var sharedExecutor = null;
|
|
2646
|
+
function getSharedExecutor2() {
|
|
2647
|
+
if (!sharedExecutor) {
|
|
2648
|
+
sharedExecutor = new TransformerChainExecutor();
|
|
2649
|
+
}
|
|
2650
|
+
return sharedExecutor;
|
|
2651
|
+
}
|
|
2652
|
+
async function resolveChainWithMain(llmConfig, providerId, model) {
|
|
2653
|
+
return resolveProviderChain(llmConfig, providerId, model);
|
|
2654
|
+
}
|
|
2655
|
+
async function completeWithTransformers(options, llmConfig, getProvider, resolveApiKey, completeFallback, logger, recording) {
|
|
2656
|
+
try {
|
|
2657
|
+
const routedInfo = await llmConfig.resolveRoutedModel(
|
|
2658
|
+
options.providerId,
|
|
2659
|
+
options.model
|
|
2660
|
+
);
|
|
2661
|
+
const actualProviderId = routedInfo?.actualProviderId || options.providerId;
|
|
2662
|
+
const actualModel = routedInfo?.actualModelId || options.model;
|
|
2663
|
+
const provider = await getProvider(actualProviderId);
|
|
2664
|
+
if (!provider) {
|
|
2665
|
+
return { success: false, error: `Provider not found: ${actualProviderId}` };
|
|
2666
|
+
}
|
|
2667
|
+
if (!provider.enabled) {
|
|
2668
|
+
return { success: false, error: `Provider is disabled: ${provider.name}` };
|
|
2669
|
+
}
|
|
2670
|
+
const { apiKey: effectiveKey } = resolveProviderEndpoint(provider);
|
|
2671
|
+
const apiKey = resolveApiKey(effectiveKey);
|
|
2672
|
+
if (!apiKey) {
|
|
2673
|
+
return { success: false, error: "API key not configured" };
|
|
2674
|
+
}
|
|
2675
|
+
const { chain, hasTransformers } = await resolveChainWithMain(
|
|
2676
|
+
llmConfig,
|
|
2677
|
+
actualProviderId,
|
|
2678
|
+
actualModel
|
|
2679
|
+
);
|
|
2680
|
+
if (!hasTransformers) {
|
|
2681
|
+
return completeFallback(options);
|
|
2682
|
+
}
|
|
2683
|
+
const unifiedRequest = {
|
|
2684
|
+
model: actualModel,
|
|
2685
|
+
messages: options.messages.map((m) => ({
|
|
2686
|
+
role: m.role,
|
|
2687
|
+
content: m.content
|
|
2688
|
+
})),
|
|
2689
|
+
max_tokens: options.maxTokens || 4096,
|
|
2690
|
+
temperature: options.temperature,
|
|
2691
|
+
stream: options.stream ?? false
|
|
2692
|
+
};
|
|
2693
|
+
const transformerProvider = {
|
|
2694
|
+
name: provider.name,
|
|
2695
|
+
baseUrl: provider.api_base_url,
|
|
2696
|
+
apiKey,
|
|
2697
|
+
models: provider.models || []
|
|
2698
|
+
};
|
|
2699
|
+
const executor = getSharedExecutor2();
|
|
2700
|
+
const { response } = await executeProviderCall({
|
|
2701
|
+
executor,
|
|
2702
|
+
request: unifiedRequest,
|
|
2703
|
+
provider: transformerProvider,
|
|
2704
|
+
chain,
|
|
2705
|
+
endpointTransformer: void 0,
|
|
2706
|
+
extendedContext: options.useExtendedContext ? { enabled: true, model: actualModel } : void 0,
|
|
2707
|
+
resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : buildProviderApiUrl(provider, { model: actualModel, stream: false }),
|
|
2708
|
+
buildHeaders: (config) => ({
|
|
2709
|
+
...getProviderHeaders(provider, apiKey),
|
|
2710
|
+
...config.headers,
|
|
2711
|
+
...isOpenRouterProvider(provider) ? OPENROUTER_APP_HEADERS : {}
|
|
2712
|
+
}),
|
|
2713
|
+
// Add OpenRouter provider routing config if applicable
|
|
2714
|
+
prepareBody: (requestBody) => addOpenRouterProviderToRequest(
|
|
2715
|
+
requestBody,
|
|
2716
|
+
provider,
|
|
2717
|
+
actualModel
|
|
2718
|
+
),
|
|
2719
|
+
fetchFn: (url, headers, body) => {
|
|
2720
|
+
logger.info("Calling completion with transformers", { url });
|
|
2721
|
+
return fetch(url, {
|
|
2722
|
+
method: "POST",
|
|
2723
|
+
headers,
|
|
2724
|
+
body: JSON.stringify(body)
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
});
|
|
2728
|
+
if (!response.ok) {
|
|
2729
|
+
const errorText = await response.text();
|
|
2730
|
+
return {
|
|
2731
|
+
success: false,
|
|
2732
|
+
error: `API error (${response.status}): ${errorText}`
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
const transformedResponse = await executor.executeResponseChain(
|
|
2736
|
+
unifiedRequest,
|
|
2737
|
+
response,
|
|
2738
|
+
transformerProvider,
|
|
2739
|
+
chain,
|
|
2740
|
+
{ endpointTransformer: void 0 }
|
|
2741
|
+
);
|
|
2742
|
+
const data = await transformedResponse.json();
|
|
2743
|
+
const choice = data.choices?.[0];
|
|
2744
|
+
if (!choice) {
|
|
2745
|
+
return { success: false, error: "No choices in response" };
|
|
2746
|
+
}
|
|
2747
|
+
const rawToolCalls = choice.message?.tool_calls;
|
|
2748
|
+
const toolCalls = rawToolCalls?.map((tc) => ({
|
|
2749
|
+
id: tc.id,
|
|
2750
|
+
name: tc.function.name,
|
|
2751
|
+
args: typeof tc.function.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function.arguments
|
|
2752
|
+
}));
|
|
2753
|
+
const tapped = readUsageFromOpenAIResponse(data.usage);
|
|
2754
|
+
if (recording?.recorder && tapped) {
|
|
2755
|
+
recording.recorder.record({
|
|
2756
|
+
messageId: options.messageId ?? null,
|
|
2757
|
+
parentMessageId: options.parentMessageId ?? null,
|
|
2758
|
+
sessionId: options.sessionId ?? null,
|
|
2759
|
+
providerId: actualProviderId,
|
|
2760
|
+
model: actualModel,
|
|
2761
|
+
apiKeyId: recording.apiKeyId ?? null,
|
|
2762
|
+
engineOrigin: "completion",
|
|
2763
|
+
usage: tapped,
|
|
2764
|
+
rawUsage: data.usage
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
return {
|
|
2768
|
+
success: true,
|
|
2769
|
+
message: {
|
|
2770
|
+
id: `msg_${Date.now()}`,
|
|
2771
|
+
role: "assistant",
|
|
2772
|
+
content: choice.message?.content || "",
|
|
2773
|
+
timestamp: Date.now(),
|
|
2774
|
+
thinking: data.reasoning_content || choice.message?.thinking?.content ? { content: data.reasoning_content || choice.message?.thinking?.content || "", signature: choice.message?.thinking?.signature } : void 0,
|
|
2775
|
+
toolCalls
|
|
2776
|
+
},
|
|
2777
|
+
usage: data.usage ? {
|
|
2778
|
+
promptTokens: data.usage.prompt_tokens || 0,
|
|
2779
|
+
completionTokens: data.usage.completion_tokens || 0,
|
|
2780
|
+
totalTokens: data.usage.total_tokens || 0
|
|
2781
|
+
} : void 0,
|
|
2782
|
+
finishReason: choice.finish_reason
|
|
2783
|
+
};
|
|
2784
|
+
} catch (error) {
|
|
2785
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2786
|
+
return { success: false, error: message };
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
async function completeStreamWithTransformers(options, callbacks, llmConfig, getProvider, resolveApiKey, completeStreamFallback, logger, recording) {
|
|
2790
|
+
try {
|
|
2791
|
+
const routedInfo = await llmConfig.resolveRoutedModel(
|
|
2792
|
+
options.providerId,
|
|
2793
|
+
options.model
|
|
2794
|
+
);
|
|
2795
|
+
const actualProviderId = routedInfo?.actualProviderId || options.providerId;
|
|
2796
|
+
const actualModel = routedInfo?.actualModelId || options.model;
|
|
2797
|
+
const provider = await getProvider(actualProviderId);
|
|
2798
|
+
if (!provider) {
|
|
2799
|
+
callbacks.onError?.(`Provider not found: ${actualProviderId}`);
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
if (!provider.enabled) {
|
|
2803
|
+
callbacks.onError?.(`Provider is disabled: ${provider.name}`);
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
const { apiKey: effectiveKey } = resolveProviderEndpoint(provider);
|
|
2807
|
+
const apiKey = resolveApiKey(effectiveKey);
|
|
2808
|
+
if (!apiKey) {
|
|
2809
|
+
callbacks.onError?.("API key not configured");
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
const { chain, hasTransformers } = await resolveChainWithMain(
|
|
2813
|
+
llmConfig,
|
|
2814
|
+
actualProviderId,
|
|
2815
|
+
actualModel
|
|
2816
|
+
);
|
|
2817
|
+
if (!hasTransformers) {
|
|
2818
|
+
return completeStreamFallback(options, callbacks);
|
|
2819
|
+
}
|
|
2820
|
+
const messageId = `msg_${Date.now()}`;
|
|
2821
|
+
callbacks.onStart?.(messageId);
|
|
2822
|
+
const unifiedRequest = {
|
|
2823
|
+
model: actualModel,
|
|
2824
|
+
messages: options.messages.map((m) => ({
|
|
2825
|
+
role: m.role,
|
|
2826
|
+
content: m.content
|
|
2827
|
+
})),
|
|
2828
|
+
max_tokens: options.maxTokens || 4096,
|
|
2829
|
+
temperature: options.temperature,
|
|
2830
|
+
stream: true
|
|
2831
|
+
};
|
|
2832
|
+
const transformerProvider = {
|
|
2833
|
+
name: provider.name,
|
|
2834
|
+
baseUrl: provider.api_base_url,
|
|
2835
|
+
apiKey,
|
|
2836
|
+
models: provider.models || []
|
|
2837
|
+
};
|
|
2838
|
+
const executor = getSharedExecutor2();
|
|
2839
|
+
const { response } = await executeProviderCall({
|
|
2840
|
+
executor,
|
|
2841
|
+
request: unifiedRequest,
|
|
2842
|
+
provider: transformerProvider,
|
|
2843
|
+
chain,
|
|
2844
|
+
endpointTransformer: void 0,
|
|
2845
|
+
extendedContext: options.useExtendedContext ? { enabled: true, model: actualModel } : void 0,
|
|
2846
|
+
resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : buildProviderApiUrl(provider, { model: actualModel, stream: true }),
|
|
2847
|
+
buildHeaders: (config) => ({
|
|
2848
|
+
...getProviderHeaders(provider, apiKey),
|
|
2849
|
+
...config.headers,
|
|
2850
|
+
...isOpenRouterProvider(provider) ? OPENROUTER_APP_HEADERS : {}
|
|
2851
|
+
}),
|
|
2852
|
+
// Add OpenRouter provider routing config if applicable
|
|
2853
|
+
prepareBody: (requestBody) => addOpenRouterProviderToRequest(
|
|
2854
|
+
requestBody,
|
|
2855
|
+
provider,
|
|
2856
|
+
actualModel
|
|
2857
|
+
),
|
|
2858
|
+
fetchFn: (url, headers, body) => {
|
|
2859
|
+
logger.info("Streaming completion with transformers", { url });
|
|
2860
|
+
return fetch(url, {
|
|
2861
|
+
method: "POST",
|
|
2862
|
+
headers,
|
|
2863
|
+
body: JSON.stringify(body)
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
});
|
|
2867
|
+
if (!response.ok) {
|
|
2868
|
+
const errorText = await response.text();
|
|
2869
|
+
callbacks.onError?.(`API error (${response.status}): ${errorText}`);
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
const transformedResponse = await executor.executeResponseChain(
|
|
2873
|
+
unifiedRequest,
|
|
2874
|
+
response,
|
|
2875
|
+
transformerProvider,
|
|
2876
|
+
chain,
|
|
2877
|
+
{ endpointTransformer: void 0 }
|
|
2878
|
+
);
|
|
2879
|
+
const reader = transformedResponse.body?.getReader();
|
|
2880
|
+
if (!reader) {
|
|
2881
|
+
callbacks.onError?.("No response body");
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
const decoder = new TextDecoder();
|
|
2885
|
+
let content = "";
|
|
2886
|
+
let reasoning = "";
|
|
2887
|
+
try {
|
|
2888
|
+
while (true) {
|
|
2889
|
+
const { done, value } = await reader.read();
|
|
2890
|
+
if (done) break;
|
|
2891
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
2892
|
+
const lines = chunk.split("\n").filter((line) => line.trim() !== "");
|
|
2893
|
+
for (const line of lines) {
|
|
2894
|
+
if (line.startsWith("data: ")) {
|
|
2895
|
+
const data = line.slice(6);
|
|
2896
|
+
if (data === "[DONE]") continue;
|
|
2897
|
+
try {
|
|
2898
|
+
const json = JSON.parse(data);
|
|
2899
|
+
const delta = json.choices?.[0]?.delta;
|
|
2900
|
+
if (delta?.content) {
|
|
2901
|
+
content += delta.content;
|
|
2902
|
+
callbacks.onDelta?.(delta.content);
|
|
2903
|
+
}
|
|
2904
|
+
if (delta?.thinking?.content) {
|
|
2905
|
+
reasoning += delta.thinking.content;
|
|
2906
|
+
callbacks.onReasoning?.(delta.thinking.content);
|
|
2907
|
+
}
|
|
2908
|
+
if (delta?.reasoning_content) {
|
|
2909
|
+
reasoning += delta.reasoning_content;
|
|
2910
|
+
callbacks.onReasoning?.(delta.reasoning_content);
|
|
2911
|
+
}
|
|
2912
|
+
if (json.usage) {
|
|
2913
|
+
const tapped = readUsageFromOpenAIResponse(json.usage);
|
|
2914
|
+
if (recording?.recorder && tapped) {
|
|
2915
|
+
recording.recorder.record({
|
|
2916
|
+
messageId: options.messageId ?? null,
|
|
2917
|
+
parentMessageId: options.parentMessageId ?? null,
|
|
2918
|
+
sessionId: options.sessionId ?? null,
|
|
2919
|
+
providerId: actualProviderId,
|
|
2920
|
+
model: actualModel,
|
|
2921
|
+
apiKeyId: recording.apiKeyId ?? null,
|
|
2922
|
+
engineOrigin: "completion",
|
|
2923
|
+
usage: tapped,
|
|
2924
|
+
rawUsage: json.usage
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
callbacks.onDone?.(
|
|
2928
|
+
{
|
|
2929
|
+
id: messageId,
|
|
2930
|
+
role: "assistant",
|
|
2931
|
+
content,
|
|
2932
|
+
timestamp: Date.now(),
|
|
2933
|
+
thinking: reasoning ? { content: reasoning } : void 0
|
|
2934
|
+
},
|
|
2935
|
+
{
|
|
2936
|
+
promptTokens: json.usage.prompt_tokens || 0,
|
|
2937
|
+
completionTokens: json.usage.completion_tokens || 0,
|
|
2938
|
+
totalTokens: json.usage.total_tokens || 0
|
|
2939
|
+
}
|
|
2940
|
+
);
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
} catch (_e) {
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
callbacks.onDone?.({
|
|
2949
|
+
id: messageId,
|
|
2950
|
+
role: "assistant",
|
|
2951
|
+
content,
|
|
2952
|
+
timestamp: Date.now(),
|
|
2953
|
+
thinking: reasoning ? { content: reasoning } : void 0
|
|
2954
|
+
});
|
|
2955
|
+
} finally {
|
|
2956
|
+
reader.releaseLock();
|
|
2957
|
+
}
|
|
2958
|
+
} catch (error) {
|
|
2959
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2960
|
+
callbacks.onError?.(message);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
// src/completion/CompletionService.ts
|
|
2965
|
+
var CompletionService = class {
|
|
2966
|
+
constructor(paths, llmConfig, logger) {
|
|
2967
|
+
this.paths = paths;
|
|
2968
|
+
this.llmConfig = llmConfig;
|
|
2969
|
+
this.logger = logger;
|
|
2970
|
+
}
|
|
2971
|
+
paths;
|
|
2972
|
+
llmConfig;
|
|
2973
|
+
logger;
|
|
2974
|
+
apiKeyPool = null;
|
|
2975
|
+
usageRecorder = null;
|
|
2976
|
+
usageEventSink = null;
|
|
2977
|
+
visionFallbackProvider = null;
|
|
2978
|
+
/**
|
|
2979
|
+
* Set the API key pool service for multi-key load balancing.
|
|
2980
|
+
* When set, keys are resolved via the pool instead of directly from the provider.
|
|
2981
|
+
*/
|
|
2982
|
+
setApiKeyPool(pool) {
|
|
2983
|
+
this.apiKeyPool = pool;
|
|
2984
|
+
}
|
|
2985
|
+
/**
|
|
2986
|
+
* Set the usage recorder so completion paths can persist token/cost stats.
|
|
2987
|
+
* Optional — when unset, all calls succeed but nothing is recorded.
|
|
2988
|
+
*/
|
|
2989
|
+
setUsageRecorder(recorder) {
|
|
2990
|
+
this.usageRecorder = recorder;
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Set the usage-event sink so completion paths can push live usage events
|
|
2994
|
+
* (context-meter, aggregate recorder) into the in-process hub. Injected DOWN
|
|
2995
|
+
* at bootstrap with `getUsageEventHub()` immediately after construction, so
|
|
2996
|
+
* emission is unconditional in production. Optional — when unset (unit-test
|
|
2997
|
+
* constructors), the emit calls no-op, identical to `usageRecorder`.
|
|
2998
|
+
*/
|
|
2999
|
+
setUsageEventSink(sink) {
|
|
3000
|
+
this.usageEventSink = sink;
|
|
3001
|
+
}
|
|
3002
|
+
/**
|
|
3003
|
+
* Set the vision-fallback provider used by `applyVisionFallback` to describe
|
|
3004
|
+
* images for non-vision models. Injected DOWN by the host at bootstrap
|
|
3005
|
+
* (the host's impl is built on top of CompletionService).
|
|
3006
|
+
* Optional — when unset, `applyVisionFallback` strips images instead.
|
|
3007
|
+
*/
|
|
3008
|
+
setVisionFallbackProvider(provider) {
|
|
3009
|
+
this.visionFallbackProvider = provider;
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Get provider by ID
|
|
3013
|
+
* Delegates to the `ProviderConfigSource` which maintains its own in-memory cache.
|
|
3014
|
+
*/
|
|
3015
|
+
async getProvider(providerId) {
|
|
3016
|
+
return this.llmConfig.getProvider(providerId);
|
|
3017
|
+
}
|
|
3018
|
+
/**
|
|
3019
|
+
* Send a completion request
|
|
3020
|
+
*/
|
|
3021
|
+
async complete(options) {
|
|
3022
|
+
try {
|
|
3023
|
+
const routedInfo = await this.llmConfig.resolveRoutedModel(
|
|
3024
|
+
options.providerId,
|
|
3025
|
+
options.model
|
|
3026
|
+
);
|
|
3027
|
+
const actualProviderId = routedInfo?.actualProviderId || options.providerId;
|
|
3028
|
+
const actualModel = routedInfo?.actualModelId || options.model;
|
|
3029
|
+
const provider = await this.getProvider(actualProviderId);
|
|
3030
|
+
if (!provider) {
|
|
3031
|
+
return { success: false, error: `Provider not found: ${actualProviderId}` };
|
|
3032
|
+
}
|
|
3033
|
+
if (!provider.enabled) {
|
|
3034
|
+
return { success: false, error: `Provider is disabled: ${provider.name}` };
|
|
3035
|
+
}
|
|
3036
|
+
const apiKey = await this.resolveApiKeyForRequest(provider, actualProviderId, options.sessionId);
|
|
3037
|
+
if (!apiKey) {
|
|
3038
|
+
return { success: false, error: "API key not configured" };
|
|
3039
|
+
}
|
|
3040
|
+
const apiFormat = resolveApiFormat(provider);
|
|
3041
|
+
this.logger.info("Using API format for completion", { apiFormat, providerId: actualProviderId });
|
|
3042
|
+
const resolvedOptions = { ...options, model: actualModel };
|
|
3043
|
+
let result = await this.callDirectHandler(apiFormat, provider, apiKey, resolvedOptions);
|
|
3044
|
+
if (!result.success && result.error && this.apiKeyPool && options.sessionId) {
|
|
3045
|
+
const status = this.extractHttpStatus(result.error);
|
|
3046
|
+
if (status && (status === 429 || status === 529 || status === 401 || status === 403)) {
|
|
3047
|
+
const newKey = await this.apiKeyPool.reportError(actualProviderId, options.sessionId, status);
|
|
3048
|
+
if (newKey) {
|
|
3049
|
+
this.logger.info("Retrying completion with new API key", {
|
|
3050
|
+
providerId: actualProviderId,
|
|
3051
|
+
statusCode: status
|
|
3052
|
+
});
|
|
3053
|
+
result = await this.callDirectHandler(apiFormat, provider, newKey, resolvedOptions);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
if (result.success && this.apiKeyPool && options.sessionId) {
|
|
3058
|
+
this.apiKeyPool.reportSuccess(options.sessionId);
|
|
3059
|
+
}
|
|
3060
|
+
if (result.success && result.usage && options.sessionId) {
|
|
3061
|
+
this.usageEventSink?.emit({
|
|
3062
|
+
sessionId: options.sessionId,
|
|
3063
|
+
modelId: actualModel,
|
|
3064
|
+
usage: result.usage,
|
|
3065
|
+
engineOrigin: "completion"
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
return result;
|
|
3069
|
+
} catch (error) {
|
|
3070
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3071
|
+
return { success: false, error: message };
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
3075
|
+
* Send a streaming completion request
|
|
3076
|
+
*/
|
|
3077
|
+
async completeStream(options, callbacks) {
|
|
3078
|
+
try {
|
|
3079
|
+
await this.applyVisionFallback(options);
|
|
3080
|
+
this.logger.info("Starting stream completion", {
|
|
3081
|
+
providerId: options.providerId,
|
|
3082
|
+
model: options.model,
|
|
3083
|
+
messagesCount: options.messages.length
|
|
3084
|
+
});
|
|
3085
|
+
const routedInfo = await this.llmConfig.resolveRoutedModel(
|
|
3086
|
+
options.providerId,
|
|
3087
|
+
options.model
|
|
3088
|
+
);
|
|
3089
|
+
this.logger.debug("Resolved routed model", { routedInfo });
|
|
3090
|
+
const actualProviderId = routedInfo?.actualProviderId || options.providerId;
|
|
3091
|
+
const actualModel = routedInfo?.actualModelId || options.model;
|
|
3092
|
+
this.logger.debug("Resolved provider and model", { actualProviderId, actualModel });
|
|
3093
|
+
const provider = await this.getProvider(actualProviderId);
|
|
3094
|
+
this.logger.debug("Retrieved provider", provider ? {
|
|
3095
|
+
id: provider.id,
|
|
3096
|
+
name: provider.name,
|
|
3097
|
+
apiType: provider.apiType,
|
|
3098
|
+
apiFormat: provider.apiFormat,
|
|
3099
|
+
api_base_url: provider.api_base_url,
|
|
3100
|
+
enabled: provider.enabled
|
|
3101
|
+
} : { error: "Provider not found" });
|
|
3102
|
+
if (!provider) {
|
|
3103
|
+
callbacks.onError?.(`Provider not found: ${actualProviderId}`);
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
if (!provider.enabled) {
|
|
3107
|
+
callbacks.onError?.(`Provider is disabled: ${provider.name}`);
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
const apiKey = await this.resolveApiKeyForRequest(provider, actualProviderId, options.sessionId);
|
|
3111
|
+
if (!apiKey) {
|
|
3112
|
+
callbacks.onError?.("API key not configured");
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
const apiFormat = resolveApiFormat(provider);
|
|
3116
|
+
this.logger.info("Using API format for stream", { apiFormat });
|
|
3117
|
+
const messageId = `msg_${Date.now()}`;
|
|
3118
|
+
callbacks.onStart?.(messageId);
|
|
3119
|
+
const resolvedOptions = { ...options, model: actualModel };
|
|
3120
|
+
if (options.sessionId) {
|
|
3121
|
+
const sid = options.sessionId;
|
|
3122
|
+
const userOnDone = callbacks.onDone;
|
|
3123
|
+
callbacks = {
|
|
3124
|
+
...callbacks,
|
|
3125
|
+
onDone: (message, usage, metrics) => {
|
|
3126
|
+
if (usage) {
|
|
3127
|
+
this.usageEventSink?.emit({
|
|
3128
|
+
sessionId: sid,
|
|
3129
|
+
modelId: actualModel,
|
|
3130
|
+
usage,
|
|
3131
|
+
engineOrigin: "completion"
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
userOnDone?.(message, usage, metrics);
|
|
3135
|
+
}
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
if (this.apiKeyPool && options.sessionId) {
|
|
3139
|
+
const retryState = { error: null };
|
|
3140
|
+
const interceptCallbacks = {
|
|
3141
|
+
...callbacks,
|
|
3142
|
+
onStart: void 0,
|
|
3143
|
+
// already called above
|
|
3144
|
+
onError: (error) => {
|
|
3145
|
+
const status = this.extractHttpStatus(error);
|
|
3146
|
+
if (status && (status === 429 || status === 529 || status === 401 || status === 403)) {
|
|
3147
|
+
retryState.error = { status, message: error };
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
callbacks.onError?.(error);
|
|
3151
|
+
},
|
|
3152
|
+
onDone: (message, usage, metrics) => {
|
|
3153
|
+
this.apiKeyPool.reportSuccess(options.sessionId);
|
|
3154
|
+
callbacks.onDone?.(message, usage, metrics);
|
|
3155
|
+
}
|
|
3156
|
+
};
|
|
3157
|
+
await this.callStreamHandler(apiFormat, provider, apiKey, resolvedOptions, messageId, interceptCallbacks);
|
|
3158
|
+
if (retryState.error) {
|
|
3159
|
+
const newKey = await this.apiKeyPool.reportError(
|
|
3160
|
+
actualProviderId,
|
|
3161
|
+
options.sessionId,
|
|
3162
|
+
retryState.error.status
|
|
3163
|
+
);
|
|
3164
|
+
if (newKey) {
|
|
3165
|
+
this.logger.info("Retrying stream with new API key", {
|
|
3166
|
+
providerId: actualProviderId,
|
|
3167
|
+
statusCode: retryState.error.status
|
|
3168
|
+
});
|
|
3169
|
+
await this.callStreamHandler(apiFormat, provider, newKey, resolvedOptions, messageId, {
|
|
3170
|
+
...callbacks,
|
|
3171
|
+
onStart: void 0
|
|
3172
|
+
// don't fire onStart again
|
|
3173
|
+
});
|
|
3174
|
+
} else {
|
|
3175
|
+
callbacks.onError?.(retryState.error.message);
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
} else {
|
|
3179
|
+
await this.callStreamHandler(apiFormat, provider, apiKey, resolvedOptions, messageId, {
|
|
3180
|
+
...callbacks,
|
|
3181
|
+
onStart: void 0
|
|
3182
|
+
// already called above
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
} catch (error) {
|
|
3186
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3187
|
+
this.logger.error("Stream completion error", error instanceof Error ? error : void 0, { message });
|
|
3188
|
+
callbacks.onError?.(message);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
/**
|
|
3192
|
+
* Get available models for a provider
|
|
3193
|
+
*/
|
|
3194
|
+
async getAvailableModels(providerId) {
|
|
3195
|
+
const provider = await this.getProvider(providerId);
|
|
3196
|
+
if (!provider) return [];
|
|
3197
|
+
return provider.models || [];
|
|
3198
|
+
}
|
|
3199
|
+
/**
|
|
3200
|
+
* Test provider connection with a specific model.
|
|
3201
|
+
* Sends "Hello" and returns the AI response, duration, etc.
|
|
3202
|
+
*/
|
|
3203
|
+
async testModel(providerId, modelId) {
|
|
3204
|
+
try {
|
|
3205
|
+
const provider = await this.getProvider(providerId);
|
|
3206
|
+
if (!provider) {
|
|
3207
|
+
return { success: false, message: "Provider not found", model: modelId };
|
|
3208
|
+
}
|
|
3209
|
+
const apiKey = this.resolveApiKey(provider.api_key);
|
|
3210
|
+
if (!apiKey) {
|
|
3211
|
+
return { success: false, message: "API key not configured", model: modelId };
|
|
3212
|
+
}
|
|
3213
|
+
const testMessages = [
|
|
3214
|
+
{ id: "test", role: "user", content: "Hello", timestamp: Date.now() }
|
|
3215
|
+
];
|
|
3216
|
+
const startTime = Date.now();
|
|
3217
|
+
const result = await this.complete({
|
|
3218
|
+
providerId,
|
|
3219
|
+
model: modelId,
|
|
3220
|
+
messages: testMessages,
|
|
3221
|
+
maxTokens: 100
|
|
3222
|
+
});
|
|
3223
|
+
const durationMs = Date.now() - startTime;
|
|
3224
|
+
if (result.success) {
|
|
3225
|
+
return {
|
|
3226
|
+
success: true,
|
|
3227
|
+
message: "Connection successful",
|
|
3228
|
+
response: result.message?.content || "",
|
|
3229
|
+
model: modelId,
|
|
3230
|
+
durationMs
|
|
3231
|
+
};
|
|
3232
|
+
} else {
|
|
3233
|
+
return {
|
|
3234
|
+
success: false,
|
|
3235
|
+
message: result.error || "Unknown error",
|
|
3236
|
+
model: modelId,
|
|
3237
|
+
durationMs
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
} catch (error) {
|
|
3241
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3242
|
+
return { success: false, message, model: modelId };
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Check if any messages contain images and the model lacks vision capability.
|
|
3247
|
+
* If so, use the auxiliary vision model to describe images as text and remove
|
|
3248
|
+
* the image attachments from the messages.
|
|
3249
|
+
*
|
|
3250
|
+
* Mutates options.messages in-place.
|
|
3251
|
+
*/
|
|
3252
|
+
async applyVisionFallback(options) {
|
|
3253
|
+
const hasImages = options.messages.some((m) => m.images && m.images.length > 0);
|
|
3254
|
+
if (!hasImages) return;
|
|
3255
|
+
const hasVision = await this.llmConfig.hasVisionCapability(options.providerId, options.model);
|
|
3256
|
+
if (hasVision) return;
|
|
3257
|
+
const { vision: effectiveVisionModel } = await this.llmConfig.resolveEffectiveModels();
|
|
3258
|
+
if (!effectiveVisionModel || !this.visionFallbackProvider) {
|
|
3259
|
+
this.logger.info("Messages contain images but no vision auxiliary model configured; stripping images");
|
|
3260
|
+
for (const msg of options.messages) {
|
|
3261
|
+
if (msg.images && msg.images.length > 0) {
|
|
3262
|
+
msg.images = void 0;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
this.logger.info("Model lacks vision capability, using auxiliary vision model for image descriptions", {
|
|
3268
|
+
providerId: options.providerId,
|
|
3269
|
+
model: options.model
|
|
3270
|
+
});
|
|
3271
|
+
const visionService = this.visionFallbackProvider;
|
|
3272
|
+
for (const msg of options.messages) {
|
|
3273
|
+
if (msg.images && msg.images.length > 0) {
|
|
3274
|
+
const imageDataUrls = msg.images.map((img) => ({ data: img.url }));
|
|
3275
|
+
const description = await visionService.describeImages(imageDataUrls, msg.content, effectiveVisionModel);
|
|
3276
|
+
if (description && description !== "[Image description unavailable]") {
|
|
3277
|
+
msg.content = `${msg.content}
|
|
3278
|
+
|
|
3279
|
+
[Image Description]
|
|
3280
|
+
${description}`;
|
|
3281
|
+
}
|
|
3282
|
+
msg.images = void 0;
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Resolve API key for a request with priority:
|
|
3288
|
+
* 1. Coding Plan override (if enabled)
|
|
3289
|
+
* 2. API key pool (session-affinity weighted round-robin)
|
|
3290
|
+
* 3. Legacy single key from provider config
|
|
3291
|
+
*/
|
|
3292
|
+
async resolveApiKeyForRequest(provider, providerId, sessionId) {
|
|
3293
|
+
if (provider.codingPlan?.enabled && provider.codingPlan.apiKey) {
|
|
3294
|
+
return this.resolveApiKey(provider.codingPlan.apiKey);
|
|
3295
|
+
}
|
|
3296
|
+
if (this.apiKeyPool) {
|
|
3297
|
+
const poolKey = sessionId ? await this.apiKeyPool.getKeyForSession(providerId, sessionId) : await this.apiKeyPool.getKey(providerId);
|
|
3298
|
+
if (poolKey) return poolKey;
|
|
3299
|
+
}
|
|
3300
|
+
const { apiKey: effectiveKey } = resolveProviderEndpoint(provider);
|
|
3301
|
+
return this.resolveApiKey(effectiveKey);
|
|
3302
|
+
}
|
|
3303
|
+
// ==========================================================================
|
|
3304
|
+
// Handler dispatch helpers
|
|
3305
|
+
// ==========================================================================
|
|
3306
|
+
/**
|
|
3307
|
+
* Extract HTTP status code from error messages like "API error (429): ..."
|
|
3308
|
+
*/
|
|
3309
|
+
extractHttpStatus(error) {
|
|
3310
|
+
const match = error.match(/\((\d{3})\):/);
|
|
3311
|
+
return match ? parseInt(match[1], 10) : null;
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Dispatch a non-streaming completion to the appropriate handler.
|
|
3315
|
+
*/
|
|
3316
|
+
async callDirectHandler(apiFormat, provider, apiKey, options) {
|
|
3317
|
+
switch (apiFormat) {
|
|
3318
|
+
case "anthropic":
|
|
3319
|
+
return callAnthropicCompletion(provider, apiKey, options, this.logger);
|
|
3320
|
+
case "google":
|
|
3321
|
+
return callGeminiCompletion(provider, apiKey, options, this.logger);
|
|
3322
|
+
case "openai-response":
|
|
3323
|
+
return callOpenAIResponseCompletion(provider, apiKey, options, this.logger);
|
|
3324
|
+
case "azure-openai":
|
|
3325
|
+
case "openai":
|
|
3326
|
+
default:
|
|
3327
|
+
return callOpenAICompletion(provider, apiKey, options, this.logger);
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Dispatch a streaming completion to the appropriate handler.
|
|
3332
|
+
*/
|
|
3333
|
+
async callStreamHandler(apiFormat, provider, apiKey, options, messageId, callbacks) {
|
|
3334
|
+
switch (apiFormat) {
|
|
3335
|
+
case "openai-response":
|
|
3336
|
+
await streamOpenAIResponseCompletion(provider, apiKey, options, messageId, callbacks, this.logger);
|
|
3337
|
+
return;
|
|
3338
|
+
case "anthropic":
|
|
3339
|
+
await streamAnthropicCompletion(provider, apiKey, options, messageId, callbacks, this.logger);
|
|
3340
|
+
return;
|
|
3341
|
+
case "google":
|
|
3342
|
+
await streamGeminiCompletion(provider, apiKey, options, messageId, callbacks, this.logger);
|
|
3343
|
+
return;
|
|
3344
|
+
case "azure-openai":
|
|
3345
|
+
case "openai":
|
|
3346
|
+
default:
|
|
3347
|
+
await streamOpenAICompletion(provider, apiKey, options, messageId, callbacks, this.logger);
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
/**
|
|
3352
|
+
* Resolve API key (handle environment variable references)
|
|
3353
|
+
*/
|
|
3354
|
+
resolveApiKey(apiKey) {
|
|
3355
|
+
if (!apiKey) return "";
|
|
3356
|
+
if (apiKey.startsWith("$")) {
|
|
3357
|
+
const envVar = apiKey.slice(1);
|
|
3358
|
+
return process.env[envVar] || "";
|
|
3359
|
+
}
|
|
3360
|
+
return apiKey;
|
|
3361
|
+
}
|
|
3362
|
+
/**
|
|
3363
|
+
* Resolve effective max_tokens value with priority:
|
|
3364
|
+
* 1. Session settings (if provided)
|
|
3365
|
+
* 2. Global model parameters (if enabled)
|
|
3366
|
+
* 3. Model's maxTokens from provider config
|
|
3367
|
+
* 4. Discovered models cache (from API)
|
|
3368
|
+
* 5. undefined - let API use its default
|
|
3369
|
+
*/
|
|
3370
|
+
async resolveEffectiveMaxTokens(providerId, modelId, sessionMaxTokens) {
|
|
3371
|
+
return resolveEffectiveMaxTokens(
|
|
3372
|
+
this.llmConfig,
|
|
3373
|
+
this.getProvider.bind(this),
|
|
3374
|
+
this.logger,
|
|
3375
|
+
providerId,
|
|
3376
|
+
modelId,
|
|
3377
|
+
sessionMaxTokens
|
|
3378
|
+
);
|
|
3379
|
+
}
|
|
3380
|
+
/**
|
|
3381
|
+
* Get required max_tokens for providers that need it (e.g., Anthropic)
|
|
3382
|
+
* Falls back to DEFAULT_MAX_TOKENS if no value is configured
|
|
3383
|
+
*/
|
|
3384
|
+
async getRequiredMaxTokens(providerId, modelId, sessionMaxTokens) {
|
|
3385
|
+
return getRequiredMaxTokens(
|
|
3386
|
+
this.llmConfig,
|
|
3387
|
+
this.getProvider.bind(this),
|
|
3388
|
+
this.logger,
|
|
3389
|
+
providerId,
|
|
3390
|
+
modelId,
|
|
3391
|
+
sessionMaxTokens
|
|
3392
|
+
);
|
|
3393
|
+
}
|
|
3394
|
+
/**
|
|
3395
|
+
* Calculate thinking budget and adjust max_tokens for the provider
|
|
3396
|
+
*/
|
|
3397
|
+
async resolveThinkingBudget(providerId, modelId, maxTokens, thinkLevel) {
|
|
3398
|
+
return resolveThinkingBudget(
|
|
3399
|
+
this.getProvider.bind(this),
|
|
3400
|
+
this.logger,
|
|
3401
|
+
providerId,
|
|
3402
|
+
modelId,
|
|
3403
|
+
maxTokens,
|
|
3404
|
+
thinkLevel
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
// --------------------------------------------------------------------------
|
|
3408
|
+
// Transformer Chain Completion
|
|
3409
|
+
// --------------------------------------------------------------------------
|
|
3410
|
+
/**
|
|
3411
|
+
* Send a completion request using transformer chain
|
|
3412
|
+
*/
|
|
3413
|
+
async completeWithTransformers(options) {
|
|
3414
|
+
return completeWithTransformers(
|
|
3415
|
+
options,
|
|
3416
|
+
this.llmConfig,
|
|
3417
|
+
this.getProvider.bind(this),
|
|
3418
|
+
this.resolveApiKey.bind(this),
|
|
3419
|
+
this.complete.bind(this),
|
|
3420
|
+
this.logger,
|
|
3421
|
+
this.usageRecorder ? { recorder: this.usageRecorder } : void 0
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Send a streaming completion request using transformer chain
|
|
3426
|
+
*/
|
|
3427
|
+
async completeStreamWithTransformers(options, callbacks) {
|
|
3428
|
+
return completeStreamWithTransformers(
|
|
3429
|
+
options,
|
|
3430
|
+
callbacks,
|
|
3431
|
+
this.llmConfig,
|
|
3432
|
+
this.getProvider.bind(this),
|
|
3433
|
+
this.resolveApiKey.bind(this),
|
|
3434
|
+
this.completeStream.bind(this),
|
|
3435
|
+
this.logger,
|
|
3436
|
+
this.usageRecorder ? { recorder: this.usageRecorder } : void 0
|
|
3437
|
+
);
|
|
3438
|
+
}
|
|
3439
|
+
// --------------------------------------------------------------------------
|
|
3440
|
+
// Tool-based Completion
|
|
3441
|
+
// --------------------------------------------------------------------------
|
|
3442
|
+
/**
|
|
3443
|
+
* Stream completion with MCP tools support (direct API call)
|
|
3444
|
+
* Implements agentic loop: calls LLM -> executes tools -> calls LLM again until done
|
|
3445
|
+
*/
|
|
3446
|
+
async streamWithTools(options, callbacks, mcpService, builtinExecutor) {
|
|
3447
|
+
return streamWithTools(
|
|
3448
|
+
options,
|
|
3449
|
+
callbacks,
|
|
3450
|
+
mcpService,
|
|
3451
|
+
this.llmConfig,
|
|
3452
|
+
this.getProvider.bind(this),
|
|
3453
|
+
this.resolveApiKey.bind(this),
|
|
3454
|
+
this.logger,
|
|
3455
|
+
builtinExecutor
|
|
3456
|
+
);
|
|
3457
|
+
}
|
|
3458
|
+
};
|
|
3459
|
+
export {
|
|
3460
|
+
CompletionService
|
|
3461
|
+
};
|