Hermes 双 ChatGPT 账号模型列表:制作与更新指导

核验日期:2026-10-17。适用于本机 ATM 审查镜像与 Hermes chatgpt-accounts 插件;升级后先核对接口。本文提供操作方法,不宣称任何账号、模型或额度当前可用。

TO AGENT PROMPT

读取 a 库的 AGENTS.md、AGENT.md 和本文。为 Hermes 制作或更新 chatgpt-cn、chatgpt-com 两份独立模型列表。沿用 live 插件旁已有的私有 accounts.json,不得从别名猜邮箱、互换账号、复制 auth 或跨账号兜底。使用现有 Hermes Python 环境和原生目录解析函数;先生成候选和哈希计划,再在授权范围内应用缓存。查询失败保留旧列表并标记陈旧,不能拿通用 Codex 清单补齐。只查询模型目录,不调用模型、不强制刷新令牌、不改默认模型、不发送消息。最后报告每个账号的目录证据、增删项、缓存写入与运行界面验收,分清未执行的部分。原始凭据、账号证明、响应和私有映射不进入对话、Git 或 a 库;输出按下文三套规则检查。

1. 列表到底来自哪里

对象本机位置或来源用途与边界
插件审查镜像$HOME/dev/atm/integrations/hermes/chatgpt-accounts/代码、插件声明、虚构映射示例;不是现用账号配置
live 插件$HERMES_HOME/plugins/model-providers/chatgpt-accounts/当前 profile 实际加载的代码;私有 accounts.json 固定两个账号身份
账号目录原生 agent.model_metadata.fetch_codex_catalog_entries使用各账号自己的有效 bearer、账号/workspace header,向官方 Codex 目录做 GET
每账号菜单缓存$HERMES_HOME/cache/chatgpt-cn-models.json、chatgpt-com-models.jsonJSON 字符串数组;彼此独立,不能复制一份冒充另一份
Hermes 外层缓存$HERMES_HOME/provider_models_cache.json另有 TTL 和进程内状态;写内层缓存不等于运行菜单已更新
ATM catalog.json审查镜像目录下的同名文件人工维护的脱敏快照;当前代码未把它作为运行列表入口,单改此文件不会更新菜单
公共免费目录$HOME/.config/token-savings/model-filter.json公共免费/本地模型用途;双 ChatGPT 是订阅登录入口,不写成零价 API

cn 与 com 是用户指定的两个独立账号,真实映射仅留在私有文件。命名 profile 应借用既有 root 权威认证池,不新建相同 OAuth 登录链的副本。不要复制 auth.json、Codex 登录缓存或 .env 来“修好模型列表”。

记录两种列表: server_models 是当前账号目录返回、按原生规则筛选可见性并排序的 ID;models 是 Hermes 原生 _finalize_codex_models(server_models) 处理后的菜单 ID。后者可能补充兼容名称和 -900k 上下文选项,差集记为 derived_models。-900k 是 Hermes 选项,不能当作另一款服务端模型。不要自行拼新型号,也不要用 supported_in_api=false 排除 Codex 订阅模型。

2026-10-26 排障补充:若服务端报 gpt-6.1-sol-900k 不受支持,先核对当前进程实际源码中的 agent.model_metadata eligibility 和 strip_codex_context_variant_suffix,以及 transport 最终请求。Soma 曾回到旧源码,菜单中保留的别名因此被原样上送;补齐映射后已验证 SDK 发送基础 ID gpt-6.1-sol。此项映射验证不证明 900k 大输入或当前账号真实推理已通过,证据见 fork 管理。

同日后续出现 Provider fallback: chatgpt-com/gpt-6.1-sol-900k unavailable:先区分模型请求失败与请求前的账号池选择失败。07:45 的错误别名请求曾给该账号的三条 credential row 写入一年期 model_cooldowns,修正 transport 后这些负缓存仍会排除该选项。此次仅在核对错误时间、固定账号和实际别名修复后,持权威认证库锁清除对应三项;未清除其他模型、额度、失效状态或更换凭据。不可把清理所有冷却当作通用修复。共享 gateway 的 session override 失败路径现也遵守 provider 的 disable_runtime_fallback,避免绑定账号不可用时擅自转向默认 provider。相关离线测试与加载证据见 fork 管理。

目录 GET 成功只证明本次目录可见;登录元数据、订阅资格、剩余额度和真实推理成功须分别记录。禁止用 ChatGPT 网页模型名、网上发布新闻或另一个账号的结果证明本账号支持某 ID。

2. 前置检查与路径

以下命令在本机 Bash 执行。其他设备先定位 Hermes checkout、Python、ATM 仓库及实际 profile;缺少插件或私有映射时停止,不从本文生成真实映射。不要向生产 Hermes venv 安装依赖。

export ATM_REPO="$HOME/dev/atm"
export HERMES_SOURCE="$HOME/.hermes/hermes-agent"
export HERMES_HOME="$HOME/.hermes/profiles/soma"
export PYTHONPATH="$HERMES_SOURCE"
test -d "$HERMES_HOME" && test -x "$HERMES_SOURCE/venv/bin/python"
test -r "$HERMES_HOME/plugins/model-providers/chatgpt-accounts/__init__.py"

soma 只是目标 profile 示例;要维护 Alaya 等实例,先核对该实例的实际 home 和插件,分别执行。别把 home 指向 root 后误写其他 profile,也别顺手修改 service。默认 hermes-gateway.service 停用、Alaya 保留是本会话的正常基线。

需要登录元数据时,原生命令为 hermes -p soma auth status chatgpt-cn / chatgpt-com。只保留 provider、configured、logged_in、needs_refresh、accounts、credential_chains 等允许字段;不公开账号证明或身份信息。logged_in=true 不证明服务端接受 bearer。不要为生成列表执行 auth refresh。

3. 生成两份候选:不写认证或运行缓存

下面的完整片段只读取私有映射和凭据于本地进程中;不输出它们,不调用 select()、peek()、fetch_models() 或刷新接口。它从既有池读取状态正常且未临近过期的登录链,复用原生请求身份 header、目录端点选择及模型解析;401/403/429 立即停止。候选和计划放在仓库外的私有运行目录。

fetch_models() 是正常交互菜单使用的原生插件入口,但内部 select() 可能触发 OAuth 轮换;持续巡检或仅整理列表时不要拿它当无副作用的读取方法。

"$HERMES_SOURCE/venv/bin/python" - <<'PY'
import contextlib, hashlib, importlib.util, io, json, logging, os, re, time
from datetime import datetime, timezone
from pathlib import Path
 
os.umask(0o077)
logging.disable(logging.CRITICAL)
class Stop(Exception):
    pass
def require(ok, category):
    if not ok:
        raise Stop(category)
def digest(path):
    require(not path.is_symlink(), 'SYMLINK_REQUIRES_REVIEW')
    return hashlib.sha256(path.read_bytes()).hexdigest() if path.exists() else None
def run():
    home = Path(os.environ['HERMES_HOME']).resolve()
    repo = Path(os.environ['ATM_REPO']).resolve()
    live = home / 'plugins/model-providers/chatgpt-accounts'
    mirror = repo / 'integrations/hermes/chatgpt-accounts'
    manifest = json.loads((repo / 'integrations/manifest.json').read_text())
    checked = {}
    for name in ('__init__.py', 'plugin.yaml'):
        source = 'integrations/hermes/chatgpt-accounts/' + name
        expected = next(r['sha256'] for r in manifest['files'] if r['source'] == source)
        require(digest(mirror / name) == expected == digest(live / name), 'PLUGIN_HASH_DRIFT')
        checked[str(live / name)] = expected
    mapping = live / 'accounts.json'
    require(mapping.stat().st_mode & 0o777 == 0o600, 'PRIVATE_MAPPING_PERMISSIONS')
    checked[str(mapping)] = digest(mapping)
    spec = importlib.util.spec_from_file_location('chatgpt_catalog_guide', live / '__init__.py')
    plugin = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(plugin)
    accounts = plugin.configured_accounts()
    aliases = ('chatgpt-cn', 'chatgpt-com')
    require(all(a in accounts for a in aliases), 'MISSING_PINNED_MAPPING')
    require(len({accounts[a].strip().casefold() for a in aliases}) == 2, 'DUPLICATE_ACCOUNT_MAPPING')
    from agent.codex_headers import codex_cloudflare_headers, is_official_codex_base_url
    from agent.model_metadata import fetch_codex_catalog_entries
    from hermes_cli.codex_models import _ranked_slugs, _finalize_codex_models
    import httpx
    targets = {a: digest(home / 'cache' / (a + '-models.json')) for a in aliases}
    routes = []
    for alias in aliases:
        entries = plugin.account_pool(accounts[alias], alias).entries()
        entry = next((e for e in entries if e.last_status in (None, 'ok')
                      and plugin.token_expiry(e.access_token) > time.time() + 120), None)
        require(entry is not None, alias + ': NO_READY_CREDENTIAL')
        require(plugin.email_of(entry.access_token) == accounts[alias], alias + ': ACCOUNT_MISMATCH')
        headers = {'Authorization': 'Bearer ' + entry.access_token,
                   **codex_cloudflare_headers(entry.access_token)}
        require(bool(headers.get('ChatGPT-Account-ID')), alias + ': MISSING_WORKSPACE_HEADER')
        with httpx.Client(timeout=10, follow_redirects=False) as client:
            def get(url):
                require(is_official_codex_base_url(url), 'UNEXPECTED_ENDPOINT')
                response = client.get(url, headers=headers)
                if response.status_code in (401, 403, 429):
                    raise Stop(alias + ': HTTP_' + str(response.status_code))
                return response
            rows, status = fetch_codex_catalog_entries(get)
        server = _ranked_slugs(rows)
        require(status == 200 and bool(server), alias + ': EMPTY_OR_FAILED_CATALOG')
        models = _finalize_codex_models(server)
        require(all(re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}', m)
                    for m in models), 'UNEXPECTED_MODEL_ID_FORMAT')
        routes.append(dict(provider=alias, cost='subscription', source='account-scoped catalog GET',
                           server_models=server, models=models,
                           derived_models=[m for m in models if m not in server],
                           quota_checked=False, inference_verified=False))
    stamp = datetime.now(timezone.utc)
    output = Path.home() / 'var/atm/chatgpt-model-lists' / (stamp.strftime('%Y%m%dT%H%M%SZ') + '-' + str(os.getpid()))
    output.mkdir(parents=True, mode=0o700, exist_ok=False)
    catalog = dict(kind='subscription-account-routes', public_free_api=False, auth_copied=False,
                   checked_at=stamp.isoformat(), routes=routes)
    catalog_text = json.dumps(catalog, ensure_ascii=False, indent=2) + '\n'
    plan = dict(home=str(home), inputs=checked, targets=targets, created_at=time.time(),
                catalog_sha256=hashlib.sha256(catalog_text.encode()).hexdigest())
    for name, value in (('catalog.json', catalog), ('plan.json', plan)):
        (output / name).write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n')
    return output
 
try:
    with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
        output = run()
except Stop as exc:
    print('STOP:', str(exc))  # 仅本片段定义的安全类别
    raise SystemExit(1)
except Exception as exc:
    print('STOP:', type(exc).__name__)  # 不打印异常正文、响应或堆栈
    raise SystemExit(1)
print('CANDIDATES_READY:', output)
PY

检查候选 catalog.json 中每个 provider 的 ID、原生派生差集和时间,与同 provider 的旧缓存比较新增/移除项。两个账号恰好得到相同 ID 是可能的,但必须各自查询成功。任一账号失败,本次不进入应用阶段;旧文件和旧时间保留,注明 stale。响应正文、header、异常详情不要回显;上面的候选文件只留下允许的目录字段。

4. 应用到目标 profile

只有当前任务已授权更新列表时执行;用户只要求研究或文档时止于候选。将 ATM_MODEL_STAGE 设为上一阶段输出目录。下面先核对计划年龄、插件/映射及两个缓存的哈希漂移,再备份模型缓存,逐文件原子替换;不会备份、恢复或写入令牌。

export ATM_MODEL_STAGE='/absolute/path/printed/as/CANDIDATES_READY'
"$HERMES_SOURCE/venv/bin/python" - <<'PY'
import hashlib, json, os, tempfile, time
from pathlib import Path
 
os.umask(0o077)
def digest(path):
    if path.is_symlink():
        raise RuntimeError('SYMLINK')
    return hashlib.sha256(path.read_bytes()).hexdigest() if path.exists() else None
def require(ok):
    if not ok:
        raise RuntimeError('PLAN_OR_HASH_CHECK_FAILED')
try:
    stage = Path(os.environ['ATM_MODEL_STAGE'])
    plan = json.loads((stage / 'plan.json').read_text())
    catalog = json.loads((stage / 'catalog.json').read_text())
    home = Path(os.environ['HERMES_HOME']).resolve()
    require(str(home) == plan['home'] and 0 <= time.time() - plan['created_at'] <= 3600)
    require(digest(stage / 'catalog.json') == plan['catalog_sha256'])
    require(len(catalog['routes']) == 2
            and {r['provider'] for r in catalog['routes']} == {'chatgpt-cn', 'chatgpt-com'})
    require(set(plan['targets']) == {'chatgpt-cn', 'chatgpt-com'})
    for name, expected in plan['inputs'].items():
        require(digest(Path(name)) == expected)
    for alias, expected in plan['targets'].items():
        require(digest(home / 'cache' / (alias + '-models.json')) == expected)
    for route in catalog['routes']:
        alias, models = route['provider'], route['models']
        require(models and all(isinstance(m, str) and m for m in models))
        path = home / 'cache' / (alias + '-models.json')
        if path.exists():
            with (stage / (alias + '-models.before.json')).open('xb') as backup:
                backup.write(path.read_bytes())
    for route in catalog['routes']:
        path = home / 'cache' / (route['provider'] + '-models.json')
        path.parent.mkdir(parents=True, exist_ok=True)
        payload = (json.dumps(route['models'], ensure_ascii=False) + '\n').encode()
        fd, temporary = tempfile.mkstemp(dir=path.parent, prefix='.models-')
        try:
            with os.fdopen(fd, 'wb') as stream:
                stream.write(payload)
                stream.flush()
                os.fsync(stream.fileno())
            os.replace(temporary, path)
            require(path.read_bytes() == payload)
        finally:
            if os.path.exists(temporary):
                os.unlink(temporary)
except Exception as exc:
    print('APPLY_STOP:', type(exc).__name__, '(inspect local state; may be partially applied)')
    raise SystemExit(1)
print('CACHE_WRITE_VERIFIED: chatgpt-cn, chatgpt-com')
PY

两个文件不构成跨文件事务;应用中断时先核对实际写入项,再依据私有备份和当前哈希决定恢复哪个缓存,不盲目重跑。若有其他进程同时刷新目录,停止竞争后重新预览;上述哈希检查不等于进程间写锁。

要同步 ATM 审查快照,只把检查后的候选目录字段合入 $ATM_REPO/integrations/hermes/chatgpt-accounts/catalog.json;当前快照的旧 source 字段不能独自证明每项都是服务端返回。不要把 plan.json、备份或私有映射提交进去。只更新目录数据通常不需要改插件代码或 manifest;若确需改代码,按 ATM 的变更计划、隔离测试、manifest 哈希和运行重载流程另行验收。

5. 菜单验收、故障处理和交接

  1. 两个缓存均须是非空、去重的字符串数组,分别等于本次相应 provider 的 models;server_models、derived_models 和检查时间在脱敏快照中可追溯。失败不得把列表清空。
  2. 在新进程/新会话检查相应 profile 的两个 provider 是否注册、菜单能否显示 ID。外层缓存及在运行进程的 fallback_models 可能仍旧;先观察其原生过期/刷新行为。clear_provider_models_cache(provider) 只清对应 provider 的外层磁盘缓存,不能清除另一个长期进程的内存。
  3. hermes model --refresh 是交互式默认模型选择器,会清全局菜单缓存并重新发现多个 provider;不要当成定向、只读刷新命令。没有通用的 hermes models list --provider ... 命令可在此代替上述流程。
  4. 需要验证账号切换时,在用户指定会话使用 /model <本账号目录中的ID> --provider chatgpt-cn 或 chatgpt-com。这属于会话操作;整理列表不自动执行。菜单可见和缓存写入完成都不等于已有网关重载或真实推理通过。仅改目录不顺带重启网关;确需重载时单独核对服务、授权及运行进程。
安全类别下一步
PLUGIN_HASH_DRIFT / 接口导入失败比较 live、ATM 镜像与 manifest,隔离审查实际版本;不覆盖已有修改、不盲目升级
MISSING_PINNED_MAPPING / 重复映射 / 权限异常本地核对既有私有映射;缺失信息请用户确定。别把真实邮箱填进本文或示例配置
NO_READY_CREDENTIAL可能过期、冷却或已隔离;先做元数据诊断,不能直接断言必须重登,更不能靠借用另一账号来通过
HTTP 401保留旧目录,交给同账号认证恢复流程;不要为目录查询反复刷新。明确不可恢复时仅对该别名重新登录
HTTP 403 / 429 / 网络异常分别诊断权限/区域、限额或网络;保留旧目录及时间,不自动改代理、不改模型、不付费兜底
HTTP 200 但空目录检查原生账号/workspace header、目录版本兼容与返回类别;空结果不证明账号没有模型
缓存漂移 / 计划超时重新制作候选并审查差异,不用旧计划覆盖新状态

确需重新登录且任务已授权时,用 hermes -p soma auth add chatgpt-cn --type oauth 或对应的 chatgpt-com;确认浏览器选择正确账号,插件核对身份后才保存。不要直接登录通用 openai-codex 来绕过固定映射。OAuth 刷新成功后不能恢复旧 refresh token 备份。详细恢复依据在 $ATM_REPO/docs/chatgpt-auth-recovery.md,先读最新记录。

如修改了账号插件逻辑,使用虚构凭据的现有离线测试:

cd "$ATM_REPO"
"$HERMES_SOURCE/venv/bin/python" -m unittest discover \
  -s tests/integrations -p test_chatgpt_accounts.py -q

只改说明或模型快照时做 JSON/链接/内容检查即可;不要为验收列表发起真实模型请求。交接只写:目标 profile、两账号各自目录检查时间与状态、新增/移除/派生 ID、缓存是否写入、运行菜单是否验收、推理与额度是否另有证据、失败或待办。运行候选在 ~/var/atm/chatgpt-model-lists/;a 库只保留吸收后的说明和脱敏结论。

分享本文、快照或诊断摘要前,分别使用 privacy-redaction、nsfw-output-guard、sensitive-words-guard。只报告安全类别与数量;命中详情留在本地,规则缺失或检查失败不能声称通过。

本文的验证范围

本文依据已安装 Hermes 的 providers、hermes_cli/codex_models.py、agent/model_metadata.py、agent/codex_headers.py 与 ATM 插件镜像编写。2026-10-17 已检查全部 Bash/Python 片段语法,并使用虚构账号、实际插件和原生解析函数、模拟 HTTP 通过 10 个离线场景:双账号隔离及缓存应用、401、空目录、重复映射、令牌过期、插件漂移、缓存漂移、映射漂移、候选修改、计划超时。测试禁止网络连接、认证保存及凭据选择/刷新入口;确认虚构认证文件未变、失败保留缓存、成功保留旧缓存备份。验证脚本留在本机仓库外 ~/var/atm/guide-validation/test_guide.py;后续修改片段应重新验证。文档编写不构成新的在线目录、额度或真实模型调用证据。