如题所示,CLI Proxy API,也就是 CPA,它是支持导出已登录的 Codex 凭据的,我们只要稍作调整即可适配到 OpenCode 上。
神秘代码:
"""Convert CPA/CLIProxyAPI Codex OAuth credentials to opencode auth.json."""
from __future__ import annotations
import json
import os
import shutil
import sys
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import unquote, urlparse
DEFAULT_PROVIDER_ID = "openai"
REQUIRED_FIELDS = ("access_token", "refresh_token", "expired", "account_id")
def mask_token(value: object) -> str:
if not isinstance(value, str) or not value:
return "<missing>"
if len(value) <= 12:
return "******"
return f"{value[:6]}...{value[-4:]}"
def normalize_path(raw: str) -> Path:
text = raw.strip()
if text.startswith("& "):
text = text[2:].strip()
if (text.startswith('"') and text.endswith('"')) or (
text.startswith("'") and text.endswith("'")
):
text = text[1:-1].strip()
if text.lower().startswith("file:"):
parsed = urlparse(text)
text = unquote(parsed.path or "")
if os.name == "nt" and len(text) >= 3 and text[0] == "/" and text[2] == ":":
text = text[1:]
text = os.path.expandvars(os.path.expanduser(text))
return Path(text).resolve()
def read_json(path: Path) -> dict:
with path.open("r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
raise ValueError("JSON 顶层必须是对象。")
return data
def prompt_credential_path() -> Path:
while True:
raw = input("请输入 CPA/Codex 凭据 JSON 路径: ").strip()
if not raw:
print("路径不能为空。")
continue
path = normalize_path(raw)
if not path.is_file():
print(f"未找到文件: {path}")
continue
if path.suffix.lower() != ".json":
print("请输入 .json 文件。")
continue
return path
def parse_expired_to_ms(value: object) -> tuple[int, str]:
if isinstance(value, (int, float)):
number = int(value)
expires_ms = number if number > 10_000_000_000 else number * 1000
dt = datetime.fromtimestamp(expires_ms / 1000, tz=timezone.utc).astimezone()
return expires_ms, dt.strftime("%Y-%m-%d %H:%M:%S %z")
if not isinstance(value, str) or not value.strip():
raise ValueError("expired 字段不能为空。")
text = value.strip()
candidates = []
iso_text = text[:-1] + "+00:00" if text.endswith("Z") else text
try:
candidates.append(datetime.fromisoformat(iso_text))
except ValueError:
pass
formats = (
"%Y-%m-%dT%H:%M:%S%z",
"%Y-%m-%d %H:%M:%S%z",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%d %H:%M:%S",
"%Y/%m/%d %H:%M:%S",
"%m/%d/%Y %H:%M:%S",
)
for fmt in formats:
try:
candidates.append(datetime.strptime(text, fmt))
except ValueError:
continue
if not candidates:
raise ValueError(f"无法解析 expired 时间: {text}")
dt = candidates[0]
expires_ms = int(dt.timestamp() * 1000)
display_dt = dt.astimezone() if dt.tzinfo else datetime.fromtimestamp(dt.timestamp()).astimezone()
return expires_ms, display_dt.strftime("%Y-%m-%d %H:%M:%S %z")
def ask_yes_no(prompt: str, default: bool = False) -> bool:
suffix = "[Y/n]" if default else "[y/N]"
while True:
answer = input(f"{prompt} {suffix}: ").strip().lower()
if not answer:
return default
if answer in {"y", "yes"}:
return True
if answer in {"n", "no"}:
return False
print("请输入 y 或 n。")
def default_auth_candidates() -> list[Path]:
candidates: list[Path] = []
local_app_data = os.environ.get("LOCALAPPDATA")
user_profile = os.environ.get("USERPROFILE")
if local_app_data:
candidates.append(Path(local_app_data) / "opencode" / "auth.json")
if user_profile:
candidates.append(Path(user_profile) / ".local" / "share" / "opencode" / "auth.json")
return candidates
def prompt_auth_path() -> Path:
while True:
raw = input("未找到默认 opencode 目录,请输入 auth.json 路径或目标目录: ").strip()
if not raw:
print("路径不能为空。")
continue
path = normalize_path(raw)
if path.exists() and path.is_dir():
return path / "auth.json"
if path.suffix.lower() == ".json":
target = path
target_dir = path.parent
else:
target_dir = path
target = target_dir / "auth.json"
if target_dir.exists():
return target
if ask_yes_no(f"目标目录不存在,是否创建?{target_dir}"):
target_dir.mkdir(parents=True, exist_ok=True)
return target
print("已取消使用该路径,请重新输入。")
def resolve_auth_path() -> Path:
candidates = default_auth_candidates()
for candidate in candidates:
if candidate.is_file():
return candidate
for candidate in candidates:
if candidate.parent.is_dir():
return candidate
return prompt_auth_path()
def load_auth(path: Path) -> dict:
if not path.exists():
return {}
data = read_json(path)
return data
def backup_auth(path: Path) -> Path | None:
if not path.exists():
return None
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
backup_path = path.with_name(f"{path.name}.bak-{timestamp}")
shutil.copy2(path, backup_path)
return backup_path
def write_auth(path: Path, auth_data: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8", newline="\n") as f:
json.dump(auth_data, f, ensure_ascii=False, indent=2)
f.write("\n")
def validate_credential(data: dict) -> None:
missing = [field for field in REQUIRED_FIELDS if not data.get(field)]
if missing:
raise ValueError("凭据 JSON 缺少必要字段: " + ", ".join(missing))
def main() -> int:
print("Codex OAuth -> opencode auth.json 转换工具")
print("安全提示: 本脚本只读取/写入本机文件,不会联网;token 只打码显示。")
credential_path = prompt_credential_path()
try:
credential = read_json(credential_path)
validate_credential(credential)
expires_ms, expires_human = parse_expired_to_ms(credential["expired"])
except Exception as exc:
print(f"读取或解析凭据失败: {exc}")
return 1
provider_id = input(f"请输入 provider ID,直接回车默认 {DEFAULT_PROVIDER_ID}: ").strip()
if not provider_id:
provider_id = DEFAULT_PROVIDER_ID
auth_path = resolve_auth_path()
print("\n即将写入:")
print(f"凭据文件: {credential_path}")
print(f"auth.json: {auth_path}")
print(f"provider ID: {provider_id}")
print(f"access_token: {mask_token(credential.get('access_token'))}")
print(f"refresh_token: {mask_token(credential.get('refresh_token'))}")
print(f"expires: {expires_human} ({expires_ms})")
print(f"account_id: {credential.get('account_id')}")
if credential.get("id_token"):
print("id_token: 已忽略")
if credential.get("email"):
print(f"email: {credential.get('email')}")
if not ask_yes_no("确认写入?"):
print("已取消,未写入任何文件。")
return 0
try:
auth_data = load_auth(auth_path)
backup_path = backup_auth(auth_path)
auth_data[provider_id] = {
"type": "oauth",
"refresh": credential["refresh_token"],
"access": credential["access_token"],
"expires": expires_ms,
"accountId": credential["account_id"],
}
write_auth(auth_path, auth_data)
except Exception as exc:
print(f"写入失败: {exc}")
return 1
print("\n写入完成:")
print(f"auth.json 路径: {auth_path}")
print(f"provider ID: {provider_id}")
print(f"expires: {expires_human}")
if backup_path:
print(f"备份: 已创建 {backup_path}")
else:
print("备份: 未创建,原 auth.json 不存在")
return 0
if __name__ == "__main__":
sys.exit(main())
按照提示输入凭据文件位置就好。
[分享创造] AI Memory Hub 是一个企业级 AI 知识资产管理平台。它自动采集、整理、关联员工与 AI(ChatGPT、Claude、DeepSeek 等)的所有高质量对话,将其转化为可检索、可关联、可复用的结构化知识资产,实现
[生活] 和 Gemini 聊了四个小时,现在感觉在大学去上课就是浪费时间
然后你的 C:\Users\用户名\.local\share\opencode\auth.json 大概长这样:
{
"openai": {
"type": "oauth",
"refresh": "",
"access": "",
"expires": 1780837337000,
"accountId": ""
}
}
这个时候再 opencode,/models,就会发现那个 OpenAI 官方标志的 GPT 5.5 了:
顺带一提,这是直接请求了 OpenAI Codex 官方的后端地址(Cloudflare CDN) https://chatgpt.com/backend-api/codex/responses:
2 个帖子 - 2 位参与者
