Skip to content

Commit 4571d1d

Browse files
committed
update for tests and minor fixes
1 parent c6362b7 commit 4571d1d

File tree

2 files changed

+95
-52
lines changed

2 files changed

+95
-52
lines changed

src/api/routers/test_vertex.py

Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from unittest.mock import patch, MagicMock
44
from fastapi import Request
55
from starlette.datastructures import Headers, QueryParams
6+
from fastapi import Response
67

78
import api.routers.vertex as vertex
89

@@ -33,38 +34,25 @@ def test_to_vertex_anthropic():
3334
assert isinstance(result["messages"], list)
3435
assert result["messages"][0]["role"] == "user"
3536
assert result["messages"][0]["content"][0]["text"] == "Hello!"
37+
assert result["messages"][1]["role"] == "assistant"
38+
assert result["messages"][1]["content"][0]["text"] == "Hi there!"
3639

37-
def test_from_vertex_anthropic_to_openai():
40+
def test_from_anthropic_to_openai_response():
3841
msg = json.dumps({
3942
"id": "abc123",
4043
"role": "assistant",
41-
"content": [{"type": "text", "text": "Hello!"}],
44+
"content": [{"type": "text", "text": "Hello!"}, {"type": "text", "text": "Bye!"}],
4245
"stop_reason": "stop",
4346
"usage": {"prompt_tokens": 5, "completion_tokens": 2}
4447
})
45-
result = json.loads(vertex.from_vertex_anthropic_to_openai(msg))
48+
result = json.loads(vertex.from_anthropic_to_openai_response(msg))
4649
assert result["id"] == "abc123"
4750
assert result["object"] == "chat.completion"
48-
assert result["choices"][0]["message"]["content"] == "Hello!"
51+
assert len(result["choices"]) == 1
52+
assert result["choices"][0]["message"]["content"] == "Hello!Bye!"
4953
assert result["choices"][0]["finish_reason"] == "stop"
5054
assert result["usage"]["prompt_tokens"] == 5
5155

52-
def test_to_openai_response():
53-
resp = {
54-
"candidates": [
55-
{
56-
"content": {"parts": [{"text": "Hello!"}]},
57-
"finishReason": "STOP"
58-
}
59-
]
60-
}
61-
result = vertex.to_openai_response(resp)
62-
assert result["object"] == "chat.completion"
63-
assert result["choices"][0]["message"]["content"] == "Hello!"
64-
assert result["choices"][0]["finish_reason"] == "stop"
65-
assert result["choices"][0]["index"] == 0
66-
assert result["id"].startswith("chatcmpl-")
67-
6856
def test_get_gcp_target_env(monkeypatch):
6957
monkeypatch.setenv("PROXY_TARGET", "https://custom-proxy")
7058
result = vertex.get_gcp_target("any-model", "/v1/chat/completions")
@@ -105,7 +93,7 @@ def test_get_header_removes_hop_headers(mock_token, dummy_request):
10593
assert "Connection" not in headers
10694
assert "Authorization" in headers
10795
assert headers["Authorization"] == "Bearer dummy-token"
108-
assert headers["X-Custom"] == "foo"
96+
assert headers["x-custom"] == "foo"
10997

11098
@pytest.mark.asyncio
11199
@patch("api.routers.vertex.httpx.AsyncClient")
@@ -126,3 +114,86 @@ async def test_handle_proxy_basic(mock_get_model, mock_get_header, mock_async_cl
126114
assert result.status_code == 200
127115
assert b"hi" in result.body
128116
assert result.headers["content-type"] == "application/json"
117+
118+
@pytest.mark.asyncio
119+
@patch("api.routers.vertex.httpx.AsyncClient")
120+
@patch("api.routers.vertex.get_header")
121+
@patch("api.routers.vertex.get_model", return_value="test-model")
122+
async def test_handle_proxy_known_chat_model(
123+
mock_get_model, mock_get_header, mock_async_client, dummy_request
124+
):
125+
req = dummy_request(body=json.dumps({"model": "foo"}).encode())
126+
mock_get_header.return_value = ("http://target", {"Authorization": "Bearer token"})
127+
mock_response = MagicMock()
128+
mock_response.content = b'{"candidates":[{"content":{"parts":[{"text":"hi"}]}, "finishReason":"STOP"}]}'
129+
mock_response.status_code = 200
130+
mock_response.headers = {"content-type": "application/json"}
131+
mock_async_client.return_value.__aenter__.return_value.request.return_value = mock_response
132+
133+
vertex.USE_MODEL_MAPPING = True
134+
if "test-model" not in vertex.known_chat_models:
135+
vertex.known_chat_models.append("test-model")
136+
137+
result = await vertex.handle_proxy(req, "/v1/chat/completions")
138+
assert isinstance(result, Response)
139+
assert result.status_code == 200
140+
assert b"hi" in result.body
141+
assert result.headers["content-type"] == "application/json"
142+
143+
@pytest.mark.asyncio
144+
@patch("api.routers.vertex.httpx.AsyncClient")
145+
@patch("api.routers.vertex.get_header")
146+
@patch("api.routers.vertex.get_model", return_value="anthropic-model")
147+
async def test_handle_proxy_anthropic_conversion(
148+
mock_get_model, mock_get_header, mock_async_client, dummy_request
149+
):
150+
req = dummy_request(body=json.dumps({"model": "foo", "messages": [{"role": "user", "content": "hi"}]}).encode())
151+
mock_get_header.return_value = ("http://target", {"Authorization": "Bearer token"})
152+
mock_response = MagicMock()
153+
# Simulate anthropic response
154+
anthropic_resp = json.dumps({
155+
"id": "abc123",
156+
"role": "assistant",
157+
"content": [{"type": "text", "text": "Hello!"}],
158+
"stop_reason": "stop",
159+
"usage": {"prompt_tokens": 5, "completion_tokens": 2}
160+
}).encode()
161+
mock_response.content = anthropic_resp
162+
mock_response.status_code = 200
163+
mock_response.headers = {"content-type": "application/json"}
164+
mock_async_client.return_value.__aenter__.return_value.request.return_value = mock_response
165+
166+
vertex.USE_MODEL_MAPPING = True
167+
# Ensure model is not in known_chat_models to trigger conversion
168+
if "anthropic-model" in vertex.known_chat_models:
169+
vertex.known_chat_models.remove("anthropic-model")
170+
result = await vertex.handle_proxy(req, "/v1/chat/completions")
171+
assert isinstance(result, Response)
172+
data = json.loads(result.body)
173+
assert data["object"] == "chat.completion"
174+
assert data["choices"][0]["message"]["content"] == "Hello!"
175+
176+
@pytest.mark.asyncio
177+
@patch("api.routers.vertex.httpx.AsyncClient", side_effect=Exception("network error"))
178+
@patch("api.routers.vertex.get_header")
179+
@patch("api.routers.vertex.get_model", return_value="test-model")
180+
async def test_handle_proxy_httpx_exception(
181+
mock_get_model, mock_get_header, mock_async_client, dummy_request
182+
):
183+
req = dummy_request(body=json.dumps({"model": "foo"}).encode())
184+
mock_get_header.return_value = ("http://target", {"Authorization": "Bearer token"})
185+
vertex.USE_MODEL_MAPPING = True
186+
if "test-model" not in vertex.known_chat_models:
187+
vertex.known_chat_models.append("test-model")
188+
# Patch httpx.RequestError to be raised
189+
with patch("api.routers.vertex.httpx.RequestError", Exception):
190+
result = await vertex.handle_proxy(req, "/v1/chat/completions")
191+
assert isinstance(result, Response)
192+
assert result.status_code == 502
193+
assert b"Upstream request failed" in result.body
194+
# Assert that the status code is 502 (Bad Gateway) due to upstream failure
195+
assert result.status_code == 502
196+
197+
# Assert that the response body contains the expected error message
198+
assert b"Upstream request failed" in result.body
199+

src/api/routers/vertex.py

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def get_header(model, request, path):
9797
# Fetch service account token
9898
access_token = get_access_token()
9999
headers["Authorization"] = f"Bearer {access_token}"
100-
return target_url,headers
100+
return target_url, headers
101101

102102
def to_vertex_anthropic(openai_messages):
103103
message = [
@@ -114,7 +114,7 @@ def to_vertex_anthropic(openai_messages):
114114
"messages": message
115115
}
116116

117-
def from_vertex_anthropic_to_openai(msg):
117+
def from_anthropic_to_openai_response(msg):
118118
msg_json = json.loads(msg)
119119
return json.dumps({
120120
"id": msg_json["id"],
@@ -136,35 +136,7 @@ def from_vertex_anthropic_to_openai(msg):
136136
"usage": msg_json.get("usage", {})
137137
})
138138

139-
def to_openai_response(resp):
140-
"""
141-
Convert an Vertex AI response to an OpenAI-style chat completion format.
142-
"""
143-
if "candidates" not in resp or not resp["candidates"]:
144-
raise ValueError("No candidates in response")
145-
146-
choices = []
147-
for i, candidate in enumerate(resp["candidates"]):
148-
content_parts = candidate.get("content", {}).get("parts", [])
149-
text = "".join(part.get("text", "") for part in content_parts)
150-
151-
choices.append({
152-
"index": i,
153-
"message": {
154-
"role": "assistant",
155-
"content": text
156-
},
157-
"finish_reason": candidate.get("finishReason", "stop").lower()
158-
})
159-
160-
return {
161-
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
162-
"object": "chat.completion",
163-
"choices": choices
164-
}
165-
166139
async def handle_proxy(request: Request, path: str):
167-
168140
try:
169141
content = await request.body()
170142
content_json = json.loads(content)
@@ -203,7 +175,7 @@ async def handle_proxy(request: Request, path: str):
203175
if needs_conversion:
204176
# convert vertex response to openai format
205177
if "anthropic" in model:
206-
content = from_vertex_anthropic_to_openai(response.content)
178+
content = from_anthropic_to_openai_response(response.content)
207179

208180
except httpx.RequestError as e:
209181
logging.error(f"Proxy request failed: {e}")

0 commit comments

Comments
 (0)