satgate 1.5.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/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/bin/satgate.d.ts +3 -0
- package/dist/bin/satgate.d.ts.map +1 -0
- package/dist/bin/satgate.js +7 -0
- package/dist/bin/satgate.js.map +1 -0
- package/dist/src/auth/allowlist.d.ts +24 -0
- package/dist/src/auth/allowlist.d.ts.map +1 -0
- package/dist/src/auth/allowlist.js +130 -0
- package/dist/src/auth/allowlist.js.map +1 -0
- package/dist/src/auth/middleware.d.ts +15 -0
- package/dist/src/auth/middleware.d.ts.map +1 -0
- package/dist/src/auth/middleware.js +29 -0
- package/dist/src/auth/middleware.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +275 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +133 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +237 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/discovery/llms-txt.d.ts +10 -0
- package/dist/src/discovery/llms-txt.d.ts.map +1 -0
- package/dist/src/discovery/llms-txt.js +25 -0
- package/dist/src/discovery/llms-txt.js.map +1 -0
- package/dist/src/discovery/openapi.d.ts +8 -0
- package/dist/src/discovery/openapi.d.ts.map +1 -0
- package/dist/src/discovery/openapi.js +116 -0
- package/dist/src/discovery/openapi.js.map +1 -0
- package/dist/src/discovery/well-known.d.ts +22 -0
- package/dist/src/discovery/well-known.d.ts.map +1 -0
- package/dist/src/discovery/well-known.js +44 -0
- package/dist/src/discovery/well-known.js.map +1 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lightning.d.ts +12 -0
- package/dist/src/lightning.d.ts.map +1 -0
- package/dist/src/lightning.js +38 -0
- package/dist/src/lightning.js.map +1 -0
- package/dist/src/logger.d.ts +16 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +99 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/proxy/capacity.d.ts +15 -0
- package/dist/src/proxy/capacity.d.ts.map +1 -0
- package/dist/src/proxy/capacity.js +28 -0
- package/dist/src/proxy/capacity.js.map +1 -0
- package/dist/src/proxy/handler.d.ts +27 -0
- package/dist/src/proxy/handler.d.ts.map +1 -0
- package/dist/src/proxy/handler.js +165 -0
- package/dist/src/proxy/handler.js.map +1 -0
- package/dist/src/proxy/pricing.d.ts +17 -0
- package/dist/src/proxy/pricing.d.ts.map +1 -0
- package/dist/src/proxy/pricing.js +42 -0
- package/dist/src/proxy/pricing.js.map +1 -0
- package/dist/src/proxy/streaming.d.ts +12 -0
- package/dist/src/proxy/streaming.d.ts.map +1 -0
- package/dist/src/proxy/streaming.js +68 -0
- package/dist/src/proxy/streaming.js.map +1 -0
- package/dist/src/proxy/token-counter.d.ts +22 -0
- package/dist/src/proxy/token-counter.d.ts.map +1 -0
- package/dist/src/proxy/token-counter.js +66 -0
- package/dist/src/proxy/token-counter.js.map +1 -0
- package/dist/src/server.d.ts +9 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +239 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/tunnel.d.ts +26 -0
- package/dist/src/tunnel.d.ts.map +1 -0
- package/dist/src/tunnel.js +78 -0
- package/dist/src/tunnel.js.map +1 -0
- package/dist/src/x402/facilitator.d.ts +7 -0
- package/dist/src/x402/facilitator.d.ts.map +1 -0
- package/dist/src/x402/facilitator.js +33 -0
- package/dist/src/x402/facilitator.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { TokenCounter } from './token-counter.js';
|
|
2
|
+
import { createStreamingProxy } from './streaming.js';
|
|
3
|
+
import { resolveModelPrice, tokenCostToSats } from './pricing.js';
|
|
4
|
+
/** Allowed upstream path prefixes — anything else is rejected. */
|
|
5
|
+
const ALLOWED_PATH_PREFIXES = ['/v1/chat/completions', '/v1/completions', '/v1/embeddings'];
|
|
6
|
+
/**
|
|
7
|
+
* Extracts the model name from an OpenAI-compatible request body.
|
|
8
|
+
*/
|
|
9
|
+
function extractModel(body) {
|
|
10
|
+
return typeof body.model === 'string' ? body.model : '';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates the AI proxy handler.
|
|
14
|
+
*
|
|
15
|
+
* @returns A function that proxies a single inference request to the upstream.
|
|
16
|
+
*/
|
|
17
|
+
export function createProxyHandler(deps) {
|
|
18
|
+
return async function handleProxy(req, paymentHash) {
|
|
19
|
+
// Capacity check (before any payment deduction)
|
|
20
|
+
if (!deps.capacity.tryAcquire()) {
|
|
21
|
+
return new Response(JSON.stringify({ error: 'Service at capacity, try again later' }), { status: 503, headers: { 'Content-Type': 'application/json', 'Retry-After': '5' } });
|
|
22
|
+
}
|
|
23
|
+
const start = Date.now();
|
|
24
|
+
let streamingResponse = false;
|
|
25
|
+
try {
|
|
26
|
+
// Validate request path — only allow known OpenAI-compatible endpoints
|
|
27
|
+
const requestPath = new URL(req.url).pathname;
|
|
28
|
+
if (!ALLOWED_PATH_PREFIXES.some(p => requestPath === p)) {
|
|
29
|
+
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: { 'Content-Type': 'application/json' } });
|
|
30
|
+
}
|
|
31
|
+
// Validate Content-Type
|
|
32
|
+
const contentType = req.headers.get('content-type');
|
|
33
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
34
|
+
return new Response(JSON.stringify({ error: 'Content-Type must be application/json' }), { status: 415, headers: { 'Content-Type': 'application/json' } });
|
|
35
|
+
}
|
|
36
|
+
// Enforce body size limit
|
|
37
|
+
const contentLength = req.headers.get('content-length');
|
|
38
|
+
if (contentLength !== null) {
|
|
39
|
+
const len = parseInt(contentLength, 10);
|
|
40
|
+
if (!Number.isFinite(len) || len > deps.maxBodySize) {
|
|
41
|
+
return new Response(JSON.stringify({ error: 'Request body too large' }), { status: 413, headers: { 'Content-Type': 'application/json' } });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Parse the request body to extract model name
|
|
45
|
+
const bodyText = await req.text();
|
|
46
|
+
if (new TextEncoder().encode(bodyText).byteLength > deps.maxBodySize) {
|
|
47
|
+
return new Response(JSON.stringify({ error: 'Request body too large' }), { status: 413, headers: { 'Content-Type': 'application/json' } });
|
|
48
|
+
}
|
|
49
|
+
let body;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(bodyText);
|
|
52
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
53
|
+
return new Response(JSON.stringify({ error: 'Request body must be a JSON object' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
54
|
+
}
|
|
55
|
+
body = parsed;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return new Response(JSON.stringify({ error: 'Invalid JSON body' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
59
|
+
}
|
|
60
|
+
const model = extractModel(body);
|
|
61
|
+
const pricePerThousand = resolveModelPrice(deps.pricing, model);
|
|
62
|
+
const isStreaming = body.stream === true;
|
|
63
|
+
// Inject stream_options for usage reporting if streaming
|
|
64
|
+
if (isStreaming && !body.stream_options) {
|
|
65
|
+
body.stream_options = { include_usage: true };
|
|
66
|
+
}
|
|
67
|
+
// Build upstream URL
|
|
68
|
+
const url = new URL(req.url);
|
|
69
|
+
const upstreamUrl = `${deps.upstream}${url.pathname}`;
|
|
70
|
+
// Fetch from upstream
|
|
71
|
+
const timeout = deps.upstreamTimeout ?? 120_000;
|
|
72
|
+
let upstreamRes;
|
|
73
|
+
try {
|
|
74
|
+
upstreamRes = await fetch(upstreamUrl, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': 'application/json' },
|
|
77
|
+
body: JSON.stringify(body),
|
|
78
|
+
signal: AbortSignal.timeout(timeout),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
// Upstream unreachable - refund estimated cost
|
|
83
|
+
if (paymentHash) {
|
|
84
|
+
deps.reconcile(paymentHash, 0);
|
|
85
|
+
}
|
|
86
|
+
deps.logger?.error('upstream error', {
|
|
87
|
+
endpoint: new URL(req.url).pathname,
|
|
88
|
+
method: req.method,
|
|
89
|
+
latencyMs: Date.now() - start,
|
|
90
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
91
|
+
});
|
|
92
|
+
return new Response(JSON.stringify({ error: 'Upstream inference API unreachable' }), { status: 502, headers: { 'Content-Type': 'application/json' } });
|
|
93
|
+
}
|
|
94
|
+
// If upstream returned an error, refund and return a generic error
|
|
95
|
+
// (don't forward raw upstream body — may leak internal details)
|
|
96
|
+
if (!upstreamRes.ok) {
|
|
97
|
+
if (paymentHash) {
|
|
98
|
+
deps.reconcile(paymentHash, 0);
|
|
99
|
+
}
|
|
100
|
+
// Consume and discard the upstream error body to prevent connection leaks
|
|
101
|
+
await upstreamRes.body?.cancel().catch(() => { });
|
|
102
|
+
const status = upstreamRes.status >= 400 && upstreamRes.status < 600
|
|
103
|
+
? upstreamRes.status
|
|
104
|
+
: 502;
|
|
105
|
+
return new Response(JSON.stringify({ error: `Upstream returned ${upstreamRes.status}` }), { status, headers: { 'Content-Type': 'application/json' } });
|
|
106
|
+
}
|
|
107
|
+
// Handle streaming response
|
|
108
|
+
if (isStreaming && upstreamRes.body) {
|
|
109
|
+
const { readable } = createStreamingProxy(upstreamRes.body, (tokenCount) => {
|
|
110
|
+
// Release capacity slot when stream ends (not in finally)
|
|
111
|
+
deps.capacity.release();
|
|
112
|
+
if (!deps.flatPricing && paymentHash) {
|
|
113
|
+
const satCost = tokenCostToSats(tokenCount, pricePerThousand);
|
|
114
|
+
deps.reconcile(paymentHash, satCost);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
// Mark as streaming so finally doesn't double-release
|
|
118
|
+
streamingResponse = true;
|
|
119
|
+
return new Response(readable, {
|
|
120
|
+
status: 200,
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'text/event-stream',
|
|
123
|
+
'Cache-Control': 'no-cache',
|
|
124
|
+
'Connection': 'keep-alive',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Handle non-streaming response — enforce size limit before parsing
|
|
129
|
+
const responseText = await upstreamRes.text();
|
|
130
|
+
if (new TextEncoder().encode(responseText).byteLength > deps.maxBodySize) {
|
|
131
|
+
if (paymentHash)
|
|
132
|
+
deps.reconcile(paymentHash, 0);
|
|
133
|
+
return new Response(JSON.stringify({ error: 'Upstream response too large' }), { status: 502, headers: { 'Content-Type': 'application/json' } });
|
|
134
|
+
}
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
+
let responseBody;
|
|
137
|
+
try {
|
|
138
|
+
responseBody = JSON.parse(responseText);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return new Response(JSON.stringify({ error: 'Upstream returned invalid JSON' }), { status: 502, headers: { 'Content-Type': 'application/json' } });
|
|
142
|
+
}
|
|
143
|
+
const counter = new TokenCounter();
|
|
144
|
+
if (responseBody && typeof responseBody === 'object' && responseBody.usage) {
|
|
145
|
+
counter.setBufferedUsage(responseBody.usage);
|
|
146
|
+
}
|
|
147
|
+
const tokenCount = counter.finalCount();
|
|
148
|
+
const satCost = tokenCostToSats(tokenCount, pricePerThousand);
|
|
149
|
+
if (!deps.flatPricing && paymentHash) {
|
|
150
|
+
deps.reconcile(paymentHash, satCost);
|
|
151
|
+
}
|
|
152
|
+
return new Response(JSON.stringify(responseBody), {
|
|
153
|
+
status: 200,
|
|
154
|
+
headers: { 'Content-Type': 'application/json' },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
// Streaming responses release capacity in the onComplete callback
|
|
159
|
+
if (!streamingResponse) {
|
|
160
|
+
deps.capacity.release();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../../../src/proxy/handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ModelPricing } from '../config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the price per 1k tokens for a model.
|
|
4
|
+
*
|
|
5
|
+
* Resolution order:
|
|
6
|
+
* 1. Exact match on model name
|
|
7
|
+
* 2. Case-insensitive match
|
|
8
|
+
* 3. Strip Ollama tag (model:tag -> model) and retry
|
|
9
|
+
* 4. Fall back to default price
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveModelPrice(pricing: ModelPricing, model: string): number;
|
|
12
|
+
/**
|
|
13
|
+
* Converts a token count to a sat cost.
|
|
14
|
+
* Always rounds up (ceil) so the operator is never short-changed.
|
|
15
|
+
*/
|
|
16
|
+
export declare function tokenCostToSats(totalTokens: number, pricePerThousand: number): number;
|
|
17
|
+
//# sourceMappingURL=pricing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../../src/proxy/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAsB9E;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAGrF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the price per 1k tokens for a model.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. Exact match on model name
|
|
6
|
+
* 2. Case-insensitive match
|
|
7
|
+
* 3. Strip Ollama tag (model:tag -> model) and retry
|
|
8
|
+
* 4. Fall back to default price
|
|
9
|
+
*/
|
|
10
|
+
export function resolveModelPrice(pricing, model) {
|
|
11
|
+
if (!model)
|
|
12
|
+
return pricing.default;
|
|
13
|
+
// Exact match
|
|
14
|
+
if (model in pricing.models)
|
|
15
|
+
return pricing.models[model];
|
|
16
|
+
// Case-insensitive match
|
|
17
|
+
const lower = model.toLowerCase();
|
|
18
|
+
for (const [key, value] of Object.entries(pricing.models)) {
|
|
19
|
+
if (key.toLowerCase() === lower)
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
// Strip Ollama tag (e.g. llama3:latest -> llama3)
|
|
23
|
+
const colonIdx = lower.indexOf(':');
|
|
24
|
+
if (colonIdx !== -1) {
|
|
25
|
+
const base = lower.slice(0, colonIdx);
|
|
26
|
+
for (const [key, value] of Object.entries(pricing.models)) {
|
|
27
|
+
if (key.toLowerCase() === base)
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return pricing.default;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Converts a token count to a sat cost.
|
|
35
|
+
* Always rounds up (ceil) so the operator is never short-changed.
|
|
36
|
+
*/
|
|
37
|
+
export function tokenCostToSats(totalTokens, pricePerThousand) {
|
|
38
|
+
if (totalTokens <= 0)
|
|
39
|
+
return 0;
|
|
40
|
+
return Math.ceil(totalTokens * pricePerThousand / 1000);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=pricing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing.js","sourceRoot":"","sources":["../../../src/proxy/pricing.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAqB,EAAE,KAAa;IACpE,IAAI,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC,OAAO,CAAA;IAElC,cAAc;IACd,IAAI,KAAK,IAAI,OAAO,CAAC,MAAM;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAEzD,yBAAyB;IACzB,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAA;IACjC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1D,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,KAAK;YAAE,OAAO,KAAK,CAAA;IAC/C,CAAC;IAED,kDAAkD;IAClD,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACnC,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAA;QACrC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1D,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,IAAI;gBAAE,OAAO,KAAK,CAAA;QAC9C,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC,OAAO,CAAA;AACxB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,WAAmB,EAAE,gBAAwB;IAC3E,IAAI,WAAW,IAAI,CAAC;QAAE,OAAO,CAAC,CAAA;IAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,gBAAgB,GAAG,IAAI,CAAC,CAAA;AACzD,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a ReadableStream that pipes upstream SSE chunks through while counting tokens.
|
|
3
|
+
*
|
|
4
|
+
* @param upstream - The upstream SSE ReadableStream
|
|
5
|
+
* @param onComplete - Called with the final token count after the stream ends or errors
|
|
6
|
+
* @param inactivityTimeoutMs - Max time between chunks before aborting (default: 120s)
|
|
7
|
+
* @returns An object with the readable side
|
|
8
|
+
*/
|
|
9
|
+
export declare function createStreamingProxy(upstream: ReadableStream<Uint8Array>, onComplete: (tokenCount: number) => void, inactivityTimeoutMs?: number): {
|
|
10
|
+
readable: ReadableStream<Uint8Array>;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=streaming.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../../../src/proxy/streaming.ts"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,EACpC,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,EACxC,mBAAmB,GAAE,MAAsC,GAC1D;IAAE,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,CAAA;CAAE,CAsD1C"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { TokenCounter } from './token-counter.js';
|
|
2
|
+
/** Default inactivity timeout for streaming responses (2 minutes). */
|
|
3
|
+
const DEFAULT_INACTIVITY_TIMEOUT_MS = 120_000;
|
|
4
|
+
/**
|
|
5
|
+
* Creates a ReadableStream that pipes upstream SSE chunks through while counting tokens.
|
|
6
|
+
*
|
|
7
|
+
* @param upstream - The upstream SSE ReadableStream
|
|
8
|
+
* @param onComplete - Called with the final token count after the stream ends or errors
|
|
9
|
+
* @param inactivityTimeoutMs - Max time between chunks before aborting (default: 120s)
|
|
10
|
+
* @returns An object with the readable side
|
|
11
|
+
*/
|
|
12
|
+
export function createStreamingProxy(upstream, onComplete, inactivityTimeoutMs = DEFAULT_INACTIVITY_TIMEOUT_MS) {
|
|
13
|
+
const counter = new TokenCounter();
|
|
14
|
+
const decoder = new TextDecoder();
|
|
15
|
+
let completeCalled = false;
|
|
16
|
+
let inactivityTimer;
|
|
17
|
+
function finish() {
|
|
18
|
+
if (completeCalled)
|
|
19
|
+
return;
|
|
20
|
+
completeCalled = true;
|
|
21
|
+
clearTimeout(inactivityTimer);
|
|
22
|
+
onComplete(counter.finalCount());
|
|
23
|
+
}
|
|
24
|
+
const readable = new ReadableStream({
|
|
25
|
+
async start(controller) {
|
|
26
|
+
const reader = upstream.getReader();
|
|
27
|
+
function resetTimer() {
|
|
28
|
+
clearTimeout(inactivityTimer);
|
|
29
|
+
inactivityTimer = setTimeout(() => {
|
|
30
|
+
reader.cancel('inactivity timeout').catch(() => { });
|
|
31
|
+
controller.close();
|
|
32
|
+
finish();
|
|
33
|
+
}, inactivityTimeoutMs);
|
|
34
|
+
}
|
|
35
|
+
resetTimer();
|
|
36
|
+
try {
|
|
37
|
+
while (true) {
|
|
38
|
+
const { done, value } = await reader.read();
|
|
39
|
+
if (done)
|
|
40
|
+
break;
|
|
41
|
+
resetTimer();
|
|
42
|
+
controller.enqueue(value);
|
|
43
|
+
const text = decoder.decode(value, { stream: true });
|
|
44
|
+
counter.ingestSSEChunk(text);
|
|
45
|
+
}
|
|
46
|
+
controller.close();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Upstream errored — close gracefully
|
|
50
|
+
try {
|
|
51
|
+
controller.close();
|
|
52
|
+
}
|
|
53
|
+
catch { /* already closed */ }
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
clearTimeout(inactivityTimer);
|
|
57
|
+
finish();
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
cancel() {
|
|
61
|
+
// Client disconnected
|
|
62
|
+
clearTimeout(inactivityTimer);
|
|
63
|
+
finish();
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return { readable };
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=streaming.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming.js","sourceRoot":"","sources":["../../../src/proxy/streaming.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEjD,sEAAsE;AACtE,MAAM,6BAA6B,GAAG,OAAO,CAAA;AAE7C;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAAoC,EACpC,UAAwC,EACxC,sBAA8B,6BAA6B;IAE3D,MAAM,OAAO,GAAG,IAAI,YAAY,EAAE,CAAA;IAClC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;IACjC,IAAI,cAAc,GAAG,KAAK,CAAA;IAC1B,IAAI,eAA0D,CAAA;IAE9D,SAAS,MAAM;QACb,IAAI,cAAc;YAAE,OAAM;QAC1B,cAAc,GAAG,IAAI,CAAA;QACrB,YAAY,CAAC,eAAe,CAAC,CAAA;QAC7B,UAAU,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAA;IAClC,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAa;QAC9C,KAAK,CAAC,KAAK,CAAC,UAAU;YACpB,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAA;YAEnC,SAAS,UAAU;gBACjB,YAAY,CAAC,eAAe,CAAC,CAAA;gBAC7B,eAAe,GAAG,UAAU,CAAC,GAAG,EAAE;oBAChC,MAAM,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;oBACnD,UAAU,CAAC,KAAK,EAAE,CAAA;oBAClB,MAAM,EAAE,CAAA;gBACV,CAAC,EAAE,mBAAmB,CAAC,CAAA;YACzB,CAAC;YAED,UAAU,EAAE,CAAA;YAEZ,IAAI,CAAC;gBACH,OAAO,IAAI,EAAE,CAAC;oBACZ,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;oBAC3C,IAAI,IAAI;wBAAE,MAAK;oBACf,UAAU,EAAE,CAAA;oBACZ,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;oBACzB,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;oBACpD,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,CAAA;gBAC9B,CAAC;gBACD,UAAU,CAAC,KAAK,EAAE,CAAA;YACpB,CAAC;YAAC,MAAM,CAAC;gBACP,sCAAsC;gBACtC,IAAI,CAAC;oBAAC,UAAU,CAAC,KAAK,EAAE,CAAA;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,CAAC;YAC3D,CAAC;oBAAS,CAAC;gBACT,YAAY,CAAC,eAAe,CAAC,CAAA;gBAC7B,MAAM,EAAE,CAAA;YACV,CAAC;QACH,CAAC;QACD,MAAM;YACJ,sBAAsB;YACtB,YAAY,CAAC,eAAe,CAAC,CAAA;YAC7B,MAAM,EAAE,CAAA;QACV,CAAC;KACF,CAAC,CAAA;IAEF,OAAO,EAAE,QAAQ,EAAE,CAAA;AACrB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts tokens from OpenAI-compatible responses.
|
|
3
|
+
*
|
|
4
|
+
* Priority:
|
|
5
|
+
* 1. Buffered usage (from non-streaming JSON response)
|
|
6
|
+
* 2. Usage from final SSE chunk (stream_options: { include_usage: true })
|
|
7
|
+
* 3. Content chunk count (fallback - 1 chunk ~= 1 token)
|
|
8
|
+
*/
|
|
9
|
+
export declare class TokenCounter {
|
|
10
|
+
private bufferedUsage;
|
|
11
|
+
private sseUsage;
|
|
12
|
+
private contentChunkCount;
|
|
13
|
+
/** Set usage from a buffered (non-streaming) JSON response. */
|
|
14
|
+
setBufferedUsage(usage: Record<string, unknown>): void;
|
|
15
|
+
/** Ingest an SSE chunk (may contain multiple events). */
|
|
16
|
+
ingestSSEChunk(chunk: string): void;
|
|
17
|
+
/** Returns the final token count using the best available source.
|
|
18
|
+
* Uses prompt_tokens from usage stats + content chunk count for completion.
|
|
19
|
+
* This avoids billing for reasoning/thinking tokens that some models produce. */
|
|
20
|
+
finalCount(): number;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=token-counter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-counter.d.ts","sourceRoot":"","sources":["../../../src/proxy/token-counter.ts"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,iBAAiB,CAAI;IAE7B,+DAA+D;IAC/D,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAQtD,yDAAyD;IACzD,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAkCnC;;sFAEkF;IAClF,UAAU,IAAI,MAAM;CAQrB"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts tokens from OpenAI-compatible responses.
|
|
3
|
+
*
|
|
4
|
+
* Priority:
|
|
5
|
+
* 1. Buffered usage (from non-streaming JSON response)
|
|
6
|
+
* 2. Usage from final SSE chunk (stream_options: { include_usage: true })
|
|
7
|
+
* 3. Content chunk count (fallback - 1 chunk ~= 1 token)
|
|
8
|
+
*/
|
|
9
|
+
export class TokenCounter {
|
|
10
|
+
bufferedUsage = null;
|
|
11
|
+
sseUsage = null;
|
|
12
|
+
contentChunkCount = 0;
|
|
13
|
+
/** Set usage from a buffered (non-streaming) JSON response. */
|
|
14
|
+
setBufferedUsage(usage) {
|
|
15
|
+
this.bufferedUsage = {
|
|
16
|
+
prompt_tokens: typeof usage.prompt_tokens === 'number' ? usage.prompt_tokens : undefined,
|
|
17
|
+
completion_tokens: typeof usage.completion_tokens === 'number' ? usage.completion_tokens : undefined,
|
|
18
|
+
total_tokens: typeof usage.total_tokens === 'number' ? usage.total_tokens : undefined,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Ingest an SSE chunk (may contain multiple events). */
|
|
22
|
+
ingestSSEChunk(chunk) {
|
|
23
|
+
const lines = chunk.split('\n');
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
if (!line.startsWith('data: '))
|
|
26
|
+
continue;
|
|
27
|
+
const data = line.slice(6).trim();
|
|
28
|
+
if (data === '[DONE]')
|
|
29
|
+
continue;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(data);
|
|
32
|
+
// Check for usage in this chunk
|
|
33
|
+
if (parsed.usage) {
|
|
34
|
+
this.sseUsage = {
|
|
35
|
+
prompt_tokens: parsed.usage.prompt_tokens,
|
|
36
|
+
completion_tokens: parsed.usage.completion_tokens,
|
|
37
|
+
total_tokens: parsed.usage.total_tokens,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Count content chunks
|
|
41
|
+
const choices = parsed.choices;
|
|
42
|
+
if (Array.isArray(choices)) {
|
|
43
|
+
for (const choice of choices) {
|
|
44
|
+
if (choice.delta?.content !== undefined && choice.delta.content !== '') {
|
|
45
|
+
this.contentChunkCount++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Malformed JSON - skip
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Returns the final token count using the best available source.
|
|
56
|
+
* Uses prompt_tokens from usage stats + content chunk count for completion.
|
|
57
|
+
* This avoids billing for reasoning/thinking tokens that some models produce. */
|
|
58
|
+
finalCount() {
|
|
59
|
+
const usage = this.bufferedUsage ?? this.sseUsage;
|
|
60
|
+
const promptTokens = usage?.prompt_tokens ?? 0;
|
|
61
|
+
// Content chunk count is the most reliable measure of actual output
|
|
62
|
+
// since it excludes reasoning tokens that models like qwen3 produce
|
|
63
|
+
return promptTokens + this.contentChunkCount;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=token-counter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-counter.js","sourceRoot":"","sources":["../../../src/proxy/token-counter.ts"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AACH,MAAM,OAAO,YAAY;IACf,aAAa,GAAqB,IAAI,CAAA;IACtC,QAAQ,GAAqB,IAAI,CAAA;IACjC,iBAAiB,GAAG,CAAC,CAAA;IAE7B,+DAA+D;IAC/D,gBAAgB,CAAC,KAA8B;QAC7C,IAAI,CAAC,aAAa,GAAG;YACnB,aAAa,EAAE,OAAO,KAAK,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS;YACxF,iBAAiB,EAAE,OAAO,KAAK,CAAC,iBAAiB,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC,SAAS;YACpG,YAAY,EAAE,OAAO,KAAK,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS;SACtF,CAAA;IACH,CAAC;IAED,yDAAyD;IACzD,cAAc,CAAC,KAAa;QAC1B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;gBAAE,SAAQ;YACxC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;YACjC,IAAI,IAAI,KAAK,QAAQ;gBAAE,SAAQ;YAE/B,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;gBAE/B,gCAAgC;gBAChC,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;oBACjB,IAAI,CAAC,QAAQ,GAAG;wBACd,aAAa,EAAE,MAAM,CAAC,KAAK,CAAC,aAAa;wBACzC,iBAAiB,EAAE,MAAM,CAAC,KAAK,CAAC,iBAAiB;wBACjD,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,YAAY;qBACxC,CAAA;gBACH,CAAC;gBAED,uBAAuB;gBACvB,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;gBAC9B,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC3B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;wBAC7B,IAAI,MAAM,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,KAAK,EAAE,EAAE,CAAC;4BACvE,IAAI,CAAC,iBAAiB,EAAE,CAAA;wBAC1B,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,wBAAwB;YAC1B,CAAC;QACH,CAAC;IACH,CAAC;IAED;;sFAEkF;IAClF,UAAU;QACR,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,QAAQ,CAAA;QACjD,MAAM,YAAY,GAAG,KAAK,EAAE,aAAa,IAAI,CAAC,CAAA;QAE9C,oEAAoE;QACpE,oEAAoE;QACpE,OAAO,YAAY,GAAG,IAAI,CAAC,iBAAiB,CAAA;IAC9C,CAAC;CACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import type { TollBoothEnv } from '@thecryptodonkey/toll-booth/hono';
|
|
3
|
+
import type { TokenTollConfig } from './config.js';
|
|
4
|
+
export interface TokenTollServer {
|
|
5
|
+
app: Hono<TollBoothEnv>;
|
|
6
|
+
close: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function createTokenTollServer(config: TokenTollConfig): TokenTollServer;
|
|
9
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAU3B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAUlD,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;IACvB,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAgBD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CAqO9E"}
|