callModel
Call a language model with tools, structured output, and system prompts.
Quick Example
import { callModel } from '@noetic-tools/core';
const chat = callModel({
id: 'chat',
model: 'openai/gpt-4o',
instructions: 'You are a helpful assistant.',
});What It Does
callModel calls a language model. You provide the model name and optionally a system prompt, tools, structured output schema, and generation parameters. The interpreter handles message formatting, tool execution loops, and response parsing.
API Reference
Options
| Property | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique step identifier |
model | Lazy<string> | Yes | Model identifier (e.g. 'openai/gpt-4o', 'claude-sonnet-4-20250514'), or a (ctx) => string getter resolved per execution |
instructions | Lazy<string | undefined> | No | System prompt, sent as the request's instructions field rather than as a conversation item. Any system messages in the log are appended to it. Accepts a (ctx) => string | undefined getter |
tools | Lazy<Tool[] | undefined> | No | Allowed tool subset (undefined = all, [] = none). Accepts a (ctx) => Tool[] | undefined getter; function-form tools are resolved per execution and do not contribute to the pre-computed ctx.unifiedTools |
toolChoice | ToolChoice | No | How the model may use its tools: 'auto' (default), 'required', 'none', or { tool: name } to force one client tool. See Tool Choice |
output | StandardSchemaV1<unknown, O> | No | Schema for structured output — any Standard Schema v1 validator (Zod is the default) |
outputJsonSchema | Record<string, unknown> | No | Explicit JSON Schema override/fallback for non-Zod output. Zod uses its native converter; other validators can implement StandardJSONSchemaV1. A validation-only schema must provide this field or conversion throws MISSING_JSON_SCHEMA |
params | ModelParams | No | Generation parameters |
emit | boolean | (eventType, data) => boolean | No | Controls framework event emission for this step. Defaults to true; false suppresses all framework events; a filter function decides per event |
projection | ProjectionPolicy | No | Projection policy for this step's context window, overriding the harness-level default |
Lazy<T> means T | ((ctx: Context) => T | Promise<T>) — pass a plain value, or a function that the interpreter resolves against the live context each time the step runs (useful for model routing, dynamic prompts, and context-dependent tool sets).
ModelParams
| Property | Type | Description |
|---|---|---|
temperature | number | Sampling temperature |
topP | number | Nucleus sampling threshold |
maxTokens | number | Maximum tokens to generate |
stopSequences | string[] | Deprecated and ignored — the OpenRouter Responses API has no stop parameter, so it is never sent |
serviceTier | ServiceTier | OpenRouter service tier: auto, default, fast (alias for priority), flex, priority, scale, or ultrafast (prefers ultrafast endpoints, falls back to priority, then default) |
The tier the provider served is recorded on ctx.lastStepMeta.serviceTier (and LLMResponse.serviceTier). It stays undefined when the provider reports none rather than echoing the request, so a value that differs from what you asked for means a fallback happened.
import { callModel, ServiceTier } from '@noetic-tools/core';
const quick = callModel({
id: 'quick',
model: 'openai/gpt-4o-mini',
instructions: 'Answer in one line.',
params: { serviceTier: ServiceTier.Ultrafast },
});Tool Interface
Tools passed to callModel follow the standard Noetic Tool interface. See invokeTool for the full definition.
Structured Output
Provide a Zod schema to output and the model response will be parsed and validated at runtime.
import { z } from 'zod';
import { callModel } from '@noetic-tools/core';
const extract = callModel({
id: 'extract-entities',
model: 'openai/gpt-4o',
instructions: 'Extract named entities from the text.',
output: z.object({
people: z.array(z.string()),
places: z.array(z.string()),
organizations: z.array(z.string()),
}),
});Any Standard Schema v1 validator works in place of Zod. Non-Zod schemas are validated via ~standard.validate (sync or Promise results supported) and the parsed/transformed value is returned. The model constraint resolves through Zod, then the Standard JSON Schema v1 trait, then outputJsonSchema. For non-Zod schemas, the explicit field overrides the trait and falls back when conversion throws. See invokeTool for a Valibot trait example.
If the model response does not match the schema, a validation error is thrown (NoeticError kind model_parse_error; see Error Model).
With Tools
import { z } from 'zod';
import { callModel, tool } from '@noetic-tools/core';
const searchTool = tool({
name: 'search',
description: 'Search the knowledge base',
input: z.object({
query: z.string(),
}),
output: z.object({
results: z.array(z.string()),
}),
execute: async (args) => {
return {
results: ['Result 1', 'Result 2'],
};
},
});
const researcher = callModel({
id: 'research',
model: 'openai/gpt-4o',
instructions: 'Answer the question using the search tool.',
tools: [searchTool],
});When the model emits tool calls, the harness runs them and calls the model again with the results, round after round, until the model answers without calling a tool. One callModel step therefore makes as many model requests as it needs (up to 32 tool rounds). A round that comes back with a provider error status or no output is asked again, up to three times in a row; its tokens and cost still count toward the step's usage, since the provider billed it. The step's ctx.lastStepMeta.toolCalls lists every call from every round. To run the whole step again until a step makes no tool calls at all, wrap it in a loop with until.noToolCalls().
Unified Tool Set
Before execution, the harness collects all tools from every callModel step in the step tree plus layer-provided tools into a single unified set. Every LLM call sends the whole set, so the tool definitions stay identical from step to step and the provider's prompt cache keeps hitting. tools on each step narrows which of them the model may call: the harness sends the subset as tool_choice: { type: "allowed_tools" }, and a call naming any other tool comes back as an unknown tool error result without running. A step with tools: [] sends no tools at all.
tools: undefined(or omitted) -- the model may call any tool in the unified set.tools: [searchTool]-- the model may only callsearchTool.tools: []-- no tools are available for this step.
Tool Choice
toolChoice says how the model may use the tools the step allows. It works inside the tools restriction, and every row still sends the full unified set, so the cache prefix holds:
toolChoice | Effect |
|---|---|
'auto' (default) | The model decides whether to call a tool. |
'required' | The model must call at least one of the allowed tools. |
'none' | The model must not call a tool. The tools are still sent, to keep the cache prefix. |
{ tool: 'search' } | The model must call search. |
'required' and { tool } hold for the first model request only. Once the model has called a tool, the next rounds go back to 'auto', so the step can finish with an answer instead of calling tools until the round cap.
import { z } from 'zod';
import { callModel, tool, ToolChoiceMode } from '@noetic-tools/core';
const lookup = tool({
name: 'lookup',
description: 'Look up an order',
input: z.object({ orderId: z.string() }),
output: z.string(),
execute: async ({ orderId }) => `order ${orderId}: shipped`,
});
// Always start with a lookup.
const support = callModel({
id: 'support',
model: 'openai/gpt-4o',
instructions: 'Answer questions about orders.',
tools: [lookup],
toolChoice: { tool: 'lookup' },
});
// Must use some tool, any allowed one.
const triage = callModel({
id: 'triage',
model: 'openai/gpt-4o',
tools: [lookup],
toolChoice: ToolChoiceMode.Required,
});{ tool } must name a client tool the step may call. When tools is an eager array without it, callModel() throws UNKNOWN_TOOL_CHOICE. When tools is omitted or a getter, the same error is thrown at request time if the tool is not in the unified set (or not in the getter's tools). Server tools cannot be forced.
Auto-Injected Layer Tools
Functions declared in a context layer's provides field are automatically included in the unified tool set. These tools are namespaced as layerId/fnName (e.g., scratchpad/update). You do not need to pass them in the tools array -- the runtime resolves them from the active context layers.
Tuning Generation
import { callModel } from '@noetic-tools/core';
const creative = callModel({
id: 'brainstorm',
model: 'openai/gpt-4o',
instructions: 'Generate creative ideas.',
params: {
temperature: 0.9,
maxTokens: 2e3,
},
});Related
- runCode -- custom async logic.
- invokeTool -- invoke a tool directly without an LLM.
- Loop & Until -- wrap a callModel step in a ReAct-style loop.