@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 +183 -92
- package/contracts/openapi.yaml +374 -15
- package/dist/src/docs/index.d.ts.map +1 -1
- package/dist/src/docs/index.js +15 -21
- package/dist/src/docs/index.js.map +1 -1
- package/dist/src/generated/openapi.d.ts +370 -14
- package/dist/src/generated/openapi.d.ts.map +1 -1
- package/dist/src/generated/package-version.d.ts +1 -1
- package/dist/src/generated/package-version.js +1 -1
- package/dist/src/sdk/base64.d.ts.map +1 -1
- package/dist/src/sdk/base64.js +5 -1
- package/dist/src/sdk/base64.js.map +1 -1
- package/dist/src/sdk/client.d.ts +22 -2
- package/dist/src/sdk/client.d.ts.map +1 -1
- package/dist/src/sdk/client.js +66 -7
- package/dist/src/sdk/client.js.map +1 -1
- package/dist/src/sdk/response-body.d.ts +1 -1
- package/dist/src/sdk/response-body.d.ts.map +1 -1
- package/dist/src/sdk/response-body.js +1 -1
- package/dist/src/sdk/response-body.js.map +1 -1
- package/dist/src/sdk/types.d.ts +37 -7
- package/dist/src/sdk/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,54 +1,188 @@
|
|
|
1
1
|
# @spicyapi/sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
33
|
-
{
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
[
|
|
104
|
-
[
|
|
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
|
-
##
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
`
|
|
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).
|