NOETIC
Framework
Steps

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

PropertyTypeRequiredDescription
idstringYesUnique step identifier
modelLazy<string>YesModel identifier (e.g. 'openai/gpt-4o', 'claude-sonnet-4-20250514'), or a (ctx) => string getter resolved per execution
instructionsLazy<string | undefined>NoSystem 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
toolsLazy<Tool[] | undefined>NoAllowed 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
toolChoiceToolChoiceNoHow the model may use its tools: 'auto' (default), 'required', 'none', or { tool: name } to force one client tool. See Tool Choice
outputStandardSchemaV1<unknown, O>NoSchema for structured output — any Standard Schema v1 validator (Zod is the default)
outputJsonSchemaRecord<string, unknown>NoExplicit 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
paramsModelParamsNoGeneration parameters
emitboolean | (eventType, data) => booleanNoControls framework event emission for this step. Defaults to true; false suppresses all framework events; a filter function decides per event
projectionProjectionPolicyNoProjection 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

PropertyTypeDescription
temperaturenumberSampling temperature
topPnumberNucleus sampling threshold
maxTokensnumberMaximum tokens to generate
stopSequencesstring[]Deprecated and ignored — the OpenRouter Responses API has no stop parameter, so it is never sent
serviceTierServiceTierOpenRouter 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 call searchTool.
  • 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:

toolChoiceEffect
'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,
  },
});
  • runCode -- custom async logic.
  • invokeTool -- invoke a tool directly without an LLM.
  • Loop & Until -- wrap a callModel step in a ReAct-style loop.

On this page