Skip to content

feat(adapter): add native tool-call controls - #56

Open
isaacbmiller wants to merge 1 commit into
isaac/react-v2-pr4-native-historyfrom
isaac/react-v2-pr5-native-adapter-controls
Open

feat(adapter): add native tool-call controls#56
isaacbmiller wants to merge 1 commit into
isaac/react-v2-pr4-native-historyfrom
isaac/react-v2-pr5-native-adapter-controls

Conversation

@isaacbmiller

Copy link
Copy Markdown

Summary

  • keep public adapter format() output OpenAI-chat-shaped while using normalized render_messages() internally
  • add native tool-call instructions, optional parallel tool-call control, and forced tool-call config
  • preserve legacy preprocess/postprocess hooks over the normalized request pipeline

Stack

  • Base PR: native history rendering
  • Next PR: ReActV2 tool loop

Validation

  • uv run --extra dev pytest -q tests/adapters/test_history_formatting.py tests/adapters/test_history_lm_messages.py tests/adapters/test_tool.py::test_native_tool_response_preserves_call_ids tests/adapters/test_chat_adapter.py::test_chat_adapter_with_tool tests/adapters/test_chat_adapter.py::test_tool_call_with_null_content_does_not_raise
  • Full stack focused validation command listed in feat(history): add typed history frames #51.

@greptile-apps

greptile-apps Bot commented May 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR refactors the adapter layer to split the previously unified format() method into a public OpenAI-chat-shaped format() wrapper and an internal render_messages() method that returns normalized LMMessage objects. On top of that, it adds native tool-call controls: an instruction injected into the system prompt when tools are active, an allow_parallel_tool_calls knob, and a force_tool_call_config helper for forcing a specific tool at call time.

  • render_request now calls render_messages internally instead of format, keeping the call pipeline on normalized types while preserving the public format() signature for external callers.
  • allow_parallel_tool_calls and native_response_types are threaded through all three adapter constructors (Adapter, ChatAdapter, JSONAdapter); ToolCalls is added to the default native_response_types list.
  • _call_preprocess / _call_postprocess hooks are introduced to bridge the new normalized pipeline with legacy adapter usage patterns, and _legacy_output_to_lm_output is hardened to handle None outputs.

Confidence Score: 3/5

The call pipeline change is functionally correct, but it silently drops adapter format callbacks for all existing users.

The rerouting of render_request through render_messages instead of format means on_adapter_format_start / on_adapter_format_end callbacks no longer fire during normal DSPy module execution. Any deployed observability, logging, or tracing code that hooks those callbacks will silently stop receiving events without any error or deprecation warning. The rest of the changes — allow_parallel_tool_calls, force_tool_call_config, _call_preprocess/_call_postprocess, and the ToolCalls addition to default native response types — look correct and well-guarded.

dspy/adapters/base.py — specifically the relationship between the callback-decorated format() and the now-internal render_messages() path used by render_request.

Important Files Changed

Filename Overview
dspy/adapters/base.py Major restructuring: format() is now a public OpenAI-shaped wrapper around render_messages(), which is the new internal method. Adds allow_parallel_tool_calls, force_tool_call_config, _call_preprocess/_call_postprocess hooks, and native tool-call instruction injection. The render_requestrender_messages change silently breaks on_adapter_format_start/on_adapter_format_end callbacks during normal execution.
dspy/adapters/chat_adapter.py Adds allow_parallel_tool_calls passthrough and updates format_finetune_data to use render_messages instead of the now-OpenAI-shaped format. Docstring for the new parameter is missing.
dspy/adapters/json_adapter.py Exposes native_response_types and allow_parallel_tool_calls parameters that were previously inaccessible on JSONAdapter. Straightforward passthrough change.
tests/adapters/test_history_formatting.py Updates assertions from LMMessage attribute access to OpenAI-dict key access to match the new format() return type (list[dict] instead of list[LMMessage]).

Sequence Diagram

sequenceDiagram
    participant Caller
    participant Adapter
    participant plan_fields
    participant render_request
    participant render_messages
    participant format

    Caller->>Adapter: __call__(lm, lm_kwargs, signature, demos, inputs)
    Adapter->>plan_fields: plan_fields(lm, lm_kwargs, signature, inputs)
    Note over plan_fields: Strips tool/reasoning/citations fields<br/>Appends native_feature_instructions<br/>Sets parallel_tool_calls in lm_kwargs
    plan_fields-->>Adapter: "plan{prompt_signature, inputs, lm_kwargs, tools, ...}"
    Adapter->>render_request: render_request(plan, lm, demos, inputs)
    render_request->>render_messages: render_messages(prompt_signature, demos, plan.inputs)
    Note over render_messages: Returns list[LMMessage] — NO callbacks
    render_messages-->>render_request: list[LMMessage]
    render_request-->>Adapter: LMRequest
    Adapter->>Adapter: call_lm(lm, request)
    Adapter->>Adapter: parse_response(plan, response, lm)
    Adapter-->>Caller: list[dict]

    Note over format: Public API only (OpenAI-shaped output)<br/>Callbacks fire here, but this path<br/>is NOT used during normal calls
    Caller->>format: format(signature, demos, inputs)
    format->>render_messages: render_messages(...)
    format-->>Caller: list[dict] (OpenAI-chat-shaped)
Loading

Comments Outside Diff (2)

  1. dspy/adapters/base.py, line 88-89 (link)

    P1 on_adapter_format_start/on_adapter_format_end callbacks silently stop firing

    __init_subclass__ wraps format with the callback decorator, but render_request now calls render_messages directly, bypassing that wrapper entirely. Any user who has configured on_adapter_format_start or on_adapter_format_end on an adapter — e.g., for observability/logging — will stop receiving those events during normal dspy.Predict execution without any warning. The public format() wrapper still triggers callbacks when called directly, but the internal call path (render_requestrender_messages) never does.

  2. dspy/adapters/chat_adapter.py, line 48-56 (link)

    P2 The allow_parallel_tool_calls parameter is added to the constructor but is absent from the docstring. Since it was also added to JSONAdapter without documentation there, all three adapter classes have this gap.

Reviews (1): Last reviewed commit: "feat(adapter): add native tool-call cont..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant