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.json | JSON 字符串数组;彼此独立,不能复制一份冒充另一份 |
| 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. 菜单验收、故障处理和交接
- 两个缓存均须是非空、去重的字符串数组,分别等于本次相应 provider 的
models;server_models、derived_models和检查时间在脱敏快照中可追溯。失败不得把列表清空。 - 在新进程/新会话检查相应 profile 的两个 provider 是否注册、菜单能否显示 ID。外层缓存及在运行进程的
fallback_models可能仍旧;先观察其原生过期/刷新行为。clear_provider_models_cache(provider)只清对应 provider 的外层磁盘缓存,不能清除另一个长期进程的内存。 hermes model --refresh是交互式默认模型选择器,会清全局菜单缓存并重新发现多个 provider;不要当成定向、只读刷新命令。没有通用的hermes models list --provider ...命令可在此代替上述流程。- 需要验证账号切换时,在用户指定会话使用
/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;后续修改片段应重新验证。文档编写不构成新的在线目录、额度或真实模型调用证据。