ctxoptimizer 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. ctxoptimizer/__init__.py +5 -0
  2. ctxoptimizer/__main__.py +14 -0
  3. ctxoptimizer/admin_security.py +130 -0
  4. ctxoptimizer/analysis/__init__.py +1 -0
  5. ctxoptimizer/analysis/baseline.py +69 -0
  6. ctxoptimizer/analysis/break_even.py +49 -0
  7. ctxoptimizer/analysis/drift.py +151 -0
  8. ctxoptimizer/analysis/edits.py +148 -0
  9. ctxoptimizer/analysis/markers.py +34 -0
  10. ctxoptimizer/analysis/recommend.py +114 -0
  11. ctxoptimizer/analysis/segments.py +57 -0
  12. ctxoptimizer/analysis/session.py +62 -0
  13. ctxoptimizer/analysis/state.py +239 -0
  14. ctxoptimizer/analysis/tokens.py +62 -0
  15. ctxoptimizer/analysis/waste.py +322 -0
  16. ctxoptimizer/cli.py +873 -0
  17. ctxoptimizer/config.py +512 -0
  18. ctxoptimizer/dashboard/__init__.py +1 -0
  19. ctxoptimizer/dashboard/api.py +537 -0
  20. ctxoptimizer/dashboard/service.py +157 -0
  21. ctxoptimizer/dashboard/static/.gitkeep +1 -0
  22. ctxoptimizer/dashboard/templates/.gitkeep +1 -0
  23. ctxoptimizer/dashboard/templates/doctor.html +46 -0
  24. ctxoptimizer/dashboard/templates/live.html +91 -0
  25. ctxoptimizer/dashboard/templates/overview.html +172 -0
  26. ctxoptimizer/dashboard/templates/pricing.html +115 -0
  27. ctxoptimizer/dashboard/templates/request.html +111 -0
  28. ctxoptimizer/dashboard/templates/session.html +67 -0
  29. ctxoptimizer/dashboard/templates/settings.html +79 -0
  30. ctxoptimizer/dashboard/templates/table.html +56 -0
  31. ctxoptimizer/dashboard/templates/transforms.html +118 -0
  32. ctxoptimizer/dashboard/templates/waste.html +120 -0
  33. ctxoptimizer/dashboard/views.py +429 -0
  34. ctxoptimizer/data/cache_limits.json +23 -0
  35. ctxoptimizer/data/default_config.toml +185 -0
  36. ctxoptimizer/data/pricing.json +421 -0
  37. ctxoptimizer/doctor.py +129 -0
  38. ctxoptimizer/guard.py +139 -0
  39. ctxoptimizer/ids.py +73 -0
  40. ctxoptimizer/log.py +73 -0
  41. ctxoptimizer/mcp.py +124 -0
  42. ctxoptimizer/models.py +145 -0
  43. ctxoptimizer/pipeline.py +126 -0
  44. ctxoptimizer/profiles.py +152 -0
  45. ctxoptimizer/providers/__init__.py +37 -0
  46. ctxoptimizer/providers/anthropic.py +105 -0
  47. ctxoptimizer/providers/base.py +191 -0
  48. ctxoptimizer/providers/gemini.py +143 -0
  49. ctxoptimizer/providers/ollama.py +133 -0
  50. ctxoptimizer/providers/openai_chat.py +135 -0
  51. ctxoptimizer/providers/openai_responses.py +121 -0
  52. ctxoptimizer/proxy.py +665 -0
  53. ctxoptimizer/routing.py +259 -0
  54. ctxoptimizer/server.py +319 -0
  55. ctxoptimizer/service.py +150 -0
  56. ctxoptimizer/telemetry/__init__.py +1 -0
  57. ctxoptimizer/telemetry/blobs.py +57 -0
  58. ctxoptimizer/telemetry/expand.py +62 -0
  59. ctxoptimizer/telemetry/pricing.py +168 -0
  60. ctxoptimizer/telemetry/redact.py +53 -0
  61. ctxoptimizer/telemetry/sse.py +145 -0
  62. ctxoptimizer/telemetry/store.py +829 -0
  63. ctxoptimizer/telemetry/usage.py +435 -0
  64. ctxoptimizer/transforms/__init__.py +87 -0
  65. ctxoptimizer/transforms/ansi.py +21 -0
  66. ctxoptimizer/transforms/anthropic_cache.py +100 -0
  67. ctxoptimizer/transforms/backref.py +74 -0
  68. ctxoptimizer/transforms/base.py +93 -0
  69. ctxoptimizer/transforms/cache_serve.py +146 -0
  70. ctxoptimizer/transforms/dedupe_lines.py +69 -0
  71. ctxoptimizer/transforms/eligibility.py +24 -0
  72. ctxoptimizer/transforms/openai_hints.py +84 -0
  73. ctxoptimizer/transforms/progress.py +52 -0
  74. ctxoptimizer/transforms/stale_result_elision.py +109 -0
  75. ctxoptimizer/transforms/truncate.py +100 -0
  76. ctxoptimizer/transforms/volatile.py +177 -0
  77. ctxoptimizer/transforms/whitespace.py +25 -0
  78. ctxoptimizer-0.4.1.dist-info/METADATA +196 -0
  79. ctxoptimizer-0.4.1.dist-info/RECORD +83 -0
  80. ctxoptimizer-0.4.1.dist-info/WHEEL +4 -0
  81. ctxoptimizer-0.4.1.dist-info/entry_points.txt +3 -0
  82. ctxoptimizer-0.4.1.dist-info/licenses/LICENSE +21 -0
  83. ctxoptimizer-0.4.1.dist-info/licenses/NOTICE +13 -0
ctxoptimizer/config.py ADDED
@@ -0,0 +1,512 @@
1
+ """Configuration loading and validation (SPEC section 7).
2
+
3
+ Precedence is CLI overrides > ``CTXOPTIMIZER_*`` environment > config file >
4
+ defaults. The models are frozen so a request always sees one immutable snapshot;
5
+ hot reload builds a new :class:`Config` instead of mutating the old one.
6
+
7
+ ``config_hash`` covers only the settings that change forwarded bytes
8
+ (transforms, tool eligibility and pipeline policy), per SPEC section 7.1.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ import os
16
+ import tomllib
17
+ from collections.abc import Mapping
18
+ from hashlib import sha256
19
+ from importlib import resources
20
+ from pathlib import Path
21
+ from typing import Any, Literal
22
+
23
+ import platformdirs
24
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
25
+
26
+ Mode = Literal["passthrough", "shadow", "active"]
27
+ CaptureMode = Literal["off", "changed_blocks", "full"]
28
+ UpstreamKind = Literal["anthropic", "openai", "gemini", "ollama"]
29
+ UsageStyle = Literal[
30
+ "anthropic",
31
+ "openai",
32
+ "deepseek",
33
+ "openrouter",
34
+ "gemini",
35
+ "gemini-openai",
36
+ "ollama",
37
+ "ollama-openai",
38
+ ]
39
+ CachePolicy = Literal[
40
+ "explicit-anthropic",
41
+ "implicit-openai",
42
+ "automatic-deepseek",
43
+ "gemini-implicit",
44
+ "none",
45
+ ]
46
+ CostSource = Literal["pricing", "provider", "none"]
47
+ ShellKind = Literal["bash", "zsh", "fish", "powershell"]
48
+
49
+ LOOPBACK_HOSTS = frozenset({"127.0.0.1", "::1", "localhost"})
50
+ ENV_PREFIX = "CTXOPTIMIZER_"
51
+ CONFIG_RESOURCE = "default_config.toml"
52
+ logger = logging.getLogger(__name__)
53
+
54
+ _ENV_FIELDS = {
55
+ "CTXOPTIMIZER_HOST": "server.host",
56
+ "CTXOPTIMIZER_PORT": "server.port",
57
+ "CTXOPTIMIZER_MODE": "mode.default",
58
+ "CTXOPTIMIZER_LOG_LEVEL": "server.log_level",
59
+ }
60
+
61
+
62
+ class _Section(BaseModel):
63
+ """Base for every config section: immutable and tolerant of new keys."""
64
+
65
+ model_config = ConfigDict(extra="ignore", frozen=True)
66
+
67
+
68
+ class ServerConfig(_Section):
69
+ host: str = "127.0.0.1"
70
+ port: int = Field(default=8787, ge=1, le=65535)
71
+ allow_remote: bool = False
72
+ admin_token: str = ""
73
+ max_body_bytes: int = Field(default=33554432, gt=0)
74
+ log_level: str = "info"
75
+
76
+ @model_validator(mode="after")
77
+ def _check_binding(self) -> ServerConfig:
78
+ if self.allow_remote:
79
+ if not self.admin_token:
80
+ raise ValueError("server.admin_token is required when allow_remote = true")
81
+ elif self.host not in LOOPBACK_HOSTS:
82
+ raise ValueError("server.host must be loopback unless allow_remote = true")
83
+ return self
84
+
85
+
86
+ class ModeSettings(_Section):
87
+ default: Mode = "shadow"
88
+
89
+
90
+ class UpstreamEntry(_Section):
91
+ url: str
92
+ kind: UpstreamKind
93
+ usage: UsageStyle | None = None
94
+ cache_policy: CachePolicy | None = None
95
+ cost_source: CostSource | None = None
96
+ cache_hints: bool | None = None
97
+ inject_stream_usage: bool | None = None
98
+ pricing_provider: str | None = None
99
+
100
+ @field_validator("url")
101
+ @classmethod
102
+ def _check_url(cls, value: str) -> str:
103
+ if not value.startswith(("http://", "https://")):
104
+ raise ValueError("upstream url must start with http:// or https://")
105
+ return value
106
+
107
+
108
+ class HttpConfig(_Section):
109
+ connect_timeout_s: float = Field(default=10, gt=0)
110
+ write_timeout_s: float = Field(default=60, gt=0)
111
+ pool_timeout_s: float = Field(default=30, gt=0)
112
+ read_timeout_s: float = Field(default=0, ge=0)
113
+ http2: bool = True
114
+ max_connections: int = Field(default=100, gt=0)
115
+ force_identity_encoding: bool = True
116
+
117
+
118
+ class PipelineConfig(_Section):
119
+ time_budget_ms: int = Field(default=250, gt=0)
120
+ offload_threshold_bytes: int = Field(default=262144, gt=0)
121
+ min_saving_tokens: int = Field(default=20, ge=0)
122
+ retry_original_on_status: list[int] = Field(default_factory=lambda: [400, 422])
123
+
124
+
125
+ class AnsiStripConfig(_Section):
126
+ enabled: bool = True
127
+
128
+
129
+ class ProgressCollapseConfig(_Section):
130
+ enabled: bool = True
131
+
132
+
133
+ class LineDedupeConfig(_Section):
134
+ enabled: bool = True
135
+ min_repeat: int = Field(default=3, ge=2)
136
+
137
+
138
+ class WhitespaceTrimConfig(_Section):
139
+ enabled: bool = True
140
+ max_blank_run: int = Field(default=2, ge=1)
141
+
142
+
143
+ class SmartTruncateConfig(_Section):
144
+ enabled: bool = True
145
+ trigger_bytes: int = Field(default=16384, gt=0)
146
+ target_bytes: int = Field(default=10240, gt=0)
147
+ head_lines: int = Field(default=40, ge=0)
148
+ tail_lines: int = Field(default=60, ge=0)
149
+ context_lines: int = Field(default=3, ge=0)
150
+ max_line_chars: int = Field(default=2000, gt=0)
151
+ max_signal_lines: int = Field(default=150, ge=1)
152
+
153
+
154
+ class DuplicateBackrefConfig(_Section):
155
+ enabled: bool = True
156
+ min_bytes: int = Field(default=2048, ge=0)
157
+ include_never_touch: bool = True
158
+
159
+
160
+ class StaleResultElisionConfig(_Section):
161
+ enabled: bool = False
162
+ keep_recent_turns: int = Field(default=12, ge=0)
163
+ batch_turns: int = Field(default=8, ge=1)
164
+ min_bytes: int = Field(default=4096, ge=0)
165
+
166
+
167
+ class AnthropicCacheConfig(_Section):
168
+ enabled: bool = True
169
+ ttl: Literal["5m", "1h"] = "5m"
170
+ respect_existing: bool = True
171
+ max_breakpoints: int = Field(default=4, ge=1, le=4)
172
+
173
+
174
+ class OpenaiHintsConfig(_Section):
175
+ enabled: bool = False
176
+ send_prompt_cache_key: bool = True
177
+ inject_stream_usage: bool = False
178
+
179
+
180
+ class CacheServeConfig(_Section):
181
+ enabled: bool = False
182
+ include_never_touch: bool = True
183
+ min_bytes: int = Field(default=2048, ge=0)
184
+ max_stubs_per_request: int = Field(default=6, ge=0)
185
+ batch_turns: int = Field(default=8, ge=1)
186
+ keep_recent_turns: int = Field(default=12, ge=0)
187
+ max_state_sessions: int = Field(default=256, ge=1)
188
+ max_entries_per_session: int = Field(default=512, ge=1)
189
+
190
+
191
+ class VolatileNormalizerConfig(_Section):
192
+ enabled: bool = False
193
+
194
+
195
+ class TransformsConfig(_Section):
196
+ ansi_strip: AnsiStripConfig = Field(default_factory=AnsiStripConfig)
197
+ progress_collapse: ProgressCollapseConfig = Field(default_factory=ProgressCollapseConfig)
198
+ line_dedupe: LineDedupeConfig = Field(default_factory=LineDedupeConfig)
199
+ whitespace_trim: WhitespaceTrimConfig = Field(default_factory=WhitespaceTrimConfig)
200
+ smart_truncate: SmartTruncateConfig = Field(default_factory=SmartTruncateConfig)
201
+ duplicate_backref: DuplicateBackrefConfig = Field(default_factory=DuplicateBackrefConfig)
202
+ stale_result_elision: StaleResultElisionConfig = Field(default_factory=StaleResultElisionConfig)
203
+ anthropic_cache: AnthropicCacheConfig = Field(default_factory=AnthropicCacheConfig)
204
+ openai_hints: OpenaiHintsConfig = Field(default_factory=OpenaiHintsConfig)
205
+ cache_serve: CacheServeConfig = Field(default_factory=CacheServeConfig)
206
+ volatile_normalizer: VolatileNormalizerConfig = Field(default_factory=VolatileNormalizerConfig)
207
+
208
+
209
+ class ToolsConfig(_Section):
210
+ compactable: list[str] = Field(
211
+ default_factory=lambda: [
212
+ "*bash*",
213
+ "*shell*",
214
+ "*terminal*",
215
+ "*exec*",
216
+ "*run*",
217
+ "*command*",
218
+ "*test*",
219
+ "*build*",
220
+ "*npm*",
221
+ "*pytest*",
222
+ "*cargo*",
223
+ ]
224
+ )
225
+ never_touch: list[str] = Field(
226
+ default_factory=lambda: [
227
+ "*read*",
228
+ "*view*",
229
+ "*cat*",
230
+ "*edit*",
231
+ "*write*",
232
+ "*patch*",
233
+ "*apply*",
234
+ "*notebook*",
235
+ ]
236
+ )
237
+
238
+
239
+ class TelemetryConfig(_Section):
240
+ db_path: str = ""
241
+ capture_payloads: CaptureMode = "changed_blocks"
242
+ retention_days: int = Field(default=14, ge=1)
243
+ redact_secrets: bool = True
244
+ writer_queue_max: int = Field(default=10000, ge=1)
245
+ max_blob_bytes: int = Field(default=524288000, ge=0)
246
+
247
+
248
+ class RedundantRereadConfig(_Section):
249
+ enabled: bool = True
250
+ min_bytes: int = Field(default=512, ge=0)
251
+
252
+
253
+ class IdenticalOutputConfig(_Section):
254
+ enabled: bool = True
255
+ min_bytes: int = Field(default=1024, ge=0)
256
+
257
+
258
+ class UnbatchedFanoutConfig(_Section):
259
+ enabled: bool = True
260
+ min_calls: int = Field(default=4, ge=2)
261
+
262
+
263
+ class RunawaySessionConfig(_Section):
264
+ enabled: bool = True
265
+ idle_gap_s: int = Field(default=900, ge=0)
266
+ min_turns: int = Field(default=6, ge=1)
267
+
268
+
269
+ class RecurringDriftConfig(_Section):
270
+ enabled: bool = True
271
+ min_occurrences: int = Field(default=2, ge=1)
272
+
273
+
274
+ class ChurnDetectorConfig(_Section):
275
+ enabled: bool = False
276
+
277
+
278
+ class SchemaBloatConfig(_Section):
279
+ enabled: bool = True
280
+ min_session_requests: int = Field(default=10, ge=1)
281
+
282
+
283
+ class WasteConfig(_Section):
284
+ enabled: bool = True
285
+ dedupe_window_min: int = Field(default=30, ge=0)
286
+ capture_assistant_hashes: bool = True
287
+ redundant_reread: RedundantRereadConfig = Field(default_factory=RedundantRereadConfig)
288
+ identical_output: IdenticalOutputConfig = Field(default_factory=IdenticalOutputConfig)
289
+ unbatched_fanout: UnbatchedFanoutConfig = Field(default_factory=UnbatchedFanoutConfig)
290
+ runaway_session: RunawaySessionConfig = Field(default_factory=RunawaySessionConfig)
291
+ recurring_drift: RecurringDriftConfig = Field(default_factory=RecurringDriftConfig)
292
+ churn_detector: ChurnDetectorConfig = Field(default_factory=ChurnDetectorConfig)
293
+ schema_bloat: SchemaBloatConfig = Field(default_factory=SchemaBloatConfig)
294
+
295
+
296
+ class GuardConfig(_Section):
297
+ enabled: bool = True
298
+ fallback_window: int = Field(default=50, ge=1)
299
+ fallback_limit: int = Field(default=3, ge=0)
300
+ our_fault_drift_limit: int = Field(default=1, ge=0)
301
+ min_savings_to_stay: float = Field(default=0.01, ge=0)
302
+ notify: Literal["dashboard", "dashboard+banner"] = "dashboard"
303
+ auto_reenable: int = Field(default=0, ge=0)
304
+ expand_error_rate: float = Field(default=0.20, ge=0, le=1)
305
+
306
+
307
+ class PricingConfig(_Section):
308
+ file: str = ""
309
+
310
+
311
+ class DashboardConfig(_Section):
312
+ enabled: bool = True
313
+
314
+
315
+ def _default_upstreams() -> dict[str, str | UpstreamEntry]:
316
+ return {
317
+ "anthropic": "https://api.anthropic.com",
318
+ "openai": "https://api.openai.com",
319
+ "gemini": "https://generativelanguage.googleapis.com",
320
+ "ollama": "http://127.0.0.1:11434",
321
+ }
322
+
323
+
324
+ class Config(_Section):
325
+ """Effective configuration for one process."""
326
+
327
+ schema_version: int = 1
328
+ server: ServerConfig = Field(default_factory=ServerConfig)
329
+ mode: ModeSettings = Field(default_factory=ModeSettings)
330
+ upstreams: dict[str, str | UpstreamEntry] = Field(default_factory=_default_upstreams)
331
+ http: HttpConfig = Field(default_factory=HttpConfig)
332
+ pipeline: PipelineConfig = Field(default_factory=PipelineConfig)
333
+ transforms: TransformsConfig = Field(default_factory=TransformsConfig)
334
+ tools: ToolsConfig = Field(default_factory=ToolsConfig)
335
+ telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig)
336
+ waste: WasteConfig = Field(default_factory=WasteConfig)
337
+ guard: GuardConfig = Field(default_factory=GuardConfig)
338
+ pricing: PricingConfig = Field(default_factory=PricingConfig)
339
+ dashboard: DashboardConfig = Field(default_factory=DashboardConfig)
340
+
341
+ @field_validator("upstreams")
342
+ @classmethod
343
+ def _check_upstreams(
344
+ cls, value: dict[str, str | UpstreamEntry]
345
+ ) -> dict[str, str | UpstreamEntry]:
346
+ for name, entry in value.items():
347
+ url = entry if isinstance(entry, str) else entry.url
348
+ if not url.startswith(("http://", "https://")):
349
+ raise ValueError(f"upstream {name!r} url must start with http:// or https://")
350
+ return value
351
+
352
+ @model_validator(mode="after")
353
+ def _check_consistency(self) -> Config:
354
+ truncate = self.transforms.smart_truncate
355
+ if truncate.target_bytes * 1.5 > truncate.trigger_bytes:
356
+ raise ValueError(
357
+ "transforms.smart_truncate.target_bytes * 1.5 must be <= trigger_bytes"
358
+ )
359
+ if self.transforms.cache_serve.enabled and self.telemetry.capture_payloads == "off":
360
+ raise ValueError(
361
+ "transforms.cache_serve.enabled requires telemetry.capture_payloads != 'off'"
362
+ )
363
+ return self
364
+
365
+ @property
366
+ def config_hash(self) -> str:
367
+ """Hash of the settings that change forwarded bytes (SPEC section 7.1)."""
368
+ return compute_config_hash(self)
369
+
370
+
371
+ def compute_config_hash(config: Config) -> str:
372
+ """Return a stable sha256 over transform-effective configuration."""
373
+ payload = {
374
+ "transforms": config.transforms.model_dump(mode="json"),
375
+ "tools": config.tools.model_dump(mode="json"),
376
+ "pipeline": config.pipeline.model_dump(mode="json"),
377
+ }
378
+ canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
379
+ return sha256(canonical.encode("utf-8")).hexdigest()
380
+
381
+
382
+ def default_config_path() -> Path:
383
+ """Return the per-user config file path."""
384
+ return Path(platformdirs.user_config_dir("ctxoptimizer")) / "config.toml"
385
+
386
+
387
+ def data_dir(env: Mapping[str, str] | None = None) -> Path:
388
+ """Return the data directory, honouring ``CTXOPTIMIZER_DATA_DIR``."""
389
+ source = os.environ if env is None else env
390
+ override = source.get("CTXOPTIMIZER_DATA_DIR")
391
+ if override:
392
+ return Path(override).expanduser()
393
+ return Path(platformdirs.user_data_dir("ctxoptimizer"))
394
+
395
+
396
+ def resolve_config_path(path: Path | None = None, env: Mapping[str, str] | None = None) -> Path:
397
+ """Resolve the config path from an explicit value, env or platform default."""
398
+ source = os.environ if env is None else env
399
+ if path is not None:
400
+ return path.expanduser()
401
+ from_env = source.get("CTXOPTIMIZER_CONFIG")
402
+ if from_env:
403
+ return Path(from_env).expanduser()
404
+ return default_config_path()
405
+
406
+
407
+ def default_config_text() -> str:
408
+ """Return the commented default config shipped with the package."""
409
+ resource = resources.files("ctxoptimizer").joinpath("data", CONFIG_RESOURCE)
410
+ return resource.read_text(encoding="utf-8")
411
+
412
+
413
+ def ensure_config_file(path: Path) -> bool:
414
+ """Create *path* from the shipped template if missing. Return True if created."""
415
+ if path.exists():
416
+ return False
417
+ path.parent.mkdir(parents=True, exist_ok=True)
418
+ path.write_text(default_config_text(), encoding="utf-8")
419
+ return True
420
+
421
+
422
+ def load_config(
423
+ *,
424
+ path: Path | None = None,
425
+ overrides: Mapping[str, object] | None = None,
426
+ env: Mapping[str, str] | None = None,
427
+ ) -> Config:
428
+ """Load and validate configuration (CLI > env > file > defaults)."""
429
+ source = dict(os.environ if env is None else env)
430
+ explicit = path is not None or "CTXOPTIMIZER_CONFIG" in source
431
+ config_path = resolve_config_path(path, source)
432
+ raw: dict[str, Any] = {}
433
+ if config_path.exists():
434
+ with config_path.open("rb") as handle:
435
+ raw = tomllib.load(handle)
436
+ elif explicit:
437
+ raise FileNotFoundError(f"config file not found: {config_path}")
438
+ _apply_env(raw, source)
439
+ if overrides:
440
+ for dotted, value in overrides.items():
441
+ _set_dotted(raw, dotted, value)
442
+ unknown = unknown_keys(raw)
443
+ if unknown:
444
+ logger.warning("unknown config keys ignored: %s", ", ".join(unknown))
445
+ return Config.model_validate(raw)
446
+
447
+
448
+ def unknown_keys(raw: Mapping[str, Any]) -> list[str]:
449
+ """Return dotted paths present in *raw* that the config models do not define."""
450
+ return _unknown_in(raw, Config, "")
451
+
452
+
453
+ def _unknown_in(raw: Mapping[str, Any], model: type[BaseModel], prefix: str) -> list[str]:
454
+ found: list[str] = []
455
+ fields = model.model_fields
456
+ for key, value in raw.items():
457
+ path = f"{prefix}{key}"
458
+ field = fields.get(key)
459
+ if field is None:
460
+ found.append(path)
461
+ continue
462
+ annotation = field.annotation
463
+ if (
464
+ isinstance(value, Mapping)
465
+ and isinstance(annotation, type)
466
+ and issubclass(annotation, BaseModel)
467
+ ):
468
+ found.extend(_unknown_in(value, annotation, f"{path}."))
469
+ return found
470
+
471
+
472
+ def _apply_env(raw: dict[str, Any], env: Mapping[str, str]) -> None:
473
+ for name, dotted in _ENV_FIELDS.items():
474
+ if name in env:
475
+ _set_dotted(raw, dotted, env[name])
476
+ for name, value in env.items():
477
+ if not name.startswith(f"{ENV_PREFIX}UPSTREAM_"):
478
+ continue
479
+ upstream = name.removeprefix(f"{ENV_PREFIX}UPSTREAM_").lower()
480
+ if upstream:
481
+ _set_dotted(raw, f"upstreams.{upstream}", value)
482
+
483
+
484
+ def _set_dotted(raw: dict[str, Any], dotted: str, value: object) -> None:
485
+ parts = dotted.split(".")
486
+ cursor = raw
487
+ for part in parts[:-1]:
488
+ existing = cursor.get(part)
489
+ if not isinstance(existing, dict):
490
+ existing = {}
491
+ cursor[part] = existing
492
+ cursor = existing
493
+ cursor[parts[-1]] = value
494
+
495
+
496
+ __all__ = [
497
+ "CachePolicy",
498
+ "Config",
499
+ "CostSource",
500
+ "Mode",
501
+ "ShellKind",
502
+ "UpstreamKind",
503
+ "UsageStyle",
504
+ "compute_config_hash",
505
+ "data_dir",
506
+ "default_config_path",
507
+ "default_config_text",
508
+ "ensure_config_file",
509
+ "load_config",
510
+ "resolve_config_path",
511
+ "unknown_keys",
512
+ ]
@@ -0,0 +1 @@
1
+ """Admin API and server-rendered dashboard (SPEC section 16)."""