121 lines
5.1 KiB
Python
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}"
|