@spicyapi/sdk 0.4.0 → 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,54 +1,188 @@
1
1
  # @spicyapi/sdk
2
2
 
3
- Official TypeScript SDK for the SpicyAPI public API. It contains the typed client, webhook
4
- verification helpers, documentation index, and generated OpenAPI types. It does not install the CLI,
5
- MCP server, or Agent Skill.
3
+ The official TypeScript SDK for [SpicyAPI](https://spicyapi.ai) one API for video, image and chat
4
+ models. It contains the typed client, webhook verification helpers, a documentation index and
5
+ generated OpenAPI types.
6
+
7
+ It does **not** install the CLI, MCP server or Agent Skill.
6
8
 
7
9
  ```bash
8
10
  npm install @spicyapi/sdk
9
11
  ```
10
12
 
13
+ Requires Node.js 22.13 or later.
14
+
15
+ ## Get a key first
16
+
17
+ 1. Create an account at [spicyapi.ai/register](https://spicyapi.ai/register) — accounts are opened
18
+ in batches, so you may join the waitlist first.
19
+ 2. Create a key on the [API keys page](https://spicyapi.ai/console/keys). It starts with `sk-spicy-`
20
+ and is shown once.
21
+ 3. Put it in the environment your server reads, never in source:
22
+
23
+ ```bash
24
+ export SPICY_API_KEY="sk-spicy-..." # paste your own key
25
+ ```
26
+
27
+ ## Your first task
28
+
11
29
  ```ts
30
+ import { randomUUID } from "node:crypto";
12
31
  import { SpicyClient } from "@spicyapi/sdk";
13
32
 
33
+ // Reads SPICY_API_KEY from the environment; never pass a key as an argument.
14
34
  const client = new SpicyClient();
15
- const models = await client.listModels({ includeSchema: true });
16
- ```
17
35
 
18
- Set `SPICY_API_KEY` with your process secret manager. Task creation and retry may reserve funds; use
19
- stable idempotency keys and explicit user confirmation. When a caller needs an exact price
20
- confirmation, call `quoteTask` with the request, display its `estimatedCost`, `maxCharge` and
21
- `expiresAt`, then pass unchanged input, `quoteId` and `expectedCost` to `createTask` or `run`.
22
- Pre-authorized server workflows can submit directly at current pricing; separate health, balance and
23
- quote requests are not mandatory API steps.
36
+ // 1. Take a model id from the live catalog instead of hard-coding one. Ids read
37
+ // `<maker>/<model>/<task>`, e.g. `bytedance/seedream-5.0-pro/text-to-image`.
38
+ const catalog = await client.listModels({ modality: "image" });
39
+ const model = catalog.items[0].model;
24
40
 
25
- ## Submit and wait
41
+ // 2. Read that model's input schema, and build `input` from it — two models in
42
+ // one family can take different fields.
43
+ const { inputSchema } = await client.getModel(model);
26
44
 
27
- ```ts
28
- const model = process.env.SPICY_MODEL;
29
- const operationId = process.env.SPICY_IDEMPOTENCY_KEY;
30
- if (!model || !operationId) throw new Error("Set a model and a persisted operation key");
45
+ // 3. One billable task, waited to a terminal state.
31
46
  const task = await client.run(
32
- { model, input: { prompt: "A quiet mountain lake" } },
33
- { idempotencyKey: operationId, onAccepted: ({ taskId }) => console.log({ taskId }) },
47
+ { model, input: { prompt: "A cinematic night portrait" } },
48
+ {
49
+ idempotencyKey: randomUUID(),
50
+ onAccepted: ({ taskId }) => console.log({ taskId }),
51
+ },
34
52
  );
53
+
35
54
  if (task.state === "succeeded") {
36
55
  console.log(task.output?.assets?.[0]?.url);
37
56
  }
38
57
  ```
39
58
 
40
- Use a model and input supported by its current schema. `run` submits once and handles adaptive
41
- polling, including pending media transfer. Its local timeout includes submission and waiting; timing
42
- out or aborting does not cancel the accepted task. Resume with the saved task ID. Use a webhook for
43
- background jobs to avoid polling entirely.
59
+ `run` submits once and handles adaptive polling, including pending media transfer. Its local timeout
60
+ covers submission and waiting; timing out or aborting **does not cancel the accepted task** — resume
61
+ later with the saved task ID. For background jobs, use a webhook and skip polling entirely.
62
+
63
+ ## Three rules
64
+
65
+ 1. **The key stays on a server.** Set `SPICY_API_KEY` through the process environment or a secret
66
+ manager. Never place a live key in browser code, a mobile bundle, source, prompts, logs or
67
+ committed configuration.
68
+ 2. **Read the schema before building `input`.** Two models in one family can take different fields.
69
+ `getModel` returns the current schema; do not copy fields from a different model.
70
+ 3. **`createTask`, `run` and `retryTask` spend money.** Persist one idempotency key per logical
71
+ attempt and reuse it for every retry of that attempt. A new key means a new generation.
72
+
73
+ ## Client reference
74
+
75
+ | Method | Purpose |
76
+ | --------------------------------------------- | --------------------------------------------------------- |
77
+ | `listModels` / `getModel` | Live models, schemas, examples and your prices |
78
+ | `quoteTask` | Exact price for a request, without creating anything |
79
+ | `createTask` / `run` / `retryTask` | Create or retry a billable task (`run` also waits) |
80
+ | `purgeTask` | Destroy one terminal task's stored content; idempotent |
81
+ | `getTask` / `waitForTask` | Read once, or wait for a terminal state with a time bound |
82
+ | `listTasks` | One page of task metadata for the current key |
83
+ | `getBalance` / `getUsage` | Available, held and total balance; settled spending |
84
+ | `getStatus` | Public health and readiness |
85
+ | `createUploadUrl` / `commitUploadedFile` | Direct-upload ticket, then the `spicy://` commit |
86
+ | `uploadBytes` / `uploadFile` / `uploadBase64` | Shortcuts around that upload flow |
87
+ | `createDownloadUrl` | Short-lived URL for a task output |
88
+
89
+ `createTask` and `run` accept `retentionSeconds`, sent as `X-Spicy-Retention`, to shorten how long
90
+ that one task's outputs and prompt are kept. It only ever shortens; the account settings and the
91
+ platform maximum still apply, and the deadlines that took effect come back in the task record's
92
+ `retention` object alongside `contentState`.
93
+
94
+ `purgeTask` destroys one terminal task's generated media, result payload, prompt and input text. It
95
+ is idempotent — repeating it on an already destroyed task succeeds — and it **destroys content, not
96
+ the record of what it cost**: the ledger entry, charged amount, model, state, timestamps and request
97
+ ID all remain queryable. It is not a refund and does not reverse a charge.
98
+
99
+ TypeScript is the current official SDK. For Python, Go and other languages, generate a client from
100
+ the [OpenAPI 3.1 contract](https://spicyapi.ai/docs/api-reference). Do not describe an unpublished
101
+ third-party wrapper as an official SDK.
102
+
103
+ ## Confirming a price before you spend
104
+
105
+ When a caller needs an exact price confirmation, quote the request, show the numbers, then pass the
106
+ quote into creation:
107
+
108
+ ```ts
109
+ const payload = { model, input };
110
+ const quote = await client.quoteTask(payload);
111
+ // Show quote.estimatedCost, quote.maxCharge and quote.expiresAt to the user.
112
+
113
+ const accepted = await client.createTask(
114
+ { ...payload, quoteId: quote.quoteId, expectedCost: quote.estimatedCost },
115
+ { idempotencyKey: operationId },
116
+ );
117
+ ```
44
118
 
45
- Documentation: <https://spicyapi.ai/en/docs>
119
+ Pre-authorized server workflows can submit directly at current pricing. Separate health, balance and
120
+ quote requests are not mandatory steps — the exact `estimatedCost` comes back with the acceptance.
121
+
122
+ ## Media inputs and results
123
+
124
+ Pass a publicly accessible HTTPS image, video or audio URL directly in the field the model schema
125
+ specifies. Uploading is optional:
126
+
127
+ - `createUploadUrl` → PUT → `commitUploadedFile` returns an account-bound `spicy://` URI valid for
128
+ one day. `uploadFile` wraps all three steps.
129
+ - `uploadBase64` accepts a Data URI or raw standard Base64 with an explicit `contentType`.
130
+ - Small schema-declared image fields accept Data URIs directly in `input` (1 MiB decoded per image,
131
+ 2 MiB total JSON body, with documented pixel limits). The upload helper handles larger files
132
+ within upload and model limits.
133
+
134
+ `getTask` and `waitForTask` return ready media in `output.assets` — `url`, `expiresAt`, MIME, and
135
+ available dimensions, duration and byte count. Download `asset.url` directly **without forwarding
136
+ `SPICY_API_KEY`**; no separate `createDownloadUrl` call is needed. `pending` assets need another
137
+ poll. URLs normally last 20 minutes, inside the 14-day result retention window; poll again to
138
+ refresh one. See the [media guide](https://spicyapi.ai/docs/media).
139
+
140
+ ## Webhooks
141
+
142
+ ```ts
143
+ import { verifyWebhook } from "@spicyapi/sdk";
144
+
145
+ const { payload, taskId } = verifyWebhook({
146
+ rawBody, // the exact received bytes, before any parsing
147
+ secret: process.env.SPICY_WEBHOOK_SECRET!,
148
+ timestamp: headers["x-webhook-timestamp"],
149
+ signature: headers["x-webhook-signature"],
150
+ payloadVersion: 2,
151
+ });
152
+ ```
153
+
154
+ Verify against the **exact received bytes**, before any JSON parsing or re-serialization, and
155
+ deduplicate on `request_id` rather than a full-body hash. V2 webhooks carry the same result fields
156
+ as a task read; a retry keeps the business event and `request_id` but refreshes `url` and
157
+ `expiresAt`. V1 payloads are unchanged.
158
+
159
+ ## Task history
160
+
161
+ `listTasks` reads one page of metadata for the current API key. It does not fetch each task's
162
+ inputs, results or media URLs. Keep the UTC dates fixed while paging and pass the cursor unchanged:
163
+
164
+ ```ts
165
+ const filters = { from: "2026-09-01", to: "2026-09-07", limit: 20 };
166
+ const page = await client.listTasks(filters);
167
+ if (page.hasMore && page.nextCursor) {
168
+ const nextPage = await client.listTasks({ ...filters, cursor: page.nextCursor });
169
+ console.log(nextPage.items);
170
+ }
171
+ ```
172
+
173
+ The UTC interval is `[from,to)`, defaults to seven days ending tomorrow UTC, and cannot exceed 92
174
+ days. Page size defaults to 20 and is capped at 100. Optional `state` and `model` filters narrow the
175
+ results. Each `cost` is an exact decimal USD string, final only when `settled` is true. Hidden tasks
176
+ are excluded, so use `getUsage` for settled spending reports.
177
+
178
+ History is for discovery and recovery. Use `run`, `waitForTask` or webhooks to track normal
179
+ completion, and `getTask` only for a result you actually selected.
46
180
 
47
181
  ## Chat and LLM streaming
48
182
 
49
- Use the official `openai` client for `/v1/chat/completions`; install it separately with
50
- `npm install openai`. The native SDK intentionally keeps tasks, quotes and uploads in one small
51
- client. It does not reimplement the OpenAI streaming protocol.
183
+ This SDK deliberately keeps tasks, quotes and uploads in one small client and does not reimplement
184
+ the OpenAI streaming protocol. For token-by-token chat, use the official `openai` client against
185
+ SpicyAPI's compatible endpoints (`npm install openai`):
52
186
 
53
187
  ```js
54
188
  import OpenAI from "openai";
@@ -56,8 +190,9 @@ import OpenAI from "openai";
56
190
  const client = new OpenAI({
57
191
  apiKey: process.env.SPICY_API_KEY,
58
192
  baseURL: "https://api.spicyapi.ai/v1",
59
- maxRetries: 0,
193
+ maxRetries: 0, // never silently repeat a billable operation
60
194
  });
195
+
61
196
  const signal = AbortSignal.timeout(120_000);
62
197
  const stream = await client.chat.completions.create(
63
198
  {
@@ -66,82 +201,38 @@ const stream = await client.chat.completions.create(
66
201
  stream: true,
67
202
  stream_options: { include_usage: true },
68
203
  },
69
- {
70
- signal,
71
- headers: { "Idempotency-Key": process.env.SPICY_IDEMPOTENCY_KEY },
72
- },
204
+ { signal, headers: { "Idempotency-Key": process.env.SPICY_IDEMPOTENCY_KEY } },
73
205
  );
206
+
74
207
  try {
75
208
  for await (const chunk of stream) {
76
209
  process.stdout.write(chunk.choices[0]?.delta.content ?? "");
77
- if (chunk.usage) process.stderr.write(JSON.stringify(chunk.usage) + "\n");
210
+ if (chunk.usage) process.stderr.write(`${JSON.stringify(chunk.usage)}\n`);
78
211
  }
79
212
  } finally {
80
213
  stream.controller.abort();
81
214
  }
82
215
  ```
83
216
 
84
- The abort signal covers stream consumption as well as connection setup. Aborting stops the local
85
- stream; it does not cancel an accepted task or promise a refund. With `stream: false`, read
86
- `choices[0].message.content`. In a tool conversation, accumulate tool-call fragments by index and
87
- preserve the complete assistant message before adding matching `tool_call_id` results. Only enable
88
- tools or reasoning fields when the model schema supports them.
89
-
90
- CLI and MCP currently submit and track native tasks; they do not expose live chat token streaming.
91
- Use this client path for a token-by-token interface, or native `jobs/stream` when you need quote
92
- confirmation and the platform event envelope.
93
-
94
- Select `SPICY_MODEL` from the live model schema. Keep API keys on your server and persist
95
- `SPICY_IDEMPOTENCY_KEY` for the same logical action; disable automatic retries to avoid silently
96
- repeating a billable operation. Compatibility requests use the price at acceptance. Use native
97
- `quoteTask` and `createTask` when confirming `expectedCost` is required.
217
+ Notes that matter in production:
98
218
 
99
- Reasoning-capable models may return `delta.reasoning_content`; it is optional and may need an
100
- explicit type extension in the official client. Reasoning tokens are already included in
101
- `completion_tokens`. Stream completion is not proof of financial settlement.
219
+ - The abort signal covers stream consumption as well as connection setup. Aborting stops the local
220
+ stream; it does not cancel an accepted task or promise a refund.
221
+ - With `stream: false`, read `choices[0].message.content`.
222
+ - In a tool conversation, accumulate tool-call fragments by index and preserve the complete
223
+ assistant message before adding matching `tool_call_id` results.
224
+ - Enable tool or reasoning fields only when the model schema supports them. Reasoning-capable models
225
+ may return `delta.reasoning_content`, which can need an explicit type extension in the official
226
+ client; its tokens are already counted in `completion_tokens`.
227
+ - Compatibility requests use the price at acceptance. Use native `quoteTask` and `createTask` when
228
+ confirming `expectedCost` is required.
229
+ - Stream completion is not proof of financial settlement.
102
230
 
103
- [Complete compatibility guide](https://spicyapi.ai/en/docs/quotes-and-compatibility) ·
104
- [Official JavaScript client](https://github.com/openai/openai-node)
231
+ See the [complete compatibility guide](https://spicyapi.ai/docs/quotes-and-compatibility) and the
232
+ [official JavaScript client](https://github.com/openai/openai-node).
105
233
 
106
- ## Media inputs and results
107
-
108
- Pass a publicly accessible HTTPS image, video, or audio URL directly in the field specified by the
109
- model schema. Uploading is optional: local files use `createUploadUrl` → PUT → `commitUploadedFile`,
110
- then the returned account-bound `spicy://` URI (valid for one day). `uploadFile` wraps these steps;
111
- `uploadBase64` also accepts a Data URI or raw standard Base64 with an explicit `contentType`. Small
112
- schema-declared image fields can accept Data URIs directly in `input` (1 MiB decoded per image, 2
113
- MiB total JSON body, with documented pixel limits). The upload helper is separate and supports
114
- larger files within upload and model limits. See the
115
- [media guide](https://docs.spicyapi.ai/en/docs/media).
116
-
117
- `getTask` and `waitForTask` return ready media in `output.assets`, including `url`, `expiresAt`,
118
- MIME, and available dimensions, duration, and byte count. Download `asset.url` directly without
119
- forwarding `SPICY_API_KEY`; no separate `createDownloadUrl` call is required. `pending` assets need
120
- another poll. URLs normally last 20 minutes, within the 14-day result retention window. Poll again
121
- to refresh a URL; `createDownloadUrl` remains available for older integrations.
122
-
123
- V2 webhooks use the same result fields. A retry keeps the business event and `request_id`, but
124
- refreshes `url` and `expiresAt`. Verify the signature against the exact received body and
125
- deduplicate using `request_id`, not a full-body hash. V1 payloads remain unchanged.
234
+ ## More
126
235
 
127
- ## Task history
128
-
129
- `listTasks` reads one page of metadata for the current API key. It does not fetch each task's
130
- inputs, results, or media URLs. Keep the UTC dates fixed while paging and pass the returned cursor
131
- unchanged:
132
-
133
- ```ts
134
- const filters = { from: "2026-09-01", to: "2026-09-07", limit: 20 };
135
- const page = await client.listTasks(filters);
136
- if (page.hasMore && page.nextCursor) {
137
- const nextPage = await client.listTasks({ ...filters, cursor: page.nextCursor });
138
- console.log(nextPage.items);
139
- }
140
- ```
141
-
142
- The UTC interval is `[from,to)`, defaults to seven days ending tomorrow UTC, and cannot exceed 92
143
- days. Page size defaults to 20 and is capped at 100. Optional `state` and `model` filters narrow the
144
- results. Each item's `cost` remains an exact decimal USD string; it is final only when `settled` is
145
- true. Hidden tasks are excluded, so use `getUsage` for settled spending reports. Use `getTask` for a
146
- selected result, not automatically for every item. History is for discovery and recovery; use `run`,
147
- `waitForTask`, or webhooks for normal completion tracking.
236
+ - [Developer hub](https://spicyapi.ai/developers) · [Full documentation](https://spicyapi.ai/docs)
237
+ - Prefer a terminal? [`@spicyapi/cli`](https://www.npmjs.com/package/@spicyapi/cli). Prefer a coding
238
+ agent? [`@spicyapi/mcp`](https://www.npmjs.com/package/@spicyapi/mcp).