@oneciel-ai/ciel-runtime 0.2.2 → 0.2.3

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 (126) hide show
  1. package/ciel_runtime.py +2553 -9635
  2. package/ciel_runtime_support/advisor_request_builder.py +8 -21
  3. package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
  4. package/ciel_runtime_support/architecture.py +68 -0
  5. package/ciel_runtime_support/architecture_budget.py +1 -1
  6. package/ciel_runtime_support/channel_connection_context.py +233 -0
  7. package/ciel_runtime_support/channel_delivery_context.py +332 -0
  8. package/ciel_runtime_support/channel_mcp_context.py +313 -0
  9. package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
  10. package/ciel_runtime_support/channel_mcp_transport.py +5 -1
  11. package/ciel_runtime_support/channel_message_context.py +212 -0
  12. package/ciel_runtime_support/channel_message_repository.py +14 -3
  13. package/ciel_runtime_support/channel_pending_injection.py +9 -0
  14. package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
  15. package/ciel_runtime_support/channel_replay_policy.py +38 -0
  16. package/ciel_runtime_support/channel_runtime_environment.py +8 -0
  17. package/ciel_runtime_support/channel_session_context.py +236 -0
  18. package/ciel_runtime_support/channel_terminal_context.py +350 -0
  19. package/ciel_runtime_support/channel_wake_context.py +532 -0
  20. package/ciel_runtime_support/claude_environment.py +60 -0
  21. package/ciel_runtime_support/claude_launch_assembly.py +249 -0
  22. package/ciel_runtime_support/claude_router.py +62 -12
  23. package/ciel_runtime_support/cli_application_context.py +132 -0
  24. package/ciel_runtime_support/cli_assembly.py +50 -0
  25. package/ciel_runtime_support/codex_backend_context.py +363 -0
  26. package/ciel_runtime_support/codex_config.py +13 -1
  27. package/ciel_runtime_support/codex_launch_assembly.py +213 -0
  28. package/ciel_runtime_support/codex_launch_configuration.py +30 -1
  29. package/ciel_runtime_support/codex_mcp_integration.py +90 -8
  30. package/ciel_runtime_support/codex_model_catalog.py +4 -1
  31. package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
  32. package/ciel_runtime_support/codex_router.py +38 -8
  33. package/ciel_runtime_support/codex_turn_recovery.py +154 -0
  34. package/ciel_runtime_support/config_migrations.py +103 -0
  35. package/ciel_runtime_support/configuration_cli.py +38 -0
  36. package/ciel_runtime_support/context_compaction.py +9 -4
  37. package/ciel_runtime_support/credential_management.py +12 -0
  38. package/ciel_runtime_support/credentials.py +12 -0
  39. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  40. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  41. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  42. package/ciel_runtime_support/llm_preset_context.py +338 -0
  43. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  44. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  45. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  46. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  47. package/ciel_runtime_support/mcp_transport.py +14 -1
  48. package/ciel_runtime_support/native_context_recovery.py +72 -0
  49. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  50. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  51. package/ciel_runtime_support/ollama_thinking.py +6 -1
  52. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  53. package/ciel_runtime_support/openai_forwarding.py +32 -10
  54. package/ciel_runtime_support/openai_responses_router.py +12 -0
  55. package/ciel_runtime_support/package_lifecycle.py +39 -0
  56. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  57. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  58. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  59. package/ciel_runtime_support/prompt_compaction.py +144 -0
  60. package/ciel_runtime_support/prompt_injection.py +45 -0
  61. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  62. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  63. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  64. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  65. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  66. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  67. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  68. package/ciel_runtime_support/provider_adapters.py +26 -0
  69. package/ciel_runtime_support/provider_administration_context.py +207 -0
  70. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  71. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  72. package/ciel_runtime_support/provider_model_context.py +107 -0
  73. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  74. package/ciel_runtime_support/provider_model_selection.py +10 -3
  75. package/ciel_runtime_support/provider_models.py +45 -2
  76. package/ciel_runtime_support/provider_option_cli.py +19 -0
  77. package/ciel_runtime_support/provider_policy.py +1 -1
  78. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  79. package/ciel_runtime_support/provider_request_builder.py +64 -28
  80. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  81. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  82. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  83. package/ciel_runtime_support/providers/__init__.py +6 -0
  84. package/ciel_runtime_support/providers/alibaba.py +634 -0
  85. package/ciel_runtime_support/providers/catalog.py +24 -16
  86. package/ciel_runtime_support/providers/deepseek.py +73 -0
  87. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  88. package/ciel_runtime_support/providers/kimi.py +69 -9
  89. package/ciel_runtime_support/providers/ollama.py +8 -0
  90. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  91. package/ciel_runtime_support/providers/vllm.py +7 -1
  92. package/ciel_runtime_support/response_collection.py +68 -18
  93. package/ciel_runtime_support/response_collection_context.py +391 -0
  94. package/ciel_runtime_support/response_stream_context.py +555 -0
  95. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  96. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  97. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  98. package/ciel_runtime_support/router_http.py +239 -3
  99. package/ciel_runtime_support/router_observability_context.py +251 -0
  100. package/ciel_runtime_support/router_process_context.py +200 -0
  101. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  102. package/ciel_runtime_support/router_request_assembly.py +399 -0
  103. package/ciel_runtime_support/router_request_context.py +215 -0
  104. package/ciel_runtime_support/router_server_context.py +82 -0
  105. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  106. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  107. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  108. package/ciel_runtime_support/runtime_constants.py +16 -1
  109. package/ciel_runtime_support/runtime_launch.py +9 -5
  110. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  111. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  112. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  113. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  114. package/ciel_runtime_support/runtime_paths.py +60 -40
  115. package/ciel_runtime_support/runtime_primitives.py +78 -0
  116. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  117. package/ciel_runtime_support/statusline_script.py +57 -8
  118. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  119. package/ciel_runtime_support/tool_schema.py +40 -2
  120. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  121. package/ciel_runtime_support/upstream_dump.py +68 -0
  122. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  123. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  124. package/docs/Configuration.md +50 -0
  125. package/docs/Test-Suite.md +1 -0
  126. package/package.json +1 -1
@@ -0,0 +1,363 @@
1
+ """Codex backend and provider-native passthrough bounded context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from http.server import BaseHTTPRequestHandler
7
+ from typing import Any, Callable
8
+
9
+ from .codex_router import CodexChannelContextPorts, CodexChannelContextProjector
10
+ from .openai_chat_passthrough import OpenAIChatPassthrough, OpenAIChatPassthroughPorts
11
+ from .provider_responses_passthrough import (
12
+ ProviderResponsesPassthrough,
13
+ ProviderResponsesPassthroughPorts,
14
+ )
15
+ from .router_http import (
16
+ CodexBackendHttpAdapter,
17
+ CodexBackendRequestPorts,
18
+ CodexBackendRetryPorts,
19
+ CodexRoutedHeaderPolicy,
20
+ )
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class CodexBackendChannelPorts:
25
+ responses_to_anthropic: Callable[..., dict[str, Any]]
26
+ inject_pending: Callable[..., dict[str, Any]]
27
+ inject_tool_results: Callable[..., dict[str, Any]]
28
+ content_to_text: Callable[..., str]
29
+ begin_delivery: Callable[..., Any]
30
+ mark_delivery_success: Callable[..., Any]
31
+ commit_delivery: Callable[..., Any]
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class CodexBackendTransportPorts:
36
+ upstream_base: str
37
+ decorate_headers: Callable[..., dict[str, str]]
38
+ urlopen: Callable[..., Any]
39
+ timeout_seconds: Callable[..., float]
40
+ read_preamble: Callable[..., Any]
41
+ retry_wait_seconds: Callable[..., float]
42
+ log: Callable[..., Any]
43
+ publish_event: Callable[..., Any]
44
+ sleep: Callable[[float], None]
45
+ env_get: Callable[..., str | None]
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class CodexBackendReplayPorts:
50
+ """Durable upstream verdicts about replayed turns."""
51
+
52
+ rejected_reasoning_contains: Callable[[str], bool] = lambda _sealed: False
53
+ rejected_reasoning_record: Callable[[str], Any] = lambda _sealed: None
54
+ estimate_tokens: Callable[[Any], int] = lambda _body: 0
55
+ compact_responses: Callable[..., dict[str, Any]] = lambda body, _budget, **_kw: body
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class ProviderPassthroughProjectionPorts:
60
+ provider_headers: Callable[..., dict[str, str]]
61
+ chat_headers: Callable[..., dict[str, str]]
62
+ responses_headers: Callable[..., dict[str, str]]
63
+ upstream_model: Callable[..., str]
64
+ resolve_model: Callable[..., str]
65
+ apply_request_policy: Callable[..., dict[str, Any]]
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class ProviderPassthroughTransportPorts:
70
+ upstream_base: Callable[..., str]
71
+ join_url: Callable[[str, str], str]
72
+ urlopen: Callable[..., Any]
73
+ timeout_seconds: Callable[..., float]
74
+ copy_response_headers: Callable[..., None]
75
+ write_activity: Callable[..., None]
76
+
77
+
78
+ @dataclass(frozen=True, slots=True)
79
+ class CodexBackendContext:
80
+ channel: CodexBackendChannelPorts
81
+ transport: CodexBackendTransportPorts
82
+ provider_projection: ProviderPassthroughProjectionPorts
83
+ provider_transport: ProviderPassthroughTransportPorts
84
+ replay: CodexBackendReplayPorts = CodexBackendReplayPorts()
85
+
86
+ def routed_headers(
87
+ self,
88
+ pcfg: dict[str, Any],
89
+ inbound_headers: Any | None = None,
90
+ ) -> dict[str, str]:
91
+ del pcfg
92
+ return CodexRoutedHeaderPolicy(
93
+ decorate=self.transport.decorate_headers
94
+ ).project(inbound_headers)
95
+
96
+ @staticmethod
97
+ def routed_auth_error_message(message: str) -> str:
98
+ low = str(message or "").lower()
99
+ markers = (
100
+ "api.responses.write",
101
+ "insufficient permissions",
102
+ "unauthorized",
103
+ )
104
+ if not any(marker in low for marker in markers):
105
+ return message
106
+ guidance = (
107
+ " Codex routed is expected to forward Codex CLI native auth to the "
108
+ "ChatGPT Codex backend. If this mentions api.responses.write, the "
109
+ "request is still using the OpenAI Platform /v1 endpoint; upgrade "
110
+ "ciel-runtime and relaunch Codex routed so the local base URL is "
111
+ "/backend-api/codex."
112
+ )
113
+ return f"{message}{guidance}"
114
+
115
+ def project_channel_context(
116
+ self, body: dict[str, Any]
117
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
118
+ return CodexChannelContextProjector(
119
+ CodexChannelContextPorts(
120
+ self.channel.responses_to_anthropic,
121
+ self.channel.inject_pending,
122
+ self.channel.inject_tool_results,
123
+ self.channel.content_to_text,
124
+ )
125
+ ).project(body)
126
+
127
+ @staticmethod
128
+ def responses_input_as_list(value: Any) -> list[Any]:
129
+ return CodexChannelContextProjector.input_items(value)
130
+
131
+ @staticmethod
132
+ def copy_response_headers(
133
+ handler: BaseHTTPRequestHandler,
134
+ headers: Any,
135
+ ) -> None:
136
+ CodexBackendHttpAdapter.copy_response_headers(handler, headers)
137
+
138
+ def capacity_retry_limit(self) -> int:
139
+ raw = str(
140
+ self.transport.env_get("CIEL_RUNTIME_CODEX_CAPACITY_RETRIES") or "3"
141
+ ).strip()
142
+ try:
143
+ return max(0, min(10, int(raw)))
144
+ except ValueError:
145
+ return 3
146
+
147
+ def backend_adapter(self) -> CodexBackendHttpAdapter:
148
+ return CodexBackendHttpAdapter(
149
+ self.transport.upstream_base,
150
+ CodexBackendRequestPorts(
151
+ self.project_channel_context,
152
+ self.channel.begin_delivery,
153
+ self.routed_headers,
154
+ self.transport.urlopen,
155
+ self.transport.timeout_seconds,
156
+ ),
157
+ CodexBackendRetryPorts(
158
+ self.capacity_retry_limit,
159
+ self.transport.read_preamble,
160
+ self.transport.retry_wait_seconds,
161
+ self.transport.log,
162
+ self.transport.publish_event,
163
+ self.transport.sleep,
164
+ rejected_reasoning_contains=self.replay.rejected_reasoning_contains,
165
+ rejected_reasoning_record=self.replay.rejected_reasoning_record,
166
+ estimate_tokens=self.replay.estimate_tokens,
167
+ compact_responses=self.replay.compact_responses,
168
+ ),
169
+ )
170
+
171
+ def provider_responses_headers(
172
+ self,
173
+ provider: str,
174
+ pcfg: dict[str, Any],
175
+ inbound_headers: Any | None = None,
176
+ ) -> dict[str, str]:
177
+ headers = self.provider_projection.provider_headers(
178
+ provider,
179
+ pcfg,
180
+ inbound_headers,
181
+ "openai_responses",
182
+ )
183
+ if inbound_headers is None:
184
+ headers = {
185
+ name: value
186
+ for name, value in headers.items()
187
+ if str(name).casefold() != "anthropic-version"
188
+ }
189
+ return headers
190
+
191
+ def provider_chat_headers(
192
+ self,
193
+ provider: str,
194
+ pcfg: dict[str, Any],
195
+ inbound_headers: Any | None = None,
196
+ ) -> dict[str, str]:
197
+ return self.provider_projection.provider_headers(
198
+ provider,
199
+ pcfg,
200
+ inbound_headers,
201
+ "openai_chat",
202
+ )
203
+
204
+ def normalize_model(
205
+ self,
206
+ provider: str,
207
+ pcfg: dict[str, Any],
208
+ model: Any,
209
+ ) -> str:
210
+ return self.provider_projection.upstream_model(
211
+ provider,
212
+ pcfg,
213
+ self.provider_projection.resolve_model(provider, pcfg, model),
214
+ )
215
+
216
+ def normalize_request(
217
+ self,
218
+ provider: str,
219
+ pcfg: dict[str, Any],
220
+ body: dict[str, Any],
221
+ ) -> dict[str, Any]:
222
+ return self.provider_projection.apply_request_policy(
223
+ provider, pcfg, dict(body)
224
+ )
225
+
226
+ def chat_passthrough(self) -> OpenAIChatPassthrough:
227
+ return OpenAIChatPassthrough(
228
+ OpenAIChatPassthroughPorts(
229
+ normalize_model=self.normalize_model,
230
+ normalize_request=self.normalize_request,
231
+ upstream_base=self.provider_transport.upstream_base,
232
+ join_url=self.provider_transport.join_url,
233
+ headers=self.provider_projection.chat_headers,
234
+ urlopen=self.provider_transport.urlopen,
235
+ timeout_seconds=self.provider_transport.timeout_seconds,
236
+ copy_response_headers=self.provider_transport.copy_response_headers,
237
+ )
238
+ )
239
+
240
+ def forward_provider_chat(self, *args: Any, **kwargs: Any) -> None:
241
+ self.chat_passthrough().forward(*args, **kwargs)
242
+
243
+ def responses_passthrough(self) -> ProviderResponsesPassthrough:
244
+ return ProviderResponsesPassthrough(
245
+ ProviderResponsesPassthroughPorts(
246
+ project_channel_context=self.project_channel_context,
247
+ begin_channel_delivery=self.channel.begin_delivery,
248
+ normalize_model=self.normalize_model,
249
+ normalize_request=self.normalize_request,
250
+ upstream_base=self.provider_transport.upstream_base,
251
+ join_url=self.provider_transport.join_url,
252
+ headers=self.provider_projection.responses_headers,
253
+ urlopen=self.provider_transport.urlopen,
254
+ timeout_seconds=self.provider_transport.timeout_seconds,
255
+ copy_response_headers=self.provider_transport.copy_response_headers,
256
+ record_usage=lambda provider, model, usage: self.provider_transport.write_activity(
257
+ "success",
258
+ provider,
259
+ model,
260
+ protocol="openai_responses",
261
+ **usage,
262
+ ),
263
+ log=self.transport.log,
264
+ )
265
+ )
266
+
267
+ def forward_provider_responses(self, *args: Any, **kwargs: Any) -> Any:
268
+ return self.responses_passthrough().forward(*args, **kwargs)
269
+
270
+ def upstream_url(self, request_path: str, query: str = "") -> str:
271
+ return self.backend_adapter().upstream_url(request_path, query)
272
+
273
+ def forward_json(self, *args: Any, **kwargs: Any) -> Any:
274
+ return self.backend_adapter().forward_json(*args, **kwargs)
275
+
276
+ def forward_get(self, *args: Any, **kwargs: Any) -> None:
277
+ self.backend_adapter().forward_get(*args, **kwargs)
278
+
279
+ def forward_responses(
280
+ self,
281
+ handler: BaseHTTPRequestHandler,
282
+ provider: str,
283
+ pcfg: dict[str, Any],
284
+ body: dict[str, Any],
285
+ ) -> None:
286
+ delivery_body = self.forward_json(
287
+ handler,
288
+ provider,
289
+ pcfg,
290
+ body,
291
+ mutate_responses=True,
292
+ )
293
+ if delivery_body is None:
294
+ return
295
+ self.channel.mark_delivery_success(handler, "codex_responses_proxy")
296
+ self.channel.commit_delivery(delivery_body, handler)
297
+
298
+
299
+ @dataclass(frozen=True, slots=True)
300
+ class CodexBackendCompatibilityApi:
301
+ context: Callable[[], CodexBackendContext]
302
+
303
+ def routed_headers(self, *args: Any, **kwargs: Any) -> Any:
304
+ return self.context().routed_headers(*args, **kwargs)
305
+
306
+ def routed_auth_error_message(self, message: str) -> str:
307
+ return self.context().routed_auth_error_message(message)
308
+
309
+ def project_channel_context(self, *args: Any, **kwargs: Any) -> Any:
310
+ return self.context().project_channel_context(*args, **kwargs)
311
+
312
+ def responses_input_as_list(self, value: Any) -> list[Any]:
313
+ return self.context().responses_input_as_list(value)
314
+
315
+ def copy_response_headers(self, *args: Any, **kwargs: Any) -> None:
316
+ self.context().copy_response_headers(*args, **kwargs)
317
+
318
+ def backend_adapter(self) -> CodexBackendHttpAdapter:
319
+ return self.context().backend_adapter()
320
+
321
+ def provider_responses_headers(self, *args: Any, **kwargs: Any) -> Any:
322
+ return self.context().provider_responses_headers(*args, **kwargs)
323
+
324
+ def provider_chat_headers(self, *args: Any, **kwargs: Any) -> Any:
325
+ return self.context().provider_chat_headers(*args, **kwargs)
326
+
327
+ def chat_passthrough(self) -> OpenAIChatPassthrough:
328
+ return self.context().chat_passthrough()
329
+
330
+ def forward_provider_chat(self, *args: Any, **kwargs: Any) -> None:
331
+ self.context().forward_provider_chat(*args, **kwargs)
332
+
333
+ def responses_passthrough(self) -> ProviderResponsesPassthrough:
334
+ return self.context().responses_passthrough()
335
+
336
+ def forward_provider_responses(self, *args: Any, **kwargs: Any) -> Any:
337
+ return self.context().forward_provider_responses(*args, **kwargs)
338
+
339
+ def upstream_url(self, *args: Any, **kwargs: Any) -> str:
340
+ return self.context().upstream_url(*args, **kwargs)
341
+
342
+ def forward_json(self, *args: Any, **kwargs: Any) -> Any:
343
+ return self.context().forward_json(*args, **kwargs)
344
+
345
+ def capacity_retry_limit(self) -> int:
346
+ return self.context().capacity_retry_limit()
347
+
348
+ def forward_get(self, *args: Any, **kwargs: Any) -> None:
349
+ self.context().forward_get(*args, **kwargs)
350
+
351
+ def forward_responses(self, *args: Any, **kwargs: Any) -> None:
352
+ self.context().forward_responses(*args, **kwargs)
353
+
354
+
355
+ __all__ = [
356
+ "CodexBackendChannelPorts",
357
+ "CodexBackendCompatibilityApi",
358
+ "CodexBackendContext",
359
+ "CodexBackendReplayPorts",
360
+ "CodexBackendTransportPorts",
361
+ "ProviderPassthroughProjectionPorts",
362
+ "ProviderPassthroughTransportPorts",
363
+ ]
@@ -146,8 +146,20 @@ def normalize_codex_mcp_server(
146
146
  out: dict[str, Any] = {"type": server_type, "url": url}
147
147
  if explicit_type:
148
148
  out["_ciel_runtime_explicit_type"] = True
149
+ headers: dict[str, Any] = {}
150
+ raw_headers = raw_server.get("headers")
151
+ if isinstance(raw_headers, dict):
152
+ headers.update(raw_headers)
153
+ codex_http_headers = raw_server.get("http_headers")
154
+ if isinstance(codex_http_headers, dict):
155
+ headers.update(codex_http_headers)
156
+ if headers:
157
+ # Codex names literal HTTP headers ``http_headers`` while the shared
158
+ # MCP discovery/transport contract uses ``headers``. Normalize the
159
+ # spelling here so the independently owned notification worker keeps
160
+ # the same authentication as Codex's native MCP client.
161
+ out["headers"] = headers
149
162
  for key in (
150
- "headers",
151
163
  "env_http_headers",
152
164
  "bearer_token_env_var",
153
165
  "token_env_var",
@@ -0,0 +1,213 @@
1
+ """Assemble Codex CLI and app-server launch services from shared typed ports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+ from . import runtime_launch
9
+
10
+
11
+ Callback = Callable[..., Any]
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class CodexLaunchSharedConfigPorts:
16
+ apply_endpoint_policy: Callback
17
+ current_alias: Callback
18
+ current_launch_cwd_key: Callback
19
+ ensure_model_cache: Callback
20
+ get_current_provider: Callback
21
+ load_config: Callback
22
+ provider_mode_label: Callback
23
+ record_launch_state: Callback
24
+ model_catalog_args: Callback
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class CodexLaunchSharedInstallationPorts:
29
+ find_executable: Callback
30
+ install_codex: Callback
31
+ warn_multiple_installs: Callback
32
+ disable_prompts: Callback
33
+ has_passthrough_option: Callback
34
+ install_prompts: Callback
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class CodexLaunchSharedDispatchPorts:
39
+ launch_agy: Callback
40
+ launch_claude: Callback
41
+ launch_codex: Callback
42
+ launch_app_server: Callback
43
+ materialize_command: Callback
44
+ run_runtime_update: Callback
45
+ run_codex_update: Callback
46
+ run_prelaunch_menu: Callback
47
+ log_passthrough_mapping: Callback
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class CodexLaunchSharedRoutingPorts:
52
+ cleanup_services: Callback
53
+ routed_enabled: Callback
54
+ direct_native_enabled: Callback
55
+ readiness_errors: Callback
56
+ native_enabled: Callback
57
+ launch_enabled: Callback
58
+ run_with_router_lifetime: Callback
59
+ start_router: Callback
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class CodexLaunchSharedChannelPorts:
64
+ auto_import_channels: Callback
65
+ delivery_mode: Callback
66
+ capable_server_names: Callback
67
+ native_http_args: Callback
68
+ split_proxy_enabled: Callback
69
+ restore_config: Callback
70
+ start_sse: Callback
71
+ write_discovery_config: Callback
72
+ select_resume_session: Callback
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class CodexCliLaunchPorts:
77
+ process: runtime_launch.CodexLaunchProcess
78
+ policy: runtime_launch.CodexLaunchCliPolicy
79
+
80
+
81
+ @dataclass(frozen=True, slots=True)
82
+ class CodexAppServerLaunchPorts:
83
+ process: runtime_launch.CodexAppServerProcess
84
+ policy: runtime_launch.CodexAppServerCliPolicy
85
+
86
+
87
+ @dataclass(frozen=True, slots=True)
88
+ class CodexLaunchAssembly:
89
+ config: CodexLaunchSharedConfigPorts
90
+ installation: CodexLaunchSharedInstallationPorts
91
+ dispatch: CodexLaunchSharedDispatchPorts
92
+ routing: CodexLaunchSharedRoutingPorts
93
+ channel: CodexLaunchSharedChannelPorts
94
+ cli: CodexCliLaunchPorts
95
+ app_server: CodexAppServerLaunchPorts
96
+
97
+ def cli_services(self) -> runtime_launch.CodexLaunchServices:
98
+ return runtime_launch.CodexLaunchServices(
99
+ constants=runtime_launch.build_default_codex_launch_constants(),
100
+ process=self.cli.process,
101
+ cli_policy=self.cli.policy,
102
+ config=runtime_launch.CodexLaunchConfig(
103
+ apply_launch_endpoint_policy=self.config.apply_endpoint_policy,
104
+ current_alias=self.config.current_alias,
105
+ current_launch_cwd_key=self.config.current_launch_cwd_key,
106
+ ensure_model_cache_for_launch=self.config.ensure_model_cache,
107
+ get_current_provider=self.config.get_current_provider,
108
+ load_config=self.config.load_config,
109
+ provider_mode_label=self.config.provider_mode_label,
110
+ record_launch_state_for_cwd=self.config.record_launch_state,
111
+ codex_runtime_model_catalog_args=self.config.model_catalog_args,
112
+ ),
113
+ installation=runtime_launch.CodexLaunchInstallation(
114
+ disable_ciel_runtime_codex_prompts_for_native=self.installation.disable_prompts,
115
+ find_executable=self.installation.find_executable,
116
+ has_passthrough_option=self.installation.has_passthrough_option,
117
+ install_ciel_runtime_codex_prompts=self.installation.install_prompts,
118
+ install_codex_if_missing=self.installation.install_codex,
119
+ warn_if_multiple_ciel_runtime_installs=self.installation.warn_multiple_installs,
120
+ ),
121
+ dispatch=runtime_launch.CodexLaunchDispatch(
122
+ launch_agy=self.dispatch.launch_agy,
123
+ launch_claude=self.dispatch.launch_claude,
124
+ launch_codex_app_server=self.dispatch.launch_app_server,
125
+ log_codex_passthrough_mapping=self.dispatch.log_passthrough_mapping,
126
+ materialize_runtime_command=self.dispatch.materialize_command,
127
+ run_ciel_runtime_update_check=self.dispatch.run_runtime_update,
128
+ run_codex_update_check=self.dispatch.run_codex_update,
129
+ run_prelaunch_menu=self.dispatch.run_prelaunch_menu,
130
+ ),
131
+ routing=runtime_launch.CodexLaunchRouting(
132
+ cleanup_managed_services_for_provider=self.routing.cleanup_services,
133
+ codex_routed_enabled=self.routing.routed_enabled,
134
+ direct_native_codex_enabled=self.routing.direct_native_enabled,
135
+ launch_readiness_errors=self.routing.readiness_errors,
136
+ native_codex_enabled=self.routing.native_enabled,
137
+ run_with_router_lifetime=self.routing.run_with_router_lifetime,
138
+ start_router_if_needed=self.routing.start_router,
139
+ ),
140
+ channel=runtime_launch.CodexLaunchChannel(
141
+ auto_import_passthrough_channels=self.channel.auto_import_channels,
142
+ channel_delivery_mode=self.channel.delivery_mode,
143
+ codex_channel_capable_mcp_server_names=self.channel.capable_server_names,
144
+ codex_mcp_native_http_compat_args=self.channel.native_http_args,
145
+ codex_mcp_split_proxy_enabled=self.channel.split_proxy_enabled,
146
+ restore_codex_mcp_config_from_managed=self.channel.restore_config,
147
+ select_codex_resume_session=self.channel.select_resume_session,
148
+ start_codex_mcp_channel_sse_for_launch=self.channel.start_sse,
149
+ write_codex_mcp_config_for_channel_discovery=self.channel.write_discovery_config,
150
+ ),
151
+ )
152
+
153
+ def app_server_services(self) -> runtime_launch.CodexAppServerLaunchServices:
154
+ return runtime_launch.CodexAppServerLaunchServices(
155
+ constants=runtime_launch.build_default_codex_launch_constants(),
156
+ process=self.app_server.process,
157
+ config=runtime_launch.CodexAppServerConfig(
158
+ apply_launch_endpoint_policy=self.config.apply_endpoint_policy,
159
+ current_alias=self.config.current_alias,
160
+ current_launch_cwd_key=self.config.current_launch_cwd_key,
161
+ ensure_model_cache_for_launch=self.config.ensure_model_cache,
162
+ get_current_provider=self.config.get_current_provider,
163
+ load_config=self.config.load_config,
164
+ provider_mode_label=self.config.provider_mode_label,
165
+ record_launch_state_for_cwd=self.config.record_launch_state,
166
+ ),
167
+ cli_policy=self.app_server.policy,
168
+ installation=runtime_launch.CodexAppServerInstallation(
169
+ find_executable=self.installation.find_executable,
170
+ install_codex_if_missing=self.installation.install_codex,
171
+ warn_if_multiple_ciel_runtime_installs=self.installation.warn_multiple_installs,
172
+ ),
173
+ dispatch=runtime_launch.CodexAppServerDispatch(
174
+ launch_agy=self.dispatch.launch_agy,
175
+ launch_claude=self.dispatch.launch_claude,
176
+ launch_codex=self.dispatch.launch_codex,
177
+ run_ciel_runtime_update_check=self.dispatch.run_runtime_update,
178
+ run_codex_update_check=self.dispatch.run_codex_update,
179
+ run_prelaunch_menu=self.dispatch.run_prelaunch_menu,
180
+ ),
181
+ routing=runtime_launch.CodexAppServerRouting(
182
+ cleanup_managed_services_for_provider=self.routing.cleanup_services,
183
+ codex_launch_enabled_for_provider=self.routing.launch_enabled,
184
+ codex_routed_enabled=self.routing.routed_enabled,
185
+ direct_native_codex_enabled=self.routing.direct_native_enabled,
186
+ launch_readiness_errors=self.routing.readiness_errors,
187
+ native_codex_enabled=self.routing.native_enabled,
188
+ run_with_router_lifetime=self.routing.run_with_router_lifetime,
189
+ start_router_if_needed=self.routing.start_router,
190
+ ),
191
+ channel=runtime_launch.CodexAppServerChannel(
192
+ auto_import_passthrough_channels=self.channel.auto_import_channels,
193
+ channel_delivery_mode=self.channel.delivery_mode,
194
+ codex_channel_capable_mcp_server_names=self.channel.capable_server_names,
195
+ codex_mcp_native_http_compat_args=self.channel.native_http_args,
196
+ codex_mcp_split_proxy_enabled=self.channel.split_proxy_enabled,
197
+ restore_codex_mcp_config_from_managed=self.channel.restore_config,
198
+ start_codex_mcp_channel_sse_for_launch=self.channel.start_sse,
199
+ write_codex_mcp_config_for_channel_discovery=self.channel.write_discovery_config,
200
+ ),
201
+ )
202
+
203
+
204
+ __all__ = [
205
+ "CodexAppServerLaunchPorts",
206
+ "CodexCliLaunchPorts",
207
+ "CodexLaunchAssembly",
208
+ "CodexLaunchSharedChannelPorts",
209
+ "CodexLaunchSharedConfigPorts",
210
+ "CodexLaunchSharedDispatchPorts",
211
+ "CodexLaunchSharedInstallationPorts",
212
+ "CodexLaunchSharedRoutingPorts",
213
+ ]
@@ -178,6 +178,9 @@ class CodexLaunchConfigurationService:
178
178
  auto_compact_token_limit = None
179
179
  if auto_compact_token_limit is not None and auto_compact_token_limit <= 0:
180
180
  auto_compact_token_limit = None
181
+ metadata = provider_config.get("codex_model_catalog")
182
+ if not isinstance(metadata, Mapping):
183
+ metadata = None
181
184
  return self.catalog.write(
182
185
  codex,
183
186
  CodexModelCatalogSpec(
@@ -186,16 +189,42 @@ class CodexLaunchConfigurationService:
186
189
  context_window=context_window,
187
190
  effort=str(provider_config.get("effort_level") or "").strip().lower(),
188
191
  auto_compact_token_limit=auto_compact_token_limit,
192
+ metadata=metadata,
189
193
  ),
190
194
  catalog_env,
191
195
  )
192
196
 
197
+ def auto_compact_config_args(self, cfg: dict[str, Any]) -> list[str]:
198
+ """Move Codex's own compaction trigger to the operator's threshold.
199
+
200
+ A session that crosses providers carries history built under whatever
201
+ window was in force at the time. Codex compacts on its own, but only
202
+ once its configured limit is reached — and by then a history grown
203
+ under a larger window no longer fits the smaller one, so the compaction
204
+ request itself is refused and the turn cannot recover.
205
+
206
+ Passing the threshold keeps that trigger where the operator wants it
207
+ rather than claiming a window size of our own: it only decides when
208
+ Codex compacts, never how much context it believes it has.
209
+ """
210
+
211
+ _provider, provider_config = self.model.current_provider(cfg)
212
+ try:
213
+ limit = int(provider_config.get("codex_auto_compact_window"))
214
+ except (TypeError, ValueError):
215
+ return []
216
+ if limit <= 0:
217
+ return []
218
+ return ["-c", f"model_auto_compact_token_limit={limit}"]
219
+
193
220
  def runtime_model_catalog_args(
194
221
  self, codex: str, cfg: dict[str, Any]
195
222
  ) -> list[str]:
196
223
  path = self.write_runtime_model_catalog(codex, cfg)
197
224
  if path is None:
198
- return []
225
+ # A native provider keeps its own bundled catalog; only the
226
+ # compaction threshold is ours to set.
227
+ return self.auto_compact_config_args(cfg)
199
228
  value = self.policy.toml_string(str(path.resolve()))
200
229
  return ["-c", f"model_catalog_json={value}"]
201
230