@wtfalch/ai
v0.3.0
Published
Public client SDK and API types for the AI service.
Downloads
111
Readme
@wtfalch/ai
Public client SDK and API types for the AI service. Requires Node 22+ or a
browser with Fetch and AbortSignal.timeout support. The client has no runtime
package dependencies. It is an ES module package; use import in Node.js.
The optional @wtfalch/ai/webhooks entrypoint requires Node.js.
npm install @wtfalch/aiimport { createAiClient, ApiError } from '@wtfalch/ai';
const ai = createAiClient({
organisationId: 'your-organisation-id',
url: serviceOrigin,
credential: () => serviceKey,
});
const run = await ai.submit({
source: { mode: 'offering', id: offeringId },
input: { prompt: 'Hello', maxOutputTokens: 256 },
requestId: crypto.randomUUID(),
});
const status = await ai.get(run.id);Use the deployed service's HTTPS origin and a scoped service credential. The credential callback runs for every request, so rotation does not require a new client. The client refuses redirects and omits browser cookies. An HTTP origin is accepted only for localhost development. Do not embed a privileged service credential in public browser code.
Requests require current service permissions and configured budgets. A retry
of the same operation should retain its request ID. API failures throw
ApiError with status and code; the SDK does not expose raw upstream error
bodies. A 202 AI response means queued, not completed.
submit(input, { wait }) waits up to wait seconds (integer, 1-60) for the
run to finish before responding: HTTP 200 with the finished RunView if it
settles in time, HTTP 202 with the run's current state (usually still
queued) if wait elapses first. Either way the SDK returns the same
RunView get returns, so check state to tell the two apart. Execution
keeps going after the deadline even if the caller did not wait for it.
Without wait, submit returns as soon as the run is queued, as before.
submitBatch(runs) queues up to 100 runs in one request (POST /v1/runs/batch).
Each item is an ordinary queued submit with its own requestId, rate-limit slot
and budget reservation. The result array is in request order, and each item is
either { run } or { error: { code, message } }. A refused item does not affect
the others, and resending the same batch replays the runs already created. Batch
items cannot stream or wait, and the whole request body is capped at 256 KB.
A fal image offering (seedream-v4, seedream-v4-edit, birefnet) settles the same way and
returns its files in output.files:
const run = await ai.submit(
{
source: { mode: 'offering', id: falOfferingId },
input: { prompt: 'a red cube' },
requestId: crypto.randomUUID(),
},
{ wait: 60 },
);
if (run.state === 'succeeded') {
for (const file of (run.output as { files: { url: string }[] }).files) console.log(file.url);
}Each file's url is an https URL fal itself serves; the SDK neither downloads nor re-hosts it.
For a text model that supports streaming, stream returns an async iterable:
for await (const event of ai.stream({
source: { mode: 'offering', id: textOfferingId },
input: { prompt: 'Hello', maxOutputTokens: 256 },
requestId: crypto.randomUUID(),
})) {
if (event.type === 'delta') console.log(event.text);
else console.log(event.run.state);
}Pass { signal: controller.signal } as the second argument to abort a stream,
or break out of the loop to disconnect. A normal stream ends with a done
event carrying the final run. A disconnect may prevent receipt of that event;
use get when the run ID is known to check its state. Disconnecting does not
guarantee cancellation or prevent charges for work already performed. Use
stream instead of passing stream: true to submit, which expects JSON.
Webhook receivers can verify the x-wtfalch-signature header using the separate
Node.js entrypoint. Verify the exact raw request body before parsing JSON:
import { verifyWebhookSignature } from '@wtfalch/ai/webhooks';
const rawBody = await request.text();
const valid = verifyWebhookSignature(
request.headers.get('x-wtfalch-signature') ?? '',
[webhookSecret],
rawBody,
Math.floor(Date.now() / 1000),
);
if (!valid) throw new Error('Invalid webhook signature');
const event = JSON.parse(rawBody);Timestamps are Unix seconds, with a default tolerance of 300 seconds. During
secret rotation, include the previous secret only while its overlap window is
valid. signWebhookPayload and SIGNATURE_TOLERANCE_SECONDS are also exported
from /webhooks. Signature verification does not deduplicate deliveries;
receivers should handle repeated events idempotently.
listConnections, listRuns, listOfferings, listBudgets and listUsage
each take an optional { after, limit } page (limit 1–100, default 50) and
return only the rows the credential's grants allow in the selected
organisation. Connections and offerings page by id; runs and usage page
newest first, after being the last id seen. Budgets page by key id and
cover the current period only, with a null key id for the organisation-wide
row. On listRuns and listUsage, an after id the credential cannot read,
or one that does not exist, throws ApiError with status: 400.
updateBudget({ keyId, limitMicros }) sets the organisation's cap (keyId: null) or one
key's, needing ai.budgets:update reaching whichever it targets; zero blocks spending, and
no caller can remove a cap. exportUsage({ from, to }) returns usage as CSV text (ISO
timestamps, at most 92 days), needing an ai.usage:export grant that itself reaches the
organisation — an owner- or key-scoped grant throws 403; use listUsage instead.
createOffering, updateOffering, disableOffering and setOfferingAccess administer
platform offerings and need ai.offerings:create/:update/:disable, platform-boundary
permissions no customer organisation's grants can reach. createOffering throws
ApiError with status: 503 while the host has no provider key store configured.
The root export provides the client and public request/response types.
@wtfalch/ai/client is also supported. There are no server, database,
migration, provider-adapter or authorization-engine exports. The service
implementation is maintained in a private repository.
The first 0.1.0 SDK targets the new service API. Production migration from Valet is a separate rollout; installing the SDK does not migrate accounts or files.
