Skip to content

feat: add litellm.agent() to run claude code, codex, opencode and deep agents through the ai gateway - #43885

Merged
ishaan-berri merged 229 commits into
mainfrom
litellm_harness_sdk
Oct 1, 2026
Merged

ishaan-berri merged 229 commits into
mainfrom
litellm_harness_sdk

Conversation

@ishaan-berri

@ishaan-berri ishaan-berri commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • No way to run Claude Code, Codex, OpenCode or Deep Agents on any LiteLLM model
  • Agent model calls skip the AI Gateway, so spend and keys are scattered

How it solves it:

  • New litellm.agent() / litellm.aagent(), plus agent_session, agent_resume, agent_capabilities
  • Harness is a typed enum; passing a string raises TypeError
  • model="litellm_proxy/<group>" sends every agent model call through the AI Gateway
  • Each harness is a config on BaseHarnessConfig (litellm/llms/<harness>/harness/transformation.py); one generic handler does the I/O
  • The runtime only sees a per-session token; real keys stay on the host
  • No new dependencies in litellm

What you get

import litellm
from litellm import Harness, sandbox

result = litellm.agent(
    Harness.CODEX,                        # or CLAUDE_CODE, OPENCODE, DEEPAGENTS
    "Create hello.py that prints 'hi from codex', then run it with python3.",
    sandbox=sandbox.local("./repo"),
    model="litellm_proxy/bedrock_mantle/openai.gpt-5.4",   # a model group on your AI Gateway
)

result.text         # 'Done — created `hello.py` and ran it successfully. ...'
result.files        # [FileChange(path='hello.py', kind='created', diff='--- a/hello.py ...')]
result.cost         # 0.05512  (USD, summed over every model call the agent made)
result.usage        # Usage(input_tokens=..., output_tokens=..., calls=2)
result.stop_reason  # 'done'

With stream=True you get typed events as they happen. Real output from all four harnesses on the same task, each routed through the gateway (metadata={"run": "pr-proof"} shows up in spend logs, tagged harness,<name>):

>>> litellm.agent(Harness.CLAUDE_CODE, ..., model='litellm_proxy/claude-haiku-4-5-20251001', stream=True)
  Text       "I'll create the hello.py file and run it."
  ToolCall   write  <tmp>/repo/hello.py
  ToolCall   bash   python3 <tmp>/repo/hello.py
  Text       'Done! Created hello.py and ran it successfully — it printed "hi from claude_code".'
  FileChange created hello.py
  Done       stop_reason=done calls=3 tokens=487 cost=$0.00233
  hello.py:  "print('hi from claude_code')"
>>> litellm.agent(Harness.CODEX, ..., model='litellm_proxy/bedrock_mantle/openai.gpt-5.4', stream=True)
  Text       'I’ll add the tiny script, then execute it to verify.'
  ToolCall   bash   /bin/zsh -lc "printf \"%s\n\" \"print('hi from codex')\" > hello.py && 
  Text       'Done — created `hello.py` and ran it successfully. - File: `hello.py:1` - Output: `hi from codex`'
  FileChange created hello.py
  Done       stop_reason=done calls=2 tokens=19293 cost=$0.05512
  hello.py:  "print('hi from codex')"
>>> litellm.agent(Harness.OPENCODE, ..., model='litellm_proxy/claude-haiku-4-5-20251001', stream=True)
  Text       "I'll create the hello.py file and then run it with python3."
  ToolCall   write  
  Text       'Now let me run it with python3:'
  ToolCall   bash   python3 hello.py
  Text       "Done! The hello.py file has been created and executed successfully. It prints 'hi from opencode' as expected."
  FileChange created hello.py
  Done       stop_reason=done calls=3 tokens=33688 cost=$0.03474
  hello.py:  "print('hi from opencode')"
>>> litellm.agent(Harness.DEEPAGENTS, ..., model='litellm_proxy/claude-haiku-4-5-20251001', stream=True)
  Text       "I'll create the hello.py file and then run it with python3."
  ToolCall   write  /hello.py
  ToolCall   bash   python3 /hello.py
  Text       'Let me check the current working directory and create the file there:'
  ToolCall   bash   pwd
  ToolCall   write  ./hello.py
  Text       'Now let me run it:'
  ToolCall   bash   python3 <tmp>/repo/hello.py
  Text       "Perfect! I've successfully created `hello.py` and run it. The output is: ``` hi from deepagents ``` The script"
  FileChange created hello.py
  Done       stop_reason=done calls=4 tokens=16204 cost=$0.01527
  hello.py:  "print('hi from deepagents')"

Sessions keep the runtime's own conversation across turns and can be saved and resumed in another process:

with litellm.agent_session(Harness.CLAUDE_CODE, sandbox=box, model="litellm_proxy/claude") as s:
    s.run("Remember the code word PELICAN.")
    s.run("What was the code word?").text   # 'PELICAN'
    state = s.detach().dumps()              # bytes; no credentials inside

litellm.agent_resume(State.loads(state), sandbox=box, model="litellm_proxy/claude")

User Flow

Before: a developer who wants Codex to fix a repo on their team's Bedrock models has no LiteLLM path

  1. They install Codex and hand-edit ~/.codex/config.toml to point it at their LiteLLM gateway
  2. They paste their virtual key into the runtime's env, where any tool call can read it
  3. Spend shows up untagged on the gateway, mixed with every other request

After: the same developer runs Codex from Python through the gateway with one call

  1. They pip install litellm starlette uvicorn, have codex on PATH, and set LITELLM_PROXY_API_BASE / LITELLM_PROXY_API_KEY
  2. They call litellm.agent(Harness.CODEX, "fix the flaky test", sandbox=sandbox.local("./repo"), model="litellm_proxy/coder")
  3. Codex edits the repo; the call returns the final text, the changed files and result.cost
  4. The gateway spend logs show each request tagged harness,codex

Pre-Submission checklist

  • I have added meaningful tests
  • The handful of test files covering my change pass locally
  • My PR passes all required CI/CD checks
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5

Screenshots / Proof of Fix

Real runs against https://gateway.litellm-sandbox.ai, real model calls, no mocks, real claude, codex and opencode CLIs and real deepagents.

Before (82d8b37)

All harnesses

  1. python -c "import litellm; litellm.agent"
  2. AttributeError: module 'litellm' has no attribute 'agent'

After (f76c2ac)

One task on each harness

  1. python .claude/harness-docs/demo.py (the script behind the output under "What you get")
  2. All four created and ran hello.py: Claude Code $0.00233, Codex $0.05512, OpenCode $0.03474, Deep Agents $0.01527

E2E suite, all four harnesses

  1. LITELLM_PROXY_API_BASE=... LITELLM_PROXY_API_KEY=... pytest tests/harness_e2e -v
  2. 26 passed. For each harness: creates a file and reports cost, streams events with Done last, returns validated structured output, remembers an earlier turn, detaches and resumes, read-only blocks writes. Plus a string harness is rejected and an unsupported option fails before the runtime starts

Unit tests

  1. pytest tests/unit/harness tests/unit/llms/{base_llm/harness,claude_code,codex,opencode,deepagents}
  2. 285 passed, 1 skipped (the skip is a live test that needs gateway env)

Type

🆕 New Feature
✅ Test

Caveats (if any)

Medium

  • Runtimes aren't auto-installed; claude/codex/opencode must be on PATH
  • permissions="ask" (interactive approval) only on Deep Agents for now
  • CLI harnesses keep session transcripts in ~/.cache/litellm-harness so resume works
  • Docker sandbox: local endpoint binds 127.0.0.1, not yet verified from inside containers
  • tests/harness_e2e is allowlisted out of CI (needs live CLIs and a gateway); run it manually

Low

  • OSV scan fails on urllib3/pyjwt/next lockfile versions; also red on main since 18:57 today, not from this PR
  • Deep Agents resume across processes needs a persistent LangGraph checkpointer (in-process works)

Docs: BerriAI/litellm-docs#1945

@ishaan-berri

Copy link
Copy Markdown
Contributor Author

@Veria please re-review at 8e8f811. Both open issues (OpenCode repo plugins, Deep Agents symlink escape) are fixed with regression tests; details in the earlier comment. @greptileai please re-review the latest commits.

Comment thread litellm/harness/runtime.py Outdated
Comment thread litellm/llms/deepagents/harness/sandbox_backend.py
Comment on lines +487 to +493
post_routes = tuple(
Route(
f"{prefix}/{route}",
self._handle,
methods=["POST"], # mutable-ok: Starlette Route takes a methods list
)
for prefix, route in itertools.product(ROUTE_PREFIXES, POST_ROUTES)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Custom HTTP handler violates requirement

This endpoint registers its own Starlette routes and uses its own handler to parse and dispatch model requests. The repository requires new HTTP handlers to reuse LiteLLM’s existing HTTP-handler infrastructure instead of implementing a custom handler.

Rule Used: What: Block any PR which writes it's own custom http handler instead of reusing litellm's existing http handlers. (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@ishaan-berri

Copy link
Copy Markdown
Contributor Author

@greptileai thanks, addressed in d31d713:

  1. Successful turns can lose events: fixed. _put_end now waits for room in the queue, so the end marker lands behind every event and nothing is dropped. It only discards queued events when the turn was cancelled, because then there is no consumer left to drain the queue. New test test_turn_keeps_every_event_when_queue_overflows pushes 40 events through a 4-slot queue and asserts all 40 arrive; it fails on the old code.
  2. Nested file writes fail: fixed. The symlink check now resolves the nearest existing ancestor, so new_dir/sub/file.py is allowed. A path under a symlink that leaves the workspace is still refused. Tests: test_write_into_new_nested_directory_is_allowed, test_write_under_symlinked_dir_is_refused.
  3. Custom HTTP handler: this endpoint is not an outbound HTTP handler, so there is no existing handler to reuse for it. It is a small local server the CLI runtimes (Claude Code, Codex, OpenCode) call as their model base URL. They only speak HTTP, so something has to listen on a port for them. Its outbound calls already go through LiteLLM's shared HTTP infrastructure: gateway mode uses the cached client from get_async_httpx_client, and SDK mode calls litellm.anthropic.messages.acreate / litellm.acompletion / litellm.aresponses. Please re-review with that in mind.

Please re-review.

@ishaan-berri

Copy link
Copy Markdown
Contributor Author

@Veria please re-review at d31d713. Both of your open issues are fixed with regression tests (OpenCode runs with --pure; Deep Agents file tools refuse paths that resolve outside the workspace through symlinks).

@ishaan-berri
ishaan-berri enabled auto-merge (squash) October 1, 2026 22:07
@ishaan-berri
ishaan-berri merged commit 0f6a06a into main Oct 1, 2026
95 of 97 checks passed
@ishaan-berri
ishaan-berri deleted the litellm_harness_sdk branch October 1, 2026 22:27
Ryangr0 added a commit to webgrip/unfold that referenced this pull request Oct 8, 2026
litellm.agent() (BerriAI/litellm#43885, merged 2026-10-01) runs Claude
Code, Codex, opencode and Deep Agents through the gateway with a
per-session token. It overlaps with Ploeg's Run execution; Ploeg
already meters through LiteLLM.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ryangr0 pushed a commit to webgrip/unfold that referenced this pull request Oct 8, 2026
## [unfold-v0.4.0-rc.46](https://forgejo.webgrip.dev/webgrip/unfold/compare/unfold-v0.4.0-rc.45...unfold-v0.4.0-rc.46) (2026-10-08)

### Added

* **unfold:** answer VS Code's getNetworkDiagnosticsInfo on the AHP host ([b4009da](https://forgejo.webgrip.dev/webgrip/unfold/commit/b4009da67d51e5a79e79978cb3ef95236e0b393f))
* **unfold:** keep AHP read and archive state per person ([d91f8db](https://forgejo.webgrip.dev/webgrip/unfold/commit/d91f8db0a1bde5fb911a5b275e071a5bc026fa38))
* **unfold:** let an AHP client accept a candidate, and drop the dead operation ([288c2bb](https://forgejo.webgrip.dev/webgrip/unfold/commit/288c2bb23e07bb1492cf01f26c794cddd2483223))
* **unfold:** resume a known AHP client with reconnect on a new connection ([68e3da7](https://forgejo.webgrip.dev/webgrip/unfold/commit/68e3da765bac19cd0e0d4072511a018d640865ee))

### Fixed

* **unfold:** accept GHE.com API roots for GitHub task sources ([39a7e75](https://forgejo.webgrip.dev/webgrip/unfold/commit/39a7e7574fda64a5c01d706b9e05f0c78f09e6bb))
* **unfold:** advertise the AHP candidate as a session changeset on the chat ([c2ee2f6](https://forgejo.webgrip.dev/webgrip/unfold/commit/c2ee2f6699059c58c5a1e1dc977b7825f5b24cdd))
* **unfold:** announce AHP chat activity with chat/activityChanged ([2ad38a0](https://forgejo.webgrip.dev/webgrip/unfold/commit/2ad38a0cca2c4bfd8dfbcd1d6fd76592befd9cd3))
* **unfold:** clear finished AHP activity with null in summary changes ([e1ed1dc](https://forgejo.webgrip.dev/webgrip/unfold/commit/e1ed1dc0f16f4dde3c8601c362db6b97eafb469e))
* **unfold:** echo every accepted AHP client action in server order ([a921c21](https://forgejo.webgrip.dev/webgrip/unfold/commit/a921c2189cb39c446f6df170ccbbf25629294626))
* **unfold:** honour createSession.activeClient on the AHP host ([18f908e](https://forgejo.webgrip.dev/webgrip/unfold/commit/18f908e51c25326370a29dbd6c62d85da367f3b3))
* **unfold:** keep a dropped AHP active client through its reconnect ([b614313](https://forgejo.webgrip.dev/webgrip/unfold/commit/b614313b3ac10f20f98c92d96a7bef8631182fa2))
* **unfold:** keep a resumed AHP client's session spelling ([2ee4ce5](https://forgejo.webgrip.dev/webgrip/unfold/commit/2ee4ce5a300b6c2dbfa20cfd9ae0a5644ea760ba))
* **unfold:** keep every AHP input request inside a turn the client holds ([fd334a6](https://forgejo.webgrip.dev/webgrip/unfold/commit/fd334a68b23ece7bdd5e98d2ed5914b373fedc62))
* **unfold:** keep GHE.com enterprises apart when matching checkout remotes ([1039e00](https://forgejo.webgrip.dev/webgrip/unfold/commit/1039e00e1ebd6b8dfe538060bc2deffcbc19c899))
* **unfold:** negotiate the AHP protocol version by the 1.0 rule ([748ddf5](https://forgejo.webgrip.dev/webgrip/unfold/commit/748ddf5c3eff6dad0ee495a2d15b6c2be67b2875))
* **unfold:** send AHP questions with a message, their choices and a real answer path ([3334a9e](https://forgejo.webgrip.dev/webgrip/unfold/commit/3334a9e0d1b11e5a355d60d9989889def62977ac))
* **unfold:** send root/activeSessionsChanged to each AHP viewer ([eaf46e4](https://forgejo.webgrip.dev/webgrip/unfold/commit/eaf46e4c1130fa2ea029fb807387e4ef747de3a5))
* **unfold:** serve each AHP candidate file whole on both sides ([78d9951](https://forgejo.webgrip.dev/webgrip/unfold/commit/78d995166e2e33c14e98da531c43d91244f4ac39))
* **unfold:** spell AHP session URIs by VS Code's own host rule ([f087609](https://forgejo.webgrip.dev/webgrip/unfold/commit/f0876092b6467000a6f70fbe8252da71cfd282b1))
* **unfold:** spell AHP session URIs the way each client asked ([f71edfb](https://forgejo.webgrip.dev/webgrip/unfold/commit/f71edfb512c6e74094f45149d04c85496a25f185))
* **unfold:** stop doubling the first chunk of every streamed AHP markdown part ([a97a9ec](https://forgejo.webgrip.dev/webgrip/unfold/commit/a97a9ececc9983a9a3a1f2329e5e55fe21a0bb5b))

### Docs

* add LiteLLM agent() to the market landscape ([b6ddb52](https://forgejo.webgrip.dev/webgrip/unfold/commit/b6ddb527dbffa49e54ad6233827613b1b2c970da)), references [BerriAI/litellm#43885](https://forgejo.webgrip.dev/BerriAI/litellm/issues/43885)
* record the VS Code 1.141 fit, AHP 1.0 and the two 1.142 host breaks ([1d0fef3](https://forgejo.webgrip.dev/webgrip/unfold/commit/1d0fef3e5271e60c3a66e3ea36b320584a2c1a01))
* **unfold:** record the AHP host fixes and correct the Open VSX risk ([c9ec4f0](https://forgejo.webgrip.dev/webgrip/unfold/commit/c9ec4f0fb2f4df58bf287cecc32921c1ff55c4b3))
* **unfold:** record the final AHP end-to-end run and reconnect fixes ([62d0e65](https://forgejo.webgrip.dev/webgrip/unfold/commit/62d0e6555692cce85e8984db79ee4a067548ed23))

### Tests

* **unfold:** replay VS Code main and 1.141 against the AHP host ([9b884ec](https://forgejo.webgrip.dev/webgrip/unfold/commit/9b884ecbb6191e0373a90c9880fb34f9c2fea708))
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.

4 participants