Model: claude-opus-4-6 (anthropic/claude-opus-4-6) Generation Date: 2025-02-17
A Message is the smallest unit of information exchange within a session. OpenCode's message model is elegantly designed, supporting rich content types through the Part system.
The message system is defined in session/message-v2.ts. Messages are divided into two roles:
User Messages:
export const User = Base.extend({
role: z.literal("user"),
time: z.object({ created: z.number() }),
summary: z.object({ // Change summary (optional)
title: z.string().optional(),
body: z.string().optional(),
diffs: Snapshot.FileDiff.array(),
}).optional(),
agent: z.string(), // Name of the Agent being used
model: z.object({ // Model being used
providerID: z.string(),
modelID: z.string(),
}),
system: z.string().optional(), // Additional system instructions
tools: z.record(z.string(), z.boolean()).optional(), // Tool toggles (deprecated)
variant: z.string().optional(), // Variant identifier
})Assistant Messages:
export const Assistant = Base.extend({
role: z.literal("assistant"),
time: z.object({
created: z.number(),
completed: z.number().optional(), // Generation completion time
}),
error: z.discriminatedUnion("name", [
AuthError.Schema,
NamedError.Unknown.Schema,
OutputLengthError.Schema,
AbortedError.Schema,
ContextOverflowError.Schema,
APIError.Schema,
]).optional(),
parentID: z.string(), // ID of the corresponding User message
modelID: z.string(),
providerID: z.string(),
agent: z.string(),
cost: z.number(), // Cost of this response (in USD)
tokens: z.object({
total: z.number().optional(),
input: z.number(), // Input token count
output: z.number(), // Output token count
reasoning: z.number(), // Chain-of-thought token count
cache: z.object({
read: z.number(), // Cache read token count
write: z.number(), // Cache write token count
}),
}),
summary: z.boolean().optional(), // Whether this is a compaction summary
finish: z.string().optional(), // Finish reason
})Token Statistics Field Breakdown:
The tokens object records the token consumption for this LLM call:
input: The number of input tokens sent to the model (including system prompt, message history, tool results, etc.)output: The number of output tokens generated by the modelreasoning: The number of chain-of-thought tokens used by the model (only for models that support chain-of-thought, such as Claude's extended thinking)cache.read: The number of tokens read from the Prompt Cachecache.write: The number of tokens written to the Prompt Cachetotal: Total token count (some Providers return the total directly)
Extended Explanation: Prompt Cache
In multi-turn conversations, each request sends the full message history. The System Prompt and earlier messages are identical in every request. Prompt Cache is an optimization offered by LLM Providers -- it caches these repeated portions on the server side to avoid redundant computation.
cache.writeindicates how many tokens were cached for the first time in this request;cache.readindicates how many tokens hit the cache in this request. Cache-hit tokens are billed at a much lower rate than regular tokens, significantly reducing costs.
The actual content of a message is not stored directly on the Message object; instead, it is organized through the Part system. Each message can contain multiple Parts of different types. All Parts share a common base structure:
const PartBase = z.object({
id: z.string(), // Unique Part identifier
sessionID: z.string(), // ID of the owning session
messageID: z.string(), // ID of the owning message
})Complete Part types (12 types):
| Part Type | Purpose | Key Fields |
|---|---|---|
| TextPart | Text content | text, time, synthetic (whether synthesized), ignored |
| ReasoningPart | Chain of thought | text, time.start, time.end, metadata |
| ToolPart | Tool invocation | tool, callID, state (four states) |
| FilePart | File attachment | mime, filename, url, source |
| AgentPart | Agent reference | name, source |
| SubtaskPart | Subtask | prompt, description, agent, model |
| CompactionPart | Compaction marker | auto (whether auto-triggered) |
| StepStartPart | Step start | snapshot |
| StepFinishPart | Step finish | reason, snapshot, cost, tokens |
| SnapshotPart | Snapshot | snapshot |
| PatchPart | Patch | hash, files |
| RetryPart | Retry marker | attempt, error, time |
The Four States of ToolPart:
ToolPart is the most complex Part type, tracking the full lifecycle of a tool invocation via its state field:
pending ---> running ---> completed
+---> error
// 1. Pending: Tool call identified, arguments still being received
ToolStatePending = { status: "pending", input: {}, raw: "" }
// 2. Running: Arguments complete, execution begins
ToolStateRunning = { status: "running", input: {...}, time: { start } }
// 3. Completed: Execution succeeded
ToolStateCompleted = {
status: "completed",
input: {...}, output: "...",
title: "Read file /src/index.ts",
metadata: { truncated: false },
time: { start, end, compacted? } // compacted: Prune timestamp
}
// 4. Error: Execution failed
ToolStateError = {
status: "error",
input: {...}, error: "Permission denied",
time: { start, end }
}The v2 in the filename hints that this is not the first version of the message model. The coexistence of message.ts (legacy) and message-v2.ts (current) indicates that OpenCode underwent a major refactoring of its message model.
Key improvements in the new version:
- Part System: The legacy version stored all content directly on the message; the new version achieves content modularity through Parts
- Finer-Grained State Tracking: The four-state model for ToolPart, StepStart/StepFinish markers
- Cost Tracking: Each Assistant message records precise token consumption and cost
This incremental migration strategy (keeping the old version, introducing the new version, and gradually switching over) is a common practice in large projects -- it ensures that all existing data is not broken at once.