@polimorfapp/sdk
v0.1.1
Published
Official TypeScript SDK for the Polimorf AI application platform — run and stream assistants over the public runtime API.
Maintainers
Readme
@polimorfapp/sdk
The official TypeScript client for the Polimorf AI Application Platform —
the "integrate once" seam. This package currently provides the base client:
an authenticated, JSON HTTP transport with timeouts, cancellation, and typed
error handling, plus the runtime surface (runtime.execute, runtime.stream).
Quick start
npm install @polimorfapp/sdkimport { createClient } from '@polimorfapp/sdk';
// The client talks to the managed Polimorf API (https://api.polimorf.app);
// all you provide is your API key.
const client = createClient({ apiKey: process.env.POLIMORF_API_KEY });
const result = await client.assistant('support').run({
input: 'How do I cancel my subscription?',
});
console.log(result.message.content);Get an API key (csk_live_…) from your Polimorf dashboard. It can be passed as
apiKey or read from the POLIMORF_API_KEY environment variable.
Usage
import { createClient } from '@polimorfapp/sdk';
// apiKey passed explicitly…
const client = createClient({ apiKey: 'csk_live_…' });
// …or read from POLIMORF_API_KEY in the environment:
const client2 = createClient();
// Every request is sent with `Authorization: Bearer <apiKey>` and
// `Accept: application/json`.
const assistant = await client.request<{ id: string; name: string }>({
method: 'GET',
path: '/assistants/asst_123',
});Options
createClient({
apiKey, // required — `csk_live_…` key; falls back to POLIMORF_API_KEY
baseUrl, // optional — override the API host (defaults to https://api.polimorf.app)
timeoutMs, // optional — per-request timeout (default 30000)
fetch, // optional — inject a `fetch` implementation (tests/proxies)
defaultHeaders, // optional — headers sent on every request
});apiKey is required (passed explicitly or read from POLIMORF_API_KEY);
createClient throws a clear error when it is missing. baseUrl defaults to the
managed host https://api.polimorf.app and rarely needs changing — override it
(or set POLIMORF_BASE_URL) only to target a different environment; an explicit
option always wins over the env var.
fetch defaults to globalThis.fetch (Node ≥20). Inject your own for tests,
proxies, or custom transport wrappers.
Making requests
client.request<T>({
method, // 'GET' | 'POST' | ...
path, // resolved against baseUrl; leading slash optional
body, // optional — JSON-serialized, sent as application/json
query, // optional — object → query string (undefined values omitted)
headers, // optional — per-request headers (override defaultHeaders; the
// SDK-owned Authorization/Accept/Content-Type always win)
signal, // optional — AbortSignal, composed with the client timeout
});- A 2xx response resolves to the parsed JSON body typed as
T. - A
204/empty response resolves toundefined.
Running an assistant (non-streaming)
client.runtime.execute runs one generation and resolves with the full result:
const result = await client.runtime.execute({
providerName: 'openai',
model: 'gpt-4o',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'How do I cancel my subscription?' },
],
config: { temperature: 0.2, maxOutputTokens: 512 }, // optional
});
result.message; // { role: 'assistant', content: '…' }
result.finishReason; // 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'error'
result.usage; // { inputTokens, outputTokens, totalTokens }Pass an AbortSignal to cancel in flight:
await client.runtime.execute(request, { signal: controller.signal });This maps to POST /runtime/execute and sends the raw model-execution contract
(provider, model, messages, config).
Running a deployed assistant by slug
client.assistant(slug).run runs a published, deployed assistant by its slug
— the provider/model/prompt come from the deployed version's frozen snapshot, so
your application sends only the input and needs no redeploy when a product team
publishes a new version (PRD §5.10):
const result = await client.assistant('support').run({
input: 'How do I cancel my subscription?',
variables: { plan: 'pro' }, // optional — the version's declared variables
environment: 'production', // optional — defaults to 'production'
});
result.message; // { role: 'assistant', content: '…' }
result.usage; // { inputTokens, outputTokens, totalTokens }This maps to POST /runtime/assistants/:slug. An unknown slug, unknown
environment, or an assistant never deployed there all throw a NotFoundError
(404 DEPLOYMENT_NOT_FOUND). Pass an AbortSignal to cancel in flight, exactly
as with runtime.execute.
client.assistant(slug).stream is the streaming counterpart — it yields events
as the deployed model produces them (over POST /runtime/assistants/:slug/stream):
for await (const event of client.assistant('support').stream({ input: 'hi' })) {
if (event.type === 'delta') process.stdout.write(event.content);
else if (event.type === 'done') console.log('\n', event.finishReason);
}A pre-stream failure (e.g. 404 for an undeployed slug) throws before the loop
starts; the terminal provider error is delivered as an event, not a throw.
Running an assistant (streaming)
client.runtime.stream runs one generation and yields events as the model
produces them. It returns an AsyncIterable — consume it with for await:
for await (const event of client.runtime.stream({
providerName: 'openai',
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Write a haiku about the sea.' }],
})) {
switch (event.type) {
case 'delta':
process.stdout.write(event.content); // incremental text
break;
case 'usage':
event.usage; // { inputTokens, outputTokens, totalTokens }
break;
case 'done':
event.finishReason; // 'stop' | 'length' | ...
break;
case 'error':
// Terminal provider error — delivered as an event, not thrown.
event.error; // { code, message, retryable, providerName? }
break;
}
}This maps to POST /runtime/stream (Server-Sent Events). A terminal provider
error arrives as an error event (mirroring the wire contract), not a thrown
exception; only a transport failure (network, connect timeout, aborted request)
or a non-2xx response opening the stream throws (PolimorfError /
PolimorfApiError). The client timeoutMs bounds only the wait to open the
stream — it never truncates a live stream. Pass an AbortSignal to cancel in
flight:
const controller = new AbortController();
const stream = client.runtime.stream(request, { signal: controller.signal });
// …later: controller.abort();Errors
Every error the SDK throws extends PolimorfError. A non-2xx response
throws a PolimorfApiError — thrown as the status-mapped subclass so
you can branch on the error type with narrowing:
| HTTP status | Class |
| ----------- | ----------------------- |
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 429 | RateLimitError |
| ≥ 500 | ServerError |
| other | PolimorfApiError |
import {
PolimorfApiError,
PolimorfError,
NotFoundError,
RateLimitError,
} from '@polimorfapp/sdk';
try {
await client.request({ method: 'GET', path: '/assistants/missing' });
} catch (error) {
if (error instanceof RateLimitError) {
// 429 — back off and retry later
} else if (error instanceof NotFoundError) {
// 404 — the resource does not exist
} else if (error instanceof PolimorfApiError) {
// any other non-2xx response
error.status; // e.g. 409
error.code; // stable machine code, e.g. 'ASSISTANT_SLUG_CONFLICT' — autocompleted
error.message; // human-readable, may change across versions
error.fields; // [{ field, issue }] — only on VALIDATION_ERROR
} else if (error instanceof PolimorfError) {
// transport failure: network error, timeout, or aborted request
error.cause;
}
}Every subclass is still instanceof PolimorfApiError (and
PolimorfError), so a catch on a base class keeps matching. error.code
is typed against the platform's public error catalog (ErrorCode, exported)
with a string fallback, so known codes autocomplete while a newly published
code still type-checks.
