Chat models and messages
Chronicler already classifies Discord messages with an LLM, through hand-rolled plumbing in @lumiere/openai-json: a four-attempt retry loop, markdown-fence stripping, a manual zod safeParse. This module gives you a grounded verdict on whether LangChain’s structured-output primitives can replace that dance, and along the way you learn to read any LangChain code fluently.
A chat model is a function from messages to a message. In LangChain the model is an object you construct once and call many times.
import { ChatAnthropic } from "@langchain/anthropic";
import { initChatModel } from "langchain";
const model = new ChatAnthropic({ model: "claude-sonnet-4-6" });
// or provider-agnostic, resolved from a string:
const model2 = await initChatModel("anthropic:claude-opus-4-8");initChatModel is the indirection layer: you pass "provider:model" and it constructs the right class. This earns its keep when the provider is config, not code.
Messages are the input. A conversation is an array of message objects: HumanMessage, AIMessage, SystemMessage, ToolMessage. You call await model.invoke(messages) and get back an AIMessage. That is the whole contract at this level.
Try it yourself
The invoke contract
The asymmetry between the two sides is the part worth remembering.
What goes into model.invoke(...) and what comes back?
Reveal answer
An array of message objects (HumanMessage / AIMessage / SystemMessage / ToolMessage) goes in; a single AIMessage comes back.
Why initChatModel
You have a codebase that needs to swap model providers based on a config file, not a code change. Which construction do you reach for?
Show answer
Correct answer: B — initChatModel("provider:model") resolved from config
initChatModel takes a "provider:model" string and resolves the right class for you. That is exactly the indirection you want when the provider is a config value rather than a compile-time choice.