You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
calfkit's model layer is pydantic-ai, vendored wholesale into calfkit/_vendor/pydantic_ai/ (182 files, ~51k LOC) — a heavyweight dependency
for what calfkit actually uses it for. Because tools are remote nodes, calfkit
hands pydantic-ai only tool schemas (ExternalToolset of tool_defs, agent.py:370); pydantic-ai runs one model step and returns DeferredToolRequests. The multi-turn loop is driven by Kafka re-entry
(agent.py:344-372), not by pydantic-ai.
This issue replaces that layer with LiteLLM as the LLM-call transport, and
defines calfkit-native message types on top of LiteLLM's data types. This
sheds pydantic-ai and the ~51k vendored lines while leaving the distributed
runtime (envelope, tool dispatch, aggregation) untouched.
Motivation
Drop a heavyweight dependency. pydantic-ai brings a full agent framework
(~51k vendored LOC) for what is, in calfkit, a single model call + a message
model + tool-schema formatting. LiteLLM is a thin transport.
One provider surface. LiteLLM reaches 100+ providers behind one acompletion call and one config, replacing three native-SDK-bound clients.
Matches the stated v1 non-goal (calfkit-v1-design.md §1.5): "We don't
compete with LiteLLM."
Scope: a contained layer
This replaces the single-model-step + message-model + output/tool-schema
layer. It does not touch the distributed runtime.
Replace (today → after):
_agent_loop.run() (one step) → a calfkit call to litellm.acompletion.
ModelMessage/ModelResponse/parts in State.message_history → calfkit
pydantic message types built on LiteLLM's ModelResponse/Message/Usage
(OpenAI-shaped: content, tool_calls, reasoning_content, usage).
DeferredToolRequests/DeferredToolResults → parse tool_calls off the
response; the deferred bridge is already calfkit-owned
(ToolCallRef, State.add_tool_call, get_tool_result).
RetryPromptPart → a calfkit retry-prompt message (a tool-role message with
error content fed back on re-entry).
Durable fan-out / aggregation (ktables, PendingToolBatch, latest_tool_calls()) — only contact point is "message types must still expose
tool calls," which the new types do. No design overlap.
Tests — ~28 files; offline test seam (FunctionModel/TestModel in tests/providers.py, tests/conftest.py) rebuilt on LiteLLM mock_response.
Breaking changes (contained)
message_history shape changes → persisted/in-flight State with the old
pydantic-ai message shape won't deserialize. Accepted clean break (pre-1.0).
Note: this is independent of the Envelope.reply/fault fields — the mechanism
(Envelope → State → message_history) is unchanged.
Public API: InvocationResult/NodeResult and ConsumerContext return
the new message type instead of ModelMessage.
Message-type mapping. Define calfkit pydantic models over LiteLLM's ModelResponse/Message/Usage (confirm exact current LiteLLM types). Decide
how thinking/reasoning_content and Anthropic prompt-cache controls (today's anthropic_thinking, anthropic_cache_*) ride through as LiteLLM passthrough
params.
Structured output. LiteLLM response_format JSON + calfkit pydantic
validation, preserving the current final_output_type behavior.
Offline testing. Replace FunctionModel/TestModel with LiteLLM mock_response so the default suite stays fast and provider-free.
ModelRetry (tool-author API). Provide a calfkit equivalent for tools that
ask the model to retry.
Doc reconciliation.calfkit-v1-design.md line 3106 (un-vendor pydantic-ai
for v1.0, SDK-native abstraction for v1.1) is superseded — update / ADR.
Sequencing
No design collision with the durable fan-out build (worktree feat/durable-fanout-store) or the fault rail — they own different layers. The
only practical note: both this and the fan-out build edit agent.py/state.py,
so land one and rebase the other to avoid textual merge conflicts.
Non-goals
A LiteLLM proxy-server deployment mode — in-process SDK only.
A general model-abstraction layer beyond LiteLLM (LiteLLM owns that).
Preserving pydantic-ai message types on the wire.
Acceptance criteria
Single in-process LiteLLMModelClient; _vendor/pydantic_ai and the three
native clients removed.
calfkit message types defined over LiteLLM data types; message_history
serializes them; _projection.py updated.
Summary
calfkit's model layer is pydantic-ai, vendored wholesale into
calfkit/_vendor/pydantic_ai/(182 files, ~51k LOC) — a heavyweight dependencyfor what calfkit actually uses it for. Because tools are remote nodes, calfkit
hands pydantic-ai only tool schemas (
ExternalToolsetoftool_defs,agent.py:370); pydantic-ai runs one model step and returnsDeferredToolRequests. The multi-turn loop is driven by Kafka re-entry(
agent.py:344-372), not by pydantic-ai.This issue replaces that layer with LiteLLM as the LLM-call transport, and
defines calfkit-native message types on top of LiteLLM's data types. This
sheds pydantic-ai and the ~51k vendored lines while leaving the distributed
runtime (envelope, tool dispatch, aggregation) untouched.
Motivation
(~51k vendored LOC) for what is, in calfkit, a single model call + a message
model + tool-schema formatting. LiteLLM is a thin transport.
acompletioncall and one config, replacing three native-SDK-bound clients.num_retries,fallbacks,Router).calfkit-v1-design.md§1.5): "We don'tcompete with LiteLLM."
Scope: a contained layer
This replaces the single-model-step + message-model + output/tool-schema
layer. It does not touch the distributed runtime.
Replace (today → after):
_agent_loop.run()(one step) → a calfkit call tolitellm.acompletion.ModelMessage/ModelResponse/parts inState.message_history→ calfkitpydantic message types built on LiteLLM's
ModelResponse/Message/Usage(OpenAI-shaped:
content,tool_calls,reasoning_content,usage).DeferredToolRequests/DeferredToolResults→ parsetool_callsoff theresponse; the deferred bridge is already calfkit-owned
(
ToolCallRef,State.add_tool_call,get_tool_result).RetryPromptPart→ a calfkit retry-prompt message (atool-role message witherror content fed back on re-entry).
ToolDefinition/ExternalToolset→ emit OpenAI-formattools=[...]specs.final_output_type(OutputSpec) → calfkit-owned pydantic validation overLiteLLM
response_format.RequestUsage→ accumulate LiteLLMUsage.AnthropicModelClient,OpenAIModelClient,OpenAIResponsesModelClient) → a singleLiteLLMModelClient(in-process SDK).Leave untouched (and why it stays orthogonal):
x-calf-kind/ fault rail — a different layer(inter-node), not message-history internals.
Call,ToolCallRef,dispatch_topic) — alreadycalfkit-owned, outside pydantic-ai.
PendingToolBatch,latest_tool_calls()) — only contact point is "message types must still exposetool calls," which the new types do. No design overlap.
Coupling to migrate
Non-vendored pydantic-ai imports to replace:
calfkit/nodes/agent.py,calfkit/nodes/tool.py,calfkit/providers/pydantic_ai/*,calfkit/providers/__init__.pycalfkit/models/state.py,calfkit/client/client.py,calfkit/models/tool_dispatch.py,calfkit/models/capability.py,calfkit/models/tool_context.py(RunContext)calfkit/models/node_result.py,calfkit/models/consumer_context.py(both exposeModelMessage)calfkit/nodes/_projection.py,calfkit/mcp/mcp_toolbox.pyFunctionModel/TestModelintests/providers.py,tests/conftest.py) rebuilt on LiteLLMmock_response.Breaking changes (contained)
message_historyshape changes → persisted/in-flightStatewith the oldpydantic-ai message shape won't deserialize. Accepted clean break (pre-1.0).
Note: this is independent of the
Envelope.reply/fault fields — the mechanism(Envelope → State → message_history) is unchanged.
InvocationResult/NodeResultandConsumerContextreturnthe new message type instead of
ModelMessage.model_client=takesLiteLLMModelClient.Design considerations / open questions
ModelResponse/Message/Usage(confirm exact current LiteLLM types). Decidehow thinking/
reasoning_contentand Anthropic prompt-cache controls (today'santhropic_thinking,anthropic_cache_*) ride through as LiteLLM passthroughparams.
response_formatJSON + calfkit pydanticvalidation, preserving the current
final_output_typebehavior.(rate-limit, context-window, transient) into the already-converged
fault-rail seam contract (Reliability successor to the fault rail: retry, redelivery, timeouts, DLT #222 umbrella, Ack policy & redelivery: close the per-hop at-most-once crash windows #218–Dead-letter topic for unroutable faults #221; ADR-0003/0004/0006). This
conforms to that contract; it does not change it.
FunctionModel/TestModelwith LiteLLMmock_responseso the default suite stays fast and provider-free.ModelRetry(tool-author API). Provide a calfkit equivalent for tools thatask the model to retry.
calfkit-v1-design.mdline 3106 (un-vendor pydantic-aifor v1.0, SDK-native abstraction for v1.1) is superseded — update / ADR.
Sequencing
No design collision with the durable fan-out build (worktree
feat/durable-fanout-store) or the fault rail — they own different layers. Theonly practical note: both this and the fan-out build edit
agent.py/state.py,so land one and rebase the other to avoid textual merge conflicts.
Non-goals
Acceptance criteria
LiteLLMModelClient;_vendor/pydantic_aiand the threenative clients removed.
message_historyserializes them;
_projection.pyupdated.prompt, deferred dispatch over Kafka, usage accumulation.
InvocationResult,ConsumerContext) updated +documented as breaking.
mock_response; real-LLM opt-intests pass on ≥2 providers.
make fix && make checkclean; CHANGELOG + docs updated;calfkit-v1-design.mdreconciled (ADR if warranted).