tolvyn 1.0.1 → 1.0.3

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.
Files changed (48) hide show
  1. package/README.md +63 -0
  2. package/dist/cjs/anthropic.d.ts +20 -0
  3. package/dist/cjs/anthropic.d.ts.map +1 -0
  4. package/dist/cjs/anthropic.js +50 -0
  5. package/dist/cjs/anthropic.js.map +1 -0
  6. package/dist/cjs/client.d.ts +21 -0
  7. package/dist/cjs/client.d.ts.map +1 -0
  8. package/dist/cjs/client.js +83 -0
  9. package/dist/cjs/client.js.map +1 -0
  10. package/dist/cjs/failopen.d.ts +7 -0
  11. package/dist/cjs/failopen.d.ts.map +1 -0
  12. package/dist/cjs/failopen.js +82 -0
  13. package/dist/cjs/failopen.js.map +1 -0
  14. package/dist/cjs/google.d.ts +23 -0
  15. package/dist/cjs/google.d.ts.map +1 -0
  16. package/dist/cjs/google.js +47 -0
  17. package/dist/cjs/google.js.map +1 -0
  18. package/dist/cjs/index.d.ts +7 -0
  19. package/dist/cjs/index.d.ts.map +1 -0
  20. package/dist/cjs/index.js +10 -0
  21. package/dist/cjs/index.js.map +1 -0
  22. package/dist/esm/anthropic.d.ts +20 -0
  23. package/dist/esm/anthropic.d.ts.map +1 -0
  24. package/dist/esm/anthropic.js +43 -0
  25. package/dist/esm/anthropic.js.map +1 -0
  26. package/dist/esm/client.d.ts +21 -0
  27. package/dist/esm/client.d.ts.map +1 -0
  28. package/dist/esm/client.js +76 -0
  29. package/dist/esm/client.js.map +1 -0
  30. package/dist/esm/failopen.d.ts +7 -0
  31. package/dist/esm/failopen.d.ts.map +1 -0
  32. package/dist/esm/failopen.js +77 -0
  33. package/dist/esm/failopen.js.map +1 -0
  34. package/dist/esm/google.d.ts +23 -0
  35. package/dist/esm/google.d.ts.map +1 -0
  36. package/dist/esm/google.js +43 -0
  37. package/dist/esm/google.js.map +1 -0
  38. package/dist/esm/index.d.ts +7 -0
  39. package/dist/esm/index.d.ts.map +1 -0
  40. package/dist/esm/index.js +4 -0
  41. package/dist/esm/index.js.map +1 -0
  42. package/package.json +44 -8
  43. package/src/anthropic.ts +70 -0
  44. package/src/client.ts +115 -0
  45. package/src/failopen.ts +94 -0
  46. package/src/google.ts +70 -0
  47. package/src/index.ts +6 -0
  48. package/index.js +0 -1
@@ -0,0 +1,76 @@
1
+ /**
2
+ * TOLVYN OpenAI wrapper — thin drop-in over the official openai package.
3
+ */
4
+ import OpenAIBase from 'openai';
5
+ import { isProxyError, shouldNotFailOpen } from './failopen';
6
+ const OPENAI_DEFAULT_PROXY_URL = 'https://proxy.tolvyn.io/v1/proxy/openai/';
7
+ const OPENAI_DIRECT_URL = 'https://api.openai.com/v1';
8
+ function makeFailOpenFetch(fallbackKey, directUrl, provider) {
9
+ return async function failOpenFetch(input, init) {
10
+ try {
11
+ const res = await fetch(input, init);
12
+ if (res.status === 503) {
13
+ throw Object.assign(new Error('503 from proxy'), { status: 503 });
14
+ }
15
+ return res;
16
+ }
17
+ catch (err) {
18
+ if (shouldNotFailOpen(err) || !isProxyError(err))
19
+ throw err;
20
+ console.error(`TOLVYN proxy unreachable — routing direct to ${provider} (fail-open)`);
21
+ const originalUrl = typeof input === 'string'
22
+ ? input
23
+ : input instanceof URL
24
+ ? input.href
25
+ : input.url;
26
+ const url = new URL(originalUrl);
27
+ const directBase = new URL(directUrl);
28
+ url.hostname = directBase.hostname;
29
+ url.protocol = directBase.protocol;
30
+ url.port = directBase.port;
31
+ url.pathname = url.pathname; // keep path intact
32
+ const newInit = { ...(init ?? {}) };
33
+ const headers = new Headers(init?.headers ?? {});
34
+ headers.set('Authorization', `Bearer ${fallbackKey}`);
35
+ newInit.headers = headers;
36
+ return fetch(url.toString(), newInit);
37
+ }
38
+ };
39
+ }
40
+ export class OpenAI extends OpenAIBase {
41
+ constructor(options = {}) {
42
+ const tolvynApiKey = options.tolvynApiKey ?? process.env['TOLVYN_API_KEY'];
43
+ if (!tolvynApiKey) {
44
+ throw new Error('tolvynApiKey required. Set TOLVYN_API_KEY env var or pass tolvynApiKey.');
45
+ }
46
+ const proxyUrl = options.proxyUrl ??
47
+ process.env['TOLVYN_PROXY_URL'] ??
48
+ OPENAI_DEFAULT_PROXY_URL;
49
+ const defaultHeaders = {};
50
+ if (options.team)
51
+ defaultHeaders['X-Tolvyn-Team'] = options.team;
52
+ if (options.service)
53
+ defaultHeaders['X-Tolvyn-Service'] = options.service;
54
+ if (options.feature)
55
+ defaultHeaders['X-Tolvyn-Feature'] = options.feature;
56
+ if (options.agent)
57
+ defaultHeaders['X-Tolvyn-Agent'] = options.agent;
58
+ const fallbackKey = options.openAIApiKey ?? process.env['OPENAI_API_KEY'];
59
+ const failOpen = options.failOpen ?? true;
60
+ const { tolvynApiKey: _tk, proxyUrl: _pu, team: _t, service: _sv, feature: _f, agent: _a, failOpen: _fo, openAIApiKey: _oak, ...rest } = options;
61
+ const superOptions = {
62
+ ...rest,
63
+ apiKey: tolvynApiKey,
64
+ baseURL: proxyUrl,
65
+ defaultHeaders,
66
+ };
67
+ if (failOpen && fallbackKey && !superOptions.fetch) {
68
+ superOptions.fetch = makeFailOpenFetch(fallbackKey, OPENAI_DIRECT_URL, 'OpenAI');
69
+ }
70
+ super(superOptions);
71
+ this._tolvynFailOpen = failOpen;
72
+ this._tolvynFallbackKey = fallbackKey;
73
+ this._tolvynProxyUrl = proxyUrl;
74
+ }
75
+ }
76
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Fail-open helpers: detect proxy unreachability and retry direct.
3
+ */
4
+ export declare function isProxyError(error: unknown): boolean;
5
+ export declare function shouldNotFailOpen(error: unknown): boolean;
6
+ export declare function makeFailOpenFetch(fallbackKey: string, directUrl: string, provider: string): typeof globalThis.fetch;
7
+ //# sourceMappingURL=failopen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failopen.d.ts","sourceRoot":"","sources":["../../src/failopen.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAoCpD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CASzD;AAED,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,UAAU,CAAC,KAAK,CAoCzB"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Fail-open helpers: detect proxy unreachability and retry direct.
3
+ */
4
+ export function isProxyError(error) {
5
+ if (!error || typeof error !== 'object')
6
+ return false;
7
+ const err = error;
8
+ // Node.js connection errors (ECONNREFUSED, ETIMEDOUT, etc.)
9
+ const code = err['code'];
10
+ if (typeof code === 'string' && (code === 'ECONNREFUSED' ||
11
+ code === 'ECONNRESET' ||
12
+ code === 'ETIMEDOUT' ||
13
+ code === 'ENOTFOUND' ||
14
+ code.startsWith('ERR_'))) {
15
+ return true;
16
+ }
17
+ // HTTP 503 from proxy
18
+ const status = err['status'] ?? err['statusCode'];
19
+ if (status === 503)
20
+ return true;
21
+ // fetch-level errors
22
+ const message = typeof err['message'] === 'string' ? err['message'] : '';
23
+ if (message.includes('ECONNREFUSED') ||
24
+ message.includes('ETIMEDOUT') ||
25
+ message.includes('fetch failed') ||
26
+ message.includes('connect ECONNREFUSED')) {
27
+ return true;
28
+ }
29
+ // Check cause (Node 18+ wraps errors)
30
+ const cause = err['cause'];
31
+ if (cause && isProxyError(cause))
32
+ return true;
33
+ return false;
34
+ }
35
+ export function shouldNotFailOpen(error) {
36
+ // Never fail-open on real API errors (401, 429, other 4xx except 503).
37
+ if (!error || typeof error !== 'object')
38
+ return false;
39
+ const err = error;
40
+ const status = err['status'] ?? err['statusCode'];
41
+ if (typeof status === 'number' && status >= 400 && status < 500 && status !== 503) {
42
+ return true;
43
+ }
44
+ return false;
45
+ }
46
+ export function makeFailOpenFetch(fallbackKey, directUrl, provider) {
47
+ return async function failOpenFetch(input, init) {
48
+ try {
49
+ const res = await fetch(input, init);
50
+ if (res.status === 503) {
51
+ throw Object.assign(new Error('503 from proxy'), { status: 503 });
52
+ }
53
+ return res;
54
+ }
55
+ catch (err) {
56
+ if (shouldNotFailOpen(err) || !isProxyError(err))
57
+ throw err;
58
+ console.error(`TOLVYN proxy unreachable — routing direct to ${provider} (fail-open)`);
59
+ const originalUrl = typeof input === 'string'
60
+ ? input
61
+ : input instanceof URL
62
+ ? input.href
63
+ : input.url;
64
+ const url = new URL(originalUrl);
65
+ const directBase = new URL(directUrl);
66
+ url.hostname = directBase.hostname;
67
+ url.protocol = directBase.protocol;
68
+ url.port = directBase.port;
69
+ const newInit = { ...(init ?? {}) };
70
+ const headers = new Headers(init?.headers ?? {});
71
+ headers.set('Authorization', `Bearer ${fallbackKey}`);
72
+ newInit.headers = headers;
73
+ return fetch(url.toString(), newInit);
74
+ }
75
+ };
76
+ }
77
+ //# sourceMappingURL=failopen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failopen.js","sourceRoot":"","sources":["../../src/failopen.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * TOLVYN Google wrapper — thin drop-in over @google/generative-ai.
3
+ */
4
+ import { GoogleGenerativeAI, ModelParams, RequestOptions } from '@google/generative-ai';
5
+ export interface TolvynGoogleOptions {
6
+ tolvynApiKey?: string;
7
+ proxyUrl?: string;
8
+ team?: string;
9
+ service?: string;
10
+ feature?: string;
11
+ agent?: string;
12
+ failOpen?: boolean;
13
+ googleApiKey?: string;
14
+ }
15
+ export declare class TolvynGoogle extends GoogleGenerativeAI {
16
+ readonly _tolvynFailOpen: boolean;
17
+ readonly _tolvynFallbackKey: string | undefined;
18
+ private readonly _tolvynProxyUrl;
19
+ private readonly _tolvynHeaders;
20
+ constructor(options?: TolvynGoogleOptions);
21
+ getGenerativeModel(modelParams: ModelParams, requestOptions?: RequestOptions): import("@google/generative-ai").GenerativeModel;
22
+ }
23
+ //# sourceMappingURL=google.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google.d.ts","sourceRoot":"","sources":["../../src/google.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,cAAc,EACf,MAAM,uBAAuB,CAAC;AAM/B,MAAM,WAAW,mBAAmB;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,YAAa,SAAQ,kBAAkB;IAClD,SAAgB,eAAe,EAAE,OAAO,CAAC;IACzC,SAAgB,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IACvD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;gBAE5C,OAAO,GAAE,mBAAwB;IA4BpC,kBAAkB,CACzB,WAAW,EAAE,WAAW,EACxB,cAAc,CAAC,EAAE,cAAc;CASlC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * TOLVYN Google wrapper — thin drop-in over @google/generative-ai.
3
+ */
4
+ import { GoogleGenerativeAI, } from '@google/generative-ai';
5
+ // The proxy base URL is prepended to Google API paths (/v1beta/models/...).
6
+ // The TOLVYN proxy strips /v1/proxy/google and forwards the remainder to Google.
7
+ const GOOGLE_DEFAULT_PROXY_URL = 'https://proxy.tolvyn.io/v1/proxy/google';
8
+ export class TolvynGoogle extends GoogleGenerativeAI {
9
+ constructor(options = {}) {
10
+ const tolvynApiKey = options.tolvynApiKey ?? process.env['TOLVYN_API_KEY'];
11
+ if (!tolvynApiKey) {
12
+ throw new Error('tolvynApiKey required. Set TOLVYN_API_KEY env var or pass tolvynApiKey.');
13
+ }
14
+ // GoogleGenerativeAI sends this value as x-goog-api-key.
15
+ // The TOLVYN proxy's extractBearer reads x-goog-api-key as a fallback.
16
+ super(tolvynApiKey);
17
+ this._tolvynProxyUrl =
18
+ options.proxyUrl ??
19
+ process.env['TOLVYN_PROXY_URL'] ??
20
+ GOOGLE_DEFAULT_PROXY_URL;
21
+ this._tolvynHeaders = {};
22
+ if (options.team)
23
+ this._tolvynHeaders['X-Tolvyn-Team'] = options.team;
24
+ if (options.service)
25
+ this._tolvynHeaders['X-Tolvyn-Service'] = options.service;
26
+ if (options.feature)
27
+ this._tolvynHeaders['X-Tolvyn-Feature'] = options.feature;
28
+ if (options.agent)
29
+ this._tolvynHeaders['X-Tolvyn-Agent'] = options.agent;
30
+ this._tolvynFailOpen = options.failOpen ?? true;
31
+ this._tolvynFallbackKey =
32
+ options.googleApiKey ?? process.env['GOOGLE_API_KEY'];
33
+ }
34
+ getGenerativeModel(modelParams, requestOptions) {
35
+ const mergedOptions = {
36
+ baseUrl: this._tolvynProxyUrl,
37
+ customHeaders: this._tolvynHeaders,
38
+ ...requestOptions,
39
+ };
40
+ return super.getGenerativeModel(modelParams, mergedOptions);
41
+ }
42
+ }
43
+ //# sourceMappingURL=google.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google.js","sourceRoot":"","sources":["../../src/google.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EACL,kBAAkB,GAGnB,MAAM,uBAAuB,CAAC;AAE/B,4EAA4E;AAC5E,iFAAiF;AACjF,MAAM,wBAAwB,GAAG,yCAAyC,CAAC;AAa3E,MAAM,OAAO,YAAa,SAAQ,kBAAkB;IAMlD,YAAY,UAA+B,EAAE;QAC3C,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAC3E,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CACb,yEAAyE,CAC1E,CAAC;QACJ,CAAC;QAED,yDAAyD;QACzD,uEAAuE;QACvE,KAAK,CAAC,YAAY,CAAC,CAAC;QAEpB,IAAI,CAAC,eAAe;YAClB,OAAO,CAAC,QAAQ;gBAChB,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;gBAC/B,wBAAwB,CAAC;QAE3B,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC;QACzB,IAAI,OAAO,CAAC,IAAI;YAAK,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,GAAM,OAAO,CAAC,IAAI,CAAC;QAC5E,IAAI,OAAO,CAAC,OAAO;YAAE,IAAI,CAAC,cAAc,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/E,IAAI,OAAO,CAAC,OAAO;YAAE,IAAI,CAAC,cAAc,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/E,IAAI,OAAO,CAAC,KAAK;YAAI,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAAC,GAAK,OAAO,CAAC,KAAK,CAAC;QAE7E,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC;QAChD,IAAI,CAAC,kBAAkB;YACrB,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAC1D,CAAC;IAEQ,kBAAkB,CACzB,WAAwB,EACxB,cAA+B;QAE/B,MAAM,aAAa,GAAmB;YACpC,OAAO,EAAE,IAAI,CAAC,eAAe;YAC7B,aAAa,EAAE,IAAI,CAAC,cAAc;YAClC,GAAG,cAAc;SAClB,CAAC;QACF,OAAO,KAAK,CAAC,kBAAkB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAC9D,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ export { OpenAI } from './client';
2
+ export type { TolvynOpenAIOptions } from './client';
3
+ export { Anthropic } from './anthropic';
4
+ export type { TolvynAnthropicOptions } from './anthropic';
5
+ export { TolvynGoogle as Google } from './google';
6
+ export type { TolvynGoogleOptions } from './google';
7
+ //# 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,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,YAAY,IAAI,MAAM,EAAE,MAAM,UAAU,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { OpenAI } from './client';
2
+ export { Anthropic } from './anthropic';
3
+ export { TolvynGoogle as Google } from './google';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,OAAO,EAAE,YAAY,IAAI,MAAM,EAAE,MAAM,UAAU,CAAC"}
package/package.json CHANGED
@@ -1,13 +1,49 @@
1
1
  {
2
2
  "name": "tolvyn",
3
- "version": "1.0.1",
4
- "description": "",
5
- "main": "index.js",
3
+ "version": "1.0.3",
4
+ "description": "Drop-in replacement for the OpenAI/Anthropic SDK — routes through TOLVYN for cost attribution, budget enforcement, and audit logging.",
5
+ "main": "./dist/cjs/index.js",
6
+ "module": "./dist/esm/index.js",
7
+ "types": "./dist/cjs/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/esm/index.js",
11
+ "require": "./dist/cjs/index.js",
12
+ "types": "./dist/cjs/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
6
19
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
20
+ "build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json",
21
+ "test": "jest",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "dependencies": {
25
+ "@anthropic-ai/sdk": "^0.20.0",
26
+ "@google/generative-ai": "^0.21.0",
27
+ "openai": "^4.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/jest": "^29.5.0",
31
+ "@types/node": "^20.0.0",
32
+ "jest": "^29.5.0",
33
+ "ts-jest": "^29.1.0",
34
+ "typescript": "^5.0.0"
35
+ },
36
+ "jest": {
37
+ "preset": "ts-jest",
38
+ "testEnvironment": "node",
39
+ "testMatch": [
40
+ "**/tests/**/*.test.ts"
41
+ ],
42
+ "globals": {
43
+ "ts-jest": {
44
+ "tsconfig": "tsconfig.cjs.json"
45
+ }
46
+ }
8
47
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "ISC",
12
- "type": "commonjs"
48
+ "license": "MIT"
13
49
  }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * TOLVYN Anthropic wrapper — thin drop-in over @anthropic-ai/sdk.
3
+ */
4
+ import AnthropicBase, { ClientOptions } from '@anthropic-ai/sdk';
5
+ import { makeFailOpenFetch } from './failopen';
6
+
7
+ const ANTHROPIC_DEFAULT_PROXY_URL = 'https://proxy.tolvyn.io/v1/proxy/anthropic/';
8
+ const ANTHROPIC_DIRECT_URL = 'https://api.anthropic.com';
9
+
10
+ export interface TolvynAnthropicOptions
11
+ extends Omit<ClientOptions, 'apiKey' | 'baseURL'> {
12
+ tolvynApiKey?: string;
13
+ proxyUrl?: string;
14
+ team?: string;
15
+ service?: string;
16
+ feature?: string;
17
+ agent?: string;
18
+ failOpen?: boolean;
19
+ anthropicApiKey?: string;
20
+ }
21
+
22
+ export class Anthropic extends AnthropicBase {
23
+ public readonly _tolvynFailOpen: boolean;
24
+ public readonly _tolvynFallbackKey: string | undefined;
25
+
26
+ constructor(options: TolvynAnthropicOptions = {}) {
27
+ const tolvynApiKey = options.tolvynApiKey ?? process.env['TOLVYN_API_KEY'];
28
+ if (!tolvynApiKey) {
29
+ throw new Error(
30
+ 'tolvynApiKey required. Set TOLVYN_API_KEY env var or pass tolvynApiKey.'
31
+ );
32
+ }
33
+
34
+ const proxyUrl =
35
+ options.proxyUrl ??
36
+ process.env['TOLVYN_PROXY_URL'] ??
37
+ ANTHROPIC_DEFAULT_PROXY_URL;
38
+
39
+ const defaultHeaders: Record<string, string> = {};
40
+ if (options.team) defaultHeaders['X-Tolvyn-Team'] = options.team;
41
+ if (options.service) defaultHeaders['X-Tolvyn-Service'] = options.service;
42
+ if (options.feature) defaultHeaders['X-Tolvyn-Feature'] = options.feature;
43
+ if (options.agent) defaultHeaders['X-Tolvyn-Agent'] = options.agent;
44
+
45
+ const fallbackKey = options.anthropicApiKey ?? process.env['ANTHROPIC_API_KEY'];
46
+ const failOpen = options.failOpen ?? true;
47
+
48
+ const {
49
+ tolvynApiKey: _tk, proxyUrl: _pu, team: _t, service: _sv,
50
+ feature: _f, agent: _a, failOpen: _fo, anthropicApiKey: _aak,
51
+ ...rest
52
+ } = options;
53
+
54
+ const superOptions: ClientOptions = {
55
+ ...rest,
56
+ apiKey: tolvynApiKey,
57
+ baseURL: proxyUrl,
58
+ defaultHeaders,
59
+ };
60
+
61
+ if (failOpen && fallbackKey && !superOptions.fetch) {
62
+ superOptions.fetch = makeFailOpenFetch(fallbackKey, ANTHROPIC_DIRECT_URL, 'Anthropic');
63
+ }
64
+
65
+ super(superOptions);
66
+
67
+ this._tolvynFailOpen = failOpen;
68
+ this._tolvynFallbackKey = fallbackKey;
69
+ }
70
+ }
package/src/client.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * TOLVYN OpenAI wrapper — thin drop-in over the official openai package.
3
+ */
4
+ import OpenAIBase, { ClientOptions } from 'openai';
5
+ import { isProxyError, shouldNotFailOpen } from './failopen';
6
+
7
+ const OPENAI_DEFAULT_PROXY_URL = 'https://proxy.tolvyn.io/v1/proxy/openai/';
8
+ const OPENAI_DIRECT_URL = 'https://api.openai.com/v1';
9
+
10
+ export interface TolvynOpenAIOptions
11
+ extends Omit<ClientOptions, 'apiKey' | 'baseURL'> {
12
+ tolvynApiKey?: string;
13
+ proxyUrl?: string;
14
+ team?: string;
15
+ service?: string;
16
+ feature?: string;
17
+ agent?: string;
18
+ failOpen?: boolean;
19
+ openAIApiKey?: string;
20
+ }
21
+
22
+ function makeFailOpenFetch(
23
+ fallbackKey: string,
24
+ directUrl: string,
25
+ provider: string
26
+ ): typeof globalThis.fetch {
27
+ return async function failOpenFetch(
28
+ input: RequestInfo | URL,
29
+ init?: RequestInit
30
+ ): Promise<Response> {
31
+ try {
32
+ const res = await fetch(input, init);
33
+ if (res.status === 503) {
34
+ throw Object.assign(new Error('503 from proxy'), { status: 503 });
35
+ }
36
+ return res;
37
+ } catch (err: unknown) {
38
+ if (shouldNotFailOpen(err) || !isProxyError(err)) throw err;
39
+ console.error(
40
+ `TOLVYN proxy unreachable — routing direct to ${provider} (fail-open)`
41
+ );
42
+ const originalUrl =
43
+ typeof input === 'string'
44
+ ? input
45
+ : input instanceof URL
46
+ ? input.href
47
+ : (input as Request).url;
48
+ const url = new URL(originalUrl);
49
+ const directBase = new URL(directUrl);
50
+ url.hostname = directBase.hostname;
51
+ url.protocol = directBase.protocol;
52
+ url.port = directBase.port;
53
+ url.pathname = url.pathname; // keep path intact
54
+
55
+ const newInit: RequestInit = { ...(init ?? {}) };
56
+ const headers = new Headers((init?.headers as HeadersInit) ?? {});
57
+ headers.set('Authorization', `Bearer ${fallbackKey}`);
58
+ newInit.headers = headers;
59
+
60
+ return fetch(url.toString(), newInit);
61
+ }
62
+ };
63
+ }
64
+
65
+ export class OpenAI extends OpenAIBase {
66
+ public readonly _tolvynFailOpen: boolean;
67
+ public readonly _tolvynFallbackKey: string | undefined;
68
+ public readonly _tolvynProxyUrl: string;
69
+
70
+ constructor(options: TolvynOpenAIOptions = {}) {
71
+ const tolvynApiKey = options.tolvynApiKey ?? process.env['TOLVYN_API_KEY'];
72
+ if (!tolvynApiKey) {
73
+ throw new Error(
74
+ 'tolvynApiKey required. Set TOLVYN_API_KEY env var or pass tolvynApiKey.'
75
+ );
76
+ }
77
+
78
+ const proxyUrl =
79
+ options.proxyUrl ??
80
+ process.env['TOLVYN_PROXY_URL'] ??
81
+ OPENAI_DEFAULT_PROXY_URL;
82
+
83
+ const defaultHeaders: Record<string, string> = {};
84
+ if (options.team) defaultHeaders['X-Tolvyn-Team'] = options.team;
85
+ if (options.service) defaultHeaders['X-Tolvyn-Service'] = options.service;
86
+ if (options.feature) defaultHeaders['X-Tolvyn-Feature'] = options.feature;
87
+ if (options.agent) defaultHeaders['X-Tolvyn-Agent'] = options.agent;
88
+
89
+ const fallbackKey = options.openAIApiKey ?? process.env['OPENAI_API_KEY'];
90
+ const failOpen = options.failOpen ?? true;
91
+
92
+ const {
93
+ tolvynApiKey: _tk, proxyUrl: _pu, team: _t, service: _sv,
94
+ feature: _f, agent: _a, failOpen: _fo, openAIApiKey: _oak,
95
+ ...rest
96
+ } = options;
97
+
98
+ const superOptions: ClientOptions = {
99
+ ...rest,
100
+ apiKey: tolvynApiKey,
101
+ baseURL: proxyUrl,
102
+ defaultHeaders,
103
+ };
104
+
105
+ if (failOpen && fallbackKey && !superOptions.fetch) {
106
+ superOptions.fetch = makeFailOpenFetch(fallbackKey, OPENAI_DIRECT_URL, 'OpenAI');
107
+ }
108
+
109
+ super(superOptions);
110
+
111
+ this._tolvynFailOpen = failOpen;
112
+ this._tolvynFallbackKey = fallbackKey;
113
+ this._tolvynProxyUrl = proxyUrl;
114
+ }
115
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Fail-open helpers: detect proxy unreachability and retry direct.
3
+ */
4
+
5
+ export function isProxyError(error: unknown): boolean {
6
+ if (!error || typeof error !== 'object') return false;
7
+ const err = error as Record<string, unknown>;
8
+
9
+ // Node.js connection errors (ECONNREFUSED, ETIMEDOUT, etc.)
10
+ const code = err['code'];
11
+ if (typeof code === 'string' && (
12
+ code === 'ECONNREFUSED' ||
13
+ code === 'ECONNRESET' ||
14
+ code === 'ETIMEDOUT' ||
15
+ code === 'ENOTFOUND' ||
16
+ code.startsWith('ERR_')
17
+ )) {
18
+ return true;
19
+ }
20
+
21
+ // HTTP 503 from proxy
22
+ const status = err['status'] ?? err['statusCode'];
23
+ if (status === 503) return true;
24
+
25
+ // fetch-level errors
26
+ const message = typeof err['message'] === 'string' ? err['message'] : '';
27
+ if (
28
+ message.includes('ECONNREFUSED') ||
29
+ message.includes('ETIMEDOUT') ||
30
+ message.includes('fetch failed') ||
31
+ message.includes('connect ECONNREFUSED')
32
+ ) {
33
+ return true;
34
+ }
35
+
36
+ // Check cause (Node 18+ wraps errors)
37
+ const cause = err['cause'];
38
+ if (cause && isProxyError(cause)) return true;
39
+
40
+ return false;
41
+ }
42
+
43
+ export function shouldNotFailOpen(error: unknown): boolean {
44
+ // Never fail-open on real API errors (401, 429, other 4xx except 503).
45
+ if (!error || typeof error !== 'object') return false;
46
+ const err = error as Record<string, unknown>;
47
+ const status = err['status'] ?? err['statusCode'];
48
+ if (typeof status === 'number' && status >= 400 && status < 500 && status !== 503) {
49
+ return true;
50
+ }
51
+ return false;
52
+ }
53
+
54
+ export function makeFailOpenFetch(
55
+ fallbackKey: string,
56
+ directUrl: string,
57
+ provider: string
58
+ ): typeof globalThis.fetch {
59
+ return async function failOpenFetch(
60
+ input: RequestInfo | URL,
61
+ init?: RequestInit
62
+ ): Promise<Response> {
63
+ try {
64
+ const res = await fetch(input, init);
65
+ if (res.status === 503) {
66
+ throw Object.assign(new Error('503 from proxy'), { status: 503 });
67
+ }
68
+ return res;
69
+ } catch (err: unknown) {
70
+ if (shouldNotFailOpen(err) || !isProxyError(err)) throw err;
71
+ console.error(
72
+ `TOLVYN proxy unreachable — routing direct to ${provider} (fail-open)`
73
+ );
74
+ const originalUrl =
75
+ typeof input === 'string'
76
+ ? input
77
+ : input instanceof URL
78
+ ? input.href
79
+ : (input as Request).url;
80
+ const url = new URL(originalUrl);
81
+ const directBase = new URL(directUrl);
82
+ url.hostname = directBase.hostname;
83
+ url.protocol = directBase.protocol;
84
+ url.port = directBase.port;
85
+
86
+ const newInit: RequestInit = { ...(init ?? {}) };
87
+ const headers = new Headers((init?.headers as HeadersInit) ?? {});
88
+ headers.set('Authorization', `Bearer ${fallbackKey}`);
89
+ newInit.headers = headers;
90
+
91
+ return fetch(url.toString(), newInit);
92
+ }
93
+ };
94
+ }