@nanogpt/private-mode 0.2.9 → 0.2.11
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 +25 -7
- package/bin/nanogpt-private-mode.js +8 -0
- package/lib/catalogCompatibility.js +76 -0
- package/lib/constants.js +6 -0
- package/lib/packageInfo.js +8 -0
- package/lib/requestTransforms.js +117 -2
- package/lib/server.js +64 -53
- package/lib/sseResponsePump.js +163 -0
- package/models/private-tee.json +6 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ OpenAI-compatible localhost proxy for NanoGPT Private Mode with supported TEE mo
|
|
|
5
5
|
Requires Node.js 22 or later.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
NANOGPT_API_KEY=sk-your-key npx @nanogpt/private-mode
|
|
8
|
+
NANOGPT_API_KEY=sk-your-key npx --yes @nanogpt/private-mode@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Then point any OpenAI-compatible client at:
|
|
@@ -32,6 +32,14 @@ const response = await client.chat.completions.create({
|
|
|
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
|
+
The model list is bundled with the installed proxy package. Before the proxy starts listening, it checks that list against NanoGPT's hosted Private Mode catalog and stops with an upgrade command if models were added or retired. This prevents an old package from advertising enclave targets that are no longer available. The running proxy reports its exact version at `GET http://127.0.0.1:8787/v1/private-mode/status`. To print the latest published package version, run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx --yes @nanogpt/private-mode@latest --version
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If an older proxy is already running, stop it before starting the command above. API clients must use the localhost base URL shown below; sending `private/*` model IDs directly to NanoGPT's standard `/api/v1/chat/completions` endpoint does not enable Private Mode.
|
|
42
|
+
|
|
35
43
|
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
44
|
|
|
37
45
|
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.
|
|
@@ -44,6 +52,12 @@ In the hosted web app, decrypted Private Mode turns remain in local browser hist
|
|
|
44
52
|
|
|
45
53
|
Private Mode also requires enough NanoGPT balance and API-key spend-limit headroom before dispatch. For streaming calls, NanoGPT may precharge a conservative reserve before dispatch and refund unused balance after verified usage metadata is available.
|
|
46
54
|
|
|
55
|
+
Because the request body is encrypted before NanoGPT receives it, the service cannot inspect the request's `max_tokens` value during the balance precheck. The default streaming reserve therefore includes up to 32,768 output tokens, even when the encrypted request asks for fewer. This is a temporary reserve, not the final charge: unused balance is refunded when verified usage metadata arrives. The encrypted `max_tokens` value still controls generation inside the verified TEE.
|
|
56
|
+
|
|
57
|
+
Private Mode streaming has a 29-minute soft deadline so billing can settle before the route's 30-minute hard runtime limit. If the decrypted upstream stream ends without both a non-null `finish_reason` and the OpenAI `[DONE]` marker, the local proxy emits an SSE error with code `private_stream_incomplete`. Any text received before that error may be partial.
|
|
58
|
+
|
|
59
|
+
Private Mode currently supports Chat Completions only. The Responses API and Batch API are not available through the encrypted Private Mode endpoint. The standard Responses API outside Private Mode supports background requests within its configured 30-minute runtime limit; work that must reliably continue longer needs a durable job system.
|
|
60
|
+
|
|
47
61
|
Useful local checks:
|
|
48
62
|
|
|
49
63
|
```text
|
|
@@ -54,17 +68,21 @@ GET http://127.0.0.1:8787/v1/private-mode/attestation
|
|
|
54
68
|
|
|
55
69
|
Supported private model IDs include:
|
|
56
70
|
|
|
57
|
-
- `private/deepseek-v4-flash`
|
|
71
|
+
- `private/deepseek-v4-flash` - DeepSeek V4 Flash 0731
|
|
58
72
|
- `private/kimi-k3`
|
|
59
|
-
- `private/
|
|
60
|
-
- `private/
|
|
73
|
+
- `private/gpt-oss-120b`
|
|
74
|
+
- `private/llama3-3-70b`
|
|
61
75
|
- `private/glm-5-2`
|
|
62
76
|
- `private/glm-5-2:thinking`
|
|
77
|
+
- `private/gemma4-31b`
|
|
78
|
+
- `private/gemma4-31b:thinking`
|
|
79
|
+
|
|
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.
|
|
63
81
|
|
|
64
82
|
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:
|
|
65
83
|
|
|
66
84
|
```bash
|
|
67
|
-
NANOGPT_API_KEY=sk-your-key npx @nanogpt/private-mode --allow-origin http://localhost:3000
|
|
85
|
+
NANOGPT_API_KEY=sk-your-key npx --yes @nanogpt/private-mode@latest --allow-origin http://localhost:3000
|
|
68
86
|
```
|
|
69
87
|
|
|
70
88
|
You can also set `NANOGPT_PRIVATE_ALLOWED_ORIGINS=http://localhost:3000`. Wildcard browser origins are not supported.
|
|
@@ -86,13 +104,13 @@ NanoGPT's hosted web app shows a Private Mode receipt on completed Private Mode
|
|
|
86
104
|
To independently verify a copied receipt:
|
|
87
105
|
|
|
88
106
|
```bash
|
|
89
|
-
npx @nanogpt/private-mode verify receipt.json
|
|
107
|
+
npx --yes @nanogpt/private-mode@latest verify receipt.json
|
|
90
108
|
```
|
|
91
109
|
|
|
92
110
|
You can also pipe JSON on stdin:
|
|
93
111
|
|
|
94
112
|
```bash
|
|
95
|
-
cat receipt.json | npx @nanogpt/private-mode verify
|
|
113
|
+
cat receipt.json | npx --yes @nanogpt/private-mode@latest verify
|
|
96
114
|
```
|
|
97
115
|
|
|
98
116
|
The verifier re-fetches attestation material for the receipt's enclave, verifies it locally, and checks the resulting measurements, release digest, and HPKE key against the copied receipt. If an optional attestation bundle hash is present but the freshly fetched bundle serializes differently, the verifier prints a warning instead of failing the core verification.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
|
+
import { PRIVATE_MODE_PROXY_VERSION } from '../lib/packageInfo.js';
|
|
4
5
|
import { startPrivateModeProxy } from '../lib/server.js';
|
|
5
6
|
import { verifyPrivateModeReceiptCli } from '../lib/verifyReceipt.js';
|
|
6
7
|
|
|
@@ -14,10 +15,16 @@ const { values, positionals } = parseArgs({
|
|
|
14
15
|
},
|
|
15
16
|
'allow-origin': { type: 'string', multiple: true },
|
|
16
17
|
help: { type: 'boolean', short: 'h', default: false },
|
|
18
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
17
19
|
},
|
|
18
20
|
allowPositionals: true,
|
|
19
21
|
});
|
|
20
22
|
|
|
23
|
+
if (values.version) {
|
|
24
|
+
process.stdout.write(`${PRIVATE_MODE_PROXY_VERSION}\n`);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
if (values.help) {
|
|
22
29
|
process.stdout.write(`NanoGPT Private Mode Proxy
|
|
23
30
|
|
|
@@ -32,6 +39,7 @@ Options:
|
|
|
32
39
|
--api-base <url> NanoGPT API base. Default: https://nano-gpt.com
|
|
33
40
|
--allow-origin <origin>
|
|
34
41
|
Browser origin allowed to call the proxy, for example http://localhost:3000
|
|
42
|
+
--version, -v Print the installed proxy version
|
|
35
43
|
`);
|
|
36
44
|
process.exit(0);
|
|
37
45
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const DEFAULT_CATALOG_CHECK_TIMEOUT_MS = 10_000;
|
|
2
|
+
|
|
3
|
+
function normalizeModelIds(models) {
|
|
4
|
+
if (!Array.isArray(models)) return null;
|
|
5
|
+
|
|
6
|
+
const ids = models.map((model) => (
|
|
7
|
+
typeof model?.id === 'string' ? model.id.trim().toLowerCase() : ''
|
|
8
|
+
));
|
|
9
|
+
if (ids.some((id) => !id)) return null;
|
|
10
|
+
|
|
11
|
+
return new Set(ids);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function comparePrivateModeCatalogs(localModels, hostedCatalog) {
|
|
15
|
+
const localIds = normalizeModelIds(localModels);
|
|
16
|
+
const hostedIds = normalizeModelIds(hostedCatalog?.data);
|
|
17
|
+
if (!localIds || !hostedIds) {
|
|
18
|
+
throw new Error('Private Mode model catalog response was malformed.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
status: 'checked',
|
|
23
|
+
retiredLocalModels: [...localIds].filter((id) => !hostedIds.has(id)).sort(),
|
|
24
|
+
missingLocalModels: [...hostedIds].filter((id) => !localIds.has(id)).sort(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function checkHostedPrivateModeCatalog({
|
|
29
|
+
apiBase,
|
|
30
|
+
localModels,
|
|
31
|
+
fetchImpl = fetch,
|
|
32
|
+
timeoutMs = DEFAULT_CATALOG_CHECK_TIMEOUT_MS,
|
|
33
|
+
}) {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetchImpl(`${apiBase}/api/v1/private/tinfoil/models`, {
|
|
39
|
+
headers: { accept: 'application/json' },
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
return { status: 'unavailable', reason: `HTTP ${response.status}` };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return comparePrivateModeCatalogs(localModels, await response.json());
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
status: 'unavailable',
|
|
50
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
51
|
+
};
|
|
52
|
+
} finally {
|
|
53
|
+
clearTimeout(timeout);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function hasPrivateModeCatalogMismatch(result) {
|
|
58
|
+
return result.status === 'checked'
|
|
59
|
+
&& (result.retiredLocalModels.length > 0 || result.missingLocalModels.length > 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildPrivateModeCatalogUnavailableMessage(result, apiBase) {
|
|
63
|
+
return `Could not verify the bundled Private Mode model catalog against ${apiBase}: ${result.reason}. The proxy did not start because catalog compatibility could not be confirmed. Check your network connection and NanoGPT service availability, then retry.`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildPrivateModeCatalogMismatchMessage(result, proxyVersion) {
|
|
67
|
+
const details = [];
|
|
68
|
+
if (result.retiredLocalModels.length > 0) {
|
|
69
|
+
details.push(`retired local models: ${result.retiredLocalModels.join(', ')}`);
|
|
70
|
+
}
|
|
71
|
+
if (result.missingLocalModels.length > 0) {
|
|
72
|
+
details.push(`hosted models missing locally: ${result.missingLocalModels.join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `The bundled Private Mode model catalog in @nanogpt/private-mode v${proxyVersion} is out of date (${details.join('; ')}). Stop this proxy and restart with: NANOGPT_API_KEY=... npx --yes @nanogpt/private-mode@latest`;
|
|
76
|
+
}
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const PRIVATE_MODE_STREAM_ROUTE_MAX_DURATION_MS = 30 * 60 * 1000;
|
|
2
|
+
export const PRIVATE_MODE_STREAM_SOFT_DEADLINE_LEAD_MS = 60 * 1000;
|
|
3
|
+
export const PRIVATE_MODE_STREAM_SETTLEMENT_SAFETY_MS = 30 * 1000;
|
|
4
|
+
export const PRIVATE_MODE_STREAM_SOFT_DEADLINE_MS =
|
|
5
|
+
PRIVATE_MODE_STREAM_ROUTE_MAX_DURATION_MS - PRIVATE_MODE_STREAM_SOFT_DEADLINE_LEAD_MS;
|
|
6
|
+
export const PRIVATE_MODE_DEFAULT_RESERVE_MAX_OUTPUT_TOKENS = 32_768;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const packageJson = JSON.parse(
|
|
4
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
export const PRIVATE_MODE_PROXY_VERSION = String(packageJson.version);
|
|
8
|
+
export const PRIVATE_MODE_PROXY_VERSION_HEADER = 'x-nanogpt-private-proxy-version';
|
package/lib/requestTransforms.js
CHANGED
|
@@ -679,13 +679,128 @@ export function createPrivateModeReasoningContentSuppressor() {
|
|
|
679
679
|
}
|
|
680
680
|
|
|
681
681
|
export function splitCompleteSseFrames(buffer) {
|
|
682
|
-
const parts = buffer.split(
|
|
682
|
+
const parts = buffer.split(/(?:\r\n|\r|\n){2}/);
|
|
683
683
|
return {
|
|
684
|
-
frames: parts.slice(0, -1).map((frame) => frame.replace(/\r\n/g, '\n')),
|
|
684
|
+
frames: parts.slice(0, -1).map((frame) => frame.replace(/\r\n|\r/g, '\n')),
|
|
685
685
|
remainder: parts.at(-1) || '',
|
|
686
686
|
};
|
|
687
687
|
}
|
|
688
688
|
|
|
689
|
+
export function createPrivateModeSseTerminalState(expectedChoiceCount = 1) {
|
|
690
|
+
return {
|
|
691
|
+
expectedChoiceCount: Number.isSafeInteger(expectedChoiceCount) && expectedChoiceCount > 0
|
|
692
|
+
? expectedChoiceCount
|
|
693
|
+
: 1,
|
|
694
|
+
finishedChoiceIndexes: new Set(),
|
|
695
|
+
sawDone: false,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function readPrivateModeSseDataPayload(frame) {
|
|
700
|
+
if (typeof frame !== 'string') return '';
|
|
701
|
+
return frame
|
|
702
|
+
.split('\n')
|
|
703
|
+
.filter((line) => line.startsWith('data:'))
|
|
704
|
+
.map((line) => line.slice('data:'.length).trim())
|
|
705
|
+
.join('\n')
|
|
706
|
+
.trim();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export function observePrivateModeSseTerminalFrame(state, frame) {
|
|
710
|
+
if (!isPlainObject(state) || typeof frame !== 'string') return state;
|
|
711
|
+
const payload = readPrivateModeSseDataPayload(frame);
|
|
712
|
+
if (!payload) return state;
|
|
713
|
+
if (payload === '[DONE]') {
|
|
714
|
+
state.sawDone = true;
|
|
715
|
+
return state;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
const parsed = JSON.parse(payload);
|
|
720
|
+
if (isPlainObject(parsed) && Array.isArray(parsed.choices)) {
|
|
721
|
+
for (const choice of parsed.choices) {
|
|
722
|
+
if (
|
|
723
|
+
!isPlainObject(choice) ||
|
|
724
|
+
choice.finish_reason === undefined ||
|
|
725
|
+
choice.finish_reason === null
|
|
726
|
+
) continue;
|
|
727
|
+
const choiceIndex = Number.isSafeInteger(choice.index)
|
|
728
|
+
? choice.index
|
|
729
|
+
: state.expectedChoiceCount === 1
|
|
730
|
+
? 0
|
|
731
|
+
: null;
|
|
732
|
+
if (choiceIndex !== null && choiceIndex >= 0 && choiceIndex < state.expectedChoiceCount) {
|
|
733
|
+
state.finishedChoiceIndexes.add(choiceIndex);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
} catch {
|
|
738
|
+
// Other SSE payloads do not contribute to the OpenAI terminal sequence.
|
|
739
|
+
}
|
|
740
|
+
return state;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export function isPrivateModeSseTerminalStateComplete(state) {
|
|
744
|
+
return state?.sawDone === true &&
|
|
745
|
+
state?.finishedChoiceIndexes instanceof Set &&
|
|
746
|
+
state.finishedChoiceIndexes.size === state.expectedChoiceCount;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export function createPrivateModeSseTerminalGuard(expectedChoiceCount = 1) {
|
|
750
|
+
const state = createPrivateModeSseTerminalState(expectedChoiceCount);
|
|
751
|
+
let emittedDone = false;
|
|
752
|
+
return {
|
|
753
|
+
state,
|
|
754
|
+
observe(frame, forwardedFrame = frame) {
|
|
755
|
+
const doneAlreadyEmitted = emittedDone;
|
|
756
|
+
const containsDone = readPrivateModeSseDataPayload(frame) === '[DONE]';
|
|
757
|
+
observePrivateModeSseTerminalFrame(state, frame);
|
|
758
|
+
// The pump drains transport bytes after client-visible completion so the
|
|
759
|
+
// hosted request can finish settlement. Nothing from that drain belongs
|
|
760
|
+
// on the client side of the canonical terminal marker.
|
|
761
|
+
if (doneAlreadyEmitted) return '';
|
|
762
|
+
// Drop every raw DONE marker so duplicates collapse to one canonical
|
|
763
|
+
// marker. Reasoning suppression may prepend a synthetic visible-content
|
|
764
|
+
// delta, which must remain ahead of that marker.
|
|
765
|
+
const forwardedWithoutDone = containsDone
|
|
766
|
+
? forwardedFrame
|
|
767
|
+
.split(/(?:\r\n|\r|\n){2}/)
|
|
768
|
+
.filter((forwarded) => readPrivateModeSseDataPayload(forwarded) !== '[DONE]')
|
|
769
|
+
.join('\n\n')
|
|
770
|
+
: forwardedFrame;
|
|
771
|
+
|
|
772
|
+
// A hosted stream can keep transport EOF open while billing settles.
|
|
773
|
+
// Release successful completion as soon as the full SSE terminal
|
|
774
|
+
// sequence is observed, then let the response pump drain the transport.
|
|
775
|
+
if (!emittedDone && isPrivateModeSseTerminalStateComplete(state)) {
|
|
776
|
+
emittedDone = true;
|
|
777
|
+
return [forwardedWithoutDone, 'data: [DONE]'].filter(Boolean).join('\n\n');
|
|
778
|
+
}
|
|
779
|
+
return forwardedWithoutDone;
|
|
780
|
+
},
|
|
781
|
+
hasEmittedDone() {
|
|
782
|
+
return emittedDone;
|
|
783
|
+
},
|
|
784
|
+
finish() {
|
|
785
|
+
if (isPrivateModeSseTerminalStateComplete(state)) {
|
|
786
|
+
if (emittedDone) return { complete: true, frame: '' };
|
|
787
|
+
emittedDone = true;
|
|
788
|
+
return { complete: true, frame: 'data: [DONE]' };
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
complete: false,
|
|
792
|
+
frame: `data: ${JSON.stringify({
|
|
793
|
+
error: {
|
|
794
|
+
message: 'Private Mode stream ended before a complete terminal sequence. The response may be partial; retry the request.',
|
|
795
|
+
type: 'api_error',
|
|
796
|
+
code: 'private_stream_incomplete',
|
|
797
|
+
},
|
|
798
|
+
})}`,
|
|
799
|
+
};
|
|
800
|
+
},
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
689
804
|
const PRIVATE_MODE_SSE_CHUNK_METADATA_FIELDS = [
|
|
690
805
|
'id',
|
|
691
806
|
'object',
|
package/lib/server.js
CHANGED
|
@@ -2,23 +2,33 @@ import { createServer } from 'node:http';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
|
|
4
4
|
import { buildPrivateModeCacheScopeProof } from './cacheScope.js';
|
|
5
|
+
import {
|
|
6
|
+
buildPrivateModeCatalogMismatchMessage,
|
|
7
|
+
buildPrivateModeCatalogUnavailableMessage,
|
|
8
|
+
checkHostedPrivateModeCatalog,
|
|
9
|
+
hasPrivateModeCatalogMismatch,
|
|
10
|
+
} from './catalogCompatibility.js';
|
|
11
|
+
import {
|
|
12
|
+
PRIVATE_MODE_DEFAULT_RESERVE_MAX_OUTPUT_TOKENS,
|
|
13
|
+
PRIVATE_MODE_STREAM_SOFT_DEADLINE_MS,
|
|
14
|
+
} from './constants.js';
|
|
5
15
|
import {
|
|
6
16
|
buildPrivateModeOriginPolicy,
|
|
7
17
|
getCorsHeadersForRequest,
|
|
8
18
|
} from './originPolicy.js';
|
|
9
19
|
import {
|
|
10
20
|
applyPrivateModelRequestMutations,
|
|
11
|
-
buildPrivateModeSseContentDelta,
|
|
12
|
-
createPrivateModeReasoningContentSuppressor,
|
|
13
21
|
shouldSuppressPrivateModelReasoning,
|
|
14
|
-
splitCompleteSseFrames,
|
|
15
|
-
suppressPrivateModeReasoningFromSseFrame,
|
|
16
22
|
suppressPrivateModeReasoningFromJsonText,
|
|
17
23
|
} from './requestTransforms.js';
|
|
18
24
|
import {
|
|
19
25
|
normalizePrivateModeUpstreamErrorMessage,
|
|
20
26
|
readErrorMessage,
|
|
21
27
|
} from './serverErrorNormalization.js';
|
|
28
|
+
import {
|
|
29
|
+
PRIVATE_MODE_PROXY_VERSION,
|
|
30
|
+
PRIVATE_MODE_PROXY_VERSION_HEADER,
|
|
31
|
+
} from './packageInfo.js';
|
|
22
32
|
import {
|
|
23
33
|
createSecureState,
|
|
24
34
|
fetchWithSecureClientRecovery,
|
|
@@ -27,6 +37,10 @@ import {
|
|
|
27
37
|
buildPrivateModeStatusContract,
|
|
28
38
|
buildPublicAttestationSummary,
|
|
29
39
|
} from './statusContract.js';
|
|
40
|
+
import {
|
|
41
|
+
isPrivateModeSseResponse,
|
|
42
|
+
pipePrivateModeSseResponse,
|
|
43
|
+
} from './sseResponsePump.js';
|
|
30
44
|
|
|
31
45
|
const MODELS = JSON.parse(
|
|
32
46
|
readFileSync(new URL('../models/private-tee.json', import.meta.url), 'utf8'),
|
|
@@ -79,16 +93,18 @@ function openAIModelList() {
|
|
|
79
93
|
object: 'model',
|
|
80
94
|
created: model.created,
|
|
81
95
|
owned_by: model.ownedBy,
|
|
96
|
+
...(model.maxInputTokens ? { context_length: model.maxInputTokens } : {}),
|
|
82
97
|
...(model.maxOutputTokens ? { max_output_tokens: model.maxOutputTokens } : {}),
|
|
83
98
|
})),
|
|
84
99
|
};
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
export function privateModeStatus(apiBase, secureState, localBase, originPolicy) {
|
|
102
|
+
export function privateModeStatus(apiBase, secureState, localBase, originPolicy, catalogCompatibility) {
|
|
88
103
|
const verificationState = secureState.getVerificationState();
|
|
89
104
|
return {
|
|
90
105
|
ok: true,
|
|
91
106
|
mode: 'private_tee',
|
|
107
|
+
proxy_version: PRIVATE_MODE_PROXY_VERSION,
|
|
92
108
|
apiBase,
|
|
93
109
|
local_base_url: localBase,
|
|
94
110
|
models_path: '/v1/models',
|
|
@@ -101,11 +117,18 @@ export function privateModeStatus(apiBase, secureState, localBase, originPolicy)
|
|
|
101
117
|
response_body_encrypted: true,
|
|
102
118
|
streaming: true,
|
|
103
119
|
streaming_billing: 'precharged_reserve_with_verified_usage_refund',
|
|
120
|
+
// This deadline is enforced by the hosted route, not this process. A
|
|
121
|
+
// local environment override cannot configure the remote service.
|
|
122
|
+
streaming_soft_deadline_ms: PRIVATE_MODE_STREAM_SOFT_DEADLINE_MS,
|
|
123
|
+
default_stream_reserve_max_output_tokens: PRIVATE_MODE_DEFAULT_RESERVE_MAX_OUTPUT_TOKENS,
|
|
124
|
+
responses_api_supported: false,
|
|
125
|
+
batch_api_supported: false,
|
|
104
126
|
api_local_proxy_required: true,
|
|
105
127
|
browser_frontend_local_proxy_required: false,
|
|
106
128
|
...buildPrivateModeStatusContract(verificationState),
|
|
107
129
|
browser_origins_allowed: originPolicy.allowedOrigins,
|
|
108
130
|
models: openAIModelList().data,
|
|
131
|
+
catalog_compatibility: catalogCompatibility,
|
|
109
132
|
attestation: buildPublicAttestationSummary(verificationState),
|
|
110
133
|
};
|
|
111
134
|
}
|
|
@@ -178,6 +201,7 @@ async function runPreflight({ apiBase, apiKey, model, req, requestBodyBytes }) {
|
|
|
178
201
|
headers: {
|
|
179
202
|
authorization: `Bearer ${apiKey}`,
|
|
180
203
|
'content-type': 'application/json',
|
|
204
|
+
[PRIVATE_MODE_PROXY_VERSION_HEADER]: PRIVATE_MODE_PROXY_VERSION,
|
|
181
205
|
...copyLocalHeaders(req),
|
|
182
206
|
},
|
|
183
207
|
body: JSON.stringify(preflightBody),
|
|
@@ -269,7 +293,7 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
|
|
|
269
293
|
if (!model) {
|
|
270
294
|
jsonResponse(res, 400, {
|
|
271
295
|
error: {
|
|
272
|
-
message: `Unsupported private model "${body.model || ''}"
|
|
296
|
+
message: `Unsupported private model "${body.model || ''}" in @nanogpt/private-mode v${PRIVATE_MODE_PROXY_VERSION}. Check this proxy's /v1/models endpoint or restart with: NANOGPT_API_KEY=... npx --yes @nanogpt/private-mode@latest`,
|
|
273
297
|
type: 'invalid_request_error',
|
|
274
298
|
code: 'model_not_supported',
|
|
275
299
|
},
|
|
@@ -280,6 +304,7 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
|
|
|
280
304
|
const suppressReasoning = shouldSuppressPrivateModelReasoning(body, model);
|
|
281
305
|
applyPrivateModelRequestMutations(body, model);
|
|
282
306
|
const privateStreamRequested = body.stream === true;
|
|
307
|
+
const expectedChoiceCount = Number.isSafeInteger(body.n) && body.n > 0 ? body.n : 1;
|
|
283
308
|
|
|
284
309
|
const privateRequestBody = JSON.stringify(body);
|
|
285
310
|
const preflight = await runPreflight({
|
|
@@ -316,6 +341,7 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
|
|
|
316
341
|
'x-nanogpt-private-model': model.id,
|
|
317
342
|
'x-nanogpt-private-stream': privateStreamRequested ? 'true' : 'false',
|
|
318
343
|
'x-nanogpt-private-cache-scope': buildPrivateModeCacheScopeProof(preflight.cacheScope),
|
|
344
|
+
[PRIVATE_MODE_PROXY_VERSION_HEADER]: PRIVATE_MODE_PROXY_VERSION,
|
|
319
345
|
'x-query-source': 'api',
|
|
320
346
|
...copyLocalHeaders(req),
|
|
321
347
|
},
|
|
@@ -364,59 +390,30 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
|
|
|
364
390
|
return;
|
|
365
391
|
}
|
|
366
392
|
|
|
367
|
-
if (privateStreamRequested
|
|
393
|
+
if (isPrivateModeSseResponse(privateStreamRequested, response)) {
|
|
368
394
|
res.writeHead(response.status, headers);
|
|
369
|
-
const reader = response.body.getReader();
|
|
370
|
-
const decoder = suppressReasoning ? new TextDecoder() : null;
|
|
371
|
-
const contentSuppressor = suppressReasoning
|
|
372
|
-
? createPrivateModeReasoningContentSuppressor()
|
|
373
|
-
: null;
|
|
374
|
-
const sseChunkMetadata = {};
|
|
375
|
-
let sseBuffer = '';
|
|
376
|
-
let streamFailed = false;
|
|
377
395
|
try {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
sseBuffer += decoder.decode(value, { stream: true });
|
|
392
|
-
const { frames, remainder } = splitCompleteSseFrames(sseBuffer);
|
|
393
|
-
sseBuffer = remainder;
|
|
394
|
-
for (const frame of frames) {
|
|
395
|
-
res.write(`${suppressPrivateModeReasoningFromSseFrame(frame, contentSuppressor, sseChunkMetadata)}\n\n`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
if (suppressReasoning && decoder && contentSuppressor) {
|
|
399
|
-
sseBuffer += decoder.decode();
|
|
400
|
-
if (sseBuffer) {
|
|
401
|
-
res.write(`${suppressPrivateModeReasoningFromSseFrame(sseBuffer, contentSuppressor, sseChunkMetadata)}\n\n`);
|
|
402
|
-
}
|
|
403
|
-
const flushedContent = contentSuppressor.flush();
|
|
404
|
-
if (flushedContent) {
|
|
405
|
-
const finalDelta = buildPrivateModeSseContentDelta(flushedContent, sseChunkMetadata);
|
|
406
|
-
res.write(`data: ${finalDelta}\n\n`);
|
|
407
|
-
}
|
|
396
|
+
const outcome = await pipePrivateModeSseResponse({
|
|
397
|
+
body: response.body,
|
|
398
|
+
res,
|
|
399
|
+
suppressReasoning,
|
|
400
|
+
upstreamAbortController,
|
|
401
|
+
expectedChoiceCount,
|
|
402
|
+
});
|
|
403
|
+
// Successful completion and downstream disconnect still count as
|
|
404
|
+
// activity. Truncated or transport-failed streams keep the prior failure
|
|
405
|
+
// behavior and do not refresh the secure-client lifecycle.
|
|
406
|
+
if (!outcome.transportFailed && !outcome.streamFailed) {
|
|
407
|
+
secureState.markClientUsed(responseClient);
|
|
408
408
|
}
|
|
409
409
|
} catch (error) {
|
|
410
|
-
streamFailed = true;
|
|
411
410
|
upstreamAbortController.abort();
|
|
412
411
|
if (!res.destroyed) {
|
|
413
|
-
|
|
412
|
+
process.stderr.write(`Private Mode stream pump failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
414
413
|
}
|
|
415
|
-
return;
|
|
416
414
|
} finally {
|
|
417
415
|
responseComplete = true;
|
|
418
|
-
if (!
|
|
419
|
-
if (!streamFailed && !res.writableEnded && !res.destroyed) res.end();
|
|
416
|
+
if (!res.writableEnded && !res.destroyed) res.end();
|
|
420
417
|
}
|
|
421
418
|
return;
|
|
422
419
|
}
|
|
@@ -472,6 +469,19 @@ function handleOptions(req, res, corsHeaders) {
|
|
|
472
469
|
|
|
473
470
|
export async function startPrivateModeProxy(options) {
|
|
474
471
|
const apiBase = normalizeApiBase(options.apiBase);
|
|
472
|
+
const catalogCompatibility = await checkHostedPrivateModeCatalog({
|
|
473
|
+
apiBase,
|
|
474
|
+
localModels: MODELS,
|
|
475
|
+
});
|
|
476
|
+
if (catalogCompatibility.status === 'unavailable') {
|
|
477
|
+
throw new Error(buildPrivateModeCatalogUnavailableMessage(catalogCompatibility, apiBase));
|
|
478
|
+
}
|
|
479
|
+
if (hasPrivateModeCatalogMismatch(catalogCompatibility)) {
|
|
480
|
+
throw new Error(buildPrivateModeCatalogMismatchMessage(
|
|
481
|
+
catalogCompatibility,
|
|
482
|
+
PRIVATE_MODE_PROXY_VERSION,
|
|
483
|
+
));
|
|
484
|
+
}
|
|
475
485
|
const secureState = createSecureState(apiBase);
|
|
476
486
|
const localBase = `http://${options.host}:${options.port}/v1`;
|
|
477
487
|
const originPolicy = buildPrivateModeOriginPolicy(options);
|
|
@@ -497,7 +507,7 @@ export async function startPrivateModeProxy(options) {
|
|
|
497
507
|
}
|
|
498
508
|
|
|
499
509
|
if (req.method === 'GET' && url.pathname === '/health') {
|
|
500
|
-
jsonResponse(res, 200, privateModeStatus(apiBase, secureState, localBase, originPolicy), corsHeaders);
|
|
510
|
+
jsonResponse(res, 200, privateModeStatus(apiBase, secureState, localBase, originPolicy, catalogCompatibility), corsHeaders);
|
|
501
511
|
return;
|
|
502
512
|
}
|
|
503
513
|
|
|
@@ -507,7 +517,7 @@ export async function startPrivateModeProxy(options) {
|
|
|
507
517
|
}
|
|
508
518
|
|
|
509
519
|
if (req.method === 'GET' && url.pathname === '/v1/private-mode/status') {
|
|
510
|
-
jsonResponse(res, 200, privateModeStatus(apiBase, secureState, localBase, originPolicy), corsHeaders);
|
|
520
|
+
jsonResponse(res, 200, privateModeStatus(apiBase, secureState, localBase, originPolicy, catalogCompatibility), corsHeaders);
|
|
511
521
|
return;
|
|
512
522
|
}
|
|
513
523
|
|
|
@@ -543,7 +553,7 @@ export async function startPrivateModeProxy(options) {
|
|
|
543
553
|
}
|
|
544
554
|
|
|
545
555
|
if (req.method === 'GET' && url.pathname === '/') {
|
|
546
|
-
textResponse(res, 200, `NanoGPT Private Mode Proxy\nOpenAI base URL: ${localBase}\nStatus: /v1/private-mode/status\n`, corsHeaders);
|
|
556
|
+
textResponse(res, 200, `NanoGPT Private Mode Proxy v${PRIVATE_MODE_PROXY_VERSION}\nOpenAI base URL: ${localBase}\nStatus: /v1/private-mode/status\n`, corsHeaders);
|
|
547
557
|
return;
|
|
548
558
|
}
|
|
549
559
|
|
|
@@ -571,6 +581,7 @@ export async function startPrivateModeProxy(options) {
|
|
|
571
581
|
|
|
572
582
|
if (options.quiet !== true) {
|
|
573
583
|
process.stdout.write(`NanoGPT Private Mode Proxy
|
|
584
|
+
Version: ${PRIVATE_MODE_PROXY_VERSION}
|
|
574
585
|
Local base URL: ${localBase}
|
|
575
586
|
NanoGPT API: ${apiBase}
|
|
576
587
|
Models: ${MODELS.map((model) => model.id).join(', ')}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildPrivateModeSseContentDelta,
|
|
3
|
+
createPrivateModeReasoningContentSuppressor,
|
|
4
|
+
createPrivateModeSseTerminalGuard,
|
|
5
|
+
splitCompleteSseFrames,
|
|
6
|
+
suppressPrivateModeReasoningFromSseFrame,
|
|
7
|
+
} from './requestTransforms.js';
|
|
8
|
+
|
|
9
|
+
const MAX_SSE_FRAME_BYTES = 2 * 1024 * 1024;
|
|
10
|
+
const DEFAULT_DOWNSTREAM_DRAIN_TIMEOUT_MS = 60 * 1000;
|
|
11
|
+
|
|
12
|
+
function assertSseFrameWithinLimit(frame) {
|
|
13
|
+
if (Buffer.byteLength(frame, 'utf8') > MAX_SSE_FRAME_BYTES) {
|
|
14
|
+
throw new Error('Private Mode upstream SSE frame exceeded the proxy buffer limit.');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isPrivateModeSseResponse(streamRequested, response) {
|
|
19
|
+
const contentType = response?.headers?.get?.('content-type') || '';
|
|
20
|
+
return streamRequested === true
|
|
21
|
+
&& Boolean(response?.body)
|
|
22
|
+
&& contentType.toLowerCase().includes('text/event-stream');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeWithBackpressure(res, chunk, timeoutMs) {
|
|
26
|
+
if (res.destroyed || res.writableEnded) return Promise.resolve(false);
|
|
27
|
+
try {
|
|
28
|
+
if (res.write(chunk)) return Promise.resolve(true);
|
|
29
|
+
} catch {
|
|
30
|
+
return Promise.resolve(false);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
let drainTimer;
|
|
35
|
+
const cleanup = () => {
|
|
36
|
+
if (drainTimer) clearTimeout(drainTimer);
|
|
37
|
+
res.off('drain', onDrain);
|
|
38
|
+
res.off('close', onClose);
|
|
39
|
+
res.off('error', onError);
|
|
40
|
+
};
|
|
41
|
+
const settle = (writable) => {
|
|
42
|
+
cleanup();
|
|
43
|
+
resolve(writable);
|
|
44
|
+
};
|
|
45
|
+
const onDrain = () => settle(!res.destroyed && !res.writableEnded);
|
|
46
|
+
const onClose = () => settle(false);
|
|
47
|
+
const onError = () => settle(false);
|
|
48
|
+
res.once('drain', onDrain);
|
|
49
|
+
res.once('close', onClose);
|
|
50
|
+
res.once('error', onError);
|
|
51
|
+
drainTimer = setTimeout(() => settle(false), timeoutMs);
|
|
52
|
+
drainTimer.unref?.();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function pipePrivateModeSseResponse({
|
|
57
|
+
body,
|
|
58
|
+
res,
|
|
59
|
+
suppressReasoning,
|
|
60
|
+
upstreamAbortController,
|
|
61
|
+
expectedChoiceCount = 1,
|
|
62
|
+
downstreamDrainTimeoutMs = DEFAULT_DOWNSTREAM_DRAIN_TIMEOUT_MS,
|
|
63
|
+
}) {
|
|
64
|
+
let reader;
|
|
65
|
+
const decoder = new TextDecoder();
|
|
66
|
+
const contentSuppressor = suppressReasoning
|
|
67
|
+
? createPrivateModeReasoningContentSuppressor()
|
|
68
|
+
: null;
|
|
69
|
+
const terminalGuard = createPrivateModeSseTerminalGuard(expectedChoiceCount);
|
|
70
|
+
const sseChunkMetadata = {};
|
|
71
|
+
let sseBuffer = '';
|
|
72
|
+
let transportFailed = false;
|
|
73
|
+
|
|
74
|
+
const writeFrame = async (frame) => {
|
|
75
|
+
if (!frame) return true;
|
|
76
|
+
return writeWithBackpressure(res, `${frame}\n\n`, downstreamDrainTimeoutMs);
|
|
77
|
+
};
|
|
78
|
+
const forwardFrame = async (frame) => {
|
|
79
|
+
const forwarded = suppressReasoning && contentSuppressor
|
|
80
|
+
? suppressPrivateModeReasoningFromSseFrame(frame, contentSuppressor, sseChunkMetadata)
|
|
81
|
+
: frame;
|
|
82
|
+
return writeFrame(terminalGuard.observe(frame, forwarded));
|
|
83
|
+
};
|
|
84
|
+
const finishStream = async () => {
|
|
85
|
+
// A complete terminal sequence may be released before transport EOF.
|
|
86
|
+
// Never append a defensive suppressor flush after that marker.
|
|
87
|
+
if (contentSuppressor && !terminalGuard.hasEmittedDone()) {
|
|
88
|
+
const flushedContent = contentSuppressor.flush();
|
|
89
|
+
if (flushedContent) {
|
|
90
|
+
const delta = buildPrivateModeSseContentDelta(flushedContent, sseChunkMetadata);
|
|
91
|
+
if (!await writeFrame(`data: ${delta}`)) return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const terminal = terminalGuard.finish();
|
|
95
|
+
if (!await writeFrame(terminal.frame)) return null;
|
|
96
|
+
return terminal;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
reader = body.getReader();
|
|
101
|
+
while (true) {
|
|
102
|
+
const { done, value } = await reader.read();
|
|
103
|
+
if (done) break;
|
|
104
|
+
if (res.destroyed || res.writableEnded) {
|
|
105
|
+
await reader.cancel('client disconnected').catch(() => {});
|
|
106
|
+
upstreamAbortController.abort();
|
|
107
|
+
return { streamFailed: false, clientDisconnected: true, transportFailed: false };
|
|
108
|
+
}
|
|
109
|
+
if (!value) continue;
|
|
110
|
+
sseBuffer += decoder.decode(value, { stream: true });
|
|
111
|
+
const { frames, remainder } = splitCompleteSseFrames(sseBuffer);
|
|
112
|
+
sseBuffer = remainder;
|
|
113
|
+
assertSseFrameWithinLimit(sseBuffer);
|
|
114
|
+
for (const frame of frames) {
|
|
115
|
+
assertSseFrameWithinLimit(frame);
|
|
116
|
+
if (!await forwardFrame(frame)) {
|
|
117
|
+
await reader.cancel('client disconnected').catch(() => {});
|
|
118
|
+
upstreamAbortController.abort();
|
|
119
|
+
return { streamFailed: false, clientDisconnected: true, transportFailed: false };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
sseBuffer += decoder.decode();
|
|
124
|
+
if (sseBuffer && !await forwardFrame(sseBuffer)) {
|
|
125
|
+
await reader.cancel('client disconnected').catch(() => {});
|
|
126
|
+
upstreamAbortController.abort();
|
|
127
|
+
return { streamFailed: false, clientDisconnected: true, transportFailed: false };
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
transportFailed = true;
|
|
131
|
+
upstreamAbortController.abort();
|
|
132
|
+
process.stderr.write(`Private Mode upstream stream failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (res.destroyed || res.writableEnded) {
|
|
136
|
+
upstreamAbortController.abort();
|
|
137
|
+
return { streamFailed: false, clientDisconnected: true, transportFailed };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let terminal;
|
|
141
|
+
try {
|
|
142
|
+
terminal = await finishStream();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
transportFailed = true;
|
|
145
|
+
upstreamAbortController.abort();
|
|
146
|
+
process.stderr.write(`Private Mode stream finalization failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
147
|
+
if (!terminalGuard.hasEmittedDone()) {
|
|
148
|
+
await writeFrame(`data: ${JSON.stringify({
|
|
149
|
+
error: {
|
|
150
|
+
message: 'Private Mode stream ended before a complete terminal sequence. The response may be partial; retry the request.',
|
|
151
|
+
type: 'api_error',
|
|
152
|
+
code: 'private_stream_incomplete',
|
|
153
|
+
},
|
|
154
|
+
})}`);
|
|
155
|
+
}
|
|
156
|
+
return { streamFailed: true, clientDisconnected: false, transportFailed };
|
|
157
|
+
}
|
|
158
|
+
if (!terminal) {
|
|
159
|
+
upstreamAbortController.abort();
|
|
160
|
+
return { streamFailed: false, clientDisconnected: true, transportFailed };
|
|
161
|
+
}
|
|
162
|
+
return { streamFailed: !terminal.complete, clientDisconnected: false, transportFailed };
|
|
163
|
+
}
|
package/models/private-tee.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"id": "private/deepseek-v4-flash",
|
|
4
|
-
"name": "DeepSeek V4 Flash Private",
|
|
4
|
+
"name": "DeepSeek V4 Flash 0731 Private",
|
|
5
5
|
"upstreamModel": "deepseek-v4-flash",
|
|
6
6
|
"billingModel": "private/deepseek-v4-flash",
|
|
7
7
|
"providerPricingModel": "TEE/deepseek-v4-flash",
|
|
8
8
|
"teeTargetModel": "deepseek-v4-flash",
|
|
9
9
|
"thinkingMode": "deepseek-v4",
|
|
10
|
+
"maxInputTokens": 1048576,
|
|
10
11
|
"maxOutputTokens": 1048576,
|
|
11
12
|
"created": 1786406400,
|
|
12
13
|
"ownedBy": "nanogpt-private-mode",
|
|
@@ -55,6 +56,8 @@
|
|
|
55
56
|
"providerPricingModel": "TEE/glm-5.2",
|
|
56
57
|
"teeTargetModel": "glm-5-2",
|
|
57
58
|
"thinkingMode": "glm-5.2",
|
|
59
|
+
"maxInputTokens": 393216,
|
|
60
|
+
"maxOutputTokens": 131072,
|
|
58
61
|
"created": 1781827200,
|
|
59
62
|
"ownedBy": "nanogpt-private-mode",
|
|
60
63
|
"aliases": ["private/glm-5.2", "TEE/glm-5-2", "TEE/glm-5.2"]
|
|
@@ -67,6 +70,8 @@
|
|
|
67
70
|
"providerPricingModel": "TEE/glm-5.2:thinking",
|
|
68
71
|
"teeTargetModel": "glm-5-2",
|
|
69
72
|
"thinkingMode": "glm-5.2",
|
|
73
|
+
"maxInputTokens": 393216,
|
|
74
|
+
"maxOutputTokens": 131072,
|
|
70
75
|
"created": 1781827200,
|
|
71
76
|
"ownedBy": "nanogpt-private-mode",
|
|
72
77
|
"aliases": ["private/glm-5.2:thinking", "TEE/glm-5-2:thinking", "TEE/glm-5.2:thinking"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanogpt/private-mode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "OpenAI-compatible localhost proxy for NanoGPT Private Mode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -13,11 +13,15 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"bin",
|
|
15
15
|
"lib/cacheScope.js",
|
|
16
|
+
"lib/catalogCompatibility.js",
|
|
17
|
+
"lib/constants.js",
|
|
16
18
|
"lib/originPolicy.js",
|
|
19
|
+
"lib/packageInfo.js",
|
|
17
20
|
"lib/requestTransforms.js",
|
|
18
21
|
"lib/secureClientLifecycle.js",
|
|
19
22
|
"lib/server.js",
|
|
20
23
|
"lib/serverErrorNormalization.js",
|
|
24
|
+
"lib/sseResponsePump.js",
|
|
21
25
|
"lib/statusContract.js",
|
|
22
26
|
"lib/verifyReceipt.js",
|
|
23
27
|
"models",
|