@vybestack/llxprt-code-core 0.6.1-nightly.251202.1e208436b → 0.6.1-nightly.251204.b119e390d

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.
Files changed (33) hide show
  1. package/dist/src/auth/precedence.js +9 -10
  2. package/dist/src/auth/precedence.js.map +1 -1
  3. package/dist/src/index.d.ts +1 -0
  4. package/dist/src/index.js +1 -0
  5. package/dist/src/index.js.map +1 -1
  6. package/dist/src/providers/BaseProvider.d.ts +3 -0
  7. package/dist/src/providers/BaseProvider.js +11 -0
  8. package/dist/src/providers/BaseProvider.js.map +1 -1
  9. package/dist/src/providers/IProvider.d.ts +3 -0
  10. package/dist/src/providers/ProviderManager.js +6 -0
  11. package/dist/src/providers/ProviderManager.js.map +1 -1
  12. package/dist/src/providers/openai-vercel/OpenAIVercelProvider.d.ts +130 -0
  13. package/dist/src/providers/openai-vercel/OpenAIVercelProvider.js +943 -0
  14. package/dist/src/providers/openai-vercel/OpenAIVercelProvider.js.map +1 -0
  15. package/dist/src/providers/openai-vercel/errors.d.ts +46 -0
  16. package/dist/src/providers/openai-vercel/errors.js +137 -0
  17. package/dist/src/providers/openai-vercel/errors.js.map +1 -0
  18. package/dist/src/providers/openai-vercel/index.d.ts +22 -0
  19. package/dist/src/providers/openai-vercel/index.js +23 -0
  20. package/dist/src/providers/openai-vercel/index.js.map +1 -0
  21. package/dist/src/providers/openai-vercel/messageConversion.d.ts +33 -0
  22. package/dist/src/providers/openai-vercel/messageConversion.js +394 -0
  23. package/dist/src/providers/openai-vercel/messageConversion.js.map +1 -0
  24. package/dist/src/providers/openai-vercel/toolIdUtils.d.ts +33 -0
  25. package/dist/src/providers/openai-vercel/toolIdUtils.js +117 -0
  26. package/dist/src/providers/openai-vercel/toolIdUtils.js.map +1 -0
  27. package/dist/src/utils/filesearch/ignore.js +3 -2
  28. package/dist/src/utils/filesearch/ignore.js.map +1 -1
  29. package/dist/src/utils/gitIgnoreParser.js +2 -1
  30. package/dist/src/utils/gitIgnoreParser.js.map +1 -1
  31. package/dist/src/utils/schemaValidator.js +41 -6
  32. package/dist/src/utils/schemaValidator.js.map +1 -1
  33. package/package.json +3 -1
@@ -0,0 +1,943 @@
1
+ /**
2
+ * Copyright 2025 Vybestack LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * @plan PLAN-20250218-STATELESSPROVIDER.P04
18
+ * @requirement REQ-SP-001
19
+ *
20
+ * OpenAI provider implemented on top of Vercel AI SDK v5, using the
21
+ * OpenAI chat completions API via @ai-sdk/openai + ai.
22
+ */
23
+ import crypto from 'node:crypto';
24
+ import * as Ai from 'ai';
25
+ import { createOpenAI } from '@ai-sdk/openai';
26
+ import { BaseProvider, } from '../BaseProvider.js';
27
+ import { DebugLogger } from '../../debug/index.js';
28
+ import { ToolFormatter } from '../../tools/ToolFormatter.js';
29
+ import { processToolParameters } from '../../tools/doubleEscapeUtils.js';
30
+ import { getCoreSystemPromptAsync } from '../../core/prompts.js';
31
+ import { resolveUserMemory } from '../utils/userMemory.js';
32
+ import { convertToVercelMessages } from './messageConversion.js';
33
+ import { resolveRuntimeAuthToken } from '../utils/authToken.js';
34
+ import { filterOpenAIRequestParams } from '../openai/openaiRequestParams.js';
35
+ import { isLocalEndpoint } from '../utils/localEndpoint.js';
36
+ import { AuthenticationError, wrapError } from './errors.js';
37
+ const streamText = Ai.streamText;
38
+ const generateText = Ai.generateText;
39
+ /**
40
+ * Vercel OpenAI-based provider using AI SDK v5.
41
+ *
42
+ * NOTE:
43
+ * - No dependency on the official `openai` SDK.
44
+ * - Uses `openai.chat(modelId)` to talk to the Chat Completions API.
45
+ * - Tools are configured via AI SDK `tool()` with JSON schema input.
46
+ */
47
+ export class OpenAIVercelProvider extends BaseProvider {
48
+ getLogger() {
49
+ return new DebugLogger('llxprt:provider:openaivercel');
50
+ }
51
+ /**
52
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
53
+ * @requirement:REQ-SP4-003
54
+ * Constructor reduced to minimal initialization - no state captured.
55
+ */
56
+ constructor(apiKey, baseURL, config, oauthManager) {
57
+ // Normalize empty string to undefined for proper precedence handling
58
+ const normalizedApiKey = apiKey && apiKey.trim() !== '' ? apiKey : undefined;
59
+ super({
60
+ name: 'openaivercel',
61
+ apiKey: normalizedApiKey,
62
+ baseURL,
63
+ envKeyNames: ['OPENAI_API_KEY'],
64
+ // AI SDK-based provider does not use OAuth directly here.
65
+ isOAuthEnabled: false,
66
+ oauthProvider: undefined,
67
+ oauthManager,
68
+ }, config);
69
+ }
70
+ supportsOAuth() {
71
+ return false;
72
+ }
73
+ /**
74
+ * Create an OpenAI provider instance for this call using AI SDK v5.
75
+ *
76
+ * Uses the resolved runtime auth token and baseURL, and still allows
77
+ * local endpoints without authentication (for Ollama-style servers).
78
+ */
79
+ async createOpenAIClient(options) {
80
+ const authToken = (await resolveRuntimeAuthToken(options.resolved.authToken)) ?? '';
81
+ const baseURL = options.resolved.baseURL ?? this.baseProviderConfig.baseURL;
82
+ // Allow local endpoints without authentication
83
+ if (!authToken && !isLocalEndpoint(baseURL)) {
84
+ throw new AuthenticationError(`Auth token unavailable for runtimeId=${options.runtime?.runtimeId} (REQ-SP4-003).`, this.name);
85
+ }
86
+ const headers = this.getCustomHeaders();
87
+ return createOpenAI({
88
+ apiKey: authToken || undefined,
89
+ baseURL: baseURL || undefined,
90
+ headers: headers || undefined,
91
+ });
92
+ }
93
+ /**
94
+ * Extract model parameters from normalized options instead of settings service.
95
+ * This mirrors OpenAIProvider but feeds AI SDK call options instead.
96
+ */
97
+ extractModelParamsFromOptions(options) {
98
+ const providerSettings = options.settings?.getProviderSettings(this.name) ?? {};
99
+ const configEphemerals = options.invocation?.ephemerals ?? {};
100
+ const filteredProviderParams = filterOpenAIRequestParams(providerSettings);
101
+ const filteredEphemeralParams = filterOpenAIRequestParams(configEphemerals);
102
+ if (!filteredProviderParams && !filteredEphemeralParams) {
103
+ return undefined;
104
+ }
105
+ return {
106
+ ...(filteredProviderParams ?? {}),
107
+ ...(filteredEphemeralParams ?? {}),
108
+ };
109
+ }
110
+ /**
111
+ * Tool formatter instances cannot be shared between stateless calls,
112
+ * so construct a fresh one for every invocation.
113
+ */
114
+ createToolFormatter() {
115
+ return new ToolFormatter();
116
+ }
117
+ getAiJsonSchema() {
118
+ try {
119
+ const candidate = Ai.jsonSchema;
120
+ return typeof candidate === 'function'
121
+ ? candidate
122
+ : undefined;
123
+ }
124
+ catch {
125
+ return undefined;
126
+ }
127
+ }
128
+ getAiTool() {
129
+ try {
130
+ const candidate = Ai.tool;
131
+ return typeof candidate === 'function'
132
+ ? candidate
133
+ : undefined;
134
+ }
135
+ catch {
136
+ return undefined;
137
+ }
138
+ }
139
+ /**
140
+ * Normalize tool IDs from various formats to OpenAI-style format.
141
+ * Kept for compatibility with existing history/tool logic.
142
+ */
143
+ normalizeToOpenAIToolId(id) {
144
+ const sanitize = (value) => value.replace(/[^a-zA-Z0-9_]/g, '') ||
145
+ 'call_' + crypto.randomUUID().replace(/-/g, '');
146
+ // If already in OpenAI format, return as-is
147
+ if (id.startsWith('call_')) {
148
+ return sanitize(id);
149
+ }
150
+ // For history format, extract the UUID and add OpenAI prefix
151
+ if (id.startsWith('hist_tool_')) {
152
+ const uuid = id.substring('hist_tool_'.length);
153
+ return sanitize('call_' + uuid);
154
+ }
155
+ // For Anthropic format, extract the UUID and add OpenAI prefix
156
+ if (id.startsWith('toolu_')) {
157
+ const uuid = id.substring('toolu_'.length);
158
+ return sanitize('call_' + uuid);
159
+ }
160
+ // Unknown format - assume it's a raw UUID
161
+ return sanitize('call_' + id);
162
+ }
163
+ /**
164
+ * Normalize tool IDs from OpenAI-style format to history format.
165
+ */
166
+ normalizeToHistoryToolId(id) {
167
+ // If already in history format, return as-is
168
+ if (id.startsWith('hist_tool_')) {
169
+ return id;
170
+ }
171
+ // For OpenAI format, extract the UUID and add history prefix
172
+ if (id.startsWith('call_')) {
173
+ const uuid = id.substring('call_'.length);
174
+ return 'hist_tool_' + uuid;
175
+ }
176
+ // For Anthropic format, extract the UUID and add history prefix
177
+ if (id.startsWith('toolu_')) {
178
+ const uuid = id.substring('toolu_'.length);
179
+ return 'hist_tool_' + uuid;
180
+ }
181
+ // Unknown format - assume it's a raw UUID
182
+ return 'hist_tool_' + id;
183
+ }
184
+ /**
185
+ * Convert internal history IContent[] to AI SDK ModelMessage[].
186
+ *
187
+ * This implementation uses textual tool replay for past tool calls/results.
188
+ * New tool calls in the current response still use structured ToolCallBlocks.
189
+ */
190
+ convertToModelMessages(contents) {
191
+ return convertToVercelMessages(contents);
192
+ }
193
+ /**
194
+ * Build an AI SDK ToolSet from already-normalized OpenAI-style tool definitions.
195
+ *
196
+ * Input is the same array produced by ToolFormatter.convertGeminiToFormat(…, 'openai'|'qwen').
197
+ */
198
+ buildVercelTools(formattedTools) {
199
+ if (!formattedTools || formattedTools.length === 0) {
200
+ return undefined;
201
+ }
202
+ const jsonSchemaFn = this.getAiJsonSchema() ??
203
+ ((schema) => schema);
204
+ const toolFn = this.getAiTool() ??
205
+ ((config) => config);
206
+ const toolsRecord = {};
207
+ for (const t of formattedTools) {
208
+ if (!t || t.type !== 'function')
209
+ continue;
210
+ const fn = t.function;
211
+ if (!fn?.name)
212
+ continue;
213
+ if (toolsRecord[fn.name])
214
+ continue;
215
+ const inputSchema = fn.parameters
216
+ ? jsonSchemaFn(fn.parameters)
217
+ : jsonSchemaFn({
218
+ type: 'object',
219
+ properties: {},
220
+ additionalProperties: false,
221
+ });
222
+ toolsRecord[fn.name] = toolFn({
223
+ description: fn.description,
224
+ inputSchema,
225
+ // No execute() – we only surface tool calls back to the caller,
226
+ // execution is handled by the existing external tool pipeline.
227
+ });
228
+ }
229
+ return Object.keys(toolsRecord).length > 0 ? toolsRecord : undefined;
230
+ }
231
+ mapUsageToMetadata(usage) {
232
+ if (!usage)
233
+ return undefined;
234
+ const promptTokens = usage.inputTokens ??
235
+ usage.promptTokens ??
236
+ 0;
237
+ const completionTokens = usage.outputTokens ??
238
+ usage.completionTokens ??
239
+ 0;
240
+ const totalTokens = usage.totalTokens ??
241
+ (typeof promptTokens === 'number' && typeof completionTokens === 'number'
242
+ ? promptTokens + completionTokens
243
+ : 0);
244
+ return {
245
+ promptTokens,
246
+ completionTokens,
247
+ totalTokens,
248
+ };
249
+ }
250
+ /**
251
+ * Get a short preview of a message's content for debug logging.
252
+ */
253
+ getContentPreview(content, maxLength = 200) {
254
+ if (content === null || content === undefined) {
255
+ return undefined;
256
+ }
257
+ if (typeof content === 'string') {
258
+ if (content.length <= maxLength) {
259
+ return content;
260
+ }
261
+ return `${content.slice(0, maxLength)}…`;
262
+ }
263
+ if (Array.isArray(content)) {
264
+ // text parts, tool-call parts, etc.
265
+ const textParts = content.map((part) => {
266
+ if (typeof part === 'object' &&
267
+ part !== null &&
268
+ 'type' in part &&
269
+ part.type === 'text') {
270
+ return part.text ?? '';
271
+ }
272
+ try {
273
+ return JSON.stringify(part);
274
+ }
275
+ catch {
276
+ return '[unserializable part]';
277
+ }
278
+ });
279
+ const joined = textParts.join('\n');
280
+ if (joined.length <= maxLength) {
281
+ return joined;
282
+ }
283
+ return `${joined.slice(0, maxLength)}…`;
284
+ }
285
+ try {
286
+ const serialized = JSON.stringify(content);
287
+ if (serialized.length <= maxLength) {
288
+ return serialized;
289
+ }
290
+ return `${serialized.slice(0, maxLength)}…`;
291
+ }
292
+ catch {
293
+ return '[unserializable content]';
294
+ }
295
+ }
296
+ /**
297
+ * Core chat completion implementation using AI SDK v5.
298
+ *
299
+ * This replaces the original OpenAI SDK v5 client usage with:
300
+ * - createOpenAI({ apiKey, baseURL })
301
+ * - openai.chat(modelId)
302
+ * - generateText / streamText
303
+ */
304
+ async *generateChatCompletionWithOptions(options) {
305
+ const logger = this.getLogger();
306
+ const { contents, tools, metadata } = options;
307
+ const modelId = options.resolved.model || this.getDefaultModel();
308
+ const abortSignal = metadata?.abortSignal;
309
+ const ephemerals = options.invocation?.ephemerals ?? {};
310
+ const toolFormatter = this.createToolFormatter();
311
+ const resolved = options.resolved;
312
+ if (logger.enabled) {
313
+ logger.debug(() => `[OpenAIVercelProvider] Resolved request context`, {
314
+ provider: this.name,
315
+ model: modelId,
316
+ resolvedModel: resolved.model,
317
+ resolvedBaseUrl: resolved.baseURL,
318
+ authTokenPresent: Boolean(resolved.authToken),
319
+ messageCount: contents.length,
320
+ toolCount: tools?.length ?? 0,
321
+ metadataKeys: Object.keys(metadata ?? {}),
322
+ });
323
+ }
324
+ // Determine streaming vs non-streaming mode (default: enabled)
325
+ const streamingSetting = ephemerals['streaming'];
326
+ const streamingResolved = options.resolved?.streaming;
327
+ const streamingEnabled = streamingResolved === false
328
+ ? false
329
+ : streamingResolved === true
330
+ ? true
331
+ : streamingSetting !== 'disabled';
332
+ // System prompt (same core-prompt mechanism as OpenAIProvider)
333
+ const flattenedToolNames = tools?.flatMap((group) => group.functionDeclarations
334
+ .map((decl) => decl.name)
335
+ .filter((name) => !!name)) ?? [];
336
+ const toolNamesArg = tools === undefined ? undefined : Array.from(new Set(flattenedToolNames));
337
+ const userMemory = await resolveUserMemory(options.userMemory, () => options.invocation?.userMemory);
338
+ const systemPrompt = await getCoreSystemPromptAsync(userMemory, modelId, toolNamesArg);
339
+ // Convert internal history to AI SDK ModelMessages with structured tool replay.
340
+ const messages = this.convertToModelMessages(contents);
341
+ if (logger.enabled) {
342
+ logger.debug(() => `[OpenAIVercelProvider] Chat payload snapshot`, {
343
+ messageCount: messages.length,
344
+ messages: messages.map((msg) => ({
345
+ role: msg.role,
346
+ contentPreview: this.getContentPreview(msg.content),
347
+ })),
348
+ });
349
+ }
350
+ // Detect tool format ('openai' or 'qwen') and convert Gemini tools to OpenAI-style definitions
351
+ const detectedFormat = this.detectToolFormat();
352
+ const formattedTools = toolFormatter.convertGeminiToFormat(tools, detectedFormat);
353
+ if (logger.enabled && formattedTools) {
354
+ logger.debug(() => `[OpenAIVercelProvider] Tool conversion summary`, {
355
+ detectedFormat,
356
+ hasTools: !!formattedTools,
357
+ toolCount: formattedTools.length,
358
+ toolNames: formattedTools.map((t) => t.function.name),
359
+ });
360
+ }
361
+ // Build AI SDK ToolSet
362
+ const aiTools = this.buildVercelTools(formattedTools);
363
+ // Model parameters (temperature, top_p, etc.)
364
+ const modelParams = this.extractModelParamsFromOptions(options) ?? {};
365
+ const maxTokensMeta = metadata?.maxTokens ??
366
+ ephemerals['max-tokens'];
367
+ const maxTokensOverride = modelParams['max_tokens'] ?? undefined;
368
+ const maxOutputTokens = typeof maxTokensMeta === 'number' && Number.isFinite(maxTokensMeta)
369
+ ? maxTokensMeta
370
+ : typeof maxTokensOverride === 'number' &&
371
+ Number.isFinite(maxTokensOverride)
372
+ ? maxTokensOverride
373
+ : undefined;
374
+ const temperature = modelParams['temperature'];
375
+ const topP = modelParams['top_p'];
376
+ const presencePenalty = modelParams['presence_penalty'];
377
+ const frequencyPenalty = modelParams['frequency_penalty'];
378
+ const stopSetting = modelParams['stop'];
379
+ const stopSequences = typeof stopSetting === 'string'
380
+ ? [stopSetting]
381
+ : Array.isArray(stopSetting)
382
+ ? stopSetting
383
+ : undefined;
384
+ const seed = modelParams['seed'];
385
+ const maxRetries = ephemerals['retries'] ?? 2; // AI SDK default is 2
386
+ // Instantiate AI SDK OpenAI provider + chat model
387
+ const openaiProvider = await this.createOpenAIClient(options);
388
+ const providerWithChat = openaiProvider;
389
+ const model = (providerWithChat.chat
390
+ ? providerWithChat.chat(modelId)
391
+ : providerWithChat(modelId));
392
+ if (logger.enabled) {
393
+ logger.debug(() => `[OpenAIVercelProvider] Sending chat request`, {
394
+ model: modelId,
395
+ baseURL: resolved.baseURL ?? this.getBaseURL(),
396
+ streamingEnabled,
397
+ hasTools: !!aiTools,
398
+ toolCount: aiTools ? Object.keys(aiTools).length : 0,
399
+ maxOutputTokens,
400
+ });
401
+ }
402
+ if (streamingEnabled) {
403
+ // Streaming mode via streamText()
404
+ const streamOptions = {
405
+ model,
406
+ system: systemPrompt,
407
+ messages,
408
+ tools: aiTools,
409
+ maxOutputTokens,
410
+ temperature,
411
+ topP,
412
+ presencePenalty,
413
+ frequencyPenalty,
414
+ stopSequences,
415
+ seed,
416
+ maxRetries,
417
+ abortSignal,
418
+ };
419
+ if (maxOutputTokens !== undefined) {
420
+ streamOptions['maxTokens'] = maxOutputTokens;
421
+ }
422
+ let result;
423
+ try {
424
+ result = await streamText(streamOptions);
425
+ }
426
+ catch (error) {
427
+ logger.error(() => `[OpenAIVercelProvider] streamText failed: ${error instanceof Error ? error.message : String(error)}`, { error });
428
+ throw wrapError(error, this.name);
429
+ }
430
+ const collectedToolCalls = [];
431
+ let totalUsage;
432
+ let finishReason;
433
+ const hasFullStream = result &&
434
+ typeof result === 'object' &&
435
+ 'fullStream' in result;
436
+ if (hasFullStream && result.fullStream) {
437
+ try {
438
+ for await (const part of result.fullStream) {
439
+ if (abortSignal?.aborted) {
440
+ break;
441
+ }
442
+ switch (part.type) {
443
+ case 'text-delta': {
444
+ const text = typeof part.text === 'string' ? part.text : '';
445
+ if (text) {
446
+ const content = {
447
+ speaker: 'ai',
448
+ blocks: [
449
+ {
450
+ type: 'text',
451
+ text,
452
+ },
453
+ ],
454
+ };
455
+ yield content;
456
+ }
457
+ break;
458
+ }
459
+ case 'tool-call': {
460
+ // Single completed tool call with already-parsed input
461
+ if (part.toolCallId && part.toolName) {
462
+ collectedToolCalls.push({
463
+ toolCallId: String(part.toolCallId),
464
+ toolName: String(part.toolName),
465
+ input: part.input,
466
+ });
467
+ }
468
+ break;
469
+ }
470
+ case 'finish': {
471
+ totalUsage = part.totalUsage;
472
+ finishReason = part.finishReason;
473
+ if (logger.enabled) {
474
+ logger.debug(() => `[OpenAIVercelProvider] streamText finished with reason: ${part.finishReason}`, {
475
+ finishReason: part.finishReason,
476
+ hasUsage: !!totalUsage,
477
+ toolCallCount: collectedToolCalls.length,
478
+ });
479
+ }
480
+ break;
481
+ }
482
+ case 'error': {
483
+ throw part.error ?? new Error('Streaming error from AI SDK');
484
+ }
485
+ default:
486
+ // Ignore other parts: reasoning, source, start-step, finish-step, etc.
487
+ break;
488
+ }
489
+ }
490
+ }
491
+ catch (error) {
492
+ if (abortSignal?.aborted ||
493
+ (error &&
494
+ typeof error === 'object' &&
495
+ 'name' in error &&
496
+ error.name === 'AbortError')) {
497
+ logger.debug(() => `[OpenAIVercelProvider] Streaming response cancelled by AbortSignal`);
498
+ throw error;
499
+ }
500
+ logger.error(() => `[OpenAIVercelProvider] Error processing streaming response: ${error instanceof Error ? error.message : String(error)}`, { error });
501
+ throw wrapError(error, this.name);
502
+ }
503
+ }
504
+ else {
505
+ const legacyStream = result;
506
+ try {
507
+ if (legacyStream.textStream) {
508
+ for await (const textChunk of legacyStream.textStream) {
509
+ if (!textChunk) {
510
+ continue;
511
+ }
512
+ yield {
513
+ speaker: 'ai',
514
+ blocks: [
515
+ {
516
+ type: 'text',
517
+ text: textChunk,
518
+ },
519
+ ],
520
+ };
521
+ }
522
+ }
523
+ }
524
+ catch (error) {
525
+ if (abortSignal?.aborted ||
526
+ (error &&
527
+ typeof error === 'object' &&
528
+ 'name' in error &&
529
+ error.name === 'AbortError')) {
530
+ throw error;
531
+ }
532
+ logger.error(() => `[OpenAIVercelProvider] Legacy streaming response failed: ${error instanceof Error ? error.message : String(error)}`, { error });
533
+ throw wrapError(error, this.name);
534
+ }
535
+ const legacyToolCalls = (legacyStream.toolCalls
536
+ ? await legacyStream.toolCalls.catch(() => [])
537
+ : []) ?? [];
538
+ for (const call of legacyToolCalls) {
539
+ collectedToolCalls.push({
540
+ toolCallId: String(call.toolCallId ?? crypto.randomUUID()),
541
+ toolName: String(call.toolName ?? 'unknown_tool'),
542
+ input: call.input,
543
+ });
544
+ }
545
+ totalUsage = legacyStream.usage
546
+ ? await legacyStream.usage.catch(() => undefined)
547
+ : undefined;
548
+ finishReason = legacyStream.finishReason
549
+ ? await legacyStream.finishReason.catch(() => undefined)
550
+ : undefined;
551
+ }
552
+ // Emit accumulated tool calls as a single IContent, with usage metadata if available
553
+ if (collectedToolCalls.length > 0) {
554
+ const blocks = collectedToolCalls.map((call) => {
555
+ let argsString = '{}';
556
+ try {
557
+ argsString =
558
+ typeof call.input === 'string'
559
+ ? call.input
560
+ : JSON.stringify(call.input ?? {});
561
+ }
562
+ catch {
563
+ argsString = '{}';
564
+ }
565
+ const processedParameters = processToolParameters(argsString, call.toolName);
566
+ return {
567
+ type: 'tool_call',
568
+ id: this.normalizeToHistoryToolId(this.normalizeToOpenAIToolId(call.toolCallId)),
569
+ name: call.toolName,
570
+ parameters: processedParameters,
571
+ };
572
+ });
573
+ const usageMeta = this.mapUsageToMetadata(totalUsage);
574
+ const metadata = usageMeta || finishReason
575
+ ? {
576
+ ...(usageMeta ? { usage: usageMeta } : {}),
577
+ ...(finishReason ? { finishReason } : {}),
578
+ }
579
+ : undefined;
580
+ const toolContent = {
581
+ speaker: 'ai',
582
+ blocks,
583
+ ...(metadata ? { metadata } : {}),
584
+ };
585
+ yield toolContent;
586
+ }
587
+ else {
588
+ // Emit metadata-only message so callers can see usage/finish reason
589
+ const usageMeta = this.mapUsageToMetadata(totalUsage);
590
+ const metadata = usageMeta || finishReason
591
+ ? {
592
+ ...(usageMeta ? { usage: usageMeta } : {}),
593
+ ...(finishReason ? { finishReason } : {}),
594
+ }
595
+ : undefined;
596
+ if (metadata) {
597
+ yield {
598
+ speaker: 'ai',
599
+ blocks: [],
600
+ metadata,
601
+ };
602
+ }
603
+ }
604
+ }
605
+ else {
606
+ // Non-streaming mode via generateText()
607
+ let result;
608
+ try {
609
+ const aiToolFn = this.getAiTool();
610
+ const toolsForGenerate = (!aiToolFn && formattedTools ? formattedTools : aiTools) ?? undefined;
611
+ const generateOptions = {
612
+ model,
613
+ system: systemPrompt,
614
+ messages,
615
+ tools: toolsForGenerate,
616
+ maxOutputTokens,
617
+ temperature,
618
+ topP,
619
+ presencePenalty,
620
+ frequencyPenalty,
621
+ stopSequences,
622
+ seed,
623
+ maxRetries,
624
+ abortSignal,
625
+ };
626
+ if (maxOutputTokens !== undefined) {
627
+ generateOptions['maxTokens'] = maxOutputTokens;
628
+ }
629
+ result = await generateText(generateOptions);
630
+ }
631
+ catch (error) {
632
+ logger.error(() => `[OpenAIVercelProvider] Non-streaming chat completion failed: ${error instanceof Error ? error.message : String(error)}`, { error });
633
+ throw wrapError(error, this.name);
634
+ }
635
+ const blocks = [];
636
+ if (result.text) {
637
+ blocks.push({
638
+ type: 'text',
639
+ text: result.text,
640
+ });
641
+ }
642
+ // Typed tool calls from AI SDK; execution is not automatic because we did not provide execute().
643
+ const toolCalls = 'toolCalls' in result && result.toolCalls ? await result.toolCalls : [];
644
+ for (const call of toolCalls) {
645
+ const toolName = call.toolName ?? 'unknown_tool';
646
+ const id = call.toolCallId ?? crypto.randomUUID();
647
+ const rawInput = call.input ??
648
+ call.args ??
649
+ call.arguments;
650
+ let argsString = '{}';
651
+ try {
652
+ argsString =
653
+ typeof rawInput === 'string'
654
+ ? rawInput
655
+ : JSON.stringify(rawInput ?? {});
656
+ }
657
+ catch {
658
+ argsString = '{}';
659
+ }
660
+ const processedParameters = processToolParameters(argsString, toolName);
661
+ blocks.push({
662
+ type: 'tool_call',
663
+ id: this.normalizeToHistoryToolId(this.normalizeToOpenAIToolId(id)),
664
+ name: toolName,
665
+ parameters: processedParameters,
666
+ });
667
+ }
668
+ if (blocks.length > 0 || result.usage) {
669
+ const usageMeta = this.mapUsageToMetadata(result.usage);
670
+ const content = {
671
+ speaker: 'ai',
672
+ blocks,
673
+ ...(usageMeta
674
+ ? {
675
+ metadata: {
676
+ usage: usageMeta,
677
+ },
678
+ }
679
+ : {}),
680
+ };
681
+ yield content;
682
+ }
683
+ }
684
+ }
685
+ /**
686
+ * Models listing – uses HTTP GET /models via fetch instead of the OpenAI SDK.
687
+ * Falls back to a small static list if the request fails.
688
+ */
689
+ async getModels() {
690
+ const logger = this.getLogger();
691
+ try {
692
+ const authToken = await this.getAuthToken();
693
+ const baseURL = this.getBaseURL() ?? 'https://api.openai.com/v1';
694
+ const url = baseURL.endsWith('/') || baseURL.endsWith('\\')
695
+ ? `${baseURL}models`
696
+ : `${baseURL}/models`;
697
+ const headers = {
698
+ ...(this.getCustomHeaders() ?? {}),
699
+ };
700
+ if (authToken) {
701
+ headers['Authorization'] = `Bearer ${authToken}`;
702
+ }
703
+ const res = await fetch(url, {
704
+ headers,
705
+ });
706
+ if (!res.ok) {
707
+ throw new Error(`HTTP ${res.status}`);
708
+ }
709
+ const data = (await res.json());
710
+ const models = [];
711
+ for (const model of data.data ?? []) {
712
+ // Filter out non-chat models (embeddings, audio, image, etc.)
713
+ if (!/embedding|whisper|audio|tts|image|vision|dall[- ]?e|moderation/i.test(model.id)) {
714
+ const contextWindow = model.context_window ??
715
+ model.contextWindow;
716
+ models.push({
717
+ id: model.id,
718
+ name: model.name ?? model.id,
719
+ provider: this.name,
720
+ supportedToolFormats: ['openai'],
721
+ ...(typeof contextWindow === 'number'
722
+ ? { contextWindow }
723
+ : undefined),
724
+ });
725
+ }
726
+ }
727
+ const sortedModels = models.length > 0
728
+ ? models.sort((a, b) => a.name.localeCompare(b.name))
729
+ : this.getFallbackModels();
730
+ return sortedModels;
731
+ }
732
+ catch (error) {
733
+ logger.debug(() => `Error fetching models from OpenAI via Vercel provider: ${error}`);
734
+ return this.getFallbackModels();
735
+ }
736
+ }
737
+ getFallbackModels() {
738
+ const providerName = this.name;
739
+ const models = [
740
+ {
741
+ id: 'gpt-3.5-turbo',
742
+ name: 'GPT-3.5 Turbo',
743
+ provider: providerName,
744
+ supportedToolFormats: ['openai'],
745
+ contextWindow: 16385,
746
+ },
747
+ {
748
+ id: 'gpt-4',
749
+ name: 'GPT-4',
750
+ provider: providerName,
751
+ supportedToolFormats: ['openai'],
752
+ contextWindow: 8192,
753
+ },
754
+ {
755
+ id: 'gpt-4-turbo',
756
+ name: 'GPT-4 Turbo',
757
+ provider: providerName,
758
+ supportedToolFormats: ['openai'],
759
+ contextWindow: 128000,
760
+ },
761
+ {
762
+ id: 'gpt-4o',
763
+ name: 'GPT-4o',
764
+ provider: providerName,
765
+ supportedToolFormats: ['openai'],
766
+ contextWindow: 128000,
767
+ },
768
+ {
769
+ id: 'gpt-4o-mini',
770
+ name: 'GPT-4o Mini',
771
+ provider: providerName,
772
+ supportedToolFormats: ['openai'],
773
+ contextWindow: 128000,
774
+ },
775
+ {
776
+ id: 'o1-mini',
777
+ name: 'o1-mini',
778
+ provider: providerName,
779
+ supportedToolFormats: ['openai'],
780
+ contextWindow: 128000,
781
+ },
782
+ {
783
+ id: 'o1-preview',
784
+ name: 'o1-preview',
785
+ provider: providerName,
786
+ supportedToolFormats: ['openai'],
787
+ contextWindow: 128000,
788
+ },
789
+ ];
790
+ return models.sort((a, b) => a.name.localeCompare(b.name));
791
+ }
792
+ getDefaultModel() {
793
+ const baseURL = this.getBaseURL();
794
+ if (baseURL &&
795
+ (baseURL.includes('qwen') || baseURL.includes('dashscope'))) {
796
+ return process.env.LLXPRT_DEFAULT_MODEL || 'qwen3-coder-plus';
797
+ }
798
+ return process.env.LLXPRT_DEFAULT_MODEL || 'gpt-4o';
799
+ }
800
+ getCurrentModel() {
801
+ return this.getModel();
802
+ }
803
+ // No client caching for AI SDK provider – kept as no-op for compatibility.
804
+ clearClientCache(runtimeKey) {
805
+ void runtimeKey;
806
+ }
807
+ clearState() {
808
+ this.clearClientCache();
809
+ this.clearAuthCache();
810
+ }
811
+ getServerTools() {
812
+ return [];
813
+ }
814
+ async invokeServerTool(toolName, _params, _config, _signal) {
815
+ throw new Error(`Server tool '${toolName}' not supported by OpenAIVercelProvider`);
816
+ }
817
+ getToolFormat() {
818
+ const format = this.detectToolFormat();
819
+ const logger = new DebugLogger('llxprt:provider:openaivercel');
820
+ logger.debug(() => `getToolFormat() called, returning: ${format}`, {
821
+ provider: this.name,
822
+ model: this.getModel(),
823
+ format,
824
+ });
825
+ return format;
826
+ }
827
+ /**
828
+ * Detects the tool call format based on the model being used.
829
+ * Mirrors OpenAIProvider behavior so existing ToolFormatter logic works.
830
+ */
831
+ detectToolFormat() {
832
+ const modelName = (this.getModel() || this.getDefaultModel()).toLowerCase();
833
+ const logger = new DebugLogger('llxprt:provider:openaivercel');
834
+ if (modelName.includes('glm-4')) {
835
+ logger.debug(() => `Auto-detected 'qwen' format for GLM-4.x model: ${modelName}`);
836
+ return 'qwen';
837
+ }
838
+ if (modelName.includes('qwen')) {
839
+ logger.debug(() => `Auto-detected 'qwen' format for Qwen model: ${modelName}`);
840
+ return 'qwen';
841
+ }
842
+ logger.debug(() => `Using default 'openai' format for model: ${modelName}`);
843
+ return 'openai';
844
+ }
845
+ parseToolResponse(response) {
846
+ return response;
847
+ }
848
+ /**
849
+ * Disallow memoization of model params to preserve stateless behavior.
850
+ */
851
+ setModelParams(_params) {
852
+ throw new Error('ProviderCacheError("Attempted to memoize model parameters for openaivercel")');
853
+ }
854
+ /**
855
+ * Gets model parameters from SettingsService per call (stateless).
856
+ * Mirrors OpenAIProvider.getModelParams for compatibility.
857
+ */
858
+ getModelParams() {
859
+ try {
860
+ const settingsService = this.resolveSettingsService();
861
+ const providerSettings = settingsService.getProviderSettings(this.name);
862
+ const reservedKeys = new Set([
863
+ 'enabled',
864
+ 'apiKey',
865
+ 'api-key',
866
+ 'apiKeyfile',
867
+ 'api-keyfile',
868
+ 'baseUrl',
869
+ 'base-url',
870
+ 'model',
871
+ 'toolFormat',
872
+ 'tool-format',
873
+ 'toolFormatOverride',
874
+ 'tool-format-override',
875
+ 'defaultModel',
876
+ ]);
877
+ const params = {};
878
+ if (providerSettings) {
879
+ for (const [key, value] of Object.entries(providerSettings)) {
880
+ if (reservedKeys.has(key) || value === undefined || value === null) {
881
+ continue;
882
+ }
883
+ params[key] = value;
884
+ }
885
+ }
886
+ return Object.keys(params).length > 0 ? params : undefined;
887
+ }
888
+ catch (error) {
889
+ this.getLogger().debug(() => `Failed to get OpenAIVercel provider settings from SettingsService: ${error}`);
890
+ return undefined;
891
+ }
892
+ }
893
+ /**
894
+ * Determines whether a response should be retried based on error codes.
895
+ *
896
+ * This is retained for compatibility with existing retryWithBackoff
897
+ * callers, even though AI SDK's generateText/streamText have their
898
+ * own built-in retry logic.
899
+ */
900
+ shouldRetryResponse(error) {
901
+ const logger = new DebugLogger('llxprt:provider:openaivercel');
902
+ // Don't retry if it's a "successful" 200 error wrapper
903
+ if (error &&
904
+ typeof error === 'object' &&
905
+ 'status' in error &&
906
+ error.status === 200) {
907
+ return false;
908
+ }
909
+ let status;
910
+ if (error && typeof error === 'object' && 'status' in error) {
911
+ status = error.status;
912
+ }
913
+ if (!status && error && typeof error === 'object' && 'response' in error) {
914
+ const response = error.response;
915
+ if (response && typeof response === 'object' && 'status' in response) {
916
+ status = response.status;
917
+ }
918
+ }
919
+ if (!status && error instanceof Error) {
920
+ if (error.message.includes('429')) {
921
+ status = 429;
922
+ }
923
+ }
924
+ logger.debug(() => `shouldRetryResponse checking error:`, {
925
+ hasError: !!error,
926
+ errorType: error && typeof error === 'object'
927
+ ? error.constructor?.name
928
+ : undefined,
929
+ status,
930
+ errorMessage: error instanceof Error ? error.message : String(error),
931
+ errorKeys: error && typeof error === 'object' ? Object.keys(error) : [],
932
+ errorData: error && typeof error === 'object' && 'error' in error
933
+ ? error.error
934
+ : undefined,
935
+ });
936
+ const shouldRetry = Boolean(status === 429 || status === 503 || status === 504);
937
+ if (shouldRetry) {
938
+ logger.debug(() => `Will retry request due to status ${status}`);
939
+ }
940
+ return shouldRetry;
941
+ }
942
+ }
943
+ //# sourceMappingURL=OpenAIVercelProvider.js.map