Skip to content

fix(models): tolerate a null Codex account_id before it reaches the request header - #5601

Merged
WillemJiang merged 1 commit into
bytedance:mainfrom
BlueX888:fix/prep-codex-auth-null-account-id-typeerror
Sep 20, 2026
Merged

WillemJiang merged 1 commit into
bytedance:mainfrom
BlueX888:fix/prep-codex-auth-null-account-id-typeerror

Conversation

@BlueX888

Copy link
Copy Markdown
Contributor

Why

I hit this while wiring a Codex CLI credential through CODEX_AUTH_PATH on this checkout: from a JSON-encoded-twice handoff, ~/.codex/auth.json carried "account_id": null, and constructing the model aborted with a raw TypeError instead of either loading or degrading.

load_codex_cli_credential() reads account_id with or-chaining (backend/packages/harness/deerflow/models/credential_loader.py:238 on main, 03505ac4e):

account_id = data.get("account_id") or tokens.get("account_id", "")

The "" default on tokens.get only covers a missing key. A key that is present but JSON null is falsy, so the value falls through to the default of the next link, not to "" — the expression evaluates to "" or None → None, welded into CodexCliCredential(account_id=None) (credential_loader.py:68). The sibling "missing key" test in the same file asserts cred.account_id == "" for a file with no account_id at all, so None is the one shape this field was never supposed to hold.

None survives the loader and only detonates downstream, because CodexChatModel.model_post_init assumes a non-empty string (backend/packages/harness/deerflow/models/openai_codex_provider.py:99):

logger.info(f"Using Codex CLI credential (account: {self._account_id[:8]}...)")
TypeError: 'NoneType' object is not subscriptable

So a credentials file whose account is unknown makes every CodexChatModel construction raise, rather than load and run unaccounted. (A numeric account_id — also plausible from a hand-edited or hand-generated file — is a second failure mode on the same line: httpx.Request(headers={"ChatGPT-Account-ID": 12345}) raises TypeError: Header value must be str or bytes, not <class 'int'>, so an int gets past model_post_init intact and dies at request time instead.)

This is the same class of malformed-content robustness that has already been merged into this exact function and its sibling: #5494 / #5584 guard the non-object file and container cases and add CodexCliCredential's account_id default for a missing key. Note that #5584 (2b8c6a970) is already on main and does not cover this path — after it merged, a present-but-null account_id still produces None. #5552 concerns the non-object top-level shape only, and is not fixed or superseded here.

What changed

account_id can no longer leave the loader as anything but a string. A present-but-null value falls back to the field's documented unknown-account value "", and any other non-string (int, float, bool from JSON) is coerced to its string form rather than being handed to a header map.

account_id = data.get("account_id") or tokens.get("account_id") or ""
if not isinstance(account_id, str):
    account_id = str(account_id)

Every string account_id — including "" for a file with no account_id at all — takes exactly the same path as before; nothing is reordered or re-parsed.

Surface area

None of the listed boxes apply. The change is confined to the harness credential loader (backend/packages/harness/deerflow/models/credential_loader.py), which is not an endpoint under backend/app, not an agent node/graph/prompt, not a sandbox/skills/dependency change, and does not alter a default.

  • Frontend UI
  • Backend API
  • Agents / LangGraph
  • Sandbox
  • Skills
  • Dependencies
  • Default behavior change
  • Docs / tests / CI only

Bug fix verification

  • Test paths that reproduce the bug:
    • backend/tests/test_credential_loader.py::test_load_codex_cli_credential_defaults_null_account_id
    • backend/tests/test_credential_loader.py::test_load_codex_cli_credential_stringifies_numeric_account_id
    • backend/tests/test_codex_provider.py::test_model_post_init_accepts_null_account_id
  • Did it go red on main and green on this branch? yes

Red on main 03505ac4e (tests applied, source reverted) — cd backend && PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run --locked pytest tests/test_credential_loader.py::test_load_codex_cli_credential_defaults_null_account_id tests/test_credential_loader.py::test_load_codex_cli_credential_stringifies_numeric_account_id tests/test_codex_provider.py::test_model_post_init_accepts_null_account_id -q:

FFF                                                                      [100%]
=================================== FAILURES ===================================
___________ test_load_codex_cli_credential_defaults_null_account_id ____________

        cred = load_codex_cli_credential()

        assert cred is not None
        assert cred.access_token == "codex-access-token"
>       assert cred.account_id == ""
E       AssertionError: assert None == ''
E        +  where None = CodexCliCredential(access_token='codex-access-token', account_id=None, source='codex-cli').account_id

tests/test_credential_loader.py:373: AssertionError
________ test_load_codex_cli_credential_stringifies_numeric_account_id _________

tests/test_credential_loader.py:394: AssertionError

_________________ test_model_post_init_accepts_null_account_id _________________

        from deerflow.models.openai_codex_provider import CodexChatModel

>       model = CodexChatModel(model="gpt-5.4", reasoning_effort="medium")
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

        cred = self._load_codex_auth()
        if cred:
            self._access_token = cred.access_token
            self._account_id = cred.account_id
>           logger.info(f"Using Codex CLI credential (account: {self._account_id[:8]}...)")
                                                                 ^^^^^^^^^^^^^^^^^^^^
E           TypeError: 'NoneType' object is not subscriptable

packages/harness/deerflow/models/openai_codex_provider.py:99: TypeError
=========================== short test summary info ============================
FAILED tests/test_credential_loader.py::test_load_codex_cli_credential_defaults_null_account_id
FAILED tests/test_credential_loader.py::test_load_codex_cli_credential_stringifies_numeric_account_id
FAILED tests/test_codex_provider.py::test_model_post_init_accepts_null_account_id
3 failed in 1.06s

Green on this branch, same command:

...                                                                      [100%]
3 passed in 1.51s

Validation

cd backend && uv run ruff check .
All checks passed!

cd backend && uv run ruff format --check .
1545 files already formatted

cd backend && PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run --locked pytest tests/test_credential_loader.py -q
25 passed in 2.51s

cd backend && PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run --locked pytest tests/test_credential_loader.py tests/test_codex_provider.py tests/test_claude_provider_oauth_billing.py tests/test_claude_provider_prompt_caching.py -q
74 passed in 1.36s

cd backend && PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run --locked pytest -m "not live" --ignore=tests/blocking_io tests/ -q
17554 passed, 199 skipped, 3 deselected, 32 warnings in 454.63s (0:07:34)

AI assistance

Tool(s) used: Claude Code

How you used it: Claude Code drafted the change, the regression tests and the commit message. I ran the red/green cycle by reverting the source fix and re-running the new tests on main, then on this branch, ran the full backend suite and ruff on the branch, and reviewed the final diff before opening this PR.

  • I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

load_codex_cli_credential copied tokens.account_id straight into CodexCliCredential.account_id with no type check. A Codex auth file whose account_id is JSON null therefore propagated None into CodexChatModel._account_id, and model_post_init raised TypeError: 'NoneType' object is not subscriptable on the account prefix in its log line. A numeric or boolean account_id slipped through the same way, reaching the ChatGPT-Account-ID header as a non-string.

Treat any non-string account_id as absent and fall back to the credential's empty-string default, matching how a missing account_id already behaves.
@github-actions github-actions Bot added area:backend Gateway / runtime / core backend under backend/ risk:medium Medium risk: regular code changes size/S PR changes 20-100 lines labels Sep 20, 2026
@WillemJiang
WillemJiang merged commit 4e8e2ce into bytedance:main Sep 20, 2026
16 checks passed
@WillemJiang WillemJiang added this to the 2.1.0 milestone Sep 20, 2026
@WillemJiang WillemJiang modified the milestones: 2.1.0, 2.2.0 Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:backend Gateway / runtime / core backend under backend/ risk:medium Medium risk: regular code changes size/S PR changes 20-100 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants