mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
0445b290fe
commit
4c89772314
@@ -1,10 +1,14 @@
|
||||
from ._version import __version__
|
||||
from .api import (
|
||||
ApprovalMode,
|
||||
AsyncChatgptLoginHandle,
|
||||
AsyncCodex,
|
||||
AsyncDeviceCodeLoginHandle,
|
||||
AsyncThread,
|
||||
AsyncTurnHandle,
|
||||
ChatgptLoginHandle,
|
||||
Codex,
|
||||
DeviceCodeLoginHandle,
|
||||
ImageInput,
|
||||
Input,
|
||||
InputItem,
|
||||
@@ -39,6 +43,10 @@ __all__ = [
|
||||
"Codex",
|
||||
"AsyncCodex",
|
||||
"ApprovalMode",
|
||||
"ChatgptLoginHandle",
|
||||
"DeviceCodeLoginHandle",
|
||||
"AsyncChatgptLoginHandle",
|
||||
"AsyncDeviceCodeLoginHandle",
|
||||
"Thread",
|
||||
"AsyncThread",
|
||||
"TurnHandle",
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
|
||||
from .async_client import AsyncAppServerClient
|
||||
from .client import AppServerClient
|
||||
from .generated.v2_all import (
|
||||
AccountLoginCompletedNotification,
|
||||
CancelLoginAccountResponse,
|
||||
ChatgptDeviceCodeLoginAccountParams,
|
||||
ChatgptDeviceCodeLoginAccountResponse,
|
||||
ChatgptLoginAccountParams,
|
||||
ChatgptLoginAccountResponse,
|
||||
LoginAccountParams,
|
||||
)
|
||||
|
||||
|
||||
class _AsyncLoginOwner(Protocol):
|
||||
"""Subset of AsyncCodex needed by async login handles."""
|
||||
|
||||
_client: AsyncAppServerClient
|
||||
|
||||
async def _ensure_initialized(self) -> None:
|
||||
"""Ensure the owning SDK client has a live app-server connection."""
|
||||
...
|
||||
|
||||
|
||||
def start_chatgpt_login(client: AppServerClient) -> ChatgptLoginHandle:
|
||||
"""Start browser ChatGPT login and return the handle for that attempt."""
|
||||
response = client.account_login_start(
|
||||
LoginAccountParams(
|
||||
root=ChatgptLoginAccountParams(type="chatgpt"),
|
||||
)
|
||||
)
|
||||
response_root = response.root
|
||||
if not isinstance(response_root, ChatgptLoginAccountResponse):
|
||||
raise RuntimeError(f"unexpected ChatGPT login response: {response_root!r}")
|
||||
return ChatgptLoginHandle(
|
||||
client,
|
||||
response_root.login_id,
|
||||
response_root.auth_url,
|
||||
)
|
||||
|
||||
|
||||
async def async_start_chatgpt_login(owner: _AsyncLoginOwner) -> AsyncChatgptLoginHandle:
|
||||
"""Start async browser ChatGPT login and return that attempt's handle."""
|
||||
response = await owner._client.account_login_start(
|
||||
LoginAccountParams(
|
||||
root=ChatgptLoginAccountParams(type="chatgpt"),
|
||||
)
|
||||
)
|
||||
response_root = response.root
|
||||
if not isinstance(response_root, ChatgptLoginAccountResponse):
|
||||
raise RuntimeError(f"unexpected ChatGPT login response: {response_root!r}")
|
||||
return AsyncChatgptLoginHandle(
|
||||
owner,
|
||||
response_root.login_id,
|
||||
response_root.auth_url,
|
||||
)
|
||||
|
||||
|
||||
def start_device_code_login(client: AppServerClient) -> DeviceCodeLoginHandle:
|
||||
"""Start device-code ChatGPT login and return the handle for that attempt."""
|
||||
response = client.account_login_start(
|
||||
LoginAccountParams(
|
||||
root=ChatgptDeviceCodeLoginAccountParams(type="chatgptDeviceCode"),
|
||||
)
|
||||
)
|
||||
response_root = response.root
|
||||
if not isinstance(response_root, ChatgptDeviceCodeLoginAccountResponse):
|
||||
raise RuntimeError(f"unexpected device-code login response: {response_root!r}")
|
||||
return DeviceCodeLoginHandle(
|
||||
client,
|
||||
response_root.login_id,
|
||||
response_root.verification_url,
|
||||
response_root.user_code,
|
||||
)
|
||||
|
||||
|
||||
async def async_start_device_code_login(
|
||||
owner: _AsyncLoginOwner,
|
||||
) -> AsyncDeviceCodeLoginHandle:
|
||||
"""Start async device-code ChatGPT login and return that attempt's handle."""
|
||||
response = await owner._client.account_login_start(
|
||||
LoginAccountParams(
|
||||
root=ChatgptDeviceCodeLoginAccountParams(type="chatgptDeviceCode"),
|
||||
)
|
||||
)
|
||||
response_root = response.root
|
||||
if not isinstance(response_root, ChatgptDeviceCodeLoginAccountResponse):
|
||||
raise RuntimeError(f"unexpected device-code login response: {response_root!r}")
|
||||
return AsyncDeviceCodeLoginHandle(
|
||||
owner,
|
||||
response_root.login_id,
|
||||
response_root.verification_url,
|
||||
response_root.user_code,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ChatgptLoginHandle:
|
||||
"""Live browser-login attempt returned by `Codex.login_chatgpt()`."""
|
||||
|
||||
_client: AppServerClient
|
||||
login_id: str
|
||||
auth_url: str
|
||||
|
||||
def wait(self) -> AccountLoginCompletedNotification:
|
||||
"""Wait for this browser login attempt's completion notification."""
|
||||
return self._client.wait_for_login_completed(self.login_id)
|
||||
|
||||
def cancel(self) -> CancelLoginAccountResponse:
|
||||
"""Cancel this browser login attempt."""
|
||||
return self._client.account_login_cancel(self.login_id)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DeviceCodeLoginHandle:
|
||||
"""Live device-code login attempt returned by `Codex.login_chatgpt_device_code()`."""
|
||||
|
||||
_client: AppServerClient
|
||||
login_id: str
|
||||
verification_url: str
|
||||
user_code: str
|
||||
|
||||
def wait(self) -> AccountLoginCompletedNotification:
|
||||
"""Wait for this device-code login attempt's completion notification."""
|
||||
return self._client.wait_for_login_completed(self.login_id)
|
||||
|
||||
def cancel(self) -> CancelLoginAccountResponse:
|
||||
"""Cancel this device-code login attempt."""
|
||||
return self._client.account_login_cancel(self.login_id)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AsyncChatgptLoginHandle:
|
||||
"""Live browser-login attempt returned by `AsyncCodex.login_chatgpt()`."""
|
||||
|
||||
_codex: _AsyncLoginOwner
|
||||
login_id: str
|
||||
auth_url: str
|
||||
|
||||
async def wait(self) -> AccountLoginCompletedNotification:
|
||||
"""Wait for this browser login attempt's completion notification."""
|
||||
await self._codex._ensure_initialized()
|
||||
return await self._codex._client.wait_for_login_completed(self.login_id)
|
||||
|
||||
async def cancel(self) -> CancelLoginAccountResponse:
|
||||
"""Cancel this browser login attempt."""
|
||||
await self._codex._ensure_initialized()
|
||||
return await self._codex._client.account_login_cancel(self.login_id)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AsyncDeviceCodeLoginHandle:
|
||||
"""Live device-code attempt returned by `AsyncCodex.login_chatgpt_device_code()`."""
|
||||
|
||||
_codex: _AsyncLoginOwner
|
||||
login_id: str
|
||||
verification_url: str
|
||||
user_code: str
|
||||
|
||||
async def wait(self) -> AccountLoginCompletedNotification:
|
||||
"""Wait for this device-code login attempt's completion notification."""
|
||||
await self._codex._ensure_initialized()
|
||||
return await self._codex._client.wait_for_login_completed(self.login_id)
|
||||
|
||||
async def cancel(self) -> CancelLoginAccountResponse:
|
||||
"""Cancel this device-code login attempt."""
|
||||
await self._codex._ensure_initialized()
|
||||
return await self._codex._client.account_login_cancel(self.login_id)
|
||||
@@ -6,6 +6,7 @@ from collections import deque
|
||||
|
||||
from .errors import AppServerError, map_jsonrpc_error
|
||||
from .generated.notification_registry import notification_turn_id
|
||||
from .generated.v2_all import AccountLoginCompletedNotification
|
||||
from .models import JsonValue, Notification, UnknownNotification
|
||||
|
||||
ResponseQueueItem = JsonValue | BaseException
|
||||
@@ -25,6 +26,8 @@ class MessageRouter:
|
||||
"""Create empty response, turn, and global notification queues."""
|
||||
self._lock = threading.Lock()
|
||||
self._response_waiters: dict[str, queue.Queue[ResponseQueueItem]] = {}
|
||||
self._login_notifications: dict[str, queue.Queue[NotificationQueueItem]] = {}
|
||||
self._pending_login_notifications: dict[str, deque[Notification]] = {}
|
||||
self._turn_notifications: dict[str, queue.Queue[NotificationQueueItem]] = {}
|
||||
self._pending_turn_notifications: dict[str, deque[Notification]] = {}
|
||||
self._global_notifications: queue.Queue[NotificationQueueItem] = queue.Queue()
|
||||
@@ -51,6 +54,36 @@ class MessageRouter:
|
||||
raise item
|
||||
return item
|
||||
|
||||
def register_login(self, login_id: str) -> None:
|
||||
"""Register a queue for one interactive login attempt."""
|
||||
|
||||
login_queue: queue.Queue[NotificationQueueItem] = queue.Queue()
|
||||
with self._lock:
|
||||
if login_id in self._login_notifications:
|
||||
return
|
||||
pending = self._pending_login_notifications.pop(login_id, deque())
|
||||
self._login_notifications[login_id] = login_queue
|
||||
for notification in pending:
|
||||
login_queue.put(notification)
|
||||
|
||||
def unregister_login(self, login_id: str) -> None:
|
||||
"""Stop routing future notifications for one login attempt."""
|
||||
|
||||
with self._lock:
|
||||
self._login_notifications.pop(login_id, None)
|
||||
|
||||
def next_login_notification(self, login_id: str) -> Notification:
|
||||
"""Block until the next notification for a registered login attempt."""
|
||||
|
||||
with self._lock:
|
||||
login_queue = self._login_notifications.get(login_id)
|
||||
if login_queue is None:
|
||||
raise RuntimeError(f"login {login_id!r} is not registered for waiting")
|
||||
item = login_queue.get()
|
||||
if isinstance(item, BaseException):
|
||||
raise item
|
||||
return item
|
||||
|
||||
def register_turn(self, turn_id: str) -> None:
|
||||
"""Register a queue for a turn stream and replay early events."""
|
||||
|
||||
@@ -111,6 +144,18 @@ class MessageRouter:
|
||||
def route_notification(self, notification: Notification) -> None:
|
||||
"""Deliver a notification to a turn queue or the global queue."""
|
||||
|
||||
login_id = self._notification_login_id(notification)
|
||||
if login_id is not None:
|
||||
with self._lock:
|
||||
login_queue = self._login_notifications.get(login_id)
|
||||
if login_queue is None:
|
||||
self._pending_login_notifications.setdefault(login_id, deque()).append(
|
||||
notification
|
||||
)
|
||||
return
|
||||
login_queue.put(notification)
|
||||
return
|
||||
|
||||
turn_id = self._notification_turn_id(notification)
|
||||
if turn_id is None:
|
||||
self._global_notifications.put(notification)
|
||||
@@ -132,16 +177,35 @@ class MessageRouter:
|
||||
with self._lock:
|
||||
response_waiters = list(self._response_waiters.values())
|
||||
self._response_waiters.clear()
|
||||
login_queues = list(self._login_notifications.values())
|
||||
self._login_notifications.clear()
|
||||
self._pending_login_notifications.clear()
|
||||
turn_queues = list(self._turn_notifications.values())
|
||||
self._pending_turn_notifications.clear()
|
||||
# Put the same transport failure into every queue so no SDK call blocks
|
||||
# forever waiting for a response that cannot arrive.
|
||||
for waiter in response_waiters:
|
||||
waiter.put(exc)
|
||||
for login_queue in login_queues:
|
||||
login_queue.put(exc)
|
||||
for turn_queue in turn_queues:
|
||||
turn_queue.put(exc)
|
||||
self._global_notifications.put(exc)
|
||||
|
||||
def _notification_login_id(self, notification: Notification) -> str | None:
|
||||
"""Extract the login attempt id from completion notifications."""
|
||||
if notification.method != "account/login/completed":
|
||||
return None
|
||||
|
||||
payload = notification.payload
|
||||
if isinstance(payload, AccountLoginCompletedNotification):
|
||||
return payload.login_id
|
||||
if isinstance(payload, UnknownNotification):
|
||||
raw_login_id = payload.params.get("loginId")
|
||||
if isinstance(raw_login_id, str):
|
||||
return raw_login_id
|
||||
return None
|
||||
|
||||
def _notification_turn_id(self, notification: Notification) -> str | None:
|
||||
"""Extract routing ids from known generated payloads or raw unknown payloads."""
|
||||
payload = notification.payload
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -8,7 +8,14 @@ from pydantic import BaseModel
|
||||
|
||||
from .client import AppServerClient, AppServerConfig
|
||||
from .generated.v2_all import (
|
||||
AccountLoginCompletedNotification,
|
||||
AgentMessageDeltaNotification,
|
||||
CancelLoginAccountResponse,
|
||||
GetAccountParams as V2GetAccountParams,
|
||||
GetAccountResponse,
|
||||
LoginAccountParams as V2LoginAccountParams,
|
||||
LoginAccountResponse,
|
||||
LogoutAccountResponse,
|
||||
ModelListResponse,
|
||||
ThreadArchiveResponse,
|
||||
ThreadCompactStartResponse,
|
||||
@@ -88,6 +95,14 @@ class AsyncAppServerClient:
|
||||
"""Register a turn notification queue on the wrapped sync client."""
|
||||
self._sync.register_turn_notifications(turn_id)
|
||||
|
||||
def register_login_notifications(self, login_id: str) -> None:
|
||||
"""Register a login notification queue on the wrapped sync client."""
|
||||
self._sync.register_login_notifications(login_id)
|
||||
|
||||
def unregister_login_notifications(self, login_id: str) -> None:
|
||||
"""Unregister a login notification queue on the wrapped sync client."""
|
||||
self._sync.unregister_login_notifications(login_id)
|
||||
|
||||
def unregister_turn_notifications(self, turn_id: str) -> None:
|
||||
"""Unregister a turn notification queue on the wrapped sync client."""
|
||||
self._sync.unregister_turn_notifications(turn_id)
|
||||
@@ -107,6 +122,28 @@ class AsyncAppServerClient:
|
||||
response_model=response_model,
|
||||
)
|
||||
|
||||
async def account_login_start(
|
||||
self,
|
||||
params: V2LoginAccountParams | JsonObject,
|
||||
) -> LoginAccountResponse:
|
||||
"""Start one account login attempt through the wrapped sync client."""
|
||||
return await self._call_sync(self._sync.account_login_start, params)
|
||||
|
||||
async def account_login_cancel(self, login_id: str) -> CancelLoginAccountResponse:
|
||||
"""Cancel one active account login attempt through the wrapped sync client."""
|
||||
return await self._call_sync(self._sync.account_login_cancel, login_id)
|
||||
|
||||
async def account_read(
|
||||
self,
|
||||
params: V2GetAccountParams | JsonObject | None = None,
|
||||
) -> GetAccountResponse:
|
||||
"""Read current account state through the wrapped sync client."""
|
||||
return await self._call_sync(self._sync.account_read, params)
|
||||
|
||||
async def account_logout(self) -> LogoutAccountResponse:
|
||||
"""Clear the active account session through the wrapped sync client."""
|
||||
return await self._call_sync(self._sync.account_logout)
|
||||
|
||||
async def thread_start(
|
||||
self, params: V2ThreadStartParams | JsonObject | None = None
|
||||
) -> ThreadStartResponse:
|
||||
@@ -211,10 +248,21 @@ class AsyncAppServerClient:
|
||||
"""Wait for the next global notification without blocking the event loop."""
|
||||
return await self._call_sync(self._sync.next_notification)
|
||||
|
||||
async def next_login_notification(self, login_id: str) -> Notification:
|
||||
"""Wait for the next notification routed to one login attempt."""
|
||||
return await self._call_sync(self._sync.next_login_notification, login_id)
|
||||
|
||||
async def next_turn_notification(self, turn_id: str) -> Notification:
|
||||
"""Wait for the next notification routed to one turn."""
|
||||
return await self._call_sync(self._sync.next_turn_notification, turn_id)
|
||||
|
||||
async def wait_for_login_completed(
|
||||
self,
|
||||
login_id: str,
|
||||
) -> AccountLoginCompletedNotification:
|
||||
"""Wait for the completion notification routed to one login attempt."""
|
||||
return await self._call_sync(self._sync.wait_for_login_completed, login_id)
|
||||
|
||||
async def wait_for_turn_completed(self, turn_id: str) -> TurnCompletedNotification:
|
||||
"""Wait for the completion notification routed to one turn."""
|
||||
return await self._call_sync(self._sync.wait_for_turn_completed, turn_id)
|
||||
|
||||
@@ -17,7 +17,16 @@ from ._version import __version__ as SDK_VERSION
|
||||
from .errors import AppServerError, TransportClosedError
|
||||
from .generated.notification_registry import NOTIFICATION_MODELS
|
||||
from .generated.v2_all import (
|
||||
AccountLoginCompletedNotification,
|
||||
AgentMessageDeltaNotification,
|
||||
CancelLoginAccountResponse,
|
||||
ChatgptDeviceCodeLoginAccountResponse,
|
||||
ChatgptLoginAccountResponse,
|
||||
GetAccountParams as V2GetAccountParams,
|
||||
GetAccountResponse,
|
||||
LoginAccountParams as V2LoginAccountParams,
|
||||
LoginAccountResponse,
|
||||
LogoutAccountResponse,
|
||||
ModelListResponse,
|
||||
ThreadArchiveResponse,
|
||||
ThreadCompactStartResponse,
|
||||
@@ -59,6 +68,8 @@ def _params_dict(
|
||||
| V2ThreadListParams
|
||||
| V2ThreadForkParams
|
||||
| V2TurnStartParams
|
||||
| V2GetAccountParams
|
||||
| V2LoginAccountParams
|
||||
| JsonObject
|
||||
| None
|
||||
),
|
||||
@@ -246,7 +257,10 @@ class AppServerClient:
|
||||
waiter = self._router.create_response_waiter(request_id)
|
||||
|
||||
try:
|
||||
self._write_message({"id": request_id, "method": method, "params": params or {}})
|
||||
message: JsonObject = {"id": request_id, "method": method}
|
||||
if params is not None:
|
||||
message["params"] = params
|
||||
self._write_message(message)
|
||||
except BaseException:
|
||||
self._router.discard_response_waiter(request_id)
|
||||
raise
|
||||
@@ -258,12 +272,27 @@ class AppServerClient:
|
||||
|
||||
def notify(self, method: str, params: JsonObject | None = None) -> None:
|
||||
"""Send a JSON-RPC notification without waiting for a response."""
|
||||
self._write_message({"method": method, "params": params or {}})
|
||||
message: JsonObject = {"method": method}
|
||||
if params is not None:
|
||||
message["params"] = params
|
||||
self._write_message(message)
|
||||
|
||||
def next_notification(self) -> Notification:
|
||||
"""Return the next notification that is not scoped to an active turn."""
|
||||
return self._router.next_global_notification()
|
||||
|
||||
def register_login_notifications(self, login_id: str) -> None:
|
||||
"""Start routing notifications for one interactive login attempt."""
|
||||
self._router.register_login(login_id)
|
||||
|
||||
def unregister_login_notifications(self, login_id: str) -> None:
|
||||
"""Stop routing notifications for one interactive login attempt."""
|
||||
self._router.unregister_login(login_id)
|
||||
|
||||
def next_login_notification(self, login_id: str) -> Notification:
|
||||
"""Return the next routed notification for the requested login id."""
|
||||
return self._router.next_login_notification(login_id)
|
||||
|
||||
def register_turn_notifications(self, turn_id: str) -> None:
|
||||
"""Start routing notifications for one turn into its dedicated queue."""
|
||||
self._router.register_turn(turn_id)
|
||||
@@ -276,6 +305,43 @@ class AppServerClient:
|
||||
"""Return the next routed notification for the requested turn id."""
|
||||
return self._router.next_turn_notification(turn_id)
|
||||
|
||||
def account_login_start(
|
||||
self,
|
||||
params: V2LoginAccountParams | JsonObject,
|
||||
) -> LoginAccountResponse:
|
||||
response = self.request(
|
||||
"account/login/start",
|
||||
_params_dict(params),
|
||||
response_model=LoginAccountResponse,
|
||||
)
|
||||
response_root = response.root
|
||||
if isinstance(
|
||||
response_root,
|
||||
ChatgptLoginAccountResponse | ChatgptDeviceCodeLoginAccountResponse,
|
||||
):
|
||||
self.register_login_notifications(response_root.login_id)
|
||||
return response
|
||||
|
||||
def account_login_cancel(self, login_id: str) -> CancelLoginAccountResponse:
|
||||
return self.request(
|
||||
"account/login/cancel",
|
||||
{"loginId": login_id},
|
||||
response_model=CancelLoginAccountResponse,
|
||||
)
|
||||
|
||||
def account_read(
|
||||
self,
|
||||
params: V2GetAccountParams | JsonObject | None = None,
|
||||
) -> GetAccountResponse:
|
||||
return self.request(
|
||||
"account/read",
|
||||
_params_dict(params),
|
||||
response_model=GetAccountResponse,
|
||||
)
|
||||
|
||||
def account_logout(self) -> LogoutAccountResponse:
|
||||
return self.request("account/logout", None, response_model=LogoutAccountResponse)
|
||||
|
||||
def thread_start(
|
||||
self, params: V2ThreadStartParams | JsonObject | None = None
|
||||
) -> ThreadStartResponse:
|
||||
@@ -417,6 +483,24 @@ class AppServerClient:
|
||||
finally:
|
||||
self.unregister_turn_notifications(turn_id)
|
||||
|
||||
def wait_for_login_completed(
|
||||
self,
|
||||
login_id: str,
|
||||
) -> AccountLoginCompletedNotification:
|
||||
"""Block until the matching interactive login attempt completes."""
|
||||
self.register_login_notifications(login_id)
|
||||
try:
|
||||
while True:
|
||||
notification = self.next_login_notification(login_id)
|
||||
if (
|
||||
notification.method == "account/login/completed"
|
||||
and isinstance(notification.payload, AccountLoginCompletedNotification)
|
||||
and notification.payload.login_id == login_id
|
||||
):
|
||||
return notification.payload
|
||||
finally:
|
||||
self.unregister_login_notifications(login_id)
|
||||
|
||||
def stream_text(
|
||||
self,
|
||||
thread_id: str,
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .generated.v2_all import (
|
||||
Account,
|
||||
AccountLoginCompletedNotification,
|
||||
ApprovalsReviewer,
|
||||
AskForApproval,
|
||||
CancelLoginAccountResponse,
|
||||
CancelLoginAccountStatus,
|
||||
GetAccountResponse,
|
||||
ModelListResponse,
|
||||
Personality,
|
||||
PlanType,
|
||||
@@ -35,8 +40,13 @@ from .generated.v2_all import (
|
||||
from .models import InitializeResponse, JsonObject, Notification
|
||||
|
||||
__all__ = [
|
||||
"Account",
|
||||
"AccountLoginCompletedNotification",
|
||||
"ApprovalsReviewer",
|
||||
"AskForApproval",
|
||||
"CancelLoginAccountResponse",
|
||||
"CancelLoginAccountStatus",
|
||||
"GetAccountResponse",
|
||||
"InitializeResponse",
|
||||
"JsonObject",
|
||||
"ModelListResponse",
|
||||
|
||||
Reference in New Issue
Block a user