Auto-stream long generations (max_tokens>8192) and aggregate to the non-stream response shape: gateways hang on large buffered NON-streaming requests (aime25's 32k budget stalled forever); streaming starts emitting immediately so a stuck endpoint surfaces in ~60s instead of the full adaptive read timeout; lazy httpx import fix
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5af8e2a7fd
commit
7473170784
@ -202,6 +202,12 @@ class OpenAICompatible(ModelAdapter):
|
|||||||
if stream:
|
if stream:
|
||||||
out = await self._post_stream_perf(
|
out = await self._post_stream_perf(
|
||||||
f'{self.api_base}/chat/completions', payload, headers, t0)
|
f'{self.api_base}/chat/completions', payload, headers, t0)
|
||||||
|
elif int(payload.get('max_tokens') or 0) > 8192:
|
||||||
|
# long generation: stream and aggregate (gateway-safe)
|
||||||
|
payload['stream'] = True
|
||||||
|
data = await self._post_stream_aggregate(
|
||||||
|
f'{self.api_base}/chat/completions', payload, headers)
|
||||||
|
out = self._parse(data)
|
||||||
else:
|
else:
|
||||||
data = await self._post(f'{self.api_base}/chat/completions', payload, headers)
|
data = await self._post(f'{self.api_base}/chat/completions', payload, headers)
|
||||||
out = self._parse(data)
|
out = self._parse(data)
|
||||||
@ -262,7 +268,9 @@ class OpenAICompatible(ModelAdapter):
|
|||||||
status = None
|
status = None
|
||||||
import json as _json
|
import json as _json
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=httpx.Timeout(
|
import httpx as _hx
|
||||||
|
|
||||||
|
async with _hx.AsyncClient(timeout=_hx.Timeout(
|
||||||
connect=self.extra.get('connect_timeout', 15),
|
connect=self.extra.get('connect_timeout', 15),
|
||||||
read=self.extra.get('timeout', 300), write=30, pool=15)) as client:
|
read=self.extra.get('timeout', 300), write=30, pool=15)) as client:
|
||||||
async with client.stream('POST', url, json=payload, headers=headers) as resp:
|
async with client.stream('POST', url, json=payload, headers=headers) as resp:
|
||||||
@ -387,6 +395,68 @@ class OpenAICompatible(ModelAdapter):
|
|||||||
return ModelOutput(text=text, tool_calls=calls, usage=usage,
|
return ModelOutput(text=text, tool_calls=calls, usage=usage,
|
||||||
raw=data, model=data.get('model', self.model))
|
raw=data, model=data.get('model', self.model))
|
||||||
|
|
||||||
|
async def _post_stream_aggregate(self, url: str, payload: Dict,
|
||||||
|
headers: Dict) -> Dict[str, Any]:
|
||||||
|
"""STREAM a long generation and reassemble the non-stream response
|
||||||
|
shape. Some gateways hang on large NON-streaming requests (the full
|
||||||
|
32k-token body must be buffered before any byte is sent); streaming
|
||||||
|
starts emitting immediately, so a stuck endpoint surfaces in ~60s
|
||||||
|
instead of after the whole (possibly 80-minute) read timeout."""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
import httpx as _hx
|
||||||
|
|
||||||
|
async with _hx.AsyncClient(timeout=_hx.Timeout(
|
||||||
|
connect=self.extra.get('connect_timeout', 15),
|
||||||
|
read=60, write=30, pool=15)) as client:
|
||||||
|
async with client.stream('POST', url, json=payload, headers=headers) as resp:
|
||||||
|
if resp.status_code != 200:
|
||||||
|
body = (await resp.aread()).decode('utf-8', 'replace')[:300]
|
||||||
|
raise RuntimeError(f'HTTP {resp.status_code}: {body}')
|
||||||
|
content = []
|
||||||
|
reasoning = []
|
||||||
|
tool_calls = {}
|
||||||
|
usage = {}
|
||||||
|
finish = None
|
||||||
|
async for line in resp.aiter_lines():
|
||||||
|
if not line.startswith('data:'):
|
||||||
|
continue
|
||||||
|
chunk = line[5:].strip()
|
||||||
|
if chunk in ('', '[DONE]'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
ev = _json.loads(chunk)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
u = ev.get('usage')
|
||||||
|
if u:
|
||||||
|
usage = u
|
||||||
|
for ch in ev.get('choices') or []:
|
||||||
|
delta = ch.get('delta') or {}
|
||||||
|
if delta.get('content'):
|
||||||
|
content.append(delta['content'])
|
||||||
|
if delta.get('reasoning_content'):
|
||||||
|
reasoning.append(delta['reasoning_content'])
|
||||||
|
for tc in delta.get('tool_calls') or []:
|
||||||
|
i = tc.get('index', 0)
|
||||||
|
slot = tool_calls.setdefault(i, {'id': '', 'type': 'function',
|
||||||
|
'function': {'name': '', 'arguments': ''}})
|
||||||
|
fn = tc.get('function') or {}
|
||||||
|
slot['id'] = tc.get('id') or slot['id']
|
||||||
|
slot['function']['name'] += fn.get('name') or ''
|
||||||
|
slot['function']['arguments'] += fn.get('arguments') or ''
|
||||||
|
if ch.get('finish_reason'):
|
||||||
|
finish = ch['finish_reason']
|
||||||
|
msg = {'role': 'assistant', 'content': ''.join(content)}
|
||||||
|
if reasoning:
|
||||||
|
msg['reasoning_content'] = ''.join(reasoning)
|
||||||
|
if tool_calls:
|
||||||
|
msg['tool_calls'] = [tool_calls[i] for i in sorted(tool_calls)]
|
||||||
|
return {'choices': [{'index': 0, 'message': msg,
|
||||||
|
'finish_reason': finish or 'stop'}],
|
||||||
|
'usage': usage or {},
|
||||||
|
'model': payload.get('model', '')}
|
||||||
|
|
||||||
async def _post(self, url: str, payload: Dict, headers: Dict) -> Dict[str, Any]:
|
async def _post(self, url: str, payload: Dict, headers: Dict) -> Dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
import httpx
|
import httpx
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user