Skip to content

LLM Integration

Prerequisites:

  • npm install @jaypie/llm
  • API key for at least one provider (Anthropic, Fireworks, Google, Meta, Mistral, OpenAI, OpenRouter, or xAI)

@jaypie/llm provides a unified interface for calling multiple LLM providers. The Llm class handles provider-specific implementations while exposing a consistent API for operate, stream, and tool calling.

Models are listed as LLM.MODEL.* names rather than ids. The alias is stable across releases; the id behind it moves whenever the provider ships a successor.

Provider Models Env Variable
Anthropic MODEL.SONNET, MODEL.OPUS, MODEL.HAIKU, MODEL.FABLE ANTHROPIC_API_KEY
Bedrock MODEL.NOVA_PRO, MODEL.NOVA_LITE (AWS credentials)
Fireworks MODEL.FIREWORKS.* (DEEPSEEK, GLM, GPT_OSS, INKLING, KIMI, MINIMAX, NEMOTRON, QWEN) FIREWORKS_API_KEY
Google MODEL.GEMINI_FLASH, MODEL.GEMINI_FLASH_LITE, MODEL.GEMINI_PRO GOOGLE_API_KEY
Meta MODEL.MUSE_SPARK, MODEL.MUSE_SPARK_CONTRIBUTOR META_API_KEY (or MODEL_API_KEY)
Mistral MODEL.MISTRAL.* (LARGE, MEDIUM, SMALL, OCR) MISTRAL_API_KEY
OpenAI MODEL.ASTRA, MODEL.SOL, MODEL.LUNA, MODEL.TERRA OPENAI_API_KEY
OpenRouter MODEL.OPENROUTER.* (GLM, LUNA, SONNET) OPENROUTER_API_KEY
xAI MODEL.GROK XAI_API_KEY
Method Purpose Returns
Llm.operate() Single prompt/response string
Llm.stream() Streaming response AsyncGenerator
llm.operate() Instance method with history string
Section titled “Static Method (Recommended for single calls)”
import Llm, { LLM } from "@jaypie/llm";
// Provider auto-detected from model
const response = await Llm.operate("What is 2+2?", {
model: LLM.MODEL.SONNET,
});
console.log(response); // "4"
import Llm, { LLM } from "@jaypie/llm";
const llm = new Llm("anthropic", {
model: LLM.MODEL.SONNET,
system: "You are a helpful assistant.",
});
const response1 = await llm.operate("My name is Alice.");
const response2 = await llm.operate("What is my name?");
// Maintains conversation history

The first constructor argument may be a provider name or a model name. A model name auto-detects the provider and is retained:

const llm = new Llm("claude-sonnet-4-6"); // -> anthropic, claude-sonnet-4-6
// Anthropic models
await Llm.operate(prompt, { model: LLM.MODEL.SONNET });
await Llm.operate(prompt, { model: LLM.MODEL.OPUS });
await Llm.operate(prompt, { model: LLM.MODEL.HAIKU });
// OpenAI models
await Llm.operate(prompt, { model: LLM.MODEL.SOL });
await Llm.operate(prompt, { model: LLM.MODEL.LUNA });
// Google models
await Llm.operate(prompt, { model: LLM.MODEL.GEMINI_FLASH });
const llm = new Llm("openai", {
model: LLM.MODEL.SOL,
});
for await (const chunk of Llm.stream("Tell me a story")) {
process.stdout.write(chunk.content || "");
}
import { expressStreamHandler, createExpressStream } from "jaypie";
import Llm from "@jaypie/llm";
export default expressStreamHandler(async (req, res, context) => {
const stream = createExpressStream(context);
for await (const chunk of Llm.stream(req.body.prompt)) {
stream.write(chunk.content || "");
}
stream.end();
});
import { lambdaStreamHandler } from "jaypie";
import Llm from "@jaypie/llm";
export const handler = awslambda.streamifyResponse(
lambdaStreamHandler(
async (event, context) => {
for await (const chunk of Llm.stream(event.prompt)) {
context.responseStream.write(`data: ${JSON.stringify(chunk)}\n\n`);
}
},
{
contentType: "text/event-stream",
},
),
);
import Llm, { LLM, Toolkit } from "@jaypie/llm";
const toolkit = new Toolkit([
{
name: "get_weather",
description: "Get current weather for a city",
parameters: {
type: "object",
properties: {
city: {
type: "string",
description: "City name",
},
},
required: ["city"],
},
call: async ({ city }) => {
// Actual implementation
return `Weather in ${city}: sunny, 72F`;
},
},
]);
const response = await Llm.operate("What's the weather in NYC?", {
model: LLM.MODEL.SONNET,
tools: toolkit,
});
import { z } from "zod";
import Llm, { Toolkit } from "@jaypie/llm";
const toolkit = new Toolkit([
{
name: "create_user",
description: "Create a new user",
parameters: z.object({
name: z.string().describe("User's full name"),
email: z.string().email().describe("User's email"),
age: z.number().optional().describe("User's age"),
}),
call: async ({ name, email, age }) => {
const user = await db.users.create({ name, email, age });
return { id: user.id, name, email };
},
},
]);
{
name: "transfer_money",
description: "Transfer money between accounts",
parameters: {
type: "object",
properties: {
from: { type: "string" },
to: { type: "string" },
amount: { type: "number" },
},
required: ["from", "to", "amount"],
},
call: async ({ from, to, amount }) => {
if (amount <= 0) {
throw BadRequestError("Amount must be positive");
}
// Process transfer
return { success: true, transactionId: uuid() };
},
}

Pass format to receive guaranteed-valid JSON. format accepts Jaypie’s natural schema syntax (preferred), a raw JSON Schema, or a Zod schema.

const response = await Llm.operate(
"Extract the person's name and age from: 'John is 25 years old'",
{
model: LLM.MODEL.SOL,
format: {
name: String,
age: Number,
},
},
);
// Returns: { name: "John", age: 25 }

Every array field declared in format is guaranteed present in the response as an array — an empty result surfaces as [], never undefined.

const response = await Llm.operate(prompt, {
model: LLM.MODEL.SOL,
format: {
type: "object",
properties: {
name: { type: "string" },
age: { type: "number" },
},
},
});
// Returns: { name: "John", age: 25 }
import { z } from "zod";
const PersonSchema = z.object({
name: z.string(),
age: z.number(),
});
const response = await Llm.operate(prompt, {
model: LLM.MODEL.SOL,
format: PersonSchema,
});
// Returns typed object matching PersonSchema
const llm = new Llm("anthropic", {
model: LLM.MODEL.SONNET,
system: "You are a math tutor.",
});
// Conversation history maintained automatically
await llm.operate("What is calculus?");
await llm.operate("Can you give me an example?");
await llm.operate("How is that used in physics?");
// Access history
console.log(llm.history);
const response = await Llm.operate("What's in this image?", {
model: LLM.MODEL.SONNET,
files: [
{
type: "image",
data: base64ImageData,
mediaType: "image/png",
},
],
});
const response = await Llm.operate("Describe this image", {
model: LLM.MODEL.SOL,
files: [
{
type: "image",
url: "https://example.com/image.jpg",
},
],
});

Seven hooks fire through the operate loop, each receiving the full provider request/response payloads:

const response = await Llm.operate(prompt, {
model: LLM.MODEL.SONNET,
hooks: {
beforeEachModelRequest: ({ providerRequest }) => {
log.trace("[llm] calling model");
},
afterEachModelResponse: ({ content, usage }) => {
log.trace("[llm] response received");
log.var({ tokens: usage[usage.length - 1]?.total });
},
beforeEachTool: ({ toolName, args, message }) => {
// message is the tool's resolved LlmTool.message, when defined
log.trace(message ?? `[llm] calling ${toolName}`);
},
afterEachTool: ({ result, toolName, message }) => {
log.trace(`[llm] ${toolName} returned`);
},
onToolError: ({ error, toolName, message }) => {
log.warn(`[llm] ${toolName} failed`);
log.var({ error: error.message });
},
onRetryableModelError: ({ error }) => {
log.warn("[llm] model call failed, retrying");
},
onUnrecoverableModelError: ({ error }) => {
log.error("[llm] model call failed");
log.var({ error });
},
},
});

For progress reporting (UI updates, websockets, queue notifications), prefer a single onProgress callback over wiring individual hooks. It receives lightweight, serializable events as the operate loop runs:

const response = await Llm.operate(prompt, {
model: LLM.MODEL.SONNET,
tools: toolkit,
onProgress: (event) => {
// event.type: start, model_request, model_response,
// tool_call, tool_result, tool_error, retry, done
websocket.send(JSON.stringify(event));
},
});

Fields carried by each event (turn is 1-indexed):

Event Fields
start model, provider, maxTurns
model_request turn, model
model_response turn, content (text, if any), toolCalls ([{ name, arguments }], if any), usage (this turn)
tool_call turn, tool: { name, arguments, message } — fires before the tool runs; arguments is the JSON string; message is the resolved LlmTool.message, when the tool defines one
tool_result turn, tool: { name } — the result value is deliberately omitted (it can be arbitrarily large); use the afterEachTool hook to receive it
tool_error turn, tool: { name }, error (message string)
retry turn, error (message string)
done turn (total turns used), content (final text or structured output), usage (cumulative)

Errors thrown by the callback are logged and never interrupt the loop. stream() communicates progress through its chunks; onProgress applies to operate().

import { BadGatewayError, log } from "jaypie";
import Llm, { LLM } from "@jaypie/llm";
async function askLlm(prompt) {
try {
return await Llm.operate(prompt, { model: LLM.MODEL.SONNET });
} catch (error) {
log.error("[askLlm] LLM call failed");
log.var({ error: error.message });
throw BadGatewayError("AI service unavailable");
}
}

modelOptions sets provider-specific request fields per model. Keys are an exact model id, a MODEL catalog key, a provider (or the alias gemini), or default, and every attempt in a fallback chain merges the keys that match its own model. providerOptions is deprecated, reaches the primary only, and is removed in 2.0.

max_tokens defaults to the model’s maximum output (capped at 16,384 for non-streaming requests). Override it through modelOptions:

await Llm.operate(prompt, {
model: LLM.MODEL.SONNET,
modelOptions: { anthropic: { max_tokens: 32000 } },
temperature: 0.7,
});
await Llm.operate(prompt, {
model: LLM.MODEL.SOL,
temperature: 0.5,
topP: 0.9,
});