Introduce normalized LM boundary for adapters - #9802
Conversation
0833e8d to
5f5cfa1
Compare
Greptile SummaryThis PR introduces a normalized
Confidence Score: 5/5The change is behavior-preserving for all tested adapter paths: messages are normalized to LMRequest and immediately converted back to legacy kwargs, so the LM receives the same OpenAI-shaped dict as before. The 169-test adapter suite validates the most common flows. The round-trip conversion is the dominant risk area, and it is well-guarded: provider_output on each LMOutput carries the original BaseLM output so legacy_outputs_from_lm_response can return the exact original value to postprocessing without loss. The two edge cases flagged are both off the common adapter paths tested by the suite and are straightforward to address. dspy/clients/openai_format.py deserves a focused read, especially response_content_item_to_parts, message_to_openai_chat, and the legacy compatibility helpers at the bottom of the file. Important Files Changed
Reviews (3): Last reviewed commit: "Introduce normalized LM boundary for ada..." | Re-trigger Greptile |
5f5cfa1 to
984022f
Compare
| return None | ||
|
|
||
|
|
||
| def _parse_doubly_quoted_json(value: str) -> Any: |
| if block_type == "image_url": | ||
| image_url = block.get("image_url", {}) | ||
| source = image_url.get("url") if isinstance(image_url, dict) else image_url | ||
| from dspy.core.types import LMImagePart |
| context=block.get("context"), | ||
| ) | ||
|
|
||
| return LMTextPart(text="", metadata={"legacy_content_block": block}) |
There was a problem hiding this comment.
Should this raise instead of being a generic fallback? Do we read this legacy_content_block anywhere?
| planned message/part insertions before creating `LMRequest`. | ||
| """ | ||
| return LMRequest.from_call( | ||
| model=getattr(lm, "model", ""), |
There was a problem hiding this comment.
nit: does LM ever not have model?
I have seen this particular LLMism about BaseLM and solely using getattr many times.
| def _call_lm(self, lm: BaseLM, request: LMRequest) -> LMResponse: | ||
| """Call current `BaseLM` through the normalized request/response boundary. | ||
|
|
||
| TODO(language-models): When `BaseLM` is replaced by/updated to the |
There was a problem hiding this comment.
nit: this method should probably disappear rather than becoming a direct call
984022f to
a4011e9
Compare
a4011e9 to
6e90339
Compare
|
Thanks, fixed the small nits:
I also fixed the provider-shaped {"type": "function", "function": {"name": "get_weather"}} |
Summary
This is the first PR in a stack to move adapters toward normalized LM requests and responses.
Adapters still format prompts the same way, and current
BaseLMcalls still receive OpenAI/LiteLLM-shaped kwargs. The difference is that the adapter call path now passes through a normalized boundary internally:Why
Today adapters talk directly in provider-shaped messages and legacy output objects. That makes it hard to move tools, reasoning, citations, multimodal inputs, and future adapter types onto one LM type system.
This PR adds the seam without changing parser behavior yet. We normalize before and after the LM call, then convert back to the existing postprocess shape so this can stay behavior-preserving.
Changes
dspy.clients.openai_formatfor normalizedLMRequest/LMResponseconversion to and from OpenAI/LiteLLM-shaped data.dspy.adapters._legacy_type_markersfor the old custom type marker expansion.Adapter.__call__()andacall()to route throughLMRequestandLMResponse.LMTextPart.base.pymarking the follow-up adapter plan and directLMResponseparsing work.Notes
dspy.clients.openai_formatis used instead of an adapter-local bridge because provider-format conversion belongs in the client/LM layer. Later LM backends can reuse the same formatter.The conversion back from
LMResponseto legacy outputs is temporary. A follow-up PR should replace_call_preprocess()/_call_postprocess()with an explicit adapter plan and directLMResponseparsing.Tests
Targeted adapter suite: 169 passed.