补充 OpenAI 主备验收与可复用双协议测试
This commit is contained in:
@@ -41,6 +41,21 @@ GPT路线对应备用模型,不开放图像、语音等无关能力。Claude
|
|||||||
依据:[OpenAI赠金说明](https://developers.openai.com/api/docs/guides/chatgpt-api-credits)、
|
依据:[OpenAI赠金说明](https://developers.openai.com/api/docs/guides/chatgpt-api-credits)、
|
||||||
[LiteLLM回退](https://docs.litellm.ai/docs/proxy/reliability)。
|
[LiteLLM回退](https://docs.litellm.ai/docs/proxy/reliability)。
|
||||||
|
|
||||||
|
本轮验收:18项配置测试通过;官方Luna/Sol/Terra的Responses、Luna Chat、
|
||||||
|
Codex Luna Responses完整SSE通过(正常请求fallbacks=0);embedding small、
|
||||||
|
TTS与合成语音的gpt-transcribe/mini-transcribe通过。图像未实际生成,
|
||||||
|
diarize与embedding large只配置且模型目录可见,不能据此声称完成任务验收。
|
||||||
|
`verify_openai_fallback.py`在独立容器exec进程中对Chat与Responses各注入
|
||||||
|
429/401/503,六例均收到真实官方API的短回复;不是人为耗尽订阅,也不修改
|
||||||
|
生产worker的函数或真实OAuth token。复测命令(会有少量API用量):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh tools@192.168.50.49 'sudo -n docker exec -i tools-litellm python -' < verify_openai_fallback.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Arcane Sync及Up终态success;PostgreSQL启动时间仍是
|
||||||
|
`2026-10-12T05:46:12.472887407Z`,数据库及OAuth卷未重建。
|
||||||
|
|
||||||
## 部署状态
|
## 部署状态
|
||||||
|
|
||||||
2026-10-09:已通过 Arcane 上线,LiteLLM 1.104.2 与 PostgreSQL 均 healthy。
|
2026-10-09:已通过 Arcane 上线,LiteLLM 1.104.2 与 PostgreSQL 均 healthy。
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run inside a separate LiteLLM container exec: fake OAuth failures, real API backup."""
|
||||||
|
import asyncio
|
||||||
|
from copy import deepcopy
|
||||||
|
from functools import wraps
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import litellm
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
litellm.suppress_debug_info = True
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
config = yaml.safe_load(Path('/tmp/litellm.yaml').read_text())
|
||||||
|
primary, backup = 'codex/gpt-luna-latest', 'openai/gpt-luna-latest'
|
||||||
|
routes = [deepcopy(r) for r in config['model_list'] if r['model_name'] in (primary, backup)]
|
||||||
|
assert len(routes) == 2
|
||||||
|
assert {primary: [backup]} in config['router_settings']['fallbacks']
|
||||||
|
for route in routes:
|
||||||
|
if route['model_name'] == backup:
|
||||||
|
route['litellm_params']['api_key'] = os.environ['OPENAI_API_KEY']
|
||||||
|
actual_chat, actual_responses = litellm.acompletion, litellm.aresponses
|
||||||
|
for protocol in ('chat', 'responses'):
|
||||||
|
for error_type, expected in ((litellm.RateLimitError, 429),
|
||||||
|
(litellm.AuthenticationError, 401),
|
||||||
|
(litellm.ServiceUnavailableError, 503)):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
async def invoke(real, *args, **kwargs):
|
||||||
|
model = kwargs.get('model', '')
|
||||||
|
if model.startswith('chatgpt/'):
|
||||||
|
calls.append('subscription-rejected')
|
||||||
|
raise error_type(message='Synthetic OAuth failure; isolated test only',
|
||||||
|
llm_provider='chatgpt', model=model)
|
||||||
|
assert model.startswith('openai/')
|
||||||
|
calls.append('official-api')
|
||||||
|
return await real(*args, **kwargs)
|
||||||
|
|
||||||
|
@wraps(actual_chat)
|
||||||
|
async def chat(*args, **kwargs):
|
||||||
|
return await invoke(actual_chat, *args, **kwargs)
|
||||||
|
|
||||||
|
@wraps(actual_responses)
|
||||||
|
async def responses(*args, **kwargs):
|
||||||
|
return await invoke(actual_responses, *args, **kwargs)
|
||||||
|
|
||||||
|
with patch.object(litellm, 'acompletion', chat), patch.object(litellm, 'aresponses', responses):
|
||||||
|
router = litellm.Router(model_list=deepcopy(routes), fallbacks=[{primary: [backup]}],
|
||||||
|
num_retries=0, timeout=60)
|
||||||
|
if protocol == 'chat':
|
||||||
|
result = await router.acompletion(model=primary, messages=[
|
||||||
|
{'role': 'user', 'content': 'Reply only OK. Synthetic test.'}], max_tokens=32)
|
||||||
|
assert result.choices and result.choices[0].message.content
|
||||||
|
else:
|
||||||
|
result = await router.aresponses(model=primary, input=[
|
||||||
|
{'role': 'user', 'content': 'Reply only OK. Synthetic test.'}],
|
||||||
|
max_output_tokens=32, store=False)
|
||||||
|
assert result.status == 'completed'
|
||||||
|
assert calls[0] == 'subscription-rejected' and calls[-1] == 'official-api', calls
|
||||||
|
print(json.dumps({'protocol': protocol, 'injected_error': expected,
|
||||||
|
'calls': calls, 'backup': backup, 'passed': True}), flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
Reference in New Issue
Block a user