diff --git a/servers/tools-pve/litellm/README.md b/servers/tools-pve/litellm/README.md index 434f402..7568830 100644 --- a/servers/tools-pve/litellm/README.md +++ b/servers/tools-pve/litellm/README.md @@ -41,6 +41,21 @@ GPT路线对应备用模型,不开放图像、语音等无关能力。Claude 依据:[OpenAI赠金说明](https://developers.openai.com/api/docs/guides/chatgpt-api-credits)、 [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。 diff --git a/servers/tools-pve/litellm/verify_openai_fallback.py b/servers/tools-pve/litellm/verify_openai_fallback.py new file mode 100644 index 0000000..179ac32 --- /dev/null +++ b/servers/tools-pve/litellm/verify_openai_fallback.py @@ -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())