dsh-lcx-codex 0.4.2 → 0.4.3-pre.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 +116 -202
- package/cordis.patch.yml +3 -20
- package/lib/client.js +273 -146
- package/lib/compact-v2.js +218 -199
- package/lib/dsh-compat.js +227 -100
- package/lib/dsh-responses.js +445 -277
- package/lib/index.js +851 -757
- package/lib/json-store.js +50 -31
- package/lib/native-checkpoint.js +520 -194
- package/lib/responses-request.js +106 -121
- package/lib/responses-stream.js +972 -443
- package/lib/route.js +229 -355
- package/lib/service-mutex.js +73 -64
- package/lib/token-budget.js +176 -108
- package/lib/transport.js +277 -67
- package/lib/types/client/index.d.ts +6 -0
- package/lib/types/compact-v2.d.ts +104 -0
- package/lib/types/dsh-compat.d.ts +78 -0
- package/lib/types/dsh-responses.d.ts +82 -0
- package/lib/types/index.d.ts +83 -0
- package/lib/types/json-store.d.ts +10 -0
- package/lib/types/native-checkpoint.d.ts +213 -0
- package/lib/types/responses-request.d.ts +58 -0
- package/lib/types/responses-stream.d.ts +51 -0
- package/lib/types/route.d.ts +132 -0
- package/lib/types/service-mutex.d.ts +14 -0
- package/lib/types/token-budget.d.ts +50 -0
- package/lib/types/transport.d.ts +20 -0
- package/lib/types/web-run-output.d.ts +29 -0
- package/lib/types/web-search-alpha.d.ts +286 -0
- package/lib/types/web-search-capability.d.ts +26 -0
- package/lib/types/web-search-hosted.d.ts +246 -0
- package/lib/types/web-search-ref-store.d.ts +22 -0
- package/lib/web-run-output.js +167 -18
- package/lib/web-search-alpha.js +865 -163
- package/lib/web-search-capability.js +55 -65
- package/lib/web-search-hosted.js +210 -32
- package/lib/web-search-ref-store.js +63 -59
- package/package.json +79 -27
- package/ARCHITECTURE.md +0 -117
- package/CHANGELOG.md +0 -224
- package/README_EN.md +0 -277
- package/assets/dsh-lcx-codex-banner.jpg +0 -0
- package/lib/legacy-v3.js +0 -20
- package/lib/responses-replay.js +0 -68
- package/scripts/probe-alpha.mjs +0 -43
- package/scripts/validate-dsh-schema.mjs +0 -31
package/lib/service-mutex.js
CHANGED
|
@@ -1,72 +1,81 @@
|
|
|
1
1
|
function abortReason(signal) {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
if (signal?.reason instanceof Error)
|
|
3
|
+
return signal.reason;
|
|
4
|
+
return new DOMException("The operation was aborted", "AbortError");
|
|
4
5
|
}
|
|
5
|
-
|
|
6
6
|
export class ServiceMutex {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
locked = false;
|
|
8
|
+
closed = false;
|
|
9
|
+
closeReason;
|
|
10
|
+
queue = [];
|
|
11
|
+
idleWaiters = [];
|
|
12
|
+
constructor() { }
|
|
13
|
+
acquire(signal) {
|
|
14
|
+
if (this.closed)
|
|
15
|
+
return Promise.reject(this.closeReason ?? new Error("service mutex closed"));
|
|
16
|
+
if (signal?.aborted)
|
|
17
|
+
return Promise.reject(abortReason(signal));
|
|
18
|
+
if (!this.locked) {
|
|
19
|
+
this.locked = true;
|
|
20
|
+
return Promise.resolve(() => this.release());
|
|
21
|
+
}
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const waiter = { resolve, reject, signal };
|
|
24
|
+
if (signal) {
|
|
25
|
+
const onAbort = () => {
|
|
26
|
+
const index = this.queue.indexOf(waiter);
|
|
27
|
+
if (index >= 0)
|
|
28
|
+
this.queue.splice(index, 1);
|
|
29
|
+
signal.removeEventListener("abort", onAbort);
|
|
30
|
+
reject(abortReason(signal));
|
|
31
|
+
};
|
|
32
|
+
waiter.onAbort = onAbort;
|
|
33
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
34
|
+
}
|
|
35
|
+
this.queue.push(waiter);
|
|
36
|
+
});
|
|
21
37
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
release() {
|
|
39
|
+
while (this.queue.length > 0) {
|
|
40
|
+
const waiter = this.queue.shift();
|
|
41
|
+
if (!waiter)
|
|
42
|
+
continue;
|
|
43
|
+
if (waiter.signal && waiter.onAbort)
|
|
44
|
+
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
45
|
+
if (waiter.signal?.aborted) {
|
|
46
|
+
waiter.reject(abortReason(waiter.signal));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
waiter.resolve(() => this.release());
|
|
50
|
+
return;
|
|
30
51
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
release() {
|
|
38
|
-
while (this.queue.length > 0) {
|
|
39
|
-
const waiter = this.queue.shift()
|
|
40
|
-
if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener('abort', waiter.onAbort)
|
|
41
|
-
if (waiter.signal?.aborted) {
|
|
42
|
-
waiter.reject(abortReason(waiter.signal))
|
|
43
|
-
continue
|
|
44
|
-
}
|
|
45
|
-
waiter.resolve(() => this.release())
|
|
46
|
-
return
|
|
52
|
+
this.locked = false;
|
|
53
|
+
const idle = this.idleWaiters.splice(0);
|
|
54
|
+
for (const resolve of idle)
|
|
55
|
+
resolve();
|
|
47
56
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
57
|
+
async run(signal, task) {
|
|
58
|
+
const release = await this.acquire(signal);
|
|
59
|
+
try {
|
|
60
|
+
return await task();
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
release();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
close(reason = new Error("service mutex closing")) {
|
|
67
|
+
if (!this.closed) {
|
|
68
|
+
this.closed = true;
|
|
69
|
+
this.closeReason = reason;
|
|
70
|
+
const queued = this.queue.splice(0);
|
|
71
|
+
for (const waiter of queued) {
|
|
72
|
+
if (waiter.signal && waiter.onAbort)
|
|
73
|
+
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
74
|
+
waiter.reject(reason);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!this.locked)
|
|
78
|
+
return Promise.resolve();
|
|
79
|
+
return new Promise((resolve) => this.idleWaiters.push(resolve));
|
|
68
80
|
}
|
|
69
|
-
if (!this.locked) return Promise.resolve()
|
|
70
|
-
return new Promise((resolve) => this.idleWaiters.push(resolve))
|
|
71
|
-
}
|
|
72
81
|
}
|
package/lib/token-budget.js
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const ASCII_WORD = /[A-Za-z0-9]/u
|
|
10
|
-
const STRUCTURAL = /[\p{P}\p{S}]/u
|
|
11
|
-
const DATA_URI = /^data:[^;,\s]+(?:;[^,\s]*)?;base64,[A-Za-z0-9+/\s]+={0,2}$/u
|
|
12
|
-
const ENCODED_TEXT = /^[A-Za-z0-9+/_-]{512,}={0,2}$/u
|
|
13
|
-
|
|
1
|
+
export const PORTABLE_BUDGET_ERROR_CODE = "LCX_PORTABLE_BUDGET_EXCEEDED";
|
|
2
|
+
const MAX_BUDGET_INPUT_CHARS = 2_000_000;
|
|
3
|
+
const CONSERVATIVE_IMAGE_TOKEN_COST = 2_048;
|
|
4
|
+
const CJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
|
5
|
+
const ASCII_WORD = /[A-Za-z0-9]/u;
|
|
6
|
+
const STRUCTURAL = /[\p{P}\p{S}]/u;
|
|
7
|
+
const DATA_URI = /^data:[^;,\s]+(?:;[^,\s]*)?;base64,[A-Za-z0-9+/\s]+={0,2}$/u;
|
|
8
|
+
const ENCODED_TEXT = /^[A-Za-z0-9+/_-]{512,}={0,2}$/u;
|
|
14
9
|
/** @param {string} value */
|
|
15
|
-
function looksEncodedText(value) {
|
|
16
|
-
|
|
10
|
+
function looksEncodedText(value) {
|
|
11
|
+
return DATA_URI.test(value) || ENCODED_TEXT.test(value);
|
|
12
|
+
}
|
|
17
13
|
/**
|
|
18
14
|
* A small conservative fallback, deliberately not a tokenizer. The baseline
|
|
19
15
|
* preserves legacy /4 while CJK and structural characters cost more.
|
|
@@ -21,84 +17,133 @@ function looksEncodedText(value) { return DATA_URI.test(value) || ENCODED_TEXT.t
|
|
|
21
17
|
* @returns {number | undefined}
|
|
22
18
|
*/
|
|
23
19
|
export function estimateTextTokens(value) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
20
|
+
if (typeof value !== "string")
|
|
21
|
+
return undefined;
|
|
22
|
+
if (value.length > MAX_BUDGET_INPUT_CHARS || looksEncodedText(value))
|
|
23
|
+
return Math.max(1, value.length);
|
|
24
|
+
let weighted = 0;
|
|
25
|
+
let asciiRun = 0;
|
|
26
|
+
const flushAscii = () => {
|
|
27
|
+
if (asciiRun > 0)
|
|
28
|
+
weighted += Math.ceil(asciiRun / 4);
|
|
29
|
+
asciiRun = 0;
|
|
30
|
+
};
|
|
31
|
+
for (const char of value) {
|
|
32
|
+
if (ASCII_WORD.test(char)) {
|
|
33
|
+
asciiRun += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
flushAscii();
|
|
37
|
+
if (/\s/u.test(char))
|
|
38
|
+
continue;
|
|
39
|
+
if (CJK.test(char) || STRUCTURAL.test(char))
|
|
40
|
+
weighted += 1;
|
|
41
|
+
else
|
|
42
|
+
weighted += Math.ceil(char.length / 2);
|
|
43
|
+
}
|
|
44
|
+
flushAscii();
|
|
45
|
+
return Math.max(1, Math.ceil(value.length / 4), weighted);
|
|
38
46
|
}
|
|
39
|
-
|
|
40
47
|
/**
|
|
41
48
|
* @param {unknown} value
|
|
42
49
|
* @returns {value is UnknownRecord}
|
|
43
50
|
*/
|
|
44
|
-
function isObject(value) {
|
|
51
|
+
function isObject(value) {
|
|
52
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
53
|
+
}
|
|
45
54
|
/**
|
|
46
55
|
* @param {unknown} value
|
|
47
56
|
* @param {Set<object>} [seen]
|
|
48
57
|
* @returns {boolean}
|
|
49
58
|
*/
|
|
50
59
|
function containsOpaqueValue(value, seen = new Set()) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
if (value === null || typeof value !== "object")
|
|
61
|
+
return false;
|
|
62
|
+
if (ArrayBuffer.isView(value) ||
|
|
63
|
+
value instanceof ArrayBuffer ||
|
|
64
|
+
seen.has(value))
|
|
65
|
+
return true;
|
|
66
|
+
seen.add(value);
|
|
67
|
+
return Object.values(value).some((entry) => containsOpaqueValue(entry, seen));
|
|
55
68
|
}
|
|
56
69
|
/** @param {unknown} value */
|
|
57
|
-
function safeJson(value) {
|
|
70
|
+
function safeJson(value) {
|
|
71
|
+
try {
|
|
72
|
+
if (containsOpaqueValue(value))
|
|
73
|
+
return undefined;
|
|
74
|
+
const encoded = JSON.stringify(value);
|
|
75
|
+
return typeof encoded === "string" ? encoded : undefined;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
58
81
|
/** @param {unknown} value */
|
|
59
|
-
function safeScalar(value) {
|
|
60
|
-
|
|
82
|
+
function safeScalar(value) {
|
|
83
|
+
return typeof value === "string" ||
|
|
84
|
+
typeof value === "number" ||
|
|
85
|
+
typeof value === "boolean"
|
|
86
|
+
? String(value)
|
|
87
|
+
: undefined;
|
|
88
|
+
}
|
|
61
89
|
/**
|
|
62
90
|
* @param {unknown} content
|
|
63
91
|
* @returns {unknown[] | undefined}
|
|
64
92
|
*/
|
|
65
93
|
function visibleContent(content) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
if (typeof content === "string")
|
|
95
|
+
return [{ type: "text", text: content }];
|
|
96
|
+
if (!Array.isArray(content))
|
|
97
|
+
return undefined;
|
|
98
|
+
const parts = [];
|
|
99
|
+
for (const part of content) {
|
|
100
|
+
if (!isObject(part) || typeof part.type !== "string")
|
|
101
|
+
return undefined;
|
|
102
|
+
if (["text", "input_text", "output_text"].includes(part.type)) {
|
|
103
|
+
if (typeof part.text !== "string")
|
|
104
|
+
return undefined;
|
|
105
|
+
parts.push({ type: part.type, text: part.text });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (part.type === "reasoning") {
|
|
109
|
+
if (typeof part.text !== "string")
|
|
110
|
+
return undefined;
|
|
111
|
+
parts.push({ type: "reasoning", text: part.text });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (part.type === "tool-call") {
|
|
115
|
+
const id = safeScalar(part.id);
|
|
116
|
+
const name = safeScalar(part.name);
|
|
117
|
+
const argumentsText = safeJson(part.arguments);
|
|
118
|
+
if (id === undefined || name === undefined || argumentsText === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
parts.push({ type: "tool-call", id, name, arguments: argumentsText });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (part.type === "tool-result") {
|
|
124
|
+
const toolCallId = safeScalar(part.toolCallId);
|
|
125
|
+
const toolName = safeScalar(part.toolName ?? part.name ?? "unknown");
|
|
126
|
+
const nested = visibleContent(part.content);
|
|
127
|
+
if (toolCallId === undefined ||
|
|
128
|
+
toolName === undefined ||
|
|
129
|
+
nested === undefined)
|
|
130
|
+
return undefined;
|
|
131
|
+
parts.push({
|
|
132
|
+
type: "tool-result",
|
|
133
|
+
toolCallId,
|
|
134
|
+
toolName,
|
|
135
|
+
content: nested,
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (["image", "input_image", "output_image", "dsh_image_attachment"].includes(part.type)) {
|
|
140
|
+
parts.push({ type: part.type, image: true });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
75
144
|
}
|
|
76
|
-
|
|
77
|
-
if (typeof part.text !== 'string') return undefined
|
|
78
|
-
parts.push({ type: 'reasoning', text: part.text })
|
|
79
|
-
continue
|
|
80
|
-
}
|
|
81
|
-
if (part.type === 'tool-call') {
|
|
82
|
-
const id = safeScalar(part.id); const name = safeScalar(part.name); const argumentsText = safeJson(part.arguments)
|
|
83
|
-
if (id === undefined || name === undefined || argumentsText === undefined) return undefined
|
|
84
|
-
parts.push({ type: 'tool-call', id, name, arguments: argumentsText })
|
|
85
|
-
continue
|
|
86
|
-
}
|
|
87
|
-
if (part.type === 'tool-result') {
|
|
88
|
-
const toolCallId = safeScalar(part.toolCallId); const toolName = safeScalar(part.toolName ?? part.name ?? 'unknown'); const nested = visibleContent(part.content)
|
|
89
|
-
if (toolCallId === undefined || toolName === undefined || nested === undefined) return undefined
|
|
90
|
-
parts.push({ type: 'tool-result', toolCallId, toolName, content: nested })
|
|
91
|
-
continue
|
|
92
|
-
}
|
|
93
|
-
if (['image', 'input_image', 'output_image', 'dsh_image_attachment'].includes(part.type)) {
|
|
94
|
-
parts.push({ type: part.type, image: true })
|
|
95
|
-
continue
|
|
96
|
-
}
|
|
97
|
-
return undefined
|
|
98
|
-
}
|
|
99
|
-
return parts
|
|
145
|
+
return parts;
|
|
100
146
|
}
|
|
101
|
-
|
|
102
147
|
/**
|
|
103
148
|
* Project an item to model-visible fields only. Opaque provider state, raw
|
|
104
149
|
* binary, and replay/session metadata are intentionally excluded.
|
|
@@ -106,21 +151,39 @@ function visibleContent(content) {
|
|
|
106
151
|
* @returns {unknown | undefined}
|
|
107
152
|
*/
|
|
108
153
|
export function modelVisibleBudgetView(item) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
154
|
+
if (!isObject(item))
|
|
155
|
+
return undefined;
|
|
156
|
+
if (item.type === "function_call") {
|
|
157
|
+
const callId = safeScalar(item.call_id);
|
|
158
|
+
const name = safeScalar(item.name);
|
|
159
|
+
const argumentsText = safeJson(item.arguments);
|
|
160
|
+
return callId === undefined ||
|
|
161
|
+
name === undefined ||
|
|
162
|
+
argumentsText === undefined
|
|
163
|
+
? undefined
|
|
164
|
+
: {
|
|
165
|
+
type: "function_call",
|
|
166
|
+
call_id: callId,
|
|
167
|
+
name,
|
|
168
|
+
arguments: argumentsText,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (item.type === "function_call_output") {
|
|
172
|
+
const callId = safeScalar(item.call_id);
|
|
173
|
+
const output = typeof item.output === "string" ? item.output : safeJson(item.output);
|
|
174
|
+
return callId === undefined || output === undefined
|
|
175
|
+
? undefined
|
|
176
|
+
: { type: "function_call_output", call_id: callId, output };
|
|
177
|
+
}
|
|
178
|
+
if (item.type !== undefined && item.type !== "message")
|
|
179
|
+
return undefined;
|
|
180
|
+
if (!["developer", "system", "user", "assistant"].includes(String(item.role ?? "")))
|
|
181
|
+
return undefined;
|
|
182
|
+
const content = visibleContent(item.content);
|
|
183
|
+
return content === undefined
|
|
184
|
+
? undefined
|
|
185
|
+
: { role: String(item.role), content };
|
|
122
186
|
}
|
|
123
|
-
|
|
124
187
|
/**
|
|
125
188
|
* Images have provider/model-dependent costs. Count each known image block with
|
|
126
189
|
* a fixed conservative surcharge without inspecting base64 or attachment data.
|
|
@@ -128,38 +191,43 @@ export function modelVisibleBudgetView(item) {
|
|
|
128
191
|
* @returns {number}
|
|
129
192
|
*/
|
|
130
193
|
function imageTokenCost(value) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
194
|
+
if (Array.isArray(value)) {
|
|
195
|
+
let total = 0;
|
|
196
|
+
for (const entry of value)
|
|
197
|
+
total += imageTokenCost(entry);
|
|
198
|
+
return total;
|
|
199
|
+
}
|
|
200
|
+
if (!isObject(value))
|
|
201
|
+
return 0;
|
|
202
|
+
let total = value.image === true ? CONSERVATIVE_IMAGE_TOKEN_COST : 0;
|
|
203
|
+
for (const entry of Object.values(value))
|
|
204
|
+
total += imageTokenCost(entry);
|
|
205
|
+
return total;
|
|
140
206
|
}
|
|
141
|
-
|
|
142
207
|
/**
|
|
143
208
|
* @param {unknown} item
|
|
144
209
|
* @returns {number | undefined}
|
|
145
210
|
*/
|
|
146
211
|
export function estimateBudgetItem(item) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
212
|
+
const view = modelVisibleBudgetView(item);
|
|
213
|
+
if (view === undefined)
|
|
214
|
+
return undefined;
|
|
215
|
+
const encoded = safeJson(view);
|
|
216
|
+
const textCost = estimateTextTokens(encoded);
|
|
217
|
+
return textCost === undefined ? undefined : textCost + imageTokenCost(view);
|
|
152
218
|
}
|
|
153
|
-
|
|
154
219
|
/** @param {unknown} maxChars */
|
|
155
220
|
export function portableTokenCeiling(maxChars) {
|
|
156
|
-
|
|
221
|
+
return typeof maxChars === "number" &&
|
|
222
|
+
Number.isSafeInteger(maxChars) &&
|
|
223
|
+
maxChars > 0
|
|
224
|
+
? Math.ceil(maxChars / 4)
|
|
225
|
+
: undefined;
|
|
157
226
|
}
|
|
158
|
-
|
|
159
227
|
/** @param {string} message */
|
|
160
228
|
export function portableBudgetError(message) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
229
|
+
/** @type {Error & { code?: string }} */
|
|
230
|
+
const error = new Error(message);
|
|
231
|
+
error.code = PORTABLE_BUDGET_ERROR_CODE;
|
|
232
|
+
return error;
|
|
165
233
|
}
|