1818 */
1919
2020// METADATA // {"ai-commented":{"service":"claude"}}
21+ const FSNodeParam = require ( '../../api/filesystem/FSNodeParam' ) ;
22+ const { LLRead } = require ( '../../filesystem/ll_operations/ll_read' ) ;
2123const BaseService = require ( '../../services/BaseService' ) ;
2224const { Context } = require ( '../../util/context' ) ;
25+ const { stream_to_buffer } = require ( '../../util/streamutil' ) ;
2326const OpenAIUtil = require ( './lib/OpenAIUtil' ) ;
2427
28+ // We're capping at 5MB, which sucks, but Chat Completions doesn't suuport
29+ // file inputs.
30+ const MAX_FILE_SIZE = 5 * 1_000_000 ;
31+
2532/**
2633* OpenAICompletionService class provides an interface to OpenAI's chat completion API.
2734* Extends BaseService to handle chat completions, message moderation, token counting,
@@ -34,6 +41,11 @@ class OpenAICompletionService extends BaseService {
3441 openai : require ( 'openai' ) ,
3542 tiktoken : require ( 'tiktoken' ) ,
3643 }
44+
45+ /**
46+ * @type {import('openai').OpenAI }
47+ */
48+ openai ;
3749
3850 /**
3951 * Initializes the OpenAI service by setting up the API client with credentials
@@ -325,7 +337,71 @@ class OpenAICompletionService extends BaseService {
325337 }
326338
327339 this . log . info ( 'PRIVATE UID FOR USER ' + user_private_uid )
328-
340+
341+ // Perform file uploads
342+ {
343+ const actor = Context . get ( 'actor' ) ;
344+ const { user } = actor . type ;
345+
346+ const file_input_tasks = [ ] ;
347+ for ( const message of messages ) {
348+ // We can assume `message.content` is not undefined because
349+ // Messages.normalize_single_message ensures this.
350+ for ( const contentPart of message . content ) {
351+ if ( ! contentPart . puter_path ) continue ;
352+ file_input_tasks . push ( {
353+ node : await ( new FSNodeParam ( contentPart . puter_path ) ) . consolidate ( {
354+ req : { user } ,
355+ getParam : ( ) => contentPart . puter_path ,
356+ } ) ,
357+ contentPart,
358+ } ) ;
359+ }
360+ }
361+
362+ const promises = [ ] ;
363+ for ( const task of file_input_tasks ) promises . push ( ( async ( ) => {
364+ if ( await task . node . get ( 'size' ) > MAX_FILE_SIZE ) {
365+ delete task . contentPart . puter_path ;
366+ task . contentPart . type = 'text' ;
367+ task . contentPart . text = `{error: input file exceeded maximum of ${ MAX_FILE_SIZE } bytes; ` +
368+ `the user did not write this message}` ; // "poor man's system prompt"
369+ return ; // "continue"
370+ }
371+
372+ const ll_read = new LLRead ( ) ;
373+ const stream = await ll_read . run ( {
374+ actor : Context . get ( 'actor' ) ,
375+ fsNode : task . node ,
376+ } ) ;
377+ const require = this . require ;
378+ const mime = require ( 'mime-types' ) ;
379+ const mimeType = mime . contentType ( await task . node . get ( 'name' ) ) ;
380+
381+ const buffer = await stream_to_buffer ( stream ) ;
382+ const base64 = buffer . toString ( 'base64' ) ;
383+
384+ delete task . contentPart . puter_path ;
385+ if ( mimeType . startsWith ( 'image/' ) ) {
386+ task . contentPart . type = 'image_url' ,
387+ task . contentPart . image_url = {
388+ url : `data:${ mimeType } ;base64,${ base64 } ` ,
389+ } ;
390+ } else if ( mimeType . startsWith ( 'audio/' ) ) {
391+ task . contentPart . type = 'input_audio' ,
392+ task . contentPart . input_audio = {
393+ data : `data:${ mimeType } ;base64,${ base64 } ` ,
394+ format : mimeType . split ( '/' ) [ 1 ] ,
395+ }
396+ } else {
397+ task . contentPart . type = 'text' ;
398+ task . contentPart . text = `{error: input file has unsupported MIME type; ` +
399+ `the user did not write this message}` ; // "poor man's system prompt"
400+ }
401+ } ) ( ) ) ;
402+ await Promise . all ( promises ) ;
403+ }
404+
329405 // Here's something fun; the documentation shows `type: 'image_url'` in
330406 // objects that contain an image url, but everything still works if
331407 // that's missing. We normalise it here so the token count code works.
0 commit comments