Files
mini-openclaw-v2/backend/services/model_settings.py
T

63 lines
3.4 KiB
Python
Raw Normal View History

"""BR-68 private local model configuration; keys never appear in responses."""
import json
import os
import re
from pathlib import Path
import httpx
PATH = Path(__file__).resolve().parents[1] / 'xingbang_data' / 'model-settings.private.json'
def read():
try:
return json.loads(PATH.read_text(encoding='utf-8')) if PATH.exists() else {}
except (ValueError, OSError):
return {}
def configured():
if os.getenv('XINGBANG_LOCAL_MODEL_URL'):
return {'endpoint': os.environ['XINGBANG_LOCAL_MODEL_URL'], 'name': os.getenv('XINGBANG_LOCAL_MODEL_NAME', 'local-model'), 'managed_by': 'environment'}
value=read()
return {k:value.get(k) for k in ('endpoint','name')} | {'managed_by':'local_file'}
def api_key():
if os.getenv('XINGBANG_LOCAL_MODEL_URL'):
return os.getenv('XINGBANG_LOCAL_MODEL_KEY','local')
return os.getenv('XINGBANG_LOCAL_MODEL_KEY') or read().get('api_key') or 'local'
def status():
from services.assistant_model import configuration
config=configured()
try:
ready=configuration().get('available',False)
except ValueError:
ready=False
return {'available':ready,'name':config.get('name') or '', 'endpoint':(config.get('endpoint') or '') if ready else '', 'managed_by':config['managed_by'], 'key_configured':api_key()!='local', 'provider':'DeepSeek' if 'api.deepseek.com' in (config.get('endpoint') or '') else '本地兼容模型'}
def save(endpoint,name,key):
from services.assistant_model import validate_endpoint
if os.getenv('XINGBANG_LOCAL_MODEL_URL'):
raise ValueError('MODEL_ENV_LOCKED')
if not re.fullmatch(r'[A-Za-z0-9_.:/-]{1,160}',name):
raise ValueError('MODEL_SETTINGS_INVALID')
try:
validate_endpoint(endpoint)
except ValueError:
raise ValueError('MODEL_SETTINGS_INVALID') from None
current=read()
value={'endpoint':endpoint,'name':name,'api_key':current.get('api_key','') if key is None else key}
PATH.parent.mkdir(parents=True,exist_ok=True)
fd=os.open(PATH.with_suffix('.tmp'),os.O_WRONLY|os.O_CREAT|os.O_TRUNC,0o600)
with os.fdopen(fd,'w',encoding='utf-8') as stream:
json.dump(value,stream,ensure_ascii=False)
os.replace(PATH.with_suffix('.tmp'),PATH)
return status()
def probe():
from services.assistant_model import configuration
try:
config=configuration()
if not config.get('available'): raise ValueError('not available')
with httpx.Client(trust_env=False,follow_redirects=False,timeout=20) as client:
response=client.post(config['endpoint'].rstrip('/')+'/chat/completions',headers={'Authorization':'Bearer '+api_key()},json={'model':config['name'],'messages':[{'role':'user','content':'Reply with OK.'}],'max_tokens':32,'temperature':0})
response.raise_for_status()
body=response.json()
if not body.get('choices') or not isinstance(body['choices'][0].get('message'),dict):raise ValueError('invalid response')
message=body['choices'][0]['message']
if not (message.get('content') or message.get('reasoning_content')):raise ValueError('empty response')
return {'ok':True,'message':'连接通过;未发送业务资料,尚不代表提炼质量或概率校准通过'}
except Exception:
return {'ok':False,'code':'MODEL_CONNECTION_FAILED','message':'连接未完成,请检查模型服务、名称和密钥'}