@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.
@@ -12254,7 +12254,7 @@ function zodTextFormat(zodObject, name, props) {
12254
12254
  }, (content) => zodObject.parse(JSON.parse(content)));
12255
12255
  }
12256
12256
 
12257
- const DEFAULT_MODEL = 'gpt-5-mini';
12257
+ const DEFAULT_MODEL = 'gpt-5.4-mini';
12258
12258
  const GPT_5_HIGH_CONTEXT_THRESHOLD_TOKENS = 272_000;
12259
12259
  const GPT_5_HIGH_CONTEXT_INPUT_MULTIPLIER = 2;
12260
12260
  const GPT_5_HIGH_CONTEXT_OUTPUT_MULTIPLIER = 1.5;
@@ -12892,6 +12892,7 @@ async function parseResponse(content, responseFormat) {
12892
12892
  }
12893
12893
 
12894
12894
  // llm-openai.ts
12895
+ const DEFAULT_IMAGE_MIME_TYPE = 'image/webp';
12895
12896
  /**
12896
12897
  * Checks if the given direct OpenAI model supports the temperature parameter.
12897
12898
  * @param model The model to check.
@@ -12926,6 +12927,135 @@ function buildJsonSchemaFormat(schema, schemaName, schemaDescription, schemaStri
12926
12927
  ...(schemaStrict !== undefined ? { strict: schemaStrict } : {}),
12927
12928
  };
12928
12929
  }
12930
+ function resolveOpenAIApiKey(apiKey) {
12931
+ const resolvedApiKey = apiKey || process.env.OPENAI_API_KEY;
12932
+ if (!resolvedApiKey) {
12933
+ throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
12934
+ }
12935
+ return resolvedApiKey;
12936
+ }
12937
+ function createOpenAIClient(apiKey) {
12938
+ return new OpenAI({
12939
+ apiKey,
12940
+ });
12941
+ }
12942
+ function normalizeImageDataUrl(imageData, mimeType = DEFAULT_IMAGE_MIME_TYPE) {
12943
+ return imageData.startsWith('data:') ? imageData : `data:${mimeType};base64,${imageData}`;
12944
+ }
12945
+ function getFilenameFromPath(filePath) {
12946
+ const normalizedPath = filePath.replace(/\\/g, '/');
12947
+ const pathSegments = normalizedPath.split('/');
12948
+ const filename = pathSegments[pathSegments.length - 1];
12949
+ if (!filename) {
12950
+ throw new Error(`Could not determine a filename from file path: ${filePath}`);
12951
+ }
12952
+ return filename;
12953
+ }
12954
+ function inferImageMimeType(filePath) {
12955
+ const normalizedPath = filePath.toLowerCase();
12956
+ if (normalizedPath.endsWith('.png')) {
12957
+ return 'image/png';
12958
+ }
12959
+ if (normalizedPath.endsWith('.jpg') || normalizedPath.endsWith('.jpeg')) {
12960
+ return 'image/jpeg';
12961
+ }
12962
+ if (normalizedPath.endsWith('.webp')) {
12963
+ return 'image/webp';
12964
+ }
12965
+ if (normalizedPath.endsWith('.gif')) {
12966
+ return 'image/gif';
12967
+ }
12968
+ return undefined;
12969
+ }
12970
+ async function readLocalFileBytes(filePath) {
12971
+ try {
12972
+ const dynamicImport = new Function('specifier', 'return import(specifier);');
12973
+ const { readFile } = await dynamicImport('node:fs/promises');
12974
+ return await readFile(filePath);
12975
+ }
12976
+ catch {
12977
+ throw new Error('Local image file uploads require a Node.js runtime with access to node:fs/promises');
12978
+ }
12979
+ }
12980
+ async function uploadVisionFileFromPath(openai, filePath, filename, mimeType) {
12981
+ const fileBytes = await readLocalFileBytes(filePath);
12982
+ const resolvedFilename = filename || getFilenameFromPath(filePath);
12983
+ const resolvedMimeType = mimeType || inferImageMimeType(filePath);
12984
+ const uploadableFile = await OpenAI.toFile(fileBytes, resolvedFilename, resolvedMimeType ? { type: resolvedMimeType } : undefined);
12985
+ const uploadedFile = await openai.files.create({
12986
+ file: uploadableFile,
12987
+ purpose: 'vision',
12988
+ });
12989
+ return uploadedFile.id;
12990
+ }
12991
+ function collectImageInputs(image, images, imageBase64) {
12992
+ const collectedImages = [];
12993
+ if (image) {
12994
+ collectedImages.push(image);
12995
+ }
12996
+ if (images && images.length > 0) {
12997
+ collectedImages.push(...images);
12998
+ }
12999
+ if (imageBase64) {
13000
+ collectedImages.push({ base64: imageBase64 });
13001
+ }
13002
+ return collectedImages;
13003
+ }
13004
+ function hasLocalFileImage(images) {
13005
+ return images.some((image) => 'filePath' in image);
13006
+ }
13007
+ async function buildImageContentPart(image, defaultDetail, openai) {
13008
+ const detail = image.detail ?? defaultDetail;
13009
+ if ('url' in image) {
13010
+ return {
13011
+ type: 'input_image',
13012
+ detail,
13013
+ image_url: image.url,
13014
+ };
13015
+ }
13016
+ if ('dataUrl' in image) {
13017
+ return {
13018
+ type: 'input_image',
13019
+ detail,
13020
+ image_url: image.dataUrl,
13021
+ };
13022
+ }
13023
+ if ('base64' in image) {
13024
+ return {
13025
+ type: 'input_image',
13026
+ detail,
13027
+ image_url: normalizeImageDataUrl(image.base64, image.mimeType),
13028
+ };
13029
+ }
13030
+ if ('fileId' in image) {
13031
+ return {
13032
+ type: 'input_image',
13033
+ detail,
13034
+ file_id: image.fileId,
13035
+ };
13036
+ }
13037
+ if (!openai) {
13038
+ throw new Error('A local image file path was provided, but no OpenAI client is available to upload it');
13039
+ }
13040
+ const fileId = await uploadVisionFileFromPath(openai, image.filePath, image.filename, image.mimeType);
13041
+ return {
13042
+ type: 'input_image',
13043
+ detail,
13044
+ file_id: fileId,
13045
+ };
13046
+ }
13047
+ async function buildPromptContent(input, imageInputs, imageDetail, openai) {
13048
+ if (imageInputs.length === 0) {
13049
+ return input;
13050
+ }
13051
+ const imageContent = await Promise.all(imageInputs.map((image) => buildImageContentPart(image, imageDetail, openai)));
13052
+ return [{ type: 'input_text', text: input }, ...imageContent];
13053
+ }
13054
+ async function uploadVisionFile(filePath, options = {}) {
13055
+ const resolvedApiKey = resolveOpenAIApiKey(options.apiKey);
13056
+ const openai = createOpenAIClient(resolvedApiKey);
13057
+ return await uploadVisionFileFromPath(openai, filePath, options.filename, options.mimeType);
13058
+ }
12929
13059
  /**
12930
13060
  * Makes a call to OpenAI's Responses API for more advanced use cases with built-in tools.
12931
13061
  *
@@ -12957,13 +13087,8 @@ const makeResponsesAPICall = async (input, options = {}) => {
12957
13087
  if (!isOpenAIModel(normalizedModel)) {
12958
13088
  throw new Error(`Unsupported OpenAI model: ${normalizedModel}. Please use a supported GPT-5 model.`);
12959
13089
  }
12960
- const apiKey = options.apiKey || process.env.OPENAI_API_KEY;
12961
- if (!apiKey) {
12962
- throw new Error('OpenAI API key is not provided and OPENAI_API_KEY environment variable is not set');
12963
- }
12964
- const openai = new OpenAI({
12965
- apiKey: apiKey,
12966
- });
13090
+ const apiKey = resolveOpenAIApiKey(options.apiKey);
13091
+ const openai = createOpenAIClient(apiKey);
12967
13092
  // Remove apiKey from options before creating request body
12968
13093
  const { apiKey: _, model: __, ...cleanOptions } = options;
12969
13094
  const requestBody = {
@@ -13074,12 +13199,16 @@ const makeResponsesAPICall = async (input, options = {}) => {
13074
13199
  * });
13075
13200
  */
13076
13201
  async function makeLLMCall(input, options = {}) {
13077
- const { apiKey, model = DEFAULT_MODEL, responseFormat = 'text', schema, schemaName = 'response', schemaDescription, schemaStrict, tools, useCodeInterpreter = false, useWebSearch = false, imageBase64, imageDetail = 'high', context, } = options;
13202
+ const { apiKey, model = DEFAULT_MODEL, responseFormat = 'text', schema, schemaName = 'response', schemaDescription, schemaStrict, tools, useCodeInterpreter = false, useWebSearch = false, image, images, imageBase64, imageDetail = 'high', context, } = options;
13078
13203
  // Validate model
13079
13204
  const normalizedModel = normalizeModelName(model);
13080
13205
  if (!isOpenAIModel(normalizedModel)) {
13081
13206
  throw new Error(`Unsupported OpenAI model: ${normalizedModel}. Please use a supported GPT-5 model.`);
13082
13207
  }
13208
+ const resolvedApiKey = resolveOpenAIApiKey(apiKey);
13209
+ const imageInputs = collectImageInputs(image, images, imageBase64);
13210
+ const openai = hasLocalFileImage(imageInputs) ? createOpenAIClient(resolvedApiKey) : undefined;
13211
+ const promptContent = await buildPromptContent(input, imageInputs, imageDetail, openai);
13083
13212
  // Process input for conversation context and image analysis
13084
13213
  let processedInput;
13085
13214
  if (context && context.length > 0) {
@@ -13094,18 +13223,11 @@ async function makeLLMCall(input, options = {}) {
13094
13223
  });
13095
13224
  }
13096
13225
  // Add current input message
13097
- if (imageBase64) {
13098
- // Current message includes both text and image
13226
+ if (imageInputs.length > 0) {
13227
+ // Current message includes text and one or more images
13099
13228
  conversationMessages.push({
13100
13229
  role: 'user',
13101
- content: [
13102
- { type: 'input_text', text: input },
13103
- {
13104
- type: 'input_image',
13105
- detail: imageDetail,
13106
- image_url: imageBase64.startsWith('data:') ? imageBase64 : `data:image/webp;base64,${imageBase64}`,
13107
- },
13108
- ],
13230
+ content: promptContent,
13109
13231
  type: 'message',
13110
13232
  });
13111
13233
  }
@@ -13119,19 +13241,12 @@ async function makeLLMCall(input, options = {}) {
13119
13241
  }
13120
13242
  processedInput = conversationMessages;
13121
13243
  }
13122
- else if (imageBase64) {
13123
- // No context, but has image - use the original image logic
13244
+ else if (imageInputs.length > 0) {
13245
+ // No context, but has image(s)
13124
13246
  processedInput = [
13125
13247
  {
13126
13248
  role: 'user',
13127
- content: [
13128
- { type: 'input_text', text: input },
13129
- {
13130
- type: 'input_image',
13131
- detail: imageDetail,
13132
- image_url: imageBase64.startsWith('data:') ? imageBase64 : `data:image/webp;base64,${imageBase64}`,
13133
- },
13134
- ],
13249
+ content: promptContent,
13135
13250
  type: 'message',
13136
13251
  },
13137
13252
  ];
@@ -13142,7 +13257,7 @@ async function makeLLMCall(input, options = {}) {
13142
13257
  }
13143
13258
  // Build the options object for makeResponsesAPICall
13144
13259
  let responsesOptions = {
13145
- apiKey,
13260
+ apiKey: resolvedApiKey,
13146
13261
  model: normalizedModel,
13147
13262
  parallel_tool_calls: false,
13148
13263
  tools,
@@ -14628,6 +14743,7 @@ const disco = {
14628
14743
  types: Types,
14629
14744
  llm: {
14630
14745
  call: makeLLMCall,
14746
+ uploadVisionFile: uploadVisionFile,
14631
14747
  seek: makeDeepseekCall,
14632
14748
  images: makeImagesCall,
14633
14749
  },
@@ -14642,4 +14758,5 @@ exports.isOpenAIImageModel = isOpenAIImageModel;
14642
14758
  exports.isOpenAIModel = isOpenAIModel;
14643
14759
  exports.isOpenRouterModel = isOpenRouterModel;
14644
14760
  exports.supportsTemperature = supportsTemperature;
14761
+ exports.uploadVisionFile = uploadVisionFile;
14645
14762
  //# sourceMappingURL=index-frontend.cjs.map