diff --git a/AIClasses/BaseAIClass.ts b/AIClasses/BaseAIClass.ts index eaf883d..b5cfb0a 100644 --- a/AIClasses/BaseAIClass.ts +++ b/AIClasses/BaseAIClass.ts @@ -130,6 +130,32 @@ export abstract class BaseAIClass implements IAIClass { ].filter(s => s).join("\n\n"); } + protected async processAttachments( + attachments: Attachment[], + formatBinaryFiles: (attachments: Attachment[]) => string + ): Promise<{ formattedParts: T[], errorMessage?: string }> { + const failedUploads: string[] = []; + + for (const attachment of attachments) { + try { + await this.aiFileService.uploadFile(attachment); + } catch (error) { + Exception.log(`Failed to upload ${attachment.fileName}: ${Exception.messageFrom(error)}`); + failedUploads.push(attachment.fileName); + } + } + + const formattedContent = formatBinaryFiles(attachments); + const formattedParts = JSON.parse(formattedContent) as T[]; + + return { + formattedParts, + errorMessage: failedUploads.length > 0 + ? `[Upload failed for: ${failedUploads.join(', ')}.]` + : undefined + }; + } + /** * Converts a function call to legacy text format for cross-provider compatibility. * Used when a provider doesn't have the required ID field (e.g., Gemini → Claude/OpenAI). diff --git a/AIClasses/BaseAIFileService.ts b/AIClasses/BaseAIFileService.ts index 48997c2..370be4f 100644 --- a/AIClasses/BaseAIFileService.ts +++ b/AIClasses/BaseAIFileService.ts @@ -85,44 +85,28 @@ export abstract class BaseAIFileService implements IAIFileService { // Retries operation on retryable errors (500, 502, 503, 504) with exponential backoff protected async withRetry(operationName: string, operation: () => Promise, defaultValue: T): Promise { - return await this.abortService.abortableOperation(async () => { - for (let attempt = 1; attempt <= BaseAIFileService.MAX_RETRIES; attempt++) { - try { - return await operation(); - } catch (error) { - - // Don't retry on abort errors - throw immediately - if (AbortService.isAbortError(error)) { - throw error; - } - - if (ApiError.isApiError(error) && error.info.isRetryable) { - if (attempt === BaseAIFileService.MAX_RETRIES) { - Exception.log(`${operationName}: Max retries (${BaseAIFileService.MAX_RETRIES}) exhausted. Returning default value.`); - return defaultValue; - } - - const delay = BaseAIFileService.RETRY_DELAYS[attempt]; - Exception.warn(`${operationName}: Attempt ${attempt}/${BaseAIFileService.MAX_RETRIES} failed with ${error.info.type} (status ${error.info.statusCode}). Retrying in ${delay}ms...`); - - if (this.abortService.signal().aborted) { - this.abortService.throw(); - } - - await sleep(delay); - - if (this.abortService.signal().aborted) { - this.abortService.throw(); - } - } else { - // Non-retryable error - return default value - Exception.log(`${operationName}: Non-retryable error. Returning default value.`); + for (let attempt = 1; attempt <= BaseAIFileService.MAX_RETRIES; attempt++) { + try { + return await operation(); + } catch (error) { + if (ApiError.isApiError(error) && error.info.isRetryable) { + if (attempt === BaseAIFileService.MAX_RETRIES) { + Exception.log(`${operationName}: Max retries (${BaseAIFileService.MAX_RETRIES}) exhausted. Returning default value.`); return defaultValue; } + + const delay = BaseAIFileService.RETRY_DELAYS[attempt]; + Exception.warn(`${operationName}: Attempt ${attempt}/${BaseAIFileService.MAX_RETRIES} failed with ${error.info.type} (status ${error.info.statusCode}). Retrying in ${delay}ms...`); + + await sleep(delay); + } else { + // Non-retryable error - return default value + Exception.log(`${operationName}: Non-retryable error. Returning default value.`); + return defaultValue; } } - return defaultValue; - }); + } + return defaultValue; } protected createBoundary(): string { diff --git a/AIClasses/Claude/Claude.ts b/AIClasses/Claude/Claude.ts index 972b34d..b91c272 100644 --- a/AIClasses/Claude/Claude.ts +++ b/AIClasses/Claude/Claude.ts @@ -194,28 +194,17 @@ export class Claude extends BaseAIClass { // Add binary file attachments if present if (content.attachments && content.attachments.length > 0) { - // Upload all attachments and track failures - const failedUploads: string[] = []; + const { formattedParts, errorMessage } = await this.processAttachments( + content.attachments, + (attachments) => this.formatBinaryFiles(attachments) + ); - for (const attachment of content.attachments) { - try { - await this.aiFileService.uploadFile(attachment); - } catch (error) { - Exception.log(`Failed to upload ${attachment.fileName}: ${Exception.messageFrom(error)}`); - failedUploads.push(attachment.fileName); - } - } + contentBlocks.push(...formattedParts); - // Format successfully uploaded files - const formattedContent = this.formatBinaryFiles(content.attachments); - const rawContent = JSON.parse(formattedContent) as ContentBlockParam[]; - contentBlocks.push(...rawContent); - - // Add error messages for failed uploads - if (failedUploads.length > 0) { + if (errorMessage) { contentBlocks.push({ type: "text", - text: `[Upload failed for: ${failedUploads.join(', ')}.]` + text: errorMessage }); } } diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index c574620..0f0eda8 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -10,7 +10,6 @@ import type { IAIFunctionDefinition } from "AIClasses/FunctionDefinitions/IAIFun import type { ConversationContent } from "Conversations/ConversationContent"; import type { Candidate, Part, FunctionDeclaration } from "@google/genai"; import { FinishReason } from "@google/genai"; -import { Exception } from "Helpers/Exception"; export class Gemini extends BaseAIClass { @@ -200,27 +199,16 @@ export class Gemini extends BaseAIClass { // Add binary file attachments if present if (content.attachments && content.attachments.length > 0) { - // Upload all attachments and track failures - const failedUploads: string[] = []; + const { formattedParts, errorMessage } = await this.processAttachments( + content.attachments, + (attachments) => this.formatBinaryFiles(attachments) + ); - for (const attachment of content.attachments) { - try { - await this.aiFileService.uploadFile(attachment); - } catch (error) { - Exception.log(`Failed to upload ${attachment.fileName}: ${Exception.messageFrom(error)}`); - failedUploads.push(attachment.fileName); - } - } + parts.push(...formattedParts); - // Format successfully uploaded files - const formattedContent = this.formatBinaryFiles(content.attachments); - const rawContent = JSON.parse(formattedContent) as Part[]; - parts.push(...rawContent); - - // Add error messages for failed uploads - if (failedUploads.length > 0) { + if (errorMessage) { parts.push({ - text: `[Upload failed for: ${failedUploads.join(', ')}.]` + text: errorMessage }); } } diff --git a/AIClasses/OpenAI/OpenAI.ts b/AIClasses/OpenAI/OpenAI.ts index f9ac112..7e76f77 100644 --- a/AIClasses/OpenAI/OpenAI.ts +++ b/AIClasses/OpenAI/OpenAI.ts @@ -237,29 +237,18 @@ export class OpenAI extends BaseAIClass { // Case 2: Binary file attachments if (content.attachments && content.attachments.length > 0) { - // Upload all attachments and track failures - const failedUploads: string[] = []; + const { formattedParts, errorMessage } = await this.processAttachments( + content.attachments, + (attachments) => this.formatBinaryFiles(attachments) + ); - for (const attachment of content.attachments) { - try { - await this.aiFileService.uploadFile(attachment); - } catch (error) { - Exception.log(`Failed to upload ${attachment.fileName}: ${Exception.messageFrom(error)}`); - failedUploads.push(attachment.fileName); - } - } + results.push(...formattedParts); - // Format successfully uploaded files - const formattedContent = this.formatBinaryFiles(content.attachments); - const rawContent = JSON.parse(formattedContent) as ResponsesAPIInput[]; - results.push(...rawContent); - - // Add error messages for failed uploads - if (failedUploads.length > 0) { + if (errorMessage) { // OpenAI formatBinaryFiles returns array with role wrapper, so add as separate message results.push({ role: "user", - content: `[Upload failed for: ${failedUploads.join(', ')}.]` + content: errorMessage }); } continue;