@runappsai/sdk 0.3.1 → 0.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/README.md CHANGED
@@ -1,11 +1,17 @@
1
- # RunApps SDK for JavaScript / TypeScript
1
+ # RunApps — SDK and platform docs
2
2
 
3
- The client for building apps on [RunApps](https://www.runapps.ai): call any model (Claude, GPT, Gemini, DeepSeek, Qwen, MiniMax, GLM, Grok), generate images, audio and video, keep per-user files, and let the platform handle sign-in and billing. Zero runtime dependencies — native `fetch`, `FormData` and `ReadableStream` (browsers, Node 18+, Deno, Bun).
3
+ [RunApps](https://www.runapps.ai) is a platform for AI web apps. A creator publishes a static frontend; it runs on its own address, `<slug>.runapps.dev`; users sign in with one RunApps account and pay only for the AI they use. This package is the client for building on it: call any model (Claude, GPT, Gemini, DeepSeek, Qwen, MiniMax, GLM, Grok), generate images, audio and video, keep per-user files, and let the platform handle sign-in and billing.
4
4
 
5
- Two ways to use it:
5
+ There are two ways to use it:
6
6
 
7
- - **In an app you publish on RunApps** — a static bundle served at `<slug>.runapps.dev`. Users sign in with their RunApps account and pay for their own usage; you never hold a key. This is what `authProvider: "runapps"` is for.
8
- - **From your own code** — server, script or CLI — with a personal API key (`rk_…`).
7
+ - **In an app you publish on RunApps.** Users sign in with their RunApps account and pay for their own usage. You never hold a key: create the client without one, `new RunApps()`, and it signs the user in.
8
+ - **From your own code.** A server, script or CLI uses a personal API key (`rk_…`), billed to you.
9
+
10
+ Both use the base URL `https://www.runapps.ai` and the same OpenAI-compatible API under `/v1`.
11
+
12
+ **Contents:** [Install](#install) · [Build an app](#build-an-app) · [Publish it](#publish-it) · [From your own code](#from-your-own-code) · [Chat](#chat) · [Files](#files) · [Models](#models) · [Image, audio, video, embeddings](#image-audio-video-embeddings) · [HTTP API](#http-api) · [Errors](#errors) · [Reference](#reference)
13
+
14
+ ---
9
15
 
10
16
  ## Install
11
17
 
@@ -13,27 +19,28 @@ Two ways to use it:
13
19
  npm i @runappsai/sdk
14
20
  ```
15
21
 
16
- Or as a script tag:
22
+ Or load it with a script tag, which defines the global `RunApps`:
17
23
 
18
24
  ```html
19
25
  <script src="https://cdn.jsdelivr.net/npm/@runappsai/sdk/dist/sdk.umd.js"></script>
20
26
  <script>
21
- const client = new RunApps({ authProvider: "runapps" });
27
+ const client = new RunApps(); // no key: users sign in with RunApps
22
28
  </script>
23
29
  ```
24
30
 
25
- ## Building an app
31
+ No runtime dependencies. Runs in browsers, Node 18+, Deno and Bun.
32
+
33
+ ## Build an app
26
34
 
27
35
  ### 1. Create the app, then develop against it
28
36
 
29
- An app gets its slug the moment you create it on the dashboard (Creator → Publish) — no zip needed yet. Pass that slug as `project` and your dev server runs against the real platform:
37
+ Create the app on the dashboard, under **Creator → Publish**. It gets its slug at once, as a draft visible only to you, before you upload anything. Pass the slug as `project` and run your dev server as usual:
30
38
 
31
39
  ```ts
32
40
  import { RunApps } from "@runappsai/sdk";
33
41
 
34
42
  const client = new RunApps({
35
- authProvider: "runapps", // sign-in, token, refresh — all handled
36
- project: "my-tool", // the draft's slug; unchanged after you upload
43
+ project: "my-tool", // the draft's slug; it stays the same after you upload
37
44
  });
38
45
 
39
46
  const resp = await client.chat.create({
@@ -43,99 +50,91 @@ const resp = await client.chat.create({
43
50
  console.log(resp.choices[0].message.content);
44
51
  ```
45
52
 
46
- - Sign-in works from `http://localhost:<any port>`; the first call redirects to the RunApps grant page and back.
47
- - Calls are billed to the signed-in user — while developing, that is you.
48
- - Files and private prompts already use the app's real namespace, so nothing moves when you upload.
53
+ - Sign-in works from `http://localhost` on any port. The first call sends you to RunApps to sign in and back.
54
+ - Calls are charged to the signed-in user, which is you while developing.
55
+ - Files and private prompts already use the app's real space, so nothing moves when you upload.
49
56
 
50
- When the app is ready, build it, zip it, and upload it to the same app. Set it private (reachable by link) or submit it for review to be listed in the library.
57
+ When the app is ready, build it, zip the output and upload it to the same app. See [Publish it](#publish-it).
51
58
 
52
- ### 2. Sign-in details
59
+ ### 2. Sign-in
53
60
 
54
61
  ```ts
55
- await client.chat.create({ ... }); // redirects to the grant page on first use
62
+ await client.chat.create({ ... }); // sends the user to sign in on first use
56
63
 
57
- client.signIn(); // force the redirect
58
- client.signOut(); // clear the cached token (sticky across reloads)
59
- client.user; // { id, name } | null
60
- client.auth?.onTokenChange(t => {}); // subscribe to (re)acquisition
64
+ client.signIn(); // send them now
65
+ client.signOut(); // forget the token, also across reloads
66
+ client.user; // { id, name } or null
67
+ client.auth?.onTokenChange(t => {}); // react to sign-in
61
68
  client.auth?.hasFreshToken(); // avoid showing a "sign in" button needlessly
62
69
  ```
63
70
 
64
- `showIdentityBadge` (default `true`) draws a small activity badge; `badgePosition` places it.
71
+ The SDK holds one token per user per app. It draws a small activity badge while it works; turn it off with `showIdentityBadge: false`, or move it with `badgePosition`.
65
72
 
66
- `project` is optional on `*.runapps.dev` — the origin already says which app it is. It is required on localhost and when the client runs on a personal key (see below). Sign-in only works on the app's own `*.runapps.dev` address and on `http://localhost`. In Node, `authProvider: "runapps"` is a no-op; use `apiKey`.
73
+ `project` is optional on `*.runapps.dev`, where the address already says which app it is. It is required on localhost and when the client runs on a personal key. Sign-in only works on the app's own `*.runapps.dev` address and on `http://localhost`. With no key, a browser client signs the user in on its own; outside a browser there is no one to sign in, so pass `apiKey`.
67
74
 
68
75
  ### 3. Private prompts
69
76
 
70
- Anything in your frontend can be read in the browser. Name your prompt files in a `bundle.json` at the root of your zip — `{"prompts": {"outline": "prompts/outline.md"}}` — or put them under `.runapps/prompts/<id>.md`. The platform never serves `bundle.json`, the files it names, or any dot folder; call the prompts by id:
77
+ Everything in a frontend can be read in the browser. To keep a prompt private, ship it in the bundle where the platform reads it but never serves it. Put a `bundle.json` at the root of the zip that names your prompt files:
78
+
79
+ ```json
80
+ {
81
+ "prompts": {
82
+ "outline": "prompts/outline.md",
83
+ "critic": "prompts/critic.md"
84
+ }
85
+ }
86
+ ```
87
+
88
+ Neither `bundle.json` nor the files it names can be downloaded from your app's address. Files under `.runapps/prompts/<id>.md` also work without an entry, because dot folders are never served. Call a prompt by id and fill its `{{variables}}`:
71
89
 
72
90
  ```ts
73
91
  await client.chat.create({
74
92
  model: "Claude Sonnet 4.6",
75
93
  messages: [{ role: "user", content: "A detective in a lighthouse." }],
76
- prompt: { id: "outline", vars: { genre: "mystery" } }, // {{genre}} in the file
94
+ prompt: { id: "outline", vars: { genre: "mystery" } }, // {{genre}} in outline.md
77
95
  });
78
96
  ```
79
97
 
80
- The gateway prepends the prompt as the system message and asks the model to keep it confidential; a reply that repeats 30+ characters of it verbatim is withheld (`finish_reason: "content_filter"`). While developing locally, before a bundle exists, send the text inline as `prompt.template` — accepted only from the app's creator — and keep it out of production builds with `import.meta.env.DEV`.
98
+ The platform adds the prompt as the system message and tells the model to keep it confidential. A reply that repeats 30 or more consecutive characters of it is withheld, with `finish_reason: "content_filter"`.
81
99
 
82
- ### 4. Per-user files
100
+ While developing locally, before a bundle exists, send the text inline as `prompt: { template: "…", vars }`. Only the app's creator may do this. Keep it out of production builds, for example behind `import.meta.env.DEV`.
83
101
 
84
- Every app gets a file space per signed-in user, addressed by POSIX-style paths. `FileObject.url` is a stable public address you can drop into `<img src>`.
102
+ ## Publish it
85
103
 
86
- ```ts
87
- // Write. body: Blob | File | ArrayBuffer | Uint8Array | ReadableStream | string
88
- const obj = await client.files.put("assets/logo.png", pngBlob, { contentType: "image/png" });
89
- await client.files.putString("notes.md", "# hello");
90
- await client.files.putFromURL("cache/cat.jpg", "https://example.com/cat.jpg"); // gateway fetches it
104
+ Upload your build as a **zip** on the app's publish page:
91
105
 
92
- // Read.
93
- const blob = await client.files.get("assets/logo.png");
94
- const meta = await client.files.stat("assets/logo.png"); // size, etag, last_modified, url
95
- await client.files.exists("assets/logo.png");
106
+ - **Size.** Up to 50 MB.
107
+ - **Entry page.** The zip needs at least one HTML file. `index.html` is the default page, and you can pick another.
108
+ - **Asset paths.** Your app runs at the root of its own address, so absolute paths such as `/assets/app.js` work.
109
+ - **Client-side routing.** A request for a path without a file extension that doesn't exist, such as `/settings`, gets your entry page.
110
+ - **Hidden files.** Files and folders whose name starts with a dot, such as `.env` or `.git`, are never served.
96
111
 
97
- // List, with an optional shell glob (*, **, ?, [abc]) matched against the whole path.
98
- const page = await client.files.list({ prefix: "assets/", glob: "**/*.png", limit: 100 });
99
- // page.next_cursor → pass as cursor for the next page
112
+ You can upload a new zip at any time; it replaces the old one immediately.
100
113
 
101
- // Mutate.
102
- await client.files.move("draft.md", "published.md");
103
- await client.files.copy("template.md", "instances/today.md");
104
- await client.files.del("old.log");
105
- await client.files.deleteMany({ prefix: "tmp/" });
106
- await client.files.deleteMany({ glob: "**/*.tmp" });
114
+ | Visibility | Who can open it | Listed in the library |
115
+ |---|---|---|
116
+ | Draft | Only you | No |
117
+ | Private | Anyone with the link | No |
118
+ | Public | Everyone | Yes, after review |
107
119
 
108
- // Several ops in one round trip; each result has its own ok/error.
109
- await client.files.batch([
110
- { op: "put_url", path: "a.jpg", src_url: "https://example.com/a.jpg" },
111
- { op: "copy", from: "a.jpg", to: "b.jpg" },
112
- { op: "del", path: "a.jpg" },
113
- ]);
114
- ```
120
+ Submitting an app as public sends it to review. If it is rejected, fix it and submit again.
115
121
 
116
- **Keeping app state in a file** — read, modify, write, without overwriting another tab's save. Every read carries the file's ETag; `ifMatch` makes the write fail (412) if the file changed since:
122
+ ### Earnings
117
123
 
118
- ```ts
119
- const { value, etag } = await client.files.readJSON<{ score: number }>("state.json");
120
- const next = { ...(value ?? { score: 0 }), score: (value?.score ?? 0) + 1 };
121
- try {
122
- await client.files.put("state.json", JSON.stringify(next), {
123
- contentType: "application/json",
124
- ...(etag ? { ifMatch: etag } : { ifNoneMatch: true }), // first save: must not exist
125
- });
126
- } catch (e) {
127
- if (e instanceof APIError && e.statusCode === 412) { /* re-read, merge, retry */ }
128
- }
129
- ```
124
+ Set a **creator fee** of 0% to 50% on the publish page. On every AI call a user makes in your app, they pay the fee on top of the model charge. The whole fee goes to your **earnings balance**, and your own use of your app carries no fee. You can transfer earnings to your main balance or withdraw them to PayPal, with a $10 minimum and 3 to 5 business days for payouts.
130
125
 
131
- `files.read(path)` is `get` plus the ETag for non-JSON files. `ifNoneMatch: true` refuses to overwrite (409).
126
+ ### What your users see
127
+
128
+ Users find public apps in the library and open them at `<slug>.runapps.dev`. They top up a US-dollar balance by card or PayPal, or Alipay and WeChat Pay in the Chinese interface, and each call is charged at the model's price plus your fee. Your app's page shows the fee before they open it. Users can set a daily budget, and the files your app saves for them appear on their Storage page. Removing your app from their account deletes those files.
132
129
 
133
130
  ## From your own code
134
131
 
132
+ Create a key on the dashboard's **API Keys** page.
133
+
135
134
  ```ts
136
- import { RunApps, APIError } from "@runappsai/sdk";
135
+ import { RunApps } from "@runappsai/sdk";
137
136
 
138
- const client = new RunApps({ apiKey: "rk_…" }); // Dashboard → API Keys
137
+ const client = new RunApps({ apiKey: "rk_…" });
139
138
 
140
139
  const resp = await client.chat.create({
141
140
  model: "Gemini 3 Flash",
@@ -144,23 +143,27 @@ const resp = await client.chat.create({
144
143
  console.log(resp.usage.total_cost); // USD
145
144
  ```
146
145
 
147
- The base URL is `https://www.runapps.ai` in both modes; `baseURL` overrides it.
148
-
149
- **Files with a personal key** — a personal key has no app of its own, so name the app: `new RunApps({ apiKey, project: "my-tool" })`. You get your own files in that app, the same ones the app sees for you, provided you have added the app or created it.
150
-
151
- **Any OpenAI-compatible SDK works too.** Point it at the platform with your `rk_…` key:
146
+ Any OpenAI-compatible client works too:
152
147
 
153
148
  ```python
154
149
  from openai import OpenAI
150
+
155
151
  client = OpenAI(api_key="rk_...", base_url="https://www.runapps.ai/v1")
156
152
  client.chat.completions.create(model="Claude Sonnet 4.6", messages=[{"role": "user", "content": "Hello!"}])
157
153
  ```
158
154
 
159
- Browse `GET /v1/models` (no auth) for the live catalogue and per-model prices.
155
+ ```bash
156
+ curl https://www.runapps.ai/v1/chat/completions \
157
+ -H "Authorization: Bearer $RUNAPPS_API_KEY" \
158
+ -H "Content-Type: application/json" \
159
+ -d '{"model": "Claude Sonnet 4.6", "messages": [{"role": "user", "content": "Hello!"}]}'
160
+ ```
161
+
162
+ Never put an `rk_…` key in a frontend; an app on RunApps creates its client without a key. `baseURL` overrides the base URL in either mode.
160
163
 
161
164
  ## Chat
162
165
 
163
- OpenAI-compatible, streaming or not. The final streamed chunk carries `usage`.
166
+ `POST /v1/chat/completions` follows the OpenAI format, streaming or not. If you leave out `model`, the user's default model is used, then the platform's. `usage.total_cost` is the charge in USD; when streaming, the last chunk carries `usage`.
164
167
 
165
168
  ```ts
166
169
  for await (const chunk of client.chat.stream({ model: "Gemini 3 Flash", messages })) {
@@ -169,10 +172,10 @@ for await (const chunk of client.chat.stream({ model: "Gemini 3 Flash", messages
169
172
  }
170
173
  ```
171
174
 
172
- **Images, video and audio in messages** — check the model first with `acceptsModality(model, "video")`; the gateway rejects unsupported parts with a 400 before billing:
175
+ **Images, video and audio in messages.** Check that the model accepts them with `acceptsModality(model, "video")`; unsupported parts are rejected with a 400 before anything is charged.
173
176
 
174
177
  ```ts
175
- import { userMessageParts, textPart, imagePart, videoPart, audioPart } from "@runappsai/sdk";
178
+ import { userMessageParts, textPart, videoPart } from "@runappsai/sdk";
176
179
 
177
180
  await client.chat.create({
178
181
  model: "Gemini 3 Vision",
@@ -180,9 +183,11 @@ await client.chat.create({
180
183
  });
181
184
  ```
182
185
 
183
- **Your own tools** — pass `tools: [{ type: "function", function: { name, description, parameters } }]`, read `choices[0].message.tool_calls`, answer with `toolResultMessage(toolCallId, output)` on the next turn.
186
+ **Your own tools.** Pass `tools: [{ type: "function", function: { name, description, parameters } }]`, read `choices[0].message.tool_calls`, and answer with `toolResultMessage(toolCallId, output)` on the next turn.
187
+
188
+ ### Server tools
184
189
 
185
- **Server tools** — the platform searches and reads the web for the model, looping until it answers; your code only sees the result. Combine freely with your own `tools`.
190
+ The platform can search and read the web for the model, looping until it answers. Your code only sees the final reply.
186
191
 
187
192
  ```ts
188
193
  import { ServerTools } from "@runappsai/sdk";
@@ -191,14 +196,74 @@ const resp = await client.chat.create({
191
196
  model: "Claude Sonnet 4.6",
192
197
  messages: [{ role: "user", content: "What is the latest stable Go version?" }],
193
198
  server_tools: [ServerTools.WebSearch, ServerTools.WebFetch],
194
- max_server_iterations: 5, // default 5, max 10
199
+ max_server_iterations: 5, // default 5, at most 10
195
200
  });
196
201
  for (const t of resp.usage.tool_costs ?? []) console.log(t.name, t.count, t.cost);
197
202
  ```
198
203
 
199
- `WebSearch` and `TwitterSearch` are priced per call, `WebFetch` is free; `usage.total_cost` includes tools and tokens. With `stream: true` the tool rounds run silently and the final answer streams.
204
+ | Tool | What it does | Price |
205
+ |---|---|---|
206
+ | `web_search` | Searches the web | Per call |
207
+ | `web_fetch` | Reads a page as text | Free |
208
+ | `twitter_search` | Searches posts on X | Per call |
209
+
210
+ The tokens of every round and each tool call are billed together, and `usage.total_cost` includes both. Server tools combine with your own `tools`; if the model calls one of yours, the loop stops and the call is returned to you. With `stream: true` the tool rounds run silently and the final answer streams.
211
+
212
+ ## Files
213
+
214
+ Each app has a file space for each signed-in user. Paths look like POSIX paths, and every file has a public `url` you can use in `<img src>`.
215
+
216
+ ```ts
217
+ // Write. body: Blob | File | ArrayBuffer | Uint8Array | ReadableStream | string
218
+ const obj = await client.files.put("assets/logo.png", pngBlob, { contentType: "image/png" });
219
+ await client.files.putString("notes.md", "# hello");
220
+ await client.files.putFromURL("cache/cat.jpg", "https://example.com/cat.jpg"); // the platform fetches it
221
+
222
+ // Read.
223
+ const blob = await client.files.get("assets/logo.png");
224
+ const meta = await client.files.stat("assets/logo.png"); // size, etag, last_modified, url
225
+ await client.files.exists("assets/logo.png");
226
+
227
+ // List, with an optional glob (*, **, ?, [abc]) matched against the whole path.
228
+ const page = await client.files.list({ prefix: "assets/", glob: "**/*.png", limit: 100 });
229
+ // page.next_cursor → pass as cursor for the next page
230
+
231
+ // Change.
232
+ await client.files.move("draft.md", "published.md");
233
+ await client.files.copy("template.md", "instances/today.md");
234
+ await client.files.del("old.log");
235
+ await client.files.deleteMany({ prefix: "tmp/" });
236
+
237
+ // Several operations in one round trip; each result has its own ok and error.
238
+ await client.files.batch([
239
+ { op: "put_url", path: "a.jpg", src_url: "https://example.com/a.jpg" },
240
+ { op: "copy", from: "a.jpg", to: "b.jpg" },
241
+ { op: "del", path: "a.jpg" },
242
+ ]);
243
+ ```
244
+
245
+ **Saving app state safely.** Every read returns the file's ETag. Writing with `ifMatch` fails with 412 if another tab or device changed the file since you read it, so a save never silently overwrites someone else's:
200
246
 
201
- Message builders: `userMessage`, `systemMessage`, `assistantMessage`, `toolResultMessage`, `userMessageParts`, `textPart`, `imagePart`, `videoPart`, `audioPart`.
247
+ ```ts
248
+ import { APIError } from "@runappsai/sdk";
249
+
250
+ const { value, etag } = await client.files.readJSON<{ score: number }>("state.json");
251
+ const next = { score: (value?.score ?? 0) + 1 };
252
+ try {
253
+ await client.files.put("state.json", JSON.stringify(next), {
254
+ contentType: "application/json",
255
+ ...(etag ? { ifMatch: etag } : { ifNoneMatch: true }), // first save: must not exist yet
256
+ });
257
+ } catch (e) {
258
+ if (e instanceof APIError && e.statusCode === 412) { /* read again, merge, retry */ }
259
+ }
260
+ ```
261
+
262
+ `files.read(path)` is `get` plus the ETag, for files that aren't JSON. `ifNoneMatch: true` refuses to overwrite an existing file (409).
263
+
264
+ **Files from your own code.** A personal key has no app of its own, so name the app: `new RunApps({ apiKey, project: "my-tool" })`. You get your own files in that app, the same ones the app sees for you. It must be an app you have added or created.
265
+
266
+ Paths are at most 900 bytes, and a single file is at most 200 MB.
202
267
 
203
268
  ## Models
204
269
 
@@ -207,26 +272,26 @@ const all = await client.models.list();
207
272
  const video = await client.models.list({ capability: "video_generation" });
208
273
  ```
209
274
 
210
- Prices are in pips per million tokens (1 USD = 1,000,000 pips). Each model carries `capability_tags` (stable ids such as `t2v`, `i2v`, `voice_clone`, `timestamps` — filter with `hasCapabilityTag`), chat models carry `input_modalities` (`acceptsModality`), and every model exposes its input contract through `getOptionsSchema(model)`: accepted fields, bounds, enums, cross-field constraints, and a catalogue of voices or emotions. `validateRequest(schema, body)` returns every problem at once for a form to show.
275
+ `GET /v1/models` needs no key. Token prices are in pips per million tokens, where 1 USD is 1,000,000 pips. Each model carries `capability_tags`, such as `t2v`, `i2v`, `voice_clone` or `timestamps`; filter with `hasCapabilityTag`. Chat models list `input_modalities`; check with `acceptsModality`. Every model describes its accepted options through `getOptionsSchema(model)`: fields, bounds, enums, cross-field rules, and a catalogue of voices or emotions. `validateRequest(schema, body)` returns every problem at once for a form to show. Live prices are also on the [pricing page](https://www.runapps.ai/pricing).
211
276
 
212
277
  ## Image, audio, video, embeddings
213
278
 
214
279
  ```ts
215
- // Image — data: URI (sync) or hosted blob URL (async); decodeMediaUrl() gives bytes.
280
+ // Image: a data: URI (sync) or a hosted URL (async); decodeMediaUrl() gives the bytes.
216
281
  const img = await client.image.generate("MiniMax Image-01", {
217
282
  prompt: "a developer at a laptop, anime style",
218
283
  resolution: "2K", aspect_ratio: "16:9", // or size: "WxH"
219
284
  reference_image_urls: ["https://…"],
220
285
  });
221
286
  await client.image.edit("GPT Image", { image: { data: bytes, filename: "photo.png" }, prompt: "add a party hat" });
222
- // generateAsync / submitGenerate + getAsyncStatus for jobs longer than ~100 s
287
+ // generateAsync, or submitGenerate + getAsyncStatus, for jobs longer than about 100 s
223
288
 
224
289
  // Speech.
225
290
  const speech = await client.audio.speech("MiniMax Speech 2.6 HD", { input: "Hello", voice: "English_radiant_girl", emotion: "happy" });
226
291
  const text = await client.audio.transcribe("OpenAI/Whisper", { file: { data: audioBytes, filename: "a.mp3" } });
227
292
  // speechAsync / transcribeAsync for long jobs; voices and emotions are in getOptionsSchema(model).catalog
228
293
 
229
- // Video — always async.
294
+ // Video: always async.
230
295
  const task = await client.video.generate("MiniMax Hailuo 2.3", { prompt: "a gentle ocean wave", duration: 5 });
231
296
  const status = await client.video.wait(task.id, { pollIntervalMs: 5000 });
232
297
  if (status.status === "succeeded") await client.video.getContent(task.id);
@@ -235,9 +300,66 @@ if (status.status === "succeeded") await client.video.getContent(task.id);
235
300
  const e = await client.embeddings.create("text-embedding-3-small", { input: ["alpha", "beta"] });
236
301
  ```
237
302
 
238
- `VideoGenerateParams` covers keyframes (`first_frame_url` / `last_frame_url`), reference inputs, source video/image/audio drivers, `service_tier: "flex"`, and more; every `*_url` accepts `https://` or a `data:` URI (`encodeImageUrl(bytes)`).
303
+ `VideoGenerateParams` covers keyframes (`first_frame_url`, `last_frame_url`), reference inputs, source video, image and audio drivers, `service_tier: "flex"`, and more. Every `*_url` accepts `https://` or a `data:` URI (`encodeImageUrl(bytes)`).
304
+
305
+ ## HTTP API
306
+
307
+ Base URL `https://www.runapps.ai`. Send `Authorization: Bearer <key or app token>`.
308
+
309
+ | Request | Does |
310
+ |---|---|
311
+ | `POST /v1/chat/completions` | Chat, OpenAI format |
312
+ | `GET /v1/models` | Model catalogue and prices (no key needed) |
313
+ | `POST /v1/images/generations` | Generate images |
314
+ | `POST /v1/images/edits` | Edit an image |
315
+ | `POST /v1/audio/speech` | Text to speech |
316
+ | `POST /v1/audio/transcriptions` | Speech to text |
317
+ | `GET /v1/audio/voices` | Voices for a speech model |
318
+ | `POST /v1/videos/generations` | Start a video job |
319
+ | `GET /v1/videos/generations/<id>` | Job status |
320
+ | `GET /v1/videos/<id>/content` | The finished video |
321
+ | `POST /v1/embeddings` | Embeddings |
322
+ | `/v1/async/…` | Async versions of the image and audio calls: submit, then poll |
323
+
324
+ ### Files over HTTP
325
+
326
+ With an app token the routes are under `/v1/files`. With a personal key, put the app in the path: `/v1/p/<slug>/files`.
327
+
328
+ | Request | Does |
329
+ |---|---|
330
+ | `GET /v1/files?prefix=&glob=&limit=&cursor=` | List files. `limit` defaults to 100, at most 1000. |
331
+ | `GET /v1/files/<path>` | Read a file. Returns `ETag`; sends 304 for a matching `If-None-Match`. |
332
+ | `HEAD /v1/files/<path>` | Metadata only. |
333
+ | `PUT /v1/files/<path>` | Write the request body. Honours `Content-Type`, `If-Match: <etag>` and `If-None-Match: *`. |
334
+ | `DELETE /v1/files/<path>` | Delete a file. |
335
+ | `POST /v1/files/move` | `{"from", "to"}` |
336
+ | `POST /v1/files/copy` | `{"from", "to"}` |
337
+ | `POST /v1/files/delete` | `{"prefix"}` or `{"glob"}` deletes many. |
338
+ | `POST /v1/files/put-url` | `{"path", "src_url"}`: the platform downloads the URL into the file. |
339
+ | `POST /v1/files/batch` | `{"ops": [...]}`: up to 64 operations, `put_url`, `del`, `move`, `copy`, `exists`, `stat`. |
340
+
341
+ A file object has `path`, `size`, `content_type`, `etag`, `last_modified` and `url`.
342
+
343
+ ### Sign-in without the SDK
344
+
345
+ Send the user to:
346
+
347
+ ```
348
+ https://www.runapps.ai/api/sdk/grant?origin=<your origin>&redirect_to=<url to return to>&token_name=runapps_token[&project_id=<slug>]
349
+ ```
350
+
351
+ The origin must be your app's own `https://<slug>.runapps.dev` or `http://localhost` on any port, and `redirect_to` must be on that origin. After signing in, the user comes back to `redirect_to` with `#runapps_token=<token>` in the URL fragment. Send it as `Authorization: Bearer <token>` on `/v1` calls. The token belongs to that user in that app. On localhost, pass your app's slug as `project_id`.
352
+
353
+ ## Errors
239
354
 
240
- ## Errors and cancellation
355
+ | Status | Meaning |
356
+ |---|---|
357
+ | 400 | Bad request, such as an input the model doesn't accept. Nothing is charged. |
358
+ | 401 | Missing or invalid key or token. In browser sign-in mode the SDK refreshes the token and retries once. |
359
+ | 402 | The balance is too low, or today's budget is reached. |
360
+ | 409 | `If-None-Match: *` and the file already exists. |
361
+ | 412 | `If-Match` and the file changed since you read it. |
362
+ | 429 | Too many requests at once. Retry shortly. |
241
363
 
242
364
  ```ts
243
365
  try {
@@ -248,9 +370,9 @@ try {
248
370
  }
249
371
  ```
250
372
 
251
- Every call takes `{ signal }`. In browser-auth mode a 401 is retried once after refreshing the token.
373
+ Every call takes `{ signal }` for cancellation.
252
374
 
253
- ## API reference
375
+ ## Reference
254
376
 
255
377
  | Service | Methods |
256
378
  |---|---|
@@ -262,13 +384,21 @@ Every call takes `{ signal }`. In browser-auth mode a 401 is retried once after
262
384
  | `client.video` | `generate`, `getStatus`, `wait`, `getContent` |
263
385
  | `client.embeddings` | `create` |
264
386
 
265
- Browser auth: `client.signIn()`, `client.signOut()`, `client.user`, `client.auth`.
387
+ Browser sign-in: `client.signIn()`, `client.signOut()`, `client.user`, `client.auth`.
266
388
 
267
- Helpers: `hasCapabilityTag`, `acceptsModality`, `getOptionsSchema`, `acceptsField`, `requiresField`, `allowedValuesFor`, `validateRequest`, `supportsVoiceClone`, `supportsInstructText`, `defaultVoice`, `encodeImageUrl`, `decodeMediaUrl`, the message builders, `ServerTools`.
389
+ Helpers: `hasCapabilityTag`, `acceptsModality`, `getOptionsSchema`, `acceptsField`, `requiresField`, `allowedValuesFor`, `validateRequest`, `supportsVoiceClone`, `supportsInstructText`, `defaultVoice`, `encodeImageUrl`, `decodeMediaUrl`, `ServerTools`, and the message builders `userMessage`, `systemMessage`, `assistantMessage`, `toolResultMessage`, `userMessageParts`, `textPart`, `imagePart`, `videoPart`, `audioPart`.
390
+
391
+ ### Addresses
392
+
393
+ | Address | What it serves |
394
+ |---|---|
395
+ | `www.runapps.ai` | The website, dashboard, sign-in and the API (`/v1`) |
396
+ | `<slug>.runapps.dev` | Each app |
397
+ | `files.runapps.ai` | Public file links |
268
398
 
269
- ## Compatibility
399
+ ### Upgrading from `@runjobsai/sdk`
270
400
 
271
- Browsers (any modern), Node 18+, Deno, Bun. Never ship an `rk_…` key to a browser — an app published on RunApps uses `authProvider: "runapps"` instead, and the user pays for their own usage.
401
+ The package was renamed. Install `@runappsai/sdk` and change your imports; the API is the same. Users signed in with 0.2.1 or later stay signed in after the upgrade. `authProvider` no longer needs to be set: a browser client without a key signs the user in. `RunJobs` still works as a deprecated alias of `RunApps`, and so does `authProvider: "runjobs"`.
272
402
 
273
403
  ## License
274
404
 
@@ -82,7 +82,7 @@ export class ActivityTracker {
82
82
  }
83
83
  catch (err) {
84
84
  // eslint-disable-next-line no-console
85
- console.error("[runjobs sdk] ActivityTracker onChange listener threw:", err);
85
+ console.error("[runapps sdk] ActivityTracker onChange listener threw:", err);
86
86
  }
87
87
  }
88
88
  }
@@ -165,7 +165,8 @@ export declare class BrowserAuth {
165
165
  private markSignedOut;
166
166
  private clearSignedOut;
167
167
  private removeBadge;
168
- /** Parse the #runjobs_token=… fragment, install, then strip it. */
168
+ /** Parse the #runapps_token=… fragment (or the older #runjobs_token=…),
169
+ * install, then strip it. */
169
170
  private consumeFragment;
170
171
  /** postMessage handshake with the dashboard parent (iframe path). */
171
172
  private requestTokenFromParent;
@@ -1 +1 @@
1
- {"version":3,"file":"browser-auth.d.ts","sourceRoot":"","sources":["../src/browser-auth.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,0DAA0D;AAC1D,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAEtF,MAAM,WAAW,WAAW;IAC1B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA8CD;;;;;;;;;;;GAWG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C;2EACuE;IACvE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,GAAE,kBAAuB;IA+CzC,iEAAiE;IACjE,QAAQ,QAAa,OAAO,CAAC,MAAM,CAAC,CA0DlC;IAEF,sEAAsE;IACtE,aAAa,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAQ3D,6CAA6C;IAC7C,IAAI,IAAI,IAAI,WAAW,GAAG,IAAI,CAE7B;IAED;;;;;;;;;;;OAWG;IACH,aAAa,IAAI,OAAO;IAIxB;;;;;;;;;OASG;IACH,OAAO,IAAI,IAAI;IAWf;;;;;;OAMG;IACH,UAAU,IAAI,IAAI;IASlB,0CAA0C;IAC1C,MAAM,IAAI,IAAI;IAmBd;;;;;;;OAOG;IACH,qBAAqB,CAAC,IAAI,EAAE;QAC1B,UAAU,EAAE,MAAM,CAAC;QACnB,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;KAC1B,GAAG,MAAM;IAIV;;;;;;OAMG;IACH,mBAAmB,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAO1D,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,QAAQ;IAiBhB,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,WAAW;IAKnB,mEAAmE;IACnE,OAAO,CAAC,eAAe;IAkCvB,qEAAqE;IACrE,OAAO,CAAC,sBAAsB;IAoD9B;;;;uEAImE;IACnE,OAAO,CAAC,WAAW;CAqCpB"}
1
+ {"version":3,"file":"browser-auth.d.ts","sourceRoot":"","sources":["../src/browser-auth.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,0DAA0D;AAC1D,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAEtF,MAAM,WAAW,WAAW;IAC1B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA0DD;;;;;;;;;;;GAWG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C;2EACuE;IACvE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,GAAE,kBAAuB;IA+CzC,iEAAiE;IACjE,QAAQ,QAAa,OAAO,CAAC,MAAM,CAAC,CA0DlC;IAEF,sEAAsE;IACtE,aAAa,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAQ3D,6CAA6C;IAC7C,IAAI,IAAI,IAAI,WAAW,GAAG,IAAI,CAE7B;IAED;;;;;;;;;;;OAWG;IACH,aAAa,IAAI,OAAO;IAIxB;;;;;;;;;OASG;IACH,OAAO,IAAI,IAAI;IAWf;;;;;;OAMG;IACH,UAAU,IAAI,IAAI;IASlB,0CAA0C;IAC1C,MAAM,IAAI,IAAI;IAmBd;;;;;;;OAOG;IACH,qBAAqB,CAAC,IAAI,EAAE;QAC1B,UAAU,EAAE,MAAM,CAAC;QACnB,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;KAC1B,GAAG,MAAM;IAIV;;;;;;OAMG;IACH,mBAAmB,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAO1D,OAAO,CAAC,aAAa;IAoBrB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,QAAQ;IAiBhB,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,aAAa;IA+BrB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,WAAW;IAKnB;kCAC8B;IAC9B,OAAO,CAAC,eAAe;IAmCvB,qEAAqE;IACrE,OAAO,CAAC,sBAAsB;IAqD9B;;;;uEAImE;IACnE,OAAO,CAAC,WAAW;CAqCpB"}