TL;DR
Eino Components are typed capability boundaries, not interchangeable behavior guarantees. In v0.9.21, BaseChatModel provides Generate and Stream; ToolCallingChatModel.WithTools returns a tool-bound model without mutating the shared instance; Tool parameters use ToolInfo.Desc and ParamsOneOf; and Retriever exposes a common call shape while leaving ranking, filters, authorization, and consistency to the implementation and application.
Key Takeaways
- Depend on a Component interface only after defining the semantic contract your application requires.
- Prefer immutable
WithToolsover deprecated, mutatingBindToolsin concurrent services. ToolsNodeexecutes model-proposed calls; it does not decide whether the caller is authorized.- Keep retrieval relevance, source trust, freshness, and access control as separate fields.
- Contract-test every provider adapter, including errors, streaming, options, cancellation, and upgrades.
The Component Map
The official Components documentation groups Eino capabilities by application role:
| Component | Input to output | Application-owned contract |
|---|---|---|
BaseChatModel |
Messages to one response or a response stream | Provider identity, timeout, output validation |
AgenticModel |
Agentic messages to structured content blocks | Feature support, approval semantics, beta migration |
Tool |
JSON or typed argument to result | Authentication, authorization, side effects |
Loader |
Source to documents | SSRF controls, parsing limits, provenance |
Transformer |
Documents to transformed documents | Offset lineage, deletion propagation |
Embedding |
Text to vectors | Model revision, dimensions, normalization |
Indexer |
Documents to stored index entries | Consistency, tenancy, idempotency |
Retriever |
Query to candidate documents | Filters, score meaning, ACL before context |
ChatTemplate |
Variables to messages | Escaping, role boundaries, prompt version |
Lambda |
Typed Go function | Cancellation, bounds, deterministic errors |
Concrete implementations normally come from EinoExt. Pin each imported module separately; a core tag does not automatically pin every provider adapter.
Model Interfaces in v0.9.21
The pinned components/model/interface.go defines a generic BaseModel and two message paths:
| Interface | Message type | Tool binding |
|---|---|---|
BaseChatModel |
*schema.Message |
None by itself |
ToolCallingChatModel |
*schema.Message |
Immutable WithTools |
AgenticModel |
*schema.AgenticMessage |
Request-level options |
The old ChatModel.BindTools method remains for compatibility but is deprecated. It mutates the model instance, so two concurrent requests with different tool lists can race. WithTools returns a derived ToolCallingChatModel and is the safer default for shared services.
Types cannot normalize provider behavior. One adapter may support parallel tool calls, another may ignore a common option, and another may return usage only after a stream ends. Contract tests should cover:
- unsupported options and feature negotiation;
- tool-call IDs, argument fragments, and finish reasons;
- stream errors before and after the first chunk;
- cancellation and provider timeouts;
- usage accounting and retry duplication;
- structured-output and multimodal behavior.
Tool Contracts: Description Is Not Permission
The ToolsNode and Tool guide makes the execution boundary explicit: the model puts ToolCalls in an assistant message, while ToolsNode invokes configured tools. The model chooses a proposal; trusted code decides whether it may execute.
In v0.9.21, a ToolInfo uses:
Namefor a unique callable identifier;Descto explain when and why to use it;ParamsOneOffor either lightweight parameter metadata or JSON Schema 2020-12.
A safe tool execution sequence is:
Tool aliases and schemas improve compatibility, but neither grants authority. Treat names, descriptions, aliases, arguments, and returned content as untrusted inputs.
Runnable Typed Tool
This program targets Eino Core v0.9.21. InferTool derives the argument schema from a Go struct, while the tool reads tenant identity from trusted request context rather than model-generated JSON.
package main
import (
"context"
"fmt"
"github.com/cloudwego/eino/components/tool/utils"
)
type principalKey struct{}
type Principal struct {
TenantID string
}
type LookupOrderInput struct {
OrderID string `json:"order_id" jsonschema:"required" jsonschema_description:"Order identifier in the caller tenant"`
}
type LookupOrderOutput struct {
OrderID string `json:"order_id"`
Status string `json:"status"`
}
func main() {
orders := map[string]struct {
TenantID string
Status string
}{
"A-42": {TenantID: "tenant-a", Status: "paid"},
}
lookup, err := utils.InferTool(
"lookup_order",
"Read one order already authorized for the authenticated tenant.",
func(ctx context.Context, input LookupOrderInput) (LookupOrderOutput, error) {
principal, ok := ctx.Value(principalKey{}).(Principal)
if !ok {
return LookupOrderOutput{}, fmt.Errorf("missing principal")
}
order, ok := orders[input.OrderID]
if !ok || order.TenantID != principal.TenantID {
return LookupOrderOutput{}, fmt.Errorf("order not found")
}
return LookupOrderOutput{OrderID: input.OrderID, Status: order.Status}, nil
},
)
if err != nil {
panic(err)
}
info, err := lookup.Info(context.Background())
if err != nil {
panic(err)
}
ctx := context.WithValue(context.Background(), principalKey{}, Principal{TenantID: "tenant-a"})
result, err := lookup.InvokableRun(ctx, `{"order_id":"A-42"}`)
if err != nil {
panic(err)
}
fmt.Printf("%s => %s\n", info.Name, result)
}
Expected output:
lookup_order => {"order_id":"A-42","status":"paid"}
Use an unexported typed key for request context. Never accept tenant_id, roles, approval state, or an authorization boolean from model arguments as the source of truth.
Retriever Is a Candidate Interface
A Retriever returns candidate documents. It does not establish that a document is true, current, complete, or visible to the caller. Normalize these properties before a Generator sees evidence:
| Field | Why it matters |
|---|---|
document_id and revision |
Stable citation and invalidation |
source_uri and span offsets |
Claim-level provenance |
tenant_id and object scope |
Authorization before ranking |
score_type and direction |
Comparable ranking semantics |
valid_from / valid_to |
Freshness and temporal questions |
parser / chunker / embedding revision |
Reproducible retrieval |
When swapping Elasticsearch, VikingDB, or another adapter, test the same query set and verify filter semantics, tie ordering, duplicate behavior, score calibration, pagination, timeout, partial failure, and deletion.
Component Contract Tests
For each implementation, build tests at three levels:
- Interface: accepted types, options, nil values, cancellation, and error classes.
- Semantics: provider-specific feature support, score meaning, tool schema, and metadata.
- System: authorization, budgets, retries, idempotency, redaction, and observable outcomes.
A successful compile proves type compatibility. A passing provider call proves connectivity. Neither proves task quality or policy compliance.
Common Failure Modes
| Failure | Consequence | Control |
|---|---|---|
Mutating a shared model with BindTools |
Cross-request tool leakage or race | Use WithTools; test concurrency |
| Trusting generated tool arguments | Cross-tenant read or unsafe write | Derive principal server-side; authorize object/action |
| Treating Schema as business validation | Valid shape, invalid operation | Enforce invariants after parsing |
| Swapping Retriever by interface alone | Ranking or filter regression | Adapter contract suite |
| Logging complete payloads in callbacks | Secret or personal-data exposure | Redaction, sampling, retention |
| Retrying every error | Duplicate effects and higher cost | Typed retry policy and idempotency |
Frequently Asked Questions
Does Eino automatically execute every tool a model requests?
ToolsNode executes calls present in its input against configured tools. Your application must still restrict the available tool set and enforce authorization and side-effect policy around execution.
Is AgenticModel a drop-in replacement for BaseChatModel?
No. It uses AgenticMessage content blocks and different tool-binding behavior. Treat migration as a contract change and test every provider and downstream consumer.
Should a Tool return plain text or structured data?
Return the narrowest machine-readable result the next step needs. Bound size, preserve provenance, and avoid embedding secrets. Eino also defines enhanced Tool interfaces for multimodal results.
Can a Retriever enforce access control?
An implementation may support filters, but the application must derive them from trusted identity and verify their semantics. Authorization should happen before unauthorized candidates affect ranking, caches, logs, or model context.
What belongs in a Lambda?
Small, bounded transformations with clear input, output, cancellation, and error behavior. Hidden network calls or policy decisions make a Lambda harder to test and operate.
Primary Sources
- Eino
v0.9.21model interfaces - Eino Components documentation
- Eino ChatModel guide
- Eino ToolsNode and Tool guide
- EinoExt repository
- JSON Schema 2020-12