tolvyn 1.0.1 → 1.0.2
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 +183 -0
- package/dist/cjs/anthropic.d.ts +20 -0
- package/dist/cjs/anthropic.d.ts.map +1 -0
- package/dist/cjs/anthropic.js +50 -0
- package/dist/cjs/anthropic.js.map +1 -0
- package/dist/cjs/client.d.ts +21 -0
- package/dist/cjs/client.d.ts.map +1 -0
- package/dist/cjs/client.js +83 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/failopen.d.ts +7 -0
- package/dist/cjs/failopen.d.ts.map +1 -0
- package/dist/cjs/failopen.js +82 -0
- package/dist/cjs/failopen.js.map +1 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/anthropic.d.ts +20 -0
- package/dist/esm/anthropic.d.ts.map +1 -0
- package/dist/esm/anthropic.js +43 -0
- package/dist/esm/anthropic.js.map +1 -0
- package/dist/esm/client.d.ts +21 -0
- package/dist/esm/client.d.ts.map +1 -0
- package/dist/esm/client.js +76 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/failopen.d.ts +7 -0
- package/dist/esm/failopen.d.ts.map +1 -0
- package/dist/esm/failopen.js +77 -0
- package/dist/esm/failopen.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -0
- package/package.json +41 -8
- package/src/anthropic.ts +70 -0
- package/src/client.ts +115 -0
- package/src/failopen.ts +94 -0
- package/src/index.ts +4 -0
- package/index.js +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# tolvyn (Node.js / TypeScript SDK)
|
|
2
|
+
|
|
3
|
+
Drop-in replacement for the `openai` and `@anthropic-ai/sdk` packages. Add cost metering, team attribution, and budget enforcement to your AI calls in one line.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install tolvyn
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### OpenAI (ESM / TypeScript)
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// Before
|
|
17
|
+
import OpenAI from "openai";
|
|
18
|
+
const client = new OpenAI();
|
|
19
|
+
|
|
20
|
+
// After — one line change
|
|
21
|
+
import { OpenAI } from "tolvyn";
|
|
22
|
+
const client = new OpenAI({
|
|
23
|
+
tolvynApiKey: "tlv_live_...", // or set TOLVYN_API_KEY env var
|
|
24
|
+
team: "backend",
|
|
25
|
+
service: "summariser",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Everything else stays the same
|
|
29
|
+
const response = await client.chat.completions.create({
|
|
30
|
+
model: "gpt-4o",
|
|
31
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### OpenAI (CommonJS)
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
const { OpenAI } = require("tolvyn");
|
|
39
|
+
|
|
40
|
+
const client = new OpenAI({
|
|
41
|
+
tolvynApiKey: process.env.TOLVYN_API_KEY,
|
|
42
|
+
team: "backend",
|
|
43
|
+
service: "summariser",
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Anthropic (ESM / TypeScript)
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// Before
|
|
51
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
52
|
+
const client = new Anthropic();
|
|
53
|
+
|
|
54
|
+
// After
|
|
55
|
+
import { Anthropic } from "tolvyn";
|
|
56
|
+
const client = new Anthropic({
|
|
57
|
+
tolvynApiKey: "tlv_live_...",
|
|
58
|
+
team: "ml-team",
|
|
59
|
+
service: "classifier",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const message = await client.messages.create({
|
|
63
|
+
model: "claude-sonnet-4-6",
|
|
64
|
+
max_tokens: 1024,
|
|
65
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Classes
|
|
72
|
+
|
|
73
|
+
| Class | Extends | Provider |
|
|
74
|
+
|--------------------|-------------------------------|-----------|
|
|
75
|
+
| `tolvyn.OpenAI` | `openai` OpenAI | OpenAI |
|
|
76
|
+
| `tolvyn.Anthropic` | `@anthropic-ai/sdk` Anthropic | Anthropic |
|
|
77
|
+
|
|
78
|
+
Both classes are strict drop-ins. Every method, event, and behaviour of the underlying SDK is preserved.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Parameters
|
|
83
|
+
|
|
84
|
+
### `OpenAI` (`TolvynOpenAIOptions`)
|
|
85
|
+
|
|
86
|
+
Extends `openai.ClientOptions` (omitting `apiKey` and `baseURL`).
|
|
87
|
+
|
|
88
|
+
| Parameter | Type | Default | Description |
|
|
89
|
+
|----------------|-----------------------|-------------|-----------------------------------------------------------------------------------------------------------|
|
|
90
|
+
| `tolvynApiKey` | `string or undefined` | `undefined` | Your TOLVYN API key. Falls back to `TOLVYN_API_KEY` env var. Required. |
|
|
91
|
+
| `proxyUrl` | `string or undefined` | `undefined` | TOLVYN proxy URL. Falls back to `TOLVYN_PROXY_URL` env var, then `http://localhost:8081/v1/proxy/openai`. |
|
|
92
|
+
| `team` | `string or undefined` | `undefined` | Team name for cost attribution. Sent as `X-Tolvyn-Team` header. |
|
|
93
|
+
| `service` | `string or undefined` | `undefined` | Service name. Sent as `X-Tolvyn-Service` header. |
|
|
94
|
+
| `feature` | `string or undefined` | `undefined` | Feature name. Sent as `X-Tolvyn-Feature` header. |
|
|
95
|
+
| `agent` | `string or undefined` | `undefined` | Agent name. Sent as `X-Tolvyn-Agent` header. |
|
|
96
|
+
| `failOpen` | `boolean` | `true` | If `true` and the proxy is unreachable, retry directly against OpenAI using `openAIApiKey`. |
|
|
97
|
+
| `openAIApiKey` | `string or undefined` | `undefined` | OpenAI key used only for fail-open fallback. Falls back to `OPENAI_API_KEY` env var. |
|
|
98
|
+
| `...rest` | any | — | All other `openai.ClientOptions` fields are forwarded to the underlying client. |
|
|
99
|
+
|
|
100
|
+
### `Anthropic` (`TolvynAnthropicOptions`)
|
|
101
|
+
|
|
102
|
+
Extends `@anthropic-ai/sdk` ClientOptions (omitting `apiKey` and `baseURL`).
|
|
103
|
+
|
|
104
|
+
| Parameter | Type | Default | Description |
|
|
105
|
+
|------------------|-----------------------|-------------|--------------------------------------------------------------------------------------------------------------|
|
|
106
|
+
| `tolvynApiKey` | `string or undefined` | `undefined` | Your TOLVYN API key. Falls back to `TOLVYN_API_KEY` env var. Required. |
|
|
107
|
+
| `proxyUrl` | `string or undefined` | `undefined` | TOLVYN proxy URL. Falls back to `TOLVYN_PROXY_URL` env var, then `http://localhost:8081/v1/proxy/anthropic`. |
|
|
108
|
+
| `team` | `string or undefined` | `undefined` | Team name for cost attribution. |
|
|
109
|
+
| `service` | `string or undefined` | `undefined` | Service name. |
|
|
110
|
+
| `feature` | `string or undefined` | `undefined` | Feature name. |
|
|
111
|
+
| `agent` | `string or undefined` | `undefined` | Agent name. |
|
|
112
|
+
| `failOpen` | `boolean` | `true` | If `true` and the proxy is unreachable, retry directly against Anthropic. |
|
|
113
|
+
| `anthropicApiKey`| `string or undefined` | `undefined` | Anthropic key used only for fail-open fallback. Falls back to `ANTHROPIC_API_KEY` env var. |
|
|
114
|
+
| `...rest` | any | — | All other Anthropic ClientOptions fields are forwarded. |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Tagging
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const client = new OpenAI({
|
|
122
|
+
tolvynApiKey: "tlv_live_...",
|
|
123
|
+
team: "search-team", // Maps to a team in TOLVYN → budget applies
|
|
124
|
+
service: "semantic-search", // Sub-component (e.g. microservice name)
|
|
125
|
+
feature: "query-expansion", // Feature within the service
|
|
126
|
+
agent: "reranker-v2", // Agent name for multi-agent pipelines
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
All four tags are optional and independent. They appear in `tolvyn tail` output, the dashboard, and usage breakdown endpoints.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Fail-open behaviour
|
|
135
|
+
|
|
136
|
+
By default, `failOpen: true`. When the TOLVYN proxy is unreachable (connection refused, HTTP 503), the SDK retries the request directly against the AI provider using the fallback API key.
|
|
137
|
+
|
|
138
|
+
A proxy outage **never breaks your application**. Requests that bypass the proxy are not metered; they appear in the provider's billing but not in TOLVYN.
|
|
139
|
+
|
|
140
|
+
To hard-fail on proxy errors:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const client = new OpenAI({ tolvynApiKey: "tlv_live_...", failOpen: false });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Imports
|
|
149
|
+
|
|
150
|
+
### ESM
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { OpenAI, Anthropic } from "tolvyn";
|
|
154
|
+
import type { TolvynOpenAIOptions, TolvynAnthropicOptions } from "tolvyn";
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### CommonJS
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
const { OpenAI, Anthropic } = require("tolvyn");
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Both ESM (`dist/esm/`) and CJS (`dist/cjs/`) builds are included.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Environment variables
|
|
168
|
+
|
|
169
|
+
| Variable | Used by | Description |
|
|
170
|
+
|---------------------|-------------------|--------------------------------------|
|
|
171
|
+
| `TOLVYN_API_KEY` | All classes | TOLVYN API key |
|
|
172
|
+
| `TOLVYN_PROXY_URL` | All classes | Proxy URL override |
|
|
173
|
+
| `OPENAI_API_KEY` | OpenAI classes | OpenAI key for fail-open fallback |
|
|
174
|
+
| `ANTHROPIC_API_KEY` | Anthropic classes | Anthropic key for fail-open fallback |
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Requirements
|
|
179
|
+
|
|
180
|
+
- Node.js 18+
|
|
181
|
+
- `openai >= 4.0.0` (peer dependency)
|
|
182
|
+
- `@anthropic-ai/sdk >= 0.20.0` (peer dependency)
|
|
183
|
+
- TypeScript 5.0+ (if using TypeScript)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOLVYN Anthropic wrapper — thin drop-in over @anthropic-ai/sdk.
|
|
3
|
+
*/
|
|
4
|
+
import AnthropicBase, { ClientOptions } from '@anthropic-ai/sdk';
|
|
5
|
+
export interface TolvynAnthropicOptions extends Omit<ClientOptions, 'apiKey' | 'baseURL'> {
|
|
6
|
+
tolvynApiKey?: string;
|
|
7
|
+
proxyUrl?: string;
|
|
8
|
+
team?: string;
|
|
9
|
+
service?: string;
|
|
10
|
+
feature?: string;
|
|
11
|
+
agent?: string;
|
|
12
|
+
failOpen?: boolean;
|
|
13
|
+
anthropicApiKey?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class Anthropic extends AnthropicBase {
|
|
16
|
+
readonly _tolvynFailOpen: boolean;
|
|
17
|
+
readonly _tolvynFallbackKey: string | undefined;
|
|
18
|
+
constructor(options?: TolvynAnthropicOptions);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../src/anthropic.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,aAAa,EAAE,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAMjE,MAAM,WAAW,sBACf,SAAQ,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,SAAS,CAAC;IACjD,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,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,SAAU,SAAQ,aAAa;IAC1C,SAAgB,eAAe,EAAE,OAAO,CAAC;IACzC,SAAgB,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;gBAE3C,OAAO,GAAE,sBAA2B;CA4CjD"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Anthropic = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* TOLVYN Anthropic wrapper — thin drop-in over @anthropic-ai/sdk.
|
|
9
|
+
*/
|
|
10
|
+
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
11
|
+
const failopen_1 = require("./failopen");
|
|
12
|
+
const ANTHROPIC_DEFAULT_PROXY_URL = 'https://proxy.tolvyn.io/v1/proxy/anthropic/';
|
|
13
|
+
const ANTHROPIC_DIRECT_URL = 'https://api.anthropic.com';
|
|
14
|
+
class Anthropic extends sdk_1.default {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
const tolvynApiKey = options.tolvynApiKey ?? process.env['TOLVYN_API_KEY'];
|
|
17
|
+
if (!tolvynApiKey) {
|
|
18
|
+
throw new Error('tolvynApiKey required. Set TOLVYN_API_KEY env var or pass tolvynApiKey.');
|
|
19
|
+
}
|
|
20
|
+
const proxyUrl = options.proxyUrl ??
|
|
21
|
+
process.env['TOLVYN_PROXY_URL'] ??
|
|
22
|
+
ANTHROPIC_DEFAULT_PROXY_URL;
|
|
23
|
+
const defaultHeaders = {};
|
|
24
|
+
if (options.team)
|
|
25
|
+
defaultHeaders['X-Tolvyn-Team'] = options.team;
|
|
26
|
+
if (options.service)
|
|
27
|
+
defaultHeaders['X-Tolvyn-Service'] = options.service;
|
|
28
|
+
if (options.feature)
|
|
29
|
+
defaultHeaders['X-Tolvyn-Feature'] = options.feature;
|
|
30
|
+
if (options.agent)
|
|
31
|
+
defaultHeaders['X-Tolvyn-Agent'] = options.agent;
|
|
32
|
+
const fallbackKey = options.anthropicApiKey ?? process.env['ANTHROPIC_API_KEY'];
|
|
33
|
+
const failOpen = options.failOpen ?? true;
|
|
34
|
+
const { tolvynApiKey: _tk, proxyUrl: _pu, team: _t, service: _sv, feature: _f, agent: _a, failOpen: _fo, anthropicApiKey: _aak, ...rest } = options;
|
|
35
|
+
const superOptions = {
|
|
36
|
+
...rest,
|
|
37
|
+
apiKey: tolvynApiKey,
|
|
38
|
+
baseURL: proxyUrl,
|
|
39
|
+
defaultHeaders,
|
|
40
|
+
};
|
|
41
|
+
if (failOpen && fallbackKey && !superOptions.fetch) {
|
|
42
|
+
superOptions.fetch = (0, failopen_1.makeFailOpenFetch)(fallbackKey, ANTHROPIC_DIRECT_URL, 'Anthropic');
|
|
43
|
+
}
|
|
44
|
+
super(superOptions);
|
|
45
|
+
this._tolvynFailOpen = failOpen;
|
|
46
|
+
this._tolvynFallbackKey = fallbackKey;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.Anthropic = Anthropic;
|
|
50
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../src/anthropic.ts"],"names":[],"mappings":";;;;;;AAAA;;GAEG;AACH,4DAAiE;AACjE,yCAA+C;AAE/C,MAAM,2BAA2B,GAAG,6CAA6C,CAAC;AAClF,MAAM,oBAAoB,GAAG,2BAA2B,CAAC;AAczD,MAAa,SAAU,SAAQ,aAAa;IAI1C,YAAY,UAAkC,EAAE;QAC9C,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,MAAM,QAAQ,GACZ,OAAO,CAAC,QAAQ;YAChB,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;YAC/B,2BAA2B,CAAC;QAE9B,MAAM,cAAc,GAA2B,EAAE,CAAC;QAClD,IAAI,OAAO,CAAC,IAAI;YAAK,cAAc,CAAC,eAAe,CAAC,GAAM,OAAO,CAAC,IAAI,CAAC;QACvE,IAAI,OAAO,CAAC,OAAO;YAAE,cAAc,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;QAC1E,IAAI,OAAO,CAAC,OAAO;YAAE,cAAc,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;QAC1E,IAAI,OAAO,CAAC,KAAK;YAAI,cAAc,CAAC,gBAAgB,CAAC,GAAK,OAAO,CAAC,KAAK,CAAC;QAExE,MAAM,WAAW,GAAG,OAAO,CAAC,eAAe,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QAChF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC;QAE1C,MAAM,EACJ,YAAY,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,GAAG,EACxD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,eAAe,EAAE,IAAI,EAC5D,GAAG,IAAI,EACR,GAAG,OAAO,CAAC;QAEZ,MAAM,YAAY,GAAkB;YAClC,GAAG,IAAI;YACP,MAAM,EAAE,YAAY;YACpB,OAAO,EAAE,QAAQ;YACjB,cAAc;SACf,CAAC;QAEF,IAAI,QAAQ,IAAI,WAAW,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;YACnD,YAAY,CAAC,KAAK,GAAG,IAAA,4BAAiB,EAAC,WAAW,EAAE,oBAAoB,EAAE,WAAW,CAAC,CAAC;QACzF,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,CAAC;QAEpB,IAAI,CAAC,eAAe,GAAG,QAAQ,CAAC;QAChC,IAAI,CAAC,kBAAkB,GAAG,WAAW,CAAC;IACxC,CAAC;CACF;AAhDD,8BAgDC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOLVYN OpenAI wrapper — thin drop-in over the official openai package.
|
|
3
|
+
*/
|
|
4
|
+
import OpenAIBase, { ClientOptions } from 'openai';
|
|
5
|
+
export interface TolvynOpenAIOptions extends Omit<ClientOptions, 'apiKey' | 'baseURL'> {
|
|
6
|
+
tolvynApiKey?: string;
|
|
7
|
+
proxyUrl?: string;
|
|
8
|
+
team?: string;
|
|
9
|
+
service?: string;
|
|
10
|
+
feature?: string;
|
|
11
|
+
agent?: string;
|
|
12
|
+
failOpen?: boolean;
|
|
13
|
+
openAIApiKey?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class OpenAI extends OpenAIBase {
|
|
16
|
+
readonly _tolvynFailOpen: boolean;
|
|
17
|
+
readonly _tolvynFallbackKey: string | undefined;
|
|
18
|
+
readonly _tolvynProxyUrl: string;
|
|
19
|
+
constructor(options?: TolvynOpenAIOptions);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,UAAU,EAAE,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAMnD,MAAM,WAAW,mBACf,SAAQ,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,SAAS,CAAC;IACjD,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;AA6CD,qBAAa,MAAO,SAAQ,UAAU;IACpC,SAAgB,eAAe,EAAE,OAAO,CAAC;IACzC,SAAgB,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IACvD,SAAgB,eAAe,EAAE,MAAM,CAAC;gBAE5B,OAAO,GAAE,mBAAwB;CA6C9C"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OpenAI = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* TOLVYN OpenAI wrapper — thin drop-in over the official openai package.
|
|
9
|
+
*/
|
|
10
|
+
const openai_1 = __importDefault(require("openai"));
|
|
11
|
+
const failopen_1 = require("./failopen");
|
|
12
|
+
const OPENAI_DEFAULT_PROXY_URL = 'https://proxy.tolvyn.io/v1/proxy/openai/';
|
|
13
|
+
const OPENAI_DIRECT_URL = 'https://api.openai.com/v1';
|
|
14
|
+
function makeFailOpenFetch(fallbackKey, directUrl, provider) {
|
|
15
|
+
return async function failOpenFetch(input, init) {
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(input, init);
|
|
18
|
+
if (res.status === 503) {
|
|
19
|
+
throw Object.assign(new Error('503 from proxy'), { status: 503 });
|
|
20
|
+
}
|
|
21
|
+
return res;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if ((0, failopen_1.shouldNotFailOpen)(err) || !(0, failopen_1.isProxyError)(err))
|
|
25
|
+
throw err;
|
|
26
|
+
console.error(`TOLVYN proxy unreachable — routing direct to ${provider} (fail-open)`);
|
|
27
|
+
const originalUrl = typeof input === 'string'
|
|
28
|
+
? input
|
|
29
|
+
: input instanceof URL
|
|
30
|
+
? input.href
|
|
31
|
+
: input.url;
|
|
32
|
+
const url = new URL(originalUrl);
|
|
33
|
+
const directBase = new URL(directUrl);
|
|
34
|
+
url.hostname = directBase.hostname;
|
|
35
|
+
url.protocol = directBase.protocol;
|
|
36
|
+
url.port = directBase.port;
|
|
37
|
+
url.pathname = url.pathname; // keep path intact
|
|
38
|
+
const newInit = { ...(init ?? {}) };
|
|
39
|
+
const headers = new Headers(init?.headers ?? {});
|
|
40
|
+
headers.set('Authorization', `Bearer ${fallbackKey}`);
|
|
41
|
+
newInit.headers = headers;
|
|
42
|
+
return fetch(url.toString(), newInit);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
class OpenAI extends openai_1.default {
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
const tolvynApiKey = options.tolvynApiKey ?? process.env['TOLVYN_API_KEY'];
|
|
49
|
+
if (!tolvynApiKey) {
|
|
50
|
+
throw new Error('tolvynApiKey required. Set TOLVYN_API_KEY env var or pass tolvynApiKey.');
|
|
51
|
+
}
|
|
52
|
+
const proxyUrl = options.proxyUrl ??
|
|
53
|
+
process.env['TOLVYN_PROXY_URL'] ??
|
|
54
|
+
OPENAI_DEFAULT_PROXY_URL;
|
|
55
|
+
const defaultHeaders = {};
|
|
56
|
+
if (options.team)
|
|
57
|
+
defaultHeaders['X-Tolvyn-Team'] = options.team;
|
|
58
|
+
if (options.service)
|
|
59
|
+
defaultHeaders['X-Tolvyn-Service'] = options.service;
|
|
60
|
+
if (options.feature)
|
|
61
|
+
defaultHeaders['X-Tolvyn-Feature'] = options.feature;
|
|
62
|
+
if (options.agent)
|
|
63
|
+
defaultHeaders['X-Tolvyn-Agent'] = options.agent;
|
|
64
|
+
const fallbackKey = options.openAIApiKey ?? process.env['OPENAI_API_KEY'];
|
|
65
|
+
const failOpen = options.failOpen ?? true;
|
|
66
|
+
const { tolvynApiKey: _tk, proxyUrl: _pu, team: _t, service: _sv, feature: _f, agent: _a, failOpen: _fo, openAIApiKey: _oak, ...rest } = options;
|
|
67
|
+
const superOptions = {
|
|
68
|
+
...rest,
|
|
69
|
+
apiKey: tolvynApiKey,
|
|
70
|
+
baseURL: proxyUrl,
|
|
71
|
+
defaultHeaders,
|
|
72
|
+
};
|
|
73
|
+
if (failOpen && fallbackKey && !superOptions.fetch) {
|
|
74
|
+
superOptions.fetch = makeFailOpenFetch(fallbackKey, OPENAI_DIRECT_URL, 'OpenAI');
|
|
75
|
+
}
|
|
76
|
+
super(superOptions);
|
|
77
|
+
this._tolvynFailOpen = failOpen;
|
|
78
|
+
this._tolvynFallbackKey = fallbackKey;
|
|
79
|
+
this._tolvynProxyUrl = proxyUrl;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.OpenAI = OpenAI;
|
|
83
|
+
//# 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,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Fail-open helpers: detect proxy unreachability and retry direct.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isProxyError = isProxyError;
|
|
7
|
+
exports.shouldNotFailOpen = shouldNotFailOpen;
|
|
8
|
+
exports.makeFailOpenFetch = makeFailOpenFetch;
|
|
9
|
+
function isProxyError(error) {
|
|
10
|
+
if (!error || typeof error !== 'object')
|
|
11
|
+
return false;
|
|
12
|
+
const err = error;
|
|
13
|
+
// Node.js connection errors (ECONNREFUSED, ETIMEDOUT, etc.)
|
|
14
|
+
const code = err['code'];
|
|
15
|
+
if (typeof code === 'string' && (code === 'ECONNREFUSED' ||
|
|
16
|
+
code === 'ECONNRESET' ||
|
|
17
|
+
code === 'ETIMEDOUT' ||
|
|
18
|
+
code === 'ENOTFOUND' ||
|
|
19
|
+
code.startsWith('ERR_'))) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
// HTTP 503 from proxy
|
|
23
|
+
const status = err['status'] ?? err['statusCode'];
|
|
24
|
+
if (status === 503)
|
|
25
|
+
return true;
|
|
26
|
+
// fetch-level errors
|
|
27
|
+
const message = typeof err['message'] === 'string' ? err['message'] : '';
|
|
28
|
+
if (message.includes('ECONNREFUSED') ||
|
|
29
|
+
message.includes('ETIMEDOUT') ||
|
|
30
|
+
message.includes('fetch failed') ||
|
|
31
|
+
message.includes('connect ECONNREFUSED')) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
// Check cause (Node 18+ wraps errors)
|
|
35
|
+
const cause = err['cause'];
|
|
36
|
+
if (cause && isProxyError(cause))
|
|
37
|
+
return true;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
function shouldNotFailOpen(error) {
|
|
41
|
+
// Never fail-open on real API errors (401, 429, other 4xx except 503).
|
|
42
|
+
if (!error || typeof error !== 'object')
|
|
43
|
+
return false;
|
|
44
|
+
const err = error;
|
|
45
|
+
const status = err['status'] ?? err['statusCode'];
|
|
46
|
+
if (typeof status === 'number' && status >= 400 && status < 500 && status !== 503) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function makeFailOpenFetch(fallbackKey, directUrl, provider) {
|
|
52
|
+
return async function failOpenFetch(input, init) {
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(input, init);
|
|
55
|
+
if (res.status === 503) {
|
|
56
|
+
throw Object.assign(new Error('503 from proxy'), { status: 503 });
|
|
57
|
+
}
|
|
58
|
+
return res;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (shouldNotFailOpen(err) || !isProxyError(err))
|
|
62
|
+
throw err;
|
|
63
|
+
console.error(`TOLVYN proxy unreachable — routing direct to ${provider} (fail-open)`);
|
|
64
|
+
const originalUrl = typeof input === 'string'
|
|
65
|
+
? input
|
|
66
|
+
: input instanceof URL
|
|
67
|
+
? input.href
|
|
68
|
+
: input.url;
|
|
69
|
+
const url = new URL(originalUrl);
|
|
70
|
+
const directBase = new URL(directUrl);
|
|
71
|
+
url.hostname = directBase.hostname;
|
|
72
|
+
url.protocol = directBase.protocol;
|
|
73
|
+
url.port = directBase.port;
|
|
74
|
+
const newInit = { ...(init ?? {}) };
|
|
75
|
+
const headers = new Headers(init?.headers ?? {});
|
|
76
|
+
headers.set('Authorization', `Bearer ${fallbackKey}`);
|
|
77
|
+
newInit.headers = headers;
|
|
78
|
+
return fetch(url.toString(), newInit);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# 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 @@
|
|
|
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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Anthropic = exports.OpenAI = void 0;
|
|
4
|
+
var client_1 = require("./client");
|
|
5
|
+
Object.defineProperty(exports, "OpenAI", { enumerable: true, get: function () { return client_1.OpenAI; } });
|
|
6
|
+
var anthropic_1 = require("./anthropic");
|
|
7
|
+
Object.defineProperty(exports, "Anthropic", { enumerable: true, get: function () { return anthropic_1.Anthropic; } });
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,mCAAkC;AAAzB,gGAAA,MAAM,OAAA;AAEf,yCAAwC;AAA/B,sGAAA,SAAS,OAAA"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOLVYN Anthropic wrapper — thin drop-in over @anthropic-ai/sdk.
|
|
3
|
+
*/
|
|
4
|
+
import AnthropicBase, { ClientOptions } from '@anthropic-ai/sdk';
|
|
5
|
+
export interface TolvynAnthropicOptions extends Omit<ClientOptions, 'apiKey' | 'baseURL'> {
|
|
6
|
+
tolvynApiKey?: string;
|
|
7
|
+
proxyUrl?: string;
|
|
8
|
+
team?: string;
|
|
9
|
+
service?: string;
|
|
10
|
+
feature?: string;
|
|
11
|
+
agent?: string;
|
|
12
|
+
failOpen?: boolean;
|
|
13
|
+
anthropicApiKey?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class Anthropic extends AnthropicBase {
|
|
16
|
+
readonly _tolvynFailOpen: boolean;
|
|
17
|
+
readonly _tolvynFallbackKey: string | undefined;
|
|
18
|
+
constructor(options?: TolvynAnthropicOptions);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../src/anthropic.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,aAAa,EAAE,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAMjE,MAAM,WAAW,sBACf,SAAQ,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,SAAS,CAAC;IACjD,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,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,SAAU,SAAQ,aAAa;IAC1C,SAAgB,eAAe,EAAE,OAAO,CAAC;IACzC,SAAgB,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;gBAE3C,OAAO,GAAE,sBAA2B;CA4CjD"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOLVYN Anthropic wrapper — thin drop-in over @anthropic-ai/sdk.
|
|
3
|
+
*/
|
|
4
|
+
import AnthropicBase from '@anthropic-ai/sdk';
|
|
5
|
+
import { makeFailOpenFetch } from './failopen';
|
|
6
|
+
const ANTHROPIC_DEFAULT_PROXY_URL = 'https://proxy.tolvyn.io/v1/proxy/anthropic/';
|
|
7
|
+
const ANTHROPIC_DIRECT_URL = 'https://api.anthropic.com';
|
|
8
|
+
export class Anthropic extends AnthropicBase {
|
|
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
|
+
const proxyUrl = options.proxyUrl ??
|
|
15
|
+
process.env['TOLVYN_PROXY_URL'] ??
|
|
16
|
+
ANTHROPIC_DEFAULT_PROXY_URL;
|
|
17
|
+
const defaultHeaders = {};
|
|
18
|
+
if (options.team)
|
|
19
|
+
defaultHeaders['X-Tolvyn-Team'] = options.team;
|
|
20
|
+
if (options.service)
|
|
21
|
+
defaultHeaders['X-Tolvyn-Service'] = options.service;
|
|
22
|
+
if (options.feature)
|
|
23
|
+
defaultHeaders['X-Tolvyn-Feature'] = options.feature;
|
|
24
|
+
if (options.agent)
|
|
25
|
+
defaultHeaders['X-Tolvyn-Agent'] = options.agent;
|
|
26
|
+
const fallbackKey = options.anthropicApiKey ?? process.env['ANTHROPIC_API_KEY'];
|
|
27
|
+
const failOpen = options.failOpen ?? true;
|
|
28
|
+
const { tolvynApiKey: _tk, proxyUrl: _pu, team: _t, service: _sv, feature: _f, agent: _a, failOpen: _fo, anthropicApiKey: _aak, ...rest } = options;
|
|
29
|
+
const superOptions = {
|
|
30
|
+
...rest,
|
|
31
|
+
apiKey: tolvynApiKey,
|
|
32
|
+
baseURL: proxyUrl,
|
|
33
|
+
defaultHeaders,
|
|
34
|
+
};
|
|
35
|
+
if (failOpen && fallbackKey && !superOptions.fetch) {
|
|
36
|
+
superOptions.fetch = makeFailOpenFetch(fallbackKey, ANTHROPIC_DIRECT_URL, 'Anthropic');
|
|
37
|
+
}
|
|
38
|
+
super(superOptions);
|
|
39
|
+
this._tolvynFailOpen = failOpen;
|
|
40
|
+
this._tolvynFallbackKey = fallbackKey;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../src/anthropic.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,aAAgC,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,2BAA2B,GAAG,6CAA6C,CAAC;AAClF,MAAM,oBAAoB,GAAG,2BAA2B,CAAC;AAczD,MAAM,OAAO,SAAU,SAAQ,aAAa;IAI1C,YAAY,UAAkC,EAAE;QAC9C,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,MAAM,QAAQ,GACZ,OAAO,CAAC,QAAQ;YAChB,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;YAC/B,2BAA2B,CAAC;QAE9B,MAAM,cAAc,GAA2B,EAAE,CAAC;QAClD,IAAI,OAAO,CAAC,IAAI;YAAK,cAAc,CAAC,eAAe,CAAC,GAAM,OAAO,CAAC,IAAI,CAAC;QACvE,IAAI,OAAO,CAAC,OAAO;YAAE,cAAc,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;QAC1E,IAAI,OAAO,CAAC,OAAO;YAAE,cAAc,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;QAC1E,IAAI,OAAO,CAAC,KAAK;YAAI,cAAc,CAAC,gBAAgB,CAAC,GAAK,OAAO,CAAC,KAAK,CAAC;QAExE,MAAM,WAAW,GAAG,OAAO,CAAC,eAAe,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QAChF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC;QAE1C,MAAM,EACJ,YAAY,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,GAAG,EACxD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,eAAe,EAAE,IAAI,EAC5D,GAAG,IAAI,EACR,GAAG,OAAO,CAAC;QAEZ,MAAM,YAAY,GAAkB;YAClC,GAAG,IAAI;YACP,MAAM,EAAE,YAAY;YACpB,OAAO,EAAE,QAAQ;YACjB,cAAc;SACf,CAAC;QAEF,IAAI,QAAQ,IAAI,WAAW,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;YACnD,YAAY,CAAC,KAAK,GAAG,iBAAiB,CAAC,WAAW,EAAE,oBAAoB,EAAE,WAAW,CAAC,CAAC;QACzF,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,CAAC;QAEpB,IAAI,CAAC,eAAe,GAAG,QAAQ,CAAC;QAChC,IAAI,CAAC,kBAAkB,GAAG,WAAW,CAAC;IACxC,CAAC;CACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOLVYN OpenAI wrapper — thin drop-in over the official openai package.
|
|
3
|
+
*/
|
|
4
|
+
import OpenAIBase, { ClientOptions } from 'openai';
|
|
5
|
+
export interface TolvynOpenAIOptions extends Omit<ClientOptions, 'apiKey' | 'baseURL'> {
|
|
6
|
+
tolvynApiKey?: string;
|
|
7
|
+
proxyUrl?: string;
|
|
8
|
+
team?: string;
|
|
9
|
+
service?: string;
|
|
10
|
+
feature?: string;
|
|
11
|
+
agent?: string;
|
|
12
|
+
failOpen?: boolean;
|
|
13
|
+
openAIApiKey?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class OpenAI extends OpenAIBase {
|
|
16
|
+
readonly _tolvynFailOpen: boolean;
|
|
17
|
+
readonly _tolvynFallbackKey: string | undefined;
|
|
18
|
+
readonly _tolvynProxyUrl: string;
|
|
19
|
+
constructor(options?: TolvynOpenAIOptions);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,UAAU,EAAE,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAMnD,MAAM,WAAW,mBACf,SAAQ,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,SAAS,CAAC;IACjD,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;AA6CD,qBAAa,MAAO,SAAQ,UAAU;IACpC,SAAgB,eAAe,EAAE,OAAO,CAAC;IACzC,SAAgB,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IACvD,SAAgB,eAAe,EAAE,MAAM,CAAC;gBAE5B,OAAO,GAAE,mBAAwB;CA6C9C"}
|
|
@@ -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":"AAAA;;GAEG;AAEH,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,GAAG,GAAG,KAAgC,CAAC;IAE7C,4DAA4D;IAC5D,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAC9B,IAAI,KAAK,cAAc;QACvB,IAAI,KAAK,YAAY;QACrB,IAAI,KAAK,WAAW;QACpB,IAAI,KAAK,WAAW;QACpB,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CACxB,EAAE,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,sBAAsB;IACtB,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,YAAY,CAAC,CAAC;IAClD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAEhC,qBAAqB;IACrB,MAAM,OAAO,GAAG,OAAO,GAAG,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzE,IACE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;QAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;QAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;QAChC,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAC,EACxC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,sCAAsC;IACtC,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;IAC3B,IAAI,KAAK,IAAI,YAAY,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE9C,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,uEAAuE;IACvE,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,GAAG,GAAG,KAAgC,CAAC;IAC7C,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,YAAY,CAAC,CAAC;IAClD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QAClF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,WAAmB,EACnB,SAAiB,EACjB,QAAgB;IAEhB,OAAO,KAAK,UAAU,aAAa,CACjC,KAAwB,EACxB,IAAkB;QAElB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACrC,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBACvB,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;YACpE,CAAC;YACD,OAAO,GAAG,CAAC;QACb,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,IAAI,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;gBAAE,MAAM,GAAG,CAAC;YAC5D,OAAO,CAAC,KAAK,CACX,gDAAgD,QAAQ,cAAc,CACvE,CAAC;YACF,MAAM,WAAW,GACf,OAAO,KAAK,KAAK,QAAQ;gBACvB,CAAC,CAAC,KAAK;gBACP,CAAC,CAAC,KAAK,YAAY,GAAG;oBACtB,CAAC,CAAC,KAAK,CAAC,IAAI;oBACZ,CAAC,CAAE,KAAiB,CAAC,GAAG,CAAC;YAC7B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;YACjC,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;YACtC,GAAG,CAAC,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC;YACnC,GAAG,CAAC,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC;YACnC,GAAG,CAAC,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC;YAE3B,MAAM,OAAO,GAAgB,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,CAAC;YACjD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAE,IAAI,EAAE,OAAuB,IAAI,EAAE,CAAC,CAAC;YAClE,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,WAAW,EAAE,CAAC,CAAC;YACtD,OAAO,CAAC,OAAO,GAAG,OAAO,CAAC;YAE1B,OAAO,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,OAAO,CAAC,CAAC;QACxC,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -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"}
|
|
@@ -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"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tolvyn",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"main": "index.js",
|
|
3
|
+
"version": "1.0.2",
|
|
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
|
-
"
|
|
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
|
+
"openai": "^4.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/jest": "^29.5.0",
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"jest": "^29.5.0",
|
|
32
|
+
"ts-jest": "^29.1.0",
|
|
33
|
+
"typescript": "^5.0.0"
|
|
34
|
+
},
|
|
35
|
+
"jest": {
|
|
36
|
+
"preset": "ts-jest",
|
|
37
|
+
"testEnvironment": "node",
|
|
38
|
+
"testMatch": ["**/tests/**/*.test.ts"],
|
|
39
|
+
"globals": {
|
|
40
|
+
"ts-jest": {
|
|
41
|
+
"tsconfig": "tsconfig.cjs.json"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
8
44
|
},
|
|
9
|
-
"
|
|
10
|
-
"author": "",
|
|
11
|
-
"license": "ISC",
|
|
12
|
-
"type": "commonjs"
|
|
45
|
+
"license": "MIT"
|
|
13
46
|
}
|
package/src/anthropic.ts
ADDED
|
@@ -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
|
+
}
|
package/src/failopen.ts
ADDED
|
@@ -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
|
+
}
|
package/src/index.ts
ADDED
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
console.log("TOLVYN SDK coming soon");
|