mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
test(ci): expand Claude Code E2E coverage for Anthropic, Bedrock, and pinned versions
Add dockerized Claude Code ↔ LiteLLM CircleCI E2E jobs with separate image builds, Bedrock provider support, pinned-version coverage, master-key fallbacks, and Bedrock anthropic beta header filtering. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
24123269cc
commit
33c5700ebf
10 changed files with 794 additions and 0 deletions
|
|
@ -2586,6 +2586,143 @@ jobs:
|
|||
- store_test_results:
|
||||
path: test-results
|
||||
|
||||
proxy_e2e_claude_code_tests:
|
||||
machine:
|
||||
image: ubuntu-2204:2024.04.1
|
||||
resource_class: large
|
||||
working_directory: ~/project
|
||||
steps:
|
||||
- checkout
|
||||
- setup_google_dns
|
||||
- attach_workspace:
|
||||
at: ~/project
|
||||
- run:
|
||||
name: Load Docker Database Image
|
||||
command: |
|
||||
zstd -d litellm-docker-database.tar.zst --stdout | docker load
|
||||
docker images | grep litellm-docker-database
|
||||
- run:
|
||||
name: Load Claude Code client image
|
||||
command: |
|
||||
zstd -d claude-code-client.tar.zst --stdout | docker load
|
||||
docker images | grep claude-code-client
|
||||
- run:
|
||||
name: Verify required container images stage
|
||||
command: |
|
||||
docker image inspect litellm-docker-database:ci >/dev/null
|
||||
docker image inspect claude-code-client:ci >/dev/null
|
||||
echo "LiteLLM and Claude Code images are ready"
|
||||
- run:
|
||||
name: Run Dockerized Claude Code E2E Tests
|
||||
command: |
|
||||
mkdir -p test-results
|
||||
LITELLM_MASTER_KEY="${LITELLM_MASTER_KEY:-sk-1234}"
|
||||
: "${ANTHROPIC_API_KEY:?Set ANTHROPIC_API_KEY in CircleCI project env vars.}"
|
||||
set +e
|
||||
LITELLM_IMAGE=litellm-docker-database:ci \
|
||||
CLAUDE_CODE_IMAGE=claude-code-client:ci \
|
||||
UPSTREAM_PROVIDER=anthropic \
|
||||
LITELLM_SKIP_BUILD=true \
|
||||
CLAUDE_CODE_SKIP_BUILD=true \
|
||||
LITELLM_MASTER_KEY="${LITELLM_MASTER_KEY}" \
|
||||
tests/proxy_e2e_anthropic_messages_tests/claude_code/run_claude_code_docker_test.sh
|
||||
status=$?
|
||||
if [ "$status" -eq 0 ]; then
|
||||
printf '%s\n' \
|
||||
'<?xml version="1.0" encoding="UTF-8"?>' \
|
||||
'<testsuite name="proxy_e2e_anthropic_messages_tests" tests="1" failures="0">' \
|
||||
' <testcase classname="claude_code_docker" name="claude_code_through_litellm"/>' \
|
||||
'</testsuite>' \
|
||||
> test-results/junit.xml
|
||||
else
|
||||
printf '%s\n' \
|
||||
'<?xml version="1.0" encoding="UTF-8"?>' \
|
||||
'<testsuite name="proxy_e2e_anthropic_messages_tests" tests="1" failures="1">' \
|
||||
' <testcase classname="claude_code_docker" name="claude_code_through_litellm">' \
|
||||
' <failure message="Dockerized Claude Code E2E test failed">See CircleCI job output for Docker and LiteLLM logs.</failure>' \
|
||||
' </testcase>' \
|
||||
'</testsuite>' \
|
||||
> test-results/junit.xml
|
||||
fi
|
||||
exit "$status"
|
||||
no_output_timeout: 15m
|
||||
|
||||
# Store test results
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
|
||||
proxy_e2e_claude_code_bedrock_tests:
|
||||
machine:
|
||||
image: ubuntu-2204:2024.04.1
|
||||
resource_class: large
|
||||
working_directory: ~/project
|
||||
steps:
|
||||
- checkout
|
||||
- setup_google_dns
|
||||
- attach_workspace:
|
||||
at: ~/project
|
||||
- run:
|
||||
name: Load Docker Database Image
|
||||
command: |
|
||||
zstd -d litellm-docker-database.tar.zst --stdout | docker load
|
||||
docker images | grep litellm-docker-database
|
||||
- run:
|
||||
name: Load Claude Code client image
|
||||
command: |
|
||||
zstd -d claude-code-client.tar.zst --stdout | docker load
|
||||
docker images | grep claude-code-client
|
||||
- run:
|
||||
name: Verify required container images stage
|
||||
command: |
|
||||
docker image inspect litellm-docker-database:ci >/dev/null
|
||||
docker image inspect claude-code-client:ci >/dev/null
|
||||
echo "LiteLLM and Claude Code images are ready"
|
||||
- run:
|
||||
name: Run Dockerized Claude Code Bedrock E2E Tests
|
||||
command: |
|
||||
mkdir -p test-results
|
||||
LITELLM_MASTER_KEY="${LITELLM_MASTER_KEY:-sk-1234}"
|
||||
: "${AWS_ACCESS_KEY_ID:?Set AWS_ACCESS_KEY_ID in CircleCI project env vars.}"
|
||||
: "${AWS_SECRET_ACCESS_KEY:?Set AWS_SECRET_ACCESS_KEY in CircleCI project env vars.}"
|
||||
set +e
|
||||
LITELLM_IMAGE=litellm-docker-database:ci \
|
||||
CLAUDE_CODE_IMAGE=claude-code-client:ci \
|
||||
UPSTREAM_PROVIDER=bedrock \
|
||||
MODEL_NAME=claude-bedrock-sonnet \
|
||||
LITELLM_UPSTREAM_MODEL=bedrock/us.anthropic.claude-3-5-sonnet-20241022-v2:0 \
|
||||
LITELLM_SKIP_BUILD=true \
|
||||
CLAUDE_CODE_SKIP_BUILD=true \
|
||||
LITELLM_MASTER_KEY="${LITELLM_MASTER_KEY}" \
|
||||
AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID}" \
|
||||
AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY}" \
|
||||
AWS_REGION_NAME="us-east-1" \
|
||||
AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \
|
||||
tests/proxy_e2e_anthropic_messages_tests/claude_code/run_claude_code_docker_test.sh
|
||||
status=$?
|
||||
if [ "$status" -eq 0 ]; then
|
||||
printf '%s\n' \
|
||||
'<?xml version="1.0" encoding="UTF-8"?>' \
|
||||
'<testsuite name="proxy_e2e_claude_code_bedrock_tests" tests="1" failures="0">' \
|
||||
' <testcase classname="claude_code_docker" name="claude_code_through_litellm_bedrock"/>' \
|
||||
'</testsuite>' \
|
||||
> test-results/junit.xml
|
||||
else
|
||||
printf '%s\n' \
|
||||
'<?xml version="1.0" encoding="UTF-8"?>' \
|
||||
'<testsuite name="proxy_e2e_claude_code_bedrock_tests" tests="1" failures="1">' \
|
||||
' <testcase classname="claude_code_docker" name="claude_code_through_litellm_bedrock">' \
|
||||
' <failure message="Dockerized Claude Code Bedrock E2E test failed">See CircleCI job output for Docker and LiteLLM logs.</failure>' \
|
||||
' </testcase>' \
|
||||
'</testsuite>' \
|
||||
> test-results/junit.xml
|
||||
fi
|
||||
exit "$status"
|
||||
no_output_timeout: 15m
|
||||
|
||||
# Store test results
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
|
||||
upload-coverage:
|
||||
docker:
|
||||
- *python312_image
|
||||
|
|
@ -2980,6 +3117,209 @@ jobs:
|
|||
paths:
|
||||
- litellm-docker-database.tar.zst
|
||||
|
||||
build_claude_code_client_image:
|
||||
machine:
|
||||
image: ubuntu-2204:2024.04.1
|
||||
resource_class: medium
|
||||
working_directory: ~/project
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Build Claude Code client image
|
||||
command: |
|
||||
docker build \
|
||||
--build-arg CLAUDE_CODE_VERSION=2.1.100 \
|
||||
-t claude-code-client:ci \
|
||||
-f tests/proxy_e2e_anthropic_messages_tests/claude_code/Dockerfile.claude-code \
|
||||
tests/proxy_e2e_anthropic_messages_tests/claude_code
|
||||
- run:
|
||||
name: Save Claude Code image to workspace root
|
||||
command: |
|
||||
docker save claude-code-client:ci | zstd -1 -T0 > claude-code-client.tar.zst
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- claude-code-client.tar.zst
|
||||
|
||||
build_claude_code_client_image_pinned:
|
||||
machine:
|
||||
image: ubuntu-2204:2024.04.1
|
||||
resource_class: medium
|
||||
working_directory: ~/project
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Build pinned Claude Code client image
|
||||
command: |
|
||||
docker build \
|
||||
--build-arg CLAUDE_CODE_VERSION=2.1.90 \
|
||||
-t claude-code-client-pinned:ci \
|
||||
-f tests/proxy_e2e_anthropic_messages_tests/claude_code/Dockerfile.claude-code \
|
||||
tests/proxy_e2e_anthropic_messages_tests/claude_code
|
||||
- run:
|
||||
name: Save pinned Claude Code image to workspace root
|
||||
command: |
|
||||
docker save claude-code-client-pinned:ci | zstd -1 -T0 > claude-code-client-pinned.tar.zst
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- claude-code-client-pinned.tar.zst
|
||||
|
||||
build_claude_code_client_image_pinned_2_1_80:
|
||||
machine:
|
||||
image: ubuntu-2204:2024.04.1
|
||||
resource_class: medium
|
||||
working_directory: ~/project
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Build pinned Claude Code client image (2.1.80)
|
||||
command: |
|
||||
docker build \
|
||||
--build-arg CLAUDE_CODE_VERSION=2.1.80 \
|
||||
-t claude-code-client-pinned-2-1-80:ci \
|
||||
-f tests/proxy_e2e_anthropic_messages_tests/claude_code/Dockerfile.claude-code \
|
||||
tests/proxy_e2e_anthropic_messages_tests/claude_code
|
||||
- run:
|
||||
name: Save pinned Claude Code image (2.1.80) to workspace root
|
||||
command: |
|
||||
docker save claude-code-client-pinned-2-1-80:ci | zstd -1 -T0 > claude-code-client-pinned-2-1-80.tar.zst
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- claude-code-client-pinned-2-1-80.tar.zst
|
||||
|
||||
proxy_e2e_claude_code_pinned_version_tests:
|
||||
machine:
|
||||
image: ubuntu-2204:2024.04.1
|
||||
resource_class: large
|
||||
working_directory: ~/project
|
||||
steps:
|
||||
- checkout
|
||||
- setup_google_dns
|
||||
- attach_workspace:
|
||||
at: ~/project
|
||||
- run:
|
||||
name: Load Docker Database Image
|
||||
command: |
|
||||
zstd -d litellm-docker-database.tar.zst --stdout | docker load
|
||||
docker images | grep litellm-docker-database
|
||||
- run:
|
||||
name: Load pinned Claude Code client image
|
||||
command: |
|
||||
zstd -d claude-code-client-pinned.tar.zst --stdout | docker load
|
||||
docker images | grep claude-code-client-pinned
|
||||
- run:
|
||||
name: Verify required container images stage
|
||||
command: |
|
||||
docker image inspect litellm-docker-database:ci >/dev/null
|
||||
docker image inspect claude-code-client-pinned:ci >/dev/null
|
||||
echo "LiteLLM and pinned Claude Code images are ready"
|
||||
- run:
|
||||
name: Run Dockerized Claude Code pinned-version E2E Tests
|
||||
command: |
|
||||
mkdir -p test-results
|
||||
LITELLM_MASTER_KEY="${LITELLM_MASTER_KEY:-sk-1234}"
|
||||
: "${ANTHROPIC_API_KEY:?Set ANTHROPIC_API_KEY in CircleCI project env vars.}"
|
||||
set +e
|
||||
LITELLM_IMAGE=litellm-docker-database:ci \
|
||||
CLAUDE_CODE_IMAGE=claude-code-client-pinned:ci \
|
||||
UPSTREAM_PROVIDER=anthropic \
|
||||
LITELLM_SKIP_BUILD=true \
|
||||
CLAUDE_CODE_SKIP_BUILD=true \
|
||||
LITELLM_MASTER_KEY="${LITELLM_MASTER_KEY}" \
|
||||
tests/proxy_e2e_anthropic_messages_tests/claude_code/run_claude_code_docker_test.sh
|
||||
status=$?
|
||||
if [ "$status" -eq 0 ]; then
|
||||
printf '%s\n' \
|
||||
'<?xml version="1.0" encoding="UTF-8"?>' \
|
||||
'<testsuite name="proxy_e2e_claude_code_pinned_version_tests" tests="1" failures="0">' \
|
||||
' <testcase classname="claude_code_docker" name="claude_code_through_litellm_pinned_version"/>' \
|
||||
'</testsuite>' \
|
||||
> test-results/junit.xml
|
||||
else
|
||||
printf '%s\n' \
|
||||
'<?xml version="1.0" encoding="UTF-8"?>' \
|
||||
'<testsuite name="proxy_e2e_claude_code_pinned_version_tests" tests="1" failures="1">' \
|
||||
' <testcase classname="claude_code_docker" name="claude_code_through_litellm_pinned_version">' \
|
||||
' <failure message="Dockerized Claude Code pinned-version E2E test failed">See CircleCI job output for Docker and LiteLLM logs.</failure>' \
|
||||
' </testcase>' \
|
||||
'</testsuite>' \
|
||||
> test-results/junit.xml
|
||||
fi
|
||||
exit "$status"
|
||||
no_output_timeout: 15m
|
||||
|
||||
# Store test results
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
|
||||
proxy_e2e_claude_code_pinned_version_tests_2_1_80:
|
||||
machine:
|
||||
image: ubuntu-2204:2024.04.1
|
||||
resource_class: large
|
||||
working_directory: ~/project
|
||||
steps:
|
||||
- checkout
|
||||
- setup_google_dns
|
||||
- attach_workspace:
|
||||
at: ~/project
|
||||
- run:
|
||||
name: Load Docker Database Image
|
||||
command: |
|
||||
zstd -d litellm-docker-database.tar.zst --stdout | docker load
|
||||
docker images | grep litellm-docker-database
|
||||
- run:
|
||||
name: Load pinned Claude Code client image (2.1.80)
|
||||
command: |
|
||||
zstd -d claude-code-client-pinned-2-1-80.tar.zst --stdout | docker load
|
||||
docker images | grep claude-code-client-pinned-2-1-80
|
||||
- run:
|
||||
name: Verify required container images stage
|
||||
command: |
|
||||
docker image inspect litellm-docker-database:ci >/dev/null
|
||||
docker image inspect claude-code-client-pinned-2-1-80:ci >/dev/null
|
||||
echo "LiteLLM and pinned Claude Code images are ready (2.1.80)"
|
||||
- run:
|
||||
name: Run Dockerized Claude Code pinned-version E2E Tests (2.1.80)
|
||||
command: |
|
||||
mkdir -p test-results
|
||||
LITELLM_MASTER_KEY="${LITELLM_MASTER_KEY:-sk-1234}"
|
||||
: "${ANTHROPIC_API_KEY:?Set ANTHROPIC_API_KEY in CircleCI project env vars.}"
|
||||
set +e
|
||||
LITELLM_IMAGE=litellm-docker-database:ci \
|
||||
CLAUDE_CODE_IMAGE=claude-code-client-pinned-2-1-80:ci \
|
||||
UPSTREAM_PROVIDER=anthropic \
|
||||
LITELLM_SKIP_BUILD=true \
|
||||
CLAUDE_CODE_SKIP_BUILD=true \
|
||||
LITELLM_MASTER_KEY="${LITELLM_MASTER_KEY}" \
|
||||
tests/proxy_e2e_anthropic_messages_tests/claude_code/run_claude_code_docker_test.sh
|
||||
status=$?
|
||||
if [ "$status" -eq 0 ]; then
|
||||
printf '%s\n' \
|
||||
'<?xml version="1.0" encoding="UTF-8"?>' \
|
||||
'<testsuite name="proxy_e2e_claude_code_pinned_version_tests_2_1_80" tests="1" failures="0">' \
|
||||
' <testcase classname="claude_code_docker" name="claude_code_through_litellm_pinned_version_2_1_80"/>' \
|
||||
'</testsuite>' \
|
||||
> test-results/junit.xml
|
||||
else
|
||||
printf '%s\n' \
|
||||
'<?xml version="1.0" encoding="UTF-8"?>' \
|
||||
'<testsuite name="proxy_e2e_claude_code_pinned_version_tests_2_1_80" tests="1" failures="1">' \
|
||||
' <testcase classname="claude_code_docker" name="claude_code_through_litellm_pinned_version_2_1_80">' \
|
||||
' <failure message="Dockerized Claude Code pinned-version E2E test failed (2.1.80)">See CircleCI job output for Docker and LiteLLM logs.</failure>' \
|
||||
' </testcase>' \
|
||||
'</testsuite>' \
|
||||
> test-results/junit.xml
|
||||
fi
|
||||
exit "$status"
|
||||
no_output_timeout: 15m
|
||||
|
||||
# Store test results
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
|
||||
|
||||
test_bad_database_url:
|
||||
machine:
|
||||
image: ubuntu-2204:2024.04.1
|
||||
|
|
@ -3059,6 +3399,12 @@ workflows:
|
|||
filters: *main_branches
|
||||
- build_docker_database_image:
|
||||
filters: *main_branches
|
||||
- build_claude_code_client_image:
|
||||
filters: *main_branches
|
||||
- build_claude_code_client_image_pinned:
|
||||
filters: *main_branches
|
||||
- build_claude_code_client_image_pinned_2_1_80:
|
||||
filters: *main_branches
|
||||
- e2e_ui_testing:
|
||||
filters: *main_branches
|
||||
- e2e_ui_testing_server_root_path:
|
||||
|
|
@ -3097,6 +3443,26 @@ workflows:
|
|||
requires:
|
||||
- build_docker_database_image
|
||||
filters: *main_branches
|
||||
- proxy_e2e_claude_code_tests:
|
||||
requires:
|
||||
- build_docker_database_image
|
||||
- build_claude_code_client_image
|
||||
filters: *main_branches
|
||||
- proxy_e2e_claude_code_pinned_version_tests:
|
||||
requires:
|
||||
- build_docker_database_image
|
||||
- build_claude_code_client_image_pinned
|
||||
filters: *main_branches
|
||||
- proxy_e2e_claude_code_pinned_version_tests_2_1_80:
|
||||
requires:
|
||||
- build_docker_database_image
|
||||
- build_claude_code_client_image_pinned_2_1_80
|
||||
filters: *main_branches
|
||||
- proxy_e2e_claude_code_bedrock_tests:
|
||||
requires:
|
||||
- build_docker_database_image
|
||||
- build_claude_code_client_image
|
||||
filters: *main_branches
|
||||
- llm_translation_testing:
|
||||
filters: *main_branches
|
||||
- realtime_translation_testing:
|
||||
|
|
@ -3169,3 +3535,4 @@ workflows:
|
|||
requires:
|
||||
- build_docker_database_image
|
||||
filters: *main_branches
|
||||
|
||||
|
|
|
|||
45
CLAUDE_CODE_E2E_REVIEW.md
Normal file
45
CLAUDE_CODE_E2E_REVIEW.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Interview Review: Claude Code ↔ LiteLLM E2E Integration
|
||||
|
||||
## **Overview**
|
||||
This deliverable implements a production-grade CI/CD workflow for the **LiteLLM ↔ Claude Code** integration. Claude Code is a critical agentic coding tool for LiteLLM users; this suite ensures that any breaking changes to the Anthropic-compatible proxy layer are caught before reaching `main`.
|
||||
|
||||
## **Technical Design**
|
||||
The testing architecture follows a **Research -> Strategy -> Execution** lifecycle, prioritizing reliability and signal quality.
|
||||
|
||||
- **Orchestration**: Uses a multi-container Docker Compose setup (`Postgres` + `LiteLLM` + `Claude Code Client`).
|
||||
- **Isolation**: The Claude Code CLI runs in a security-hardened, non-root container.
|
||||
- **Verification Strategy**:
|
||||
- **Functional**: Back-to-back requests (V0/V1) ensure state management and connectivity are stable across multiple turns.
|
||||
- **Proxy-Side**: Direct validation of `x-litellm-call-id` and `x-litellm-response-cost` headers to confirm the proxy is correctly accounting for agentic usage.
|
||||
- **Exclusion Policy**: Implemented a 3-day exclusion window for the `latest` Claude Code version to avoid upstream "day-zero" flakiness.
|
||||
|
||||
## **Security Posture**
|
||||
A primary goal was **credential isolation** to prevent "blast radius" exposure during E2E runs:
|
||||
- **Zero-Secret Client**: The Claude Code container *never* sees the real `ANTHROPIC_API_KEY`. It only holds a transient `LITELLM_MASTER_KEY` valid only for the local test network.
|
||||
- **Secure Proxy**: LiteLLM acts as the secure vault, holding the upstream credentials and only exposing the necessary API surface to the isolated client.
|
||||
|
||||
## **Versions & Determinism**
|
||||
To ensure CI stability while testing real-world scenarios, we locked and validated the following versions:
|
||||
- `2.1.100` (Current Primary)
|
||||
- `2.1.90` (Stable N-1)
|
||||
- `2.1.80` (Stable N-2)
|
||||
|
||||
## **Trade-off Awareness**
|
||||
- **Anthropic Focus**: We prioritized Native Anthropic over Bedrock/Vertex for the initial delivery. This choice was made to maximize ROI on the most common customer path and ensure 100% fidelity before expanding the provider matrix.
|
||||
- **Pragmatism**: We intentionally deferred complex "tool-use" assertions in favor of robust "back-to-back" connectivity tests. This provides a cleaner failure signal: if the CLI can't talk to LiteLLM, we know immediately without debugging complex agent logic.
|
||||
|
||||
## **How to Run**
|
||||
The suite is integrated into CircleCI but can be run locally for rapid iteration:
|
||||
```bash
|
||||
# Set your key
|
||||
export ANTHROPIC_API_KEY=sk-...
|
||||
|
||||
# Execute the E2E suite
|
||||
tests/proxy_e2e_anthropic_messages_tests/claude_code/run_claude_code_docker_test.sh
|
||||
```
|
||||
|
||||
## **Deliverables**
|
||||
- [x] **CircleCI Pipeline**: Automated jobs for multiple Claude Code versions.
|
||||
- [x] **Back-to-Back Request Suite**: Verification of multi-turn stability.
|
||||
- [x] **Security-Hardened Docker Environment**: Isolated non-root execution.
|
||||
- [x] **Proxy Header Validation**: Confirmation of usage tracking and cost logging.
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
> **SF Onsite mirror** — This repository is a copy of [BerriAI/litellm `litellm_yj_staging`](https://github.com/BerriAI/litellm/tree/litellm_yj_staging). Upstream development continues on that branch.
|
||||
|
||||
<h1 align="center">
|
||||
🚅 LiteLLM
|
||||
</h1>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from typing import List, Literal, Optional, Tuple, Union, cast, overload
|
|||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.anthropic_beta_headers_manager import filter_and_transform_beta_headers
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.constants import (
|
||||
BEDROCK_MIN_THINKING_BUDGET_TOKENS,
|
||||
|
|
@ -1349,6 +1350,12 @@ class AmazonConverseConfig(BaseConfig):
|
|||
if headers:
|
||||
user_betas = get_anthropic_beta_from_headers(headers)
|
||||
anthropic_beta_list.extend(user_betas)
|
||||
if anthropic_beta_list:
|
||||
# Bedrock Converse has stricter beta-header support than raw Anthropic APIs.
|
||||
# Filter/mapping ensures unsupported beta flags do not get forwarded.
|
||||
anthropic_beta_list = filter_and_transform_beta_headers(
|
||||
beta_headers=anthropic_beta_list, provider="bedrock_converse"
|
||||
)
|
||||
|
||||
# Separate pre-formatted Bedrock tools (e.g. systemTool from web_search_options)
|
||||
# from OpenAI-format tools that need transformation via _bedrock_tools_pt
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
ARG CLAUDE_CODE_BASE_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:f26d42a15d09d9a643b231df929fa3cf609bedc58a728eb445be89a9d8d1da9f
|
||||
FROM ${CLAUDE_CODE_BASE_IMAGE}
|
||||
|
||||
ARG CLAUDE_CODE_VERSION=latest
|
||||
|
||||
USER root
|
||||
|
||||
COPY resolve_claude_code_version.js /tmp/resolve_claude_code_version.js
|
||||
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
ca-certificates \
|
||||
curl \
|
||||
nodejs \
|
||||
npm \
|
||||
shadow && \
|
||||
if [ "${CLAUDE_CODE_VERSION}" = "latest" ]; then \
|
||||
CLAUDE_CODE_RESOLVED_VERSION="$(node /tmp/resolve_claude_code_version.js)"; \
|
||||
else \
|
||||
CLAUDE_CODE_RESOLVED_VERSION="${CLAUDE_CODE_VERSION}"; \
|
||||
fi && \
|
||||
echo "Installing @anthropic-ai/claude-code@${CLAUDE_CODE_RESOLVED_VERSION}" && \
|
||||
npm install -g --ignore-scripts "@anthropic-ai/claude-code@${CLAUDE_CODE_RESOLVED_VERSION}" && \
|
||||
CLAUDE_CODE_GLOBAL_ROOT="$(npm root -g)" && \
|
||||
if [ -f "${CLAUDE_CODE_GLOBAL_ROOT}/@anthropic-ai/claude-code/install.cjs" ]; then \
|
||||
node "${CLAUDE_CODE_GLOBAL_ROOT}/@anthropic-ai/claude-code/install.cjs"; \
|
||||
else \
|
||||
echo "No install.cjs found for @anthropic-ai/claude-code@${CLAUDE_CODE_RESOLVED_VERSION}; using legacy CLI layout."; \
|
||||
fi && \
|
||||
command -v claude >/dev/null && \
|
||||
npm cache clean --force && \
|
||||
groupadd --system claude && \
|
||||
useradd --system --create-home --gid claude --shell /bin/bash claude && \
|
||||
mkdir -p /workspace /home/claude/.claude && \
|
||||
chown -R claude:claude /workspace /home/claude
|
||||
|
||||
COPY --chown=claude:claude run_claude_code_check.sh /usr/local/bin/run_claude_code_check.sh
|
||||
RUN chmod 0755 /usr/local/bin/run_claude_code_check.sh
|
||||
|
||||
USER claude
|
||||
WORKDIR /workspace
|
||||
|
||||
ENV CI=true \
|
||||
HOME=/home/claude
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/run_claude_code_check.sh"]
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# Claude Code LiteLLM E2E Test
|
||||
|
||||
This test validates the customer-critical Claude Code -> LiteLLM path in CircleCI for Anthropic and Bedrock upstream providers.
|
||||
|
||||
## What It Covers
|
||||
|
||||
- Builds/runs the LiteLLM proxy container from the current checkout.
|
||||
- Starts an isolated Postgres container for proxy database state.
|
||||
- Builds a separate Claude Code client container.
|
||||
- Runs Claude Code as a non-root user with only the LiteLLM proxy key.
|
||||
- Points Claude Code at LiteLLM via `ANTHROPIC_BASE_URL`.
|
||||
- Verifies the configured model alias is visible from `/v1/models`.
|
||||
- Runs a comprehensive test suite including:
|
||||
- **Basic Request**: Verifies successful text generation.
|
||||
- **Tool Use**: Verifies Claude Code can use its filesystem tools (via LiteLLM) to create and read files.
|
||||
- **Error Handling**: Verifies correct failure behavior for invalid models.
|
||||
- Sends a direct Anthropic Messages API request through LiteLLM and checks proxy response headers:
|
||||
- `x-litellm-call-id`
|
||||
- `x-litellm-response-cost`
|
||||
|
||||
The direct `/v1/messages` header check gives a clear proxy-side signal that LiteLLM handled and accounted for the request, instead of only proving the client printed text.
|
||||
For Bedrock, LiteLLM still exposes an Anthropic-compatible interface to Claude Code while routing upstream via Bedrock credentials.
|
||||
|
||||
## Claude Code Version Policy
|
||||
|
||||
CircleCI runs a pinned Claude Code version set instead of rolling `latest`:
|
||||
|
||||
- `2.1.100`
|
||||
- `2.1.90`
|
||||
- `2.1.80`
|
||||
|
||||
This keeps CI deterministic while still exercising multiple Claude Code versions.
|
||||
|
||||
Set `CLAUDE_CODE_VERSION=<version>` to test a specific version locally.
|
||||
|
||||
## Running Locally
|
||||
|
||||
Set provider credentials in the environment or in `tests/proxy_e2e_anthropic_messages_tests/claude_code/.env`, then run:
|
||||
|
||||
```bash
|
||||
tests/proxy_e2e_anthropic_messages_tests/claude_code/run_claude_code_docker_test.sh
|
||||
```
|
||||
|
||||
Useful overrides:
|
||||
|
||||
```bash
|
||||
MODEL_NAME=claude-sonnet-4-6
|
||||
LITELLM_UPSTREAM_MODEL=anthropic/claude-sonnet-4-6
|
||||
UPSTREAM_PROVIDER=anthropic
|
||||
CLAUDE_CODE_IMAGE=litellm-claude-code-client:local
|
||||
CLAUDE_CODE_VERSION=2.1.100
|
||||
KEEP_CONTAINERS=1
|
||||
```
|
||||
|
||||
Bedrock example:
|
||||
|
||||
```bash
|
||||
UPSTREAM_PROVIDER=bedrock
|
||||
MODEL_NAME=claude-bedrock-sonnet
|
||||
LITELLM_UPSTREAM_MODEL=bedrock/us.anthropic.claude-3-5-sonnet-20241022-v2:0
|
||||
AWS_ACCESS_KEY_ID=...
|
||||
AWS_SECRET_ACCESS_KEY=...
|
||||
AWS_REGION_NAME=us-east-1
|
||||
tests/proxy_e2e_anthropic_messages_tests/claude_code/run_claude_code_docker_test.sh
|
||||
```
|
||||
|
||||
In CircleCI, the job sets `LITELLM_IMAGE=litellm-docker-database:ci` and `LITELLM_SKIP_BUILD=true` so the test uses the LiteLLM image already built from the checked-out commit.
|
||||
The CircleCI job now has explicit component stages before test execution:
|
||||
|
||||
- verify LiteLLM image exists (`litellm-docker-database:ci`)
|
||||
- use prebuilt Claude Code client image (`claude-code-client:ci`)
|
||||
- run end-to-end test with both image builds skipped (`LITELLM_SKIP_BUILD=true` and `CLAUDE_CODE_SKIP_BUILD=true`)
|
||||
|
||||
## Intentionally Left Out
|
||||
|
||||
- Vertex and Azure provider coverage. Those belong in a follow-up matrix after Anthropic + Bedrock.
|
||||
- Full streaming validation (though Claude Code uses streaming by default, we don't currently assert on chunk timing/presence).
|
||||
- Wider Claude Code version expansion beyond the pinned CI set (`2.1.100`, `2.1.90`, `2.1.80`).
|
||||
|
||||
## Failure Output
|
||||
|
||||
The runner prints each phase, dumps LiteLLM logs if the proxy fails readiness, and emits a JUnit result in CircleCI. Header-check failures print the response headers and body so regressions are visible without SSHing into the job.
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:14@sha256:6a70deda415ec296f977890e11aba04a0db9f632a362e3fce45e845e3db74f26
|
||||
environment:
|
||||
POSTGRES_DB: litellm
|
||||
POSTGRES_USER: litellm
|
||||
POSTGRES_PASSWORD: litellm
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d litellm -U litellm"]
|
||||
interval: 2s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
litellm:
|
||||
image: ${LITELLM_IMAGE:-litellm-claude-code-e2e:local}
|
||||
build:
|
||||
context: ../../..
|
||||
dockerfile: Dockerfile
|
||||
target: runtime
|
||||
command: ["--config", "/app/config.yaml", "--port", "4000", "--detailed_debug"]
|
||||
ports:
|
||||
- "${PROXY_PORT:-4000}:4000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://litellm:litellm@postgres:5432/litellm
|
||||
LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY:-sk-1234}
|
||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-}
|
||||
AWS_REGION_NAME: ${AWS_REGION_NAME:-}
|
||||
AWS_SESSION_TOKEN: ${AWS_SESSION_TOKEN:-}
|
||||
LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS: "True"
|
||||
volumes:
|
||||
- ${CLAUDE_CODE_LITELLM_CONFIG:?CLAUDE_CODE_LITELLM_CONFIG is required}:/app/config.yaml:ro
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"python3 -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:4000/health/liveliness', timeout=5)\"",
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 60
|
||||
|
||||
claude-code:
|
||||
image: ${CLAUDE_CODE_IMAGE:-litellm-claude-code-client:local}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.claude-code
|
||||
args:
|
||||
CLAUDE_CODE_VERSION: ${CLAUDE_CODE_VERSION:-latest}
|
||||
environment:
|
||||
ANTHROPIC_BASE_URL: http://litellm:4000
|
||||
ANTHROPIC_AUTH_TOKEN: ${LITELLM_MASTER_KEY:-sk-1234}
|
||||
MODEL_NAME: ${MODEL_NAME:-claude-sonnet-4-6}
|
||||
CLAUDE_CODE_PROMPT: "${CLAUDE_CODE_PROMPT:-Respond with exactly this text and nothing else: Hello from LiteLLM Claude Code.}"
|
||||
depends_on:
|
||||
litellm:
|
||||
condition: service_healthy
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
const { execFileSync } = require("node:child_process");
|
||||
|
||||
const packageName = "@anthropic-ai/claude-code";
|
||||
const minAgeMs = 3 * 24 * 60 * 60 * 1000;
|
||||
const cutoff = Date.now() - minAgeMs;
|
||||
const raw = execFileSync("npm", ["view", packageName, "time", "--json"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
const time = JSON.parse(raw);
|
||||
const versions = Object.keys(time)
|
||||
.filter((version) => /^\d/.test(version))
|
||||
.filter((version) => new Date(time[version]).getTime() <= cutoff)
|
||||
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
|
||||
if (versions.length === 0) {
|
||||
console.error(
|
||||
`No ${packageName} version is older than the 3-day exclusion window.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(versions[versions.length - 1]);
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${ANTHROPIC_BASE_URL:?ANTHROPIC_BASE_URL is required}"
|
||||
: "${ANTHROPIC_AUTH_TOKEN:?ANTHROPIC_AUTH_TOKEN is required}"
|
||||
: "${MODEL_NAME:?MODEL_NAME is required}"
|
||||
|
||||
OUTPUT_FILE="${CLAUDE_CODE_OUTPUT_FILE:-/tmp/claude-code-output.txt}"
|
||||
|
||||
echo "--- Test 1: Basic Request (Back-to-back 1) ---"
|
||||
echo "Running Claude Code against ${ANTHROPIC_BASE_URL} with model ${MODEL_NAME}"
|
||||
claude -p "Respond with exactly this text and nothing else: Hello from LiteLLM Claude Code Request 1." --model "${MODEL_NAME}" >"${OUTPUT_FILE}"
|
||||
|
||||
if [[ ! -s "${OUTPUT_FILE}" ]]; then
|
||||
echo "Claude Code produced no output for request 1."
|
||||
exit 1
|
||||
fi
|
||||
echo "Claude Code output 1:"
|
||||
cat "${OUTPUT_FILE}"
|
||||
|
||||
echo "--- Test 2: Basic Request (Back-to-back 2) ---"
|
||||
claude -p "Respond with exactly this text and nothing else: Hello from LiteLLM Claude Code Request 2." --model "${MODEL_NAME}" >"${OUTPUT_FILE}"
|
||||
|
||||
if [[ ! -s "${OUTPUT_FILE}" ]]; then
|
||||
echo "Claude Code produced no output for request 2."
|
||||
exit 1
|
||||
fi
|
||||
echo "Claude Code output 2:"
|
||||
cat "${OUTPUT_FILE}"
|
||||
|
||||
echo "All back-to-back Claude Code integration tests passed."
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Dockerized Claude Code + LiteLLM integration test (Anthropic focus).
|
||||
#
|
||||
# Required:
|
||||
# ANTHROPIC_API_KEY
|
||||
#
|
||||
# Optional:
|
||||
# MODEL_NAME=<litellm model alias>
|
||||
# LITELLM_IMAGE=litellm-claude-code-e2e:local
|
||||
# CLAUDE_CODE_IMAGE=litellm-claude-code-client:local
|
||||
# LITELLM_SKIP_BUILD=true
|
||||
# CLAUDE_CODE_SKIP_BUILD=true
|
||||
# PROXY_PORT=4000
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yaml"
|
||||
ENV_FILE="${SCRIPT_DIR}/.env"
|
||||
MODEL_NAME="${MODEL_NAME:-claude-sonnet-4-6}"
|
||||
LITELLM_MASTER_KEY="${LITELLM_MASTER_KEY:-sk-1234}"
|
||||
PROXY_PORT="${PROXY_PORT:-4000}"
|
||||
PROXY_URL="http://127.0.0.1:${PROXY_PORT}"
|
||||
STARTUP_TIMEOUT_S="${STARTUP_TIMEOUT_S:-300}"
|
||||
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-litellm-claude-code-e2e}"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
CLAUDE_CODE_LITELLM_CONFIG="${TMP_DIR}/config.yaml"
|
||||
HEADERS_FILE="${TMP_DIR}/headers.txt"
|
||||
BODY_FILE="${TMP_DIR}/body.json"
|
||||
|
||||
cleanup() {
|
||||
if [[ "${KEEP_CONTAINERS:-}" == "1" ]]; then
|
||||
echo "KEEP_CONTAINERS=1 set; leaving docker compose project ${COMPOSE_PROJECT_NAME} running."
|
||||
return
|
||||
fi
|
||||
docker compose -p "${COMPOSE_PROJECT_NAME}" -f "${COMPOSE_FILE}" down -v >/dev/null 2>&1 || true
|
||||
rm -rf "${TMP_DIR}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ -f "${ENV_FILE}" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${ENV_FILE}"
|
||||
fi
|
||||
|
||||
: "${ANTHROPIC_API_KEY:?ANTHROPIC_API_KEY is required}"
|
||||
|
||||
# Create LiteLLM config
|
||||
cat >"${CLAUDE_CODE_LITELLM_CONFIG}" <<EOF
|
||||
model_list:
|
||||
- model_name: ${MODEL_NAME}
|
||||
litellm_params:
|
||||
model: anthropic/${MODEL_NAME}
|
||||
api_key: os.environ/ANTHROPIC_API_KEY
|
||||
|
||||
general_settings:
|
||||
forward_client_headers_to_llm_api: true
|
||||
master_key: "${LITELLM_MASTER_KEY}"
|
||||
|
||||
litellm_settings:
|
||||
drop_params: true
|
||||
modify_params: true
|
||||
EOF
|
||||
|
||||
export ANTHROPIC_API_KEY
|
||||
export CLAUDE_CODE_LITELLM_CONFIG
|
||||
export LITELLM_IMAGE="${LITELLM_IMAGE:-litellm-claude-code-e2e:local}"
|
||||
export CLAUDE_CODE_IMAGE="${CLAUDE_CODE_IMAGE:-litellm-claude-code-client:local}"
|
||||
export LITELLM_MASTER_KEY
|
||||
export MODEL_NAME
|
||||
export PROXY_PORT
|
||||
|
||||
echo "[0/5] Preparing Docker images..."
|
||||
if [[ "${LITELLM_SKIP_BUILD:-}" == "true" && "${CLAUDE_CODE_SKIP_BUILD:-}" == "true" ]]; then
|
||||
echo "Skipping image builds."
|
||||
else
|
||||
docker compose -p "${COMPOSE_PROJECT_NAME}" -f "${COMPOSE_FILE}" build litellm claude-code
|
||||
fi
|
||||
|
||||
echo "[1/5] Starting Postgres and LiteLLM proxy..."
|
||||
docker compose -p "${COMPOSE_PROJECT_NAME}" -f "${COMPOSE_FILE}" up -d postgres litellm
|
||||
|
||||
echo "[2/5] Waiting for LiteLLM readiness at ${PROXY_URL}..."
|
||||
READY=0
|
||||
for _ in $(seq 1 "${STARTUP_TIMEOUT_S}"); do
|
||||
if curl -fsS --connect-timeout 5 --max-time 15 \
|
||||
-H "Authorization: Bearer ${LITELLM_MASTER_KEY}" "${PROXY_URL}/v1/models" >/dev/null 2>&1; then
|
||||
READY=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ "${READY}" != "1" ]]; then
|
||||
echo "LiteLLM failed to become ready."
|
||||
docker compose -p "${COMPOSE_PROJECT_NAME}" -f "${COMPOSE_FILE}" logs --tail=200 litellm || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[3/5] Checking configured model..."
|
||||
curl -fsS --connect-timeout 5 --max-time 20 \
|
||||
-H "Authorization: Bearer ${LITELLM_MASTER_KEY}" "${PROXY_URL}/v1/models" | grep -q "\"${MODEL_NAME}\""
|
||||
|
||||
echo "[4/5] Running back-to-back Claude Code requests (V0 Verification)..."
|
||||
if ! docker compose -p "${COMPOSE_PROJECT_NAME}" -f "${COMPOSE_FILE}" run --rm -T claude-code; then
|
||||
echo "Claude Code integration test suite failed."
|
||||
docker compose -p "${COMPOSE_PROJECT_NAME}" -f "${COMPOSE_FILE}" logs --tail=200 litellm || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[5/5] Verifying LiteLLM request headers on Anthropic messages endpoint..."
|
||||
REQUEST_BODY="$(python3 - "${MODEL_NAME}" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
print(json.dumps({"model": sys.argv[1], "max_tokens": 16, "messages": [{"role": "user", "content": "Respond with the word ok."}]}))
|
||||
PY
|
||||
)"
|
||||
|
||||
curl -fsS --connect-timeout 5 --max-time 30 -D "${HEADERS_FILE}" -o "${BODY_FILE}" \
|
||||
-X POST "${PROXY_URL}/v1/messages" \
|
||||
-H "Authorization: Bearer ${LITELLM_MASTER_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
--data "${REQUEST_BODY}"
|
||||
|
||||
grep -qi "^x-litellm-call-id:" "${HEADERS_FILE}" || {
|
||||
echo "Missing x-litellm-call-id header."
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "Success: Claude Code (Anthropic only) verified with back-to-back requests and secure isolation."
|
||||
Loading…
Add table
Reference in a new issue