gpt-codex-client 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from ._async_auth import afinish_login, aget_token, alogin, arefresh
4
+ from ._async_client import AsyncCodexClient
5
+ from ._auth import PendingLogin, finish_login, get_token, login, refresh, start_login
6
+ from ._client import CodexClient
7
+ from ._config import Token, build_headers, get_account_id
8
+ from ._errors import (
9
+ APIConnectionError,
10
+ APIError,
11
+ APITimeoutError,
12
+ AuthError,
13
+ CodexError,
14
+ InvalidRequestError,
15
+ RateLimitError,
16
+ ServerError,
17
+ StreamError,
18
+ )
19
+ from ._types import (
20
+ ChatCompletion,
21
+ ChatCompletionChunk,
22
+ FunctionTool,
23
+ Model,
24
+ ParsedResponse,
25
+ Reasoning,
26
+ Response,
27
+ ResponseStreamEvent,
28
+ TextConfig,
29
+ )
30
+
31
+ __version__ = "0.1.0"
32
+
33
+ __all__ = [
34
+ "APIConnectionError",
35
+ "APIError",
36
+ "APITimeoutError",
37
+ "AsyncCodexClient",
38
+ "AuthError",
39
+ "ChatCompletion",
40
+ "ChatCompletionChunk",
41
+ "CodexClient",
42
+ "CodexError",
43
+ "FunctionTool",
44
+ "InvalidRequestError",
45
+ "Model",
46
+ "ParsedResponse",
47
+ "PendingLogin",
48
+ "RateLimitError",
49
+ "Reasoning",
50
+ "Response",
51
+ "ResponseStreamEvent",
52
+ "ServerError",
53
+ "StreamError",
54
+ "TextConfig",
55
+ "Token",
56
+ "__version__",
57
+ "afinish_login",
58
+ "aget_token",
59
+ "alogin",
60
+ "arefresh",
61
+ "build_headers",
62
+ "finish_login",
63
+ "get_account_id",
64
+ "get_token",
65
+ "login",
66
+ "refresh",
67
+ "start_login",
68
+ ]
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from ._auth import (
10
+ LoginHandler,
11
+ PendingLogin,
12
+ _code_from_callback,
13
+ _token_from_response,
14
+ start_login,
15
+ )
16
+ from ._config import DEFAULT_TOKEN_PATH, TOKEN_URL, Token, get_client_id, load_token, save_token
17
+ from ._errors import AuthError
18
+
19
+
20
+ async def afinish_login(
21
+ callback_url: str,
22
+ pending: PendingLogin,
23
+ *,
24
+ token_path: str | Path = DEFAULT_TOKEN_PATH,
25
+ http_client: httpx.AsyncClient | None = None,
26
+ timeout: float | httpx.Timeout | None = 120.0,
27
+ ) -> Token:
28
+ code = _code_from_callback(callback_url, pending.state)
29
+ owns_client = http_client is None
30
+ client = http_client or httpx.AsyncClient(timeout=timeout)
31
+ try:
32
+ response = await client.post(
33
+ TOKEN_URL,
34
+ data={
35
+ "grant_type": "authorization_code",
36
+ "client_id": pending.client_id or get_client_id(),
37
+ "code": code,
38
+ "redirect_uri": pending.redirect_uri,
39
+ "code_verifier": pending.verifier,
40
+ },
41
+ timeout=timeout,
42
+ )
43
+ token = _token_from_response(response)
44
+ save_token(token, token_path)
45
+ return token
46
+ finally:
47
+ if owns_client:
48
+ await client.aclose()
49
+
50
+
51
+ async def alogin(
52
+ *,
53
+ headless: bool = False,
54
+ no_browser: bool = False,
55
+ token_path: str | Path = DEFAULT_TOKEN_PATH,
56
+ login_handler: LoginHandler | None = None,
57
+ client_id: str | None = None,
58
+ http_client: httpx.AsyncClient | None = None,
59
+ timeout: float | httpx.Timeout | None = 120.0,
60
+ ) -> Token:
61
+ if login_handler is not None:
62
+ pending = start_login(client_id=client_id)
63
+ callback_url = login_handler(pending.url)
64
+ return await afinish_login(
65
+ callback_url,
66
+ pending,
67
+ token_path=token_path,
68
+ http_client=http_client,
69
+ timeout=timeout,
70
+ )
71
+
72
+ from ._auth import login
73
+
74
+ def run_sync_login() -> Token:
75
+ return login(
76
+ headless=headless,
77
+ no_browser=no_browser,
78
+ token_path=token_path,
79
+ login_handler=None,
80
+ client_id=client_id,
81
+ timeout=timeout,
82
+ )
83
+
84
+ return await asyncio.to_thread(run_sync_login)
85
+
86
+
87
+ async def arefresh(
88
+ *,
89
+ token_path: str | Path = DEFAULT_TOKEN_PATH,
90
+ refresh_token: str | None = None,
91
+ client_id: str | None = None,
92
+ http_client: httpx.AsyncClient | None = None,
93
+ timeout: float | httpx.Timeout | None = 120.0,
94
+ ) -> Token:
95
+ cached = load_token(token_path)
96
+ resolved_refresh_token = refresh_token or (cached.refresh_token if cached is not None else None)
97
+ if not resolved_refresh_token:
98
+ raise AuthError("No refresh token is available")
99
+
100
+ owns_client = http_client is None
101
+ client = http_client or httpx.AsyncClient(timeout=timeout)
102
+ try:
103
+ response = await client.post(
104
+ TOKEN_URL,
105
+ data={
106
+ "grant_type": "refresh_token",
107
+ "client_id": get_client_id(client_id),
108
+ "refresh_token": resolved_refresh_token,
109
+ },
110
+ timeout=timeout,
111
+ )
112
+ token = _token_from_response(response)
113
+ if token.refresh_token is None:
114
+ token.refresh_token = resolved_refresh_token
115
+ save_token(token, token_path)
116
+ return token
117
+ finally:
118
+ if owns_client:
119
+ await client.aclose()
120
+
121
+
122
+ async def aget_token(
123
+ *,
124
+ token_path: str | Path = DEFAULT_TOKEN_PATH,
125
+ headless: bool = False,
126
+ no_browser: bool = False,
127
+ login_handler: LoginHandler | None = None,
128
+ client_id: str | None = None,
129
+ http_client: httpx.AsyncClient | None = None,
130
+ timeout: float | httpx.Timeout | None = 120.0,
131
+ ) -> Token:
132
+ cached = load_token(token_path)
133
+ if cached is not None and not cached.is_expired():
134
+ return cached
135
+
136
+ if cached is not None and cached.refresh_token:
137
+ try:
138
+ return await arefresh(
139
+ token_path=token_path,
140
+ refresh_token=cached.refresh_token,
141
+ client_id=client_id,
142
+ http_client=http_client,
143
+ timeout=timeout,
144
+ )
145
+ except AuthError:
146
+ pass
147
+
148
+ return await alogin(
149
+ headless=headless,
150
+ no_browser=no_browser,
151
+ token_path=token_path,
152
+ login_handler=login_handler,
153
+ client_id=client_id,
154
+ http_client=http_client,
155
+ timeout=timeout,
156
+ )
157
+
158
+
159
+ def _unused(value: Any) -> None:
160
+ return None
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from types import TracebackType
6
+ from typing import Any
7
+ from urllib.parse import urljoin
8
+
9
+ import httpx
10
+
11
+ from ._async_auth import aget_token
12
+ from ._async_models import AsyncModelsResource
13
+ from ._async_responses import AsyncResponsesResource
14
+ from ._async_stream import AsyncResponseStream
15
+ from ._auth import LoginHandler
16
+ from ._chat import AsyncChatResource
17
+ from ._config import (
18
+ DEFAULT_BASE_URL,
19
+ DEFAULT_TOKEN_PATH,
20
+ Token,
21
+ build_headers,
22
+ get_client_version,
23
+ )
24
+ from ._errors import (
25
+ APIConnectionError,
26
+ APIError,
27
+ APITimeoutError,
28
+ CodexError,
29
+ error_from_response,
30
+ is_retryable_error,
31
+ )
32
+ from ._types import JsonObject
33
+
34
+
35
+ class AsyncCodexClient:
36
+ def __init__(
37
+ self,
38
+ *,
39
+ headless: bool = False,
40
+ no_browser: bool = False,
41
+ token_path: str | Path = DEFAULT_TOKEN_PATH,
42
+ login_handler: LoginHandler | None = None,
43
+ auth_client_id: str | None = None,
44
+ client_version: str | None = None,
45
+ models_manifest_url: str | None = None,
46
+ timeout: float = 120.0,
47
+ max_retries: int = 2,
48
+ default_headers: dict[str, str] | None = None,
49
+ http_client: httpx.AsyncClient | None = None,
50
+ base_url: str = DEFAULT_BASE_URL,
51
+ ) -> None:
52
+ self.headless = headless
53
+ self.no_browser = no_browser
54
+ self.token_path = token_path
55
+ self.login_handler = login_handler
56
+ self.auth_client_id = auth_client_id
57
+ self.client_version = get_client_version(client_version)
58
+ self.models_manifest_url = models_manifest_url
59
+ self.timeout = timeout
60
+ self.max_retries = max_retries
61
+ self.default_headers = default_headers or {}
62
+ self.base_url = base_url.rstrip("/") + "/"
63
+ self._http_client = http_client or httpx.AsyncClient(timeout=timeout)
64
+ self._owns_http_client = http_client is None
65
+ self._token: Token | None = None
66
+
67
+ self.responses = AsyncResponsesResource(self)
68
+ self.chat = AsyncChatResource(self)
69
+ self.models = AsyncModelsResource(self, manifest_url=models_manifest_url)
70
+
71
+ async def __aenter__(self) -> AsyncCodexClient:
72
+ return self
73
+
74
+ async def __aexit__(
75
+ self,
76
+ exc_type: type[BaseException] | None,
77
+ exc: BaseException | None,
78
+ traceback: TracebackType | None,
79
+ ) -> None:
80
+ await self.aclose()
81
+
82
+ async def aclose(self) -> None:
83
+ if self._owns_http_client:
84
+ await self._http_client.aclose()
85
+
86
+ async def _request(
87
+ self,
88
+ method: str,
89
+ path: str,
90
+ *,
91
+ json: JsonObject | None = None,
92
+ timeout: float | None = None,
93
+ extra_headers: dict[str, str] | None = None,
94
+ ) -> JsonObject:
95
+ url = urljoin(self.base_url, path.lstrip("/"))
96
+ attempts = self.max_retries + 1
97
+ for attempt in range(attempts):
98
+ try:
99
+ response = await self._http_client.request(
100
+ method,
101
+ url,
102
+ json=json,
103
+ params=self._params(),
104
+ headers=await self._headers(extra_headers),
105
+ timeout=timeout or self.timeout,
106
+ )
107
+ except httpx.TimeoutException as exc:
108
+ if attempt + 1 < attempts:
109
+ await self._sleep_before_retry(attempt, None)
110
+ continue
111
+ raise APITimeoutError(str(exc)) from exc
112
+ except httpx.RequestError as exc:
113
+ if attempt + 1 < attempts:
114
+ await self._sleep_before_retry(attempt, None)
115
+ continue
116
+ raise APIConnectionError(str(exc)) from exc
117
+
118
+ if response.status_code < 400:
119
+ return _json_object(response)
120
+ error = error_from_response(response)
121
+ if attempt + 1 < attempts and is_retryable_error(error):
122
+ retry_after = error.retry_after if isinstance(error, APIError) else None
123
+ await self._sleep_before_retry(attempt, retry_after)
124
+ continue
125
+ raise error
126
+ raise APIConnectionError("Request retry loop exited unexpectedly")
127
+
128
+ async def _stream(
129
+ self,
130
+ method: str,
131
+ path: str,
132
+ *,
133
+ json: JsonObject | None = None,
134
+ timeout: float | None = None,
135
+ extra_headers: dict[str, str] | None = None,
136
+ ) -> AsyncResponseStream:
137
+ url = urljoin(self.base_url, path.lstrip("/"))
138
+ manager = self._http_client.stream(
139
+ method,
140
+ url,
141
+ json=json,
142
+ params=self._params(),
143
+ headers=await self._headers(extra_headers),
144
+ timeout=timeout or self.timeout,
145
+ )
146
+ return AsyncResponseStream(manager)
147
+
148
+ def _params(self) -> dict[str, str]:
149
+ return {"client_version": self.client_version}
150
+
151
+ async def _headers(self, extra_headers: dict[str, str] | None = None) -> dict[str, str]:
152
+ token = self._token
153
+ if token is None or token.is_expired():
154
+ token = await aget_token(
155
+ token_path=self.token_path,
156
+ headless=self.headless,
157
+ no_browser=self.no_browser,
158
+ login_handler=self.login_handler,
159
+ client_id=self.auth_client_id,
160
+ http_client=self._http_client,
161
+ timeout=self.timeout,
162
+ )
163
+ self._token = token
164
+ headers = build_headers(token, default_headers=self.default_headers)
165
+ if extra_headers:
166
+ headers.update(extra_headers)
167
+ return headers
168
+
169
+ async def _sleep_before_retry(self, attempt: int, retry_after: float | None) -> None:
170
+ delay = retry_after if retry_after is not None else min(2.0, 0.25 * (2**attempt))
171
+ await asyncio.sleep(delay)
172
+
173
+
174
+ def _json_object(response: httpx.Response) -> JsonObject:
175
+ if not response.content:
176
+ return {}
177
+ try:
178
+ payload: Any = response.json()
179
+ except ValueError as exc:
180
+ raise CodexError("Response was not valid JSON") from exc
181
+ if not isinstance(payload, dict):
182
+ raise CodexError("Response JSON was not an object")
183
+ return payload
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ from ._config import get_models_manifest_url
9
+ from ._errors import (
10
+ APIConnectionError,
11
+ APIError,
12
+ APITimeoutError,
13
+ CodexError,
14
+ error_from_response,
15
+ is_retryable_error,
16
+ )
17
+ from ._models import _models_from_payload
18
+ from ._types import JsonObject, Model
19
+
20
+
21
+ class AsyncModelsResource:
22
+ def __init__(
23
+ self,
24
+ client: Any,
25
+ *,
26
+ ttl_seconds: float = 300.0,
27
+ manifest_url: str | None = None,
28
+ ) -> None:
29
+ self._client = client
30
+ self._ttl_seconds = ttl_seconds
31
+ self._manifest_url = get_models_manifest_url(manifest_url)
32
+ self._cache: list[Model] | None = None
33
+ self._cache_at = 0.0
34
+
35
+ async def list(
36
+ self, *, force_refresh: bool = False, timeout: float | None = None
37
+ ) -> list[Model]:
38
+ if (
39
+ not force_refresh
40
+ and self._cache is not None
41
+ and time.time() - self._cache_at < self._ttl_seconds
42
+ ):
43
+ return list(self._cache)
44
+ payload = await self._fetch_manifest(timeout=timeout)
45
+ models = _models_from_payload(payload)
46
+ self._cache = models
47
+ self._cache_at = time.time()
48
+ return list(models)
49
+
50
+ async def _fetch_manifest(self, *, timeout: float | None = None) -> JsonObject:
51
+ attempts = self._client.max_retries + 1
52
+ for attempt in range(attempts):
53
+ try:
54
+ response = await self._client._http_client.get(
55
+ self._manifest_url,
56
+ headers={"accept": "application/json"},
57
+ timeout=timeout or self._client.timeout,
58
+ )
59
+ except httpx.TimeoutException as exc:
60
+ if attempt + 1 < attempts:
61
+ await self._client._sleep_before_retry(attempt, None)
62
+ continue
63
+ raise APITimeoutError(str(exc)) from exc
64
+ except httpx.RequestError as exc:
65
+ if attempt + 1 < attempts:
66
+ await self._client._sleep_before_retry(attempt, None)
67
+ continue
68
+ raise APIConnectionError(str(exc)) from exc
69
+ if response.status_code < 400:
70
+ try:
71
+ payload = response.json()
72
+ except ValueError as exc:
73
+ raise CodexError("Models manifest response was not valid JSON") from exc
74
+ if not isinstance(payload, dict):
75
+ raise CodexError("Models manifest JSON was not an object")
76
+ return payload
77
+ error = error_from_response(response)
78
+ if attempt + 1 < attempts and is_retryable_error(error):
79
+ retry_after = error.retry_after if isinstance(error, APIError) else None
80
+ await self._client._sleep_before_retry(attempt, retry_after)
81
+ continue
82
+ raise error
83
+ raise APIConnectionError("Models manifest retry loop exited unexpectedly")
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal, TypeVar, cast, overload
4
+
5
+ from ._async_stream import AsyncResponseStream
6
+ from ._converters import response_request_body
7
+ from ._responses import _parse_output, _text_config_for_format
8
+ from ._types import FunctionTool, JsonObject, ParsedResponse, Reasoning, Response, TextConfig
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ class AsyncResponsesResource:
14
+ def __init__(self, client: Any) -> None:
15
+ self._client = client
16
+
17
+ @overload
18
+ async def create(
19
+ self,
20
+ *,
21
+ model: str,
22
+ input: str | list[Any],
23
+ instructions: str | None = None,
24
+ stream: Literal[False] = False,
25
+ tools: list[FunctionTool | JsonObject] | None = None,
26
+ tool_choice: str | JsonObject | None = None,
27
+ parallel_tool_calls: bool | None = None,
28
+ reasoning: Reasoning | JsonObject | None = None,
29
+ text: TextConfig | JsonObject | None = None,
30
+ include: list[str] | None = None,
31
+ previous_response_id: str | None = None,
32
+ timeout: float | None = None,
33
+ extra_headers: dict[str, str] | None = None,
34
+ ) -> Response: ...
35
+
36
+ @overload
37
+ async def create(
38
+ self,
39
+ *,
40
+ model: str,
41
+ input: str | list[Any],
42
+ instructions: str | None = None,
43
+ stream: Literal[True],
44
+ tools: list[FunctionTool | JsonObject] | None = None,
45
+ tool_choice: str | JsonObject | None = None,
46
+ parallel_tool_calls: bool | None = None,
47
+ reasoning: Reasoning | JsonObject | None = None,
48
+ text: TextConfig | JsonObject | None = None,
49
+ include: list[str] | None = None,
50
+ previous_response_id: str | None = None,
51
+ timeout: float | None = None,
52
+ extra_headers: dict[str, str] | None = None,
53
+ ) -> AsyncResponseStream: ...
54
+
55
+ async def create(
56
+ self,
57
+ *,
58
+ model: str,
59
+ input: str | list[Any],
60
+ instructions: str | None = None,
61
+ stream: bool = False,
62
+ tools: list[FunctionTool | JsonObject] | None = None,
63
+ tool_choice: str | JsonObject | None = None,
64
+ parallel_tool_calls: bool | None = None,
65
+ reasoning: Reasoning | JsonObject | None = None,
66
+ text: TextConfig | JsonObject | None = None,
67
+ include: list[str] | None = None,
68
+ previous_response_id: str | None = None,
69
+ timeout: float | None = None,
70
+ extra_headers: dict[str, str] | None = None,
71
+ ) -> Response | AsyncResponseStream:
72
+ body = response_request_body(
73
+ model=model,
74
+ input=input,
75
+ instructions=instructions,
76
+ stream=True,
77
+ tools=tools,
78
+ tool_choice=tool_choice,
79
+ parallel_tool_calls=parallel_tool_calls,
80
+ reasoning=reasoning,
81
+ text=text,
82
+ include=include,
83
+ previous_response_id=previous_response_id,
84
+ )
85
+ if stream:
86
+ return cast(
87
+ AsyncResponseStream,
88
+ await self._client._stream(
89
+ "POST",
90
+ "/responses",
91
+ json=body,
92
+ timeout=timeout,
93
+ extra_headers=extra_headers,
94
+ ),
95
+ )
96
+ response_stream = cast(
97
+ AsyncResponseStream,
98
+ await self._client._stream(
99
+ "POST",
100
+ "/responses",
101
+ json=body,
102
+ timeout=timeout,
103
+ extra_headers=extra_headers,
104
+ ),
105
+ )
106
+ async with response_stream as opened_stream:
107
+ async for _event in opened_stream:
108
+ pass
109
+ return await opened_stream.get_final_response()
110
+
111
+ async def parse(
112
+ self,
113
+ *,
114
+ model: str,
115
+ input: str | list[Any],
116
+ text_format: type[T] | JsonObject,
117
+ instructions: str | None = None,
118
+ tools: list[FunctionTool | JsonObject] | None = None,
119
+ tool_choice: str | JsonObject | None = None,
120
+ parallel_tool_calls: bool | None = None,
121
+ reasoning: Reasoning | JsonObject | None = None,
122
+ include: list[str] | None = None,
123
+ previous_response_id: str | None = None,
124
+ timeout: float | None = None,
125
+ extra_headers: dict[str, str] | None = None,
126
+ ) -> ParsedResponse[T]:
127
+ text_config = _text_config_for_format(text_format)
128
+ response = await self.create(
129
+ model=model,
130
+ input=input,
131
+ instructions=instructions,
132
+ tools=tools,
133
+ tool_choice=tool_choice,
134
+ parallel_tool_calls=parallel_tool_calls,
135
+ reasoning=reasoning,
136
+ text=text_config,
137
+ include=include,
138
+ previous_response_id=previous_response_id,
139
+ timeout=timeout,
140
+ extra_headers=extra_headers,
141
+ )
142
+ if isinstance(response, AsyncResponseStream):
143
+ raise TypeError("parse() does not support stream=True")
144
+ return ParsedResponse(
145
+ response=response, parsed=_parse_output(response.output_text, text_format)
146
+ )