@vidiemme/copilot-proxy 0.1.0
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 +57 -0
- package/dist/attribution.d.ts +34 -0
- package/dist/attribution.js +118 -0
- package/dist/attribution.js.map +1 -0
- package/dist/autostart.d.ts +10 -0
- package/dist/autostart.js +119 -0
- package/dist/autostart.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +393 -0
- package/dist/cli.js.map +1 -0
- package/dist/collector/forwarder.d.ts +44 -0
- package/dist/collector/forwarder.js +203 -0
- package/dist/collector/forwarder.js.map +1 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +105 -0
- package/dist/config.js.map +1 -0
- package/dist/defaults.d.ts +33 -0
- package/dist/defaults.js +35 -0
- package/dist/defaults.js.map +1 -0
- package/dist/gitRepository.d.ts +21 -0
- package/dist/gitRepository.js +147 -0
- package/dist/gitRepository.js.map +1 -0
- package/dist/paths.d.ts +12 -0
- package/dist/paths.js +39 -0
- package/dist/paths.js.map +1 -0
- package/dist/projectDetector.d.ts +48 -0
- package/dist/projectDetector.js +130 -0
- package/dist/projectDetector.js.map +1 -0
- package/dist/proxy/handler.d.ts +5 -0
- package/dist/proxy/handler.js +149 -0
- package/dist/proxy/handler.js.map +1 -0
- package/dist/proxy/headers.d.ts +6 -0
- package/dist/proxy/headers.js +52 -0
- package/dist/proxy/headers.js.map +1 -0
- package/dist/proxy/inspectRequest.d.ts +18 -0
- package/dist/proxy/inspectRequest.js +41 -0
- package/dist/proxy/inspectRequest.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +66 -0
- package/dist/server.js.map +1 -0
- package/dist/usage/collector.d.ts +25 -0
- package/dist/usage/collector.js +63 -0
- package/dist/usage/collector.js.map +1 -0
- package/dist/usage/parseAnthropic.d.ts +23 -0
- package/dist/usage/parseAnthropic.js +83 -0
- package/dist/usage/parseAnthropic.js.map +1 -0
- package/dist/usage/parseOpenAI.d.ts +24 -0
- package/dist/usage/parseOpenAI.js +72 -0
- package/dist/usage/parseOpenAI.js.map +1 -0
- package/dist/usage/sse.d.ts +17 -0
- package/dist/usage/sse.js +59 -0
- package/dist/usage/sse.js.map +1 -0
- package/dist/usage/types.d.ts +23 -0
- package/dist/usage/types.js +19 -0
- package/dist/usage/types.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, sep } from 'node:path';
|
|
3
|
+
import { readRepositoryIdentity } from './gitRepository.js';
|
|
4
|
+
const MAX_CACHE_ENTRIES = 5_000;
|
|
5
|
+
const MAX_STICKY_ENTRIES = 1_000;
|
|
6
|
+
function escapeRegExp(value) {
|
|
7
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
+
}
|
|
9
|
+
function normalizeRoot(root) {
|
|
10
|
+
return root.trim().replace(/[\\/]+$/, '');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Deduce il progetto di appartenenza di una richiesta dai path assoluti che
|
|
14
|
+
* questa cita.
|
|
15
|
+
*
|
|
16
|
+
* Le richieste di Copilot Chat trasportano il contesto di lavoro: file aperti,
|
|
17
|
+
* riferimenti espliciti, risultati degli strumenti dell'agente. Tutti nominano
|
|
18
|
+
* file reali sul disco. Risalendo da quei path fino al marker di progetto piu'
|
|
19
|
+
* vicino si ottiene l'attribuzione senza chiedere al developer di configurare
|
|
20
|
+
* nulla per ogni repository, e senza conservare il contenuto della richiesta:
|
|
21
|
+
* dei path si tiene solo l'identita' del repository.
|
|
22
|
+
*/
|
|
23
|
+
export class ProjectDetector {
|
|
24
|
+
roots;
|
|
25
|
+
markers;
|
|
26
|
+
stickyTtlMs;
|
|
27
|
+
maxScanBytes;
|
|
28
|
+
pattern;
|
|
29
|
+
/** Path candidato -> repository risolto (o `null` se fuori da ogni progetto). */
|
|
30
|
+
cache = new Map();
|
|
31
|
+
sticky = new Map();
|
|
32
|
+
constructor(options) {
|
|
33
|
+
// I root piu' lunghi vanno provati per primi: con root annidati vince il
|
|
34
|
+
// piu' specifico.
|
|
35
|
+
this.roots = options.roots
|
|
36
|
+
.map(normalizeRoot)
|
|
37
|
+
.filter((root) => root.length > 0)
|
|
38
|
+
.sort((a, b) => b.length - a.length);
|
|
39
|
+
this.markers = options.markers;
|
|
40
|
+
this.stickyTtlMs = options.stickyTtlMs;
|
|
41
|
+
this.maxScanBytes = options.maxScanBytes;
|
|
42
|
+
this.pattern =
|
|
43
|
+
this.roots.length > 0
|
|
44
|
+
? new RegExp(`(?:${this.roots.map(escapeRegExp).join('|')})(?:/[^\\s"'\`,;:*?<>|()\\[\\]{}\\\\]+)+`, 'g')
|
|
45
|
+
: undefined;
|
|
46
|
+
}
|
|
47
|
+
get enabled() {
|
|
48
|
+
return this.pattern !== undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* @param sessionKey chiave della finestra/conversazione, usata per ereditare
|
|
52
|
+
* il progetto nelle richieste che non citano file (titoli, domande secche).
|
|
53
|
+
*/
|
|
54
|
+
detect(body, sessionKey, now = Date.now()) {
|
|
55
|
+
const repository = this.fromBody(body);
|
|
56
|
+
if (repository) {
|
|
57
|
+
this.remember(sessionKey, repository, now);
|
|
58
|
+
return { repository, source: 'workspace' };
|
|
59
|
+
}
|
|
60
|
+
const previous = this.sticky.get(sessionKey);
|
|
61
|
+
if (previous && previous.expiresAt > now) {
|
|
62
|
+
return { repository: previous.repository, source: 'session' };
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
fromBody(body) {
|
|
67
|
+
if (!this.pattern || body.length === 0)
|
|
68
|
+
return undefined;
|
|
69
|
+
const text = body.toString('utf-8', 0, Math.min(body.length, this.maxScanBytes));
|
|
70
|
+
const candidates = new Set();
|
|
71
|
+
this.pattern.lastIndex = 0;
|
|
72
|
+
for (let match = this.pattern.exec(text); match; match = this.pattern.exec(text)) {
|
|
73
|
+
candidates.add(match[0]);
|
|
74
|
+
}
|
|
75
|
+
if (candidates.size === 0)
|
|
76
|
+
return undefined;
|
|
77
|
+
// Vince il progetto piu' citato: un riferimento isolato a un file di un
|
|
78
|
+
// altro repository non deve dirottare l'attribuzione.
|
|
79
|
+
const scores = new Map();
|
|
80
|
+
let best;
|
|
81
|
+
let bestScore = 0;
|
|
82
|
+
for (const candidate of candidates) {
|
|
83
|
+
const repository = this.projectFor(candidate);
|
|
84
|
+
if (!repository)
|
|
85
|
+
continue;
|
|
86
|
+
const score = (scores.get(repository.projectId) ?? 0) + 1;
|
|
87
|
+
scores.set(repository.projectId, score);
|
|
88
|
+
if (score > bestScore) {
|
|
89
|
+
best = repository;
|
|
90
|
+
bestScore = score;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return best;
|
|
94
|
+
}
|
|
95
|
+
/** Risale dal path fino al primo marker di progetto, senza uscire dal root. */
|
|
96
|
+
projectFor(candidate) {
|
|
97
|
+
const cached = this.cache.get(candidate);
|
|
98
|
+
if (cached !== undefined)
|
|
99
|
+
return cached ?? undefined;
|
|
100
|
+
const root = this.roots.find((item) => candidate.startsWith(item) && candidate[item.length] === '/');
|
|
101
|
+
let result;
|
|
102
|
+
let directory = candidate;
|
|
103
|
+
while (root && directory.length > root.length) {
|
|
104
|
+
if (this.markers.some((marker) => existsSync(`${directory}${sep}${marker}`))) {
|
|
105
|
+
result = readRepositoryIdentity(directory);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
const parent = dirname(directory);
|
|
109
|
+
if (parent === directory)
|
|
110
|
+
break;
|
|
111
|
+
directory = parent;
|
|
112
|
+
}
|
|
113
|
+
if (this.cache.size >= MAX_CACHE_ENTRIES)
|
|
114
|
+
this.cache.clear();
|
|
115
|
+
this.cache.set(candidate, result ?? null);
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
remember(sessionKey, repository, now) {
|
|
119
|
+
if (this.sticky.size >= MAX_STICKY_ENTRIES) {
|
|
120
|
+
for (const [key, entry] of this.sticky) {
|
|
121
|
+
if (entry.expiresAt <= now)
|
|
122
|
+
this.sticky.delete(key);
|
|
123
|
+
}
|
|
124
|
+
if (this.sticky.size >= MAX_STICKY_ENTRIES)
|
|
125
|
+
this.sticky.clear();
|
|
126
|
+
}
|
|
127
|
+
this.sticky.set(sessionKey, { repository, expiresAt: now + this.stickyTtlMs });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=projectDetector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectDetector.js","sourceRoot":"","sources":["../src/projectDetector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from 'fastify';
|
|
2
|
+
import type { ClientConfig } from '../config.js';
|
|
3
|
+
import type { UsageForwarder } from '../collector/forwarder.js';
|
|
4
|
+
import { ProjectDetector } from '../projectDetector.js';
|
|
5
|
+
export declare function createProxyHandler(config: ClientConfig, forwarder: UsageForwarder, detector?: ProjectDetector): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { PassThrough, Readable } from 'node:stream';
|
|
3
|
+
import { Agent, request as undiciRequest } from 'undici';
|
|
4
|
+
import { resolveAttribution } from '../attribution.js';
|
|
5
|
+
import { isStreamingResponse, UsageCollector } from '../usage/collector.js';
|
|
6
|
+
import { buildDownstreamHeaders, buildUpstreamHeaders } from './headers.js';
|
|
7
|
+
import { inspectRequest } from './inspectRequest.js';
|
|
8
|
+
/**
|
|
9
|
+
* Le risposte agentiche possono restare aperte per molti minuti: i timeout di
|
|
10
|
+
* default di undici (300s) interromperebbero gli stream lunghi.
|
|
11
|
+
*/
|
|
12
|
+
const dispatcher = new Agent({
|
|
13
|
+
headersTimeout: 0,
|
|
14
|
+
bodyTimeout: 0,
|
|
15
|
+
connect: { timeout: 30_000 },
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Header con cui i client identificano la finestra o la conversazione. Servono
|
|
19
|
+
* a tenere separate sessioni parallele dello stesso developer: senza, due
|
|
20
|
+
* progetti aperti insieme condividerebbero l'ereditarieta' del progetto.
|
|
21
|
+
*/
|
|
22
|
+
const SESSION_HEADERS = ['vscode-sessionid', 'x-vscode-session-id', 'x-session-id'];
|
|
23
|
+
function sessionKey(headers, bodySessionId, developerId) {
|
|
24
|
+
for (const name of SESSION_HEADERS) {
|
|
25
|
+
const value = headers[name];
|
|
26
|
+
if (typeof value === 'string' && value.length > 0)
|
|
27
|
+
return `s:${value}`;
|
|
28
|
+
}
|
|
29
|
+
return bodySessionId ? `s:${bodySessionId}` : `d:${developerId}`;
|
|
30
|
+
}
|
|
31
|
+
export function createProxyHandler(config, forwarder, detector) {
|
|
32
|
+
const upstream = new URL(config.upstreamBaseUrl);
|
|
33
|
+
return async function proxyHandler(request, reply) {
|
|
34
|
+
const startedAt = Date.now();
|
|
35
|
+
const rawBody = Buffer.isBuffer(request.body) ? request.body : Buffer.alloc(0);
|
|
36
|
+
const attribution = resolveAttribution({ path: request.url, headers: request.headers }, config);
|
|
37
|
+
const [pathname, search = ''] = attribution.upstreamPath.split('?', 2);
|
|
38
|
+
const facts = inspectRequest(pathname, rawBody);
|
|
39
|
+
// Se nessun segnale esplicito ha nominato il progetto, lo si deduce dai
|
|
40
|
+
// path citati nella richiesta: e' cio' che rende superflua la
|
|
41
|
+
// configurazione per singolo repository.
|
|
42
|
+
let { projectId, projectSource } = attribution;
|
|
43
|
+
let repository;
|
|
44
|
+
if (projectSource === 'fallback' && detector?.enabled) {
|
|
45
|
+
const detected = detector.detect(rawBody, sessionKey(request.headers, facts.sessionId, attribution.developerId));
|
|
46
|
+
if (detected) {
|
|
47
|
+
repository = detected.repository;
|
|
48
|
+
projectId = detected.repository.projectId;
|
|
49
|
+
projectSource = detected.source;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const targetUrl = `${upstream.origin}${upstream.pathname.replace(/\/$/, '')}${pathname}${search ? `?${search}` : ''}`;
|
|
53
|
+
const headers = buildUpstreamHeaders(request.headers, {
|
|
54
|
+
projectHeader: config.projectHeader,
|
|
55
|
+
overrideAuthToken: config.upstreamAuthToken,
|
|
56
|
+
upstreamHost: upstream.host,
|
|
57
|
+
});
|
|
58
|
+
if (facts.body.length > 0)
|
|
59
|
+
headers.set('content-length', String(facts.body.length));
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
// L'upstream va interrotto solo se e' il client a sganciarsi prima della
|
|
62
|
+
// fine: `request.raw` chiude gia' a corpo letto, quindi non e' un segnale
|
|
63
|
+
// valido di disconnessione.
|
|
64
|
+
reply.raw.on('close', () => {
|
|
65
|
+
if (!reply.raw.writableEnded)
|
|
66
|
+
controller.abort();
|
|
67
|
+
});
|
|
68
|
+
let upstreamResponse;
|
|
69
|
+
try {
|
|
70
|
+
upstreamResponse = await undiciRequest(targetUrl, {
|
|
71
|
+
method: request.method,
|
|
72
|
+
headers: Object.fromEntries(headers.entries()),
|
|
73
|
+
body: facts.body.length > 0 ? facts.body : undefined,
|
|
74
|
+
dispatcher,
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
request.log.error({ err: error, targetUrl }, 'richiesta upstream fallita');
|
|
80
|
+
await reply.code(502).send({ error: 'upstream_unreachable' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const responseHeaders = new Headers();
|
|
84
|
+
for (const [name, value] of Object.entries(upstreamResponse.headers)) {
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
for (const item of Array.isArray(value) ? value : [value])
|
|
88
|
+
responseHeaders.append(name, item);
|
|
89
|
+
}
|
|
90
|
+
const contentType = responseHeaders.get('content-type') ?? undefined;
|
|
91
|
+
const streaming = isStreamingResponse(contentType);
|
|
92
|
+
const collector = new UsageCollector(streaming);
|
|
93
|
+
const requestId = responseHeaders.get('x-request-id') ??
|
|
94
|
+
request.headers['x-request-id'] ??
|
|
95
|
+
randomUUID();
|
|
96
|
+
let recorded = false;
|
|
97
|
+
const record = () => {
|
|
98
|
+
if (recorded)
|
|
99
|
+
return;
|
|
100
|
+
recorded = true;
|
|
101
|
+
const parsed = collector.finish();
|
|
102
|
+
// Nessun contatore: probabilmente non e' una richiesta di inferenza
|
|
103
|
+
// (es. /models, health check). Non va registrata.
|
|
104
|
+
if (!parsed)
|
|
105
|
+
return;
|
|
106
|
+
forwarder.enqueue({
|
|
107
|
+
requestId,
|
|
108
|
+
occurredAt: new Date(startedAt).toISOString(),
|
|
109
|
+
developerId: attribution.developerId,
|
|
110
|
+
developerSource: attribution.developerSource,
|
|
111
|
+
projectId,
|
|
112
|
+
projectSource,
|
|
113
|
+
repository: repository ?? null,
|
|
114
|
+
clientName: attribution.clientName,
|
|
115
|
+
sessionId: facts.sessionId ?? null,
|
|
116
|
+
endpoint: pathname,
|
|
117
|
+
model: parsed.model ?? facts.model ?? null,
|
|
118
|
+
usage: {
|
|
119
|
+
inputTokens: parsed.inputTokens,
|
|
120
|
+
cachedInputTokens: parsed.cachedInputTokens,
|
|
121
|
+
cacheWriteTokens: parsed.cacheWriteTokens,
|
|
122
|
+
outputTokens: parsed.outputTokens,
|
|
123
|
+
},
|
|
124
|
+
streamed: streaming,
|
|
125
|
+
durationMs: Date.now() - startedAt,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
reply.code(upstreamResponse.statusCode);
|
|
129
|
+
reply.headers(buildDownstreamHeaders(responseHeaders));
|
|
130
|
+
const source = Readable.from(upstreamResponse.body, { objectMode: false });
|
|
131
|
+
const passthrough = new PassThrough();
|
|
132
|
+
source.on('data', (chunk) => {
|
|
133
|
+
try {
|
|
134
|
+
collector.push(chunk);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
request.log.warn({ err: error }, 'parsing usage fallito');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Anche in caso di interruzione i token di input sono stati consumati:
|
|
141
|
+
// si registra comunque quanto raccolto.
|
|
142
|
+
source.on('end', record);
|
|
143
|
+
source.on('error', record);
|
|
144
|
+
source.on('close', record);
|
|
145
|
+
source.pipe(passthrough);
|
|
146
|
+
await reply.send(passthrough);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//# 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,6 @@
|
|
|
1
|
+
export declare function buildUpstreamHeaders(incoming: Record<string, string | string[] | undefined>, options: {
|
|
2
|
+
projectHeader: string;
|
|
3
|
+
overrideAuthToken?: string | undefined;
|
|
4
|
+
upstreamHost: string;
|
|
5
|
+
}): Headers;
|
|
6
|
+
export declare function buildDownstreamHeaders(upstream: Headers): Record<string, string>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Header hop-by-hop: non vanno mai propagati oltre il proxy. */
|
|
2
|
+
const HOP_BY_HOP = new Set([
|
|
3
|
+
'connection',
|
|
4
|
+
'keep-alive',
|
|
5
|
+
'proxy-authenticate',
|
|
6
|
+
'proxy-authorization',
|
|
7
|
+
'te',
|
|
8
|
+
'trailer',
|
|
9
|
+
'transfer-encoding',
|
|
10
|
+
'upgrade',
|
|
11
|
+
]);
|
|
12
|
+
/** Header interni al proxy, non devono raggiungere l'upstream. */
|
|
13
|
+
const INTERNAL_HEADERS = new Set(['x-project-id', 'x-developer-id']);
|
|
14
|
+
export function buildUpstreamHeaders(incoming, options) {
|
|
15
|
+
const headers = new Headers();
|
|
16
|
+
for (const [name, value] of Object.entries(incoming)) {
|
|
17
|
+
const key = name.toLowerCase();
|
|
18
|
+
if (HOP_BY_HOP.has(key) || INTERNAL_HEADERS.has(key))
|
|
19
|
+
continue;
|
|
20
|
+
if (key === options.projectHeader)
|
|
21
|
+
continue;
|
|
22
|
+
if (key === 'host' || key === 'content-length')
|
|
23
|
+
continue;
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
continue;
|
|
26
|
+
for (const item of Array.isArray(value) ? value : [value])
|
|
27
|
+
headers.append(name, item);
|
|
28
|
+
}
|
|
29
|
+
headers.set('host', options.upstreamHost);
|
|
30
|
+
// Il corpo deve arrivare in chiaro: undici non decomprime automaticamente e
|
|
31
|
+
// uno stream SSE compresso sarebbe illeggibile sia per il parser di usage sia
|
|
32
|
+
// per il client, a cui inoltriamo i byte cosi' come li riceviamo.
|
|
33
|
+
headers.set('accept-encoding', 'identity');
|
|
34
|
+
if (options.overrideAuthToken) {
|
|
35
|
+
headers.set('authorization', `Bearer ${options.overrideAuthToken}`);
|
|
36
|
+
}
|
|
37
|
+
return headers;
|
|
38
|
+
}
|
|
39
|
+
export function buildDownstreamHeaders(upstream) {
|
|
40
|
+
const out = {};
|
|
41
|
+
upstream.forEach((value, name) => {
|
|
42
|
+
const key = name.toLowerCase();
|
|
43
|
+
if (HOP_BY_HOP.has(key))
|
|
44
|
+
return;
|
|
45
|
+
// La lunghezza viene ricalcolata da Node: il corpo passa in streaming.
|
|
46
|
+
if (key === 'content-length' || key === 'content-encoding')
|
|
47
|
+
return;
|
|
48
|
+
out[name] = value;
|
|
49
|
+
});
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/proxy/headers.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC;IACzB,YAAY;IACZ,YAAY;IACZ,oBAAoB;IACpB,qBAAqB;IACrB,IAAI;IACJ,SAAS;IACT,mBAAmB;IACnB,SAAS;CACV,CAAC,CAAC;AAEH,kEAAkE;AAClE,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAErE,MAAM,UAAU,oBAAoB,CAClC,QAAuD,EACvD,OAAgG;IAEhG,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrD,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAC/B,IAAI,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC/D,IAAI,GAAG,KAAK,OAAO,CAAC,aAAa;YAAE,SAAS;QAC5C,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,gBAAgB;YAAE,SAAS;QACzD,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAElC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACxF,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAE1C,4EAA4E;IAC5E,8EAA8E;IAC9E,kEAAkE;IAClE,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,UAAU,CAAC,CAAC;IAE3C,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,QAAiB;IACtD,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAC/B,IAAI,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO;QAChC,uEAAuE;QACvE,IAAI,GAAG,KAAK,gBAAgB,IAAI,GAAG,KAAK,kBAAkB;YAAE,OAAO;QACnE,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IACpB,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface RequestFacts {
|
|
2
|
+
model?: string;
|
|
3
|
+
sessionId?: string;
|
|
4
|
+
stream: boolean;
|
|
5
|
+
/** Corpo eventualmente riscritto per abilitare la contabilizzazione. */
|
|
6
|
+
body: Buffer;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Ispeziona il corpo della richiesta per ricavare modello e sessione, e
|
|
10
|
+
* garantisce che l'upstream restituisca i contatori di usage.
|
|
11
|
+
*
|
|
12
|
+
* Per gli endpoint in stile OpenAI l'usage in streaming e' opzionale e va
|
|
13
|
+
* richiesto esplicitamente con `stream_options.include_usage`; senza di esso
|
|
14
|
+
* la risposta non conterrebbe alcun contatore. Gli endpoint Anthropic lo
|
|
15
|
+
* includono sempre e il corpo resta invariato.
|
|
16
|
+
*/
|
|
17
|
+
export declare function inspectRequest(path: string, body: Buffer): RequestFacts;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const SESSION_PATTERN = /session_([0-9a-f-]{8,})/i;
|
|
2
|
+
function asObject(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Ispeziona il corpo della richiesta per ricavare modello e sessione, e
|
|
7
|
+
* garantisce che l'upstream restituisca i contatori di usage.
|
|
8
|
+
*
|
|
9
|
+
* Per gli endpoint in stile OpenAI l'usage in streaming e' opzionale e va
|
|
10
|
+
* richiesto esplicitamente con `stream_options.include_usage`; senza di esso
|
|
11
|
+
* la risposta non conterrebbe alcun contatore. Gli endpoint Anthropic lo
|
|
12
|
+
* includono sempre e il corpo resta invariato.
|
|
13
|
+
*/
|
|
14
|
+
export function inspectRequest(path, body) {
|
|
15
|
+
if (body.length === 0)
|
|
16
|
+
return { stream: false, body };
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(body.toString('utf-8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return { stream: false, body };
|
|
23
|
+
}
|
|
24
|
+
const root = asObject(parsed);
|
|
25
|
+
if (!root)
|
|
26
|
+
return { stream: false, body };
|
|
27
|
+
const model = typeof root.model === 'string' ? root.model : undefined;
|
|
28
|
+
const stream = root.stream === true;
|
|
29
|
+
let sessionId;
|
|
30
|
+
const metadata = asObject(root.metadata);
|
|
31
|
+
if (typeof metadata?.user_id === 'string') {
|
|
32
|
+
sessionId = SESSION_PATTERN.exec(metadata.user_id)?.[1];
|
|
33
|
+
}
|
|
34
|
+
const isOpenAiStyle = path.includes('/chat/completions') || path.includes('/responses');
|
|
35
|
+
if (stream && isOpenAiStyle && root.stream_options === undefined) {
|
|
36
|
+
root.stream_options = { include_usage: true };
|
|
37
|
+
return { model, sessionId, stream, body: Buffer.from(JSON.stringify(root), 'utf-8') };
|
|
38
|
+
}
|
|
39
|
+
return { model, sessionId, stream, body };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=inspectRequest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspectRequest.js","sourceRoot":"","sources":["../../src/proxy/inspectRequest.ts"],"names":[],"mappings":"AAQA,MAAM,eAAe,GAAG,0BAA0B,CAAC;AAEnD,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAE,KAAiC,CAAC,CAAC,CAAC,SAAS,CAAC;AACtG,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,IAAY;IACvD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAEtD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACjC,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC9B,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAE1C,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IACtE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC;IAEpC,IAAI,SAA6B,CAAC;IAClC,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,OAAO,QAAQ,EAAE,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC1C,SAAS,GAAG,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC,mBAAmB,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IACxF,IAAI,MAAM,IAAI,aAAa,IAAI,IAAI,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;QACjE,IAAI,CAAC,cAAc,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;QAC9C,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC;IACxF,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC5C,CAAC"}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import Fastify from 'fastify';
|
|
3
|
+
import { UsageForwarder } from './collector/forwarder.js';
|
|
4
|
+
import { createProxyHandler } from './proxy/handler.js';
|
|
5
|
+
import { ProjectDetector } from './projectDetector.js';
|
|
6
|
+
function createServer(config) {
|
|
7
|
+
const options = {
|
|
8
|
+
logger: { level: config.logLevel },
|
|
9
|
+
// Le sessioni agentiche possono restare aperte a lungo.
|
|
10
|
+
requestTimeout: 0,
|
|
11
|
+
keepAliveTimeout: 120_000,
|
|
12
|
+
bodyLimit: 64 * 1024 * 1024,
|
|
13
|
+
// Il log per-richiesta duplicherebbe il record di usage senza aggiungere
|
|
14
|
+
// nulla. Deprecato in Fastify 5, da migrare a `logController` in v6.
|
|
15
|
+
disableRequestLogging: true,
|
|
16
|
+
};
|
|
17
|
+
if (!config.tlsKeyPath || !config.tlsCertPath)
|
|
18
|
+
return Fastify(options);
|
|
19
|
+
// In TLS l'istanza Fastify e' tipizzata su https.Server: l'interfaccia
|
|
20
|
+
// pubblica usata qui e' identica, quindi si normalizza il tipo.
|
|
21
|
+
return Fastify({
|
|
22
|
+
...options,
|
|
23
|
+
https: {
|
|
24
|
+
key: readFileSync(config.tlsKeyPath),
|
|
25
|
+
cert: readFileSync(config.tlsCertPath),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export async function buildApp(config) {
|
|
30
|
+
const server = createServer(config);
|
|
31
|
+
// Il proxy deve inoltrare il corpo byte per byte, senza reinterpretarlo.
|
|
32
|
+
server.removeAllContentTypeParsers();
|
|
33
|
+
server.addContentTypeParser('*', { parseAs: 'buffer' }, (_request, body, done) => {
|
|
34
|
+
done(null, body);
|
|
35
|
+
});
|
|
36
|
+
const forwarder = new UsageForwarder(config, server.log);
|
|
37
|
+
forwarder.start();
|
|
38
|
+
const detector = new ProjectDetector({
|
|
39
|
+
roots: config.workspaceRoots,
|
|
40
|
+
markers: config.projectMarkers,
|
|
41
|
+
stickyTtlMs: config.projectStickyTtlMs,
|
|
42
|
+
maxScanBytes: 4 * 1024 * 1024,
|
|
43
|
+
});
|
|
44
|
+
if (!detector.enabled) {
|
|
45
|
+
server.log.warn('WORKSPACE_ROOTS non impostato: il progetto va dichiarato esplicitamente, altrimenti il traffico finisce in `unassigned`');
|
|
46
|
+
}
|
|
47
|
+
server.get('/_health', async () => ({
|
|
48
|
+
ok: true,
|
|
49
|
+
upstream: config.upstreamBaseUrl,
|
|
50
|
+
collector: config.collectorUrl,
|
|
51
|
+
projectDetection: detector.enabled,
|
|
52
|
+
}));
|
|
53
|
+
server.route({
|
|
54
|
+
method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'],
|
|
55
|
+
url: '/*',
|
|
56
|
+
handler: createProxyHandler(config, forwarder, detector),
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
server,
|
|
60
|
+
close: async () => {
|
|
61
|
+
await server.close();
|
|
62
|
+
await forwarder.close();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ParsedUsage } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Raccoglitore di usage indipendente dal dialetto upstream.
|
|
4
|
+
*
|
|
5
|
+
* Il proxy non sa a priori se la risposta e' in formato Anthropic Messages o
|
|
6
|
+
* OpenAI chat completions (dipende da come e' configurato l'upstream), quindi
|
|
7
|
+
* alimenta entrambi gli accumulatori e tiene il primo che produce un risultato.
|
|
8
|
+
* Gli eventi dell'uno sono inerti per l'altro, perche' le forme dei payload
|
|
9
|
+
* non si sovrappongono.
|
|
10
|
+
*/
|
|
11
|
+
export declare class UsageCollector {
|
|
12
|
+
private readonly streaming;
|
|
13
|
+
private readonly anthropic;
|
|
14
|
+
private readonly openai;
|
|
15
|
+
private readonly sse;
|
|
16
|
+
private nonStreamChunks;
|
|
17
|
+
private nonStreamBytes;
|
|
18
|
+
/** Oltre questa soglia si smette di bufferizzare una risposta non-stream. */
|
|
19
|
+
private static readonly MAX_BUFFERED_BYTES;
|
|
20
|
+
constructor(streaming: boolean);
|
|
21
|
+
push(chunk: Uint8Array): void;
|
|
22
|
+
finish(): ParsedUsage | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Euristica: la risposta e' uno stream SSE? */
|
|
25
|
+
export declare function isStreamingResponse(contentType: string | undefined): boolean;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { AnthropicUsageAccumulator, parseAnthropicResponse } from './parseAnthropic.js';
|
|
2
|
+
import { OpenAIUsageAccumulator, parseOpenAIResponse } from './parseOpenAI.js';
|
|
3
|
+
import { SseParser } from './sse.js';
|
|
4
|
+
/**
|
|
5
|
+
* Raccoglitore di usage indipendente dal dialetto upstream.
|
|
6
|
+
*
|
|
7
|
+
* Il proxy non sa a priori se la risposta e' in formato Anthropic Messages o
|
|
8
|
+
* OpenAI chat completions (dipende da come e' configurato l'upstream), quindi
|
|
9
|
+
* alimenta entrambi gli accumulatori e tiene il primo che produce un risultato.
|
|
10
|
+
* Gli eventi dell'uno sono inerti per l'altro, perche' le forme dei payload
|
|
11
|
+
* non si sovrappongono.
|
|
12
|
+
*/
|
|
13
|
+
export class UsageCollector {
|
|
14
|
+
streaming;
|
|
15
|
+
anthropic = new AnthropicUsageAccumulator();
|
|
16
|
+
openai = new OpenAIUsageAccumulator();
|
|
17
|
+
sse;
|
|
18
|
+
nonStreamChunks = [];
|
|
19
|
+
nonStreamBytes = 0;
|
|
20
|
+
/** Oltre questa soglia si smette di bufferizzare una risposta non-stream. */
|
|
21
|
+
static MAX_BUFFERED_BYTES = 8 * 1024 * 1024;
|
|
22
|
+
constructor(streaming) {
|
|
23
|
+
this.streaming = streaming;
|
|
24
|
+
this.sse = new SseParser((payload) => {
|
|
25
|
+
this.anthropic.handleEvent(payload);
|
|
26
|
+
this.openai.handleEvent(payload);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
push(chunk) {
|
|
30
|
+
if (this.streaming) {
|
|
31
|
+
this.sse.push(chunk);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (this.nonStreamBytes + chunk.byteLength > UsageCollector.MAX_BUFFERED_BYTES)
|
|
35
|
+
return;
|
|
36
|
+
this.nonStreamChunks.push(chunk);
|
|
37
|
+
this.nonStreamBytes += chunk.byteLength;
|
|
38
|
+
}
|
|
39
|
+
finish() {
|
|
40
|
+
if (this.streaming) {
|
|
41
|
+
this.sse.end();
|
|
42
|
+
return this.anthropic.result() ?? this.openai.result();
|
|
43
|
+
}
|
|
44
|
+
if (this.nonStreamChunks.length === 0)
|
|
45
|
+
return undefined;
|
|
46
|
+
let body;
|
|
47
|
+
try {
|
|
48
|
+
body = JSON.parse(Buffer.concat(this.nonStreamChunks).toString('utf-8'));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
this.nonStreamChunks = [];
|
|
55
|
+
}
|
|
56
|
+
return parseAnthropicResponse(body) ?? parseOpenAIResponse(body);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Euristica: la risposta e' uno stream SSE? */
|
|
60
|
+
export function isStreamingResponse(contentType) {
|
|
61
|
+
return (contentType ?? '').toLowerCase().includes('text/event-stream');
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=collector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collector.js","sourceRoot":"","sources":["../../src/usage/collector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACxF,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAGrC;;;;;;;;GAQG;AACH,MAAM,OAAO,cAAc;IAUI;IATZ,SAAS,GAAG,IAAI,yBAAyB,EAAE,CAAC;IAC5C,MAAM,GAAG,IAAI,sBAAsB,EAAE,CAAC;IACtC,GAAG,CAAY;IACxB,eAAe,GAAiB,EAAE,CAAC;IACnC,cAAc,GAAG,CAAC,CAAC;IAE3B,6EAA6E;IACrE,MAAM,CAAU,kBAAkB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAE7D,YAA6B,SAAkB;QAAlB,cAAS,GAAT,SAAS,CAAS;QAC7C,IAAI,CAAC,GAAG,GAAG,IAAI,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE;YACnC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YACpC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC,KAAiB;QACpB,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,UAAU,GAAG,cAAc,CAAC,kBAAkB;YAAE,OAAO;QACvF,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,IAAI,CAAC,cAAc,IAAI,KAAK,CAAC,UAAU,CAAC;IAC1C,CAAC;IAED,MAAM;QACJ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;YACf,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACzD,CAAC;QAED,IAAI,IAAI,CAAC,eAAe,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QACxD,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QAC3E,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC;QACnB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,eAAe,GAAG,EAAE,CAAC;QAC5B,CAAC;QACD,OAAO,sBAAsB,CAAC,IAAI,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,CAAC;IACnE,CAAC;;AAGH,gDAAgD;AAChD,MAAM,UAAU,mBAAmB,CAAC,WAA+B;IACjE,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC;AACzE,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ParsedUsage } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Estrae i token da un blocco `usage` in formato Anthropic Messages API.
|
|
4
|
+
*
|
|
5
|
+
* In Anthropic `input_tokens` esclude gia' i token di cache, quindi non
|
|
6
|
+
* serve alcuna sottrazione.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readAnthropicUsageBlock(usage: unknown): ParsedUsage | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Accumulatore per uno stream Anthropic.
|
|
11
|
+
*
|
|
12
|
+
* Lo stream distribuisce l'informazione su due eventi:
|
|
13
|
+
* - `message_start` porta gli input token (definitivi) e un output parziale
|
|
14
|
+
* - `message_delta` porta gli output token cumulativi finali
|
|
15
|
+
*/
|
|
16
|
+
export declare class AnthropicUsageAccumulator {
|
|
17
|
+
private usage;
|
|
18
|
+
private seen;
|
|
19
|
+
handleEvent(payload: unknown): void;
|
|
20
|
+
result(): ParsedUsage | undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Risposta Anthropic non-streaming. */
|
|
23
|
+
export declare function parseAnthropicResponse(body: unknown): ParsedUsage | undefined;
|