iobroker.ai-usage 0.14.0 → 0.15.0
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 +10 -12
- package/build/lib/error-text.js +48 -0
- package/build/lib/error-text.js.map +7 -0
- package/build/lib/http.js +41 -4
- package/build/lib/http.js.map +2 -2
- package/build/lib/poll-engine.js +47 -8
- package/build/lib/poll-engine.js.map +2 -2
- package/build/lib/sign-in-manager.js +4 -3
- package/build/lib/sign-in-manager.js.map +2 -2
- package/build/main.js +41 -19
- package/build/main.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -155,6 +155,16 @@ so instead of pretending to be connected; signing in again is all it takes.
|
|
|
155
155
|
Placeholder for the next version (at the beginning of the line):
|
|
156
156
|
-->
|
|
157
157
|
|
|
158
|
+
### 0.15.0 (2026-09-16)
|
|
159
|
+
|
|
160
|
+
- Fixed: Model channels of an organisation account no longer vanish at the turn of a month — a model with no usage yet was deleted with its history and re-created on its next use
|
|
161
|
+
- Fixed: Stopping the instance right after it started no longer leaves the accounts showing as connected while the instance is switched off
|
|
162
|
+
- Fixed: Failures that reached the log, the `info.error` datapoint and Sentry as `[object Object]` now name the actual error
|
|
163
|
+
- Improved: A provider answer that keeps growing can no longer push the adapter towards running out of memory — it is cut off and reported as a service fault
|
|
164
|
+
|
|
165
|
+
The month-boundary fix concerns OpenAI organisation accounts, which have no real account here; it is
|
|
166
|
+
covered by tests and by the counter-test that limit windows are still cleaned up.
|
|
167
|
+
|
|
158
168
|
### 0.14.0 (2026-09-15)
|
|
159
169
|
|
|
160
170
|
- Fixed: A failed write of the token file after a refresh lost the sign-in for good — the provider had already rotated them, so the next poll reported a rejected sign-in
|
|
@@ -214,18 +224,6 @@ OpenRouter, DeepSeek, OpenAI or Anthropic account.
|
|
|
214
224
|
- Fixed: A per-model folder is now named in your ioBroker language as well, instead of carrying the provider's bare model identifier as its only name
|
|
215
225
|
- New: The datapoints whose meaning is not obvious from their name now carry a short explanation in eleven languages, shown in the object tree
|
|
216
226
|
|
|
217
|
-
### 0.11.0 (2026-09-05)
|
|
218
|
-
|
|
219
|
-
- Fixed: Signing in from the instance settings works again — a leftover setting from an earlier version had silently closed the adapter's message channel, so none of the three flows reached it
|
|
220
|
-
- Fixed: A subscription whose stored sign-in was rejected no longer claims to be signed in — the row now offers the sign-in again instead of showing a green check next to an error
|
|
221
|
-
- Fixed: The status badge of an account no longer blanks out for a moment when a single status read is missed — a hiccup in the settings page is not an account without a status
|
|
222
|
-
- Fixed: A stored credential whose name sorts high in the alphabet is no longer missing from the account list in the instance settings
|
|
223
|
-
- Fixed: The settings page falls back to English for a browser language the adapter does not ship, instead of passing that language on unchecked
|
|
224
|
-
- Improved: All object names are now available in eleven languages instead of English only, and a renamed object reaches installations that already exist
|
|
225
|
-
- Improved: ChatGPT usage is read with the identity that endpoint expects, the way the Claude query already did — fewer rejected requests on that account
|
|
226
|
-
- Improved: Monthly cost reports can no longer be cut short in silence — a report that does not fit is reported in the log instead of producing a figure that is too low
|
|
227
|
-
- Changed: "Highest account utilisation" says what it always measured — the fullest limit window **or** the account's remaining budget
|
|
228
|
-
|
|
229
227
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
230
228
|
|
|
231
229
|
## Support
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var error_text_exports = {};
|
|
20
|
+
__export(error_text_exports, {
|
|
21
|
+
errorText: () => errorText
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(error_text_exports);
|
|
24
|
+
const MAX_OBJECT_CHARS = 200;
|
|
25
|
+
function errorText(err) {
|
|
26
|
+
if (err instanceof Error) {
|
|
27
|
+
return err.message || err.name || "Error";
|
|
28
|
+
}
|
|
29
|
+
if (typeof err === "string") {
|
|
30
|
+
return err;
|
|
31
|
+
}
|
|
32
|
+
if (err === null || err === void 0 || typeof err !== "object") {
|
|
33
|
+
return String(err);
|
|
34
|
+
}
|
|
35
|
+
let json;
|
|
36
|
+
try {
|
|
37
|
+
json = JSON.stringify(err);
|
|
38
|
+
} catch {
|
|
39
|
+
json = void 0;
|
|
40
|
+
}
|
|
41
|
+
const text = json != null ? json : Object.prototype.toString.call(err);
|
|
42
|
+
return text.length > MAX_OBJECT_CHARS ? `${text.slice(0, MAX_OBJECT_CHARS)}\u2026` : text;
|
|
43
|
+
}
|
|
44
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
45
|
+
0 && (module.exports = {
|
|
46
|
+
errorText
|
|
47
|
+
});
|
|
48
|
+
//# sourceMappingURL=error-text.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/error-text.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * One readable line for anything a `catch` can hand over.\n *\n * Deliberately without imports: every layer uses it, including `http.ts`, which\n * sits below the helper modules.\n */\n\n/** Longest text a stringified object may contribute before it is cut. */\nconst MAX_OBJECT_CHARS = 200;\n\n/**\n * Turn any thrown value into text a user can read.\n *\n * JavaScript lets code throw anything, and the inline\n * `e instanceof Error ? e.message : String(e)` this replaces rendered every plain\n * object as `[object Object]` \u2014 a rejected `{ code: \"ECONNRESET\" }` or an HTTP\n * client's error object reached the log and Sentry with nothing in it.\n *\n * The object branch is the point of the helper; `String()` already covers strings,\n * numbers and symbols. `JSON.stringify` is guarded twice because it is hostile in\n * exactly the situation this runs in: it THROWS on a circular structure (an error\n * carrying the response it came from) and RETURNS `undefined` for a value it\n * cannot represent at all.\n *\n * @param err the caught value\n * @returns a single-line description, never empty\n */\nexport function errorText(err: unknown): string {\n if (err instanceof Error) {\n // `message` can be empty on a hand-built error \u2014 the name still says something.\n return err.message || err.name || \"Error\";\n }\n if (typeof err === \"string\") {\n return err;\n }\n if (err === null || err === undefined || typeof err !== \"object\") {\n // Numbers, booleans, symbols, bigint: `String()` is the readable form.\n return String(err);\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(err);\n } catch {\n // Circular, or a getter that throws \u2014 fall through to the tag below.\n json = undefined;\n }\n const text = json ?? Object.prototype.toString.call(err);\n return text.length > MAX_OBJECT_CHARS ? `${text.slice(0, MAX_OBJECT_CHARS)}\u2026` : text;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,MAAM,mBAAmB;AAmBlB,SAAS,UAAU,KAAsB;AAC9C,MAAI,eAAe,OAAO;AAExB,WAAO,IAAI,WAAW,IAAI,QAAQ;AAAA,EACpC;AACA,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,QAAQ,QAAQ,UAAa,OAAO,QAAQ,UAAU;AAEhE,WAAO,OAAO,GAAG;AAAA,EACnB;AACA,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,UAAU,GAAG;AAAA,EAC3B,QAAQ;AAEN,WAAO;AAAA,EACT;AACA,QAAM,OAAO,sBAAQ,OAAO,UAAU,SAAS,KAAK,GAAG;AACvD,SAAO,KAAK,SAAS,mBAAmB,GAAG,KAAK,MAAM,GAAG,gBAAgB,CAAC,WAAM;AAClF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/build/lib/http.js
CHANGED
|
@@ -23,14 +23,45 @@ __export(http_exports, {
|
|
|
23
23
|
postJson: () => postJson
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(http_exports);
|
|
26
|
+
var import_error_text = require("./error-text");
|
|
26
27
|
var import_provider = require("./provider");
|
|
27
28
|
const REQUEST_TIMEOUT_MS = 15e3;
|
|
29
|
+
const MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
30
|
+
async function readCappedText(response) {
|
|
31
|
+
if (!response.body) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
const reader = response.body.getReader();
|
|
35
|
+
const decoder = new TextDecoder();
|
|
36
|
+
let text = "";
|
|
37
|
+
let bytes = 0;
|
|
38
|
+
try {
|
|
39
|
+
for (; ; ) {
|
|
40
|
+
const { done, value } = await reader.read();
|
|
41
|
+
if (done) {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
if (!value) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
bytes += value.byteLength;
|
|
48
|
+
if (bytes > MAX_BODY_BYTES) {
|
|
49
|
+
await reader.cancel();
|
|
50
|
+
throw new import_provider.FetchError("service", `response body exceeds ${MAX_BODY_BYTES} bytes`);
|
|
51
|
+
}
|
|
52
|
+
text += decoder.decode(value, { stream: true });
|
|
53
|
+
}
|
|
54
|
+
} finally {
|
|
55
|
+
reader.releaseLock();
|
|
56
|
+
}
|
|
57
|
+
return text + decoder.decode();
|
|
58
|
+
}
|
|
28
59
|
async function request(url, init, authOn400 = false) {
|
|
29
60
|
let response;
|
|
30
61
|
try {
|
|
31
62
|
response = await fetch(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
32
63
|
} catch (e) {
|
|
33
|
-
throw new import_provider.FetchError("network",
|
|
64
|
+
throw new import_provider.FetchError("network", (0, import_error_text.errorText)(e));
|
|
34
65
|
}
|
|
35
66
|
if (!response.ok) {
|
|
36
67
|
const detail = await errorDetail(response);
|
|
@@ -42,10 +73,16 @@ async function request(url, init, authOn400 = false) {
|
|
|
42
73
|
}
|
|
43
74
|
throw new import_provider.FetchError("service", `HTTP ${response.status}${detail}`);
|
|
44
75
|
}
|
|
76
|
+
let text;
|
|
77
|
+
try {
|
|
78
|
+
text = await readCappedText(response);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
throw e instanceof import_provider.FetchError ? e : new import_provider.FetchError("service", `unreadable body: ${(0, import_error_text.errorText)(e)}`);
|
|
81
|
+
}
|
|
45
82
|
try {
|
|
46
|
-
return
|
|
83
|
+
return JSON.parse(text);
|
|
47
84
|
} catch (e) {
|
|
48
|
-
throw new import_provider.FetchError("service", `invalid JSON: ${
|
|
85
|
+
throw new import_provider.FetchError("service", `invalid JSON: ${(0, import_error_text.errorText)(e)}`);
|
|
49
86
|
}
|
|
50
87
|
}
|
|
51
88
|
const MAX_DETAIL_CHARS = 200;
|
|
@@ -53,7 +90,7 @@ async function errorDetail(response) {
|
|
|
53
90
|
var _a, _b;
|
|
54
91
|
let text;
|
|
55
92
|
try {
|
|
56
|
-
text = await response
|
|
93
|
+
text = await readCappedText(response);
|
|
57
94
|
} catch {
|
|
58
95
|
return "";
|
|
59
96
|
}
|
package/build/lib/http.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/http.ts"],
|
|
4
|
-
"sourcesContent": ["import { FetchError } from \"./provider\";\n\n/** Per-request timeout (ms). */\nconst REQUEST_TIMEOUT_MS = 15000;\n\n/** The JSON-GET seam the provider modules use \u2014 injectable for tests. */\nexport type JsonFetch = (url: string, headers: Record<string, string>) => Promise<unknown>;\n\n/**\n * Per-call options of the two POST seams.\n *\n * `authOn400` used to be baked into `postJson`/`postForm`, which made EVERY post\n * read a 400 as a rejected sign-in. That is right for the OAuth token endpoints\n * and wrong everywhere else: the ChatGPT device-code poll takes an auth failure\n * as \"the user has not confirmed yet\" and would have waited out the whole\n * fifteen minutes on a 400, and Google's Code-Assist call skipped its second\n * host because it thought the sign-in was gone. The flag belongs at the call.\n */\nexport interface PostOptions {\n /** Extra request headers. */\n headers?: Record<string, string>;\n /** True only for OAuth token endpoints, which answer a dead code or refresh token with 400. */\n authOn400?: boolean;\n}\n\n/** The JSON-POST seam \u2014 one definition for every provider module. */\nexport type JsonPost = (url: string, body: Record<string, unknown>, options?: PostOptions) => Promise<unknown>;\n\n/** The form-POST seam (OAuth code redemption). */\nexport type FormPost = (url: string, form: Record<string, string>, options?: PostOptions) => Promise<unknown>;\n\n/**\n * Run one request and turn its outcome into the shared failure classification:\n * 401/403 (and 400 where the provider answers a rejected grant that way) become an\n * auth error, 429 a rate-limit error, any other bad status or unparsable body a\n * SERVICE error (the service answered and is broken), and only a throw \u2014 refused\n * connection, DNS failure, timeout \u2014 a network error. The poll engine turns that\n * split into \"the AI service is down\" versus \"this host has no connection\".\n *\n * @param url the request URL\n * @param init the request options (method, headers, body)\n * @param authOn400 true when a 400 means a rejected grant rather than a service fault\n * @returns the parsed JSON body\n */\nasync function request(url: string, init: RequestInit, authOn400 = false): Promise<unknown> {\n let response: Response;\n try {\n response = await fetch(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });\n } catch (e) {\n throw new FetchError(\"network\",
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAA2B;AAG3B,MAAM,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import { errorText } from \"./error-text\";\nimport { FetchError } from \"./provider\";\n\n/** Per-request timeout (ms). */\nconst REQUEST_TIMEOUT_MS = 15000;\n\n/**\n * Largest response body this adapter will hold in memory (bytes).\n *\n * Generous on purpose: the biggest answer any provider sends is a month of daily\n * cost buckets grouped by model, which is orders of magnitude below this. The cap\n * is not a budget, it is a backstop.\n */\nconst MAX_BODY_BYTES = 8 * 1024 * 1024;\n\n/**\n * Read a response body as text, refusing to grow past {@link MAX_BODY_BYTES}.\n *\n * The request timeout bounds how LONG a body may take, not how BIG it may get \u2014 on\n * a fast link fifteen seconds is a lot of memory, and this runs in a process that\n * stays up for months. Counted while reading rather than from `Content-Length`: a\n * chunked answer carries no length at all, and a declared one is the server's claim,\n * not a measurement.\n *\n * @param response the response to read\n * @returns the decoded body\n * @throws {FetchError} `service` once the body passes the cap\n */\nasync function readCappedText(response: Response): Promise<string> {\n if (!response.body) {\n return \"\";\n }\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let text = \"\";\n let bytes = 0;\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (!value) {\n continue;\n }\n bytes += value.byteLength;\n if (bytes > MAX_BODY_BYTES) {\n // Stop the transfer instead of draining a body we have already refused.\n await reader.cancel();\n throw new FetchError(\"service\", `response body exceeds ${MAX_BODY_BYTES} bytes`);\n }\n text += decoder.decode(value, { stream: true });\n }\n } finally {\n reader.releaseLock();\n }\n return text + decoder.decode();\n}\n\n/** The JSON-GET seam the provider modules use \u2014 injectable for tests. */\nexport type JsonFetch = (url: string, headers: Record<string, string>) => Promise<unknown>;\n\n/**\n * Per-call options of the two POST seams.\n *\n * `authOn400` used to be baked into `postJson`/`postForm`, which made EVERY post\n * read a 400 as a rejected sign-in. That is right for the OAuth token endpoints\n * and wrong everywhere else: the ChatGPT device-code poll takes an auth failure\n * as \"the user has not confirmed yet\" and would have waited out the whole\n * fifteen minutes on a 400, and Google's Code-Assist call skipped its second\n * host because it thought the sign-in was gone. The flag belongs at the call.\n */\nexport interface PostOptions {\n /** Extra request headers. */\n headers?: Record<string, string>;\n /** True only for OAuth token endpoints, which answer a dead code or refresh token with 400. */\n authOn400?: boolean;\n}\n\n/** The JSON-POST seam \u2014 one definition for every provider module. */\nexport type JsonPost = (url: string, body: Record<string, unknown>, options?: PostOptions) => Promise<unknown>;\n\n/** The form-POST seam (OAuth code redemption). */\nexport type FormPost = (url: string, form: Record<string, string>, options?: PostOptions) => Promise<unknown>;\n\n/**\n * Run one request and turn its outcome into the shared failure classification:\n * 401/403 (and 400 where the provider answers a rejected grant that way) become an\n * auth error, 429 a rate-limit error, any other bad status or unparsable body a\n * SERVICE error (the service answered and is broken), and only a throw \u2014 refused\n * connection, DNS failure, timeout \u2014 a network error. The poll engine turns that\n * split into \"the AI service is down\" versus \"this host has no connection\".\n *\n * @param url the request URL\n * @param init the request options (method, headers, body)\n * @param authOn400 true when a 400 means a rejected grant rather than a service fault\n * @returns the parsed JSON body\n */\nasync function request(url: string, init: RequestInit, authOn400 = false): Promise<unknown> {\n let response: Response;\n try {\n response = await fetch(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });\n } catch (e) {\n throw new FetchError(\"network\", errorText(e));\n }\n if (!response.ok) {\n // The status alone reaches the user as `info.error`, and \"HTTP 401\" tells them\n // nothing they can act on. OpenRouter, OpenAI and Anthropic all answer with\n // `{ error: { message } }` \u2014 reading it turns the datapoint into \"invalid API\n // key\". Best effort in the strictest sense: the status decides the class, the\n // body only decorates the text, and a body that cannot be read changes nothing.\n const detail = await errorDetail(response);\n if (response.status === 401 || response.status === 403 || (authOn400 && response.status === 400)) {\n throw new FetchError(\"auth\", `HTTP ${response.status}${detail}`);\n }\n if (response.status === 429) {\n throw new FetchError(\"rate-limit\", `HTTP 429${detail}`);\n }\n // 5xx = the service answered and is broken; anything else unexpected is treated\n // the same way, because the service DID answer \u2014 only a throw above means we\n // never reached it.\n throw new FetchError(\"service\", `HTTP ${response.status}${detail}`);\n }\n let text: string;\n try {\n text = await readCappedText(response);\n } catch (e) {\n // The cap's own verdict passes through unchanged; anything else here is a\n // connection that died mid-body, which counts as a service fault exactly as an\n // unreadable body always did.\n throw e instanceof FetchError ? e : new FetchError(\"service\", `unreadable body: ${errorText(e)}`);\n }\n try {\n return JSON.parse(text) as unknown;\n } catch (e) {\n throw new FetchError(\"service\", `invalid JSON: ${errorText(e)}`);\n }\n}\n\n/** Longest provider message taken over into the error text. */\nconst MAX_DETAIL_CHARS = 200;\n\n/**\n * The provider's own words for a failed request, ready to append.\n *\n * Never throws and never rejects: a body that is missing, unreadable, not JSON or\n * shaped differently simply yields \"\" and the caller keeps the bare status. The\n * body of a failed response is consumed here either way, so nothing is left\n * dangling (measured 2026-09-12: the \"socket leak\" this used to be blamed on does\n * not exist).\n *\n * @param response the failed response\n * @returns \" \u2014 <message>\" or an empty string\n */\nasync function errorDetail(response: Response): Promise<string> {\n let text: string;\n try {\n text = await readCappedText(response);\n } catch {\n // Unreadable, or an error page past the cap \u2014 the caller keeps the bare status.\n return \"\";\n }\n let message: unknown;\n try {\n const body = JSON.parse(text) as { error?: unknown; message?: unknown };\n const error = body.error;\n message =\n typeof error === \"string\" ? error : ((error as { message?: unknown })?.message ?? body.message ?? undefined);\n } catch {\n // Not JSON: a short plain-text body is still better than nothing, a long one\n // (an HTML error page from a proxy) is noise.\n message = text.trim().length > 0 && text.trim().length <= MAX_DETAIL_CHARS ? text.trim() : undefined;\n }\n if (typeof message !== \"string\" || message.trim().length === 0) {\n return \"\";\n }\n const trimmed = message.trim();\n return ` \u2014 ${trimmed.length > MAX_DETAIL_CHARS ? `${trimmed.slice(0, MAX_DETAIL_CHARS)}\u2026` : trimmed}`;\n}\n\n/**\n * GET a JSON document.\n *\n * @param url the request URL\n * @param headers request headers (Authorization etc.)\n * @returns the parsed JSON body\n */\nexport async function getJson(url: string, headers: Record<string, string>): Promise<unknown> {\n return request(url, { headers });\n}\n\n/**\n * POST a JSON document.\n *\n * @param url the request URL\n * @param body the JSON body\n * @param options extra headers, and whether a 400 means a rejected grant\n * @returns the parsed JSON response\n */\nexport async function postJson(\n url: string,\n body: Record<string, unknown>,\n options: PostOptions = {},\n): Promise<unknown> {\n return request(\n url,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", ...options.headers },\n body: JSON.stringify(body),\n },\n options.authOn400 === true,\n );\n}\n\n/**\n * POST a form-encoded body and return the parsed JSON answer.\n *\n * OAuth code redemption uses form encoding while token refresh often uses JSON \u2014\n * ChatGPT/Codex needs BOTH, so the two shapes are separate helpers rather than one\n * guessing wrapper.\n *\n * @param url the request URL\n * @param form the form fields\n * @param options extra headers, and whether a 400 means a rejected grant\n * @returns the parsed JSON body\n */\nexport async function postForm(url: string, form: Record<string, string>, options: PostOptions = {}): Promise<unknown> {\n return request(\n url,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\", ...options.headers },\n body: new URLSearchParams(form).toString(),\n },\n options.authOn400 === true,\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/lib/poll-engine.js
CHANGED
|
@@ -22,6 +22,7 @@ __export(poll_engine_exports, {
|
|
|
22
22
|
isDelivering: () => isDelivering
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(poll_engine_exports);
|
|
25
|
+
var import_error_text = require("./error-text");
|
|
25
26
|
var import_i18n = require("./i18n");
|
|
26
27
|
var import_sign_in = require("./sign-in");
|
|
27
28
|
var import_provider = require("./provider");
|
|
@@ -344,12 +345,52 @@ class PollEngine {
|
|
|
344
345
|
async removeVanished(runtime, delivered) {
|
|
345
346
|
var _a;
|
|
346
347
|
const known = (_a = runtime.deliveredIds) != null ? _a : await this.deps.listStateIds(runtime.config.id);
|
|
347
|
-
|
|
348
|
+
const reported = this.zeroUnusedModels(known, delivered);
|
|
349
|
+
for (const id of (0, import_snapshot_tree.orphanObjectIds)(known, reported, runtime.staticIds)) {
|
|
348
350
|
await this.deps.deleteObject(id);
|
|
349
351
|
runtime.createdObjects.delete(id);
|
|
350
352
|
this.deps.log.info(`${runtime.config.name}: removed "${id}" \u2014 the provider no longer reports it`);
|
|
351
353
|
}
|
|
352
|
-
runtime.deliveredIds =
|
|
354
|
+
runtime.deliveredIds = reported;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* A model the report does not mention this round is IDLE, not gone — write its 0.
|
|
358
|
+
*
|
|
359
|
+
* The usage report is a statement about a PERIOD, not an inventory: a model is
|
|
360
|
+
* missing from it because nothing ran on it, not because the provider dropped it.
|
|
361
|
+
* The sweep cannot tell those apart, so it used to delete on the weaker reading.
|
|
362
|
+
*
|
|
363
|
+
* Decision 49 closed the daily half of this — the OpenAI model list is built from
|
|
364
|
+
* the whole month, so UTC midnight no longer empties it. The MONTH boundary was
|
|
365
|
+
* the half left open: the report starts over on the 1st, and the moment the first
|
|
366
|
+
* model of the new month reports usage, every other model's channel was swept —
|
|
367
|
+
* history, enum membership and all — and re-created on its next use.
|
|
368
|
+
*
|
|
369
|
+
* Writing the 0 fixes both halves at once. The channel stays (it is in the
|
|
370
|
+
* delivered set, so {@link orphanObjectIds} leaves it alone) and it stops lying:
|
|
371
|
+
* without this, an unreported model would simply freeze on its last count under a
|
|
372
|
+
* name that says "today" — the exact lie decision 49 removed from the block as a
|
|
373
|
+
* whole. `limits.*` keeps the old reading, and rightly so: there the provider
|
|
374
|
+
* reports the PLAN, so a window that stops appearing really is gone.
|
|
375
|
+
*
|
|
376
|
+
* @param known every state id the account had before
|
|
377
|
+
* @param delivered the state ids this snapshot wrote
|
|
378
|
+
* @returns `delivered` plus the idle model states, which now carry a written 0
|
|
379
|
+
*/
|
|
380
|
+
zeroUnusedModels(known, delivered) {
|
|
381
|
+
const delivering = new Set(delivered);
|
|
382
|
+
const idle = [];
|
|
383
|
+
for (const id of known) {
|
|
384
|
+
if (delivering.has(id)) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const parts = id.split(".");
|
|
388
|
+
if (parts.length === 4 && parts[1] === "models" && parts[3] === "tokensToday") {
|
|
389
|
+
this.deps.setState(id, 0);
|
|
390
|
+
idle.push(id);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return idle.length > 0 ? [...delivered, ...idle] : delivered;
|
|
353
394
|
}
|
|
354
395
|
/**
|
|
355
396
|
* The numbers arrived, the object database did not take them.
|
|
@@ -366,7 +407,7 @@ class PollEngine {
|
|
|
366
407
|
* @param error the thrown error
|
|
367
408
|
*/
|
|
368
409
|
handleStorageFailure(runtime, error) {
|
|
369
|
-
const message =
|
|
410
|
+
const message = (0, import_error_text.errorText)(error);
|
|
370
411
|
runtime.state = "storage-error";
|
|
371
412
|
runtime.error = `Values fetched but not stored \u2014 the object database rejected the write (${message})`;
|
|
372
413
|
if (runtime.storageFailed) {
|
|
@@ -391,7 +432,7 @@ class PollEngine {
|
|
|
391
432
|
* @param error the thrown error
|
|
392
433
|
*/
|
|
393
434
|
handleSweepFailure(runtime, error) {
|
|
394
|
-
const message =
|
|
435
|
+
const message = (0, import_error_text.errorText)(error);
|
|
395
436
|
if (runtime.sweepFailed) {
|
|
396
437
|
this.deps.log.debug(`${runtime.config.name}: the cleanup of vanished entries still fails (${message})`);
|
|
397
438
|
return;
|
|
@@ -417,7 +458,7 @@ class PollEngine {
|
|
|
417
458
|
handleFailure(runtime, error) {
|
|
418
459
|
var _a, _b, _c, _d;
|
|
419
460
|
const { config } = runtime;
|
|
420
|
-
const message =
|
|
461
|
+
const message = (0, import_error_text.errorText)(error);
|
|
421
462
|
if (error instanceof import_provider.FetchError && error.kind === "no-credentials") {
|
|
422
463
|
runtime.failCount = 0;
|
|
423
464
|
runtime.serviceOnline = true;
|
|
@@ -664,9 +705,7 @@ class PollEngine {
|
|
|
664
705
|
try {
|
|
665
706
|
runtime.status.warning = await this.deps.readState(`${config.id}.warning`) === true;
|
|
666
707
|
} catch (e) {
|
|
667
|
-
this.deps.log.debug(
|
|
668
|
-
`${config.name}: could not read the previous warning state (${e instanceof Error ? e.message : String(e)})`
|
|
669
|
-
);
|
|
708
|
+
this.deps.log.debug(`${config.name}: could not read the previous warning state (${(0, import_error_text.errorText)(e)})`);
|
|
670
709
|
}
|
|
671
710
|
}
|
|
672
711
|
/** The totals skeleton (channel + states). */
|