接入 OpenAI 官方 API 并为 Codex 订阅配置同型号回退

This commit is contained in:
Codex
2026-10-28 18:19:04 -07:00
parent ed5f6514bb
commit 205bd3bbbc
5 changed files with 133 additions and 3 deletions
+41
View File
@@ -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。
+1
View File
@@ -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:-}
+13
View File
@@ -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
+38 -3
View File
@@ -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__':
+40
View File
@@ -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')