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.
- codex_configure/__init__.py +3 -0
- codex_configure/__main__.py +4 -0
- codex_configure/catalog.py +211 -0
- codex_configure/cli.py +725 -0
- codex_configure/errors.py +2 -0
- codex_configure/patcher.py +636 -0
- codex_configure/providers.py +522 -0
- codex_configure/runtime.py +951 -0
- codex_configure-0.2.0.dist-info/METADATA +201 -0
- codex_configure-0.2.0.dist-info/RECORD +22 -0
- codex_configure-0.2.0.dist-info/WHEEL +5 -0
- codex_configure-0.2.0.dist-info/entry_points.txt +2 -0
- codex_configure-0.2.0.dist-info/licenses/LICENSE +201 -0
- codex_configure-0.2.0.dist-info/licenses/NOTICE +12 -0
- codex_configure-0.2.0.dist-info/top_level.txt +2 -0
- core_provider_model_picker/README.md +90 -0
- core_provider_model_picker/__init__.py +1 -0
- core_provider_model_picker/app_server_canary.py +474 -0
- core_provider_model_picker/codex-provider-model-picker.patch +2884 -0
- core_provider_model_picker/prepare.sh +127 -0
- core_provider_model_picker/test_canary.py +126 -0
- core_provider_model_picker/upstream-pin.env +5 -0
|
@@ -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
|
+
]
|