sdk/python: add first-class login support (#23093)

## Why

The Python SDK can already create threads and run turns, but
authentication still has to be arranged outside the SDK. App-server
already exposes account login, account inspection, logout, and
`account/login/completed` notifications, so SDK users currently have to
work around a missing public client layer for a core setup step.

This change makes authentication a normal SDK workflow while preserving
the backend flow shape: API-key login completes immediately, and
interactive ChatGPT flows return live handles that complete later
through app-server notifications.

## What changed

- Added public sync and async auth methods on `Codex` / `AsyncCodex`:
  - `login_api_key(...)`
  - `login_chatgpt()`
  - `login_chatgpt_device_code()`
  - `account(...)`
  - `logout()`
- Added public browser-login and device-code handle types with
attempt-local `wait()` and `cancel()` helpers. Cancellation stays on the
handle instead of a root-level SDK method.
- Extended the Python app-server client and notification router so login
completion events are routed by `login_id` without consuming unrelated
global notifications.
- Kept login request/handle logic in a focused internal `_login.py`
module so `api.py` remains the public facade instead of absorbing more
auth plumbing.
- Exported the new handle types plus curated account/login response
types from the SDK surfaces.
- Updated SDK docs, added sync/async login walkthrough examples, and
added a notebook login walkthrough cell.

## Verification

Added SDK coverage for:

- API-key login, account readback, and logout through the app-server
harness in both sync and async clients.
- Browser login cancellation plus `handle.wait()` completion through the
real app-server boundary used by the Python SDK harness.
- Waiter routing that stays scoped across replaced interactive login
attempts, plus async handle cancellation coverage.
- Login notification demuxing, replay of early completion events, and
async client delegation.
- Public export/signature assertions.
- Real integration-suite smoke coverage for the new examples and
notebook login cell.
This commit is contained in:
Ahmed Ibrahim
2026-05-17 05:49:28 +03:00
committed by GitHub
Unverified
parent 0445b290fe
commit 4c89772314
19 changed files with 772 additions and 13 deletions
+73
View File
@@ -22,6 +22,16 @@ from ._inputs import (
_normalize_run_input,
_to_wire_input,
)
from ._login import (
AsyncChatgptLoginHandle,
AsyncDeviceCodeLoginHandle,
ChatgptLoginHandle,
DeviceCodeLoginHandle,
async_start_chatgpt_login,
async_start_device_code_login,
start_chatgpt_login,
start_device_code_login,
)
from ._run import (
RunResult,
_collect_async_run_result,
@@ -30,6 +40,10 @@ from ._run import (
from .async_client import AsyncAppServerClient
from .client import AppServerClient, AppServerConfig
from .generated.v2_all import (
ApiKeyLoginAccountParams,
GetAccountParams,
GetAccountResponse,
LoginAccountParams,
ModelListResponse,
Personality,
ReasoningEffort,
@@ -85,6 +99,33 @@ class Codex:
def close(self) -> None:
self._client.close()
def login_api_key(self, api_key: str) -> None:
"""Authenticate app-server with an API key."""
self._client.account_login_start(
LoginAccountParams(
root=ApiKeyLoginAccountParams(
api_key=api_key,
type="apiKey",
)
)
)
def login_chatgpt(self) -> ChatgptLoginHandle:
"""Start browser-based ChatGPT login and return its live handle."""
return start_chatgpt_login(self._client)
def login_chatgpt_device_code(self) -> DeviceCodeLoginHandle:
"""Start device-code ChatGPT login and return its live handle."""
return start_device_code_login(self._client)
def account(self, *, refresh_token: bool = False) -> GetAccountResponse:
"""Read the current app-server account state."""
return self._client.account_read(GetAccountParams(refresh_token=refresh_token))
def logout(self) -> None:
"""Clear the current app-server account session."""
self._client.account_logout()
# BEGIN GENERATED: Codex.flat_methods
def thread_start(
self,
@@ -286,6 +327,38 @@ class AsyncCodex:
self._init = None
self._initialized = False
async def login_api_key(self, api_key: str) -> None:
"""Authenticate app-server with an API key."""
await self._ensure_initialized()
await self._client.account_login_start(
LoginAccountParams(
root=ApiKeyLoginAccountParams(
api_key=api_key,
type="apiKey",
)
)
)
async def login_chatgpt(self) -> AsyncChatgptLoginHandle:
"""Start browser-based ChatGPT login and return its live handle."""
await self._ensure_initialized()
return await async_start_chatgpt_login(self)
async def login_chatgpt_device_code(self) -> AsyncDeviceCodeLoginHandle:
"""Start device-code ChatGPT login and return its live handle."""
await self._ensure_initialized()
return await async_start_device_code_login(self)
async def account(self, *, refresh_token: bool = False) -> GetAccountResponse:
"""Read the current app-server account state."""
await self._ensure_initialized()
return await self._client.account_read(GetAccountParams(refresh_token=refresh_token))
async def logout(self) -> None:
"""Clear the current app-server account session."""
await self._ensure_initialized()
await self._client.account_logout()
# BEGIN GENERATED: AsyncCodex.flat_methods
async def thread_start(
self,