typevet 0.1.0.dev1__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 (133) hide show
  1. typevet/__init__.py +67 -0
  2. typevet/_version.py +43 -0
  3. typevet/adapters/__init__.py +16 -0
  4. typevet/adapters/diagnostics/__init__.py +59 -0
  5. typevet/adapters/diagnostics/fields.py +108 -0
  6. typevet/adapters/diagnostics/generation_events.py +75 -0
  7. typevet/adapters/diagnostics/http_events.py +91 -0
  8. typevet/adapters/diagnostics/logs.py +116 -0
  9. typevet/adapters/diagnostics/redaction.py +94 -0
  10. typevet/adapters/diagnostics/settings.py +77 -0
  11. typevet/adapters/inbound/__init__.py +65 -0
  12. typevet/adapters/inbound/api.py +61 -0
  13. typevet/adapters/inbound/backend_settings.py +608 -0
  14. typevet/adapters/inbound/cord_semantic_acceptance_cli.py +135 -0
  15. typevet/adapters/inbound/helpers.py +67 -0
  16. typevet/adapters/inbound/settings.py +154 -0
  17. typevet/adapters/outbound/__init__.py +62 -0
  18. typevet/adapters/outbound/async_fake.py +123 -0
  19. typevet/adapters/outbound/async_llama_cpp.py +168 -0
  20. typevet/adapters/outbound/chat_completion.py +148 -0
  21. typevet/adapters/outbound/fake.py +119 -0
  22. typevet/adapters/outbound/gemma/__init__.py +84 -0
  23. typevet/adapters/outbound/gemma/answer_binding.py +290 -0
  24. typevet/adapters/outbound/gemma/scoring_prefix.py +102 -0
  25. typevet/adapters/outbound/gemma/served_template.py +131 -0
  26. typevet/adapters/outbound/gemma_native_vision_factory.py +286 -0
  27. typevet/adapters/outbound/generation_finite.py +50 -0
  28. typevet/adapters/outbound/http_errors.py +38 -0
  29. typevet/adapters/outbound/judgment_scoring.py +471 -0
  30. typevet/adapters/outbound/llama_cpp.py +187 -0
  31. typevet/adapters/outbound/llama_cpp_http.py +103 -0
  32. typevet/adapters/outbound/llama_cpp_multimodal.py +145 -0
  33. typevet/adapters/outbound/llama_cpp_scoring.py +333 -0
  34. typevet/adapters/outbound/vllm_content.py +63 -0
  35. typevet/adapters/outbound/vllm_generation.py +163 -0
  36. typevet/adapters/outbound/vllm_generation_async.py +257 -0
  37. typevet/adapters/outbound/vllm_http.py +121 -0
  38. typevet/adapters/outbound/vllm_judgment_factory.py +210 -0
  39. typevet/adapters/outbound/vllm_scoring.py +372 -0
  40. typevet/domain/__init__.py +193 -0
  41. typevet/domain/candidate_scoring_request.py +134 -0
  42. typevet/domain/candidate_scoring_response.py +96 -0
  43. typevet/domain/candidate_scoring_validate.py +197 -0
  44. typevet/domain/decision_compile.py +368 -0
  45. typevet/domain/decision_execute.py +229 -0
  46. typevet/domain/decisions.py +129 -0
  47. typevet/domain/errors.py +235 -0
  48. typevet/domain/field_instructions.py +134 -0
  49. typevet/domain/judgment_answers.py +198 -0
  50. typevet/domain/judgment_normalize.py +243 -0
  51. typevet/domain/judgment_questions.py +187 -0
  52. typevet/domain/judgment_response.py +102 -0
  53. typevet/domain/media.py +101 -0
  54. typevet/domain/models.py +115 -0
  55. typevet/domain/question_schema.py +254 -0
  56. typevet/domain/scoring_stage.py +34 -0
  57. typevet/evaluation/__init__.py +19 -0
  58. typevet/evaluation/consumer_http_accounting.py +135 -0
  59. typevet/evaluation/cord_expense_call_accounting.py +108 -0
  60. typevet/evaluation/cord_expense_live_harness.py +75 -0
  61. typevet/evaluation/cord_expense_receipt_requirement.py +212 -0
  62. typevet/evaluation/cord_expense_smoke.py +328 -0
  63. typevet/evaluation/cord_semantic_acceptance.py +463 -0
  64. typevet/evaluation/cord_semantic_acceptance_report.py +90 -0
  65. typevet/evaluation/cord_semantic_metrics.py +57 -0
  66. typevet/evaluation/datasets/__init__.py +30 -0
  67. typevet/evaluation/datasets/banking77.py +270 -0
  68. typevet/evaluation/datasets/boolq.py +550 -0
  69. typevet/evaluation/datasets/boolq_download.py +132 -0
  70. typevet/evaluation/datasets/civil_comments.py +450 -0
  71. typevet/evaluation/datasets/clinc.py +125 -0
  72. typevet/evaluation/datasets/clinc_domains.json +172 -0
  73. typevet/evaluation/datasets/clinc_download.py +103 -0
  74. typevet/evaluation/datasets/clinc_plus_intent_names.json +153 -0
  75. typevet/evaluation/datasets/clinc_rows.py +306 -0
  76. typevet/evaluation/datasets/clinc_shard.py +156 -0
  77. typevet/evaluation/datasets/cord_expense.py +381 -0
  78. typevet/evaluation/datasets/difraud.py +256 -0
  79. typevet/evaluation/datasets/go_emotions.py +368 -0
  80. typevet/evaluation/datasets/go_emotions_download.py +154 -0
  81. typevet/evaluation/datasets/hyperpartisan.py +562 -0
  82. typevet/evaluation/datasets/partner_guard.py +170 -0
  83. typevet/evaluation/datasets/psai.py +415 -0
  84. typevet/evaluation/datasets/psai_download.py +167 -0
  85. typevet/evaluation/datasets/psai_evidence_pilot.py +570 -0
  86. typevet/evaluation/datasets/psai_schema.py +157 -0
  87. typevet/evaluation/datasets/psai_stream.py +166 -0
  88. typevet/evaluation/datasets/psai_vision.py +534 -0
  89. typevet/evaluation/datasets/psai_vision_controls.py +502 -0
  90. typevet/evaluation/datasets/pubmedqa.py +420 -0
  91. typevet/evaluation/experiment_identity.py +677 -0
  92. typevet/evaluation/psai_vision_consumer_accounting.py +190 -0
  93. typevet/evaluation/psai_vision_consumer_dispatch.py +216 -0
  94. typevet/evaluation/psai_vision_consumer_harness.py +342 -0
  95. typevet/evaluation/psai_vision_consumer_live.py +332 -0
  96. typevet/evaluation/psai_vision_consumer_live_identity.py +156 -0
  97. typevet/evaluation/psai_vision_consumer_live_receipt.py +132 -0
  98. typevet/evaluation/psai_vision_consumer_live_router.py +162 -0
  99. typevet/evaluation/psai_vision_consumer_offline.py +418 -0
  100. typevet/evaluation/psai_vision_consumer_outcomes.py +224 -0
  101. typevet/evaluation/psai_vision_consumer_protocol.py +69 -0
  102. typevet/evaluation/psai_vision_consumer_receipt.py +161 -0
  103. typevet/evaluation/psai_vision_consumer_receipt_structure.py +263 -0
  104. typevet/evaluation/psai_vision_probability_evidence.py +336 -0
  105. typevet/evaluation/runner/__init__.py +56 -0
  106. typevet/evaluation/runner/core.py +104 -0
  107. typevet/evaluation/runner/datasets.py +186 -0
  108. typevet/evaluation/runner/live_gate.py +149 -0
  109. typevet/evaluation/runner/report.py +116 -0
  110. typevet/evaluation/tpjep/__init__.py +81 -0
  111. typevet/evaluation/tpjep/loader.py +237 -0
  112. typevet/evaluation/tpjep/outcome.py +101 -0
  113. typevet/evaluation/tpjep/records.py +428 -0
  114. typevet/evaluation/tpjep/runner.py +325 -0
  115. typevet/ports/__init__.py +39 -0
  116. typevet/ports/async_generation.py +57 -0
  117. typevet/ports/framing.py +59 -0
  118. typevet/ports/generation.py +58 -0
  119. typevet/ports/judgment.py +71 -0
  120. typevet/ports/scoring.py +64 -0
  121. typevet/py.typed +0 -0
  122. typevet/runtime/__init__.py +50 -0
  123. typevet/runtime/categorical.py +196 -0
  124. typevet/runtime/judgment.py +17 -0
  125. typevet/runtime/llama_cpp_gemma_vision.py +28 -0
  126. typevet/runtime/scoring_prefix.py +14 -0
  127. typevet/runtime/vllm_judgment.py +28 -0
  128. typevet/testing/__init__.py +22 -0
  129. typevet/testing/fakes.py +152 -0
  130. typevet-0.1.0.dev1.dist-info/METADATA +125 -0
  131. typevet-0.1.0.dev1.dist-info/RECORD +133 -0
  132. typevet-0.1.0.dev1.dist-info/WHEEL +4 -0
  133. typevet-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
typevet/__init__.py ADDED
@@ -0,0 +1,67 @@
1
+ """typevet: type-safe structured generation under hexagonal architecture.
2
+
3
+ Examples:
4
+ ```python
5
+ from typevet import GenerationRequest, GenerationResult
6
+ from typevet.adapters.outbound import FakeGenerationAdapter
7
+
8
+ schema = {
9
+ "type": "object",
10
+ "properties": {"ok": {"type": "boolean"}},
11
+ "required": ["ok"],
12
+ "additionalProperties": False,
13
+ }
14
+ port = FakeGenerationAdapter(value={"ok": True})
15
+ result = port.generate(
16
+ GenerationRequest(prompt="Say ok.", schema=schema, model="fake")
17
+ )
18
+ assert result.value["ok"] is True
19
+ ```
20
+
21
+ See Also:
22
+ - [typevet._version][]: Resolves ``__version__`` from distribution metadata
23
+ - [typevet.domain][]: Request, result and error types
24
+ - [typevet.ports][]: GenerationPort protocol
25
+ - [typevet.adapters.outbound][]: llama.cpp and fake adapters
26
+ - [typevet.runtime][]: Source of the ``decide_categorical`` re-export
27
+
28
+ Attributes:
29
+ BackendHttpError (type): llama.cpp HTTP error status with body snippet.
30
+ GenerationError (type): Base failure for a generation call.
31
+ TransportError (type): HTTP client failure before a response.
32
+ AsyncGenerationPort (type): Structural protocol for async typed generation.
33
+ GenerationPort (type): Structural protocol for typed generation.
34
+ GenerationRequest (type): Prompt, schema and model ask.
35
+ GenerationResult (type): Validated structured value.
36
+ SchemaValidationError (type): Output failed the requested schema.
37
+ decide_categorical (callable): M1 categorical decision via scoring port;
38
+ re-exported from ``typevet.runtime.categorical``.
39
+ __version__ (str): Installed distribution version; matches
40
+ ``importlib.metadata.version("typevet")`` when the package is on
41
+ ``PYTHONPATH``. Exported in ``__all__``.
42
+ """
43
+
44
+ from typevet._version import __version__
45
+ from typevet.domain.errors import (
46
+ BackendHttpError,
47
+ GenerationError,
48
+ SchemaValidationError,
49
+ TransportError,
50
+ )
51
+ from typevet.domain.models import GenerationRequest, GenerationResult
52
+ from typevet.ports.async_generation import AsyncGenerationPort
53
+ from typevet.ports.generation import GenerationPort
54
+ from typevet.runtime.categorical import decide_categorical
55
+
56
+ __all__ = [
57
+ "AsyncGenerationPort",
58
+ "BackendHttpError",
59
+ "GenerationError",
60
+ "GenerationPort",
61
+ "GenerationRequest",
62
+ "GenerationResult",
63
+ "SchemaValidationError",
64
+ "TransportError",
65
+ "__version__",
66
+ "decide_categorical",
67
+ ]
typevet/_version.py ADDED
@@ -0,0 +1,43 @@
1
+ """Resolve the package version from distribution metadata.
2
+
3
+ When the wheel or sdist is installed, ``__version__`` comes from
4
+ ``importlib.metadata``. In editable checkouts without metadata, it falls back
5
+ to ``[project].version`` in ``pyproject.toml``.
6
+
7
+ Examples:
8
+ ```python
9
+ import typevet
10
+ from importlib.metadata import version
11
+
12
+ assert typevet.__version__ == version("typevet")
13
+ ```
14
+
15
+ See Also:
16
+ - [typevet][]: Re-exports ``__version__`` in ``__all__``
17
+ """
18
+
19
+ import tomllib
20
+ from importlib.metadata import PackageNotFoundError, version
21
+ from pathlib import Path
22
+
23
+ __all__ = ["__version__"]
24
+
25
+
26
+ def _read_pyproject_version() -> str:
27
+ root = Path(__file__).resolve().parents[2]
28
+ data = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))
29
+ project_version = data["project"]["version"]
30
+ if not isinstance(project_version, str):
31
+ msg = "pyproject [project].version must be a string"
32
+ raise TypeError(msg)
33
+ return project_version
34
+
35
+
36
+ def _package_version() -> str:
37
+ try:
38
+ return version("typevet")
39
+ except PackageNotFoundError:
40
+ return _read_pyproject_version()
41
+
42
+
43
+ __version__ = _package_version()
@@ -0,0 +1,16 @@
1
+ """Adapters between the domain and the outside world.
2
+
3
+ Examples:
4
+ ```python
5
+ from typevet.adapters.inbound import generate
6
+ from typevet.adapters.outbound import FakeGenerationAdapter
7
+ ```
8
+
9
+ See Also:
10
+ - [typevet.adapters.inbound][]: Library entry points
11
+ - [typevet.adapters.outbound][]: Inference backends
12
+ - [typevet.ports][]: Port protocols
13
+
14
+ Attributes:
15
+ None: This package provides organizational structure only.
16
+ """
@@ -0,0 +1,59 @@
1
+ """Structured stderr diagnostics for generation and HTTP adapters.
2
+
3
+ Diagnostics are operator signals, not telemetry: no automatic network export.
4
+ Importing this package does not configure structlog.
5
+
6
+ Examples:
7
+ ```python
8
+ from typevet.adapters.diagnostics import (
9
+ bind_run_id,
10
+ configure_from_environ,
11
+ generation_call_event,
12
+ http_request_event,
13
+ new_run_id,
14
+ )
15
+ ```
16
+
17
+ See Also:
18
+ - [typevet.adapters.diagnostics.logs][]: Composition-root configuration
19
+ - [typevet.adapters.diagnostics.redaction][]: Secret and prompt redaction
20
+
21
+ Attributes:
22
+ REDACTED (str): Placeholder written over redacted secret values.
23
+ SECRET_KEYS (frozenset[str]): Field names always masked in diagnostics.
24
+ LogSettings (type): Frozen log format, level and prompt policy.
25
+ bind_run_id (function): Bind ``run_id`` on every diagnostic line.
26
+ configure (function): Apply ``LogSettings`` to stderr structlog.
27
+ configure_from_environ (function): Load env settings and configure once.
28
+ diagnostic_model (function): Keep safe model aliases in event fields.
29
+ generation_call_event (function): Terminal ``generation.call`` context manager.
30
+ http_request_event (function): Terminal ``http.request`` context manager.
31
+ load_log_settings (function): Read ``TYPEVET_LOG__*`` variables.
32
+ new_run_id (function): Create a short hex invocation id.
33
+ """
34
+
35
+ from typevet.adapters.diagnostics.fields import diagnostic_model
36
+ from typevet.adapters.diagnostics.generation_events import generation_call_event
37
+ from typevet.adapters.diagnostics.http_events import http_request_event
38
+ from typevet.adapters.diagnostics.logs import (
39
+ bind_run_id,
40
+ configure,
41
+ configure_from_environ,
42
+ new_run_id,
43
+ )
44
+ from typevet.adapters.diagnostics.redaction import REDACTED, SECRET_KEYS
45
+ from typevet.adapters.diagnostics.settings import LogSettings, load_log_settings
46
+
47
+ __all__ = [
48
+ "REDACTED",
49
+ "SECRET_KEYS",
50
+ "LogSettings",
51
+ "bind_run_id",
52
+ "configure",
53
+ "configure_from_environ",
54
+ "diagnostic_model",
55
+ "generation_call_event",
56
+ "http_request_event",
57
+ "load_log_settings",
58
+ "new_run_id",
59
+ ]
@@ -0,0 +1,108 @@
1
+ """Filter built-in diagnostic events and bound model identifiers.
2
+
3
+ Built-in events form a closed set until the reference table lands (#40).
4
+ Importing this module does not configure logging.
5
+
6
+ Examples:
7
+ ```python
8
+ from typevet.adapters.diagnostics.fields import diagnostic_model
9
+
10
+ assert diagnostic_model("gemma-4-test") == "gemma-4-test"
11
+ ```
12
+
13
+ See Also:
14
+ - [typevet.adapters.diagnostics.logs][]: Wires ``filter_event_fields`` into structlog
15
+ - [typevet.adapters.diagnostics.generation_events][]: ``generation.call`` field set
16
+ - [typevet.adapters.diagnostics.http_events][]: ``http.request`` field set
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from contextvars import ContextVar, Token
23
+ from typing import Any
24
+
25
+ _RUN_ID: ContextVar[str | None] = ContextVar("typevet_run_id", default=None)
26
+ _MODEL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$")
27
+ _EVENT_FIELDS = {
28
+ "generation.call": frozenset(
29
+ {
30
+ "event",
31
+ "model",
32
+ "outcome",
33
+ "error_type",
34
+ "run_id",
35
+ }
36
+ ),
37
+ "http.request": frozenset(
38
+ {
39
+ "event",
40
+ "method",
41
+ "path",
42
+ "model",
43
+ "status_code",
44
+ "outcome",
45
+ "error_type",
46
+ "run_id",
47
+ }
48
+ ),
49
+ }
50
+
51
+
52
+ def current_run_id() -> str | None:
53
+ """Return the bound invocation ``run_id``, if any.
54
+
55
+ Returns:
56
+ The current ``run_id``, or ``None`` when nothing is bound.
57
+ """
58
+ return _RUN_ID.get()
59
+
60
+
61
+ def set_run_id_context(run_id: str | None) -> Token[str | None]:
62
+ """Store ``run_id`` in a context variable; returns the reset token.
63
+
64
+ Args:
65
+ run_id: Identifier to attach to subsequent diagnostic lines.
66
+
67
+ Returns:
68
+ Token passed to ``reset_run_id_context`` to restore the prior value.
69
+ """
70
+ return _RUN_ID.set(run_id)
71
+
72
+
73
+ def reset_run_id_context(token: Token[str | None]) -> None:
74
+ """Restore the previous ``run_id`` binding."""
75
+ _RUN_ID.reset(token)
76
+
77
+
78
+ def diagnostic_model(model: str | None) -> str | None:
79
+ """Keep bounded model aliases in diagnostics without changing API values.
80
+
81
+ Args:
82
+ model: Requested or resolved router model alias.
83
+
84
+ Returns:
85
+ The identifier when it matches the safe pattern, else ``None``.
86
+ """
87
+ if model is not None and _MODEL.fullmatch(model):
88
+ return model
89
+ return None
90
+
91
+
92
+ def filter_event_fields(
93
+ _logger: Any, _method: str, event_dict: dict[str, Any]
94
+ ) -> dict[str, Any]:
95
+ """Drop unknown keys from built-in events; keep application events intact.
96
+
97
+ Prompts, schemas, headers and response bodies never appear in the
98
+ closed field sets for ``generation.call`` and ``http.request``.
99
+
100
+ Returns:
101
+ The event dict unchanged for unknown events, or filtered to the
102
+ closed field set for built-in events.
103
+ """
104
+ name = event_dict.get("event")
105
+ fields = _EVENT_FIELDS.get(name) if isinstance(name, str) else None
106
+ if fields is None:
107
+ return event_dict
108
+ return {key: value for key, value in event_dict.items() if key in fields}
@@ -0,0 +1,75 @@
1
+ """Emit one terminal generation diagnostic per logical call when configured.
2
+
3
+ Examples:
4
+ ```python
5
+ from typevet.adapters.diagnostics.generation_events import generation_call_event
6
+
7
+ with generation_call_event(model="fake") as event:
8
+ event.outcome = "success"
9
+ ```
10
+
11
+ See Also:
12
+ - [typevet.adapters.diagnostics.fields][]: ``run_id`` and model field helpers
13
+ - [typevet.adapters.diagnostics.logs][]: Configures structlog before events emit
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Iterator
19
+ from contextlib import contextmanager
20
+ from dataclasses import dataclass
21
+
22
+ import structlog
23
+
24
+ from typevet.adapters.diagnostics.fields import current_run_id, diagnostic_model
25
+
26
+
27
+ @dataclass
28
+ class GenerationCallEvent:
29
+ """Terminal generation metadata without prompt or schema payloads.
30
+
31
+ Attributes:
32
+ outcome (str): ``success`` or ``error`` after the adapter finishes.
33
+
34
+ Examples:
35
+ ```python
36
+ from typevet.adapters.diagnostics.generation_events import GenerationCallEvent
37
+
38
+ event = GenerationCallEvent(outcome="success")
39
+ assert event.outcome == "success"
40
+ ```
41
+ """
42
+
43
+ outcome: str = "error"
44
+
45
+
46
+ @contextmanager
47
+ def generation_call_event(*, model: str) -> Iterator[GenerationCallEvent]:
48
+ """Yield terminal metadata and emit ``generation.call`` when configured.
49
+
50
+ Args:
51
+ model: Router model alias for the call.
52
+
53
+ Yields:
54
+ Event state the adapter updates before the block exits.
55
+
56
+ Raises:
57
+ BaseException: Re-raises any exception from the wrapped block after
58
+ recording ``error_type`` on the terminal event.
59
+ """
60
+ event = GenerationCallEvent()
61
+ failure: BaseException | None = None
62
+ try:
63
+ yield event
64
+ except BaseException as exc:
65
+ failure = exc
66
+ raise
67
+ finally:
68
+ if structlog.is_configured():
69
+ structlog.get_logger().debug(
70
+ "generation.call",
71
+ model=diagnostic_model(model),
72
+ outcome=event.outcome,
73
+ error_type=None if failure is None else type(failure).__name__,
74
+ run_id=current_run_id(),
75
+ )
@@ -0,0 +1,91 @@
1
+ """Emit one terminal HTTP diagnostic per logical request when logging is configured.
2
+
3
+ Unconfigured library calls stay silent. This module never opens a network sink;
4
+ it only writes through the configured stderr renderer.
5
+
6
+ Examples:
7
+ ```python
8
+ from typevet.adapters.diagnostics.http_events import http_request_event
9
+
10
+ with http_request_event(model="fake", path="v1/chat/completions") as event:
11
+ event.status_code = 200
12
+ event.outcome = "success"
13
+ ```
14
+
15
+ See Also:
16
+ - [typevet.adapters.diagnostics.fields][]: ``run_id`` and model field helpers
17
+ - [typevet.adapters.diagnostics.logs][]: Configures structlog before events emit
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections.abc import Iterator
23
+ from contextlib import contextmanager
24
+ from dataclasses import dataclass
25
+
26
+ import structlog
27
+
28
+ from typevet.adapters.diagnostics.fields import current_run_id, diagnostic_model
29
+
30
+
31
+ @dataclass
32
+ class HttpRequestEvent:
33
+ """Terminal HTTP metadata without prompts, schemas or response bodies.
34
+
35
+ Attributes:
36
+ status_code (int | None): HTTP status when the adapter knows it.
37
+ outcome (str): ``success`` or ``error`` after the request completes.
38
+
39
+ Examples:
40
+ ```python
41
+ from typevet.adapters.diagnostics.http_events import HttpRequestEvent
42
+
43
+ event = HttpRequestEvent(status_code=200, outcome="success")
44
+ assert event.status_code == 200
45
+ ```
46
+ """
47
+
48
+ status_code: int | None = None
49
+ outcome: str = "error"
50
+
51
+
52
+ @contextmanager
53
+ def http_request_event(
54
+ *,
55
+ model: str,
56
+ method: str = "POST",
57
+ path: str = "v1/chat/completions",
58
+ ) -> Iterator[HttpRequestEvent]:
59
+ """Yield mutable terminal metadata and emit ``http.request`` when configured.
60
+
61
+ Args:
62
+ model: Router model alias for the call.
63
+ method: HTTP method (for example ``POST``).
64
+ path: Path relative to the adapter base URL.
65
+
66
+ Yields:
67
+ Event state the adapter updates before the block exits.
68
+
69
+ Raises:
70
+ BaseException: Re-raises any exception from the wrapped block after
71
+ recording ``error_type`` on the terminal event.
72
+ """
73
+ event = HttpRequestEvent()
74
+ failure: BaseException | None = None
75
+ try:
76
+ yield event
77
+ except BaseException as exc:
78
+ failure = exc
79
+ raise
80
+ finally:
81
+ if structlog.is_configured():
82
+ structlog.get_logger().debug(
83
+ "http.request",
84
+ method=method,
85
+ path=path,
86
+ model=diagnostic_model(model),
87
+ status_code=event.status_code,
88
+ outcome=event.outcome,
89
+ error_type=None if failure is None else type(failure).__name__,
90
+ run_id=current_run_id(),
91
+ )
@@ -0,0 +1,116 @@
1
+ """Configure stderr structlog diagnostics at the composition root.
2
+
3
+ Logs are diagnostics, not telemetry: no automatic network export. Library
4
+ imports stay silent until ``configure`` or ``configure_from_environ`` runs.
5
+
6
+ Examples:
7
+ ```python
8
+ from typevet.adapters.diagnostics.logs import (
9
+ bind_run_id,
10
+ configure_from_environ,
11
+ new_run_id,
12
+ )
13
+
14
+ configure_from_environ()
15
+ bind_run_id(new_run_id())
16
+ ```
17
+
18
+ See Also:
19
+ - [typevet.adapters.diagnostics.http_events][]: HTTP terminal events
20
+ - [typevet.adapters.diagnostics.generation_events][]: Generation events
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ import sys
27
+ import uuid
28
+ from typing import Any
29
+
30
+ import structlog
31
+ from structlog.tracebacks import ExceptionDictTransformer
32
+
33
+ from typevet.adapters.diagnostics.fields import filter_event_fields, set_run_id_context
34
+ from typevet.adapters.diagnostics.redaction import make_redact_processor
35
+ from typevet.adapters.diagnostics.settings import LogSettings, load_log_settings
36
+
37
+ LEVELS = {
38
+ "debug": logging.DEBUG,
39
+ "info": logging.INFO,
40
+ "warning": logging.WARNING,
41
+ "error": logging.ERROR,
42
+ "critical": logging.CRITICAL,
43
+ }
44
+ _DICT_TRACEBACKS = structlog.processors.ExceptionRenderer(
45
+ ExceptionDictTransformer(show_locals=False)
46
+ )
47
+
48
+
49
+ def wants_json(settings: LogSettings, stream: Any) -> bool:
50
+ """Return true when lines should render as JSON objects.
51
+
52
+ Returns:
53
+ ``True`` when JSON rendering is selected explicitly or inferred from
54
+ a non-TTY stream under ``auto`` format.
55
+ """
56
+ if settings.format != "auto":
57
+ return settings.format == "json"
58
+ return not (hasattr(stream, "isatty") and stream.isatty())
59
+
60
+
61
+ def configure(settings: LogSettings, stream: Any = None) -> None:
62
+ """Send structured diagnostics to stderr with redaction and field filters.
63
+
64
+ Args:
65
+ settings: Format, level and prompt logging policy.
66
+ stream: Output stream; defaults to ``sys.stderr``.
67
+ """
68
+ target = sys.stderr if stream is None else stream
69
+ redact = make_redact_processor(log_prompts=settings.log_prompts)
70
+ processors: list[Any] = [
71
+ structlog.contextvars.merge_contextvars,
72
+ filter_event_fields,
73
+ structlog.processors.add_log_level,
74
+ structlog.processors.TimeStamper(fmt="iso", utc=True),
75
+ ]
76
+ if wants_json(settings, target):
77
+ processors += [_DICT_TRACEBACKS, redact, structlog.processors.JSONRenderer()]
78
+ else:
79
+ processors += [
80
+ redact,
81
+ structlog.dev.ConsoleRenderer(
82
+ exception_formatter=structlog.dev.plain_traceback
83
+ ),
84
+ ]
85
+ structlog.configure(
86
+ processors=processors,
87
+ wrapper_class=structlog.make_filtering_bound_logger(LEVELS[settings.level]),
88
+ logger_factory=structlog.PrintLoggerFactory(target),
89
+ cache_logger_on_first_use=False,
90
+ )
91
+
92
+
93
+ def configure_from_environ(stream: Any = None) -> LogSettings:
94
+ """Load ``TYPEVET_LOG__*`` settings and configure stderr diagnostics once.
95
+
96
+ Returns:
97
+ The settings that were applied.
98
+ """
99
+ settings = load_log_settings()
100
+ configure(settings, stream=stream)
101
+ return settings
102
+
103
+
104
+ def new_run_id() -> str:
105
+ """Return a short hex id that joins lines for one invocation.
106
+
107
+ Returns:
108
+ Twelve lowercase hex characters from a random UUID.
109
+ """
110
+ return uuid.uuid4().hex[:12]
111
+
112
+
113
+ def bind_run_id(run_id: str) -> None:
114
+ """Bind ``run_id`` on every line until the context is cleared or replaced."""
115
+ structlog.contextvars.bind_contextvars(run_id=run_id)
116
+ set_run_id_context(run_id)
@@ -0,0 +1,94 @@
1
+ """Redact secrets and optional prompt content before stderr rendering.
2
+
3
+ Examples:
4
+ ```python
5
+ from typevet.adapters.diagnostics.redaction import REDACTED, redact
6
+
7
+ assert redact(None, "info", {"api_key": "k"})["api_key"] == REDACTED
8
+ ```
9
+
10
+ See Also:
11
+ - [typevet.adapters.diagnostics.logs][]: Installs the redact processor on configure
12
+ - [typevet.adapters.diagnostics.settings][]: ``log_prompts`` policy for prompt keys
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Callable
18
+ from typing import Any
19
+
20
+ REDACTED = "***"
21
+ SECRET_KEYS = frozenset(
22
+ {
23
+ "api_key",
24
+ "authorization",
25
+ "password",
26
+ "private_key",
27
+ "private_key_pem",
28
+ "token",
29
+ }
30
+ )
31
+ PROMPT_KEYS = frozenset(
32
+ {
33
+ "content",
34
+ "messages",
35
+ "prompt",
36
+ "raw_text",
37
+ "system",
38
+ "user",
39
+ }
40
+ )
41
+ _SCALARS = (bool, int, float)
42
+ _EXC_INFO = "exc_info"
43
+
44
+
45
+ def _is_pem(value: str) -> bool:
46
+ return "-----BEGIN" in value
47
+
48
+
49
+ def _redact_pair(key: Any, value: Any, *, log_prompts: bool) -> Any:
50
+ if isinstance(key, str):
51
+ lowered = key.lower()
52
+ if lowered in {k.lower() for k in SECRET_KEYS}:
53
+ return REDACTED
54
+ if not log_prompts and lowered in {k.lower() for k in PROMPT_KEYS}:
55
+ return REDACTED
56
+ return _mask(value, log_prompts=log_prompts)
57
+
58
+
59
+ def _mask(value: Any, *, log_prompts: bool) -> Any:
60
+ if isinstance(value, str):
61
+ return REDACTED if _is_pem(value) else value
62
+ if value is None or isinstance(value, _SCALARS):
63
+ return value
64
+ if isinstance(value, dict):
65
+ return {
66
+ k: _redact_pair(k, v, log_prompts=log_prompts) for k, v in value.items()
67
+ }
68
+ if isinstance(value, (list, tuple, set, frozenset)):
69
+ return [_mask(item, log_prompts=log_prompts) for item in value]
70
+ return type(value).__name__
71
+
72
+
73
+ def make_redact_processor(
74
+ *, log_prompts: bool
75
+ ) -> Callable[[Any, str, dict[str, Any]], dict[str, Any]]:
76
+ """Build a structlog processor that masks secrets and optional prompts.
77
+
78
+ Args:
79
+ log_prompts: When false, prompt-like field names and long strings
80
+ are replaced with ``REDACTED``.
81
+
82
+ Returns:
83
+ A structlog-compatible processor callable.
84
+ """
85
+
86
+ def _redact_event(
87
+ _logger: Any, _method: str, event_dict: dict[str, Any]
88
+ ) -> dict[str, Any]:
89
+ return {
90
+ k: v if k == _EXC_INFO else _redact_pair(k, v, log_prompts=log_prompts)
91
+ for k, v in event_dict.items()
92
+ }
93
+
94
+ return _redact_event