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