接入 OpenAI 官方 API 并为 Codex 订阅配置同型号回退
This commit is contained in:
@@ -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。
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user