@undefineds.co/xpod 0.2.24 → 0.2.27
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/api/chatkit/pod-store.d.ts +2 -0
- package/dist/api/chatkit/pod-store.js +95 -0
- package/dist/api/chatkit/pod-store.js.map +1 -1
- package/dist/api/handlers/ChatHandler.d.ts +9 -5
- package/dist/api/handlers/ChatHandler.js +2 -4
- package/dist/api/handlers/ChatHandler.js.map +1 -1
- package/dist/api/service/VercelChatService.d.ts +5 -16
- package/dist/api/service/VercelChatService.js +109 -348
- package/dist/api/service/VercelChatService.js.map +1 -1
- package/dist/api/service/ai-gateway-transport.d.ts +23 -0
- package/dist/api/service/ai-gateway-transport.js +131 -0
- package/dist/api/service/ai-gateway-transport.js.map +1 -0
- package/dist/api/service/chat-protocol-adapters.d.ts +5 -0
- package/dist/api/service/chat-protocol-adapters.js +146 -0
- package/dist/api/service/chat-protocol-adapters.js.map +1 -0
- package/dist/api/service/chat-routing.d.ts +8 -0
- package/dist/api/service/chat-routing.js +16 -0
- package/dist/api/service/chat-routing.js.map +1 -0
- package/dist/api/service/provider-http-transport.d.ts +16 -0
- package/dist/api/service/provider-http-transport.js +52 -0
- package/dist/api/service/provider-http-transport.js.map +1 -0
- package/package.json +1 -1
|
@@ -9,6 +9,10 @@ const AuthContext_1 = require("../auth/AuthContext");
|
|
|
9
9
|
const types_1 = require("../../credential/schema/types");
|
|
10
10
|
const provider_registry_1 = require("./provider-registry");
|
|
11
11
|
const platform_ai_config_1 = require("./platform-ai-config");
|
|
12
|
+
const chat_protocol_adapters_1 = require("./chat-protocol-adapters");
|
|
13
|
+
const ai_gateway_transport_1 = require("./ai-gateway-transport");
|
|
14
|
+
const provider_http_transport_1 = require("./provider-http-transport");
|
|
15
|
+
const chat_routing_1 = require("./chat-routing");
|
|
12
16
|
// Create a proxy-aware fetch function
|
|
13
17
|
function createProxyFetch(proxyUrl) {
|
|
14
18
|
const agent = new undici_1.ProxyAgent(proxyUrl);
|
|
@@ -18,9 +22,13 @@ class VercelChatService {
|
|
|
18
22
|
constructor(store) {
|
|
19
23
|
this.store = store;
|
|
20
24
|
this.logger = (0, global_logger_factory_1.getLoggerFor)(this);
|
|
21
|
-
this.
|
|
22
|
-
this.aiGatewayModelCachePromise = null;
|
|
25
|
+
this.providerHttpTransport = new provider_http_transport_1.ProviderHttpTransport();
|
|
23
26
|
this.logger.info('Initializing VercelChatService with Pod-based config support');
|
|
27
|
+
this.aiGatewayTransport = new ai_gateway_transport_1.AiGatewayTransport({
|
|
28
|
+
getBaseUrl: () => this.getAiGatewayBaseUrl(),
|
|
29
|
+
getApiKey: () => this.getAiGatewayApiKey(),
|
|
30
|
+
getTimeoutMs: () => this.getAiGatewayTimeoutMs(),
|
|
31
|
+
});
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
34
|
* Set optional usage tracking dependencies (injected after construction)
|
|
@@ -47,194 +55,33 @@ class VercelChatService {
|
|
|
47
55
|
getAiGatewayApiKey() {
|
|
48
56
|
return (0, platform_ai_config_1.getAiGatewayApiKey)() ?? null;
|
|
49
57
|
}
|
|
58
|
+
async shouldUseAiGateway(model) {
|
|
59
|
+
return this.aiGatewayTransport.shouldHandleModel(model);
|
|
60
|
+
}
|
|
50
61
|
toModelId(model) {
|
|
51
62
|
return typeof model?.id === 'string' ? model.id : JSON.stringify(model);
|
|
52
63
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!this.getAiGatewayBaseUrl()) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
if (this.isAiGatewayModelCacheFresh()) {
|
|
62
|
-
return this.aiGatewayModelCache;
|
|
63
|
-
}
|
|
64
|
-
if (this.aiGatewayModelCachePromise) {
|
|
65
|
-
return this.aiGatewayModelCachePromise;
|
|
66
|
-
}
|
|
67
|
-
this.aiGatewayModelCachePromise = (async () => {
|
|
68
|
-
const response = await this.sendAiGatewayRequest('/v1/models', 'GET', undefined, {
|
|
69
|
-
'Accept': 'application/json',
|
|
70
|
-
});
|
|
71
|
-
const data = await response.json();
|
|
72
|
-
const items = Array.isArray(data.data) ? data.data : [];
|
|
73
|
-
const cache = {
|
|
74
|
-
fetchedAt: Date.now(),
|
|
75
|
-
items,
|
|
76
|
-
modelIds: new Set(items.map((item) => this.toModelId(item))),
|
|
77
|
-
};
|
|
78
|
-
this.aiGatewayModelCache = cache;
|
|
79
|
-
return cache;
|
|
80
|
-
})();
|
|
81
|
-
try {
|
|
82
|
-
return await this.aiGatewayModelCachePromise;
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
if (this.aiGatewayModelCache) {
|
|
86
|
-
this.logger.warn(`Failed to refresh ai-gateway models, using stale cache: ${error}`);
|
|
87
|
-
return this.aiGatewayModelCache;
|
|
64
|
+
pushModelsWithDedup(models, seenModelIds, items) {
|
|
65
|
+
for (const model of items) {
|
|
66
|
+
const modelId = this.toModelId(model);
|
|
67
|
+
if (seenModelIds.has(modelId)) {
|
|
68
|
+
continue;
|
|
88
69
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
finally {
|
|
93
|
-
this.aiGatewayModelCachePromise = null;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async shouldUseAiGateway(model) {
|
|
97
|
-
if (!model || !this.getAiGatewayBaseUrl()) {
|
|
98
|
-
return false;
|
|
70
|
+
seenModelIds.add(modelId);
|
|
71
|
+
models.push(model);
|
|
99
72
|
}
|
|
100
|
-
const cache = await this.getAiGatewayModelCache();
|
|
101
|
-
return cache?.modelIds.has(model) ?? false;
|
|
102
|
-
}
|
|
103
|
-
buildAiGatewayUrl(path) {
|
|
104
|
-
const baseUrl = this.getAiGatewayBaseUrl();
|
|
105
|
-
if (!baseUrl) {
|
|
106
|
-
throw new Error('DEFAULT_API_BASE is not configured');
|
|
107
|
-
}
|
|
108
|
-
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
109
|
-
if (baseUrl.endsWith('/v1') && normalizedPath.startsWith('/v1/')) {
|
|
110
|
-
return `${baseUrl}${normalizedPath.slice(3)}`;
|
|
111
|
-
}
|
|
112
|
-
return `${baseUrl}${normalizedPath}`;
|
|
113
|
-
}
|
|
114
|
-
createAiGatewayAbortSignal() {
|
|
115
|
-
const abortSignal = AbortSignal;
|
|
116
|
-
return typeof abortSignal.timeout === 'function'
|
|
117
|
-
? abortSignal.timeout(this.getAiGatewayTimeoutMs())
|
|
118
|
-
: undefined;
|
|
119
|
-
}
|
|
120
|
-
async sendAiGatewayRequest(path, method, body, headers) {
|
|
121
|
-
const apiKey = this.getAiGatewayApiKey();
|
|
122
|
-
if (!apiKey) {
|
|
123
|
-
throw new Error('DEFAULT_API_KEY is not configured');
|
|
124
|
-
}
|
|
125
|
-
const requestHeaders = new Headers(headers);
|
|
126
|
-
requestHeaders.set('Authorization', `Bearer ${apiKey}`);
|
|
127
|
-
if (body !== undefined && !requestHeaders.has('Content-Type')) {
|
|
128
|
-
requestHeaders.set('Content-Type', 'application/json');
|
|
129
|
-
}
|
|
130
|
-
const response = await fetch(this.buildAiGatewayUrl(path), {
|
|
131
|
-
method,
|
|
132
|
-
headers: requestHeaders,
|
|
133
|
-
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
134
|
-
signal: this.createAiGatewayAbortSignal(),
|
|
135
|
-
});
|
|
136
|
-
if (!response.ok) {
|
|
137
|
-
const errorText = await response.text().catch(() => '');
|
|
138
|
-
this.logger.warn(`Platform AI request failed: ${response.status} ${errorText}`);
|
|
139
|
-
const error = new Error(`Platform AI error: ${response.status} ${response.statusText}`);
|
|
140
|
-
error.status = response.status;
|
|
141
|
-
error.headers = response.headers;
|
|
142
|
-
error.body = errorText;
|
|
143
|
-
throw error;
|
|
144
|
-
}
|
|
145
|
-
return response;
|
|
146
73
|
}
|
|
147
74
|
async forwardAiGatewayJson(path, body, _auth) {
|
|
148
|
-
|
|
149
|
-
'Accept': 'application/json',
|
|
150
|
-
});
|
|
151
|
-
return response.json();
|
|
75
|
+
return this.aiGatewayTransport.sendJson(path, body);
|
|
152
76
|
}
|
|
153
77
|
async forwardAiGatewayStream(path, body, _auth) {
|
|
154
|
-
|
|
155
|
-
'Accept': 'text/event-stream',
|
|
156
|
-
});
|
|
157
|
-
return {
|
|
158
|
-
toTextStreamResponse: () => new Response(response.body, {
|
|
159
|
-
status: response.status,
|
|
160
|
-
statusText: response.statusText,
|
|
161
|
-
headers: new Headers(response.headers),
|
|
162
|
-
}),
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
extractCompletionText(content) {
|
|
166
|
-
if (typeof content === 'string') {
|
|
167
|
-
return content;
|
|
168
|
-
}
|
|
169
|
-
if (Array.isArray(content)) {
|
|
170
|
-
return content
|
|
171
|
-
.filter((item) => item && typeof item === 'object' && typeof item.text === 'string')
|
|
172
|
-
.map((item) => item.text)
|
|
173
|
-
.join('\n');
|
|
174
|
-
}
|
|
175
|
-
return content == null ? '' : String(content);
|
|
176
|
-
}
|
|
177
|
-
buildChatCompletionsBodyFromMessages(body) {
|
|
178
|
-
const messages = [];
|
|
179
|
-
if (body?.system) {
|
|
180
|
-
const systemText = this.extractCompletionText(body.system);
|
|
181
|
-
if (systemText) {
|
|
182
|
-
messages.push({ role: 'system', content: systemText });
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (Array.isArray(body?.messages)) {
|
|
186
|
-
for (const message of body.messages) {
|
|
187
|
-
if (!message?.role || message?.content == null) {
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
messages.push({
|
|
191
|
-
role: String(message.role),
|
|
192
|
-
content: this.extractCompletionText(message.content),
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (messages.length === 0 && body?.content != null) {
|
|
197
|
-
messages.push({
|
|
198
|
-
role: 'user',
|
|
199
|
-
content: this.extractCompletionText(body.content),
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
model: body?.model,
|
|
204
|
-
messages,
|
|
205
|
-
...(body?.temperature != null ? { temperature: body.temperature } : {}),
|
|
206
|
-
...(body?.max_tokens != null ? { max_tokens: body.max_tokens } : {}),
|
|
207
|
-
...(Array.isArray(body?.stop_sequences) && body.stop_sequences.length > 0
|
|
208
|
-
? { stop: body.stop_sequences }
|
|
209
|
-
: {}),
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
mapChatCompletionFinishReason(reason) {
|
|
213
|
-
if (reason === 'length') {
|
|
214
|
-
return 'max_tokens';
|
|
215
|
-
}
|
|
216
|
-
if (reason === 'content_filter') {
|
|
217
|
-
return 'stop_sequence';
|
|
218
|
-
}
|
|
219
|
-
return 'end_turn';
|
|
78
|
+
return this.aiGatewayTransport.sendStream(path, body);
|
|
220
79
|
}
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
id: completion?.id ?? `msg_${Date.now()}`,
|
|
227
|
-
type: 'message',
|
|
228
|
-
role: 'assistant',
|
|
229
|
-
model: completion?.model ?? body?.model,
|
|
230
|
-
content: [{ type: 'text', text }],
|
|
231
|
-
stop_reason: this.mapChatCompletionFinishReason(choice?.finish_reason),
|
|
232
|
-
stop_sequence: null,
|
|
233
|
-
usage: {
|
|
234
|
-
input_tokens: completion?.usage?.prompt_tokens ?? prompt.length,
|
|
235
|
-
output_tokens: completion?.usage?.completion_tokens ?? text.length,
|
|
236
|
-
},
|
|
237
|
-
};
|
|
80
|
+
getProviderChatCompletionsUrl(baseURL) {
|
|
81
|
+
const cleanBaseUrl = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
|
|
82
|
+
return cleanBaseUrl.endsWith('/chat/completions')
|
|
83
|
+
? cleanBaseUrl
|
|
84
|
+
: `${cleanBaseUrl}/chat/completions`;
|
|
238
85
|
}
|
|
239
86
|
extractTotalTokens(usage) {
|
|
240
87
|
if (!usage || typeof usage !== 'object') {
|
|
@@ -302,13 +149,13 @@ class VercelChatService {
|
|
|
302
149
|
return (0, openai_1.createOpenAI)(options);
|
|
303
150
|
}
|
|
304
151
|
async complete(request, auth) {
|
|
305
|
-
const { model
|
|
152
|
+
const { model } = request;
|
|
306
153
|
const context = this.createStoreContext(auth);
|
|
307
154
|
const accountId = (0, AuthContext_1.getAccountId)(auth);
|
|
308
155
|
if (accountId) {
|
|
309
156
|
await this.checkTokenQuota(accountId);
|
|
310
157
|
}
|
|
311
|
-
if (await this.shouldUseAiGateway(
|
|
158
|
+
if (await (0, chat_routing_1.resolveChatExecutionRoute)({ model, shouldUseAiGateway: this.shouldUseAiGateway.bind(this) }) === 'ai-gateway') {
|
|
312
159
|
this.logger.info(`Forwarding chat completion for model ${model} to ai-gateway`);
|
|
313
160
|
const result = await this.forwardAiGatewayJson('/v1/chat/completions', request, auth);
|
|
314
161
|
this.recordForwardedUsage(accountId, String(context.userId), result);
|
|
@@ -321,16 +168,11 @@ class VercelChatService {
|
|
|
321
168
|
throw err;
|
|
322
169
|
}
|
|
323
170
|
try {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const result = await (0, ai_1.generateText)({
|
|
330
|
-
model: provider.chat(model),
|
|
331
|
-
messages: coreMessages,
|
|
332
|
-
temperature,
|
|
333
|
-
maxTokens: max_tokens,
|
|
171
|
+
const result = await this.providerHttpTransport.postJson({
|
|
172
|
+
url: this.getProviderChatCompletionsUrl(config.baseURL),
|
|
173
|
+
apiKey: config.apiKey,
|
|
174
|
+
proxy: config.proxy,
|
|
175
|
+
body: request,
|
|
334
176
|
});
|
|
335
177
|
// Record successful API call
|
|
336
178
|
if (config?.credentialId) {
|
|
@@ -339,31 +181,11 @@ class VercelChatService {
|
|
|
339
181
|
});
|
|
340
182
|
}
|
|
341
183
|
// Record token usage
|
|
342
|
-
const totalTokens = result.usage
|
|
184
|
+
const totalTokens = this.extractTotalTokens(result.usage);
|
|
343
185
|
if (accountId && totalTokens > 0) {
|
|
344
186
|
this.recordTokenUsage(accountId, String(context.userId), totalTokens);
|
|
345
187
|
}
|
|
346
|
-
return
|
|
347
|
-
id: `chatcmpl-${Date.now()}`,
|
|
348
|
-
object: 'chat.completion',
|
|
349
|
-
created: Math.floor(Date.now() / 1000),
|
|
350
|
-
model,
|
|
351
|
-
choices: [
|
|
352
|
-
{
|
|
353
|
-
index: 0,
|
|
354
|
-
message: {
|
|
355
|
-
role: 'assistant',
|
|
356
|
-
content: result.text,
|
|
357
|
-
},
|
|
358
|
-
finish_reason: this.mapFinishReason(result.finishReason),
|
|
359
|
-
},
|
|
360
|
-
],
|
|
361
|
-
usage: {
|
|
362
|
-
prompt_tokens: result.usage.promptTokens,
|
|
363
|
-
completion_tokens: result.usage.completionTokens,
|
|
364
|
-
total_tokens: result.usage.totalTokens,
|
|
365
|
-
},
|
|
366
|
-
};
|
|
188
|
+
return result;
|
|
367
189
|
}
|
|
368
190
|
catch (error) {
|
|
369
191
|
this.logger.error(`AI completion failed: ${error}`);
|
|
@@ -375,9 +197,9 @@ class VercelChatService {
|
|
|
375
197
|
}
|
|
376
198
|
}
|
|
377
199
|
async stream(request, auth) {
|
|
378
|
-
const { model
|
|
200
|
+
const { model } = request;
|
|
379
201
|
const context = this.createStoreContext(auth);
|
|
380
|
-
if (await this.shouldUseAiGateway(
|
|
202
|
+
if (await (0, chat_routing_1.resolveChatExecutionRoute)({ model, shouldUseAiGateway: this.shouldUseAiGateway.bind(this) }) === 'ai-gateway') {
|
|
381
203
|
this.logger.info(`Forwarding chat stream for model ${model} to ai-gateway`);
|
|
382
204
|
return this.forwardAiGatewayStream('/v1/chat/completions', request, auth);
|
|
383
205
|
}
|
|
@@ -387,25 +209,31 @@ class VercelChatService {
|
|
|
387
209
|
err.code = 'model_not_configured';
|
|
388
210
|
throw err;
|
|
389
211
|
}
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
temperature,
|
|
399
|
-
maxTokens: max_tokens,
|
|
212
|
+
const response = await this.providerHttpTransport.postStream({
|
|
213
|
+
url: this.getProviderChatCompletionsUrl(config.baseURL),
|
|
214
|
+
apiKey: config.apiKey,
|
|
215
|
+
proxy: config.proxy,
|
|
216
|
+
body: request,
|
|
217
|
+
headers: {
|
|
218
|
+
Accept: 'text/event-stream',
|
|
219
|
+
},
|
|
400
220
|
});
|
|
221
|
+
return {
|
|
222
|
+
toTextStreamResponse: () => new Response(response.body, {
|
|
223
|
+
status: response.status,
|
|
224
|
+
statusText: response.statusText,
|
|
225
|
+
headers: new Headers(response.headers),
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
401
228
|
}
|
|
402
229
|
async responses(body, auth) {
|
|
403
230
|
const context = this.createStoreContext(auth);
|
|
404
231
|
const displayName = (0, AuthContext_1.getDisplayName)(auth) || context.userId;
|
|
405
232
|
const accountId = (0, AuthContext_1.getAccountId)(auth);
|
|
406
|
-
if (await
|
|
233
|
+
if (await (0, chat_routing_1.resolveChatExecutionRoute)({ model: body?.model, shouldUseAiGateway: this.shouldUseAiGateway.bind(this) }) === 'ai-gateway') {
|
|
407
234
|
this.logger.info(`Forwarding responses request for model ${body?.model} to ai-gateway for ${displayName} (acc: ${accountId})`);
|
|
408
|
-
const
|
|
235
|
+
const sanitizedBody = (0, chat_protocol_adapters_1.sanitizeAiGatewayResponsesBody)(body);
|
|
236
|
+
const result = await this.forwardAiGatewayJson('/v1/responses', sanitizedBody, auth);
|
|
409
237
|
this.recordForwardedUsage(accountId, String(context.userId), result);
|
|
410
238
|
return result;
|
|
411
239
|
}
|
|
@@ -417,7 +245,7 @@ class VercelChatService {
|
|
|
417
245
|
}
|
|
418
246
|
const { baseURL } = providerConfig;
|
|
419
247
|
// Only OpenAI natively supports /v1/responses; all others go through Chat Completions
|
|
420
|
-
if (
|
|
248
|
+
if ((0, chat_routing_1.resolveResponsesProviderRoute)(baseURL) === 'chat-fallback') {
|
|
421
249
|
this.logger.info(`Provider ${baseURL} does not support Responses API, converting to Chat Completions for ${displayName} (acc: ${accountId})`);
|
|
422
250
|
return this.responsesViaCompletions(body, context, providerConfig);
|
|
423
251
|
}
|
|
@@ -426,33 +254,29 @@ class VercelChatService {
|
|
|
426
254
|
const cleanBaseUrl = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
|
|
427
255
|
const url = `${cleanBaseUrl}/responses`;
|
|
428
256
|
this.logger.info(`Proxying responses request to ${url} for ${displayName} (acc: ${accountId}), proxy: ${proxy || 'none'}`);
|
|
429
|
-
const fetchFn = proxy ? createProxyFetch(proxy) : fetch;
|
|
430
257
|
try {
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
},
|
|
437
|
-
body: JSON.stringify(body),
|
|
258
|
+
const result = await this.providerHttpTransport.postJson({
|
|
259
|
+
url,
|
|
260
|
+
apiKey,
|
|
261
|
+
proxy,
|
|
262
|
+
body,
|
|
438
263
|
});
|
|
439
|
-
if (!response.ok) {
|
|
440
|
-
const errorText = await response.text();
|
|
441
|
-
this.logger.error(`Responses API failed: ${response.status} ${errorText}`);
|
|
442
|
-
// Handle error and update credential status
|
|
443
|
-
if (credentialId) {
|
|
444
|
-
await this.handleApiError({ status: response.status, headers: response.headers }, context, credentialId);
|
|
445
|
-
}
|
|
446
|
-
throw new Error(`Provider error: ${response.statusText}`);
|
|
447
|
-
}
|
|
448
|
-
// Record successful API call
|
|
449
264
|
if (credentialId) {
|
|
450
265
|
this.store.recordCredentialSuccess(context, credentialId).catch(() => { });
|
|
451
266
|
}
|
|
452
|
-
return
|
|
267
|
+
return result;
|
|
453
268
|
}
|
|
454
269
|
catch (error) {
|
|
455
|
-
|
|
270
|
+
const status = error?.status;
|
|
271
|
+
const headers = error?.headers;
|
|
272
|
+
const bodyText = error?.body;
|
|
273
|
+
if (typeof status === 'number') {
|
|
274
|
+
this.logger.error(`Responses API failed: ${status} ${bodyText ?? ''}`);
|
|
275
|
+
if (credentialId) {
|
|
276
|
+
await this.handleApiError({ status, headers }, context, credentialId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else if (credentialId) {
|
|
456
280
|
await this.handleApiError(error, context, credentialId);
|
|
457
281
|
}
|
|
458
282
|
throw error;
|
|
@@ -462,11 +286,11 @@ class VercelChatService {
|
|
|
462
286
|
const context = this.createStoreContext(auth);
|
|
463
287
|
const displayName = (0, AuthContext_1.getDisplayName)(auth) || context.userId;
|
|
464
288
|
const accountId = (0, AuthContext_1.getAccountId)(auth);
|
|
465
|
-
if (await
|
|
289
|
+
if (await (0, chat_routing_1.resolveChatExecutionRoute)({ model: body?.model, shouldUseAiGateway: this.shouldUseAiGateway.bind(this) }) === 'ai-gateway') {
|
|
466
290
|
this.logger.info(`Forwarding messages request for model ${body?.model} to ai-gateway for ${displayName} (acc: ${accountId})`);
|
|
467
|
-
const completionBody =
|
|
291
|
+
const completionBody = (0, chat_protocol_adapters_1.buildChatCompletionsBodyFromMessages)(body);
|
|
468
292
|
const completion = await this.forwardAiGatewayJson('/v1/chat/completions', completionBody, auth);
|
|
469
|
-
const result =
|
|
293
|
+
const result = (0, chat_protocol_adapters_1.mapChatCompletionToMessagesResponse)(body, completion);
|
|
470
294
|
this.recordForwardedUsage(accountId, String(context.userId), result);
|
|
471
295
|
return result;
|
|
472
296
|
}
|
|
@@ -478,7 +302,7 @@ class VercelChatService {
|
|
|
478
302
|
}
|
|
479
303
|
const { baseURL } = providerConfig;
|
|
480
304
|
// Only Anthropic natively supports /v1/messages; all others go through Chat Completions
|
|
481
|
-
if (
|
|
305
|
+
if ((0, chat_routing_1.resolveMessagesProviderRoute)(baseURL) === 'chat-fallback') {
|
|
482
306
|
this.logger.info(`Provider ${baseURL} does not support Messages API, converting to Chat Completions for ${displayName} (acc: ${accountId})`);
|
|
483
307
|
return this.messagesViaCompletions(body, context, providerConfig);
|
|
484
308
|
}
|
|
@@ -487,42 +311,40 @@ class VercelChatService {
|
|
|
487
311
|
const cleanBaseUrl = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
|
|
488
312
|
const url = `${cleanBaseUrl}/messages`;
|
|
489
313
|
this.logger.info(`Proxying messages request to ${url} for ${displayName} (acc: ${accountId}), proxy: ${proxy || 'none'}`);
|
|
490
|
-
const fetchFn = proxy ? createProxyFetch(proxy) : fetch;
|
|
491
314
|
try {
|
|
492
|
-
const
|
|
493
|
-
|
|
315
|
+
const result = await this.providerHttpTransport.postJson({
|
|
316
|
+
url,
|
|
317
|
+
apiKey,
|
|
318
|
+
proxy,
|
|
319
|
+
body,
|
|
494
320
|
headers: {
|
|
495
|
-
'Content-Type': 'application/json',
|
|
496
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
497
321
|
'x-api-key': apiKey,
|
|
498
322
|
'anthropic-version': '2023-06-01',
|
|
499
323
|
},
|
|
500
|
-
body: JSON.stringify(body),
|
|
501
324
|
});
|
|
502
|
-
if (!response.ok) {
|
|
503
|
-
const errorText = await response.text();
|
|
504
|
-
this.logger.error(`Messages API failed: ${response.status} ${errorText}`);
|
|
505
|
-
// Handle error and update credential status
|
|
506
|
-
if (credentialId) {
|
|
507
|
-
await this.handleApiError({ status: response.status, headers: response.headers }, context, credentialId);
|
|
508
|
-
}
|
|
509
|
-
throw new Error(`Provider error: ${response.statusText}`);
|
|
510
|
-
}
|
|
511
|
-
// Record successful API call
|
|
512
325
|
if (credentialId) {
|
|
513
326
|
this.store.recordCredentialSuccess(context, credentialId).catch(() => { });
|
|
514
327
|
}
|
|
515
|
-
return
|
|
328
|
+
return result;
|
|
516
329
|
}
|
|
517
330
|
catch (error) {
|
|
518
|
-
|
|
331
|
+
const status = error?.status;
|
|
332
|
+
const headers = error?.headers;
|
|
333
|
+
const bodyText = error?.body;
|
|
334
|
+
if (typeof status === 'number') {
|
|
335
|
+
this.logger.error(`Messages API failed: ${status} ${bodyText ?? ''}`);
|
|
336
|
+
if (credentialId) {
|
|
337
|
+
await this.handleApiError({ status, headers }, context, credentialId);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else if (credentialId) {
|
|
519
341
|
await this.handleApiError(error, context, credentialId);
|
|
520
342
|
}
|
|
521
343
|
throw error;
|
|
522
344
|
}
|
|
523
345
|
}
|
|
524
346
|
async responsesViaCompletions(body, context, providerConfig) {
|
|
525
|
-
const prompt =
|
|
347
|
+
const prompt = (0, chat_protocol_adapters_1.extractPromptFromResponsesBody)(body);
|
|
526
348
|
const model = body?.model || (0, platform_ai_config_1.getPlatformDefaultModel)();
|
|
527
349
|
const provider = await this.getProvider(context);
|
|
528
350
|
const result = await (0, ai_1.generateText)({
|
|
@@ -555,7 +377,7 @@ class VercelChatService {
|
|
|
555
377
|
};
|
|
556
378
|
}
|
|
557
379
|
async messagesViaCompletions(body, context, providerConfig) {
|
|
558
|
-
const prompt =
|
|
380
|
+
const prompt = (0, chat_protocol_adapters_1.extractPromptFromMessagesBody)(body);
|
|
559
381
|
const model = body?.model || (0, platform_ai_config_1.getPlatformDefaultModel)();
|
|
560
382
|
const coreMessages = [];
|
|
561
383
|
if (body?.system) {
|
|
@@ -608,85 +430,29 @@ class VercelChatService {
|
|
|
608
430
|
},
|
|
609
431
|
};
|
|
610
432
|
}
|
|
611
|
-
extractPromptFromResponsesBody(body) {
|
|
612
|
-
if (!body || typeof body !== 'object') {
|
|
613
|
-
return '';
|
|
614
|
-
}
|
|
615
|
-
if (typeof body.input === 'string') {
|
|
616
|
-
return body.input;
|
|
617
|
-
}
|
|
618
|
-
if (typeof body.prompt === 'string') {
|
|
619
|
-
return body.prompt;
|
|
620
|
-
}
|
|
621
|
-
if (Array.isArray(body.input)) {
|
|
622
|
-
const textParts = [];
|
|
623
|
-
for (const item of body.input) {
|
|
624
|
-
if (item && typeof item === 'object') {
|
|
625
|
-
const candidate = item.content;
|
|
626
|
-
if (typeof candidate === 'string') {
|
|
627
|
-
textParts.push(candidate);
|
|
628
|
-
}
|
|
629
|
-
else if (Array.isArray(candidate)) {
|
|
630
|
-
for (const part of candidate) {
|
|
631
|
-
if (part && typeof part === 'object' && typeof part.text === 'string') {
|
|
632
|
-
textParts.push(part.text);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
if (textParts.length > 0) {
|
|
639
|
-
return textParts.join('\n');
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
return '';
|
|
643
|
-
}
|
|
644
|
-
extractPromptFromMessagesBody(body) {
|
|
645
|
-
if (!body || typeof body !== 'object') {
|
|
646
|
-
return '';
|
|
647
|
-
}
|
|
648
|
-
if (typeof body.content === 'string') {
|
|
649
|
-
return body.content;
|
|
650
|
-
}
|
|
651
|
-
if (Array.isArray(body.messages)) {
|
|
652
|
-
const lastUser = [...body.messages].reverse().find((item) => item?.role === 'user');
|
|
653
|
-
if (lastUser) {
|
|
654
|
-
if (typeof lastUser.content === 'string') {
|
|
655
|
-
return lastUser.content;
|
|
656
|
-
}
|
|
657
|
-
if (Array.isArray(lastUser.content)) {
|
|
658
|
-
return lastUser.content
|
|
659
|
-
.filter((part) => part && typeof part === 'object' && typeof part.text === 'string')
|
|
660
|
-
.map((part) => part.text)
|
|
661
|
-
.join('\n');
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
return '';
|
|
666
|
-
}
|
|
667
433
|
async listModels(_auth) {
|
|
668
434
|
const models = [];
|
|
669
435
|
const seenModelIds = new Set();
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
seenModelIds.add(modelId);
|
|
677
|
-
models.push(model);
|
|
436
|
+
if (_auth) {
|
|
437
|
+
try {
|
|
438
|
+
const context = this.createStoreContext(_auth);
|
|
439
|
+
const userModels = await this.store.listAvailableModels(context);
|
|
440
|
+
this.pushModelsWithDedup(models, seenModelIds, userModels);
|
|
678
441
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
442
|
+
catch (error) {
|
|
443
|
+
this.logger.warn(`Failed to load user Pod models: ${error}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const aiGatewayModels = await this.aiGatewayTransport.listModels();
|
|
447
|
+
if (aiGatewayModels) {
|
|
448
|
+
this.pushModelsWithDedup(models, seenModelIds, aiGatewayModels);
|
|
683
449
|
}
|
|
684
450
|
// 平台 Provider 模型(从 DEFAULT_API_BASE 获取)
|
|
685
451
|
const platformBase = (0, platform_ai_config_1.getPlatformApiBaseUrl)();
|
|
686
452
|
const platformKey = (0, platform_ai_config_1.getPlatformApiKey)();
|
|
687
453
|
const aiGatewayBase = this.getAiGatewayBaseUrl();
|
|
688
454
|
const normalizedAiGatewayModelsUrl = aiGatewayBase
|
|
689
|
-
? this.
|
|
455
|
+
? this.aiGatewayTransport.buildUrl('/v1/models')
|
|
690
456
|
: undefined;
|
|
691
457
|
const normalizedPlatformModelsUrl = platformBase
|
|
692
458
|
? `${platformBase.replace(/\/$/, '')}/models`
|
|
@@ -702,7 +468,7 @@ class VercelChatService {
|
|
|
702
468
|
if (resp.ok) {
|
|
703
469
|
const data = await resp.json();
|
|
704
470
|
if (Array.isArray(data.data)) {
|
|
705
|
-
|
|
471
|
+
this.pushModelsWithDedup(models, seenModelIds, data.data);
|
|
706
472
|
}
|
|
707
473
|
}
|
|
708
474
|
else {
|
|
@@ -713,12 +479,8 @@ class VercelChatService {
|
|
|
713
479
|
this.logger.warn(`Failed to fetch platform models: ${error}`);
|
|
714
480
|
}
|
|
715
481
|
}
|
|
716
|
-
// TODO: 合并用户 Pod Providers 的模型
|
|
717
482
|
return models;
|
|
718
483
|
}
|
|
719
|
-
mapFinishReason(reason) {
|
|
720
|
-
return reason;
|
|
721
|
-
}
|
|
722
484
|
/**
|
|
723
485
|
* Handle API errors and update credential status accordingly
|
|
724
486
|
*/
|
|
@@ -815,5 +577,4 @@ class VercelChatService {
|
|
|
815
577
|
}
|
|
816
578
|
}
|
|
817
579
|
exports.VercelChatService = VercelChatService;
|
|
818
|
-
VercelChatService.AI_GATEWAY_MODEL_CACHE_TTL_MS = 30_000;
|
|
819
580
|
//# sourceMappingURL=VercelChatService.js.map
|