@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 +21 -2
- package/bin/nanogpt-private-mode.js +2 -0
- package/lib/requestTransforms.js +51 -14
- package/lib/serverErrorNormalization.js +1 -1
- package/lib/statusContract.js +2 -1
- package/lib/transportTimeouts.js +28 -0
- package/models/private-tee.json +31 -0
- package/package.json +4 -2
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
|
|
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,
|
package/lib/requestTransforms.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
1051
|
-
|
|
1052
|
-
|
|
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|
|
|
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) {
|
package/lib/statusContract.js
CHANGED
|
@@ -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
|
-
'
|
|
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
|
+
}
|
package/models/private-tee.json
CHANGED
|
@@ -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.
|
|
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"
|