Skip to content

fix(dspy): align span shape with spec, drop over-serialization, avoid nested llm spans#588

Merged
Abhijeet Prasad (AbhiPrasad) merged 2 commits into
mainfrom
dspy-spec-alignment
Jul 17, 2026
Merged

fix(dspy): align span shape with spec, drop over-serialization, avoid nested llm spans#588
Abhijeet Prasad (AbhiPrasad) merged 2 commits into
mainfrom
dspy-spec-alignment

Conversation

@starfolkai

@starfolkai starfolkai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Audit of py/src/braintrust/integrations/dspy/ against .agents/skills/sdk-integrations/SKILL.md.

  • Type dspy.module.*, dspy.adapter.format, dspy.adapter.parse, dspy.evaluate as task (were untyped).
  • Add metadata.provider on the LM span (derived from LiteLLM-style "openai/..." prefix on instance.model), keep metadata.model.
  • Drop over-serialization: removed outputs.toDict() / outputs.__dict__ conversion in on_module_end. Braintrust serializes at send time; DSPy Prediction objects are passed through as-is.
  • Fix on_evaluate_end: aggregate eval stats (accuracy, score, total, correct) now go into metadata via an allowlist, not into metrics — the spec restricts metrics to a listed set (tokens, prompt_tokens, time_to_first_token, …).
  • Remove redundant current_span().export() parent passing at 5 call sites; start_span picks up the parent from contextvars automatically (matches anthropic/adk).
  • Name the LM param whitelist _LM_METADATA_PARAM_ALLOWLIST at module scope (already allowlist-shaped; no denylists found anywhere).
  • Do NOT type dspy.lm as llm. DSPy delegates transport to LiteLLM (or a patched provider client), which owns the llm leaf and its token accounting. Typing dspy.lm as llm would produce two nested llm spans per API call when the underlying client is also instrumented — and since DSPy's callback contract doesn't expose usage in outputs, it would also produce a token-less llm span that violates the spec's "tokens required for LLM spans" rule.
  • Add a paragraph under Token metrics in sdk-integrations/SKILL.md documenting this pattern for future framework integrations that delegate transport to another instrumentable client.

Test plan

  • cd py && uv run nox -s "test_dspy(latest)" — 9/9 passed (dspy 3.2.1)
  • cd py && uv run nox -s "test_dspy(2.6.0)" — 9/9 passed
  • Extended test_dspy_callback assertions cover the new span_attributes.type values (task on module/adapter spans, not-llm on dspy.lm) and metadata.provider == "openai" on the LM span.
  • Existing VCR cassettes reused unchanged.

🤖 Generated with Claude Code

Created by abhijeet

Slack thread

starfolkbot and others added 2 commits July 17, 2026 16:34
… nested llm spans

- Type module/adapter/evaluate spans as `task` per spec.
- Add `metadata.provider` (derived from litellm-style "openai/..." model prefix)
  and keep `metadata.model` on the LM span.
- Drop `outputs.toDict()` / `outputs.__dict__` conversion on module end; braintrust
  serializes at send time.
- Fix `on_evaluate_end` to place aggregate eval stats (accuracy/score/total/correct)
  in `metadata` via an allowlist instead of inventing new `metrics` keys.
- Remove redundant `current_span().export()` parent-passing; start_span picks up
  the parent from contextvars.
- Do NOT type `dspy.lm` as `llm`. DSPy delegates transport to LiteLLM/patched
  provider clients, which own the `llm` leaf span and its token accounting;
  typing dspy.lm as `llm` would double-emit and produce a token-less `llm` span.
  Document the pattern under Token Metrics in the sdk-integrations SKILL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The SKILL.md paragraph carries the "why" for dspy.lm; variable names carry
the rest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) merged commit 429a5e8 into main Jul 17, 2026
82 checks passed
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) deleted the dspy-spec-alignment branch July 17, 2026 16:45
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.

2 participants