feat: per-task model/effort/approval + relay models endpoint (#59)

Co-authored-by: host <host@test.local>
This commit is contained in:
openqjandhost authored and GitHub committed 2026-10-27 01:44:53 +08:00
1 parent cff152c5a2
commit 8b2f3968dc
9 files changed
+521 -16

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
"""从中转站(OpenAI 兼容 /v1/models)取可用模型列表。
只返回模型 id,不返回、不记录任何密钥:key 只进请求头,
绝不进返回值、日志或缓存。列表缓存 1 小时(中转模型目录
很少变),中转挂了时调用方拿到缓存;无缓存又取不到时报
RelayError,由端点翻成 502。
base_url:环境变量 XCM_RELAY_BASE_URL,缺省
https://xcmapi.org/v1(与 codex-home 里 relay 配置的
base_url 一致)。key:环境变量 XCM_API_KEY(与
config.xcmapi.example.toml 的 env_key 一致)。
"""
import json
import os
import time
import urllib.error
import urllib.request
DEFAULT_BASE_URL = "https://xcmapi.org/v1"
CACHE_TTL_SECONDS = 3600
class RelayError(RuntimeError):
pass
_cache = {"models": None, "updated_at": 0.0}
def _default_fetcher(url, headers, timeout):
req = urllib.request.Request(url, headers=headers, method="GET")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
raise RelayError("relay models HTTP %s" % exc.code)
except (urllib.error.URLError, ValueError, TimeoutError) as exc:
raise RelayError("relay models fetch failed: %s" % exc)
def get_relay_models(fetcher=None, now=None):
"""返回 (models, cached, updated_at):models 为 id 字符串列表。
fetcher 可注入(单测用):签名 fetcher(url, headers, timeout)
-> 已解析的 JSON dict。缓存命中时不调 fetcher。
"""
now = time.time() if now is None else now
if (_cache["models"] is not None
and now - _cache["updated_at"] < CACHE_TTL_SECONDS):
return list(_cache["models"]), True, _cache["updated_at"]
base_url = os.environ.get("XCM_RELAY_BASE_URL") or DEFAULT_BASE_URL
url = base_url.rstrip("/") + "/models"
if fetcher is not None:
# 单测注入:不带 key、不碰环境,生产路径仍走下面强制 key。
data = fetcher(url, {}, 10)
else:
api_key = os.environ.get("XCM_API_KEY")
if not api_key:
raise RelayError("relay key not configured")
data = _default_fetcher(
url, {"Authorization": "Bearer " + api_key}, 10)
if not isinstance(data, dict) or not isinstance(data.get("data"), list):
raise RelayError("relay models unexpected response")
models = []
for item in data["data"]:
model_id = item.get("id") if isinstance(item, dict) else None
if isinstance(model_id, str) and model_id and model_id not in models:
models.append(model_id)
_cache["models"] = list(models)
_cache["updated_at"] = now
return list(models), False, now
def _reset_cache_for_tests():
_cache["models"] = None
_cache["updated_at"] = 0.0