@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.
@@ -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.aiGatewayModelCache = null;
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
- isAiGatewayModelCacheFresh() {
54
- return !!this.aiGatewayModelCache
55
- && Date.now() - this.aiGatewayModelCache.fetchedAt < VercelChatService.AI_GATEWAY_MODEL_CACHE_TTL_MS;
56
- }
57
- async getAiGatewayModelCache() {
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
- this.logger.warn(`Failed to fetch ai-gateway models: ${error}`);
90
- return null;
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
- const response = await this.sendAiGatewayRequest(path, 'POST', body, {
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
- const response = await this.sendAiGatewayRequest(path, 'POST', body, {
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
- mapChatCompletionToMessagesResponse(body, completion) {
222
- const choice = Array.isArray(completion?.choices) ? completion.choices[0] : undefined;
223
- const text = this.extractCompletionText(choice?.message?.content);
224
- const prompt = this.extractPromptFromMessagesBody(body);
225
- return {
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, messages, temperature, max_tokens } = request;
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(model)) {
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 provider = await this.getProvider(context);
325
- const coreMessages = messages.map((m) => ({
326
- role: m.role,
327
- content: m.content,
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?.totalTokens ?? 0;
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, messages, temperature, max_tokens } = request;
200
+ const { model } = request;
379
201
  const context = this.createStoreContext(auth);
380
- if (await this.shouldUseAiGateway(model)) {
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 provider = await this.getProvider(context);
391
- const coreMessages = messages.map((m) => ({
392
- role: m.role,
393
- content: m.content,
394
- }));
395
- return (0, ai_1.streamText)({
396
- model: provider.chat(model),
397
- messages: coreMessages,
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 this.shouldUseAiGateway(body?.model)) {
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 result = await this.forwardAiGatewayJson('/v1/responses', body, auth);
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 (!(0, provider_registry_1.supportsResponsesApi)(baseURL)) {
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 response = await fetchFn(url, {
432
- method: 'POST',
433
- headers: {
434
- 'Content-Type': 'application/json',
435
- 'Authorization': `Bearer ${apiKey}`,
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 response.json();
267
+ return result;
453
268
  }
454
269
  catch (error) {
455
- if (credentialId && !(error instanceof Error && error.message.startsWith('Provider error'))) {
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 this.shouldUseAiGateway(body?.model)) {
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 = this.buildChatCompletionsBodyFromMessages(body);
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 = this.mapChatCompletionToMessagesResponse(body, completion);
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 (!(0, provider_registry_1.supportsMessagesApi)(baseURL)) {
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 response = await fetchFn(url, {
493
- method: 'POST',
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 response.json();
328
+ return result;
516
329
  }
517
330
  catch (error) {
518
- if (credentialId && !(error instanceof Error && error.message.startsWith('Provider error'))) {
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 = this.extractPromptFromResponsesBody(body);
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 = this.extractPromptFromMessagesBody(body);
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
- const pushModels = (items) => {
671
- for (const model of items) {
672
- const modelId = this.toModelId(model);
673
- if (seenModelIds.has(modelId)) {
674
- continue;
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
- const aiGatewayCache = await this.getAiGatewayModelCache();
681
- if (aiGatewayCache) {
682
- pushModels(aiGatewayCache.items);
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.buildAiGatewayUrl('/v1/models')
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
- pushModels(data.data);
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