@nanogpt/private-mode 0.2.2 → 0.2.5

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
@@ -25,14 +25,14 @@ const client = new OpenAI({
25
25
  });
26
26
 
27
27
  const response = await client.chat.completions.create({
28
- model: "private/kimi-k2-6",
28
+ model: "private/glm-5-2",
29
29
  messages: [{ role: "user", content: "Hello" }],
30
30
  });
31
31
  ```
32
32
 
33
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.
34
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.
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, or `0` to disable idle resets.
36
36
 
37
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.
38
38
 
@@ -54,7 +54,7 @@ GET http://127.0.0.1:8787/v1/private-mode/attestation
54
54
 
55
55
  Supported private model IDs include:
56
56
 
57
- - `private/kimi-k2-6`
57
+ - `private/kimi-k3`
58
58
  - `private/glm-5-1`
59
59
  - `private/glm-5-1-thinking`
60
60
  - `private/glm-5-2`
@@ -8,9 +8,25 @@ const ASSISTANT_REASONING_MESSAGE_FIELDS = [
8
8
  ];
9
9
  const MAX_TOKEN_ALIAS_FIELDS = [
10
10
  'max_completion_tokens',
11
+ 'max_output_tokens',
11
12
  'maxCompletionTokens',
12
13
  'maxTokens',
13
14
  ];
15
+ const KIMI_K3_STANDARD_DEFAULT_MAX_COMPLETION_TOKENS = 16_384;
16
+ const KIMI_K3_DEFAULT_MAX_COMPLETION_TOKENS = 65_536;
17
+ const KIMI_K3_MAX_COMPLETION_TOKENS = 1_048_576;
18
+ const KIMI_K3_CONTEXT_WINDOW_TOKENS = 1_048_576;
19
+ const KIMI_K3_CONTEXT_SAFETY_MARGIN_TOKENS = 10;
20
+ const KIMI_K3_ESTIMATED_TEXT_BYTES_PER_TOKEN = 3;
21
+ const KIMI_K3_HIGH_ENTROPY_TEXT_MIN_LENGTH = 2_048;
22
+ const KIMI_K3_HIGH_ENTROPY_TOKENS_PER_BYTE = 0.75;
23
+ const KIMI_K3_IMAGE_TOKENS = 85;
24
+ const KIMI_K3_DOCUMENT_TOKENS = 85;
25
+ const KIMI_K3_AUDIO_TOKENS = 32_768;
26
+ const KIMI_K3_AUDIO_URL_TOKENS = 4_096;
27
+ const KIMI_K3_VIDEO_TOKENS = 65_536;
28
+ const KIMI_K3_REASONING_EFFORT_LEVELS = new Set(['low', 'high', 'max']);
29
+ const KIMI_K3_TEXT_ENCODER = new TextEncoder();
14
30
  const PRIVATE_TINFOIL_CHAT_COMPLETION_BODY_FIELDS = new Set([
15
31
  'chat_template_kwargs',
16
32
  'frequency_penalty',
@@ -44,6 +60,149 @@ function isPlainObject(value) {
44
60
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
45
61
  }
46
62
 
63
+ function estimateKimiK3TextTokens(value) {
64
+ if (typeof value !== 'string' || value.length === 0) return 0;
65
+ const utf8Bytes = KIMI_K3_TEXT_ENCODER.encode(value).byteLength;
66
+ const ordinaryEstimate = Math.ceil(utf8Bytes / KIMI_K3_ESTIMATED_TEXT_BYTES_PER_TOKEN);
67
+ if (value.length < KIMI_K3_HIGH_ENTROPY_TEXT_MIN_LENGTH) return ordinaryEstimate;
68
+
69
+ let ascii = 0;
70
+ let whitespace = 0;
71
+ const uniqueChars = new Set();
72
+ let currentRun = 0;
73
+ let longestRun = 0;
74
+ let previous = '';
75
+
76
+ for (const char of value) {
77
+ const code = char.charCodeAt(0);
78
+ if (code >= 32 && code <= 126) ascii += 1;
79
+ if (/\s/.test(char)) whitespace += 1;
80
+ uniqueChars.add(char);
81
+ if (char === previous) {
82
+ currentRun += 1;
83
+ } else {
84
+ previous = char;
85
+ currentRun = 1;
86
+ }
87
+ if (currentRun > longestRun) longestRun = currentRun;
88
+ }
89
+
90
+ const isHighEntropy =
91
+ ascii / value.length >= 0.98 &&
92
+ whitespace / value.length <= 0.02 &&
93
+ uniqueChars.size >= 32 &&
94
+ longestRun / value.length <= 0.02;
95
+ if (!isHighEntropy) return ordinaryEstimate;
96
+
97
+ return Math.max(
98
+ ordinaryEstimate,
99
+ Math.ceil(utf8Bytes * KIMI_K3_HIGH_ENTROPY_TOKENS_PER_BYTE),
100
+ );
101
+ }
102
+
103
+ function estimateKimiK3ContentTokens(content) {
104
+ if (typeof content === 'string') return estimateKimiK3TextTokens(content);
105
+ if (Array.isArray(content)) {
106
+ return content.reduce((total, part) => total + estimateKimiK3ContentTokens(part), 0);
107
+ }
108
+ if (!isPlainObject(content)) return 0;
109
+
110
+ const type = String(content.type || '').toLowerCase();
111
+ const nestedFile = isPlainObject(content.file) ? content.file : {};
112
+ const nestedSource = isPlainObject(content.source) ? content.source : {};
113
+ const nestedDocument = isPlainObject(content.document) ? content.document : {};
114
+ const mediaType = String(
115
+ content.media_type ||
116
+ content.mime_type ||
117
+ content.file_type ||
118
+ nestedFile.media_type ||
119
+ nestedFile.mime_type ||
120
+ nestedFile.file_type ||
121
+ nestedSource.media_type ||
122
+ nestedSource.mime_type ||
123
+ nestedDocument.media_type ||
124
+ nestedDocument.mime_type ||
125
+ '',
126
+ ).trim().toLowerCase();
127
+ const hasVideoMediaValue = [
128
+ content.url,
129
+ content.file_url,
130
+ content.file_data,
131
+ nestedFile.url,
132
+ nestedFile.file_url,
133
+ nestedFile.file_data,
134
+ nestedFile.data,
135
+ nestedSource.url,
136
+ nestedSource.data,
137
+ nestedDocument.url,
138
+ nestedDocument.file_url,
139
+ ].some((value) => {
140
+ if (isPlainObject(value)) value = value.url;
141
+ if (typeof value !== 'string') return false;
142
+ const normalized = value.trim().toLowerCase();
143
+ if (normalized.startsWith('data:video/')) return true;
144
+ let pathname = normalized.split(/[?#]/, 1)[0] || normalized;
145
+ try {
146
+ pathname = new URL(value).pathname.toLowerCase();
147
+ } catch {}
148
+ return /\.(?:mp4|mov|mkv|avi|m4v|mpeg|mpg|webm|ogv)$/.test(pathname);
149
+ });
150
+ if (type === 'audio_url' || (!type && content.audio_url)) return KIMI_K3_AUDIO_URL_TOKENS;
151
+ if (type === 'input_audio' || type === 'audio') return KIMI_K3_AUDIO_TOKENS;
152
+ if (
153
+ type === 'video' ||
154
+ type === 'video_url' ||
155
+ type === 'input_video' ||
156
+ mediaType.startsWith('video/') ||
157
+ hasVideoMediaValue ||
158
+ 'video_url' in content ||
159
+ 'input_video' in content
160
+ ) return KIMI_K3_VIDEO_TOKENS;
161
+ if (
162
+ type === 'image' ||
163
+ type === 'image_url' ||
164
+ type === 'input_image' ||
165
+ 'image_url' in content
166
+ ) return KIMI_K3_IMAGE_TOKENS;
167
+ if (
168
+ type === 'document' ||
169
+ type === 'input_file' ||
170
+ type === 'input_document' ||
171
+ 'file_url' in content
172
+ ) return KIMI_K3_DOCUMENT_TOKENS;
173
+
174
+ for (const key of ['text', 'content', 'message']) {
175
+ if (typeof content[key] === 'string') return estimateKimiK3TextTokens(content[key]);
176
+ }
177
+ return estimateKimiK3TextTokens(JSON.stringify(content));
178
+ }
179
+
180
+ function estimatePrivateModeKimiK3PromptTokens(body) {
181
+ let tokens = 3;
182
+ for (const message of Array.isArray(body.messages) ? body.messages : []) {
183
+ if (!isPlainObject(message)) continue;
184
+ tokens += 3;
185
+ tokens += estimateKimiK3TextTokens(message.role);
186
+ tokens += estimateKimiK3TextTokens(message.name);
187
+ tokens += estimateKimiK3ContentTokens(message.content);
188
+ tokens += estimateKimiK3ContentTokens(message.prompt);
189
+ tokens += estimateKimiK3TextTokens(message.reasoning_content);
190
+ if (Array.isArray(message.tools)) {
191
+ tokens += estimateKimiK3TextTokens(JSON.stringify(message.tools));
192
+ }
193
+ if (message.function_call !== undefined) {
194
+ tokens += estimateKimiK3TextTokens(JSON.stringify(message.function_call));
195
+ }
196
+ if (message.tool_calls !== undefined) {
197
+ tokens += estimateKimiK3TextTokens(JSON.stringify(message.tool_calls));
198
+ }
199
+ }
200
+ if (Array.isArray(body.tools)) {
201
+ tokens += estimateKimiK3TextTokens(JSON.stringify(body.tools));
202
+ }
203
+ return tokens;
204
+ }
205
+
47
206
  function isThinkingEnabled(thinking) {
48
207
  if (typeof thinking === 'boolean') return thinking;
49
208
  if (!isPlainObject(thinking)) return undefined;
@@ -123,8 +282,32 @@ function stripPrivateModeReasoningBlocks(value) {
123
282
  .replace(/◁think▷/g, '<think>')
124
283
  .replace(/◁\/think▷/g, '</think>')
125
284
  .replace(/<think>[\s\S]*?<\/think>\s*/gi, '')
285
+ .replace(/<thinking>[\s\S]*?<\/thinking>\s*/gi, '')
126
286
  .replace(/<previous_reasoning>[\s\S]*?<\/previous_reasoning>\s*/gi, '')
127
- .trimStart();
287
+ .replace(/<(?:think|thinking|previous_reasoning)>[\s\S]*$/i, '');
288
+ }
289
+
290
+ function extractPrivateModeReasoningBlocks(value) {
291
+ const textValues = typeof value === 'string'
292
+ ? [value]
293
+ : Array.isArray(value)
294
+ ? value.flatMap((part) => (
295
+ isPlainObject(part) && part.type === 'text' && typeof part.text === 'string'
296
+ ? [part.text]
297
+ : []
298
+ ))
299
+ : [];
300
+ const reasoningParts = textValues.flatMap((text) => {
301
+ const normalized = text
302
+ .replace(/◁think▷/g, '<think>')
303
+ .replace(/◁\/think▷/g, '</think>');
304
+ return Array.from(
305
+ normalized.matchAll(/<(think|thinking|previous_reasoning)>([\s\S]*?)<\/\1>/gi),
306
+ (match) => match[2],
307
+ );
308
+ });
309
+ const reasoning = reasoningParts.join('');
310
+ return reasoning.trim() ? reasoning : undefined;
128
311
  }
129
312
 
130
313
  function stripPrivateModeReasoningFromContent(value) {
@@ -163,6 +346,441 @@ function stripPrivateModeReasoningFromMessages(body) {
163
346
  });
164
347
  }
165
348
 
349
+ function normalizePrivateModeReasoningPayload(value) {
350
+ if (typeof value === 'string') return value.trim() ? value : undefined;
351
+ if (Array.isArray(value)) {
352
+ const joined = value
353
+ .map(normalizePrivateModeReasoningPayload)
354
+ .filter(Boolean)
355
+ .join('');
356
+ return joined || undefined;
357
+ }
358
+ if (!isPlainObject(value)) return undefined;
359
+ return normalizePrivateModeReasoningPayload(
360
+ value.text ?? value.content ?? value.reasoning_content ?? value.reasoning ?? value.thinking,
361
+ );
362
+ }
363
+
364
+ function normalizePrivateModeKimiK3ReasoningFromMessages(body) {
365
+ if (!Array.isArray(body.messages)) return;
366
+
367
+ body.messages = body.messages.map((message) => {
368
+ if (!isPlainObject(message) || message.role !== 'assistant') return message;
369
+
370
+ const normalized = { ...message };
371
+ const reasoningContent =
372
+ normalizePrivateModeReasoningPayload(message.reasoning_content) ??
373
+ normalizePrivateModeReasoningPayload(message.reasoning) ??
374
+ normalizePrivateModeReasoningPayload(message.thinking) ??
375
+ normalizePrivateModeReasoningPayload(message.reasoning_details) ??
376
+ extractPrivateModeReasoningBlocks(message.content) ??
377
+ extractPrivateModeReasoningBlocks(message.prompt);
378
+ delete normalized.reasoning;
379
+ delete normalized.reasoning_details;
380
+ delete normalized.thinking;
381
+ if (reasoningContent) normalized.reasoning_content = reasoningContent;
382
+ else delete normalized.reasoning_content;
383
+ if ('content' in normalized) {
384
+ normalized.content = stripPrivateModeReasoningFromContent(normalized.content);
385
+ }
386
+ if ('prompt' in normalized) {
387
+ normalized.prompt = stripPrivateModeReasoningFromContent(normalized.prompt);
388
+ }
389
+ return normalized;
390
+ });
391
+ }
392
+
393
+ function normalizePrivateModeKimiK3DynamicToolMessages(body) {
394
+ if (!Array.isArray(body.messages)) return;
395
+
396
+ body.messages = body.messages.flatMap((message) => {
397
+ if (!isPlainObject(message) || message.role !== 'system' || !Array.isArray(message.tools)) {
398
+ return [message];
399
+ }
400
+
401
+ const { tools, ...messageWithoutTools } = message;
402
+ const hasContent =
403
+ messageWithoutTools.content !== undefined &&
404
+ messageWithoutTools.content !== null &&
405
+ !(typeof messageWithoutTools.content === 'string' && messageWithoutTools.content.trim() === '');
406
+ const normalizedMessages = hasContent ? [messageWithoutTools] : [];
407
+ if (tools.length > 0) normalizedMessages.push({ role: 'system', tools });
408
+ return normalizedMessages;
409
+ });
410
+ }
411
+
412
+ function isPrivateModeKimiK3Model(model) {
413
+ return [model.upstreamModel, model.billingModel, ...(model.aliases || [])]
414
+ .some((value) => {
415
+ const normalized = String(value).trim().toLowerCase();
416
+ return normalized === 'kimi-k3' || normalized === 'tee/kimi-k3';
417
+ });
418
+ }
419
+
420
+ function getPrivateModeFunctionToolName(tool) {
421
+ if (!isPlainObject(tool) || tool.type !== 'function' || !isPlainObject(tool.function)) {
422
+ return undefined;
423
+ }
424
+ const name = tool.function.name;
425
+ return typeof name === 'string' && name.trim() ? name.trim() : undefined;
426
+ }
427
+
428
+ function getPrivateModeNamedToolChoiceName(toolChoice) {
429
+ if (!isPlainObject(toolChoice)) return undefined;
430
+ if (toolChoice.type !== undefined && String(toolChoice.type).toLowerCase() !== 'function') {
431
+ return undefined;
432
+ }
433
+ const name = isPlainObject(toolChoice.function)
434
+ ? toolChoice.function.name
435
+ : toolChoice.name;
436
+ return typeof name === 'string' && name.trim() ? name.trim() : undefined;
437
+ }
438
+
439
+ function normalizePrivateModeKimiK3NamedToolChoice(body) {
440
+ const selectedToolName = getPrivateModeNamedToolChoiceName(body.tool_choice);
441
+ if (!selectedToolName) return;
442
+
443
+ const dynamicTools = Array.isArray(body.messages)
444
+ ? body.messages.flatMap((message) => (
445
+ isPlainObject(message) && message.role === 'system' && Array.isArray(message.tools)
446
+ ? message.tools
447
+ : []
448
+ ))
449
+ : [];
450
+ const selectedTool = [
451
+ ...(Array.isArray(body.tools) ? body.tools : []),
452
+ ...dynamicTools,
453
+ ].find((tool) => getPrivateModeFunctionToolName(tool) === selectedToolName);
454
+ if (!selectedTool) return;
455
+
456
+ body.tools = [selectedTool];
457
+ body.tool_choice = 'required';
458
+ if (dynamicTools.length === 0 || !Array.isArray(body.messages)) return;
459
+ body.messages = body.messages.flatMap((message) => {
460
+ if (!isPlainObject(message) || message.role !== 'system' || !Array.isArray(message.tools)) {
461
+ return [message];
462
+ }
463
+ const { tools: _tools, ...messageWithoutTools } = message;
464
+ const hasContent =
465
+ messageWithoutTools.content !== undefined &&
466
+ messageWithoutTools.content !== null &&
467
+ !(typeof messageWithoutTools.content === 'string' && messageWithoutTools.content.trim() === '');
468
+ return hasContent ? [messageWithoutTools] : [];
469
+ });
470
+ }
471
+
472
+ function getKimiK3ReasoningEffortCandidate(value) {
473
+ return isPlainObject(value) ? value.effort : value;
474
+ }
475
+
476
+ function isKimiK3ReasoningOptOutCandidate(value) {
477
+ if (value === false) return true;
478
+ if (typeof value === 'string') {
479
+ const normalized = value.trim().toLowerCase();
480
+ return normalized === 'none' || normalized === 'off';
481
+ }
482
+ if (!isPlainObject(value)) return false;
483
+ const effort = String(value.effort || '').trim().toLowerCase();
484
+ const type = String(value.type || '').trim().toLowerCase();
485
+ return value.enabled === false ||
486
+ effort === 'none' ||
487
+ effort === 'off' ||
488
+ type === 'disabled' ||
489
+ type === 'off';
490
+ }
491
+
492
+ function coercePrivateModeBooleanFlag(value) {
493
+ if (typeof value === 'boolean') return value;
494
+ if (typeof value !== 'string') return undefined;
495
+ const normalized = value.trim().toLowerCase();
496
+ if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
497
+ if (['false', '0', 'no', 'off'].includes(normalized)) return false;
498
+ return undefined;
499
+ }
500
+
501
+ export function normalizePrivateModelReasoningControls(body, model) {
502
+ if (!isPrivateModeKimiK3Model(model)) return body;
503
+ const includeReasoning = coercePrivateModeBooleanFlag(body.include_reasoning);
504
+ if (includeReasoning === undefined) return body;
505
+ const reasoning = isPlainObject(body.reasoning) ? { ...body.reasoning } : {};
506
+ if (includeReasoning) reasoning.enabled = true;
507
+ else reasoning.exclude = true;
508
+ body.reasoning = reasoning;
509
+ return body;
510
+ }
511
+
512
+ export function shouldSuppressPrivateModelReasoning(body, model) {
513
+ if (!isPrivateModeKimiK3Model(model)) return false;
514
+ return body.reasoningOptOut === true ||
515
+ body.exposeReasoning === false ||
516
+ body.materializeReasoning === false ||
517
+ coercePrivateModeBooleanFlag(body.include_reasoning) === false ||
518
+ [body.enable_thinking, body.thinking, body.reasoning_effort, body.reasoning]
519
+ .some((value) => isKimiK3ReasoningOptOutCandidate(value) || (
520
+ isPlainObject(value) && value.exclude === true
521
+ ));
522
+ }
523
+
524
+ function resolvePrivateModeKimiK3ReasoningEffort(body) {
525
+ const candidates = [
526
+ body.reasoningOptOut === true ? false : undefined,
527
+ body.exposeReasoning === false ? false : undefined,
528
+ body.materializeReasoning === false ? false : undefined,
529
+ body.enable_thinking,
530
+ body.thinking,
531
+ body.reasoning_effort,
532
+ body.reasoning,
533
+ ];
534
+ if (candidates.some(isKimiK3ReasoningOptOutCandidate)) return 'low';
535
+
536
+ const explicitEffort = candidates
537
+ .map(getKimiK3ReasoningEffortCandidate)
538
+ .find((value) => typeof value === 'string' && KIMI_K3_REASONING_EFFORT_LEVELS.has(value.trim().toLowerCase()));
539
+ const normalizedEffort = typeof explicitEffort === 'string'
540
+ ? explicitEffort.trim().toLowerCase()
541
+ : undefined;
542
+ const excludesReasoning = candidates.some((value) => isPlainObject(value) && value.exclude === true);
543
+ return excludesReasoning ? normalizedEffort ?? 'low' : normalizedEffort ?? 'max';
544
+ }
545
+
546
+ function applyPrivateModeKimiK3RequestParams(body) {
547
+ const reasoningEffort = resolvePrivateModeKimiK3ReasoningEffort(body);
548
+ const requestedMaxTokens = typeof body.max_tokens === 'number' && Number.isFinite(body.max_tokens)
549
+ ? body.max_tokens
550
+ : undefined;
551
+ const normalizedMaxTokens = requestedMaxTokens === undefined
552
+ ? reasoningEffort === 'max'
553
+ ? KIMI_K3_DEFAULT_MAX_COMPLETION_TOKENS
554
+ : KIMI_K3_STANDARD_DEFAULT_MAX_COMPLETION_TOKENS
555
+ : requestedMaxTokens < 0
556
+ ? KIMI_K3_MAX_COMPLETION_TOKENS
557
+ : requestedMaxTokens;
558
+ normalizePrivateModeKimiK3NamedToolChoice(body);
559
+ const promptTokenEstimate = estimatePrivateModeKimiK3PromptTokens(body);
560
+ const remainingContext = Math.max(
561
+ 1,
562
+ KIMI_K3_CONTEXT_WINDOW_TOKENS -
563
+ promptTokenEstimate -
564
+ KIMI_K3_CONTEXT_SAFETY_MARGIN_TOKENS,
565
+ );
566
+
567
+ delete body.temperature;
568
+ delete body.top_p;
569
+ delete body.n;
570
+ delete body.frequency_penalty;
571
+ delete body.presence_penalty;
572
+ delete body.logit_bias;
573
+ delete body.thinking;
574
+ delete body.enable_thinking;
575
+ delete body.reasoning;
576
+ body.reasoning_effort = reasoningEffort;
577
+ body.max_tokens = Math.min(
578
+ Math.max(1, Math.floor(normalizedMaxTokens)),
579
+ remainingContext,
580
+ );
581
+ }
582
+
583
+ export function createPrivateModeReasoningContentSuppressor() {
584
+ const openTags = ['<think>', '<thinking>', '<previous_reasoning>', '◁think▷'];
585
+ const closeTags = ['</think>', '</thinking>', '</previous_reasoning>', '◁/think▷'];
586
+ const allTags = [...openTags, ...closeTags];
587
+ const maxTagLength = Math.max(...allTags.map((tag) => tag.length));
588
+ let buffered = '';
589
+ let insideReasoning = false;
590
+
591
+ const findEarliestTag = (text, tags) => {
592
+ let earliest = null;
593
+ const normalizedText = text.toLowerCase();
594
+ for (const tag of tags) {
595
+ const index = normalizedText.indexOf(tag.toLowerCase());
596
+ if (index >= 0 && (!earliest || index < earliest.index)) earliest = { index, tag };
597
+ }
598
+ return earliest;
599
+ };
600
+ const findPartialTagIndex = (text, tags) => {
601
+ const start = Math.max(0, text.length - maxTagLength + 1);
602
+ for (let index = start; index < text.length; index += 1) {
603
+ const suffix = text.slice(index).toLowerCase();
604
+ if (tags.some((tag) => {
605
+ const normalizedTag = tag.toLowerCase();
606
+ return normalizedTag.startsWith(suffix) && normalizedTag !== suffix;
607
+ })) return index;
608
+ }
609
+ return -1;
610
+ };
611
+
612
+ return {
613
+ process(value) {
614
+ let input = buffered + String(value || '');
615
+ buffered = '';
616
+ let output = '';
617
+ while (input) {
618
+ const tags = insideReasoning ? closeTags : openTags;
619
+ const match = findEarliestTag(input, tags);
620
+ if (match) {
621
+ if (!insideReasoning) output += input.slice(0, match.index);
622
+ input = input.slice(match.index + match.tag.length);
623
+ insideReasoning = !insideReasoning;
624
+ continue;
625
+ }
626
+ const partialIndex = findPartialTagIndex(input, tags);
627
+ if (!insideReasoning) {
628
+ output += partialIndex >= 0 ? input.slice(0, partialIndex) : input;
629
+ }
630
+ if (partialIndex >= 0) buffered = input.slice(partialIndex);
631
+ return output;
632
+ }
633
+ return output;
634
+ },
635
+ flush() {
636
+ const output = insideReasoning ? '' : buffered;
637
+ buffered = '';
638
+ return output;
639
+ },
640
+ };
641
+ }
642
+
643
+ export function splitCompleteSseFrames(buffer) {
644
+ const parts = buffer.split(/\r?\n\r?\n/);
645
+ return {
646
+ frames: parts.slice(0, -1).map((frame) => frame.replace(/\r\n/g, '\n')),
647
+ remainder: parts.at(-1) || '',
648
+ };
649
+ }
650
+
651
+ const PRIVATE_MODE_SSE_CHUNK_METADATA_FIELDS = [
652
+ 'id',
653
+ 'object',
654
+ 'created',
655
+ 'model',
656
+ 'system_fingerprint',
657
+ 'service_tier',
658
+ ];
659
+
660
+ export function capturePrivateModeSseChunkMetadata(metadata, payload) {
661
+ if (!isPlainObject(metadata) || !isPlainObject(payload)) return metadata;
662
+ for (const field of PRIVATE_MODE_SSE_CHUNK_METADATA_FIELDS) {
663
+ if (payload[field] !== undefined) metadata[field] = payload[field];
664
+ }
665
+ return metadata;
666
+ }
667
+
668
+ export function buildPrivateModeSseContentDelta(content, metadata = {}) {
669
+ return JSON.stringify({
670
+ ...metadata,
671
+ choices: [{ index: 0, delta: { content } }],
672
+ });
673
+ }
674
+
675
+ export function suppressPrivateModeReasoningFromResponsePayload(
676
+ payload,
677
+ stripContent = stripPrivateModeReasoningBlocks,
678
+ ) {
679
+ if (!isPlainObject(payload)) {
680
+ throw new TypeError('Private Mode response payload must be an object.');
681
+ }
682
+ const targets = [
683
+ payload,
684
+ ...(Array.isArray(payload.choices) ? payload.choices : []),
685
+ ];
686
+ for (const target of targets) {
687
+ if (!isPlainObject(target)) continue;
688
+ const nestedTargets = [target, target.message, target.delta].filter(isPlainObject);
689
+ for (const nested of nestedTargets) {
690
+ for (const field of ASSISTANT_REASONING_MESSAGE_FIELDS) delete nested[field];
691
+ delete nested.thinking;
692
+ for (const field of ['content', 'text', 'output_text']) {
693
+ if (typeof nested[field] === 'string') {
694
+ nested[field] = stripContent(nested[field]);
695
+ } else if (Array.isArray(nested[field])) {
696
+ nested[field] = nested[field].map((part) => (
697
+ isPlainObject(part) && part.type === 'text' && typeof part.text === 'string'
698
+ ? { ...part, text: stripContent(part.text) }
699
+ : part
700
+ ));
701
+ }
702
+ }
703
+ }
704
+ }
705
+ return payload;
706
+ }
707
+
708
+ export function suppressPrivateModeReasoningFromJsonText(value) {
709
+ const parsed = JSON.parse(String(value));
710
+ suppressPrivateModeReasoningFromResponsePayload(parsed);
711
+ return JSON.stringify(parsed);
712
+ }
713
+
714
+ export function suppressPrivateModeReasoningFromSseFrame(frame, contentSuppressor, chunkMetadata) {
715
+ const lines = frame.split('\n');
716
+ const dataLineIndexes = lines.flatMap((line, index) => line.startsWith('data:') ? [index] : []);
717
+ if (dataLineIndexes.length === 0) {
718
+ const isCommentOnlyFrame =
719
+ lines.some((line) => line.startsWith(':')) &&
720
+ lines.every((line) => line === '' || line.startsWith(':'));
721
+ if (isCommentOnlyFrame) return frame;
722
+
723
+ // A stream request can still receive an ordinary JSON (or otherwise
724
+ // non-SSE) body. Reasoning opt-outs are a privacy boundary, so never pass
725
+ // a frame through unless its data payload can be parsed and sanitized.
726
+ return '';
727
+ }
728
+
729
+ const payload = dataLineIndexes
730
+ .map((index) => lines[index].slice('data:'.length).trim())
731
+ .join('\n')
732
+ .trim();
733
+ if (!payload) return frame;
734
+
735
+ const replaceDataPayload = (replacement) => lines
736
+ .flatMap((line, index) => {
737
+ if (index === dataLineIndexes[0]) return [`data: ${replacement}`];
738
+ return dataLineIndexes.includes(index) ? [] : [line];
739
+ })
740
+ .join('\n');
741
+
742
+ if (payload === '[DONE]') {
743
+ const flushedContent = contentSuppressor.flush();
744
+ if (!flushedContent) return frame;
745
+ const finalDelta = buildPrivateModeSseContentDelta(flushedContent, chunkMetadata);
746
+ return `data: ${finalDelta}\n\n${replaceDataPayload('[DONE]')}`;
747
+ }
748
+
749
+ try {
750
+ const parsed = JSON.parse(payload);
751
+ capturePrivateModeSseChunkMetadata(chunkMetadata, parsed);
752
+ suppressPrivateModeReasoningFromResponsePayload(
753
+ parsed,
754
+ (content) => contentSuppressor.process(content),
755
+ );
756
+ const terminalChoice = Array.isArray(parsed.choices) && parsed.choices.find(
757
+ (choice) => choice && choice.finish_reason !== undefined && choice.finish_reason !== null,
758
+ );
759
+ if (!terminalChoice) return replaceDataPayload(JSON.stringify(parsed));
760
+
761
+ const flushedContent = contentSuppressor.flush();
762
+ if (!flushedContent) return replaceDataPayload(JSON.stringify(parsed));
763
+
764
+ let visibleContent = '';
765
+ if (isPlainObject(terminalChoice.delta) && typeof terminalChoice.delta.content === 'string') {
766
+ visibleContent = terminalChoice.delta.content;
767
+ terminalChoice.delta.content = '';
768
+ } else if (typeof terminalChoice.text === 'string') {
769
+ visibleContent = terminalChoice.text;
770
+ terminalChoice.text = '';
771
+ }
772
+ const finalDelta = buildPrivateModeSseContentDelta(
773
+ `${visibleContent}${flushedContent}`,
774
+ chunkMetadata,
775
+ );
776
+ return `data: ${finalDelta}\n\n${replaceDataPayload(JSON.stringify(parsed))}`;
777
+ } catch {
778
+ // Reasoning opt-outs are a privacy boundary. Never pass an unparseable
779
+ // upstream data frame through because it may contain unsanitized reasoning.
780
+ return '';
781
+ }
782
+ }
783
+
166
784
  function buildJsonSchemaPromptSuffix(responseFormat) {
167
785
  if (!isPlainObject(responseFormat?.json_schema)) return null;
168
786
  const schemaWrapper = responseFormat.json_schema;
@@ -240,12 +858,20 @@ function normalizeStreamOptions(body) {
240
858
  }
241
859
 
242
860
  export function applyPrivateModelRequestMutations(body, model) {
243
- stripPrivateModeReasoningFromMessages(body);
861
+ if (isPrivateModeKimiK3Model(model)) {
862
+ normalizePrivateModelReasoningControls(body, model);
863
+ normalizePrivateModeKimiK3ReasoningFromMessages(body);
864
+ normalizePrivateModeKimiK3DynamicToolMessages(body);
865
+ } else {
866
+ stripPrivateModeReasoningFromMessages(body);
867
+ }
244
868
  normalizeMaxTokenAliases(body);
245
-
246
869
  body.model = model.upstreamModel;
247
870
  normalizeStreamOptions(body);
248
871
  applyTinfoilCompatibilityMutations(body, model);
872
+ if (isPrivateModeKimiK3Model(model)) {
873
+ applyPrivateModeKimiK3RequestParams(body);
874
+ }
249
875
 
250
876
  if (model.thinkingMode === 'gemma') {
251
877
  body.chat_template_kwargs = {
@@ -254,7 +880,7 @@ export function applyPrivateModelRequestMutations(body, model) {
254
880
  };
255
881
  delete body.thinking;
256
882
  delete body.reasoning_effort;
257
- } else if (model.thinkingMode === 'kimi-k2.6' || model.thinkingMode === 'glm-5.2') {
883
+ } else if (model.thinkingMode === 'glm-5.2') {
258
884
  const thinkingEnabled = shouldEnableThinking(body, model);
259
885
  body.chat_template_kwargs = {
260
886
  ...mergeChatTemplateKwargs(body),
@@ -1,13 +1,15 @@
1
1
  const DEFAULT_SECURE_CLIENT_IDLE_RESET_MS = 5 * 60 * 1000;
2
+ const EHBP_RESPONSE_NONCE_HEADER = 'ehbp-response-nonce';
2
3
 
3
4
  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);
5
+ const configured = process.env.NANOGPT_PRIVATE_CLIENT_IDLE_RESET_MS?.trim();
6
+ const parsed = configured ? Number(configured) : Number.NaN;
7
+ if (Number.isFinite(parsed) && parsed >= 0) return Math.floor(parsed);
6
8
  return DEFAULT_SECURE_CLIENT_IDLE_RESET_MS;
7
9
  }
8
10
 
9
11
  export function createSecureState(apiBase, options = {}) {
10
- const idleResetMs = Number.isFinite(options.idleResetMs) && options.idleResetMs > 0
12
+ const idleResetMs = Number.isFinite(options.idleResetMs) && options.idleResetMs >= 0
11
13
  ? Math.floor(options.idleResetMs)
12
14
  : readSecureClientIdleResetMs();
13
15
  const loadTinfoil = options.loadTinfoil || (() => import('tinfoil'));
@@ -24,8 +26,10 @@ export function createSecureState(apiBase, options = {}) {
24
26
  const idleMs = clientState ? nowMs - clientState.lastUsedAtMs : 0;
25
27
  if (
26
28
  clientState?.userCacheSecret === userCacheSecret
27
- && idleMs >= 0
28
- && idleMs < idleResetMs
29
+ && (
30
+ idleResetMs === 0
31
+ || (idleMs >= 0 && idleMs < idleResetMs)
32
+ )
29
33
  ) {
30
34
  clientState.lastUsedAtMs = nowMs;
31
35
  return clientState.promise;
@@ -74,6 +78,11 @@ export function createSecureState(apiBase, options = {}) {
74
78
 
75
79
  return {
76
80
  getClient,
81
+ markClientUsed(client) {
82
+ if (!clientState || clientState.client !== client) return false;
83
+ clientState.lastUsedAtMs = now();
84
+ return true;
85
+ },
77
86
  invalidateClient(client) {
78
87
  if (!clientState) return false;
79
88
  if (client && clientState.client !== client) return false;
@@ -94,6 +103,10 @@ export function createSecureState(apiBase, options = {}) {
94
103
 
95
104
  export async function isMissingEncryptedBodyHeaderResponse(response) {
96
105
  if (response?.status !== 400) return false;
106
+ // Only NanoGPT's plaintext pre-dispatch rejection is safe to retry. An EHBP
107
+ // nonce means the response came back through the encrypted provider path, so
108
+ // the original request may already have been processed.
109
+ if (response.headers.has(EHBP_RESPONSE_NONCE_HEADER)) return false;
97
110
  try {
98
111
  const data = await response.clone().json();
99
112
  return data?.error?.code === 'missing_encrypted_body_header';
@@ -104,6 +117,7 @@ export async function isMissingEncryptedBodyHeaderResponse(response) {
104
117
 
105
118
  export async function fetchWithSecureClientRecovery({
106
119
  fetchWithClient,
120
+ onResponseClient,
107
121
  secureState,
108
122
  shouldInvalidateError = () => true,
109
123
  userCacheSecret,
@@ -118,6 +132,7 @@ export async function fetchWithSecureClientRecovery({
118
132
  secureState.invalidateClient(client);
119
133
  continue;
120
134
  }
135
+ onResponseClient?.(client);
121
136
  return response;
122
137
  }
123
138
  } catch (error) {
package/lib/server.js CHANGED
@@ -6,7 +6,15 @@ import {
6
6
  buildPrivateModeOriginPolicy,
7
7
  getCorsHeadersForRequest,
8
8
  } from './originPolicy.js';
9
- import { applyPrivateModelRequestMutations } from './requestTransforms.js';
9
+ import {
10
+ applyPrivateModelRequestMutations,
11
+ buildPrivateModeSseContentDelta,
12
+ createPrivateModeReasoningContentSuppressor,
13
+ shouldSuppressPrivateModelReasoning,
14
+ splitCompleteSseFrames,
15
+ suppressPrivateModeReasoningFromSseFrame,
16
+ suppressPrivateModeReasoningFromJsonText,
17
+ } from './requestTransforms.js';
10
18
  import {
11
19
  normalizePrivateModeUpstreamErrorMessage,
12
20
  readErrorMessage,
@@ -68,6 +76,7 @@ function openAIModelList() {
68
76
  object: 'model',
69
77
  created: model.created,
70
78
  owned_by: model.ownedBy,
79
+ ...(model.maxOutputTokens ? { max_output_tokens: model.maxOutputTokens } : {}),
71
80
  })),
72
81
  };
73
82
  }
@@ -264,6 +273,7 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
264
273
  return;
265
274
  }
266
275
 
276
+ const suppressReasoning = shouldSuppressPrivateModelReasoning(body, model);
267
277
  applyPrivateModelRequestMutations(body, model);
268
278
  const privateStreamRequested = body.stream === true;
269
279
 
@@ -281,6 +291,7 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
281
291
  }
282
292
 
283
293
  let response;
294
+ let responseClient;
284
295
  const upstreamAbortController = new AbortController();
285
296
  let responseComplete = false;
286
297
  res.on('close', () => {
@@ -307,6 +318,9 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
307
318
  body: privateRequestBody,
308
319
  signal: upstreamAbortController.signal,
309
320
  }),
321
+ onResponseClient: (client) => {
322
+ responseClient = client;
323
+ },
310
324
  });
311
325
  } catch (error) {
312
326
  responseComplete = true;
@@ -342,12 +356,19 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
342
356
  code: 'private_mode_upstream_failed',
343
357
  },
344
358
  }, errorHeaders);
359
+ secureState.markClientUsed(responseClient);
345
360
  return;
346
361
  }
347
362
 
348
363
  if (privateStreamRequested && response.body) {
349
364
  res.writeHead(response.status, headers);
350
365
  const reader = response.body.getReader();
366
+ const decoder = suppressReasoning ? new TextDecoder() : null;
367
+ const contentSuppressor = suppressReasoning
368
+ ? createPrivateModeReasoningContentSuppressor()
369
+ : null;
370
+ const sseChunkMetadata = {};
371
+ let sseBuffer = '';
351
372
  let streamFailed = false;
352
373
  try {
353
374
  while (true) {
@@ -358,7 +379,28 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
358
379
  upstreamAbortController.abort();
359
380
  return;
360
381
  }
361
- if (value) res.write(Buffer.from(value));
382
+ if (!value) continue;
383
+ if (!suppressReasoning || !decoder || !contentSuppressor) {
384
+ res.write(Buffer.from(value));
385
+ continue;
386
+ }
387
+ sseBuffer += decoder.decode(value, { stream: true });
388
+ const { frames, remainder } = splitCompleteSseFrames(sseBuffer);
389
+ sseBuffer = remainder;
390
+ for (const frame of frames) {
391
+ res.write(`${suppressPrivateModeReasoningFromSseFrame(frame, contentSuppressor, sseChunkMetadata)}\n\n`);
392
+ }
393
+ }
394
+ if (suppressReasoning && decoder && contentSuppressor) {
395
+ sseBuffer += decoder.decode();
396
+ if (sseBuffer) {
397
+ res.write(`${suppressPrivateModeReasoningFromSseFrame(sseBuffer, contentSuppressor, sseChunkMetadata)}\n\n`);
398
+ }
399
+ const flushedContent = contentSuppressor.flush();
400
+ if (flushedContent) {
401
+ const finalDelta = buildPrivateModeSseContentDelta(flushedContent, sseChunkMetadata);
402
+ res.write(`data: ${finalDelta}\n\n`);
403
+ }
362
404
  }
363
405
  } catch (error) {
364
406
  streamFailed = true;
@@ -369,12 +411,32 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
369
411
  return;
370
412
  } finally {
371
413
  responseComplete = true;
414
+ if (!streamFailed) secureState.markClientUsed(responseClient);
372
415
  if (!streamFailed && !res.writableEnded && !res.destroyed) res.end();
373
416
  }
374
417
  return;
375
418
  }
376
419
 
377
- const responseBody = Buffer.from(await response.arrayBuffer());
420
+ let responseBody = Buffer.from(await response.arrayBuffer());
421
+ if (suppressReasoning) {
422
+ try {
423
+ responseBody = Buffer.from(
424
+ suppressPrivateModeReasoningFromJsonText(responseBody.toString('utf8')),
425
+ );
426
+ } catch {
427
+ secureState.markClientUsed(responseClient);
428
+ responseComplete = true;
429
+ jsonResponse(res, 502, {
430
+ error: {
431
+ message: 'Private Mode upstream returned an invalid response.',
432
+ type: 'api_error',
433
+ code: 'private_mode_invalid_response',
434
+ },
435
+ }, corsHeaders);
436
+ return;
437
+ }
438
+ }
439
+ secureState.markClientUsed(responseClient);
378
440
  headers['content-length'] = String(responseBody.length);
379
441
  responseComplete = true;
380
442
  res.writeHead(response.status, headers);
@@ -1,8 +1,12 @@
1
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.
2
+ // lib/privateMode/tinfoilBrowserClient.ts. Only surface sanitized client-error
3
+ // messages from responses that completed the verified Private Mode transport.
4
4
  const MISSING_EHBP_RESPONSE_NONCE_PATTERN = /missing\s+ehbp-response-nonce\s+header/i;
5
+ const EHBP_RESPONSE_NONCE_HEADER = 'ehbp-response-nonce';
5
6
  const PRIVATE_MODE_REFUND_NOTICE = 'Any reserved balance will be released or refunded.';
7
+ const MAX_UPSTREAM_ERROR_BODY_BYTES = 16 * 1024;
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;
6
10
 
7
11
  export function privateModeProviderFailureMessage(status) {
8
12
  if (status === 429) {
@@ -19,12 +23,109 @@ export function normalizePrivateModeUpstreamErrorMessage(message, fallback) {
19
23
  return privateModeProviderFailureMessage();
20
24
  }
21
25
 
22
- export async function readErrorMessage(response, fallback) {
23
- void fallback;
26
+ function sanitizeUpstreamErrorMessage(value) {
27
+ if (typeof value !== 'string') return null;
28
+ const normalized = value
29
+ .replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, ' ')
30
+ .replace(/\s+/g, ' ')
31
+ .replace(HIDDEN_PROVIDER_NAME_PATTERN, 'upstream service')
32
+ .trim();
33
+ if (!normalized) return null;
34
+ const characters = Array.from(normalized);
35
+ if (characters.length <= MAX_UPSTREAM_ERROR_MESSAGE_CHARS) return normalized;
36
+ return `${characters.slice(0, MAX_UPSTREAM_ERROR_MESSAGE_CHARS - 1).join('').trimEnd()}…`;
37
+ }
38
+
39
+ async function discardResponseBody(response) {
24
40
  try {
25
41
  await response?.body?.cancel();
26
42
  } catch {
27
- // The body may already be locked or closed. Its contents remain discarded.
43
+ // The body may already be locked or closed.
44
+ }
45
+ }
46
+
47
+ async function readLimitedResponseText(response) {
48
+ const contentLength = Number(response?.headers?.get?.('content-length') || '');
49
+ if (Number.isFinite(contentLength) && contentLength > MAX_UPSTREAM_ERROR_BODY_BYTES) {
50
+ await discardResponseBody(response);
51
+ return null;
52
+ }
53
+
54
+ const reader = response?.body?.getReader?.();
55
+ if (!reader) return null;
56
+ const decoder = new TextDecoder();
57
+ let bytesRead = 0;
58
+ let text = '';
59
+ try {
60
+ while (true) {
61
+ const { done, value } = await reader.read();
62
+ if (done) break;
63
+ bytesRead += value?.byteLength || 0;
64
+ if (bytesRead > MAX_UPSTREAM_ERROR_BODY_BYTES) {
65
+ await reader.cancel().catch(() => undefined);
66
+ return null;
67
+ }
68
+ if (value) text += decoder.decode(value, { stream: true });
69
+ }
70
+ text += decoder.decode();
71
+ return text;
72
+ } catch {
73
+ await reader.cancel().catch(() => undefined);
74
+ return null;
75
+ } finally {
76
+ reader.releaseLock();
28
77
  }
29
- return privateModeProviderFailureMessage(response?.status);
78
+ }
79
+
80
+ function extractStructuredErrorMessage(data) {
81
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return null;
82
+ const nestedError = data.error && typeof data.error === 'object' && !Array.isArray(data.error)
83
+ ? data.error
84
+ : null;
85
+ const candidates = [
86
+ nestedError?.message,
87
+ typeof data.error === 'string' ? data.error : null,
88
+ data.message,
89
+ data.detail,
90
+ ];
91
+ for (const candidate of candidates) {
92
+ const sanitized = sanitizeUpstreamErrorMessage(candidate);
93
+ if (sanitized) return sanitized;
94
+ }
95
+ return null;
96
+ }
97
+
98
+ export async function readErrorMessage(response, fallback) {
99
+ void fallback;
100
+ const status = Number(response?.status);
101
+ const contentType = String(response?.headers?.get?.('content-type') || '').toLowerCase();
102
+ const mediaType = contentType.split(';', 1)[0]?.trim() || '';
103
+ const isJsonMediaType = mediaType === 'application/json' || mediaType.endsWith('+json');
104
+ const mayExposeClientError = (
105
+ response?.headers?.get?.('x-nanogpt-private-mode') === 'tinfoil'
106
+ && response?.headers?.has?.(EHBP_RESPONSE_NONCE_HEADER)
107
+ && (status === 400 || status === 422)
108
+ && (isJsonMediaType || mediaType === 'text/plain')
109
+ );
110
+
111
+ if (!mayExposeClientError) {
112
+ await discardResponseBody(response);
113
+ return privateModeProviderFailureMessage(status);
114
+ }
115
+
116
+ const body = await readLimitedResponseText(response);
117
+ let message = null;
118
+ if (body !== null && isJsonMediaType) {
119
+ try {
120
+ message = extractStructuredErrorMessage(JSON.parse(body));
121
+ } catch {
122
+ message = null;
123
+ }
124
+ } else if (body !== null) {
125
+ message = sanitizeUpstreamErrorMessage(body);
126
+ }
127
+
128
+ return message
129
+ ? `Private Mode request was rejected: ${message} — ${PRIVATE_MODE_REFUND_NOTICE}`
130
+ : privateModeProviderFailureMessage(status);
30
131
  }
@@ -1,14 +1,14 @@
1
1
  [
2
2
  {
3
- "id": "private/kimi-k2-6",
4
- "name": "Kimi K2.6 Private",
5
- "upstreamModel": "kimi-k2-6",
6
- "billingModel": "TEE/kimi-k2-6",
7
- "teeTargetModel": "kimi-k2-6",
8
- "thinkingMode": "kimi-k2.6",
9
- "created": 1764547200,
3
+ "id": "private/kimi-k3",
4
+ "name": "Kimi K3 Private",
5
+ "upstreamModel": "kimi-k3",
6
+ "billingModel": "TEE/kimi-k3",
7
+ "teeTargetModel": "kimi-k3",
8
+ "maxOutputTokens": 1048576,
9
+ "created": 1786147200,
10
10
  "ownedBy": "nanogpt-private-mode",
11
- "aliases": ["private/kimi-k2.6", "TEE/kimi-k2-6", "TEE/kimi-k2.6"]
11
+ "aliases": ["TEE/kimi-k3"]
12
12
  },
13
13
  {
14
14
  "id": "private/gpt-oss-120b",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanogpt/private-mode",
3
- "version": "0.2.2",
3
+ "version": "0.2.5",
4
4
  "description": "OpenAI-compatible localhost proxy for NanoGPT Private Mode.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -23,6 +23,9 @@
23
23
  "models",
24
24
  "README.md"
25
25
  ],
26
+ "scripts": {
27
+ "start": "node ./bin/nanogpt-private-mode.js"
28
+ },
26
29
  "dependencies": {
27
30
  "ai": "6.0.220",
28
31
  "tinfoil": "1.1.12"
@@ -30,8 +33,5 @@
30
33
  "engines": {
31
34
  "node": ">=22"
32
35
  },
33
- "license": "MIT",
34
- "scripts": {
35
- "start": "node ./bin/nanogpt-private-mode.js"
36
- }
37
- }
36
+ "license": "MIT"
37
+ }