pi-provider-qoder-cn 0.2.0 → 0.2.2
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/README.md +23 -8
- package/dist/index.js +250 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,14 +64,18 @@ export QODERCN_PERSONAL_ACCESS_TOKEN="<your PAT>"
|
|
|
64
64
|
|
|
65
65
|
### `/qoder-cn.usage`
|
|
66
66
|
|
|
67
|
-
Shows the plan allowance and any purchased add-on credits.
|
|
67
|
+
Shows the plan allowance and any purchased add-on credits as one aligned grid.
|
|
68
|
+
In a terminal the gauge is coloured by how much of the allowance is gone —
|
|
69
|
+
green under 60%, yellow to 85%, red above that, and bold red once the quota is
|
|
70
|
+
exceeded.
|
|
68
71
|
|
|
69
72
|
```text
|
|
70
73
|
Qoder CN Plan (personal_standard)
|
|
71
|
-
[
|
|
72
|
-
[
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
Overall [███░░░░░░░░░░░░░░░░░░] 12.5% used
|
|
75
|
+
Plan quota [███░░░░░░░░░░░░░░░░░░] 250 / 2000 credits 12.5% 1750 left
|
|
76
|
+
Add-on [░░░░░░░░░░░░░░░░░░░░░] 0 / 700 credits 0% 700 left
|
|
77
|
+
Resets never
|
|
78
|
+
Manage https://qoder.com.cn/account/usage
|
|
75
79
|
```
|
|
76
80
|
|
|
77
81
|
Append `json` to print the untouched API payload — useful when a field you need
|
|
@@ -81,13 +85,24 @@ is not in the formatted view:
|
|
|
81
85
|
/qoder-cn.usage json
|
|
82
86
|
```
|
|
83
87
|
|
|
88
|
+
Append `plain` for the same grid without the colour codes, e.g. when pasting it
|
|
89
|
+
into an issue:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
/qoder-cn.usage plain
|
|
93
|
+
```
|
|
94
|
+
|
|
84
95
|
Notes:
|
|
85
96
|
|
|
86
|
-
- The plan allowance (`Plan quota`) and top-up credits (`Add-on
|
|
97
|
+
- The plan allowance (`Plan quota`) and top-up credits (`Add-on`) are
|
|
87
98
|
independent buckets with independent expiry, so both are listed when present.
|
|
88
|
-
|
|
99
|
+
Columns are sized to the rows that came back, so a single-bucket plan does
|
|
100
|
+
not reserve space for one it does not have.
|
|
101
|
+
- Colour is suppressed automatically for a `NO_COLOR` environment, a dumb
|
|
102
|
+
terminal, and any non-TTY output that is not the interactive UI.
|
|
103
|
+
- `Resets never` means the API reported the year-9999 sentinel, i.e. the
|
|
89
104
|
allowance does not reset on a schedule.
|
|
90
|
-
- `
|
|
105
|
+
- `Note plan quota is prorated` appears after a mid-cycle plan change.
|
|
91
106
|
- The command uses the same stored token as chat requests, refreshing it first,
|
|
92
107
|
so it reports the same quota the model is actually billed against.
|
|
93
108
|
|
package/dist/index.js
CHANGED
|
@@ -1719,11 +1719,37 @@ To reset: /qoder-endpoint default`,
|
|
|
1719
1719
|
|
|
1720
1720
|
// src/commands/usage.ts
|
|
1721
1721
|
init_region();
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1722
|
+
var ANSI = {
|
|
1723
|
+
reset: "\x1B[0m",
|
|
1724
|
+
bold: "\x1B[1m",
|
|
1725
|
+
dim: "\x1B[2m",
|
|
1726
|
+
red: "\x1B[31m",
|
|
1727
|
+
green: "\x1B[32m",
|
|
1728
|
+
yellow: "\x1B[33m"
|
|
1729
|
+
};
|
|
1730
|
+
var FILLED_CHAR = "\u2588";
|
|
1731
|
+
var EMPTY_CHAR = "\u2591";
|
|
1732
|
+
var UNKNOWN_CHAR = "?";
|
|
1733
|
+
var BAR_WIDTH = 20;
|
|
1734
|
+
var COLUMN_GAP = " ";
|
|
1735
|
+
function paint(text, code) {
|
|
1736
|
+
return code && text ? `${code}${text}${ANSI.reset}` : text;
|
|
1737
|
+
}
|
|
1738
|
+
function usageColor(percentage, danger = false) {
|
|
1739
|
+
if (danger) return `${ANSI.red}${ANSI.bold}`;
|
|
1740
|
+
if (typeof percentage !== "number" || !Number.isFinite(percentage)) return void 0;
|
|
1741
|
+
if (percentage >= 85) return `${ANSI.red}${ANSI.bold}`;
|
|
1742
|
+
if (percentage >= 60) return ANSI.yellow;
|
|
1743
|
+
return ANSI.green;
|
|
1744
|
+
}
|
|
1745
|
+
function usageBar(percentage, options = {}) {
|
|
1746
|
+
const width = options.width ?? BAR_WIDTH;
|
|
1747
|
+
const known = typeof percentage === "number" && Number.isFinite(percentage);
|
|
1748
|
+
const filled = known ? Math.max(0, Math.min(width, Math.round(percentage / 100 * width))) : 0;
|
|
1749
|
+
const fill = known ? FILLED_CHAR.repeat(filled) : "";
|
|
1750
|
+
const rest = known ? EMPTY_CHAR.repeat(width - filled) : UNKNOWN_CHAR.repeat(width);
|
|
1751
|
+
if (!options.color) return `[${fill}${rest}]`;
|
|
1752
|
+
return paint("[", ANSI.dim) + paint(fill, usageColor(percentage, options.danger)) + paint(rest, ANSI.dim) + paint("]", ANSI.dim);
|
|
1727
1753
|
}
|
|
1728
1754
|
function formatAmount(value, unit) {
|
|
1729
1755
|
if (typeof value !== "number" || !Number.isFinite(value)) return "?";
|
|
@@ -1748,41 +1774,82 @@ function formatResetTime(expiresAt, now = Date.now()) {
|
|
|
1748
1774
|
const relative = days > 0 ? `${days}d ${hours}h` : hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
1749
1775
|
return `${new Date(timestamp).toISOString().slice(0, 10)} (in ${relative})`;
|
|
1750
1776
|
}
|
|
1751
|
-
function
|
|
1752
|
-
|
|
1777
|
+
function isBucketRow(row) {
|
|
1778
|
+
return typeof row.used === "number" || typeof row.total === "number";
|
|
1779
|
+
}
|
|
1780
|
+
function rightCell(text, width, code) {
|
|
1781
|
+
return " ".repeat(Math.max(0, width - text.length)) + paint(text, code);
|
|
1782
|
+
}
|
|
1783
|
+
function renderUsageRows(rows, notes, color) {
|
|
1784
|
+
const buckets = rows.filter(isBucketRow);
|
|
1785
|
+
const usedWidth = Math.max(0, ...buckets.map((row) => formatAmount(row.used).length));
|
|
1786
|
+
const totalWidth = Math.max(0, ...buckets.map((row) => formatAmount(row.total).length));
|
|
1787
|
+
const unitWidth = Math.max(0, ...buckets.map((row) => (row.unit ?? "").length));
|
|
1788
|
+
const remainWidth = Math.max(0, ...buckets.map((row) => formatAmount(row.remaining).length));
|
|
1789
|
+
const labelWidth = Math.max(0, ...rows.map((row) => row.label.length), ...notes.map((note) => note.label.length));
|
|
1790
|
+
const percentWidth = Math.max(0, ...rows.map((row) => row.percent === void 0 ? 1 : `${row.percent}%`.length));
|
|
1791
|
+
const amountWidth = buckets.length > 0 ? usedWidth + 3 + totalWidth + 1 + unitWidth : 0;
|
|
1792
|
+
const remainder = (row) => isBucketRow(row) ? `${formatAmount(row.remaining).padStart(remainWidth)} left` : row.tail ?? "";
|
|
1793
|
+
const remainderWidth = Math.max(0, ...rows.map((row) => remainder(row).length));
|
|
1794
|
+
const lines = rows.map((row) => {
|
|
1795
|
+
const amount = isBucketRow(row) ? `${formatAmount(row.used).padStart(usedWidth)} / ${formatAmount(row.total).padStart(totalWidth)} ${(row.unit ?? "").padEnd(unitWidth)}` : " ".repeat(amountWidth);
|
|
1796
|
+
const percent = row.percent === void 0 ? UNKNOWN_CHAR : `${row.percent}%`;
|
|
1797
|
+
return [
|
|
1798
|
+
row.label.padEnd(labelWidth),
|
|
1799
|
+
usageBar(row.percent, { color, danger: row.danger }),
|
|
1800
|
+
amount,
|
|
1801
|
+
rightCell(percent, percentWidth, color ? usageColor(row.percent, row.danger) : void 0),
|
|
1802
|
+
remainder(row).padEnd(remainderWidth)
|
|
1803
|
+
].join(COLUMN_GAP);
|
|
1804
|
+
});
|
|
1805
|
+
for (const note of notes) {
|
|
1806
|
+
lines.push(`${note.label.padEnd(labelWidth)}${COLUMN_GAP}${color ? paint(note.value, note.color) : note.value}`);
|
|
1807
|
+
}
|
|
1808
|
+
return lines.map((line) => line.replace(/ +$/, ""));
|
|
1809
|
+
}
|
|
1810
|
+
function bucketRow(label, bucket, danger) {
|
|
1753
1811
|
const rawPercent = bucket?.percentage;
|
|
1754
1812
|
const derivedPercent = typeof bucket?.used === "number" && typeof bucket?.total === "number" && bucket.total > 0 ? bucket.used / bucket.total * 100 : void 0;
|
|
1755
1813
|
const percent = formatPercent(
|
|
1756
1814
|
typeof rawPercent === "number" && Number.isFinite(rawPercent) ? rawPercent : derivedPercent
|
|
1757
1815
|
);
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1816
|
+
return {
|
|
1817
|
+
label,
|
|
1818
|
+
percent,
|
|
1819
|
+
used: bucket?.used,
|
|
1820
|
+
total: bucket?.total,
|
|
1821
|
+
remaining: bucket?.remaining,
|
|
1822
|
+
unit: bucket?.unit || "credits",
|
|
1823
|
+
danger
|
|
1824
|
+
};
|
|
1763
1825
|
}
|
|
1764
1826
|
function hasBucket(bucket) {
|
|
1765
1827
|
if (!bucket) return false;
|
|
1766
1828
|
return (bucket.total ?? 0) > 0 || (bucket.used ?? 0) > 0;
|
|
1767
1829
|
}
|
|
1768
|
-
function formatQoderUsage(raw, mode, now = Date.now()) {
|
|
1830
|
+
function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
|
|
1831
|
+
const color = options.color ?? false;
|
|
1769
1832
|
const region = getQoderRegionConfig(mode);
|
|
1770
|
-
const
|
|
1833
|
+
const danger = Boolean(raw?.isQuotaExceeded);
|
|
1771
1834
|
const userType = raw?.userType ? ` (${raw.userType})` : "";
|
|
1772
1835
|
const overallPercent = formatPercent(raw?.totalUsagePercentage);
|
|
1773
|
-
|
|
1774
|
-
lines.push(`${usageBar(overallPercent)} ${overallPercent === void 0 ? "?" : `${overallPercent}%`} used`);
|
|
1775
|
-
if (raw?.isQuotaExceeded) lines.push(" ! quota exceeded");
|
|
1836
|
+
const rows = [{ label: "Overall", percent: overallPercent, tail: "used", danger }];
|
|
1776
1837
|
const showUserQuota = hasBucket(raw?.userQuota);
|
|
1777
1838
|
const showAddOnQuota = hasBucket(raw?.addOnQuota);
|
|
1778
|
-
if (showUserQuota)
|
|
1779
|
-
if (showAddOnQuota)
|
|
1780
|
-
|
|
1781
|
-
if (
|
|
1782
|
-
|
|
1839
|
+
if (showUserQuota) rows.push(bucketRow("Plan quota", raw.userQuota, danger));
|
|
1840
|
+
if (showAddOnQuota) rows.push(bucketRow("Add-on", raw.addOnQuota, danger));
|
|
1841
|
+
const notes = [];
|
|
1842
|
+
if (danger) notes.push({ label: "Status", value: "quota exceeded", color: ANSI.red });
|
|
1843
|
+
if (!showUserQuota && !showAddOnQuota) notes.push({ label: "Buckets", value: "none returned" });
|
|
1844
|
+
if (raw?.isPlanQuotaProrated) notes.push({ label: "Note", value: "plan quota is prorated" });
|
|
1845
|
+
notes.push({ label: "Resets", value: formatResetTime(raw?.expiresAt, now) });
|
|
1783
1846
|
const manageUrl = raw?.addOnQuota?.detailUrl || raw?.upgradeUrl || region.manageUrl;
|
|
1784
|
-
if (manageUrl)
|
|
1785
|
-
|
|
1847
|
+
if (manageUrl) notes.push({ label: "Manage", value: manageUrl });
|
|
1848
|
+
const title = `${region.usageTitle}${userType}`;
|
|
1849
|
+
return {
|
|
1850
|
+
title: region.usageTitle,
|
|
1851
|
+
lines: [paint(title, color ? ANSI.bold : void 0), ...renderUsageRows(rows, notes, color)]
|
|
1852
|
+
};
|
|
1786
1853
|
}
|
|
1787
1854
|
async function fetchQoderQuota(accessToken, mode) {
|
|
1788
1855
|
const response = await fetch(getQoderUsageURL(mode), {
|
|
@@ -1819,6 +1886,14 @@ async function resolveAccessToken(providerID, ctx) {
|
|
|
1819
1886
|
return getCachedCredentials2("", providerID)?.access || void 0;
|
|
1820
1887
|
}
|
|
1821
1888
|
var FORMAT_ARGS = /* @__PURE__ */ new Set(["json", "raw", "debug"]);
|
|
1889
|
+
var PLAIN_ARGS = /* @__PURE__ */ new Set(["plain", "no-color", "nocolor"]);
|
|
1890
|
+
function shouldColorize(args, options = {}) {
|
|
1891
|
+
if (PLAIN_ARGS.has((args || "").trim().toLowerCase())) return false;
|
|
1892
|
+
const env = options.env ?? process.env;
|
|
1893
|
+
if (env.NO_COLOR || env.TERM === "dumb" || env.FORCE_COLOR === "0") return false;
|
|
1894
|
+
if (options.hasUI) return true;
|
|
1895
|
+
return options.isTTY ?? Boolean(process.stdout?.isTTY);
|
|
1896
|
+
}
|
|
1822
1897
|
async function runUsageCommand(mode, args, ctx) {
|
|
1823
1898
|
const region = getQoderRegionConfig(mode);
|
|
1824
1899
|
const providerID = region.providerID;
|
|
@@ -1830,7 +1905,8 @@ async function runUsageCommand(mode, args, ctx) {
|
|
|
1830
1905
|
return;
|
|
1831
1906
|
}
|
|
1832
1907
|
const raw = await fetchQoderQuota(accessToken, mode);
|
|
1833
|
-
const
|
|
1908
|
+
const color = !wantsRaw && shouldColorize(args, { hasUI: Boolean(ctx?.ui) });
|
|
1909
|
+
const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode, Date.now(), { color }).lines.join("\n");
|
|
1834
1910
|
ctx?.ui?.notify(output, "info");
|
|
1835
1911
|
if (!ctx?.ui) console.log(output);
|
|
1836
1912
|
} catch (error) {
|
|
@@ -1880,6 +1956,68 @@ function qoderEncodeBody(plaintext) {
|
|
|
1880
1956
|
return out;
|
|
1881
1957
|
}
|
|
1882
1958
|
|
|
1959
|
+
// src/protocol/queue.ts
|
|
1960
|
+
var MAX_QUEUE_RETRIES = 3;
|
|
1961
|
+
var MAX_WAIT_MS = 12e4;
|
|
1962
|
+
var DEFAULT_RETRY_SECONDS = 30;
|
|
1963
|
+
function digForQueue(node, depth) {
|
|
1964
|
+
if (depth > 6) return null;
|
|
1965
|
+
if (typeof node === "string") {
|
|
1966
|
+
const trimmed = node.trim();
|
|
1967
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
|
|
1968
|
+
try {
|
|
1969
|
+
return digForQueue(JSON.parse(trimmed), depth + 1);
|
|
1970
|
+
} catch {
|
|
1971
|
+
return null;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
if (node === null || typeof node !== "object") return null;
|
|
1975
|
+
if (Array.isArray(node)) {
|
|
1976
|
+
for (const item of node) {
|
|
1977
|
+
const found = digForQueue(item, depth + 1);
|
|
1978
|
+
if (found) return found;
|
|
1979
|
+
}
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
const record = node;
|
|
1983
|
+
if (record.isQueued === true) return record;
|
|
1984
|
+
for (const value of Object.values(record)) {
|
|
1985
|
+
const found = digForQueue(value, depth + 1);
|
|
1986
|
+
if (found) return found;
|
|
1987
|
+
}
|
|
1988
|
+
return null;
|
|
1989
|
+
}
|
|
1990
|
+
function positiveNumber(value, fallback) {
|
|
1991
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
1992
|
+
}
|
|
1993
|
+
function parseQueueNotice(envelope) {
|
|
1994
|
+
const payload = digForQueue(envelope, 0);
|
|
1995
|
+
if (!payload) return null;
|
|
1996
|
+
return {
|
|
1997
|
+
retryAfterMs: Math.min(positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3, MAX_WAIT_MS),
|
|
1998
|
+
waitTimeSeconds: positiveNumber(payload.waitTime, 0),
|
|
1999
|
+
queueCount: positiveNumber(payload.queueCount, 0)
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
function sleep(ms, signal) {
|
|
2003
|
+
return new Promise((resolve, reject) => {
|
|
2004
|
+
if (signal?.aborted) {
|
|
2005
|
+
reject(new Error("Aborted while waiting for the Qoder queue."));
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
let timer;
|
|
2009
|
+
const onAbort = () => {
|
|
2010
|
+
clearTimeout(timer);
|
|
2011
|
+
reject(new Error("Aborted while waiting for the Qoder queue."));
|
|
2012
|
+
};
|
|
2013
|
+
timer = setTimeout(() => {
|
|
2014
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2015
|
+
resolve();
|
|
2016
|
+
}, ms);
|
|
2017
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1883
2021
|
// src/protocol/thinking.ts
|
|
1884
2022
|
var THINKING_TAG_VARIANTS = [
|
|
1885
2023
|
{ open: "<thinking>", close: "</thinking>" },
|
|
@@ -2270,6 +2408,49 @@ function contentToText(content) {
|
|
|
2270
2408
|
}
|
|
2271
2409
|
return "";
|
|
2272
2410
|
}
|
|
2411
|
+
var PeekableReader = class {
|
|
2412
|
+
constructor(inner) {
|
|
2413
|
+
this.inner = inner;
|
|
2414
|
+
}
|
|
2415
|
+
inner;
|
|
2416
|
+
pending = [];
|
|
2417
|
+
text = "";
|
|
2418
|
+
/** Read one chunk, replaying peeked bytes first.
|
|
2419
|
+
*/
|
|
2420
|
+
async read() {
|
|
2421
|
+
const buffered = this.pending.shift();
|
|
2422
|
+
if (buffered) return { done: false, value: buffered };
|
|
2423
|
+
return await this.inner.read();
|
|
2424
|
+
}
|
|
2425
|
+
/**
|
|
2426
|
+
* Buffer chunks until the first complete line is available, and return the
|
|
2427
|
+
* text seen so far. Every byte read here is replayed by later `read()` calls.
|
|
2428
|
+
*/
|
|
2429
|
+
async peekLine(decoder) {
|
|
2430
|
+
while (!this.text.includes("\n")) {
|
|
2431
|
+
const { done, value } = await this.inner.read();
|
|
2432
|
+
if (done) break;
|
|
2433
|
+
this.pending.push(value);
|
|
2434
|
+
this.text += decoder.decode(value, { stream: true });
|
|
2435
|
+
}
|
|
2436
|
+
return this.text;
|
|
2437
|
+
}
|
|
2438
|
+
cancel(reason) {
|
|
2439
|
+
return this.inner.cancel(reason);
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
async function peekQueueNotice(reader, decoder) {
|
|
2443
|
+
const text = await reader.peekLine(decoder);
|
|
2444
|
+
const firstLine = text.split("\n")[0]?.trim() ?? "";
|
|
2445
|
+
if (!firstLine.startsWith("data:")) return null;
|
|
2446
|
+
const payload = firstLine.slice(5).trim();
|
|
2447
|
+
if (!payload || payload === "[DONE]") return null;
|
|
2448
|
+
try {
|
|
2449
|
+
return parseQueueNotice(JSON.parse(payload));
|
|
2450
|
+
} catch {
|
|
2451
|
+
return null;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2273
2454
|
function resolveRequestContext(piAi, context) {
|
|
2274
2455
|
const {
|
|
2275
2456
|
collapseSystemMessages,
|
|
@@ -2431,29 +2612,52 @@ function streamQoder(model, context, options) {
|
|
|
2431
2612
|
machineID
|
|
2432
2613
|
});
|
|
2433
2614
|
const modelSource = modelConfig.source || "system";
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
"Content-Type": "application/json",
|
|
2438
|
-
Accept: "text/event-stream",
|
|
2439
|
-
"Cache-Control": "no-cache",
|
|
2440
|
-
"Accept-Encoding": "identity",
|
|
2441
|
-
"X-Model-Key": qoderModel,
|
|
2442
|
-
"X-Model-Source": modelSource,
|
|
2443
|
-
...headers
|
|
2444
|
-
},
|
|
2445
|
-
body: encodedBytes,
|
|
2446
|
-
signal: options?.signal
|
|
2447
|
-
});
|
|
2448
|
-
if (!response.ok) {
|
|
2449
|
-
const errText = await response.text();
|
|
2450
|
-
throw new Error(`Qoder API request failed: ${response.status} ${response.statusText}. Response: ${errText}`);
|
|
2451
|
-
}
|
|
2452
|
-
const reader = response.body?.getReader();
|
|
2453
|
-
if (!reader) throw new Error("No response body");
|
|
2454
|
-
const decoder = new TextDecoder();
|
|
2615
|
+
let queuedAttempt = 0;
|
|
2616
|
+
let reader;
|
|
2617
|
+
let decoder;
|
|
2455
2618
|
let buffer = "";
|
|
2456
2619
|
let bufferStart = 0;
|
|
2620
|
+
for (; ; ) {
|
|
2621
|
+
const response = await fetch(chatURL, {
|
|
2622
|
+
method: "POST",
|
|
2623
|
+
headers: {
|
|
2624
|
+
"Content-Type": "application/json",
|
|
2625
|
+
Accept: "text/event-stream",
|
|
2626
|
+
"Cache-Control": "no-cache",
|
|
2627
|
+
"Accept-Encoding": "identity",
|
|
2628
|
+
"X-Model-Key": qoderModel,
|
|
2629
|
+
"X-Model-Source": modelSource,
|
|
2630
|
+
...headers
|
|
2631
|
+
},
|
|
2632
|
+
body: encodedBytes,
|
|
2633
|
+
signal: options?.signal
|
|
2634
|
+
});
|
|
2635
|
+
if (!response.ok) {
|
|
2636
|
+
const errText = await response.text();
|
|
2637
|
+
throw new Error(`Qoder API request failed: ${response.status} ${response.statusText}. Response: ${errText}`);
|
|
2638
|
+
}
|
|
2639
|
+
const body = response.body;
|
|
2640
|
+
if (!body) throw new Error("No response body");
|
|
2641
|
+
reader = new PeekableReader(body.getReader());
|
|
2642
|
+
decoder = new TextDecoder();
|
|
2643
|
+
buffer = "";
|
|
2644
|
+
bufferStart = 0;
|
|
2645
|
+
const queue = await peekQueueNotice(reader, decoder);
|
|
2646
|
+
if (queue) {
|
|
2647
|
+
await reader.cancel().catch(() => {
|
|
2648
|
+
});
|
|
2649
|
+
if (queuedAttempt >= MAX_QUEUE_RETRIES) {
|
|
2650
|
+
throw new Error(
|
|
2651
|
+
`Qoder is at capacity: the request stayed queued after ${queuedAttempt + 1} attempts (~${queue.waitTimeSeconds}s wait reported). Try again shortly.`
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
queuedAttempt++;
|
|
2655
|
+
await sleep(queue.retryAfterMs, options?.signal);
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2658
|
+
break;
|
|
2659
|
+
}
|
|
2660
|
+
if (!reader || !decoder) throw new Error("No response body");
|
|
2457
2661
|
let contentBlockIndex = -1;
|
|
2458
2662
|
let thinkingBlockIndex = -1;
|
|
2459
2663
|
let rawReasoningTail = "";
|
package/package.json
CHANGED