From daa907d029fc1ef8dbb15e134663d77a3823777e Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 26 Oct 2026 17:38:55 -0700 Subject: [PATCH] feat(litellm): add provider namespaces with legacy compatibility --- servers/bwh-hk-cmi/litellm/provider_routes.py | 66 ++++++++++++++++++- servers/bwh-hk-cmi/litellm/render_config.py | 6 +- servers/bwh-hk-cmi/litellm/test_config.py | 5 +- servers/bwh-hk-cmi/litellm/模型命名.md | 24 +++++++ servers/tools-pve/litellm/render_config.py | 66 ++++++++++++++++++- servers/tools-pve/litellm/test_config.py | 33 +++++++++- servers/tools-pve/litellm/模型命名.md | 24 +++++++ 7 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 servers/bwh-hk-cmi/litellm/模型命名.md create mode 100644 servers/tools-pve/litellm/模型命名.md diff --git a/servers/bwh-hk-cmi/litellm/provider_routes.py b/servers/bwh-hk-cmi/litellm/provider_routes.py index f20dc69..b7d9fb7 100644 --- a/servers/bwh-hk-cmi/litellm/provider_routes.py +++ b/servers/bwh-hk-cmi/litellm/provider_routes.py @@ -1,6 +1,7 @@ """Render only credential-ready routes; credentials remain environment references.""" import os import json +from copy import deepcopy from pathlib import Path import yaml @@ -14,7 +15,67 @@ def oauth_ready(path): return False -def build_config(catalog, env, exists): +def namespace_config(config): + """Add explicit provider names without removing existing client aliases.""" + routes = config['model_list'] + existing = {r['model_name']: r for r in routes} + fallbacks = config['router_settings']['fallbacks'] + automatic = {name for group in fallbacks for name in group} + renamed = {} + + def clone(route, alias): + if alias in existing: + if existing[alias]['litellm_params'] != route['litellm_params']: + raise ValueError('Conflicting namespace alias: ' + alias) + return + new = deepcopy(route) + new['model_name'] = alias + existing[alias] = new + routes.append(new) + + for route in list(routes): + alias = route['model_name'] + model = route['litellm_params']['model'] + if alias in {'gemini-3.6-flash', 'gemini-3.5-flash-lite', + 'google/gemini-embedding-2', 'google/gemini-3.8-flash'}: + continue + if alias.endswith('-openrouter'): + new = 'openrouter/' + alias.removesuffix('-openrouter') + elif alias in automatic: + new = 'auto/' + alias + clone(route, 'zenmux/' + alias) + elif model.startswith('anthropic/'): + new = ('zenmux-payg/' + alias.removesuffix('-payg') + if alias.endswith('-payg') else 'zenmux/' + alias) + elif model.startswith('openai/'): + name = model.removeprefix('openai/').split('/', 1)[-1] + new = 'zenmux/' + (name if alias.startswith('asr-') else + alias.replace('-zenmux', '-latest')) + elif model.startswith('openrouter/'): + new = 'openrouter/' + alias + elif model.startswith('gemini/'): + new = 'aistudio/' + alias.removesuffix('-studio') + elif model.startswith('vertex_ai/'): + name = {'google-rerank-default': 'rerank-default-latest', + 'google-rerank-fast': 'rerank-fast-latest'}.get(alias, + alias.replace('-vertex', '-latest')) + new = 'vertex/' + name + elif model.startswith('chatgpt/'): + name = model.split('/', 1)[1] if alias in {'gpt-work-sol', 'gpt-review-5.5'} else alias + new = 'codex/' + name + elif model.startswith('xai/'): + new = 'xai/' + alias + else: + raise ValueError('Unrecognized namespace provider: ' + alias) + clone(route, new) + renamed[alias] = new + for group in list(fallbacks): + for source, targets in group.items(): + fallbacks.append({renamed[source]: [renamed[target] for target in targets]}) + return config + + +def build_config(catalog, env, exists, namespaced=True): routes = [] fallbacks = [] @@ -73,7 +134,7 @@ def build_config(catalog, env, exists): for alias, model in catalog[provider].items(): extra = {'use_xai_oauth': True} if provider == 'xai' else {} add(alias, provider + '/' + model, **extra) - return { + config = { 'model_list': routes, 'general_settings': { 'master_key': 'os.environ/LITELLM_MASTER_KEY', @@ -85,6 +146,7 @@ def build_config(catalog, env, exists): # Only explicitly approved open-model routes have cross-provider fallbacks. 'router_settings': {'num_retries': 1, 'timeout': 180, 'fallbacks': fallbacks}, } + return namespace_config(config) if namespaced else config if __name__ == '__main__': diff --git a/servers/bwh-hk-cmi/litellm/render_config.py b/servers/bwh-hk-cmi/litellm/render_config.py index 2590b47..e5ea423 100644 --- a/servers/bwh-hk-cmi/litellm/render_config.py +++ b/servers/bwh-hk-cmi/litellm/render_config.py @@ -4,11 +4,11 @@ from pathlib import Path import yaml -from provider_routes import build_config +from provider_routes import build_config, namespace_config def build_hk_config(catalog, env): - config = build_config(catalog, env, lambda path: False) + config = build_config(catalog, env, lambda path: False, namespaced=False) config['general_settings'] = {'master_key': 'os.environ/LITELLM_MASTER_KEY'} routes = config['model_list'] routes[:] = [route for route in routes if route['model_name'] != 'gemini-transcribe-personal'] @@ -47,7 +47,7 @@ def build_hk_config(catalog, env): 'model': 'vertex_ai/' + model, 'vertex_project': 'os.environ/VERTEXAI_PROJECT', 'vertex_location': 'os.environ/VERTEXAI_LOCATION'}}) - return config + return namespace_config(config) if __name__ == '__main__': diff --git a/servers/bwh-hk-cmi/litellm/test_config.py b/servers/bwh-hk-cmi/litellm/test_config.py index d6adb6d..f959c77 100644 --- a/servers/bwh-hk-cmi/litellm/test_config.py +++ b/servers/bwh-hk-cmi/litellm/test_config.py @@ -30,7 +30,10 @@ class RoutingTests(unittest.TestCase): self.assertEqual(routes[alias]['api_key'], 'os.environ/ZENMUX_SUBSCRIPTION_API_KEY') self.assertEqual(routes['gemini-transcribe']['model'], 'gemini/gemini-3.5-transcribe') self.assertEqual(routes['gemini-3.6-flash']['model'], 'vertex_ai/gemini-3.8-flash') - self.assertEqual(len(result['router_settings']['fallbacks']), 6) + self.assertEqual(len(result['router_settings']['fallbacks']), 12) + self.assertEqual(routes['vertex/gemini-transcribe-personal'], routes['gemini-transcribe-personal']) + self.assertNotIn('aistudio/gemini-transcribe-personal', routes) + self.assertEqual(routes['zenmux/gpt-transcribe'], routes['asr-gpt']) self.assertEqual(result['general_settings'], {'master_key': 'os.environ/LITELLM_MASTER_KEY'}) self.assertNotIn('synthetic', yaml.safe_dump(result)) diff --git a/servers/bwh-hk-cmi/litellm/模型命名.md b/servers/bwh-hk-cmi/litellm/模型命名.md new file mode 100644 index 0000000..e9e65f5 --- /dev/null +++ b/servers/bwh-hk-cmi/litellm/模型命名.md @@ -0,0 +1,24 @@ +# 模型命名空间 + +2026-10-27 起新增客户端命名空间,保留旧名兼容。实际型号、协议、凭据、个人热词和 SMART 均不改变;latest 不代表自动跟踪发布。 + +| 前缀 | 含义 | +| --- | --- | +| zenmux/ | ZenMux 订阅直连,不跨 provider 回退 | +| zenmux-payg/ | ZenMux PAYG,仅显式请求 | +| openrouter/ | OpenRouter 直连 | +| auto/ | 开源模型 ZenMux 订阅优先,故障回退 OpenRouter | +| aistudio/ | Google AI Studio | +| vertex/ | Google Vertex | +| codex/ | Codex OAuth 订阅 | +| xai/ | xAI OAuth 订阅 | + +常规旧别名加对应前缀。特殊转换:`gemini-flash-zenmux` → `zenmux/gemini-flash-latest`;`gemini-*-vertex` → `vertex/gemini-*-latest`;`gemini-embedding-2-studio` → `aistudio/gemini-embedding-2`;`google-rerank-default/fast` → `vertex/rerank-default/fast-latest`;`gpt-work-sol` → `codex/gpt-6-sol`;`gpt-review-5.5` → `codex/gpt-5.5`。PAYG 去掉旧 `-payg` 后缀,改加 `zenmux-payg/` 前缀。 + +六组开源模型 `deepseek-flash-latest`、`deepseek-pro-latest`、`glm-latest`、`glm-flash-latest`、`kimi-latest`、`mimo-flash-latest` 各提供 auto/、zenmux/、openrouter/ 三种选择。旧自动回退名和旧 -openrouter 名保留。 + +tools-pve 的个人语音名为 `aistudio/gemini-transcribe-personal`;HK 使用 `vertex/gemini-transcribe-personal`,不能互换两端 provider。HK 四款 ZenMux ASR 别名为 `zenmux/gpt-transcribe`、`zenmux/qwen-audio-3.1-asr-flash`、`zenmux/doubao-seed-asr-2.0`、`zenmux/mimo-v2.5-asr`。HK 的四个历史 Google 兼容名保留。 + +只有凭据已就绪的路线才生成。新旧别名的 litellm_params 必须完全相同,不能把客户端前缀当作上游 provider 参数。受模型白名单限制的 virtual key 需要另行授权新名;不扩大既有 key 权限。旧名暂不删除,待所有客户端完成迁移后单独处理。 + +修改 renderer 后,通过 Arcane Git Sync 同步,并重启对应 LiteLLM 进程使 /tmp/litellm.yaml 重建;仅同步代码不代表进程已加载。逐节点确认 /v1/models、新旧路由参数等价、fallback 配置及实际请求。 diff --git a/servers/tools-pve/litellm/render_config.py b/servers/tools-pve/litellm/render_config.py index f20dc69..b7d9fb7 100644 --- a/servers/tools-pve/litellm/render_config.py +++ b/servers/tools-pve/litellm/render_config.py @@ -1,6 +1,7 @@ """Render only credential-ready routes; credentials remain environment references.""" import os import json +from copy import deepcopy from pathlib import Path import yaml @@ -14,7 +15,67 @@ def oauth_ready(path): return False -def build_config(catalog, env, exists): +def namespace_config(config): + """Add explicit provider names without removing existing client aliases.""" + routes = config['model_list'] + existing = {r['model_name']: r for r in routes} + fallbacks = config['router_settings']['fallbacks'] + automatic = {name for group in fallbacks for name in group} + renamed = {} + + def clone(route, alias): + if alias in existing: + if existing[alias]['litellm_params'] != route['litellm_params']: + raise ValueError('Conflicting namespace alias: ' + alias) + return + new = deepcopy(route) + new['model_name'] = alias + existing[alias] = new + routes.append(new) + + for route in list(routes): + alias = route['model_name'] + model = route['litellm_params']['model'] + if alias in {'gemini-3.6-flash', 'gemini-3.5-flash-lite', + 'google/gemini-embedding-2', 'google/gemini-3.8-flash'}: + continue + if alias.endswith('-openrouter'): + new = 'openrouter/' + alias.removesuffix('-openrouter') + elif alias in automatic: + new = 'auto/' + alias + clone(route, 'zenmux/' + alias) + elif model.startswith('anthropic/'): + new = ('zenmux-payg/' + alias.removesuffix('-payg') + if alias.endswith('-payg') else 'zenmux/' + alias) + elif model.startswith('openai/'): + name = model.removeprefix('openai/').split('/', 1)[-1] + new = 'zenmux/' + (name if alias.startswith('asr-') else + alias.replace('-zenmux', '-latest')) + elif model.startswith('openrouter/'): + new = 'openrouter/' + alias + elif model.startswith('gemini/'): + new = 'aistudio/' + alias.removesuffix('-studio') + elif model.startswith('vertex_ai/'): + name = {'google-rerank-default': 'rerank-default-latest', + 'google-rerank-fast': 'rerank-fast-latest'}.get(alias, + alias.replace('-vertex', '-latest')) + new = 'vertex/' + name + elif model.startswith('chatgpt/'): + name = model.split('/', 1)[1] if alias in {'gpt-work-sol', 'gpt-review-5.5'} else alias + new = 'codex/' + name + elif model.startswith('xai/'): + new = 'xai/' + alias + else: + raise ValueError('Unrecognized namespace provider: ' + alias) + clone(route, new) + renamed[alias] = new + for group in list(fallbacks): + for source, targets in group.items(): + fallbacks.append({renamed[source]: [renamed[target] for target in targets]}) + return config + + +def build_config(catalog, env, exists, namespaced=True): routes = [] fallbacks = [] @@ -73,7 +134,7 @@ def build_config(catalog, env, exists): for alias, model in catalog[provider].items(): extra = {'use_xai_oauth': True} if provider == 'xai' else {} add(alias, provider + '/' + model, **extra) - return { + config = { 'model_list': routes, 'general_settings': { 'master_key': 'os.environ/LITELLM_MASTER_KEY', @@ -85,6 +146,7 @@ def build_config(catalog, env, exists): # Only explicitly approved open-model routes have cross-provider fallbacks. 'router_settings': {'num_retries': 1, 'timeout': 180, 'fallbacks': fallbacks}, } + return namespace_config(config) if namespaced else config if __name__ == '__main__': diff --git a/servers/tools-pve/litellm/test_config.py b/servers/tools-pve/litellm/test_config.py index a109f4c..1a62c04 100644 --- a/servers/tools-pve/litellm/test_config.py +++ b/servers/tools-pve/litellm/test_config.py @@ -1,4 +1,5 @@ import unittest +import yaml from pathlib import Path from tempfile import TemporaryDirectory from render_config import build_config, oauth_ready @@ -17,6 +18,29 @@ class ConfigTests(unittest.TestCase): def test_missing_credentials_exclude_routes(self): self.assertEqual(build_config(self.catalog, {}, lambda _: False)['model_list'], []) + def test_namespaces_preserve_every_legacy_route(self): + catalog = yaml.safe_load(Path(__file__).with_name('models.yaml').read_text()) + env = {key: 'synthetic' for key in ['ZENMUX_SUBSCRIPTION_API_KEY', + 'ZENMUX_PAYG_API_KEY', 'OPENROUTER_API_KEY', 'GOOGLE_AI_STUDIO_API_KEY', + 'VERTEXAI_PROJECT']} + env['VERTEXAI_ENABLED'] = 'true' + old = build_config(catalog, env, lambda _: True, namespaced=False) + new = build_config(catalog, env, lambda _: True) + routes = {r['model_name']: r for r in new['model_list']} + self.assertEqual(len(routes), len(new['model_list'])) + for route in old['model_list']: + self.assertEqual(route, routes[route['model_name']]) + for old_name, new_name in { + 'claude-sonnet-latest': 'zenmux/claude-sonnet-latest', + 'claude-opus-latest-payg': 'zenmux-payg/claude-opus-latest', + 'grok-latest': 'xai/grok-latest', 'gpt-sol-latest': 'codex/gpt-sol-latest', + 'gpt-work-sol': 'codex/gpt-6-sol', 'gpt-review-5.5': 'codex/gpt-5.5', + 'gemini-transcribe-personal': 'aistudio/gemini-transcribe-personal', + 'gemini-embedding-2-studio': 'aistudio/gemini-embedding-2', + 'google-rerank-fast': 'vertex/rerank-fast-latest', + }.items(): + self.assertEqual(routes[old_name]['litellm_params'], routes[new_name]['litellm_params']) + def test_env_ui_login_disabled(self): settings = build_config(self.catalog, {}, lambda _: False)['general_settings'] self.assertTrue(settings['disable_env_credential_login']) @@ -32,7 +56,7 @@ class ConfigTests(unittest.TestCase): def test_oauth_requires_persistent_file(self): routes = build_config(self.catalog, {}, lambda path: str(path) == '/tokens/xai/auth.json')['model_list'] - self.assertEqual(len(routes), 1) + self.assertEqual(len(routes), 2) self.assertTrue(routes[0]['litellm_params']['use_xai_oauth']) self.assertNotIn('api_key', routes[0]['litellm_params']) @@ -61,7 +85,12 @@ class ConfigTests(unittest.TestCase): routes = {r['model_name']: r for r in config['model_list']} self.assertEqual(routes['glm-latest']['litellm_params']['model'], 'openai/z-ai/glm-5.3-prime') self.assertEqual(routes['glm-latest-openrouter']['litellm_params']['model'], 'openrouter/z-ai/glm-5.3-prime') - self.assertEqual(config['router_settings']['fallbacks'], [{'glm-latest': ['glm-latest-openrouter']}]) + self.assertEqual(config['router_settings']['fallbacks'], [ + {'glm-latest': ['glm-latest-openrouter']}, + {'auto/glm-latest': ['openrouter/glm-latest']}]) + self.assertEqual(routes['auto/glm-latest']['litellm_params'], routes['glm-latest']['litellm_params']) + self.assertEqual(routes['zenmux/glm-latest']['litellm_params'], routes['glm-latest']['litellm_params']) + self.assertNotIn('zenmux/glm-latest', str(config['router_settings']['fallbacks'])) self.assertEqual(routes['glm-latest']['model_info']['cooldown_time'], 60) self.assertNotIn('glm-latest-payg', routes) diff --git a/servers/tools-pve/litellm/模型命名.md b/servers/tools-pve/litellm/模型命名.md new file mode 100644 index 0000000..e9e65f5 --- /dev/null +++ b/servers/tools-pve/litellm/模型命名.md @@ -0,0 +1,24 @@ +# 模型命名空间 + +2026-10-27 起新增客户端命名空间,保留旧名兼容。实际型号、协议、凭据、个人热词和 SMART 均不改变;latest 不代表自动跟踪发布。 + +| 前缀 | 含义 | +| --- | --- | +| zenmux/ | ZenMux 订阅直连,不跨 provider 回退 | +| zenmux-payg/ | ZenMux PAYG,仅显式请求 | +| openrouter/ | OpenRouter 直连 | +| auto/ | 开源模型 ZenMux 订阅优先,故障回退 OpenRouter | +| aistudio/ | Google AI Studio | +| vertex/ | Google Vertex | +| codex/ | Codex OAuth 订阅 | +| xai/ | xAI OAuth 订阅 | + +常规旧别名加对应前缀。特殊转换:`gemini-flash-zenmux` → `zenmux/gemini-flash-latest`;`gemini-*-vertex` → `vertex/gemini-*-latest`;`gemini-embedding-2-studio` → `aistudio/gemini-embedding-2`;`google-rerank-default/fast` → `vertex/rerank-default/fast-latest`;`gpt-work-sol` → `codex/gpt-6-sol`;`gpt-review-5.5` → `codex/gpt-5.5`。PAYG 去掉旧 `-payg` 后缀,改加 `zenmux-payg/` 前缀。 + +六组开源模型 `deepseek-flash-latest`、`deepseek-pro-latest`、`glm-latest`、`glm-flash-latest`、`kimi-latest`、`mimo-flash-latest` 各提供 auto/、zenmux/、openrouter/ 三种选择。旧自动回退名和旧 -openrouter 名保留。 + +tools-pve 的个人语音名为 `aistudio/gemini-transcribe-personal`;HK 使用 `vertex/gemini-transcribe-personal`,不能互换两端 provider。HK 四款 ZenMux ASR 别名为 `zenmux/gpt-transcribe`、`zenmux/qwen-audio-3.1-asr-flash`、`zenmux/doubao-seed-asr-2.0`、`zenmux/mimo-v2.5-asr`。HK 的四个历史 Google 兼容名保留。 + +只有凭据已就绪的路线才生成。新旧别名的 litellm_params 必须完全相同,不能把客户端前缀当作上游 provider 参数。受模型白名单限制的 virtual key 需要另行授权新名;不扩大既有 key 权限。旧名暂不删除,待所有客户端完成迁移后单独处理。 + +修改 renderer 后,通过 Arcane Git Sync 同步,并重启对应 LiteLLM 进程使 /tmp/litellm.yaml 重建;仅同步代码不代表进程已加载。逐节点确认 /v1/models、新旧路由参数等价、fallback 配置及实际请求。