@tokenite/sdk 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +632 -565
- package/dist/client.d.ts +12 -8
- package/dist/client.js +25 -17
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +80 -15
- package/package.json +54 -53
- package/dist/client.test.d.ts +0 -2
- package/dist/client.test.js +0 -66
package/README.md
CHANGED
|
@@ -1,225 +1,225 @@
|
|
|
1
|
-
# @tokenite/sdk
|
|
2
|
-
|
|
3
|
-
One wallet for all your AI apps. Users store their Anthropic, OpenAI, and Google API keys in Tokenite, then grant your app metered access via OAuth. You pay nothing for AI usage.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @tokenite/sdk
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quick Start
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import { Tokenite, isProxyError } from '@tokenite/sdk';
|
|
15
|
-
|
|
16
|
-
const tw = Tokenite({
|
|
17
|
-
clientId: 'your-app-id',
|
|
18
|
-
clientSecret: 'your-app-secret',
|
|
19
|
-
redirectUri: 'https://yourapp.com/callback',
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// 1. Redirect the user to the consent screen
|
|
23
|
-
app.get('/login', (req, res) => res.redirect(tk.getAuthorizeUrl()));
|
|
24
|
-
|
|
25
|
-
// 2. Exchange the OAuth code for an access token in your callback
|
|
26
|
-
app.get('/callback', async (req, res) => {
|
|
27
|
-
const { access_token } = await tk.exchangeCode(req.query.code as string);
|
|
28
|
-
req.session.tokeniteToken = access_token;
|
|
29
|
-
res.redirect('/');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// 3. Call the LLM through the proxy
|
|
33
|
-
app.post('/chat', async (req, res) => {
|
|
34
|
-
const result = await tk.call({
|
|
35
|
-
accessToken: req.session.tokeniteToken,
|
|
36
|
-
provider: 'anthropic',
|
|
37
|
-
path: '/v1/messages',
|
|
38
|
-
body: {
|
|
39
|
-
model: 'claude-3-5-sonnet-latest',
|
|
40
|
-
max_tokens: 1024,
|
|
41
|
-
messages: [{ role: 'user', content: req.body.prompt }],
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
if (isProxyError(result)) {
|
|
46
|
-
return res.status(400).json({ error: result.error });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
res.json({ model: result.model, usage: result.usage, data: result.data });
|
|
50
|
-
});
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
The same `tk.call(...)` works for OpenAI and Google — change `provider` and `path`:
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
await tk.call({
|
|
57
|
-
accessToken,
|
|
58
|
-
provider: 'openai',
|
|
59
|
-
path: '/v1/chat/completions',
|
|
60
|
-
body: { model: 'gpt-4o', messages: [...] },
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
await tk.call({
|
|
64
|
-
accessToken,
|
|
65
|
-
provider: 'google',
|
|
66
|
-
path: '/v1beta/models/gemini-1.5-pro:generateContent',
|
|
67
|
-
body: { contents: [...] },
|
|
68
|
-
});
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Modal flow (single-page apps)
|
|
72
|
-
|
|
73
|
-
If your app is a SPA, open the consent screen in an iframe modal:
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
const { code } = await tk.popup({ suggestedBudget: 5 });
|
|
77
|
-
|
|
78
|
-
// Send the code to your backend, which calls tk.exchangeCode(code).
|
|
79
|
-
// The exchange requires clientSecret and must never run in browser code.
|
|
80
|
-
await fetch('/api/auth/exchange', {
|
|
81
|
-
method: 'POST',
|
|
82
|
-
body: JSON.stringify({ code }),
|
|
83
|
-
});
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Managed agents (Anthropic)
|
|
87
|
-
|
|
88
|
-
Tokenite proxies Anthropic's [Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) surface — the `/v1/agents`, `/v1/environments`, `/v1/sessions`, `/v1/vaults`, `/v1/files`, and `/v1/skills` endpoints — under the same BYOK model as Messages. The app points the Anthropic SDK at `tk.proxyUrl('anthropic')`; the proxy injects the right beta header per surface (`managed-agents-2026-04-01` for agents/environments/sessions/vaults, `files-api-2025-04-14` for files, `skills-2025-10-02` for skills) and forwards calls to the user's Anthropic org. Client-supplied `anthropic-beta` headers are passed through and merged with the auto-injected ones, so callers can opt into additional betas (e.g. multi-version skills) without losing the proxy-managed defaults.
|
|
89
|
-
|
|
90
|
-
**Explicit consent required.** Because agent sessions run long-lived, billable server-side work on Anthropic (`$0.08/hr` session runtime on top of tokens), the proxy rejects agent-surface calls unless the app was created with `allowsManagedAgents: true`. The flag sits alongside the other app fields on creation:
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
await admin.apps.create({
|
|
94
|
-
name: 'Life Coach',
|
|
95
|
-
callbackUrl: 'https://lifecoach.ai/callback',
|
|
96
|
-
modelStrategy: 'models',
|
|
97
|
-
allowedModels: ['claude-opus-4-7', 'claude-sonnet-4-6'],
|
|
98
|
-
allowsManagedAgents: true, // ← opt in
|
|
99
|
-
});
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Without it, every agent endpoint returns `403 AGENT_SCOPE_MISSING`.
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
106
|
-
|
|
107
|
-
const anthropic = new Anthropic({
|
|
108
|
-
apiKey: accessToken,
|
|
109
|
-
baseURL: tk.proxyUrl('anthropic'),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Provision once per user.
|
|
113
|
-
const agent = await anthropic.beta.agents.create({
|
|
114
|
-
name: 'Life Coach',
|
|
115
|
-
model: 'claude-haiku-4-5',
|
|
116
|
-
system: 'You are a terse life coach.',
|
|
117
|
-
tools: [{ type: 'agent_toolset_20260401' }],
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Spin up a session.
|
|
121
|
-
const env = await anthropic.beta.environments.create({ name: 'prod' });
|
|
122
|
-
const session = await anthropic.beta.sessions.create({
|
|
123
|
-
agent: agent.id,
|
|
124
|
-
environment_id: env.id,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// Send events, stream responses — all proxied.
|
|
128
|
-
await anthropic.beta.sessions.events.post(session.id, {
|
|
129
|
-
type: 'user.message', content: 'What should I focus on today?',
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
for await (const event of anthropic.beta.sessions.events.stream(session.id)) {
|
|
133
|
-
// ...
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Archive when done. This is what triggers Tokenite to pull the final
|
|
137
|
-
// usage totals from Anthropic and debit tokens + runtime against the budget.
|
|
138
|
-
await anthropic.beta.sessions.archive(session.id);
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Tokenite tracks each session for attribution and auto-terminates running sessions when the user revokes the connection — otherwise Anthropic would keep billing `$0.08/hr` for the session runtime on top of tokens. The proxy bills the user's wallet at archive time using Anthropic's reported cumulative usage and runtime, plus the standard token rate from the model's pricing.
|
|
142
|
-
|
|
143
|
-
Budget is checked pre-call but not enforced mid-session: a session that started under budget can run past it. Revoke is the user's hard kill switch.
|
|
144
|
-
|
|
145
|
-
### Handling the OAuth callback
|
|
146
|
-
|
|
147
|
-
When Tokenite redirects back to your `redirect_uri`, the URL has one of:
|
|
148
|
-
|
|
149
|
-
- `?code=...&state=...` — user approved; exchange the code for an access token.
|
|
150
|
-
- `?error=access_denied&error_description=user_denied&state=...` — user clicked **Cancel** on the consent screen.
|
|
151
|
-
- `?error=...&error_description=...&state=...` — any other OAuth failure.
|
|
152
|
-
- nothing (or just `state`) — user landed on your callback path without completing a real OAuth round-trip.
|
|
153
|
-
|
|
154
|
-
`parseCallback(input, options?)` turns the URL into a typed result so you can switch on it instead of hand-parsing strings. Pure function; works in any runtime (Node, Bun, edge, browser).
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
import { parseCallback } from '@tokenite/sdk';
|
|
158
|
-
|
|
159
|
-
// In your /api/auth/callback handler:
|
|
160
|
-
const result = parseCallback(req.url, { expectedState: sessionStoredState });
|
|
161
|
-
|
|
162
|
-
if (result.ok) {
|
|
163
|
-
const { access_token } = await tk.exchangeCode(result.code);
|
|
164
|
-
// ... establish your app's session, redirect to /
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
switch (result.reason) {
|
|
169
|
-
case 'user_denied':
|
|
170
|
-
// User clicked Cancel. Don't show a scary error — render a
|
|
171
|
-
// friendly "you cancelled" screen with a "Try again" button.
|
|
172
|
-
return renderCancelled();
|
|
173
|
-
case 'invalid_state':
|
|
174
|
-
// CSRF check failed (state mismatch). Force a fresh login flow.
|
|
175
|
-
return renderRetry('Your sign-in session expired. Please try again.');
|
|
176
|
-
case 'missing_code':
|
|
177
|
-
// User navigated to /callback directly (bookmark, browser back).
|
|
178
|
-
return redirectToHome();
|
|
179
|
-
case 'access_denied':
|
|
180
|
-
case 'oauth_error':
|
|
181
|
-
// Real OAuth failure. result.error + result.description have details.
|
|
182
|
-
return renderError(result);
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
`parseCallback` accepts:
|
|
187
|
-
|
|
188
|
-
- A full URL string: `parseCallback('https://yourapp.com/cb?code=…&state=…')`
|
|
189
|
-
- A path + query: `parseCallback(req.url)` (Node's `req.url` works directly)
|
|
190
|
-
- A query string: `parseCallback('?code=…&state=…')` or `parseCallback('code=…&state=…')`
|
|
191
|
-
- A `URL` object: `parseCallback(new URL(...))`
|
|
192
|
-
- A `URLSearchParams`: `parseCallback(params)`
|
|
193
|
-
|
|
194
|
-
`expectedState` is optional. When you provide it, a mismatch returns `{ ok: false, reason: 'invalid_state' }` even if a `code` is present — treating a stolen code paired with a forged state as a CSRF attempt.
|
|
195
|
-
|
|
196
|
-
### Streaming responses
|
|
197
|
-
|
|
198
|
-
`tk.call()` is for non-streaming requests. Streaming responses bypass the unified envelope and are forwarded as-is, so use any vendor SDK with `baseURL: tk.proxyUrl(...)`:
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
202
|
-
|
|
203
|
-
const anthropic = new Anthropic({
|
|
204
|
-
apiKey: accessToken,
|
|
205
|
-
baseURL: tk.proxyUrl('anthropic'),
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const stream = await anthropic.messages.stream({
|
|
209
|
-
model: 'claude-3-5-sonnet-latest',
|
|
210
|
-
max_tokens: 1024,
|
|
211
|
-
messages: [{ role: 'user', content: 'Tell me a story.' }],
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
for await (const event of stream) {
|
|
215
|
-
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
|
|
216
|
-
process.stdout.write(event.delta.text);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
## API
|
|
222
|
-
|
|
1
|
+
# @tokenite/sdk
|
|
2
|
+
|
|
3
|
+
One wallet for all your AI apps. Users store their Anthropic, OpenAI, and Google API keys in Tokenite, then grant your app metered access via OAuth. You pay nothing for AI usage.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tokenite/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Tokenite, isProxyError } from '@tokenite/sdk';
|
|
15
|
+
|
|
16
|
+
const tw = Tokenite({
|
|
17
|
+
clientId: 'your-app-id',
|
|
18
|
+
clientSecret: 'your-app-secret',
|
|
19
|
+
redirectUri: 'https://yourapp.com/callback',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 1. Redirect the user to the consent screen
|
|
23
|
+
app.get('/login', (req, res) => res.redirect(tk.getAuthorizeUrl()));
|
|
24
|
+
|
|
25
|
+
// 2. Exchange the OAuth code for an access token in your callback
|
|
26
|
+
app.get('/callback', async (req, res) => {
|
|
27
|
+
const { access_token } = await tk.exchangeCode(req.query.code as string);
|
|
28
|
+
req.session.tokeniteToken = access_token;
|
|
29
|
+
res.redirect('/');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 3. Call the LLM through the proxy
|
|
33
|
+
app.post('/chat', async (req, res) => {
|
|
34
|
+
const result = await tk.call({
|
|
35
|
+
accessToken: req.session.tokeniteToken,
|
|
36
|
+
provider: 'anthropic',
|
|
37
|
+
path: '/v1/messages',
|
|
38
|
+
body: {
|
|
39
|
+
model: 'claude-3-5-sonnet-latest',
|
|
40
|
+
max_tokens: 1024,
|
|
41
|
+
messages: [{ role: 'user', content: req.body.prompt }],
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (isProxyError(result)) {
|
|
46
|
+
return res.status(400).json({ error: result.error });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
res.json({ model: result.model, usage: result.usage, data: result.data });
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The same `tk.call(...)` works for OpenAI and Google — change `provider` and `path`:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
await tk.call({
|
|
57
|
+
accessToken,
|
|
58
|
+
provider: 'openai',
|
|
59
|
+
path: '/v1/chat/completions',
|
|
60
|
+
body: { model: 'gpt-4o', messages: [...] },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await tk.call({
|
|
64
|
+
accessToken,
|
|
65
|
+
provider: 'google',
|
|
66
|
+
path: '/v1beta/models/gemini-1.5-pro:generateContent',
|
|
67
|
+
body: { contents: [...] },
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Modal flow (single-page apps)
|
|
72
|
+
|
|
73
|
+
If your app is a SPA, open the consent screen in an iframe modal:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const { code } = await tk.popup({ suggestedBudget: 5 });
|
|
77
|
+
|
|
78
|
+
// Send the code to your backend, which calls tk.exchangeCode(code).
|
|
79
|
+
// The exchange requires clientSecret and must never run in browser code.
|
|
80
|
+
await fetch('/api/auth/exchange', {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
body: JSON.stringify({ code }),
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Managed agents (Anthropic)
|
|
87
|
+
|
|
88
|
+
Tokenite proxies Anthropic's [Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) surface — the `/v1/agents`, `/v1/environments`, `/v1/sessions`, `/v1/vaults`, `/v1/files`, and `/v1/skills` endpoints — under the same BYOK model as Messages. The app points the Anthropic SDK at `tk.proxyUrl('anthropic')`; the proxy injects the right beta header per surface (`managed-agents-2026-04-01` for agents/environments/sessions/vaults, `files-api-2025-04-14` for files, `skills-2025-10-02` for skills) and forwards calls to the user's Anthropic org. Client-supplied `anthropic-beta` headers are passed through and merged with the auto-injected ones, so callers can opt into additional betas (e.g. multi-version skills) without losing the proxy-managed defaults.
|
|
89
|
+
|
|
90
|
+
**Explicit consent required.** Because agent sessions run long-lived, billable server-side work on Anthropic (`$0.08/hr` session runtime on top of tokens), the proxy rejects agent-surface calls unless the app was created with `allowsManagedAgents: true`. The flag sits alongside the other app fields on creation:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
await admin.apps.create({
|
|
94
|
+
name: 'Life Coach',
|
|
95
|
+
callbackUrl: 'https://lifecoach.ai/callback',
|
|
96
|
+
modelStrategy: 'models',
|
|
97
|
+
allowedModels: ['claude-opus-4-7', 'claude-sonnet-4-6'],
|
|
98
|
+
allowsManagedAgents: true, // ← opt in
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Without it, every agent endpoint returns `403 AGENT_SCOPE_MISSING`.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
106
|
+
|
|
107
|
+
const anthropic = new Anthropic({
|
|
108
|
+
apiKey: accessToken,
|
|
109
|
+
baseURL: tk.proxyUrl('anthropic'),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Provision once per user.
|
|
113
|
+
const agent = await anthropic.beta.agents.create({
|
|
114
|
+
name: 'Life Coach',
|
|
115
|
+
model: 'claude-haiku-4-5',
|
|
116
|
+
system: 'You are a terse life coach.',
|
|
117
|
+
tools: [{ type: 'agent_toolset_20260401' }],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Spin up a session.
|
|
121
|
+
const env = await anthropic.beta.environments.create({ name: 'prod' });
|
|
122
|
+
const session = await anthropic.beta.sessions.create({
|
|
123
|
+
agent: agent.id,
|
|
124
|
+
environment_id: env.id,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Send events, stream responses — all proxied.
|
|
128
|
+
await anthropic.beta.sessions.events.post(session.id, {
|
|
129
|
+
type: 'user.message', content: 'What should I focus on today?',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
for await (const event of anthropic.beta.sessions.events.stream(session.id)) {
|
|
133
|
+
// ...
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Archive when done. This is what triggers Tokenite to pull the final
|
|
137
|
+
// usage totals from Anthropic and debit tokens + runtime against the budget.
|
|
138
|
+
await anthropic.beta.sessions.archive(session.id);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Tokenite tracks each session for attribution and auto-terminates running sessions when the user revokes the connection — otherwise Anthropic would keep billing `$0.08/hr` for the session runtime on top of tokens. The proxy bills the user's wallet at archive time using Anthropic's reported cumulative usage and runtime, plus the standard token rate from the model's pricing.
|
|
142
|
+
|
|
143
|
+
Budget is checked pre-call but not enforced mid-session: a session that started under budget can run past it. Revoke is the user's hard kill switch.
|
|
144
|
+
|
|
145
|
+
### Handling the OAuth callback
|
|
146
|
+
|
|
147
|
+
When Tokenite redirects back to your `redirect_uri`, the URL has one of:
|
|
148
|
+
|
|
149
|
+
- `?code=...&state=...` — user approved; exchange the code for an access token.
|
|
150
|
+
- `?error=access_denied&error_description=user_denied&state=...` — user clicked **Cancel** on the consent screen.
|
|
151
|
+
- `?error=...&error_description=...&state=...` — any other OAuth failure.
|
|
152
|
+
- nothing (or just `state`) — user landed on your callback path without completing a real OAuth round-trip.
|
|
153
|
+
|
|
154
|
+
`parseCallback(input, options?)` turns the URL into a typed result so you can switch on it instead of hand-parsing strings. Pure function; works in any runtime (Node, Bun, edge, browser).
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { parseCallback } from '@tokenite/sdk';
|
|
158
|
+
|
|
159
|
+
// In your /api/auth/callback handler:
|
|
160
|
+
const result = parseCallback(req.url, { expectedState: sessionStoredState });
|
|
161
|
+
|
|
162
|
+
if (result.ok) {
|
|
163
|
+
const { access_token } = await tk.exchangeCode(result.code);
|
|
164
|
+
// ... establish your app's session, redirect to /
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
switch (result.reason) {
|
|
169
|
+
case 'user_denied':
|
|
170
|
+
// User clicked Cancel. Don't show a scary error — render a
|
|
171
|
+
// friendly "you cancelled" screen with a "Try again" button.
|
|
172
|
+
return renderCancelled();
|
|
173
|
+
case 'invalid_state':
|
|
174
|
+
// CSRF check failed (state mismatch). Force a fresh login flow.
|
|
175
|
+
return renderRetry('Your sign-in session expired. Please try again.');
|
|
176
|
+
case 'missing_code':
|
|
177
|
+
// User navigated to /callback directly (bookmark, browser back).
|
|
178
|
+
return redirectToHome();
|
|
179
|
+
case 'access_denied':
|
|
180
|
+
case 'oauth_error':
|
|
181
|
+
// Real OAuth failure. result.error + result.description have details.
|
|
182
|
+
return renderError(result);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`parseCallback` accepts:
|
|
187
|
+
|
|
188
|
+
- A full URL string: `parseCallback('https://yourapp.com/cb?code=…&state=…')`
|
|
189
|
+
- A path + query: `parseCallback(req.url)` (Node's `req.url` works directly)
|
|
190
|
+
- A query string: `parseCallback('?code=…&state=…')` or `parseCallback('code=…&state=…')`
|
|
191
|
+
- A `URL` object: `parseCallback(new URL(...))`
|
|
192
|
+
- A `URLSearchParams`: `parseCallback(params)`
|
|
193
|
+
|
|
194
|
+
`expectedState` is optional. When you provide it, a mismatch returns `{ ok: false, reason: 'invalid_state' }` even if a `code` is present — treating a stolen code paired with a forged state as a CSRF attempt.
|
|
195
|
+
|
|
196
|
+
### Streaming responses
|
|
197
|
+
|
|
198
|
+
`tk.call()` is for non-streaming requests. Streaming responses bypass the unified envelope and are forwarded as-is, so use any vendor SDK with `baseURL: tk.proxyUrl(...)`:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
202
|
+
|
|
203
|
+
const anthropic = new Anthropic({
|
|
204
|
+
apiKey: accessToken,
|
|
205
|
+
baseURL: tk.proxyUrl('anthropic'),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const stream = await anthropic.messages.stream({
|
|
209
|
+
model: 'claude-3-5-sonnet-latest',
|
|
210
|
+
max_tokens: 1024,
|
|
211
|
+
messages: [{ role: 'user', content: 'Tell me a story.' }],
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
for await (const event of stream) {
|
|
215
|
+
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
|
|
216
|
+
process.stdout.write(event.delta.text);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## API
|
|
222
|
+
|
|
223
223
|
<!-- GEN:API -->
|
|
224
224
|
### `.getAuthorizeUrl(options?: AuthorizeOptions) => string`
|
|
225
225
|
|
|
@@ -248,350 +248,417 @@ The Tokenite dashboard base URL
|
|
|
248
248
|
### `.proxyBase`: `string`
|
|
249
249
|
|
|
250
250
|
The Tokenite proxy base URL
|
|
251
|
-
<!-- /GEN:API -->
|
|
252
|
-
|
|
253
|
-
## Types
|
|
254
|
-
|
|
251
|
+
<!-- /GEN:API -->
|
|
252
|
+
|
|
253
|
+
## Types
|
|
254
|
+
|
|
255
255
|
<!-- GEN:TYPES -->
|
|
256
256
|
```typescript
|
|
257
|
-
export type TokeniteConfig = {
|
|
258
|
-
/** Your app's client ID (from the Tokenite dashboard) */
|
|
259
|
-
readonly clientId: string;
|
|
260
|
-
/** Your app's client secret — only needed for server-side code exchange */
|
|
261
|
-
readonly clientSecret?: string;
|
|
262
|
-
/** The URL Tokenite redirects back to after authorization */
|
|
263
|
-
readonly redirectUri: string;
|
|
264
|
-
/** Tokenite base URL. Default: https://tokenite.ai */
|
|
265
|
-
readonly baseUrl?: string;
|
|
266
|
-
/** Tokenite proxy URL. Default: https://api.tokenite.ai */
|
|
267
|
-
readonly proxyUrl?: string;
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* OAuth 2.0 / OIDC `prompt` parameter. Tells Tokenite whether to
|
|
272
|
-
* re-prompt the user even when they have an existing session and/or
|
|
273
|
-
* an existing grant for this app.
|
|
274
|
-
*
|
|
275
|
-
* - `'
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
export type
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
readonly
|
|
301
|
-
/**
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
*
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
* - `'
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
readonly
|
|
328
|
-
/**
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
*
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
readonly
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
*
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
readonly
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
readonly
|
|
400
|
-
/**
|
|
401
|
-
readonly
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
readonly
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
* |
|
|
456
|
-
*
|
|
457
|
-
* | `
|
|
458
|
-
* | `
|
|
459
|
-
* | `
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
* |
|
|
463
|
-
*
|
|
464
|
-
* | `
|
|
465
|
-
* | `
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
* |
|
|
469
|
-
*
|
|
470
|
-
* | `
|
|
471
|
-
* | `
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
readonly
|
|
483
|
-
|
|
257
|
+
export type TokeniteConfig = {
|
|
258
|
+
/** Your app's client ID (from the Tokenite dashboard) */
|
|
259
|
+
readonly clientId: string;
|
|
260
|
+
/** Your app's client secret — only needed for server-side code exchange */
|
|
261
|
+
readonly clientSecret?: string;
|
|
262
|
+
/** The URL Tokenite redirects back to after authorization */
|
|
263
|
+
readonly redirectUri: string;
|
|
264
|
+
/** Tokenite base URL. Default: https://tokenite.ai */
|
|
265
|
+
readonly baseUrl?: string;
|
|
266
|
+
/** Tokenite proxy URL. Default: https://api.tokenite.ai */
|
|
267
|
+
readonly proxyUrl?: string;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* OAuth 2.0 / OIDC `prompt` parameter. Tells Tokenite whether to
|
|
272
|
+
* re-prompt the user even when they have an existing session and/or
|
|
273
|
+
* an existing grant for this app.
|
|
274
|
+
*
|
|
275
|
+
* - `'select_account'` — **recommended for "Sign in with Tokenite"
|
|
276
|
+
* buttons that follow a sign-out.** Interrupts the silent re-auth
|
|
277
|
+
* that would otherwise drop the user straight back into your app:
|
|
278
|
+
* instead Tokenite shows a "Continue as <email>?" confirmation card
|
|
279
|
+
* with a "Use a different account" option. The user's existing
|
|
280
|
+
* spending limit is preserved — they aren't asked to re-set it.
|
|
281
|
+
* - `'consent'` — re-show the **full** consent screen (budget input
|
|
282
|
+
* and all), discarding any existing grant. Use this only when you
|
|
283
|
+
* want the user to actively re-set their budget; for the common
|
|
284
|
+
* "they signed out, now they're signing back in" case, prefer
|
|
285
|
+
* `'select_account'`.
|
|
286
|
+
* - `'login'` — request that the user re-authenticate. Today this
|
|
287
|
+
* behaves the same as `'consent'` on Tokenite (full session-cookie
|
|
288
|
+
* clear is a TODO); the consent screen still shows.
|
|
289
|
+
* - `'none'` — never show UI; fail if interaction is required.
|
|
290
|
+
* Currently treated as the default (silent reauth).
|
|
291
|
+
*
|
|
292
|
+
* The OAuth 2.0 spec allows a space-separated combination
|
|
293
|
+
* (e.g. `'login consent'`); for that, pass the raw string. The
|
|
294
|
+
* union above is just for autocomplete on the common values.
|
|
295
|
+
*/
|
|
296
|
+
export type OAuthPrompt = 'login' | 'consent' | 'select_account' | 'none' | (string & {});
|
|
297
|
+
|
|
298
|
+
export type AuthorizeOptions = {
|
|
299
|
+
/** Custom state parameter for CSRF protection. Auto-generated if not provided. */
|
|
300
|
+
readonly state?: string;
|
|
301
|
+
/** Suggested budget amount (user can override on consent screen) */
|
|
302
|
+
readonly suggestedBudget?: number;
|
|
303
|
+
/**
|
|
304
|
+
* OAuth `prompt` parameter. Most common use: pass `'select_account'`
|
|
305
|
+
* on "Sign in with Tokenite" buttons that follow a sign-out — it
|
|
306
|
+
* stops the silent reauth that would otherwise drop the user
|
|
307
|
+
* straight back into your app, and shows a "Continue as <email>?"
|
|
308
|
+
* confirmation card instead. See {@link OAuthPrompt}.
|
|
309
|
+
*/
|
|
310
|
+
readonly prompt?: OAuthPrompt;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export type PopupOptions = {
|
|
314
|
+
/** Suggested budget amount (user can override on consent screen) */
|
|
315
|
+
readonly suggestedBudget?: number;
|
|
316
|
+
/**
|
|
317
|
+
* How to host the consent screen.
|
|
318
|
+
*
|
|
319
|
+
* - `'iframe'` (default) — overlay an iframe modal in the current
|
|
320
|
+
* window. Requires the dashboard to allow being framed by your
|
|
321
|
+
* origin (`Content-Security-Policy: frame-ancestors`). Cleaner UX
|
|
322
|
+
* but blocked by `X-Frame-Options: DENY`.
|
|
323
|
+
* - `'window'` — open a separate browser popup window via
|
|
324
|
+
* `window.open`. Works regardless of frame policy, but the user
|
|
325
|
+
* may be prompted by their popup blocker.
|
|
326
|
+
*/
|
|
327
|
+
readonly mode?: 'iframe' | 'window';
|
|
328
|
+
/** Modal/popup width in pixels. Default: 480 */
|
|
329
|
+
readonly width?: number;
|
|
330
|
+
/** Modal/popup height in pixels. Default: 620 */
|
|
331
|
+
readonly height?: number;
|
|
332
|
+
/**
|
|
333
|
+
* OAuth `prompt` parameter. Most common use: pass `'select_account'`
|
|
334
|
+
* on "Sign in with Tokenite" buttons that follow a sign-out — it
|
|
335
|
+
* stops the silent reauth that would otherwise drop the user
|
|
336
|
+
* straight back into your app, and shows a "Continue as <email>?"
|
|
337
|
+
* confirmation card instead. See {@link OAuthPrompt}.
|
|
338
|
+
*/
|
|
339
|
+
readonly prompt?: OAuthPrompt;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export type PopupResult = {
|
|
343
|
+
/**
|
|
344
|
+
* OAuth authorization code returned by the consent screen.
|
|
345
|
+
* Send this to your backend, which exchanges it for an access token
|
|
346
|
+
* via `tk.exchangeCode(code)`. The exchange requires `clientSecret`
|
|
347
|
+
* and must never run in browser code.
|
|
348
|
+
*/
|
|
349
|
+
readonly code: string;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
export type TopUpOptions = {
|
|
353
|
+
/**
|
|
354
|
+
* How to host the top-up screen.
|
|
355
|
+
*
|
|
356
|
+
* - `'popup'` (default) — open in a separate browser window via
|
|
357
|
+
* `window.open`. The user completes the form; the SDK resolves
|
|
358
|
+
* when Tokenite posts back the new limit.
|
|
359
|
+
* - `'redirect'` — full-page navigation. The builder's app loses
|
|
360
|
+
* in-memory state and must reload from the callback URL.
|
|
361
|
+
*/
|
|
362
|
+
readonly mode?: 'popup' | 'redirect';
|
|
363
|
+
/** Pre-fill the new-limit field. Default: 2 × current limit. */
|
|
364
|
+
readonly suggestedAmount?: number;
|
|
365
|
+
/** Popup width in pixels. Default: 480 */
|
|
366
|
+
readonly width?: number;
|
|
367
|
+
/** Popup height in pixels. Default: 560 */
|
|
368
|
+
readonly height?: number;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
export type TopUpResult =
|
|
372
|
+
| { readonly ok: true; readonly newLimit: number; readonly remaining: number }
|
|
373
|
+
| { readonly ok: false; readonly reason: 'cancelled' | 'popup-blocked' | 'closed' | 'redirected' };
|
|
374
|
+
|
|
375
|
+
export type CallWithRecoveryOptions = {
|
|
376
|
+
/**
|
|
377
|
+
* What to do when the proxy returns a recoverable funding error.
|
|
378
|
+
*
|
|
379
|
+
* - `'popup'` (default) — open Tokenite's top-up popup, then retry the
|
|
380
|
+
* call once on success.
|
|
381
|
+
* - `'redirect'` — full-page navigation; the call does not retry (the
|
|
382
|
+
* builder's app re-loads from the callback and re-invokes manually).
|
|
383
|
+
* - `'throw'` — disable recovery; surface the original error.
|
|
384
|
+
*/
|
|
385
|
+
readonly onFundingNeeded?: 'popup' | 'redirect' | 'throw';
|
|
386
|
+
/** Override the suggested top-up amount. */
|
|
387
|
+
readonly suggestedAmount?: number;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
export type TokenResponse = {
|
|
391
|
+
readonly access_token: string;
|
|
392
|
+
readonly token_type: string;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
export type Provider = 'anthropic' | 'openai' | 'google' | 'grok' | 'bedrock';
|
|
396
|
+
|
|
397
|
+
export type ProxyCallOptions = {
|
|
398
|
+
/** The user's Tokenite access token (returned by `tk.exchangeCode()`) */
|
|
399
|
+
readonly accessToken: string;
|
|
400
|
+
/** Which LLM provider to call */
|
|
401
|
+
readonly provider: Provider;
|
|
402
|
+
/** Path on the provider's API (e.g. `/v1/messages`, `/v1/chat/completions`) */
|
|
403
|
+
readonly path: string;
|
|
404
|
+
/** HTTP method. Default: `POST` */
|
|
405
|
+
readonly method?: string;
|
|
406
|
+
/** Request body — the vendor's request shape, JSON-serialised by the SDK */
|
|
407
|
+
readonly body: unknown;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// ─── Unified proxy response types ───
|
|
411
|
+
//
|
|
412
|
+
// Every non-streaming response from the Tokenite proxy returns one of
|
|
413
|
+
// these shapes. SDK consumers can always check for `.error` to distinguish
|
|
414
|
+
// success from failure — no vendor-specific parsing required.
|
|
415
|
+
|
|
416
|
+
/** Normalised token counts (identical across all providers) */
|
|
417
|
+
export type ProxyUsage = {
|
|
418
|
+
/** Number of tokens in the prompt / input */
|
|
419
|
+
readonly inputTokens: number;
|
|
420
|
+
/** Number of tokens in the completion / output */
|
|
421
|
+
readonly outputTokens: number;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Successful proxy response.
|
|
426
|
+
*
|
|
427
|
+
* `data` contains the original vendor response body (e.g. Anthropic's
|
|
428
|
+
* message object, OpenAI's chat completion, etc.). `provider`, `model`,
|
|
429
|
+
* and `usage` are extracted and normalised by the proxy so you don't
|
|
430
|
+
* need to parse vendor-specific fields.
|
|
431
|
+
*/
|
|
432
|
+
export type ProxySuccess = {
|
|
433
|
+
/** Which LLM provider handled the request */
|
|
434
|
+
readonly provider: Provider;
|
|
435
|
+
/** The model that generated the response */
|
|
436
|
+
readonly model: string;
|
|
437
|
+
/** Normalised token usage, or null if the provider didn't report it */
|
|
438
|
+
readonly usage: ProxyUsage | null;
|
|
439
|
+
/** The original, unmodified response body from the LLM provider */
|
|
440
|
+
readonly data: unknown;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Where the error originated.
|
|
445
|
+
*
|
|
446
|
+
* - `"proxy"` — Tokenite rejected the request (auth, budget, config).
|
|
447
|
+
* - `"provider"` — The upstream LLM returned an error (rate limit, overload, etc.).
|
|
448
|
+
*/
|
|
449
|
+
export type ErrorSource = 'proxy' | 'provider';
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Error response (both proxy-level and provider-level errors share this shape).
|
|
453
|
+
*
|
|
454
|
+
* **Proxy error codes** (`source: "proxy"`):
|
|
455
|
+
* | Code | HTTP | Description |
|
|
456
|
+
* |---|---|---|
|
|
457
|
+
* | `TOKEN_INVALID` | 401 | Missing or invalid bearer token |
|
|
458
|
+
* | `TOKEN_REVOKED` | 401 | Access token was revoked by the user |
|
|
459
|
+
* | `TOKEN_SUSPENDED` | 403 | Access suspended by the user |
|
|
460
|
+
* | `TOKEN_EXPIRED` | 401 | Access token past its expiration date |
|
|
461
|
+
* | `BUDGET_EXCEEDED` | 402 | Spending reached the user-defined budget limit |
|
|
462
|
+
* | `PROVIDER_KEY_MISSING` | 402 | No API key or credits available for the provider |
|
|
463
|
+
* | `CREDITS_DEPLETED` | 402 | Insufficient platform credits |
|
|
464
|
+
* | `APP_NOT_FOUND` | 404 | Application not found |
|
|
465
|
+
* | `MODEL_NOT_ALLOWED` | 403 | Model not in the app's allowed models list |
|
|
466
|
+
*
|
|
467
|
+
* **Provider error codes** (`source: "provider"`):
|
|
468
|
+
* | Code | HTTP | Description |
|
|
469
|
+
* |---|---|---|
|
|
470
|
+
* | `RATE_LIMITED` | 429 | Upstream rate limit hit — retry after backoff |
|
|
471
|
+
* | `CONTEXT_LENGTH_EXCEEDED` | 400 | Request exceeds model's context window |
|
|
472
|
+
* | `INVALID_REQUEST` | 400 | Provider rejected the request as malformed |
|
|
473
|
+
* | `AUTHENTICATION_FAILED` | 401 | Provider rejected the API key |
|
|
474
|
+
* | `PROVIDER_OVERLOADED` | 503 | Provider temporarily overloaded |
|
|
475
|
+
* | `CONTENT_FILTERED` | 400 | Content blocked by safety filter |
|
|
476
|
+
* | `PROVIDER_TIMEOUT` | 504 | Provider did not respond in time |
|
|
477
|
+
* | `PROVIDER_ERROR` | 502 | Catch-all for other provider errors |
|
|
478
|
+
*/
|
|
479
|
+
export type ProxyError = {
|
|
480
|
+
readonly error: {
|
|
481
|
+
/** Machine-readable error code */
|
|
482
|
+
readonly code: string;
|
|
483
|
+
/** Human-readable description */
|
|
484
|
+
readonly message: string;
|
|
485
|
+
/** Where the error originated */
|
|
486
|
+
readonly source: ErrorSource;
|
|
487
|
+
/** Optional structured context (e.g. `retryAfter`, `provider`, `providerMessage`) */
|
|
488
|
+
readonly details?: Record<string, unknown>;
|
|
489
|
+
};
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
/** Discriminated union for all non-streaming proxy responses */
|
|
493
|
+
export type ProxyResponse = ProxySuccess | ProxyError;
|
|
494
|
+
|
|
495
|
+
/** Type guard: returns true if the response is an error */
|
|
496
|
+
export const isProxyError = (response: ProxyResponse): response is ProxyError =>
|
|
497
|
+
'error' in response;
|
|
498
|
+
|
|
499
|
+
/** Type guard: returns true if the response is a success */
|
|
500
|
+
export const isProxySuccess = (response: ProxyResponse): response is ProxySuccess =>
|
|
501
|
+
'provider' in response;
|
|
502
|
+
|
|
503
|
+
// ─── Access context (tk.getAccessContext) ───
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Identity of the user who holds this access token.
|
|
507
|
+
*
|
|
508
|
+
* Use `id` as the stable key for per-user state in your app — it survives
|
|
509
|
+
* token refreshes, re-logins, and device switches. `email` is suitable for
|
|
510
|
+
* display in your UI; treat it as user-controlled and re-fetch on each
|
|
511
|
+
* session if you cache it.
|
|
512
|
+
*/
|
|
513
|
+
export type UserInfo = {
|
|
514
|
+
/** Stable Tokenite user id (UUID) */
|
|
515
|
+
readonly id: string;
|
|
516
|
+
/** The user's email address as registered with Tokenite */
|
|
517
|
+
readonly email: string;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/** Visual + identity metadata for a single provider */
|
|
521
|
+
export type ProviderInfo = {
|
|
522
|
+
/** Stable provider id (same value as the `Provider` union) */
|
|
523
|
+
readonly id: Provider;
|
|
524
|
+
/** Human-readable name, e.g. "Anthropic" */
|
|
525
|
+
readonly displayName: string;
|
|
526
|
+
/** Brand colour (hex string, e.g. "#d97706") */
|
|
527
|
+
readonly color: string;
|
|
528
|
+
/** Absolute URL to the provider's logo (PNG or SVG) */
|
|
529
|
+
readonly logoUrl: string;
|
|
530
|
+
/** Whether the logo is a glyph/symbol or a full wordmark */
|
|
531
|
+
readonly logoStyle: 'symbol' | 'wordmark';
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* A model the access token may call, scoped to the app's model strategy
|
|
536
|
+
* and the holder's provider keys.
|
|
537
|
+
*
|
|
538
|
+
* - `servedBy` — every provider that hosts this model (catalog fact).
|
|
539
|
+
* - `callableNow` — the subset the holder can run *right now* (they hold
|
|
540
|
+
* a key for it). Empty means the model is visible but not yet usable —
|
|
541
|
+
* render it disabled, or prompt the user to add a key.
|
|
542
|
+
*
|
|
543
|
+
* Pass `slug` as the `model` field in `tk.call()`.
|
|
544
|
+
*/
|
|
545
|
+
export type ModelInfo = {
|
|
546
|
+
/** Stable Tokenite model slug — pass this as `model` in tk.call() */
|
|
547
|
+
readonly slug: string;
|
|
548
|
+
/** Human-readable name, e.g. "Claude Haiku 4.5" */
|
|
549
|
+
readonly displayName: string;
|
|
550
|
+
/** The lab that built the model */
|
|
551
|
+
readonly creator: 'anthropic' | 'openai' | 'google' | 'grok';
|
|
552
|
+
/** Absolute URL to the creator's logo — use it as the model's icon in a picker */
|
|
553
|
+
readonly creatorLogoUrl: string;
|
|
554
|
+
/** Capability tiers this model satisfies (cheap / fast / smart / reasoning) */
|
|
555
|
+
readonly tiers: readonly string[];
|
|
556
|
+
/** Feature capabilities, e.g. "vision", "tools", "thinking" */
|
|
557
|
+
readonly capabilities: readonly string[];
|
|
558
|
+
/** Every provider that serves this model */
|
|
559
|
+
readonly servedBy: readonly Provider[];
|
|
560
|
+
/** Providers the holder can run it through right now (subset of servedBy) */
|
|
561
|
+
readonly callableNow: readonly Provider[];
|
|
562
|
+
/** Indicative price per million tokens */
|
|
563
|
+
readonly pricing: {
|
|
564
|
+
readonly inputPerMillion: number;
|
|
565
|
+
readonly outputPerMillion: number;
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* A provider-agnostic capability bucket. Use this for a "pick a speed /
|
|
571
|
+
* quality" UI where the user never sees a model name.
|
|
572
|
+
*/
|
|
573
|
+
export type TierInfo = {
|
|
574
|
+
/** Tier id: "cheap" | "fast" | "smart" | "reasoning" */
|
|
575
|
+
readonly id: string;
|
|
576
|
+
/** Whether the holder can run at least one model in this tier */
|
|
577
|
+
readonly reachable: boolean;
|
|
578
|
+
/** A representative callable model slug for this tier, or null */
|
|
579
|
+
readonly recommendedModel: string | null;
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
/** Summary of the app the access token belongs to */
|
|
583
|
+
export type AppInfo = {
|
|
584
|
+
readonly id: string;
|
|
585
|
+
readonly name: string;
|
|
586
|
+
readonly description: string | null;
|
|
587
|
+
readonly websiteUrl: string | null;
|
|
588
|
+
/** Absolute URL to the app's icon (PNG/SVG). null when the developer hasn't set one — render initials or a generic glyph. */
|
|
589
|
+
readonly iconUrl: string | null;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Full access context for a single access token: the app it belongs to,
|
|
594
|
+
* the user who holds it, and the providers it can call.
|
|
595
|
+
*
|
|
596
|
+
* `providers` lists only the providers the user has an active key for —
|
|
597
|
+
* exactly the set that will succeed through `tk.call()` (budget permitting).
|
|
598
|
+
*
|
|
599
|
+
* `user` identifies the human who holds the token. Use `user.id` as the
|
|
600
|
+
* stable key for any per-user state in your app — it survives token
|
|
601
|
+
* refreshes and re-logins, unlike the access token itself.
|
|
602
|
+
*/
|
|
603
|
+
export type AccessContext = {
|
|
604
|
+
readonly app: AppInfo;
|
|
605
|
+
readonly user: UserInfo;
|
|
606
|
+
readonly providers: readonly ProviderInfo[];
|
|
607
|
+
/**
|
|
608
|
+
* Models the token may call — already filtered to the app's strategy
|
|
609
|
+
* and the user's keys. Render a picker from this; no need to maintain
|
|
610
|
+
* your own model list. Each entry's `callableNow` says whether it's
|
|
611
|
+
* usable now or needs a key.
|
|
612
|
+
*/
|
|
613
|
+
readonly models: readonly ModelInfo[];
|
|
614
|
+
/**
|
|
615
|
+
* Provider-agnostic capability buckets. For a "pick a tier" UI where
|
|
616
|
+
* the user never sees a model name — `recommendedModel` gives you a
|
|
617
|
+
* concrete slug to pass to `tk.call()`.
|
|
618
|
+
*/
|
|
619
|
+
readonly tiers: readonly TierInfo[];
|
|
484
620
|
};
|
|
485
|
-
|
|
486
|
-
/** Discriminated union for all non-streaming proxy responses */
|
|
487
|
-
export type ProxyResponse = ProxySuccess | ProxyError;
|
|
488
|
-
|
|
489
|
-
/** Type guard: returns true if the response is an error */
|
|
490
|
-
export const isProxyError = (response: ProxyResponse): response is ProxyError =>
|
|
491
|
-
'error' in response;
|
|
492
|
-
|
|
493
|
-
/** Type guard: returns true if the response is a success */
|
|
494
|
-
export const isProxySuccess = (response: ProxyResponse): response is ProxySuccess =>
|
|
495
|
-
'provider' in response;
|
|
496
|
-
|
|
497
|
-
// ─── Access context (tk.getAccessContext) ───
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Identity of the user who holds this access token.
|
|
501
|
-
*
|
|
502
|
-
* Use `id` as the stable key for per-user state in your app — it survives
|
|
503
|
-
* token refreshes, re-logins, and device switches. `email` is suitable for
|
|
504
|
-
* display in your UI; treat it as user-controlled and re-fetch on each
|
|
505
|
-
* session if you cache it.
|
|
506
|
-
*/
|
|
507
|
-
export type UserInfo = {
|
|
508
|
-
/** Stable Tokenite user id (UUID) */
|
|
509
|
-
readonly id: string;
|
|
510
|
-
/** The user's email address as registered with Tokenite */
|
|
511
|
-
readonly email: string;
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
/** Visual + identity metadata for a single provider */
|
|
515
|
-
export type ProviderInfo = {
|
|
516
|
-
/** Stable provider id (same value as the `Provider` union) */
|
|
517
|
-
readonly id: Provider;
|
|
518
|
-
/** Human-readable name, e.g. "Anthropic" */
|
|
519
|
-
readonly displayName: string;
|
|
520
|
-
/** Brand colour (hex string, e.g. "#d97706") */
|
|
521
|
-
readonly color: string;
|
|
522
|
-
/** Absolute URL to the provider's logo (PNG or SVG) */
|
|
523
|
-
readonly logoUrl: string;
|
|
524
|
-
/** Whether the logo is a glyph/symbol or a full wordmark */
|
|
525
|
-
readonly logoStyle: 'symbol' | 'wordmark';
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
/** Summary of the app the access token belongs to */
|
|
529
|
-
export type AppInfo = {
|
|
530
|
-
readonly id: string;
|
|
531
|
-
readonly name: string;
|
|
532
|
-
readonly description: string | null;
|
|
533
|
-
readonly websiteUrl: string | null;
|
|
534
|
-
/** Absolute URL to the app's icon (PNG/SVG). null when the developer hasn't set one — render initials or a generic glyph. */
|
|
535
|
-
readonly iconUrl: string | null;
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Full access context for a single access token: the app it belongs to,
|
|
540
|
-
* the user who holds it, and the providers it can call.
|
|
541
|
-
*
|
|
542
|
-
* `providers` lists only the providers the user has an active key for —
|
|
543
|
-
* exactly the set that will succeed through `tk.call()` (budget permitting).
|
|
544
|
-
*
|
|
545
|
-
* `user` identifies the human who holds the token. Use `user.id` as the
|
|
546
|
-
* stable key for any per-user state in your app — it survives token
|
|
547
|
-
* refreshes and re-logins, unlike the access token itself.
|
|
548
|
-
*/
|
|
549
|
-
export type AccessContext = {
|
|
550
|
-
readonly app: AppInfo;
|
|
551
|
-
readonly user: UserInfo;
|
|
552
|
-
readonly providers: readonly ProviderInfo[];
|
|
553
|
-
};
|
|
554
|
-
```
|
|
555
|
-
<!-- /GEN:TYPES -->
|
|
556
|
-
|
|
557
|
-
## Error Codes
|
|
558
|
-
|
|
559
|
-
Non-streaming proxy responses return a unified error envelope:
|
|
560
|
-
|
|
561
|
-
```json
|
|
562
|
-
{ "error": { "code": "...", "message": "...", "source": "proxy" | "provider", "details": { } } }
|
|
563
621
|
```
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
|
579
|
-
|
|
580
|
-
| `
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
|
585
|
-
|
|
586
|
-
| `
|
|
587
|
-
| `
|
|
588
|
-
| `
|
|
589
|
-
| `
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
622
|
+
<!-- /GEN:TYPES -->
|
|
623
|
+
|
|
624
|
+
## Error Codes
|
|
625
|
+
|
|
626
|
+
Non-streaming proxy responses return a unified error envelope:
|
|
627
|
+
|
|
628
|
+
```json
|
|
629
|
+
{ "error": { "code": "...", "message": "...", "source": "proxy" | "provider", "details": { } } }
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
`source` distinguishes errors raised by the Tokenite proxy from errors forwarded from the upstream LLM. Use the `isProxyError` type guard to narrow a `ProxyResponse` before reading `error.code`.
|
|
633
|
+
|
|
634
|
+
### Proxy errors (`source: "proxy"`)
|
|
635
|
+
|
|
636
|
+
| Code | Status | Meaning |
|
|
637
|
+
|------|--------|---------|
|
|
638
|
+
| `TOKEN_INVALID` | 401 | Missing or unrecognized access token |
|
|
639
|
+
| `TOKEN_REVOKED` | 401 | User revoked access |
|
|
640
|
+
| `TOKEN_EXPIRED` | 401 | Token expired (30-day lifetime) |
|
|
641
|
+
| `TOKEN_SUSPENDED` | 403 | User temporarily suspended access |
|
|
642
|
+
| `BUDGET_EXCEEDED` | 402 | Spending limit reached |
|
|
643
|
+
| `PROVIDER_KEY_MISSING` | 402 | No API key or credits available for the provider |
|
|
644
|
+
| `CREDITS_DEPLETED` | 402 | Insufficient platform credits |
|
|
645
|
+
| `MODEL_NOT_ALLOWED` | 403 | Model not in your app's allowed list |
|
|
646
|
+
| `AGENT_SCOPE_MISSING` | 403 | App doesn't have the managed-agents scope (set `allowsManagedAgents: true` at app creation) |
|
|
647
|
+
| `APP_NOT_FOUND` | 404 | Application not found |
|
|
648
|
+
|
|
649
|
+
### Provider errors (`source: "provider"`)
|
|
650
|
+
|
|
651
|
+
| Code | Status | Meaning |
|
|
652
|
+
|------|--------|---------|
|
|
653
|
+
| `RATE_LIMITED` | 429 | Upstream rate limit hit — retry after backoff |
|
|
654
|
+
| `CONTEXT_LENGTH_EXCEEDED` | 400 | Request exceeds the model's context window |
|
|
655
|
+
| `INVALID_REQUEST` | 400 | Provider rejected the request as malformed |
|
|
656
|
+
| `CONTENT_FILTERED` | 400 | Content blocked by the provider's safety filter |
|
|
657
|
+
| `AUTHENTICATION_FAILED` | 401 | Provider rejected the API key |
|
|
658
|
+
| `PROVIDER_OVERLOADED` | 503 | Provider temporarily overloaded |
|
|
659
|
+
| `PROVIDER_TIMEOUT` | 504 | Provider did not respond in time |
|
|
660
|
+
| `PROVIDER_ERROR` | 502 | Catch-all for other provider errors |
|
|
661
|
+
|
|
662
|
+
## License
|
|
663
|
+
|
|
664
|
+
MIT
|