"""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':'连接未完成,请检查模型服务、名称和密钥'}