@discomedia/utils 1.0.71 → 1.0.72

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/dist/index.cjs CHANGED
@@ -13608,7 +13608,7 @@ function zodTextFormat(zodObject, name, props) {
13608
13608
  }, (content) => zodObject.parse(JSON.parse(content)));
13609
13609
  }
13610
13610
 
13611
- const DEFAULT_MODEL = 'gpt-5-mini';
13611
+ const DEFAULT_MODEL = 'gpt-5.4-mini';
13612
13612
  const GPT_5_HIGH_CONTEXT_THRESHOLD_TOKENS = 272_000;
13613
13613
  const GPT_5_HIGH_CONTEXT_INPUT_MULTIPLIER = 2;
13614
13614
  const GPT_5_HIGH_CONTEXT_OUTPUT_MULTIPLIER = 1.5;
@@ -14246,6 +14246,7 @@ async function parseResponse(content, responseFormat) {
14246
14246
  }
14247
14247
 
14248
14248
  // llm-openai.ts
14249
+ const DEFAULT_IMAGE_MIME_TYPE = 'image/webp';
14249
14250
  /**
14250
14251
  * Checks if the given direct OpenAI model supports the temperature parameter.
14251
14252
  * @param model The model to check.
@@ -14280,6 +14281,135 @@ function buildJsonSchemaFormat(schema, schemaName, schemaDescription, schemaStri
14280
14281
  ...(schemaStrict !== undefined ? { strict: schemaStrict } : {}),
14281
14282
  };
14282
14283
  }
14284
+ function resolveOpenAIApiKey(apiKey) {
14285
+ const resolvedApiKey = apiKey || process.env.OPENAI_API_KEY;
14286
+ if (!resolvedApiKey) {
14287
+ throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
14288
+ }
14289
+ return resolvedApiKey;
14290
+ }
14291
+ function createOpenAIClient(apiKey) {
14292
+ return new OpenAI({
14293
+ apiKey,
14294
+ });
14295
+ }
14296
+ function normalizeImageDataUrl(imageData, mimeType = DEFAULT_IMAGE_MIME_TYPE) {
14297
+ return imageData.startsWith('data:') ? imageData : `data:${mimeType};base64,${imageData}`;
14298
+ }
14299
+ function getFilenameFromPath(filePath) {
14300
+ const normalizedPath = filePath.replace(/\\/g, '/');
14301
+ const pathSegments = normalizedPath.split('/');
14302
+ const filename = pathSegments[pathSegments.length - 1];
14303
+ if (!filename) {
14304
+ throw new Error(`Could not determine a filename from file path: ${filePath}`);
14305
+ }
14306
+ return filename;
14307
+ }
14308
+ function inferImageMimeType(filePath) {
14309
+ const normalizedPath = filePath.toLowerCase();
14310
+ if (normalizedPath.endsWith('.png')) {
14311
+ return 'image/png';
14312
+ }
14313
+ if (normalizedPath.endsWith('.jpg') || normalizedPath.endsWith('.jpeg')) {
14314
+ return 'image/jpeg';
14315
+ }
14316
+ if (normalizedPath.endsWith('.webp')) {
14317
+ return 'image/webp';
14318
+ }
14319
+ if (normalizedPath.endsWith('.gif')) {
14320
+ return 'image/gif';
14321
+ }
14322
+ return undefined;
14323
+ }
14324
+ async function readLocalFileBytes(filePath) {
14325
+ try {
14326
+ const dynamicImport = new Function('specifier', 'return import(specifier);');
14327
+ const { readFile } = await dynamicImport('node:fs/promises');
14328
+ return await readFile(filePath);
14329
+ }
14330
+ catch {
14331
+ throw new Error('Local image file uploads require a Node.js runtime with access to node:fs/promises');
14332
+ }
14333
+ }
14334
+ async function uploadVisionFileFromPath(openai, filePath, filename, mimeType) {
14335
+ const fileBytes = await readLocalFileBytes(filePath);
14336
+ const resolvedFilename = filename || getFilenameFromPath(filePath);
14337
+ const resolvedMimeType = mimeType || inferImageMimeType(filePath);
14338
+ const uploadableFile = await OpenAI.toFile(fileBytes, resolvedFilename, resolvedMimeType ? { type: resolvedMimeType } : undefined);
14339
+ const uploadedFile = await openai.files.create({
14340
+ file: uploadableFile,
14341
+ purpose: 'vision',
14342
+ });
14343
+ return uploadedFile.id;
14344
+ }
14345
+ function collectImageInputs(image, images, imageBase64) {
14346
+ const collectedImages = [];
14347
+ if (image) {
14348
+ collectedImages.push(image);
14349
+ }
14350
+ if (images && images.length > 0) {
14351
+ collectedImages.push(...images);
14352
+ }
14353
+ if (imageBase64) {
14354
+ collectedImages.push({ base64: imageBase64 });
14355
+ }
14356
+ return collectedImages;
14357
+ }
14358
+ function hasLocalFileImage(images) {
14359
+ return images.some((image) => 'filePath' in image);
14360
+ }
14361
+ async function buildImageContentPart(image, defaultDetail, openai) {
14362
+ const detail = image.detail ?? defaultDetail;
14363
+ if ('url' in image) {
14364
+ return {
14365
+ type: 'input_image',
14366
+ detail,
14367
+ image_url: image.url,
14368
+ };
14369
+ }
14370
+ if ('dataUrl' in image) {
14371
+ return {
14372
+ type: 'input_image',
14373
+ detail,
14374
+ image_url: image.dataUrl,
14375
+ };
14376
+ }
14377
+ if ('base64' in image) {
14378
+ return {
14379
+ type: 'input_image',
14380
+ detail,
14381
+ image_url: normalizeImageDataUrl(image.base64, image.mimeType),
14382
+ };
14383
+ }
14384
+ if ('fileId' in image) {
14385
+ return {
14386
+ type: 'input_image',
14387
+ detail,
14388
+ file_id: image.fileId,
14389
+ };
14390
+ }
14391
+ if (!openai) {
14392
+ throw new Error('A local image file path was provided, but no OpenAI client is available to upload it');
14393
+ }
14394
+ const fileId = await uploadVisionFileFromPath(openai, image.filePath, image.filename, image.mimeType);
14395
+ return {
14396
+ type: 'input_image',
14397
+ detail,
14398
+ file_id: fileId,
14399
+ };
14400
+ }
14401
+ async function buildPromptContent(input, imageInputs, imageDetail, openai) {
14402
+ if (imageInputs.length === 0) {
14403
+ return input;
14404
+ }
14405
+ const imageContent = await Promise.all(imageInputs.map((image) => buildImageContentPart(image, imageDetail, openai)));
14406
+ return [{ type: 'input_text', text: input }, ...imageContent];
14407
+ }
14408
+ async function uploadVisionFile(filePath, options = {}) {
14409
+ const resolvedApiKey = resolveOpenAIApiKey(options.apiKey);
14410
+ const openai = createOpenAIClient(resolvedApiKey);
14411
+ return await uploadVisionFileFromPath(openai, filePath, options.filename, options.mimeType);
14412
+ }
14283
14413
  /**
14284
14414
  * Makes a call to OpenAI's Responses API for more advanced use cases with built-in tools.
14285
14415
  *
@@ -14311,13 +14441,8 @@ const makeResponsesAPICall = async (input, options = {}) => {
14311
14441
  if (!isOpenAIModel(normalizedModel)) {
14312
14442
  throw new Error(`Unsupported OpenAI model: ${normalizedModel}. Please use a supported GPT-5 model.`);
14313
14443
  }
14314
- const apiKey = options.apiKey || process.env.OPENAI_API_KEY;
14315
- if (!apiKey) {
14316
- throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
14317
- }
14318
- const openai = new OpenAI({
14319
- apiKey: apiKey,
14320
- });
14444
+ const apiKey = resolveOpenAIApiKey(options.apiKey);
14445
+ const openai = createOpenAIClient(apiKey);
14321
14446
  // Remove apiKey from options before creating request body
14322
14447
  const { apiKey: _, model: __, ...cleanOptions } = options;
14323
14448
  const requestBody = {
@@ -14428,12 +14553,16 @@ const makeResponsesAPICall = async (input, options = {}) => {
14428
14553
  * });
14429
14554
  */
14430
14555
  async function makeLLMCall(input, options = {}) {
14431
- const { apiKey, model = DEFAULT_MODEL, responseFormat = 'text', schema, schemaName = 'response', schemaDescription, schemaStrict, tools, useCodeInterpreter = false, useWebSearch = false, imageBase64, imageDetail = 'high', context, } = options;
14556
+ const { apiKey, model = DEFAULT_MODEL, responseFormat = 'text', schema, schemaName = 'response', schemaDescription, schemaStrict, tools, useCodeInterpreter = false, useWebSearch = false, image, images, imageBase64, imageDetail = 'high', context, } = options;
14432
14557
  // Validate model
14433
14558
  const normalizedModel = normalizeModelName(model);
14434
14559
  if (!isOpenAIModel(normalizedModel)) {
14435
14560
  throw new Error(`Unsupported OpenAI model: ${normalizedModel}. Please use a supported GPT-5 model.`);
14436
14561
  }
14562
+ const resolvedApiKey = resolveOpenAIApiKey(apiKey);
14563
+ const imageInputs = collectImageInputs(image, images, imageBase64);
14564
+ const openai = hasLocalFileImage(imageInputs) ? createOpenAIClient(resolvedApiKey) : undefined;
14565
+ const promptContent = await buildPromptContent(input, imageInputs, imageDetail, openai);
14437
14566
  // Process input for conversation context and image analysis
14438
14567
  let processedInput;
14439
14568
  if (context && context.length > 0) {
@@ -14448,18 +14577,11 @@ async function makeLLMCall(input, options = {}) {
14448
14577
  });
14449
14578
  }
14450
14579
  // Add current input message
14451
- if (imageBase64) {
14452
- // Current message includes both text and image
14580
+ if (imageInputs.length > 0) {
14581
+ // Current message includes text and one or more images
14453
14582
  conversationMessages.push({
14454
14583
  role: 'user',
14455
- content: [
14456
- { type: 'input_text', text: input },
14457
- {
14458
- type: 'input_image',
14459
- detail: imageDetail,
14460
- image_url: imageBase64.startsWith('data:') ? imageBase64 : `data:image/webp;base64,${imageBase64}`,
14461
- },
14462
- ],
14584
+ content: promptContent,
14463
14585
  type: 'message',
14464
14586
  });
14465
14587
  }
@@ -14473,19 +14595,12 @@ async function makeLLMCall(input, options = {}) {
14473
14595
  }
14474
14596
  processedInput = conversationMessages;
14475
14597
  }
14476
- else if (imageBase64) {
14477
- // No context, but has image - use the original image logic
14598
+ else if (imageInputs.length > 0) {
14599
+ // No context, but has image(s)
14478
14600
  processedInput = [
14479
14601
  {
14480
14602
  role: 'user',
14481
- content: [
14482
- { type: 'input_text', text: input },
14483
- {
14484
- type: 'input_image',
14485
- detail: imageDetail,
14486
- image_url: imageBase64.startsWith('data:') ? imageBase64 : `data:image/webp;base64,${imageBase64}`,
14487
- },
14488
- ],
14603
+ content: promptContent,
14489
14604
  type: 'message',
14490
14605
  },
14491
14606
  ];
@@ -14496,7 +14611,7 @@ async function makeLLMCall(input, options = {}) {
14496
14611
  }
14497
14612
  // Build the options object for makeResponsesAPICall
14498
14613
  let responsesOptions = {
14499
- apiKey,
14614
+ apiKey: resolvedApiKey,
14500
14615
  model: normalizedModel,
14501
14616
  parallel_tool_calls: false,
14502
14617
  tools,
@@ -23638,6 +23753,7 @@ const disco = {
23638
23753
  },
23639
23754
  llm: {
23640
23755
  call: makeLLMCall,
23756
+ uploadVisionFile: uploadVisionFile,
23641
23757
  seek: makeDeepseekCall,
23642
23758
  images: makeImagesCall,
23643
23759
  open: makeOpenRouterCall,
@@ -23678,4 +23794,5 @@ exports.isOpenAIImageModel = isOpenAIImageModel;
23678
23794
  exports.isOpenAIModel = isOpenAIModel;
23679
23795
  exports.isOpenRouterModel = isOpenRouterModel;
23680
23796
  exports.supportsTemperature = supportsTemperature;
23797
+ exports.uploadVisionFile = uploadVisionFile;
23681
23798
  //# sourceMappingURL=index.cjs.map