@cogitator-ai/ai-sdk
v0.10.1
Published
Vercel AI SDK adapter for Cogitator - bidirectional compatibility
Maintainers
Readme
@cogitator-ai/ai-sdk
Vercel AI SDK adapter for Cogitator — bidirectional compatibility between Cogitator agents and the AI SDK, for AI SDK 4, 5, 6 and 7.
Full guide: cogitator.app/docs/integrations/ai-sdk
Installation
pnpm add @cogitator-ai/ai-sdk @cogitator-ai/core aiai is a peer dependency: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0.
Features
- Use Cogitator agents with the AI SDK —
generateText,streamText, structured output, any AI SDK major - Use AI SDK models in Cogitator — wrap any AI SDK provider model (
LanguageModelV1–LanguageModelV4) as a Cogitator backend - Tool conversion — convert tools in both directions; converted tools work with the
toolsoption of every AI SDK major
Compatibility
| AI SDK | Model specs it accepts | Default cogitatorModel() spec | fromAISDK() input |
| ------ | ---------------------- | ------------------------------- | ------------------- |
| ai@4 | v1 | v1 | LanguageModelV1 |
| ai@5 | v2 | v2 | LanguageModelV2 |
| ai@6 | v2, v3 | v3 | LanguageModelV3 |
| ai@7 | v2, v3, v4 | v4 | LanguageModelV4 |
Specification auto-detection
When specificationVersion is omitted, cogitatorModel() and createCogitatorProvider() pick the spec of the installed ai package, so existing code keeps working on every major:
- Runtime:
ai/package.jsonis resolved fromprocess.cwd()(your app), then from this package — also for ESM-only installs whoseexportshidepackage.json. The result is cached per process. Ifaicannot be resolved (for example in runtimes withoutprocess.getBuiltinModule),'v2'is used, which ai@5 – ai@7 accept. - Types: the default return type is derived from the
LanguageModeltype of theaipackage your TypeScript resolves, with the same mapping, sogenerateText({ model: cogitatorModel(cog, agent) })type-checks on ai@4 – ai@7 without changes.
Pass specificationVersion to override the detection — for example when several ai majors are installed, or when the runtime and the type-level ai differ. AI SDK 6 and 7 run v2 models in a compatibility mode that logs a "Using v2 specification compatibility mode" warning per call; the detected v3 / v4 defaults avoid it.
Usage
Cogitator Agent as AI SDK Model
import { generateText, streamText } from 'ai';
import { Cogitator, Agent, tool } from '@cogitator-ai/core';
import { cogitatorModel } from '@cogitator-ai/ai-sdk';
import { z } from 'zod';
const cog = new Cogitator({
llm: { providers: { openai: { apiKey: process.env.OPENAI_API_KEY! } } },
});
const searchTool = tool({
name: 'search',
description: 'Search the web',
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => ({ results: [`Result for: ${query}`] }),
});
const researcher = new Agent({
name: 'researcher',
model: 'openai/gpt-6.1-sol',
instructions: 'You are a research assistant.',
tools: [searchTool],
});
// spec detected from the installed ai package (ai@4 – ai@7)
const { text } = await generateText({
model: cogitatorModel(cog, researcher),
prompt: 'Research the latest AI developments',
});
// explicit override
const result = streamText({
model: cogitatorModel(cog, researcher, { specificationVersion: 'v2' }),
prompt: 'Write an article about TypeScript',
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}With ai@4 the same call returns a LanguageModelV1; { specificationVersion: 'v1' } forces it explicitly.
How the agent maps to a model
- The agent runs its whole loop (LLM calls and its own tool calls) inside one model call. The result's
finishReasonsays how the run ended:stopfor an answer,lengthwhen the answer stopped at the token limit (RunResult.truncated),content-filterwhen it was withheld (rawcontent_filterfor a provider's filter,refusalwhen the model declined),other(rawiteration-limit) when tool calls used up the agent'smaxIterations,tool-callswhen the run waits for tool approvals, andother(rawpaused) when it waits only on questions of its tools (below).v1andv2models get the same reasons in their form (otherfor a paused run).providerMetadata.cogitatoralso carriestruncated,blockedanditerationLimitReached. generateTextandstreamTextsee the same text: what the agent writes before a tool call ("Let me check.") comes before that call in both, as text parts of the response. In JSON mode (Output.object,generateObject) only the agent's final answer is text, so a preamble never breaks the parsed object, and a stream then delivers the JSON when the run ends.- The agent's tool calls are reported as provider-executed tool calls with their results (
result.toolCalls/result.toolResults,tool-call/tool-resultstream parts). The AI SDK never executes them again.- With
v3/v4models they are always reported. - With
v2models they are reported only for tools you also pass intools(ai@5 cannot handle provider-executed calls of undeclared tools). - Every model also lists them in
providerMetadata.cogitator.toolCalls, together withrunId,threadId,agentId,model,costandduration.
- With
- The trailing user messages of the prompt are the agent's input, and the turns before them reach the run as its history (
RunOptions.history), as messages: user and assistant text, assistant tool calls and their results, so the model sees earlier turns the way it wrote them. System messages, files and images are not forwarded (the agent'sinstructionsare its system prompt) and the AI SDK reports a warning. A prompt that does not end with a user message is refused, except one that answers tool approvals. providerOptions.cogitator(providerMetadata.cogitatoron ai@4) takesthreadId,userIdandsaveHistory. With athreadIdthe run's history is that memory thread, so only the new turn of the prompt is used, and the turn is saved there (saveHistorydefaults totrue). Without one the prompt is the whole conversation and nothing is saved unlesssaveHistory: true.userIdreaches memory, tools (context.userId) and resumes.v4models report what happens inside the run as custom parts:cogitator.handoffwhen the agent hands the conversation to another (from,to,reason) andcogitator.model-fallbackwhen a turn moves to a fallback model (from,to,error), with the details inproviderMetadata.cogitator, in the answer's content and in the stream as they happen.temperature,topP,maxOutputTokens(maxTokenson ai@4) andstopSequencesoverride the agent settings for the call;abortSignalcancels the run, as does cancelling the stream.- The
reasoningcall option ofv4models (ai@7) overrides the effort of the agent'sreasoningfor the call. An agent withreasoning: { summary: true }returns its summary asreasoningcontent (reasoning-start/reasoning-delta/reasoning-endstream parts,reasoningtext on ai@4), and usage carries its reasoning and cached input tokens. - JSON response formats (
generateObject,Output.object, ai@4object-json/object-toolmodes) switch the agent to JSON mode and append the schema to the input. - AI SDK
toolsthe agent does not own cannot be called by the agent; the model reports a warning for each of them. toolChoice: 'none'runs the agent without its tools and handoffs (tools registered on theCogitatoritself stay, with a warning).requiredand a named tool of the agent become the run'stoolChoice: the model must call a tool (or that one) before it answers, and the run goes back toautoonce it has. A named tool the agent does not have comes back as an unsupported-setting warning.- When the agent calls a tool that needs approval (
requiresApproval), its run pauses and the call never passes for an answer.v3andv4models (ai@6, ai@7) end the turn withfinishReason: 'tool-calls'and atool-approval-requestfor each waiting call (a provider-executedtool-callcomes with it). Answer with atool-approval-response(providerExecuted: true, whichconvertToModelMessagessets for UI messages) as the last message of the next prompt and the run resumes: approved calls run, denied ones are declined with yourreason.v1andv2models cannot ask, so they finish withfinishReason: 'other'and a warning, andproviderMetadata.cogitatorholdsstatus: 'paused', thethreadIdandpendingApprovalsto resume withcogitator.resume(agent, threadId, { decisions }). - A question a tool asks with
context.interrupt()pauses the run too, and no AI SDK version has a part that asks it. Every model version then finishes withfinishReason: 'other'(rawpausedonv3andv4) and a warning that names each question, andproviderMetadata.cogitatorholdsstatus: 'paused', thethreadIdandpendingQuestions(id,reason,message,responseSchema) to answer withcogitator.resume(agent, threadId, { responses: { [id]: answer } }).
const paused = await generateText({ model: cogitatorModel(cog, agent), prompt: 'Refund A-1' });
const request = paused.content.find((part) => part.type === 'tool-approval-request');
if (request?.type === 'tool-approval-request') {
const done = await generateText({
model: cogitatorModel(cog, agent),
messages: [
{ role: 'user', content: 'Refund A-1' },
...paused.response.messages,
{
role: 'tool',
content: [
{
type: 'tool-approval-response',
approvalId: request.approvalId,
approved: true,
providerExecuted: true,
},
],
},
],
});
}Named Agents
import { createCogitatorProvider } from '@cogitator-ai/ai-sdk';
const provider = createCogitatorProvider(cog, {
agents: [researcher, writer],
specificationVersion: 'v2', // optional, detected from the installed ai package
});
const model = provider('researcher', { temperature: 0.5 });
const sameModel = provider.languageModel('researcher');The provider follows the specification of its models, so an AI SDK provider registry takes it. Agents are its language models, and its embedding and image models (and an unknown agent) throw NoSuchModelError:
import { createProviderRegistry, generateText } from 'ai';
const registry = createProviderRegistry({ cogitator: provider, openai });
const { text } = await generateText({ model: registry.languageModel('cogitator:writer'), prompt });Cogitator Agent as AI SDK Agent
toAISDKAgent (ai@7) turns a Cogitator agent into an AI SDK agent (agent-v1) for everything that takes one, such as createAgentUIStreamResponse. It is a ToolLoopAgent on cogitatorModel(): the Cogitator agent runs its own tools, memory and approvals in one model step.
import { toAISDKAgent } from '@cogitator-ai/ai-sdk/agent';
const support = toAISDKAgent(cog, supportAgent, { temperature: 0.2 });
const { text } = await support.generate({ prompt: 'Where is order 42?' });It takes the settings of ToolLoopAgent except model, instructions and the tool settings, which the Cogitator agent has itself, and is in its own entry point because older ai versions have no Agent type.
AI SDK Models in Cogitator
fromAISDK() wraps any AI SDK language model — LanguageModelV1 (ai@4 providers) through LanguageModelV4 (ai@7 providers) — as a Cogitator LLMBackend:
import { google } from '@ai-sdk/google';
import { fromAISDK } from '@cogitator-ai/ai-sdk';
const backend = fromAISDK(google('gemini-3.5-flash-lite'));
const messages = [
{ role: 'system' as const, content: 'Be brief.' },
{ role: 'user' as const, content: 'Write a haiku about programming' },
];
const response = await backend.chat({ model: 'gemini-3.5-flash-lite', messages });
for await (const chunk of backend.chatStream({ model: 'gemini-3.5-flash-lite', messages })) {
process.stdout.write(chunk.delta.content ?? '');
}The backend converts Cogitator messages (text, images, assistant tool calls and tool results), tools, toolChoice, responseFormat, sampling settings and the abort signal to the model's specification, and maps text, reasoning, tool calls, finish reasons and usage (including cached, cache-write and reasoning tokens) back: reasoning content and stream parts become ChatResponse.reasoning / delta.reasoning. ChatRequest.reasoning.effort is sent as the reasoning call option of v4 models (max as xhigh). Older specifications have no provider-neutral reasoning setting, so configure effort, budgets and summaries on the AI SDK provider model (its providerOptions or settings). Gemini thought signatures on tool calls are preserved across turns, and so are the reasoning parts before a tool call with their provider metadata (Anthropic thinking signatures, redacted thinking) and each tool call's own provider metadata, which models with extended thinking need on the turn after a tool call. Tool results with images go to the model as images (content output with media, image-data or file parts by specification, content on ai@4), and failed tool calls as error-text.
To run Cogitator agents on an AI SDK model, register the backend under a name in llm.backends and point agents at it with name/model:
import { Agent, Cogitator } from '@cogitator-ai/core';
const cog = new Cogitator({
llm: { backends: { gemini: fromAISDK(google('gemini-3.5-flash-lite')) } },
});
const agent = new Agent({
name: 'writer',
model: 'gemini/gemini-3.5-flash-lite',
instructions: 'You write short poems.',
});
const result = await cog.run(agent, { input: 'A haiku about types' });Tools, memory, streaming and every other run feature work as with built-in providers. The wrapped model is used whatever model name the request carries; cost is reported only for models the price registry knows.
Tool Conversion
import { tool as aiTool } from 'ai';
import { tool as cogTool } from '@cogitator-ai/core';
import { fromAISDKTool, toAISDKTool, convertToolsToAISDK } from '@cogitator-ai/ai-sdk';
import { z } from 'zod';
// AI SDK tool → Cogitator tool (ai@5+ `inputSchema` or ai@4 `parameters`)
const aiWeather = aiTool({
description: 'Get weather for a city',
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => ({ temp: 20, city }),
});
const cogWeather = fromAISDKTool(aiWeather, 'weather');
// Cogitator tool → AI SDK tool (works with the `tools` option of ai@4 – ai@7)
const cogCalculator = cogTool({
name: 'calculator',
description: 'Perform calculations',
parameters: z.object({ expression: z.string() }),
execute: async ({ expression }) => ({ result: evaluate(expression) }),
});
const aiCalculator = toAISDKTool(cogCalculator);
const aiTools = convertToolsToAISDK([cogCalculator]);toAISDKTool() returns a tool with both schema fields:
inputSchema(read by ai@5 – ai@7) — the Cogitator tool's zod schemaparameters(read by ai@4) — an AI SDKSchemawith draft-07 JSON Schema and synchronous validation; it is also a Standard JSON Schema
execute(input, options) maps the AI SDK options to a Cogitator ToolContext: toolCallId becomes toolCallId (and runId, unless the tool context names one), abortSignal becomes signal, and string fields agentId, runId, threadId, userId, channelType, channelId, topicId, conversationId of the tool context (context on ai@7, experimental_context on ai@5 / ai@6) are copied over.
A tool result with media (toolContent() of @cogitator-ai/core, or an object with a base64 image such as a browser screenshot) reaches the model of generateText / streamText as an image through the tool's toModelOutput (experimental_toToolResultContent on ai@4). Files are described, other results go as text or JSON as usual. A toolOutput() result reaches the model as its content only, while the application gets the whole result back from the AI SDK, details and presentation included (with an invoker, the call goes through invokeTool and the application gets content). In the other direction, fromAISDKTool() gives the AI SDK tool the id of the call it runs for as toolCallId, and keeps the $defs of its JSON Schema.
Approval flags survive both ways: toAISDKTool() sets needsApproval from requiresApproval, so ai@6 and ai@7 ask before running the tool, and fromAISDKTool() sets requiresApproval from needsApproval, so a Cogitator run pauses (a needsApproval function that answers with a promise counts as "needs approval").
Running converted tools through Cogitator
Without options the AI SDK calls the tool's execute directly, so the tool runs without the guardrails, the sandbox of a sandboxed tool or tool.timeout, and ai@4 and ai@5, which do not read needsApproval, run a tool that needs approval without asking. Pass your Cogitator as invoker and every call goes through cogitator.invokeTool, the way an agent run executes it:
import { generateText } from 'ai';
import { convertToolsToAISDK } from '@cogitator-ai/ai-sdk';
const { text } = await generateText({
model,
tools: convertToolsToAISDK([refund, lookupOrder], {
invoker: cogitator,
onApproval: async (request) => ({ approved: await askUser(request) }),
}),
prompt: 'Refund order A-1',
});- A call the AI SDK asks approval for (the tool's
requiresApprovalholds for its input) and that the AI SDK approval flow approved (ai@6, ai@7: an approvingtool-approval-responsefor the call in the last message, as the AI SDK reads it) runs without being asked again. In most apps those messages come from the client, so set the AI SDK'sexperimental_toolApprovalSecretto have it refuse approvals your server did not sign. An approval in the messages for a call the AI SDK never asks about counts for nothing. - Any other call that needs approval, every one on ai@4 and ai@5, or one the guardrails want approved, goes to
onApproval, then toguardrails.onToolApproval. A call nobody decides fails withTool "refund" needs approval, and nobody approved this call, a declined one with its reason, and the model reads the error. - ai@7 can also decide approval for a whole call with
toolApproval, which leaves no approval response in the messages: with an invoker, decide those calls inonApproval. - The AI SDK already parsed the arguments with the tool's schema, so they are not parsed again and transforms apply once.
fromAISDKTool() accepts zod 4 schemas, Standard JSON Schemas, jsonSchema() / zodSchema() schemas from any AI SDK major and plain JSON Schema objects. Non-zod schemas are converted to JSON Schema and validated before the AI SDK execute runs. It also resolves ai@7 dynamic descriptions and the final value of streaming (async function*) execute functions, and passes the Cogitator context to the AI SDK tool as context / experimental_context. Zod 3 schemas cannot be converted to JSON Schema here — wrap them with zodSchema() from ai. The tool name (the second argument, else the tool's own name) must be one every LLM provider accepts, as for tool() of @cogitator-ai/core: fromAISDKTool() throws a TypeError otherwise, and normalizeToolName() of core turns an object key such as weather.get into weather_get.
API Reference
cogitatorModel(cogitator, agent, options?)
function cogitatorModel<V extends CogitatorSpecificationVersion = DefaultSpecificationVersion>(
cogitator: Cogitator,
agent: Agent,
options?: CogitatorModelOptions<V>
): CogitatorLanguageModel<V>;
type CogitatorSpecificationVersion = 'v1' | 'v2' | 'v3' | 'v4';
// 'v1' | 'v2' | 'v3' | 'v4', derived from the installed ai package's LanguageModel type
type DefaultSpecificationVersion;
interface CogitatorModelOptions<V> {
temperature?: number;
maxTokens?: number;
topP?: number;
specificationVersion?: V;
}CogitatorLanguageModel<'v1'> is a LanguageModelV1; 'v2', 'v3', 'v4' return CogitatorLanguageModelV2 / V3 / V4, which satisfy LanguageModelV2 / V3 / V4 of every @ai-sdk/provider release.
createCogitatorProvider(cogitator, config)
function createCogitatorProvider<
V extends CogitatorSpecificationVersion = DefaultSpecificationVersion,
>(cogitator: Cogitator, config: CogitatorProviderConfig<V>): CogitatorProvider<V>;
interface CogitatorProviderConfig<V> {
agents: Agent[] | Map<string, Agent> | Record<string, Agent>;
specificationVersion?: V;
}The provider has the members of its specification (CogitatorProviderMembers<V>): specificationVersion, embeddingModel and imageModel for v4 and v3, textEmbeddingModel and imageModel for v2, textEmbeddingModel for v1, all throwing NoSuchModelError.
toAISDKAgent(cogitator, agent, settings?) (@cogitator-ai/ai-sdk/agent)
A ToolLoopAgent of ai@7 on cogitatorModel(cogitator, agent, { specificationVersion: 'v4' }), with the id of the agent's name. settings (CogitatorAgentSettings) are those of ToolLoopAgent without model, instructions, tools, activeTools and toolChoice.
providerOptions.cogitator (CogitatorCallOptions)
| Field | Type | Description |
| ------------- | --------- | ------------------------------------------------------------------------------------------- |
| threadId | string | Memory thread of the run: its history comes from memory, the prompt gives only the new turn |
| userId | string | The user the run belongs to |
| saveHistory | boolean | Save the turn to memory. Default: true with a threadId, false without |
fromAISDK(model) / new AISDKBackend(model)
function fromAISDK(model: AISDKLanguageModel): LLMBackend;
type AISDKLanguageModel = LanguageModelV1 | LanguageModelV2 | LanguageModelV3 | LanguageModelV4;Tools
function fromAISDKTool<TParams, TResult>(
aiTool: AISDKToolLike,
toolName?: string
): Tool<TParams, TResult>;
function toAISDKTool<TParams, TResult>(
cogTool: Tool<TParams, TResult>,
options?: AISDKToolOptions
): AISDKTool<TParams, TResult>;
function convertToolsFromAISDK(aiTools: Record<string, AISDKToolLike>): Tool[];
function convertToolsToAISDK(
cogTools: Tool[],
options?: AISDKToolOptions
): Record<string, AISDKTool>;
interface AISDKToolOptions {
invoker?: ToolInvoker; // a Cogitator: calls go through invokeTool
onApproval?: RunOptions['onApproval']; // decides calls the AI SDK did not approve
}Migrating from 0.2.x
cogitatorModel()andcreateCogitatorProvider()detect the installedaimajor and return the matching model spec (v1on ai@4, as before);specificationVersionoverrides it.- Agent tool calls are no longer reported as client tool calls with
finishReason: 'tool-calls'(which made the AI SDK execute them again or fail withNoSuchToolError); see How the agent maps to a model. toAISDKTool()now emitsinputSchema(ai@5+) next toparameters(ai@4).fromAISDK()acceptsLanguageModelV1–LanguageModelV4and forwards tool calls and tool results as structured prompt parts instead of flattened text.
Documentation
License
MIT
