JavaScript SDK

JavaScript/TypeScript SDK for inference.sh agents and apps.

The JavaScript SDK provides a fully-typed client for running apps and chatting with agents.


Installation

bash
1npm install @inferencesh/sdk

The package is ES modules. Import it:

typescript
1import { inference } from '@inferencesh/sdk';2import type { Task, InferenceConfig } from '@inferencesh/sdk';

@inferencesh/sdk ≥ v0.23.0 loads in plain Node with no bundler: import works from Node ESM, and require() works from CommonJS on Node 22.12+ (see CommonJS support). This covers every subpath, @inferencesh/sdk/proxy/* included. Earlier releases failed in plain Node with ERR_MODULE_NOT_FOUND.

ImportPurpose
@inferencesh/sdkClient, types and helpers
@inferencesh/sdk/agentReact agent chat provider and hooks
@inferencesh/sdk/proxyShared server-proxy code (PROXY_ENDPOINTS, options types)
@inferencesh/sdk/proxy/nextjsNext.js App Router and Page Router handlers
@inferencesh/sdk/proxy/expressExpress middleware
@inferencesh/sdk/proxy/honoHono handler
@inferencesh/sdk/proxy/remixRemix loader/action handler
@inferencesh/sdk/proxy/svelteSvelteKit handler
@inferencesh/sdk/internal/uploadUpload helper used by the SDK (internal)

Type definitions ship with the package; there is no separate @types install.

Node.js: @inferencesh/sdk ≥ v0.9.0 requires Node.js 22.12.0 or later on the server (see npm engines). The release depends on eventsource 5.x for SSE; that package is ESM-only and needs Node 22.12+. Upgrade Node before bumping past v0.8.x if you are still on Node 18–22.11.

The client runs in the browser, but an API key must never reach client-side code. Put it behind a server proxy and point the browser at that.


Client API

The inference() client exposes namespaced APIs:

PropertyPurpose
client.tasksRun, get, cancel, delete, list, and stream tasks; files and deleteFiles for a task's input and output files; getLogs, getTimings, and getTelemetry for observability
client.filesUpload, list, get, and delete files
client.agentsChat via client.agent(); template CRUD, getByName, getA2ACard, listRunInterrupts, and resolveInterrupt
client.sessionsList, inspect, keepalive, and end sessions
client.chatsList, get, update, delete chats; getStatus, stop, stream, and getTrace
client.appsList, create, and manage deployed apps (requires app scopes)
client.flows / client.flowRunsFlow definitions and saved flow run lifecycle (flows:read / flows:execute)
client.enginesList, match resources, drain, updateBinary, stop, and restart private engines
client.knowledge / client.skillsKnowledge entries and skill CRUD
client.teamsWorkspace membership, invites, and me()
client.secretsWorkspace secret CRUD and reveal
client.apiKeysList, create, and delete API keys
client.credentialsConnect OAuth services, check requirements, MCP connections
client.searchUnified discovery (/suggest) and full-text search (/search)
client.projectsWorkspace project folders
client.mcpServersMCP directory browse, owned servers, and tool calls

Platform namespaces ship in @inferencesh/sdk ≥ v0.6.12. See SDK overview — client structure for Python parity.

client.run() and client.uploadFile() remain supported as legacy aliases for client.tasks.run() and client.files.upload().

Partial updates (PATCH)

@inferencesh/sdk ≥ v0.22.0: update() on client.projects, client.knowledge, client.skills, client.chats, client.engines and client.teams sends PATCH instead of POST. A field left out of the body keeps its value. An API that does not answer PATCH on these routes returns 405.

Typed write bodies (ProjectWriteBody, KnowledgeUpdateBody, TeamUpdateBody and the other *UpdateBody / *WriteBody types) name the fields each method sends. Spreading a loaded DTO into update() still type-checks; an explicit field the route does not write does not.

NamespacePartial-update helpers
client.appspatch(appId, AppPatchBody): PATCH the app's copy fields. update() stays POST, since it also edits the version in place
client.flowspatch(flowId, FlowPatchBody); saveViewport(flowId, FlowViewportBody) for the editor viewport
client.chatsupdateSettings(chatId, settings): name, visibility, tool approval, hooks, memory (POST /chats/{id}/settings)

client.flowRuns.update() is deprecated: the API refuses it, because a flow run has no caller-writable fields. Use client.flowRuns.updateVisibility() to change who can see a run.

Tool approvals (approveTool, rejectTool, alwaysAllowTool, resolveInterrupt) and widening a chat's approval settings need the account holder's own sign-in. With an API key, an app's token or the server proxy, the API answers 403 requires_sign_in (ErrorCodeRequiresSignIn).

→ Engines REST API — drain, safe binary updates, and cloud rental extensions
→ Flow Runs REST API — run saved flows programmatically

Apps API

Use client.apps to manage apps you own (not for running tasks — use client.tasks.run() for that):

typescript
1import {2  AppStatusActive,3  AppStatusDeprecated,4  AppStatusMaintenance,5  AppStatusRetired,6} from '@inferencesh/sdk';78const app = await client.apps.getByName('myteam/my-app');9const versions = await client.apps.listVersions(app.id);1011// Promote a staged deploy to current (same as the version picker in the web app)12await client.apps.setCurrentVersion(app.id, versions.items[0].id);1314// Mark deprecated while keeping runs working (see Apps REST API — lifecycle status)15await client.apps.updateStatus(app.id, AppStatusDeprecated, 'Use myteam/flux-v2 instead');1617// Temporarily block new runs18await client.apps.updateStatus(app.id, AppStatusMaintenance, 'Upgrading GPU drivers');1920// Permanently retire (excluded from store/search; historical tasks remain)21await client.apps.updateStatus(app.id, AppStatusRetired, 'Replaced by myteam/flux-v2');
MethodDescription
list, get, getByNameList and fetch apps
create, update, patch, delete, duplicateLifecycle (patch sends PATCH with copy fields only)
listVersions, getByVersionIdVersion history
setCurrentVersion(appId, versionId)PUT /apps/{id}/versions/{versionId}/current
updateStatus(appId, status, message?)POST /apps/{id}/status — lifecycle status
updateVisibility, transferOwnershipOwnership and visibility
getLicense, saveLicenseLicense records

Categories and tags — AppDTO includes category (AppCategory*, including AppCategoryDecision for decision models) and tags (string[]). Task payloads for decision apps use DecisionInput / DecisionOutput (and DecisionVisionInput when images are required) — same JSON shape as App code — Decision models; types ship in @inferencesh/sdk ≥ v0.21.0. Browseable tag slugs and display titles come from GET /store/tags (Store API), not from generated SDK constants — v0.21.0 removes the AppTag* / AppTagTitle* constants that older releases exported. Pass tags on client.apps.create to replace the app's tag list; omit it on deploy to inherit from version metadata. Tag rules: Apps API — App tags.

Store browse metadata — @inferencesh/sdk ≥ v0.21.0 exports StoreCategoryDTO and StoreTagDTO for Store API GET /store/categories and List browseable tags GET /store/tags. There is no client.store helper yet; call the public endpoints with client.http.request (no API key required):

typescript
1import type { StoreCategoryDTO, StoreTagDTO } from '@inferencesh/sdk';23const { data: categories } = await client.http.request<StoreCategoryDTO[]>(4  'get',5  '/store/categories',6);7const { data: tags } = await client.http.request<StoreTagDTO[]>('get', '/store/tags');

→ Apps REST API — lifecycle status · Deploy staged versions · Instance types REST API

Flows API

Use client.flows for flow definitions and client.flowRuns to execute saved flows:

typescript
1const flow = await client.flows.create('image-pipeline');2await client.flows.updateVisibility(flow.id, 'public');34const run = await client.flowRuns.create(flow.id, { text: 'hello' });
MethodDescription
list, get, create, update, patch, deleteFlow definition CRUD (patch writes only the fields sent)
saveViewport(flowId, body)POST /flows/{id}/viewport
listVersions, duplicate, createAppVersions, copy, deploy as app
updateVisibility(flowId, visibility)POST /flows/{id}/visibility
transferOwnership(flowId, teamId)POST /flows/{id}/transfer
stream(flowId)SSE for editor graph updates

→ Flows REST API · Flow Runs REST API

Search API

Use client.search for programmatic discovery. It calls the same endpoints as belt suggest and the web app's search bar:

typescript
1const { results } = await client.search.suggest({2  query: 'flux image',3  context: 'user asked about image models', // optional POST /suggest context; query is embedded first, then context4  scope: ['git:github.com/acme/design', 'lang:typescript'], // optional; environment signals for ranking5  limit: 5,6});7// Result descriptions are clipped to DescriptionLimitListing (200 characters).89for (const hit of results) {10  console.log(`[${hit.type}] ${hit.name} — ${hit.command}`);11}1213// Full-text search in a single collection14const apps = await client.search.search({ q: 'transcribe', type: 'apps', limit: 10 });
MethodDescription
suggest(params)POST /suggest — ranked discovery across apps, skills, knowledge, and docs
search(params)POST /search — full-text search with optional type filter

→ Search REST API · Search concept

Workspace APIs

Manage workspace context, secrets, API keys, and project folders programmatically:

typescript
1const { data: { user, team } } = await client.teams.me();2const { data: teams } = await client.teams.list();34await client.secrets.create({5  key: 'OPENAI_API_KEY',6  value: 'sk-...',7  description: 'OpenAI',8});910const { data: { items: keys } } = await client.apiKeys.list({ limit: 50 });11const { data: newKey } = await client.apiKeys.create({12  name: 'deploy-bot',13  scopes: ['tasks:read', 'tasks:write'],14  scope: 'workspace', // optional: 'user' (default) or 'workspace'; a workspace key needs manage_keys15  expires_at: '2026-12-31T00:00:00Z', // optional16});1718const project = await client.projects.create({ name: 'Production' });
NamespaceKey methodsREST reference
client.teamsme, list, get, create, getMembers, addMember, invite helpersWorkspaces
client.secretslist, create, update, reveal, deleteSecrets
client.apiKeyslist, create, deleteAPI Keys
client.projectslist, get, create, update, delete—

Knowledge and skills

Use client.knowledge for observations, references, and other knowledge types. Use client.skills for skill packages (prefer over knowledge entries with type: "skill"):

typescript
1const entry = await client.knowledge.create({2  name: 'deployment-order',3  description: 'Services must deploy in dependency order',4  type: 'observation',5  version: {6    content: {7      path: 'content.md',8      content: '# Deployment Order\n\nDeploy common-go before api.',9    },10    scope: ['git:github.com/acme/api', 'lang:go'], // optional; see Knowledge API — Scoping11  },12});1314const skill = await client.skills.getByName('myteam', 'code-review');15const content = await client.skills.getContent('myteam', 'code-review');16const { items } = await client.skills.listStore({ limit: 20 });

→ Knowledge REST API · Skills REST API · Skill registry

Credentials and MCP servers

Connect OAuth services and call MCP connector tools with client.credentials (CredentialsAPI). v0.8.0 removed the integration-named SDK aliases (client.integrations, IntegrationsAPI, Integration* types). Upgrade from v0.7.x before bumping.

typescript
1const caps = await client.credentials.getCapabilities();2const { auth_url } = await client.credentials.connect({3  provider: 'google',4  type: 'oauth',5  scopes: ['https://www.googleapis.com/auth/spreadsheets'],6});78await client.credentials.checkRequirements({9  secrets: [{ key: 'OPENAI_API_KEY' }],10  credentials: [{ key: 'google.sheets' }],11});1213const linear = await client.mcpServers.get('linear');14const tools = await client.mcpServers.listTools('linear');15const result = await client.mcpServers.callTool('linear', 'list_issues', { limit: 10 });

→ Credentials REST API · MCP servers REST API · Connectors overview

Chats and task observability

Inspect agent runs and task execution outside the high-level client.agent() helper:

typescript
1const { data: chat } = await client.chats.get('chat_abc123');2const { data: { status } } = await client.chats.getStatus('chat_abc123');3const { data: trace } = await client.chats.getTrace('chat_abc123');4await client.chats.stop('chat_abc123');56const { data: logs } = await client.tasks.getLogs('task_abc123');7const { data: timings } = await client.tasks.getTimings('task_abc123');8const { data: samples } = await client.tasks.getTelemetry('task_abc123');

→ Agents REST API · Tasks REST API · Observability

Task files

@inferencesh/sdk ≥ v0.20.0 adds client.tasks.files(), client.tasks.deleteFiles(), and client.tasks.delete(taskId, { files: true }). Earlier releases delete the task record only.

typescript
1import type { TaskFileDTO, DeleteTaskFilesResponse } from '@inferencesh/sdk';2import { TaskFileRoleInput, TaskFileRoleOutput } from '@inferencesh/sdk';34const { data: files } = await client.tasks.files('task_abc123');5const { data: inputs } = await client.tasks.files('task_abc123', TaskFileRoleInput);67const { data: result } = await client.tasks.deleteFiles('task_abc123', TaskFileRoleOutput);8// result.deleted, result.skipped910await client.tasks.delete('task_abc123', { files: true });
MethodRESTNotes
files(taskId, role?)GET /tasks/{id}/filesrole is 'input' or 'output'
deleteFiles(taskId, role?)DELETE /tasks/{id}/filesTask must be finished
delete(taskId, { files: true })DELETE /tasks/{id}?files=trueDeletes the files, then the task

→ List task files · Delete task files · Delete task

Private engines

For cloud engine rentals and private hardware:

typescript
1const { data: { items } } = await client.engines.list({ limit: 20 });2const { data: engine } = await client.engines.get('eng_abc123');3console.log(engine.engine_version); // installed inferencesh-engine binary, e.g. "v1.10.15"45await client.engines.drain('eng_abc123');6await client.engines.updateBinary('eng_abc123'); // POST /engines/{id}/update7await client.engines.extend('eng_abc123');89// Cloud engines include nested instance pricing (spot vs on-demand)10if (engine.instance?.rental_type === InstanceRentalTypeSpot) {11  console.log('Spot price (cents/hr):', engine.instance.hourly_price);12}

@inferencesh/sdk ≥ v0.14.2 adds InstanceRentalType (on_demand, spot) on catalog availability rows, engine-picker offers from GET /engines/types, and engine.instance on EngineDTO. Spot cloud engines can be reclaimed by the provider at any time — see Instance types.

→ Engines REST API · Managing engines · Instance types


Running Apps

Basic Usage

typescript
1import { inference } from '@inferencesh/sdk';23const client = inference({ apiKey: 'inf_your_key' });45// Run and wait for completion (default)6const result = await client.run({7  app: 'infsh/echo',8  input: { message: 'Hello from JavaScript!' }9});1011console.log(result.output);

With Progress Updates

typescript
1const result = await client.run(2  { app: 'infsh/flux-schnell', input: {...} },3  {4    onUpdate: (update) => {5      console.log(`Status: ${update.status}`);6      if (update.logs?.length) {7        console.log(update.logs[update.logs.length - 1]);8      }9    }10  }11);

Fire and Forget

typescript
1const task = await client.run(2  { app: 'infsh/slow-task', input: {...} },3  { wait: false }4);5console.log(`Task ID: ${task.id}`);

POST /apps/run returns a slim task result (id, status, output, and optional socket for streaming functions) — typed as TaskResultDTO on client.tasks.create(). @inferencesh/sdk v0.14.0+: client.run(..., { wait: false }) (and client.tasks.run) performs GET /tasks/{id} before returning, so the value is a full Task (input, logs, session_id, and other fields when the server sends them). Use create() when you only need the run acknowledgement; call client.tasks.get(id) yourself if you want the full record without starting a watch.


Agent SDK

Chat with AI agents programmatically.

Using an Existing Agent

Reference agents created in the web app:

typescript
1import { inference } from '@inferencesh/sdk';23const client = inference({ apiKey: 'inf_your_key' });45// Use an agent you created in the web app6const agent = client.agent('my-org/my-agent@abc123'); // Format: namespace/name@version78const response = await agent.sendMessage('Hello!');9console.log(response);

Template agents that declare context fields take values on the first message. The agent's tools use them as {{context.NAME}}:

typescript
1const agent = client.agent('my-team/pricing-agent@latest', {2  context: { version_id: 'v1', app_id: 'app_123' },3});

With AgentChatProvider, pass context on agentConfig — see Template agents — per-chat context.

Chat settings and switching agents

When a chat is already bound (for example you passed chatId to AgentChatProvider or the provider created one on the first message), useAgentActions() can change per-chat settings or hand the conversation to another template agent without starting a new chat.

typescript
1import { AgentChatProvider, useAgentActions } from '@inferencesh/sdk/agent';2import type { ChatSettingsRequest } from '@inferencesh/sdk';34function ChatControls() {5  const { updateChatSettings, switchAgent } = useAgentActions();67  const rename = () =>8    updateChatSettings({ name: 'Incident triage', visibility: 'team' });910  const clearStaleMemory = () =>11    updateChatSettings({ forget_memory: ['stale_goal'] });1213  const handOff = () => switchAgent('my-team/escalation-agent@latest');1415  // ...16}
MethodRESTNotes
updateChatSettings(settings)POST /chats/:chatId/settingsPartial update — omit fields you do not want to change. Includes forget_memory to drop keys from agent_data.memory. Resolves to ChatSettingsDTO.
switchAgent(agentRef)POST /chats/:id/agentNext run uses the new agent's tools and system prompt. Only between agents on the platform loop — not onto/off remote harness agents. Resolves to ChatAgentDTO.

Both methods no-op without a bound chatId (they log to the console). On failure they dispatch SET_ERROR and rethrow so your onError callback can surface the message.

updateChatSettings and switchAgent ship in @inferencesh/sdk ≥ v0.15.0. @inferencesh/sdk ≥ v0.17.0: The API answers with ChatSettingsDTO and ChatAgentDTO (only the fields those endpoints change), not a full ChatDTO. AgentChatProvider merges them into useAgentChat().chat and leaves messages unchanged; responses for a different chat_id are ignored. Low-level updateChatSettings / setAgent in the agent module return the same DTOs. If you call the REST endpoints yourself, merge the same fields onto agent_data / agent refs instead of replacing your chat object or message list.

ChatDTO.chat_messages is optional in generated types because chat metadata endpoints never load messages — use GET /chats/:id/messages or useAgentChat().messages.

→ Update chat settings · Set agent

Scoped always allow and explain

Requires @inferencesh/sdk ≥ v0.16.0. When a platform loop tool pauses with awaiting_approval, the API suggests scoped always-allow choices (exact command, prefix, whole remote, whole tool, …). The React Agent SDK loads those options, saves the one the person picks, and approves the current call. Call explainTool only when the person asks for a plain-language summary (not on every render).

typescript
1import { useAgentActions } from '@inferencesh/sdk/agent';23const {4  approveTool,5  rejectTool,6  getAlwaysAllowOptions,7  alwaysAllowTool,8  explainTool,9} = useAgentActions();1011async function onApprovalPrompt(toolInvocationId: string) {12  const options = await getAlwaysAllowOptions(toolInvocationId);13  if (!options?.options.length) {14    console.warn(options?.unavailable ?? 'No always-allow choices');15    return;16  }1718  // options.default is the narrowest key that would auto-approve this call again19  const key = options.default ?? options.options[0].key;20  try {21    const result = await alwaysAllowTool(toolInvocationId, { option: key });22    console.log('Saved chat rules:', result?.rules);23  } catch (err) {24    // 409: option stale — re-fetch getAlwaysAllowOptions and try again25    // 400: call no longer awaiting approval26    throw err;27  }28}2930async function onExplainClick(toolInvocationId: string) {31  const { risk_level, explanation, reasoning, risk } = await explainTool(toolInvocationId);32  console.log(risk_level, explanation, reasoning, risk);33}
MethodREST endpoint
getAlwaysAllowOptions(toolInvocationId)GET /chats/{chatId}/tools/{toolInvocationId}/always-allow/options
alwaysAllowTool(toolInvocationId, { option })POST /chats/{chatId}/tools/{toolInvocationId}/always-allow
explainTool(toolInvocationId)POST /chats/{chatId}/tools/{toolInvocationId}/explain
approveTool / rejectToolPOST /tools/{toolInvocationId}/invoke · POST /tools/{toolInvocationId}/reject

alwaysAllowTool resolves to AlwaysAllowResultDTO (saved rules and the chosen option). Passing a string as the second argument is still accepted from older integrations (it was the tool name) and means “use the API default option.” On 409 or 400, the SDK rethrows without putting the chat in an error state so you can refresh options or show why always-allow is unavailable.

getAlwaysAllowOptions returns null when no chat is bound yet. explainTool is not available on harness-backed chats (no platform model on the harness).

Exported types: AlwaysAllowOptionsDTO, AlwaysAllowOptionDTO, AlwaysAllowScope, AlwaysAllowRequest, AlwaysAllowResultDTO, AlwaysAllowChoice, PolicyRuleDTO, ToolExplanationDTO, ToolRiskLevel.

Policy kinds and agent permissions (@inferencesh/sdk ≥ v0.18.0) — saved always-allow choices and workspace policy rows share PolicyRuleDTO. kind is a typed PolicyKind (import the PolicyKind* constants instead of raw strings):

ConstantGoverns
PolicyKindRemoteExecShell commands on a remote (remote_exec)
PolicyKindWorkspaceFolders on a remote
PolicyKindHarnessHarness tool approvals (for example remote_session_resolve)
PolicyKindToolPlatform-loop tool calls by tool name
PolicyKindWebFetchOutbound HTTP fetch domains
PolicyKindApp, PolicyKindAgent, PolicyKindKnowledge, PolicyKindMcp, PolicyKindFlowUsage policy (resolved resource ids)
typescript
1import type { AgentPermissions, PolicyRuleDTO } from '@inferencesh/sdk';2import { PolicyKindRemoteExec, PolicyKindTool } from '@inferencesh/sdk';34function describeRule(rule: PolicyRuleDTO): string {5  if (rule.kind === PolicyKindRemoteExec) {6    return `remote command: ${rule.specifier}`;7  }8  if (rule.kind === PolicyKindTool) {9    return `tool: ${rule.specifier}`;10  }11  return `${rule.kind} ${rule.specifier}`;12}1314// Agent template default for new chats (YAML `version.permissions` / AgentDTO)15const defaults: AgentPermissions = { allow_all_tools: true };

AgentPermissions (allow_all_tools today) is on agent create/update payloads as permissions. It seeds agent_data.allow_all_tools when a chat is created; team and org policy can still ask or deny over it. See Allow all tools on new chats (agent template).

@inferencesh/sdk ≥ v0.18.0 removed ChatData.always_allowed_tools from generated types — the API stores always-allow as chat rules. Read result.rules from alwaysAllowTool, call GET /chats/{id}/rules, or use the workspace Permissions pane. Legacy chats may still return always_allowed_tools on the wire until migrated; the REST docs describe synthesized rule rows.

→ List always-allow options · Explain tool call · Chat permissions rules · Approval settings

A2A agent card

Fetch the Agent2Agent (A2A) protocol card for marketplace listings (for example GCP AI Agents Producer Portal). Requires agents:read.

typescript
1const card = await client.agents.getA2ACard('agent_abc123');2// or by namespace/name ref via GET /agents/{namespace}/{name}/card

Returns raw A2A JSON (not wrapped in { data: … }). See Agents REST API — Get A2A agent card.

List internal tool definitions

Fetch the built-in internal tool categories for agent configuration UIs. Requires agents:read.

typescript
1const { data: categories } = await client.agents.getInternalTools();23for (const category of categories) {4  // default_enabled is false for opt-in categories5  console.log(category.id, category.default_enabled);6}

Each InternalToolDefinition includes id, name, description, tools, scope, and default_enabled (what the runtime applies when that category's flag is unset in internal_tools). See Internal Tools: Default behavior and Agents REST API: List internal tool definitions.

Creating Ad-Hoc Agents

Create agents on-the-fly without saving to workspace:

typescript
1import { inference, tool, string, internalTools } from '@inferencesh/sdk';23const client = inference({ apiKey: 'inf_your_key' });45// Define tools6const calculator = tool('calculator')7  .describe('Performs math operations')8  .param('expression', string('Math expression'))9  .build();1011// Create ad-hoc agent12const agent = client.agent({13  core_app: { ref: 'infsh/claude-sonnet-4@latest' },14  name: 'Math Assistant',15  system_prompt: 'You help with math. Use the calculator tool.',16  tools: [calculator],17  internal_tools: internalTools().memory().build(),18});1920const response = await agent.sendMessage('What is 42 * 17?');

Streaming Responses

Tokens arrive through onDelta while the assistant message is being written; onMessage delivers each message as a whole once it is stored. onDelta is streaming-only: with stream: false the message arrives whole through onMessage. Pass a signal (an AbortSignal) to stop waiting for the turn; the agent keeps running server-side, stopChat() cancels it.

typescript
1await agent.sendMessage('Explain quantum computing', {2  onDelta: (d) => {3    // d.delta is this batch, d.output everything so far for d.messageId4    process.stdout.write(d.delta.response ?? '');5  },6  onMessage: (msg) => {7    if (msg.content) {8      for (const c of msg.content) {9        if (c.type === 'text' && c.text) {10          process.stdout.write(c.text);11        }12      }13    }14  },15  onToolCall: async (call) => {16    console.log(`\n[Tool: ${call.name}]`);17    // Execute the tool and submit its result string18    const result = await executeMyTool(call.name, call.args);19    await agent.submitToolResult(call.id, result);2021    // A result can carry files uploaded first with client.files.upload:22    // const ref = await client.files.upload(pngBlob);23    // await agent.submitToolResult(call.id, 'Screenshot attached.', [ref]);24    25    // For widget interactions, you can also pass action + form_data:26    // await agent.submitToolResult(call.id, {27    //   action: { type: 'confirm' },28    //   form_data: { name: 'John', email: '[email protected]' }29    // });30  },31});

File Attachments

typescript
1// From Blob2const imageBlob = new Blob([imageBuffer], { type: 'image/png' });3const response = await agent.sendMessage('What\'s in this image?', {4  files: [imageBlob]5});67// From data URI8const response = await agent.sendMessage('Analyze this', {9  files: ['data:image/png;base64,iVBORw0KGgo...']10});1112// Reuse an uploaded FileRef without re-uploading13const ref = await agent.uploadFile(imageBlob, { filename: 'photo.png' });14await agent.sendMessage('Summarize this', { files: [ref] });

client.agent().sendMessage() uploads the files and sends their URIs on POST /agents/run as input.images / input.files. AgentChatProvider / useAgentActions().sendMessage() create the chat, then post the uploaded refs as attachments on POST /chats/{id}/messages. @inferencesh/sdk ≥ v0.22.1 sends them; earlier releases uploaded the files but posted only the text, so the agent never saw them.

Channel routing (channel_context)

channel_context on agent.sendMessage() and agent.run() is deprecated. @inferencesh/sdk 0.23.0 accepts the option and does not send it, and the API ignores it on runs. A chat's reply routing (Slack thread, Telegram chat, …) comes only from messages that arrive through the platform's channel integrations.

Read the routing of a channel-originated chat from getChat():

typescript
1const chat = await agent.getChat();2console.log(chat?.channel_context?.channel_type); // "slack"

See Agents REST API: Channel context.

Structured output (agent.run)

When the agent uses the finish tool, call agent.run() to send a message, wait for completion, and receive the parsed chat.output payload:

typescript
1const output = await agent.run('Summarize this document in JSON');2console.log(output);

For manual control (streaming, tool callbacks), use sendMessage() and read chat.output from getChat() instead.

→ Structured output

Chat Management

typescript
1// Get current chat2const chat = await agent.getChat();3console.log(`Chat ID: ${chat?.id}`);4console.log(`Messages: ${chat?.messages.length}`);56// Stop generation7await agent.stopChat();89// Start fresh10agent.reset();1112// Cleanup streams13agent.disconnect();

Interrupt gates

When an agent run pauses for tool approval, auth, widgets, or a lifecycle hook gate, list and resolve interrupts:

typescript
1import { InterruptResourceHookEvent, InterruptResourceToolInvocation } from '@inferencesh/sdk';23// List pending interrupts for a run (GET /agent-runs/{runId}/interrupts)4const { data: pending } = await client.agents.listRunInterrupts('ar_abc123');56for (const interrupt of pending ?? []) {7  if (interrupt.resource_type === InterruptResourceHookEvent) {8    console.log('Hook gate:', interrupt.meta);9  }10}1112// Resolve with allow or deny (POST /interrupts/{id}/resolve)13const { data: resolved } = await client.agents.resolveInterrupt('int_abc123', 'allow');1415// From the Agent SDK helper (same endpoints)16await agent.resolveInterrupt('int_abc123', 'deny');

When a run is paused, client.chats.get() includes active_run.interrupt_reason, interrupt_tool_id, and interrupt_meta. See Agents REST API — Interrupt gates.

MethodREST endpoint
client.agents.listRunInterrupts(runId)GET /agent-runs/{runId}/interrupts
client.agents.resolveInterrupt(id, decision)POST /interrupts/{id}/resolve
agent.resolveInterrupt(id, decision)Same as above

→ CLI — Interrupt · Python SDK — Interrupt gates

MCP input requests (MRTR)

When an agent calls a remote MCP tool that needs extra user input mid-flight, the runtime parks the tool invocation in awaiting_input and stores an MCPInputState object in the invocation’s data field (input_required, input_requests, optional request_state, and round). The same Multi Round-Trip Request (MRTR) flow applies whether you call MCP tools directly or through an agent.

@inferencesh/sdk ≥ v0.12.0 adds helpers under @inferencesh/sdk/agent and submitMCPInput on the agent client and useAgentActions() hook. Import ElicitResult (and optional ElicitActionAccept / ElicitActionDecline / ElicitActionCancel) from @inferencesh/sdk for typed responses.

typescript
1import type { ToolInvocationDTO } from '@inferencesh/sdk';2import {3  parseMCPInputState,4  elicitParams,5  isURLElicitation,6  useAgentActions,7} from '@inferencesh/sdk/agent';89function MCPInputForm({ invocation }: { invocation: ToolInvocationDTO }) {10  const { submitMCPInput } = useAgentActions();11  const state = parseMCPInputState(invocation.data);12  if (!state) return null;1314  async function submitLogin() {15    await submitMCPInput(invocation.id, {16      login: { action: 'accept' },17    });18  }1920  return (21    <>22      {Object.entries(state.input_requests).map(([key, request]) => {23        const params = elicitParams(request);24        if (!params) return null;25        if (isURLElicitation(params)) {26          return (27            <a key={key} href={params.url} target="_blank" rel="noreferrer">28              {params.message}29            </a>30          );31        }32        // Render a form from params.requestedSchema, then:33        // await submitMCPInput(invocation.id, { [key]: { action: 'accept', content: { field: value } } });34        return <p key={key}>{params.message}</p>;35      })}36      <button type="button" onClick={submitLogin}>37        Continue38      </button>39    </>40  );41}

Standalone client.agent(...) callers use the same API:

typescript
1const agent = client.agent('my-org/support@latest');23// After you locate the awaiting_input MCP invocation on a message:4await agent.submitMCPInput('tinv_abc123', {5  step_a: { action: 'accept', content: { email: '[email protected]' } },6  step_b: { action: 'decline' },7});

Under the hood, submitMCPInput posts the JSON string from buildMCPInputResult(responses) via the same REST path as submitToolResult (POST /tools/{id}). You must include one ElicitResult per key in input_requests. If the server rejects the payload (HTTP 400, for example a missing key), the SDK rethrows the error without marking the chat connection failed so your UI can show validation next to the form. Other failures behave like submitToolResult (connection error + onError).

HelperPurpose
parseMCPInputState(data)Parse invocation data (object or JSON string) into MCPInputState, or null
elicitParams(request)Read elicitation/create params (mode, message, requestedSchema, url)
isURLElicitation(params)true for URL/OAuth steps (mode: 'url' or a url with no explicit form mode)
buildMCPInputResult(responses)Serialize { [key]: ElicitResult } for custom UIs that call submitToolResult directly

A2UI surfaces (Widget, McpApp)

Interactive agent UI is an A2UI surface on ToolInvocationDTO.widget. The SDK exports Widget as an alias of A2UISurface, plus component types such as A2UIMcpApp ("McpApp") and A2UIForm ("Form").

MCP Apps embeds use A2UIComponent fields mcpPageHash, mcpHtml, mcpCsp, mcpServerSlug, mcpCredentialId, mcpToolName, mcpToolInput, and mcpToolResult. When mcpPageHash is set, the host loads the page from GET /mcp-ui-pages/{hash} so repeated tool runs in one chat share one stored copy. mcpHtml carries the document inline — for example artifact pages, messages written before hash storage, or pages that could not be stored. When mcpArtifactId is set, the host serves the page through artifact data endpoints instead of the MCP proxy.

→ Connector tools — MCP Apps UI

→ Using tools — Interactive elicitation · Connector tools


Tool Builder

Define tools with the fluent API:

typescript
1import {2  tool, appTool, agentTool, callTool, httpTool, mcpTool, webhookTool, internalTools,3  string, number, integer, boolean, enumOf, array, object, optional,4  CredentialProviderGoogle,5} from '@inferencesh/sdk';

httpTool is an alias for callTool (authenticated HTTP). webhookTool is legacy for unsigned webhooks. Use mcpTool for connector (MCP) tools.

Typed constants (v0.6.8+)

The SDK exports typed string constants for credentials, instances, and tool schemas:

typescript
1import {2  CredentialProviderMCP,3  CredentialStatusConnected,4  EngineStatusDisconnected,5  CloudAWS,6  InstanceStatusRunning,7  InstanceRentalTypeOnDemand,8  InstanceRentalTypeSpot,9  ToolParamTypeString,10  ToolParamTypeObject,11  ToolAuthTypeNone,12  ToolAuthTypeCredential,13  ToolAuthTypeBearer,14  ToolAuthTypeAPIKey,15} from '@inferencesh/sdk';

Use these instead of raw strings when building credential clients or validating tool parameter types.

Entitlements — use with the Entitlements REST API:

typescript
1import {2  ResourceConcurrency,3  ResourceFeatureSeedance,4  EntitlementTypeLimit,5  EntitlementSourceTier,6  type EntitlementDTO,7  type EntitlementErrorMeta,8} from '@inferencesh/sdk';910const ent: EntitlementDTO = {11  resource: ResourceConcurrency,12  type: EntitlementTypeLimit,13  limit: 3,14  source: EntitlementSourceTier,15};

Entitlement error metadata — EntitlementErrorMeta mirrors the meta object on limit_exceeded and feature_not_available responses. Parse it from InferenceError.responseBody:

typescript
1import { InferenceError, type EntitlementErrorMeta } from '@inferencesh/sdk';23try {4  await client.tasks.run({ app: 'my-app', input: {} });5} catch (e) {6  if (e instanceof InferenceError) {7    const body = JSON.parse(e.responseBody);8    const meta: EntitlementErrorMeta = body.meta ?? body.error?.meta ?? {};9    if (meta.upgrade_available) {10      console.log(meta.resource_label, meta.addon_plan_name);11    }12  }13}

See Entitlements API — Error metadata for field definitions.

Constant (feature gates)API value
ResourceFeatureBYOKfeature:byok
ResourceFeatureSeedancefeature:seedance
ResourceFeatureWebhooksfeature:webhooks
ResourceFeaturePublishAppsfeature:publish_apps

See Entitlements API — Resources for the full list.

typescript
1// Client tool (runs in your code)2const search = tool('search_files')3  .describe('Search for files')4  .param('pattern', string('Glob pattern'))5  .param('recursive', optional(boolean('Search subdirs')))6  .build();78// App tool (calls another inference app)9const generate = appTool('generate', 'infsh/flux-schnell@latest')10  .describe('Generate an image')11  .param('prompt', string('Image description'))12  .build();1314// Agent tool (delegates to sub-agent)15const researcher = agentTool('research', 'my-org/researcher@v1')16  .describe('Research a topic')17  .param('topic', string('Topic'))18  .build();1920// Authenticated HTTP (callTool and httpTool are aliases)21const getData = httpTool('get_data', 'https://api.example.com/data')22  .describe('Fetch data from the API')23  .method('GET')24  .auth({ bearer: 'my_api_key' })25  .build();2627// OAuth: runtime injects the caller's own connected account's access token28const gmailSend = httpTool('gmail_send', 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send')29  .describe('Send email via Gmail')30  .method('POST')31  .auth({ credential: CredentialProviderGoogle })32  .build();3334// Webhook tool (calls external API)35const notify = webhookTool('slack', 'https://hooks.slack.com/...')36  .describe('Send Slack message')37  .secret('SLACK_SECRET')38  .param('message', string('Message'))39  .build();4041// Internal tools config42const internals = internalTools().memory().plan().build();4344// Meta tools are opt-in (search_tools, list_tools, execute_tool)45const withMeta = internalTools().meta().build();

→ Full Tool Builder Reference


TypeScript Types

The SDK is fully typed. Import status constants and DTOs from @inferencesh/sdk.

DescriptionLimitListing (200) and DescriptionLimitSkill (1024) are generated constants for validating writes (@inferencesh/sdk ≥ v0.20.0); see description length limits.

typescript
1import type {2  TaskDTO,3  ChatDTO,4  ChatMessageDTO,5  ChannelContext,6  AgentTool,7  AppStatus,8  TaskStatusCompleted,9  TaskStatusFailed,10} from '@inferencesh/sdk';11import {12  AppStatusActive,13  AppStatusDeprecated,14  AppStatusMaintenance,15  AppStatusRetired,16} from '@inferencesh/sdk';1718// Check task status19if (result.status === TaskStatusCompleted) {20  console.log('Done!');21} else if (result.status === TaskStatusFailed) {22  console.log('Failed:', result.error);23}2425// App lifecycle status (on AppDTO.status)26if (app.status === AppStatusDeprecated) {27  console.log(app.status_message);28}

Generated types for REST integrations

OpenAPI-aligned types ship in @inferencesh/sdk for endpoints you call directly with fetch or when building consent UIs. See the Python SDK generated types for parity.

Current user (GET /me) — import MeResponse for the envelope returned by GET /me. team is a full TeamDTO (not TeamRelationDTO from GET /teams). Settings gates use team_view.can and team_view.org.can:

typescript
1import type { MeResponse } from '@inferencesh/sdk';23const { data: me } = await client.teams.me(); // Response<MeResponse>4const caps = me.team_view?.can ?? [];

Run acknowledgement (POST /apps/run) — import TaskResultDTO for the JSON body from POST /apps/run (and from client.tasks.create()). It is not a full task document; fetch or watch with client.tasks.get() / client.tasks.watch() when you need input, logs, or session_id.

Workspaces (GET /teams, GET /teams/{id}) — @inferencesh/sdk v0.14.0+: client.teams.list(), get(), create(), and update() return TeamDTO (including your role and usage_policy_id on list/get). GET /me still wraps a full TeamDTO in MeResponse.team as above.

Credential types were named Integration* until 2026-09; SDK 0.8.0 removed the old names. Use the Credential* types and client.credentials.

Credential configs: merged setup view from GET /credentials/configs. It returns the provider catalog, connection state, and connected MCP servers. One call is enough for connect/disconnect UIs. See Credential configs.

typescript
1import type { CredentialConfigDTO, CredentialDTO } from '@inferencesh/sdk';2import { CredentialScopeUser } from '@inferencesh/sdk';34function connectLabel(config: CredentialConfigDTO): string {5  if (config.connection_scope === CredentialScopeUser) {6    return `connect your ${config.name} account`;7  }8  return `connect ${config.name}`;9}1011function listSubtitle(credential: CredentialDTO): string {12  const accountId = credential.account_identifier ?? '';13  if (credential.scope === CredentialScopeUser) {14    return `${accountId} (your account)`;15  }16  return accountId;17}1819const perUserOAuth: CredentialConfigDTO = {20  slug: 'x',21  provider: 'x',22  type: 'oauth',23  name: 'X',24  short_name: 'X',25  description: '',26  allows_byok: false,27  available: true,28  has_managed: true,29  connection_scope: CredentialScopeUser,30};
ConstantAPI valueMeaning
CredentialGrantCredentialscredentialsOAuth application row (client id/secret and related app keys), not a login
CredentialGrantTokentokenConnection row: OAuth login, API key, service account, or MCP authorization
CredentialScopeUseruserPersonal connection. Use it for scope-aware list labels, section headings, and disconnect copy.
CredentialScopeTeamteamShared workspace connection
CredentialScopeOrgorgShared across all workspaces of an org. Connecting one requires an org admin, from the organization workspace.
CredentialScopePlatformplatformPlatform-managed credentials
CredentialScopeAgentagentAgent-scoped credential

CredentialConfigDTO exposes connection_scope and optional nested app (OAuth application) instead of grant. grant and app_credential_id appear on CredentialDTO — use grant to tell app rows (credentials) from logins (token). scope on CredentialDTO indicates who owns the connection. CredentialConfigDTO.credential holds the connected login; CredentialConfigDTO.app holds the OAuth app when configured. See Managed credentials and grant type.

Flow utility nodes — use with the Flows REST API when calling POST /flows/{id}/actions:

typescript
1import type { UtilityConfig } from '@inferencesh/sdk';23const gate: UtilityConfig = {4  preset: 'gate',5  gate: { field: 'status', operator: 'equals', value: 'approved' },6};78const selector: UtilityConfig = {9  preset: 'selector',10  selector: { field: 'items', mode: 'first' },11};1213// node.add payload: { id, type: 'gate', position, data: { utility: gate } }
TypeDescription
FlowNodeDataPer-node graph data. Has utility plus the legacy gate_condition and selector_config
UtilityConfigpreset (gate, selector, merge, constant), optional expression, gate, selector, constant, random, random_min, random_max
GateConditionfield, operator (equals, not_equals, contains, gt, lt, is_empty, is_not_empty), value
SelectorConfigfield, mode (first, last, index), optional index

Flow run progress — use with Flow Runs REST API: Per-node progress:

typescript
1import {2  GraphNodeStatusBlocked,3  GraphNodeStatusFailed,4  GraphNodeStatusRunning,5} from '@inferencesh/sdk';67const { data: run } = await client.flowRuns.get('flowrun_abc123');89// A gate whose condition is false is reported as failed; its dependents are blocked10if (run.node_statuses?.gate_1 === GraphNodeStatusFailed) {11  console.log('Gate condition was false');12}13if (run.node_statuses?.upscale === GraphNodeStatusBlocked) {14  console.log('Upscale did not run');15}16if (run.node_statuses?.generate === GraphNodeStatusRunning) {17  console.log('Still running:', run.node_tasks.generate?.task_id);18}1920console.log(run.node_outputs?.selector_1?.value);
Type / constantDescription
FlowRunDTOFlow run response. Includes node_tasks, optional node_statuses, and node_outputs
GraphNodeStatusPer-node status string: pending, ready, running, completed, failed, cancelled, skipped, blocked
GraphNodeStatusPending, GraphNodeStatusReady, GraphNodeStatusRunning, GraphNodeStatusCompleted, GraphNodeStatusFailed, GraphNodeStatusCancelled, GraphNodeStatusSkipped, GraphNodeStatusBlockedConstants for each status value

Decision app types

Apps with category: decision exchange the same JSON on task runs. Import the generated types when you build or parse payloads in TypeScript:

typescript
1import type {2  DecisionInput,3  DecisionOutput,4  DecisionVisionInput,5  DecisionChoiceQuestion,6  DecisionScoreQuestion,7  DecisionNoulQuestion,8} from '@inferencesh/sdk';910const input: DecisionInput = {11  state: { summary: 'User cannot log in after password reset' },12  choices: [13    {14      id: 'root_cause',15      instructions: 'Most likely cause',16      options: [17        { name: 'cache', description: 'Stale session cache' },18        { name: 'identity', description: 'IdP sync delay' },19      ],20    },21  ],22};2324// After client.tasks.create / poll — cast or validate result.output25const output = result.output as DecisionOutput;26console.log(output.choices.root_cause.choice, output.choices.root_cause.probabilities);
TypeRole
DecisionInputstate plus optional choices, scores, nouls question arrays
DecisionVisionInputDecisionInput plus images (file URIs visible to every question)
DecisionChoiceQuestion / DecisionChoiceOptionFixed options; answers keyed by option name
DecisionScoreQuestionOrdered levels (low → high); level numbers are array indices
DecisionNoulQuestion / DecisionNoulCriteriaYes/no probability; optional criteria.true / criteria.false labels
DecisionOutputchoices, scores, nouls maps keyed by question id, plus model and input_tokens
DecisionChoiceAnswerchoice, confidence, probabilities
DecisionScoreAnswerscore, normalized, confidence, probabilities, legend
DecisionNoulAnswernoul (probability true)

Conceptual overview and JSON examples: Decision apps.

Interrupt gates — use with Agents REST API — Interrupt gates:

typescript
1import type { InterruptDTO } from '@inferencesh/sdk';2import {3  InterruptReasonToolApproval,4  InterruptStatusPending,5  InterruptResourceToolInvocation,6} from '@inferencesh/sdk';78const pending: InterruptDTO = {9  id: 'int_abc123',10  run_id: 'ar_abc123',11  chat_id: 'chat_abc123',12  reason: InterruptReasonToolApproval,13  status: InterruptStatusPending,14  resource_id: 'ti_abc123',15  resource_type: InterruptResourceToolInvocation,16};
Type / constantValues
InterruptReasontool_approval, client_tool, widget, auth, confirmation, hook_gate — mirrors active_run.interrupt_reason
InterruptStatuspending, resolved, expired, cancelled
InterruptResolutionallow, deny — set on InterruptDTO after resolve; also used as default_resolution on gate hooks
InterruptResourceTypetool_invocation (tool approval gates), hook_event (lifecycle hook gates)
InterruptDTOFull interrupt record (run_id, chat_id, source, resource_id, resource_type, meta, resolved_data, expires_at, …)

Remote harness chats

@inferencesh/sdk ≥ v0.8.3 exports OpenAPI fields for harness-driven agents and chats opened via Launch harness session. There is no client.remotes namespace yet; call POST /remotes/{id}/sessions with fetch (or your HTTP client) and type the JSON with ChatDTO.

typescript
1import type { AgentDTO, ChatDTO } from '@inferencesh/sdk';23const { data: agent } = await client.agents.get('agent_abc123');4if (agent.harness !== 'inference') {5  console.log(agent.harness, agent.profile_id, agent.remote_id);6}78const { data: chat } = await client.chats.get('chat_abc123');9console.log(chat.work_dir, chat.harness_session_id);1011type LaunchHarnessSessionRequest = {12  profile_id: string;13  work_dir?: string;14  resume_session_id?: string;15};1617const launchBody: LaunchHarnessSessionRequest = {18  profile_id: 'prof_xyz',19  work_dir: '/home/you/project',20};2122const launched = (await fetch(`${baseUrl}/remotes/${remoteId}/sessions`, {23  method: 'POST',24  headers: {25    Authorization: `Bearer ${apiKey}`,26    'Content-Type': 'application/json',27  },28  body: JSON.stringify(launchBody),29}).then((r) => r.json())) as ChatDTO;
Field / typeDescription
AgentDTO.harness"inference" for the platform loop, or a harness registry id (claude, codex, …) when a remote profile drives the agent
AgentDTO.profile_id / AgentDTO.remote_idSet on harness-backed agents (see REST docs)
ChatDTO.work_dirFolder the harness uses for this chat
ChatDTO.harness_session_idHarness-native session id (for example to resume with claude --resume)
ChatDTO.channel_contextExternal channel origin for reply routing — see Channel context

Channel routing: ChannelContext, ChannelTypeSlack, ChannelTypeTelegram, ChannelTypeTeams, and ChannelTypeDiscord type channel_context on ChatDTO. The sendMessage() option is deprecated and not sent (0.23.0). Wire keys are channel_type and channel_metadata (object). See Channel routing (channel_context).

Agent run events — harness and platform runs publish sequenced AgentEvent envelopes on the run and chat event bus. Custom clients (for example a dashboard that consumes remote_session_event from a remote daemon) can type them with the generated constants:

typescript
1import type { AgentEvent } from '@inferencesh/sdk';2import {3  AgentEventRunStarted,4  AgentEventRunStateChanged,5  AgentEventContentDelta,6  AgentEventToolCompleted,7  AgentEventApprovalRequired,8} from '@inferencesh/sdk';910function isRunStarted(event: AgentEvent): boolean {11  return event.type === AgentEventRunStarted;12}
ConstantWire type
AgentEventRunStartedrun.started
AgentEventRunStateChangedrun.state_changed
AgentEventTurnStarted / AgentEventTurnCompletedturn.started / turn.completed
AgentEventContentDeltacontent.delta
AgentEventToolStarted / AgentEventToolCompletedtool.started / tool.completed
AgentEventApprovalRequired / AgentEventApprovalResolvedapproval.required / approval.resolved
AgentEventHookExecutedhook.executed
AgentEventUsageUpdatedusage.updated
AgentEventContextUpdatedcontext.updated
AgentEventLimitsUpdatedlimits.updated
AgentEventAccountUpdatedaccount.updated
AgentEventContextCompactedcontext.compacted
AgentEventSubagentStarted / AgentEventSubagentUpdated / AgentEventSubagentCompletedsubagent.started / subagent.updated / subagent.completed
AgentEventErrorerror

Each event has id, run_id, chat_id, timestamp, and a payload whose shape depends on type (RunStartedPayload, ContentDeltaPayload, …). See Harness session frames.

The context, limits, account and subagent constants and their payload types ship in @inferencesh/sdk ≥ v0.23.0. The events come from agentprotocol v0.21.0 (usage cache fields, context.updated, limits.updated, account.updated) and v0.22.0 (subagent.*). A remote daemon may send them in remote_session_event; the platform does not forward context.updated, limits.updated, account.updated or subagent.* to chat and run streams yet.

usage.updated is emitted once when a turn ends. UsageUpdatedPayload:

FieldMeaning
prompt_tokensEvery input token of the turn, cache reads and writes included
cached_input_tokensInput tokens read from a prompt cache (part of prompt_tokens)
cache_creation_tokensInput tokens written to a prompt cache (part of prompt_tokens)
completion_tokensOutput tokens
reasoning_tokensReasoning tokens (part of completion_tokens)
total_tokensTurn total
cost_usdThe agent's own cost figure
statuscomplete for a whole turn, partial when the turn ended early

The cache, reasoning, cost and status fields are omitted when zero or empty.

EventPayload
context.updatedused_tokens, max_tokens: how full the context window is
limits.updatedwindows[]: id, kind (session, weekly, monthly, other), label, used_percent (0 to 100), resets_at, duration_minutes
account.updatedaccount: status (authenticated, unauthenticated, unknown), type (subscription, api_key, cloud), plan, plan_label, email, workspace

Chat transcript types: ChatMessageRoleEvent, ChatEventTypeHook, ChatEvent, and ChatHookEvent describe role: "event" messages for notable lifecycle hook runs (inject, non-allow decision, or error; not every run). See Chat message roles.

App requirement and setup-action types

Use with Check requirements, client.credentials.checkRequirements(), and the credentials: / secrets: blocks of inf.yml:

typescript
1import type {2  CheckRequirementsRequest,3  CredentialRequirement,4  SecretRequirement,5  SetupAction,6} from '@inferencesh/sdk';7import {8  SetupActionAddSecret,9  SetupActionConnect,10  SetupActionAddScopes,11  RequirementTypeCredential,12} from '@inferencesh/sdk';1314const request: CheckRequirementsRequest = {15  secrets: [{ key: 'OPENAI_API_KEY', optional: false }],16  credentials: [17    { slug: 'google', capabilities: ['google.sheets'] },18    {19      slug: 'acme',20      secrets: ['ACME_API_KEY'],21      provider: { name: 'Acme CRM', website: 'acme.com' },22    },23  ],24};2526const { data } = await client.credentials.checkRequirements(request);27if (!data?.satisfied) {28  for (const err of data?.errors ?? []) {29    if (err.type === RequirementTypeCredential && err.action?.type === SetupActionConnect) {30      console.log('Connect', err.action.provider_name);31    }32  }33}
TypeFields
SecretRequirementkey, description, optional
CredentialRequirementslug (credential slug such as google or acme), capabilities (platform grants such as google.sheets), secrets, scopes, description, optional, provider (CompanySeed: name, website, … for a company the platform does not list; company: in inf.yml), auth (AuthMethod, for a slug no auth provider defines yet). v0.26.0+; before that, key, provider (a string), name and website sat on the entry. The API still reads those spellings. CompanySeed and AuthMethod were ProviderSeed and AuthSchemeSpec before INF-1079; AuthSchemeDTO is now AuthProviderDTO.
SetupActiontype (SetupActionAddSecret, SetupActionConnect, SetupActionAddScopes), provider, provider_name, scopes, scope_descriptions, secrets, provider_website
CheckRequirementsResponsesatisfied, errors (each RequirementError may include action: SetupAction)
AppVersionDTO.required_credentialsSame CredentialRequirement shape as in inf.yml
AppFunction.credentialsCredentials one function needs (CredentialRequirement[]), each covered by required_credentials. Empty means the function gets the app-wide list (v0.23.0+)
AppFunction.annotationsToolAnnotations MCP hints (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint) (v0.23.0+)
AppVersionDTO.warningsDeployWarning[] (code, function, message), only on the version a deploy just created (v0.23.0+). See Deploying: Warnings

Cost estimation — use with Estimate app cost (POST /store/apps/{appId}/estimate):

typescript
1import type { EstimateCostRequest, EstimateCostResponse } from '@inferencesh/sdk';23const req: EstimateCostRequest = {4  input: { n: 4 },5  function: 'generate', // optional for multi-function apps6};78const resp: EstimateCostResponse = {9  confidence: 'range',10  min: 150_000,11  max: 400_000,12  depends_on: ['output_tokens'],13  pricing_description: '$0.0015–$0.004 per run',14};1516const failed: EstimateCostResponse = {17  confidence: 'unknown',18  depends_on: ['output_tokens'],19  estimate_error: 'CEL evaluation failed: undefined variable "output_tokens"',20};
TypeFields
EstimateCostRequestinput, function
EstimateCostResponseconfidence, microcents, min, max, depends_on, estimate_error, pricing_description

Plans and subscriptions — use with the Subscription REST API:

typescript
1import type { PlanDTO } from '@inferencesh/sdk';2import { PlanTypeBase, PlanTypeAddon } from '@inferencesh/sdk';34const base: PlanDTO = {5  name: 'pro',6  plan_type: PlanTypeBase,7  credits_monthly: 5_000_000_000,8  stackable: false,9  required_plan_ids: [],10  limits: {},11};1213const addon: PlanDTO = {14  name: 'extra concurrency',15  plan_type: PlanTypeAddon,16  stackable: true,17  required_plan_ids: ['plan_pro', 'plan_team'],18  required_plan_names: ['Pro', 'Team'],19  limits: {},20};

stackable controls whether a workspace can purchase multiple instances of the same add-on. required_plan_ids gates add-on checkout to workspaces on a qualifying base plan (see Add-on subscriptions).

API key scopes — use with API keys — Scope catalog (GET /scopes):

typescript
1import type { ScopesResponse, ScopePreset } from '@inferencesh/sdk';23const catalog: ScopesResponse = {4  scopes: [{ value: 'agents:execute', label: 'execute', group: 'agents' }],5  groups: [{ id: 'agents', label: 'agents' }],6  presets: [7    {8      id: 'run',9      label: 'run',10      description: 'execute agents, apps, and flows',11      scopes: ['agents:read', 'apps:read', 'agents:execute', 'apps:execute'],12      summary: ['run apps and agents on your behalf'],13      hidden: false,14    },15  ],16};

ScopePreset.summary is a human-readable permission list for consent UIs. hidden presets (for example engine) are omitted from non-admin GET /scopes responses.

API key scope (who the key acts as) — distinct from the permission scopes above. See API Keys API — personal and workspace keys (@inferencesh/sdk ≥ v0.15.0):

typescript
1import {2  ApiKeyScopeWorkspace,3  type ApiKeyScope,4} from '@inferencesh/sdk';56const scope: ApiKeyScope = ApiKeyScopeWorkspace; // or ApiKeyScopeUser78const { data: key } = await client.apiKeys.create({9  name: 'ci',10  scope,11  scopes: ['apps:execute'],12});13console.log(key.key); // the only time the full value is returned

CreateApiKeyRequest.scope accepts user (default) or workspace. ApiKeyDTO carries scope, created_by, and, on list responses, creator.

Instance catalog — use with Instance types REST API for GPU pricing and availability (@inferencesh/sdk ≥ v0.14.2):

typescript
1import type {2  InstanceTypeAvailability,3  InstanceTypeConfiguration,4  InstanceTypeDTO,5} from '@inferencesh/sdk';6import { InstanceRentalTypeOnDemand, InstanceRentalTypeSpot } from '@inferencesh/sdk';78// GET /instances/types returns InstanceTypeDTO[]9const row: InstanceTypeDTO = {10  cloud: 'runpod',11  cloud_logo_url: 'https://cloud.inference.sh/logos/runpod.com.png',12  configuration: {13    gpu_type: 'H100',14    gpu_manufacturer: 'nvidia',15    num_gpus: 1,16    nvlink: false,17  },18  availability: [19    { available: true, region: 'us-east-1', rental_type: InstanceRentalTypeOnDemand },20    {21      available: true,22      region: 'us-east-1',23      rental_type: InstanceRentalTypeSpot,24      hourly_price: 199, // cents per hour — spot rows only25    },26  ],27};2829const spotRow: InstanceTypeAvailability = row.availability![1];

InstanceTypeConfiguration includes gpu_manufacturer, nvlink, gpu_type, num_gpus, and the other fields documented on the REST page. Each availability entry includes rental_type (on_demand or spot); spot rows also set hourly_price (cents). Engine-picker offers from GET /engines/types set top-level rental_type, region, and hourly_price on InstanceTypeDTO, and include an options array (every in-stock provider and per-region prices for that offer) — see Engines API — List engine instance types.

Engines — use with the Engines REST API:

typescript
1import type { EngineDTO } from '@inferencesh/sdk';2import {3  EngineStatusRunning,4  InstanceRentalTypeSpot,5} from '@inferencesh/sdk';67const engine: EngineDTO = {8  id: 'eng_abc123',9  name: 'gpu-box-1',10  status: EngineStatusRunning,11  engine_version: 'v1.10.15', // inferencesh-engine binary; empty until first telemetry sync12  api_url: 'https://api.inference.sh',13  workers: [],14  instance: {15    rental_type: InstanceRentalTypeSpot,16    hourly_price: 199,17  },18};

Spot cloud engines stop permanently when the provider reclaims the instance; provision a new engine or use on-demand — see Engines API — Get engine.

Constant / typeNotes
InstanceRentalTypeon_demand, spot — catalog rows, EngineDTO.instance, and nested engine.instance
InstanceRentalTypeOnDemand / InstanceRentalTypeSpotTyped constants for rental_type

Store browse metadata — category and tag catalogs from the public Store API (@inferencesh/sdk ≥ v0.21.0 for StoreCategoryDTO / StoreTagDTO):

typescript
1import type { StoreCategoryDTO, StoreTagDTO } from '@inferencesh/sdk';23const categoriesPayload = await fetch('https://api.inference.sh/store/categories?type=app').then((r) =>4  r.json(),5);6const categories: StoreCategoryDTO[] = categoriesPayload.data;7const tagsPayload = await fetch('https://api.inference.sh/store/tags').then((r) => r.json());8const tags: StoreTagDTO[] = tagsPayload.data;
TypeREST endpointNotes
StoreCategoryDTOGET /store/categoriesslug, name, description, icon, rank, count — pass type to scope count per resource kind
StoreTagDTOGET /store/tagsslug, title, count — tags on ≥ 3 public apps

There is no client.store namespace yet; call these endpoints with fetch. See List store categories and List browseable tags.

Per-app store listings — use with the Store REST API:

typescript
1import type { AppStoreListingDTO } from '@inferencesh/sdk';23const listing: AppStoreListingDTO = {4  id: 'app_abc123',5  allows_cloud_workers: true,6  allows_private_workers: true,7  max_concurrency: 0,8  max_concurrency_per_team: 5,9  min_concurrency: 2,10  required_feature: 'feature:seedance', // optional — explicit entitlement grant required11};

min_concurrency protects cloud workers holding the app from displacement by other apps; it does not start workers. See Reserved cloud capacity. When required_feature is set, dispatch requires a boolean entitlement with enabled: true — see Feature-gated store apps.

ConstantAPI value
AppStatusActiveactive
AppStatusMaintenancemaintenance
AppStatusDeprecateddeprecated
AppStatusRetiredretired

See Apps REST API — lifecycle status for run and discovery behavior.


CommonJS Support

@inferencesh/sdk ≥ v0.23.0 can be loaded with require() on Node 22.12+, which loads ES modules from CommonJS:

javascript
1const { inference, tool, string } = require('@inferencesh/sdk');23async function main() {4  const client = inference({ apiKey: 'inf_...' });5  const result = await client.run({...});6}78main();

Error Handling

Responses are unwrapped from the { "data", "messages" } envelope (.data and .messages on every result) and errors are RFC 9457 application/problem+json.

InferenceError.message uses detail when present, otherwise title, then message, then the raw response body for non-JSON failures — the same extraction order as the Python SDK.

Pass onError when creating the client to intercept errors and optionally retry (token refresh, OTP, custom backoff). @inferencesh/sdk ≥ v0.21.0 routes these failures through the same handler:

  • REST calls via request()
  • Raw HttpClient.fetch() calls (including stream bodies)
  • Failed initial connections for SSE (.stream() / createEventSource) and NDJSON (tasks.run() / tasks.watch(), agent chat streams)

The handler receives (error, retry, request) where request.token is the bearer token the failed attempt used (undefined in proxy mode or when no token was sent). retry() sends the request again with the token from getToken or apiKey at retry time — it does not re-enter onError. Compare request.token with your in-memory credential when tokens can rotate while requests are in flight (for example an app embedded in an MCP host).

getToken may be async; the client awaits it before every request, stream handshake, and fetch().

Return the promise from retry() to retry after refreshing credentials. For fetch() and the EventSource handshake, you can also resolve with a Response directly; any other return value leaves the original failure in place.

HttpClient.request() also accepts handleErrors (default true). With handleErrors: false, that call bypasses onError: there is no retry, no credential prompt, and the failure is thrown as-is (InferenceError for HTTP refusals, or the underlying network error). The flag is client-only and is not sent on the wire. Use it when a global handler would be wrong for one endpoint — for example handling an expected 403 locally before deciding whether to refresh credentials.

typescript
1const http = createHttpClient({ apiKey: 'inf_...', onError: globalHandler });23try {4  await http.request('get', '/tasks/task_abc', { handleErrors: false });5} catch (error) {6  if (error instanceof InferenceError && error.statusCode === 403) {7    // Inspect the refusal without invoking onError8  }9  throw error;10}
typescript
1import { inference, InferenceError, type FailedRequest } from '@inferencesh/sdk';23const client = inference({4  getToken: () => session.accessToken,5  onError: async (error, retry, request: FailedRequest) => {6    if (error instanceof InferenceError && error.statusCode === 401) {7      if (request.token !== session.accessToken) {8        // Credentials were already replaced; retry with the new token.9        return retry();10      }11      await refreshSession();12      return retry();13    }14    throw error;15  },16});

Raw fetch on HttpClient

@inferencesh/sdk ≥ v0.21.0 adds HttpClient.fetch(endpoint, init?) on createHttpClient() and on the internal client behind inference(). It applies the same base URL, proxy routing, auth headers, and credentials mode as request(), but returns the raw Response so you can read streams, text, or non-JSON bodies. HTTP refusals throw the same errors as request() (InferenceError, RequirementsNotMetException) after onError runs. Aborts via init.signal reject without calling onError.

Use it for custom NDJSON or SSE integrations:

typescript
1import { createHttpClient, StreamableManager } from '@inferencesh/sdk';23const http = createHttpClient({ getToken: () => host.getAccessToken() });45const manager = new StreamableManager({6  request: (init) => http.fetch('/tasks/task_abc123/stream', init),7  onData: (update) => console.log(update.status),8});9await manager.start();

HttpClient.getStreamableConfig() is deprecated: config built synchronously bypasses onError and freezes the token from when it was read. It throws when getToken is async — use fetch() or a StreamRequest as above instead.

v0.21.0 breaking changes for custom HTTP wiring: a hand-built AgentClient must expose http.fetch (not only request()). StreamableManagerOptions is a type alias, not an interface — extend it with an intersection (type X = StreamableManagerOptions<T> & { … }) instead of interface X extends StreamableManagerOptions<T>.

→ Streaming — SSE and NDJSON transports

typescript
1import {2  RequirementsNotMetException,3  InferenceError,4  RequirementTypeCredential,5  CredentialProviderGoogle,6} from '@inferencesh/sdk';78try {9  const result = await client.run({ app: 'my-app', input: {...} });10} catch (e) {11  if (e instanceof RequirementsNotMetException) {12    // HTTP 412 — requirements payload is not wrapped in v213    console.log('Missing requirements:');14    for (const err of e.errors) {15      console.log(`  - ${err.type}: ${err.key}`);16      if (err.type === RequirementTypeCredential && err.action?.provider === CredentialProviderGoogle) {17        // User must connect Google — see /docs/extend/credentials18      }19    }20  } else if (e instanceof InferenceError) {21    console.log(`HTTP ${e.statusCode}: ${e.message}`); // e.responseBody has full problem+json22  }23}

Entitlement errors (HTTP 402/403 with plan limits or feature gates) attach structured metadata on a top-level meta extension member of the problem details. Parse InferenceError.responseBody and type the result with EntitlementErrorMeta:

typescript
1import { InferenceError, type EntitlementErrorMeta } from '@inferencesh/sdk';23function parseEntitlementMeta(error: InferenceError): EntitlementErrorMeta | null {4  if (!error.responseBody) return null;5  try {6    const body = JSON.parse(error.responseBody);7    return body.error?.meta ?? body.meta ?? null;8  } catch {9    return null;10  }11}1213try {14  await client.run({ app: 'my-app', input: {} });15} catch (e) {16  if (e instanceof InferenceError && (e.statusCode === 402 || e.statusCode === 403)) {17    const meta = parseEntitlementMeta(e);18    if (meta?.upgrade_available) {19      console.log(`Blocked on ${meta.resource_label ?? meta.resource}`);20      if (meta.addon_plan_id) {21        console.log(`Add-on available: ${meta.addon_plan_name} ($${(meta.addon_plan_price ?? 0) / 100}/mo)`);22      }23    }24  }25}

→ Entitlements API — Error metadata — field reference and response examples

→ REST overview — response format — problem+json shape


File Upload

typescript
1// Upload separately2const file = await client.uploadFile(buffer, {3  filename: 'image.png',4  contentType: 'image/png'5});6console.log(`Uploaded: ${file.uri}`);78// Use in app input9const result = await client.run({10  app: 'image-processor',11  input: { image: file.uri }12});

REST API (curl)

For simple integrations, you can use the REST API directly with streaming:

bash
1# Single call with streaming2curl -N -X POST \3  -H "Authorization: Bearer YOUR_API_KEY" \4  -H "Content-Type: application/json" \5  -d '{6    "agent": "my-org/assistant@abc123",7    "input": {"text": "Hello!", "role": "user"},8    "stream": true9  }' \10  https://api.inference.sh/agents/run

The -N flag disables curl's buffering so you see events in real-time.

Without streaming (returns JSON with chat_id for subsequent messages):

bash
1curl -X POST \2  -H "Authorization: Bearer YOUR_API_KEY" \3  -H "Content-Type: application/json" \4  -d '{5    "agent": "my-org/assistant@abc123",6    "input": {"text": "Hello!", "role": "user"}7  }' \8  https://api.inference.sh/agents/run

What's Next?

we use cookies

we use cookies to ensure you get the best experience on our website. for more information on how we use cookies, please see our cookie policy.

by clicking "accept", you agree to our use of cookies.
learn more.