jaitus 0.3.12 → 0.3.13
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/bin/jaitus.js +86 -73
- package/package.json +2 -2
- package/src/agent/loop.js +990 -980
- package/src/api.js +174 -140
- package/src/errors.js +120 -106
- package/src/index.js +3148 -3142
- package/src/rewind.js +149 -148
- package/src/tui/clipboard.js +172 -161
- package/src/tui-ink.js +2619 -2605
- package/src/tui.js +2560 -2559
package/src/api.js
CHANGED
|
@@ -1,140 +1,174 @@
|
|
|
1
|
-
// ─── Talking to Jaitus ──────────────────────────────────────────────────────────
|
|
2
|
-
//
|
|
3
|
-
// The CLI is the only client of the gateway. Every failure it gets back is turned into
|
|
4
|
-
// a JaitusError here, at the edge, so no caller ever has an HTTP body in its hands to
|
|
5
|
-
// print — see errors.js for why that matters.
|
|
6
|
-
|
|
7
|
-
import { endpoints } from './config.js';
|
|
8
|
-
import { signedIn, withAuth } from './session.js';
|
|
9
|
-
import { fromGatewayBody, fromNetwork, JaitusError } from './errors.js';
|
|
10
|
-
|
|
11
|
-
export class NotSignedIn extends Error {}
|
|
12
|
-
|
|
13
|
-
// fetch() has no timeout of its own,
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
t.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
1
|
+
// ─── Talking to Jaitus ──────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// The CLI is the only client of the gateway. Every failure it gets back is turned into
|
|
4
|
+
// a JaitusError here, at the edge, so no caller ever has an HTTP body in its hands to
|
|
5
|
+
// print — see errors.js for why that matters.
|
|
6
|
+
|
|
7
|
+
import { endpoints } from './config.js';
|
|
8
|
+
import { signedIn, withAuth } from './session.js';
|
|
9
|
+
import { fromGatewayBody, fromNetwork, JaitusError } from './errors.js';
|
|
10
|
+
|
|
11
|
+
export class NotSignedIn extends Error {}
|
|
12
|
+
|
|
13
|
+
// fetch() has no timeout of its own, so a request the gateway never answered used to
|
|
14
|
+
// pin the CLI on "Working..." forever. Every call below carries a deadline: slow
|
|
15
|
+
// becomes a `timeout` JaitusError (retryable by the agent loop, shown with a request
|
|
16
|
+
// id) instead of eternity. The caller's signal (Esc) still wins: it aborts through the
|
|
17
|
+
// same controller, and only a genuine user abort reads as one.
|
|
18
|
+
//
|
|
19
|
+
// A turn is bounded by SILENCE, not by its length. Until the reply starts, the gateway
|
|
20
|
+
// may be failing over between provider keys (each gets a 20-45 s budget), so the first
|
|
21
|
+
// byte gets a few minutes. Once it starts, the gateway writes a heartbeat at least every
|
|
22
|
+
// 10 s for as long as the provider is working, so 90 s without a single byte means the
|
|
23
|
+
// connection is dead — while a long, healthy answer streams for as long as it needs.
|
|
24
|
+
// (A fixed 10-minute ceiling on the whole request used to cut long answers off and
|
|
25
|
+
// start them again from nothing.)
|
|
26
|
+
// (Overridable only so the tests can drive a stall in under a second.)
|
|
27
|
+
const FIRST_BYTE_TIMEOUT_MS = Number(process.env.JAITUS_FIRST_BYTE_MS) || 3 * 60 * 1000;
|
|
28
|
+
const STREAM_IDLE_TIMEOUT_MS = Number(process.env.JAITUS_STREAM_IDLE_MS) || 90 * 1000;
|
|
29
|
+
const API_TIMEOUT_MS = 30 * 1000; // dashboard calls are small JSON
|
|
30
|
+
|
|
31
|
+
// Combine an optional caller signal with a restartable deadline into one signal.
|
|
32
|
+
// Manual combine (not AbortSignal.any): .any needs Node 20.3+, this works everywhere.
|
|
33
|
+
function withTimeout(signal, ms) {
|
|
34
|
+
const ctrl = new AbortController();
|
|
35
|
+
let timedOut = false;
|
|
36
|
+
let timer = null;
|
|
37
|
+
const arm = (after) => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
timer = setTimeout(() => {
|
|
40
|
+
timedOut = true;
|
|
41
|
+
try { ctrl.abort(new DOMException('Request timed out', 'TimeoutError')); }
|
|
42
|
+
catch { try { ctrl.abort(); } catch { /* already gone */ } }
|
|
43
|
+
}, after);
|
|
44
|
+
if (timer.unref) timer.unref();
|
|
45
|
+
};
|
|
46
|
+
arm(ms);
|
|
47
|
+
const onAbort = () => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
try { ctrl.abort(signal.reason); } catch { try { ctrl.abort(); } catch { /* already gone */ } }
|
|
50
|
+
};
|
|
51
|
+
if (signal) {
|
|
52
|
+
if (signal.aborted) onAbort();
|
|
53
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
signal: ctrl.signal,
|
|
57
|
+
// True only when OUR timer fired. A user abort also lands here as an AbortError,
|
|
58
|
+
// but signal.aborted is set then — never mistake Esc for a timeout.
|
|
59
|
+
timedOut: () => timedOut && !signal?.aborted,
|
|
60
|
+
/** Start the deadline again from now (with a new length). */
|
|
61
|
+
restart: (after) => { if (!ctrl.signal.aborted) arm(after); },
|
|
62
|
+
cleanup: () => { clearTimeout(timer); signal?.removeEventListener?.('abort', onAbort); },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The response with its body watched for silence: every chunk restarts the idle
|
|
68
|
+
* deadline, and the end of the body (or the reader letting go of it) stops the timer
|
|
69
|
+
* and drops the caller's abort listener, so nothing outlives the request.
|
|
70
|
+
*/
|
|
71
|
+
function watchedResponse(res, t, idleMs) {
|
|
72
|
+
if (!res.body) { t.cleanup(); return res; }
|
|
73
|
+
t.restart(idleMs);
|
|
74
|
+
const body = res.body.pipeThrough(new TransformStream({
|
|
75
|
+
transform(chunk, controller) { t.restart(idleMs); controller.enqueue(chunk); },
|
|
76
|
+
flush() { t.cleanup(); },
|
|
77
|
+
cancel() { t.cleanup(); },
|
|
78
|
+
}));
|
|
79
|
+
return new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function timeoutError() {
|
|
83
|
+
return new JaitusError({ code: 'timeout', status: 504, message: 'Jaitus took too long to respond. Try again.' });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The context window the gateway last advertised, or null before it has been asked.
|
|
87
|
+
let contextWindowSeen = null;
|
|
88
|
+
export const advertisedContextWindow = () => contextWindowSeen;
|
|
89
|
+
|
|
90
|
+
export function createClient() {
|
|
91
|
+
const { baseUrl } = endpoints();
|
|
92
|
+
if (!signedIn()) throw new NotSignedIn('Not signed in. Run `jaitus login` first.');
|
|
93
|
+
|
|
94
|
+
// x-jaitus-features: what this build understands, so the gateway can use newer error
|
|
95
|
+
// codes (a used-up usage window) without an older CLI reading them as "went wrong".
|
|
96
|
+
const base = { 'Content-Type': 'application/json', 'user-agent': 'jaitus-cli', 'x-jaitus-features': 'usage-window' };
|
|
97
|
+
const headers = (token) => ({ ...base, authorization: `Bearer ${token}` });
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
baseUrl,
|
|
101
|
+
|
|
102
|
+
/** POST /v1/messages. Returns the raw Response so the caller can stream it. */
|
|
103
|
+
async messages(body, { signal } = {}) {
|
|
104
|
+
const t = withTimeout(signal, FIRST_BYTE_TIMEOUT_MS);
|
|
105
|
+
let res;
|
|
106
|
+
try {
|
|
107
|
+
res = await withAuth((token) => fetch(`${baseUrl}/v1/messages`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: headers(token),
|
|
110
|
+
body: JSON.stringify(body),
|
|
111
|
+
signal: t.signal,
|
|
112
|
+
}));
|
|
113
|
+
} catch (cause) {
|
|
114
|
+
t.cleanup();
|
|
115
|
+
if (cause?.name === 'JaitusError') throw cause;
|
|
116
|
+
if (t.timedOut()) throw timeoutError();
|
|
117
|
+
if (cause?.name === 'AbortError') throw cause;
|
|
118
|
+
throw fromNetwork(cause);
|
|
119
|
+
}
|
|
120
|
+
// A mid-body stall (headers fast, then nothing) aborts the body; the reader sees
|
|
121
|
+
// an AbortError while the caller's signal is NOT aborted, and turns it into a
|
|
122
|
+
// timeout — keeping whatever already arrived (agent/loop.js).
|
|
123
|
+
return watchedResponse(res, t, STREAM_IDLE_TIMEOUT_MS);
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/** A dashboard API call, returning parsed JSON. */
|
|
127
|
+
async api(pathname, init = {}) {
|
|
128
|
+
const t = withTimeout(init.signal, API_TIMEOUT_MS);
|
|
129
|
+
let res;
|
|
130
|
+
try {
|
|
131
|
+
res = await withAuth((token) => fetch(`${baseUrl}/api${pathname}`, {
|
|
132
|
+
...init,
|
|
133
|
+
headers: { ...headers(token), ...(init.headers || {}) },
|
|
134
|
+
signal: t.signal,
|
|
135
|
+
}));
|
|
136
|
+
const body = await res.json().catch(() => ({}));
|
|
137
|
+
return { ok: res.ok, status: res.status, body };
|
|
138
|
+
} catch (cause) {
|
|
139
|
+
if (cause?.name === 'JaitusError') throw cause;
|
|
140
|
+
if (t.timedOut()) throw timeoutError();
|
|
141
|
+
if (cause?.name === 'AbortError') throw cause;
|
|
142
|
+
throw fromNetwork(cause);
|
|
143
|
+
} finally {
|
|
144
|
+
t.cleanup();
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/** Model ids the gateway advertises. */
|
|
149
|
+
async models() {
|
|
150
|
+
const t = withTimeout(undefined, API_TIMEOUT_MS);
|
|
151
|
+
let res;
|
|
152
|
+
try {
|
|
153
|
+
res = await withAuth((token) => fetch(`${baseUrl}/v1/models`, { headers: headers(token), signal: t.signal }));
|
|
154
|
+
} catch (cause) {
|
|
155
|
+
t.cleanup();
|
|
156
|
+
if (cause?.name === 'JaitusError') throw cause;
|
|
157
|
+
if (t.timedOut()) throw timeoutError();
|
|
158
|
+
if (cause?.name === 'AbortError') throw cause;
|
|
159
|
+
throw fromNetwork(cause);
|
|
160
|
+
}
|
|
161
|
+
const body = await res.json().catch(() => ({}));
|
|
162
|
+
t.cleanup();
|
|
163
|
+
if (!res.ok) throw fromGatewayBody(res.status, body, { requestId: res.headers.get('x-request-id') || '' });
|
|
164
|
+
const models = (body?.data || []).map((m) => m?.id).filter(Boolean);
|
|
165
|
+
// How much conversation fits, as the gateway computed it for this account.
|
|
166
|
+
const ctx = (body?.data || []).map((m) => Number(m?.context_window)).find((n) => n > 0);
|
|
167
|
+
if (ctx) contextWindowSeen = ctx;
|
|
168
|
+
// An empty list is a service fault, not something the user can fix — and naming
|
|
169
|
+
// the gateway's model map to them would be naming our internals.
|
|
170
|
+
if (!models.length) throw new JaitusError({ code: 'unavailable', status: 502 });
|
|
171
|
+
return models;
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
package/src/errors.js
CHANGED
|
@@ -1,106 +1,120 @@
|
|
|
1
|
-
// ─── What the CLI says when something fails ─────────────────────────────────────
|
|
2
|
-
//
|
|
3
|
-
// The gateway answers every failure with a `jaitus_*` code from one catalog. This file
|
|
4
|
-
// is the CLI's half of that contract: a code maps to a title and a hint written here,
|
|
5
|
-
// and anything WITHOUT a known code — an HTML page from a proxy, a body in some other
|
|
6
|
-
// shape, a code this build has never heard of — becomes "something went wrong in
|
|
7
|
-
// Jaitus" plus the request id. The CLI never prints an error body it does not
|
|
8
|
-
// recognise, because the only text that is safe to show is text we wrote.
|
|
9
|
-
|
|
10
|
-
const CATALOG = {
|
|
11
|
-
auth_required: { title: 'Not signed in', hint: 'Type /login to sign in (or run `jaitus login`).' },
|
|
12
|
-
session_expired: { title: 'Signed out', hint: 'Type /login to sign in again (or run `jaitus login`).' },
|
|
13
|
-
account_suspended: { title: 'Account suspended', hint: 'Contact support if you think this is a mistake.' },
|
|
14
|
-
email_unverified: { title: 'Email not verified', hint: 'Open the verification link in your inbox, then retry.' },
|
|
15
|
-
free_limit_reached: { title: 'Free prompts used up', hint: 'Subscribe on the website, then retry this turn.' },
|
|
16
|
-
free_request_too_large: { title: 'Too large for the free plan', hint: 'Run /compact or start a new session, or subscribe.' },
|
|
17
|
-
plan_limit_reached: { title: 'Monthly allowance used', hint: 'It resets at the start of your next period, or upgrade on the website. /usage shows where you are.' },
|
|
18
|
-
rate_limited: { title: 'Slow down', hint: 'Wait a moment, then retry.' },
|
|
19
|
-
context_too_long: { title: 'Conversation too long', hint: 'Run /compact, or start a new session.' },
|
|
20
|
-
request_too_large: { title: 'Request too large', hint: 'Usually images: remove some, or run /compact to drop old ones. Images over 3.5 MB are shrunk automatically.' },
|
|
21
|
-
daily_limit_reached: { title: 'Usage limit reached', hint: 'It refills automatically. /usage shows where you are.' },
|
|
22
|
-
window_limit_reached: { title: 'This usage window is used up', hint: 'It refills when the window ends — the rest of your month is still there. /usage shows when.' },
|
|
23
|
-
token_expired: { title: 'Signed out', hint: 'Type /login to sign in again (or run `jaitus login`).' },
|
|
24
|
-
refresh_race: { title: 'Sign-in was being renewed', hint: 'Try again.' },
|
|
25
|
-
content_blocked: { title: 'Request refused', hint: 'Rephrase the request and try again.' },
|
|
26
|
-
bad_request: { title: 'Request not accepted', hint: 'Try again; if it repeats, run /debug and contact support.' },
|
|
27
|
-
model_unavailable: { title: 'Model unavailable', hint: 'Run /model to pick another.' },
|
|
28
|
-
busy: { title: 'Jaitus is busy', hint: 'Try again in a minute.' },
|
|
29
|
-
unavailable: { title: 'Jaitus is unavailable', hint: 'Try again shortly. `jaitus doctor` checks your side.' },
|
|
30
|
-
maintenance: { title: 'Down for maintenance', hint: 'Try again shortly.' },
|
|
31
|
-
timeout: { title: 'Timed out', hint: 'Try again.' },
|
|
32
|
-
interrupted: { title: 'Response interrupted', hint: 'Try again.' },
|
|
33
|
-
internal: { title: 'Something went wrong', hint: 'Try again; if it keeps happening, contact support with the request id.' },
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const GENERIC = 'Something went wrong in Jaitus.';
|
|
37
|
-
|
|
38
|
-
export class JaitusError extends Error {
|
|
39
|
-
constructor({ code = 'internal', message, status = 0, requestId = '', retryAfter = null } = {}) {
|
|
40
|
-
const known = Object.prototype.hasOwnProperty.call(CATALOG, code) ? code : 'internal';
|
|
41
|
-
super(message || GENERIC);
|
|
42
|
-
this.name = 'JaitusError';
|
|
43
|
-
this.code = known;
|
|
44
|
-
this.status = status;
|
|
45
|
-
this.requestId = requestId;
|
|
46
|
-
this.retryAfter = retryAfter;
|
|
47
|
-
this.title = CATALOG[known].title;
|
|
48
|
-
this.hint = CATALOG[known].hint;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* An error body from the gateway → a JaitusError.
|
|
54
|
-
*
|
|
55
|
-
* The gateway's message is shown ONLY when the body carries a recognised `jaitus_`
|
|
56
|
-
* code, because only then is it guaranteed to be our wording (it may carry specifics,
|
|
57
|
-
* like how many free prompts there were). Otherwise the message is ours, generic.
|
|
58
|
-
*/
|
|
59
|
-
export function fromGatewayBody(status, body, { requestId = '', retryAfter = null } = {}) {
|
|
60
|
-
let parsed = body;
|
|
61
|
-
if (typeof body === 'string') {
|
|
62
|
-
try { parsed = JSON.parse(body); } catch { parsed = null; }
|
|
63
|
-
}
|
|
64
|
-
const err = parsed?.error || null;
|
|
65
|
-
const rawCode = String(err?.code || '');
|
|
66
|
-
const code = rawCode.startsWith('jaitus_') ? rawCode.slice(7) : '';
|
|
67
|
-
const known = code && Object.prototype.hasOwnProperty.call(CATALOG, code);
|
|
68
|
-
const id = String(parsed?.request_id || requestId || '');
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
retryAfter,
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
error
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
1
|
+
// ─── What the CLI says when something fails ─────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// The gateway answers every failure with a `jaitus_*` code from one catalog. This file
|
|
4
|
+
// is the CLI's half of that contract: a code maps to a title and a hint written here,
|
|
5
|
+
// and anything WITHOUT a known code — an HTML page from a proxy, a body in some other
|
|
6
|
+
// shape, a code this build has never heard of — becomes "something went wrong in
|
|
7
|
+
// Jaitus" plus the request id. The CLI never prints an error body it does not
|
|
8
|
+
// recognise, because the only text that is safe to show is text we wrote.
|
|
9
|
+
|
|
10
|
+
const CATALOG = {
|
|
11
|
+
auth_required: { title: 'Not signed in', hint: 'Type /login to sign in (or run `jaitus login`).' },
|
|
12
|
+
session_expired: { title: 'Signed out', hint: 'Type /login to sign in again (or run `jaitus login`).' },
|
|
13
|
+
account_suspended: { title: 'Account suspended', hint: 'Contact support if you think this is a mistake.' },
|
|
14
|
+
email_unverified: { title: 'Email not verified', hint: 'Open the verification link in your inbox, then retry.' },
|
|
15
|
+
free_limit_reached: { title: 'Free prompts used up', hint: 'Subscribe on the website, then retry this turn.' },
|
|
16
|
+
free_request_too_large: { title: 'Too large for the free plan', hint: 'Run /compact or start a new session, or subscribe.' },
|
|
17
|
+
plan_limit_reached: { title: 'Monthly allowance used', hint: 'It resets at the start of your next period, or upgrade on the website. /usage shows where you are.' },
|
|
18
|
+
rate_limited: { title: 'Slow down', hint: 'Wait a moment, then retry.' },
|
|
19
|
+
context_too_long: { title: 'Conversation too long', hint: 'Run /compact, or start a new session.' },
|
|
20
|
+
request_too_large: { title: 'Request too large', hint: 'Usually images: remove some, or run /compact to drop old ones. Images over 3.5 MB are shrunk automatically.' },
|
|
21
|
+
daily_limit_reached: { title: 'Usage limit reached', hint: 'It refills automatically. /usage shows where you are.' },
|
|
22
|
+
window_limit_reached: { title: 'This usage window is used up', hint: 'It refills when the window ends — the rest of your month is still there. /usage shows when.' },
|
|
23
|
+
token_expired: { title: 'Signed out', hint: 'Type /login to sign in again (or run `jaitus login`).' },
|
|
24
|
+
refresh_race: { title: 'Sign-in was being renewed', hint: 'Try again.' },
|
|
25
|
+
content_blocked: { title: 'Request refused', hint: 'Rephrase the request and try again.' },
|
|
26
|
+
bad_request: { title: 'Request not accepted', hint: 'Try again; if it repeats, run /debug and contact support.' },
|
|
27
|
+
model_unavailable: { title: 'Model unavailable', hint: 'Run /model to pick another.' },
|
|
28
|
+
busy: { title: 'Jaitus is busy', hint: 'Try again in a minute.' },
|
|
29
|
+
unavailable: { title: 'Jaitus is unavailable', hint: 'Try again shortly. `jaitus doctor` checks your side.' },
|
|
30
|
+
maintenance: { title: 'Down for maintenance', hint: 'Try again shortly.' },
|
|
31
|
+
timeout: { title: 'Timed out', hint: 'Try again.' },
|
|
32
|
+
interrupted: { title: 'Response interrupted', hint: 'Try again.' },
|
|
33
|
+
internal: { title: 'Something went wrong', hint: 'Try again; if it keeps happening, contact support with the request id.' },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const GENERIC = 'Something went wrong in Jaitus.';
|
|
37
|
+
|
|
38
|
+
export class JaitusError extends Error {
|
|
39
|
+
constructor({ code = 'internal', message, status = 0, requestId = '', retryAfter = null } = {}) {
|
|
40
|
+
const known = Object.prototype.hasOwnProperty.call(CATALOG, code) ? code : 'internal';
|
|
41
|
+
super(message || GENERIC);
|
|
42
|
+
this.name = 'JaitusError';
|
|
43
|
+
this.code = known;
|
|
44
|
+
this.status = status;
|
|
45
|
+
this.requestId = requestId;
|
|
46
|
+
this.retryAfter = retryAfter;
|
|
47
|
+
this.title = CATALOG[known].title;
|
|
48
|
+
this.hint = CATALOG[known].hint;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* An error body from the gateway → a JaitusError.
|
|
54
|
+
*
|
|
55
|
+
* The gateway's message is shown ONLY when the body carries a recognised `jaitus_`
|
|
56
|
+
* code, because only then is it guaranteed to be our wording (it may carry specifics,
|
|
57
|
+
* like how many free prompts there were). Otherwise the message is ours, generic.
|
|
58
|
+
*/
|
|
59
|
+
export function fromGatewayBody(status, body, { requestId = '', retryAfter = null } = {}) {
|
|
60
|
+
let parsed = body;
|
|
61
|
+
if (typeof body === 'string') {
|
|
62
|
+
try { parsed = JSON.parse(body); } catch { parsed = null; }
|
|
63
|
+
}
|
|
64
|
+
const err = parsed?.error || null;
|
|
65
|
+
const rawCode = String(err?.code || '');
|
|
66
|
+
const code = rawCode.startsWith('jaitus_') ? rawCode.slice(7) : '';
|
|
67
|
+
const known = code && Object.prototype.hasOwnProperty.call(CATALOG, code);
|
|
68
|
+
const id = String(parsed?.request_id || requestId || '');
|
|
69
|
+
// No Jaitus code on a gateway-class status: the answer came from the proxy in front of
|
|
70
|
+
// the gateway (its timeout page, a 502 while a deploy restarts the server), not from
|
|
71
|
+
// the gateway. Those are "not right now" — said plainly and retried — never the
|
|
72
|
+
// dead-end "Something went wrong" they used to read as.
|
|
73
|
+
if (!known && (status === 504 || status === 524)) {
|
|
74
|
+
return new JaitusError({ code: 'timeout', status, requestId: id, retryAfter, message: 'Jaitus took too long to answer and the connection was closed on the way. Try again.' });
|
|
75
|
+
}
|
|
76
|
+
if (!known && (status === 502 || status === 503 || (status >= 520 && status <= 530))) {
|
|
77
|
+
return new JaitusError({ code: 'unavailable', status, requestId: id, retryAfter, message: 'Jaitus could not be reached just now. Try again in a moment.' });
|
|
78
|
+
}
|
|
79
|
+
return new JaitusError({
|
|
80
|
+
code: known ? code : 'internal',
|
|
81
|
+
message: known && typeof err?.message === 'string' && err.message.trim() ? err.message.trim() : GENERIC,
|
|
82
|
+
status,
|
|
83
|
+
requestId: id,
|
|
84
|
+
retryAfter,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A failed fetch Response → a JaitusError, reading the body once. */
|
|
89
|
+
export async function fromResponse(res) {
|
|
90
|
+
const text = await res.text().catch(() => '');
|
|
91
|
+
const requestId = res.headers?.get?.('x-request-id') || '';
|
|
92
|
+
const retryAfter = Number(res.headers?.get?.('retry-after')) || null;
|
|
93
|
+
const error = fromGatewayBody(res.status, text, { requestId, retryAfter });
|
|
94
|
+
// The gateway says trying again cannot help yet (e.g. no provider is switched on):
|
|
95
|
+
// report it at once instead of retrying behind "Working…".
|
|
96
|
+
if (res.headers?.get?.('x-jaitus-retry') === 'no') error.noRetry = true;
|
|
97
|
+
return error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* fetch() threw before any response — the network, not the service. This is the one
|
|
102
|
+
* failure where the true cause is on the user's side and worth naming plainly.
|
|
103
|
+
*/
|
|
104
|
+
export function fromNetwork(cause) {
|
|
105
|
+
if (cause?.name === 'AbortError') return cause;
|
|
106
|
+
const error = new JaitusError({
|
|
107
|
+
code: 'unavailable',
|
|
108
|
+
message: 'Could not reach Jaitus. Check your internet connection.',
|
|
109
|
+
});
|
|
110
|
+
error.title = 'Cannot reach Jaitus';
|
|
111
|
+
error.hint = 'Check your connection, then run `jaitus doctor`.';
|
|
112
|
+
error.cause = cause;
|
|
113
|
+
return error;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isJaitusError(e) {
|
|
117
|
+
return e instanceof JaitusError || e?.name === 'JaitusError';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const ERROR_CODES = Object.freeze(Object.keys(CATALOG));
|