Skip to content

Commit a42bd71

Browse files
committed
update proxy
1 parent 9bee81d commit a42bd71

File tree

8 files changed

+213
-100
lines changed

8 files changed

+213
-100
lines changed

docs/source_en/Usage Guide/Server and Client/Tinker-Compatible-Client.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ for item in service_client.get_server_capabilities().supported_models:
2828
When calling `init_tinker_client`, the following operations are automatically executed:
2929

3030
1. **Patch Tinker SDK**: Bypass Tinker's `tinker://` prefix validation, allowing it to connect to standard HTTP addresses
31-
2. **Set Request Headers**: Inject necessary authentication headers such as `serve_multiplexed_model_id` and `Authorization`
31+
2. **Set Request Headers**: Inject necessary authentication headers such as `X-Ray-Serve-Request-Id` and `Authorization`
3232

3333
After initialization, simply import `from tinker import ServiceClient` to connect to Twinkle Server, and **all existing Tinker training code can be used directly** without any modifications.
3434

docs/source_zh/使用指引/服务端和客户端/Tinker兼容客户端.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ for item in service_client.get_server_capabilities().supported_models:
2828
调用 `init_tinker_client` 时,会自动执行以下操作:
2929

3030
1. **Patch Tinker SDK**:绕过 Tinker 的 `tinker://` 前缀校验,使其可以连接到标准 HTTP 地址
31-
2. **设置请求头**:注入 `serve_multiplexed_model_id``Authorization` 等必要的认证头
31+
2. **设置请求头**:注入 `X-Ray-Serve-Request-Id``Authorization` 等必要的认证头
3232

3333
初始化之后,直接导入 `from tinker import ServiceClient` 即可连接到 Twinkle Server,**所有已有的 Tinker 训练代码都可以直接使用**,无需任何修改。
3434

src/twinkle/server/launcher.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ def _deploy_application(self, app_config: dict[str, Any]) -> None:
214214
# Copy all deployment options from the config, except 'name'.
215215
deploy_options = {k: v for k, v in deploy_config.items() if k != 'name'}
216216

217+
# Pass http_options to server apps for internal proxy routing
218+
http_options = self.config.get('http_options', {})
219+
if http_options:
220+
args['http_options'] = http_options
221+
217222
# Build and deploy the application
218223
app = builder(deploy_options=deploy_options, **{k: v for k, v in args.items()})
219224

src/twinkle/server/tinker/proxy.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Copyright (c) ModelScope Contributors. All rights reserved.
2+
"""
3+
Proxy utilities for forwarding requests to internal services.
4+
5+
This module provides HTTP proxy functionality to route requests from the Tinker server
6+
to appropriate model or sampler services based on base_model routing.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import httpx
12+
import os
13+
from fastapi import Request, Response
14+
from typing import Any
15+
16+
from twinkle.utils.logger import get_logger
17+
18+
logger = get_logger()
19+
20+
21+
class ServiceProxy:
22+
"""HTTP proxy for routing requests to internal model and sampler services.
23+
24+
This proxy handles:
25+
1. URL construction using localhost to avoid external routing loops
26+
2. Header forwarding with appropriate cleanup
27+
3. Debug logging for troubleshooting
28+
4. Error handling and response forwarding
29+
"""
30+
31+
def __init__(
32+
self,
33+
http_options: dict[str, Any] | None = None,
34+
route_prefix: str = '/api/v1',
35+
):
36+
"""Initialize the service proxy.
37+
38+
Args:
39+
http_options: HTTP server options (host, port) for internal routing
40+
route_prefix: URL prefix for routing (default: '/api/v1')
41+
"""
42+
self.http_options = http_options or {}
43+
self.route_prefix = route_prefix
44+
# Disable proxy for internal requests to avoid routing through external proxies
45+
self.client = httpx.AsyncClient(timeout=None, trust_env=False)
46+
47+
def _build_target_url(self, service_type: str, base_model: str, endpoint: str) -> str:
48+
"""Build the target URL for internal service routing.
49+
50+
Constructs URLs using localhost to avoid extra external hops.
51+
When requests come from www.modelscope.com/twinkle, we proxy to
52+
localhost:port directly instead of back to modelscope.com.
53+
54+
Args:
55+
service_type: Either 'model' or 'sampler'
56+
base_model: The base model name for routing
57+
endpoint: The target endpoint name
58+
59+
Returns:
60+
Complete target URL for the internal service
61+
"""
62+
prefix = self.route_prefix.rstrip('/') if self.route_prefix else ''
63+
host = self.http_options.get('host', 'localhost')
64+
port = self.http_options.get('port', 8000)
65+
66+
# Use localhost for internal routing
67+
if host == '0.0.0.0':
68+
host = 'localhost'
69+
70+
base_url = f'http://{host}:{port}'
71+
return f'{base_url}{prefix}/{service_type}/{base_model}/{endpoint}'
72+
73+
def _prepare_headers(self, request_headers: dict[str, str]) -> dict[str, str]:
74+
"""Prepare headers for proxying by removing problematic headers.
75+
76+
Args:
77+
request_headers: Original request headers
78+
79+
Returns:
80+
Cleaned headers safe for proxying
81+
"""
82+
headers = dict(request_headers)
83+
# Remove headers that should not be forwarded
84+
headers.pop('host', None)
85+
headers.pop('content-length', None)
86+
# Add serve_multiplexed_model_id for sticky sessions
87+
headers['serve_multiplexed_model_id'] = request_headers.get('X-Ray-Serve-Request-Id')
88+
return headers
89+
90+
async def proxy_request(
91+
self,
92+
request: Request,
93+
endpoint: str,
94+
base_model: str,
95+
service_type: str,
96+
) -> Response:
97+
"""Generic proxy method to forward requests to model or sampler services.
98+
99+
This method consolidates the common proxy logic for both model and sampler endpoints.
100+
101+
Args:
102+
request: The incoming FastAPI request
103+
endpoint: The target endpoint name (e.g., 'create_model', 'asample')
104+
base_model: The base model name for routing
105+
service_type: Either 'model' or 'sampler' to determine the target service
106+
107+
Returns:
108+
Proxied response from the target service
109+
"""
110+
body_bytes = await request.body()
111+
target_url = self._build_target_url(service_type, base_model, endpoint)
112+
headers = self._prepare_headers(dict(request.headers))
113+
114+
try:
115+
# Debug logging for troubleshooting proxy issues
116+
if os.environ.get('TWINKLE_DEBUG_PROXY', '0') == '1':
117+
logger.info(
118+
'proxy_request service=%s endpoint=%s target_url=%s request_id=%s',
119+
service_type,
120+
endpoint,
121+
target_url,
122+
headers.get('x-ray-serve-request-id'),
123+
)
124+
125+
# Forward the request to the target service
126+
response = await self.client.request(
127+
method=request.method,
128+
url=target_url,
129+
content=body_bytes,
130+
headers=headers,
131+
params=request.query_params,
132+
)
133+
134+
# Debug logging for response
135+
if os.environ.get('TWINKLE_DEBUG_PROXY', '0') == '1':
136+
logger.info(
137+
'proxy_response status=%s body_preview=%s',
138+
response.status_code,
139+
response.text[:200],
140+
)
141+
142+
return Response(
143+
content=response.content,
144+
status_code=response.status_code,
145+
headers=dict(response.headers),
146+
media_type=response.headers.get('content-type'),
147+
)
148+
except Exception as e:
149+
logger.error('Proxy error: %s', str(e), exc_info=True)
150+
return Response(content=f'Proxy Error: {str(e)}', status_code=502)
151+
152+
async def proxy_to_model(self, request: Request, endpoint: str, base_model: str) -> Response:
153+
"""Proxy request to model endpoint.
154+
155+
Routes the request to the appropriate model deployment based on base_model.
156+
157+
Args:
158+
request: The incoming FastAPI request
159+
endpoint: The target endpoint name (e.g., 'create_model', 'forward')
160+
base_model: The base model name for routing
161+
162+
Returns:
163+
Proxied response from the model service
164+
"""
165+
return await self.proxy_request(request, endpoint, base_model, 'model')
166+
167+
async def proxy_to_sampler(self, request: Request, endpoint: str, base_model: str) -> Response:
168+
"""Proxy request to sampler endpoint.
169+
170+
Routes the request to the appropriate sampler deployment based on base_model.
171+
172+
Args:
173+
request: The incoming FastAPI request
174+
endpoint: The target endpoint name (e.g., 'asample')
175+
base_model: The base model name for routing
176+
177+
Returns:
178+
Proxied response from the sampler service
179+
"""
180+
return await self.proxy_request(request, endpoint, base_model, 'sampler')

0 commit comments

Comments
 (0)