@oneciel-ai/ciel-runtime 0.2.37 → 0.2.39
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/CHANGELOG.md +85 -0
- package/README.md +5 -2
- package/ciel_runtime.py +94 -96
- package/ciel_runtime_support/anthropic_model_policy.py +20 -3
- package/ciel_runtime_support/architecture.py +5 -0
- package/ciel_runtime_support/channel_inflight.py +4 -4
- package/ciel_runtime_support/channel_injection.py +56 -1
- package/ciel_runtime_support/channel_mcp_tools.py +34 -0
- package/ciel_runtime_support/channel_message_policy.py +9 -1
- package/ciel_runtime_support/channel_message_prompt.py +23 -0
- package/ciel_runtime_support/channel_pending_injection.py +86 -32
- package/ciel_runtime_support/channel_pending_poll.py +6 -2
- package/ciel_runtime_support/channel_runtime_environment.py +0 -11
- package/ciel_runtime_support/channel_terminal_context.py +0 -6
- package/ciel_runtime_support/channel_terminal_proxy.py +3 -42
- package/ciel_runtime_support/channel_transcript_repository.py +16 -1
- package/ciel_runtime_support/channel_wake_claim_repository.py +0 -19
- package/ciel_runtime_support/channel_wake_context.py +6 -13
- package/ciel_runtime_support/channel_wake_delivery_repository.py +50 -0
- package/ciel_runtime_support/chat_http_controller.py +88 -5
- package/ciel_runtime_support/claude_environment.py +100 -3
- package/ciel_runtime_support/claude_launch_assembly.py +4 -0
- package/ciel_runtime_support/claude_session_socket.py +181 -0
- package/ciel_runtime_support/cli_application_context.py +18 -10
- package/ciel_runtime_support/cli_dispatch.py +19 -3
- package/ciel_runtime_support/cli_parser.py +2 -0
- package/ciel_runtime_support/cli_usage.py +3 -2
- package/ciel_runtime_support/codex_backend_context.py +8 -3
- package/ciel_runtime_support/codex_completion_gate.py +198 -0
- package/ciel_runtime_support/codex_turn_recovery.py +240 -83
- package/ciel_runtime_support/compatibility_protocol.py +5 -2
- package/ciel_runtime_support/config_migrations.py +79 -0
- package/ciel_runtime_support/config_repository.py +7 -0
- package/ciel_runtime_support/context_summary_policy.py +24 -4
- package/ciel_runtime_support/external_event_menu.py +107 -0
- package/ciel_runtime_support/external_event_receiver.py +19 -1
- package/ciel_runtime_support/launch_state.py +2 -0
- package/ciel_runtime_support/managed_tool_injection.py +81 -0
- package/ciel_runtime_support/muse_runtime_context.py +291 -0
- package/ciel_runtime_support/ollama_thinking.py +12 -26
- package/ciel_runtime_support/prelaunch.py +13 -0
- package/ciel_runtime_support/prelaunch_launch_panel.py +2 -1
- package/ciel_runtime_support/prompt_compaction.py +39 -0
- package/ciel_runtime_support/provider_files_proxy.py +248 -0
- package/ciel_runtime_support/provider_model_identity.py +10 -2
- package/ciel_runtime_support/provider_option_cli.py +1 -1
- package/ciel_runtime_support/provider_request_access.py +54 -1
- package/ciel_runtime_support/provider_responses_passthrough.py +152 -15
- package/ciel_runtime_support/providers/alibaba.py +34 -1
- package/ciel_runtime_support/providers/anthropic.py +18 -1
- package/ciel_runtime_support/providers/meta.py +252 -21
- package/ciel_runtime_support/pseudo_tool_parser.py +17 -1
- package/ciel_runtime_support/remote_bridge.py +4 -0
- package/ciel_runtime_support/remote_instructions.py +5 -0
- package/ciel_runtime_support/remote_memory.py +1 -1
- package/ciel_runtime_support/responses_cache_diagnostics.py +82 -0
- package/ciel_runtime_support/responses_custom_tool_bridge.py +282 -0
- package/ciel_runtime_support/responses_input_compatibility.py +19 -3
- package/ciel_runtime_support/router_http.py +301 -4
- package/ciel_runtime_support/router_observability_context.py +26 -0
- package/ciel_runtime_support/router_server_context.py +1 -0
- package/ciel_runtime_support/runtime_adapters.py +31 -0
- package/ciel_runtime_support/runtime_constants.py +23 -1
- package/ciel_runtime_support/runtime_input_gateway.py +85 -5
- package/ciel_runtime_support/runtime_input_status.py +150 -0
- package/ciel_runtime_support/runtime_launch.py +35 -4
- package/ciel_runtime_support/runtime_paths.py +1 -0
- package/ciel_runtime_support/speech_http_controller.py +3 -2
- package/ciel_runtime_support/streaming_anthropic.py +3 -0
- package/ciel_runtime_support/tool_call_events.py +98 -0
- package/ciel_runtime_support/transcript_delta_delivery.py +140 -1
- package/ciel_runtime_support/ui_text.py +1 -0
- package/ciel_runtime_support/web_search_result_events.py +130 -0
- package/ciel_runtime_support/web_ui.py +1 -0
- package/ciel_runtime_support/windows_conpty.py +92 -8
- package/ciel_runtime_support/workspace_mcp.py +4 -1
- package/ciel_runtime_support/workspace_state.py +1 -0
- package/docs/Configuration.md +20 -4
- package/docs/MCP-Channels.md +79 -6
- package/docs/Managed-Tool-Injection.md +25 -0
- package/docs/Module-Map.md +4 -0
- package/docs/Muse-Code.md +73 -0
- package/docs/Observability.md +39 -0
- package/docs/Providers.md +29 -2
- package/docs/Remote-Bridge.md +12 -0
- package/docs/Router.md +1 -0
- package/docs/journal/2026/09/01/diagnostics/anthropic/router/fable51-usage-credits.okf +70 -0
- package/docs/journal/2026/09/01/implementation/claude/session-socket/all-input-paths/default-delivery.okf +133 -0
- package/docs/journal/2026/09/01/implementation/claude/session-socket/windows-direct-input.okf +112 -0
- package/docs/journal/2026/09/02/diagnostics/cache/alibaba/qwen38-post-restart-hit-rate.okf +107 -0
- package/docs/journal/2026/09/02/diagnostics/cache/ollama-cloud/kimi-k3-codex-hit-rate.okf +91 -0
- package/docs/journal/2026/09/02/diagnostics/codex/tui/statusline-cache-metrics.okf +44 -0
- package/docs/journal/2026/09/02/diagnostics/input/transport/fallback-behavior.okf +31 -0
- package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-agent-turn-recovery.okf +125 -0
- package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-parameters.okf +128 -0
- package/docs/journal/2026/09/02/diagnostics/remote/mia/socket-tui-visibility.okf +47 -0
- package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-latency.okf +108 -0
- package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-vs-gpt56-latency.okf +92 -0
- package/docs/journal/2026/09/02/implementation/anthropic/context/one-million-defaults.okf +85 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.okf +104 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.png +0 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/responses-session-cache.okf +97 -0
- package/docs/journal/2026/09/02/implementation/providers/alibaba/qwen38/0902-parameter-alignment.okf +97 -0
- package/docs/journal/2026/09/02/implementation/providers/meta/multimodal-tools/contributor-protocol-adoption.okf +140 -0
- package/docs/journal/2026/09/02/implementation/providers/meta/muse-spark-1.3-support.okf +53 -0
- package/docs/journal/2026/09/02/monitoring/cache/alibaba/cielarvis-qwen38-live-hit-rate.okf +858 -0
- package/docs/journal/2026/09/02/operations/release/main-merge-local-deploy.okf +36 -0
- package/docs/journal/2026/09/02/operations/release/nightly/local-cache-deployment.okf +54 -0
- package/docs/journal/2026/09/02/operations/release/nightly/ollama-kimi-recovery-model-audit-deployment.okf +58 -0
- package/docs/journal/2026/09/02/operations/release/nightly/qwen38-cache-improvement-deployment.okf +72 -0
- package/docs/journal/2026/09/02/research/cache/alibaba/qwen38-hit-rate-improvement.okf +233 -0
- package/docs/journal/2026/09/02/research/cache/codex/provider-scope.okf +77 -0
- package/docs/journal/2026/09/02/research/providers/ollama-cloud/desktop-model-exhaustive-audit.okf +159 -0
- package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-cache-hit.okf +148 -0
- package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-required-schema.okf +127 -0
- package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-resumed-session-stall.okf +160 -0
- package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-substantive-dangling-action.okf +94 -0
- package/docs/journal/2026/09/03/diagnostics/remote/kevin/codex-gpt-early-turn-completion.okf +162 -0
- package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/default-yolo.okf +40 -0
- package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/native-subscription-router.okf +86 -0
- package/docs/journal/2026/09/03/operations/deployment/local/session-socket-default-fallback.okf +41 -0
- package/docs/journal/2026/09/03/research/deployment/colab/tailscale/credential-storage.okf +41 -0
- package/docs/journal/2026/09/03/research/providers/meta/muse-code/subscription-billing-boundary.okf +64 -0
- package/docs/journal/2026/09/03/research/web-chat/voice/instruction-format.okf +135 -0
- package/docs/journal/2026/09/04/diagnostics/claude/context/early-auto-compaction.okf +75 -0
- package/docs/journal/2026/09/04/diagnostics/claude/context/fable-51-status-200k.okf +54 -0
- package/docs/journal/2026/09/04/diagnostics/runtime/codex/responses/replayed-item-id-validation.okf +74 -0
- package/docs/journal/2026/09/04/diagnostics/workspaces/onecieldmsui/codex/replay-stall.okf +100 -0
- package/docs/journal/2026/09/04/implementation/observability/tool-calls/websocket-stream.okf +49 -0
- package/docs/journal/2026/09/04/implementation/web-chat/input/raw-injection.okf +42 -0
- package/docs/journal/2026/09/05/diagnostics/tty/claude-raw-idle-render.okf +25 -0
- package/docs/journal/2026/09/05/implementation/events/search-response-urls.okf +23 -0
- package/docs/journal/2026/09/05/implementation/tools/native-injection.okf +19 -0
- package/docs/journal/2026/09/05/implementation/windows/conpty/prompt-delivery-lifecycle.okf +43 -0
- package/docs/journal/2026/09/05/release/nightly/conpty-native-tools.okf +13 -0
- package/docs/journal/2026/09/05/release/nightly/search-result-urls.okf +14 -0
- package/docs/journal/2026/09/06/implementation/codex/inherited-web-mcp.okf +22 -0
- package/docs/journal/2026/09/07/fixes/codex/native-web-transport.okf +39 -0
- package/docs/journal/2026/09/07/integration/main/nightly-merge.okf +13 -0
- package/docs/journal/2026/09/07/release/stable/0.2.38.okf +10 -0
- package/docs/journal/2026/09/07/release/stable/0.2.39.okf +10 -0
- package/package.json +1 -1
|
@@ -228,12 +228,42 @@ def responses_tail_is_safe(items: list[dict[str, Any]], start: int) -> bool:
|
|
|
228
228
|
)
|
|
229
229
|
|
|
230
230
|
|
|
231
|
+
def responses_stable_checkpoint_start(
|
|
232
|
+
items: list[dict[str, Any]], start: int, checkpoint_items: int
|
|
233
|
+
) -> int:
|
|
234
|
+
"""Move an omitted-prefix boundary onto a stable, pair-safe checkpoint.
|
|
235
|
+
|
|
236
|
+
A Responses cache reuses an exact prefix. Advancing the ordinary sliding
|
|
237
|
+
boundary by a few items on every turn regenerates the summary at item zero
|
|
238
|
+
and invalidates that prefix. Rounding the boundary forward means appended
|
|
239
|
+
turns reuse the same summary until they cross the next checkpoint.
|
|
240
|
+
|
|
241
|
+
Moving forward is conservative for the wire budget: it summarizes more old
|
|
242
|
+
input and retains less verbatim history. The final item is always retained,
|
|
243
|
+
and a checkpoint never leaves a tool output without its matching call.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
width = max(0, int(checkpoint_items or 0))
|
|
248
|
+
except (TypeError, ValueError):
|
|
249
|
+
width = 0
|
|
250
|
+
if width <= 1 or start <= 0 or start >= len(items):
|
|
251
|
+
return start
|
|
252
|
+
checkpoint = ((start + width - 1) // width) * width
|
|
253
|
+
if checkpoint >= len(items):
|
|
254
|
+
return start
|
|
255
|
+
while checkpoint < len(items) and not responses_tail_is_safe(items, checkpoint):
|
|
256
|
+
checkpoint += 1
|
|
257
|
+
return checkpoint if checkpoint < len(items) else start
|
|
258
|
+
|
|
259
|
+
|
|
231
260
|
def compact_responses_input_for_budget(
|
|
232
261
|
body: dict[str, Any],
|
|
233
262
|
budget_tokens: int,
|
|
234
263
|
*,
|
|
235
264
|
provider: str = "",
|
|
236
265
|
model: str = "",
|
|
266
|
+
stable_prefix_checkpoint_items: int = 0,
|
|
237
267
|
services: PromptCompactionServices,
|
|
238
268
|
) -> dict[str, Any]:
|
|
239
269
|
"""Fit a Responses ``input`` array inside the target model's budget.
|
|
@@ -278,6 +308,9 @@ def compact_responses_input_for_budget(
|
|
|
278
308
|
tail_start = len(typed) - 1
|
|
279
309
|
while tail_start > 0 and not responses_tail_is_safe(typed, tail_start):
|
|
280
310
|
tail_start -= 1
|
|
311
|
+
tail_start = responses_stable_checkpoint_start(
|
|
312
|
+
typed, tail_start, stable_prefix_checkpoint_items
|
|
313
|
+
)
|
|
281
314
|
|
|
282
315
|
def projected(omitted: list[dict[str, Any]]) -> dict[str, Any]:
|
|
283
316
|
summary = text.build_summary(
|
|
@@ -299,6 +332,12 @@ def compact_responses_input_for_budget(
|
|
|
299
332
|
if not tail:
|
|
300
333
|
tail = [typed[-1]]
|
|
301
334
|
break
|
|
335
|
+
next_start = responses_stable_checkpoint_start(
|
|
336
|
+
typed,
|
|
337
|
+
len(typed) - len(tail),
|
|
338
|
+
stable_prefix_checkpoint_items,
|
|
339
|
+
)
|
|
340
|
+
tail = typed[next_start:]
|
|
302
341
|
out["input"] = [projected(typed[: len(typed) - len(tail)]), *tail]
|
|
303
342
|
final_tokens = runtime.estimate_tokens(out)
|
|
304
343
|
runtime.log(
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Streaming passthrough for provider-native OpenAI Files endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import urllib.request
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
PROVIDER_FILES_PATH = "/v1/files"
|
|
13
|
+
META_FILE_UPLOAD_MAX_BYTES = 1_073_741_824
|
|
14
|
+
# Multipart boundaries and form fields are transport overhead rather than file
|
|
15
|
+
# bytes. Leave one MiB for that envelope while Meta enforces the exact 1 GiB
|
|
16
|
+
# file limit upstream.
|
|
17
|
+
META_FILE_UPLOAD_WIRE_MAX_BYTES = META_FILE_UPLOAD_MAX_BYTES + 1_048_576
|
|
18
|
+
FILES_API_PROVIDERS = frozenset({"meta"})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_provider_files_path(path: str) -> bool:
|
|
22
|
+
normalized = urllib.parse.urlparse(str(path or "")).path
|
|
23
|
+
return normalized == PROVIDER_FILES_PATH or normalized.startswith(
|
|
24
|
+
PROVIDER_FILES_PATH + "/"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _LengthLimitedReader:
|
|
29
|
+
"""Expose exactly one HTTP request body without waiting for socket EOF."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, stream: Any, length: int) -> None:
|
|
32
|
+
self._stream = stream
|
|
33
|
+
self._remaining = length
|
|
34
|
+
|
|
35
|
+
def read(self, size: int = -1) -> bytes:
|
|
36
|
+
if self._remaining <= 0:
|
|
37
|
+
return b""
|
|
38
|
+
requested = self._remaining if size is None or size < 0 else min(size, self._remaining)
|
|
39
|
+
data = self._stream.read(requested)
|
|
40
|
+
if not data and self._remaining:
|
|
41
|
+
raise EOFError(
|
|
42
|
+
f"multipart request ended with {self._remaining} bytes unread"
|
|
43
|
+
)
|
|
44
|
+
self._remaining -= len(data)
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class ProviderFilesProxyPorts:
|
|
50
|
+
current_provider: Callable[[dict[str, Any]], tuple[str, dict[str, Any]]]
|
|
51
|
+
bridge_enabled: Callable[[dict[str, Any]], bool]
|
|
52
|
+
bridge_is_request: Callable[[Any, dict[str, Any]], bool]
|
|
53
|
+
bridge_resolve: Callable[..., Any]
|
|
54
|
+
upstream_base: Callable[[str, dict[str, Any]], str]
|
|
55
|
+
join_url: Callable[[str, str], str]
|
|
56
|
+
headers: Callable[..., dict[str, str]]
|
|
57
|
+
urlopen: Callable[..., Any]
|
|
58
|
+
timeout_seconds: Callable[[dict[str, Any]], float]
|
|
59
|
+
copy_response_headers: Callable[[Any, Any], None]
|
|
60
|
+
write_json: Callable[..., Any]
|
|
61
|
+
log: Callable[[str, str], Any] = lambda _level, _message: None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ProviderFilesProxy:
|
|
65
|
+
"""Proxy Meta's Files API without buffering uploads in router memory."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, ports: ProviderFilesProxyPorts) -> None:
|
|
68
|
+
self._ports = ports
|
|
69
|
+
|
|
70
|
+
def _route(
|
|
71
|
+
self, handler: Any, config: dict[str, Any], path: str
|
|
72
|
+
) -> tuple[str, dict[str, Any]]:
|
|
73
|
+
if self._ports.bridge_enabled(config) and self._ports.bridge_is_request(
|
|
74
|
+
handler, config
|
|
75
|
+
):
|
|
76
|
+
route = self._ports.bridge_resolve(config, handler.headers, {}, path)
|
|
77
|
+
return route.provider, route.provider_config
|
|
78
|
+
return self._ports.current_provider(config)
|
|
79
|
+
|
|
80
|
+
def _reject(
|
|
81
|
+
self, handler: Any, status: int, error_type: str, message: str
|
|
82
|
+
) -> bool:
|
|
83
|
+
handler.close_connection = True
|
|
84
|
+
self._ports.write_json(
|
|
85
|
+
handler,
|
|
86
|
+
{
|
|
87
|
+
"error": {
|
|
88
|
+
"message": message,
|
|
89
|
+
"type": error_type,
|
|
90
|
+
"param": None,
|
|
91
|
+
"code": error_type,
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
status,
|
|
95
|
+
)
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
def _target(
|
|
99
|
+
self, handler: Any, config: dict[str, Any], path: str
|
|
100
|
+
) -> tuple[str, dict[str, Any], str] | None:
|
|
101
|
+
if not is_provider_files_path(path):
|
|
102
|
+
return None
|
|
103
|
+
try:
|
|
104
|
+
provider, provider_config = self._route(handler, config, path)
|
|
105
|
+
except ValueError as exc:
|
|
106
|
+
self._reject(handler, 400, "invalid_request_error", str(exc))
|
|
107
|
+
return None
|
|
108
|
+
if provider not in FILES_API_PROVIDERS:
|
|
109
|
+
self._reject(
|
|
110
|
+
handler,
|
|
111
|
+
404,
|
|
112
|
+
"not_found_error",
|
|
113
|
+
f"Provider does not expose a native Files API through Ciel Runtime: {provider}",
|
|
114
|
+
)
|
|
115
|
+
return None
|
|
116
|
+
parsed = urllib.parse.urlparse(handler.path)
|
|
117
|
+
url = self._ports.join_url(
|
|
118
|
+
self._ports.upstream_base(provider, provider_config), parsed.path
|
|
119
|
+
)
|
|
120
|
+
if parsed.query:
|
|
121
|
+
url += "?" + parsed.query
|
|
122
|
+
return provider, provider_config, url
|
|
123
|
+
|
|
124
|
+
def _relay(
|
|
125
|
+
self,
|
|
126
|
+
handler: Any,
|
|
127
|
+
provider: str,
|
|
128
|
+
provider_config: dict[str, Any],
|
|
129
|
+
request: urllib.request.Request,
|
|
130
|
+
) -> bool:
|
|
131
|
+
try:
|
|
132
|
+
response = self._ports.urlopen(
|
|
133
|
+
request,
|
|
134
|
+
timeout=self._ports.timeout_seconds(provider_config),
|
|
135
|
+
provider=provider,
|
|
136
|
+
pcfg=provider_config,
|
|
137
|
+
)
|
|
138
|
+
except urllib.error.HTTPError as exc:
|
|
139
|
+
response = exc
|
|
140
|
+
with response:
|
|
141
|
+
status = getattr(response, "status", None) or getattr(
|
|
142
|
+
response, "code", 200
|
|
143
|
+
)
|
|
144
|
+
handler.send_response(status)
|
|
145
|
+
self._ports.copy_response_headers(handler, response.headers)
|
|
146
|
+
handler.end_headers()
|
|
147
|
+
while chunk := response.read(65_536):
|
|
148
|
+
handler.wfile.write(chunk)
|
|
149
|
+
handler.wfile.flush()
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
def get(self, handler: Any, path: str, config: dict[str, Any]) -> bool:
|
|
153
|
+
target = self._target(handler, config, path)
|
|
154
|
+
if target is None:
|
|
155
|
+
return is_provider_files_path(path)
|
|
156
|
+
provider, provider_config, url = target
|
|
157
|
+
request = urllib.request.Request(
|
|
158
|
+
url,
|
|
159
|
+
headers=self._ports.headers(
|
|
160
|
+
provider, provider_config, handler.headers, "openai_responses"
|
|
161
|
+
),
|
|
162
|
+
method="GET",
|
|
163
|
+
)
|
|
164
|
+
return self._relay(handler, provider, provider_config, request)
|
|
165
|
+
|
|
166
|
+
def post(
|
|
167
|
+
self,
|
|
168
|
+
handler: Any,
|
|
169
|
+
path: str,
|
|
170
|
+
content_length: int,
|
|
171
|
+
content_type: str,
|
|
172
|
+
config: dict[str, Any],
|
|
173
|
+
) -> bool:
|
|
174
|
+
if not is_provider_files_path(path):
|
|
175
|
+
return False
|
|
176
|
+
if path != PROVIDER_FILES_PATH:
|
|
177
|
+
return self._reject(
|
|
178
|
+
handler, 404, "not_found_error", f"Unsupported Files API path: {path}"
|
|
179
|
+
)
|
|
180
|
+
if "multipart/form-data" not in content_type.casefold():
|
|
181
|
+
return self._reject(
|
|
182
|
+
handler,
|
|
183
|
+
415,
|
|
184
|
+
"invalid_request_error",
|
|
185
|
+
"POST /v1/files requires multipart/form-data",
|
|
186
|
+
)
|
|
187
|
+
if content_length <= 0 or content_length > META_FILE_UPLOAD_WIRE_MAX_BYTES:
|
|
188
|
+
return self._reject(
|
|
189
|
+
handler,
|
|
190
|
+
413,
|
|
191
|
+
"request_too_large",
|
|
192
|
+
"Meta Files API multipart upload exceeds the 1 GiB file limit",
|
|
193
|
+
)
|
|
194
|
+
target = self._target(handler, config, path)
|
|
195
|
+
if target is None:
|
|
196
|
+
return True
|
|
197
|
+
provider, provider_config, url = target
|
|
198
|
+
headers = self._ports.headers(
|
|
199
|
+
provider, provider_config, handler.headers, "openai_responses"
|
|
200
|
+
)
|
|
201
|
+
headers["content-length"] = str(content_length)
|
|
202
|
+
request = urllib.request.Request(
|
|
203
|
+
url,
|
|
204
|
+
data=_LengthLimitedReader(handler.rfile, content_length),
|
|
205
|
+
headers=headers,
|
|
206
|
+
method="POST",
|
|
207
|
+
)
|
|
208
|
+
# An upstream can reject before consuming the whole body. Closing the
|
|
209
|
+
# client connection prevents unread multipart bytes from becoming a
|
|
210
|
+
# second HTTP request on this socket.
|
|
211
|
+
handler.close_connection = True
|
|
212
|
+
self._ports.log(
|
|
213
|
+
"INFO",
|
|
214
|
+
f"provider_files_upload provider={provider} bytes={content_length}",
|
|
215
|
+
)
|
|
216
|
+
return self._relay(handler, provider, provider_config, request)
|
|
217
|
+
|
|
218
|
+
def delete(self, handler: Any, path: str, config: dict[str, Any]) -> bool:
|
|
219
|
+
target = self._target(handler, config, path)
|
|
220
|
+
if target is None:
|
|
221
|
+
return is_provider_files_path(path)
|
|
222
|
+
if path == PROVIDER_FILES_PATH:
|
|
223
|
+
return self._reject(
|
|
224
|
+
handler,
|
|
225
|
+
405,
|
|
226
|
+
"invalid_request_error",
|
|
227
|
+
"DELETE /v1/files requires a file id",
|
|
228
|
+
)
|
|
229
|
+
provider, provider_config, url = target
|
|
230
|
+
request = urllib.request.Request(
|
|
231
|
+
url,
|
|
232
|
+
headers=self._ports.headers(
|
|
233
|
+
provider, provider_config, handler.headers, "openai_responses"
|
|
234
|
+
),
|
|
235
|
+
method="DELETE",
|
|
236
|
+
)
|
|
237
|
+
return self._relay(handler, provider, provider_config, request)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
__all__ = [
|
|
241
|
+
"FILES_API_PROVIDERS",
|
|
242
|
+
"META_FILE_UPLOAD_MAX_BYTES",
|
|
243
|
+
"META_FILE_UPLOAD_WIRE_MAX_BYTES",
|
|
244
|
+
"PROVIDER_FILES_PATH",
|
|
245
|
+
"ProviderFilesProxy",
|
|
246
|
+
"ProviderFilesProxyPorts",
|
|
247
|
+
"is_provider_files_path",
|
|
248
|
+
]
|
|
@@ -64,9 +64,17 @@ class ProviderModelIdentityService:
|
|
|
64
64
|
return self.adapters.create(provider).upstream_api_model_id(str(model_id or ""))
|
|
65
65
|
|
|
66
66
|
def alias_for(self, provider: str, model_id: str) -> str:
|
|
67
|
-
|
|
67
|
+
adapter = self.adapters.create(provider)
|
|
68
|
+
if adapter.preserves_claude_model_alias(model_id):
|
|
68
69
|
return model_id
|
|
69
|
-
|
|
70
|
+
one_million = adapter.preserves_claude_context_suffix_in_alias() and bool(
|
|
71
|
+
re.search(r"\[1m\]\s*$", str(model_id or ""), re.IGNORECASE)
|
|
72
|
+
)
|
|
73
|
+
base_model_id = (
|
|
74
|
+
self.strip_claude_context_suffix(model_id) if one_million else model_id
|
|
75
|
+
)
|
|
76
|
+
alias = f"ciel-runtime-{provider}-{self.slug(base_model_id)}"
|
|
77
|
+
return f"{alias}[1m]" if one_million else alias
|
|
70
78
|
|
|
71
79
|
def unslug_alias(
|
|
72
80
|
self,
|
|
@@ -94,7 +94,7 @@ DEFAULT_PROVIDER_NOTES: Mapping[str, Sequence[str]] = {
|
|
|
94
94
|
}
|
|
95
95
|
DEFAULT_UNSUPPORTED_MESSAGE = (
|
|
96
96
|
"Provider options are available for anthropic, ollama, ollama-cloud, "
|
|
97
|
-
"deepseek, opencode, opencode-go, kimi, z.ai, fireworks, vllm, "
|
|
97
|
+
"deepseek, opencode, opencode-go, kimi, meta, z.ai, fireworks, vllm, "
|
|
98
98
|
"lm-studio, nvidia-hosted, self-hosted-nim, and openrouter."
|
|
99
99
|
)
|
|
100
100
|
|
|
@@ -7,7 +7,11 @@ from dataclasses import dataclass
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from ciel_runtime_support.architecture import MessageProtocol, ProviderRequestPolicy
|
|
10
|
-
from ciel_runtime_support.header_forwarding import
|
|
10
|
+
from ciel_runtime_support.header_forwarding import (
|
|
11
|
+
CONFIGURED_PROVIDER_CREDENTIAL_HEADERS,
|
|
12
|
+
HOP_BY_HOP_REQUEST_HEADERS,
|
|
13
|
+
project_end_to_end_request_headers,
|
|
14
|
+
)
|
|
11
15
|
from ciel_runtime_support.remote_bridge import (
|
|
12
16
|
REMOTE_BRIDGE_CONFIG_MARKER,
|
|
13
17
|
REQUEST_API_KEY_MARKER,
|
|
@@ -17,6 +21,9 @@ from ciel_runtime_support.remote_bridge import (
|
|
|
17
21
|
_HOST_CREDENTIAL_SCOPE_HEADERS = frozenset(
|
|
18
22
|
{"openai-organization", "openai-project"}
|
|
19
23
|
)
|
|
24
|
+
_CONFIGURED_PROTOCOL_HEADER_EXCLUSIONS = (
|
|
25
|
+
CONFIGURED_PROVIDER_CREDENTIAL_HEADERS | HOP_BY_HOP_REQUEST_HEADERS
|
|
26
|
+
)
|
|
20
27
|
|
|
21
28
|
|
|
22
29
|
@dataclass(frozen=True, slots=True)
|
|
@@ -46,6 +53,39 @@ class ProviderRequestAccessService:
|
|
|
46
53
|
ports: ProviderRequestAccessPorts
|
|
47
54
|
effects: ProviderRequestAccessEffects
|
|
48
55
|
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _configured_protocol_headers(
|
|
58
|
+
config: dict[str, Any], protocol: MessageProtocol | None
|
|
59
|
+
) -> dict[str, str]:
|
|
60
|
+
"""Project safe, protocol-specific provider headers from configuration.
|
|
61
|
+
|
|
62
|
+
Credentials and connection-owned headers stay under the existing
|
|
63
|
+
adapter/transport policies. This hook is for provider protocol
|
|
64
|
+
switches such as Alibaba's Responses session cache header.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
if protocol is None:
|
|
68
|
+
return {}
|
|
69
|
+
configured = config.get("protocol_headers")
|
|
70
|
+
if not isinstance(configured, Mapping):
|
|
71
|
+
return {}
|
|
72
|
+
selected = configured.get(protocol)
|
|
73
|
+
if not isinstance(selected, Mapping):
|
|
74
|
+
return {}
|
|
75
|
+
projected: dict[str, str] = {}
|
|
76
|
+
for raw_name, raw_value in selected.items():
|
|
77
|
+
name = str(raw_name).strip()
|
|
78
|
+
folded = name.casefold()
|
|
79
|
+
if (
|
|
80
|
+
not name
|
|
81
|
+
or raw_value is None
|
|
82
|
+
or folded in _CONFIGURED_PROTOCOL_HEADER_EXCLUSIONS
|
|
83
|
+
or folded.startswith("x-ciel-runtime-")
|
|
84
|
+
):
|
|
85
|
+
continue
|
|
86
|
+
projected[name] = str(raw_value)
|
|
87
|
+
return projected
|
|
88
|
+
|
|
49
89
|
def upstream_model(
|
|
50
90
|
self, provider: str, config: dict[str, Any], model: str
|
|
51
91
|
) -> str:
|
|
@@ -137,6 +177,19 @@ class ProviderRequestAccessService:
|
|
|
137
177
|
provider, config, meaningful
|
|
138
178
|
)
|
|
139
179
|
)
|
|
180
|
+
configured_protocol_headers = self._configured_protocol_headers(
|
|
181
|
+
config, protocol
|
|
182
|
+
)
|
|
183
|
+
if configured_protocol_headers:
|
|
184
|
+
configured_names = {
|
|
185
|
+
name.casefold() for name in configured_protocol_headers
|
|
186
|
+
}
|
|
187
|
+
headers = {
|
|
188
|
+
name: value
|
|
189
|
+
for name, value in headers.items()
|
|
190
|
+
if name.casefold() not in configured_names
|
|
191
|
+
}
|
|
192
|
+
headers.update(configured_protocol_headers)
|
|
140
193
|
if protocol == "anthropic_messages":
|
|
141
194
|
normalized_names = {
|
|
142
195
|
str(name).casefold() for name in headers
|