fix(models): skip Claude credentials sources with a non-numeric expiresAt (#5591)

`_extract_claude_code_credential` copied `expiresAt` straight into
`ClaudeCodeCredential.expires_at`, so a credentials file whose `expiresAt` is a
string, null, list or object reached `is_expired` and raised
`TypeError: '<=' not supported between instances of 'str' and 'int'`. That
aborted the whole lookup instead of skipping the malformed source and moving on
down the documented order, the way the rest of the loader already behaves for a
malformed `claudeAiOauth` container.

Validate the field the way the sibling branches validate their input: log a
debug line and skip the source so the next candidate is tried.
This commit is contained in:
哈基米 2026-09-20 14:46:12 +08:00 committed by GitHub
parent 492e2ac2cc
commit 5051709343
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 62 additions and 1 deletions

View File

@ -160,10 +160,15 @@ def _extract_claude_code_credential(data: dict[str, Any], source: str) -> Claude
logger.debug("Claude Code credentials container exists but no accessToken found")
return None
expires_at = oauth.get("expiresAt", 0)
if not isinstance(expires_at, (int, float)):
logger.debug("Claude Code credentials source %s has a non-numeric expiresAt; skipping", source)
return None
cred = ClaudeCodeCredential(
access_token=access_token,
refresh_token=oauth.get("refreshToken", ""),
expires_at=oauth.get("expiresAt", 0),
expires_at=expires_at,
source=source,
)

View File

@ -300,6 +300,62 @@ def test_load_claude_code_credential_falls_back_to_default_when_override_contain
assert cred.source == "claude-cli-file"
@pytest.mark.parametrize(
"expires_at",
[
"1773430695128",
None,
[],
{},
],
)
def test_load_claude_code_credential_ignores_non_numeric_expires_at(tmp_path, monkeypatch, expires_at):
_clear_claude_code_env(monkeypatch)
monkeypatch.setenv("HOME", str(tmp_path))
cred_file = tmp_path / "credentials.json"
cred_file.write_text(
json.dumps({"claudeAiOauth": {"accessToken": "sk-ant-oat01-test", "expiresAt": expires_at}}),
encoding="utf-8",
)
monkeypatch.setenv("CLAUDE_CODE_CREDENTIALS_PATH", str(cred_file))
assert load_claude_code_credential() is None
def test_load_claude_code_credential_falls_back_to_default_when_override_expires_at_is_non_numeric(tmp_path, monkeypatch):
_clear_claude_code_env(monkeypatch)
monkeypatch.setenv("HOME", str(tmp_path))
override_path = tmp_path / "credentials.json"
override_path.write_text(
json.dumps({"claudeAiOauth": {"accessToken": "sk-ant-oat01-override", "expiresAt": "1773430695128"}}),
encoding="utf-8",
)
monkeypatch.setenv("CLAUDE_CODE_CREDENTIALS_PATH", str(override_path))
default_path = tmp_path / ".claude" / ".credentials.json"
default_path.parent.mkdir()
default_path.write_text(
json.dumps(
{
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-default",
"refreshToken": "sk-ant-ort01-default",
"expiresAt": 4_102_444_800_000,
}
}
),
encoding="utf-8",
)
cred = load_claude_code_credential()
assert cred is not None
assert cred.access_token == "sk-ant-oat01-default"
assert cred.refresh_token == "sk-ant-ort01-default"
assert cred.source == "claude-cli-file"
def test_load_codex_cli_credential_supports_nested_tokens_shape(tmp_path, monkeypatch):
auth_path = tmp_path / "auth.json"
auth_path.write_text(