open-code-review-toolkit 0.8.0__tar.gz → 0.8.2__tar.gz

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 (111) hide show
  1. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/PKG-INFO +2 -1
  2. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/README.md +1 -0
  3. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/_version.py +2 -2
  4. open_code_review_toolkit-0.8.2/src/ocr_toolkit/configure.py +96 -0
  5. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/result.py +8 -6
  6. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/workflow.py +95 -47
  7. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/preflight.py +21 -68
  8. open_code_review_toolkit-0.8.2/src/ocr_toolkit/provider_config.py +296 -0
  9. open_code_review_toolkit-0.8.2/src/ocr_toolkit/provider_failure.py +247 -0
  10. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/review_runner.py +38 -8
  11. open_code_review_toolkit-0.8.0/src/ocr_toolkit/configure.py +0 -188
  12. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/.gitignore +0 -0
  13. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/LICENSE +0 -0
  14. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/pyproject.toml +0 -0
  15. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/__init__.py +0 -0
  16. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/cli.py +0 -0
  17. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/common/__init__.py +0 -0
  18. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/common/filesystem.py +0 -0
  19. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/common/git.py +0 -0
  20. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/common/language.py +0 -0
  21. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/common/markdown.py +0 -0
  22. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/common/redaction.py +0 -0
  23. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/config_writer.py +0 -0
  24. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/context/__init__.py +0 -0
  25. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/context/adapters.py +0 -0
  26. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/context/broker.py +0 -0
  27. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/context/contracts.py +0 -0
  28. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/context/dlp.py +0 -0
  29. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/context/mcp.py +0 -0
  30. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/context/policy.py +0 -0
  31. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/context/recognizers.py +0 -0
  32. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/context/store.py +0 -0
  33. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/__init__.py +0 -0
  34. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/__main__.py +0 -0
  35. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/actions.py +0 -0
  36. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/artifacts.py +0 -0
  37. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/categorize.py +0 -0
  38. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/collect.py +0 -0
  39. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/collectors/__init__.py +0 -0
  40. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/collectors/graphs.py +0 -0
  41. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/collectors/orchestration.py +0 -0
  42. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/collectors/projections.py +0 -0
  43. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/collectors/registry.py +0 -0
  44. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/collectors/sources.py +0 -0
  45. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/coverage.py +0 -0
  46. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/ecosystems/__init__.py +0 -0
  47. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +0 -0
  48. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  49. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  50. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  51. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/ecosystems/go.py +0 -0
  52. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/ecosystems/javascript.py +0 -0
  53. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/ecosystems/php.py +0 -0
  54. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/ecosystems/python.py +0 -0
  55. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/frameworks/__init__.py +0 -0
  56. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/frameworks/contracts.py +0 -0
  57. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/frameworks/detection.py +0 -0
  58. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +0 -0
  59. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +0 -0
  60. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/frameworks/providers/go.py +0 -0
  61. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/frameworks/providers/php.py +0 -0
  62. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/frameworks/providers/python.py +0 -0
  63. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/frameworks/registry.py +0 -0
  64. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/frameworks/schema.py +0 -0
  65. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/frameworks/templates.py +0 -0
  66. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/infrastructure.py +0 -0
  67. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/invocation.py +0 -0
  68. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/mcp.py +0 -0
  69. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/model.py +0 -0
  70. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/policy/__init__.py +0 -0
  71. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/policy/contracts.py +0 -0
  72. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/policy/decisions.py +0 -0
  73. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/policy/guidance.py +0 -0
  74. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/policy/registry.py +0 -0
  75. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/policy/schema.py +0 -0
  76. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/policy/scopes.py +0 -0
  77. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/project.py +0 -0
  78. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/repository.py +0 -0
  79. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/review_context.py +0 -0
  80. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/store/__init__.py +0 -0
  81. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/store/atomic.py +0 -0
  82. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/store/contracts.py +0 -0
  83. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/store/core.py +0 -0
  84. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/store/readback.py +0 -0
  85. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/evidence/store/values.py +0 -0
  86. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/mcp_config.py +0 -0
  87. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/ocr_result.py +0 -0
  88. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/__init__.py +0 -0
  89. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/__main__.py +0 -0
  90. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/approval.py +0 -0
  91. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/comments.py +0 -0
  92. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/formatting.py +0 -0
  93. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/gitlab.py +0 -0
  94. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
  95. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/markers.py +0 -0
  96. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/payloads.py +0 -0
  97. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/reconciliation.py +0 -0
  98. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/settings.py +0 -0
  99. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/snapshot.py +0 -0
  100. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/suggestions.py +0 -0
  101. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/posting/transaction.py +0 -0
  102. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/pre_execution.py +0 -0
  103. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/providers/__init__.py +0 -0
  104. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/providers/gitlab.py +0 -0
  105. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/providers/gitlab_context.py +0 -0
  106. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/providers/gitlab_discussions.py +0 -0
  107. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/providers/gitlab_identity.py +0 -0
  108. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/providers/gitlab_remediation.py +0 -0
  109. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/py.typed +0 -0
  110. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/result_contract.py +0 -0
  111. {open_code_review_toolkit-0.8.0 → open_code_review_toolkit-0.8.2}/src/ocr_toolkit/result_usage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-code-review-toolkit
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: Unofficial GitLab CI integration layer for Open Code Review
5
5
  Project-URL: Homepage, https://github.com/xeonvs/open-code-review-toolkit
6
6
  Project-URL: Repository, https://github.com/xeonvs/open-code-review-toolkit
@@ -249,6 +249,7 @@ ocr-ci --help
249
249
  The exact recommended OCR release and its verified asset checksums live in the [versioned compatibility manifest](compatibility/ocr-support.json). CI should pin that release and checksum before execution.
250
250
  The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
251
251
  Review output defaults to English. `OCR_REVIEW_LANGUAGE` accepts another explicit language name when a project needs localized review output; for example, `OCR_REVIEW_LANGUAGE=Russian`.
252
+ The current OCR 1.10.0 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries.
252
253
 
253
254
  Stable distributions are published to [PyPI](https://pypi.org/project/open-code-review-toolkit/) and mirrored as checksum-listed, provenance-attested assets in the corresponding [GitHub Release](https://github.com/xeonvs/open-code-review-toolkit/releases). Development snapshots are published only to TestPyPI.
254
255
 
@@ -22,6 +22,7 @@ ocr-ci --help
22
22
  The exact recommended OCR release and its verified asset checksums live in the [versioned compatibility manifest](compatibility/ocr-support.json). CI should pin that release and checksum before execution.
23
23
  The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
24
24
  Review output defaults to English. `OCR_REVIEW_LANGUAGE` accepts another explicit language name when a project needs localized review output; for example, `OCR_REVIEW_LANGUAGE=Russian`.
25
+ The current OCR 1.10.0 integration defaults `OCR_REVIEW_EFFORT` to `medium` for two review rounds. `low` and `high` are explicit one- and three-round alternatives; see the [configuration reference](docs/configuration.md#review-effort) for cost, budget, and precedence boundaries.
25
26
 
26
27
  Stable distributions are published to [PyPI](https://pypi.org/project/open-code-review-toolkit/) and mirrored as checksum-listed, provenance-attested assets in the corresponding [GitHub Release](https://github.com/xeonvs/open-code-review-toolkit/releases). Development snapshots are published only to TestPyPI.
27
28
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.8.0'
22
- __version_tuple__ = version_tuple = (0, 8, 0)
21
+ __version__ = version = '0.8.2'
22
+ __version_tuple__ = version_tuple = (0, 8, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,96 @@
1
+ """Configure OCR runtime settings from CI environment variables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from typing import Any
8
+
9
+ from ocr_toolkit.common.language import resolve_review_language
10
+ from ocr_toolkit.common.redaction import redact_sensitive
11
+ from ocr_toolkit.config_writer import OCRConfigError, update_ocr_config
12
+ from ocr_toolkit.provider_config import (
13
+ ProviderConfigError,
14
+ provider_config_from_environment,
15
+ )
16
+
17
+
18
+ class OCRRuntimeConfigError(Exception):
19
+ """OCR runtime config from CI env is invalid."""
20
+
21
+
22
+ REVIEW_EFFORTS = frozenset({"low", "medium", "high"})
23
+
24
+
25
+ def _env(name: str, default: str = "") -> str:
26
+ return os.environ.get(name, default).strip()
27
+
28
+
29
+ def _bool_env(name: str) -> bool:
30
+ return _env(name).lower() == "true"
31
+
32
+
33
+ def review_effort() -> str:
34
+ """Return the closed OCR review-effort preset, defaulting to medium."""
35
+
36
+ value = (_env("OCR_REVIEW_EFFORT") or "medium").lower()
37
+ if value not in REVIEW_EFFORTS:
38
+ raise OCRRuntimeConfigError("OCR_REVIEW_EFFORT must be one of: low, medium, high")
39
+ return value
40
+
41
+
42
+ def build_config_updates() -> dict[str, Any]:
43
+ """Build OCR config updates from already-normalized CI environment."""
44
+
45
+ review_language = resolve_review_language()
46
+ try:
47
+ provider = provider_config_from_environment()
48
+ llm_url = provider.require_inference_url()
49
+ llm_token = provider.require_token()
50
+ llm_model = provider.require_model()
51
+ except ProviderConfigError as exc:
52
+ raise OCRRuntimeConfigError(str(exc)) from exc
53
+ request_controls = provider.request_controls
54
+
55
+ updates: dict[str, Any] = {
56
+ "effort": review_effort(),
57
+ "language": review_language,
58
+ "llm.url": llm_url,
59
+ "llm.auth_token": llm_token,
60
+ "llm.model": llm_model,
61
+ "llm.protocol": request_controls.protocol,
62
+ "llm.use_anthropic": request_controls.protocol == "anthropic",
63
+ "llm.auth_header": request_controls.auth_header,
64
+ "telemetry.enabled": _bool_env("OCR_TELEMETRY_ENABLED"),
65
+ "telemetry.content_logging": _bool_env("OCR_TELEMETRY_CONTENT_LOGGING"),
66
+ }
67
+
68
+ if request_controls.extra_headers:
69
+ updates["llm.extra_headers"] = request_controls.extra_headers
70
+
71
+ if request_controls.extra_body is not None:
72
+ updates["llm.extra_body"] = request_controls.extra_body
73
+
74
+ if _bool_env("OCR_TELEMETRY_ENABLED"):
75
+ updates["telemetry.exporter"] = _env("OCR_TELEMETRY_EXPORTER")
76
+ otlp_endpoint = _env("OCR_TELEMETRY_OTLP_ENDPOINT")
77
+ if otlp_endpoint:
78
+ updates["telemetry.otlp_endpoint"] = otlp_endpoint
79
+
80
+ return updates
81
+
82
+
83
+ def main() -> int:
84
+ """Write OCR config and return a process exit code."""
85
+
86
+ try:
87
+ update_ocr_config(build_config_updates())
88
+ except (OCRRuntimeConfigError, OCRConfigError) as exc:
89
+ print(f"Failed to configure OCR runtime: {redact_sensitive(str(exc))}", file=sys.stderr)
90
+ return 1
91
+ print("OCR runtime config written")
92
+ return 0
93
+
94
+
95
+ if __name__ == "__main__":
96
+ raise SystemExit(main())
@@ -17,13 +17,14 @@ from ocr_toolkit.ocr_result import (
17
17
  load_ocr_result,
18
18
  )
19
19
  from ocr_toolkit.posting.comments import clean_text, compact_escaped_text
20
+ from ocr_toolkit.provider_failure import ProviderFailureReason
20
21
  from ocr_toolkit.result_contract import ReviewOutcome
21
22
 
22
23
  __all__ = [
23
24
  "OcrResultMalformed",
24
25
  "OcrResultMissing",
25
26
  "OcrResultTooLarge",
26
- "llm_billing_failure_warnings",
27
+ "llm_billing_failure_reason",
27
28
  "load_ocr_result",
28
29
  "normalize_coverage_diagnostics",
29
30
  "ocr_warning_text",
@@ -199,12 +200,13 @@ def ocr_warning_text(warning: Any, *, _seen: set[int] | None = None) -> str:
199
200
  return clean_text(warning)
200
201
 
201
202
 
202
- def llm_billing_failure_warnings(warnings: Sequence[Any]) -> list[str]:
203
- """Return OCR warnings that indicate LLM provider billing/quota failure."""
203
+ def llm_billing_failure_reason(
204
+ warnings: Sequence[Any],
205
+ ) -> ProviderFailureReason | None:
206
+ """Map a legacy OCR billing warning to the shared safe provider reason."""
204
207
 
205
- matches: list[str] = []
206
208
  for warning in warnings:
207
209
  text = ocr_warning_text(warning)
208
210
  if text and LLM_BILLING_FAILURE_RE.search(text):
209
- matches.append(text)
210
- return matches
211
+ return ProviderFailureReason.RATE_OR_SPENDING_LIMIT
212
+ return None
@@ -15,7 +15,6 @@ from typing import Any
15
15
 
16
16
  from ocr_toolkit.common.git import isolated_git_environment, read_only_git_prefix
17
17
  from ocr_toolkit.common.markdown import markdown_code_block, neutralize_quick_actions
18
- from ocr_toolkit.common.redaction import redact_sensitive
19
18
  from ocr_toolkit.evidence.artifacts import repository_artifacts
20
19
  from ocr_toolkit.ocr_result import (
21
20
  TOOLKIT_RESULT_KEY,
@@ -37,7 +36,6 @@ from ocr_toolkit.posting.comments import (
37
36
  clean_text,
38
37
  code_text,
39
38
  comment_line,
40
- compact_escaped_text,
41
39
  )
42
40
  from ocr_toolkit.posting.formatting import (
43
41
  format_fallback_comment_chunks,
@@ -71,12 +69,11 @@ from ocr_toolkit.posting.markers import (
71
69
  )
72
70
  from ocr_toolkit.posting.payloads import build_marked_note_body
73
71
  from ocr_toolkit.posting.result import (
74
- llm_billing_failure_warnings,
72
+ llm_billing_failure_reason,
75
73
  normalize_coverage_diagnostics,
76
74
  )
77
75
  from ocr_toolkit.posting.snapshot import (
78
76
  BotCommentRefs,
79
- cleanup_drafts_created_by_this_run,
80
77
  collect_previous_bot_comment_refs,
81
78
  delete_previous_bot_comments_if_collected,
82
79
  delete_previous_setup_notes,
@@ -101,6 +98,10 @@ from ocr_toolkit.pre_execution import (
101
98
  PreExecutionStatusError,
102
99
  read_pre_execution_status,
103
100
  )
101
+ from ocr_toolkit.provider_failure import (
102
+ ProviderFailureReason,
103
+ provider_failure_reason,
104
+ )
104
105
  from ocr_toolkit.result_contract import OcrResultContractError, ReviewOutcome, parse_result_outcome
105
106
 
106
107
  # Kept as a module-level compatibility seam for tests and external monkey-patching.
@@ -662,19 +663,13 @@ def post_results(config: GitLabConfig, result: dict[str, Any]) -> int:
662
663
  token_usage_summary=token_usage_summary,
663
664
  )
664
665
 
665
- billing_warnings = llm_billing_failure_warnings(warnings)
666
- if billing_warnings:
666
+ billing_reason = llm_billing_failure_reason(warnings)
667
+ if billing_reason is not None:
667
668
  print(
668
669
  "OCR reported an LLM provider billing/quota failure; refusing to publish normal review notes.",
669
670
  file=sys.stderr,
670
671
  )
671
- return post_llm_provider_failure(
672
- config,
673
- billing_warnings,
674
- tool_calls_summary=tool_calls_summary,
675
- mcp_usage_summary=mcp_usage_summary,
676
- token_usage_summary=token_usage_summary,
677
- )
672
+ return post_llm_provider_failure(config, billing_reason)
678
673
 
679
674
  previous_bot_comment_refs = collect_previous_bot_comment_refs(config)
680
675
  transaction = PostingTransaction()
@@ -1101,38 +1096,75 @@ def post_manifest_failure(
1101
1096
 
1102
1097
  def post_llm_provider_failure(
1103
1098
  config: GitLabConfig,
1104
- warnings: Sequence[str],
1105
- tool_calls_summary: str = "",
1106
- mcp_usage_summary: str = "",
1107
- token_usage_summary: str = "",
1099
+ reason: ProviderFailureReason,
1108
1100
  ) -> int:
1109
- """Post a visible OCR provider failure and preserve previous review notes."""
1101
+ """Render one closed provider failure without publishing private diagnostics."""
1110
1102
 
1111
1103
  transaction = PostingTransaction()
1112
- warning_items: list[str] = []
1113
- for warning in warnings[:10]:
1114
- safe_warning = compact_escaped_text(
1115
- neutralize_quick_actions(redact_sensitive(warning)),
1116
- 1200,
1117
- )
1118
- if safe_warning:
1119
- warning_items.append(f"- {safe_warning}")
1104
+ summary, remediation = {
1105
+ ProviderFailureReason.AUTHENTICATION: (
1106
+ "The LLM provider did not accept the configured credential.",
1107
+ "Check the protected credential and its authentication header, then rerun the pipeline.",
1108
+ ),
1109
+ ProviderFailureReason.AUTHORIZATION: (
1110
+ "The LLM provider authenticated the request but did not authorize it.",
1111
+ "Check the credential permissions and provider access policy, then rerun the pipeline.",
1112
+ ),
1113
+ ProviderFailureReason.RATE_OR_SPENDING_LIMIT: (
1114
+ "The LLM provider rejected the request under a rate or spending limit.",
1115
+ "The cause may be ordinary throttling, an account or API-key spending limit, or cost reservation from the requested output cap. Retry later and check account limits. If short probes pass but a full review fails before generation, try an explicit `OCR_LLM_MAX_COMPLETION_TOKENS`, for example `4096`.",
1116
+ ),
1117
+ ProviderFailureReason.OVERLOADED: (
1118
+ "The LLM provider reported that it was overloaded.",
1119
+ "Retry later or use another already-qualified provider deployment.",
1120
+ ),
1121
+ ProviderFailureReason.TIMEOUT: (
1122
+ "The LLM provider request timed out.",
1123
+ "Check provider latency and the network path, then rerun the pipeline.",
1124
+ ),
1125
+ ProviderFailureReason.NETWORK: (
1126
+ "The LLM provider could not be reached reliably.",
1127
+ "Check DNS, TLS, proxy, and runner connectivity, then rerun the pipeline.",
1128
+ ),
1129
+ ProviderFailureReason.ENDPOINT_OR_MODEL_NOT_FOUND: (
1130
+ "The LLM provider reported that the configured endpoint or model was not found.",
1131
+ "Verify both the provider API root and model identifier; the safe diagnostic cannot distinguish which one was absent.",
1132
+ ),
1133
+ ProviderFailureReason.REQUEST_REJECTED: (
1134
+ "The LLM provider rejected the review request.",
1135
+ "Check the explicit protocol, endpoint shape, model contract, and request controls, then rerun the pipeline.",
1136
+ ),
1137
+ ProviderFailureReason.PROVIDER_UNAVAILABLE: (
1138
+ "The LLM provider was unavailable while processing the review.",
1139
+ "Retry later and check the provider service status.",
1140
+ ),
1141
+ ProviderFailureReason.INVALID_RESPONSE: (
1142
+ "The LLM provider returned a response that could not be consumed safely.",
1143
+ "Check protocol compatibility and provider response health, then rerun the pipeline.",
1144
+ ),
1145
+ ProviderFailureReason.CANCELLED: (
1146
+ "The LLM provider request was cancelled before the review completed.",
1147
+ "Check pipeline cancellation and deadline signals, then rerun when the job can complete.",
1148
+ ),
1149
+ ProviderFailureReason.MIXED: (
1150
+ "The review encountered more than one provider failure category.",
1151
+ "Inspect the private CI diagnostics, correct every provider-side failure, then rerun the pipeline.",
1152
+ ),
1153
+ ProviderFailureReason.UNKNOWN: (
1154
+ "The LLM provider request failed for an unclassified safe reason.",
1155
+ "Inspect the private CI diagnostics and provider health, then rerun the pipeline.",
1156
+ ),
1157
+ }[reason]
1120
1158
 
1121
1159
  body_parts = [
1122
- "OCR could not complete the review because the LLM provider reported a billing, quota, or balance failure.",
1160
+ summary,
1123
1161
  "",
1162
+ f"- Safe classification: `{reason.value}`.",
1124
1163
  "- Normal review comments were not published.",
1125
1164
  "- Previous OCR review comments were preserved.",
1126
- "- Refill or rotate the LLM token, then rerun the pipeline.",
1165
+ "- Automatic approval was not attempted.",
1166
+ f"- {remediation}",
1127
1167
  ]
1128
- if warning_items:
1129
- body_parts.extend(["", "**Provider warnings:**", *warning_items])
1130
- if tool_calls_summary:
1131
- body_parts.extend(["", tool_calls_summary])
1132
- if mcp_usage_summary:
1133
- body_parts.append(mcp_usage_summary)
1134
- if token_usage_summary:
1135
- body_parts.append(token_usage_summary)
1136
1168
 
1137
1169
  response = post_review_note_bounded(
1138
1170
  config,
@@ -1142,18 +1174,23 @@ def post_llm_provider_failure(
1142
1174
  )
1143
1175
  if response is None:
1144
1176
  print("Failed to create OCR provider-failure note.", file=sys.stderr)
1145
- cleanup_drafts_created_by_this_run(config, transaction)
1146
- print_posting_failure_banner()
1147
- return 1
1177
+ return posting_failure_exit(config, None, transaction)
1148
1178
 
1149
1179
  if not finalize_posting(config, transaction):
1150
1180
  return publish_failure_exit(config, transaction)
1151
1181
 
1152
- print(
1153
- "Open Code Review did not complete because the LLM provider reported a billing/quota failure.",
1154
- file=sys.stderr,
1155
- )
1156
- return 1
1182
+ print(f"Open Code Review provider failure: {reason.value}.", file=sys.stderr)
1183
+ return 1 if strict_posting() else 0
1184
+
1185
+
1186
+ def _result_provider_failure_reason(result_path: Path) -> ProviderFailureReason | None:
1187
+ """Hostile-read one bounded private result and return only its closed reason."""
1188
+
1189
+ try:
1190
+ result = load_ocr_result(result_path)
1191
+ except (OcrResultMalformed, OcrResultMissing, OcrResultTooLarge):
1192
+ return None
1193
+ return provider_failure_reason(result)
1157
1194
 
1158
1195
 
1159
1196
  def post_parse_error(config: GitLabConfig, stderr_path: Path) -> int:
@@ -1194,11 +1231,18 @@ def post_parse_error(config: GitLabConfig, stderr_path: Path) -> int:
1194
1231
  return 1 if strict_posting() else 0
1195
1232
 
1196
1233
 
1197
- def post_ocr_failure(config: GitLabConfig, stderr_path: Path, exit_code: int) -> int:
1234
+ def post_ocr_failure(
1235
+ config: GitLabConfig,
1236
+ stderr_path: Path,
1237
+ exit_code: int,
1238
+ result_path: Path | None = None,
1239
+ ) -> int:
1198
1240
  """Post a safe failure note when OCR exits with a non-zero status.
1199
1241
 
1200
1242
  A non-zero OCR exit code means the JSON output may be partial or misleading,
1201
- so this script intentionally does not publish normal review comments.
1243
+ so this script intentionally does not publish normal review comments. A
1244
+ bounded retry-report v1 may contribute only a closed provider-failure
1245
+ reason; its raw diagnostics and stderr remain private.
1202
1246
  Previous OCR bot notes are intentionally NOT cleaned up here: the last
1203
1247
  valid review must remain visible until a successful run replaces it.
1204
1248
 
@@ -1222,6 +1266,10 @@ def post_ocr_failure(config: GitLabConfig, stderr_path: Path, exit_code: int) ->
1222
1266
  if status is not None:
1223
1267
  return post_pre_execution_status(config, status)
1224
1268
 
1269
+ reason = _result_provider_failure_reason(result_path) if result_path is not None else None
1270
+ if reason is not None:
1271
+ return post_llm_provider_failure(config, reason)
1272
+
1225
1273
  transaction = PostingTransaction()
1226
1274
  details_enabled = os.environ.get("OCR_POST_ERROR_DETAILS") == "1"
1227
1275
  details = read_stderr_excerpt(stderr_path) if details_enabled else ""
@@ -1342,7 +1390,7 @@ def main(argv: list[str] | None = None) -> int:
1342
1390
 
1343
1391
  exit_code = ocr_exit_code()
1344
1392
  if exit_code != 0:
1345
- return post_ocr_failure(config, stderr_path, exit_code)
1393
+ return post_ocr_failure(config, stderr_path, exit_code, result_path)
1346
1394
 
1347
1395
  try:
1348
1396
  result = load_ocr_result(result_path)
@@ -15,16 +15,20 @@ import urllib.request
15
15
  from typing import Any
16
16
 
17
17
  from ocr_toolkit.common.redaction import redact_sensitive
18
+ from ocr_toolkit.provider_config import (
19
+ HEADER_NAME_RE,
20
+ ProviderConfigError,
21
+ provider_config_from_environment,
22
+ )
18
23
 
19
24
  HTTP_TIMEOUT_SECONDS = 30
20
25
  MAX_RESPONSE_BODY_BYTES = 2_000_000
21
26
  RESPONSE_READ_CHUNK_BYTES = 64 * 1024
22
- HEADER_NAME_RE = re.compile(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$")
23
27
  DEFAULT_REQUEST_HEADERS = {
24
28
  "Accept": "application/json",
25
29
  "User-Agent": "open-code-review-ci-preflight/1.0",
26
30
  }
27
- EXPECTED_OCR_VERSION = "1.9.10"
31
+ EXPECTED_OCR_VERSION = "1.10.0"
28
32
 
29
33
 
30
34
  class PreflightError(Exception):
@@ -209,63 +213,10 @@ def validate_ocr_binary() -> None:
209
213
 
210
214
 
211
215
  def _models_url() -> str:
212
- explicit_url = _env("OCR_LLM_MODELS_URL")
213
- if explicit_url:
214
- return explicit_url
215
-
216
- base = ""
217
- llm_url = _env("OCR_LLM_URL")
218
- if not base:
219
- normalized_llm_url = llm_url.rstrip("/")
220
- for endpoint in ("/chat/completions", "/responses"):
221
- if normalized_llm_url.endswith(endpoint):
222
- base = normalized_llm_url[: -len(endpoint)]
223
- break
224
- else:
225
- if normalized_llm_url.endswith("/v1"):
226
- base = normalized_llm_url
227
-
228
- if not base:
229
- raise PreflightError(
230
- "Cannot derive LLM /models URL; set OCR_LLM_MODELS_URL or "
231
- "set OCR_LLM_VALIDATE_MODEL=false"
232
- )
233
- return f"{base.rstrip('/')}/models"
234
-
235
-
236
- def _parse_extra_headers(value: str) -> dict[str, str]:
237
- """Parse optional OCR LLM extra headers JSON for preflight metadata calls."""
238
-
239
- if not value.strip():
240
- return {}
241
216
  try:
242
- payload = json.loads(value)
243
- except json.JSONDecodeError as exc:
244
- raise PreflightError("OCR_LLM_EXTRA_HEADERS must be a JSON object") from exc
245
- if not isinstance(payload, dict):
246
- raise PreflightError("OCR_LLM_EXTRA_HEADERS must be a JSON object")
247
-
248
- headers: dict[str, str] = {}
249
- for key, header_value in payload.items():
250
- if not isinstance(key, str) or not HEADER_NAME_RE.fullmatch(key.strip()):
251
- raise PreflightError("OCR_LLM_EXTRA_HEADERS contains an invalid header name")
252
- if not isinstance(header_value, str):
253
- raise PreflightError("OCR_LLM_EXTRA_HEADERS contains a non-string header value")
254
- if "\r" in header_value or "\n" in header_value:
255
- raise PreflightError("OCR_LLM_EXTRA_HEADERS contains an invalid header value")
256
- headers[key.strip()] = header_value
257
- return headers
258
-
259
-
260
- def _llm_headers(token: str) -> dict[str, str]:
261
- """Build the same auth/header set used by OCR's llm.* configuration."""
262
-
263
- auth_header = _env("OCR_LLM_AUTH_HEADER", "Authorization") or "Authorization"
264
- if not HEADER_NAME_RE.fullmatch(auth_header):
265
- raise PreflightError("OCR_LLM_AUTH_HEADER is not a valid HTTP header name")
266
- headers = _parse_extra_headers(_env("OCR_LLM_EXTRA_HEADERS"))
267
- headers[auth_header] = f"Bearer {token}"
268
- return headers
217
+ return provider_config_from_environment().require_models_url()
218
+ except ProviderConfigError as exc:
219
+ raise PreflightError(str(exc)) from exc
269
220
 
270
221
 
271
222
  def _context_length(model: dict[str, Any]) -> int:
@@ -320,9 +271,11 @@ def _validate_allowed_model(model_id: str) -> bool:
320
271
  def validate_llm_model() -> None:
321
272
  """Verify the selected model exists in OpenAI-compatible metadata."""
322
273
 
323
- model_id = _env("OCR_LLM_MODEL")
324
- if not model_id:
325
- raise PreflightError("OCR_LLM_MODEL is required")
274
+ try:
275
+ provider = provider_config_from_environment()
276
+ model_id = provider.require_model()
277
+ except ProviderConfigError as exc:
278
+ raise PreflightError(str(exc)) from exc
326
279
  allowlist_matched = _validate_allowed_model(model_id)
327
280
 
328
281
  validate_mode = _env("OCR_LLM_VALIDATE_MODEL", "false").lower()
@@ -332,15 +285,15 @@ def validate_llm_model() -> None:
332
285
  if validate_mode not in {"true", "1", "yes", "on", "auto"}:
333
286
  raise PreflightError("OCR_LLM_VALIDATE_MODEL must be true, false, or auto")
334
287
 
335
- token = _env("OCR_LLM_TOKEN")
336
- if not token:
337
- raise PreflightError("OCR_LLM_TOKEN is required")
338
- headers = _llm_headers(token)
288
+ try:
289
+ headers = provider.request_headers()
290
+ except ProviderConfigError as exc:
291
+ raise PreflightError(str(exc)) from exc
339
292
 
340
293
  print("Validating OCR model against LLM gateway metadata")
341
294
  try:
342
- models_url = _models_url()
343
- except PreflightError as exc:
295
+ models_url = provider.require_models_url()
296
+ except ProviderConfigError as exc:
344
297
  if validate_mode == "auto" and allowlist_matched:
345
298
  print(
346
299
  "OCR LLM /models URL unavailable; continuing because "
@@ -348,7 +301,7 @@ def validate_llm_model() -> None:
348
301
  file=sys.stderr,
349
302
  )
350
303
  return
351
- raise
304
+ raise PreflightError(str(exc)) from exc
352
305
  try:
353
306
  payload = _request_json(models_url, headers)
354
307
  except PreflightError as exc: