mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
These three cells were failing for reasons unrelated to LiteLLM translation: - vision: tests passed `--image <path>`, a flag that no longer exists in Claude Code 2.x (image attachment is now via the Files API or via `--input-format stream-json` with inline content blocks). Rewrite the cells to feed an Anthropic-shaped user message containing both text and a base64 `image` content block through stdin in stream-json mode. Hermetic — no temp file or Files API upload needed. - extended_thinking: tests set `MAX_THINKING_TOKENS=4096` as an env var, which Claude Code 2.x ignores. Switch to `--effort max` (the current CLI knob) and use a non-trivial prompt (3-gallon / 5-gallon jug puzzle). With trivial arithmetic the modern Sonnet/Opus tiers optimize away the thinking step and arrive without a thinking block, which made the test silently false-fail. - web_search: assertion looked for `server_tool_use` / `web_search_tool_result` blocks, but Claude Code's `WebSearch` is a *client-side* tool: the CLI executes the search itself and feeds the result back as a regular `tool_result` block. The Anthropic server-side `web_search_20250305` tool only fires when injected into the request directly (which the CLI does not do). Update the assertion to look for a `tool_use` block whose name is `WebSearch` — that's the right signal that the proxy preserved both the request-side tool definition and the response-side tool_use block end-to-end. Driver change required to support stream-json input + variadic flags: - cli_driver: insert `--` before the prompt positional. Variadic flags like `--allowed-tools <tools...>` (commander.js) greedily consume every following token, so the prompt was being eaten as a tool name and the CLI would error out with "Input must be provided either through stdin or as a prompt argument when using --print". - cli_driver: thread a `stdin_input` parameter through `run_claude` and `run_claude_models_parallel` so the vision rewrite can pipe stream-json events to the CLI on stdin. Validated end-to-end against a live LiteLLM proxy: all three Anthropic cells now pass on Haiku 4.5, Sonnet 4.6, and Opus 4.7. Driver unit tests (120) still green.
142 lines
4.6 KiB
Python
142 lines
4.6 KiB
Python
"""vision x Azure.
|
|
|
|
Drive the real `claude` CLI against a running LiteLLM proxy that routes
|
|
to Azure, attach a small image as an inline base64 `image` content
|
|
block via the CLI's `--input-format stream-json` mode, and assert that
|
|
the upstream produces a non-empty reply. This proves the proxy
|
|
preserves Claude Code's multimodal content blocks end-to-end.
|
|
|
|
The (feature, provider) for this cell is inferred from the file path by
|
|
`tests/claude_code/conftest.py`:
|
|
|
|
tests/claude_code/vision/test_azure.py
|
|
^^^^^^ ^^^^^^^^^
|
|
feature_id provider
|
|
|
|
Why stream-json input rather than `--image <path>`: the claude CLI
|
|
dropped `--image` in 2.x. Image attachments are now driven via either
|
|
the Files API (server-uploaded blobs referenced by file_id) or by
|
|
sending an Anthropic-shaped user message through stdin. We use the
|
|
latter because it requires no upstream pre-upload — the test stays
|
|
hermetic and the wire shape (an `image` content block) is exactly what
|
|
the proxy must preserve.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from tests.claude_code.cli_driver import (
|
|
ClaudeCLIError,
|
|
failure_diagnostic,
|
|
run_claude_models_parallel,
|
|
)
|
|
|
|
PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
|
|
PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
|
|
|
|
AZURE_MODELS = [
|
|
"claude-haiku-4-5-azure",
|
|
"claude-sonnet-4-6-azure",
|
|
"claude-opus-4-7-azure",
|
|
]
|
|
|
|
# Minimal 1x1 red PNG, base64-encoded. We embed it directly as the
|
|
# `image` content block's source — no temp file or Files API upload
|
|
# needed, the test stays hermetic.
|
|
RED_PIXEL_PNG_B64 = (
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8"
|
|
"z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
|
|
)
|
|
|
|
VISION_PROMPT = (
|
|
"What single color do you see in the attached image? Answer in one word."
|
|
)
|
|
|
|
|
|
def _build_stdin_input() -> str:
|
|
"""Build the newline-delimited JSON payload for `--input-format stream-json`.
|
|
|
|
The CLI consumes a stream of `user` events whose `message.content` is
|
|
a list of Anthropic content blocks. A single user event with one
|
|
text block + one image block is enough to exercise the multimodal
|
|
code path.
|
|
"""
|
|
user_event = {
|
|
"type": "user",
|
|
"message": {
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": VISION_PROMPT},
|
|
{
|
|
"type": "image",
|
|
"source": {
|
|
"type": "base64",
|
|
"media_type": "image/png",
|
|
"data": RED_PIXEL_PNG_B64,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}
|
|
return json.dumps(user_event) + "\n"
|
|
|
|
|
|
def test_vision_azure(compat_result):
|
|
"""Drive the `claude` CLI against the LiteLLM proxy with an image
|
|
attached via stream-json input and assert a non-empty reply."""
|
|
base_url = os.environ.get(PROXY_BASE_URL_ENV)
|
|
api_key = os.environ.get(PROXY_API_KEY_ENV)
|
|
if not base_url or not api_key:
|
|
compat_result.set(
|
|
{
|
|
"status": "fail",
|
|
"error": (
|
|
f"missing required env: set {PROXY_BASE_URL_ENV} and "
|
|
f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
|
|
),
|
|
}
|
|
)
|
|
pytest.fail(
|
|
f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
|
|
)
|
|
|
|
outcomes = run_claude_models_parallel(
|
|
models=AZURE_MODELS,
|
|
# When using --input-format stream-json the CLI rejects a
|
|
# positional prompt; the prompt + image come in via stdin.
|
|
prompt=None,
|
|
base_url=base_url,
|
|
api_key=api_key,
|
|
extra_args=["--input-format", "stream-json"],
|
|
stdin_input=_build_stdin_input(),
|
|
)
|
|
|
|
failures = []
|
|
for model in AZURE_MODELS:
|
|
outcome = outcomes[model]
|
|
if isinstance(outcome, ClaudeCLIError):
|
|
error = f"[{model}] {outcome}"
|
|
compat_result.add({"status": "fail", "error": error})
|
|
failures.append(error)
|
|
continue
|
|
|
|
if outcome.exit_code != 0:
|
|
error = f"[{model}] claude CLI failed: {failure_diagnostic(outcome)}"
|
|
compat_result.add({"status": "fail", "error": error})
|
|
failures.append(error)
|
|
continue
|
|
|
|
if not outcome.text.strip():
|
|
error = f"[{model}] claude returned empty assistant text on a vision prompt"
|
|
compat_result.add({"status": "fail", "error": error})
|
|
failures.append(error)
|
|
continue
|
|
|
|
compat_result.add({"status": "pass"})
|
|
|
|
if failures:
|
|
pytest.fail("; ".join(failures), pytrace=False)
|