@nanogpt/private-mode 0.1.4 → 0.2.1

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
@@ -2,6 +2,8 @@
2
2
 
3
3
  OpenAI-compatible localhost proxy for NanoGPT Private Mode with supported TEE models.
4
4
 
5
+ Requires Node.js 22 or later.
6
+
5
7
  ```bash
6
8
  NANOGPT_API_KEY=sk-your-key npx @nanogpt/private-mode
7
9
  ```
@@ -30,6 +32,8 @@ const response = await client.chat.completions.create({
30
32
 
31
33
  The local proxy verifies TEE attestation, encrypts request bodies with EHBP, sends ciphertext through NanoGPT, decrypts encrypted responses locally, and returns normal OpenAI JSON to the calling app.
32
34
 
35
+ After a long idle period (five minutes by default), the proxy re-verifies attestation and creates a fresh encrypted transport before the next request. This makes laptop sleep/resume safe without requiring a proxy restart. Set `NANOGPT_PRIVATE_CLIENT_IDLE_RESET_MS` to a positive millisecond value to adjust the idle threshold.
36
+
33
37
  NanoGPT can see account identity, selected private model, selected TEE target metadata, timing, sizes, status, and usage metadata. NanoGPT cannot read the prompt or completion body for supported private models.
34
38
 
35
39
  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.
@@ -0,0 +1,9 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export function buildPrivateModeCacheScopeProof(cacheScope) {
4
+ const normalized = String(cacheScope || '').trim();
5
+ if (!/^[a-f0-9]{64}$/i.test(normalized)) {
6
+ throw new Error('Cannot prove an invalid Private Mode cache scope.');
7
+ }
8
+ return createHash('sha256').update(normalized, 'utf8').digest('hex');
9
+ }
@@ -28,20 +28,29 @@ function normalizeHostForOrigin(host) {
28
28
  return trimmed;
29
29
  }
30
30
 
31
+ function addAllowedOrigin(allowedOrigins, origin) {
32
+ const normalizedOrigin = normalizePrivateModeOrigin(origin);
33
+ if (normalizedOrigin) allowedOrigins.add(normalizedOrigin);
34
+ return normalizedOrigin;
35
+ }
36
+
37
+ function addDefaultLoopbackOrigins(allowedOrigins, host, port) {
38
+ for (const defaultHost of DEFAULT_LOCAL_HOSTS) {
39
+ allowedOrigins.add(`http://${defaultHost}:${port}`);
40
+ }
41
+
42
+ const normalizedHost = normalizeHostForOrigin(host);
43
+ if (normalizedHost) {
44
+ addAllowedOrigin(allowedOrigins, `http://${normalizedHost}:${port}`);
45
+ }
46
+ }
47
+
31
48
  export function buildPrivateModeOriginPolicy(options = {}) {
32
49
  const port = Number.parseInt(String(options.port || '8787'), 10);
33
50
  const allowedOrigins = new Set();
34
51
 
35
52
  if (Number.isFinite(port) && port > 0) {
36
- for (const host of DEFAULT_LOCAL_HOSTS) {
37
- allowedOrigins.add(`http://${host}:${port}`);
38
- }
39
-
40
- const normalizedHost = normalizeHostForOrigin(options.host);
41
- if (normalizedHost) {
42
- const origin = normalizePrivateModeOrigin(`http://${normalizedHost}:${port}`);
43
- if (origin) allowedOrigins.add(origin);
44
- }
53
+ addDefaultLoopbackOrigins(allowedOrigins, options.host, port);
45
54
  }
46
55
 
47
56
  const configuredOrigins = [
@@ -50,11 +59,9 @@ export function buildPrivateModeOriginPolicy(options = {}) {
50
59
  ];
51
60
 
52
61
  for (const configuredOrigin of configuredOrigins) {
53
- const origin = normalizePrivateModeOrigin(configuredOrigin);
54
- if (!origin) {
62
+ if (!addAllowedOrigin(allowedOrigins, configuredOrigin)) {
55
63
  throw new Error(`Invalid Private Mode browser origin "${configuredOrigin}". Use an http(s) origin, not a wildcard.`);
56
64
  }
57
- allowedOrigins.add(origin);
58
65
  }
59
66
 
60
67
  return {
@@ -6,6 +6,11 @@ const ASSISTANT_REASONING_MESSAGE_FIELDS = [
6
6
  'reasoning_content',
7
7
  'reasoning_details',
8
8
  ];
9
+ const MAX_TOKEN_ALIAS_FIELDS = [
10
+ 'max_completion_tokens',
11
+ 'maxCompletionTokens',
12
+ 'maxTokens',
13
+ ];
9
14
  const PRIVATE_TINFOIL_CHAT_COMPLETION_BODY_FIELDS = new Set([
10
15
  'chat_template_kwargs',
11
16
  'frequency_penalty',
@@ -209,34 +214,37 @@ function applyTinfoilCompatibilityMutations(body, model) {
209
214
  }
210
215
  }
211
216
 
212
- export function applyPrivateModelRequestMutations(body, model) {
213
- stripPrivateModeReasoningFromMessages(body);
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
- }
221
- if (body.max_tokens === undefined && body.maxTokens !== undefined) {
222
- body.max_tokens = body.maxTokens;
217
+ function normalizeMaxTokenAliases(body) {
218
+ for (const key of MAX_TOKEN_ALIAS_FIELDS) {
219
+ if (body.max_tokens === undefined && body[key] !== undefined) {
220
+ body.max_tokens = body[key];
221
+ }
222
+ delete body[key];
223
223
  }
224
- delete body.max_completion_tokens;
225
- delete body.maxCompletionTokens;
226
- delete body.maxTokens;
224
+ }
227
225
 
228
- body.model = model.upstreamModel;
226
+ function normalizeStreamOptions(body) {
229
227
  if (body.stream !== undefined) {
230
228
  body.stream = body.stream === true;
231
229
  }
230
+
232
231
  if (body.stream === true) {
233
232
  body.stream_options = {
234
233
  ...(body.stream_options && typeof body.stream_options === 'object' ? body.stream_options : {}),
235
234
  include_usage: true,
236
235
  };
237
- } else {
238
- delete body.stream_options;
236
+ return;
239
237
  }
238
+
239
+ delete body.stream_options;
240
+ }
241
+
242
+ export function applyPrivateModelRequestMutations(body, model) {
243
+ stripPrivateModeReasoningFromMessages(body);
244
+ normalizeMaxTokenAliases(body);
245
+
246
+ body.model = model.upstreamModel;
247
+ normalizeStreamOptions(body);
240
248
  applyTinfoilCompatibilityMutations(body, model);
241
249
 
242
250
  if (model.thinkingMode === 'gemma') {
@@ -246,14 +254,14 @@ export function applyPrivateModelRequestMutations(body, model) {
246
254
  };
247
255
  delete body.thinking;
248
256
  delete body.reasoning_effort;
249
- } else if (model.thinkingMode === 'deepseek-v4-pro' || model.thinkingMode === 'glm-5.2') {
257
+ } else if (model.thinkingMode === 'kimi-k2.6' || model.thinkingMode === 'glm-5.2') {
250
258
  const thinkingEnabled = shouldEnableThinking(body, model);
251
259
  body.chat_template_kwargs = {
252
260
  ...mergeChatTemplateKwargs(body),
253
261
  thinking: thinkingEnabled,
254
262
  };
255
263
 
256
- if (thinkingEnabled) {
264
+ if (model.thinkingMode === 'glm-5.2' && thinkingEnabled) {
257
265
  body.chat_template_kwargs.reasoning_effort =
258
266
  normalizeDeepSeekV4ReasoningEffort(body.reasoning_effort);
259
267
  } else {
@@ -0,0 +1,126 @@
1
+ const DEFAULT_SECURE_CLIENT_IDLE_RESET_MS = 5 * 60 * 1000;
2
+
3
+ function readSecureClientIdleResetMs() {
4
+ const parsed = Number(process.env.NANOGPT_PRIVATE_CLIENT_IDLE_RESET_MS || '');
5
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
6
+ return DEFAULT_SECURE_CLIENT_IDLE_RESET_MS;
7
+ }
8
+
9
+ export function createSecureState(apiBase, options = {}) {
10
+ const idleResetMs = Number.isFinite(options.idleResetMs) && options.idleResetMs > 0
11
+ ? Math.floor(options.idleResetMs)
12
+ : readSecureClientIdleResetMs();
13
+ const loadTinfoil = options.loadTinfoil || (() => import('tinfoil'));
14
+ const now = options.now || Date.now;
15
+ let clientState = null;
16
+ let verificationDocument = null;
17
+ let verificationError = null;
18
+
19
+ async function getClient(userCacheSecret = clientState?.userCacheSecret) {
20
+ if (!userCacheSecret) {
21
+ throw new Error('NanoGPT preflight must establish cache isolation before attestation.');
22
+ }
23
+ const nowMs = now();
24
+ const idleMs = clientState ? nowMs - clientState.lastUsedAtMs : 0;
25
+ if (
26
+ clientState?.userCacheSecret === userCacheSecret
27
+ && idleMs >= 0
28
+ && idleMs < idleResetMs
29
+ ) {
30
+ clientState.lastUsedAtMs = nowMs;
31
+ return clientState.promise;
32
+ }
33
+
34
+ // Re-attest after a long idle period or a wall-clock discontinuity. This
35
+ // covers laptops resuming after sleep without mutating a client that may
36
+ // still be finishing an older in-flight request.
37
+ verificationDocument = null;
38
+ verificationError = null;
39
+ const nextState = {
40
+ client: null,
41
+ lastUsedAtMs: nowMs,
42
+ promise: null,
43
+ userCacheSecret,
44
+ };
45
+ const promise = Promise.resolve(loadTinfoil()).then(async ({ SecureClient }) => {
46
+ const baseURL = `${apiBase}/api/v1/private/tinfoil/`;
47
+ const client = new SecureClient({
48
+ baseURL,
49
+ attestationBundleURL: `${apiBase}/api/v1/private/tinfoil`,
50
+ transport: 'ehbp',
51
+ userCacheSecret,
52
+ });
53
+ await client.ready();
54
+ nextState.client = client;
55
+ verificationDocument = client.getVerificationDocument();
56
+ verificationError = null;
57
+ return client;
58
+ });
59
+ nextState.promise = promise;
60
+ clientState = nextState;
61
+
62
+ try {
63
+ return await promise;
64
+ } catch (error) {
65
+ if (clientState === nextState) clientState = null;
66
+ verificationError = error;
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ return {
72
+ getClient,
73
+ invalidateClient(client) {
74
+ if (!clientState) return false;
75
+ if (client && clientState.client !== client) return false;
76
+ clientState = null;
77
+ verificationDocument = null;
78
+ return true;
79
+ },
80
+ getVerificationState() {
81
+ return {
82
+ verified: verificationDocument?.securityVerified === true,
83
+ verificationDocument,
84
+ error: verificationError ? String(verificationError.message || verificationError) : null,
85
+ };
86
+ },
87
+ };
88
+ }
89
+
90
+ export async function isMissingEncryptedBodyHeaderResponse(response) {
91
+ if (response?.status !== 400) return false;
92
+ try {
93
+ const data = await response.clone().json();
94
+ return data?.error?.code === 'missing_encrypted_body_header';
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ export async function fetchWithSecureClientRecovery({
101
+ fetchWithClient,
102
+ secureState,
103
+ shouldInvalidateError = () => true,
104
+ userCacheSecret,
105
+ }) {
106
+ let client;
107
+ try {
108
+ for (let attempt = 0; attempt < 2; attempt += 1) {
109
+ client = await secureState.getClient(userCacheSecret);
110
+ const response = await fetchWithClient(client);
111
+ if (attempt === 0 && await isMissingEncryptedBodyHeaderResponse(response)) {
112
+ await response.body?.cancel().catch(() => undefined);
113
+ secureState.invalidateClient(client);
114
+ continue;
115
+ }
116
+ return response;
117
+ }
118
+ } catch (error) {
119
+ if (client && shouldInvalidateError(error)) {
120
+ secureState.invalidateClient(client);
121
+ }
122
+ throw error;
123
+ }
124
+
125
+ throw new Error('Private Mode secure-client recovery exhausted unexpectedly.');
126
+ }
package/lib/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { readFileSync } from 'node:fs';
3
3
 
4
+ import { buildPrivateModeCacheScopeProof } from './cacheScope.js';
4
5
  import {
5
6
  buildPrivateModeOriginPolicy,
6
7
  getCorsHeadersForRequest,
@@ -10,6 +11,10 @@ import {
10
11
  normalizePrivateModeUpstreamErrorMessage,
11
12
  readErrorMessage,
12
13
  } from './serverErrorNormalization.js';
14
+ import {
15
+ createSecureState,
16
+ fetchWithSecureClientRecovery,
17
+ } from './secureClientLifecycle.js';
13
18
  import { buildPrivateModeStatusContract } from './statusContract.js';
14
19
 
15
20
  const MODELS = JSON.parse(
@@ -142,54 +147,13 @@ function readBody(req) {
142
147
 
143
148
  function copyLocalHeaders(req) {
144
149
  const headers = {};
145
- const teamId = req.headers['x-team-id'];
150
+ const teamId = req?.headers?.['x-team-id'];
146
151
  if (typeof teamId === 'string' && teamId.trim()) {
147
152
  headers['x-team-id'] = teamId.trim();
148
153
  }
149
154
  return headers;
150
155
  }
151
156
 
152
- function createSecureState(apiBase) {
153
- let clientPromise = null;
154
- let verificationDocument = null;
155
- let verificationError = null;
156
-
157
- async function getClient() {
158
- if (!clientPromise) {
159
- clientPromise = import('tinfoil')
160
- .then(async ({ SecureClient }) => {
161
- const baseURL = `${apiBase}/api/v1/private/tinfoil/`;
162
- const client = new SecureClient({
163
- baseURL,
164
- attestationBundleURL: `${apiBase}/api/v1/private/tinfoil`,
165
- transport: 'ehbp',
166
- });
167
- await client.ready();
168
- verificationDocument = client.getVerificationDocument();
169
- verificationError = null;
170
- return client;
171
- })
172
- .catch((error) => {
173
- clientPromise = null;
174
- verificationError = error;
175
- throw error;
176
- });
177
- }
178
- return clientPromise;
179
- }
180
-
181
- return {
182
- getClient,
183
- getVerificationState() {
184
- return {
185
- verified: verificationDocument?.securityVerified === true,
186
- verificationDocument,
187
- error: verificationError ? String(verificationError.message || verificationError) : null,
188
- };
189
- },
190
- };
191
- }
192
-
193
157
  async function runPreflight({ apiBase, apiKey, model, req, requestBodyBytes }) {
194
158
  const preflightBody = { model: model.id };
195
159
  if (Number.isSafeInteger(requestBodyBytes) && requestBodyBytes >= 0) {
@@ -206,7 +170,27 @@ async function runPreflight({ apiBase, apiKey, model, req, requestBodyBytes }) {
206
170
  body: JSON.stringify(preflightBody),
207
171
  });
208
172
 
209
- if (response.ok) return { ok: true };
173
+ if (response.ok) {
174
+ try {
175
+ const data = await response.json();
176
+ const cacheScope = typeof data?.cacheScope === 'string' ? data.cacheScope.trim() : '';
177
+ if (/^[a-f0-9]{64}$/i.test(cacheScope)) {
178
+ return { ok: true, cacheScope };
179
+ }
180
+ } catch {}
181
+
182
+ return {
183
+ ok: false,
184
+ status: 502,
185
+ body: {
186
+ error: {
187
+ message: 'NanoGPT preflight did not establish cache isolation.',
188
+ type: 'api_error',
189
+ code: 'cache_scope_unavailable',
190
+ },
191
+ },
192
+ };
193
+ }
210
194
 
211
195
  let errorBody = null;
212
196
  try {
@@ -223,6 +207,22 @@ async function runPreflight({ apiBase, apiKey, model, req, requestBodyBytes }) {
223
207
  return { ok: false, status: response.status, body: errorBody };
224
208
  }
225
209
 
210
+ async function getPreflightedClient({ apiBase, apiKey, secureState, req, model = MODELS[0] }) {
211
+ const preflight = await runPreflight({
212
+ apiBase,
213
+ apiKey,
214
+ model,
215
+ req,
216
+ requestBodyBytes: 0,
217
+ });
218
+ if (!preflight.ok) {
219
+ throw new Error(
220
+ preflight.body?.error?.message || `NanoGPT preflight failed with HTTP ${preflight.status}`,
221
+ );
222
+ }
223
+ return secureState.getClient(preflight.cacheScope);
224
+ }
225
+
226
226
  async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, corsHeaders }) {
227
227
  let rawBody;
228
228
  try {
@@ -288,20 +288,25 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
288
288
  });
289
289
 
290
290
  try {
291
- const client = await secureState.getClient();
292
- response = await client.fetch(`${apiBase}/api/v1/private/tinfoil/v1/chat/completions`, {
293
- method: 'POST',
294
- headers: {
295
- authorization: `Bearer ${apiKey}`,
296
- 'content-type': 'application/json',
297
- accept: privateStreamRequested ? 'text/event-stream' : 'application/json',
298
- 'x-nanogpt-private-model': model.id,
299
- 'x-nanogpt-private-stream': privateStreamRequested ? 'true' : 'false',
300
- 'x-query-source': 'api',
301
- ...copyLocalHeaders(req),
302
- },
303
- body: privateRequestBody,
304
- signal: upstreamAbortController.signal,
291
+ response = await fetchWithSecureClientRecovery({
292
+ secureState,
293
+ userCacheSecret: preflight.cacheScope,
294
+ shouldInvalidateError: () => !upstreamAbortController.signal.aborted,
295
+ fetchWithClient: (client) => client.fetch(`${apiBase}/api/v1/private/tinfoil/v1/chat/completions`, {
296
+ method: 'POST',
297
+ headers: {
298
+ authorization: `Bearer ${apiKey}`,
299
+ 'content-type': 'application/json',
300
+ accept: privateStreamRequested ? 'text/event-stream' : 'application/json',
301
+ 'x-nanogpt-private-model': model.id,
302
+ 'x-nanogpt-private-stream': privateStreamRequested ? 'true' : 'false',
303
+ 'x-nanogpt-private-cache-scope': buildPrivateModeCacheScopeProof(preflight.cacheScope),
304
+ 'x-query-source': 'api',
305
+ ...copyLocalHeaders(req),
306
+ },
307
+ body: privateRequestBody,
308
+ signal: upstreamAbortController.signal,
309
+ }),
305
310
  });
306
311
  } catch (error) {
307
312
  responseComplete = true;
@@ -309,7 +314,7 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
309
314
  const message = error instanceof Error ? error.message : String(error);
310
315
  jsonResponse(res, 502, {
311
316
  error: {
312
- message: `Private Mode request failed: ${normalizePrivateModeUpstreamErrorMessage(message, message)}`,
317
+ message: normalizePrivateModeUpstreamErrorMessage(message, 'Private Mode request failed.'),
313
318
  type: 'api_error',
314
319
  code: 'private_mode_request_failed',
315
320
  },
@@ -442,7 +447,12 @@ export async function startPrivateModeProxy(options) {
442
447
 
443
448
  if (req.method === 'GET' && url.pathname === '/v1/private-mode/attestation') {
444
449
  try {
445
- await secureState.getClient();
450
+ await getPreflightedClient({
451
+ apiBase,
452
+ apiKey: options.apiKey,
453
+ secureState,
454
+ req,
455
+ });
446
456
  jsonResponse(res, 200, secureState.getVerificationState(), corsHeaders);
447
457
  } catch (error) {
448
458
  jsonResponse(res, 502, {
@@ -508,7 +518,11 @@ The verified TEE target and this local proxy can see plaintext.
508
518
  }
509
519
 
510
520
  if (options.warmAttestation !== false) {
511
- secureState.getClient()
521
+ getPreflightedClient({
522
+ apiBase,
523
+ apiKey: options.apiKey,
524
+ secureState,
525
+ })
512
526
  .then((client) => {
513
527
  const doc = client.getVerificationDocument();
514
528
  process.stdout.write(`Attestation: verified=${doc.securityVerified === true}; enclave=${doc.enclaveHost || client.getEnclaveURL() || 'unknown'}\n`);
@@ -1,45 +1,30 @@
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;
1
+ // Keep user-facing upstream error classification in sync with
2
+ // lib/privateMode/tinfoilBrowserClient.ts. Never expose decrypted upstream
3
+ // response bodies or exception text to local proxy clients.
3
4
  const MISSING_EHBP_RESPONSE_NONCE_PATTERN = /missing\s+ehbp-response-nonce\s+header/i;
5
+ const PRIVATE_MODE_REFUND_NOTICE = 'Any reserved balance will be released or refunded.';
4
6
 
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.';
7
+ export function privateModeProviderFailureMessage(status) {
8
+ if (status === 429) {
9
+ return `Private Mode is temporarily rate-limited. ${PRIVATE_MODE_REFUND_NOTICE}`;
8
10
  }
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
+ return `The Private Mode provider temporarily failed. ${PRIVATE_MODE_REFUND_NOTICE}`;
12
+ }
13
+
14
+ export function normalizePrivateModeUpstreamErrorMessage(message, fallback) {
15
+ void fallback;
16
+ if (MISSING_EHBP_RESPONSE_NONCE_PATTERN.test(String(message || ''))) {
17
+ return `Private Mode could not verify the provider response. ${PRIVATE_MODE_REFUND_NOTICE}`;
11
18
  }
12
- return message || fallback;
19
+ return privateModeProviderFailureMessage();
13
20
  }
14
21
 
15
22
  export async function readErrorMessage(response, fallback) {
16
- let message = fallback;
23
+ void fallback;
17
24
  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
- }
25
+ await response?.body?.cancel();
35
26
  } catch {
36
- try {
37
- const text = await response.text();
38
- message = text || fallback;
39
- } catch {
40
- message = fallback;
41
- }
27
+ // The body may already be locked or closed. Its contents remain discarded.
42
28
  }
43
-
44
- return normalizePrivateModeUpstreamErrorMessage(message, fallback);
29
+ return privateModeProviderFailureMessage(response?.status);
45
30
  }
@@ -5,6 +5,7 @@
5
5
  "upstreamModel": "kimi-k2-6",
6
6
  "billingModel": "TEE/kimi-k2-6",
7
7
  "teeTargetModel": "kimi-k2-6",
8
+ "thinkingMode": "kimi-k2.6",
8
9
  "created": 1764547200,
9
10
  "ownedBy": "nanogpt-private-mode",
10
11
  "aliases": ["private/kimi-k2.6", "TEE/kimi-k2-6", "TEE/kimi-k2.6"]
@@ -72,27 +73,5 @@
72
73
  "created": 1764547200,
73
74
  "ownedBy": "nanogpt-private-mode",
74
75
  "aliases": ["TEE/gemma4-31b:thinking", "gemma4-31b:thinking"]
75
- },
76
- {
77
- "id": "private/deepseek-v4-pro",
78
- "name": "DeepSeek V4 Pro Private",
79
- "upstreamModel": "deepseek-v4-pro",
80
- "billingModel": "TEE/deepseek-v4-pro",
81
- "teeTargetModel": "deepseek-v4-pro",
82
- "thinkingMode": "deepseek-v4-pro",
83
- "created": 1764547200,
84
- "ownedBy": "nanogpt-private-mode",
85
- "aliases": ["TEE/deepseek-v4-pro", "deepseek-v4-pro"]
86
- },
87
- {
88
- "id": "private/deepseek-v4-pro:thinking",
89
- "name": "DeepSeek V4 Pro Thinking Private",
90
- "upstreamModel": "deepseek-v4-pro",
91
- "billingModel": "TEE/deepseek-v4-pro:thinking",
92
- "teeTargetModel": "deepseek-v4-pro",
93
- "thinkingMode": "deepseek-v4-pro",
94
- "created": 1764547200,
95
- "ownedBy": "nanogpt-private-mode",
96
- "aliases": ["TEE/deepseek-v4-pro:thinking", "deepseek-v4-pro:thinking"]
97
76
  }
98
77
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanogpt/private-mode",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "OpenAI-compatible localhost proxy for NanoGPT Private Mode.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -12,8 +12,10 @@
12
12
  },
13
13
  "files": [
14
14
  "bin",
15
+ "lib/cacheScope.js",
15
16
  "lib/originPolicy.js",
16
17
  "lib/requestTransforms.js",
18
+ "lib/secureClientLifecycle.js",
17
19
  "lib/server.js",
18
20
  "lib/serverErrorNormalization.js",
19
21
  "lib/statusContract.js",
@@ -21,14 +23,15 @@
21
23
  "models",
22
24
  "README.md"
23
25
  ],
24
- "scripts": {
25
- "start": "node ./bin/nanogpt-private-mode.js"
26
- },
27
26
  "dependencies": {
28
- "tinfoil": "1.1.3"
27
+ "ai": "6.0.220",
28
+ "tinfoil": "1.1.12"
29
29
  },
30
30
  "engines": {
31
- "node": ">=20"
31
+ "node": ">=22"
32
32
  },
33
- "license": "MIT"
34
- }
33
+ "license": "MIT",
34
+ "scripts": {
35
+ "start": "node ./bin/nanogpt-private-mode.js"
36
+ }
37
+ }