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 ¶
- func MarshalBlock(b Block) ([]byte, error)
- func MarshalBlocks(bs []Block) ([]byte, error)
- type AIMessage
- type AgenticMessages
- type AudioBlock
- type Block
- type BlockDecodeError
- type BlockEncodeError
- type BlockLimitError
- type BlockType
- type Chunk
- type Conversation
- type DocumentBlock
- type ImageBlock
- type ImageChunk
- type ImageSource
- type MediaType
- type Message
- type NilBlockError
- type RefusalBlock
- type RefusalChunk
- type Role
- type SystemMessage
- type TextBlock
- type TextChunk
- type ThinkingBlock
- type ThinkingChunk
- type TokenCount
- type ToolResultBlock
- type ToolResultMessage
- type ToolUseBlock
- type ToolUseChunk
- type UnknownBlockTypeError
- type Usage
- type UsageField
- type UsageOverflowError
- type UsageValidationError
- type UsageValidationReason
- type UserMessage
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func MarshalBlock ¶
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 ¶
MarshalBlocks encodes a []Block as a JSON array of tagged blocks.
Types ¶
type AIMessage ¶
AIMessage is a turn authored by the AI model.
func (AIMessage) MarshalJSON ¶
func (*AIMessage) UnmarshalJSON ¶
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 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
BlockLimitError is returned when serialized input exceeds a codec safety cap.
func (*BlockLimitError) Error ¶
func (e *BlockLimitError) Error() string
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 ¶
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 ¶
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 ¶
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 (*Message) UnmarshalJSON ¶
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 SystemMessage ¶
type SystemMessage struct{ Message }
SystemMessage carries a system prompt that shapes model behavior.
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 ToolResultBlock ¶
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 ¶
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 ¶
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 ¶
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.
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.
Source Files
¶
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. |