n8n-nodes-apexapi 0.1.1 → 0.1.3
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.
|
@@ -111,6 +111,34 @@ class ApexApi {
|
|
|
111
111
|
default: '',
|
|
112
112
|
required: true,
|
|
113
113
|
},
|
|
114
|
+
{
|
|
115
|
+
displayName: 'Size',
|
|
116
|
+
name: 'imageSize',
|
|
117
|
+
type: 'options',
|
|
118
|
+
displayOptions: { show: { resource: ['image'] } },
|
|
119
|
+
options: [
|
|
120
|
+
{ name: 'Square 1:1 — 1024×1024', value: '1024x1024' },
|
|
121
|
+
{ name: 'Square 1:1 — 1080×1080 (Instagram post)', value: '1080x1080' },
|
|
122
|
+
{ name: 'Portrait 9:16 — 1080×1920 (Story / Reel / TikTok)', value: '1080x1920' },
|
|
123
|
+
{ name: 'Landscape 16:9 — 1920×1080 (YouTube / X)', value: '1920x1080' },
|
|
124
|
+
{ name: 'Landscape 1.91:1 — 1200×630 (Facebook / link share)', value: '1200x630' },
|
|
125
|
+
{ name: 'Portrait 4:5 — 1080×1350 (Instagram portrait)', value: '1080x1350' },
|
|
126
|
+
{ name: 'Portrait 2:3 — 1024×1536', value: '1024x1536' },
|
|
127
|
+
{ name: 'Landscape 3:2 — 1536×1024', value: '1536x1024' },
|
|
128
|
+
{ name: 'Custom (WIDTHxHEIGHT)…', value: 'custom' },
|
|
129
|
+
],
|
|
130
|
+
default: '1024x1024',
|
|
131
|
+
description: 'Output resolution / aspect ratio. Note: some models only support specific sizes. Max 4096 per side.',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
displayName: 'Custom Size',
|
|
135
|
+
name: 'customSize',
|
|
136
|
+
type: 'string',
|
|
137
|
+
placeholder: '1024x1024',
|
|
138
|
+
displayOptions: { show: { resource: ['image'], imageSize: ['custom'] } },
|
|
139
|
+
default: '1024x1024',
|
|
140
|
+
description: 'Width×height in pixels, e.g. 1024x1024. Each side 1–4096.',
|
|
141
|
+
},
|
|
114
142
|
{
|
|
115
143
|
displayName: 'Options',
|
|
116
144
|
name: 'options',
|
|
@@ -118,10 +146,7 @@ class ApexApi {
|
|
|
118
146
|
placeholder: 'Add Option',
|
|
119
147
|
displayOptions: { show: { resource: ['image'] } },
|
|
120
148
|
default: {},
|
|
121
|
-
options: [
|
|
122
|
-
{ displayName: 'Size', name: 'size', type: 'string', default: '1024x1024' },
|
|
123
|
-
{ displayName: 'Number of Images', name: 'n', type: 'number', default: 1 },
|
|
124
|
-
],
|
|
149
|
+
options: [{ displayName: 'Number of Images', name: 'n', type: 'number', default: 1 }],
|
|
125
150
|
},
|
|
126
151
|
// ---- Video ----
|
|
127
152
|
{
|
|
@@ -182,7 +207,7 @@ class ApexApi {
|
|
|
182
207
|
this.methods = {
|
|
183
208
|
loadOptions: {
|
|
184
209
|
async getChatModels() {
|
|
185
|
-
return (0, loadOptions_1.loadModels)(this, '
|
|
210
|
+
return (0, loadOptions_1.loadModels)(this, 'chat');
|
|
186
211
|
},
|
|
187
212
|
async getImageModels() {
|
|
188
213
|
return (0, loadOptions_1.loadModels)(this, 'image');
|
|
@@ -217,15 +242,33 @@ class ApexApi {
|
|
|
217
242
|
}
|
|
218
243
|
else if (resource === 'image') {
|
|
219
244
|
const options = this.getNodeParameter('options', i, {});
|
|
245
|
+
const sizeChoice = this.getNodeParameter('imageSize', i, '1024x1024');
|
|
246
|
+
const size = sizeChoice === 'custom'
|
|
247
|
+
? this.getNodeParameter('customSize', i, '1024x1024')
|
|
248
|
+
: sizeChoice;
|
|
220
249
|
const body = (0, image_1.buildImageBody)({
|
|
221
250
|
model: this.getNodeParameter('model', i),
|
|
222
251
|
prompt: this.getNodeParameter('prompt', i),
|
|
223
|
-
size
|
|
252
|
+
size,
|
|
224
253
|
n: options.n,
|
|
225
254
|
});
|
|
226
255
|
const resp = await (0, transport_1.apexApiRequest)(ctx, 'POST', '/images/generations', body);
|
|
227
256
|
const parsed = (0, image_1.parseImageResponse)(resp);
|
|
228
257
|
json = { urls: parsed.urls, response: parsed.raw };
|
|
258
|
+
// Some providers (Vertex/Imagen) return inline base64 instead of a hosted
|
|
259
|
+
// link. Emit those as n8n binary so the next node can post/upload them
|
|
260
|
+
// directly — hosted-URL providers (fal/kie) just come through in `urls`.
|
|
261
|
+
const b64Images = parsed.images.filter((im) => im.base64);
|
|
262
|
+
if (b64Images.length > 0) {
|
|
263
|
+
const binary = {};
|
|
264
|
+
for (let k = 0; k < b64Images.length; k++) {
|
|
265
|
+
const im = b64Images[k];
|
|
266
|
+
const ext = (im.mimeType ?? 'image/png').split('/')[1] ?? 'png';
|
|
267
|
+
binary[k === 0 ? 'data' : `data${k}`] = await this.helpers.prepareBinaryData(Buffer.from(im.base64, 'base64'), `image_${k}.${ext}`, im.mimeType);
|
|
268
|
+
}
|
|
269
|
+
returnData.push({ json, binary, pairedItem: { item: i } });
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
229
272
|
}
|
|
230
273
|
else {
|
|
231
274
|
// video
|
package/dist/shared/image.js
CHANGED
|
@@ -10,11 +10,29 @@ function buildImageBody(params) {
|
|
|
10
10
|
body.n = params.n;
|
|
11
11
|
return body;
|
|
12
12
|
}
|
|
13
|
+
const DATA_URL_RE = /^data:([^;,]+);base64,(.+)$/;
|
|
14
|
+
/**
|
|
15
|
+
* Normalise the `/v1/images/generations` response. Providers return images two
|
|
16
|
+
* ways: a hosted URL (fal/kie) or inline base64 (Vertex/Imagen `data:` URLs, or
|
|
17
|
+
* `b64_json`). `urls` holds only real links; `images` holds every image with the
|
|
18
|
+
* base64 decoded out of `data:` URLs so the node can emit it as n8n binary.
|
|
19
|
+
*/
|
|
13
20
|
function parseImageResponse(resp) {
|
|
14
21
|
const r = resp;
|
|
15
22
|
const data = Array.isArray(r?.data) ? r.data : [];
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
const images = [];
|
|
24
|
+
for (const d of data) {
|
|
25
|
+
if (typeof d?.url === 'string') {
|
|
26
|
+
const m = d.url.match(DATA_URL_RE);
|
|
27
|
+
if (m)
|
|
28
|
+
images.push({ base64: m[2], mimeType: m[1] });
|
|
29
|
+
else
|
|
30
|
+
images.push({ url: d.url });
|
|
31
|
+
}
|
|
32
|
+
else if (typeof d?.b64_json === 'string') {
|
|
33
|
+
images.push({ base64: d.b64_json, mimeType: 'image/png' });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const urls = images.map((i) => i.url).filter((u) => typeof u === 'string');
|
|
37
|
+
return { urls, images, raw: resp };
|
|
20
38
|
}
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.loadModels = loadModels;
|
|
4
|
-
const
|
|
4
|
+
const constants_1 = require("./constants");
|
|
5
5
|
const models_1 = require("./models");
|
|
6
|
+
/**
|
|
7
|
+
* Load the model dropdown options for a resource. The `/v1/models` endpoint is
|
|
8
|
+
* PUBLIC, so this uses the unauthenticated request helper and works even before
|
|
9
|
+
* a credential is attached — that avoids n8n's "error fetching options" when the
|
|
10
|
+
* key hasn't been entered yet. The base URL comes from the credential when
|
|
11
|
+
* present, otherwise the default.
|
|
12
|
+
*/
|
|
6
13
|
async function loadModels(ctx, modelType) {
|
|
7
|
-
|
|
14
|
+
let baseUrl = constants_1.DEFAULT_BASE_URL;
|
|
15
|
+
try {
|
|
16
|
+
const creds = await ctx.getCredentials(constants_1.CREDENTIAL_NAME);
|
|
17
|
+
if (creds && creds.baseUrl)
|
|
18
|
+
baseUrl = String(creds.baseUrl);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// No credential attached yet — /models is public, so fall back to the
|
|
22
|
+
// default base URL and load the list anyway.
|
|
23
|
+
}
|
|
24
|
+
baseUrl = baseUrl.replace(/\/+$/, '');
|
|
25
|
+
const http = ctx.helpers.httpRequest;
|
|
26
|
+
if (!http)
|
|
27
|
+
return [];
|
|
28
|
+
const resp = (await http({ method: 'GET', url: `${baseUrl}/models`, json: true }));
|
|
8
29
|
const data = Array.isArray(resp?.data) ? resp.data : [];
|
|
9
30
|
return (0, models_1.mapModelsToOptions)(data, modelType);
|
|
10
31
|
}
|
package/dist/shared/models.js
CHANGED
|
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.mapModelsToOptions = mapModelsToOptions;
|
|
4
4
|
/**
|
|
5
5
|
* Map the `/v1/models` `data` array to n8n dropdown options. When `modelType`
|
|
6
|
-
* is given, keep only models of that type (
|
|
7
|
-
* nothing, fall back to all models
|
|
6
|
+
* is given, keep only models of that type (chat/image/video — matching the live
|
|
7
|
+
* /v1/models `type` field); if that leaves nothing, fall back to all models
|
|
8
|
+
* rather than showing an empty dropdown.
|
|
8
9
|
*/
|
|
9
10
|
function mapModelsToOptions(models, modelType) {
|
|
10
11
|
const filtered = modelType ? models.filter((m) => m.type === modelType) : models;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-apexapi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "n8n community node for ApexApi — call 14 AI providers (OpenAI, Anthropic, Google, Mistral, and more) through one OpenAI-compatible API.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"n8n-workflow": "^1.82.0",
|
|
48
48
|
"typescript": "^5.9.3",
|
|
49
|
-
"vitest": "^3.2.4"
|
|
49
|
+
"vitest": "^3.2.4",
|
|
50
|
+
"@types/node": "^22.0.0"
|
|
50
51
|
}
|
|
51
52
|
}
|