Repository navigation
feat: add LiteLLM messages and agents integrations - #102
Conversation
Add in-process messages and Agents SDK handlers so AI Configs can route across LiteLLM-supported providers with tools, streaming, and graph support.
Register catalog metadata, document the server SDK peer, emit native graph telemetry, and capture streamed content on the root span.
Start tracing after graph setup and always end the graph span across success, failures, result processing errors, and cancellation.
mypy cannot resolve a base class read directly off a dynamically imported module, so bind the name first as the OpenAI adapter does. Co-authored-by: Cursor <cursoragent@cursor.com>
…-lite-llm # Conflicts: # .release-please-manifest.json
jeffdupont
left a comment
There was a problem hiding this comment.
Reviewed with the 1.0 freeze in mind, alongside launchdarkly/js-ai-sdk#75 and the spec in launchdarkly/ai-sdks-monorepo#18. Tests pass at 3683c97 (uv run pytest: 1365 passed, 11 skipped, exit 0; make typecheck clean). The branch is 29 commits behind main (fee904a) and conflicts in .release-please-manifest.json and uv.lock. I resolved those by hand in a scratch tree and the merged result passes too (1445 passed, mypy clean). LiteLLM isn't in the GA plan, so the questions here are whether it's safe to ship and whether it ships as 1.0 or as experimental.
Four things I'd like settled before merge:
- An AI Config can send the customer's provider key to any host. Both handlers pass
model.parametersto LiteLLM, and LiteLLM takes connection settings as call arguments. I reproduced it at 3683c97: withANTHROPIC_API_KEYset in the process andmodel.parameters = {api_base: "http://127.0.0.1:<port>"}, bothcreate_litellm_messages_handler()andcreate_litellm_agents_handler()sent their request to my local listener withx-api-key: <the customer's key>. The agents model factory also readsbase_urlandapi_keyfrom the config on purpose (handler.py:117,native_graph.py:45). It's the same kind of problem as claude-agents on #107, and worse here: Python runs LiteLLM in-process, so no proxy sits in between. I'd use an allowlist per handler, as #107 does, and never forward connection or credential keys. Details inline. - The native graph emits the retired graph event. Since #114 (AIC-3211), every native adapter emits
$ld:ai:graph:nodeonce per node and no longer emits$ld:ai:graph:path.TESTING.md§2.2 onmainsays so.to_litellm_agentswas copied before that change. After a merge it will be the only adapter still sending$ld:ai:graph:path, and the only one sending no node events. The merge doesn't conflict there and the tests still pass, so nothing will catch it. - Python and JS don't match (js #75). The transport difference is by design and the spec explains it (A.13). These other differences aren't explained:
- Native graph telemetry: here you get a span and graph events. JS
toLiteLLMAgentsemits neither and takes nocontext. - Agent parameters: here, known snake_case keys go into
ModelSettingsand the rest intoextra_args. JS passes the raw bag asmodelSettings, wheremax_tokensandtop_pare silently ignored. So one config gets a token limit in Python and none in JS. - Framework tracing: Python leaves the Agents SDK's own tracing on. JS turns it off for the whole process.
- Factory name:
create_litellm_agents_handlerhere,createLiteLLMAgentHandlerin JS. The other agent packages use the same name in both languages. - Default
gen_ai.provider.namewhen the config has no provider:litellmhere,openaiin JS. That's a span attribute Monitoring reads.
- Native graph telemetry: here you get a span and graph events. JS
- I think this should ship as experimental. Both packages start at 0.1.0 with
bump-minor-pre-major, so they can stay 0.x when everything else goes to 1.0. The lifecycle spec (monorepo #33) only covers client features exported fromlaunchdarkly_ai_server.experimental, not whole provider packages, so it needs a line for this case. My suggestion: keep both LiteLLM packages out of the 1.0 cut, say "experimental" in their READMEs, add them to the promotion register, and promote them once the two languages match.
Smaller notes:
Runner.runis called withoutRunConfig(tracing_disabled=True). The Agents SDK's default exporter posts tohttps://api.openai.com/v1/traces/ingest(agents/tracing/processors.py:34). So ifOPENAI_API_KEYis also set, a run that LiteLLM routes to Anthropic still sends its trace, prompts included, to OpenAI. I took that from the SDK source and haven't watched it happen at runtime. Spec §2.x.2 asks for suppression. Per-runtracing_disabled(agents/run_config.py:257) does that without touching the customer's own Agents usage.- Messages streaming still records a cancelled stream as abandoned, with no
launchdarkly.run.cancelled(Bugbot's last finding). The agents stream catchesCancelledError; the messages stream doesn't. - Once #121 lands, native graph spans get
set_ld_span_attributes, andto_litellm_agentswill need the same change. - Content capture runs
str(message["content"])(litellm-messages/.../handler.py:362), so multimodal input shows up on the span as a Python list repr.
Bugbot's earlier findings (catalog entries, README install line, graph span leak, root content on stream) are fixed at this head. The history one in the convenience wrappers matches every other provider's wrapper, so it's not specific to this PR.
…tion Allowlist generation settings, take connection settings from the handler, and record cancelled streams and graph node events the way the other adapters do. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed the review on
Left as-is: convenience wrappers still drop |
jeffdupont
left a comment
There was a problem hiding this comment.
Re-checked at 642e4ca. Most of my 2 Oct points are fixed: the provider key can't be redirected, there's no $ld:ai:graph:path event, per-run tracing_disabled, cancelled-over-abandoned, and multimodal content text.
Still blocking:
- Merge conflicts with main in
.release-please-manifest.json,.sdk_metadata.json,AGENTS.md,release-please-config.jsonanduv.lock. litellm_graphcan't set connection settings.litellm-agents/.../graph.py:12-27passes onlymodel_factoryandcapture_content.base_url/api_keyland in**options, andgraph()rejects them withTypeError. Now that config can't supply URLs, graph users can't reach a proxy. JSlitellmGraphforwards these.- Agents allowlist forwards
metadata(handler.py:47), plusinclude_usageandtop_logprobs, which messages doesn't allow. JS messages explicitly tests thatmetadatanever reaches the request. Pick one list across both SDKs.
Smaller:
reasoninggoes intoModelSettingsas a raw dict (handler.py:45), while the SDK types it asReasoning, and messages readsreasoning_effortinstead. I haven't checked whetherLitellmModelaccepts the dict.- No test asserts
RunConfig(tracing_disabled=True), so a regression would bring back trace export to OpenAI unnoticed. - The spec in ai-sdks-monorepo#18 still describes the old denylist.
…-lite-llm # Conflicts: # .release-please-manifest.json # .sdk_metadata.json # AGENTS.md # release-please-config.json # uv.lock
… trim the agents allowlist - _version.py plus report_usage in every public helper; wrappers use the internal builders. - capture_content defaults to None and resolves at call time. - litellm_graph forwards base_url and api_key. - Drop metadata, include_usage and top_logprobs; reasoning becomes a typed Reasoning with effort/summary only. - to_litellm_agents: setup inside the try, graph-key totals, handoff target in the path. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 94d481f. Configure here.
…aph span Co-authored-by: Cursor <cursoragent@cursor.com>
jeffdupont
left a comment
There was a problem hiding this comment.
Looks good against the spec. One thing: _model_config only keeps snake_case keys, so topP/maxTurns get dropped, but spec §2.x.1 says either spelling is read (JS does). A quick normalize before the filter plus a test would cover it. Also, should messages take base_url/api_key like JS? A.20 is a bit ambiguous there.
…e LiteLLM endpoint _model_config only matched snake_case keys, so a caller-supplied topP or maxTurns silently vanished even though spec §2.x.1 requires either spelling to be read, as JS already does. Normalize camelCase aliases to their snake_case name before filtering, with snake_case winning when both are set. create_litellm_messages_handler had no way to point at a specific endpoint or key, unlike the agents handler and the JS messages handler. It now takes the same base_url/api_key options, forwarded to acompletion as api_base/ api_key on both the blocking and streaming paths. model.parameters still cannot set them. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Both fixed at 6aaeffb:
|

Summary
launchdarkly-ai-litellm-messagesusing in-process LiteLLM completionslaunchdarkly-ai-litellm-agentsusing the OpenAI Agents SDK LiteLLM modelUsage
Python runs LiteLLM in-process, so callers set the upstream provider credentials required by the evaluated model; no LiteLLM proxy is required.
Test plan
uv run pytestuv run mypyacross all package sourcesuv run ruff check .uv run ruff format --check .Made with Cursor