Skip to content

Commit db83feb

Browse files
Feature/fix 405 error (#66)
* bugfix: try to fix 405 error from user, safe read request, path normalizer with no redirect * feature: add version on initialize, bump version * chore: change version to 0.1.2.post1 * feature: add openAPI documentation * feature: add count tokens anthropic path * refactor(path_normalizer): add responses and messages * Update coverage badge for feature/fix_405_error * chore: add count_tokens example for anthropic --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 472a94c commit db83feb

File tree

13 files changed

+941
-173
lines changed

13 files changed

+941
-173
lines changed

badges/coverage.svg

Lines changed: 2 additions & 2 deletions
Loading

examples/anthropic/count_tokens.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Anthropic Messages API token counting example.
2+
3+
Count input tokens before sending a request, useful for
4+
estimating costs and staying within context limits.
5+
"""
6+
7+
from anthropic import Anthropic
8+
9+
client = Anthropic(base_url="http://localhost:8090/v1", api_key="any-key")
10+
11+
# 1. Simple message token count
12+
result = client.messages.count_tokens(
13+
model="GigaChat-2-Max",
14+
messages=[
15+
{"role": "user", "content": "Расскажи коротко о Python."},
16+
],
17+
)
18+
19+
print(f"Простое сообщение: {result.input_tokens} токенов")
20+
21+
# 2. Token count with system prompt
22+
result_with_system = client.messages.count_tokens(
23+
model="GigaChat-2-Max",
24+
system="Ты — опытный программист. Отвечай кратко и по делу.",
25+
messages=[
26+
{"role": "user", "content": "Что такое декораторы в Python?"},
27+
],
28+
)
29+
30+
print(f"С системным промптом: {result_with_system.input_tokens} токенов")
31+
32+
# 3. Token count with tools
33+
tools = [
34+
{
35+
"name": "get_weather",
36+
"description": "Получить текущую погоду для указанного города.",
37+
"input_schema": {
38+
"type": "object",
39+
"properties": {
40+
"city": {
41+
"type": "string",
42+
"description": "Название города, например: Москва",
43+
},
44+
},
45+
"required": ["city"],
46+
},
47+
},
48+
]
49+
50+
result_with_tools = client.messages.count_tokens(
51+
model="GigaChat-2-Max",
52+
messages=[
53+
{"role": "user", "content": "Какая погода в Москве?"},
54+
],
55+
tools=tools,
56+
)
57+
58+
print(f"С определениями инструментов: {result_with_tools.input_tokens} токенов")
59+
60+
# 4. Multi-turn conversation token count
61+
result_multi = client.messages.count_tokens(
62+
model="GigaChat-2-Max",
63+
messages=[
64+
{"role": "user", "content": "Привет!"},
65+
{"role": "assistant", "content": "Здравствуйте! Чем могу помочь?"},
66+
{"role": "user", "content": "Расскажи о машинном обучении."},
67+
],
68+
)
69+
70+
print(result_multi)
71+
print(f"Многоходовый диалог: {result_multi.input_tokens} токенов")

gpt2giga/api_server.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from gpt2giga.protocol import AttachmentProcessor, RequestTransformer, ResponseProcessor
1616
from gpt2giga.routers import anthropic_router, api_router, logs_router
1717
from gpt2giga.routers import system_router
18+
from gpt2giga.utils import _get_app_version
1819

1920

2021
@asynccontextmanager
@@ -54,7 +55,11 @@ async def lifespan(app: FastAPI):
5455

5556

5657
def create_app(config=None) -> FastAPI:
57-
app = FastAPI(lifespan=lifespan, title="Gpt2Giga converter proxy")
58+
app = FastAPI(
59+
lifespan=lifespan,
60+
title="Gpt2Giga converter proxy",
61+
version=_get_app_version(),
62+
)
5863
if config is None:
5964
config = load_config()
6065

@@ -108,7 +113,7 @@ def run():
108113
app = create_app(config)
109114
app.state.logger = logger
110115

111-
logger.info("Starting Gpt2Giga proxy server...")
116+
logger.info(f"Starting Gpt2Giga proxy server, version: {_get_app_version()}")
112117
logger.info(f"Proxy settings: {proxy_settings}")
113118
logger.info(
114119
f"GigaChat settings: {config.gigachat_settings.model_dump(exclude={'password', 'credentials', 'access_token'})}"

gpt2giga/middlewares/path_normalizer.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
from fastapi import Request
55
from starlette.middleware.base import BaseHTTPMiddleware
6-
from starlette.responses import RedirectResponse
76

87

98
class PathNormalizationMiddleware(BaseHTTPMiddleware):
@@ -15,7 +14,14 @@ class PathNormalizationMiddleware(BaseHTTPMiddleware):
1514
def __init__(self, app, valid_roots=None):
1615
super().__init__(app)
1716
# Valid entrypoints
18-
self.valid_roots = valid_roots or ["v1", "chat", "models", "embeddings"]
17+
self.valid_roots = valid_roots or [
18+
"v1",
19+
"chat",
20+
"models",
21+
"embeddings",
22+
"messages",
23+
"responses",
24+
]
1925
pattern = r".*/(" + "|".join(map(re.escape, self.valid_roots)) + r")(/.*|$)"
2026
self._pattern = re.compile(pattern)
2127

@@ -26,9 +32,11 @@ async def dispatch(self, request: Request, call_next: Callable):
2632

2733
if match and not path.startswith(f"/{match.group(1)}"):
2834
new_path = f"/{match.group(1)}{match.group(2)}"
29-
query = request.url.query
30-
if query:
31-
new_path += f"?{query}"
32-
return RedirectResponse(url=new_path)
35+
# IMPORTANT:
36+
# Do not redirect (307) here: some clients may re-issue the request
37+
# without the original body, which leads to JSONDecodeError in
38+
# downstream handlers. Instead, rewrite the ASGI scope path in-place.
39+
request.scope["path"] = new_path
40+
request.scope["raw_path"] = new_path.encode("utf-8")
3341

3442
return await call_next(request)

0 commit comments

Comments
 (0)