@ingram-tech/ai-sdk-adapter
v0.4.0
Published
Vercel AI SDK adapter for Ingram Cloud — a pre-configured OpenAI-compatible provider plus smith identity, thread memory, and human-in-the-loop approval helpers. Built on industry-standard surfaces; no proprietary protocol required.
Maintainers
Readme
@ingram-tech/ai-sdk-adapter
Drive an Ingram Cloud smith from the Vercel AI SDK. It is a thin, idiomatic extension of the AI SDK: a pre-configured provider plus small helpers for the few things Ingram Cloud adds on top — smith identity, server-side memory, and human-in-the-loop approvals.
Philosophy: stand on the standard
This package is deliberately not a bespoke protocol client. The main entry
point is @ai-sdk/openai-compatible pointed at Ingram Cloud's
OpenAI-compatible API, so your
app speaks the OpenAI Chat Completions wire format end-to-end. Approvals ride the
standard tool-call channel; memory rides a single request header. There is no
custom SSE envelope to parse and no streamText replacement to learn.
A smith still runs the agent loop server-side (that's the whole point — memory, tools, approvals, isolation), but from your code it looks like any other model.
The native run envelope (
/v1/smiths/{id}/runs) is available behind the opt-in/nativesubpath for the one thing the standard surface doesn't yet carry — live tool-progress frames. Prefer the standard provider.
Install
npm install @ingram-tech/ai-sdk-adapter aiai (v6+) is a peer dependency — you already have it. @ai-sdk/react is only
needed for the client helpers.
Quickstart
Server (streamText, generateText, agents)
import { createIngramCloud } from "@ingram-tech/ai-sdk-adapter";
import { streamText } from "ai";
// A per-smith token names exactly one smith; the agent is the one that smith runs.
// The model id is the inference LLM: "" uses the agent's configured model, or pass
// a model id (e.g. "gpt-5.5") to override the LLM for that call.
const ingram = createIngramCloud({ apiKey: process.env.IC_SMITH_TOKEN! });
const result = streamText({
model: ingram(""),
prompt: "How do I reset my password?",
});
for await (const delta of result.textStream) process.stdout.write(delta);Server-side with a tenant-admin token instead? Name the smith explicitly:
const ingram = createIngramCloud({
apiKey: process.env.IC_TENANT_TOKEN!,
smithId: "smt_…",
});Never ship a tenant-admin token to the browser — proxy through your backend.
Client (useChat)
The recommended shape is a proxy route: the browser talks to your /api/chat
route, which holds the token and runs createIngramCloud. The client is plain AI
SDK:
"use client";
import { useChat } from "@ai-sdk/react";
import { ingramCloudTransport, approvalsSettled } from "@ingram-tech/ai-sdk-adapter/react";
export function Chat() {
const { messages, sendMessage } = useChat({
transport: ingramCloudTransport({ api: "/api/chat" }),
// auto-resume a turn once every approval has a decision
sendAutomaticallyWhen: approvalsSettled,
});
// …render messages, call sendMessage(...)
}Memory: one header
Plain Chat Completions is stateless. Pass a threadId and Ingram Cloud holds the
conversation server-side (the same thread model as a native run): you send only
the new turn, and memory works.
const ingram = createIngramCloud({
apiKey: SMITH_TOKEN,
threadId: `chat_${conversationId}`, // sent as IC-Thread-Id
});Structured outputs (generateObject)
The provider advertises structured outputs, so generateObject sends your schema
as a strict response_format and Ingram Cloud enforces it — conforming JSON or an
error, never a best-effort guess:
import { generateObject } from "ai";
import { z } from "zod";
const { object } = await generateObject({
model: ingram(""),
schema: z.object({
invoice_number: z.string().nullable(),
total: z.number().nullable(),
}),
prompt: "Invoice #A-1, total 100 EUR.",
});The schema'd call is a stateless one-shot (no tools, no memory) — use a provider
without threadId for it; a threadId provider is rejected with a 400.
Approvals (human-in-the-loop)
A tool the agent marks destructiveHint pauses the run for approval. On this
surface the pause arrives as a normal tool call whose id is
"<run_id>::<tool_call_id>", and the turn ends with finish_reason:
"tool_calls". Pull the pending approvals off the result and resume by appending
a decision:
import {
createIngramCloud,
getApprovalRequests,
approvalToolResult,
} from "@ingram-tech/ai-sdk-adapter";
import { generateText } from "ai";
const ingram = createIngramCloud({ apiKey: SMITH_TOKEN, threadId });
const first = await generateText({ model: ingram(""), messages });
const approvals = getApprovalRequests(first.toolCalls);
if (approvals.length) {
const decided = await askTheHuman(approvals); // your UI/policy
const resumed = await generateText({
model: ingram(""),
messages: [
...messages,
...decided.map((a) => approvalToolResult(a.request, a.ok ? "approve" : "reject")),
],
});
}On approve, Ingram Cloud executes the tool itself and continues; on reject,
the run completes with stop_reason: "approval_rejected" and nothing runs. Calling
/v1/chat/completions directly without AI SDK message conversion? Use
approvalWireMessage(id, "approve") to build the raw tool message.
Tools
Two models, both standard — pick per use case:
Client-side tools (you run them). Define tools with the AI SDK's
tool()and pass them tostreamText/generateTextas with any provider. The model's calls come back for you to execute; the SDK loops by re-sending the conversation. Ingram Cloud executes nothing — the standard OpenAI function-call contract, no Ingram-specific setup.import { tool } from "ai"; import { z } from "zod"; const result = streamText({ model: ingram(""), messages, tools: { get_weather: tool({ description: "…", inputSchema: z.object({ city: z.string() }) }) }, });A turn that passes
toolsruns only those client tools (the agent still supplies instructions; its server-side tools/memory sit out that turn).Server-side tools (MCP). Ingram Cloud calls your MCP server and runs the tools for you, with approval gating. Don't pass
tools— register the MCP server once and it's available to the smith automatically. For shared/remote tools.
Identity & tokens
| Token | Use | How the smith is chosen |
|---|---|---|
| Smith token (sub = "<tenant>:<smith>") | browser-safe; the default | the token is the smith |
| Tenant-admin token | server-side only | pass smithId (sent as IC-Smith-Id) |
The agent is the one the smith runs — chosen by the smith, never by an argument.
The model argument is the upstream inference LLM: "" uses the agent's configured
model; a model id (e.g. gpt-5.5) overrides the LLM for that call.
Native fallback
@ingram-tech/ai-sdk-adapter/native parses Ingram Cloud's native SSE envelope
into an AI SDK UI message stream. Reach for it only when you need the native
extras the standard surface doesn't carry yet — chiefly the live
tool.executing / tool.completed progress frames:
import { pipeIngramCloudRun } from "@ingram-tech/ai-sdk-adapter/native";
const result = await pipeIngramCloudRun(icResponse, writer, {
onToolActivity: ({ tool, phase }) => console.log(tool, phase),
onApproval: (req) => surface(req),
});
// result.status: "completed" | "paused" | "failed" | "cancelled"Notes
- ESM-only, ships as
dist/. Build withnpm run build(plaintsc). - Independent of the API's api/web checks, like the
pulumi/package. Keep it in step when the OpenAI-compatible surface it wraps changes. - This is the seed of the official Ingram Cloud JavaScript SDK; the intended
long-term home is
@ai-sdk/ingram-cloud.
