@llumiverse/drivers 1.0.0-dev.20260224.234313Z → 1.0.0
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/lib/cjs/bedrock/converse.js +86 -12
- package/lib/cjs/bedrock/converse.js.map +1 -1
- package/lib/cjs/bedrock/index.js +208 -1
- package/lib/cjs/bedrock/index.js.map +1 -1
- package/lib/cjs/groq/index.js +7 -4
- package/lib/cjs/groq/index.js.map +1 -1
- package/lib/cjs/openai/index.js +457 -26
- package/lib/cjs/openai/index.js.map +1 -1
- package/lib/cjs/openai/openai_compatible.js +1 -0
- package/lib/cjs/openai/openai_compatible.js.map +1 -1
- package/lib/cjs/vertexai/index.js +42 -0
- package/lib/cjs/vertexai/index.js.map +1 -1
- package/lib/cjs/vertexai/models/claude.js +230 -2
- package/lib/cjs/vertexai/models/claude.js.map +1 -1
- package/lib/cjs/vertexai/models/gemini.js +261 -41
- package/lib/cjs/vertexai/models/gemini.js.map +1 -1
- package/lib/cjs/vertexai/models.js +1 -1
- package/lib/cjs/vertexai/models.js.map +1 -1
- package/lib/esm/bedrock/converse.js +80 -6
- package/lib/esm/bedrock/converse.js.map +1 -1
- package/lib/esm/bedrock/index.js +207 -2
- package/lib/esm/bedrock/index.js.map +1 -1
- package/lib/esm/groq/index.js +7 -4
- package/lib/esm/groq/index.js.map +1 -1
- package/lib/esm/openai/index.js +456 -27
- package/lib/esm/openai/index.js.map +1 -1
- package/lib/esm/openai/openai_compatible.js +1 -0
- package/lib/esm/openai/openai_compatible.js.map +1 -1
- package/lib/esm/vertexai/index.js +43 -1
- package/lib/esm/vertexai/index.js.map +1 -1
- package/lib/esm/vertexai/models/claude.js +229 -3
- package/lib/esm/vertexai/models/claude.js.map +1 -1
- package/lib/esm/vertexai/models/gemini.js +262 -43
- package/lib/esm/vertexai/models/gemini.js.map +1 -1
- package/lib/esm/vertexai/models.js +1 -1
- package/lib/esm/vertexai/models.js.map +1 -1
- package/lib/types/bedrock/converse.d.ts +1 -2
- package/lib/types/bedrock/converse.d.ts.map +1 -1
- package/lib/types/bedrock/index.d.ts +53 -1
- package/lib/types/bedrock/index.d.ts.map +1 -1
- package/lib/types/openai/index.d.ts +96 -1
- package/lib/types/openai/index.d.ts.map +1 -1
- package/lib/types/openai/openai_compatible.d.ts +5 -0
- package/lib/types/openai/openai_compatible.d.ts.map +1 -1
- package/lib/types/openai/openai_format.d.ts +1 -1
- package/lib/types/vertexai/index.d.ts +11 -1
- package/lib/types/vertexai/index.d.ts.map +1 -1
- package/lib/types/vertexai/models/claude.d.ts +64 -1
- package/lib/types/vertexai/models/claude.d.ts.map +1 -1
- package/lib/types/vertexai/models/gemini.d.ts +61 -1
- package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
- package/lib/types/vertexai/models.d.ts +6 -1
- package/lib/types/vertexai/models.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/bedrock/converse.ts +85 -10
- package/src/bedrock/error-handling.test.ts +352 -0
- package/src/bedrock/index.ts +225 -1
- package/src/groq/index.ts +9 -4
- package/src/openai/error-handling.test.ts +567 -0
- package/src/openai/index.ts +505 -29
- package/src/openai/openai_compatible.ts +7 -0
- package/src/openai/openai_format.ts +1 -1
- package/src/vertexai/index.ts +56 -5
- package/src/vertexai/models/claude-error-handling.test.ts +432 -0
- package/src/vertexai/models/claude.ts +273 -7
- package/src/vertexai/models/gemini-error-handling.test.ts +353 -0
- package/src/vertexai/models/gemini.ts +304 -48
- package/src/vertexai/models.ts +7 -2
package/src/openai/index.ts
CHANGED
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
ExecutionOptions,
|
|
12
12
|
ExecutionTokenUsage,
|
|
13
13
|
JSONSchema,
|
|
14
|
+
LlumiverseError,
|
|
15
|
+
LlumiverseErrorContext,
|
|
14
16
|
ModelType,
|
|
17
|
+
OpenAiDalleOptions,
|
|
18
|
+
OpenAiGptImageOptions,
|
|
15
19
|
Providers,
|
|
16
20
|
ToolDefinition,
|
|
17
21
|
ToolUse,
|
|
@@ -24,11 +28,28 @@ import {
|
|
|
24
28
|
incrementConversationTurn,
|
|
25
29
|
modelModalitiesToArray,
|
|
26
30
|
stripBase64ImagesFromConversation,
|
|
31
|
+
stripHeartbeatsFromConversation,
|
|
27
32
|
supportsToolUse,
|
|
28
33
|
truncateLargeTextInConversation,
|
|
29
34
|
unwrapConversationArray,
|
|
30
35
|
} from "@llumiverse/core";
|
|
31
36
|
import OpenAI, { AzureOpenAI } from "openai";
|
|
37
|
+
import {
|
|
38
|
+
APIConnectionError,
|
|
39
|
+
APIConnectionTimeoutError,
|
|
40
|
+
APIError,
|
|
41
|
+
AuthenticationError,
|
|
42
|
+
BadRequestError,
|
|
43
|
+
ConflictError,
|
|
44
|
+
ContentFilterFinishReasonError,
|
|
45
|
+
InternalServerError,
|
|
46
|
+
LengthFinishReasonError,
|
|
47
|
+
NotFoundError,
|
|
48
|
+
OpenAIError,
|
|
49
|
+
PermissionDeniedError,
|
|
50
|
+
RateLimitError,
|
|
51
|
+
UnprocessableEntityError,
|
|
52
|
+
} from 'openai/error';
|
|
32
53
|
import { formatOpenAILikeMultimodalPrompt } from "./openai_format.js";
|
|
33
54
|
|
|
34
55
|
// Response API types
|
|
@@ -71,15 +92,16 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
71
92
|
const tokenInfo = mapUsage(result.usage);
|
|
72
93
|
|
|
73
94
|
const tools = collectTools(result.output);
|
|
74
|
-
|
|
95
|
+
// Collect all parts in order (text and images)
|
|
96
|
+
const allResults = extractCompletionResults(result.output);
|
|
75
97
|
|
|
76
|
-
if (
|
|
98
|
+
if (allResults.length === 0 && !tools) {
|
|
77
99
|
this.logger.error({ result }, "[OpenAI] Response is not valid");
|
|
78
100
|
throw new Error("Response is not valid: no data");
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
return {
|
|
82
|
-
result:
|
|
104
|
+
result: allResults,
|
|
83
105
|
token_usage: tokenInfo,
|
|
84
106
|
finish_reason: responseFinishReason(result, tools),
|
|
85
107
|
tool_use: tools,
|
|
@@ -92,11 +114,18 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
92
114
|
}
|
|
93
115
|
|
|
94
116
|
// Include conversation history (same as non-streaming)
|
|
95
|
-
|
|
117
|
+
// Fix orphaned function_call items (can occur when agent is stopped mid-tool-execution)
|
|
118
|
+
let conversation = fixOrphanedToolUse(updateConversation(options.conversation, prompt));
|
|
96
119
|
|
|
97
120
|
const toolDefs = getToolDefinitions(options.tools);
|
|
98
121
|
const useTools: boolean = toolDefs ? supportsToolUse(options.model, this.provider, true) : false;
|
|
99
122
|
|
|
123
|
+
// When no tools are provided but conversation contains function_call/function_call_output
|
|
124
|
+
// items (e.g. checkpoint summary calls), convert them to text to avoid API errors
|
|
125
|
+
if (!useTools) {
|
|
126
|
+
conversation = convertOpenAIFunctionItemsToText(conversation);
|
|
127
|
+
}
|
|
128
|
+
|
|
100
129
|
convertRoles(prompt, options.model);
|
|
101
130
|
|
|
102
131
|
const model_options = options.model_options as any;
|
|
@@ -153,7 +182,14 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
153
182
|
const toolDefs = getToolDefinitions(options.tools);
|
|
154
183
|
const useTools: boolean = toolDefs ? supportsToolUse(options.model, this.provider) : false;
|
|
155
184
|
|
|
156
|
-
|
|
185
|
+
// Fix orphaned function_call items (can occur when agent is stopped mid-tool-execution)
|
|
186
|
+
let conversation = fixOrphanedToolUse(updateConversation(options.conversation, prompt));
|
|
187
|
+
|
|
188
|
+
// When no tools are provided but conversation contains function_call/function_call_output
|
|
189
|
+
// items (e.g. checkpoint summary calls), convert them to text to avoid API errors
|
|
190
|
+
if (!useTools) {
|
|
191
|
+
conversation = convertOpenAIFunctionItemsToText(conversation);
|
|
192
|
+
}
|
|
157
193
|
|
|
158
194
|
let parsedSchema: JSONSchema | undefined = undefined;
|
|
159
195
|
let strictMode = false;
|
|
@@ -212,12 +248,25 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
212
248
|
// Truncate large text content if configured
|
|
213
249
|
processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
|
|
214
250
|
|
|
251
|
+
// Strip old heartbeat status messages
|
|
252
|
+
processedConversation = stripHeartbeatsFromConversation(processedConversation, {
|
|
253
|
+
keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
|
|
254
|
+
currentTurn,
|
|
255
|
+
});
|
|
256
|
+
|
|
215
257
|
completion.conversation = processedConversation;
|
|
216
258
|
|
|
217
259
|
return completion;
|
|
218
260
|
}
|
|
219
261
|
|
|
220
262
|
protected canStream(_options: ExecutionOptions): Promise<boolean> {
|
|
263
|
+
// Image generation models don't support streaming
|
|
264
|
+
if (_options.model.includes("dall-e")
|
|
265
|
+
|| _options.model.includes("gpt-image")
|
|
266
|
+
|| _options.model.includes("chatgpt-image")) {
|
|
267
|
+
return Promise.resolve(false);
|
|
268
|
+
}
|
|
269
|
+
|
|
221
270
|
if (_options.model.includes("o1")
|
|
222
271
|
&& !(_options.model.includes("mini") || _options.model.includes("preview"))) {
|
|
223
272
|
//o1 full does not support streaming
|
|
@@ -277,6 +326,10 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
277
326
|
};
|
|
278
327
|
let processedConversation = stripBase64ImagesFromConversation(conversation, stripOptions);
|
|
279
328
|
processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
|
|
329
|
+
processedConversation = stripHeartbeatsFromConversation(processedConversation, {
|
|
330
|
+
keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
|
|
331
|
+
currentTurn,
|
|
332
|
+
});
|
|
280
333
|
|
|
281
334
|
return processedConversation as ResponseInputItem[];
|
|
282
335
|
}
|
|
@@ -341,7 +394,7 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
341
394
|
//Some of these use the completions API instead of the chat completions API.
|
|
342
395
|
//Others are for non-text input modalities. Therefore common to both.
|
|
343
396
|
const wordBlacklist = ["embed", "whisper", "transcribe", "audio", "moderation", "tts",
|
|
344
|
-
"realtime", "
|
|
397
|
+
"realtime", "babbage", "davinci", "codex", "o1-pro", "computer-use", "sora"];
|
|
345
398
|
|
|
346
399
|
|
|
347
400
|
//OpenAI has very little information, filtering based on name.
|
|
@@ -356,14 +409,20 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
356
409
|
if (owner == "system") {
|
|
357
410
|
owner = "openai";
|
|
358
411
|
}
|
|
412
|
+
|
|
413
|
+
// Determine model type based on capabilities
|
|
414
|
+
let modelType = ModelType.Text;
|
|
415
|
+
if (m.id.includes("dall-e") || m.id.includes("gpt-image")) {
|
|
416
|
+
modelType = ModelType.Image;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
359
420
|
return {
|
|
360
421
|
id: m.id,
|
|
361
422
|
name: m.id,
|
|
362
423
|
provider: this.provider,
|
|
363
424
|
owner: owner,
|
|
364
|
-
type:
|
|
365
|
-
can_stream: true,
|
|
366
|
-
is_multimodal: m.id.includes("gpt-4"),
|
|
425
|
+
type: modelType,
|
|
367
426
|
input_modalities: modelModalitiesToArray(modelCapability.input),
|
|
368
427
|
output_modalities: modelModalitiesToArray(modelCapability.output),
|
|
369
428
|
tool_support: modelCapability.tool_support,
|
|
@@ -398,6 +457,307 @@ export abstract class BaseOpenAIDriver extends AbstractDriver<
|
|
|
398
457
|
return { values: embeddings, model } satisfies EmbeddingsResult;
|
|
399
458
|
}
|
|
400
459
|
|
|
460
|
+
imageModels = ["dall-e", "gpt-image", "chatgpt-image"];
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Determine if a model is specifically an image generation model (not conversational image model)
|
|
464
|
+
*/
|
|
465
|
+
isImageModel(model: string): boolean {
|
|
466
|
+
// DALL-E models are standalone image generation
|
|
467
|
+
// gpt-image models can generate images in conversations, not standalone
|
|
468
|
+
return this.imageModels.some(imageModel => model.includes(imageModel));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Request image generation from standalone Images API
|
|
473
|
+
* Supports: DALL-E 2, DALL-E 3, GPT-image models (for edit/variation)
|
|
474
|
+
*/
|
|
475
|
+
async requestImageGeneration(prompt: ResponseInputItem[], options: ExecutionOptions): Promise<Completion> {
|
|
476
|
+
this.logger.debug(`[${this.provider}] Generating image with model ${options.model}`);
|
|
477
|
+
|
|
478
|
+
const model_options = options.model_options as OpenAiDalleOptions | OpenAiGptImageOptions | undefined;
|
|
479
|
+
|
|
480
|
+
// Extract prompt text from ResponseInputItem[]
|
|
481
|
+
let promptText = "";
|
|
482
|
+
for (const item of prompt) {
|
|
483
|
+
if ('content' in item && typeof item.content === 'string') {
|
|
484
|
+
promptText += item.content + "\\n";
|
|
485
|
+
} else if ('content' in item && Array.isArray(item.content)) {
|
|
486
|
+
// Extract text from content array
|
|
487
|
+
for (const part of item.content) {
|
|
488
|
+
if ('type' in part && part.type === 'input_text' && 'text' in part) {
|
|
489
|
+
promptText += part.text + "\\n";
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
promptText = promptText.trim();
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
const generateParams: OpenAI.Images.ImageGenerateParamsNonStreaming = {
|
|
498
|
+
model: options.model,
|
|
499
|
+
prompt: promptText,
|
|
500
|
+
size: model_options?.size || "1024x1024",
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Add DALL-E specific options
|
|
504
|
+
if (options.model.includes("dall-e") || model_options?._option_id === "openai-dalle") {
|
|
505
|
+
const dalleOptions = model_options as OpenAiDalleOptions | undefined;
|
|
506
|
+
generateParams.n = dalleOptions?.n || 1;
|
|
507
|
+
generateParams.response_format = dalleOptions?.response_format || "b64_json";
|
|
508
|
+
|
|
509
|
+
if (options.model.includes("dall-e-3")) {
|
|
510
|
+
generateParams.quality = dalleOptions?.image_quality || "standard";
|
|
511
|
+
if (dalleOptions?.style) {
|
|
512
|
+
generateParams.style = dalleOptions.style;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
// Default for other models
|
|
517
|
+
generateParams.n = 1;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const response = await this.service.images.generate(generateParams);
|
|
521
|
+
|
|
522
|
+
// Convert response to CompletionResults
|
|
523
|
+
const results: CompletionResult[] = [];
|
|
524
|
+
|
|
525
|
+
if (response.data) {
|
|
526
|
+
for (const image of response.data) {
|
|
527
|
+
let imageValue: string;
|
|
528
|
+
|
|
529
|
+
if (image.b64_json) {
|
|
530
|
+
// Base64 format
|
|
531
|
+
imageValue = `data:image/png;base64,${image.b64_json}`;
|
|
532
|
+
} else if (image.url) {
|
|
533
|
+
// URL format
|
|
534
|
+
imageValue = image.url;
|
|
535
|
+
} else {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
results.push({
|
|
540
|
+
type: "image",
|
|
541
|
+
value: imageValue
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
result: results
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
} catch (error: any) {
|
|
551
|
+
this.logger.error({ error }, `[${this.provider}] Image generation failed`);
|
|
552
|
+
return {
|
|
553
|
+
result: [],
|
|
554
|
+
error: {
|
|
555
|
+
message: error.message,
|
|
556
|
+
code: error.code || 'GENERATION_FAILED'
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Format OpenAI API errors into LlumiverseError with proper status codes and retryability.
|
|
564
|
+
*
|
|
565
|
+
* OpenAI API errors have a specific structure:
|
|
566
|
+
* - APIError.status: HTTP status code (400, 401, 403, 404, 409, 422, 429, 500+)
|
|
567
|
+
* - APIError.error: Error object with type, message, param, code
|
|
568
|
+
* - APIError.requestID: Request ID for support
|
|
569
|
+
* - APIError.code: Error code (e.g., 'invalid_api_key', 'rate_limit_exceeded')
|
|
570
|
+
* - APIError.param: Parameter that caused the error (optional)
|
|
571
|
+
* - APIError.type: Error type (optional)
|
|
572
|
+
*
|
|
573
|
+
* Common error types:
|
|
574
|
+
* - BadRequestError (400): Invalid request parameters
|
|
575
|
+
* - AuthenticationError (401): Invalid API key
|
|
576
|
+
* - PermissionDeniedError (403): Insufficient permissions
|
|
577
|
+
* - NotFoundError (404): Resource not found
|
|
578
|
+
* - ConflictError (409): Resource conflict
|
|
579
|
+
* - UnprocessableEntityError (422): Validation error
|
|
580
|
+
* - RateLimitError (429): Rate limit exceeded
|
|
581
|
+
* - InternalServerError (500+): Server-side errors
|
|
582
|
+
* - APIConnectionError: Connection issues (no status code)
|
|
583
|
+
* - APIConnectionTimeoutError: Request timeout (no status code)
|
|
584
|
+
* - LengthFinishReasonError: Response truncated due to length
|
|
585
|
+
* - ContentFilterFinishReasonError: Content filtered
|
|
586
|
+
*
|
|
587
|
+
* This implementation works for:
|
|
588
|
+
* - OpenAI API
|
|
589
|
+
* - Azure OpenAI
|
|
590
|
+
* - xAI (uses OpenAI-compatible API)
|
|
591
|
+
* - Azure Foundry (OpenAI-compatible)
|
|
592
|
+
* - Other OpenAI-compatible APIs
|
|
593
|
+
*
|
|
594
|
+
* @see https://platform.openai.com/docs/guides/error-codes
|
|
595
|
+
*/
|
|
596
|
+
public formatLlumiverseError(
|
|
597
|
+
error: unknown,
|
|
598
|
+
context: LlumiverseErrorContext
|
|
599
|
+
): LlumiverseError {
|
|
600
|
+
// Check if it's an OpenAI API error
|
|
601
|
+
const isOpenAIError = this.isOpenAIApiError(error);
|
|
602
|
+
|
|
603
|
+
if (!isOpenAIError) {
|
|
604
|
+
// Not an OpenAI API error, use default handling
|
|
605
|
+
throw error;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const apiError = error as APIError;
|
|
609
|
+
const httpStatusCode = apiError.status;
|
|
610
|
+
|
|
611
|
+
// Extract error message
|
|
612
|
+
const message = apiError.message || String(error);
|
|
613
|
+
|
|
614
|
+
// Extract additional error details (only available on APIError)
|
|
615
|
+
const errorCode = apiError.code;
|
|
616
|
+
const errorParam = apiError.param;
|
|
617
|
+
const errorType = apiError.type;
|
|
618
|
+
|
|
619
|
+
// Build user-facing message with status code
|
|
620
|
+
let userMessage = message;
|
|
621
|
+
|
|
622
|
+
// Include status code in message (for end-user visibility)
|
|
623
|
+
if (httpStatusCode) {
|
|
624
|
+
userMessage = `[${httpStatusCode}] ${userMessage}`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Add error code if available and not already in message
|
|
628
|
+
if (errorCode && !userMessage.includes(errorCode)) {
|
|
629
|
+
userMessage += ` (code: ${errorCode})`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Add parameter info if available and helpful
|
|
633
|
+
if (errorParam && !userMessage.toLowerCase().includes(errorParam.toLowerCase())) {
|
|
634
|
+
userMessage += ` [param: ${errorParam}]`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Add request ID if available (useful for OpenAI support)
|
|
638
|
+
if (apiError.requestID) {
|
|
639
|
+
userMessage += ` (Request ID: ${apiError.requestID})`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Determine retryability based on OpenAI error types
|
|
643
|
+
const retryable = this.isOpenAIErrorRetryable(error, httpStatusCode, errorCode, errorType);
|
|
644
|
+
|
|
645
|
+
// Use the error constructor name as the error name
|
|
646
|
+
const errorName = error.constructor?.name || 'OpenAIError';
|
|
647
|
+
|
|
648
|
+
return new LlumiverseError(
|
|
649
|
+
`[${context.provider}] ${userMessage}`,
|
|
650
|
+
retryable,
|
|
651
|
+
context,
|
|
652
|
+
error,
|
|
653
|
+
httpStatusCode,
|
|
654
|
+
errorName
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Type guard to check if error is an OpenAI API error or OpenAI-specific error.
|
|
660
|
+
*/
|
|
661
|
+
private isOpenAIApiError(error: unknown): error is APIError | OpenAIError {
|
|
662
|
+
return (
|
|
663
|
+
error !== null &&
|
|
664
|
+
typeof error === 'object' &&
|
|
665
|
+
(error instanceof APIError || error instanceof OpenAIError)
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Determine if an OpenAI API error is retryable.
|
|
671
|
+
*
|
|
672
|
+
* Retryable errors:
|
|
673
|
+
* - RateLimitError (429): Rate limit exceeded, retry with backoff
|
|
674
|
+
* - InternalServerError (500+): Server-side errors
|
|
675
|
+
* - APIConnectionTimeoutError: Request timeout
|
|
676
|
+
* - Error codes: 'timeout', 'server_error', 'service_unavailable'
|
|
677
|
+
* - Status codes: 408, 429, 502, 503, 504, 529, 5xx
|
|
678
|
+
*
|
|
679
|
+
* Non-retryable errors:
|
|
680
|
+
* - BadRequestError (400): Invalid request parameters
|
|
681
|
+
* - AuthenticationError (401): Invalid API key
|
|
682
|
+
* - PermissionDeniedError (403): Insufficient permissions
|
|
683
|
+
* - NotFoundError (404): Resource not found
|
|
684
|
+
* - ConflictError (409): Resource conflict
|
|
685
|
+
* - UnprocessableEntityError (422): Validation error
|
|
686
|
+
* - LengthFinishReasonError: Length limit reached
|
|
687
|
+
* - ContentFilterFinishReasonError: Content filtered
|
|
688
|
+
* - Error codes: 'invalid_api_key', 'invalid_request_error', 'model_not_found'
|
|
689
|
+
* - Other 4xx client errors
|
|
690
|
+
*
|
|
691
|
+
* @param error - The error object
|
|
692
|
+
* @param httpStatusCode - The HTTP status code if available
|
|
693
|
+
* @param errorCode - The error code if available
|
|
694
|
+
* @param errorType - The error type if available
|
|
695
|
+
* @returns True if retryable, false if not retryable, undefined if unknown
|
|
696
|
+
*/
|
|
697
|
+
private isOpenAIErrorRetryable(
|
|
698
|
+
error: unknown,
|
|
699
|
+
httpStatusCode: number | undefined,
|
|
700
|
+
errorCode: string | null | undefined,
|
|
701
|
+
errorType: string | undefined
|
|
702
|
+
): boolean | undefined {
|
|
703
|
+
// Check specific OpenAI error types by class
|
|
704
|
+
if (error instanceof RateLimitError) return true;
|
|
705
|
+
if (error instanceof InternalServerError) return true;
|
|
706
|
+
if (error instanceof APIConnectionTimeoutError) return true;
|
|
707
|
+
|
|
708
|
+
// Non-retryable by error type
|
|
709
|
+
if (error instanceof BadRequestError) return false;
|
|
710
|
+
if (error instanceof AuthenticationError) return false;
|
|
711
|
+
if (error instanceof PermissionDeniedError) return false;
|
|
712
|
+
if (error instanceof NotFoundError) return false;
|
|
713
|
+
if (error instanceof ConflictError) return false;
|
|
714
|
+
if (error instanceof UnprocessableEntityError) return false;
|
|
715
|
+
if (error instanceof LengthFinishReasonError) return false;
|
|
716
|
+
if (error instanceof ContentFilterFinishReasonError) return false;
|
|
717
|
+
|
|
718
|
+
// Check error codes (OpenAI specific)
|
|
719
|
+
if (errorCode) {
|
|
720
|
+
// Retryable error codes
|
|
721
|
+
if (errorCode === 'timeout') return true;
|
|
722
|
+
if (errorCode === 'server_error') return true;
|
|
723
|
+
if (errorCode === 'service_unavailable') return true;
|
|
724
|
+
if (errorCode === 'rate_limit_exceeded') return true;
|
|
725
|
+
|
|
726
|
+
// Non-retryable error codes
|
|
727
|
+
if (errorCode === 'invalid_api_key') return false;
|
|
728
|
+
if (errorCode === 'invalid_request_error') return false;
|
|
729
|
+
if (errorCode === 'model_not_found') return false;
|
|
730
|
+
if (errorCode === 'insufficient_quota') return false;
|
|
731
|
+
if (errorCode === 'invalid_model') return false;
|
|
732
|
+
if (errorCode.includes('invalid_')) return false;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Check error type
|
|
736
|
+
if (errorType === 'invalid_request_error') return false;
|
|
737
|
+
if (errorType === 'authentication_error') return false;
|
|
738
|
+
|
|
739
|
+
// Use HTTP status code
|
|
740
|
+
if (httpStatusCode !== undefined) {
|
|
741
|
+
if (httpStatusCode === 429) return true; // Rate limit
|
|
742
|
+
if (httpStatusCode === 408) return true; // Request timeout
|
|
743
|
+
if (httpStatusCode === 502) return true; // Bad gateway
|
|
744
|
+
if (httpStatusCode === 503) return true; // Service unavailable
|
|
745
|
+
if (httpStatusCode === 504) return true; // Gateway timeout
|
|
746
|
+
if (httpStatusCode === 529) return true; // Overloaded
|
|
747
|
+
if (httpStatusCode >= 500 && httpStatusCode < 600) return true; // Server errors
|
|
748
|
+
if (httpStatusCode >= 400 && httpStatusCode < 500) return false; // Client errors
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Connection errors without status codes
|
|
752
|
+
if (error instanceof APIConnectionError && !(error instanceof APIConnectionTimeoutError)) {
|
|
753
|
+
// Generic connection errors might be retryable (network issues)
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Unknown error type - let consumer decide retry strategy
|
|
758
|
+
return undefined;
|
|
759
|
+
}
|
|
760
|
+
|
|
401
761
|
}
|
|
402
762
|
|
|
403
763
|
|
|
@@ -606,6 +966,40 @@ function supportsSchema(model: string): boolean {
|
|
|
606
966
|
return supportsToolUse(model, "openai");
|
|
607
967
|
}
|
|
608
968
|
|
|
969
|
+
/**
|
|
970
|
+
* Converts function_call and function_call_output items to text messages in OpenAI conversation.
|
|
971
|
+
* Preserves tool call information while removing structured items that require
|
|
972
|
+
* tools to be defined in the API request.
|
|
973
|
+
*/
|
|
974
|
+
export function convertOpenAIFunctionItemsToText(items: ResponseInputItem[]): ResponseInputItem[] {
|
|
975
|
+
const hasFunctionItems = items.some(item => {
|
|
976
|
+
const type = (item as any).type;
|
|
977
|
+
return type === 'function_call' || type === 'function_call_output';
|
|
978
|
+
});
|
|
979
|
+
if (!hasFunctionItems) return items;
|
|
980
|
+
|
|
981
|
+
return items.map(item => {
|
|
982
|
+
const typed = item as any;
|
|
983
|
+
if (typed.type === 'function_call') {
|
|
984
|
+
const argsStr = typed.arguments || '';
|
|
985
|
+
const truncated = argsStr.length > 500 ? argsStr.substring(0, 500) + '...' : argsStr;
|
|
986
|
+
return {
|
|
987
|
+
role: 'assistant' as const,
|
|
988
|
+
content: `[Tool call: ${typed.name}(${truncated})]`,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
if (typed.type === 'function_call_output') {
|
|
992
|
+
const output = typed.output || 'No output';
|
|
993
|
+
const truncated = output.length > 500 ? output.substring(0, 500) + '...' : output;
|
|
994
|
+
return {
|
|
995
|
+
role: 'user' as const,
|
|
996
|
+
content: `[Tool result: ${truncated}]`,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
return item;
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
609
1003
|
function getToolDefinitions(tools: ToolDefinition[] | undefined | null): OpenAI.Responses.Tool[] | undefined {
|
|
610
1004
|
return tools ? tools.map(getToolDefinition) : undefined;
|
|
611
1005
|
}
|
|
@@ -671,6 +1065,43 @@ export function collectTools(output?: OpenAI.Responses.ResponseOutputItem[]): To
|
|
|
671
1065
|
return tools.length > 0 ? tools : undefined;
|
|
672
1066
|
}
|
|
673
1067
|
|
|
1068
|
+
/**
|
|
1069
|
+
* Collect all parts (text and images) from response output in order.
|
|
1070
|
+
* This preserves the original ordering of text and image parts.
|
|
1071
|
+
*/
|
|
1072
|
+
function extractCompletionResults(output?: OpenAI.Responses.ResponseOutputItem[]): CompletionResult[] {
|
|
1073
|
+
if (!output) {
|
|
1074
|
+
return [];
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const results: CompletionResult[] = [];
|
|
1078
|
+
for (const item of output) {
|
|
1079
|
+
if (item.type === 'message') {
|
|
1080
|
+
// Extract text from message content
|
|
1081
|
+
for (const part of item.content) {
|
|
1082
|
+
if (part.type === 'output_text' && part.text) {
|
|
1083
|
+
results.push({
|
|
1084
|
+
type: "text",
|
|
1085
|
+
value: part.text
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
} else if (item.type === 'image_generation_call' && 'result' in item && item.result) {
|
|
1090
|
+
// GPT-image models return base64 encoded images in result field
|
|
1091
|
+
const base64Data = item.result;
|
|
1092
|
+
// Format as data URL for consistency with other image outputs
|
|
1093
|
+
const imageUrl = base64Data.startsWith('data:')
|
|
1094
|
+
? base64Data
|
|
1095
|
+
: `data:image/png;base64,${base64Data}`;
|
|
1096
|
+
results.push({
|
|
1097
|
+
type: "image",
|
|
1098
|
+
value: imageUrl
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return results;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
674
1105
|
//For strict mode false
|
|
675
1106
|
function limitedSchemaFormat(schema: JSONSchema): JSONSchema {
|
|
676
1107
|
const formattedSchema = { ...schema };
|
|
@@ -761,26 +1192,6 @@ function openAISchemaFormat(schema: JSONSchema, nesting: number = 0): JSONSchema
|
|
|
761
1192
|
return formattedSchema
|
|
762
1193
|
}
|
|
763
1194
|
|
|
764
|
-
function extractTextFromResponse(response: OpenAI.Responses.Response): string {
|
|
765
|
-
if (response.output_text) {
|
|
766
|
-
return response.output_text;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
const collected: string[] = [];
|
|
770
|
-
for (const item of response.output ?? []) {
|
|
771
|
-
if (item.type === 'message') {
|
|
772
|
-
const text = item.content
|
|
773
|
-
.map(part => part.type === 'output_text' ? part.text : '')
|
|
774
|
-
.join('');
|
|
775
|
-
if (text) {
|
|
776
|
-
collected.push(text);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
return collected.join("\n");
|
|
782
|
-
}
|
|
783
|
-
|
|
784
1195
|
function responseFinishReason(response: OpenAI.Responses.Response, tools?: ToolUse[] | undefined): string | undefined {
|
|
785
1196
|
if (tools && tools.length > 0) {
|
|
786
1197
|
return "tool_use";
|
|
@@ -797,6 +1208,71 @@ function responseFinishReason(response: OpenAI.Responses.Response, tools?: ToolU
|
|
|
797
1208
|
return 'stop';
|
|
798
1209
|
}
|
|
799
1210
|
|
|
1211
|
+
/**
|
|
1212
|
+
* Fix orphaned function_call items in the OpenAI Responses API conversation.
|
|
1213
|
+
*
|
|
1214
|
+
* When an agent is stopped mid-tool-execution, the conversation may contain
|
|
1215
|
+
* function_call items without matching function_call_output items. The OpenAI
|
|
1216
|
+
* Responses API requires every function_call to have a matching function_call_output.
|
|
1217
|
+
*
|
|
1218
|
+
* This function detects such cases and injects synthetic function_call_output items
|
|
1219
|
+
* indicating the tools were interrupted, allowing the conversation to continue.
|
|
1220
|
+
*/
|
|
1221
|
+
export function fixOrphanedToolUse(items: ResponseInputItem[]): ResponseInputItem[] {
|
|
1222
|
+
if (items.length < 2) return items;
|
|
1223
|
+
|
|
1224
|
+
// First pass: collect all function_call_output call_ids
|
|
1225
|
+
const outputCallIds = new Set<string>();
|
|
1226
|
+
for (const item of items) {
|
|
1227
|
+
if ('type' in item && item.type === 'function_call_output') {
|
|
1228
|
+
outputCallIds.add((item as OpenAI.Responses.ResponseInputItem.FunctionCallOutput).call_id);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Second pass: build result, injecting synthetic outputs for orphaned function_calls
|
|
1233
|
+
const result: ResponseInputItem[] = [];
|
|
1234
|
+
const pendingCalls = new Map<string, string>(); // call_id -> tool name
|
|
1235
|
+
|
|
1236
|
+
for (const item of items) {
|
|
1237
|
+
if ('type' in item && item.type === 'function_call') {
|
|
1238
|
+
const fc = item as OpenAI.Responses.ResponseFunctionToolCall;
|
|
1239
|
+
// Only track if there's no matching output anywhere in the conversation
|
|
1240
|
+
if (!outputCallIds.has(fc.call_id)) {
|
|
1241
|
+
pendingCalls.set(fc.call_id, fc.name ?? 'unknown');
|
|
1242
|
+
}
|
|
1243
|
+
result.push(item);
|
|
1244
|
+
} else if ('type' in item && item.type === 'function_call_output') {
|
|
1245
|
+
result.push(item);
|
|
1246
|
+
} else {
|
|
1247
|
+
// Before any non-function item, flush pending orphaned calls
|
|
1248
|
+
if (pendingCalls.size > 0) {
|
|
1249
|
+
for (const [callId, toolName] of pendingCalls) {
|
|
1250
|
+
result.push({
|
|
1251
|
+
type: 'function_call_output',
|
|
1252
|
+
call_id: callId,
|
|
1253
|
+
output: `[Tool interrupted: The user stopped the operation before "${toolName}" could execute.]`,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
pendingCalls.clear();
|
|
1257
|
+
}
|
|
1258
|
+
result.push(item);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Handle trailing orphans at the end of the conversation
|
|
1263
|
+
if (pendingCalls.size > 0) {
|
|
1264
|
+
for (const [callId, toolName] of pendingCalls) {
|
|
1265
|
+
result.push({
|
|
1266
|
+
type: 'function_call_output',
|
|
1267
|
+
call_id: callId,
|
|
1268
|
+
output: `[Tool interrupted: The user stopped the operation before "${toolName}" could execute.]`,
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
return result;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
800
1276
|
function safeJsonParse(value: unknown): any {
|
|
801
1277
|
if (typeof value !== 'string') {
|
|
802
1278
|
return value;
|
|
@@ -13,6 +13,12 @@ export interface OpenAICompatibleDriverOptions extends DriverOptions {
|
|
|
13
13
|
* Example: https://api.example.com/v1
|
|
14
14
|
*/
|
|
15
15
|
endpoint: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Custom headers to include in every request.
|
|
19
|
+
* Useful for Apigee proxies or custom auth schemes.
|
|
20
|
+
*/
|
|
21
|
+
default_headers?: Record<string, string>;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
/**
|
|
@@ -38,6 +44,7 @@ export class OpenAICompatibleDriver extends BaseOpenAIDriver {
|
|
|
38
44
|
this.service = new OpenAI({
|
|
39
45
|
apiKey: opts.apiKey,
|
|
40
46
|
baseURL: opts.endpoint,
|
|
47
|
+
defaultHeaders: opts.default_headers,
|
|
41
48
|
});
|
|
42
49
|
}
|
|
43
50
|
|