@nanogpt/private-mode 0.2.11 → 0.2.13

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,12 @@ 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.
81
83
 
82
84
  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
85
 
@@ -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,
@@ -419,6 +428,10 @@ function isPrivateModeKimiK3Model(model) {
419
428
  });
420
429
  }
421
430
 
431
+ function isPrivateModeGlm53Model(model) {
432
+ return model.thinkingMode === 'glm-5.3';
433
+ }
434
+
422
435
  function getPrivateModeFunctionToolName(tool) {
423
436
  if (!isPlainObject(tool) || tool.type !== 'function' || !isPlainObject(tool.function)) {
424
437
  return undefined;
@@ -513,7 +526,7 @@ export function normalizePrivateModelReasoningControls(body, model) {
513
526
 
514
527
  export function shouldSuppressPrivateModelReasoning(body, model) {
515
528
  const isKimiK3 = isPrivateModeKimiK3Model(model);
516
- if (!isKimiK3 && model.thinkingMode !== 'deepseek-v4') return false;
529
+ if (!isKimiK3 && model.thinkingMode !== 'deepseek-v4' && model.thinkingMode !== 'glm-5.3') return false;
517
530
  const reasoningVisibilityOptOut = body.reasoningOptOut === true ||
518
531
  body.exposeReasoning === false ||
519
532
  body.materializeReasoning === false ||
@@ -528,9 +541,6 @@ export function shouldSuppressPrivateModelReasoning(body, model) {
528
541
 
529
542
  function resolvePrivateModeKimiK3ReasoningEffort(body) {
530
543
  const candidates = [
531
- body.reasoningOptOut === true ? false : undefined,
532
- body.exposeReasoning === false ? false : undefined,
533
- body.materializeReasoning === false ? false : undefined,
534
544
  body.enable_thinking,
535
545
  body.thinking,
536
546
  body.reasoning_effort,
@@ -544,8 +554,7 @@ function resolvePrivateModeKimiK3ReasoningEffort(body) {
544
554
  const normalizedEffort = typeof explicitEffort === 'string'
545
555
  ? explicitEffort.trim().toLowerCase()
546
556
  : undefined;
547
- const excludesReasoning = candidates.some((value) => isPlainObject(value) && value.exclude === true);
548
- return excludesReasoning ? normalizedEffort ?? 'low' : normalizedEffort ?? 'max';
557
+ return normalizedEffort ?? 'max';
549
558
  }
550
559
 
551
560
  function applyPrivateModeKimiK3RequestParams(body, model) {
@@ -618,6 +627,20 @@ function clampPrivateModeDeepSeekV4Output(body) {
618
627
  );
619
628
  }
620
629
 
630
+ function clampPrivateModeGlm53Output(body) {
631
+ const requested = typeof body.max_tokens === 'number' && Number.isFinite(body.max_tokens)
632
+ ? body.max_tokens
633
+ : undefined;
634
+ if (requested === undefined) return;
635
+ const promptTokens = estimatePrivateModeKimiK3PromptTokens(body);
636
+ const remainingContext = Math.max(1, DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS - promptTokens - DEEPSEEK_V4_CONTEXT_SAFETY_MARGIN_TOKENS);
637
+ body.max_tokens = Math.min(
638
+ requested < 0 ? GLM_53_MAX_COMPLETION_TOKENS : Math.max(1, Math.floor(requested)),
639
+ GLM_53_MAX_COMPLETION_TOKENS,
640
+ remainingContext,
641
+ );
642
+ }
643
+
621
644
  export function createPrivateModeReasoningContentSuppressor() {
622
645
  const openTags = ['<think>', '<thinking>', '<previous_reasoning>', '◁think▷'];
623
646
  const closeTags = ['</think>', '</thinking>', '</previous_reasoning>', '◁/think▷'];
@@ -1011,10 +1034,12 @@ function normalizeStreamOptions(body) {
1011
1034
  }
1012
1035
 
1013
1036
  export function applyPrivateModelRequestMutations(body, model) {
1014
- if (isPrivateModeKimiK3Model(model)) {
1037
+ if (isPrivateModeKimiK3Model(model) || isPrivateModeGlm53Model(model)) {
1015
1038
  normalizePrivateModelReasoningControls(body, model);
1016
1039
  normalizePrivateModeKimiK3ReasoningFromMessages(body);
1017
- normalizePrivateModeKimiK3DynamicToolMessages(body);
1040
+ if (isPrivateModeKimiK3Model(model)) {
1041
+ normalizePrivateModeKimiK3DynamicToolMessages(body);
1042
+ }
1018
1043
  } else {
1019
1044
  stripPrivateModeReasoningFromMessages(body);
1020
1045
  }
@@ -1026,6 +1051,8 @@ export function applyPrivateModelRequestMutations(body, model) {
1026
1051
  applyPrivateModeKimiK3RequestParams(body, model);
1027
1052
  } else if (model.thinkingMode === 'deepseek-v4') {
1028
1053
  clampPrivateModeDeepSeekV4Output(body);
1054
+ } else if (model.thinkingMode === 'glm-5.3') {
1055
+ clampPrivateModeGlm53Output(body);
1029
1056
  }
1030
1057
 
1031
1058
  if (model.thinkingMode === 'gemma') {
@@ -1037,9 +1064,12 @@ export function applyPrivateModelRequestMutations(body, model) {
1037
1064
  delete body.reasoning_effort;
1038
1065
  } else if (
1039
1066
  model.thinkingMode === 'glm-5.2' ||
1067
+ model.thinkingMode === 'glm-5.3' ||
1040
1068
  model.thinkingMode === 'deepseek-v4'
1041
1069
  ) {
1042
- const thinkingEnabled = shouldEnableThinking(body, model);
1070
+ const thinkingEnabled = model.thinkingMode === 'glm-5.3'
1071
+ ? true
1072
+ : shouldEnableThinking(body, model);
1043
1073
  const requestedReasoningEffort = body.reasoning_effort
1044
1074
  ?? (isPlainObject(body.reasoning) ? body.reasoning.effort : undefined);
1045
1075
  body.chat_template_kwargs = {
@@ -1048,8 +1078,9 @@ export function applyPrivateModelRequestMutations(body, model) {
1048
1078
  };
1049
1079
 
1050
1080
  if (thinkingEnabled) {
1051
- body.chat_template_kwargs.reasoning_effort =
1052
- normalizeDeepSeekV4ReasoningEffort(requestedReasoningEffort);
1081
+ body.chat_template_kwargs.reasoning_effort = model.thinkingMode === 'glm-5.3'
1082
+ ? normalizeGlm53ReasoningEffort(requestedReasoningEffort)
1083
+ : normalizeDeepSeekV4ReasoningEffort(requestedReasoningEffort);
1053
1084
  } else {
1054
1085
  delete body.chat_template_kwargs.reasoning_effort;
1055
1086
  }
@@ -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',
@@ -76,6 +76,34 @@
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
+ "maxInputTokens": 1048576,
101
+ "maxOutputTokens": 131072,
102
+ "thinkingMode": "glm-5.3",
103
+ "created": 1788307200,
104
+ "ownedBy": "nanogpt-private-mode",
105
+ "aliases": ["private/glm-5.3-flash", "TEE/glm-5-3-flash", "TEE/glm-5.3-flash"]
106
+ },
79
107
  {
80
108
  "id": "private/gemma4-31b",
81
109
  "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.13",
4
4
  "description": "OpenAI-compatible localhost proxy for NanoGPT Private Mode.",
5
5
  "type": "module",
6
6
  "publishConfig": {