Files
lsdefine--genericagent/frontends/model_cmd.py
T
2026-07-13 13:04:19 +08:00

121 lines
5.1 KiB
Python

"""/model 命令的 agent 无关逻辑: 拉模型列表 + 运行时改 model。
被 tuiapp_v2 import; 不 patch 任何类, 不依赖 llmcore。"""
from typing import Optional, List, Tuple
import requests
def _is_mixin(b) -> bool:
return type(b).__name__ == 'MixinSession'
def _resolve(agent, sub: Optional[int] = None):
"""返回 (真正持有 model 的 session, mixin或None)。"""
b = agent.llmclient.backend
if _is_mixin(b):
return b._sessions[b._cur_idx if sub is None else sub], b
return b, None
def list_subsessions(agent) -> Optional[List[Tuple[int, str, bool]]]:
"""mixin 渠道 → [(idx, name, is_current)]; 普通渠道 → None。"""
b = agent.llmclient.backend
if not _is_mixin(b):
return None
return [(i, s.name, i == b._cur_idx) for i, s in enumerate(b._sessions)]
def current_model(agent, sub: Optional[int] = None) -> str:
s, _ = _resolve(agent, sub)
return s.model
def fetch_models(agent, sub: Optional[int] = None, timeout: int = 10) -> List[str]:
"""GET models 列表, 自动尝试 /models 与 /v1/models、原生头与 Bearer。"""
s, _ = _resolve(agent, sub)
base = s.api_base.rstrip('/')
urls = [f'{base}/models'] + ([] if base.endswith('/v1') else [f'{base}/v1/models'])
heads = [{'Authorization': f'Bearer {s.api_key}'}]
if 'Claude' in type(s).__name__:
heads.insert(0, {'x-api-key': s.api_key, 'anthropic-version': '2023-06-01'})
err = None
for url in urls:
for h in heads:
try:
r = requests.get(url, headers=h, timeout=timeout,
proxies=getattr(s, 'proxies', None),
verify=getattr(s, 'verify', True))
r.raise_for_status()
data = r.json().get('data', []) # 非 JSON(如 HTML 首页)会抛错进入下一候选
ids = {m['id'] for m in data if isinstance(m, dict) and m.get('id')}
if ids:
return sorted(ids)
except Exception as e:
err = e
raise err or RuntimeError('no models endpoint')
def set_model(agent, model: str, sub: Optional[int] = None) -> str:
"""运行时改 model(内存态, mykey 重载/重启后还原)。返回结果描述。"""
s, mixin = _resolve(agent, sub)
old = s.model
s.model = model # mixin 的 model 是只读 property, 必须落子 session
warn = ""
try: # 对齐 agentmain.next_llm 的中文 schema 切换
from agentmain import load_tool_schema
load_tool_schema('_cn' if any(x in model.lower() for x in ('glm', 'minimax', 'kimi')) else '')
except Exception as ex: # schema 选错会实际影响 agent 行为, 半径不为零, 不静默
warn = f" (⚠ schema 切换失败: {type(ex).__name__})"
where = f"[{s.name}]" if mixin else s.name
return f"{where}: {old}{model}{warn}"
# GA 配置层允许的全部档位(llmcore BaseSession._enum)。各协议的真实支持面不同:
# Claude(output_config.effort) 只认 low/medium/high + xhigh→max, none/minimal
# 会被 _apply_claude_thinking 打 WARN 忽略; OpenAI 系(reasoning_effort /
# reasoning.effort) 原样透传, 由渠道端校验。
EFFORT_LEVELS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh']
def _protocols(agent) -> set:
"""当前渠道涉及的协议集合 {'claude', 'openai'}。mixin 看全部子渠道(广播
会落到每一个)。NativeOAISession 名字不含 'Claude', 正确归入 openai。"""
b = agent.llmclient.backend
sessions = b._sessions if _is_mixin(b) else [b]
return {('claude' if 'Claude' in type(s).__name__ else 'openai')
for s in sessions}
def effort_note(level, protocols) -> str:
"""单点描述某档位在给定协议上的特殊行为(空串=无特殊)。set_effort 的结果
描述与 /effort picker 的行内备注共用它, 领域知识只在此编码一次。"""
if level and 'claude' in protocols:
if level in ('none', 'minimal'):
return 'Claude 渠道忽略'
if level == 'xhigh':
return 'Claude 对应 max'
return ''
def current_effort(agent) -> str:
return getattr(agent.llmclient.backend, 'reasoning_effort', None) or ''
def set_effort(agent, value) -> str:
"""运行时改 reasoning_effort(内存态)。空值/off 清除(不再发送 effort 字段)。
直接设在 backend 上: MixinSession 把它列为 _BROADCAST_ATTRS, 会同步到所有
子渠道(故障切换后档位不丢); 普通渠道就是 session 本身。请求时各协议现读
该属性, 立即生效。"""
e = (value or '').strip().lower()
if e in ('', 'off', 'clear', 'unset'):
e = None
elif e not in EFFORT_LEVELS:
return (f"无效 effort: {value!r}"
f" (可选 {'/'.join(EFFORT_LEVELS)}, 留空或 off 清除)")
b = agent.llmclient.backend
old = getattr(b, 'reasoning_effort', None)
b.reasoning_effort = e
note = effort_note(e, _protocols(agent))
tail = f" ({note})" if note else ""
return f"reasoning_effort: {old or '(未设置)'}{e or '(清除)'}{tail}"