How DeepSeek-Reasonix Generates and Streams Tool Schemas to the Model
DeepSeek-Reasonix generates tool schemas by requiring each tool to implement a Schema() method that returns JSON metadata, canonicalizes those schemas once per run via provider.CanonicalizeSchema, and streams them to the model as a stable, alphabetically-sorted list before every turn.
DeepSeek-Reasonix treats every tool as a self-describing plugin that exposes its own parameter structure through JSON Schema. When the system initializes a chat session, it processes these schemas through a canonicalization pipeline to ensure deterministic ordering, then streams the complete tool definitions to the language model before any user interaction occurs.
The Tool Interface and Schema Contract
In internal/tool/tool.go, the system defines a Tool interface that every plugin must implement. The critical method for schema generation is Schema() json.RawMessage, which returns the raw JSON Schema describing the tool's parameters, allowing the model to understand exactly what arguments a tool accepts.
Implementing the Tool Interface
Each concrete tool provides its own schema definition. For example, the built-in write_file tool in internal/tool/builtin/writefile.go implements the interface by returning a minimal JSON Schema:
type writeFile struct{}
func (writeFile) Name() string { return "write_file" }
func (writeFile) Description() string { return "Write a file on the host." }
func (writeFile) Schema() json.RawMessage {
// Minimal JSON schema for the tool.
return json.RawMessage(`{
"type":"object",
"properties":{
"path":{"type":"string"},
"content":{"type":"string"}
},
"required":["path","content"]
}`)
}
Schema Canonicalization and Caching
To avoid recomputing schema order and to ensure byte-level consistency across requests, DeepSeek-Reasonix canonicalizes each schema exactly once upon registration.
The Registry Cache
The Registry type in internal/tool/tool.go maintains a canon map that stores the canonicalized form of each tool's schema. When Registry.Add is called, it immediately invokes provider.CanonicalizeSchema(t.Schema()) and caches the result:
// Registry.Add – canonicalizes and caches the schema.
r.canon[name] = provider.CanonicalizeSchema(t.Schema())
This guarantees that schema normalization happens only once per tool per run, eliminating redundant JSON marshaling during active chat turns.
Deterministic Ordering with Canonicalization
The provider package in internal/provider/schema_canonicalize.go normalizes JSON Schema objects by sorting object keys recursively and removing duplicate entries. This process ensures that logically equivalent schemas produce identical byte sequences. When Registry.Schemas() is called, it produces an alphabetically-sorted slice of provider.ToolSchema structs:
// Registry.Schemas – exports name, description and the cached schema.
out = append(out, provider.ToolSchema{
Name: t.Name(),
Description: t.Description(),
Parameters: r.canon[name],
})
The combination of canonicalization and alphabetical sorting guarantees that the tool list is deterministic, preventing cache misses or inconsistent model behavior.
Streaming Schemas to the Model
During each turn of the chat, the server constructs a "tool-definition" message containing the slice returned by Registry.Schemas(). Because the schemas are pre-canonicalized and cached, the payload is small and fast to generate.
Building the Tool Definition Message
The request-handling path (implemented in files like internal/serve/wire.go) injects the schema list into the outbound protocol message before any user or assistant content. A simplified version of this flow appears as:
func sendTurn(toModel modelClient, reg *internal/tool.Registry, turn *Turn) error {
defs := reg.Schemas()
// Build a tool-definition message.
msg := model.ToolDefinition{Tools: defs}
if err := toModel.SendMessage(ctx, msg); err != nil {
return err
}
// Follow up with the user/assistant messages for this turn.
return toModel.SendMessage(ctx, turn.Message)
}
This ensures the model receives the complete tool specification at the start of every interaction, enabling it to generate valid function calls based on the accurate parameter definitions.
Summary
- Tool interface: Each tool implements
Schema() json.RawMessageininternal/tool/tool.goto declare its parameter structure. - Canonicalization:
provider.CanonicalizeSchemaininternal/provider/schema_canonicalize.gonormalizes schemas to ensure byte-level consistency. - Registry caching: The
Registrytype caches canonicalized schemas in acanonmap duringAdd(), eliminating reprocessing during chat turns. - Stable ordering:
Registry.Schemas()returns an alphabetically-sorted list ofprovider.ToolSchemastructs for deterministic transmission. - Streaming: The server streams the complete tool definition message to the model before user and assistant messages, as implemented in the request-handling path.
Frequently Asked Questions
What is the purpose of schema canonicalization?
Schema canonicalization ensures that logically equivalent JSON schemas produce identical byte sequences by sorting object keys and removing duplicates. This guarantees deterministic ordering when the schema list is streamed to the LLM, preventing variations that could confuse the model or break caching mechanisms.
How does the Registry ensure schemas are only processed once?
The Registry type in internal/tool/tool.go maintains a canon map that stores the canonicalized result of provider.CanonicalizeSchema(t.Schema()) immediately when Registry.Add is called. Subsequent calls to Registry.Schemas() retrieve the cached value rather than re-marshaling the JSON, reducing CPU overhead during active chat sessions.
Why are schemas sent before user and assistant messages?
The tool definition message must reach the model first so it knows exactly which functions are available and what parameters they require before processing any user queries or generating responses. This ordering, implemented in the request-handling path, enables the model to generate valid tool invocations during its initial response generation rather than requiring a secondary pass.
Where are built-in tool schemas defined?
Built-in tool schemas are defined in individual files within internal/tool/builtin/, such as internal/tool/builtin/writefile.go. Each file contains a concrete type implementing the Tool interface with a hardcoded or computed json.RawMessage representing the tool's JSON Schema parameters.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →