python-codex 0.2.3__py3-none-any.whl → 0.2.5__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.
- pycodex/context.py +4 -18
- pycodex/interactive_session.py +15 -1
- pycodex/model.py +153 -11
- pycodex/model_metadata.py +31 -0
- pycodex/prompts/models.json +334 -0
- pycodex/tools/unified_exec_manager.py +32 -1
- pycodex/utils/compactor.py +5 -87
- {python_codex-0.2.3.dist-info → python_codex-0.2.5.dist-info}/METADATA +16 -4
- {python_codex-0.2.3.dist-info → python_codex-0.2.5.dist-info}/RECORD +14 -13
- workspace_server/app.py +57 -1
- workspace_server/workspace.html +44 -1
- {python_codex-0.2.3.dist-info → python_codex-0.2.5.dist-info}/WHEEL +0 -0
- {python_codex-0.2.3.dist-info → python_codex-0.2.5.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.3.dist-info → python_codex-0.2.5.dist-info}/licenses/LICENSE +0 -0
pycodex/context.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from functools import lru_cache
|
|
5
|
-
import json
|
|
6
4
|
from pathlib import Path
|
|
7
5
|
import typing
|
|
8
6
|
|
|
@@ -12,6 +10,7 @@ except ModuleNotFoundError: # pragma: no cover - Python 3.10 path
|
|
|
12
10
|
import tomli as tomllib
|
|
13
11
|
|
|
14
12
|
from .collaboration import DEFAULT_COLLABORATION_MODE, CollaborationMode
|
|
13
|
+
from .model_metadata import load_models_by_slug
|
|
15
14
|
from .protocol import ContextMessage, ConversationItem, JSONDict, Prompt, ToolSpec
|
|
16
15
|
from .utils.get_env import (
|
|
17
16
|
get_sandbox_tag,
|
|
@@ -23,7 +22,6 @@ from .utils.get_env import (
|
|
|
23
22
|
DEFAULT_BASE_INSTRUCTIONS_PATH = (
|
|
24
23
|
Path(__file__).resolve().parent / "prompts" / "default_base_instructions.md"
|
|
25
24
|
)
|
|
26
|
-
DEFAULT_MODELS_PATH = Path(__file__).resolve().parent / "prompts" / "models.json"
|
|
27
25
|
DEFAULT_COLLABORATION_INSTRUCTIONS_PATH = (
|
|
28
26
|
Path(__file__).resolve().parent / "prompts" / "collaboration_default.md"
|
|
29
27
|
)
|
|
@@ -270,7 +268,7 @@ class ContextManager:
|
|
|
270
268
|
model_metadata = None
|
|
271
269
|
model_slug = self._config.model
|
|
272
270
|
if model_slug is not None:
|
|
273
|
-
model_metadata =
|
|
271
|
+
model_metadata = load_models_by_slug().get(model_slug)
|
|
274
272
|
|
|
275
273
|
context_window = self._config.model_context_window
|
|
276
274
|
if context_window is None and model_metadata is not None:
|
|
@@ -293,7 +291,7 @@ class ContextManager:
|
|
|
293
291
|
model_slug = self._config.model
|
|
294
292
|
if model_slug is None:
|
|
295
293
|
return None
|
|
296
|
-
model_metadata =
|
|
294
|
+
model_metadata = load_models_by_slug().get(model_slug)
|
|
297
295
|
if model_metadata is None:
|
|
298
296
|
return None
|
|
299
297
|
return _normalize_int(model_metadata.get("auto_compact_token_limit"))
|
|
@@ -302,7 +300,7 @@ class ContextManager:
|
|
|
302
300
|
model_slug = self._config.model
|
|
303
301
|
if model_slug is None:
|
|
304
302
|
return None
|
|
305
|
-
model_metadata =
|
|
303
|
+
model_metadata = load_models_by_slug().get(model_slug)
|
|
306
304
|
if model_metadata is None:
|
|
307
305
|
return None
|
|
308
306
|
|
|
@@ -560,18 +558,6 @@ def _read_first_instruction_file(base: 'Path') -> 'typing.Union[str, None]':
|
|
|
560
558
|
return None
|
|
561
559
|
|
|
562
560
|
|
|
563
|
-
@lru_cache(maxsize=1)
|
|
564
|
-
def _load_models_by_slug() -> 'typing.Dict[str, JSONDict]':
|
|
565
|
-
payload = json.loads(DEFAULT_MODELS_PATH.read_text(encoding="utf-8"))
|
|
566
|
-
models = payload.get("models", [])
|
|
567
|
-
by_slug: 'typing.Dict[str, JSONDict]' = {}
|
|
568
|
-
for model in models:
|
|
569
|
-
slug = model.get("slug")
|
|
570
|
-
if isinstance(slug, str):
|
|
571
|
-
by_slug[slug] = model
|
|
572
|
-
return by_slug
|
|
573
|
-
|
|
574
|
-
|
|
575
561
|
def _resolve_personality_message(variables, personality: 'typing.Union[str, None]') -> 'str':
|
|
576
562
|
if not isinstance(variables, dict):
|
|
577
563
|
return ""
|
pycodex/interactive_session.py
CHANGED
|
@@ -25,10 +25,11 @@ MODEL_COMMAND = "/model"
|
|
|
25
25
|
QUEUE_COMMAND = "/queue"
|
|
26
26
|
RESUME_COMMAND = "/resume"
|
|
27
27
|
COMPACT_COMMAND = "/compact"
|
|
28
|
+
FORK_COMMAND = "/fork"
|
|
28
29
|
LINK_COMMAND = "/link"
|
|
29
30
|
UNLINK_COMMAND = "/unlink"
|
|
30
31
|
EXTRA_COMMANDS_LINE = (
|
|
31
|
-
"Extra commands: /help, /history, /title, /model, /resume, /compact, /link, /unlink"
|
|
32
|
+
"Extra commands: /help, /history, /title, /model, /resume, /compact, /fork, /link, /unlink"
|
|
32
33
|
)
|
|
33
34
|
|
|
34
35
|
|
|
@@ -301,6 +302,19 @@ async def run_interactive_session(
|
|
|
301
302
|
except Exception as exc: # pragma: no cover - defensive surface
|
|
302
303
|
view.show_error(str(exc))
|
|
303
304
|
continue
|
|
305
|
+
if prompt_text == FORK_COMMAND:
|
|
306
|
+
if has_pending_turn_tasks():
|
|
307
|
+
view.write_line(
|
|
308
|
+
"Cannot fork while work is running or queued."
|
|
309
|
+
)
|
|
310
|
+
continue
|
|
311
|
+
if not hasattr(model_client, "_session_id"):
|
|
312
|
+
view.write_line("Current model does not support session IDs.")
|
|
313
|
+
continue
|
|
314
|
+
new_session_id = uuid7_string()
|
|
315
|
+
model_client._session_id = new_session_id
|
|
316
|
+
view.write_line(f"Forked session: {new_session_id}")
|
|
317
|
+
continue
|
|
304
318
|
if prompt_text.startswith(f"{LINK_COMMAND} "):
|
|
305
319
|
link_target = prompt_text[len(LINK_COMMAND) :].strip()
|
|
306
320
|
if not link_target:
|
pycodex/model.py
CHANGED
|
@@ -19,16 +19,19 @@ except ModuleNotFoundError: # pragma: no cover - Python 3.10 path
|
|
|
19
19
|
|
|
20
20
|
from .protocol import (
|
|
21
21
|
AssistantMessage,
|
|
22
|
+
JSONDict,
|
|
22
23
|
ModelResponse,
|
|
23
24
|
ModelStreamEvent,
|
|
24
25
|
Prompt,
|
|
25
26
|
ReasoningItem,
|
|
26
27
|
ToolCall,
|
|
27
28
|
)
|
|
29
|
+
from .model_metadata import model_metadata
|
|
28
30
|
from .utils import build_user_agent, uuid7_string
|
|
29
31
|
|
|
30
32
|
DEFAULT_CODEX_CONFIG_PATH = Path.home() / ".codex" / "config.toml"
|
|
31
33
|
DEFAULT_ORIGINATOR = "pycodex"
|
|
34
|
+
RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite"
|
|
32
35
|
ModelStreamEventHandler = Callable[[ModelStreamEvent], None]
|
|
33
36
|
NOOP_MODEL_STREAM_EVENT_HANDLER: 'ModelStreamEventHandler' = lambda _event: None
|
|
34
37
|
DEFAULT_STREAM_MAX_RETRIES = 5
|
|
@@ -65,6 +68,7 @@ class ResponsesProviderConfig:
|
|
|
65
68
|
beta_features_header: 'typing.Union[str, None]' = None
|
|
66
69
|
stream_max_retries: 'typing.Union[int, None]' = None
|
|
67
70
|
stream_idle_timeout_ms: 'typing.Union[int, None]' = None
|
|
71
|
+
service_tier: 'typing.Union[str, None]' = None
|
|
68
72
|
|
|
69
73
|
@classmethod
|
|
70
74
|
def from_codex_config(
|
|
@@ -116,6 +120,7 @@ class ResponsesProviderConfig:
|
|
|
116
120
|
reasoning_effort=selected.get("model_reasoning_effort"),
|
|
117
121
|
reasoning_summary=selected.get("model_reasoning_summary"),
|
|
118
122
|
verbosity=selected.get("model_verbosity"),
|
|
123
|
+
service_tier=selected.get("service_tier"),
|
|
119
124
|
sandbox_mode=selected.get("sandbox_mode"),
|
|
120
125
|
beta_features_header=",".join(beta_features) or None,
|
|
121
126
|
stream_max_retries=_optional_int(provider.get("stream_max_retries")),
|
|
@@ -157,6 +162,65 @@ class ResponsesProviderConfig:
|
|
|
157
162
|
return DEFAULT_STREAM_IDLE_TIMEOUT_MS / 1000.0
|
|
158
163
|
return max(int(self.stream_idle_timeout_ms), 1) / 1000.0
|
|
159
164
|
|
|
165
|
+
def metadata(self) -> 'typing.Union[JSONDict, None]':
|
|
166
|
+
return model_metadata(self.model)
|
|
167
|
+
|
|
168
|
+
def use_responses_lite(self) -> 'bool':
|
|
169
|
+
metadata = self.metadata()
|
|
170
|
+
if metadata is None:
|
|
171
|
+
return False
|
|
172
|
+
return metadata.get("use_responses_lite") is True
|
|
173
|
+
|
|
174
|
+
def effective_reasoning_effort(self) -> 'typing.Union[str, None]':
|
|
175
|
+
if self.reasoning_effort is not None:
|
|
176
|
+
return str(self.reasoning_effort)
|
|
177
|
+
metadata = self.metadata()
|
|
178
|
+
if not _metadata_supports_reasoning(metadata):
|
|
179
|
+
return None
|
|
180
|
+
return _optional_metadata_string(metadata, "default_reasoning_level")
|
|
181
|
+
|
|
182
|
+
def effective_reasoning_summary(self) -> 'typing.Union[str, None]':
|
|
183
|
+
summary = self.reasoning_summary
|
|
184
|
+
if summary is None:
|
|
185
|
+
metadata = self.metadata()
|
|
186
|
+
if not _metadata_supports_reasoning(metadata):
|
|
187
|
+
return None
|
|
188
|
+
summary = _optional_metadata_string(metadata, "default_reasoning_summary")
|
|
189
|
+
if summary is None:
|
|
190
|
+
return None
|
|
191
|
+
if str(summary).strip().lower() == "none":
|
|
192
|
+
return None
|
|
193
|
+
return str(summary)
|
|
194
|
+
|
|
195
|
+
def effective_verbosity(self) -> 'typing.Union[str, None]':
|
|
196
|
+
if self.verbosity is not None:
|
|
197
|
+
return str(self.verbosity)
|
|
198
|
+
metadata = self.metadata()
|
|
199
|
+
if metadata is None or metadata.get("support_verbosity") is not True:
|
|
200
|
+
return None
|
|
201
|
+
return _optional_metadata_string(metadata, "default_verbosity")
|
|
202
|
+
|
|
203
|
+
def effective_service_tier(self) -> 'typing.Union[str, None]':
|
|
204
|
+
service_tier = self.service_tier
|
|
205
|
+
if service_tier is None:
|
|
206
|
+
return None
|
|
207
|
+
service_tier = str(service_tier).strip()
|
|
208
|
+
if service_tier == "fast":
|
|
209
|
+
service_tier = "priority"
|
|
210
|
+
if not service_tier or service_tier == "default":
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
metadata = self.metadata()
|
|
214
|
+
if metadata is None:
|
|
215
|
+
return None
|
|
216
|
+
supported = metadata.get("service_tiers")
|
|
217
|
+
if not isinstance(supported, list):
|
|
218
|
+
return None
|
|
219
|
+
for tier in supported:
|
|
220
|
+
if isinstance(tier, dict) and tier.get("id") == service_tier:
|
|
221
|
+
return service_tier
|
|
222
|
+
return None
|
|
223
|
+
|
|
160
224
|
|
|
161
225
|
def _optional_bool(value: 'typing.Union[bool, str, int, None]') -> 'typing.Union[bool, None]':
|
|
162
226
|
if value is None:
|
|
@@ -171,6 +235,43 @@ def _optional_bool(value: 'typing.Union[bool, str, int, None]') -> 'typing.Union
|
|
|
171
235
|
raise ValueError(f"invalid boolean config value: {value!r}")
|
|
172
236
|
|
|
173
237
|
|
|
238
|
+
def _metadata_supports_reasoning(
|
|
239
|
+
metadata: 'typing.Union[JSONDict, None]',
|
|
240
|
+
) -> 'bool':
|
|
241
|
+
return metadata is not None and metadata.get("supports_reasoning_summaries") is True
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _optional_metadata_string(
|
|
245
|
+
metadata: 'typing.Union[JSONDict, None]',
|
|
246
|
+
key: 'str',
|
|
247
|
+
) -> 'typing.Union[str, None]':
|
|
248
|
+
if metadata is None:
|
|
249
|
+
return None
|
|
250
|
+
value = metadata.get(key)
|
|
251
|
+
if value is None:
|
|
252
|
+
return None
|
|
253
|
+
text = str(value).strip()
|
|
254
|
+
return text or None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _strip_image_details(items: 'typing.Iterable[object]') -> 'None':
|
|
258
|
+
for item in items:
|
|
259
|
+
if not isinstance(item, dict):
|
|
260
|
+
continue
|
|
261
|
+
content = item.get("content")
|
|
262
|
+
if isinstance(content, list):
|
|
263
|
+
_strip_image_detail_from_content_items(content)
|
|
264
|
+
output = item.get("output")
|
|
265
|
+
if isinstance(output, list):
|
|
266
|
+
_strip_image_detail_from_content_items(output)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _strip_image_detail_from_content_items(items: 'typing.Iterable[object]') -> 'None':
|
|
270
|
+
for item in items:
|
|
271
|
+
if isinstance(item, dict) and item.get("type") == "input_image":
|
|
272
|
+
item.pop("detail", None)
|
|
273
|
+
|
|
274
|
+
|
|
174
275
|
class ResponsesApiError(RuntimeError):
|
|
175
276
|
pass
|
|
176
277
|
|
|
@@ -392,34 +493,73 @@ class ResponsesModelClient:
|
|
|
392
493
|
) from exc
|
|
393
494
|
|
|
394
495
|
def _build_payload(self, prompt: 'Prompt') -> 'typing.Dict[str, object]':
|
|
496
|
+
use_responses_lite = self._config.use_responses_lite()
|
|
497
|
+
input_items = [item.serialize() for item in prompt.input]
|
|
498
|
+
if use_responses_lite:
|
|
499
|
+
_strip_image_details(input_items)
|
|
500
|
+
|
|
501
|
+
tools = [tool.serialize() for tool in prompt.tools]
|
|
395
502
|
payload: 'typing.Dict[str, object]' = {
|
|
396
503
|
"model": self.model,
|
|
397
|
-
"
|
|
398
|
-
"
|
|
399
|
-
"tools": [tool.serialize() for tool in prompt.tools],
|
|
400
|
-
"parallel_tool_calls": prompt.parallel_tool_calls,
|
|
504
|
+
"input": input_items,
|
|
505
|
+
"parallel_tool_calls": prompt.parallel_tool_calls and not use_responses_lite,
|
|
401
506
|
"store": False,
|
|
402
507
|
"stream": True,
|
|
403
508
|
"include": ["reasoning.encrypted_content"],
|
|
404
509
|
"prompt_cache_key": self._session_id,
|
|
405
510
|
}
|
|
406
|
-
if
|
|
511
|
+
if use_responses_lite:
|
|
512
|
+
prefix: 'typing.List[typing.Dict[str, object]]' = [
|
|
513
|
+
{
|
|
514
|
+
"type": "additional_tools",
|
|
515
|
+
"role": "developer",
|
|
516
|
+
"tools": tools,
|
|
517
|
+
}
|
|
518
|
+
]
|
|
519
|
+
if prompt.base_instructions:
|
|
520
|
+
prefix.append(
|
|
521
|
+
{
|
|
522
|
+
"type": "message",
|
|
523
|
+
"role": "developer",
|
|
524
|
+
"content": [
|
|
525
|
+
{
|
|
526
|
+
"type": "input_text",
|
|
527
|
+
"text": prompt.base_instructions,
|
|
528
|
+
}
|
|
529
|
+
],
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
payload["input"] = prefix + input_items
|
|
533
|
+
else:
|
|
534
|
+
payload["instructions"] = prompt.base_instructions or ""
|
|
535
|
+
payload["tools"] = tools
|
|
536
|
+
|
|
537
|
+
if prompt.tools or use_responses_lite:
|
|
407
538
|
payload["tool_choice"] = "auto"
|
|
408
539
|
|
|
409
540
|
reasoning: 'typing.Dict[str, str]' = {}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if
|
|
413
|
-
reasoning["
|
|
541
|
+
reasoning_effort = self._config.effective_reasoning_effort()
|
|
542
|
+
reasoning_summary = self._config.effective_reasoning_summary()
|
|
543
|
+
if reasoning_effort is not None:
|
|
544
|
+
reasoning["effort"] = reasoning_effort
|
|
545
|
+
if reasoning_summary is not None:
|
|
546
|
+
reasoning["summary"] = reasoning_summary
|
|
547
|
+
if use_responses_lite and reasoning:
|
|
548
|
+
reasoning["context"] = "all_turns"
|
|
414
549
|
if reasoning:
|
|
415
550
|
payload["reasoning"] = reasoning
|
|
416
551
|
|
|
417
552
|
text = None
|
|
418
|
-
|
|
419
|
-
|
|
553
|
+
verbosity = self._config.effective_verbosity()
|
|
554
|
+
if verbosity is not None:
|
|
555
|
+
text = {"verbosity": verbosity}
|
|
420
556
|
if text is not None:
|
|
421
557
|
payload["text"] = text
|
|
422
558
|
|
|
559
|
+
service_tier = self._config.effective_service_tier()
|
|
560
|
+
if service_tier is not None:
|
|
561
|
+
payload["service_tier"] = service_tier
|
|
562
|
+
|
|
423
563
|
return payload
|
|
424
564
|
|
|
425
565
|
def _list_models_sync(self) -> 'typing.List[str]':
|
|
@@ -483,6 +623,8 @@ class ResponsesModelClient:
|
|
|
483
623
|
headers["authorization"] = f"Bearer {api_key}"
|
|
484
624
|
if self._config.beta_features_header is not None:
|
|
485
625
|
headers["x-codex-beta-features"] = self._config.beta_features_header
|
|
626
|
+
if self._config.use_responses_lite():
|
|
627
|
+
headers[RESPONSES_LITE_HEADER] = "true"
|
|
486
628
|
if self._openai_subagent is not None:
|
|
487
629
|
headers["x-openai-subagent"] = self._openai_subagent
|
|
488
630
|
if prompt.turn_metadata is not None:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Shared loader for vendored Codex model metadata."""
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
from .protocol import JSONDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_MODELS_PATH = Path(__file__).resolve().parent / "prompts" / "models.json"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@lru_cache(maxsize=1)
|
|
15
|
+
def load_models_by_slug() -> 'typing.Dict[str, JSONDict]':
|
|
16
|
+
payload = json.loads(DEFAULT_MODELS_PATH.read_text(encoding="utf-8"))
|
|
17
|
+
models = payload.get("models", [])
|
|
18
|
+
by_slug: 'typing.Dict[str, JSONDict]' = {}
|
|
19
|
+
for model in models:
|
|
20
|
+
if not isinstance(model, dict):
|
|
21
|
+
continue
|
|
22
|
+
slug = model.get("slug")
|
|
23
|
+
if isinstance(slug, str):
|
|
24
|
+
by_slug[slug] = model
|
|
25
|
+
return by_slug
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def model_metadata(slug: 'typing.Union[str, None]') -> 'typing.Union[JSONDict, None]':
|
|
29
|
+
if slug is None:
|
|
30
|
+
return None
|
|
31
|
+
return load_models_by_slug().get(slug)
|