codex-configure 0.2.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,3 @@
1
+ """Codex environment selector."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+
4
+ raise SystemExit(main())
@@ -0,0 +1,211 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import urllib.error
8
+ import urllib.request
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from . import __version__
14
+ from .errors import UserFacingError
15
+ from .providers import UMICH_TOOLKIT_DISCOVERY_URL
16
+
17
+
18
+ UMICH_MODELS_URL = UMICH_TOOLKIT_DISCOVERY_URL
19
+ FALLBACK_MODEL_IDS = ("gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna")
20
+ VERIFIED_MODEL_IDS = {"gpt-5.6-terra"}
21
+
22
+
23
+ def is_default_model_slug(slug: str) -> bool:
24
+ """Whether setup should check this compatible model by default."""
25
+
26
+ return slug == "gpt-5.6" or slug.startswith("gpt-5.6-")
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class ModelChoice:
31
+ slug: str
32
+ display_name: str
33
+ status: str
34
+ catalog_entry: dict[str, Any]
35
+ selectable: bool = True
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class CatalogResult:
40
+ models: tuple[ModelChoice, ...]
41
+ source: str
42
+ warning: str | None = None
43
+
44
+ @property
45
+ def selectable_models(self) -> tuple[ModelChoice, ...]:
46
+ return tuple(model for model in self.models if model.selectable)
47
+
48
+ @property
49
+ def advertised_ids(self) -> tuple[str, ...]:
50
+ return tuple(model.slug for model in self.models)
51
+
52
+
53
+ class CatalogService:
54
+ def __init__(
55
+ self,
56
+ codex_home: Path,
57
+ codex_command: str = "codex",
58
+ models_url: str = UMICH_MODELS_URL,
59
+ timeout_seconds: float = 10.0,
60
+ ) -> None:
61
+ self.codex_home = codex_home
62
+ self.codex_command = codex_command
63
+ self.models_url = models_url
64
+ self.timeout_seconds = timeout_seconds
65
+
66
+ def discover(self, api_key: str | None = None) -> CatalogResult:
67
+ """Discover all endpoint IDs and mark entries absent from Core metadata.
68
+
69
+ The Toolkit endpoint is key-scoped even though it currently advertises
70
+ the same public list for many keys. Always send the key when one is
71
+ available. A key-backed discovery failure is fatal: writing a
72
+ provider catalog from a maintained fallback would make the catalog
73
+ claim models the user's endpoint did not advertise.
74
+
75
+ The no-key fallback remains for the legacy OpenAI/U-M interactive flow;
76
+ new named-provider setup always supplies a key.
77
+ """
78
+ bundled = self._load_bundled_catalog()
79
+ bundled_by_slug = {
80
+ entry.get("slug"): entry
81
+ for entry in bundled
82
+ if isinstance(entry, dict) and isinstance(entry.get("slug"), str)
83
+ }
84
+
85
+ warning = None
86
+ try:
87
+ advertised_ids = self._fetch_umich_model_ids(api_key=api_key)
88
+ source = self.models_url
89
+ except (OSError, ValueError, urllib.error.URLError) as exc:
90
+ if api_key:
91
+ raise UserFacingError(
92
+ f"Could not discover models for the U-M Toolkit key ({type(exc).__name__})."
93
+ ) from exc
94
+ advertised_ids = set(FALLBACK_MODEL_IDS)
95
+ source = "maintained fallback"
96
+ warning = f"U-M model discovery failed ({type(exc).__name__}); using the maintained fallback."
97
+
98
+ if not advertised_ids:
99
+ advertised_ids = set(FALLBACK_MODEL_IDS).intersection(bundled_by_slug)
100
+ source = "maintained fallback"
101
+ warning = "U-M returned no models recognized by this Codex build; using the maintained fallback."
102
+
103
+ # Preserve every endpoint ID in the result so the setup UI can explain
104
+ # why a model is unavailable. Only a bundled, API-supported entry is
105
+ # selectable and therefore eligible for the authoritative JSON we
106
+ # write to disk.
107
+ choices = []
108
+ for slug in sorted(advertised_ids, key=lambda value: (value.casefold(), value)):
109
+ entry = bundled_by_slug.get(slug)
110
+ selectable = isinstance(entry, dict) and entry.get("supported_in_api", True)
111
+ if not selectable:
112
+ choices.append(
113
+ ModelChoice(
114
+ slug=slug,
115
+ display_name=slug,
116
+ # The CLI treats this exact status as non-selectable;
117
+ # the explanatory wording belongs in its label/help.
118
+ status="unsupported",
119
+ catalog_entry={},
120
+ selectable=False,
121
+ )
122
+ )
123
+ continue
124
+ choices.append(
125
+ ModelChoice(
126
+ slug=slug,
127
+ display_name=str(entry.get("display_name") or slug),
128
+ status="verified" if slug in VERIFIED_MODEL_IDS else "listed",
129
+ catalog_entry=entry,
130
+ selectable=True,
131
+ )
132
+ )
133
+
134
+ if not choices:
135
+ raise UserFacingError(
136
+ "The U-M endpoint returned no model identifiers."
137
+ )
138
+
139
+ return CatalogResult(models=tuple(choices), source=source, warning=warning)
140
+
141
+ def build_selected_catalog(self, models: list[ModelChoice]) -> dict[str, Any]:
142
+ selected = []
143
+ for priority, model in enumerate(models, start=1):
144
+ if not model.selectable or not model.catalog_entry:
145
+ raise UserFacingError(
146
+ f"Model `{model.slug}` is not supported by this Codex build and cannot be selected."
147
+ )
148
+ entry = copy.deepcopy(model.catalog_entry)
149
+ entry["visibility"] = "list"
150
+ entry["priority"] = priority
151
+ selected.append(entry)
152
+ return {"models": selected}
153
+
154
+ def _load_bundled_catalog(self) -> list[dict[str, Any]]:
155
+ child_env = os.environ.copy()
156
+ child_env["CODEX_HOME"] = str(self.codex_home)
157
+ try:
158
+ completed = subprocess.run(
159
+ [self.codex_command, "debug", "models", "--bundled"],
160
+ check=False,
161
+ capture_output=True,
162
+ text=True,
163
+ env=child_env,
164
+ timeout=20,
165
+ )
166
+ except (OSError, subprocess.TimeoutExpired) as exc:
167
+ raise UserFacingError(f"Could not inspect the installed Codex model catalog: {exc}") from exc
168
+ if completed.returncode != 0:
169
+ detail = completed.stderr.strip().splitlines()[-1] if completed.stderr.strip() else "unknown error"
170
+ raise UserFacingError(f"Could not inspect the installed Codex model catalog: {detail}")
171
+ try:
172
+ payload = json.loads(completed.stdout)
173
+ models = payload["models"]
174
+ except (json.JSONDecodeError, KeyError, TypeError) as exc:
175
+ raise UserFacingError("The installed Codex command returned an invalid model catalog.") from exc
176
+ if not isinstance(models, list):
177
+ raise UserFacingError("The installed Codex command returned an invalid model catalog.")
178
+ return models
179
+
180
+ def _fetch_umich_model_ids(self, api_key: str | None = None) -> set[str]:
181
+ headers = {"Accept": "application/json", "User-Agent": f"codex-configure/{__version__}"}
182
+ if api_key:
183
+ headers["x-portkey-api-key"] = api_key
184
+ request = urllib.request.Request(
185
+ self.models_url,
186
+ headers=headers,
187
+ )
188
+ with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
189
+ payload = json.load(response)
190
+ data = payload.get("data")
191
+ if not isinstance(data, list):
192
+ raise ValueError("model response has no data list")
193
+ ids = {
194
+ item["id"]
195
+ for item in data
196
+ if isinstance(item, dict) and isinstance(item.get("id"), str)
197
+ }
198
+ if not ids:
199
+ raise ValueError("model response contains no identifiers")
200
+ return ids
201
+
202
+
203
+ __all__ = [
204
+ "CatalogResult",
205
+ "CatalogService",
206
+ "FALLBACK_MODEL_IDS",
207
+ "ModelChoice",
208
+ "UMICH_MODELS_URL",
209
+ "VERIFIED_MODEL_IDS",
210
+ "is_default_model_slug",
211
+ ]