python-codex 0.1.1__py3-none-any.whl → 0.1.3__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.
Files changed (59) hide show
  1. pycodex/__init__.py +5 -1
  2. pycodex/agent.py +39 -41
  3. pycodex/cli.py +51 -43
  4. pycodex/collaboration.py +6 -7
  5. pycodex/compat.py +99 -0
  6. pycodex/context.py +87 -87
  7. pycodex/doctor.py +40 -40
  8. pycodex/model.py +69 -69
  9. pycodex/portable.py +33 -33
  10. pycodex/portable_server.py +22 -21
  11. pycodex/protocol.py +84 -86
  12. pycodex/runtime.py +36 -35
  13. pycodex/runtime_services.py +72 -69
  14. pycodex/tools/agent_tool_schemas.py +0 -2
  15. pycodex/tools/apply_patch_tool.py +43 -44
  16. pycodex/tools/base_tool.py +35 -36
  17. pycodex/tools/close_agent_tool.py +2 -4
  18. pycodex/tools/code_mode_manager.py +61 -61
  19. pycodex/tools/exec_command_tool.py +5 -6
  20. pycodex/tools/exec_runtime.js +3 -3
  21. pycodex/tools/exec_tool.py +3 -5
  22. pycodex/tools/grep_files_tool.py +10 -11
  23. pycodex/tools/list_dir_tool.py +8 -9
  24. pycodex/tools/read_file_tool.py +13 -14
  25. pycodex/tools/request_permissions_tool.py +2 -4
  26. pycodex/tools/request_user_input_tool.py +13 -14
  27. pycodex/tools/resume_agent_tool.py +2 -4
  28. pycodex/tools/send_input_tool.py +8 -9
  29. pycodex/tools/shell_command_tool.py +5 -6
  30. pycodex/tools/shell_tool.py +5 -6
  31. pycodex/tools/spawn_agent_tool.py +4 -5
  32. pycodex/tools/unified_exec_manager.py +79 -61
  33. pycodex/tools/update_plan_tool.py +4 -5
  34. pycodex/tools/view_image_tool.py +4 -5
  35. pycodex/tools/wait_agent_tool.py +2 -4
  36. pycodex/tools/wait_tool.py +4 -5
  37. pycodex/tools/web_search_tool.py +1 -3
  38. pycodex/tools/write_stdin_tool.py +4 -5
  39. pycodex/utils/dotenv.py +6 -6
  40. pycodex/utils/get_env.py +57 -34
  41. pycodex/utils/random_ids.py +1 -2
  42. pycodex/utils/visualize.py +79 -79
  43. {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/METADATA +15 -9
  44. python_codex-0.1.3.dist-info/RECORD +74 -0
  45. {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/WHEEL +1 -1
  46. responses_server/__init__.py +17 -0
  47. responses_server/__main__.py +5 -0
  48. responses_server/app.py +227 -0
  49. responses_server/config.py +63 -0
  50. responses_server/payload_processors.py +86 -0
  51. responses_server/server.py +63 -0
  52. responses_server/session_store.py +37 -0
  53. responses_server/stream_router.py +784 -0
  54. responses_server/tools/__init__.py +4 -0
  55. responses_server/tools/custom_adapter.py +235 -0
  56. responses_server/tools/web_search.py +263 -0
  57. python_codex-0.1.1.dist-info/RECORD +0 -62
  58. {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/entry_points.txt +0 -0
  59. {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/licenses/LICENSE +0 -0
pycodex/context.py CHANGED
@@ -1,10 +1,10 @@
1
- from __future__ import annotations
2
1
 
3
2
  from dataclasses import dataclass
4
3
  from datetime import datetime
5
4
  from functools import lru_cache
6
5
  import json
7
6
  from pathlib import Path
7
+ import typing
8
8
 
9
9
  try:
10
10
  import tomllib
@@ -66,26 +66,26 @@ SKILLS_GUIDANCE = """- Discovery: The list above is the skills available in this
66
66
  - Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue."""
67
67
 
68
68
 
69
- @dataclass(frozen=True, slots=True)
69
+ @dataclass(frozen=True, )
70
70
  class ContextConfig:
71
- base_instructions: str | None = None
72
- developer_instructions: str | None = None
73
- user_instructions: str | None = None
74
- codex_home_instructions: str | None = None
75
- model_instructions_file: Path | None = None
76
- codex_home: Path | None = None
77
- project_doc_max_bytes: int | None = None
78
- model: str | None = None
79
- personality: str | None = None
80
- approval_policy: str | None = None
81
- sandbox_mode: str | None = None
71
+ base_instructions: 'typing.Union[str, None]' = None
72
+ developer_instructions: 'typing.Union[str, None]' = None
73
+ user_instructions: 'typing.Union[str, None]' = None
74
+ codex_home_instructions: 'typing.Union[str, None]' = None
75
+ model_instructions_file: 'typing.Union[Path, None]' = None
76
+ codex_home: 'typing.Union[Path, None]' = None
77
+ project_doc_max_bytes: 'typing.Union[int, None]' = None
78
+ model: 'typing.Union[str, None]' = None
79
+ personality: 'typing.Union[str, None]' = None
80
+ approval_policy: 'typing.Union[str, None]' = None
81
+ sandbox_mode: 'typing.Union[str, None]' = None
82
82
 
83
83
  @classmethod
84
84
  def from_codex_config(
85
85
  cls,
86
- config_path: str | Path,
87
- profile: str | None = None,
88
- ) -> ContextConfig:
86
+ config_path: 'typing.Union[str, Path]',
87
+ profile: 'typing.Union[str, None]' = None,
88
+ ) -> 'ContextConfig':
89
89
  path = Path(config_path)
90
90
  data = tomllib.loads(path.read_text())
91
91
  selected = dict(data)
@@ -123,26 +123,26 @@ class ContextConfig:
123
123
  )
124
124
 
125
125
 
126
- @dataclass(frozen=True, slots=True)
126
+ @dataclass(frozen=True, )
127
127
  class SkillDescriptor:
128
- name: str
129
- description: str
130
- path_to_skill_md: Path
131
- scope_rank: int
128
+ name: 'str'
129
+ description: 'str'
130
+ path_to_skill_md: 'Path'
131
+ scope_rank: 'int'
132
132
 
133
133
 
134
134
  class ContextManager:
135
135
  def __init__(
136
136
  self,
137
- base_instructions_override: str | None = None,
138
- config: ContextConfig | None = None,
139
- collaboration_mode: CollaborationMode = DEFAULT_COLLABORATION_MODE,
140
- collaboration_instructions: str | None = None,
141
- include_collaboration_instructions: bool = False,
142
- include_permissions_instructions: bool = True,
143
- include_skills_instructions: bool = True,
144
- network_access: str = "enabled",
145
- ) -> None:
137
+ base_instructions_override: 'typing.Union[str, None]' = None,
138
+ config: 'typing.Union[ContextConfig, None]' = None,
139
+ collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
140
+ collaboration_instructions: 'typing.Union[str, None]' = None,
141
+ include_collaboration_instructions: 'bool' = False,
142
+ include_permissions_instructions: 'bool' = True,
143
+ include_skills_instructions: 'bool' = True,
144
+ network_access: 'str' = "enabled",
145
+ ) -> 'None':
146
146
  self.cwd = Path.cwd().resolve()
147
147
  self._shell = get_shell_name()
148
148
  self._current_date = datetime.now().date().isoformat()
@@ -160,21 +160,21 @@ class ContextManager:
160
160
  self._include_skills_instructions = include_skills_instructions
161
161
  self._network_access = network_access
162
162
  self._default_base_instructions = DEFAULT_BASE_INSTRUCTIONS_PATH.read_text()
163
- self._workspace_metadata_turn_id: str | None = None
164
- self._workspace_metadata_cache: JSONDict | None = None
163
+ self._workspace_metadata_turn_id: 'typing.Union[str, None]' = None
164
+ self._workspace_metadata_cache: 'typing.Union[JSONDict, None]' = None
165
165
 
166
166
  @classmethod
167
167
  def from_codex_config(
168
168
  cls,
169
- config_path: str | Path,
170
- profile: str | None = None,
171
- base_instructions_override: str | None = None,
172
- collaboration_mode: CollaborationMode = DEFAULT_COLLABORATION_MODE,
173
- include_collaboration_instructions: bool = False,
174
- include_permissions_instructions: bool = True,
175
- include_skills_instructions: bool = True,
176
- network_access: str = "enabled",
177
- ) -> ContextManager:
169
+ config_path: 'typing.Union[str, Path]',
170
+ profile: 'typing.Union[str, None]' = None,
171
+ base_instructions_override: 'typing.Union[str, None]' = None,
172
+ collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
173
+ include_collaboration_instructions: 'bool' = False,
174
+ include_permissions_instructions: 'bool' = True,
175
+ include_skills_instructions: 'bool' = True,
176
+ network_access: 'str' = "enabled",
177
+ ) -> 'ContextManager':
178
178
  config = ContextConfig.from_codex_config(config_path, profile)
179
179
  return cls(
180
180
  base_instructions_override=base_instructions_override,
@@ -187,11 +187,11 @@ class ContextManager:
187
187
  )
188
188
 
189
189
  @property
190
- def collaboration_mode(self) -> CollaborationMode:
190
+ def collaboration_mode(self) -> 'CollaborationMode':
191
191
  return self._collaboration_mode
192
192
 
193
- def get_turn_metadata(self, turn_id: str) -> JSONDict:
194
- metadata: JSONDict = {"turn_id": turn_id}
193
+ def get_turn_metadata(self, turn_id: 'str') -> 'JSONDict':
194
+ metadata: 'JSONDict' = {"turn_id": turn_id}
195
195
  if self._workspace_metadata_turn_id is None:
196
196
  self._workspace_metadata_turn_id = turn_id
197
197
  self._workspace_metadata_cache = get_workspace_turn_metadata(self.cwd)
@@ -205,12 +205,12 @@ class ContextManager:
205
205
 
206
206
  def build_prompt(
207
207
  self,
208
- history: tuple[ConversationItem, ...] | list[ConversationItem],
209
- tools: list[ToolSpec],
210
- parallel_tool_calls: bool,
211
- turn_id: str | None = None,
212
- ) -> Prompt:
213
- input_items: list[ConversationItem] = []
208
+ history: 'typing.Union[typing.Tuple[ConversationItem, ...], typing.List[ConversationItem]]',
209
+ tools: 'typing.List[ToolSpec]',
210
+ parallel_tool_calls: 'bool',
211
+ turn_id: 'typing.Union[str, None]' = None,
212
+ ) -> 'Prompt':
213
+ input_items: 'typing.List[ConversationItem]' = []
214
214
  turn_metadata = self.get_turn_metadata(turn_id) if turn_id is not None else None
215
215
 
216
216
  developer_message = self._build_developer_message()
@@ -228,7 +228,7 @@ class ContextManager:
228
228
  turn_metadata=turn_metadata,
229
229
  )
230
230
 
231
- def resolve_base_instructions(self) -> str:
231
+ def resolve_base_instructions(self) -> 'str':
232
232
  if self._base_instructions_override is not None:
233
233
  return self._base_instructions_override
234
234
  if self._config.base_instructions is not None:
@@ -240,7 +240,7 @@ class ContextManager:
240
240
  return resolved
241
241
  return self._default_base_instructions
242
242
 
243
- def _resolve_model_instructions(self) -> str | None:
243
+ def _resolve_model_instructions(self) -> 'typing.Union[str, None]':
244
244
  model_slug = self._config.model
245
245
  if model_slug is None:
246
246
  return None
@@ -264,8 +264,8 @@ class ContextManager:
264
264
  return base_instructions
265
265
  return None
266
266
 
267
- def _build_developer_message(self) -> ContextMessage | None:
268
- sections: list[str] = []
267
+ def _build_developer_message(self) -> 'typing.Union[ContextMessage, None]':
268
+ sections: 'typing.List[str]' = []
269
269
  if self._include_permissions_instructions:
270
270
  permissions = self._build_permissions_instructions()
271
271
  if permissions is not None:
@@ -290,7 +290,7 @@ class ContextManager:
290
290
  content_items=tuple(_input_text_item(section) for section in sections),
291
291
  )
292
292
 
293
- def _build_permissions_instructions(self) -> str | None:
293
+ def _build_permissions_instructions(self) -> 'typing.Union[str, None]':
294
294
  sandbox_mode = self._config.sandbox_mode or "danger-full-access"
295
295
  approval_policy = self._config.approval_policy or "never"
296
296
  sandbox_prompt_name = sandbox_mode.replace("-", "_")
@@ -318,7 +318,7 @@ class ContextManager:
318
318
  ]
319
319
  )
320
320
 
321
- def _build_skills_instructions(self) -> str | None:
321
+ def _build_skills_instructions(self) -> 'typing.Union[str, None]':
322
322
  skills = self._discover_skills()
323
323
  if not skills:
324
324
  return None
@@ -338,15 +338,15 @@ class ContextManager:
338
338
  body = "\n".join(lines)
339
339
  return f"{SKILLS_OPEN_TAG}\n{body}\n{SKILLS_CLOSE_TAG}"
340
340
 
341
- def _discover_skills(self) -> list[SkillDescriptor]:
341
+ def _discover_skills(self) -> 'typing.List[SkillDescriptor]':
342
342
  codex_home = self._config.codex_home
343
343
  if codex_home is None:
344
344
  return []
345
345
 
346
346
  user_root = codex_home / "skills"
347
347
  system_root = user_root / ".system"
348
- discovered: list[SkillDescriptor] = []
349
- seen: set[Path] = set()
348
+ discovered: 'typing.List[SkillDescriptor]' = []
349
+ seen: 'typing.Set[Path]' = set()
350
350
 
351
351
  user_paths = _discover_skill_files(user_root, excluded_root=system_root)
352
352
  system_paths = _discover_skill_files(system_root)
@@ -366,8 +366,8 @@ class ContextManager:
366
366
  key=lambda skill: (skill.scope_rank, skill.name, skill.path_to_skill_md),
367
367
  )
368
368
 
369
- def _build_contextual_user_messages(self) -> list[ContextMessage]:
370
- sections: list[str] = []
369
+ def _build_contextual_user_messages(self) -> 'typing.List[ContextMessage]':
370
+ sections: 'typing.List[str]' = []
371
371
  user_instructions = self._merged_user_instructions()
372
372
  if user_instructions is not None:
373
373
  sections.append(
@@ -386,8 +386,8 @@ class ContextManager:
386
386
  )
387
387
  ]
388
388
 
389
- def _merged_user_instructions(self) -> str | None:
390
- parts: list[str] = []
389
+ def _merged_user_instructions(self) -> 'typing.Union[str, None]':
390
+ parts: 'typing.List[str]' = []
391
391
  if self._config.user_instructions is not None:
392
392
  parts.append(self._config.user_instructions)
393
393
  if self._config.codex_home_instructions is not None:
@@ -402,8 +402,8 @@ class ContextManager:
402
402
 
403
403
  return "\n\n".join(parts) or None
404
404
 
405
- def _read_project_docs(self) -> str | None:
406
- docs: list[str] = []
405
+ def _read_project_docs(self) -> 'typing.Union[str, None]':
406
+ docs: 'typing.List[str]' = []
407
407
  remaining = self._config.project_doc_max_bytes
408
408
  for path in self._discover_project_doc_paths():
409
409
  text = path.read_text()
@@ -421,9 +421,9 @@ class ContextManager:
421
421
  return None
422
422
  return "\n\n".join(docs)
423
423
 
424
- def _discover_project_doc_paths(self) -> list[Path]:
425
- seen: set[Path] = set()
426
- discovered: list[Path] = []
424
+ def _discover_project_doc_paths(self) -> 'typing.List[Path]':
425
+ seen: 'typing.Set[Path]' = set()
426
+ discovered: 'typing.List[Path]' = []
427
427
 
428
428
  search_dirs = self._project_search_dirs()
429
429
  for directory in search_dirs:
@@ -435,9 +435,9 @@ class ContextManager:
435
435
  break
436
436
  return discovered
437
437
 
438
- def _project_search_dirs(self) -> list[Path]:
438
+ def _project_search_dirs(self) -> 'typing.List[Path]':
439
439
  project_root = self._find_project_root()
440
- directories: list[Path] = []
440
+ directories: 'typing.List[Path]' = []
441
441
  current = self.cwd
442
442
  chain = [current]
443
443
  while current != project_root and current.parent != current:
@@ -447,13 +447,13 @@ class ContextManager:
447
447
  directories.extend(chain)
448
448
  return directories
449
449
 
450
- def _find_project_root(self) -> Path:
450
+ def _find_project_root(self) -> 'Path':
451
451
  for ancestor in [self.cwd, *self.cwd.parents]:
452
452
  if (ancestor / ".git").exists():
453
453
  return ancestor
454
454
  return self.cwd
455
455
 
456
- def _serialize_environment_context(self) -> str:
456
+ def _serialize_environment_context(self) -> 'str':
457
457
  lines = [
458
458
  "<environment_context>",
459
459
  f" <cwd>{self.cwd}</cwd>",
@@ -465,30 +465,30 @@ class ContextManager:
465
465
  return "\n".join(lines)
466
466
 
467
467
 
468
- def _input_text_item(text: str) -> JSONDict:
468
+ def _input_text_item(text: 'str') -> 'JSONDict':
469
469
  return {"type": "input_text", "text": text}
470
470
 
471
471
 
472
- def _normalize_text(value) -> str | None:
472
+ def _normalize_text(value) -> 'typing.Union[str, None]':
473
473
  if value is None:
474
474
  return None
475
475
  text = str(value).strip()
476
476
  return text or None
477
477
 
478
478
 
479
- def _normalize_int(value) -> int | None:
479
+ def _normalize_int(value) -> 'typing.Union[int, None]':
480
480
  if value is None:
481
481
  return None
482
482
  return int(value)
483
483
 
484
484
 
485
- def _default_collaboration_instructions(mode: CollaborationMode) -> str:
485
+ def _default_collaboration_instructions(mode: 'CollaborationMode') -> 'str':
486
486
  if mode == "plan":
487
487
  return PLAN_COLLABORATION_INSTRUCTIONS_PATH.read_text()
488
488
  return DEFAULT_COLLABORATION_INSTRUCTIONS_PATH.read_text()
489
489
 
490
490
 
491
- def _read_first_instruction_file(base: Path) -> str | None:
491
+ def _read_first_instruction_file(base: 'Path') -> 'typing.Union[str, None]':
492
492
  for candidate_name in (LOCAL_PROJECT_DOC_FILENAME, DEFAULT_PROJECT_DOC_FILENAME):
493
493
  candidate = base / candidate_name
494
494
  try:
@@ -502,10 +502,10 @@ def _read_first_instruction_file(base: Path) -> str | None:
502
502
 
503
503
 
504
504
  @lru_cache(maxsize=1)
505
- def _load_models_by_slug() -> dict[str, JSONDict]:
505
+ def _load_models_by_slug() -> 'typing.Dict[str, JSONDict]':
506
506
  payload = json.loads(DEFAULT_MODELS_PATH.read_text())
507
507
  models = payload.get("models", [])
508
- by_slug: dict[str, JSONDict] = {}
508
+ by_slug: 'typing.Dict[str, JSONDict]' = {}
509
509
  for model in models:
510
510
  slug = model.get("slug")
511
511
  if isinstance(slug, str):
@@ -513,7 +513,7 @@ def _load_models_by_slug() -> dict[str, JSONDict]:
513
513
  return by_slug
514
514
 
515
515
 
516
- def _resolve_personality_message(variables, personality: str | None) -> str:
516
+ def _resolve_personality_message(variables, personality: 'typing.Union[str, None]') -> 'str':
517
517
  if not isinstance(variables, dict):
518
518
  return ""
519
519
  normalized = (personality or "").strip().lower()
@@ -532,13 +532,13 @@ def _resolve_personality_message(variables, personality: str | None) -> str:
532
532
 
533
533
 
534
534
  def _discover_skill_files(
535
- root: Path,
536
- excluded_root: Path | None = None,
537
- ) -> list[Path]:
535
+ root: 'Path',
536
+ excluded_root: 'typing.Union[Path, None]' = None,
537
+ ) -> 'typing.List[Path]':
538
538
  if not root.exists() or not root.is_dir():
539
539
  return []
540
540
  excluded = excluded_root.resolve() if excluded_root is not None and excluded_root.exists() else None
541
- paths: list[Path] = []
541
+ paths: 'typing.List[Path]' = []
542
542
  for path in root.glob("**/SKILL.md"):
543
543
  resolved = path.resolve()
544
544
  if excluded is not None and (resolved == excluded or excluded in resolved.parents):
@@ -547,7 +547,7 @@ def _discover_skill_files(
547
547
  return sorted(paths)
548
548
 
549
549
 
550
- def _parse_skill_descriptor(path: Path, scope_rank: int) -> SkillDescriptor | None:
550
+ def _parse_skill_descriptor(path: 'Path', scope_rank: 'int') -> 'typing.Union[SkillDescriptor, None]':
551
551
  text = path.read_text()
552
552
  if not text.startswith("---\n"):
553
553
  return None
@@ -556,7 +556,7 @@ def _parse_skill_descriptor(path: Path, scope_rank: int) -> SkillDescriptor | No
556
556
  if end_index == -1:
557
557
  return None
558
558
  frontmatter = text[4:end_index]
559
- fields: dict[str, str] = {}
559
+ fields: 'typing.Dict[str, str]' = {}
560
560
  for line in frontmatter.splitlines():
561
561
  if ":" not in line:
562
562
  continue
@@ -574,7 +574,7 @@ def _parse_skill_descriptor(path: Path, scope_rank: int) -> SkillDescriptor | No
574
574
  )
575
575
 
576
576
 
577
- def _strip_yaml_string(value: str) -> str:
577
+ def _strip_yaml_string(value: 'str') -> 'str':
578
578
  if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
579
579
  return value[1:-1]
580
580
  return value
pycodex/doctor.py CHANGED
@@ -1,4 +1,3 @@
1
- from __future__ import annotations
2
1
 
3
2
  import asyncio
4
3
  import json
@@ -14,31 +13,32 @@ import requests
14
13
  from .model import ResponsesModelClient, ResponsesProviderConfig
15
14
  from .protocol import AssistantMessage, Prompt, UserMessage
16
15
  from .utils.dotenv import DOTENV_FILENAME, load_codex_dotenv
16
+ import typing
17
17
 
18
18
 
19
- @dataclass(slots=True)
19
+ @dataclass
20
20
  class DoctorCheck:
21
- name: str
22
- ok: bool
23
- detail: str
21
+ name: 'str'
22
+ ok: 'bool'
23
+ detail: 'str'
24
24
 
25
25
 
26
- @dataclass(slots=True)
26
+ @dataclass
27
27
  class DoctorReport:
28
- ok: bool
29
- config_path: str
30
- dotenv_path: str
31
- profile: str | None
32
- provider_name: str | None = None
33
- model: str | None = None
34
- base_url: str | None = None
35
- responses_url: str | None = None
36
- api_key_env: str | None = None
37
- api_key_loaded: bool = False
38
- checks: list[DoctorCheck] = field(default_factory=list)
39
- live_output_text: str | None = None
40
-
41
- def to_dict(self) -> dict[str, object]:
28
+ ok: 'bool'
29
+ config_path: 'str'
30
+ dotenv_path: 'str'
31
+ profile: 'typing.Union[str, None]'
32
+ provider_name: 'typing.Union[str, None]' = None
33
+ model: 'typing.Union[str, None]' = None
34
+ base_url: 'typing.Union[str, None]' = None
35
+ responses_url: 'typing.Union[str, None]' = None
36
+ api_key_env: 'typing.Union[str, None]' = None
37
+ api_key_loaded: 'bool' = False
38
+ checks: 'typing.List[DoctorCheck]' = field(default_factory=list)
39
+ live_output_text: 'typing.Union[str, None]' = None
40
+
41
+ def to_dict(self) -> 'typing.Dict[str, object]':
42
42
  return asdict(self)
43
43
 
44
44
 
@@ -79,11 +79,11 @@ def build_doctor_parser():
79
79
 
80
80
 
81
81
  async def collect_doctor_report(
82
- config_path: str | Path,
83
- profile: str | None = None,
84
- timeout_seconds: float = 120.0,
85
- skip_live: bool = False,
86
- ) -> DoctorReport:
82
+ config_path: 'typing.Union[str, Path]',
83
+ profile: 'typing.Union[str, None]' = None,
84
+ timeout_seconds: 'float' = 120.0,
85
+ skip_live: 'bool' = False,
86
+ ) -> 'DoctorReport':
87
87
  config_file = Path(config_path).expanduser().resolve()
88
88
  dotenv_file = config_file.parent / DOTENV_FILENAME
89
89
  report = DoctorReport(
@@ -227,7 +227,7 @@ async def collect_doctor_report(
227
227
  return _finalize_report(report)
228
228
 
229
229
 
230
- async def run_doctor_cli(args) -> int:
230
+ async def run_doctor_cli(args) -> 'int':
231
231
  report = await collect_doctor_report(
232
232
  args.config,
233
233
  args.profile,
@@ -243,7 +243,7 @@ async def run_doctor_cli(args) -> int:
243
243
  return 0 if report.ok else 1
244
244
 
245
245
 
246
- def format_doctor_report(report: DoctorReport) -> str:
246
+ def format_doctor_report(report: 'DoctorReport') -> 'str':
247
247
  lines = [
248
248
  f"config: {report.config_path}",
249
249
  f"dotenv: {report.dotenv_path}",
@@ -267,7 +267,7 @@ def format_doctor_report(report: DoctorReport) -> str:
267
267
  return "\n".join(lines)
268
268
 
269
269
 
270
- def _loaded_api_key(provider_config: ResponsesProviderConfig) -> str:
270
+ def _loaded_api_key(provider_config: 'ResponsesProviderConfig') -> 'str':
271
271
  try:
272
272
  return provider_config.api_key()
273
273
  except Exception:
@@ -275,11 +275,11 @@ def _loaded_api_key(provider_config: ResponsesProviderConfig) -> str:
275
275
 
276
276
 
277
277
  def _probe_transport(
278
- scheme: str,
279
- host: str,
280
- port: int,
281
- timeout_seconds: float,
282
- ) -> tuple[bool, str]:
278
+ scheme: 'str',
279
+ host: 'str',
280
+ port: 'int',
281
+ timeout_seconds: 'float',
282
+ ) -> 'typing.Tuple[bool, str]':
283
283
  started = time.perf_counter()
284
284
  try:
285
285
  with socket.create_connection((host, port), timeout=timeout_seconds) as sock:
@@ -298,7 +298,7 @@ def _probe_transport(
298
298
  return True, f"{label} {host}:{port} connected in {elapsed:.2f}s"
299
299
 
300
300
 
301
- def _proxy_detail(proxies: dict[str, str]) -> str:
301
+ def _proxy_detail(proxies: 'typing.Dict[str, str]') -> 'str':
302
302
  if not proxies:
303
303
  return "not configured"
304
304
  return ", ".join(
@@ -306,7 +306,7 @@ def _proxy_detail(proxies: dict[str, str]) -> str:
306
306
  )
307
307
 
308
308
 
309
- def _redact_proxy_url(value: str) -> str:
309
+ def _redact_proxy_url(value: 'str') -> 'str':
310
310
  parsed = urllib.parse.urlsplit(value)
311
311
  if not parsed.scheme or not parsed.netloc:
312
312
  return value
@@ -319,10 +319,10 @@ def _redact_proxy_url(value: str) -> str:
319
319
 
320
320
 
321
321
  async def _run_live_check(
322
- config_path: Path,
323
- profile: str | None,
324
- timeout_seconds: float,
325
- ) -> tuple[bool, str, str | None]:
322
+ config_path: 'Path',
323
+ profile: 'typing.Union[str, None]',
324
+ timeout_seconds: 'float',
325
+ ) -> 'typing.Tuple[bool, str, typing.Union[str, None]]':
326
326
  client = ResponsesModelClient.from_codex_config(
327
327
  config_path,
328
328
  profile,
@@ -355,6 +355,6 @@ async def _run_live_check(
355
355
  return True, f"completed in {elapsed:.2f}s", output_text
356
356
 
357
357
 
358
- def _finalize_report(report: DoctorReport) -> DoctorReport:
358
+ def _finalize_report(report: 'DoctorReport') -> 'DoctorReport':
359
359
  report.ok = all(check.ok for check in report.checks)
360
360
  return report