claude-multiacc 2.0.57 → 2.0.59
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.
- package/README.md +111 -3
- package/bin/claude +323 -84
- package/bin/claude-accounts +273 -42
- package/bin/codex +15 -0
- package/bin/codex-accounts +31 -4
- package/docs/ACCOUNT_OPERATIONS.md +62 -2
- package/docs/AUTORESUME-HANDOFF.md +30 -3
- package/docs/AUTORESUME.md +275 -18
- package/docs/CODEX.md +1 -0
- package/docs/INSTALLATION.md +7 -1
- package/docs/VERIFICATION.md +4 -0
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
- package/lib/__pycache__/report.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/audit.py +88 -22
- package/lib/autoresume.py +1197 -82
- package/lib/autoresume_cross.py +3 -1
- package/lib/autoresume_handoff.py +82 -10
- package/lib/autoresume_probe.py +2 -2
- package/lib/claude_client_error.py +9 -4
- package/lib/codex_trust.py +263 -0
- package/lib/common.sh +131 -3
- package/lib/credential.py +5 -2
- package/lib/install_actions.sh +19 -0
- package/lib/keychain.py +180 -36
- package/lib/mcp_oauth.py +1748 -0
- package/lib/model_catalog.json +404 -0
- package/lib/model_catalog.py +201 -0
- package/lib/model_match.py +104 -0
- package/lib/report.py +8 -5
- package/lib/session_settings.py +123 -0
- package/package.json +2 -2
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
- package/tests/fake_codex_tui.py +25 -0
- package/tests/fake_security.sh +24 -2
- package/tests/fixtures/cross_provider_cli.py +111 -1
- package/tests/run-tests.sh +1028 -23
- package/tests/test_autoresume.py +215 -8
- package/tests/test_autoresume_catalog.py +226 -0
- package/tests/test_autoresume_catalog_cli.py +185 -0
- package/tests/test_autoresume_codex_records.py +16 -0
- package/tests/test_autoresume_codex_trust.py +247 -0
- package/tests/test_autoresume_cross_guards.py +28 -0
- package/tests/test_autoresume_cross_origin.py +70 -0
- package/tests/test_autoresume_cross_tmux.py +27 -2
- package/tests/test_autoresume_cross_tokens.py +32 -2
- package/tests/test_autoresume_handoff_argv.py +120 -16
- package/tests/test_autoresume_handoff_origins.py +67 -0
- package/tests/test_autoresume_memory.py +1141 -0
- package/tests/test_autoresume_memory_tmux.py +94 -0
- package/tests/test_autoresume_probe.py +2 -1
- package/tests/test_autoresume_relaunch_settings.py +317 -0
- package/tests/test_autoresume_settings.py +135 -0
- package/tests/test_autoresume_sidechain.py +1518 -0
- package/tests/test_autoresume_sidechain_tmux.py +79 -0
- package/tests/test_autoresume_trust_tmux.py +63 -0
- package/tests/test_claude_client_error.py +10 -7
- package/tests/test_claude_org_block.py +1 -0
- package/tests/test_claude_pool_prepare.py +4 -1
- package/tests/test_claude_reset.py +3 -0
- package/tests/test_keychain_nologin.py +340 -0
- package/tests/test_mcp_oauth.py +1414 -0
- package/tests/test_packaged_commands.py +41 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
{
|
|
2
|
+
"claude": {
|
|
3
|
+
"efforts": [
|
|
4
|
+
"low",
|
|
5
|
+
"medium",
|
|
6
|
+
"high",
|
|
7
|
+
"xhigh",
|
|
8
|
+
"max"
|
|
9
|
+
],
|
|
10
|
+
"families": {
|
|
11
|
+
"fable": 5,
|
|
12
|
+
"haiku": 2,
|
|
13
|
+
"mythos": 5,
|
|
14
|
+
"opus": 4,
|
|
15
|
+
"sonnet": 3
|
|
16
|
+
},
|
|
17
|
+
"fetched_at": "2026-10-14T08:49:41Z",
|
|
18
|
+
"models": {
|
|
19
|
+
"claude-fable-5": {
|
|
20
|
+
"default_effort": "high",
|
|
21
|
+
"efforts": [
|
|
22
|
+
"low",
|
|
23
|
+
"medium",
|
|
24
|
+
"high",
|
|
25
|
+
"xhigh",
|
|
26
|
+
"max"
|
|
27
|
+
],
|
|
28
|
+
"family": "fable",
|
|
29
|
+
"listed": false,
|
|
30
|
+
"upgrade": null,
|
|
31
|
+
"version": [
|
|
32
|
+
5
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"claude-fable-5-1": {
|
|
36
|
+
"default_effort": "high",
|
|
37
|
+
"efforts": [
|
|
38
|
+
"low",
|
|
39
|
+
"medium",
|
|
40
|
+
"high",
|
|
41
|
+
"xhigh",
|
|
42
|
+
"max"
|
|
43
|
+
],
|
|
44
|
+
"family": "fable",
|
|
45
|
+
"listed": true,
|
|
46
|
+
"upgrade": null,
|
|
47
|
+
"version": [
|
|
48
|
+
5,
|
|
49
|
+
1
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"claude-haiku-4-5-20251001": {
|
|
53
|
+
"default_effort": null,
|
|
54
|
+
"efforts": [],
|
|
55
|
+
"family": "haiku",
|
|
56
|
+
"listed": false,
|
|
57
|
+
"upgrade": null,
|
|
58
|
+
"version": [
|
|
59
|
+
4,
|
|
60
|
+
5
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"claude-haiku-5-5": {
|
|
64
|
+
"default_effort": "medium",
|
|
65
|
+
"efforts": [
|
|
66
|
+
"low",
|
|
67
|
+
"medium",
|
|
68
|
+
"high",
|
|
69
|
+
"xhigh",
|
|
70
|
+
"max"
|
|
71
|
+
],
|
|
72
|
+
"family": "haiku",
|
|
73
|
+
"listed": true,
|
|
74
|
+
"upgrade": null,
|
|
75
|
+
"version": [
|
|
76
|
+
5,
|
|
77
|
+
5
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"claude-opus-4-6": {
|
|
81
|
+
"default_effort": "high",
|
|
82
|
+
"efforts": [
|
|
83
|
+
"low",
|
|
84
|
+
"medium",
|
|
85
|
+
"high",
|
|
86
|
+
"max"
|
|
87
|
+
],
|
|
88
|
+
"family": "opus",
|
|
89
|
+
"listed": false,
|
|
90
|
+
"upgrade": null,
|
|
91
|
+
"version": [
|
|
92
|
+
4,
|
|
93
|
+
6
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"claude-opus-4-7": {
|
|
97
|
+
"default_effort": "xhigh",
|
|
98
|
+
"efforts": [
|
|
99
|
+
"low",
|
|
100
|
+
"medium",
|
|
101
|
+
"high",
|
|
102
|
+
"xhigh",
|
|
103
|
+
"max"
|
|
104
|
+
],
|
|
105
|
+
"family": "opus",
|
|
106
|
+
"listed": false,
|
|
107
|
+
"upgrade": null,
|
|
108
|
+
"version": [
|
|
109
|
+
4,
|
|
110
|
+
7
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
"claude-opus-4-8": {
|
|
114
|
+
"default_effort": "high",
|
|
115
|
+
"efforts": [
|
|
116
|
+
"low",
|
|
117
|
+
"medium",
|
|
118
|
+
"high",
|
|
119
|
+
"xhigh",
|
|
120
|
+
"max"
|
|
121
|
+
],
|
|
122
|
+
"family": "opus",
|
|
123
|
+
"listed": false,
|
|
124
|
+
"upgrade": null,
|
|
125
|
+
"version": [
|
|
126
|
+
4,
|
|
127
|
+
8
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
"claude-opus-5": {
|
|
131
|
+
"default_effort": "high",
|
|
132
|
+
"efforts": [
|
|
133
|
+
"low",
|
|
134
|
+
"medium",
|
|
135
|
+
"high",
|
|
136
|
+
"xhigh",
|
|
137
|
+
"max"
|
|
138
|
+
],
|
|
139
|
+
"family": "opus",
|
|
140
|
+
"listed": false,
|
|
141
|
+
"upgrade": null,
|
|
142
|
+
"version": [
|
|
143
|
+
5
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
"claude-opus-5-5": {
|
|
147
|
+
"default_effort": "medium",
|
|
148
|
+
"efforts": [
|
|
149
|
+
"low",
|
|
150
|
+
"medium",
|
|
151
|
+
"high",
|
|
152
|
+
"xhigh",
|
|
153
|
+
"max"
|
|
154
|
+
],
|
|
155
|
+
"family": "opus",
|
|
156
|
+
"listed": true,
|
|
157
|
+
"upgrade": null,
|
|
158
|
+
"version": [
|
|
159
|
+
5,
|
|
160
|
+
5
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
"claude-sonnet-4-6": {
|
|
164
|
+
"default_effort": "high",
|
|
165
|
+
"efforts": [
|
|
166
|
+
"low",
|
|
167
|
+
"medium",
|
|
168
|
+
"high",
|
|
169
|
+
"max"
|
|
170
|
+
],
|
|
171
|
+
"family": "sonnet",
|
|
172
|
+
"listed": false,
|
|
173
|
+
"upgrade": null,
|
|
174
|
+
"version": [
|
|
175
|
+
4,
|
|
176
|
+
6
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
"claude-sonnet-5": {
|
|
180
|
+
"default_effort": "high",
|
|
181
|
+
"efforts": [
|
|
182
|
+
"low",
|
|
183
|
+
"medium",
|
|
184
|
+
"high",
|
|
185
|
+
"xhigh",
|
|
186
|
+
"max"
|
|
187
|
+
],
|
|
188
|
+
"family": "sonnet",
|
|
189
|
+
"listed": false,
|
|
190
|
+
"upgrade": null,
|
|
191
|
+
"version": [
|
|
192
|
+
5
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
"claude-sonnet-5-5": {
|
|
196
|
+
"default_effort": "medium",
|
|
197
|
+
"efforts": [
|
|
198
|
+
"low",
|
|
199
|
+
"medium",
|
|
200
|
+
"high",
|
|
201
|
+
"xhigh",
|
|
202
|
+
"max"
|
|
203
|
+
],
|
|
204
|
+
"family": "sonnet",
|
|
205
|
+
"listed": true,
|
|
206
|
+
"upgrade": null,
|
|
207
|
+
"version": [
|
|
208
|
+
5,
|
|
209
|
+
5
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"source": "release"
|
|
214
|
+
},
|
|
215
|
+
"codex": {
|
|
216
|
+
"efforts": [
|
|
217
|
+
"none",
|
|
218
|
+
"minimal",
|
|
219
|
+
"low",
|
|
220
|
+
"medium",
|
|
221
|
+
"high",
|
|
222
|
+
"xhigh",
|
|
223
|
+
"max",
|
|
224
|
+
"ultra"
|
|
225
|
+
],
|
|
226
|
+
"families": {
|
|
227
|
+
"astra": 4,
|
|
228
|
+
"luna": 1,
|
|
229
|
+
"sol": 3,
|
|
230
|
+
"terra": 2
|
|
231
|
+
},
|
|
232
|
+
"fetched_at": "2026-10-14T08:54:28Z",
|
|
233
|
+
"models": {
|
|
234
|
+
"codex-auto-review": {
|
|
235
|
+
"default_effort": "medium",
|
|
236
|
+
"efforts": [
|
|
237
|
+
"low",
|
|
238
|
+
"medium",
|
|
239
|
+
"high",
|
|
240
|
+
"xhigh",
|
|
241
|
+
"max"
|
|
242
|
+
],
|
|
243
|
+
"family": null,
|
|
244
|
+
"listed": false,
|
|
245
|
+
"upgrade": null,
|
|
246
|
+
"version": []
|
|
247
|
+
},
|
|
248
|
+
"gpt-5.5": {
|
|
249
|
+
"default_effort": "medium",
|
|
250
|
+
"efforts": [
|
|
251
|
+
"low",
|
|
252
|
+
"medium",
|
|
253
|
+
"high",
|
|
254
|
+
"xhigh"
|
|
255
|
+
],
|
|
256
|
+
"family": null,
|
|
257
|
+
"listed": false,
|
|
258
|
+
"upgrade": "gpt-6.1-sol",
|
|
259
|
+
"version": [
|
|
260
|
+
5,
|
|
261
|
+
5
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
"gpt-5.6-luna": {
|
|
265
|
+
"default_effort": "medium",
|
|
266
|
+
"efforts": [
|
|
267
|
+
"low",
|
|
268
|
+
"medium",
|
|
269
|
+
"high",
|
|
270
|
+
"xhigh",
|
|
271
|
+
"max"
|
|
272
|
+
],
|
|
273
|
+
"family": "luna",
|
|
274
|
+
"listed": true,
|
|
275
|
+
"upgrade": null,
|
|
276
|
+
"version": [
|
|
277
|
+
5,
|
|
278
|
+
6
|
|
279
|
+
]
|
|
280
|
+
},
|
|
281
|
+
"gpt-5.6-sol": {
|
|
282
|
+
"default_effort": "low",
|
|
283
|
+
"efforts": [
|
|
284
|
+
"low",
|
|
285
|
+
"medium",
|
|
286
|
+
"high",
|
|
287
|
+
"xhigh",
|
|
288
|
+
"max",
|
|
289
|
+
"ultra"
|
|
290
|
+
],
|
|
291
|
+
"family": "sol",
|
|
292
|
+
"listed": true,
|
|
293
|
+
"upgrade": null,
|
|
294
|
+
"version": [
|
|
295
|
+
5,
|
|
296
|
+
6
|
|
297
|
+
]
|
|
298
|
+
},
|
|
299
|
+
"gpt-5.6-terra": {
|
|
300
|
+
"default_effort": "medium",
|
|
301
|
+
"efforts": [
|
|
302
|
+
"low",
|
|
303
|
+
"medium",
|
|
304
|
+
"high",
|
|
305
|
+
"xhigh",
|
|
306
|
+
"max",
|
|
307
|
+
"ultra"
|
|
308
|
+
],
|
|
309
|
+
"family": "terra",
|
|
310
|
+
"listed": true,
|
|
311
|
+
"upgrade": null,
|
|
312
|
+
"version": [
|
|
313
|
+
5,
|
|
314
|
+
6
|
|
315
|
+
]
|
|
316
|
+
},
|
|
317
|
+
"gpt-6-astra": {
|
|
318
|
+
"default_effort": "medium",
|
|
319
|
+
"efforts": [
|
|
320
|
+
"low",
|
|
321
|
+
"medium",
|
|
322
|
+
"high",
|
|
323
|
+
"xhigh",
|
|
324
|
+
"max",
|
|
325
|
+
"ultra"
|
|
326
|
+
],
|
|
327
|
+
"family": "astra",
|
|
328
|
+
"listed": true,
|
|
329
|
+
"upgrade": null,
|
|
330
|
+
"version": [
|
|
331
|
+
6
|
|
332
|
+
]
|
|
333
|
+
},
|
|
334
|
+
"gpt-6-luna": {
|
|
335
|
+
"default_effort": "medium",
|
|
336
|
+
"efforts": [
|
|
337
|
+
"low",
|
|
338
|
+
"medium",
|
|
339
|
+
"high",
|
|
340
|
+
"xhigh",
|
|
341
|
+
"max"
|
|
342
|
+
],
|
|
343
|
+
"family": "luna",
|
|
344
|
+
"listed": true,
|
|
345
|
+
"upgrade": null,
|
|
346
|
+
"version": [
|
|
347
|
+
6
|
|
348
|
+
]
|
|
349
|
+
},
|
|
350
|
+
"gpt-6-sol": {
|
|
351
|
+
"default_effort": "medium",
|
|
352
|
+
"efforts": [
|
|
353
|
+
"low",
|
|
354
|
+
"medium",
|
|
355
|
+
"high",
|
|
356
|
+
"xhigh",
|
|
357
|
+
"max",
|
|
358
|
+
"ultra"
|
|
359
|
+
],
|
|
360
|
+
"family": "sol",
|
|
361
|
+
"listed": true,
|
|
362
|
+
"upgrade": null,
|
|
363
|
+
"version": [
|
|
364
|
+
6
|
|
365
|
+
]
|
|
366
|
+
},
|
|
367
|
+
"gpt-6.1-sol": {
|
|
368
|
+
"default_effort": "low",
|
|
369
|
+
"efforts": [
|
|
370
|
+
"low",
|
|
371
|
+
"medium",
|
|
372
|
+
"high",
|
|
373
|
+
"xhigh",
|
|
374
|
+
"max",
|
|
375
|
+
"ultra"
|
|
376
|
+
],
|
|
377
|
+
"family": "sol",
|
|
378
|
+
"listed": true,
|
|
379
|
+
"upgrade": null,
|
|
380
|
+
"version": [
|
|
381
|
+
6,
|
|
382
|
+
1
|
|
383
|
+
]
|
|
384
|
+
},
|
|
385
|
+
"gpt-reserve": {
|
|
386
|
+
"default_effort": "medium",
|
|
387
|
+
"efforts": [
|
|
388
|
+
"low",
|
|
389
|
+
"medium",
|
|
390
|
+
"high",
|
|
391
|
+
"xhigh",
|
|
392
|
+
"max"
|
|
393
|
+
],
|
|
394
|
+
"family": null,
|
|
395
|
+
"listed": false,
|
|
396
|
+
"upgrade": null,
|
|
397
|
+
"version": []
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
"source": "release"
|
|
401
|
+
},
|
|
402
|
+
"generated_at": "2026-10-14T08:55:08Z",
|
|
403
|
+
"schema": 1
|
|
404
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The model/effort catalog of both providers, from the freshest credential-free source (3.8+).
|
|
3
|
+
|
|
4
|
+
Sources, freshest wins per provider: the catalog shipped beside this file (``model_catalog.json``,
|
|
5
|
+
a snapshot of the lineup, the fallback), a pool's refreshed copy (``<root>/model-catalog.json``, written by
|
|
6
|
+
``refresh`` on every install and self-update and kept current by the limits passes), and the
|
|
7
|
+
caches the CLIs themselves maintain while they run -- Codex's ``<acct>/models_cache.json``
|
|
8
|
+
(rewritten every few minutes by any app-server) and Claude Code's served catalog
|
|
9
|
+
``<acct>/cache/model-catalog/*-cc.json`` (hourly). No request is ever sent: the CLIs already
|
|
10
|
+
fetch these with their own credentials and budgets.
|
|
11
|
+
|
|
12
|
+
Shape: ``{"schema": 1, "<provider>": {"fetched_at": iso, "source": str, "efforts": [ascending],
|
|
13
|
+
"families": {family: tier}, "models": {id: {"family", "version", "efforts", "default_effort",
|
|
14
|
+
"listed", "upgrade"}}}}``. ``efforts``/``families`` are structure and come from the shipped file;
|
|
15
|
+
``models`` from the freshest source. ``lib/model_match.py`` does the matching.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import glob
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
import time
|
|
25
|
+
|
|
26
|
+
SHIPPED = os.path.join(os.path.dirname(os.path.abspath(__file__)), "model_catalog.json")
|
|
27
|
+
OVERLAY = "model-catalog.json"
|
|
28
|
+
PROVIDERS = ("claude", "codex")
|
|
29
|
+
CLAUDE_ID = re.compile(r"^claude-(?P<family>[a-z]+)(?:-(?P<version>\d+(?:-\d+)*?))?(?:-\d{8})?$")
|
|
30
|
+
CODEX_ID = re.compile(r"^gpt-(?P<version>\d+(?:\.\d+)*)(?:-(?P<family>[a-z]+))?$")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_id(provider, model):
|
|
34
|
+
"""(family, version tuple) of a model id; family None when the id is not understood."""
|
|
35
|
+
model = re.sub(r"\[\d+[km]\]$", "", model or "", flags=re.I).strip()
|
|
36
|
+
match = (CLAUDE_ID if provider == "claude" else CODEX_ID).match(model)
|
|
37
|
+
if not match:
|
|
38
|
+
return None, ()
|
|
39
|
+
version = match.group("version") or ""
|
|
40
|
+
return match.group("family"), tuple(int(part) for part in re.split(r"[.-]", version) if part)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read_json(path):
|
|
44
|
+
try:
|
|
45
|
+
with open(path, "rb") as fh:
|
|
46
|
+
return json.loads(fh.read().decode("utf-8"))
|
|
47
|
+
except (OSError, ValueError):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _iso(epoch):
|
|
52
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(epoch))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _entry(provider, model, efforts, default, listed, upgrade=None):
|
|
56
|
+
family, version = parse_id(provider, model)
|
|
57
|
+
return {"family": family, "version": list(version), "efforts": [e for e in efforts if isinstance(e, str)],
|
|
58
|
+
"default_effort": default if isinstance(default, str) else None, "listed": bool(listed),
|
|
59
|
+
"upgrade": upgrade if isinstance(upgrade, str) else None}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def codex_cache(path):
|
|
63
|
+
"""A ``models_cache.json`` as a provider section, or None (also for a half-written file)."""
|
|
64
|
+
doc = _read_json(path)
|
|
65
|
+
models = doc.get("models") if isinstance(doc, dict) else None
|
|
66
|
+
if not isinstance(models, list) or not isinstance(doc.get("fetched_at"), str):
|
|
67
|
+
return None
|
|
68
|
+
table = {}
|
|
69
|
+
for item in models:
|
|
70
|
+
if not isinstance(item, dict) or not isinstance(item.get("slug"), str):
|
|
71
|
+
continue
|
|
72
|
+
levels = item.get("supported_reasoning_levels") or []
|
|
73
|
+
efforts = [lvl.get("effort") for lvl in levels if isinstance(lvl, dict)]
|
|
74
|
+
upgrade = item.get("upgrade") if isinstance(item.get("upgrade"), dict) else {}
|
|
75
|
+
table[item["slug"]] = _entry("codex", item["slug"], efforts, item.get("default_reasoning_level"),
|
|
76
|
+
item.get("visibility") == "list", upgrade.get("model"))
|
|
77
|
+
return {"fetched_at": doc["fetched_at"][:19] + "Z", "source": path, "models": table} if table else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def claude_cache(path):
|
|
81
|
+
"""A served-catalog cache (``*-cc.json``) as a provider section, or None."""
|
|
82
|
+
doc = _read_json(path)
|
|
83
|
+
catalog = doc.get("catalog") if isinstance(doc, dict) else None
|
|
84
|
+
config = catalog.get("config") if isinstance(catalog, dict) else None
|
|
85
|
+
rows = config.get("models") if isinstance(config, dict) else None
|
|
86
|
+
if not isinstance(rows, list) or not isinstance(doc.get("fetchedAt"), (int, float)):
|
|
87
|
+
return None
|
|
88
|
+
state = catalog.get("state") if isinstance(catalog.get("state"), dict) else {}
|
|
89
|
+
defaults = {}
|
|
90
|
+
for item in state.get("thinking_by_model") or []:
|
|
91
|
+
thinking = item.get("thinking") if isinstance(item, dict) else None
|
|
92
|
+
if isinstance(thinking, dict) and isinstance(item.get("id"), str):
|
|
93
|
+
defaults[item["id"]] = thinking.get("effort")
|
|
94
|
+
table = {}
|
|
95
|
+
for row in rows:
|
|
96
|
+
if not isinstance(row, dict) or not isinstance(row.get("id"), str):
|
|
97
|
+
continue
|
|
98
|
+
thinking = row.get("thinking") if isinstance(row.get("thinking"), dict) else {}
|
|
99
|
+
options = [opt.get("id") for opt in thinking.get("effort_options") or [] if isinstance(opt, dict)]
|
|
100
|
+
table[row["id"]] = _entry("claude", row["id"], options, defaults.get(row["id"]),
|
|
101
|
+
row.get("section") == "main")
|
|
102
|
+
return {"fetched_at": _iso(doc["fetchedAt"] / 1000.0), "source": path, "models": table} if table else None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def sources(roots):
|
|
106
|
+
"""Every candidate section per provider: shipped, overlays, then the CLIs' own caches."""
|
|
107
|
+
found = {provider: [] for provider in PROVIDERS}
|
|
108
|
+
shipped = _read_json(SHIPPED) or {}
|
|
109
|
+
for provider in PROVIDERS:
|
|
110
|
+
if isinstance(shipped.get(provider), dict) and isinstance(shipped[provider].get("models"), dict):
|
|
111
|
+
found[provider].append(dict(shipped[provider], source=SHIPPED))
|
|
112
|
+
for root in [r for r in (roots or {}).values() if r]:
|
|
113
|
+
overlay = _read_json(os.path.join(root, OVERLAY)) or {}
|
|
114
|
+
for provider in PROVIDERS:
|
|
115
|
+
if isinstance(overlay.get(provider), dict) and isinstance(overlay[provider].get("models"), dict):
|
|
116
|
+
found[provider].append(dict(overlay[provider], source=os.path.join(root, OVERLAY)))
|
|
117
|
+
codex_root, claude_root = (roots or {}).get("codex"), (roots or {}).get("claude")
|
|
118
|
+
for path in glob.glob(os.path.join(codex_root, "acct-*", "models_cache.json")) if codex_root else []:
|
|
119
|
+
section = codex_cache(path)
|
|
120
|
+
if section:
|
|
121
|
+
found["codex"].append(section)
|
|
122
|
+
for path in glob.glob(os.path.join(claude_root, "acct-*", "cache", "model-catalog", "*-cc.json")) \
|
|
123
|
+
if claude_root else []:
|
|
124
|
+
section = claude_cache(path)
|
|
125
|
+
if section:
|
|
126
|
+
found["claude"].append(section)
|
|
127
|
+
return found
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def load(roots=None):
|
|
131
|
+
"""The merged catalog: structure from the shipped file, models from the freshest source."""
|
|
132
|
+
shipped = _read_json(SHIPPED) or {}
|
|
133
|
+
found = sources(roots)
|
|
134
|
+
catalog = {"schema": 1}
|
|
135
|
+
for provider in PROVIDERS:
|
|
136
|
+
base = shipped.get(provider) if isinstance(shipped.get(provider), dict) else {}
|
|
137
|
+
best = max(found[provider], key=lambda s: str(s.get("fetched_at") or ""), default=None)
|
|
138
|
+
catalog[provider] = {"efforts": list(base.get("efforts") or []), "families": dict(base.get("families") or {}),
|
|
139
|
+
"fetched_at": best.get("fetched_at") if best else None,
|
|
140
|
+
"source": best.get("source") if best else None,
|
|
141
|
+
"models": dict(best.get("models") or {}) if best else {}}
|
|
142
|
+
return catalog
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def refresh(roots, if_stale=False, max_age=3600):
|
|
146
|
+
"""Write the merged catalog to every pool root given; ``if_stale`` skips roots whose copy
|
|
147
|
+
is younger than ``max_age`` seconds. Returns the paths written."""
|
|
148
|
+
written = []
|
|
149
|
+
for root in sorted(set(r for r in roots.values() if r)):
|
|
150
|
+
path = os.path.join(root, OVERLAY)
|
|
151
|
+
try:
|
|
152
|
+
if if_stale and time.time() - os.stat(path).st_mtime < max_age:
|
|
153
|
+
continue
|
|
154
|
+
except OSError:
|
|
155
|
+
pass
|
|
156
|
+
catalog = load(roots)
|
|
157
|
+
catalog["generated_at"] = _iso(time.time())
|
|
158
|
+
scratch = None
|
|
159
|
+
try:
|
|
160
|
+
handle, scratch = tempfile.mkstemp(prefix=".model-catalog.", dir=root)
|
|
161
|
+
with os.fdopen(handle, "w", encoding="utf-8") as fh:
|
|
162
|
+
os.fchmod(fh.fileno(), 0o644)
|
|
163
|
+
fh.write(json.dumps(catalog, indent=1, sort_keys=True) + "\n")
|
|
164
|
+
os.replace(scratch, path)
|
|
165
|
+
written.append(path)
|
|
166
|
+
except OSError:
|
|
167
|
+
if scratch:
|
|
168
|
+
try:
|
|
169
|
+
os.unlink(scratch)
|
|
170
|
+
except OSError:
|
|
171
|
+
pass
|
|
172
|
+
return written
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def main(argv):
|
|
176
|
+
roots, verb, if_stale, i = {}, None, False, 0
|
|
177
|
+
while i < len(argv):
|
|
178
|
+
if argv[i] in ("--claude-root", "--codex-root") and i + 1 < len(argv):
|
|
179
|
+
roots[argv[i][2:-5]] = argv[i + 1]
|
|
180
|
+
i += 2
|
|
181
|
+
elif argv[i] == "--if-stale":
|
|
182
|
+
if_stale, i = True, i + 1
|
|
183
|
+
elif argv[i] in ("refresh", "show") and verb is None:
|
|
184
|
+
verb, i = argv[i], i + 1
|
|
185
|
+
else:
|
|
186
|
+
verb = None
|
|
187
|
+
break
|
|
188
|
+
if verb is None:
|
|
189
|
+
sys.stderr.write("usage: model_catalog.py <refresh|show> [--claude-root R] [--codex-root R] [--if-stale]\n")
|
|
190
|
+
return 2
|
|
191
|
+
if verb == "show":
|
|
192
|
+
print(json.dumps(load(roots), indent=1, sort_keys=True))
|
|
193
|
+
return 0
|
|
194
|
+
written = refresh(roots, if_stale)
|
|
195
|
+
for path in written:
|
|
196
|
+
print(path)
|
|
197
|
+
return 0 if written or if_stale or not roots else 1
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Which model and effort a resumed session should run with (Python 3.8+).
|
|
3
|
+
|
|
4
|
+
Same provider: the session's own model and effort, as its history recorded them. Another
|
|
5
|
+
provider: the model of the same LEVEL -- every family has a level in the catalog (Claude: haiku 2,
|
|
6
|
+
sonnet 3, opus 4, fable 5; Codex: luna 1, terra 2, sol 3, astra 4, so astra <-> opus, sol <->
|
|
7
|
+
sonnet, terra -> haiku <- luna, haiku -> terra, fable -> astra), the source model's level picks
|
|
8
|
+
the destination family, and the newest listed model of that family is the match -- and the
|
|
9
|
+
effort of the same level by name (low, medium, high, xhigh, max), with Codex's ``ultra`` landing
|
|
10
|
+
on ``max`` and its ``minimal``/``none`` on ``low``, clamped to what the destination model
|
|
11
|
+
supports (the nearest level below, else the lowest). An unknown model or effort is simply
|
|
12
|
+
omitted, and the destination CLI applies its own default.
|
|
13
|
+
|
|
14
|
+
``settings_flags`` renders the result as argv: Claude ``--model M --effort E``; Codex ``-m M -c
|
|
15
|
+
model_reasoning_effort=E`` -- always both, because any single model flag makes Codex drop the
|
|
16
|
+
thread's other saved value (the effort falls back to the model's catalog default).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, __import__("os").path.dirname(__import__("os").path.abspath(__file__)))
|
|
22
|
+
from model_catalog import load, parse_id # noqa: E402
|
|
23
|
+
|
|
24
|
+
RENAME = {"codex": {"claude": {"ultra": "max", "minimal": "low", "none": "low"}},
|
|
25
|
+
"claude": {"codex": {}}}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def tier(catalog, provider, model, hops=4):
|
|
29
|
+
"""The model's family tier; a retiring model counts as the one the catalog upgrades it to."""
|
|
30
|
+
section = catalog.get(provider) or {}
|
|
31
|
+
entry = (section.get("models") or {}).get(model) or {}
|
|
32
|
+
family = entry.get("family") or parse_id(provider, model)[0]
|
|
33
|
+
level = (section.get("families") or {}).get(family)
|
|
34
|
+
if level is None and entry.get("upgrade") and hops:
|
|
35
|
+
return tier(catalog, provider, entry["upgrade"], hops - 1)
|
|
36
|
+
return level
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def current_models(catalog, provider):
|
|
40
|
+
"""family -> the newest listed model of that family. Every listed family is a level of its
|
|
41
|
+
own, whatever its version: launches are staggered, and a tier must never borrow another
|
|
42
|
+
family's model while a listed one of its own exists."""
|
|
43
|
+
best = {}
|
|
44
|
+
for model, entry in ((catalog.get(provider) or {}).get("models") or {}).items():
|
|
45
|
+
family = entry.get("family") or parse_id(provider, model)[0]
|
|
46
|
+
if not family or not entry.get("listed"):
|
|
47
|
+
continue
|
|
48
|
+
version = tuple(entry.get("version") or parse_id(provider, model)[1])
|
|
49
|
+
if family not in best or version > best[family][0]:
|
|
50
|
+
best[family] = (version, model)
|
|
51
|
+
return {family: model for family, (_, model) in best.items()}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def match_model(catalog, source, model, target):
|
|
55
|
+
"""The target provider's current model at the source model's tier (nearest above, then
|
|
56
|
+
below, when that exact tier has no current model); None when the source is unknown."""
|
|
57
|
+
if not model:
|
|
58
|
+
return None
|
|
59
|
+
if source == target:
|
|
60
|
+
return model
|
|
61
|
+
wanted = tier(catalog, source, model)
|
|
62
|
+
if wanted is None:
|
|
63
|
+
return None
|
|
64
|
+
current = current_models(catalog, target)
|
|
65
|
+
families = (catalog.get(target) or {}).get("families") or {}
|
|
66
|
+
ranked = sorted(((level, family) for family, level in families.items() if family in current),
|
|
67
|
+
key=lambda item: (abs(item[0] - wanted), -item[0]))
|
|
68
|
+
return current[ranked[0][1]] if ranked else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def match_effort(catalog, source, effort, target, model):
|
|
72
|
+
"""The same effort by name, renamed between vocabularies, clamped to the target model."""
|
|
73
|
+
if not effort:
|
|
74
|
+
return None
|
|
75
|
+
name = RENAME.get(source, {}).get(target, {}).get(effort, effort) if source != target else effort
|
|
76
|
+
section = catalog.get(target) or {}
|
|
77
|
+
scale = list(section.get("efforts") or [])
|
|
78
|
+
supported = ((section.get("models") or {}).get(model) or {}).get("efforts") or []
|
|
79
|
+
if name not in scale:
|
|
80
|
+
return name if not supported or name in supported else None
|
|
81
|
+
if not supported or name in supported:
|
|
82
|
+
return name
|
|
83
|
+
below = [level for level in supported if level in scale and scale.index(level) <= scale.index(name)]
|
|
84
|
+
return max(below, key=scale.index) if below else min(supported, key=lambda level: scale.index(level)
|
|
85
|
+
if level in scale else len(scale))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def resume_settings(source, settings, target, catalog=None):
|
|
89
|
+
"""{"model", "effort"} for the target provider from a source session's settings."""
|
|
90
|
+
catalog = catalog or load()
|
|
91
|
+
model = match_model(catalog, source, (settings or {}).get("model"), target)
|
|
92
|
+
effort = match_effort(catalog, source, (settings or {}).get("effort"), target, model)
|
|
93
|
+
if model and not effort and target == "codex":
|
|
94
|
+
effort = (((catalog.get("codex") or {}).get("models") or {}).get(model) or {}).get("default_effort")
|
|
95
|
+
return {"model": model, "effort": effort}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def settings_flags(provider, model, effort):
|
|
99
|
+
"""argv fragment that pins ``model``/``effort`` for ``provider``; empty when nothing is known."""
|
|
100
|
+
if provider == "claude":
|
|
101
|
+
return (["--model", model] if model else []) + (["--effort", effort] if effort else [])
|
|
102
|
+
if provider == "codex" and model and effort:
|
|
103
|
+
return ["-m", model, "-c", "model_reasoning_effort=" + effort]
|
|
104
|
+
return []
|