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