content

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

Documentation

Overview

Package content defines the unified content vocabulary shared across all internal packages. Block is a sealed interface; the concrete payload type is the discriminator. Only this package can add variants (unexported marker).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MarshalBlock

func MarshalBlock(b Block) ([]byte, error)

MarshalBlock writes {"type": <tag>, ...payload}. The payload is marshaled first (so ToolResultBlock's custom MarshalJSON runs), then the tag is merged in as a sibling key — never via an embedding wrapper, which would let a Marshaler payload shadow the "type" key. Key order is not significant.

func MarshalBlocks

func MarshalBlocks(bs []Block) ([]byte, error)

MarshalBlocks encodes a []Block as a JSON array of tagged blocks.

Types

type AIMessage

type AIMessage struct {
	Message
	Usage *Usage
}

AIMessage is a turn authored by the AI model.

func (AIMessage) MarshalJSON

func (m AIMessage) MarshalJSON() ([]byte, error)

func (*AIMessage) UnmarshalJSON

func (m *AIMessage) UnmarshalJSON(data []byte) error

type AgenticMessages

type AgenticMessages []Conversation

AgenticMessages is an ordered conversation thread. A nil or empty slice is a valid zero value representing an empty thread.

type AudioBlock

type AudioBlock struct {
	MediaType MediaType
	Data      []byte
}

type Block

type Block interface {
	// contains filtered or unexported methods
}

Block is the sealed interface over all content block payloads. The concrete type is the discriminator; there is no Type field and no nil-able payload pointers. BlockType is retained only as the wire tag for the JSON codec (block_json.go, added in a later task), not as a field on any in-memory value.

func CloneBlock

func CloneBlock(block Block) Block

CloneBlock returns a block that holds the same value as block and shares none of its memory.

Why this lives in content

Copying a block is not a consumer concern. Before this function existed the workspace held five independent implementations — the loop runtime's message clone, the hook payload clone, the compaction wire adapter, the foreign-loop snapshot, and a product translator — written with three different techniques, and they had already drifted into disagreeing about the same input. Every one of them was a hand-maintained type switch over a union only this package can extend, which put the switch and the union in different modules with a release boundary between them: content could grow a variant, and the copy in another module would keep compiling while silently dropping it. With the switch here, a new variant and its copy arm land in the same commit, and this package's own test refuses a variant that has no arm.

How the copy is made

Each arm copies the struct WHOLE and then re-copies the reference-backed fields, rather than naming every field in a literal. A field added to a block is therefore carried by the struct copy without touching this function, which is the drift a literal cannot survive: a literal that forgets a new field still compiles, still passes, and loses the field. That is exactly how ThinkingBlock.ProviderState and ToolUseBlock.ProviderState went missing from three separate copies at once.

The residue that a struct copy alone cannot handle is a new field that is itself reference-backed: it would be copied by reference and alias the original. That is covered from the other side, by content/blocktest.AssertIndependent, which walks a cloned fixture with reflection and reports any byte array or pointer the copy still shares. The two together are what make this field-drift-proof: the struct copy carries unknown fields, and the reflection guard proves the ones that need deep copies got them.

Nil versus empty is preserved exactly

A clone is a copy, not a normalization. CloneBlock reproduces a nil json.RawMessage as nil and an empty non-nil one as empty non-nil, and it leaves a half-set ProviderState/ProviderStateFormat pair exactly as it found it — it does NOT route the copy through NewThinkingBlock or NewToolUseBlock.

Those constructors normalize on purpose: they are the boundary where a block is CREATED from untrusted parts, and clearing a format label that labels nothing belongs there. A copy is a different boundary. Three reasons this one must be faithful:

  • reflect.DeepEqual(block, CloneBlock(block)) holds for every block. That equality is the assertion every consumer's round-trip test wants to make, and DeepEqual distinguishes a nil slice from an empty one. A normalizing clone forces those tests to compare loosely instead, and a loose comparison is precisely how a dropped field hides.
  • The distinction is observable. A nil json.RawMessage marshals to null; an empty non-nil one is not valid JSON and fails to marshal. A copy that turns the second into the first repairs a broken value in transit, so the defect surfaces somewhere other than where it was introduced.
  • Callers who want normalization can still have it, by calling the constructor themselves at the point they mean it. Callers who want a copy cannot recover fidelity that a copy already threw away.

Return values

A nil Block interface clones to a nil Block: nothing copied faithfully is still nothing. A typed-nil payload pointer such as (*TextBlock)(nil) clones to the same typed nil, because there is no struct to copy; the interface stays non-nil, so a caller can tell the two apart. Consumers that treat either case as a fault keep their own policy — hook panics, the foreign-loop snapshot returns an error, the loop runtime drops the block — by inspecting the result rather than by maintaining a switch of their own.

A member of the sealed union with no arm below panics. It cannot be reached without editing this package: Block's marker method is unexported, so no type outside content can join the union, and TestCloneBlockCoversEverySealedVariant fails on a variant added here without an arm. The panic is the report of a bug in this file, not a runtime condition any caller can cause or handle.

func CloneBlocks

func CloneBlocks(blocks []Block) []Block

CloneBlocks returns an independent copy of every block in blocks.

The slice's own nil-versus-empty state is preserved for the same reason the blocks' fields are: a nil []Block and an empty one are different values, and DeepEqual says so.

func UnmarshalBlock

func UnmarshalBlock(data []byte) (Block, error)

UnmarshalBlock reads the tag, allocates the concrete type, and decodes the same bytes into it (the extra "type" key is ignored by the struct decode).

func UnmarshalBlocks

func UnmarshalBlocks(data []byte) ([]Block, error)

UnmarshalBlocks decodes a JSON array of tagged blocks. It is the single recursion point for nested content (ToolResultBlock.Content) and enforces the element-count cap.

type BlockDecodeError

type BlockDecodeError struct{ Cause error }

BlockDecodeError wraps a failure to unmarshal serialized block bytes.

func (*BlockDecodeError) Error

func (e *BlockDecodeError) Error() string

func (*BlockDecodeError) Unwrap

func (e *BlockDecodeError) Unwrap() error

type BlockEncodeError

type BlockEncodeError struct {
	Type  BlockType
	Cause error
}

BlockEncodeError wraps a failure to marshal a concrete block payload.

func (*BlockEncodeError) Error

func (e *BlockEncodeError) Error() string

func (*BlockEncodeError) Unwrap

func (e *BlockEncodeError) Unwrap() error

type BlockLimitError

type BlockLimitError struct {
	Limit string // "block_bytes" | "slice_count"
	Got   int
	Max   int
}

BlockLimitError is returned when serialized input exceeds a codec safety cap.

func (*BlockLimitError) Error

func (e *BlockLimitError) Error() string

type BlockType

type BlockType string
const (
	TypeText       BlockType = "text"
	TypeImage      BlockType = "image"
	TypeAudio      BlockType = "audio"
	TypeDocument   BlockType = "document"
	TypeThinking   BlockType = "thinking"
	TypeToolUse    BlockType = "tool_use"
	TypeToolResult BlockType = "tool_result"
	TypeRefusal    BlockType = "refusal"
)

type Chunk

type Chunk interface {
	// contains filtered or unexported methods
}

Chunk is the sealed interface over streaming content deltas. Separate from Block because complete blocks have fields that may arrive as terminal deltas (for example, a reasoning signature). Chunks are never serialized, so there is no codec and no ChunkType wire tag.

type Conversation

type Conversation interface {
	// contains filtered or unexported methods
}

Conversation is a sealed interface: only message types defined in this package can participate in a conversation thread. The unexported marker method prevents external packages from satisfying the interface accidentally, keeping the discriminated union closed.

type DocumentBlock

type DocumentBlock struct {
	MediaType MediaType
	Name      string
	Data      []byte
	Text      string
}

DocumentBlock carries document data. Either Data (binary) or Text (extracted text) may be populated depending on how the document was provided.

type ImageBlock

type ImageBlock struct {
	MediaType MediaType
	Source    ImageSource
}

type ImageChunk

type ImageChunk struct {
	Index     int         // image's position in the response
	MediaType MediaType   // typically set on the first delta for this Index
	Source    ImageSource // Data fragments append; URL arrives whole
}

ImageChunk is a streaming delta of image output — the streaming counterpart of ImageBlock, which it mirrors field for field so a codec maps the two the same way. The accumulator folds deltas into ImageBlocks.

Index identifies WHICH image of the response this delta belongs to, and it is load-bearing in a way TextChunk's absent index is not. Text and refusal deltas concatenate harmlessly, but image bytes do not: splicing the tail of one image onto another yields a corrupt file that no decoder can recover and that no validation in this package can detect. A single response may legitimately carry several images, so per-image identity has to survive the stream or the stream cannot be represented at all. Producers emitting one image may leave Index at its zero value; every delta then accumulates into that one image.

Source is a DELTA, not a complete source, and its two arms accumulate differently because they arrive differently. Source.Data holds RAW (already base64-decoded) bytes that append, in arrival order, to the bytes previously seen for this Index — decoding must happen in the codec, since base64 fragments only concatenate correctly on 4-character boundaries. Source.URL, by contrast, always arrives whole; it is never fragmented, so a later non-empty URL replaces an earlier one rather than extending it. MediaType typically arrives once on the first delta for an Index and likewise takes the last non-empty value.

A provider that streams successive complete PREVIEWS of one image rather than byte fragments (OpenAI's `partial_image_b64`, where each event is a whole standalone image at increasing fidelity) MUST NOT map those previews onto a single Index. Doing so would concatenate several complete images into one corrupt blob. Such a codec either drops the previews and emits only the final image, or gives each preview its own Index so each materializes as its own ImageBlock.

type ImageSource

type ImageSource struct {
	URL  string
	Data []byte
}

ImageSource is a sum type for the origin of image data. Set exactly one of URL (remote) or Data (inline bytes).

type MediaType

type MediaType string

MediaType is the IANA media type (MIME type) for block content. Named constants below cover the types accepted by current AI providers; callers may construct other values for provider-specific or future types.

const (
	MediaTypeImageJPEG MediaType = "image/jpeg"    // .jpg / .jpeg
	MediaTypeImagePNG  MediaType = "image/png"     // .png
	MediaTypeImageGIF  MediaType = "image/gif"     // .gif
	MediaTypeImageWebP MediaType = "image/webp"    // .webp
	MediaTypeImageSVG  MediaType = "image/svg+xml" // .svg
)

Image MIME types accepted by multimodal providers.

const (
	MediaTypeAudioMPEG MediaType = "audio/mpeg" // .mp3
	MediaTypeAudioWAV  MediaType = "audio/wav"  // .wav
	MediaTypeAudioOGG  MediaType = "audio/ogg"  // .ogg
	MediaTypeAudioFLAC MediaType = "audio/flac" // .flac
	MediaTypeAudioAAC  MediaType = "audio/aac"  // .aac
	MediaTypeAudioMP4  MediaType = "audio/mp4"  // .m4a
	MediaTypeAudioWebM MediaType = "audio/webm" // .webm
)

Audio MIME types.

const (
	MediaTypeDocumentPDF      MediaType = "application/pdf" // .pdf
	MediaTypeDocumentText     MediaType = "text/plain"      // .txt
	MediaTypeDocumentHTML     MediaType = "text/html"       // .html
	MediaTypeDocumentCSV      MediaType = "text/csv"        // .csv
	MediaTypeDocumentMarkdown MediaType = "text/markdown"   // .md
	// Office open XML formats — the modern .docx/.xlsx wire types.
	MediaTypeDocumentDOCX MediaType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
	MediaTypeDocumentXLSX MediaType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)

Document MIME types.

type Message

type Message struct {
	Role   Role
	Blocks []Block
}

Message is the base type for all conversation turns: a role and an ordered sequence of content blocks. Typed message structs embed this so the role and blocks are always accessible via the embedded field.

func (Message) MarshalJSON

func (m Message) MarshalJSON() ([]byte, error)

func (*Message) UnmarshalJSON

func (m *Message) UnmarshalJSON(data []byte) error

type NilBlockError

type NilBlockError struct{ Type BlockType }

NilBlockError is returned by MarshalBlock when a Block holds a typed-nil payload pointer (e.g. (*TextBlock)(nil)). Construction always uses non-nil &content.X{} literals, so this indicates a caller bug; the codec fails secure rather than emit an empty typed block that could mask data loss on restore.

func (*NilBlockError) Error

func (e *NilBlockError) Error() string

type RefusalBlock

type RefusalBlock struct {
	Text string
}

RefusalBlock carries the model's stated reason for declining to answer.

It is deliberately its OWN variant rather than a flavor of TextBlock. Every major provider models a refusal as a channel separate from the assistant's ordinary output — OpenAI declares `refusal` as a required member of the response message, emits a `refusal` streaming delta, and carries a `refusal` content part in the Responses API — and the two are not interchangeable: a refusal is the model saying it will not produce the requested output, whereas text is the output. Collapsing the two costs the caller the only signal that distinguishes "declined" from "answered", and the failure is silent in the worst direction: a structured-output refusal arrives with no text parts at all, so a decoder that has nowhere to put the refusal yields a zero-block SUCCESS. The caller then reports an empty answer for a request the model actively refused. Because a *RefusalBlock never matches a *TextBlock type switch arm, every exhaustive consumer is forced to decide what a refusal means instead of inheriting that failure by default.

Text is the refusal message as the provider worded it. An empty Text is a meaningful value, not an absent one: a provider may report a refusal with no explanation, and the presence of the block — not its contents — is the signal.

type RefusalChunk

type RefusalChunk struct{ Text string }

RefusalChunk is a streaming delta of a refusal (OpenAI's `refusal` delta on the chat-completions stream and its `response.refusal.delta` event on the Responses stream). Deltas accumulate into a single RefusalBlock; see that type for why a refusal is not modeled as text.

It carries NO Index, and the omission is deliberate. Index exists on ThinkingChunk and ToolUseChunk because folding those blocks together destroys state that cannot be reconstructed — a per-block reasoning signature, or the boundary between two tool calls' argument JSON — and the damage is silent: the concatenated result is still well-formed but wrong. A refusal carries no signature, no opaque provider state, and no identity that is ever replayed to a provider; it is terminal output whose only content is prose. Concatenating refusal deltas is therefore lossless in exactly the way concatenating TextChunk deltas is, and TextChunk is the precedent this type follows. If a provider ever attaches per-refusal identity that must survive a round trip, adding an Index then is an additive change; adding one now would be an unused field that invites codecs to invent index semantics no provider supplies.

type Role

type Role string

Role identifies the author of a message in a conversation thread.

const (
	RoleUser      Role = "user"
	RoleAssistant Role = "assistant"
	RoleSystem    Role = "system"
	RoleTool      Role = "tool"
)

type SystemMessage

type SystemMessage struct{ Message }

SystemMessage carries a system prompt that shapes model behavior.

type TextBlock

type TextBlock struct {
	Text string
}

type TextChunk

type TextChunk struct{ Text string }

type ThinkingBlock

type ThinkingBlock struct {
	Thinking            string
	Signature           string
	SignatureFormat     string          `json:"SignatureFormat,omitempty"`
	ProviderState       json.RawMessage `json:"ProviderState,omitempty"`
	ProviderStateFormat string          `json:"ProviderStateFormat,omitempty"`
}

ThinkingBlock carries model reasoning text. Signature is empty during streaming and non-empty only on a complete block.

The two provider-private channels, and why both are tagged

A reasoning block carries provider-private continuation state in EITHER of two shapes, and this type models both because a single dialect uses both:

  • Signature is the dialect's native, cleartext-adjacent seal over the visible reasoning text — Anthropic's `thinking.signature`, Bedrock Converse's `reasoningText.signature`. It accompanies a NON-EMPTY Thinking.
  • ProviderState is a wholly opaque payload with no visible counterpart — Anthropic's `redacted_thinking.data`, a Gemini thoughtSignature, an OpenAI Responses encrypted_content. Thinking is empty when it is set.

The two therefore COEXIST on one type but are never both populated by the same wire block, and neither may stand in for the other. Each carries its own format label, because a value with no label is a value whose issuer is unknown, and replaying provider-private state to an issuer that did not mint it is a guaranteed rejection.

SignatureFormat is an opaque, codec-chosen label naming the dialect that MINTED Signature (for example "anthropic" or "bedrock-converse"). It is meaningless and unset whenever Signature is empty. It exists because a signature is cryptographically verified by its issuer: replaying a Bedrock-minted Claude signature to api.anthropic.com draws `messages.N.content.0: Invalid signature in thinking block` (HTTP 400), and Bedrock and Anthropic are two endpoints for the SAME model family, so the blocks are byte-identical in every respect except the one that matters. Dropping a foreign signature is not a safe degrade either: an unsigned thinking block draws the same 400. A codec that cannot claim a signature must therefore FAIL, not forward it and not silently strip it. Read it only through SignatureReplayableAs, never through the field.

ProviderStateFormat is the same label for ProviderState (for example "gemini" or "openai-responses"). It is meaningless and unset whenever ProviderState is empty. This field exists to satisfy the inference gateway's first-milestone requirement that opaque replay state is never translated across provider dialects (see docs/plans/2026-07-31-inference-gateway-design.md, "Thinking" section): a codec MUST NEVER replay ProviderState toward a wire field it owns unless ProviderStateFormat equals that codec's own label; otherwise it MUST treat ProviderState as absent. This is the load-bearing invariant that prevents one provider's opaque bytes (e.g. a Gemini thoughtSignature) from being forwarded to a different provider (e.g. as an OpenAI Responses encrypted_content) as if it were that provider's own native state.

Construct via NewSignedThinkingBlock (or NewThinkingBlock for a block with no signature) so the bytes are defensively copied and a label that labels nothing is normalized away; a bare struct literal aliases the caller's slice and can pair a signature with no format.

func NewSignedThinkingBlock

func NewSignedThinkingBlock(thinking, signature, signatureFormat string, providerState json.RawMessage, providerStateFormat string) *ThinkingBlock

NewSignedThinkingBlock builds a ThinkingBlock, defensively copying providerState so the caller cannot mutate the retained block through its input slice. signatureFormat tags which dialect minted signature and providerStateFormat tags which dialect encoded providerState; see the ThinkingBlock doc comment for the invariant those labels enforce.

Each pair is normalized independently: a label with nothing to label is cleared, so "format, but no value" cannot be constructed here. The reverse — a value with no label — is preserved rather than dropped, because discarding a signature at construction would lose it silently at the one place nothing reports; the codecs refuse it loudly at encode time instead.

func NewThinkingBlock

func NewThinkingBlock(thinking, signature string, providerState json.RawMessage, providerStateFormat string) *ThinkingBlock

NewThinkingBlock builds a ThinkingBlock whose signature, if any, carries NO dialect label. It exists for the decoders that produce reasoning with no native signature at all (Gemini's thought parts, Anthropic's redacted_thinking), where the opaque payload travels in providerState.

A non-empty signature passed here is UNTAGGED and every codec will refuse to put it on the wire, because an unlabelled signature has no provable issuer. Use NewSignedThinkingBlock whenever a signature is present.

func (*ThinkingBlock) ReplayableAs

func (b *ThinkingBlock) ReplayableAs(format string) bool

ReplayableAs reports whether b carries provider-opaque state safe to replay toward a wire field owned by the dialect labeled format. False for a nil receiver, an empty ProviderState, or a ProviderStateFormat that does not exactly match format — the same "treat as absent" degrade every caller of this method must already apply on a false result. See the ProviderStateFormat field doc for the cross-dialect-replay invariant this method exists to let every call site enforce identically.

func (*ThinkingBlock) SignatureReplayableAs

func (b *ThinkingBlock) SignatureReplayableAs(format string) (string, bool)

SignatureReplayableAs returns the reasoning signature b carries and whether it is safe to replay toward a wire field owned by the dialect labeled format. It is false for a nil receiver, an empty Signature, an empty format, or a SignatureFormat that does not exactly match format.

A false result with a NON-EMPTY Signature is not a "treat as absent" degrade, which is where this method's contract differs from ReplayableAs: the caller holds a signature minted by somebody else, and both available degrades — forwarding it, or stripping it and sending an unsigned thinking block — are rejected by the issuing API. Such a caller must fail closed with a diagnostic naming the foreign format. See the SignatureFormat field doc.

type ThinkingChunk

type ThinkingChunk struct {
	Index               int // reasoning block's position in the response
	Thinking            string
	Signature           string
	SignatureFormat     string
	ProviderState       json.RawMessage
	ProviderStateFormat string
}

ThinkingChunk carries a reasoning-text, reasoning-signature, or opaque provider-state delta. Signature/SignatureFormat and ProviderState/ProviderStateFormat have the same replay-scoping semantics as ThinkingBlock: codecs must only replay state whose format matches their own dialect, and a signature they cannot claim is a hard error rather than a field to drop. The stream accumulator defensively copies ProviderState before retaining it, and carries both labels onto the block it folds, so the streaming path reconstructs exactly the continuation state the non-streaming decoder does — including the provenance of the signature.

Index identifies WHICH reasoning block of the response this delta belongs to, exactly as ToolUseChunk.Index does for tool calls. A response may contain more than one reasoning block (Anthropic interleaved thinking emits a fresh thinking / redacted_thinking block around every tool call), and each block carries its OWN signature: the replayed sequence of thinking blocks must match the sequence the model generated, signature-for-block. Folding several blocks into one would therefore destroy continuation state that the non-streaming decoders preserve. Producers that emit a single reasoning block may leave Index at its zero value; every delta then accumulates into that one block.

func (*ThinkingChunk) SignatureReplayableAs

func (c *ThinkingChunk) SignatureReplayableAs(format string) (string, bool)

SignatureReplayableAs is ThinkingBlock.SignatureReplayableAs for a delta, and carries the identical contract: a false result with a non-empty Signature means the chunk holds another dialect's signature, which the caller must refuse rather than forward or strip.

type TokenCount

type TokenCount uint64

TokenCount is a normalized count of model tokens.

type ToolResultBlock

type ToolResultBlock struct {
	ToolUseID string
	Content   []Block
	IsError   bool
}

ToolResultBlock nests its own []Block, so it implements json.Marshaler / json.Unmarshaler in block_json.go (a later task). Do not add a Type field.

func (*ToolResultBlock) MarshalJSON

func (t *ToolResultBlock) MarshalJSON() ([]byte, error)

func (*ToolResultBlock) UnmarshalJSON

func (t *ToolResultBlock) UnmarshalJSON(data []byte) error

type ToolResultMessage

type ToolResultMessage struct {
	Message
	ToolUseID string
	IsError   bool
}

ToolResultMessage carries the result of a tool invocation back to the model. ToolUseID ties this result to the specific ToolUseBlock that requested it. IsError is true when the tool reported an error result; it is the message-level error signal the loop carries from the result and the display layer reads.

func (ToolResultMessage) MarshalJSON

func (m ToolResultMessage) MarshalJSON() ([]byte, error)

func (*ToolResultMessage) UnmarshalJSON

func (m *ToolResultMessage) UnmarshalJSON(data []byte) error

type ToolUseBlock

type ToolUseBlock struct {
	ID                  string
	Name                string
	Input               json.RawMessage
	ProviderState       json.RawMessage `json:"ProviderState,omitempty"`
	ProviderStateFormat string          `json:"ProviderStateFormat,omitempty"`
}

func NewToolUseBlock

func NewToolUseBlock(id, name string, input, providerState json.RawMessage, providerStateFormat string) *ToolUseBlock

NewToolUseBlock builds a ToolUseBlock, defensively copying both raw-message inputs so callers cannot mutate the retained block through their slices. ProviderStateFormat scopes providerState to its issuing codec dialect.

func (*ToolUseBlock) ReplayableAs

func (b *ToolUseBlock) ReplayableAs(format string) bool

ReplayableAs reports whether b carries provider-opaque state safe to replay toward a wire field owned by the dialect labeled format.

type ToolUseChunk

type ToolUseChunk struct {
	Index               int    // tool call's position in the response
	ID                  string // tool_use id (may arrive only on the first delta for this Index)
	Name                string // tool name (likewise)
	InputJSON           string // partial JSON delta of the arguments
	ProviderState       json.RawMessage
	ProviderStateFormat string
}

ToolUseChunk is a streaming delta of a tool call. Providers emit these as they parse function-call deltas; the runner accumulates by Index into a ToolUseBlock.

type UnknownBlockTypeError

type UnknownBlockTypeError struct{ Type BlockType }

UnknownBlockTypeError is returned by the codec when serialized bytes carry a tag with no concrete type (including the empty tag). The restore path is an untrusted boundary; callers fail secure on this error.

func (*UnknownBlockTypeError) Error

func (e *UnknownBlockTypeError) Error() string

type Usage

type Usage struct {
	InputTokens         TokenCount
	OutputTokens        TokenCount
	CacheReadTokens     TokenCount
	CacheCreationTokens TokenCount
	// ReasoningTokens is the part of OutputTokens the model spent on internal
	// reasoning. It is a SUBSET of OutputTokens, never a separate addend: this
	// is why TotalTokens adds only context and output, and why a distinct
	// reasoning price applies to ReasoningTokens with the output price applying
	// to OutputTokens-ReasoningTokens rather than to the whole of it.
	//
	// The convention was undocumented until it destroyed a generation, so it is
	// recorded here against each format's own published contract:
	//
	//   - OpenAI Chat Completions — INSIDE. completion_tokens_details is a
	//     "Breakdown of tokens used in a completion", and its sibling
	//     rejected_prediction_tokens states the relationship outright: such
	//     tokens, "like reasoning tokens, ... are still counted in the total
	//     completion tokens for purposes of billing, output, and context window
	//     limits" (github.com/openai/openai-openapi, CompletionUsage).
	//   - OpenAI Responses — INSIDE. Reasoning tokens "are billed as output
	//     tokens", and the guide's worked example reports input_tokens 75 with
	//     output_tokens 1186 and total_tokens 1261 while 1024 of that output is
	//     reasoning, so reasoning is not a third addend
	//     (developers.openai.com/api/docs/guides/reasoning).
	//   - Anthropic Messages — INSIDE, and stated most explicitly of all:
	//     "output_tokens remains the inclusive, authoritative total used for
	//     billing", with thinking_tokens "Always <= output_tokens"
	//     (platform.claude.com/docs/en/api/messages).
	//   - Gemini — OUTSIDE, and the one format that must be converted.
	//     totalTokenCount is documented as "prompt + thoughts + response
	//     candidates", naming thoughts as an addend alongside candidates, so
	//     codec/geminiapi adds thoughtsTokenCount to candidatesTokenCount on
	//     that documented basis before filling this field
	//     (generativelanguage.googleapis.com discovery document, UsageMetadata).
	//   - Bedrock Converse — silent, and moot: com.amazonaws.bedrockruntime
	//     #TokenUsage carries no reasoning member at all, so this stays zero
	//     even for a Claude model whose reply contains reasoning content.
	//
	// A provider can contradict its own documentation, and one does. OpenRouter
	// documents the subset relationship — reasoning tokens are "considered
	// output tokens and charged accordingly", completion_tokens_details is a
	// "Breakdown of completion tokens", and total_tokens is the "Sum of the
	// above two fields", prompt and completion, with no reasoning addend
	// (openrouter.ai/docs/use-cases/reasoning-tokens,
	// openrouter.ai/docs/api-reference/overview) — yet returned
	// completion_tokens=216 alongside reasoning_tokens=226 on a complete HTTP
	// 200. No published arithmetic reconciles those two numbers, so counts that
	// break the convention are carried exactly as reported, are observable
	// through ReasoningWithinOutput, and never discard the generation they
	// describe.
	ReasoningTokens TokenCount
}

Usage is normalized model token usage.

func (Usage) Add

func (u Usage) Add(other Usage) (Usage, error)

Add combines two usage values field by field. The only failure is a sum that TokenCount cannot represent, which is a representability fault rather than an accounting one: an operand whose reasoning exceeds its output is summed like any other, because refusing it would let one divergent provider report invalidate every later aggregate that folds it in.

func (Usage) ContextTokens

func (u Usage) ContextTokens() (TokenCount, error)

ContextTokens returns all input tokens that occupy model context.

func (Usage) ReasoningWithinOutput

func (u Usage) ReasoningWithinOutput() bool

ReasoningWithinOutput reports whether these counts satisfy the subset convention documented on ReasoningTokens.

It is deliberately a predicate rather than a validation error. This check used to be Usage.Validate, called fatally from every decode, serialization and aggregation path, so a single provider whose reasoning count disagreed with its output count cost the caller a completed generation, made a stored transcript undecodable, and poisoned a session's running total. An accounting field is a metric; the content is the product. Report the divergence here and price or annotate around it — nothing gates on it.

func (Usage) TotalTokens

func (u Usage) TotalTokens() (TokenCount, error)

TotalTokens returns context plus output tokens.

func (Usage) Validate

func (u Usage) Validate() error

Validate verifies the historical reasoning/output relationship. Deprecated: prefer ReasoningWithinOutput. This method is retained so the compatible API addition does not break callers compiled against core v0.5.

type UsageField

type UsageField string

UsageField identifies a normalized usage value or derived total.

const (
	UsageFieldInputTokens         UsageField = "InputTokens"
	UsageFieldOutputTokens        UsageField = "OutputTokens"
	UsageFieldCacheReadTokens     UsageField = "CacheReadTokens"
	UsageFieldCacheCreationTokens UsageField = "CacheCreationTokens"
	UsageFieldReasoningTokens     UsageField = "ReasoningTokens"
	UsageFieldContextTokens       UsageField = "ContextTokens"
	UsageFieldTotalTokens         UsageField = "TotalTokens"
)

type UsageOverflowError

type UsageOverflowError struct {
	Field UsageField
	Left  TokenCount
	Right TokenCount
}

UsageOverflowError reports a token-count addition that cannot be represented.

func (*UsageOverflowError) Error

func (e *UsageOverflowError) Error() string

type UsageValidationError

type UsageValidationError struct {
	Field  UsageField
	Reason UsageValidationReason
}

UsageValidationError reports an invalid relationship between usage fields. It remains available for source compatibility; decoding and aggregation no longer use this condition as a fatal gate.

func (*UsageValidationError) Error

func (e *UsageValidationError) Error() string

type UsageValidationReason

type UsageValidationReason string

UsageValidationReason identifies why normalized usage is invalid. Deprecated: use Usage.ReasoningWithinOutput when only a predicate is needed.

const UsageValidationReasonReasoningExceedsOutput UsageValidationReason = "exceeds OutputTokens"

type UserMessage

type UserMessage struct{ Message }

UserMessage is a turn authored by the human user.

Directories

Path Synopsis
Package blocktest builds fully populated content.Block fixtures by reflection so every copy, encode, and decode path in the workspace can be tested for FIELD-BY-FIELD completeness rather than for the handful of fields whose names a hand-written fixture happened to mention.
Package blocktest builds fully populated content.Block fixtures by reflection so every copy, encode, and decode path in the workspace can be tested for FIELD-BY-FIELD completeness rather than for the handful of fields whose names a hand-written fixture happened to mention.
Package streamaccumulator folds streaming content chunks into complete content blocks.
Package streamaccumulator folds streaming content chunks into complete content blocks.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL