From 205bd3bbbca785573ddec100b9bac88d6b9fd09a Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 28 Oct 2026 18:19:04 -0700 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=20OpenAI=20=E5=AE=98?= =?UTF-8?q?=E6=96=B9=20API=20=E5=B9=B6=E4=B8=BA=20Codex=20=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E9=85=8D=E7=BD=AE=E5=90=8C=E5=9E=8B=E5=8F=B7=E5=9B=9E?= =?UTF-8?q?=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- servers/tools-pve/litellm/README.md | 41 ++++++++++++++++++++++ servers/tools-pve/litellm/compose.yaml | 1 + servers/tools-pve/litellm/models.yaml | 13 +++++++ servers/tools-pve/litellm/render_config.py | 41 ++++++++++++++++++++-- servers/tools-pve/litellm/test_config.py | 40 +++++++++++++++++++++ 5 files changed, 133 insertions(+), 3 deletions(-) diff --git a/servers/tools-pve/litellm/README.md b/servers/tools-pve/litellm/README.md index ce0f60f..434f402 100644 --- a/servers/tools-pve/litellm/README.md +++ b/servers/tools-pve/litellm/README.md @@ -1,5 +1,46 @@ # tools-pve 统一模型代理 +## OpenAI 官方 API 与订阅回退(2026-10-29) + +官方 API key 由用户生成,经过明确授权,仅加入本 tools-pve 项目的 Arcane 私有 +`OPENAI_API_KEY`。不写入 Git、不复制到 HK、不替换客户端 virtual key 或 OAuth。 +官方上游固定为 `https://api.openai.com/v1`,不能因 LiteLLM provider 同为 openai +就误归入 ZenMux。未配置 key 时不生成这些路由和 fallback。 + +| 首选订阅路由 | 官方 API 备用路由 | 当前实际模型 | +| --- | --- | --- | +| `codex/gpt-luna-latest` | `openai/gpt-luna-latest` | `gpt-6-luna` | +| `codex/gpt-sol-latest` | `openai/gpt-sol-latest` | `gpt-6.1-sol` | +| `codex/gpt-astra-latest` | `openai/gpt-astra-latest` | `gpt-6-astra` | +| `codex/gpt-6-sol` | `openai/gpt-6-sol` | `gpt-6-sol` | +| `codex/gpt-5.5` | `openai/gpt-5.5` | `gpt-5.5` | + +原不带provider的GPT兼容别名也保留同型号fallback。renderer从chatgpt表生成 +对应的API别名,因此既有latest维护更新后,两侧不会各自残留不同型号。 +`openai/...`显式路由只走官方API,不回到订阅,不改变Grok/Claude/Google/开放模型的主备。 +订阅429、认证或暂时故障可以回退;不自动跨型号降级,失败冷却仍为60秒。 +跨后端不共享prompt cache,也不承诺中途流断线或previous_response_id跨后端续接。 +需要保留完整上下文以便调用方重试,不自动重放已经执行的工具。 + +API专有补充:`openai/gpt-terra-latest`(目前6.1 Terra); +`openai/gpt-transcribe`、`openai/gpt-4o-transcribe`、 +`openai/gpt-4o-mini-transcribe`、`openai/gpt-4o-transcribe-diarize`; +`openai/text-embedding-3-small`、`openai/text-embedding-3-large`; +`openai/gpt-4o-mini-tts`;`openai/gpt-image-2.5-flare`、 +`openai/gpt-image-2.5-sunburst`。图像路线可配置不代表做过昂贵出图验收, +各接口按实际测试结果记录。没有修改现有ASR热词、Hindsight embedding或客户端默认模型。 +Terra与其他API专有模型暂按models.yaml显式维护,不擅自扩张既有Hermes自动维护授权。 + +现有lan-agents应用key增补这些API路线;Hindsight等受限key只增补其原先已允许的 +GPT路线对应备用模型,不开放图像、语音等无关能力。Claude专用key不变。 + +赠金需绑定到此key所属API组织,并在该订阅账期末到期,不结转。赠金额度先消耗, +余额不足后若有已购额度可继续消费;没有任何额度则返回429 credit_balance_exhausted。 +代理不能凭普通project key保证“只花赠金”,不替用户开启充值。这里提供的是 +标准API请求的主备,不把官方赠金解释为可给Codex CLI的API-key模式增加额度。 +依据:[OpenAI赠金说明](https://developers.openai.com/api/docs/guides/chatgpt-api-credits)、 +[LiteLLM回退](https://docs.litellm.ai/docs/proxy/reliability)。 + ## 部署状态 2026-10-09:已通过 Arcane 上线,LiteLLM 1.104.2 与 PostgreSQL 均 healthy。 diff --git a/servers/tools-pve/litellm/compose.yaml b/servers/tools-pve/litellm/compose.yaml index 1f142cc..ff9b320 100644 --- a/servers/tools-pve/litellm/compose.yaml +++ b/servers/tools-pve/litellm/compose.yaml @@ -37,6 +37,7 @@ services: ZENMUX_SUBSCRIPTION_API_KEY: ${ZENMUX_SUBSCRIPTION_API_KEY:-} ZENMUX_PAYG_API_KEY: ${ZENMUX_PAYG_API_KEY:-} OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} BAILIAN_CN_API_KEY: ${BAILIAN_CN_API_KEY:-} BAILIAN_CN_API_BASE: ${BAILIAN_CN_API_BASE:-https://ws-v68kvjnmhkiejaxo.cn-beijing.maas.aliyuncs.com/api/v1} GOOGLE_AI_STUDIO_API_KEY: ${GOOGLE_AI_STUDIO_API_KEY:-} diff --git a/servers/tools-pve/litellm/models.yaml b/servers/tools-pve/litellm/models.yaml index f25007b..31bbc60 100644 --- a/servers/tools-pve/litellm/models.yaml +++ b/servers/tools-pve/litellm/models.yaml @@ -41,3 +41,16 @@ chatgpt: gpt-astra-latest: gpt-6-astra xai: grok-latest: grok-4.7 +# Official API-only additions. OAuth model aliases above are mirrored by renderer +# so latest maintenance cannot silently leave the fallback on an older model. +openai: + gpt-terra-latest: gpt-6.1-terra + gpt-transcribe: gpt-transcribe + gpt-4o-transcribe: gpt-4o-transcribe + gpt-4o-mini-transcribe: gpt-4o-mini-transcribe + gpt-4o-transcribe-diarize: gpt-4o-transcribe-diarize + text-embedding-3-small: text-embedding-3-small + text-embedding-3-large: text-embedding-3-large + gpt-image-2.5-flare: gpt-image-2.5-flare + gpt-image-2.5-sunburst: gpt-image-2.5-sunburst + gpt-4o-mini-tts: gpt-4o-mini-tts diff --git a/servers/tools-pve/litellm/render_config.py b/servers/tools-pve/litellm/render_config.py index 8777d1f..97f27e9 100644 --- a/servers/tools-pve/litellm/render_config.py +++ b/servers/tools-pve/litellm/render_config.py @@ -91,6 +91,39 @@ def add_bailian_routes(config, env, vocabulary_path): return config +def add_openai_routes(config, catalog, env): + """Official API is separate from OAuth and third-party OpenAI-compatible APIs.""" + if not env.get('OPENAI_API_KEY'): + return config + models = dict(catalog.get('openai', {})) + for alias, model in catalog.get('chatgpt', {}).items(): + name = alias if alias.endswith('-latest') else model + if name in models and models[name] != model: + raise ValueError('Official API/OAuth model mismatch: ' + name) + models[name] = model + by_model = {} + for alias, model in models.items(): + name = 'openai/' + alias + config['model_list'].append({'model_name': name, 'litellm_params': { + 'model': 'openai/' + model, 'api_key': 'os.environ/OPENAI_API_KEY', + 'api_base': 'https://api.openai.com/v1', 'max_retries': 0}}) + by_model[model] = name + for route in list(config['model_list']): + params = route['litellm_params'] + if not params['model'].startswith('chatgpt/'): + continue + backup = by_model.get(params['model'].removeprefix('chatgpt/')) + if not backup: + raise ValueError('Missing same-model official API fallback') + params['max_retries'] = 0 + route['model_info'] = {'cooldown_time': 60, 'allowed_fails_policy': { + 'RateLimitErrorAllowedFails': 0, 'AuthenticationErrorAllowedFails': 0, + 'InternalServerErrorAllowedFails': 0, 'ServiceUnavailableErrorAllowedFails': 0, + 'TimeoutErrorAllowedFails': 0}} + config['router_settings']['fallbacks'].append({route['model_name']: [backup]}) + return config + + def build_config(catalog, env, exists, namespaced=True): routes = [] fallbacks = [] @@ -159,11 +192,13 @@ def build_config(catalog, env, exists, namespaced=True): 'disable_env_credential_login': True, }, 'litellm_settings': {'drop_params': False, 'set_verbose': False}, - # Only explicitly approved open-model routes have cross-provider fallbacks. + # Official OpenAI fallbacks are appended after namespace expansion. 'router_settings': {'num_retries': 1, 'timeout': 180, 'fallbacks': fallbacks}, } - return add_bailian_routes(namespace_config(config), env, - '/etc/litellm/personal-vocabulary.json') if namespaced else config + if namespaced: + config = add_bailian_routes(namespace_config(config), env, + '/etc/litellm/personal-vocabulary.json') + return add_openai_routes(config, catalog, env) if __name__ == '__main__': diff --git a/servers/tools-pve/litellm/test_config.py b/servers/tools-pve/litellm/test_config.py index e2b6679..3d629d6 100644 --- a/servers/tools-pve/litellm/test_config.py +++ b/servers/tools-pve/litellm/test_config.py @@ -57,6 +57,46 @@ class ConfigTests(unittest.TestCase): settings = build_config(self.catalog, {}, lambda _: False)['general_settings'] self.assertTrue(settings['disable_env_credential_login']) + def test_official_openai_missing_key_does_not_change_existing_config(self): + catalog = {**self.catalog, 'openai': {'transcribe': 'gpt-transcribe'}} + config = build_config(catalog, {}, lambda _: True) + self.assertFalse(any(r['model_name'].startswith('openai/') for r in config['model_list'])) + self.assertEqual(config['router_settings']['fallbacks'], []) + + def test_official_openai_is_not_misclassified_as_zenmux(self): + catalog = {**self.catalog, 'openai': {'transcribe': 'gpt-transcribe'}} + config = build_config(catalog, {'OPENAI_API_KEY': 'synthetic-secret'}, lambda _: False) + routes = {r['model_name']: r for r in config['model_list']} + self.assertIn('openai/transcribe', routes) + self.assertNotIn('zenmux/transcribe', routes) + self.assertEqual(routes['openai/transcribe']['litellm_params']['api_base'], 'https://api.openai.com/v1') + self.assertEqual(routes['openai/transcribe']['litellm_params']['api_key'], 'os.environ/OPENAI_API_KEY') + self.assertNotIn('synthetic-secret', str(config)) + self.assertEqual(config['router_settings']['fallbacks'], []) + + def test_oauth_fallbacks_cover_legacy_and_namespaced_routes(self): + config = build_config(self.catalog, {'OPENAI_API_KEY': 'synthetic'}, lambda _: True) + groups = {k: v for row in config['router_settings']['fallbacks'] for k, v in row.items()} + self.assertEqual(groups['codex-latest'], ['openai/codex-latest']) + self.assertEqual(groups['codex/codex-latest'], ['openai/codex-latest']) + self.assertNotIn('grok-latest', groups) + self.assertNotIn('openai/codex-latest', groups) + + def test_actual_catalog_has_same_model_api_fallbacks_and_latest_tracks(self): + catalog = yaml.safe_load(Path(__file__).with_name('models.yaml').read_text()) + catalog['chatgpt']['gpt-luna-latest'] = 'gpt-99-luna' + config = build_config(catalog, {'OPENAI_API_KEY': 'synthetic'}, lambda _: True) + routes = {r['model_name']: r for r in config['model_list']} + self.assertEqual(routes['openai/gpt-luna-latest']['litellm_params']['model'], 'openai/gpt-99-luna') + groups = {k: v for row in config['router_settings']['fallbacks'] for k, v in row.items()} + for name, route in routes.items(): + if route['litellm_params']['model'].startswith('chatgpt/'): + backup = routes[groups[name][0]] + self.assertEqual(route['litellm_params']['model'].split('/', 1)[1], + backup['litellm_params']['model'].split('/', 1)[1]) + self.assertEqual(groups['codex/gpt-6-sol'], ['openai/gpt-6-sol']) + self.assertEqual(groups['codex/gpt-5.5'], ['openai/gpt-5.5']) + def test_subscription_preserves_vendor_prefix(self): routes = build_config(self.catalog, {'ZENMUX_SUBSCRIPTION_API_KEY': 'secret'}, lambda _: False)['model_list'] self.assertEqual(routes[0]['litellm_params']['model'], 'anthropic/anthropic/claude-sonnet-5.5')