@agent-relay/credential-proxy 4.0.10 → 4.0.36
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 +71 -0
- package/dist/crypto-polyfill.d.ts +2 -0
- package/dist/crypto-polyfill.d.ts.map +1 -0
- package/dist/crypto-polyfill.js +8 -0
- package/dist/crypto-polyfill.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +20 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +102 -0
- package/dist/jwt.js.map +1 -0
- package/dist/metering.d.ts +27 -0
- package/dist/metering.d.ts.map +1 -0
- package/dist/metering.js +82 -0
- package/dist/metering.js.map +1 -0
- package/dist/providers/anthropic.d.ts +11 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +39 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/index.d.ts +9 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +27 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/openai.d.ts +11 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +56 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/openrouter.d.ts +11 -0
- package/dist/providers/openrouter.d.ts.map +1 -0
- package/dist/providers/openrouter.js +60 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/types.d.ts +36 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +248 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/router.d.ts +29 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +325 -0
- package/dist/router.js.map +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +4 -2
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @agent-relay/credential-proxy
|
|
2
|
+
|
|
3
|
+
JWT-authenticated credential proxy for upstream LLM providers. Lets sandboxed
|
|
4
|
+
agents call provider APIs (OpenAI, Anthropic, OpenRouter) without being given
|
|
5
|
+
raw provider credentials — the proxy holds the keys, agents present per-session
|
|
6
|
+
JWTs, and the proxy enforces per-credential token budgets.
|
|
7
|
+
|
|
8
|
+
## What it is
|
|
9
|
+
|
|
10
|
+
A Hono app plus a handful of helpers:
|
|
11
|
+
|
|
12
|
+
- `createCredentialProxyApp(options)` — mounts the HTTP router (`/health`,
|
|
13
|
+
`/usage`, and a catch-all provider-forwarding route under `*`). Use it
|
|
14
|
+
directly in Node (via `@hono/node-server`) or bind it inside a Cloudflare
|
|
15
|
+
Worker.
|
|
16
|
+
- `mintProxyToken(...)` / `verifyProxyToken(...)` — JWT helpers built on
|
|
17
|
+
[`jose`](https://github.com/panva/jose). HS256 by default, audience
|
|
18
|
+
`relay-llm-proxy`.
|
|
19
|
+
- `MeteringCollector` + `checkBudget` — in-memory usage accounting with
|
|
20
|
+
pessimistic reservations so concurrent requests can't bypass the declared
|
|
21
|
+
budget.
|
|
22
|
+
- Provider adapters under `providers/` — translate incoming JWT claims to the
|
|
23
|
+
correct upstream HTTP request for OpenAI / Anthropic / OpenRouter.
|
|
24
|
+
|
|
25
|
+
## Environment variables
|
|
26
|
+
|
|
27
|
+
Required at runtime (the host of the proxy):
|
|
28
|
+
|
|
29
|
+
| Variable | Purpose |
|
|
30
|
+
| ----------------------------------- | ------------------------------------------------------------ |
|
|
31
|
+
| `CREDENTIAL_PROXY_JWT_SECRET` | HS256 secret the proxy uses to verify per-session JWTs. |
|
|
32
|
+
| `CREDENTIAL_PROXY_ADMIN_JWT_SECRET` | Secret for admin-scoped tokens (e.g. the `/usage` endpoint). |
|
|
33
|
+
| `CREDENTIAL_PROXY_ADMIN_AUDIENCE` | Audience claim the proxy requires on admin tokens. |
|
|
34
|
+
| `OPENAI_API_KEY` | Upstream credential for the OpenAI provider adapter. |
|
|
35
|
+
| `ANTHROPIC_API_KEY` | Upstream credential for the Anthropic provider adapter. |
|
|
36
|
+
| `OPENROUTER_API_KEY` | Upstream credential for the OpenRouter provider adapter. |
|
|
37
|
+
|
|
38
|
+
Only the provider keys you actually forward to need to be set — missing keys
|
|
39
|
+
surface as `502 credential_unavailable` on the relevant route.
|
|
40
|
+
|
|
41
|
+
The agent side (whichever process mints tokens and launches the sandboxed CLI)
|
|
42
|
+
uses `CREDENTIAL_PROXY_JWT_SECRET` to sign tokens and puts them into the agent's
|
|
43
|
+
environment as `CREDENTIAL_PROXY_TOKEN` (alias: `RELAY_LLM_PROXY_TOKEN`) plus
|
|
44
|
+
`RELAY_LLM_PROXY` / `RELAY_LLM_PROXY_URL` so the SDK's `proxy-env` helpers can
|
|
45
|
+
wire up per-CLI `OPENAI_BASE_URL` / `ANTHROPIC_BASE_URL` overrides.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { serve } from '@hono/node-server';
|
|
51
|
+
import { createCredentialProxyApp } from '@agent-relay/credential-proxy';
|
|
52
|
+
|
|
53
|
+
const app = createCredentialProxyApp({
|
|
54
|
+
// Defaults to process.env.CREDENTIAL_PROXY_JWT_SECRET / ADMIN_JWT_SECRET.
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
serve({ fetch: app.fetch, port: Number(process.env.PORT ?? 3001) });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For the SDK-side wiring that lets workflow agents use the proxy transparently,
|
|
61
|
+
see [`@agent-relay/sdk/workflows`'s proxy-env
|
|
62
|
+
module](../sdk/src/workflows/proxy-env.ts) and the `credentialProxy` field on
|
|
63
|
+
`SwarmConfig`.
|
|
64
|
+
|
|
65
|
+
## Development
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm run build # tsc → dist/
|
|
69
|
+
npm run test # vitest
|
|
70
|
+
npm run dev # builds then runs the proxy on PORT (default 3001)
|
|
71
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto-polyfill.d.ts","sourceRoot":"","sources":["../src/crypto-polyfill.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// jose@^6 ships only a webapi build and requires globalThis.crypto. Node 18
|
|
2
|
+
// gates this behind --experimental-global-webcrypto, so expose it explicitly.
|
|
3
|
+
import { webcrypto } from 'node:crypto';
|
|
4
|
+
const target = globalThis;
|
|
5
|
+
if (typeof target.crypto === 'undefined') {
|
|
6
|
+
target.crypto = webcrypto;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=crypto-polyfill.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto-polyfill.js","sourceRoot":"","sources":["../src/crypto-polyfill.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,8EAA8E;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,MAAM,GAAG,UAAqC,CAAC;AACrD,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;IACzC,MAAM,CAAC,MAAM,GAAG,SAAS,CAAC;AAC5B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { type CredentialProxyApp, type CredentialProxyOptions } from './router.js';
|
|
3
|
+
export { app, createCredentialProxyApp } from './router.js';
|
|
4
|
+
export { MeteringCollector, checkBudget, DEFAULT_BUDGET_RESERVATION } from './metering.js';
|
|
5
|
+
export { mintProxyToken, verifyProxyToken } from './jwt.js';
|
|
6
|
+
export * from './providers/index.js';
|
|
7
|
+
export type * from './providers/types.js';
|
|
8
|
+
export type { AdminTokenClaims, MeteringRecord, ProviderType, ProxyRequest, ProxyResponse, ProxyTokenClaims, UsageSummary, } from './types.js';
|
|
9
|
+
export type { CredentialProxyOptions, CredentialProxyVariables, CredentialStore, } from './router.js';
|
|
10
|
+
export interface StandaloneServeOptions extends CredentialProxyOptions {
|
|
11
|
+
app?: CredentialProxyApp;
|
|
12
|
+
hostname?: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
}
|
|
15
|
+
export type CredentialProxyServer = ReturnType<typeof createServer>;
|
|
16
|
+
export declare function serve(options?: StandaloneServeOptions): Promise<CredentialProxyServer>;
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA6C,MAAM,WAAW,CAAC;AAKpF,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC5B,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,GAAG,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC3F,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5D,cAAc,sBAAsB,CAAC;AACrC,mBAAmB,sBAAsB,CAAC;AAC1C,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,sBAAsB,EACtB,wBAAwB,EACxB,eAAe,GAChB,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,sBAAuB,SAAQ,sBAAsB;IACpE,GAAG,CAAC,EAAE,kBAAkB,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,qBAAqB,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AAEpE,wBAAsB,KAAK,CAAC,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA4BhG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { once } from 'node:events';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { createCredentialProxyApp, } from './router.js';
|
|
6
|
+
export { app, createCredentialProxyApp } from './router.js';
|
|
7
|
+
export { MeteringCollector, checkBudget, DEFAULT_BUDGET_RESERVATION } from './metering.js';
|
|
8
|
+
export { mintProxyToken, verifyProxyToken } from './jwt.js';
|
|
9
|
+
export * from './providers/index.js';
|
|
10
|
+
export async function serve(options = {}) {
|
|
11
|
+
const proxyApp = options.app ?? createCredentialProxyApp(options);
|
|
12
|
+
const port = options.port ?? Number.parseInt(process.env.PORT ?? '3001', 10);
|
|
13
|
+
const hostname = options.hostname ?? process.env.HOST ?? '0.0.0.0';
|
|
14
|
+
const server = createServer(async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const request = createWebRequest(req);
|
|
17
|
+
const response = await proxyApp.fetch(request);
|
|
18
|
+
await writeWebResponse(res, response);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error('[credential-proxy] standalone server error', error);
|
|
22
|
+
res.statusCode = 500;
|
|
23
|
+
res.setHeader('content-type', 'application/json');
|
|
24
|
+
res.end(JSON.stringify({ error: 'Internal server error', code: 'internal_error' }));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
server.once('error', reject);
|
|
29
|
+
server.listen(port, hostname, () => {
|
|
30
|
+
server.off('error', reject);
|
|
31
|
+
resolve();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
console.log(`credential-proxy listening on http://${hostname}:${port}`);
|
|
35
|
+
return server;
|
|
36
|
+
}
|
|
37
|
+
function createWebRequest(req) {
|
|
38
|
+
const protocol = 'encrypted' in req.socket && req.socket.encrypted ? 'https' : 'http';
|
|
39
|
+
const host = req.headers.host ?? 'localhost';
|
|
40
|
+
const url = new URL(req.url ?? '/', `${protocol}://${host}`);
|
|
41
|
+
const headers = new Headers();
|
|
42
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
for (const item of value) {
|
|
45
|
+
headers.append(key, item);
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === 'string') {
|
|
50
|
+
headers.set(key, value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const method = req.method ?? 'GET';
|
|
54
|
+
const body = method === 'GET' || method === 'HEAD'
|
|
55
|
+
? undefined
|
|
56
|
+
: Readable.toWeb(req);
|
|
57
|
+
return new Request(url, {
|
|
58
|
+
method,
|
|
59
|
+
headers,
|
|
60
|
+
body,
|
|
61
|
+
duplex: body ? 'half' : undefined,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function writeWebResponse(res, response) {
|
|
65
|
+
res.statusCode = response.status;
|
|
66
|
+
res.statusMessage = response.statusText;
|
|
67
|
+
response.headers.forEach((value, key) => {
|
|
68
|
+
res.setHeader(key, value);
|
|
69
|
+
});
|
|
70
|
+
if (!response.body) {
|
|
71
|
+
res.end();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const readable = Readable.fromWeb(response.body);
|
|
75
|
+
readable.on('error', (error) => {
|
|
76
|
+
console.error('[credential-proxy] response streaming error', error);
|
|
77
|
+
res.destroy(error);
|
|
78
|
+
});
|
|
79
|
+
for await (const chunk of readable) {
|
|
80
|
+
if (!res.write(chunk)) {
|
|
81
|
+
await once(res, 'drain');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
res.end();
|
|
85
|
+
}
|
|
86
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
87
|
+
void serve();
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/jwt.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import './crypto-polyfill.js';
|
|
2
|
+
import type { ProxyTokenClaims } from './types.js';
|
|
3
|
+
export type { ProxyTokenClaims } from './types.js';
|
|
4
|
+
export declare const PROXY_TOKEN_AUDIENCE = "relay-llm-proxy";
|
|
5
|
+
export declare const DEFAULT_PROXY_TOKEN_TTL_SECONDS: number;
|
|
6
|
+
export declare class TokenInvalidError extends Error {
|
|
7
|
+
readonly cause?: unknown;
|
|
8
|
+
constructor(message?: string, options?: {
|
|
9
|
+
cause?: unknown;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export declare class TokenExpiredError extends Error {
|
|
13
|
+
readonly cause?: unknown;
|
|
14
|
+
constructor(message?: string, options?: {
|
|
15
|
+
cause?: unknown;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export declare function mintProxyToken(claims: ProxyTokenClaims, secret: string): Promise<string>;
|
|
19
|
+
export declare function verifyProxyToken(token: string, secret: string): Promise<ProxyTokenClaims>;
|
|
20
|
+
//# sourceMappingURL=jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,gBAAgB,EAAgB,MAAM,YAAY,CAAC;AAEjE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,eAAO,MAAM,+BAA+B,QAAU,CAAC;AAIvD,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEb,OAAO,SAAwB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAK3E;AAED,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEb,OAAO,SAAwB,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAK3E;AAiED,wBAAsB,cAAc,CAAC,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc9F;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmB/F"}
|
package/dist/jwt.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import './crypto-polyfill.js';
|
|
2
|
+
import { SignJWT, errors, jwtVerify } from 'jose';
|
|
3
|
+
export const PROXY_TOKEN_AUDIENCE = 'relay-llm-proxy';
|
|
4
|
+
export const DEFAULT_PROXY_TOKEN_TTL_SECONDS = 15 * 60;
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
export class TokenInvalidError extends Error {
|
|
7
|
+
cause;
|
|
8
|
+
constructor(message = 'Invalid proxy token', options) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'TokenInvalidError';
|
|
11
|
+
this.cause = options?.cause;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class TokenExpiredError extends Error {
|
|
15
|
+
cause;
|
|
16
|
+
constructor(message = 'Proxy token expired', options) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'TokenExpiredError';
|
|
19
|
+
this.cause = options?.cause;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function getSecretKey(secret) {
|
|
23
|
+
return encoder.encode(secret);
|
|
24
|
+
}
|
|
25
|
+
function isProviderType(value) {
|
|
26
|
+
return value === 'openai' || value === 'anthropic' || value === 'openrouter';
|
|
27
|
+
}
|
|
28
|
+
function normalizeAudience(value) {
|
|
29
|
+
if (value === PROXY_TOKEN_AUDIENCE) {
|
|
30
|
+
return PROXY_TOKEN_AUDIENCE;
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(value) && value.includes(PROXY_TOKEN_AUDIENCE)) {
|
|
33
|
+
return PROXY_TOKEN_AUDIENCE;
|
|
34
|
+
}
|
|
35
|
+
throw new TokenInvalidError('Invalid proxy token audience');
|
|
36
|
+
}
|
|
37
|
+
function normalizeClaims(payload) {
|
|
38
|
+
const { sub, aud, provider, credentialId, budget, exp } = payload;
|
|
39
|
+
if (typeof sub !== 'string' || sub.length === 0) {
|
|
40
|
+
throw new TokenInvalidError('Invalid proxy token subject');
|
|
41
|
+
}
|
|
42
|
+
if (!isProviderType(provider)) {
|
|
43
|
+
throw new TokenInvalidError('Invalid proxy token provider');
|
|
44
|
+
}
|
|
45
|
+
if (typeof credentialId !== 'string' || credentialId.length === 0) {
|
|
46
|
+
throw new TokenInvalidError('Invalid proxy token credentialId');
|
|
47
|
+
}
|
|
48
|
+
if (budget !== undefined && (typeof budget !== 'number' || !Number.isFinite(budget) || budget < 0)) {
|
|
49
|
+
throw new TokenInvalidError('Invalid proxy token budget');
|
|
50
|
+
}
|
|
51
|
+
if (typeof exp !== 'number' || !Number.isFinite(exp)) {
|
|
52
|
+
throw new TokenInvalidError('Invalid proxy token expiration');
|
|
53
|
+
}
|
|
54
|
+
const normalizedBudget = typeof budget === 'number' ? budget : undefined;
|
|
55
|
+
return {
|
|
56
|
+
sub,
|
|
57
|
+
aud: normalizeAudience(aud),
|
|
58
|
+
provider,
|
|
59
|
+
credentialId,
|
|
60
|
+
budget: normalizedBudget,
|
|
61
|
+
exp: Math.floor(exp),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function resolveExpirationTime(exp) {
|
|
65
|
+
if (typeof exp === 'number' && Number.isFinite(exp)) {
|
|
66
|
+
return Math.floor(exp);
|
|
67
|
+
}
|
|
68
|
+
return Math.floor(Date.now() / 1000) + DEFAULT_PROXY_TOKEN_TTL_SECONDS;
|
|
69
|
+
}
|
|
70
|
+
export async function mintProxyToken(claims, secret) {
|
|
71
|
+
const expirationTime = resolveExpirationTime(claims.exp);
|
|
72
|
+
return new SignJWT({
|
|
73
|
+
provider: claims.provider,
|
|
74
|
+
credentialId: claims.credentialId,
|
|
75
|
+
...(claims.budget === undefined ? {} : { budget: claims.budget }),
|
|
76
|
+
})
|
|
77
|
+
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
|
|
78
|
+
.setSubject(claims.sub)
|
|
79
|
+
.setAudience(claims.aud)
|
|
80
|
+
.setIssuedAt()
|
|
81
|
+
.setExpirationTime(expirationTime)
|
|
82
|
+
.sign(getSecretKey(secret));
|
|
83
|
+
}
|
|
84
|
+
export async function verifyProxyToken(token, secret) {
|
|
85
|
+
try {
|
|
86
|
+
const { payload } = await jwtVerify(token, getSecretKey(secret), {
|
|
87
|
+
algorithms: ['HS256'],
|
|
88
|
+
audience: PROXY_TOKEN_AUDIENCE,
|
|
89
|
+
});
|
|
90
|
+
return normalizeClaims(payload);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof errors.JWTExpired) {
|
|
94
|
+
throw new TokenExpiredError('Proxy token expired', { cause: error });
|
|
95
|
+
}
|
|
96
|
+
if (error instanceof TokenInvalidError) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
throw new TokenInvalidError('Proxy token is invalid', { cause: error });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=jwt.js.map
|
package/dist/jwt.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MeteringRecord, ProxyTokenClaims, UsageSummary } from './types.js';
|
|
2
|
+
export declare class MeteringCollector {
|
|
3
|
+
private readonly buffer;
|
|
4
|
+
private readonly pendingTokens;
|
|
5
|
+
record(entry: MeteringRecord): void;
|
|
6
|
+
/**
|
|
7
|
+
* Reserve tokens against a workspace budget before forwarding the request.
|
|
8
|
+
* This prevents concurrent requests from bypassing budget limits (TOCTOU).
|
|
9
|
+
*/
|
|
10
|
+
reservePending(workspaceId: string, tokens: number): void;
|
|
11
|
+
/**
|
|
12
|
+
* Release a pending reservation (after metering the actual usage or on failure).
|
|
13
|
+
*/
|
|
14
|
+
releasePending(workspaceId: string, tokens: number): void;
|
|
15
|
+
getPendingTokens(workspaceId: string): number;
|
|
16
|
+
flush(): MeteringRecord[];
|
|
17
|
+
getUsageByWorkspace(workspaceId: string): UsageSummary;
|
|
18
|
+
getUsageByCredential(credentialId: string): UsageSummary;
|
|
19
|
+
getTotalUsage(): UsageSummary;
|
|
20
|
+
}
|
|
21
|
+
export declare function checkBudget(claims: ProxyTokenClaims, collector: MeteringCollector): {
|
|
22
|
+
allowed: boolean;
|
|
23
|
+
remaining?: number;
|
|
24
|
+
};
|
|
25
|
+
/** Default pessimistic token reservation for in-flight requests. */
|
|
26
|
+
export declare const DEFAULT_BUDGET_RESERVATION = 4096;
|
|
27
|
+
//# sourceMappingURL=metering.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metering.d.ts","sourceRoot":"","sources":["../src/metering.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA8BjF,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;IAC/C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAE3D,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAInC;;;OAGG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAKzD;;OAEG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAUzD,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAI7C,KAAK,IAAI,cAAc,EAAE;IAQzB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY;IAItD,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,YAAY;IAIxD,aAAa,IAAI,YAAY;CAG9B;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,iBAAiB,GAC3B;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAa1C;AAED,oEAAoE;AACpE,eAAO,MAAM,0BAA0B,OAAO,CAAC"}
|
package/dist/metering.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
function createEmptyUsageSummary() {
|
|
2
|
+
return {
|
|
3
|
+
inputTokens: 0,
|
|
4
|
+
outputTokens: 0,
|
|
5
|
+
requests: 0,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function aggregateUsage(records, predicate) {
|
|
9
|
+
return records.reduce((summary, record) => {
|
|
10
|
+
if (predicate && !predicate(record)) {
|
|
11
|
+
return summary;
|
|
12
|
+
}
|
|
13
|
+
summary.inputTokens += record.inputTokens;
|
|
14
|
+
summary.outputTokens += record.outputTokens;
|
|
15
|
+
summary.requests += 1;
|
|
16
|
+
return summary;
|
|
17
|
+
}, createEmptyUsageSummary());
|
|
18
|
+
}
|
|
19
|
+
function getConsumedBudget(summary) {
|
|
20
|
+
return summary.inputTokens + summary.outputTokens;
|
|
21
|
+
}
|
|
22
|
+
export class MeteringCollector {
|
|
23
|
+
buffer = [];
|
|
24
|
+
pendingTokens = new Map();
|
|
25
|
+
record(entry) {
|
|
26
|
+
this.buffer.push({ ...entry });
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Reserve tokens against a workspace budget before forwarding the request.
|
|
30
|
+
* This prevents concurrent requests from bypassing budget limits (TOCTOU).
|
|
31
|
+
*/
|
|
32
|
+
reservePending(workspaceId, tokens) {
|
|
33
|
+
const current = this.pendingTokens.get(workspaceId) ?? 0;
|
|
34
|
+
this.pendingTokens.set(workspaceId, current + tokens);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Release a pending reservation (after metering the actual usage or on failure).
|
|
38
|
+
*/
|
|
39
|
+
releasePending(workspaceId, tokens) {
|
|
40
|
+
const current = this.pendingTokens.get(workspaceId) ?? 0;
|
|
41
|
+
const next = Math.max(0, current - tokens);
|
|
42
|
+
if (next === 0) {
|
|
43
|
+
this.pendingTokens.delete(workspaceId);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
this.pendingTokens.set(workspaceId, next);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
getPendingTokens(workspaceId) {
|
|
50
|
+
return this.pendingTokens.get(workspaceId) ?? 0;
|
|
51
|
+
}
|
|
52
|
+
flush() {
|
|
53
|
+
const flushed = this.buffer.map((entry) => ({ ...entry }));
|
|
54
|
+
this.buffer.length = 0;
|
|
55
|
+
// TODO: Replace the in-memory buffer with a durable backend flush target.
|
|
56
|
+
return flushed;
|
|
57
|
+
}
|
|
58
|
+
getUsageByWorkspace(workspaceId) {
|
|
59
|
+
return aggregateUsage(this.buffer, (record) => record.workspaceId === workspaceId);
|
|
60
|
+
}
|
|
61
|
+
getUsageByCredential(credentialId) {
|
|
62
|
+
return aggregateUsage(this.buffer, (record) => record.credentialId === credentialId);
|
|
63
|
+
}
|
|
64
|
+
getTotalUsage() {
|
|
65
|
+
return aggregateUsage(this.buffer);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function checkBudget(claims, collector) {
|
|
69
|
+
if (claims.budget === undefined) {
|
|
70
|
+
return { allowed: true };
|
|
71
|
+
}
|
|
72
|
+
const usage = collector.getUsageByWorkspace(claims.sub);
|
|
73
|
+
const consumed = getConsumedBudget(usage) + collector.getPendingTokens(claims.sub);
|
|
74
|
+
const remaining = Math.max(0, claims.budget - consumed);
|
|
75
|
+
return {
|
|
76
|
+
allowed: consumed < claims.budget,
|
|
77
|
+
remaining,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Default pessimistic token reservation for in-flight requests. */
|
|
81
|
+
export const DEFAULT_BUDGET_RESERVATION = 4096;
|
|
82
|
+
//# sourceMappingURL=metering.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metering.js","sourceRoot":"","sources":["../src/metering.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ProviderAdapter, type TokenUsage } from './types.js';
|
|
2
|
+
export declare class AnthropicProviderAdapter implements ProviderAdapter {
|
|
3
|
+
readonly name: "anthropic";
|
|
4
|
+
readonly baseUrl = "https://api.anthropic.com";
|
|
5
|
+
authHeader(apiKey: string): Record<string, string>;
|
|
6
|
+
matchesPath(path: string): boolean;
|
|
7
|
+
forwardRequest(req: Request, apiKey: string): Promise<Response>;
|
|
8
|
+
extractUsage(response: Response | object): TokenUsage | null;
|
|
9
|
+
}
|
|
10
|
+
export declare const anthropicProviderAdapter: AnthropicProviderAdapter;
|
|
11
|
+
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,UAAU,EAChB,MAAM,YAAY,CAAC;AAUpB,qBAAa,wBAAyB,YAAW,eAAe;IAC9D,QAAQ,CAAC,IAAI,EAAG,WAAW,CAAU;IACrC,QAAQ,CAAC,OAAO,+BAA+B;IAE/C,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAOlD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIlC,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAU/D,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,IAAI;CAO7D;AAED,eAAO,MAAM,wBAAwB,0BAAiC,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { extractAnthropicUsage, forwardProviderRequest, getCapturedUsage, normalizePath, } from './types.js';
|
|
2
|
+
function extractStreamingUsage(eventData) {
|
|
3
|
+
try {
|
|
4
|
+
return extractAnthropicUsage(JSON.parse(eventData));
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class AnthropicProviderAdapter {
|
|
11
|
+
name = 'anthropic';
|
|
12
|
+
baseUrl = 'https://api.anthropic.com';
|
|
13
|
+
authHeader(apiKey) {
|
|
14
|
+
return {
|
|
15
|
+
'x-api-key': apiKey,
|
|
16
|
+
'anthropic-version': '2023-06-01',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
matchesPath(path) {
|
|
20
|
+
return normalizePath(path) === '/v1/messages';
|
|
21
|
+
}
|
|
22
|
+
forwardRequest(req, apiKey) {
|
|
23
|
+
return forwardProviderRequest({
|
|
24
|
+
request: req,
|
|
25
|
+
baseUrl: this.baseUrl,
|
|
26
|
+
authHeaders: this.authHeader(apiKey),
|
|
27
|
+
usageExtractor: extractAnthropicUsage,
|
|
28
|
+
streamingUsageExtractor: extractStreamingUsage,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
extractUsage(response) {
|
|
32
|
+
if (response instanceof Response) {
|
|
33
|
+
return getCapturedUsage(response);
|
|
34
|
+
}
|
|
35
|
+
return extractAnthropicUsage(response);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export const anthropicProviderAdapter = new AnthropicProviderAdapter();
|
|
39
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,gBAAgB,EAChB,aAAa,GAGd,MAAM,YAAY,CAAC;AAEpB,SAAS,qBAAqB,CAAC,SAAiB;IAC9C,IAAI,CAAC;QACH,OAAO,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAY,CAAC,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,OAAO,wBAAwB;IAC1B,IAAI,GAAG,WAAoB,CAAC;IAC5B,OAAO,GAAG,2BAA2B,CAAC;IAE/C,UAAU,CAAC,MAAc;QACvB,OAAO;YACL,WAAW,EAAE,MAAM;YACnB,mBAAmB,EAAE,YAAY;SAClC,CAAC;IACJ,CAAC;IAED,WAAW,CAAC,IAAY;QACtB,OAAO,aAAa,CAAC,IAAI,CAAC,KAAK,cAAc,CAAC;IAChD,CAAC;IAED,cAAc,CAAC,GAAY,EAAE,MAAc;QACzC,OAAO,sBAAsB,CAAC;YAC5B,OAAO,EAAE,GAAG;YACZ,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;YACpC,cAAc,EAAE,qBAAqB;YACrC,uBAAuB,EAAE,qBAAqB;SAC/C,CAAC,CAAC;IACL,CAAC;IAED,YAAY,CAAC,QAA2B;QACtC,IAAI,QAAQ,YAAY,QAAQ,EAAE,CAAC;YACjC,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QACpC,CAAC;QAED,OAAO,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;CACF;AAED,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,wBAAwB,EAAE,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProviderAdapter } from './types.js';
|
|
2
|
+
export { AnthropicProviderAdapter, anthropicProviderAdapter } from './anthropic.js';
|
|
3
|
+
export { OpenAIProviderAdapter, openAIProviderAdapter } from './openai.js';
|
|
4
|
+
export { OpenRouterProviderAdapter, openRouterProviderAdapter } from './openrouter.js';
|
|
5
|
+
export * from './types.js';
|
|
6
|
+
export declare const providerRegistry: ProviderAdapter[];
|
|
7
|
+
export declare function resolveProviderByName(name: ProviderAdapter['name']): ProviderAdapter;
|
|
8
|
+
export declare function resolveProvider(path: string): ProviderAdapter;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AACpF,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACvF,cAAc,YAAY,CAAC;AAE3B,eAAO,MAAM,gBAAgB,EAAE,eAAe,EAI7C,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,eAAe,CAQpF;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAQ7D"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { anthropicProviderAdapter } from './anthropic.js';
|
|
2
|
+
import { openAIProviderAdapter } from './openai.js';
|
|
3
|
+
import { openRouterProviderAdapter } from './openrouter.js';
|
|
4
|
+
export { AnthropicProviderAdapter, anthropicProviderAdapter } from './anthropic.js';
|
|
5
|
+
export { OpenAIProviderAdapter, openAIProviderAdapter } from './openai.js';
|
|
6
|
+
export { OpenRouterProviderAdapter, openRouterProviderAdapter } from './openrouter.js';
|
|
7
|
+
export * from './types.js';
|
|
8
|
+
export const providerRegistry = [
|
|
9
|
+
anthropicProviderAdapter,
|
|
10
|
+
openAIProviderAdapter,
|
|
11
|
+
openRouterProviderAdapter,
|
|
12
|
+
];
|
|
13
|
+
export function resolveProviderByName(name) {
|
|
14
|
+
const adapter = providerRegistry.find((candidate) => candidate.name === name);
|
|
15
|
+
if (!adapter) {
|
|
16
|
+
throw new Error(`Unsupported provider: ${name}`);
|
|
17
|
+
}
|
|
18
|
+
return adapter;
|
|
19
|
+
}
|
|
20
|
+
export function resolveProvider(path) {
|
|
21
|
+
const adapter = providerRegistry.find((candidate) => candidate.matchesPath(path));
|
|
22
|
+
if (!adapter) {
|
|
23
|
+
throw new Error(`Unsupported provider path: ${path}`);
|
|
24
|
+
}
|
|
25
|
+
return adapter;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAG5D,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AACpF,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACvF,cAAc,YAAY,CAAC;AAE3B,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IACjD,wBAAwB;IACxB,qBAAqB;IACrB,yBAAyB;CAC1B,CAAC;AAEF,MAAM,UAAU,qBAAqB,CAAC,IAA6B;IACjE,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IAE9E,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;IAElF,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,8BAA8B,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ProviderAdapter, type TokenUsage } from './types.js';
|
|
2
|
+
export declare class OpenAIProviderAdapter implements ProviderAdapter {
|
|
3
|
+
readonly name: "openai";
|
|
4
|
+
readonly baseUrl = "https://api.openai.com";
|
|
5
|
+
authHeader(apiKey: string): Record<string, string>;
|
|
6
|
+
matchesPath(path: string): boolean;
|
|
7
|
+
forwardRequest(req: Request, apiKey: string): Promise<Response>;
|
|
8
|
+
extractUsage(response: Response | object): TokenUsage | null;
|
|
9
|
+
}
|
|
10
|
+
export declare const openAIProviderAdapter: OpenAIProviderAdapter;
|
|
11
|
+
//# sourceMappingURL=openai.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../src/providers/openai.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,UAAU,EAChB,MAAM,YAAY,CAAC;AA6BpB,qBAAa,qBAAsB,YAAW,eAAe;IAC3D,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAClC,QAAQ,CAAC,OAAO,4BAA4B;IAE5C,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAMlD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAKlC,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAW/D,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,IAAI;CAO7D;AAED,eAAO,MAAM,qBAAqB,uBAA8B,CAAC"}
|