@oneciel-ai/ciel-runtime 0.2.22 → 0.2.23
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 +108 -0
- package/README.md +150 -57
- package/ciel-runtime-stop.cmd +1 -0
- package/ciel-runtime.cmd +1 -0
- package/ciel-runtimectl.cmd +1 -0
- package/ciel_runtime.py +56 -53
- package/ciel_runtime_support/anthropic_tool_turns.py +81 -0
- package/ciel_runtime_support/architecture.py +17 -0
- package/ciel_runtime_support/channel_injection.py +113 -3
- package/ciel_runtime_support/channel_message_prompt.py +15 -0
- package/ciel_runtime_support/channel_pending_injection.py +28 -4
- package/ciel_runtime_support/channel_pending_poll.py +2 -0
- package/ciel_runtime_support/channel_terminal_context.py +6 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +2 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +8 -2
- package/ciel_runtime_support/channel_wake_claim_repository.py +7 -0
- package/ciel_runtime_support/channel_wake_context.py +7 -3
- package/ciel_runtime_support/cli_parser.py +6 -0
- package/ciel_runtime_support/codex_backend_context.py +4 -0
- package/ciel_runtime_support/codex_turn_recovery.py +75 -12
- package/ciel_runtime_support/config_migrations.py +102 -10
- package/ciel_runtime_support/config_repository.py +9 -0
- package/ciel_runtime_support/event_settings_cli.py +284 -2
- package/ciel_runtime_support/openai_chat_passthrough.py +2 -0
- package/ciel_runtime_support/protocols/openai_responses.py +22 -18
- package/ciel_runtime_support/provider_adapters.py +7 -0
- package/ciel_runtime_support/provider_policy.py +10 -1
- package/ciel_runtime_support/provider_request_builder.py +9 -2
- package/ciel_runtime_support/provider_responses_passthrough.py +4 -0
- package/ciel_runtime_support/providers/__init__.py +2 -0
- package/ciel_runtime_support/providers/alibaba.py +33 -10
- package/ciel_runtime_support/providers/constants.py +1 -0
- package/ciel_runtime_support/providers/opencode.py +25 -5
- package/ciel_runtime_support/providers/opencode_go.py +3 -3
- package/ciel_runtime_support/providers/openrouter.py +76 -3
- package/ciel_runtime_support/providers/tabitoken.py +130 -0
- package/ciel_runtime_support/remote_instructions.py +20 -6
- package/ciel_runtime_support/remote_memory.py +515 -55
- package/ciel_runtime_support/responses_input_compatibility.py +14 -0
- package/ciel_runtime_support/router_http.py +5 -0
- package/ciel_runtime_support/runtime_constants.py +1 -1
- package/ciel_runtime_support/runtime_launch.py +9 -2
- package/ciel_runtime_support/runtime_paths.py +2 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +7 -4
- package/ciel_runtime_support/transcript_delta_delivery.py +333 -0
- package/ciel_runtime_support/usage_events.py +45 -3
- package/ciel_runtime_support/usage_service.py +871 -0
- package/ciel_runtime_support/windows_conpty.py +108 -2
- package/ciel_runtime_support/workspace_mcp.py +4 -0
- package/docs/CLI-Reference.md +11 -0
- package/docs/Configuration.md +7 -2
- package/docs/MCP-Channels.md +7 -3
- package/docs/Module-Map.md +4 -0
- package/docs/Providers.md +32 -0
- package/docs/Remote-Memory.md +29 -10
- package/docs/Test-Suite.md +1 -0
- package/docs/journal/2026/08/20/kevin-wake-orphan-tool-repair.md +161 -0
- package/docs/journal/2026/08/20/nightly-push-local-deployment.md +59 -2
- package/docs/journal/2026/08/20/wing-remote-memory-replication-verification.md +107 -0
- package/docs/journal/2026/08/21/alibaba-singapore-qwen38-official-alignment.md +64 -0
- package/docs/journal/2026/08/21/codex-visible-sse-wake-submit-regression.md +85 -0
- package/docs/journal/2026/08/21/external-sse-wake-only-change-analysis.md +131 -0
- package/docs/journal/2026/08/21/hyundai-invalid-replayed-tool-name-repair.okf +88 -0
- package/docs/journal/2026/08/21/local-stale-temporary-runtime-pin-repair.okf +118 -0
- package/docs/journal/2026/08/21/transcript-compaction-external-events.okf +131 -0
- package/docs/journal/2026/08/21/wing-ai-net-mcp-boundary-diagnosis.okf +129 -0
- package/docs/journal/2026/08/21/wing-cold-start-wake-submit-repair.okf +161 -0
- package/docs/journal/2026/08/21/wing-powershell-memory-runaway.okf +101 -0
- package/docs/journal/2026/08/21/wing-repeated-reasoning-tool-turn-repair.okf +112 -0
- package/docs/journal/2026/08/21/wing-repeated-tool-guard-finalization.okf +81 -0
- package/docs/journal/2026/08/21/wing-working-directory-memory-deployment.md +110 -0
- package/docs/journal/2026/08/21/wing-workspace-state-memory-prompt-injection.md +88 -0
- package/docs/journal/2026/08/22/alibaba-singapore-dual-endpoint-wire-alignment.okf +142 -0
- package/docs/journal/2026/08/22/delluhiold-claude-compaction-empty-tool-name.okf +168 -0
- package/docs/journal/2026/08/22/remote-memory-relative-prompt-paths.okf +79 -0
- package/docs/journal/2026/08/22/remote-memory-root-system-prompt.okf +121 -0
- package/docs/journal/2026/08/23/claude-web-chat-stateless-mcp-repair.okf +97 -0
- package/docs/journal/2026/08/23/cross-runtime-token-usage-interfaces-research.okf +378 -0
- package/docs/journal/2026/08/23/main-0.2.23-readme-release.okf +81 -0
- package/docs/journal/2026/08/23/mia-openrouter-ox-alpha-latency-diagnosis.okf +146 -0
- package/docs/journal/2026/08/23/openrouter-opencode-ox-alpha-support.okf +114 -0
- package/docs/journal/2026/08/23/tabitoken-provider-model-catalog.okf +97 -0
- package/docs/journal/2026/08/23/usage-stream-snapshot-implementation.okf +134 -0
- package/docs/usage-observability.md +141 -0
- package/install.ps1 +24 -3
- package/package.json +2 -1
|
@@ -13,6 +13,8 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from dataclasses import dataclass
|
|
15
15
|
from typing import Any, Callable
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
16
18
|
import urllib.parse
|
|
17
19
|
from pathlib import PurePosixPath
|
|
18
20
|
|
|
@@ -32,6 +34,39 @@ EXTERNAL_EVENT_KEYS = (
|
|
|
32
34
|
"webhook_secret",
|
|
33
35
|
"authorization",
|
|
34
36
|
)
|
|
37
|
+
TRANSCRIPT_EVENT_KEYS = (
|
|
38
|
+
"enabled",
|
|
39
|
+
"url",
|
|
40
|
+
"authorization",
|
|
41
|
+
"timeout_seconds",
|
|
42
|
+
"poll_interval_ms",
|
|
43
|
+
"max_batch_bytes",
|
|
44
|
+
"start_mode",
|
|
45
|
+
)
|
|
46
|
+
TRANSCRIPT_EVENT_LIMITS = {
|
|
47
|
+
"timeout_seconds": (1, 30),
|
|
48
|
+
"poll_interval_ms": (100, 60_000),
|
|
49
|
+
"max_batch_bytes": (1_024, 16_777_216),
|
|
50
|
+
}
|
|
51
|
+
USAGE_EVENT_KEYS = (
|
|
52
|
+
"endpoint_id",
|
|
53
|
+
"enabled",
|
|
54
|
+
"url",
|
|
55
|
+
"authorization",
|
|
56
|
+
"api_key",
|
|
57
|
+
"timeout_seconds",
|
|
58
|
+
"poll_interval_seconds",
|
|
59
|
+
"start_mode",
|
|
60
|
+
"audit_interval_seconds",
|
|
61
|
+
"audit_emit_on_start",
|
|
62
|
+
"jsonl_enabled",
|
|
63
|
+
"backfill_paths",
|
|
64
|
+
)
|
|
65
|
+
USAGE_EVENT_LIMITS = {
|
|
66
|
+
"timeout_seconds": (1, 30),
|
|
67
|
+
"poll_interval_seconds": (1, 60),
|
|
68
|
+
"audit_interval_seconds": (1, 31_536_000),
|
|
69
|
+
}
|
|
35
70
|
REMOTE_INSTRUCTION_URL_KEYS = (
|
|
36
71
|
"claude_url",
|
|
37
72
|
"codex_url",
|
|
@@ -63,7 +98,7 @@ REMOTE_MEMORY_LIMITS = {
|
|
|
63
98
|
"max_total_bytes": (1_024, 134_217_728),
|
|
64
99
|
"max_files": (1, 2_048),
|
|
65
100
|
}
|
|
66
|
-
_SECRET_KEYS = frozenset({"webhook_secret", "authorization"})
|
|
101
|
+
_SECRET_KEYS = frozenset({"webhook_secret", "authorization", "api_key"})
|
|
67
102
|
|
|
68
103
|
|
|
69
104
|
class EventSettingsCliError(ValueError):
|
|
@@ -116,6 +151,7 @@ class EventSettingsCliPorts:
|
|
|
116
151
|
sync_instructions: Callable[[], list[str]]
|
|
117
152
|
sync_memories: Callable[[], list[str]]
|
|
118
153
|
output: Callable[[str], None]
|
|
154
|
+
usage_keys: Callable[[], Any] = lambda: None
|
|
119
155
|
|
|
120
156
|
|
|
121
157
|
@dataclass(frozen=True, slots=True)
|
|
@@ -186,6 +222,247 @@ class EventSettingsCli:
|
|
|
186
222
|
service.save_receiver(self.receiver_id, body)
|
|
187
223
|
self._confirm("external-events", changed)
|
|
188
224
|
|
|
225
|
+
# -- transcript events ------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def _transcript_event_settings(self) -> dict[str, Any]:
|
|
228
|
+
value = self.ports.load_config().get("transcript_events")
|
|
229
|
+
return value if isinstance(value, dict) else {}
|
|
230
|
+
|
|
231
|
+
def transcript_event_values(self) -> dict[str, Any]:
|
|
232
|
+
current = self._transcript_event_settings()
|
|
233
|
+
return {
|
|
234
|
+
"enabled": bool(current.get("enabled", False)),
|
|
235
|
+
"url": str(current.get("url") or ""),
|
|
236
|
+
"authorization": "stored" if current.get("authorization") else "unset",
|
|
237
|
+
"timeout_seconds": current.get("timeout_seconds") or 5,
|
|
238
|
+
"poll_interval_ms": current.get("poll_interval_ms") or 1000,
|
|
239
|
+
"max_batch_bytes": current.get("max_batch_bytes") or 1_048_576,
|
|
240
|
+
"start_mode": str(current.get("start_mode") or "tail"),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
def transcript_events(self, args: Any) -> None:
|
|
244
|
+
tokens = [str(value) for value in (getattr(args, "values", None) or [])]
|
|
245
|
+
if not tokens:
|
|
246
|
+
self._report("transcript-events", self.transcript_event_values())
|
|
247
|
+
return
|
|
248
|
+
current = dict(self._transcript_event_settings())
|
|
249
|
+
updates: dict[str, Any] = {}
|
|
250
|
+
changed: list[str] = []
|
|
251
|
+
for token in tokens:
|
|
252
|
+
key, value = split_assignment(token)
|
|
253
|
+
if key not in TRANSCRIPT_EVENT_KEYS:
|
|
254
|
+
raise EventSettingsCliError(
|
|
255
|
+
f"unsupported transcript event option: {key}; expected one of "
|
|
256
|
+
f"{', '.join(TRANSCRIPT_EVENT_KEYS)}"
|
|
257
|
+
)
|
|
258
|
+
if key == "enabled":
|
|
259
|
+
updates[key] = parse_flag(key, value)
|
|
260
|
+
elif key == "url":
|
|
261
|
+
updates[key] = _validated_url(key, value)
|
|
262
|
+
elif key in TRANSCRIPT_EVENT_LIMITS:
|
|
263
|
+
minimum, maximum = TRANSCRIPT_EVENT_LIMITS[key]
|
|
264
|
+
try:
|
|
265
|
+
parsed = int(value.strip())
|
|
266
|
+
except ValueError:
|
|
267
|
+
raise EventSettingsCliError(
|
|
268
|
+
f"{key} must be a whole number from {minimum} to {maximum}"
|
|
269
|
+
) from None
|
|
270
|
+
if not minimum <= parsed <= maximum:
|
|
271
|
+
raise EventSettingsCliError(
|
|
272
|
+
f"{key} must be a whole number from {minimum} to {maximum}"
|
|
273
|
+
)
|
|
274
|
+
updates[key] = parsed
|
|
275
|
+
elif key == "start_mode":
|
|
276
|
+
mode = value.strip().lower()
|
|
277
|
+
if mode not in {"tail", "beginning"}:
|
|
278
|
+
raise EventSettingsCliError(
|
|
279
|
+
"start_mode must be tail or beginning"
|
|
280
|
+
)
|
|
281
|
+
updates[key] = mode
|
|
282
|
+
else:
|
|
283
|
+
updates[key] = value
|
|
284
|
+
changed.append(key)
|
|
285
|
+
resulting = {**current, **updates}
|
|
286
|
+
if resulting.get("enabled") and not str(resulting.get("url") or "").strip():
|
|
287
|
+
raise EventSettingsCliError(
|
|
288
|
+
"transcript-events requires url when enabled=true"
|
|
289
|
+
)
|
|
290
|
+
config = self.ports.load_config()
|
|
291
|
+
stored = config.get("transcript_events")
|
|
292
|
+
if not isinstance(stored, dict):
|
|
293
|
+
stored = {}
|
|
294
|
+
config["transcript_events"] = stored
|
|
295
|
+
stored.update(updates)
|
|
296
|
+
self.ports.save_config(config)
|
|
297
|
+
self._confirm("transcript-events", changed)
|
|
298
|
+
|
|
299
|
+
# -- usage events -----------------------------------------------------
|
|
300
|
+
|
|
301
|
+
def _usage_settings(self) -> dict[str, Any]:
|
|
302
|
+
value = self.ports.load_config().get("usage")
|
|
303
|
+
return value if isinstance(value, dict) else {}
|
|
304
|
+
|
|
305
|
+
def _usage_endpoint(self, endpoint_id: str) -> dict[str, Any]:
|
|
306
|
+
endpoints = self._usage_settings().get("push_endpoints")
|
|
307
|
+
for endpoint in endpoints if isinstance(endpoints, list) else []:
|
|
308
|
+
if isinstance(endpoint, dict) and str(endpoint.get("id") or "") == endpoint_id:
|
|
309
|
+
return dict(endpoint)
|
|
310
|
+
return {"id": endpoint_id, "enabled": False}
|
|
311
|
+
|
|
312
|
+
def usage_event_values(self, endpoint_id: str = "default") -> dict[str, Any]:
|
|
313
|
+
current = self._usage_endpoint(endpoint_id)
|
|
314
|
+
settings = self._usage_settings()
|
|
315
|
+
return {
|
|
316
|
+
"endpoint_id": endpoint_id,
|
|
317
|
+
"enabled": bool(current.get("enabled", False)),
|
|
318
|
+
"url": str(current.get("url") or ""),
|
|
319
|
+
"authorization": "stored" if current.get("authorization") else "unset",
|
|
320
|
+
"api_key": "stored" if current.get("api_key") else "unset",
|
|
321
|
+
"timeout_seconds": current.get("timeout_seconds") or 5,
|
|
322
|
+
"poll_interval_seconds": current.get("poll_interval_seconds") or 1,
|
|
323
|
+
"start_mode": str(current.get("start_mode") or "tail"),
|
|
324
|
+
"audit_interval_seconds": current.get("audit_interval_seconds") or 86400,
|
|
325
|
+
"audit_emit_on_start": bool(current.get("audit_emit_on_start", True)),
|
|
326
|
+
"jsonl_enabled": bool(settings.get("jsonl_enabled", True)),
|
|
327
|
+
"backfill_paths": os.pathsep.join(str(path) for path in (settings.get("backfill_paths") or [])),
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
def usage_events(self, args: Any) -> None:
|
|
331
|
+
tokens = [str(value) for value in (getattr(args, "values", None) or [])]
|
|
332
|
+
endpoint_id = "default"
|
|
333
|
+
for token in tokens:
|
|
334
|
+
key, value = split_assignment(token)
|
|
335
|
+
if key == "endpoint_id":
|
|
336
|
+
endpoint_id = value.strip() or "default"
|
|
337
|
+
if not tokens or all(split_assignment(token)[0] == "endpoint_id" for token in tokens):
|
|
338
|
+
self._report("usage-events", self.usage_event_values(endpoint_id))
|
|
339
|
+
return
|
|
340
|
+
current = self._usage_endpoint(endpoint_id)
|
|
341
|
+
updates: dict[str, Any] = {"id": endpoint_id}
|
|
342
|
+
changed: list[str] = []
|
|
343
|
+
backfill_paths: list[str] | None = None
|
|
344
|
+
jsonl_enabled: bool | None = None
|
|
345
|
+
for token in tokens:
|
|
346
|
+
key, value = split_assignment(token)
|
|
347
|
+
if key not in USAGE_EVENT_KEYS:
|
|
348
|
+
raise EventSettingsCliError(
|
|
349
|
+
f"unsupported usage event option: {key}; expected one of {', '.join(USAGE_EVENT_KEYS)}"
|
|
350
|
+
)
|
|
351
|
+
if key == "endpoint_id":
|
|
352
|
+
continue
|
|
353
|
+
if key in {"enabled", "audit_emit_on_start"}:
|
|
354
|
+
updates[key] = parse_flag(key, value)
|
|
355
|
+
elif key == "jsonl_enabled":
|
|
356
|
+
jsonl_enabled = parse_flag(key, value)
|
|
357
|
+
elif key == "url":
|
|
358
|
+
updates[key] = _validated_url(key, value)
|
|
359
|
+
elif key in USAGE_EVENT_LIMITS:
|
|
360
|
+
minimum, maximum = USAGE_EVENT_LIMITS[key]
|
|
361
|
+
try:
|
|
362
|
+
parsed = int(value.strip())
|
|
363
|
+
except ValueError:
|
|
364
|
+
raise EventSettingsCliError(
|
|
365
|
+
f"{key} must be a whole number from {minimum} to {maximum}"
|
|
366
|
+
) from None
|
|
367
|
+
if not minimum <= parsed <= maximum:
|
|
368
|
+
raise EventSettingsCliError(
|
|
369
|
+
f"{key} must be a whole number from {minimum} to {maximum}"
|
|
370
|
+
)
|
|
371
|
+
updates[key] = parsed
|
|
372
|
+
elif key == "start_mode":
|
|
373
|
+
mode = value.strip().lower()
|
|
374
|
+
if mode not in {"tail", "beginning"}:
|
|
375
|
+
raise EventSettingsCliError("start_mode must be tail or beginning")
|
|
376
|
+
updates[key] = mode
|
|
377
|
+
elif key == "backfill_paths":
|
|
378
|
+
backfill_paths = [path.strip() for path in value.split(os.pathsep) if path.strip()]
|
|
379
|
+
else:
|
|
380
|
+
updates[key] = value
|
|
381
|
+
changed.append(key)
|
|
382
|
+
resulting = {**current, **updates}
|
|
383
|
+
if resulting.get("enabled") and not str(resulting.get("url") or "").strip():
|
|
384
|
+
raise EventSettingsCliError("usage-events requires url when enabled=true")
|
|
385
|
+
config = self.ports.load_config()
|
|
386
|
+
settings = config.get("usage")
|
|
387
|
+
if not isinstance(settings, dict):
|
|
388
|
+
settings = {}
|
|
389
|
+
config["usage"] = settings
|
|
390
|
+
endpoints = settings.get("push_endpoints")
|
|
391
|
+
stored_endpoints = [dict(item) for item in endpoints if isinstance(item, dict)] if isinstance(endpoints, list) else []
|
|
392
|
+
stored_endpoints = [item for item in stored_endpoints if str(item.get("id") or "") != endpoint_id]
|
|
393
|
+
stored_endpoints.append(resulting)
|
|
394
|
+
settings["push_endpoints"] = stored_endpoints
|
|
395
|
+
if backfill_paths is not None:
|
|
396
|
+
settings["backfill_paths"] = backfill_paths
|
|
397
|
+
if jsonl_enabled is not None:
|
|
398
|
+
settings["jsonl_enabled"] = jsonl_enabled
|
|
399
|
+
self.ports.save_config(config)
|
|
400
|
+
self._confirm("usage-events", changed)
|
|
401
|
+
|
|
402
|
+
def usage_api_key(self, args: Any) -> None:
|
|
403
|
+
tokens = [str(value) for value in (getattr(args, "values", None) or [])]
|
|
404
|
+
repository = self.ports.usage_keys()
|
|
405
|
+
if repository is None:
|
|
406
|
+
raise EventSettingsCliError("usage API key repository is unavailable")
|
|
407
|
+
action = "list"
|
|
408
|
+
assignments: dict[str, str] = {}
|
|
409
|
+
for index, token in enumerate(tokens):
|
|
410
|
+
key, value = split_assignment(token, bare_keys=("list", "issue", "revoke"))
|
|
411
|
+
if index == 0 and key in {"list", "issue", "revoke"}:
|
|
412
|
+
action = key
|
|
413
|
+
else:
|
|
414
|
+
assignments[key] = value
|
|
415
|
+
if action == "list":
|
|
416
|
+
rows = repository.list()
|
|
417
|
+
self.ports.output("usage-api-key:")
|
|
418
|
+
for row in rows:
|
|
419
|
+
self.ports.output(
|
|
420
|
+
f" {row['key_id']} name={row['name']} scopes={','.join(row['scopes'])} "
|
|
421
|
+
f"expires_at={row['expires_at']} revoked_at={row['revoked_at']}"
|
|
422
|
+
)
|
|
423
|
+
if not rows:
|
|
424
|
+
self.ports.output(" none")
|
|
425
|
+
return
|
|
426
|
+
if action == "revoke":
|
|
427
|
+
key_id = assignments.get("key_id", "").strip()
|
|
428
|
+
if not key_id:
|
|
429
|
+
raise EventSettingsCliError("usage-api-key revoke requires key_id=<id>")
|
|
430
|
+
self.ports.output(f"usage-api-key revoked: {key_id} ok={repository.revoke(key_id)}")
|
|
431
|
+
return
|
|
432
|
+
allowed = {"name", "scopes", "ttl_seconds", "expires_at", "api_key", "key_id"}
|
|
433
|
+
unknown = sorted(set(assignments) - allowed)
|
|
434
|
+
if unknown:
|
|
435
|
+
raise EventSettingsCliError(f"unsupported usage API key option: {unknown[0]}")
|
|
436
|
+
scopes = []
|
|
437
|
+
for scope in assignments.get("scopes", "read,stream").split(","):
|
|
438
|
+
normalized = scope.strip().lower()
|
|
439
|
+
if normalized in {"read", "usage:read"}:
|
|
440
|
+
scopes.append("usage:read")
|
|
441
|
+
elif normalized in {"stream", "usage:stream"}:
|
|
442
|
+
scopes.append("usage:stream")
|
|
443
|
+
elif normalized:
|
|
444
|
+
raise EventSettingsCliError(f"unsupported usage API key scope: {scope}")
|
|
445
|
+
try:
|
|
446
|
+
ttl = int(assignments.get("ttl_seconds", "0") or 0)
|
|
447
|
+
except ValueError:
|
|
448
|
+
raise EventSettingsCliError("ttl_seconds must be a non-negative whole number") from None
|
|
449
|
+
if ttl < 0:
|
|
450
|
+
raise EventSettingsCliError("ttl_seconds must be a non-negative whole number")
|
|
451
|
+
try:
|
|
452
|
+
expires_at = float(assignments.get("expires_at", "0") or 0)
|
|
453
|
+
except ValueError:
|
|
454
|
+
raise EventSettingsCliError("expires_at must be a non-negative epoch timestamp") from None
|
|
455
|
+
if expires_at < 0:
|
|
456
|
+
raise EventSettingsCliError("expires_at must be a non-negative epoch timestamp")
|
|
457
|
+
result = repository.issue(
|
|
458
|
+
assignments.get("name", "usage-consumer"), scopes,
|
|
459
|
+
expires_at or (time.time() + ttl if ttl else 0),
|
|
460
|
+
secret=assignments.get("api_key", ""), key_id=assignments.get("key_id", ""),
|
|
461
|
+
)
|
|
462
|
+
self.ports.output(f"usage-api-key issued: key_id={result['key_id']}")
|
|
463
|
+
self.ports.output(f" api_key={result['api_key']}")
|
|
464
|
+
self.ports.output(f" scopes={','.join(result['scopes'])}")
|
|
465
|
+
|
|
189
466
|
# -- remote instructions -----------------------------------------------
|
|
190
467
|
|
|
191
468
|
def _remote_settings(self) -> dict[str, Any]:
|
|
@@ -377,11 +654,14 @@ def _guarded(handler: Callable[[Any], None]) -> Callable[[Any], None]:
|
|
|
377
654
|
|
|
378
655
|
|
|
379
656
|
def handlers(ports: EventSettingsCliPorts) -> tuple[Callable[[Any], None], ...]:
|
|
380
|
-
"""Return
|
|
657
|
+
"""Return event, usage, instruction, and memory handlers."""
|
|
381
658
|
|
|
382
659
|
controller = EventSettingsCli(ports)
|
|
383
660
|
return (
|
|
384
661
|
_guarded(controller.external_events),
|
|
662
|
+
_guarded(controller.transcript_events),
|
|
663
|
+
_guarded(controller.usage_events),
|
|
664
|
+
_guarded(controller.usage_api_key),
|
|
385
665
|
_guarded(controller.remote_instructions),
|
|
386
666
|
_guarded(controller.remote_memory),
|
|
387
667
|
)
|
|
@@ -389,6 +669,8 @@ def handlers(ports: EventSettingsCliPorts) -> tuple[Callable[[Any], None], ...]:
|
|
|
389
669
|
|
|
390
670
|
__all__ = [
|
|
391
671
|
"EXTERNAL_EVENT_KEYS",
|
|
672
|
+
"TRANSCRIPT_EVENT_KEYS",
|
|
673
|
+
"USAGE_EVENT_KEYS",
|
|
392
674
|
"REMOTE_MEMORY_KEYS",
|
|
393
675
|
"REMOTE_INSTRUCTION_KEYS",
|
|
394
676
|
"REMOTE_INSTRUCTION_URL_KEYS",
|
|
@@ -18,6 +18,7 @@ class OpenAIChatPassthroughPorts:
|
|
|
18
18
|
urlopen: Callable[..., Any]
|
|
19
19
|
timeout_seconds: Callable[[dict[str, Any]], float]
|
|
20
20
|
copy_response_headers: Callable[[Any, Any], None]
|
|
21
|
+
finalize_body: Callable[[dict[str, Any]], dict[str, Any]] = lambda body: body
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class OpenAIChatPassthrough:
|
|
@@ -40,6 +41,7 @@ class OpenAIChatPassthrough:
|
|
|
40
41
|
upstream_body = dict(
|
|
41
42
|
self._ports.normalize_request(provider, config, upstream_body)
|
|
42
43
|
)
|
|
44
|
+
upstream_body = self._ports.finalize_body(upstream_body)
|
|
43
45
|
url = self._ports.join_url(
|
|
44
46
|
self._ports.upstream_base(provider, config),
|
|
45
47
|
"/v1/chat/completions",
|
|
@@ -175,6 +175,7 @@ def openai_responses_to_anthropic_messages(body: dict[str, Any], fallback_model:
|
|
|
175
175
|
raw_input = []
|
|
176
176
|
saw_conversation_item = False
|
|
177
177
|
pending_reasoning = ""
|
|
178
|
+
pending_tool_role = ""
|
|
178
179
|
for item in raw_input:
|
|
179
180
|
if not isinstance(item, dict):
|
|
180
181
|
continue
|
|
@@ -200,30 +201,33 @@ def openai_responses_to_anthropic_messages(body: dict[str, Any], fallback_model:
|
|
|
200
201
|
),
|
|
201
202
|
}
|
|
202
203
|
)
|
|
203
|
-
messages
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
if pending_tool_role == "assistant" and messages:
|
|
205
|
+
messages[-1]["content"].extend(content)
|
|
206
|
+
else:
|
|
207
|
+
messages.append(
|
|
208
|
+
{
|
|
209
|
+
"role": "assistant",
|
|
210
|
+
"content": content,
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
pending_tool_role = "assistant"
|
|
209
214
|
saw_conversation_item = True
|
|
210
215
|
continue
|
|
211
216
|
if item_type in {"function_call_output", "custom_tool_call_output"}:
|
|
212
217
|
call_id = str(item.get("call_id") or item.get("id") or "call_tool")
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
)
|
|
218
|
+
result = {
|
|
219
|
+
"type": "tool_result",
|
|
220
|
+
"tool_use_id": call_id,
|
|
221
|
+
"content": _content_text(item.get("output")),
|
|
222
|
+
}
|
|
223
|
+
if pending_tool_role == "user" and messages:
|
|
224
|
+
messages[-1]["content"].append(result)
|
|
225
|
+
else:
|
|
226
|
+
messages.append({"role": "user", "content": [result]})
|
|
227
|
+
pending_tool_role = "user"
|
|
225
228
|
saw_conversation_item = True
|
|
226
229
|
continue
|
|
230
|
+
pending_tool_role = ""
|
|
227
231
|
role = str(item.get("role") or "user").strip().lower()
|
|
228
232
|
blocks = _content_blocks(item.get("content", item.get("text", "")))
|
|
229
233
|
if not blocks:
|
|
@@ -19,6 +19,7 @@ from .providers.constants import PROVIDER_DEFAULT_BASE_URLS, ZAI_MODEL_FALLBACK_
|
|
|
19
19
|
from .providers.native import AgyProviderAdapter, CodexProviderAdapter
|
|
20
20
|
from .providers.ollama import OllamaCloudProviderAdapter, OllamaProviderAdapter
|
|
21
21
|
from .providers.openrouter import OpenRouterProviderAdapter
|
|
22
|
+
from .providers.tabitoken import TabitokenProviderAdapter
|
|
22
23
|
from .providers.lm_studio import LMStudioProviderAdapter
|
|
23
24
|
from .providers.nim import SelfHostedNimProviderAdapter
|
|
24
25
|
from .providers.nvidia import NvidiaHostedProviderAdapter
|
|
@@ -64,6 +65,12 @@ PROVIDER_DESCRIPTORS = ProviderDescriptorRegistry(
|
|
|
64
65
|
ProviderDescriptor("nvidia-hosted", "Nvidia Hosted", NvidiaHostedProviderAdapter),
|
|
65
66
|
ProviderDescriptor("self-hosted-nim", "Self Hosted NIM", SelfHostedNimProviderAdapter),
|
|
66
67
|
ProviderDescriptor("openrouter", "OpenRouter", OpenRouterProviderAdapter),
|
|
68
|
+
ProviderDescriptor(
|
|
69
|
+
"tabitoken",
|
|
70
|
+
"TaBiAI (Tabitoken.com)",
|
|
71
|
+
TabitokenProviderAdapter,
|
|
72
|
+
aliases=("tabi", "tabiai", "tabi-token"),
|
|
73
|
+
),
|
|
67
74
|
ProviderDescriptor("xai", "xAI", XaiProviderAdapter, aliases=("grok",)),
|
|
68
75
|
ProviderDescriptor("fireworks", "Fireworks.ai", FireworksProviderAdapter),
|
|
69
76
|
ProviderDescriptor(
|
|
@@ -72,6 +72,7 @@ class ProviderRequestServices:
|
|
|
72
72
|
normalize_tool_choice_for_provider: Callable[..., Any]
|
|
73
73
|
provider_wire_profile: Callable[..., Any]
|
|
74
74
|
sanitize_assistant_pseudo_tool_text_history: Callable[..., Any]
|
|
75
|
+
sanitize_invalid_anthropic_tool_history: Callable[..., Any]
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
def normalize_provider_request(provider: str, pcfg: dict[str, Any], body: dict[str, Any],
|
|
@@ -87,10 +88,18 @@ def normalize_provider_request(provider: str, pcfg: dict[str, Any], body: dict[s
|
|
|
87
88
|
normalize_tool_choice_for_provider = services.normalize_tool_choice_for_provider
|
|
88
89
|
provider_wire_profile = services.provider_wire_profile
|
|
89
90
|
sanitize_assistant_pseudo_tool_text_history = services.sanitize_assistant_pseudo_tool_text_history
|
|
91
|
+
sanitize_invalid_anthropic_tool_history = services.sanitize_invalid_anthropic_tool_history
|
|
90
92
|
profile = provider_wire_profile(provider, pcfg, body)
|
|
91
93
|
out = normalize_thinking_for_non_anthropic_provider(provider, pcfg, body)
|
|
92
|
-
|
|
94
|
+
selected_protocol = str(profile.get("upstream_format") or "").replace("-", "_")
|
|
95
|
+
out = apply_provider_adapter_request_policy(
|
|
96
|
+
provider,
|
|
97
|
+
pcfg,
|
|
98
|
+
out,
|
|
99
|
+
selected_protocol or None,
|
|
100
|
+
)
|
|
93
101
|
out = normalize_tool_choice_for_provider(provider, pcfg, out)
|
|
102
|
+
out = sanitize_invalid_anthropic_tool_history(out)
|
|
94
103
|
out = sanitize_assistant_pseudo_tool_text_history(out)
|
|
95
104
|
out = normalize_anthropic_tool_turns_for_provider(provider, pcfg, out)
|
|
96
105
|
if profile.get("upstream_format") == "anthropic-messages":
|
|
@@ -50,6 +50,9 @@ class ProviderOptionPorts:
|
|
|
50
50
|
sampling_options: tuple[str, ...]
|
|
51
51
|
anthropic_runtime_hints: Callable[[str], dict[str, Any]]
|
|
52
52
|
log: Callable[[str, str], None]
|
|
53
|
+
finalize_messages: Callable[[list[dict[str, Any]]], list[dict[str, Any]]] = (
|
|
54
|
+
lambda messages: messages
|
|
55
|
+
)
|
|
53
56
|
|
|
54
57
|
|
|
55
58
|
class ProviderRequestBuilder:
|
|
@@ -198,7 +201,7 @@ class ProviderRequestBuilder:
|
|
|
198
201
|
configured,
|
|
199
202
|
_token_cache=token_cache,
|
|
200
203
|
)
|
|
201
|
-
|
|
204
|
+
request = self.ollama.apply_optional(
|
|
202
205
|
request,
|
|
203
206
|
provider,
|
|
204
207
|
model,
|
|
@@ -206,6 +209,8 @@ class ProviderRequestBuilder:
|
|
|
206
209
|
body,
|
|
207
210
|
output_limit=num_predict,
|
|
208
211
|
)
|
|
212
|
+
request["messages"] = self.options.finalize_messages(request["messages"])
|
|
213
|
+
return request
|
|
209
214
|
|
|
210
215
|
def openai_chat(
|
|
211
216
|
self,
|
|
@@ -255,7 +260,9 @@ class ProviderRequestBuilder:
|
|
|
255
260
|
for key in ("temperature", "top_p"):
|
|
256
261
|
if self.openai.sampling_allowed(provider, config) and config.get(key) is not None:
|
|
257
262
|
request[key] = config[key]
|
|
258
|
-
|
|
263
|
+
normalized = self.openai.normalize_request(provider, config, request)
|
|
264
|
+
normalized["messages"] = self.options.finalize_messages(normalized["messages"])
|
|
265
|
+
return normalized
|
|
259
266
|
|
|
260
267
|
|
|
261
268
|
@dataclass(frozen=True, slots=True)
|
|
@@ -44,6 +44,7 @@ class ProviderResponsesPassthroughPorts:
|
|
|
44
44
|
compact_responses: Callable[..., dict[str, Any]] = (
|
|
45
45
|
lambda body, _budget, **_kwargs: body
|
|
46
46
|
)
|
|
47
|
+
finalize_body: Callable[[dict[str, Any]], dict[str, Any]] = lambda body: body
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
class ProviderResponsesPassthrough:
|
|
@@ -65,6 +66,7 @@ class ProviderResponsesPassthrough:
|
|
|
65
66
|
upstream_body["model"] = self._ports.normalize_model(
|
|
66
67
|
provider, config, str(body.get("model") or "")
|
|
67
68
|
)
|
|
69
|
+
upstream_body = self._ports.finalize_body(upstream_body)
|
|
68
70
|
data = self._encode(upstream_body)
|
|
69
71
|
url = self._ports.join_url(
|
|
70
72
|
self._ports.upstream_base(provider, config),
|
|
@@ -127,6 +129,7 @@ class ProviderResponsesPassthrough:
|
|
|
127
129
|
provider=provider,
|
|
128
130
|
model=str(current.get("model") or ""),
|
|
129
131
|
)
|
|
132
|
+
compacted = self._ports.finalize_body(compacted)
|
|
130
133
|
compacted_data = self._encode(compacted)
|
|
131
134
|
self._ports.log(
|
|
132
135
|
"WARN",
|
|
@@ -280,6 +283,7 @@ class ProviderResponsesPassthrough:
|
|
|
280
283
|
upstream_body, delivery_body = self._ports.project_channel_context(
|
|
281
284
|
upstream_body
|
|
282
285
|
)
|
|
286
|
+
upstream_body = self._ports.finalize_body(upstream_body)
|
|
283
287
|
self._ports.begin_channel_delivery(handler, delivery_body)
|
|
284
288
|
url = self._ports.join_url(
|
|
285
289
|
self._ports.upstream_base(provider, config),
|
|
@@ -10,6 +10,7 @@ from .constants import PROVIDER_DEFAULT_BASE_URLS, ZAI_MODEL_FALLBACK_IDS
|
|
|
10
10
|
from .native import AgyProviderAdapter, CodexProviderAdapter
|
|
11
11
|
from .ollama import OllamaCloudProviderAdapter, OllamaProviderAdapter
|
|
12
12
|
from .openrouter import OpenRouterProviderAdapter
|
|
13
|
+
from .tabitoken import TabitokenProviderAdapter
|
|
13
14
|
from .lm_studio import LMStudioProviderAdapter
|
|
14
15
|
from .nim import SelfHostedNimProviderAdapter
|
|
15
16
|
from .nvidia import NvidiaHostedProviderAdapter
|
|
@@ -56,6 +57,7 @@ __all__ = [
|
|
|
56
57
|
"OllamaCloudProviderAdapter",
|
|
57
58
|
"OllamaProviderAdapter",
|
|
58
59
|
"OpenRouterProviderAdapter",
|
|
60
|
+
"TabitokenProviderAdapter",
|
|
59
61
|
"LMStudioProviderAdapter",
|
|
60
62
|
"NvidiaHostedProviderAdapter",
|
|
61
63
|
"SelfHostedNimProviderAdapter",
|
|
@@ -20,13 +20,16 @@ from .base import HttpBearerProviderAdapter, provider_configuration
|
|
|
20
20
|
|
|
21
21
|
QWEN38_MAX_MODEL = "qwen3.8-max"
|
|
22
22
|
QWEN38_MAX_PREVIEW_MODEL = "qwen3.8-max-preview"
|
|
23
|
-
QWEN38_CONTEXT_WINDOW =
|
|
23
|
+
QWEN38_CONTEXT_WINDOW = 1_000_000
|
|
24
|
+
QWEN38_MAX_INPUT = 991_808
|
|
25
|
+
QWEN38_THINKING_MAX_INPUT = 983_616
|
|
24
26
|
QWEN38_MAX_OUTPUT = 131_072
|
|
27
|
+
QWEN38_MAX_REASONING = 262_144
|
|
25
28
|
QWEN38_AUTO_COMPACT = 900_000
|
|
26
29
|
ALIBABA_TOKEN_PLAN_RESPONSES_MAX_BYTES = 10 * 1024 * 1024
|
|
27
30
|
QWEN38_CODEX_CATALOG = {
|
|
28
|
-
"context_window":
|
|
29
|
-
"max_context_window":
|
|
31
|
+
"context_window": QWEN38_THINKING_MAX_INPUT,
|
|
32
|
+
"max_context_window": QWEN38_THINKING_MAX_INPUT,
|
|
30
33
|
"effective_context_window_percent": 95,
|
|
31
34
|
"supports_parallel_tool_calls": False,
|
|
32
35
|
"supports_image_detail_original": True,
|
|
@@ -58,6 +61,7 @@ ALIBABA_CODING_PLAN_MODELS = (
|
|
|
58
61
|
"glm-4.7",
|
|
59
62
|
)
|
|
60
63
|
ALIBABA_MODEL_STUDIO_MODELS = (
|
|
64
|
+
QWEN38_MAX_MODEL,
|
|
61
65
|
QWEN37_MAX_MODEL,
|
|
62
66
|
"qwen3.7-plus",
|
|
63
67
|
"qwen3.6-plus",
|
|
@@ -119,32 +123,33 @@ _EFFORTS = ("none", "minimal", "low", "medium", "high", "xhigh", "max")
|
|
|
119
123
|
|
|
120
124
|
@dataclass(frozen=True)
|
|
121
125
|
class AlibabaModelStudioProviderAdapter(HttpBearerProviderAdapter):
|
|
122
|
-
"""
|
|
126
|
+
"""Expose Qwen's OpenAI and native Anthropic-compatible wire surfaces."""
|
|
123
127
|
|
|
124
128
|
name: str = "alims-intl"
|
|
125
129
|
base_url: str = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
|
126
130
|
configuration_defaults_value: dict = field(
|
|
127
131
|
default_factory=lambda: provider_configuration(
|
|
128
|
-
|
|
132
|
+
QWEN38_MAX_MODEL,
|
|
129
133
|
custom_models=ALIBABA_MODEL_STUDIO_MODELS,
|
|
130
134
|
native_compat=True,
|
|
131
135
|
supports_tool_choice=True,
|
|
132
|
-
context_window=
|
|
133
|
-
max_model_len=
|
|
134
|
-
max_output_tokens=
|
|
136
|
+
context_window=QWEN38_CONTEXT_WINDOW,
|
|
137
|
+
max_model_len=QWEN38_CONTEXT_WINDOW,
|
|
138
|
+
max_output_tokens=QWEN38_MAX_OUTPUT,
|
|
135
139
|
context_reserve_tokens=8192,
|
|
136
140
|
auto_compact_window=QWEN38_AUTO_COMPACT,
|
|
137
141
|
codex_auto_compact_window=QWEN38_AUTO_COMPACT,
|
|
138
142
|
request_timeout_ms=DEFAULT_REQUEST_TIMEOUT_MS,
|
|
139
143
|
stream_enabled=True,
|
|
140
144
|
stream_word_chunking=False,
|
|
141
|
-
effort_level="
|
|
145
|
+
effort_level="xhigh",
|
|
142
146
|
explicit_cache=True,
|
|
143
147
|
explicit_cache_markers=4,
|
|
144
148
|
haiku_model="qwen3.6-flash",
|
|
145
|
-
opus_model=
|
|
149
|
+
opus_model=QWEN38_MAX_MODEL,
|
|
146
150
|
sonnet_model="qwen3.7-plus",
|
|
147
151
|
subagent_model="qwen3.7-plus",
|
|
152
|
+
region="ap-southeast-1",
|
|
148
153
|
)
|
|
149
154
|
)
|
|
150
155
|
authorization_header: str = "authorization"
|
|
@@ -155,6 +160,7 @@ class AlibabaModelStudioProviderAdapter(HttpBearerProviderAdapter):
|
|
|
155
160
|
default_factory=lambda: ProviderCapabilities(
|
|
156
161
|
upstream_protocol="openai_responses",
|
|
157
162
|
supports_thinking=True,
|
|
163
|
+
preserves_anthropic_thinking=True,
|
|
158
164
|
requires_api_key=True,
|
|
159
165
|
)
|
|
160
166
|
)
|
|
@@ -282,6 +288,20 @@ class AlibabaModelStudioProviderAdapter(HttpBearerProviderAdapter):
|
|
|
282
288
|
self._normalize_chat(config, normalized, model)
|
|
283
289
|
return normalized
|
|
284
290
|
|
|
291
|
+
def normalize_request_options_for_protocol(
|
|
292
|
+
self,
|
|
293
|
+
config: ProviderConfig,
|
|
294
|
+
request: Mapping[str, Any],
|
|
295
|
+
protocol: MessageProtocol | None,
|
|
296
|
+
) -> Mapping[str, Any]:
|
|
297
|
+
if protocol == "anthropic_messages":
|
|
298
|
+
# Alibaba's /apps/anthropic endpoint accepts the Anthropic Messages
|
|
299
|
+
# body directly. In particular, max_tokens and thinking must not be
|
|
300
|
+
# rewritten as OpenAI Chat fields merely because both formats use a
|
|
301
|
+
# top-level messages array.
|
|
302
|
+
return deepcopy(request)
|
|
303
|
+
return self.normalize_request_options(config, request)
|
|
304
|
+
|
|
285
305
|
def openai_reasoning_effort(
|
|
286
306
|
self, config: ProviderConfig, model: str, request: Mapping[str, Any]
|
|
287
307
|
) -> str | None:
|
|
@@ -640,6 +660,9 @@ __all__ = [
|
|
|
640
660
|
"AlibabaTokenPlanProviderAdapter",
|
|
641
661
|
"QWEN38_AUTO_COMPACT",
|
|
642
662
|
"QWEN38_CONTEXT_WINDOW",
|
|
663
|
+
"QWEN38_MAX_INPUT",
|
|
643
664
|
"QWEN38_MAX_MODEL",
|
|
644
665
|
"QWEN38_MAX_OUTPUT",
|
|
666
|
+
"QWEN38_MAX_REASONING",
|
|
667
|
+
"QWEN38_THINKING_MAX_INPUT",
|
|
645
668
|
]
|
|
@@ -19,6 +19,7 @@ PROVIDER_DEFAULT_BASE_URLS: dict[str, str] = {
|
|
|
19
19
|
"nvidia-hosted": "https://integrate.api.nvidia.com/v1",
|
|
20
20
|
"self-hosted-nim": "http://127.0.0.1:8000",
|
|
21
21
|
"openrouter": "https://openrouter.ai/api/v1",
|
|
22
|
+
"tabitoken": "https://tabitoken.com",
|
|
22
23
|
"fireworks": "https://api.fireworks.ai/inference",
|
|
23
24
|
"meta": "https://api.meta.ai/v1",
|
|
24
25
|
}
|