Skip to content

Commit 3951cc9

Browse files
pescnclaude
andcommitted
test: add comprehensive Python integration test suite
Add Python test code for testing all API formats: - OpenAI Chat API, Anthropic Messages API, OpenAI Responses API - Streaming and non-streaming modes - Function calling / tool use - Vision Language Model (VLM) support - Rate limiting tests - Request deduplication (ReqId) tests - Client abort handling tests The unified test suite (test_unified_suite.py) contains 52 tests covering API formats, streaming, function calling, VLM, and error handling. Run with: cd python_test_code && uv run test_unified_suite.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 01a9c33 commit 3951cc9

20 files changed

Lines changed: 7583 additions & 2 deletions

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,7 @@ backend/docs/
2424

2525
.turbo
2626

27-
# Python test code (local testing only)
28-
python_test_code/
27+
# Python test code - local artifacts
28+
python_test_code/.venv/
29+
python_test_code/.ruff_cache/
30+
python_test_code/test_report_*.txt

python_test_code/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

python_test_code/README.md

Whitespace-only changes.

python_test_code/main.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import anthropic
2+
import os
3+
4+
client = anthropic.Anthropic(
5+
api_key="sk-d493c33eabaa3b71486d5de4194fa4ab",
6+
base_url="http://localhost:3000",
7+
)
8+
message = client.messages.create(
9+
model="deepseek-v3-2",
10+
max_tokens=1024,
11+
# 流式输出
12+
stream=True,
13+
messages=[
14+
{
15+
"role": "user",
16+
"content": [
17+
{
18+
"type": "text",
19+
"text": "你是谁?"
20+
}
21+
]
22+
}
23+
]
24+
)
25+
print("=== 思考过程 ===")
26+
first_text = True
27+
for chunk in message:
28+
if chunk.type == "content_block_delta":
29+
if hasattr(chunk.delta, 'thinking'):
30+
print(chunk.delta.thinking, end="", flush=True)
31+
elif hasattr(chunk.delta, 'text'):
32+
if first_text:
33+
print("\n\n=== 回答 ===")
34+
first_text = False
35+
print(chunk.delta.text, end="", flush=True)
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Background monitor for multi-key rate limit test.
4+
Polls API key usage 5 times per second and logs to file.
5+
"""
6+
import asyncio
7+
import json
8+
import time
9+
import sys
10+
from dataclasses import dataclass, asdict
11+
from datetime import datetime
12+
13+
import httpx
14+
15+
BASE_URL = "http://localhost:3000/api"
16+
ADMIN_SECRET = "admin"
17+
OUTPUT_FILE = "/tmp/rate_limit_monitor.jsonl"
18+
POLL_INTERVAL = 0.2 # 5 times per second
19+
20+
21+
@dataclass
22+
class UsageSnapshot:
23+
timestamp: float
24+
datetime_str: str
25+
api_key_id: int
26+
api_key_short: str
27+
rpm_current: int
28+
rpm_limit: int
29+
rpm_remaining: int
30+
tpm_current: int
31+
tpm_limit: int
32+
tpm_remaining: int
33+
34+
35+
async def get_all_api_keys(client: httpx.AsyncClient) -> list[dict]:
36+
"""Get all API keys"""
37+
try:
38+
resp = await client.get(
39+
f"{BASE_URL}/admin/apiKey",
40+
headers={"Authorization": f"Bearer {ADMIN_SECRET}"},
41+
timeout=5.0
42+
)
43+
if resp.status_code == 200:
44+
return resp.json()
45+
except Exception as e:
46+
print(f"[Monitor] Error getting API keys: {e}", file=sys.stderr)
47+
return []
48+
49+
50+
async def get_usage(client: httpx.AsyncClient, key_info: dict) -> UsageSnapshot | None:
51+
"""Get usage for a single API key"""
52+
try:
53+
resp = await client.get(
54+
f"{BASE_URL}/admin/apiKey/{key_info['key']}/usage",
55+
headers={"Authorization": f"Bearer {ADMIN_SECRET}"},
56+
timeout=5.0
57+
)
58+
if resp.status_code == 200:
59+
data = resp.json()
60+
now = time.time()
61+
return UsageSnapshot(
62+
timestamp=now,
63+
datetime_str=datetime.fromtimestamp(now).strftime("%H:%M:%S.%f")[:-3],
64+
api_key_id=key_info["id"],
65+
api_key_short=key_info["key"][:15] + "...",
66+
rpm_current=data.get("usage", {}).get("rpm", {}).get("current", 0),
67+
rpm_limit=data.get("limits", {}).get("rpm", 0),
68+
rpm_remaining=data.get("usage", {}).get("rpm", {}).get("remaining", 0),
69+
tpm_current=data.get("usage", {}).get("tpm", {}).get("current", 0),
70+
tpm_limit=data.get("limits", {}).get("tpm", 0),
71+
tpm_remaining=data.get("usage", {}).get("tpm", {}).get("remaining", 0),
72+
)
73+
except Exception as e:
74+
pass # Silent fail for monitoring
75+
return None
76+
77+
78+
async def monitor_loop(stop_file: str = "/tmp/stop_monitor"):
79+
"""Main monitoring loop"""
80+
print(f"[Monitor] Starting rate limit monitor")
81+
print(f"[Monitor] Output file: {OUTPUT_FILE}")
82+
print(f"[Monitor] Poll interval: {POLL_INTERVAL}s (5x per second)")
83+
print(f"[Monitor] Stop by creating file: {stop_file}")
84+
print()
85+
86+
# Clear output file
87+
with open(OUTPUT_FILE, "w") as f:
88+
f.write("")
89+
90+
snapshot_count = 0
91+
last_keys_check = 0
92+
api_keys: list[dict] = []
93+
94+
async with httpx.AsyncClient() as client:
95+
while True:
96+
# Check stop condition
97+
import os
98+
if os.path.exists(stop_file):
99+
print(f"\n[Monitor] Stop file detected, exiting...")
100+
try:
101+
os.remove(stop_file)
102+
except:
103+
pass
104+
break
105+
106+
# Refresh API keys list every 2 seconds
107+
now = time.time()
108+
if now - last_keys_check > 2:
109+
api_keys = await get_all_api_keys(client)
110+
last_keys_check = now
111+
if api_keys:
112+
# Filter to recent test keys (created in last 10 minutes)
113+
# This is a simple heuristic based on comment pattern
114+
test_keys = [k for k in api_keys if k.get("comment", "").startswith("test-multikey-")]
115+
if test_keys:
116+
api_keys = test_keys
117+
print(f"[Monitor] Tracking {len(api_keys)} test API keys: {[k['id'] for k in api_keys]}")
118+
119+
if not api_keys:
120+
await asyncio.sleep(POLL_INTERVAL)
121+
continue
122+
123+
# Get usage for all keys concurrently
124+
tasks = [get_usage(client, k) for k in api_keys]
125+
snapshots = await asyncio.gather(*tasks, return_exceptions=True)
126+
127+
# Write snapshots to file
128+
with open(OUTPUT_FILE, "a") as f:
129+
for snapshot in snapshots:
130+
if isinstance(snapshot, UsageSnapshot):
131+
f.write(json.dumps(asdict(snapshot)) + "\n")
132+
snapshot_count += 1
133+
134+
# Print summary every 10 snapshots
135+
if snapshot_count % 10 == 0:
136+
print(f"[Monitor] {snapshot.datetime_str} Key-{snapshot.api_key_id}: "
137+
f"RPM {snapshot.rpm_current}/{snapshot.rpm_limit} "
138+
f"(rem={snapshot.rpm_remaining}), "
139+
f"TPM {snapshot.tpm_current}/{snapshot.tpm_limit}")
140+
141+
await asyncio.sleep(POLL_INTERVAL)
142+
143+
print(f"\n[Monitor] Total snapshots collected: {snapshot_count}")
144+
print(f"[Monitor] Output saved to: {OUTPUT_FILE}")
145+
146+
147+
async def main():
148+
# Remove stop file if exists
149+
import os
150+
stop_file = "/tmp/stop_monitor"
151+
if os.path.exists(stop_file):
152+
os.remove(stop_file)
153+
154+
try:
155+
await monitor_loop(stop_file)
156+
except KeyboardInterrupt:
157+
print("\n[Monitor] Interrupted by user")
158+
except Exception as e:
159+
print(f"\n[Monitor] Error: {e}")
160+
161+
162+
if __name__ == "__main__":
163+
asyncio.run(main())
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""
2+
Rate Limit Monitor
3+
Polls the rate limit usage API every 200ms and displays real-time status
4+
"""
5+
import time
6+
import httpx
7+
8+
# Configuration
9+
BASE_URL = "http://localhost:3000"
10+
ADMIN_SECRET = "admin"
11+
API_KEY = "sk-d493c33eabaa3b71486d5de4194fa4ab"
12+
POLL_INTERVAL = 0.2 # 200ms
13+
14+
15+
def get_usage():
16+
"""Fetch current rate limit usage"""
17+
try:
18+
response = httpx.get(
19+
f"{BASE_URL}/api/admin/apiKey/{API_KEY}/usage",
20+
headers={"Authorization": f"Bearer {ADMIN_SECRET}"},
21+
timeout=5.0
22+
)
23+
if response.status_code == 200:
24+
return response.json()
25+
return None
26+
except Exception as e:
27+
return None
28+
29+
30+
def format_bar(current: int, limit: int, width: int = 30) -> str:
31+
"""Create a visual progress bar"""
32+
ratio = min(1.0, current / limit) if limit > 0 else 0
33+
filled = int(width * ratio)
34+
bar = "█" * filled + "░" * (width - filled)
35+
36+
if current > limit:
37+
return f"\033[93m{bar}\033[0m" # Yellow for over limit
38+
elif current > limit * 0.8:
39+
return f"\033[91m{bar}\033[0m" # Red for near limit
40+
else:
41+
return f"\033[92m{bar}\033[0m" # Green for normal
42+
43+
44+
def monitor():
45+
"""Main monitoring loop"""
46+
print("=" * 70)
47+
print("Rate Limit Monitor - Press Ctrl+C to stop")
48+
print("=" * 70)
49+
print(f"Monitoring API Key: {API_KEY[:20]}...")
50+
print(f"Poll interval: {POLL_INTERVAL * 1000:.0f}ms")
51+
print()
52+
53+
start_time = time.time()
54+
max_rpm = 0
55+
max_tpm = 0
56+
57+
try:
58+
while True:
59+
usage = get_usage()
60+
elapsed = time.time() - start_time
61+
62+
if usage:
63+
rpm_current = usage["usage"]["rpm"]["current"]
64+
rpm_limit = usage["limits"]["rpm"]
65+
tpm_current = usage["usage"]["tpm"]["current"]
66+
tpm_limit = usage["limits"]["tpm"]
67+
68+
max_rpm = max(max_rpm, rpm_current)
69+
max_tpm = max(max_tpm, tpm_current)
70+
71+
# Clear line and print status
72+
rpm_bar = format_bar(rpm_current, rpm_limit)
73+
tpm_bar = format_bar(tpm_current, tpm_limit)
74+
75+
print(f"\r[{elapsed:6.1f}s] RPM: {rpm_bar} {rpm_current:4d}/{rpm_limit:4d} | "
76+
f"TPM: {tpm_bar} {tpm_current:6d}/{tpm_limit:6d}", end="", flush=True)
77+
else:
78+
print(f"\r[{elapsed:6.1f}s] Waiting for API...", end="", flush=True)
79+
80+
time.sleep(POLL_INTERVAL)
81+
82+
except KeyboardInterrupt:
83+
print("\n")
84+
print("=" * 70)
85+
print("Monitor Summary")
86+
print("=" * 70)
87+
print(f"Duration: {time.time() - start_time:.1f}s")
88+
print(f"Max RPM: {max_rpm}")
89+
print(f"Max TPM: {max_tpm}")
90+
print("=" * 70)
91+
92+
93+
if __name__ == "__main__":
94+
monitor()

python_test_code/pyproject.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[project]
2+
name = "python-test-code"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
requires-python = ">=3.12"
7+
dependencies = [
8+
"anthropic>=0.75.0",
9+
"openai>=2.15.0",
10+
"httpx>=0.27.0",
11+
]

0 commit comments

Comments
 (0)