@runappsai/sdk 0.3.0 → 0.4.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,19 +1,25 @@
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. This is `authProvider: "runapps"`.
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
 
12
18
  ```bash
13
- npm install @runappsai/sdk
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>
@@ -22,18 +28,20 @@ Or as a script tag:
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
+ authProvider: "runapps", // sign-in and tokens are handled for you
44
+ project: "my-tool", // the draft's slug; it stays the same after you upload
37
45
  });
38
46
 
39
47
  const resp = await client.chat.create({
@@ -43,99 +51,91 @@ const resp = await client.chat.create({
43
51
  console.log(resp.choices[0].message.content);
44
52
  ```
45
53
 
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.
54
+ - Sign-in works from `http://localhost` on any port. The first call sends you to RunApps to sign in and back.
55
+ - Calls are charged to the signed-in user, which is you while developing.
56
+ - Files and private prompts already use the app's real space, so nothing moves when you upload.
49
57
 
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.
58
+ When the app is ready, build it, zip the output and upload it to the same app. See [Publish it](#publish-it).
51
59
 
52
- ### 2. Sign-in details
60
+ ### 2. Sign-in
53
61
 
54
62
  ```ts
55
- await client.chat.create({ ... }); // redirects to the grant page on first use
63
+ await client.chat.create({ ... }); // sends the user to sign in on first use
56
64
 
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
65
+ client.signIn(); // send them now
66
+ client.signOut(); // forget the token, also across reloads
67
+ client.user; // { id, name } or null
68
+ client.auth?.onTokenChange(t => {}); // react to sign-in
61
69
  client.auth?.hasFreshToken(); // avoid showing a "sign in" button needlessly
62
70
  ```
63
71
 
64
- `showIdentityBadge` (default `true`) draws a small activity badge; `badgePosition` places it.
72
+ 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
73
 
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`.
74
+ `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`. In Node, `authProvider: "runapps"` does nothing; use `apiKey`.
67
75
 
68
76
  ### 3. Private prompts
69
77
 
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:
78
+ 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:
79
+
80
+ ```json
81
+ {
82
+ "prompts": {
83
+ "outline": "prompts/outline.md",
84
+ "critic": "prompts/critic.md"
85
+ }
86
+ }
87
+ ```
88
+
89
+ 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
90
 
72
91
  ```ts
73
92
  await client.chat.create({
74
93
  model: "Claude Sonnet 4.6",
75
94
  messages: [{ role: "user", content: "A detective in a lighthouse." }],
76
- prompt: { id: "outline", vars: { genre: "mystery" } }, // {{genre}} in the file
95
+ prompt: { id: "outline", vars: { genre: "mystery" } }, // {{genre}} in outline.md
77
96
  });
78
97
  ```
79
98
 
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`.
99
+ 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
100
 
82
- ### 4. Per-user files
101
+ 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
102
 
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>`.
103
+ ## Publish it
85
104
 
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
105
+ Upload your build as a **zip** on the app's publish page:
91
106
 
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");
107
+ - **Size.** Up to 50 MB.
108
+ - **Entry page.** The zip needs at least one HTML file. `index.html` is the default page, and you can pick another.
109
+ - **Asset paths.** Your app runs at the root of its own address, so absolute paths such as `/assets/app.js` work.
110
+ - **Client-side routing.** A request for a path without a file extension that doesn't exist, such as `/settings`, gets your entry page.
111
+ - **Hidden files.** Files and folders whose name starts with a dot, such as `.env` or `.git`, are never served.
96
112
 
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
113
+ You can upload a new zip at any time; it replaces the old one immediately.
100
114
 
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" });
115
+ | Visibility | Who can open it | Listed in the library |
116
+ |---|---|---|
117
+ | Draft | Only you | No |
118
+ | Private | Anyone with the link | No |
119
+ | Public | Everyone | Yes, after review |
107
120
 
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
- ```
121
+ Submitting an app as public sends it to review. If it is rejected, fix it and submit again.
115
122
 
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:
123
+ ### Earnings
117
124
 
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
- ```
125
+ 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
126
 
131
- `files.read(path)` is `get` plus the ETag for non-JSON files. `ifNoneMatch: true` refuses to overwrite (409).
127
+ ### What your users see
128
+
129
+ 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
130
 
133
131
  ## From your own code
134
132
 
133
+ Create a key on the dashboard's **API Keys** page.
134
+
135
135
  ```ts
136
- import { RunApps, APIError } from "@runappsai/sdk";
136
+ import { RunApps } from "@runappsai/sdk";
137
137
 
138
- const client = new RunApps({ apiKey: "rk_…" }); // Dashboard → API Keys
138
+ const client = new RunApps({ apiKey: "rk_…" });
139
139
 
140
140
  const resp = await client.chat.create({
141
141
  model: "Gemini 3 Flash",
@@ -144,23 +144,27 @@ const resp = await client.chat.create({
144
144
  console.log(resp.usage.total_cost); // USD
145
145
  ```
146
146
 
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:
147
+ Any OpenAI-compatible client works too:
152
148
 
153
149
  ```python
154
150
  from openai import OpenAI
151
+
155
152
  client = OpenAI(api_key="rk_...", base_url="https://www.runapps.ai/v1")
156
153
  client.chat.completions.create(model="Claude Sonnet 4.6", messages=[{"role": "user", "content": "Hello!"}])
157
154
  ```
158
155
 
159
- Browse `GET /v1/models` (no auth) for the live catalogue and per-model prices.
156
+ ```bash
157
+ curl https://www.runapps.ai/v1/chat/completions \
158
+ -H "Authorization: Bearer $RUNAPPS_API_KEY" \
159
+ -H "Content-Type: application/json" \
160
+ -d '{"model": "Claude Sonnet 4.6", "messages": [{"role": "user", "content": "Hello!"}]}'
161
+ ```
162
+
163
+ Never put an `rk_…` key in a frontend; an app on RunApps uses `authProvider: "runapps"`. `baseURL` overrides the base URL in either mode.
160
164
 
161
165
  ## Chat
162
166
 
163
- OpenAI-compatible, streaming or not. The final streamed chunk carries `usage`.
167
+ `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
168
 
165
169
  ```ts
166
170
  for await (const chunk of client.chat.stream({ model: "Gemini 3 Flash", messages })) {
@@ -169,10 +173,10 @@ for await (const chunk of client.chat.stream({ model: "Gemini 3 Flash", messages
169
173
  }
170
174
  ```
171
175
 
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:
176
+ **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
177
 
174
178
  ```ts
175
- import { userMessageParts, textPart, imagePart, videoPart, audioPart } from "@runappsai/sdk";
179
+ import { userMessageParts, textPart, videoPart } from "@runappsai/sdk";
176
180
 
177
181
  await client.chat.create({
178
182
  model: "Gemini 3 Vision",
@@ -180,9 +184,11 @@ await client.chat.create({
180
184
  });
181
185
  ```
182
186
 
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.
187
+ **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.
188
+
189
+ ### Server tools
184
190
 
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`.
191
+ The platform can search and read the web for the model, looping until it answers. Your code only sees the final reply.
186
192
 
187
193
  ```ts
188
194
  import { ServerTools } from "@runappsai/sdk";
@@ -191,14 +197,74 @@ const resp = await client.chat.create({
191
197
  model: "Claude Sonnet 4.6",
192
198
  messages: [{ role: "user", content: "What is the latest stable Go version?" }],
193
199
  server_tools: [ServerTools.WebSearch, ServerTools.WebFetch],
194
- max_server_iterations: 5, // default 5, max 10
200
+ max_server_iterations: 5, // default 5, at most 10
195
201
  });
196
202
  for (const t of resp.usage.tool_costs ?? []) console.log(t.name, t.count, t.cost);
197
203
  ```
198
204
 
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.
205
+ | Tool | What it does | Price |
206
+ |---|---|---|
207
+ | `web_search` | Searches the web | Per call |
208
+ | `web_fetch` | Reads a page as text | Free |
209
+ | `twitter_search` | Searches posts on X | Per call |
210
+
211
+ 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.
212
+
213
+ ## Files
214
+
215
+ 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>`.
216
+
217
+ ```ts
218
+ // Write. body: Blob | File | ArrayBuffer | Uint8Array | ReadableStream | string
219
+ const obj = await client.files.put("assets/logo.png", pngBlob, { contentType: "image/png" });
220
+ await client.files.putString("notes.md", "# hello");
221
+ await client.files.putFromURL("cache/cat.jpg", "https://example.com/cat.jpg"); // the platform fetches it
222
+
223
+ // Read.
224
+ const blob = await client.files.get("assets/logo.png");
225
+ const meta = await client.files.stat("assets/logo.png"); // size, etag, last_modified, url
226
+ await client.files.exists("assets/logo.png");
227
+
228
+ // List, with an optional glob (*, **, ?, [abc]) matched against the whole path.
229
+ const page = await client.files.list({ prefix: "assets/", glob: "**/*.png", limit: 100 });
230
+ // page.next_cursor → pass as cursor for the next page
231
+
232
+ // Change.
233
+ await client.files.move("draft.md", "published.md");
234
+ await client.files.copy("template.md", "instances/today.md");
235
+ await client.files.del("old.log");
236
+ await client.files.deleteMany({ prefix: "tmp/" });
237
+
238
+ // Several operations in one round trip; each result has its own ok and error.
239
+ await client.files.batch([
240
+ { op: "put_url", path: "a.jpg", src_url: "https://example.com/a.jpg" },
241
+ { op: "copy", from: "a.jpg", to: "b.jpg" },
242
+ { op: "del", path: "a.jpg" },
243
+ ]);
244
+ ```
245
+
246
+ **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
247
 
201
- Message builders: `userMessage`, `systemMessage`, `assistantMessage`, `toolResultMessage`, `userMessageParts`, `textPart`, `imagePart`, `videoPart`, `audioPart`.
248
+ ```ts
249
+ import { APIError } from "@runappsai/sdk";
250
+
251
+ const { value, etag } = await client.files.readJSON<{ score: number }>("state.json");
252
+ const next = { score: (value?.score ?? 0) + 1 };
253
+ try {
254
+ await client.files.put("state.json", JSON.stringify(next), {
255
+ contentType: "application/json",
256
+ ...(etag ? { ifMatch: etag } : { ifNoneMatch: true }), // first save: must not exist yet
257
+ });
258
+ } catch (e) {
259
+ if (e instanceof APIError && e.statusCode === 412) { /* read again, merge, retry */ }
260
+ }
261
+ ```
262
+
263
+ `files.read(path)` is `get` plus the ETag, for files that aren't JSON. `ifNoneMatch: true` refuses to overwrite an existing file (409).
264
+
265
+ **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.
266
+
267
+ Paths are at most 900 bytes, and a single file is at most 200 MB.
202
268
 
203
269
  ## Models
204
270
 
@@ -207,26 +273,26 @@ const all = await client.models.list();
207
273
  const video = await client.models.list({ capability: "video_generation" });
208
274
  ```
209
275
 
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.
276
+ `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
277
 
212
278
  ## Image, audio, video, embeddings
213
279
 
214
280
  ```ts
215
- // Image — data: URI (sync) or hosted blob URL (async); decodeMediaUrl() gives bytes.
281
+ // Image: a data: URI (sync) or a hosted URL (async); decodeMediaUrl() gives the bytes.
216
282
  const img = await client.image.generate("MiniMax Image-01", {
217
283
  prompt: "a developer at a laptop, anime style",
218
284
  resolution: "2K", aspect_ratio: "16:9", // or size: "WxH"
219
285
  reference_image_urls: ["https://…"],
220
286
  });
221
287
  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
288
+ // generateAsync, or submitGenerate + getAsyncStatus, for jobs longer than about 100 s
223
289
 
224
290
  // Speech.
225
291
  const speech = await client.audio.speech("MiniMax Speech 2.6 HD", { input: "Hello", voice: "English_radiant_girl", emotion: "happy" });
226
292
  const text = await client.audio.transcribe("OpenAI/Whisper", { file: { data: audioBytes, filename: "a.mp3" } });
227
293
  // speechAsync / transcribeAsync for long jobs; voices and emotions are in getOptionsSchema(model).catalog
228
294
 
229
- // Video — always async.
295
+ // Video: always async.
230
296
  const task = await client.video.generate("MiniMax Hailuo 2.3", { prompt: "a gentle ocean wave", duration: 5 });
231
297
  const status = await client.video.wait(task.id, { pollIntervalMs: 5000 });
232
298
  if (status.status === "succeeded") await client.video.getContent(task.id);
@@ -235,9 +301,66 @@ if (status.status === "succeeded") await client.video.getContent(task.id);
235
301
  const e = await client.embeddings.create("text-embedding-3-small", { input: ["alpha", "beta"] });
236
302
  ```
237
303
 
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)`).
304
+ `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)`).
305
+
306
+ ## HTTP API
307
+
308
+ Base URL `https://www.runapps.ai`. Send `Authorization: Bearer <key or app token>`.
309
+
310
+ | Request | Does |
311
+ |---|---|
312
+ | `POST /v1/chat/completions` | Chat, OpenAI format |
313
+ | `GET /v1/models` | Model catalogue and prices (no key needed) |
314
+ | `POST /v1/images/generations` | Generate images |
315
+ | `POST /v1/images/edits` | Edit an image |
316
+ | `POST /v1/audio/speech` | Text to speech |
317
+ | `POST /v1/audio/transcriptions` | Speech to text |
318
+ | `GET /v1/audio/voices` | Voices for a speech model |
319
+ | `POST /v1/videos/generations` | Start a video job |
320
+ | `GET /v1/videos/generations/<id>` | Job status |
321
+ | `GET /v1/videos/<id>/content` | The finished video |
322
+ | `POST /v1/embeddings` | Embeddings |
323
+ | `/v1/async/…` | Async versions of the image and audio calls: submit, then poll |
324
+
325
+ ### Files over HTTP
326
+
327
+ With an app token the routes are under `/v1/files`. With a personal key, put the app in the path: `/v1/p/<slug>/files`.
328
+
329
+ | Request | Does |
330
+ |---|---|
331
+ | `GET /v1/files?prefix=&glob=&limit=&cursor=` | List files. `limit` defaults to 100, at most 1000. |
332
+ | `GET /v1/files/<path>` | Read a file. Returns `ETag`; sends 304 for a matching `If-None-Match`. |
333
+ | `HEAD /v1/files/<path>` | Metadata only. |
334
+ | `PUT /v1/files/<path>` | Write the request body. Honours `Content-Type`, `If-Match: <etag>` and `If-None-Match: *`. |
335
+ | `DELETE /v1/files/<path>` | Delete a file. |
336
+ | `POST /v1/files/move` | `{"from", "to"}` |
337
+ | `POST /v1/files/copy` | `{"from", "to"}` |
338
+ | `POST /v1/files/delete` | `{"prefix"}` or `{"glob"}` deletes many. |
339
+ | `POST /v1/files/put-url` | `{"path", "src_url"}`: the platform downloads the URL into the file. |
340
+ | `POST /v1/files/batch` | `{"ops": [...]}`: up to 64 operations, `put_url`, `del`, `move`, `copy`, `exists`, `stat`. |
341
+
342
+ A file object has `path`, `size`, `content_type`, `etag`, `last_modified` and `url`.
343
+
344
+ ### Sign-in without the SDK
345
+
346
+ Send the user to:
347
+
348
+ ```
349
+ https://www.runapps.ai/api/sdk/grant?origin=<your origin>&redirect_to=<url to return to>&token_name=runapps_token[&project_id=<slug>]
350
+ ```
351
+
352
+ 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`.
353
+
354
+ ## Errors
239
355
 
240
- ## Errors and cancellation
356
+ | Status | Meaning |
357
+ |---|---|
358
+ | 400 | Bad request, such as an input the model doesn't accept. Nothing is charged. |
359
+ | 401 | Missing or invalid key or token. In browser sign-in mode the SDK refreshes the token and retries once. |
360
+ | 402 | The balance is too low, or today's budget is reached. |
361
+ | 409 | `If-None-Match: *` and the file already exists. |
362
+ | 412 | `If-Match` and the file changed since you read it. |
363
+ | 429 | Too many requests at once. Retry shortly. |
241
364
 
242
365
  ```ts
243
366
  try {
@@ -248,9 +371,9 @@ try {
248
371
  }
249
372
  ```
250
373
 
251
- Every call takes `{ signal }`. In browser-auth mode a 401 is retried once after refreshing the token.
374
+ Every call takes `{ signal }` for cancellation.
252
375
 
253
- ## API reference
376
+ ## Reference
254
377
 
255
378
  | Service | Methods |
256
379
  |---|---|
@@ -262,13 +385,21 @@ Every call takes `{ signal }`. In browser-auth mode a 401 is retried once after
262
385
  | `client.video` | `generate`, `getStatus`, `wait`, `getContent` |
263
386
  | `client.embeddings` | `create` |
264
387
 
265
- Browser auth: `client.signIn()`, `client.signOut()`, `client.user`, `client.auth`.
388
+ Browser sign-in: `client.signIn()`, `client.signOut()`, `client.user`, `client.auth`.
266
389
 
267
- Helpers: `hasCapabilityTag`, `acceptsModality`, `getOptionsSchema`, `acceptsField`, `requiresField`, `allowedValuesFor`, `validateRequest`, `supportsVoiceClone`, `supportsInstructText`, `defaultVoice`, `encodeImageUrl`, `decodeMediaUrl`, the message builders, `ServerTools`.
390
+ 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`.
391
+
392
+ ### Addresses
393
+
394
+ | Address | What it serves |
395
+ |---|---|
396
+ | `www.runapps.ai` | The website, dashboard, sign-in and the API (`/v1`) |
397
+ | `<slug>.runapps.dev` | Each app |
398
+ | `files.runapps.ai` | Public file links |
268
399
 
269
- ## Compatibility
400
+ ### Upgrading from `@runjobsai/sdk`
270
401
 
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.
402
+ 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. `RunJobs` and `authProvider: "runjobs"` still work as deprecated aliases of `RunApps` and `authProvider: "runapps"`.
272
403
 
273
404
  ## License
274
405
 
@@ -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"}