codexloop 0.1.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.
Files changed (104) hide show
  1. codexloop/__init__.py +3 -0
  2. codexloop/application/__init__.py +1 -0
  3. codexloop/application/dto.py +41 -0
  4. codexloop/application/ports.py +158 -0
  5. codexloop/application/runner.py +467 -0
  6. codexloop/application/usecases/__init__.py +1 -0
  7. codexloop/application/usecases/doctor.py +37 -0
  8. codexloop/application/usecases/list_threads.py +14 -0
  9. codexloop/application/usecases/preflight.py +10 -0
  10. codexloop/application/usecases/resume_thread.py +11 -0
  11. codexloop/application/usecases/run_control.py +20 -0
  12. codexloop/application/usecases/run_plan.py +11 -0
  13. codexloop/bootstrap.py +461 -0
  14. codexloop/cli/__init__.py +1 -0
  15. codexloop/cli/app.py +94 -0
  16. codexloop/cli/asyncio.py +80 -0
  17. codexloop/cli/commands/__init__.py +1 -0
  18. codexloop/cli/commands/approval_cmd.py +24 -0
  19. codexloop/cli/commands/capacity.py +33 -0
  20. codexloop/cli/commands/cwd_cmd.py +22 -0
  21. codexloop/cli/commands/doctor.py +27 -0
  22. codexloop/cli/commands/effort_cmd.py +24 -0
  23. codexloop/cli/commands/logs.py +15 -0
  24. codexloop/cli/commands/model_cmd.py +22 -0
  25. codexloop/cli/commands/prompt.py +32 -0
  26. codexloop/cli/commands/reset.py +25 -0
  27. codexloop/cli/commands/resume.py +38 -0
  28. codexloop/cli/commands/run.py +50 -0
  29. codexloop/cli/commands/runs.py +13 -0
  30. codexloop/cli/commands/sandbox_cmd.py +24 -0
  31. codexloop/cli/commands/savepoints.py +25 -0
  32. codexloop/cli/commands/snapshot.py +26 -0
  33. codexloop/cli/commands/status.py +15 -0
  34. codexloop/cli/commands/stop.py +21 -0
  35. codexloop/cli/commands/threads.py +15 -0
  36. codexloop/cli/commands/unwind.py +24 -0
  37. codexloop/cli/commands/watch.py +66 -0
  38. codexloop/cli/render.py +39 -0
  39. codexloop/domain/__init__.py +26 -0
  40. codexloop/domain/approval.py +38 -0
  41. codexloop/domain/backoff.py +34 -0
  42. codexloop/domain/budget.py +56 -0
  43. codexloop/domain/capacity.py +81 -0
  44. codexloop/domain/classify.py +98 -0
  45. codexloop/domain/completion.py +130 -0
  46. codexloop/domain/control.py +167 -0
  47. codexloop/domain/error_codes.py +75 -0
  48. codexloop/domain/errors.py +35 -0
  49. codexloop/domain/loop.py +190 -0
  50. codexloop/domain/model_profile.py +30 -0
  51. codexloop/domain/plan.py +38 -0
  52. codexloop/domain/savepoint.py +32 -0
  53. codexloop/domain/savepoint_message.py +56 -0
  54. codexloop/domain/session.py +32 -0
  55. codexloop/domain/signals.py +25 -0
  56. codexloop/domain/waiting.py +120 -0
  57. codexloop/infrastructure/__init__.py +0 -0
  58. codexloop/infrastructure/agent/__init__.py +0 -0
  59. codexloop/infrastructure/agent/argv.py +102 -0
  60. codexloop/infrastructure/agent/events.py +274 -0
  61. codexloop/infrastructure/agent/gateway.py +189 -0
  62. codexloop/infrastructure/agent/probe.py +74 -0
  63. codexloop/infrastructure/agent/process.py +208 -0
  64. codexloop/infrastructure/agent/schema.py +31 -0
  65. codexloop/infrastructure/agent/scripted.py +201 -0
  66. codexloop/infrastructure/agent/translate.py +120 -0
  67. codexloop/infrastructure/api/__init__.py +26 -0
  68. codexloop/infrastructure/api/api_baseline.json +340 -0
  69. codexloop/infrastructure/api/binder.py +170 -0
  70. codexloop/infrastructure/api/gateway.py +142 -0
  71. codexloop/infrastructure/api/introspect.py +248 -0
  72. codexloop/infrastructure/api/json_io.py +26 -0
  73. codexloop/infrastructure/api/params.py +162 -0
  74. codexloop/infrastructure/api/providers.py +70 -0
  75. codexloop/infrastructure/api/registry.py +13 -0
  76. codexloop/infrastructure/appserver/__init__.py +6 -0
  77. codexloop/infrastructure/appserver/client.py +245 -0
  78. codexloop/infrastructure/appserver/gateway.py +437 -0
  79. codexloop/infrastructure/appserver/ratelimits.py +100 -0
  80. codexloop/infrastructure/audit.py +26 -0
  81. codexloop/infrastructure/capacity_probe.py +57 -0
  82. codexloop/infrastructure/clock.py +26 -0
  83. codexloop/infrastructure/config.py +150 -0
  84. codexloop/infrastructure/control.py +89 -0
  85. codexloop/infrastructure/doctor_env.py +239 -0
  86. codexloop/infrastructure/events.py +23 -0
  87. codexloop/infrastructure/git_savepoints.py +176 -0
  88. codexloop/infrastructure/lock.py +88 -0
  89. codexloop/infrastructure/logging.py +124 -0
  90. codexloop/infrastructure/notify.py +27 -0
  91. codexloop/infrastructure/progress.py +14 -0
  92. codexloop/infrastructure/redact.py +52 -0
  93. codexloop/infrastructure/rollout.py +113 -0
  94. codexloop/infrastructure/rundir.py +57 -0
  95. codexloop/infrastructure/snapshot.py +39 -0
  96. codexloop/infrastructure/state.py +32 -0
  97. codexloop/infrastructure/state_bus.py +27 -0
  98. codexloop/infrastructure/stream_ui.py +44 -0
  99. codexloop/py.typed +0 -0
  100. codexloop-0.1.0.dist-info/METADATA +104 -0
  101. codexloop-0.1.0.dist-info/RECORD +104 -0
  102. codexloop-0.1.0.dist-info/WHEEL +4 -0
  103. codexloop-0.1.0.dist-info/entry_points.txt +2 -0
  104. codexloop-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,340 @@
1
+ {
2
+ "openai_version": "3.0.0",
3
+ "method_count": 320,
4
+ "local_helpers": [
5
+ "beta.chat.completions.parse",
6
+ "beta.chat.completions.stream",
7
+ "beta.threads.create_and_run_stream",
8
+ "beta.threads.runs.create_and_stream",
9
+ "beta.threads.runs.stream",
10
+ "beta.threads.runs.submit_tool_outputs_stream",
11
+ "chat.completions.parse",
12
+ "chat.completions.stream",
13
+ "responses.parse",
14
+ "responses.stream",
15
+ "webhooks.unwrap",
16
+ "webhooks.verify_signature"
17
+ ],
18
+ "methods": [
19
+ "admin.organization.admin_api_keys.create",
20
+ "admin.organization.admin_api_keys.delete",
21
+ "admin.organization.admin_api_keys.list",
22
+ "admin.organization.admin_api_keys.retrieve",
23
+ "admin.organization.audit_logs.list",
24
+ "admin.organization.certificates.activate",
25
+ "admin.organization.certificates.create",
26
+ "admin.organization.certificates.deactivate",
27
+ "admin.organization.certificates.delete",
28
+ "admin.organization.certificates.list",
29
+ "admin.organization.certificates.retrieve",
30
+ "admin.organization.certificates.update",
31
+ "admin.organization.data_retention.retrieve",
32
+ "admin.organization.data_retention.update",
33
+ "admin.organization.groups.create",
34
+ "admin.organization.groups.delete",
35
+ "admin.organization.groups.list",
36
+ "admin.organization.groups.retrieve",
37
+ "admin.organization.groups.roles.create",
38
+ "admin.organization.groups.roles.delete",
39
+ "admin.organization.groups.roles.list",
40
+ "admin.organization.groups.roles.retrieve",
41
+ "admin.organization.groups.update",
42
+ "admin.organization.groups.users.create",
43
+ "admin.organization.groups.users.delete",
44
+ "admin.organization.groups.users.list",
45
+ "admin.organization.groups.users.retrieve",
46
+ "admin.organization.invites.create",
47
+ "admin.organization.invites.delete",
48
+ "admin.organization.invites.list",
49
+ "admin.organization.invites.retrieve",
50
+ "admin.organization.projects.api_keys.delete",
51
+ "admin.organization.projects.api_keys.list",
52
+ "admin.organization.projects.api_keys.retrieve",
53
+ "admin.organization.projects.archive",
54
+ "admin.organization.projects.certificates.activate",
55
+ "admin.organization.projects.certificates.deactivate",
56
+ "admin.organization.projects.certificates.list",
57
+ "admin.organization.projects.create",
58
+ "admin.organization.projects.data_retention.retrieve",
59
+ "admin.organization.projects.data_retention.update",
60
+ "admin.organization.projects.groups.create",
61
+ "admin.organization.projects.groups.delete",
62
+ "admin.organization.projects.groups.list",
63
+ "admin.organization.projects.groups.retrieve",
64
+ "admin.organization.projects.groups.roles.create",
65
+ "admin.organization.projects.groups.roles.delete",
66
+ "admin.organization.projects.groups.roles.list",
67
+ "admin.organization.projects.groups.roles.retrieve",
68
+ "admin.organization.projects.hosted_tool_permissions.retrieve",
69
+ "admin.organization.projects.hosted_tool_permissions.update",
70
+ "admin.organization.projects.list",
71
+ "admin.organization.projects.model_permissions.delete",
72
+ "admin.organization.projects.model_permissions.retrieve",
73
+ "admin.organization.projects.model_permissions.update",
74
+ "admin.organization.projects.rate_limits.list_rate_limits",
75
+ "admin.organization.projects.rate_limits.update_rate_limit",
76
+ "admin.organization.projects.retrieve",
77
+ "admin.organization.projects.roles.create",
78
+ "admin.organization.projects.roles.delete",
79
+ "admin.organization.projects.roles.list",
80
+ "admin.organization.projects.roles.retrieve",
81
+ "admin.organization.projects.roles.update",
82
+ "admin.organization.projects.service_accounts.api_keys.create",
83
+ "admin.organization.projects.service_accounts.create",
84
+ "admin.organization.projects.service_accounts.delete",
85
+ "admin.organization.projects.service_accounts.list",
86
+ "admin.organization.projects.service_accounts.retrieve",
87
+ "admin.organization.projects.service_accounts.update",
88
+ "admin.organization.projects.spend_alerts.create",
89
+ "admin.organization.projects.spend_alerts.delete",
90
+ "admin.organization.projects.spend_alerts.list",
91
+ "admin.organization.projects.spend_alerts.retrieve",
92
+ "admin.organization.projects.spend_alerts.update",
93
+ "admin.organization.projects.spend_limit.delete",
94
+ "admin.organization.projects.spend_limit.retrieve",
95
+ "admin.organization.projects.spend_limit.update",
96
+ "admin.organization.projects.update",
97
+ "admin.organization.projects.users.create",
98
+ "admin.organization.projects.users.delete",
99
+ "admin.organization.projects.users.list",
100
+ "admin.organization.projects.users.retrieve",
101
+ "admin.organization.projects.users.roles.create",
102
+ "admin.organization.projects.users.roles.delete",
103
+ "admin.organization.projects.users.roles.list",
104
+ "admin.organization.projects.users.roles.retrieve",
105
+ "admin.organization.projects.users.update",
106
+ "admin.organization.roles.create",
107
+ "admin.organization.roles.delete",
108
+ "admin.organization.roles.list",
109
+ "admin.organization.roles.retrieve",
110
+ "admin.organization.roles.update",
111
+ "admin.organization.spend_alerts.create",
112
+ "admin.organization.spend_alerts.delete",
113
+ "admin.organization.spend_alerts.list",
114
+ "admin.organization.spend_alerts.retrieve",
115
+ "admin.organization.spend_alerts.update",
116
+ "admin.organization.spend_limit.delete",
117
+ "admin.organization.spend_limit.retrieve",
118
+ "admin.organization.spend_limit.update",
119
+ "admin.organization.usage.audio_speeches",
120
+ "admin.organization.usage.audio_transcriptions",
121
+ "admin.organization.usage.code_interpreter_sessions",
122
+ "admin.organization.usage.completions",
123
+ "admin.organization.usage.costs",
124
+ "admin.organization.usage.embeddings",
125
+ "admin.organization.usage.file_search_calls",
126
+ "admin.organization.usage.images",
127
+ "admin.organization.usage.moderations",
128
+ "admin.organization.usage.vector_stores",
129
+ "admin.organization.usage.web_search_calls",
130
+ "admin.organization.users.delete",
131
+ "admin.organization.users.list",
132
+ "admin.organization.users.retrieve",
133
+ "admin.organization.users.roles.create",
134
+ "admin.organization.users.roles.delete",
135
+ "admin.organization.users.roles.list",
136
+ "admin.organization.users.roles.retrieve",
137
+ "admin.organization.users.update",
138
+ "audio.speech.create",
139
+ "audio.transcriptions.create",
140
+ "audio.translations.create",
141
+ "batches.cancel",
142
+ "batches.create",
143
+ "batches.list",
144
+ "batches.retrieve",
145
+ "beta.assistants.create",
146
+ "beta.assistants.delete",
147
+ "beta.assistants.list",
148
+ "beta.assistants.retrieve",
149
+ "beta.assistants.update",
150
+ "beta.chat.completions.create",
151
+ "beta.chat.completions.delete",
152
+ "beta.chat.completions.list",
153
+ "beta.chat.completions.messages.list",
154
+ "beta.chat.completions.parse",
155
+ "beta.chat.completions.retrieve",
156
+ "beta.chat.completions.stream",
157
+ "beta.chat.completions.update",
158
+ "beta.chatkit.sessions.cancel",
159
+ "beta.chatkit.sessions.create",
160
+ "beta.chatkit.threads.delete",
161
+ "beta.chatkit.threads.list",
162
+ "beta.chatkit.threads.list_items",
163
+ "beta.chatkit.threads.retrieve",
164
+ "beta.realtime.connect",
165
+ "beta.realtime.sessions.create",
166
+ "beta.realtime.transcription_sessions.create",
167
+ "beta.responses.cancel",
168
+ "beta.responses.compact",
169
+ "beta.responses.connect",
170
+ "beta.responses.create",
171
+ "beta.responses.delete",
172
+ "beta.responses.input_items.list",
173
+ "beta.responses.input_tokens.count",
174
+ "beta.responses.retrieve",
175
+ "beta.threads.create",
176
+ "beta.threads.create_and_run",
177
+ "beta.threads.create_and_run_poll",
178
+ "beta.threads.create_and_run_stream",
179
+ "beta.threads.delete",
180
+ "beta.threads.messages.create",
181
+ "beta.threads.messages.delete",
182
+ "beta.threads.messages.list",
183
+ "beta.threads.messages.retrieve",
184
+ "beta.threads.messages.update",
185
+ "beta.threads.retrieve",
186
+ "beta.threads.runs.cancel",
187
+ "beta.threads.runs.create",
188
+ "beta.threads.runs.create_and_poll",
189
+ "beta.threads.runs.create_and_stream",
190
+ "beta.threads.runs.list",
191
+ "beta.threads.runs.poll",
192
+ "beta.threads.runs.retrieve",
193
+ "beta.threads.runs.steps.list",
194
+ "beta.threads.runs.steps.retrieve",
195
+ "beta.threads.runs.stream",
196
+ "beta.threads.runs.submit_tool_outputs",
197
+ "beta.threads.runs.submit_tool_outputs_and_poll",
198
+ "beta.threads.runs.submit_tool_outputs_stream",
199
+ "beta.threads.runs.update",
200
+ "beta.threads.update",
201
+ "chat.completions.create",
202
+ "chat.completions.delete",
203
+ "chat.completions.list",
204
+ "chat.completions.messages.list",
205
+ "chat.completions.parse",
206
+ "chat.completions.retrieve",
207
+ "chat.completions.stream",
208
+ "chat.completions.update",
209
+ "completions.create",
210
+ "containers.create",
211
+ "containers.delete",
212
+ "containers.files.content.retrieve",
213
+ "containers.files.create",
214
+ "containers.files.delete",
215
+ "containers.files.list",
216
+ "containers.files.retrieve",
217
+ "containers.list",
218
+ "containers.retrieve",
219
+ "content_provenance_checks.create",
220
+ "conversations.create",
221
+ "conversations.delete",
222
+ "conversations.items.create",
223
+ "conversations.items.delete",
224
+ "conversations.items.list",
225
+ "conversations.items.retrieve",
226
+ "conversations.retrieve",
227
+ "conversations.update",
228
+ "embeddings.create",
229
+ "evals.create",
230
+ "evals.delete",
231
+ "evals.list",
232
+ "evals.retrieve",
233
+ "evals.runs.cancel",
234
+ "evals.runs.create",
235
+ "evals.runs.delete",
236
+ "evals.runs.list",
237
+ "evals.runs.output_items.list",
238
+ "evals.runs.output_items.retrieve",
239
+ "evals.runs.retrieve",
240
+ "evals.update",
241
+ "files.content",
242
+ "files.create",
243
+ "files.delete",
244
+ "files.list",
245
+ "files.retrieve",
246
+ "files.retrieve_content",
247
+ "files.wait_for_processing",
248
+ "fine_tuning.alpha.graders.run",
249
+ "fine_tuning.alpha.graders.validate",
250
+ "fine_tuning.checkpoints.permissions.create",
251
+ "fine_tuning.checkpoints.permissions.delete",
252
+ "fine_tuning.checkpoints.permissions.list",
253
+ "fine_tuning.checkpoints.permissions.retrieve",
254
+ "fine_tuning.jobs.cancel",
255
+ "fine_tuning.jobs.checkpoints.list",
256
+ "fine_tuning.jobs.create",
257
+ "fine_tuning.jobs.list",
258
+ "fine_tuning.jobs.list_events",
259
+ "fine_tuning.jobs.pause",
260
+ "fine_tuning.jobs.resume",
261
+ "fine_tuning.jobs.retrieve",
262
+ "images.create_variation",
263
+ "images.edit",
264
+ "images.generate",
265
+ "models.delete",
266
+ "models.list",
267
+ "models.retrieve",
268
+ "moderations.create",
269
+ "realtime.calls.accept",
270
+ "realtime.calls.create",
271
+ "realtime.calls.hangup",
272
+ "realtime.calls.refer",
273
+ "realtime.calls.reject",
274
+ "realtime.client_secrets.create",
275
+ "realtime.connect",
276
+ "responses.cancel",
277
+ "responses.compact",
278
+ "responses.connect",
279
+ "responses.create",
280
+ "responses.delete",
281
+ "responses.input_items.list",
282
+ "responses.input_tokens.count",
283
+ "responses.parse",
284
+ "responses.retrieve",
285
+ "responses.stream",
286
+ "skills.content.retrieve",
287
+ "skills.create",
288
+ "skills.delete",
289
+ "skills.list",
290
+ "skills.retrieve",
291
+ "skills.update",
292
+ "skills.versions.content.retrieve",
293
+ "skills.versions.create",
294
+ "skills.versions.delete",
295
+ "skills.versions.list",
296
+ "skills.versions.retrieve",
297
+ "uploads.cancel",
298
+ "uploads.complete",
299
+ "uploads.create",
300
+ "uploads.parts.create",
301
+ "uploads.upload_file_chunked",
302
+ "vector_stores.create",
303
+ "vector_stores.delete",
304
+ "vector_stores.file_batches.cancel",
305
+ "vector_stores.file_batches.create",
306
+ "vector_stores.file_batches.create_and_poll",
307
+ "vector_stores.file_batches.list_files",
308
+ "vector_stores.file_batches.poll",
309
+ "vector_stores.file_batches.retrieve",
310
+ "vector_stores.file_batches.upload_and_poll",
311
+ "vector_stores.files.content",
312
+ "vector_stores.files.create",
313
+ "vector_stores.files.create_and_poll",
314
+ "vector_stores.files.delete",
315
+ "vector_stores.files.list",
316
+ "vector_stores.files.poll",
317
+ "vector_stores.files.retrieve",
318
+ "vector_stores.files.update",
319
+ "vector_stores.files.upload",
320
+ "vector_stores.files.upload_and_poll",
321
+ "vector_stores.list",
322
+ "vector_stores.retrieve",
323
+ "vector_stores.search",
324
+ "vector_stores.update",
325
+ "videos.create",
326
+ "videos.create_and_poll",
327
+ "videos.create_character",
328
+ "videos.delete",
329
+ "videos.download_content",
330
+ "videos.edit",
331
+ "videos.extend",
332
+ "videos.get_character",
333
+ "videos.list",
334
+ "videos.poll",
335
+ "videos.remix",
336
+ "videos.retrieve",
337
+ "webhooks.unwrap",
338
+ "webhooks.verify_signature"
339
+ ]
340
+ }
@@ -0,0 +1,170 @@
1
+ """Bind discovered SDK methods to a nested Typer command tree."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import typer
10
+
11
+ from codexloop.infrastructure.api.gateway import OpenAIApiGateway, default_gateway
12
+ from codexloop.infrastructure.api.introspect import EndpointSpec, discover_surface, resolve_callable
13
+ from codexloop.infrastructure.api.params import (
14
+ ScalarParam,
15
+ scalar_parameters,
16
+ typer_type_for_annotation,
17
+ )
18
+ from codexloop.infrastructure.api.providers import (
19
+ PROVIDER_FACTORIES,
20
+ client_class_for_provider,
21
+ surface_roots_for_provider,
22
+ )
23
+ from codexloop.infrastructure.api.registry import clear_registry, register_command_path
24
+
25
+
26
+ def _scalar_default_src(scalar: ScalarParam) -> str:
27
+ py_type = typer_type_for_annotation(scalar.annotation).__name__
28
+ return (
29
+ f"{scalar.name}: {py_type} | None = typer.Option("
30
+ f'None, "--{scalar.cli_name}", help="SDK parameter {scalar.name!r}.")'
31
+ )
32
+
33
+
34
+ def _make_typer_command(
35
+ method: EndpointSpec,
36
+ gateway: OpenAIApiGateway,
37
+ *,
38
+ client_cls: type,
39
+ ) -> Any:
40
+ fn = resolve_callable(method, client_cls=client_cls)
41
+ signature = inspect.signature(fn)
42
+ scalars = scalar_parameters(signature)
43
+ param_names = [s.name for s in scalars]
44
+
45
+ def bound_callback(ctx: typer.Context, **kwargs: Any) -> None:
46
+ root: Any = ctx
47
+ while root.parent is not None:
48
+ root = root.parent
49
+ obj = root.obj if isinstance(root.obj, dict) else {}
50
+ provider = str(obj.get("provider", "openai"))
51
+ base_url = obj.get("base_url")
52
+ json_body = kwargs.pop("json_body", None)
53
+ json_file = kwargs.pop("json_file", None)
54
+ raw = bool(kwargs.pop("raw", False))
55
+ stream = bool(kwargs.pop("stream", False))
56
+ max_items = kwargs.pop("max_items", None)
57
+ scalar_values = {name: kwargs.get(name) for name in param_names}
58
+ try:
59
+ text = gateway.invoke_and_print(
60
+ method.path,
61
+ provider=provider,
62
+ base_url=base_url if isinstance(base_url, str) else None,
63
+ json_body=json_body,
64
+ json_file=json_file,
65
+ raw=raw,
66
+ stream=stream,
67
+ max_items=max_items,
68
+ scalar_values=scalar_values,
69
+ method=method,
70
+ )
71
+ except (ValueError, TypeError, OSError) as exc:
72
+ typer.echo(str(exc), err=True)
73
+ raise typer.Exit(2) from exc
74
+ typer.echo(text)
75
+
76
+ # Option-as-default (not Annotated) so Typer works under postponed annotations.
77
+ args_src = [
78
+ "ctx: typer.Context",
79
+ 'json_body: str | None = typer.Option(None, "--json", help="Inline JSON object.")',
80
+ 'json_file: Path | None = typer.Option(None, "--json-file", help="JSON file path.")',
81
+ 'raw: bool = typer.Option(False, "--raw", help="Use with_raw_response.")',
82
+ 'stream: bool = typer.Option(False, "--stream", help="Use with_streaming_response.")',
83
+ 'max_items: int | None = typer.Option(None, "--max-items", help="Auto-pagination cap.")',
84
+ ]
85
+ args_src.extend(_scalar_default_src(s) for s in scalars)
86
+ scalar_dict = ", ".join(f"{n!r}: {n}" for n in param_names)
87
+ body = (
88
+ f"def command({', '.join(args_src)}) -> None:\n"
89
+ f" scalar_values = {{{scalar_dict}}}\n"
90
+ " bound_callback(\n"
91
+ " ctx,\n"
92
+ " json_body=json_body,\n"
93
+ " json_file=json_file,\n"
94
+ " raw=raw,\n"
95
+ " stream=stream,\n"
96
+ " max_items=max_items,\n"
97
+ " **scalar_values,\n"
98
+ " )\n"
99
+ )
100
+ ns: dict[str, Any] = {
101
+ "Path": Path,
102
+ "typer": typer,
103
+ "bound_callback": bound_callback,
104
+ }
105
+ exec(body, ns) # nosec B102 — binder-owned source string builds Typer callbacks
106
+ command_fn = ns["command"]
107
+ command_fn.__name__ = method.method_name
108
+ command_fn.__doc__ = f"SDK `{method.path}`."
109
+ return command_fn
110
+
111
+
112
+ def _ensure_group(parent: typer.Typer, name: str, groups: dict[str, typer.Typer]) -> typer.Typer:
113
+ key = f"{id(parent)}:{name}"
114
+ existing = groups.get(key)
115
+ if existing is not None:
116
+ return existing
117
+ child = typer.Typer(name=name, help=f"OpenAI `{name}` resource.", no_args_is_help=True)
118
+ parent.add_typer(child, name=name)
119
+ groups[key] = child
120
+ return child
121
+
122
+
123
+ def build_api_typer_app(
124
+ *,
125
+ gateway: OpenAIApiGateway | None = None,
126
+ provider: str = "openai",
127
+ ) -> typer.Typer:
128
+ """Build the nested Typer app mounted at ``codexloop api``."""
129
+ clear_registry()
130
+ gw = gateway or default_gateway()
131
+ client_cls = client_class_for_provider(provider)
132
+ roots = surface_roots_for_provider(provider)
133
+
134
+ api = typer.Typer(
135
+ name="api",
136
+ help="Generated 1:1 OpenAI SDK REST surface.",
137
+ no_args_is_help=True,
138
+ )
139
+
140
+ @api.callback()
141
+ def _api_callback(
142
+ ctx: typer.Context,
143
+ provider_opt: str = typer.Option(
144
+ "openai",
145
+ "--provider",
146
+ help="SDK client (azure, custom, openai).",
147
+ ),
148
+ base_url: str | None = typer.Option(
149
+ None,
150
+ "--base-url",
151
+ help="Override API base URL (custom / openai).",
152
+ ),
153
+ ) -> None:
154
+ if provider_opt not in PROVIDER_FACTORIES:
155
+ typer.echo(f"unknown provider {provider_opt!r}", err=True)
156
+ raise typer.Exit(2)
157
+ ctx.ensure_object(dict)
158
+ ctx.obj["provider"] = provider_opt
159
+ ctx.obj["base_url"] = base_url
160
+
161
+ groups: dict[str, typer.Typer] = {}
162
+ for method in discover_surface(roots=roots, client_cls=client_cls):
163
+ parts = method.path.split(".")
164
+ current = api
165
+ for segment in parts[:-1]:
166
+ current = _ensure_group(current, segment, groups)
167
+ current.command(method.method_name)(_make_typer_command(method, gw, client_cls=client_cls))
168
+ register_command_path(method.path)
169
+
170
+ return api
@@ -0,0 +1,142 @@
1
+ """Invoke generated ``codexloop api`` commands against the OpenAI SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from openai import Omit
11
+
12
+ from codexloop.infrastructure.api.introspect import (
13
+ LOCAL_HELPER_PATHS,
14
+ EndpointSpec,
15
+ discover_surface,
16
+ resolve_callable,
17
+ )
18
+ from codexloop.infrastructure.api.json_io import load_json_payload
19
+ from codexloop.infrastructure.api.params import build_call_kwargs
20
+ from codexloop.infrastructure.api.providers import (
21
+ build_client,
22
+ client_class_for_provider,
23
+ surface_roots_for_provider,
24
+ )
25
+
26
+
27
+ def _serialize(value: Any) -> Any:
28
+ if hasattr(value, "model_dump"):
29
+ return value.model_dump()
30
+ if hasattr(value, "read"):
31
+ return {"stream": "<streaming response>"}
32
+ if isinstance(value, (list, tuple)):
33
+ return [_serialize(v) for v in value]
34
+ if isinstance(value, dict):
35
+ return {k: _serialize(v) for k, v in value.items()}
36
+ return value
37
+
38
+
39
+ def _navigate_resource(client: Any, resource_path: tuple[str, ...]) -> Any:
40
+ resource: Any = client
41
+ for segment in resource_path:
42
+ resource = getattr(resource, segment)
43
+ return resource
44
+
45
+
46
+ def _collect_paginated(
47
+ first_page: Any,
48
+ *,
49
+ max_items: int | None,
50
+ ) -> list[Any]:
51
+ data = getattr(first_page, "data", first_page)
52
+ items: list[Any] = list(data or []) if not isinstance(data, list) else list(data)
53
+ page = first_page
54
+ while getattr(page, "has_more", False):
55
+ if max_items is not None and len(items) >= max_items:
56
+ break
57
+ page = page.get_next_page()
58
+ items.extend(page.data)
59
+ if max_items is not None:
60
+ items = items[:max_items]
61
+ return items
62
+
63
+
64
+ class OpenAIApiGateway:
65
+ """Concrete adapter for the generated REST surface."""
66
+
67
+ def invoke(
68
+ self,
69
+ method_path: str,
70
+ *,
71
+ provider: str = "openai",
72
+ base_url: str | None = None,
73
+ json_body: str | None = None,
74
+ json_file: Path | None = None,
75
+ raw: bool = False,
76
+ stream: bool = False,
77
+ max_items: int | None = None,
78
+ scalar_values: dict[str, Any] | None = None,
79
+ method: EndpointSpec | None = None,
80
+ ) -> Any:
81
+ client_cls = client_class_for_provider(provider)
82
+ if method is None:
83
+ segments = method_path.split(".")
84
+ method = EndpointSpec(
85
+ resource_path=tuple(segments[:-1]),
86
+ method_name=segments[-1],
87
+ signature="",
88
+ is_list=segments[-1] == "list",
89
+ is_streaming=False,
90
+ )
91
+ roots = surface_roots_for_provider(provider)
92
+ if roots is not None:
93
+ allowed = {m.path for m in discover_surface(roots=roots, client_cls=client_cls)}
94
+ if method.path not in allowed:
95
+ msg = f"method {method.path!r} is not available for provider {provider!r}"
96
+ raise ValueError(msg)
97
+
98
+ client = build_client(provider, base_url=base_url)
99
+ payload = load_json_payload(inline=json_body, json_file=json_file)
100
+ fn = resolve_callable(method, client_cls=client_cls)
101
+ signature = inspect.signature(fn)
102
+ kwargs = build_call_kwargs(
103
+ signature,
104
+ json_payload=payload,
105
+ scalar_values=scalar_values or {},
106
+ )
107
+ cleaned = {k: v for k, v in kwargs.items() if v is not None and not isinstance(v, Omit)}
108
+
109
+ resource = _navigate_resource(client, method.resource_path)
110
+ if raw:
111
+ resource = resource.with_raw_response
112
+ elif stream:
113
+ resource = resource.with_streaming_response
114
+ bound = getattr(resource, method.method_name)
115
+ result = bound(**cleaned)
116
+
117
+ if not raw and not stream and method.is_list and max_items is not None:
118
+ return _collect_paginated(result, max_items=max_items)
119
+ return result
120
+
121
+ def invoke_and_print(
122
+ self,
123
+ method_path: str,
124
+ **options: Any,
125
+ ) -> str:
126
+ result = self.invoke(method_path, **options)
127
+ if hasattr(result, "read"):
128
+ text = result.read()
129
+ return text.decode() if isinstance(text, bytes) else str(text)
130
+ return json.dumps(_serialize(result), indent=2, default=str)
131
+
132
+
133
+ def default_gateway() -> OpenAIApiGateway:
134
+ return OpenAIApiGateway()
135
+
136
+
137
+ # Re-export for callers that need helper membership checks without importing introspect.
138
+ __all__ = [
139
+ "LOCAL_HELPER_PATHS",
140
+ "OpenAIApiGateway",
141
+ "default_gateway",
142
+ ]