@nanogpt/private-mode 0.2.11 → 0.2.14

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 CHANGED
@@ -46,7 +46,7 @@ NanoGPT can see account identity, selected private model, selected TEE target me
46
46
 
47
47
  NanoGPT's web app can also use these models without running this local proxy. Select an eligible Private Mode model and use the Private Mode control in the model picker. This package is for API clients, CLIs, agents, and other OpenAI-compatible tools.
48
48
 
49
- The web-app toggle is narrower than the proxy path in v1. It supports text chat and model settings, and disables attachments, web search, URL-scraped content, project tools, multi-model chat, Context Memory injection, quick replies, and automatic title generation for private turns.
49
+ The web app supports local image attachments for Private Mode vision models (Kimi K3 and Gemma 4 31B). Image bytes are converted to base64 in the browser and included inside the EHBP-encrypted request; they are not uploaded to NanoGPT object storage. Other attachment types, web search, URL-scraped content, project tools, multi-model chat, Context Memory injection, quick replies, and automatic title generation remain disabled for private turns.
50
50
 
51
51
  In the hosted web app, decrypted Private Mode turns remain in local browser history. Cloud conversation sync is blocked for Private Mode chats unless password-based end-to-end sync is enabled; the recoverable default sync mode is not used for those chats.
52
52
 
@@ -74,10 +74,29 @@ Supported private model IDs include:
74
74
  - `private/llama3-3-70b`
75
75
  - `private/glm-5-2`
76
76
  - `private/glm-5-2:thinking`
77
+ - `private/glm-5-3`
78
+ - `private/glm-5-3-flash`
77
79
  - `private/gemma4-31b`
78
80
  - `private/gemma4-31b:thinking`
79
81
 
80
- Both GLM 5.2 variants are deployed with a 393,216-token total context limit, with prompt and output sharing that window. Kimi K3 Private has a 256,000-token context limit. DeepSeek V4 Flash 0731 Private has a 1,048,576-token total context limit. The model-list extension `context_length` is the combined prompt-and-output window, while `max_output_tokens` is the output ceiling within that same window. These values are not additive.
82
+ Both GLM 5.2 variants are deployed with a 393,216-token total context limit, with prompt and output sharing that window. GLM 5.3 Private and GLM 5.3 Flash Private each have a 1,048,576-token total context limit and a 131,072-token output ceiling. Kimi K3 Private has a 256,000-token context limit. DeepSeek V4 Flash 0731 Private has a 1,048,576-token total context limit. The model-list extension `context_length` is the combined prompt-and-output window, while `max_output_tokens` is the output ceiling within that same window. These values are not additive.
83
+
84
+ ### Thinking without leaving Private Mode
85
+
86
+ Keep the same private model ID when enabling thinking. For example:
87
+
88
+ ```json
89
+ {
90
+ "model": "private/deepseek-v4-flash",
91
+ "messages": [{ "role": "user", "content": "What is 17 times 19?" }],
92
+ "reasoning_effort": "high",
93
+ "stream": true
94
+ }
95
+ ```
96
+
97
+ DeepSeek V4 Flash uses the 0731 release and supports optional thinking. GLM 5.2 and Gemma 4 also support optional thinking. Use `thinking: true` or `enable_thinking: true` to enable it; `thinking: false` disables it for these optional-thinking models. Kimi K3, GLM 5.3, and GLM 5.3 Flash always generate reasoning. GLM 5.3 models accept `low`, `high`, or `max` effort. `reasoning: { "exclude": true }` controls response visibility and does not disable thinking.
98
+
99
+ Non-streaming reasoning requests may take more than five minutes. The CLI allows the encrypted request to wait for the hosted route's response, with a 30-minute transport limit. The hosted route stops non-streaming generation after at most 29 minutes to leave time before its platform deadline. Use `stream: true` for earlier output, and configure your API client's timeout to allow long generations. Upgrade the local proxy as well as the hosted service to receive both timeout fixes.
81
100
 
82
101
  Browser requests are locked down by default. The proxy only accepts same-machine clients and rejects browser `Origin` headers that are not explicitly allowed, so a random website or LAN client cannot spend the local `NANOGPT_API_KEY` while the proxy is running. If a local browser app needs to call the proxy directly, allow that exact origin:
83
102
 
@@ -4,6 +4,7 @@ import { parseArgs } from 'node:util';
4
4
  import { PRIVATE_MODE_PROXY_VERSION } from '../lib/packageInfo.js';
5
5
  import { startPrivateModeProxy } from '../lib/server.js';
6
6
  import { verifyPrivateModeReceiptCli } from '../lib/verifyReceipt.js';
7
+ import { installPrivateModeTransportTimeouts } from '../lib/transportTimeouts.js';
7
8
 
8
9
  const { values, positionals } = parseArgs({
9
10
  options: {
@@ -69,6 +70,7 @@ if (!apiKey) {
69
70
  process.exit(1);
70
71
  }
71
72
 
73
+ installPrivateModeTransportTimeouts(String(values['api-base']));
72
74
  startPrivateModeProxy({
73
75
  apiBase: String(values['api-base']),
74
76
  apiKey,
@@ -29,6 +29,7 @@ const KIMI_K3_TEXT_ENCODER = new TextEncoder();
29
29
  const DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS = 1_048_576;
30
30
  const DEEPSEEK_V4_MAX_COMPLETION_TOKENS = 1_048_576;
31
31
  const DEEPSEEK_V4_CONTEXT_SAFETY_MARGIN_TOKENS = 10;
32
+ const GLM_53_MAX_COMPLETION_TOKENS = 131_072;
32
33
  const PRIVATE_TINFOIL_CHAT_COMPLETION_BODY_FIELDS = new Set([
33
34
  'chat_template_kwargs',
34
35
  'frequency_penalty',
@@ -243,6 +244,14 @@ function normalizeDeepSeekV4ReasoningEffort(value) {
243
244
  return 'high';
244
245
  }
245
246
 
247
+ function normalizeGlm53ReasoningEffort(value) {
248
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
249
+ if (normalized === 'max' || normalized === 'xhigh') return 'max';
250
+ if (normalized === 'low' || normalized === 'minimal') return 'low';
251
+ if (normalized === 'high' || normalized === 'medium') return 'high';
252
+ return 'max';
253
+ }
254
+
246
255
  function getPrivateModeModelSignals(body, model) {
247
256
  return [
248
257
  body.model,
@@ -262,8 +271,11 @@ function shouldEnableThinking(body, model) {
262
271
  ));
263
272
 
264
273
  return explicitThinking
274
+ ?? isThinkingEnabled(body.enable_thinking)
265
275
  ?? explicitReasoning
266
276
  ?? (body.reasoning_effort !== undefined ? reasoningEffort !== 'none' : undefined)
277
+ ?? isThinkingEnabled(body.chat_template_kwargs?.thinking)
278
+ ?? isThinkingEnabled(body.chat_template_kwargs?.enable_thinking)
267
279
  ?? hasThinkingSuffix;
268
280
  }
269
281
 
@@ -419,6 +431,10 @@ function isPrivateModeKimiK3Model(model) {
419
431
  });
420
432
  }
421
433
 
434
+ function isPrivateModeGlm53Model(model) {
435
+ return model.thinkingMode === 'glm-5.3';
436
+ }
437
+
422
438
  function getPrivateModeFunctionToolName(tool) {
423
439
  if (!isPlainObject(tool) || tool.type !== 'function' || !isPlainObject(tool.function)) {
424
440
  return undefined;
@@ -513,7 +529,6 @@ export function normalizePrivateModelReasoningControls(body, model) {
513
529
 
514
530
  export function shouldSuppressPrivateModelReasoning(body, model) {
515
531
  const isKimiK3 = isPrivateModeKimiK3Model(model);
516
- if (!isKimiK3 && model.thinkingMode !== 'deepseek-v4') return false;
517
532
  const reasoningVisibilityOptOut = body.reasoningOptOut === true ||
518
533
  body.exposeReasoning === false ||
519
534
  body.materializeReasoning === false ||
@@ -528,9 +543,6 @@ export function shouldSuppressPrivateModelReasoning(body, model) {
528
543
 
529
544
  function resolvePrivateModeKimiK3ReasoningEffort(body) {
530
545
  const candidates = [
531
- body.reasoningOptOut === true ? false : undefined,
532
- body.exposeReasoning === false ? false : undefined,
533
- body.materializeReasoning === false ? false : undefined,
534
546
  body.enable_thinking,
535
547
  body.thinking,
536
548
  body.reasoning_effort,
@@ -544,8 +556,7 @@ function resolvePrivateModeKimiK3ReasoningEffort(body) {
544
556
  const normalizedEffort = typeof explicitEffort === 'string'
545
557
  ? explicitEffort.trim().toLowerCase()
546
558
  : undefined;
547
- const excludesReasoning = candidates.some((value) => isPlainObject(value) && value.exclude === true);
548
- return excludesReasoning ? normalizedEffort ?? 'low' : normalizedEffort ?? 'max';
559
+ return normalizedEffort ?? 'max';
549
560
  }
550
561
 
551
562
  function applyPrivateModeKimiK3RequestParams(body, model) {
@@ -618,6 +629,20 @@ function clampPrivateModeDeepSeekV4Output(body) {
618
629
  );
619
630
  }
620
631
 
632
+ function clampPrivateModeGlm53Output(body) {
633
+ const requested = typeof body.max_tokens === 'number' && Number.isFinite(body.max_tokens)
634
+ ? body.max_tokens
635
+ : undefined;
636
+ if (requested === undefined) return;
637
+ const promptTokens = estimatePrivateModeKimiK3PromptTokens(body);
638
+ const remainingContext = Math.max(1, DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS - promptTokens - DEEPSEEK_V4_CONTEXT_SAFETY_MARGIN_TOKENS);
639
+ body.max_tokens = Math.min(
640
+ requested < 0 ? GLM_53_MAX_COMPLETION_TOKENS : Math.max(1, Math.floor(requested)),
641
+ GLM_53_MAX_COMPLETION_TOKENS,
642
+ remainingContext,
643
+ );
644
+ }
645
+
621
646
  export function createPrivateModeReasoningContentSuppressor() {
622
647
  const openTags = ['<think>', '<thinking>', '<previous_reasoning>', '◁think▷'];
623
648
  const closeTags = ['</think>', '</thinking>', '</previous_reasoning>', '◁/think▷'];
@@ -1011,10 +1036,12 @@ function normalizeStreamOptions(body) {
1011
1036
  }
1012
1037
 
1013
1038
  export function applyPrivateModelRequestMutations(body, model) {
1014
- if (isPrivateModeKimiK3Model(model)) {
1039
+ if (isPrivateModeKimiK3Model(model) || isPrivateModeGlm53Model(model)) {
1015
1040
  normalizePrivateModelReasoningControls(body, model);
1016
1041
  normalizePrivateModeKimiK3ReasoningFromMessages(body);
1017
- normalizePrivateModeKimiK3DynamicToolMessages(body);
1042
+ if (isPrivateModeKimiK3Model(model)) {
1043
+ normalizePrivateModeKimiK3DynamicToolMessages(body);
1044
+ }
1018
1045
  } else {
1019
1046
  stripPrivateModeReasoningFromMessages(body);
1020
1047
  }
@@ -1026,6 +1053,8 @@ export function applyPrivateModelRequestMutations(body, model) {
1026
1053
  applyPrivateModeKimiK3RequestParams(body, model);
1027
1054
  } else if (model.thinkingMode === 'deepseek-v4') {
1028
1055
  clampPrivateModeDeepSeekV4Output(body);
1056
+ } else if (model.thinkingMode === 'glm-5.3') {
1057
+ clampPrivateModeGlm53Output(body);
1029
1058
  }
1030
1059
 
1031
1060
  if (model.thinkingMode === 'gemma') {
@@ -1037,25 +1066,33 @@ export function applyPrivateModelRequestMutations(body, model) {
1037
1066
  delete body.reasoning_effort;
1038
1067
  } else if (
1039
1068
  model.thinkingMode === 'glm-5.2' ||
1069
+ model.thinkingMode === 'glm-5.3' ||
1040
1070
  model.thinkingMode === 'deepseek-v4'
1041
1071
  ) {
1042
- const thinkingEnabled = shouldEnableThinking(body, model);
1072
+ const thinkingEnabled = model.thinkingMode === 'glm-5.3'
1073
+ ? true
1074
+ : shouldEnableThinking(body, model);
1043
1075
  const requestedReasoningEffort = body.reasoning_effort
1044
- ?? (isPlainObject(body.reasoning) ? body.reasoning.effort : undefined);
1076
+ ?? (isPlainObject(body.reasoning) ? body.reasoning.effort : undefined)
1077
+ ?? body.chat_template_kwargs?.reasoning_effort;
1045
1078
  body.chat_template_kwargs = {
1046
1079
  ...mergeChatTemplateKwargs(body),
1047
1080
  thinking: thinkingEnabled,
1048
1081
  };
1049
1082
 
1050
- if (thinkingEnabled) {
1051
- body.chat_template_kwargs.reasoning_effort =
1052
- normalizeDeepSeekV4ReasoningEffort(requestedReasoningEffort);
1083
+ if (model.thinkingMode === 'glm-5.3') {
1084
+ // Match the native GLM API and the browser client: low/high/max belongs
1085
+ // at the top level, including on tool turns.
1086
+ body.reasoning_effort = normalizeGlm53ReasoningEffort(requestedReasoningEffort);
1087
+ delete body.chat_template_kwargs.reasoning_effort;
1088
+ } else if (thinkingEnabled) {
1089
+ body.chat_template_kwargs.reasoning_effort = normalizeDeepSeekV4ReasoningEffort(requestedReasoningEffort);
1053
1090
  } else {
1054
1091
  delete body.chat_template_kwargs.reasoning_effort;
1055
1092
  }
1056
1093
 
1057
1094
  delete body.thinking;
1058
- delete body.reasoning_effort;
1095
+ if (model.thinkingMode !== 'glm-5.3') delete body.reasoning_effort;
1059
1096
  }
1060
1097
 
1061
1098
  stripUnsupportedPrivateTinfoilFields(body);
@@ -6,7 +6,7 @@ const EHBP_RESPONSE_NONCE_HEADER = 'ehbp-response-nonce';
6
6
  const PRIVATE_MODE_REFUND_NOTICE = 'Any reserved balance will be released or refunded.';
7
7
  const MAX_UPSTREAM_ERROR_BODY_BYTES = 16 * 1024;
8
8
  const MAX_UPSTREAM_ERROR_MESSAGE_CHARS = 1_000;
9
- const HIDDEN_PROVIDER_NAME_PATTERN = /\b(?:openrouter|spoke\s*ai|aihubmix|ollama|mimas|comet|azure|digital\s*ocean|axionic|whale\s*ai|langfork)\b/gi;
9
+ const HIDDEN_PROVIDER_NAME_PATTERN = /\b(?:openrouter|spoke\s*ai|aihubmix|mimas|comet|azure|digital\s*ocean|axionic|whale\s*ai|langfork)\b/gi;
10
10
 
11
11
  export function privateModeProviderFailureMessage(status) {
12
12
  if (status === 429) {
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
 
3
3
  export const PRIVATE_MODE_FRONTEND_SUPPORTED_FEATURES = Object.freeze([
4
4
  'text_chat',
5
+ 'vision_image_inputs',
5
6
  'streaming',
6
7
  'conversation_history',
7
8
  'model_settings',
@@ -9,7 +10,7 @@ export const PRIVATE_MODE_FRONTEND_SUPPORTED_FEATURES = Object.freeze([
9
10
  ]);
10
11
 
11
12
  export const PRIVATE_MODE_FRONTEND_DISABLED_FEATURES = Object.freeze([
12
- 'attachments',
13
+ 'non_image_attachments',
13
14
  'web_search',
14
15
  'url_scraped_content',
15
16
  'project_chats',
@@ -0,0 +1,28 @@
1
+ import { Agent, setGlobalDispatcher } from 'undici';
2
+ import { PRIVATE_MODE_STREAM_ROUTE_MAX_DURATION_MS } from './constants.js';
3
+
4
+ export class PrivateModeDispatcher extends Agent {
5
+ constructor(apiBase) {
6
+ super();
7
+ this.chatUrl = new URL(`${apiBase.replace(/\/+$/, '')}/api/v1/private/tinfoil/v1/chat/completions`);
8
+ }
9
+
10
+ dispatch(options, handler) {
11
+ // EHBP calls native fetch after encryption, without retaining a custom
12
+ // dispatcher. Scope the CLI's override to its configured encrypted endpoint.
13
+ // Override dispatch options, not Agent defaults: fetch supplies its own
14
+ // five-minute timeout values on every dispatch.
15
+ if (String(options.origin) === this.chatUrl.origin && options.path === this.chatUrl.pathname) {
16
+ options = {
17
+ ...options,
18
+ headersTimeout: PRIVATE_MODE_STREAM_ROUTE_MAX_DURATION_MS,
19
+ bodyTimeout: PRIVATE_MODE_STREAM_ROUTE_MAX_DURATION_MS,
20
+ };
21
+ }
22
+ return super.dispatch(options, handler);
23
+ }
24
+ }
25
+
26
+ export function installPrivateModeTransportTimeouts(apiBase) {
27
+ setGlobalDispatcher(new PrivateModeDispatcher(apiBase));
28
+ }
@@ -76,6 +76,37 @@
76
76
  "ownedBy": "nanogpt-private-mode",
77
77
  "aliases": ["private/glm-5.2:thinking", "TEE/glm-5-2:thinking", "TEE/glm-5.2:thinking"]
78
78
  },
79
+ {
80
+ "id": "private/glm-5-3",
81
+ "name": "GLM 5.3 Private",
82
+ "upstreamModel": "glm-5-3",
83
+ "billingModel": "TEE/glm-5.3",
84
+ "providerPricingModel": "TEE/glm-5.3",
85
+ "teeTargetModel": "glm-5-3",
86
+ "maxInputTokens": 1048576,
87
+ "maxOutputTokens": 131072,
88
+ "thinkingMode": "glm-5.3",
89
+ "created": 1788391929,
90
+ "ownedBy": "nanogpt-private-mode",
91
+ "aliases": ["private/glm-5.3", "TEE/glm-5-3", "TEE/glm-5.3"]
92
+ },
93
+ {
94
+ "id": "private/glm-5-3-flash",
95
+ "name": "GLM 5.3 Flash Private",
96
+ "upstreamModel": "glm-5-3-flash",
97
+ "billingModel": "TEE/glm-5.3-flash",
98
+ "providerPricingModel": "TEE/glm-5.3-flash",
99
+ "teeTargetModel": "glm-5-3-flash",
100
+ "teeTargetAliases": [
101
+ "glm5-3-flash"
102
+ ],
103
+ "maxInputTokens": 1048576,
104
+ "maxOutputTokens": 131072,
105
+ "thinkingMode": "glm-5.3",
106
+ "created": 1788307200,
107
+ "ownedBy": "nanogpt-private-mode",
108
+ "aliases": ["private/glm-5.3-flash", "TEE/glm-5-3-flash", "TEE/glm-5.3-flash"]
109
+ },
79
110
  {
80
111
  "id": "private/gemma4-31b",
81
112
  "name": "Gemma 4 31B Private",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanogpt/private-mode",
3
- "version": "0.2.11",
3
+ "version": "0.2.14",
4
4
  "description": "OpenAI-compatible localhost proxy for NanoGPT Private Mode.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -23,6 +23,7 @@
23
23
  "lib/serverErrorNormalization.js",
24
24
  "lib/sseResponsePump.js",
25
25
  "lib/statusContract.js",
26
+ "lib/transportTimeouts.js",
26
27
  "lib/verifyReceipt.js",
27
28
  "models",
28
29
  "README.md"
@@ -32,7 +33,8 @@
32
33
  },
33
34
  "dependencies": {
34
35
  "ai": "6.0.220",
35
- "tinfoil": "1.1.12"
36
+ "tinfoil": "1.1.12",
37
+ "undici": "6.28.0"
36
38
  },
37
39
  "engines": {
38
40
  "node": ">=22"