aisubs 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,543 +1,526 @@
1
1
  <table>
2
2
  <tr>
3
3
  <td><img src="./dashboard/public/aisubs-mark.svg" alt="AISubs icon" width="72"></td>
4
- <td>
5
- <h1>AISubs</h1>
6
- <strong>Connect your AI subscriptions. Use them anywhere.</strong>
7
- </td>
4
+ <td><h1>AISubs</h1><strong>Connect your AI subscriptions. Use them anywhere.</strong></td>
8
5
  </tr>
9
6
  </table>
10
7
 
11
- Connect once, then use your subscriptions through an SDK in your app, a local
12
- API, cURL, or any compatible tool.
8
+ AISubs connects provider accounts once and exposes each account through a local,
9
+ account-scoped API. Use it from the official OpenAI or Anthropic SDK, Vercel AI SDK,
10
+ TanStack AI, Python, cURL, or an app that accepts a custom API base URL.
13
11
 
14
12
  <p align="center">
15
- <img src="./public/aisubs-dashboard.png" alt="AISubs dashboard showing connected accounts" width="100%" />
13
+ <img src="./public/aisubs-dashboard.png" alt="AI Subs dashboard with provider connections and local API access" width="100%" />
16
14
  </p>
17
15
 
18
- <p align="center">
19
- <img src="./public/aisubs-chatgpt-account.png" alt="AISubs ChatGPT account details showing plan, usage, and integration examples" width="33%" />
20
- <img src="./public/aisubs-grok-account.png" alt="AISubs Grok account details showing plan, usage, and integration examples" width="33%" />
21
- <img src="./public/aisubs-copilot-account.png" alt="AISubs GitHub Copilot account details showing credits and integration examples" width="33%" />
22
- </p>
23
-
24
- > AISubs keeps credentials on your computer. It collects no telemetry, analytics, request logs, or activity history.
16
+ > Credentials, API keys, and requests remain on your computer. AISubs collects no
17
+ > telemetry or analytics. The dashboard keeps up to 200 redacted account request logs in memory for debugging.
25
18
 
26
19
  ## Quick start
27
20
 
21
+ AISubs requires [Node.js 24 or newer](https://nodejs.org/en/download/). Run it
22
+ without adding it to a project:
23
+
28
24
  ```bash
29
- nubx aisubs dashboard # Nub (recommended)
30
- npx aisubs dashboard # npm
31
- pnpm exec aisubs dashboard # pnpm
32
- bunx aisubs dashboard # Bun
25
+ nubx aisubs@latest dashboard # Nub
26
+ npx aisubs@latest dashboard # npm
27
+ pnpm dlx aisubs@latest dashboard # pnpm
28
+ bunx aisubs@latest dashboard # Bun
33
29
  ```
34
30
 
35
- Click **Add account**, choose a provider, complete sign-in, and give the
36
- account a local name such as `personal` or `work`.
31
+ The terminal prints only the local dashboard URL. Open it, click **Add account**,
32
+ and complete the provider sign-in. The dashboard manages the persistent local API
33
+ key: reveal it, copy it, or deliberately regenerate it there.
37
34
 
38
- ## The idea
35
+ The default URL is `http://127.0.0.1:4319`. Credentials and the API key are stored
36
+ under `~/.aisubs` and reused on later starts.
39
37
 
40
- 1. Connect a provider account and give it a local name, such as `personal`.
41
- 2. Ask AISubs which models and request format that account supports.
42
- 3. Send the provider-native request through that account.
38
+ ## Use an account from any compatible app
43
39
 
44
- One provider can have many accounts:
40
+ Open an account in the dashboard and copy its base URL:
45
41
 
46
42
  ```text
47
- ChatGPT / personal
48
- ChatGPT / work
49
- Claude / team
43
+ http://127.0.0.1:4319/aisubs/PROVIDER/ACCOUNT/v1
50
44
  ```
51
45
 
52
- AISubs never silently switches accounts. Your application chooses the account
53
- for each request.
46
+ Then configure the app with:
54
47
 
55
- ## Providers
48
+ ```text
49
+ API base URL: the account URL copied from AISubs
50
+ API key: the persistent key shown on the AISubs dashboard
51
+ Model: an exact model ID shown for that account
52
+ ```
56
53
 
57
- | Provider | ID | Sign-in | Request format |
58
- | -------------- | -------------- | ---------------------- | --------------------------- |
59
- | ChatGPT | `chatgpt` | Browser or device code | Responses |
60
- | Claude | `claude` | Browser | Anthropic Messages |
61
- | GitHub Copilot | `copilot` | Device code | Read from the model catalog |
62
- | Grok | `grok` | Device code | Read from the model catalog |
63
- | OpenCode Go | `opencode-go` | API key | Read from the model catalog |
64
- | OpenCode Zen | `opencode-zen` | API key | Read from the model catalog |
54
+ For apps configured with environment variables:
65
55
 
66
- Provider model lists and protocols can change. Discover models at runtime and
67
- pin the AISubs version your application has tested.
56
+ ```bash
57
+ export OPENAI_BASE_URL="http://127.0.0.1:4319/aisubs/grok/personal/v1"
58
+ export OPENAI_API_KEY="aisubs_..."
59
+ ```
68
60
 
69
- ## Fastest start: direct Node.js
61
+ AISubs removes its local key before forwarding a request and adds only the
62
+ selected account's provider credential.
70
63
 
71
- This is the simplest integration. It needs no local server and no AISubs API
72
- key.
64
+ ### Compatibility contract
73
65
 
74
- ### 1. Create AISubs once
66
+ AISubs preserves the provider's native request and response protocol. It also
67
+ provides a focused, non-streaming text Chat Completions adapter for ChatGPT
68
+ accounts so apps such as [Handy](https://github.com/cjpais/Handy) can use them.
75
69
 
76
- ```js
77
- // subscriptions.js
78
- import { chatGptProvider, createSubscriptionAuth } from "aisubs";
70
+ | Provider/model protocol | Base URL | Supported request path |
71
+ | ---------------------------------- | --------------------------- | --------------------------------------- |
72
+ | OpenAI Responses | Account URL ending in `/v1` | `POST /responses` |
73
+ | OpenAI-compatible Chat Completions | Account URL ending in `/v1` | `POST /chat/completions` |
74
+ | Anthropic Messages | Account URL ending in `/v1` | `POST /messages` |
75
+ | Google generateContent | Account URL ending in `/v1` | `POST /models/MODEL_ID:generateContent` |
76
+ | Model discovery | Account URL ending in `/v1` | `GET /models` |
79
77
 
80
- export const subscriptions = createSubscriptionAuth({
81
- providers: [chatGptProvider()],
82
- });
83
- ```
84
-
85
- By default, credentials are stored at `~/.aisubs/credentials.json`.
78
+ Embeddings are intentionally not exposed. The ChatGPT compatibility adapter
79
+ supports text messages and JSON-schema response formats; tools, images, audio,
80
+ and streaming Chat Completions remain native-Responses-only features.
86
81
 
87
- Keep this object in trusted backend code. Do not send it to a browser.
82
+ ### Handy
88
83
 
89
- ### 2. Connect an account
84
+ Choose **Custom** in Handy and copy these values from the ChatGPT account page:
90
85
 
91
- ```js
92
- import { subscriptions } from "./subscriptions.js";
86
+ ```text
87
+ Base URL: http://127.0.0.1:4319/aisubs/chatgpt/ACCOUNT/v1
88
+ API key: the persistent key shown on the AISubs dashboard
89
+ Model: an exact model ID shown for the account
90
+ ```
93
91
 
94
- const account = subscriptions.account("chatgpt", "personal");
92
+ Handy sends `POST /chat/completions` with streaming disabled. AISubs translates
93
+ that request to ChatGPT Responses and returns the `choices[0].message.content`
94
+ shape Handy reads.
95
95
 
96
- if (!(await account.status()).authenticated) {
97
- const login = await account.signIn();
96
+ ## SDK examples
98
97
 
99
- if (login.prompt.mode === "browser") {
100
- console.log("Open:", login.prompt.authorizationUri);
101
- }
102
- if (login.prompt.mode === "device") {
103
- console.log("Open:", login.prompt.verificationUri);
104
- console.log("Code:", login.prompt.userCode);
105
- }
98
+ Set the key once for the shell running your client:
106
99
 
107
- await login.wait();
108
- }
100
+ ```bash
101
+ export AISUBS_API_KEY="aisubs_..."
109
102
  ```
110
103
 
111
- ### 3. Discover a model and send a request
104
+ <details>
105
+ <summary><strong>Official OpenAI JavaScript SDK — Responses and Chat Completions</strong></summary>
112
106
 
113
- This example selects the first available ChatGPT model, so it does not depend
114
- on a model ID that may change:
107
+ Install:
108
+
109
+ ```bash
110
+ nub install openai
111
+ npm install openai
112
+ pnpm add openai
113
+ bun add openai
114
+ ```
115
115
 
116
116
  ```js
117
- const catalog = await account.getModels();
118
- if (!catalog) throw new Error("This provider does not expose models");
119
- console.log(
120
- "Available model IDs:",
121
- catalog.models.map((item) => item.id),
122
- );
117
+ import OpenAI from "openai";
123
118
 
124
- const modelId = catalog.models.find((item) => item.selectable !== false)?.id;
125
- if (!modelId) throw new Error("No ChatGPT model is available");
126
- console.log("Using model:", modelId);
119
+ const client = new OpenAI({
120
+ baseURL: "http://127.0.0.1:4319/aisubs/chatgpt/personal/v1",
121
+ apiKey: process.env.AISUBS_API_KEY,
122
+ });
127
123
 
128
- const response = await account.proxy("responses", {
129
- method: "POST",
130
- headers: { "content-type": "application/json" },
131
- body: JSON.stringify({
132
- model: modelId,
133
- store: false,
134
- stream: true,
135
- input: "Hello from AISubs",
136
- }),
124
+ const stream = await client.responses.create({
125
+ model: "MODEL_ID_FROM_DASHBOARD",
126
+ input: "Hello from AISubs",
127
+ store: false,
128
+ stream: true,
137
129
  });
138
130
 
139
- if (!response.ok) throw new Error(await response.text());
140
- if (response.body) {
141
- for await (const chunk of response.body) process.stdout.write(Buffer.from(chunk));
142
- }
131
+ for await (const event of stream) console.log(event);
143
132
  ```
144
133
 
145
- Here, `modelId` is the exact ID returned by `account.getModels()`. Model IDs
146
- belong to the connected provider account and can change, so list them first
147
- instead of copying a fixed ID from documentation.
134
+ Chat Completions uses the same client with an account/model that reports that
135
+ endpoint:
148
136
 
149
- `store: false` is a request option for the Responses API; it is unrelated to
150
- AISubs credential storage and keeps response storage disabled. `stream: true`
151
- asks for incremental output, which is why the example reads `response.body` in
152
- chunks. Keep both options for ChatGPT subscription Responses requests.
137
+ ```js
138
+ const response = await client.chat.completions.create({
139
+ model: "MODEL_ID_FROM_DASHBOARD",
140
+ messages: [{ role: "user", content: "Hello from AISubs" }],
141
+ });
153
142
 
154
- AISubs refreshes expired credentials automatically and retries a provider `401`
155
- once.
143
+ console.log(response.choices[0]?.message.content);
144
+ ```
156
145
 
157
- <details>
158
- <summary><strong>Use another provider</strong></summary>
146
+ </details>
159
147
 
160
- Add its provider factory when creating AISubs:
148
+ <details>
149
+ <summary><strong>Official Anthropic JavaScript SDK</strong></summary>
161
150
 
162
- ```js
163
- import {
164
- chatGptProvider,
165
- claudeProvider,
166
- copilotProvider,
167
- createSubscriptionAuth,
168
- grokProvider,
169
- openCodeGoProvider,
170
- openCodeZenProvider,
171
- } from "aisubs";
151
+ Install:
172
152
 
173
- const subscriptions = createSubscriptionAuth({
174
- providers: [
175
- chatGptProvider(),
176
- claudeProvider(),
177
- copilotProvider(),
178
- grokProvider(),
179
- openCodeGoProvider(),
180
- openCodeZenProvider(),
181
- ],
182
- });
153
+ ```bash
154
+ nub install @anthropic-ai/sdk
155
+ npm install @anthropic-ai/sdk
156
+ pnpm add @anthropic-ai/sdk
157
+ bun add @anthropic-ai/sdk
183
158
  ```
184
159
 
185
- To use a different file, pass a custom store:
186
-
187
160
  ```js
188
- import { chatGptProvider, createSubscriptionAuth, FileCredentialStore } from "aisubs";
161
+ import Anthropic from "@anthropic-ai/sdk";
189
162
 
190
- const subscriptions = createSubscriptionAuth({
191
- store: new FileCredentialStore("./data/aisubs-credentials.json"),
192
- providers: [chatGptProvider()],
163
+ const client = new Anthropic({
164
+ baseURL: "http://127.0.0.1:4319/aisubs/claude/team",
165
+ apiKey: process.env.AISUBS_API_KEY,
193
166
  });
194
- ```
195
167
 
196
- Then select the account by provider ID:
168
+ const message = await client.messages.create({
169
+ model: "MODEL_ID_FROM_DASHBOARD",
170
+ max_tokens: 1024,
171
+ messages: [{ role: "user", content: "Hello from AISubs" }],
172
+ });
197
173
 
198
- ```js
199
- const claude = subscriptions.account("claude", "team");
200
- const copilot = subscriptions.account("copilot", "github");
201
- const grok = subscriptions.account("grok", "personal");
202
- const go = subscriptions.account("opencode-go", "team");
203
- const zen = subscriptions.account("opencode-zen", "lab");
174
+ console.log(message.content);
204
175
  ```
205
176
 
206
- OpenCode uses an API key:
177
+ </details>
207
178
 
208
- ```js
209
- const login = await go.signIn({ apiKey: process.env.OPENCODE_API_KEY });
210
- await login.wait();
179
+ <details>
180
+ <summary><strong>Vercel AI SDK</strong></summary>
181
+
182
+ For a Responses model:
183
+
184
+ ```bash
185
+ nub install ai @ai-sdk/openai
186
+ npm install ai @ai-sdk/openai
187
+ pnpm add ai @ai-sdk/openai
188
+ bun add ai @ai-sdk/openai
211
189
  ```
212
190
 
213
- For ChatGPT on a headless machine, use `signIn({ mode: "device" })`. Copilot
214
- also accepts `enterpriseDomain` for a supported GitHub Enterprise Cloud domain.
191
+ ```js
192
+ import { createOpenAI } from "@ai-sdk/openai";
193
+ import { streamText } from "ai";
194
+
195
+ const aisubs = createOpenAI({
196
+ baseURL: "http://127.0.0.1:4319/aisubs/chatgpt/personal/v1",
197
+ apiKey: process.env.AISUBS_API_KEY,
198
+ });
215
199
 
216
- Provider request paths are:
200
+ const result = streamText({
201
+ model: aisubs.responses("MODEL_ID_FROM_DASHBOARD"),
202
+ prompt: "Hello from AISubs",
203
+ providerOptions: { openai: { store: false } },
204
+ });
217
205
 
218
- | Model catalog endpoint | `account.proxy()` path |
219
- | ---------------------- | --------------------------------- |
220
- | `responses` | `responses` |
221
- | `chat/completions` | `chat/completions` |
222
- | `messages` | `messages` |
223
- | `models/MODEL_ID` | `models/MODEL_ID:generateContent` |
206
+ for await (const text of result.textStream) process.stdout.write(text);
207
+ ```
224
208
 
225
- Use `account.getModels()` first for Copilot, Grok, and OpenCode because one
226
- provider can expose more than one request format.
209
+ For Chat Completions, install `@ai-sdk/openai-compatible`, create the provider
210
+ with the same account base URL, and select the model with `provider("MODEL_ID")`.
227
211
 
228
212
  </details>
229
213
 
230
214
  <details>
231
- <summary><strong>Read account, usage, and model information</strong></summary>
215
+ <summary><strong>TanStack AI Chat Completions</strong></summary>
232
216
 
233
- ```js
234
- const details = await account.details();
217
+ TanStack's generic compatibility adapter targets Chat Completions. Use it only
218
+ for a model that lists `chat/completions`.
235
219
 
236
- console.log(details.session); // connection state and safe account identity
237
- console.log(details.credential); // expiry and refresh state, never token values
238
- console.log(details.usage); // limits and reset information, or null
239
- console.log(details.models); // available models, or null
220
+ ```bash
221
+ nub install @tanstack/ai @tanstack/ai-openai
222
+ npm install @tanstack/ai @tanstack/ai-openai
223
+ pnpm add @tanstack/ai @tanstack/ai-openai
224
+ bun add @tanstack/ai @tanstack/ai-openai
240
225
  ```
241
226
 
242
- Useful methods:
227
+ ```ts
228
+ import { chat } from "@tanstack/ai";
229
+ import { openaiCompatible } from "@tanstack/ai-openai/compatible";
230
+
231
+ const aisubs = openaiCompatible({
232
+ name: "aisubs",
233
+ baseURL: "http://127.0.0.1:4319/aisubs/grok/personal/v1",
234
+ apiKey: process.env.AISUBS_API_KEY!,
235
+ models: ["MODEL_ID_FROM_DASHBOARD"],
236
+ });
243
237
 
244
- | Method | Purpose |
245
- | ---------------------------- | ------------------------------------------------------ |
246
- | `account.status()` | Check whether the account is connected |
247
- | `account.signIn(options?)` | Start a browser, device-code, or API-key login |
248
- | `account.signOut()` | Remove this account's locally stored credentials |
249
- | `account.getModels()` | Get the provider's current model catalog |
250
- | `account.getUsage()` | Get current plan usage, if supported |
251
- | `account.details()` | Get safe session, credential, usage, and model data |
252
- | `account.fetch(url, init?)` | Make an authorized request to an allowed provider URL |
253
- | `account.proxy(path, init?)` | Make a provider-native request without handling tokens |
238
+ const stream = chat({
239
+ adapter: aisubs("MODEL_ID_FROM_DASHBOARD"),
240
+ messages: [{ role: "user", content: "Hello from AISubs" }],
241
+ });
254
242
 
255
- `details()`, `getUsage()`, and `getModels()` never return access or refresh
256
- tokens. `getAccessToken()` exists for advanced backend integrations; keep its
257
- result secret and prefer `fetch()` or `proxy()` when possible.
243
+ for await (const event of stream) console.log(event);
244
+ ```
258
245
 
259
246
  </details>
260
247
 
261
248
  <details>
262
- <summary><strong>Use multiple accounts</strong></summary>
263
-
264
- ```js
265
- const personal = subscriptions.account("chatgpt", "personal");
266
- const work = subscriptions.account("chatgpt", "work");
249
+ <summary><strong>Python OpenAI SDK</strong></summary>
267
250
 
268
- const selected = user.isWorkAccount ? work : personal;
269
- const response = await selected.proxy("responses", requestOptions);
251
+ ```bash
252
+ python -m pip install openai
270
253
  ```
271
254
 
272
- Account names are 1–128 characters and cannot contain control characters.
273
- Each account has separate credentials, refresh state, usage, and model data.
274
-
275
- </details>
255
+ ```python
256
+ import os
257
+ from openai import OpenAI
276
258
 
277
- ## Dashboard
259
+ client = OpenAI(
260
+ base_url="http://127.0.0.1:4319/aisubs/grok/personal/v1",
261
+ api_key=os.environ["AISUBS_API_KEY"],
262
+ )
278
263
 
279
- ### Requirements and installation
264
+ response = client.chat.completions.create(
265
+ model="MODEL_ID_FROM_DASHBOARD",
266
+ messages=[{"role": "user", "content": "Hello from AISubs"}],
267
+ )
268
+ print(response.choices[0].message.content)
269
+ ```
280
270
 
281
- - [Node.js 24 or newer](https://nodejs.org/en/download/).
282
- - A terminal and a browser for provider sign-in.
283
- - [Nub 0.6 or newer](https://nubjs.com/docs/install) is recommended. It is not
284
- required; [pnpm](https://pnpm.io/installation),
285
- [npm](https://docs.npmjs.com/cli/install/), and
286
- [Bun](https://bun.sh/docs/installation) also work.
287
- - An API key for OpenCode Go or OpenCode Zen; the other providers use browser
288
- or device-code sign-in.
271
+ </details>
289
272
 
290
- Install AISubs in your project:
273
+ <details>
274
+ <summary><strong>cURL — Responses, Chat Completions, Anthropic, and Google</strong></summary>
291
275
 
292
276
  ```bash
293
- nub install aisubs # Nub (recommended)
294
- npm install aisubs # npm
295
- pnpm add aisubs # pnpm
296
- bun add aisubs # Bun
277
+ curl "http://127.0.0.1:4319/aisubs/chatgpt/personal/v1/responses" \
278
+ -H "Authorization: Bearer $AISUBS_API_KEY" \
279
+ -H "Content-Type: application/json" \
280
+ -d '{"model":"MODEL_ID_FROM_DASHBOARD","store":false,"input":"Hello"}'
297
281
  ```
298
282
 
299
- Use the dashboard when you want to connect accounts without writing login UI:
283
+ Chat Completions:
300
284
 
301
285
  ```bash
302
- nubx aisubs dashboard # Nub (recommended)
303
- npx aisubs dashboard # npm
304
- pnpm exec aisubs dashboard # pnpm
305
- bunx aisubs dashboard # Bun
286
+ curl "http://127.0.0.1:4319/aisubs/grok/personal/v1/chat/completions" \
287
+ -H "Authorization: Bearer $AISUBS_API_KEY" \
288
+ -H "Content-Type: application/json" \
289
+ -d '{"model":"MODEL_ID_FROM_DASHBOARD","messages":[{"role":"user","content":"Hello from AISubs"}]}'
306
290
  ```
307
291
 
308
- Then click **Add account**, choose a provider, finish sign-in, and choose a
309
- local account name. The dashboard shows safe account details, usage, models,
310
- and copy-ready integration examples.
311
-
312
- By default, AISubs uses Node.js 24 or newer, listens on
313
- `127.0.0.1:4319`, and stores credentials at `~/.aisubs/credentials.json`.
314
-
315
- To use another directory, choose an available port, or prevent the browser
316
- from opening:
292
+ Anthropic Messages:
317
293
 
318
294
  ```bash
319
- nubx aisubs dashboard \
320
- --data-dir ./data/aisubs \
321
- --port 0 \
322
- --no-open
295
+ curl "http://127.0.0.1:4319/aisubs/claude/team/v1/messages" \
296
+ -H "x-api-key: $AISUBS_API_KEY" \
297
+ -H "anthropic-version: 2023-06-01" \
298
+ -H "Content-Type: application/json" \
299
+ -d '{"model":"MODEL_ID_FROM_DASHBOARD","max_tokens":1024,"messages":[{"role":"user","content":"Hello from AISubs"}]}'
323
300
  ```
324
301
 
325
- Use the equivalent `npx`, `pnpm exec`, or `bunx` command if you use npm, pnpm,
326
- or Bun.
327
-
328
- The dashboard prints the secure link when `--no-open` is used. Credentials stay
329
- on your computer; do not commit the data directory or expose the dashboard to
330
- the network.
331
-
332
- Use `AISUBS_DATA_DIR` or `--data-dir` to choose another data directory. Use
333
- `--port 0` for an available port and `--no-open` to print the secure link
334
- without opening a browser.
335
-
336
- ## Local HTTP bridge
337
-
338
- Use the bridge for an existing SDK, Python, cURL, or another program that
339
- cannot import AISubs:
302
+ Google `generateContent`:
340
303
 
341
304
  ```bash
342
- nubx aisubs dashboard # Nub (recommended)
343
- npx aisubs dashboard # npm
344
- pnpm exec aisubs dashboard # pnpm
345
- bunx aisubs dashboard # Bun
346
- export AISUBS_API_KEY="the-control-key-printed-by-aisubs"
305
+ curl "http://127.0.0.1:4319/aisubs/opencode-zen/lab/v1/models/MODEL_ID_FROM_DASHBOARD:generateContent" \
306
+ -H "Authorization: Bearer $AISUBS_API_KEY" \
307
+ -H "Content-Type: application/json" \
308
+ -d '{"contents":[{"role":"user","parts":[{"text":"Hello from AISubs"}]}]}'
347
309
  ```
348
310
 
349
- The base URL chooses the provider and account. Append the provider's request
350
- path shown by `getModels()` or the dashboard:
351
-
352
- ```text
353
- http://127.0.0.1:4319/aisubs/chatgpt/personal/responses
354
- http://127.0.0.1:4319/aisubs/claude/team/messages
355
- http://127.0.0.1:4319/aisubs/grok/personal/chat/completions
356
- ```
311
+ </details>
357
312
 
358
- The account name is URL-decoded by AISubs, so URL-encode names containing
359
- spaces or other URL characters. AISubs removes the control key before sending
360
- the request to a provider.
313
+ ## Direct AISubs SDK
361
314
 
362
- <details>
363
- <summary><strong>Vercel AI SDK</strong></summary>
315
+ Use the in-process SDK when AISubs is part of your trusted Node.js backend. It
316
+ needs no local server or AISubs API key.
364
317
 
365
- For a `responses` model:
318
+ Install the package in a project:
366
319
 
367
320
  ```bash
368
- nub install ai @ai-sdk/openai
321
+ nub install aisubs
322
+ npm install aisubs
323
+ pnpm add aisubs
324
+ bun add aisubs
369
325
  ```
370
326
 
371
327
  ```js
372
- import { createOpenAI } from "@ai-sdk/openai";
373
- import { streamText } from "ai";
328
+ import { chatGptProvider, createSubscriptionAuth } from "aisubs";
374
329
 
375
- const provider = createOpenAI({
376
- baseURL: "http://127.0.0.1:4319/aisubs/chatgpt/personal",
377
- apiKey: process.env.AISUBS_API_KEY,
378
- });
330
+ const subscriptions = createSubscriptionAuth({ providers: [chatGptProvider()] });
331
+ const account = subscriptions.account("chatgpt", "personal");
379
332
 
380
- const result = streamText({
381
- model: provider.responses("MODEL_ID"),
382
- prompt: "Hello",
383
- providerOptions: { openai: { store: false } },
333
+ if (!(await account.status()).authenticated) {
334
+ const login = await account.signIn();
335
+ console.log(login.prompt);
336
+ await login.wait();
337
+ }
338
+
339
+ const catalog = await account.getModels();
340
+ const model = catalog?.models.find((item) => item.selectable !== false)?.id;
341
+ if (!model) throw new Error("No selectable model is available");
342
+
343
+ const response = await account.proxy("responses", {
344
+ method: "POST",
345
+ headers: { "content-type": "application/json" },
346
+ body: JSON.stringify({ model, input: "Hello", store: false, stream: true }),
384
347
  });
385
348
 
386
- for await (const text of result.textStream) process.stdout.write(text);
349
+ if (!response.ok) throw new Error(await response.text());
350
+ for await (const chunk of response.body ?? []) process.stdout.write(Buffer.from(chunk));
387
351
  ```
388
352
 
389
- For `chat/completions`, use `@ai-sdk/openai-compatible`. For `messages`, use
390
- `@ai-sdk/anthropic`. For OpenCode Zen Gemini, use the model-specific
391
- `models/MODEL_ID:generateContent` URL shown in the dashboard.
353
+ Available provider factories are `chatGptProvider()`, `claudeProvider()`,
354
+ `copilotProvider()`, `grokProvider()`, `openCodeGoProvider()`, and
355
+ `openCodeZenProvider()`.
392
356
 
393
- </details>
357
+ Useful account methods:
358
+
359
+ | Method | Purpose |
360
+ | -------------------- | ------------------------------------------------------ |
361
+ | `status()` | Check whether the account is connected |
362
+ | `signIn(options?)` | Start browser, device-code, or API-key sign-in |
363
+ | `signOut()` | Remove the account's locally stored credential |
364
+ | `getModels()` | Read the current provider model catalog |
365
+ | `getUsage()` | Read current provider usage when available |
366
+ | `details()` | Read safe identity, credential, usage, and model data |
367
+ | `fetch(url, init?)` | Send an authorized request to an allowed provider host |
368
+ | `proxy(path, init?)` | Send a provider-native request without handling tokens |
394
369
 
395
370
  <details>
396
- <summary><strong>TanStack AI</strong></summary>
371
+ <summary><strong>Configure every provider and custom credential storage</strong></summary>
397
372
 
398
- TanStack AI can use AISubs through its OpenAI-compatible adapter:
373
+ ```js
374
+ import {
375
+ FileCredentialStore,
376
+ chatGptProvider,
377
+ claudeProvider,
378
+ copilotProvider,
379
+ createSubscriptionAuth,
380
+ grokProvider,
381
+ openCodeGoProvider,
382
+ openCodeZenProvider,
383
+ } from "aisubs";
399
384
 
400
- ```bash
401
- nub install @tanstack/ai @tanstack/ai-openai # Nub (recommended)
402
- npm install @tanstack/ai @tanstack/ai-openai # npm
403
- pnpm add @tanstack/ai @tanstack/ai-openai # pnpm
404
- bun add @tanstack/ai @tanstack/ai-openai # Bun
385
+ const subscriptions = createSubscriptionAuth({
386
+ store: new FileCredentialStore("./data/aisubs-credentials.json"),
387
+ providers: [
388
+ chatGptProvider(),
389
+ claudeProvider(),
390
+ copilotProvider(),
391
+ grokProvider(),
392
+ openCodeGoProvider(),
393
+ openCodeZenProvider(),
394
+ ],
395
+ });
405
396
  ```
406
397
 
407
- ```ts
408
- import { chat } from "@tanstack/ai";
409
- import { openaiCompatibleText } from "@tanstack/ai-openai/compatible";
410
-
411
- const stream = chat({
412
- adapter: openaiCompatibleText("MODEL_ID", {
413
- baseURL: "http://127.0.0.1:4319/aisubs/grok/personal",
414
- apiKey: process.env.AISUBS_API_KEY!,
415
- }),
416
- messages: [{ role: "user", content: "Hello" }],
417
- });
398
+ Without a custom store, credentials are saved to
399
+ `~/.aisubs/credentials.json`. Select an account with its provider ID and a
400
+ local account name:
418
401
 
419
- for await (const chunk of stream) {
420
- if (chunk.type === "TEXT_MESSAGE_CONTENT") process.stdout.write(chunk.delta);
421
- }
402
+ ```js
403
+ const chatgpt = subscriptions.account("chatgpt", "personal");
404
+ const claude = subscriptions.account("claude", "team");
405
+ const copilot = subscriptions.account("copilot", "github");
406
+ const grok = subscriptions.account("grok", "personal");
407
+ const go = subscriptions.account("opencode-go", "team");
408
+ const zen = subscriptions.account("opencode-zen", "lab");
422
409
  ```
423
410
 
424
- Use the equivalent `npm install`, `pnpm add`, or `bun add` command if you use
425
- another package manager. Replace the provider, account, and model with values
426
- from your dashboard.
411
+ Account names are 1–128 characters and cannot contain control characters.
427
412
 
428
413
  </details>
429
414
 
430
415
  <details>
431
- <summary><strong>OpenAI, Anthropic, Python, or cURL</strong></summary>
416
+ <summary><strong>Browser, device-code, and API-key sign-in</strong></summary>
432
417
 
433
- OpenAI Responses:
418
+ The default ChatGPT flow opens a browser. On a headless machine, request its
419
+ device-code flow explicitly:
434
420
 
435
- ```bash
436
- nub install openai
421
+ ```js
422
+ const login = await chatgpt.signIn({ mode: "device" });
423
+ console.log(login.prompt);
424
+ await login.wait();
437
425
  ```
438
426
 
439
- ```js
440
- import OpenAI from "openai";
427
+ Copilot uses device-code sign-in and can target a supported GitHub Enterprise
428
+ Cloud domain:
441
429
 
442
- const client = new OpenAI({
443
- baseURL: "http://127.0.0.1:4319/aisubs/chatgpt/personal",
444
- apiKey: process.env.AISUBS_API_KEY,
445
- });
430
+ ```js
431
+ const login = await copilot.signIn({ enterpriseDomain: "company.ghe.com" });
432
+ console.log(login.prompt);
433
+ await login.wait();
434
+ ```
446
435
 
447
- const stream = await client.responses.create({
448
- model: "MODEL_ID",
449
- store: false,
450
- stream: true,
451
- input: "Hello",
452
- });
436
+ OpenCode Go and Zen use API keys:
453
437
 
454
- for await (const event of stream) console.log(event);
438
+ ```js
439
+ const login = await go.signIn({ apiKey: process.env.OPENCODE_API_KEY });
440
+ await login.wait();
455
441
  ```
456
442
 
457
- Anthropic Messages uses `@anthropic-ai/sdk` with this base URL:
443
+ </details>
458
444
 
459
- ```text
460
- http://127.0.0.1:4319/aisubs/claude/team
461
- ```
445
+ <details>
446
+ <summary><strong>Inspect account details and switch between accounts</strong></summary>
462
447
 
463
- Chat Completions with LiteLLM:
448
+ ```js
449
+ const details = await chatgpt.details();
464
450
 
465
- ```bash
466
- pip install litellm
451
+ console.log(details.session); // connection state and safe account identity
452
+ console.log(details.credential); // expiry and refresh state, never token values
453
+ console.log(details.usage); // limits and reset information, or null
454
+ console.log(details.models); // available models, or null
467
455
  ```
468
456
 
469
- ```python
470
- import os
471
- from litellm import completion
472
-
473
- response = completion(
474
- model="openai/MODEL_ID",
475
- api_base="http://127.0.0.1:4319/aisubs/grok/personal",
476
- api_key=os.environ["AISUBS_API_KEY"],
477
- messages=[{"role": "user", "content": "Hello"}],
478
- stream=True,
479
- )
457
+ `details()`, `getUsage()`, and `getModels()` never return access or refresh
458
+ tokens. Prefer `fetch()` or `proxy()` over handling a token directly.
480
459
 
481
- for event in response:
482
- print(event)
483
- ```
460
+ Each named account keeps separate credentials, refresh state, usage, and model
461
+ data, so selection can happen at request time:
484
462
 
485
- cURL:
463
+ ```js
464
+ const personal = subscriptions.account("chatgpt", "personal");
465
+ const work = subscriptions.account("chatgpt", "work");
486
466
 
487
- ```bash
488
- curl "http://127.0.0.1:4319/aisubs/chatgpt/personal/responses" \
489
- -H "Authorization: Bearer $AISUBS_API_KEY" \
490
- -H "Content-Type: application/json" \
491
- -d '{"model":"MODEL_ID","store":false,"stream":true,"input":"Hello"}'
467
+ const selected = user.isWorkAccount ? work : personal;
468
+ const response = await selected.proxy("responses", requestOptions);
492
469
  ```
493
470
 
494
- For Messages, use `x-api-key: $AISUBS_API_KEY` and
495
- `anthropic-version: 2023-06-01`.
496
-
497
471
  </details>
498
472
 
499
473
  <details>
500
- <summary><strong>Run a server from Node.js</strong></summary>
474
+ <summary><strong>Run the local HTTP server from Node.js</strong></summary>
501
475
 
502
- HTTP bridge without the dashboard:
476
+ This is the programmatic equivalent of `aisubs dashboard`. The API key is
477
+ created once and reused across restarts; delete or regenerate the key file only
478
+ when clients should receive a new key.
503
479
 
504
480
  ```js
505
- import { randomBytes } from "node:crypto";
506
- import { chatGptProvider, createSubscriptionAuth, FileCredentialStore } from "aisubs";
481
+ import { homedir } from "node:os";
482
+ import { join } from "node:path";
483
+ import {
484
+ FileApiKeyStore,
485
+ FileCredentialStore,
486
+ chatGptProvider,
487
+ claudeProvider,
488
+ createSubscriptionAuth,
489
+ } from "aisubs";
507
490
  import { createSubscriptionAuthServer } from "aisubs/http";
508
491
 
492
+ const directory = join(homedir(), ".aisubs");
493
+ const apiKey = await new FileApiKeyStore(join(directory, "api-key")).readOrCreate();
509
494
  const auth = createSubscriptionAuth({
510
- store: new FileCredentialStore("./data/aisubs-credentials.json"),
511
- providers: [chatGptProvider()],
495
+ store: new FileCredentialStore(join(directory, "credentials.json")),
496
+ providers: [chatGptProvider(), claudeProvider()],
512
497
  });
513
498
 
514
- const server = await createSubscriptionAuthServer({
515
- auth,
516
- apiKey: randomBytes(24).toString("hex"),
517
- port: 4319,
518
- });
519
-
520
- console.log(server.url);
521
- // await server.close();
499
+ const server = await createSubscriptionAuthServer({ auth, apiKey, port: 4319 });
500
+ console.log(`AISubs API: ${server.url}`);
522
501
  ```
523
502
 
524
- Dashboard inside a Node.js application:
503
+ A runnable version is available in [`examples/server.mjs`](./examples/server.mjs).
525
504
 
526
- ```js
527
- import { createSubscriptionAuthDashboardServer } from "aisubs/dashboard";
505
+ </details>
506
+
507
+ ## Dashboard options
528
508
 
529
- const dashboard = await createSubscriptionAuthDashboardServer({ auth });
530
- console.log(dashboard.bootstrapUrl);
531
- // await dashboard.close();
509
+ ```text
510
+ aisubs dashboard [options]
511
+
512
+ --data-dir <path> State directory (default: ~/.aisubs)
513
+ --port <number> Local port (default: 4319; 0 chooses an available port)
514
+ --no-open Do not open the browser
515
+ --help Show help
532
516
  ```
533
517
 
534
- Both servers bind only to localhost. The programmatic HTTP server requires its
535
- API key; the dashboard also provides a one-time browser link.
518
+ `AISUBS_DATA_DIR` also changes the state directory. The built-in server binds
519
+ only to localhost.
536
520
 
537
- </details>
521
+ ## Local management API
538
522
 
539
- <details>
540
- <summary><strong>Local HTTP API</strong></summary>
523
+ These routes are for the dashboard and advanced integrations:
541
524
 
542
525
  ```text
543
526
  GET /health
@@ -546,84 +529,57 @@ GET /v1/auth
546
529
  GET /v1/auth/:provider
547
530
  GET /v1/auth/:provider/accounts
548
531
  POST /v1/auth/:provider/login
532
+ DELETE /v1/auth/:provider?account=work
549
533
  GET /v1/logins/:loginId
550
534
  DELETE /v1/logins/:loginId
551
- GET /v1/auth/:provider/details?account=work
552
- DELETE /v1/auth/:provider?account=work
553
- POST /v1/fetch/:provider
554
535
  GET /v1/usage/:provider?account=work
555
536
  GET /v1/models/:provider?account=work
556
- * /aisubs/:provider/:account/*
537
+ GET /v1/api-key # dashboard session only
538
+ POST /v1/api-key/regenerate # dashboard session only
539
+ * /aisubs/:provider/:account/v1/*
557
540
  ```
558
541
 
559
- The dashboard's `/bootstrap` link is one-time. Other routes require the
560
- control API key or dashboard session cookie. Login responses return an attempt
561
- ID; poll `/v1/logins/:loginId` until it is complete, failed, or cancelled.
542
+ The provider and account routes require `Authorization: Bearer AISUBS_API_KEY`
543
+ or `x-api-key: AISUBS_API_KEY`. Regenerating the key immediately invalidates the
544
+ old key.
562
545
 
563
- </details>
546
+ ## Storage and security
564
547
 
565
- <details>
566
- <summary><strong>Storage and security</strong></summary>
567
-
568
- - Default credentials: `~/.aisubs/credentials.json`.
569
- - Override the directory with `AISUBS_DATA_DIR` or `--data-dir`.
570
- - `FileCredentialStore` creates private directories/files and uses file locks.
571
- - `MemoryCredentialStore` is available for tests and temporary processes.
572
- - Usage is cached for 15 seconds; model catalogs are cached for five minutes.
573
- - Sign-in, refresh, and sign-out clear the affected metadata cache.
574
- - Provider credentials are added only after host allowlist validation.
575
- - Local auth and control-key headers are removed before forwarding.
576
- - Account APIs return safe summaries, never token values.
577
- - Never expose provider credentials or `AISUBS_API_KEY` in browser code.
548
+ - Credentials: `~/.aisubs/credentials.json`.
549
+ - Persistent local API key: `~/.aisubs/api-key`.
550
+ - State directories and files use private permissions where the platform supports them.
551
+ - Provider credentials are attached only after provider-host allowlist validation.
552
+ - Local authorization, cookie, origin, and proxy headers are never forwarded.
553
+ - Account and model APIs never return provider access or refresh tokens.
554
+ - Keep AISubs on localhost and never put its API key in browser-delivered code.
578
555
 
579
- </details>
556
+ ## Development
580
557
 
581
- <details>
582
- <summary><strong>Local development</strong></summary>
583
-
584
- From the package directory, run:
558
+ From the package directory:
585
559
 
586
560
  ```bash
587
- nub run dev
561
+ nub install
562
+ nub run check
588
563
  ```
589
564
 
590
- Equivalent commands are `pnpm dev`, `npm run dev`, and `bun run dev`. Nub is
591
- recommended, but it is not required. The command builds the package once,
592
- watches backend and dashboard changes, and opens the local dashboard. Pass
593
- `-- --no-open` to keep the browser closed.
594
-
595
- </details>
596
-
597
- <details>
598
- <summary><strong>Maintainer pre-publish check</strong></summary>
565
+ Package-manager equivalents:
599
566
 
600
567
  ```bash
601
- nub run check
602
- nub pack --dry-run
568
+ npm install && npm run check
569
+ pnpm install && pnpm run check
570
+ bun install && bun run check
603
571
  ```
604
572
 
605
- With another package manager, use `pnpm check` / `pnpm pack`,
606
- `npm run check` / `npm pack --dry-run`, or `bun run check` / `bun pm pack`.
607
-
608
- Confirm that the package contains `dist`, `examples`, `public`, `README.md`,
609
- `LICENSE`, and the README logo asset. Test at least one real account for every
610
- provider your release claims to support.
611
-
612
- Runnable examples:
613
-
614
- - [`examples/direct.mjs`](./examples/direct.mjs)
615
- - [`examples/server.mjs`](./examples/server.mjs)
616
-
617
- </details>
573
+ For the watch dashboard use `nub run dev`, `npm run dev`, `pnpm run dev`, or
574
+ `bun run dev`. Before publishing, also inspect the package with `nub pack --dry-run`,
575
+ `npm pack --dry-run`, `pnpm pack --dry-run`, or `bun pm pack --dry-run`.
618
576
 
619
- ## Contributing and bug reports
577
+ ## Provider terms
620
578
 
621
- Please read [`CONTRIBUTING.md`](./CONTRIBUTING.md) before opening an issue or
622
- pull request. To report a reproducible bug, use the
623
- [Bug report form](./.github/ISSUE_TEMPLATE/bug_report.yml) and include the
624
- version, environment, steps to reproduce, expected and actual behavior, and
625
- sanitized error output where relevant.
579
+ Provider subscriptions, OAuth clients, and model access are governed by each
580
+ provider's terms and may change. Use accounts you are authorized to use, discover
581
+ models at runtime, and pin the AISubs version your integration has tested.
626
582
 
627
583
  ## License
628
584
 
629
- AISubs is licensed under the [MIT License](./LICENSE).
585
+ [MIT](./LICENSE)