@naisys/common 3.0.0-beta.5 → 3.0.0-beta.7

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.
@@ -2,259 +2,259 @@ import { LlmApiType } from "./modelTypes.js";
2
2
  // --- Built-in LLM models ---
3
3
  // Prices are per 1M tokens in USD. Last updated: February 2026.
4
4
  export const builtInLlmModels = [
5
- {
6
- key: LlmApiType.None,
7
- label: "None",
8
- versionName: LlmApiType.None,
9
- apiType: LlmApiType.None,
10
- apiKeyVar: "",
11
- maxTokens: 10_000,
12
- inputCost: 0,
13
- outputCost: 0,
14
- },
15
- {
16
- key: LlmApiType.Mock,
17
- label: "Mock",
18
- versionName: LlmApiType.Mock,
19
- apiType: LlmApiType.Mock,
20
- apiKeyVar: "",
21
- maxTokens: 10_000,
22
- inputCost: 0,
23
- outputCost: 0,
24
- },
25
- // ── Open Router ──────────────────────────────────────────────────────
26
- {
27
- key: "llama4",
28
- label: "Llama 4 Maverick",
29
- versionName: "meta-llama/llama-4-maverick",
30
- baseUrl: "https://openrouter.ai/api/v1",
31
- apiType: LlmApiType.OpenAI,
32
- apiKeyVar: "OPENROUTER_API_KEY",
33
- maxTokens: 1_000_000,
34
- inputCost: 0.15,
35
- outputCost: 0.6,
36
- supportsVision: true,
37
- },
38
- // ── xAI / Grok ──────────────────────────────────────────────────────
39
- // https://docs.x.ai/developers/models
40
- {
41
- key: "grok4",
42
- label: "Grok 4",
43
- versionName: "grok-4",
44
- baseUrl: "https://api.x.ai/v1",
45
- apiType: LlmApiType.OpenAI,
46
- apiKeyVar: "XAI_API_KEY",
47
- maxTokens: 256_000,
48
- inputCost: 3,
49
- outputCost: 15,
50
- cacheWriteCost: 0.75,
51
- cacheReadCost: 0.75,
52
- cacheTtlSeconds: 300,
53
- supportsVision: true,
54
- },
55
- {
56
- key: "grok4fast",
57
- label: "Grok 4.1 Fast",
58
- versionName: "grok-4.1-fast",
59
- baseUrl: "https://api.x.ai/v1",
60
- apiType: LlmApiType.OpenAI,
61
- apiKeyVar: "XAI_API_KEY",
62
- maxTokens: 2_000_000,
63
- inputCost: 0.2,
64
- outputCost: 0.5,
65
- cacheWriteCost: 0.05,
66
- cacheReadCost: 0.05,
67
- cacheTtlSeconds: 300,
68
- supportsVision: true,
69
- },
70
- // ── OpenAI Models ────────────────────────────────────────────────────
71
- // https://openai.com/api/pricing/
72
- {
73
- key: "gpt5",
74
- label: "GPT 5.4",
75
- versionName: "gpt-5.4",
76
- apiType: LlmApiType.OpenAI,
77
- apiKeyVar: "OPENAI_API_KEY",
78
- maxTokens: 400_000,
79
- inputCost: 2.5,
80
- outputCost: 15.0,
81
- cacheWriteCost: 0.25,
82
- cacheReadCost: 0.25,
83
- cacheTtlSeconds: 300,
84
- supportsVision: true,
85
- supportsComputerUse: true,
86
- },
87
- {
88
- key: "gpt5mini",
89
- label: "GPT 5 Mini",
90
- versionName: "gpt-5-mini",
91
- apiType: LlmApiType.OpenAI,
92
- apiKeyVar: "OPENAI_API_KEY",
93
- maxTokens: 400_000,
94
- inputCost: 0.25,
95
- outputCost: 2.0,
96
- cacheWriteCost: 0.025,
97
- cacheReadCost: 0.025,
98
- cacheTtlSeconds: 300,
99
- supportsVision: true,
100
- },
101
- {
102
- key: "gpt5nano",
103
- label: "GPT 5 Nano",
104
- versionName: "gpt-5-nano",
105
- apiType: LlmApiType.OpenAI,
106
- apiKeyVar: "OPENAI_API_KEY",
107
- maxTokens: 400_000,
108
- inputCost: 0.05,
109
- outputCost: 0.4,
110
- cacheWriteCost: 0.005,
111
- cacheReadCost: 0.005,
112
- cacheTtlSeconds: 300,
113
- supportsVision: true,
114
- },
115
- // ── Google Models ────────────────────────────────────────────────────
116
- // https://ai.google.dev/gemini-api/docs/pricing
117
- {
118
- key: "gemini3pro",
119
- label: "Gemini 3.1 Pro",
120
- versionName: "gemini-3.1-pro-preview",
121
- apiType: LlmApiType.Google,
122
- apiKeyVar: "GOOGLE_API_KEY",
123
- maxTokens: 2_000_000,
124
- inputCost: 2.0,
125
- outputCost: 12.0,
126
- cacheWriteCost: 0.2,
127
- cacheReadCost: 0.2,
128
- cacheTtlSeconds: 300,
129
- supportsVision: true,
130
- supportsHearing: true,
131
- },
132
- {
133
- key: "gemini3flash",
134
- label: "Gemini 3 Flash",
135
- versionName: "gemini-3-flash-preview",
136
- apiType: LlmApiType.Google,
137
- apiKeyVar: "GOOGLE_API_KEY",
138
- maxTokens: 1_000_000,
139
- inputCost: 0.5,
140
- outputCost: 3.0,
141
- cacheWriteCost: 0.05,
142
- cacheReadCost: 0.05,
143
- cacheTtlSeconds: 300,
144
- supportsVision: true,
145
- supportsHearing: true,
146
- supportsComputerUse: true,
147
- },
148
- {
149
- key: "gemini2pro",
150
- label: "Gemini 2 Pro",
151
- versionName: "gemini-2.5-computer-use-preview-10-2025",
152
- apiType: LlmApiType.Google,
153
- apiKeyVar: "GOOGLE_API_KEY",
154
- maxTokens: 2_000_000,
155
- inputCost: 2.0,
156
- outputCost: 12.0,
157
- cacheWriteCost: 0.2,
158
- cacheReadCost: 0.2,
159
- cacheTtlSeconds: 300,
160
- supportsVision: true,
161
- supportsHearing: true,
162
- supportsComputerUse: true,
163
- },
164
- // ── Anthropic Models ─────────────────────────────────────────────────
165
- // https://platform.claude.com/docs/en/about-claude/pricing
166
- // Cache: 5m write = 1.25× input, read = 0.1× input
167
- {
168
- key: "claude4opus",
169
- label: "Claude Opus 4.6",
170
- versionName: "claude-opus-4-6",
171
- apiType: LlmApiType.Anthropic,
172
- apiKeyVar: "ANTHROPIC_API_KEY",
173
- maxTokens: 200_000,
174
- inputCost: 5,
175
- outputCost: 25,
176
- cacheWriteCost: 6.25,
177
- cacheReadCost: 0.5,
178
- cacheTtlSeconds: 300,
179
- supportsVision: true,
180
- supportsComputerUse: true,
181
- },
182
- {
183
- key: "claude4sonnet",
184
- label: "Claude Sonnet 4.6",
185
- versionName: "claude-sonnet-4-6",
186
- apiType: LlmApiType.Anthropic,
187
- apiKeyVar: "ANTHROPIC_API_KEY",
188
- maxTokens: 200_000,
189
- inputCost: 3,
190
- outputCost: 15,
191
- cacheWriteCost: 3.75,
192
- cacheReadCost: 0.3,
193
- cacheTtlSeconds: 300,
194
- supportsVision: true,
195
- supportsComputerUse: true,
196
- },
197
- {
198
- key: "claude4haiku",
199
- label: "Claude Haiku 4.5",
200
- versionName: "claude-haiku-4-5",
201
- apiType: LlmApiType.Anthropic,
202
- apiKeyVar: "ANTHROPIC_API_KEY",
203
- maxTokens: 200_000,
204
- inputCost: 1,
205
- outputCost: 5,
206
- cacheWriteCost: 1.25,
207
- cacheReadCost: 0.1,
208
- cacheTtlSeconds: 300,
209
- supportsVision: true,
210
- supportsComputerUse: true,
211
- },
5
+ {
6
+ key: LlmApiType.None,
7
+ label: "None",
8
+ versionName: LlmApiType.None,
9
+ apiType: LlmApiType.None,
10
+ apiKeyVar: "",
11
+ maxTokens: 10_000,
12
+ inputCost: 0,
13
+ outputCost: 0,
14
+ },
15
+ {
16
+ key: LlmApiType.Mock,
17
+ label: "Mock",
18
+ versionName: LlmApiType.Mock,
19
+ apiType: LlmApiType.Mock,
20
+ apiKeyVar: "",
21
+ maxTokens: 10_000,
22
+ inputCost: 0,
23
+ outputCost: 0,
24
+ },
25
+ // ── Open Router ──────────────────────────────────────────────────────
26
+ {
27
+ key: "llama4",
28
+ label: "Llama 4 Maverick",
29
+ versionName: "meta-llama/llama-4-maverick",
30
+ baseUrl: "https://openrouter.ai/api/v1",
31
+ apiType: LlmApiType.OpenAI,
32
+ apiKeyVar: "OPENROUTER_API_KEY",
33
+ maxTokens: 1_000_000,
34
+ inputCost: 0.15,
35
+ outputCost: 0.6,
36
+ supportsVision: true,
37
+ },
38
+ // ── xAI / Grok ──────────────────────────────────────────────────────
39
+ // https://docs.x.ai/developers/models
40
+ {
41
+ key: "grok4",
42
+ label: "Grok 4",
43
+ versionName: "grok-4",
44
+ baseUrl: "https://api.x.ai/v1",
45
+ apiType: LlmApiType.OpenAI,
46
+ apiKeyVar: "XAI_API_KEY",
47
+ maxTokens: 256_000,
48
+ inputCost: 3,
49
+ outputCost: 15,
50
+ cacheWriteCost: 0.75,
51
+ cacheReadCost: 0.75,
52
+ cacheTtlSeconds: 300,
53
+ supportsVision: true,
54
+ },
55
+ {
56
+ key: "grok4fast",
57
+ label: "Grok 4.1 Fast",
58
+ versionName: "grok-4.1-fast",
59
+ baseUrl: "https://api.x.ai/v1",
60
+ apiType: LlmApiType.OpenAI,
61
+ apiKeyVar: "XAI_API_KEY",
62
+ maxTokens: 2_000_000,
63
+ inputCost: 0.2,
64
+ outputCost: 0.5,
65
+ cacheWriteCost: 0.05,
66
+ cacheReadCost: 0.05,
67
+ cacheTtlSeconds: 300,
68
+ supportsVision: true,
69
+ },
70
+ // ── OpenAI Models ────────────────────────────────────────────────────
71
+ // https://openai.com/api/pricing/
72
+ {
73
+ key: "gpt5",
74
+ label: "GPT 5.4",
75
+ versionName: "gpt-5.4",
76
+ apiType: LlmApiType.OpenAI,
77
+ apiKeyVar: "OPENAI_API_KEY",
78
+ maxTokens: 400_000,
79
+ inputCost: 2.5,
80
+ outputCost: 15.0,
81
+ cacheWriteCost: 0.25,
82
+ cacheReadCost: 0.25,
83
+ cacheTtlSeconds: 300,
84
+ supportsVision: true,
85
+ supportsComputerUse: true,
86
+ },
87
+ {
88
+ key: "gpt5mini",
89
+ label: "GPT 5 Mini",
90
+ versionName: "gpt-5-mini",
91
+ apiType: LlmApiType.OpenAI,
92
+ apiKeyVar: "OPENAI_API_KEY",
93
+ maxTokens: 400_000,
94
+ inputCost: 0.25,
95
+ outputCost: 2.0,
96
+ cacheWriteCost: 0.025,
97
+ cacheReadCost: 0.025,
98
+ cacheTtlSeconds: 300,
99
+ supportsVision: true,
100
+ },
101
+ {
102
+ key: "gpt5nano",
103
+ label: "GPT 5 Nano",
104
+ versionName: "gpt-5-nano",
105
+ apiType: LlmApiType.OpenAI,
106
+ apiKeyVar: "OPENAI_API_KEY",
107
+ maxTokens: 400_000,
108
+ inputCost: 0.05,
109
+ outputCost: 0.4,
110
+ cacheWriteCost: 0.005,
111
+ cacheReadCost: 0.005,
112
+ cacheTtlSeconds: 300,
113
+ supportsVision: true,
114
+ },
115
+ // ── Google Models ────────────────────────────────────────────────────
116
+ // https://ai.google.dev/gemini-api/docs/pricing
117
+ {
118
+ key: "gemini3pro",
119
+ label: "Gemini 3.1 Pro",
120
+ versionName: "gemini-3.1-pro-preview",
121
+ apiType: LlmApiType.Google,
122
+ apiKeyVar: "GOOGLE_API_KEY",
123
+ maxTokens: 2_000_000,
124
+ inputCost: 2.0,
125
+ outputCost: 12.0,
126
+ cacheWriteCost: 0.2,
127
+ cacheReadCost: 0.2,
128
+ cacheTtlSeconds: 300,
129
+ supportsVision: true,
130
+ supportsHearing: true,
131
+ },
132
+ {
133
+ key: "gemini3flash",
134
+ label: "Gemini 3 Flash",
135
+ versionName: "gemini-3-flash-preview",
136
+ apiType: LlmApiType.Google,
137
+ apiKeyVar: "GOOGLE_API_KEY",
138
+ maxTokens: 1_000_000,
139
+ inputCost: 0.5,
140
+ outputCost: 3.0,
141
+ cacheWriteCost: 0.05,
142
+ cacheReadCost: 0.05,
143
+ cacheTtlSeconds: 300,
144
+ supportsVision: true,
145
+ supportsHearing: true,
146
+ supportsComputerUse: true,
147
+ },
148
+ {
149
+ key: "gemini2pro",
150
+ label: "Gemini 2 Pro",
151
+ versionName: "gemini-2.5-computer-use-preview-10-2025",
152
+ apiType: LlmApiType.Google,
153
+ apiKeyVar: "GOOGLE_API_KEY",
154
+ maxTokens: 2_000_000,
155
+ inputCost: 2.0,
156
+ outputCost: 12.0,
157
+ cacheWriteCost: 0.2,
158
+ cacheReadCost: 0.2,
159
+ cacheTtlSeconds: 300,
160
+ supportsVision: true,
161
+ supportsHearing: true,
162
+ supportsComputerUse: true,
163
+ },
164
+ // ── Anthropic Models ─────────────────────────────────────────────────
165
+ // https://platform.claude.com/docs/en/about-claude/pricing
166
+ // Cache: 5m write = 1.25× input, read = 0.1× input
167
+ {
168
+ key: "claude4opus",
169
+ label: "Claude Opus 4.6",
170
+ versionName: "claude-opus-4-6",
171
+ apiType: LlmApiType.Anthropic,
172
+ apiKeyVar: "ANTHROPIC_API_KEY",
173
+ maxTokens: 200_000,
174
+ inputCost: 5,
175
+ outputCost: 25,
176
+ cacheWriteCost: 6.25,
177
+ cacheReadCost: 0.5,
178
+ cacheTtlSeconds: 300,
179
+ supportsVision: true,
180
+ supportsComputerUse: true,
181
+ },
182
+ {
183
+ key: "claude4sonnet",
184
+ label: "Claude Sonnet 4.6",
185
+ versionName: "claude-sonnet-4-6",
186
+ apiType: LlmApiType.Anthropic,
187
+ apiKeyVar: "ANTHROPIC_API_KEY",
188
+ maxTokens: 200_000,
189
+ inputCost: 3,
190
+ outputCost: 15,
191
+ cacheWriteCost: 3.75,
192
+ cacheReadCost: 0.3,
193
+ cacheTtlSeconds: 300,
194
+ supportsVision: true,
195
+ supportsComputerUse: true,
196
+ },
197
+ {
198
+ key: "claude4haiku",
199
+ label: "Claude Haiku 4.5",
200
+ versionName: "claude-haiku-4-5",
201
+ apiType: LlmApiType.Anthropic,
202
+ apiKeyVar: "ANTHROPIC_API_KEY",
203
+ maxTokens: 200_000,
204
+ inputCost: 1,
205
+ outputCost: 5,
206
+ cacheWriteCost: 1.25,
207
+ cacheReadCost: 0.1,
208
+ cacheTtlSeconds: 300,
209
+ supportsVision: true,
210
+ supportsComputerUse: true,
211
+ },
212
212
  ];
213
213
  // --- Built-in image models ---
214
214
  // Costs are approximate per-image for 1024x1024.
215
215
  export const builtInImageModels = [
216
- {
217
- key: "gptimage1high",
218
- label: "GPT Image 1.5 High",
219
- versionName: "gpt-image-1.5",
220
- size: "1024x1024",
221
- apiKeyVar: "OPENAI_API_KEY",
222
- quality: "high",
223
- cost: 0.17,
224
- },
225
- {
226
- key: "gptimage1medium",
227
- label: "GPT Image 1.5 Medium",
228
- versionName: "gpt-image-1.5",
229
- size: "1024x1024",
230
- apiKeyVar: "OPENAI_API_KEY",
231
- quality: "medium",
232
- cost: 0.04,
233
- },
234
- {
235
- key: "gptimage1low",
236
- label: "GPT Image 1.5 Low",
237
- versionName: "gpt-image-1.5",
238
- size: "1024x1024",
239
- apiKeyVar: "OPENAI_API_KEY",
240
- quality: "low",
241
- cost: 0.01,
242
- },
243
- {
244
- key: "dalle3-1024-HD",
245
- label: "DALL-E 3 1024 HD",
246
- versionName: "dall-e-3",
247
- size: "1024x1024",
248
- apiKeyVar: "OPENAI_API_KEY",
249
- quality: "hd",
250
- cost: 0.08,
251
- },
252
- {
253
- key: "dalle3-1024",
254
- label: "DALL-E 3 1024",
255
- versionName: "dall-e-3",
256
- size: "1024x1024",
257
- apiKeyVar: "OPENAI_API_KEY",
258
- cost: 0.04,
259
- },
216
+ {
217
+ key: "gptimage1high",
218
+ label: "GPT Image 1.5 High",
219
+ versionName: "gpt-image-1.5",
220
+ size: "1024x1024",
221
+ apiKeyVar: "OPENAI_API_KEY",
222
+ quality: "high",
223
+ cost: 0.17,
224
+ },
225
+ {
226
+ key: "gptimage1medium",
227
+ label: "GPT Image 1.5 Medium",
228
+ versionName: "gpt-image-1.5",
229
+ size: "1024x1024",
230
+ apiKeyVar: "OPENAI_API_KEY",
231
+ quality: "medium",
232
+ cost: 0.04,
233
+ },
234
+ {
235
+ key: "gptimage1low",
236
+ label: "GPT Image 1.5 Low",
237
+ versionName: "gpt-image-1.5",
238
+ size: "1024x1024",
239
+ apiKeyVar: "OPENAI_API_KEY",
240
+ quality: "low",
241
+ cost: 0.01,
242
+ },
243
+ {
244
+ key: "dalle3-1024-HD",
245
+ label: "DALL-E 3 1024 HD",
246
+ versionName: "dall-e-3",
247
+ size: "1024x1024",
248
+ apiKeyVar: "OPENAI_API_KEY",
249
+ quality: "hd",
250
+ cost: 0.08,
251
+ },
252
+ {
253
+ key: "dalle3-1024",
254
+ label: "DALL-E 3 1024",
255
+ versionName: "dall-e-3",
256
+ size: "1024x1024",
257
+ apiKeyVar: "OPENAI_API_KEY",
258
+ cost: 0.04,
259
+ },
260
260
  ];
@@ -1,8 +1,9 @@
1
1
  export function sanitizeSpendLimit(num) {
2
- if (num === undefined) return undefined;
3
- const n = Number(num);
4
- if (isNaN(n) || n <= 0) {
5
- return undefined;
6
- }
7
- return n;
2
+ if (num === undefined)
3
+ return undefined;
4
+ const n = Number(num);
5
+ if (isNaN(n) || n <= 0) {
6
+ return undefined;
7
+ }
8
+ return n;
8
9
  }
package/dist/costUtils.js CHANGED
@@ -5,23 +5,19 @@
5
5
  * only for the window to close again, and the llm cache to *expire* creating a cycle of constant cache misses
6
6
  */
7
7
  export function calculatePeriodBoundaries(hours) {
8
- const now = new Date();
9
- // Get midnight of current day in local time
10
- const midnight = new Date(now);
11
- midnight.setHours(0, 0, 0, 0);
12
- // Calculate milliseconds since midnight
13
- const msSinceMidnight = now.getTime() - midnight.getTime();
14
- const hoursSinceMidnight = msSinceMidnight / (1000 * 60 * 60);
15
- // Calculate which period we're in (0, 1, 2, ...)
16
- const periodIndex = Math.floor(hoursSinceMidnight / hours);
17
- // Calculate period start and end
18
- const periodStartHours = periodIndex * hours;
19
- const periodEndHours = (periodIndex + 1) * hours;
20
- const periodStart = new Date(
21
- midnight.getTime() + periodStartHours * 60 * 60 * 1000,
22
- );
23
- const periodEnd = new Date(
24
- midnight.getTime() + periodEndHours * 60 * 60 * 1000,
25
- );
26
- return { periodStart, periodEnd };
8
+ const now = new Date();
9
+ // Get midnight of current day in local time
10
+ const midnight = new Date(now);
11
+ midnight.setHours(0, 0, 0, 0);
12
+ // Calculate milliseconds since midnight
13
+ const msSinceMidnight = now.getTime() - midnight.getTime();
14
+ const hoursSinceMidnight = msSinceMidnight / (1000 * 60 * 60);
15
+ // Calculate which period we're in (0, 1, 2, ...)
16
+ const periodIndex = Math.floor(hoursSinceMidnight / hours);
17
+ // Calculate period start and end
18
+ const periodStartHours = periodIndex * hours;
19
+ const periodEndHours = (periodIndex + 1) * hours;
20
+ const periodStart = new Date(midnight.getTime() + periodStartHours * 60 * 60 * 1000);
21
+ const periodEnd = new Date(midnight.getTime() + periodEndHours * 60 * 60 * 1000);
22
+ return { periodStart, periodEnd };
27
23
  }
@@ -7,45 +7,38 @@
7
7
  * Interfaces are duck-typed so @naisys/common doesn't need a Fastify dependency.
8
8
  */
9
9
  function errorLabel(statusCode) {
10
- switch (statusCode) {
11
- case 400:
12
- return "Bad Request";
13
- case 401:
14
- return "Unauthorized";
15
- case 403:
16
- return "Forbidden";
17
- case 404:
18
- return "Not Found";
19
- case 409:
20
- return "Conflict";
21
- default:
22
- return statusCode >= 500 ? "Internal Server Error" : "Error";
23
- }
10
+ switch (statusCode) {
11
+ case 400:
12
+ return "Bad Request";
13
+ case 401:
14
+ return "Unauthorized";
15
+ case 403:
16
+ return "Forbidden";
17
+ case 404:
18
+ return "Not Found";
19
+ case 409:
20
+ return "Conflict";
21
+ default:
22
+ return statusCode >= 500 ? "Internal Server Error" : "Error";
23
+ }
24
24
  }
25
25
  export function commonErrorHandler(error, request, reply) {
26
- let statusCode = error.statusCode ?? 500;
27
- let message = error.message;
28
- // Prisma unique-constraint violation → 409 Conflict
29
- if (
30
- error.name === "PrismaClientKnownRequestError" &&
31
- error.code === "P2002"
32
- ) {
33
- statusCode = 409;
34
- message = "A record with that unique value already exists";
35
- }
36
- request.log.error(
37
- { err: error, url: request.url, method: request.method },
38
- "Request error",
39
- );
40
- // Pre-stringify to bypass the route's Zod response serializer
41
- reply
42
- .status(statusCode)
43
- .header("content-type", "application/json; charset=utf-8")
44
- .send(
45
- JSON.stringify({
26
+ let statusCode = error.statusCode ?? 500;
27
+ let message = error.message;
28
+ // Prisma unique-constraint violation → 409 Conflict
29
+ if (error.name === "PrismaClientKnownRequestError" &&
30
+ error.code === "P2002") {
31
+ statusCode = 409;
32
+ message = "A record with that unique value already exists";
33
+ }
34
+ request.log.error({ err: error, url: request.url, method: request.method }, "Request error");
35
+ // Pre-stringify to bypass the route's Zod response serializer
36
+ reply
37
+ .status(statusCode)
38
+ .header("content-type", "application/json; charset=utf-8")
39
+ .send(JSON.stringify({
46
40
  statusCode,
47
41
  error: errorLabel(statusCode),
48
42
  message,
49
- }),
50
- );
43
+ }));
51
44
  }
@@ -2,7 +2,9 @@
2
2
  export const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
3
3
  /** Format a byte count into a human-readable size string (e.g. "1.2 KB") */
4
4
  export function formatFileSize(bytes) {
5
- if (bytes < 1024) return `${bytes} B`;
6
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
7
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
5
+ if (bytes < 1024)
6
+ return `${bytes} B`;
7
+ if (bytes < 1024 * 1024)
8
+ return `${(bytes / 1024).toFixed(1)} KB`;
9
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
8
10
  }