clixad 0.0.1-beta.19 → 0.0.1-beta.20
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/clixad.mjs +155 -16
- package/package.json +1 -1
package/dist/clixad.mjs
CHANGED
|
@@ -56,6 +56,68 @@ function edgeBlockMessage(block, outcome) {
|
|
|
56
56
|
Request id (for support): ${block.rayId}` : "";
|
|
57
57
|
return head + tail + id;
|
|
58
58
|
}
|
|
59
|
+
function isOutageStatus(status) {
|
|
60
|
+
return OUTAGE_STATUSES.has(status);
|
|
61
|
+
}
|
|
62
|
+
function detectEdgeOutage(status, headers, body) {
|
|
63
|
+
if (!OUTAGE_STATUSES.has(status)) return void 0;
|
|
64
|
+
if (isGatewayError(body)) return void 0;
|
|
65
|
+
const kind = status === 502 || status === 504 ? "interrupted" : "unreachable";
|
|
66
|
+
const via = headers.get("server") ?? void 0;
|
|
67
|
+
const ray = rayId(headers, body);
|
|
68
|
+
const wait = retryAfterMs(headers);
|
|
69
|
+
return {
|
|
70
|
+
status,
|
|
71
|
+
kind,
|
|
72
|
+
...ray ? { rayId: ray } : {},
|
|
73
|
+
...via ? { via } : {},
|
|
74
|
+
...wait !== void 0 ? { retryAfterMs: wait } : {}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function retryAfterMs(headers) {
|
|
78
|
+
const raw = headers.get("retry-after");
|
|
79
|
+
if (!raw) return void 0;
|
|
80
|
+
const seconds = Number(raw.trim());
|
|
81
|
+
if (!Number.isFinite(seconds) || seconds < 0) return void 0;
|
|
82
|
+
return Math.min(seconds * 1e3, MAX_RETRY_AFTER_MS);
|
|
83
|
+
}
|
|
84
|
+
function outageBackoffMs(attempt) {
|
|
85
|
+
return 1e3 * 2 ** attempt;
|
|
86
|
+
}
|
|
87
|
+
function outageWaitMs(attempt, outage) {
|
|
88
|
+
return Math.max(outage.retryAfterMs ?? 0, outageBackoffMs(attempt));
|
|
89
|
+
}
|
|
90
|
+
function outageWaitNotice(outage) {
|
|
91
|
+
return ` The gateway did not answer that request (${edgeOutageSummary(outage)}).
|
|
92
|
+
Nothing reached Clixad and nothing was charged. Waiting for it to come
|
|
93
|
+
back and resending \u2014 esc stops.`;
|
|
94
|
+
}
|
|
95
|
+
function edgeOutageSummary(outage) {
|
|
96
|
+
const parts = [`${outage.status} from ${outage.via ?? "an intermediary"}`];
|
|
97
|
+
if (outage.rayId) parts.push(`ray ${outage.rayId}`);
|
|
98
|
+
return parts.join(", ");
|
|
99
|
+
}
|
|
100
|
+
function edgeOutageMessage(outage, resends, healthUrl) {
|
|
101
|
+
if (outage.kind === "interrupted") {
|
|
102
|
+
return `Something in front of the gateway ended that request (${outage.status}) before an
|
|
103
|
+
answer came back. Unlike a 503 it may have reached Clixad first, so the turn
|
|
104
|
+
may have been generated and charged even though nothing arrived \u2014 that is why
|
|
105
|
+
it was not resent automatically.
|
|
106
|
+
Check /wallet, then send the message again. The conversation is intact.` + raySuffix(outage);
|
|
107
|
+
}
|
|
108
|
+
const tried = resends.count > 0 ? `
|
|
109
|
+
It was resent ${resends.count} time${resends.count === 1 ? "" : "s"} over about ${Math.round(resends.waitedMs / 1e3)}s and answered the same way.` : "";
|
|
110
|
+
const check = healthUrl ? `
|
|
111
|
+
${healthUrl} says whether it is up.` : "";
|
|
112
|
+
return `The gateway did not answer that request (${outage.status}), so it never reached
|
|
113
|
+
Clixad. Nothing was sent to a model and nothing was charged.${tried}
|
|
114
|
+
This is the gateway being restarted or unreachable, not anything about the
|
|
115
|
+
conversation \u2014 so do not /clear it. Send the message again in a minute.${check}` + raySuffix(outage);
|
|
116
|
+
}
|
|
117
|
+
function raySuffix(outage) {
|
|
118
|
+
return outage.rayId ? `
|
|
119
|
+
Request id (for support): ${outage.rayId}` : "";
|
|
120
|
+
}
|
|
59
121
|
function safeDetail(body, limit = DETAIL_LIMIT) {
|
|
60
122
|
if (!body) return "";
|
|
61
123
|
const title = /<title[^>]*>([^<]*)<\/title>/i.exec(body)?.[1];
|
|
@@ -68,10 +130,23 @@ async function peek(res) {
|
|
|
68
130
|
const body = await res.text().catch(() => "");
|
|
69
131
|
return { body, res: new Response(body, { status: res.status, headers: res.headers }) };
|
|
70
132
|
}
|
|
71
|
-
var EdgeBlockedError, DETAIL_LIMIT;
|
|
133
|
+
var OUTAGE_STATUSES, MAX_RETRY_AFTER_MS, OUTAGE_RETRIES, EdgeOutageError, EdgeBlockedError, DETAIL_LIMIT;
|
|
72
134
|
var init_edge = __esm({
|
|
73
135
|
"src/edge.ts"() {
|
|
74
136
|
"use strict";
|
|
137
|
+
OUTAGE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
138
|
+
MAX_RETRY_AFTER_MS = 3e4;
|
|
139
|
+
OUTAGE_RETRIES = 5;
|
|
140
|
+
EdgeOutageError = class extends Error {
|
|
141
|
+
constructor(outage, resends, healthUrl) {
|
|
142
|
+
super(edgeOutageMessage(outage, resends, healthUrl));
|
|
143
|
+
this.outage = outage;
|
|
144
|
+
this.resends = resends;
|
|
145
|
+
this.name = "EdgeOutageError";
|
|
146
|
+
}
|
|
147
|
+
outage;
|
|
148
|
+
resends;
|
|
149
|
+
};
|
|
75
150
|
EdgeBlockedError = class extends Error {
|
|
76
151
|
constructor(block, outcome) {
|
|
77
152
|
super(edgeBlockMessage(block, outcome));
|
|
@@ -207,6 +282,22 @@ async function httpFailure(res, what) {
|
|
|
207
282
|
const detail = safeDetail(await res.text().catch(() => ""));
|
|
208
283
|
return detail ? `${what} failed: ${res.status} \u2014 ${detail}` : `${what} failed: ${res.status}`;
|
|
209
284
|
}
|
|
285
|
+
function sleep(ms, signal) {
|
|
286
|
+
return new Promise((resolve2) => {
|
|
287
|
+
if (!signal) {
|
|
288
|
+
setTimeout(resolve2, ms);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (signal.aborted) return resolve2();
|
|
292
|
+
const done = () => {
|
|
293
|
+
clearTimeout(timer);
|
|
294
|
+
signal.removeEventListener("abort", done);
|
|
295
|
+
resolve2();
|
|
296
|
+
};
|
|
297
|
+
const timer = setTimeout(done, ms);
|
|
298
|
+
signal.addEventListener("abort", done, { once: true });
|
|
299
|
+
});
|
|
300
|
+
}
|
|
210
301
|
function freeMeta(meta) {
|
|
211
302
|
if (meta.free !== true) return {};
|
|
212
303
|
return {
|
|
@@ -719,7 +810,50 @@ var init_client = __esm({
|
|
|
719
810
|
});
|
|
720
811
|
}
|
|
721
812
|
/**
|
|
722
|
-
* POST a chat request, and
|
|
813
|
+
* POST a chat request, and keep asking while the answer is the edge's rather
|
|
814
|
+
* than the gateway's.
|
|
815
|
+
*
|
|
816
|
+
* Two failures live in front of this route and they are opposites, which is
|
|
817
|
+
* why they are two layers rather than one. The WAF's 403 is *deterministic* —
|
|
818
|
+
* the same bytes are blocked identically forever — so `postPastTheFilter`
|
|
819
|
+
* below changes the bytes exactly once and never merely asks again. An
|
|
820
|
+
* outage is the reverse: nothing about the request caused it, so nothing
|
|
821
|
+
* about the request can avoid it, and asking again is the only move there is.
|
|
822
|
+
*
|
|
823
|
+
* Which outages may be asked again is `EdgeOutage.kind`'s decision, not this
|
|
824
|
+
* loop's: `unreachable` never reached the gateway, `interrupted` may have
|
|
825
|
+
* been generated and billed into a connection that had already gone.
|
|
826
|
+
*
|
|
827
|
+
* The two notices are the price of a locked keyboard. A turn that sits silent
|
|
828
|
+
* for half a minute is indistinguishable from a hung one, so the wait is
|
|
829
|
+
* announced before it starts and its end is announced when the answer finally
|
|
830
|
+
* comes from the gateway.
|
|
831
|
+
*/
|
|
832
|
+
async postChat(payload, signal) {
|
|
833
|
+
const resends = { count: 0, waitedMs: 0 };
|
|
834
|
+
for (; ; ) {
|
|
835
|
+
const res = await this.postPastTheFilter(payload, signal);
|
|
836
|
+
if (!isOutageStatus(res.status)) {
|
|
837
|
+
if (resends.count > 0) {
|
|
838
|
+
this.onNotice?.(" The gateway is answering again \u2014 the request went through.");
|
|
839
|
+
}
|
|
840
|
+
return res;
|
|
841
|
+
}
|
|
842
|
+
const peeked = await peek(res);
|
|
843
|
+
const outage = detectEdgeOutage(res.status, res.headers, peeked.body);
|
|
844
|
+
if (!outage) return peeked.res;
|
|
845
|
+
const health = `${this.config.gatewayUrl}/healthz`;
|
|
846
|
+
if (outage.kind === "interrupted") throw new EdgeOutageError(outage, resends, health);
|
|
847
|
+
if (resends.count >= OUTAGE_RETRIES) throw new EdgeOutageError(outage, resends, health);
|
|
848
|
+
if (resends.count === 0) this.onNotice?.(outageWaitNotice(outage));
|
|
849
|
+
const wait = outageWaitMs(resends.count, outage);
|
|
850
|
+
await sleep(wait, signal);
|
|
851
|
+
resends.count++;
|
|
852
|
+
resends.waitedMs += wait;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* One POST, past the security filter if the filter is in the way.
|
|
723
857
|
*
|
|
724
858
|
* Plain JSON first, always, on every fresh process. That is the OpenAI-compatible
|
|
725
859
|
* contract this route serves — Kimi CLI's kosong client speaks it, and so does
|
|
@@ -731,15 +865,19 @@ var init_client = __esm({
|
|
|
731
865
|
* The retry is not a general "try again": a 403 from a WAF is deterministic,
|
|
732
866
|
* and resending the identical bytes would be blocked identically. It is the
|
|
733
867
|
* *same* request in a form the filter has nothing to match on, which is why it
|
|
734
|
-
* is worth exactly one attempt and no more.
|
|
868
|
+
* is worth exactly one attempt and no more. General "try again" is `postChat`
|
|
869
|
+
* above, around this, for the failure where it is the right answer.
|
|
735
870
|
*
|
|
736
|
-
*
|
|
871
|
+
* Four outcomes, and each has to be distinguishable or the user is left
|
|
737
872
|
* guessing: the retry works (stick with it, say so once), the gateway answers
|
|
738
873
|
* 415 because it predates this (say that — the fix is a deploy, not a
|
|
739
|
-
* `/clear`),
|
|
740
|
-
* history is the way out, because the history is what keeps triggering it)
|
|
874
|
+
* `/clear`), it is blocked again (say that too, and say that clearing the
|
|
875
|
+
* history is the way out, because the history is what keeps triggering it), or
|
|
876
|
+
* the edge answers instead of the gateway — in which case nothing is claimed
|
|
877
|
+
* and nothing is remembered, because a 503 from a router says nothing at all
|
|
878
|
+
* about whether the filter would have let that body through.
|
|
741
879
|
*/
|
|
742
|
-
async
|
|
880
|
+
async postPastTheFilter(payload, signal) {
|
|
743
881
|
const already = this.chatTransport === "encoded";
|
|
744
882
|
const first = await this.sendChat(payload, already, signal);
|
|
745
883
|
if (first.status !== 403) return first;
|
|
@@ -753,6 +891,7 @@ var init_client = __esm({
|
|
|
753
891
|
if (second.block) throw new EdgeBlockedError(second.block, "retried");
|
|
754
892
|
return second.res;
|
|
755
893
|
}
|
|
894
|
+
if (isOutageStatus(retry.status)) return retry;
|
|
756
895
|
this.chatTransport = "encoded";
|
|
757
896
|
this.onNotice?.(
|
|
758
897
|
` A security filter in front of the gateway blocked that request (${edgeBlockSummary(block)}).
|
|
@@ -1063,7 +1202,7 @@ async function githubLogin(client, config, start, invite, report, signal) {
|
|
|
1063
1202
|
const deadline = Date.now() + start.expires_in * 1e3;
|
|
1064
1203
|
let interval = Math.max(start.interval, 1);
|
|
1065
1204
|
while (Date.now() < deadline) {
|
|
1066
|
-
await
|
|
1205
|
+
await sleep2(interval * 1e3, signal);
|
|
1067
1206
|
if (signal?.aborted) return { status: "aborted" };
|
|
1068
1207
|
report.tick?.();
|
|
1069
1208
|
const poll = await client.devicePoll(start.session, invite);
|
|
@@ -1114,7 +1253,7 @@ async function devLogin(client, config, email, invite) {
|
|
|
1114
1253
|
saveConfig(config);
|
|
1115
1254
|
return { status: "ok", account: { email: res.email, balance: res.balance, created: true } };
|
|
1116
1255
|
}
|
|
1117
|
-
function
|
|
1256
|
+
function sleep2(ms, signal) {
|
|
1118
1257
|
return new Promise((resolve2) => {
|
|
1119
1258
|
if (!signal) {
|
|
1120
1259
|
setTimeout(resolve2, ms);
|
|
@@ -2659,7 +2798,7 @@ function isBinary(bytes) {
|
|
|
2659
2798
|
}
|
|
2660
2799
|
return head.length > 0 && control / head.length > 0.15;
|
|
2661
2800
|
}
|
|
2662
|
-
function
|
|
2801
|
+
function sleep3(ms, signal) {
|
|
2663
2802
|
return new Promise((done) => {
|
|
2664
2803
|
if (signal?.aborted) return done();
|
|
2665
2804
|
const finish = () => {
|
|
@@ -3132,7 +3271,7 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
3132
3271
|
} catch (err) {
|
|
3133
3272
|
asToolError(err);
|
|
3134
3273
|
}
|
|
3135
|
-
await
|
|
3274
|
+
await sleep3(BACKGROUND_SETTLE_MS, ctx.signal);
|
|
3136
3275
|
const first = registry.read(started.id);
|
|
3137
3276
|
ctx.onOutput?.(first.output);
|
|
3138
3277
|
return `Started in the background as ${started.id}.
|
|
@@ -5883,7 +6022,7 @@ ${NO_SEARCH_INSTRUCTION}`),
|
|
|
5883
6022
|
await runBusy(WAITING_FOR_REWARD, async (signal) => {
|
|
5884
6023
|
const deadline = Date.now() + 5 * 6e4;
|
|
5885
6024
|
while (Date.now() < deadline && !credited && !signal.aborted) {
|
|
5886
|
-
await
|
|
6025
|
+
await sleep4(3e3, signal);
|
|
5887
6026
|
if (signal.aborted) break;
|
|
5888
6027
|
const w = await client.wallet(signal).catch(() => void 0);
|
|
5889
6028
|
if (w && w.balance > before) {
|
|
@@ -6705,7 +6844,7 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
|
|
|
6705
6844
|
] })
|
|
6706
6845
|
] });
|
|
6707
6846
|
}
|
|
6708
|
-
function
|
|
6847
|
+
function sleep4(ms, signal) {
|
|
6709
6848
|
return new Promise((resolve2) => {
|
|
6710
6849
|
if (signal.aborted) return resolve2();
|
|
6711
6850
|
const done = () => {
|
|
@@ -7093,7 +7232,7 @@ function wrapPlain(text, width) {
|
|
|
7093
7232
|
if (line2) lines.push(line2);
|
|
7094
7233
|
return lines;
|
|
7095
7234
|
}
|
|
7096
|
-
var
|
|
7235
|
+
var sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
7097
7236
|
var credits = (n) => n.toLocaleString("en-US");
|
|
7098
7237
|
function logout(config) {
|
|
7099
7238
|
for (const key of IDENTITY_KEYS) delete config[key];
|
|
@@ -7183,7 +7322,7 @@ async function earn(client, config) {
|
|
|
7183
7322
|
if (c.enabled) process.stdout.write(c.dim(" waiting for an offer to clear"));
|
|
7184
7323
|
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
7185
7324
|
while (Date.now() < deadline) {
|
|
7186
|
-
await
|
|
7325
|
+
await sleep5(3e3);
|
|
7187
7326
|
if (c.enabled) process.stdout.write(c.dim("."));
|
|
7188
7327
|
const balance = (await safeWallet(client))?.balance ?? before;
|
|
7189
7328
|
if (balance > before) {
|
|
@@ -7272,7 +7411,7 @@ async function buyCmd(client, config, pack) {
|
|
|
7272
7411
|
if (c.enabled) process.stdout.write(c.dim(" waiting for payment to clear"));
|
|
7273
7412
|
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
7274
7413
|
while (Date.now() < deadline) {
|
|
7275
|
-
await
|
|
7414
|
+
await sleep5(3e3);
|
|
7276
7415
|
if (c.enabled) process.stdout.write(c.dim("."));
|
|
7277
7416
|
const balance = (await safeWallet(client))?.balance ?? before;
|
|
7278
7417
|
if (balance > before) {
|