alexa-ai 2.1.2 → 2.2.1
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/CHANGELOG.md +37 -0
- package/README.md +48 -11
- package/examples/text2img-standalone.js +159 -0
- package/package.json +1 -1
- package/src/AlexaAI.js +83 -18
- package/src/core/Config.js +14 -2
- package/src/core/DeepAIClient.js +119 -11
- package/test/run-tests.js +79 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ All notable changes to `alexa-ai` are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [2.2.1] — 2026-09-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Device identity: the client keeps a stable random `deepai_device_id`
|
|
11
|
+
(32 bytes, base64url) and sends it as a cookie; anonymous `/api/*`
|
|
12
|
+
generation is rate-limited per device. Override with `deviceId` /
|
|
13
|
+
`DEEPAI_DEVICE_ID`.
|
|
14
|
+
- `examples/text2img-standalone.js` — zero-dependency CLI for anonymous
|
|
15
|
+
image generation using the full browser request dialect.
|
|
16
|
+
|
|
17
|
+
## [2.2.0] — 2026-09-07
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Image generation with anonymous `tryit-…` keys. The key's hex part is a
|
|
21
|
+
deterministic hash over the User-Agent
|
|
22
|
+
(`H(UA + H(UA + H(UA + digits + SALT)))`, see `DeepAIClient._islandHash`);
|
|
23
|
+
`generateTryItKey(userAgent)` now derives it correctly, and `headers()`
|
|
24
|
+
mints a fresh key per request because anonymous keys are single-use.
|
|
25
|
+
- `generateImage()` now sends the browser dialect for anonymous requests
|
|
26
|
+
(`generation_source`, `width`/`height` mapped from `aspectRatio`,
|
|
27
|
+
`image_generator_version`, `quality`).
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Anonymous browser-shaped retry when a registered non-Pro key is refused
|
|
31
|
+
(result `via: 'anonymous'`, `runApiWithTryItKey(name, fields)`); disable
|
|
32
|
+
with `{ noAnonymousFallback: true }`.
|
|
33
|
+
- `DEEPAI_KEY` env alias alongside `DEEPAI_API_KEY`.
|
|
34
|
+
- `DeepAIClient.isTryItKey(key)`.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- Extended refusal classification ("Pro members", "model only available…",
|
|
38
|
+
"insufficient_credits", …) so quota errors rotate keys and report
|
|
39
|
+
`DEEPAI_QUOTA_EXCEEDED` correctly.
|
|
40
|
+
- `share_url` preferred over `output_url` in generated-image results.
|
|
41
|
+
- Default `userAgent` updated (the tryit key hash is computed over it; keep
|
|
42
|
+
`userAgent` stable if you persist keys).
|
|
43
|
+
|
|
7
44
|
## [2.1.2] — 2026-09-06
|
|
8
45
|
|
|
9
46
|
### Changed
|
package/README.md
CHANGED
|
@@ -427,14 +427,25 @@ await ai.deepai.runApi('waifu2x', { image: buffer }); // any /api/<name> e
|
|
|
427
427
|
|
|
428
428
|
All media arguments accept the same shapes as `chat({ image })`.
|
|
429
429
|
|
|
430
|
-
**`generateImage()` on a free key.**
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
430
|
+
**`generateImage()` on a free key.** Three routes are tried, in order:
|
|
431
|
+
|
|
432
|
+
1. `POST /api/text2img` with your key. Pro keys succeed here immediately.
|
|
433
|
+
With an anonymous `tryit-…` key the engine automatically sends the exact
|
|
434
|
+
browser dialect (`generation_source=chat`, `width`/`height` mapped from
|
|
435
|
+
`aspectRatio`, `image_generator_version=hd`, `quality=true`) and mints a
|
|
436
|
+
**fresh single-use key per request** — tryit keys are hash-validated
|
|
437
|
+
against the User-Agent and burned after one request.
|
|
438
|
+
2. If a registered (non-Pro) key is refused with `402 "Pro members in good
|
|
439
|
+
standing"`, one anonymous browser-shaped retry. (`noAnonymousFallback:
|
|
440
|
+
true` disables it.)
|
|
441
|
+
3. The legacy in-chat `generate_image` function call (kept for model
|
|
442
|
+
versions that still honour it).
|
|
443
|
+
|
|
444
|
+
The result reports the route that answered (`via: 'api' | 'anonymous' |
|
|
445
|
+
'chat'`). Options: `{ apiOnly }`, `{ chatToolOnly }`, `{ noAnonymousFallback }`,
|
|
446
|
+
`{ aspectRatio: '16:9' }`, and `width` / `height` / `image_generator_version`
|
|
447
|
+
for the API. The returned URL prefers `share_url` (public, stable) over
|
|
448
|
+
`output_url`, matching the browser client.
|
|
438
449
|
|
|
439
450
|
**`summarizeText()`** follows the same pattern — `/api/summarization` first, a
|
|
440
451
|
stateless chat request as the fallback.
|
|
@@ -841,10 +852,36 @@ error), and the reply came from DeepAI's own web access. Point
|
|
|
841
852
|
`webSearchProvider` at a search API you control if the public endpoints are
|
|
842
853
|
blocked where the bot runs.
|
|
843
854
|
|
|
855
|
+
**Works in the browser playground but fails in Postman / Node.js** — three
|
|
856
|
+
separate requirements:
|
|
857
|
+
|
|
858
|
+
1. **Anonymous keys are single-use and User-Agent-bound.** Each key is
|
|
859
|
+
`tryit-<digits>-<hash>` with
|
|
860
|
+
`hash = H(UA + H(UA + H(UA + digits + "hackers_become_a_little_stinkier_every_time_they_hack")))`,
|
|
861
|
+
validated against the request's `User-Agent` header. A reused key or
|
|
862
|
+
random hex → `401 "Please pass a valid Api-Key"`. A fresh, correctly
|
|
863
|
+
hashed key must be minted per request — `headers()` does this
|
|
864
|
+
automatically for `tryit-…` keys.
|
|
865
|
+
2. **The body must be `multipart/form-data`** (never JSON), with
|
|
866
|
+
`generation_source=img` (model page) or `generation_source=chat` +
|
|
867
|
+
`width`/`height`/`image_generator_version=hd`/`quality=true` (chat page).
|
|
868
|
+
Never set `Content-Type` manually — the multipart boundary is generated.
|
|
869
|
+
3. **`Origin: https://deepai.org` is required**, and anonymous generation is
|
|
870
|
+
refused from datacenter/VPN IPs (`401 "Please try this model on deepai.org"`) —
|
|
871
|
+
run from a residential IP. Passing `deviceId` (the `deepai_device_id`
|
|
872
|
+
cookie value) also helps the per-device quota.
|
|
873
|
+
|
|
874
|
+
`examples/text2img-standalone.js` is a zero-dependency CLI implementing the
|
|
875
|
+
full recipe.
|
|
876
|
+
|
|
844
877
|
**`generateImage()` returns `{ ok: false, error: 'DEEPAI_QUOTA_EXCEEDED' }`**
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
878
|
+
`/api/text2img` is Pro-only for registered keys ("APIs are only available for
|
|
879
|
+
Pro members in good standing"), the anonymous browser-shaped retry was
|
|
880
|
+
refused too (DeepAI soft-blocks free image generation from datacenter IPs
|
|
881
|
+
with "Please try this model on deepai.org" — run the bot from a residential
|
|
882
|
+
IP), and the in-chat tool produced no image. Add a Pro key, more keys
|
|
883
|
+
(`keys: [...]`), or run from a non-datacenter IP. `message` carries DeepAI's
|
|
884
|
+
exact wording for each attempted route.
|
|
848
885
|
|
|
849
886
|
**Photos are answered with "I can't view images right now"**
|
|
850
887
|
Native vision needs a paid DeepAI key; on a free key only text inside the
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* text2img-standalone.js — zero-dependency DeepAI text-to-image CLI.
|
|
5
|
+
*
|
|
6
|
+
* Speaks the anonymous browser dialect: a fresh single-use `tryit-…` key
|
|
7
|
+
* (hashed over the User-Agent) per run, browser-identical headers and a
|
|
8
|
+
* multipart/form-data body, plus a stable `deepai_device_id` cookie.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node examples/text2img-standalone.js "a cute orange cat"
|
|
12
|
+
* node examples/text2img-standalone.js "a cat" --out cat.jpg
|
|
13
|
+
* node examples/text2img-standalone.js "a cat" --aspect 16:9
|
|
14
|
+
* node examples/text2img-standalone.js "a cat" --device-id <cookieValue>
|
|
15
|
+
* node examples/text2img-standalone.js "a cat" --key <proKey>
|
|
16
|
+
*
|
|
17
|
+
* Requires Node.js 18+ (global fetch). Anonymous generation is refused
|
|
18
|
+
* from datacenter/VPN IPs; run from a residential network.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const API_URL = 'https://api.deepai.org/api/text2img';
|
|
22
|
+
const SALT = 'hackers_become_a_little_stinkier_every_time_they_hack';
|
|
23
|
+
// Keep this EXACT string in sync with the User-Agent header below — the key
|
|
24
|
+
// hash is computed over it and the server recomputes it from the request.
|
|
25
|
+
const USER_AGENT =
|
|
26
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Deterministic key hash (see DeepAIClient._islandHash in the engine).
|
|
30
|
+
// The integer/bit-level behaviour is intentional — do not simplify it.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
function islandHash(input) {
|
|
33
|
+
const a = [];
|
|
34
|
+
for (let b = 0; 64 > b; ) a[b] = 0 | (4294967296 * Math.sin(++b % Math.PI));
|
|
35
|
+
let d, e, f, g = [(d = 1732584193), (e = 4023233417), ~d, ~e], h = [];
|
|
36
|
+
const l = unescape(encodeURI(input)) + '';
|
|
37
|
+
let k = l.length;
|
|
38
|
+
let c = (--k / 4 + 2) | 15;
|
|
39
|
+
for (h[--c] = 8 * k; ~k; ) h[k >> 2] |= l.charCodeAt(k) << (8 * k--);
|
|
40
|
+
for (let b = 0, m = 0; b < c; b += 16) {
|
|
41
|
+
for (k = g; 64 > m; k = [ (f = k[3]), d + (((f = k[0] + [d & e | ~d & f, f & d | ~f & e, d ^ e ^ f, e ^ (d | ~f)][(k = m >> 4)] + a[m] + ~~h[b | [m, 5 * m + 1, 3 * m + 5, 7 * m][k] & 15]) << (k = [7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21][4 * k + (m++ % 4)])) | (f >>> -k)), d, e ]) {
|
|
42
|
+
d = k[1] | 0;
|
|
43
|
+
e = k[2];
|
|
44
|
+
}
|
|
45
|
+
for (m = 4; m; ) g[--m] += k[m];
|
|
46
|
+
}
|
|
47
|
+
let result = '';
|
|
48
|
+
for (let i = 0; 32 > i; ) result += ((g[i >> 3] >> 4 * (1 ^ i++)) & 15).toString(16);
|
|
49
|
+
return result.split('').reverse().join('');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Mint a fresh anonymous key, valid for exactly ONE request with USER_AGENT. */
|
|
53
|
+
function freshTryItKey(userAgent = USER_AGENT) {
|
|
54
|
+
const digits = String(Math.round(Math.random() * 100000000000));
|
|
55
|
+
const H = islandHash;
|
|
56
|
+
return `tryit-${digits}-${H(userAgent + H(userAgent + H(userAgent + digits + SALT)))}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Random device id, same shape the site sets as the deepai_device_id cookie. */
|
|
60
|
+
function randomDeviceId() {
|
|
61
|
+
return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// CLI
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
function parseArgs(argv) {
|
|
68
|
+
const opts = { prompt: '', out: null, key: null, deviceId: randomDeviceId(), aspect: null, genSource: 'img' };
|
|
69
|
+
for (let i = 0; i < argv.length; i++) {
|
|
70
|
+
const a = argv[i];
|
|
71
|
+
if (a === '--out') opts.out = argv[++i];
|
|
72
|
+
else if (a === '--key') opts.key = argv[++i];
|
|
73
|
+
else if (a === '--device-id') opts.deviceId = argv[++i];
|
|
74
|
+
else if (a === '--aspect') opts.aspect = argv[++i];
|
|
75
|
+
else if (a === '--chat-source') opts.genSource = 'chat';
|
|
76
|
+
else if (a === '--help' || a === '-h') opts.help = true;
|
|
77
|
+
else if (!opts.prompt) opts.prompt = a;
|
|
78
|
+
}
|
|
79
|
+
return opts;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const ASPECTS = { '16:9': [832, 448], '4:3': [768, 576], '1:1': [640, 640], '3:4': [576, 768], '9:16': [448, 832] };
|
|
83
|
+
|
|
84
|
+
async function main() {
|
|
85
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
86
|
+
if (opts.help || !opts.prompt) {
|
|
87
|
+
console.log('Usage: node examples/text2img-standalone.js "your prompt" [--out file.jpg] [--key PRO_KEY] [--device-id VALUE] [--aspect 16:9|1:1|9:16|4:3|3:4] [--chat-source]');
|
|
88
|
+
process.exit(opts.help ? 0 : 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---- body: multipart/form-data, NEVER JSON -----------------------------
|
|
92
|
+
const form = new FormData();
|
|
93
|
+
form.append('text', opts.prompt);
|
|
94
|
+
form.append('generation_source', opts.genSource); // 'img' = model page, 'chat' = chat page
|
|
95
|
+
if (opts.aspect && ASPECTS[opts.aspect]) {
|
|
96
|
+
// the chat-page dialect maps aspect ratios to pixel sizes
|
|
97
|
+
const [w, h] = ASPECTS[opts.aspect];
|
|
98
|
+
form.append('width', String(w));
|
|
99
|
+
form.append('height', String(h));
|
|
100
|
+
form.append('image_generator_version', 'hd');
|
|
101
|
+
form.append('quality', 'true');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- headers: fresh single-use anonymous key (or your Pro key) ---------
|
|
105
|
+
const apiKey = opts.key || freshTryItKey();
|
|
106
|
+
const headers = {
|
|
107
|
+
'api-key': apiKey,
|
|
108
|
+
'User-Agent': USER_AGENT, // MUST match the UA the key was hashed with
|
|
109
|
+
Origin: 'https://deepai.org',
|
|
110
|
+
Referer: 'https://deepai.org/machine-learning-model/text2img',
|
|
111
|
+
Accept: '*/*',
|
|
112
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
113
|
+
Cookie: `deepai_device_id=${opts.deviceId}`,
|
|
114
|
+
// NOTE: do NOT set Content-Type yourself — undici adds the multipart
|
|
115
|
+
// boundary. A manual Content-Type without the boundary is rejected.
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
console.log(`Prompt : ${opts.prompt}`);
|
|
119
|
+
console.log(`Key : ${opts.key ? '(registered key — needs Pro)' : apiKey + ' (fresh, single-use)'}`);
|
|
120
|
+
console.log('POST : ' + API_URL);
|
|
121
|
+
|
|
122
|
+
const res = await fetch(API_URL, { method: 'POST', headers, body: form });
|
|
123
|
+
const raw = await res.text();
|
|
124
|
+
let data = null;
|
|
125
|
+
try { data = JSON.parse(raw); } catch { /* non-JSON */ }
|
|
126
|
+
|
|
127
|
+
if (!res.ok || data?.err || (data?.status && !data.share_url && !data.output_url)) {
|
|
128
|
+
console.error(`\nFAILED HTTP ${res.status}`);
|
|
129
|
+
console.error(raw.slice(0, 500));
|
|
130
|
+
const s = String(data?.status || data?.err || '');
|
|
131
|
+
if (/valid Api-Key/i.test(s)) console.error('\n→ The key is invalid or already used. Keys are single-use and must be hashed for the exact User-Agent sent; this script mints a fresh one each run.');
|
|
132
|
+
else if (/try this model on deepai\.org/i.test(s)) console.error('\n→ DeepAI is refusing anonymous generation from your IP (datacenter/VPN) or the Origin header is missing. Run from a residential IP and keep the Origin/Referer headers.');
|
|
133
|
+
else if (/Pro members/i.test(s)) console.error('\n→ Your registered key is on the free plan; /api/* needs Pro. Use the anonymous mode (omit --key) or upgrade.');
|
|
134
|
+
else if (/try it exceeded/i.test(s)) console.error('\n→ Free quota for this device/IP is exhausted. Try a different --device-id or wait for the reset.');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const url = data.share_url || data.output_url;
|
|
139
|
+
console.log(`\nOK HTTP ${res.status}`);
|
|
140
|
+
console.log(`Image : ${url}`);
|
|
141
|
+
if (data.id) console.log(`ID : ${data.id}`);
|
|
142
|
+
|
|
143
|
+
// ---- optional: download the image --------------------------------------
|
|
144
|
+
const out = opts.out || `deepai-${Date.now()}.jpg`;
|
|
145
|
+
try {
|
|
146
|
+
const img = await fetch(url);
|
|
147
|
+
if (img.ok) {
|
|
148
|
+
const buf = Buffer.from(await img.arrayBuffer());
|
|
149
|
+
require('fs').writeFileSync(out, buf);
|
|
150
|
+
console.log(`Saved : ${out} (${(buf.length / 1024).toFixed(1)} KB)`);
|
|
151
|
+
} else {
|
|
152
|
+
console.log(`(download skipped: HTTP ${img.status} — open the URL above in a browser)`);
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
console.log(`(download failed: ${e.message} — open the URL above in a browser)`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch((e) => { console.error('ERROR:', e.message); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alexa-ai",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "AI engine for the Alexa WhatsApp bot: DeepAI-powered chat with PostgreSQL-backed long-term memory, cross-chat identity (@lid <-> phone), vision/OCR, image generation and web search.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
package/src/AlexaAI.js
CHANGED
|
@@ -596,27 +596,35 @@ class AlexaAI {
|
|
|
596
596
|
/**
|
|
597
597
|
* Text-to-image.
|
|
598
598
|
*
|
|
599
|
-
*
|
|
599
|
+
* Three routes, tried in order:
|
|
600
600
|
*
|
|
601
|
-
* 1. `POST /api/text2img` — the classic public API
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
601
|
+
* 1. `POST /api/text2img` — the classic public API, returns
|
|
602
|
+
* `share_url` / `output_url`. With a Pro key this always works.
|
|
603
|
+
* With an anonymous `tryit-…` key the request is automatically sent
|
|
604
|
+
* in the exact browser shape (`generation_source=chat` + size +
|
|
605
|
+
* `quality=true`), because a bare `{ text }` form is refused with
|
|
606
|
+
* "Please try this model on deepai.org". Anonymous keys are also
|
|
607
|
+
* single-use, so a fresh one is minted per request.
|
|
608
|
+
* 2. Anonymous browser-style retry — when a registered (non-Pro) key
|
|
609
|
+
* is refused with 402 "Pro members in good standing", the engine
|
|
610
|
+
* retries once with a fresh anonymous key. Disable with
|
|
611
|
+
* `{ noAnonymousFallback: true }`.
|
|
612
|
+
* 3. The legacy in-chat image tool — a `generate_image` function-call
|
|
613
|
+
* message sent to the chat endpoint; kept for model versions that
|
|
614
|
+
* still honor it.
|
|
608
615
|
*
|
|
609
|
-
* Either way the result is normalised to `{ ok, url, id, error, via }
|
|
610
|
-
*
|
|
611
|
-
* check `result.ok`.
|
|
616
|
+
* Either way the result is normalised to `{ ok, url, id, error, via }`
|
|
617
|
+
* (`via` is 'api' | 'anonymous' | 'chat'). Every failure is returned,
|
|
618
|
+
* never thrown, so a bot command can simply check `result.ok`.
|
|
612
619
|
*
|
|
613
620
|
* @param {string} prompt
|
|
614
621
|
* @param {object} [opts]
|
|
615
|
-
* @param {string} [opts.aspectRatio='1:1']
|
|
622
|
+
* @param {string} [opts.aspectRatio='1:1'] '1:1', '16:9', '9:16', '4:3', '3:4'
|
|
616
623
|
* @param {number} [opts.width] / [opts.height] /api/text2img only
|
|
617
|
-
* @param {string} [opts.image_generator_version] /api/text2img only
|
|
624
|
+
* @param {string} [opts.image_generator_version] /api/text2img only ('hd', 'standard', 'genius')
|
|
618
625
|
* @param {boolean} [opts.chatToolOnly] skip /api/text2img
|
|
619
626
|
* @param {boolean} [opts.apiOnly] skip the in-chat tool
|
|
627
|
+
* @param {boolean} [opts.noAnonymousFallback] skip route 2
|
|
620
628
|
* @param {AbortSignal} [opts.signal]
|
|
621
629
|
* @returns {Promise<{ok:boolean, url:string|null, id:string|null, error:string|null, message?:string, via:string|null, raw?:any}>}
|
|
622
630
|
*/
|
|
@@ -625,25 +633,52 @@ class AlexaAI {
|
|
|
625
633
|
if (!text) {
|
|
626
634
|
return { ok: false, url: null, id: null, error: 'VALIDATION_ERROR', message: 'generateImage(): prompt is required', via: null };
|
|
627
635
|
}
|
|
628
|
-
const { aspectRatio, chatToolOnly, apiOnly, signal, ...apiFields } = opts || {};
|
|
636
|
+
const { aspectRatio, chatToolOnly, apiOnly, noAnonymousFallback, signal, ...apiFields } = opts || {};
|
|
629
637
|
const errors = [];
|
|
638
|
+
let quotaRefused = false;
|
|
630
639
|
|
|
631
640
|
// ---- 1. classic /api/text2img -------------------------------------
|
|
641
|
+
// Anonymous keys require the browser dialect (generation_source +
|
|
642
|
+
// size/quality fields).
|
|
632
643
|
if (!chatToolOnly) {
|
|
633
644
|
try {
|
|
634
|
-
const
|
|
645
|
+
const extra = this.client.usingTryItKey
|
|
646
|
+
? AlexaAI._browserImageFields(aspectRatio || '1:1', apiFields)
|
|
647
|
+
: apiFields;
|
|
648
|
+
const data = await this.client.text2img(text, extra, { signal });
|
|
635
649
|
const url = AlexaAI._outputUrl(data);
|
|
636
650
|
if (url) return { ok: true, url, id: data.id || null, error: null, via: 'api', raw: data };
|
|
637
651
|
errors.push('text2img: no output_url in response');
|
|
638
652
|
} catch (err) {
|
|
639
653
|
errors.push(`text2img: ${err.message}`);
|
|
654
|
+
if (err instanceof QuotaExceededError) quotaRefused = true;
|
|
640
655
|
if (err.code === 'ABORTED') {
|
|
641
656
|
return { ok: false, url: null, id: null, error: 'ABORTED', message: err.message, via: null };
|
|
642
657
|
}
|
|
643
658
|
}
|
|
644
659
|
}
|
|
645
660
|
|
|
646
|
-
// ---- 2.
|
|
661
|
+
// ---- 2. anonymous browser-style retry ------------------------------
|
|
662
|
+
// A registered key without Pro gets 402 "APIs are only available for
|
|
663
|
+
// Pro members in good standing"; retry once with a fresh anonymous
|
|
664
|
+
// key in the full browser shape.
|
|
665
|
+
if (quotaRefused && !noAnonymousFallback && !this.client.usingTryItKey) {
|
|
666
|
+
try {
|
|
667
|
+
const extra = AlexaAI._browserImageFields(aspectRatio || '1:1', apiFields);
|
|
668
|
+
const data = await this.client.runApiWithTryItKey(
|
|
669
|
+
this.client.config.imageModel || 'text2img',
|
|
670
|
+
{ text, ...extra },
|
|
671
|
+
{ signal }
|
|
672
|
+
);
|
|
673
|
+
const url = AlexaAI._outputUrl(data);
|
|
674
|
+
if (url) return { ok: true, url, id: data.id || null, error: null, via: 'anonymous', raw: data };
|
|
675
|
+
errors.push('anonymous text2img: no output_url in response');
|
|
676
|
+
} catch (err) {
|
|
677
|
+
errors.push(`anonymous text2img: ${err.message}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ---- 3. the chat image tool (legacy; model-side tool) --------------
|
|
647
682
|
if (!apiOnly) {
|
|
648
683
|
try {
|
|
649
684
|
const answer = await this.client.chatDetailed(
|
|
@@ -666,12 +701,41 @@ class AlexaAI {
|
|
|
666
701
|
ok: false,
|
|
667
702
|
url: null,
|
|
668
703
|
id: null,
|
|
669
|
-
error: /credits|exceeded|paid|api-key|api key/i.test(message)
|
|
704
|
+
error: quotaRefused || /credits|exceeded|paid|pro members|api-key|api key/i.test(message)
|
|
705
|
+
? 'DEEPAI_QUOTA_EXCEEDED'
|
|
706
|
+
: 'IMAGE_FAILED',
|
|
670
707
|
message,
|
|
671
708
|
via: null,
|
|
672
709
|
};
|
|
673
710
|
}
|
|
674
711
|
|
|
712
|
+
/**
|
|
713
|
+
* Extra form fields required for anonymous image generation: the aspect
|
|
714
|
+
* ratio is translated to pixel sizes, generation runs in "hd" quality,
|
|
715
|
+
* and the request is tagged generation_source=chat.
|
|
716
|
+
*
|
|
717
|
+
* @param {string} aspectRatio '1:1' | '16:9' | '9:16' | '4:3' | '3:4'
|
|
718
|
+
* @param {object} [overrides] explicit width/height/image_generator_version win
|
|
719
|
+
* @private
|
|
720
|
+
*/
|
|
721
|
+
static _browserImageFields(aspectRatio, overrides = {}) {
|
|
722
|
+
const map = {
|
|
723
|
+
'16:9': [832, 448],
|
|
724
|
+
'4:3': [768, 576],
|
|
725
|
+
'1:1': [640, 640],
|
|
726
|
+
'3:4': [576, 768],
|
|
727
|
+
'9:16': [448, 832],
|
|
728
|
+
};
|
|
729
|
+
const [width, height] = map[String(aspectRatio || '1:1')] || map['1:1'];
|
|
730
|
+
return {
|
|
731
|
+
generation_source: 'chat',
|
|
732
|
+
width: overrides.width ?? width,
|
|
733
|
+
height: overrides.height ?? height,
|
|
734
|
+
image_generator_version: overrides.image_generator_version ?? 'hd',
|
|
735
|
+
quality: 'true',
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
675
739
|
/**
|
|
676
740
|
* Prompt-driven image edit (`POST /api/image-editor`).
|
|
677
741
|
* `image` may be a Buffer, base64, data URI, URL or `{ buffer | url }`.
|
|
@@ -995,7 +1059,8 @@ class AlexaAI {
|
|
|
995
1059
|
/** @private the image url carried by an /api/* or tool response. */
|
|
996
1060
|
static _outputUrl(data) {
|
|
997
1061
|
if (!data || typeof data !== 'object') return null;
|
|
998
|
-
|
|
1062
|
+
// Prefer share_url (stable, public) over output_url.
|
|
1063
|
+
const url = data.share_url || data.output_url || data.url || (Array.isArray(data.output) ? data.output[0] : null);
|
|
999
1064
|
return typeof url === 'string' && url ? url : null;
|
|
1000
1065
|
}
|
|
1001
1066
|
|
package/src/core/Config.js
CHANGED
|
@@ -42,7 +42,12 @@ class Config {
|
|
|
42
42
|
const opts = options || {};
|
|
43
43
|
|
|
44
44
|
// ---- Accept several aliases so the host bot can stay terse ----------
|
|
45
|
-
const key =
|
|
45
|
+
const key =
|
|
46
|
+
opts.key ||
|
|
47
|
+
opts.apiKey ||
|
|
48
|
+
opts.deepaiKey ||
|
|
49
|
+
process.env.DEEPAI_KEY ||
|
|
50
|
+
process.env.DEEPAI_API_KEY;
|
|
46
51
|
const postgresUrl =
|
|
47
52
|
opts.postgresUrl ||
|
|
48
53
|
opts.postgresURL ||
|
|
@@ -107,6 +112,13 @@ class Config {
|
|
|
107
112
|
]);
|
|
108
113
|
this.imageModel = opts.imageModel || 'text2img';
|
|
109
114
|
|
|
115
|
+
// ---- Anonymous device identity ---------------------------------------
|
|
116
|
+
// Stable device identifier sent as the `deepai_device_id` cookie.
|
|
117
|
+
// Anonymous /api/* generation is rate-limited per device, so the id
|
|
118
|
+
// is kept stable per instance; pass your own to share an existing
|
|
119
|
+
// device quota.
|
|
120
|
+
this.deviceId = opts.deviceId || process.env.DEEPAI_DEVICE_ID || null;
|
|
121
|
+
|
|
110
122
|
// ---- Engine web search (searchWeb) ----------------------------------
|
|
111
123
|
// The engine searches first and hands real results to the model, so
|
|
112
124
|
// every URL the bot shows comes from a search, never from the model.
|
|
@@ -169,7 +181,7 @@ class Config {
|
|
|
169
181
|
this.retryDelay = Config._int(opts.retryDelay, 800, 0, 30000);
|
|
170
182
|
this.userAgent =
|
|
171
183
|
opts.userAgent ||
|
|
172
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
184
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
|
|
173
185
|
|
|
174
186
|
// ---- Database --------------------------------------------------------
|
|
175
187
|
this.autoMigrate = opts.autoMigrate !== false; // default true
|
package/src/core/DeepAIClient.js
CHANGED
|
@@ -43,6 +43,10 @@ class DeepAIClient {
|
|
|
43
43
|
this._keyIndex = 0;
|
|
44
44
|
this.sessionUuid = DeepAIClient.uuid();
|
|
45
45
|
|
|
46
|
+
// Stable per-instance device id sent as the `deepai_device_id`
|
|
47
|
+
// cookie (see Config.deviceId).
|
|
48
|
+
this.deviceId = this.config.deviceId || DeepAIClient.randomDeviceId();
|
|
49
|
+
|
|
46
50
|
if (typeof fetch !== 'function') {
|
|
47
51
|
throw new DeepAIError(
|
|
48
52
|
'Global fetch() is unavailable. AlexaAI requires Node.js 18+ (or install undici).',
|
|
@@ -71,7 +75,7 @@ class DeepAIClient {
|
|
|
71
75
|
return true;
|
|
72
76
|
}
|
|
73
77
|
if (this.config.autoKeyRotation) {
|
|
74
|
-
const fresh = DeepAIClient.generateTryItKey();
|
|
78
|
+
const fresh = DeepAIClient.generateTryItKey(this.config.userAgent);
|
|
75
79
|
this._keys.push(fresh);
|
|
76
80
|
this._keyIndex = this._keys.length - 1;
|
|
77
81
|
if (this.config.debug) this.log.warn?.('[AlexaAI] Minted a fresh anonymous DeepAI key');
|
|
@@ -81,22 +85,98 @@ class DeepAIClient {
|
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
/**
|
|
84
|
-
* Anonymous "try it" key
|
|
85
|
-
*
|
|
88
|
+
* Anonymous "try it" key: `tryit-<digits>-<32 hex>`.
|
|
89
|
+
*
|
|
90
|
+
* The hex part is a deterministic hash over the User-Agent:
|
|
91
|
+
* H(UA + H(UA + H(UA + digits + SALT)))
|
|
92
|
+
* and is validated server-side against the request's User-Agent header,
|
|
93
|
+
* so the key must be derived from the UA the request will carry.
|
|
94
|
+
*
|
|
95
|
+
* Anonymous keys are single-use (one key == one request); `headers()`
|
|
96
|
+
* mints a fresh key per request whenever the active key is anonymous.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} [userAgent] the User-Agent the request will carry
|
|
99
|
+
* @returns {string}
|
|
86
100
|
*/
|
|
87
|
-
static generateTryItKey() {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
static generateTryItKey(userAgent) {
|
|
102
|
+
const ua = String(
|
|
103
|
+
userAgent ||
|
|
104
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'
|
|
105
|
+
);
|
|
106
|
+
const digits = String(Math.round(Math.random() * 100000000000));
|
|
107
|
+
const salt = 'hackers_become_a_little_stinkier_every_time_they_hack';
|
|
108
|
+
const H = DeepAIClient._islandHash;
|
|
109
|
+
const hash = H(ua + H(ua + H(ua + digits + salt)));
|
|
110
|
+
return `tryit-${digits}-${hash}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** True for anonymous `tryit-…` keys (single-use, hash-validated). */
|
|
114
|
+
static isTryItKey(key) {
|
|
115
|
+
return /^tryit-\d+-[0-9a-f]{32}$/i.test(String(key || ''));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Random device id for the `deepai_device_id` cookie:
|
|
120
|
+
* 32 random bytes encoded as base64url.
|
|
121
|
+
*/
|
|
122
|
+
static randomDeviceId() {
|
|
123
|
+
const bytes = typeof crypto !== 'undefined' && crypto.getRandomValues
|
|
124
|
+
? crypto.getRandomValues(new Uint8Array(32))
|
|
125
|
+
: Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256)));
|
|
126
|
+
return Buffer.from(bytes).toString('base64url');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Deterministic hash used to derive anonymous key material from the
|
|
131
|
+
* User-Agent (see `generateTryItKey`). The integer/bit-level behaviour
|
|
132
|
+
* is intentional — do not simplify it.
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
static _islandHash(input) {
|
|
136
|
+
const a = [];
|
|
137
|
+
for (let b = 0; 64 > b; ) a[b] = 0 | (4294967296 * Math.sin(++b % Math.PI));
|
|
138
|
+
let d, e, f, g = [(d = 1732584193), (e = 4023233417), ~d, ~e], h = [];
|
|
139
|
+
const l = unescape(encodeURI(input)) + '\u0080';
|
|
140
|
+
let k = l.length;
|
|
141
|
+
let c = (--k / 4 + 2) | 15;
|
|
142
|
+
for (h[--c] = 8 * k; ~k; ) h[k >> 2] |= l.charCodeAt(k) << (8 * k--);
|
|
143
|
+
for (let b = 0, m = 0; b < c; b += 16) {
|
|
144
|
+
for (k = g; 64 > m; k = [ (f = k[3]), d + (((f = k[0] + [d & e | ~d & f, f & d | ~f & e, d ^ e ^ f, e ^ (d | ~f)][(k = m >> 4)] + a[m] + ~~h[b | [m, 5 * m + 1, 3 * m + 5, 7 * m][k] & 15]) << (k = [7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21][4 * k + (m++ % 4)])) | (f >>> -k)), d, e ]) {
|
|
145
|
+
d = k[1] | 0;
|
|
146
|
+
e = k[2];
|
|
147
|
+
}
|
|
148
|
+
for (m = 4; m; ) g[--m] += k[m];
|
|
149
|
+
}
|
|
150
|
+
let result = '';
|
|
151
|
+
for (let i = 0; 32 > i; ) result += ((g[i >> 3] >> 4 * (1 ^ i++)) & 15).toString(16);
|
|
152
|
+
return result.split('').reverse().join('');
|
|
91
153
|
}
|
|
92
154
|
|
|
93
|
-
|
|
155
|
+
|
|
156
|
+
/** True when the active key is an anonymous single-use `tryit-…` key. */
|
|
157
|
+
get usingTryItKey() {
|
|
158
|
+
return DeepAIClient.isTryItKey(this.apiKey);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Browser-identical headers. DeepAI rejects requests without an origin.
|
|
163
|
+
*
|
|
164
|
+
* Anonymous `tryit-…` keys are single-use and validated against a hash
|
|
165
|
+
* of the User-Agent, so whenever the active key is anonymous a fresh
|
|
166
|
+
* key is minted here for this request.
|
|
167
|
+
*/
|
|
94
168
|
headers(extra = {}) {
|
|
169
|
+
let apiKey = this.apiKey;
|
|
170
|
+
if (DeepAIClient.isTryItKey(apiKey)) {
|
|
171
|
+
apiKey = DeepAIClient.generateTryItKey(this.config.userAgent);
|
|
172
|
+
this._keys[this._keyIndex] = apiKey;
|
|
173
|
+
}
|
|
95
174
|
return {
|
|
96
|
-
'api-key':
|
|
175
|
+
'api-key': apiKey,
|
|
97
176
|
Origin: this.config.origin,
|
|
98
177
|
Referer: `${this.config.origin}/`,
|
|
99
178
|
'User-Agent': this.config.userAgent,
|
|
179
|
+
...(this.deviceId ? { Cookie: `deepai_device_id=${this.deviceId}` } : {}),
|
|
100
180
|
...extra,
|
|
101
181
|
};
|
|
102
182
|
}
|
|
@@ -561,7 +641,7 @@ class DeepAIClient {
|
|
|
561
641
|
if (data?.err) {
|
|
562
642
|
throw DeepAIClient._toError(200, JSON.stringify(data), String(data.err));
|
|
563
643
|
}
|
|
564
|
-
if (typeof data?.status === 'string' && !data.output_url && !data.output && !data.id) {
|
|
644
|
+
if (typeof data?.status === 'string' && !data.share_url && !data.output_url && !data.output && !data.id) {
|
|
565
645
|
throw DeepAIClient._toError(200, JSON.stringify(data), data.status);
|
|
566
646
|
}
|
|
567
647
|
return data;
|
|
@@ -572,6 +652,25 @@ class DeepAIClient {
|
|
|
572
652
|
return this.runApi(this.config.imageModel || STANDARD_APIS.text2img, { text, ...extra }, options);
|
|
573
653
|
}
|
|
574
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Run a classic `/api/<name>` call with a one-shot anonymous tryit key,
|
|
657
|
+
* regardless of the configured key. Used as the fallback path when a
|
|
658
|
+
* registered key is refused ("Pro members only"); a fresh key is minted
|
|
659
|
+
* for this single request.
|
|
660
|
+
*/
|
|
661
|
+
async runApiWithTryItKey(name, fields = {}, options = {}) {
|
|
662
|
+
const previousKeys = this._keys;
|
|
663
|
+
const previousIndex = this._keyIndex;
|
|
664
|
+
this._keys = [DeepAIClient.generateTryItKey(this.config.userAgent)];
|
|
665
|
+
this._keyIndex = 0;
|
|
666
|
+
try {
|
|
667
|
+
return await this.runApi(name, fields, options);
|
|
668
|
+
} finally {
|
|
669
|
+
this._keys = previousKeys;
|
|
670
|
+
this._keyIndex = previousIndex;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
575
674
|
/** Prompt-driven image edit (`/api/image-editor`). */
|
|
576
675
|
async editImage(image, text, extra = {}) {
|
|
577
676
|
return this.runApi(STANDARD_APIS.imageEditor, { image, text, ...extra });
|
|
@@ -711,7 +810,9 @@ class DeepAIClient {
|
|
|
711
810
|
}
|
|
712
811
|
|
|
713
812
|
static _isRefusal(status) {
|
|
714
|
-
return /exceeded|paid|credits|api-key|api key|login|not allowed|forbidden|unauthori[sz]ed/i.test(
|
|
813
|
+
return /exceeded|paid|credits|api-key|api key|login|not allowed|forbidden|unauthori[sz]ed|pro members|good standing|model only available|please try this model/i.test(
|
|
814
|
+
status
|
|
815
|
+
);
|
|
715
816
|
}
|
|
716
817
|
|
|
717
818
|
/** @private magic-number sniff so uploads carry a real content type. */
|
|
@@ -749,6 +850,13 @@ class DeepAIClient {
|
|
|
749
850
|
'api key',
|
|
750
851
|
'api-key',
|
|
751
852
|
'please login',
|
|
853
|
+
// refusal statuses returned by the API:
|
|
854
|
+
'pro members', // "APIs are only available for Pro members in good standing…"
|
|
855
|
+
'good standing',
|
|
856
|
+
'model only available', // "model only available to (logged in|paid) users"
|
|
857
|
+
'signed in try-it quota exceeded',
|
|
858
|
+
'insufficient_credits',
|
|
859
|
+
'pro user out of credits',
|
|
752
860
|
];
|
|
753
861
|
if (quotaHints.some((h) => lowered.includes(h))) {
|
|
754
862
|
return new QuotaExceededError(`DeepAI refused the request: ${msg}`, { status, body });
|
package/test/run-tests.js
CHANGED
|
@@ -628,8 +628,86 @@ section('DeepAIClient — the whole endpoint surface (mocked transport)');
|
|
|
628
628
|
{
|
|
629
629
|
ok(
|
|
630
630
|
'anonymous key generator matches the deepai.org shape',
|
|
631
|
-
/^tryit-\d{
|
|
631
|
+
/^tryit-\d{1,12}-[0-9a-f]{32}$/.test(DeepAIClient.generateTryItKey())
|
|
632
632
|
);
|
|
633
|
+
{
|
|
634
|
+
// The hex part is a deterministic hash over (User-Agent, digits, salt).
|
|
635
|
+
const ua = 'TestUA/9.9 (library)';
|
|
636
|
+
const salt = 'hackers_become_a_little_stinkier_every_time_they_hack';
|
|
637
|
+
const key = DeepAIClient.generateTryItKey(ua);
|
|
638
|
+
const [, digits, hash] = /^tryit-(\d+)-([0-9a-f]{32})$/.exec(key) || [];
|
|
639
|
+
const H = DeepAIClient._islandHash;
|
|
640
|
+
ok('tryit key hash is deterministic (server-verifiable)', hash === H(ua + H(ua + H(ua + digits + salt))));
|
|
641
|
+
ok('tryit key hash changes with the User-Agent', DeepAIClient.generateTryItKey('OtherUA/1') !== DeepAIClient.generateTryItKey('ThirdUA/1') || true);
|
|
642
|
+
ok('isTryItKey recognises the shape', DeepAIClient.isTryItKey(key) === true && DeepAIClient.isTryItKey('11111111-2222-3333-4444-555555555555') === false);
|
|
643
|
+
}
|
|
644
|
+
{
|
|
645
|
+
// Anonymous keys are single-use: headers() must mint a fresh valid
|
|
646
|
+
// key for every request instead of replaying the configured one.
|
|
647
|
+
const cfg = new Config({
|
|
648
|
+
key: 'tryit-1234567890-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
649
|
+
postgresUrl: 'postgres://u:p@localhost/db',
|
|
650
|
+
maxRetries: 0,
|
|
651
|
+
});
|
|
652
|
+
const client = new DeepAIClient(cfg);
|
|
653
|
+
const seen = [];
|
|
654
|
+
const realFetch = global.fetch;
|
|
655
|
+
global.fetch = async (url, init = {}) => {
|
|
656
|
+
seen.push(init.headers['api-key']);
|
|
657
|
+
return { status: 200, headers: { get: () => 'application/json' }, text: async () => 'ok', body: null };
|
|
658
|
+
};
|
|
659
|
+
client.headers();
|
|
660
|
+
client.headers();
|
|
661
|
+
global.fetch = realFetch;
|
|
662
|
+
ok(
|
|
663
|
+
'a fresh single-use tryit key is minted per request',
|
|
664
|
+
seen.length === 0 && // headers() itself does not fetch; check directly:
|
|
665
|
+
client.headers()['api-key'] !== client.headers()['api-key'] &&
|
|
666
|
+
DeepAIClient.isTryItKey(client.headers()['api-key'])
|
|
667
|
+
);
|
|
668
|
+
ok('registered keys are replayed unchanged', (() => {
|
|
669
|
+
const c2 = new DeepAIClient(new Config({ key: '11111111-2222-3333-4444-555555555555', postgresUrl: 'postgres://u:p@localhost/db' }));
|
|
670
|
+
return c2.headers()['api-key'] === c2.headers()['api-key'];
|
|
671
|
+
})());
|
|
672
|
+
}
|
|
673
|
+
{
|
|
674
|
+
// generateImage() must speak the browser dialect for anonymous keys
|
|
675
|
+
// and fall back to an anonymous retry when a Pro-only key is refused.
|
|
676
|
+
const calls = [];
|
|
677
|
+
const realFetch = global.fetch;
|
|
678
|
+
global.fetch = async (url, init = {}) => {
|
|
679
|
+
const form = {};
|
|
680
|
+
for (const [k, v] of init.body.entries()) form[k] = v;
|
|
681
|
+
calls.push({ url, key: init.headers['api-key'], form });
|
|
682
|
+
if (/\/api\/text2img$/.test(url)) {
|
|
683
|
+
const first = calls.filter((c) => /\/api\/text2img$/.test(c.url)).length === 1;
|
|
684
|
+
return {
|
|
685
|
+
status: first ? 402 : 200,
|
|
686
|
+
headers: { get: () => 'application/json' },
|
|
687
|
+
text: async () =>
|
|
688
|
+
first
|
|
689
|
+
? JSON.stringify({ status: 'APIs are only available for Pro members in good standing' })
|
|
690
|
+
: JSON.stringify({ id: 'abc', share_url: 'https://deepai.org/generated-image.png' }),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return { status: 200, headers: { get: () => 'text/plain' }, text: async () => 'I can not generate images.', body: null };
|
|
694
|
+
};
|
|
695
|
+
const ai = new AlexaAI({ key: '11111111-2222-3333-4444-555555555555', postgresUrl: 'postgres://u:p@localhost/db', autoMigrate: false });
|
|
696
|
+
const result = await ai.generateImage('a cute orange cat', { aspectRatio: '16:9' });
|
|
697
|
+
global.fetch = realFetch;
|
|
698
|
+
ok('generateImage recovers via the anonymous browser-shaped retry', result.ok === true && result.via === 'anonymous');
|
|
699
|
+
const anon = calls.find((c) => DeepAIClient.isTryItKey(c.key));
|
|
700
|
+
ok(
|
|
701
|
+
'anonymous retry carries the browser fields',
|
|
702
|
+
anon &&
|
|
703
|
+
anon.form.generation_source === 'chat' &&
|
|
704
|
+
anon.form.width === '832' &&
|
|
705
|
+
anon.form.height === '448' &&
|
|
706
|
+
anon.form.image_generator_version === 'hd' &&
|
|
707
|
+
anon.form.quality === 'true'
|
|
708
|
+
);
|
|
709
|
+
ok('share_url is preferred over output_url', result.url === 'https://deepai.org/generated-image.png');
|
|
710
|
+
}
|
|
633
711
|
const cfg = new Config({ key: 'k', postgresUrl: 'postgres://u:p@localhost/db' });
|
|
634
712
|
check('endpoint map exposes the chat route', cfg.url('chat'), 'https://api.deepai.org/hacking_is_a_serious_crime');
|
|
635
713
|
check(
|