@nanogpt/private-mode 0.1.2 → 0.1.3

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.
@@ -6,6 +6,32 @@ const ASSISTANT_REASONING_MESSAGE_FIELDS = [
6
6
  'reasoning_content',
7
7
  'reasoning_details',
8
8
  ];
9
+ const PRIVATE_TINFOIL_CHAT_COMPLETION_BODY_FIELDS = new Set([
10
+ 'chat_template_kwargs',
11
+ 'frequency_penalty',
12
+ 'function_call',
13
+ 'functions',
14
+ 'logit_bias',
15
+ 'logprobs',
16
+ 'max_tokens',
17
+ 'messages',
18
+ 'model',
19
+ 'n',
20
+ 'parallel_tool_calls',
21
+ 'presence_penalty',
22
+ 'reasoning_effort',
23
+ 'response_format',
24
+ 'seed',
25
+ 'stop',
26
+ 'stream',
27
+ 'stream_options',
28
+ 'temperature',
29
+ 'tool_choice',
30
+ 'tools',
31
+ 'top_logprobs',
32
+ 'top_p',
33
+ 'user',
34
+ ]);
9
35
 
10
36
  // Keep request mutation behavior in sync with lib/privateMode/tinfoilBrowserClient.ts
11
37
  // for explicit fields. The local proxy preserves OpenAI's non-streaming default.
@@ -79,6 +105,14 @@ function mergeChatTemplateKwargs(body) {
79
105
  return isPlainObject(body.chat_template_kwargs) ? body.chat_template_kwargs : {};
80
106
  }
81
107
 
108
+ function stripUnsupportedPrivateTinfoilFields(body) {
109
+ for (const key of Object.keys(body)) {
110
+ if (!PRIVATE_TINFOIL_CHAT_COMPLETION_BODY_FIELDS.has(key)) {
111
+ delete body[key];
112
+ }
113
+ }
114
+ }
115
+
82
116
  function stripPrivateModeReasoningBlocks(value) {
83
117
  return value
84
118
  .replace(/◁think▷/g, '<think>')
@@ -178,9 +212,17 @@ function applyTinfoilCompatibilityMutations(body, model) {
178
212
  export function applyPrivateModelRequestMutations(body, model) {
179
213
  stripPrivateModeReasoningFromMessages(body);
180
214
 
215
+ if (body.max_tokens === undefined && body.max_completion_tokens !== undefined) {
216
+ body.max_tokens = body.max_completion_tokens;
217
+ }
218
+ if (body.max_tokens === undefined && body.maxCompletionTokens !== undefined) {
219
+ body.max_tokens = body.maxCompletionTokens;
220
+ }
181
221
  if (body.max_tokens === undefined && body.maxTokens !== undefined) {
182
222
  body.max_tokens = body.maxTokens;
183
223
  }
224
+ delete body.max_completion_tokens;
225
+ delete body.maxCompletionTokens;
184
226
  delete body.maxTokens;
185
227
 
186
228
  body.model = model.upstreamModel;
@@ -192,6 +234,8 @@ export function applyPrivateModelRequestMutations(body, model) {
192
234
  ...(body.stream_options && typeof body.stream_options === 'object' ? body.stream_options : {}),
193
235
  include_usage: true,
194
236
  };
237
+ } else {
238
+ delete body.stream_options;
195
239
  }
196
240
  applyTinfoilCompatibilityMutations(body, model);
197
241
 
@@ -202,10 +246,7 @@ export function applyPrivateModelRequestMutations(body, model) {
202
246
  };
203
247
  delete body.thinking;
204
248
  delete body.reasoning_effort;
205
- return body;
206
- }
207
-
208
- if (model.thinkingMode === 'deepseek-v4-pro') {
249
+ } else if (model.thinkingMode === 'deepseek-v4-pro') {
209
250
  const thinkingEnabled = shouldEnableThinking(body, model);
210
251
  body.chat_template_kwargs = {
211
252
  ...mergeChatTemplateKwargs(body),
@@ -223,5 +264,6 @@ export function applyPrivateModelRequestMutations(body, model) {
223
264
  delete body.reasoning_effort;
224
265
  }
225
266
 
267
+ stripUnsupportedPrivateTinfoilFields(body);
226
268
  return body;
227
269
  }
package/lib/server.js CHANGED
@@ -6,6 +6,10 @@ import {
6
6
  getCorsHeadersForRequest,
7
7
  } from './originPolicy.js';
8
8
  import { applyPrivateModelRequestMutations } from './requestTransforms.js';
9
+ import {
10
+ normalizePrivateModeUpstreamErrorMessage,
11
+ readErrorMessage,
12
+ } from './serverErrorNormalization.js';
9
13
  import { buildPrivateModeStatusContract } from './statusContract.js';
10
14
 
11
15
  const MODELS = JSON.parse(
@@ -302,9 +306,10 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
302
306
  } catch (error) {
303
307
  responseComplete = true;
304
308
  if (res.destroyed) return;
309
+ const message = error instanceof Error ? error.message : String(error);
305
310
  jsonResponse(res, 502, {
306
311
  error: {
307
- message: `Private Mode request failed: ${error instanceof Error ? error.message : String(error)}`,
312
+ message: `Private Mode request failed: ${normalizePrivateModeUpstreamErrorMessage(message, message)}`,
308
313
  type: 'api_error',
309
314
  code: 'private_mode_request_failed',
310
315
  },
@@ -321,6 +326,20 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
321
326
  const privateMode = response.headers.get('x-nanogpt-private-mode');
322
327
  if (privateMode) headers['x-nanogpt-private-mode'] = privateMode;
323
328
 
329
+ if (!response.ok) {
330
+ responseComplete = true;
331
+ const errorHeaders = { ...headers };
332
+ delete errorHeaders['content-type'];
333
+ jsonResponse(res, response.status, {
334
+ error: {
335
+ message: await readErrorMessage(response, `Private Mode request failed with HTTP ${response.status}`),
336
+ type: 'api_error',
337
+ code: 'private_mode_upstream_failed',
338
+ },
339
+ }, errorHeaders);
340
+ return;
341
+ }
342
+
324
343
  if (privateStreamRequested && response.body) {
325
344
  res.writeHead(response.status, headers);
326
345
  const reader = response.body.getReader();
@@ -0,0 +1,45 @@
1
+ // Keep upstream error normalization in sync with lib/privateMode/tinfoilBrowserClient.ts.
2
+ const UPSTREAM_OBJECT_SHAPE_ERROR_PATTERN = /object has no attribute ['"]get['"]/i;
3
+ const MISSING_EHBP_RESPONSE_NONCE_PATTERN = /missing\s+ehbp-response-nonce\s+header/i;
4
+
5
+ export function normalizePrivateModeUpstreamErrorMessage(message, fallback) {
6
+ if (UPSTREAM_OBJECT_SHAPE_ERROR_PATTERN.test(message)) {
7
+ return 'Private Mode upstream returned an internal error while processing request metadata. Retry or choose another Private Mode model.';
8
+ }
9
+ if (MISSING_EHBP_RESPONSE_NONCE_PATTERN.test(message)) {
10
+ return 'Private Mode did not receive an encrypted response from Tinfoil. The private request may have been rejected before completion.';
11
+ }
12
+ return message || fallback;
13
+ }
14
+
15
+ export async function readErrorMessage(response, fallback) {
16
+ let message = fallback;
17
+ try {
18
+ const data = await response.clone().json();
19
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
20
+ const error = data.error && typeof data.error === 'object' && !Array.isArray(data.error)
21
+ ? data.error
22
+ : {};
23
+ message = (
24
+ typeof error.message === 'string' && error.message.trim()
25
+ ? error.message
26
+ : typeof data.message === 'string' && data.message.trim()
27
+ ? data.message
28
+ : fallback
29
+ );
30
+ } else if (typeof data === 'string' && data.trim()) {
31
+ message = data;
32
+ } else if (typeof data === 'number' || typeof data === 'boolean') {
33
+ message = String(data);
34
+ }
35
+ } catch {
36
+ try {
37
+ const text = await response.text();
38
+ message = text || fallback;
39
+ } catch {
40
+ message = fallback;
41
+ }
42
+ }
43
+
44
+ return normalizePrivateModeUpstreamErrorMessage(message, fallback);
45
+ }
@@ -30,24 +30,14 @@
30
30
  "aliases": ["private/llama-3.3-70b", "TEE/llama3-3-70b"]
31
31
  },
32
32
  {
33
- "id": "private/glm-5-1",
34
- "name": "GLM 5.1 Private",
35
- "upstreamModel": "glm-5-1",
36
- "billingModel": "TEE/glm-5-1",
37
- "teeTargetModel": "glm-5-1",
38
- "created": 1764547200,
39
- "ownedBy": "nanogpt-private-mode",
40
- "aliases": ["private/glm-5.1", "TEE/glm-5-1", "TEE/glm-5.1"]
41
- },
42
- {
43
- "id": "private/glm-5-1-thinking",
44
- "name": "GLM 5.1 Thinking Private",
45
- "upstreamModel": "glm-5-1",
46
- "billingModel": "TEE/glm-5-1-thinking",
47
- "teeTargetModel": "glm-5-1",
48
- "created": 1764547200,
33
+ "id": "private/glm-5-2",
34
+ "name": "GLM 5.2 Private",
35
+ "upstreamModel": "glm-5-2",
36
+ "billingModel": "TEE/glm-5-2",
37
+ "teeTargetModel": "glm-5-2",
38
+ "created": 1781827200,
49
39
  "ownedBy": "nanogpt-private-mode",
50
- "aliases": ["private/glm-5.1-thinking", "TEE/glm-5-1-thinking", "TEE/glm-5.1-thinking"]
40
+ "aliases": ["private/glm-5.2", "TEE/glm-5-2", "TEE/glm-5.2"]
51
41
  },
52
42
  {
53
43
  "id": "private/gemma4-31b",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanogpt/private-mode",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "OpenAI-compatible localhost proxy for NanoGPT Private Mode.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -15,6 +15,7 @@
15
15
  "lib/originPolicy.js",
16
16
  "lib/requestTransforms.js",
17
17
  "lib/server.js",
18
+ "lib/serverErrorNormalization.js",
18
19
  "lib/statusContract.js",
19
20
  "lib/verifyReceipt.js",
20
21
  "models",