code-mower 0.5.0b5__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.
- code_mower/__init__.py +3 -0
- code_mower/adapters/__init__.py +39 -0
- code_mower/adapters/_base.py +148 -0
- code_mower/adapters/cursor_bugbot.py +97 -0
- code_mower/adapters/gitar.py +111 -0
- code_mower/adapters/greptile.py +191 -0
- code_mower/adapters/qodo.py +140 -0
- code_mower/antigravity_cli_audit_pr.py +241 -0
- code_mower/audit_handoff_log.py +345 -0
- code_mower/audit_labeler_lib.py +311 -0
- code_mower/audit_progress.py +203 -0
- code_mower/blind_review_artifacts.py +562 -0
- code_mower/blind_review_coordinator.py +276 -0
- code_mower/bootstrap.py +524 -0
- code_mower/builder_experiment.py +539 -0
- code_mower/calibration/__init__.py +181 -0
- code_mower/calibration/arms.py +267 -0
- code_mower/calibration/auto_discovery.py +348 -0
- code_mower/calibration/commands.py +192 -0
- code_mower/calibration/context_inputs.py +199 -0
- code_mower/calibration/corpus.py +91 -0
- code_mower/calibration/evidence.py +20 -0
- code_mower/calibration/evidence_report.py +360 -0
- code_mower/calibration/identity.py +27 -0
- code_mower/calibration/metrics.py +18 -0
- code_mower/calibration/overlap.py +103 -0
- code_mower/calibration/planning.py +309 -0
- code_mower/calibration/policy.py +207 -0
- code_mower/calibration/results.py +315 -0
- code_mower/calibration/run_results.py +147 -0
- code_mower/calibration/run_status.py +64 -0
- code_mower/calibration/runner.py +360 -0
- code_mower/calibration/truth.py +188 -0
- code_mower/calibration/value_report.py +142 -0
- code_mower/checks.py +402 -0
- code_mower/claude_audit_pr.py +1126 -0
- code_mower/claude_cli_bounce.py +303 -0
- code_mower/claude_cli_environment.py +73 -0
- code_mower/clear_stale.py +374 -0
- code_mower/cli.py +537 -0
- code_mower/cloud.py +674 -0
- code_mower/cloud_client/__init__.py +173 -0
- code_mower/cloud_client/bundle.py +155 -0
- code_mower/cloud_client/doctor.py +206 -0
- code_mower/cloud_client/dogfood.py +78 -0
- code_mower/cloud_client/endpoints.py +114 -0
- code_mower/cloud_client/errors.py +7 -0
- code_mower/cloud_client/events.py +278 -0
- code_mower/cloud_client/export.py +272 -0
- code_mower/cloud_client/git_metadata.py +46 -0
- code_mower/cloud_client/manifest.py +39 -0
- code_mower/cloud_client/operations.py +448 -0
- code_mower/cloud_client/reports.py +45 -0
- code_mower/cloud_client/setup.py +205 -0
- code_mower/cloud_client/upload.py +97 -0
- code_mower/code_mower_calibration.py +598 -0
- code_mower/code_mower_context_packs.py +591 -0
- code_mower/code_mower_merge.py +227 -0
- code_mower/code_mower_telemetry.py +561 -0
- code_mower/coderabbit_cli_audit_pr.py +526 -0
- code_mower/codex_audit_env_preflight.py +220 -0
- code_mower/codex_audit_pr.py +1738 -0
- code_mower/codex_audit_schema_smoke.py +160 -0
- code_mower/codex_audit_verdict.schema.json +44 -0
- code_mower/config.py +655 -0
- code_mower/doctor.py +161 -0
- code_mower/doctor_checks/__init__.py +104 -0
- code_mower/doctor_checks/cloud.py +129 -0
- code_mower/doctor_checks/common.py +215 -0
- code_mower/doctor_checks/github.py +128 -0
- code_mower/doctor_checks/github_actions.py +11 -0
- code_mower/doctor_checks/github_actions_cost.py +99 -0
- code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
- code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
- code_mower/doctor_checks/github_actions_failure_models.py +47 -0
- code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
- code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
- code_mower/doctor_checks/github_actions_failures.py +103 -0
- code_mower/doctor_checks/github_actions_permissions.py +55 -0
- code_mower/doctor_checks/github_api.py +79 -0
- code_mower/doctor_checks/github_branch.py +56 -0
- code_mower/doctor_checks/github_config.py +25 -0
- code_mower/doctor_checks/github_provider.py +61 -0
- code_mower/doctor_checks/github_repo.py +120 -0
- code_mower/doctor_checks/groups.py +36 -0
- code_mower/doctor_checks/models.py +96 -0
- code_mower/doctor_checks/output.py +86 -0
- code_mower/doctor_checks/presets.py +64 -0
- code_mower/doctor_checks/privacy.py +20 -0
- code_mower/doctor_checks/provider_api_model.py +138 -0
- code_mower/doctor_checks/provider_api_model_openai.py +29 -0
- code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
- code_mower/doctor_checks/provider_env.py +113 -0
- code_mower/doctor_checks/provider_env_required.py +56 -0
- code_mower/doctor_checks/provider_env_tokens.py +100 -0
- code_mower/doctor_checks/provider_local_cli.py +162 -0
- code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
- code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
- code_mower/doctor_checks/provider_probe.py +20 -0
- code_mower/doctor_checks/provider_probe_auth.py +52 -0
- code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
- code_mower/doctor_checks/provider_probe_json.py +45 -0
- code_mower/doctor_checks/provider_probe_remediation.py +39 -0
- code_mower/doctor_checks/providers.py +159 -0
- code_mower/doctor_checks/registry.py +69 -0
- code_mower/doctor_checks/runner.py +188 -0
- code_mower/doctor_checks/runtime.py +89 -0
- code_mower/doctor_checks/runtime_github_auth.py +148 -0
- code_mower/gemini_cli_audit_pr.py +897 -0
- code_mower/hermes_cli_audit_pr.py +436 -0
- code_mower/init.py +888 -0
- code_mower/lane_configs/__init__.py +37 -0
- code_mower/lane_configs/aider.py +32 -0
- code_mower/lane_configs/antigravity_cli.py +35 -0
- code_mower/lane_configs/claude.py +35 -0
- code_mower/lane_configs/codex.py +32 -0
- code_mower/lane_configs/devin.py +33 -0
- code_mower/lane_configs/gemini_cli.py +35 -0
- code_mower/lane_configs/hermes_cli.py +35 -0
- code_mower/lane_configs/local_llm.py +31 -0
- code_mower/local_llm_audit_pr.py +1364 -0
- code_mower/local_llm_bakeoff.py +458 -0
- code_mower/local_llm_calibration.py +441 -0
- code_mower/local_llm_profiles.py +66 -0
- code_mower/migration.py +508 -0
- code_mower/migration_install.py +292 -0
- code_mower/migration_mirror.py +392 -0
- code_mower/migration_readiness.py +237 -0
- code_mower/migration_rehearsal.py +718 -0
- code_mower/next_steps.py +441 -0
- code_mower/package.py +673 -0
- code_mower/package_content.py +444 -0
- code_mower/package_manifest.py +452 -0
- code_mower/package_paths.py +53 -0
- code_mower/package_rendering.py +90 -0
- code_mower/package_static.py +585 -0
- code_mower/prompts.py +267 -0
- code_mower/provider_registry.py +469 -0
- code_mower/provider_runners/__init__.py +60 -0
- code_mower/provider_runners/comments.py +31 -0
- code_mower/provider_runners/git.py +46 -0
- code_mower/provider_runners/github_auth.py +61 -0
- code_mower/provider_runners/github_pr.py +120 -0
- code_mower/provider_runners/process.py +58 -0
- code_mower/provider_runners/repo_paths.py +23 -0
- code_mower/provider_runners/text_schema.py +41 -0
- code_mower/provider_runners/verdict_artifacts.py +103 -0
- code_mower/provider_runners/workspace.py +57 -0
- code_mower/release_readiness.py +549 -0
- code_mower/reviewer_metrics.py +389 -0
- code_mower/saas_reviewer_labeler.py +809 -0
- code_mower/secrets.py +89 -0
- code_mower/templates/builder-experiment.example.json +55 -0
- code_mower/templates/calibration-corpus.example.json +129 -0
- code_mower/templates/calibration-corpus.json +129 -0
- code_mower/templates/code-mower.example.yml +423 -0
- code_mower/templates/context-packs.example.json +150 -0
- code_mower/templates/lane_prompts/base-audit.md +22 -0
- code_mower/templates/lane_prompts/calibration-policy.md +21 -0
- code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
- code_mower/templates/lane_prompts/docs-design.md +12 -0
- code_mower/templates/lane_prompts/generic-programming.md +21 -0
- code_mower/templates/lane_prompts/operability.md +22 -0
- code_mower/templates/lane_prompts/package-runtime.md +12 -0
- code_mower/templates/lane_prompts/security-threat-model.md +22 -0
- code_mower/templates/product-support/code_mower +216 -0
- code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
- code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
- code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
- code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
- code_mower/templates/product-support/safe_gh_comment.py +96 -0
- code_mower/templates/providers.yml +454 -0
- code_mower/templates/reviewer-spend.example.json +28 -0
- code_mower/templates/reviewer-value-report.example.md +20 -0
- code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
- code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
- code_mower/trailer_comment_labeler.py +207 -0
- code_mower/versioning.py +32 -0
- code_mower-0.5.0b5.dist-info/METADATA +302 -0
- code_mower-0.5.0b5.dist-info/RECORD +185 -0
- code_mower-0.5.0b5.dist-info/WHEEL +5 -0
- code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
- code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
- code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
- code_mower-0.5.0b5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Human-readable doctor output rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import OrderedDict
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
from .groups import GROUP_LABELS, doctor_check_group_id
|
|
9
|
+
from .models import STATUS_FAIL, STATUS_SKIP, STATUS_WARN, DoctorCheck
|
|
10
|
+
from .models import DoctorReport
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def doctor_output_group(check: DoctorCheck) -> str:
|
|
14
|
+
"""Return the human-output group for a doctor check."""
|
|
15
|
+
|
|
16
|
+
return doctor_check_group_id(check.name, check.lane)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _group_checks(checks: Iterable[DoctorCheck]) -> OrderedDict[str, list[DoctorCheck]]:
|
|
20
|
+
grouped: OrderedDict[str, list[DoctorCheck]] = OrderedDict(
|
|
21
|
+
(group_id, []) for group_id in GROUP_LABELS
|
|
22
|
+
)
|
|
23
|
+
for check in checks:
|
|
24
|
+
grouped.setdefault(doctor_output_group(check), []).append(check)
|
|
25
|
+
return grouped
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _format_status_summary(report: DoctorReport) -> str:
|
|
29
|
+
parts = [f"{len(report.checks)} total"]
|
|
30
|
+
if report.failures:
|
|
31
|
+
parts.append(f"{report.failures} failed")
|
|
32
|
+
if report.warnings:
|
|
33
|
+
parts.append(f"{report.warnings} warnings")
|
|
34
|
+
skipped = sum(1 for check in report.checks if check.status == STATUS_SKIP)
|
|
35
|
+
if skipped:
|
|
36
|
+
parts.append(f"{skipped} skipped")
|
|
37
|
+
if len(parts) == 1:
|
|
38
|
+
parts.append("all passing")
|
|
39
|
+
return ", ".join(parts)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _format_check(check: DoctorCheck) -> list[str]:
|
|
43
|
+
lane = f" [{check.lane}]" if check.lane else ""
|
|
44
|
+
lines = [f"- {check.status.upper()} {check.name}{lane}: {check.message}"]
|
|
45
|
+
if check.remediation:
|
|
46
|
+
lines.append(f" remediation: {check.remediation}")
|
|
47
|
+
return lines
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def render_doctor_text(report: DoctorReport) -> str:
|
|
51
|
+
"""Render a doctor report for terminal output."""
|
|
52
|
+
lines = [
|
|
53
|
+
"Code Mower doctor",
|
|
54
|
+
f"Status: {report.status}",
|
|
55
|
+
f"Config: {report.config_path}",
|
|
56
|
+
f"Provider templates: {report.provider_templates_path}",
|
|
57
|
+
]
|
|
58
|
+
if report.profile:
|
|
59
|
+
lines.append(f"Profile: {report.profile}")
|
|
60
|
+
lines.append(f"Checks: {_format_status_summary(report)}")
|
|
61
|
+
lines.append("")
|
|
62
|
+
|
|
63
|
+
if not report.checks:
|
|
64
|
+
lines.append("No checks ran.")
|
|
65
|
+
return "\n".join(lines) + "\n"
|
|
66
|
+
|
|
67
|
+
for group_id, checks in _group_checks(report.checks).items():
|
|
68
|
+
if not checks:
|
|
69
|
+
continue
|
|
70
|
+
failed = sum(1 for check in checks if check.status == STATUS_FAIL)
|
|
71
|
+
warnings = sum(1 for check in checks if check.status == STATUS_WARN)
|
|
72
|
+
summary = []
|
|
73
|
+
if failed:
|
|
74
|
+
summary.append(f"{failed} failed")
|
|
75
|
+
if warnings:
|
|
76
|
+
summary.append(f"{warnings} warnings")
|
|
77
|
+
heading = GROUP_LABELS.get(group_id, group_id.title())
|
|
78
|
+
if summary:
|
|
79
|
+
heading = f"{heading} ({', '.join(summary)})"
|
|
80
|
+
lines.append(heading)
|
|
81
|
+
for check in checks:
|
|
82
|
+
lines.extend(_format_check(check))
|
|
83
|
+
lines.append("")
|
|
84
|
+
if lines[-1] == "":
|
|
85
|
+
lines.pop()
|
|
86
|
+
return "\n".join(lines) + "\n"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""First-run doctor presets and package-aware path resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from argparse import Namespace
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from code_mower import package as code_mower_package
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def resolve_doctor_config_path_for_script(
|
|
12
|
+
config_arg: str,
|
|
13
|
+
*,
|
|
14
|
+
easy: bool = False,
|
|
15
|
+
script_path: Path,
|
|
16
|
+
) -> Path:
|
|
17
|
+
path = Path(config_arg)
|
|
18
|
+
if path.is_file() or config_arg != "code-mower.yml" or not easy:
|
|
19
|
+
return path
|
|
20
|
+
|
|
21
|
+
script_path = script_path.resolve()
|
|
22
|
+
candidates = [
|
|
23
|
+
script_path.parent / "templates" / "code-mower.example.yml",
|
|
24
|
+
script_path.parent.parent / "templates" / "code-mower.example.yml",
|
|
25
|
+
script_path.parents[1] / "code-mower.example.yml",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
for candidate in candidates:
|
|
29
|
+
if candidate.is_file():
|
|
30
|
+
return candidate
|
|
31
|
+
return path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_doctor_config_path(
|
|
35
|
+
config_arg: str,
|
|
36
|
+
*,
|
|
37
|
+
easy: bool = False,
|
|
38
|
+
script_path: Path | None = None,
|
|
39
|
+
) -> Path:
|
|
40
|
+
return resolve_doctor_config_path_for_script(
|
|
41
|
+
config_arg,
|
|
42
|
+
easy=easy,
|
|
43
|
+
script_path=script_path or Path(__file__),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def resolve_doctor_provider_templates_path(path_text: str) -> Path:
|
|
48
|
+
path = Path(path_text)
|
|
49
|
+
if path_text == code_mower_package.DEFAULT_PROVIDER_TEMPLATES and not path.is_absolute():
|
|
50
|
+
project_catalog = Path.cwd() / code_mower_package.DEFAULT_PROVIDER_TEMPLATES
|
|
51
|
+
if project_catalog.exists():
|
|
52
|
+
return project_catalog
|
|
53
|
+
return code_mower_package.resolve_provider_templates_path(path_text)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def apply_first_run_defaults(args: Namespace) -> None:
|
|
57
|
+
if not (getattr(args, "v05", False) or getattr(args, "preflight", False)):
|
|
58
|
+
return
|
|
59
|
+
args.easy = True
|
|
60
|
+
if args.profile is None:
|
|
61
|
+
args.profile = "recommended"
|
|
62
|
+
args.probe_runtime = True
|
|
63
|
+
args.github = True
|
|
64
|
+
args.cloud = True
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Privacy-preserving doctor output helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def auth_probe_output_detail(output: str) -> dict[str, Any]:
|
|
9
|
+
"""Return non-content diagnostics for auth probes.
|
|
10
|
+
|
|
11
|
+
Doctor probes sometimes receive provider auth output containing account
|
|
12
|
+
names, emails, scopes, or token-adjacent diagnostics. Keep only shape
|
|
13
|
+
metadata so reports can explain that output existed without storing it.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
text = output.strip()
|
|
17
|
+
return {
|
|
18
|
+
"output_redacted": bool(text),
|
|
19
|
+
"output_line_count": len(text.splitlines()) if text else 0,
|
|
20
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Provider API-model profile checks and probe orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.error
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
from .common import (
|
|
10
|
+
DoctorCheck,
|
|
11
|
+
STATUS_FAIL,
|
|
12
|
+
STATUS_PASS,
|
|
13
|
+
STATUS_SKIP,
|
|
14
|
+
STATUS_WARN,
|
|
15
|
+
)
|
|
16
|
+
from .provider_api_model_openai import fetch_openai_compatible_models
|
|
17
|
+
from .provider_api_model_profiles import (
|
|
18
|
+
api_model_key,
|
|
19
|
+
local_llm_lane_profiles,
|
|
20
|
+
runtime_profile,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def check_api_model(
|
|
25
|
+
lane_id: str,
|
|
26
|
+
lane: Mapping[str, Any],
|
|
27
|
+
*,
|
|
28
|
+
probe_runtime: bool,
|
|
29
|
+
http_timeout: int,
|
|
30
|
+
) -> list[DoctorCheck]:
|
|
31
|
+
checks: list[DoctorCheck] = []
|
|
32
|
+
try:
|
|
33
|
+
profiles = local_llm_lane_profiles(lane)
|
|
34
|
+
except (TypeError, ValueError) as exc:
|
|
35
|
+
checks.append(
|
|
36
|
+
DoctorCheck(
|
|
37
|
+
name="runtime.api_model.profiles",
|
|
38
|
+
status=STATUS_FAIL,
|
|
39
|
+
lane=lane_id,
|
|
40
|
+
message=str(exc),
|
|
41
|
+
remediation=(
|
|
42
|
+
"Fix provider_config.profiles in code-mower.yml or use a "
|
|
43
|
+
"known packaged local LLM profile id."
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return checks
|
|
48
|
+
if not profiles:
|
|
49
|
+
checks.append(
|
|
50
|
+
DoctorCheck(
|
|
51
|
+
name="runtime.api_model.profiles",
|
|
52
|
+
status=STATUS_WARN,
|
|
53
|
+
lane=lane_id,
|
|
54
|
+
message="no API model profiles configured",
|
|
55
|
+
remediation=(
|
|
56
|
+
"Add provider_config.profiles to this lane or select a "
|
|
57
|
+
"packaged local LLM profile."
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
return checks
|
|
62
|
+
|
|
63
|
+
checks.append(
|
|
64
|
+
DoctorCheck(
|
|
65
|
+
name="runtime.api_model.profiles",
|
|
66
|
+
status=STATUS_PASS,
|
|
67
|
+
lane=lane_id,
|
|
68
|
+
message=f"{len(profiles)} API model profile(s) configured",
|
|
69
|
+
detail={"profiles": [profile.profile_id for profile in profiles]},
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
if not probe_runtime:
|
|
73
|
+
checks.append(
|
|
74
|
+
DoctorCheck(
|
|
75
|
+
name="runtime.api_model.probe",
|
|
76
|
+
status=STATUS_SKIP,
|
|
77
|
+
lane=lane_id,
|
|
78
|
+
message="runtime probing skipped; pass --probe-runtime to query model endpoints",
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return checks
|
|
82
|
+
|
|
83
|
+
for profile in profiles:
|
|
84
|
+
runtime = runtime_profile(lane, profile)
|
|
85
|
+
try:
|
|
86
|
+
models = fetch_openai_compatible_models(
|
|
87
|
+
runtime.api_base,
|
|
88
|
+
api_model_key(lane, profile),
|
|
89
|
+
http_timeout,
|
|
90
|
+
)
|
|
91
|
+
except (OSError, ValueError, urllib.error.URLError, json.JSONDecodeError) as exc:
|
|
92
|
+
checks.append(
|
|
93
|
+
DoctorCheck(
|
|
94
|
+
name="runtime.api_model.probe",
|
|
95
|
+
status=STATUS_WARN,
|
|
96
|
+
lane=lane_id,
|
|
97
|
+
message=f"{runtime.profile_id} probe failed: {exc}",
|
|
98
|
+
detail={
|
|
99
|
+
"profile": runtime.profile_id,
|
|
100
|
+
"api_base": runtime.api_base,
|
|
101
|
+
},
|
|
102
|
+
remediation=(
|
|
103
|
+
"Start the local OpenAI-compatible endpoint, fix api_base "
|
|
104
|
+
"or api key overrides, then rerun doctor --probe-runtime."
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
continue
|
|
109
|
+
expected = runtime.model
|
|
110
|
+
status = STATUS_PASS if expected in models else STATUS_WARN
|
|
111
|
+
message = (
|
|
112
|
+
f"{runtime.profile_id} reports expected model {expected}"
|
|
113
|
+
if status == STATUS_PASS
|
|
114
|
+
else f"{runtime.profile_id} did not report expected model {expected}"
|
|
115
|
+
)
|
|
116
|
+
checks.append(
|
|
117
|
+
DoctorCheck(
|
|
118
|
+
name="runtime.api_model.probe",
|
|
119
|
+
status=status,
|
|
120
|
+
lane=lane_id,
|
|
121
|
+
message=message,
|
|
122
|
+
detail={
|
|
123
|
+
"profile": runtime.profile_id,
|
|
124
|
+
"api_base": runtime.api_base,
|
|
125
|
+
"expected_model": expected,
|
|
126
|
+
"models": models,
|
|
127
|
+
},
|
|
128
|
+
remediation=(
|
|
129
|
+
None
|
|
130
|
+
if status == STATUS_PASS
|
|
131
|
+
else (
|
|
132
|
+
"Update the configured local LLM model or start an endpoint "
|
|
133
|
+
"that serves the expected model."
|
|
134
|
+
)
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return checks
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""OpenAI-compatible provider API-model probes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.request
|
|
7
|
+
from typing import Mapping
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fetch_openai_compatible_models(
|
|
11
|
+
api_base: str,
|
|
12
|
+
api_key: str,
|
|
13
|
+
timeout: int,
|
|
14
|
+
) -> list[str]:
|
|
15
|
+
request = urllib.request.Request(
|
|
16
|
+
f"{api_base.rstrip('/')}/models",
|
|
17
|
+
headers={
|
|
18
|
+
"Authorization": f"Bearer {api_key}",
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
23
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
24
|
+
data = payload.get("data", []) if isinstance(payload, Mapping) else []
|
|
25
|
+
return [
|
|
26
|
+
str(entry.get("id"))
|
|
27
|
+
for entry in data
|
|
28
|
+
if isinstance(entry, Mapping) and entry.get("id")
|
|
29
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Provider API-model profile expansion and runtime overrides."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
from .common import local_llm_profiles
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def profile_from_config(
|
|
12
|
+
profile_id: str, raw: Mapping[str, Any]
|
|
13
|
+
) -> local_llm_profiles.LocalLlmProfile:
|
|
14
|
+
canonical = local_llm_profiles.LOCAL_LLM_PROFILES.get(profile_id)
|
|
15
|
+
|
|
16
|
+
def profile_int(field: str, default: int) -> int:
|
|
17
|
+
value = raw.get(field, default)
|
|
18
|
+
try:
|
|
19
|
+
return int(value)
|
|
20
|
+
except (TypeError, ValueError) as exc:
|
|
21
|
+
raise ValueError(
|
|
22
|
+
f"API model profile {profile_id!r} field {field!r} must be an integer"
|
|
23
|
+
) from exc
|
|
24
|
+
|
|
25
|
+
return local_llm_profiles.LocalLlmProfile(
|
|
26
|
+
profile_id=profile_id,
|
|
27
|
+
description=str(
|
|
28
|
+
raw.get(
|
|
29
|
+
"description",
|
|
30
|
+
canonical.description if canonical else f"{profile_id} local LLM profile",
|
|
31
|
+
)
|
|
32
|
+
),
|
|
33
|
+
api_base=str(raw.get("api_base", canonical.api_base if canonical else "")),
|
|
34
|
+
model=str(raw.get("model", canonical.model if canonical else "")),
|
|
35
|
+
endpoint=str(raw.get("endpoint", canonical.endpoint if canonical else "")),
|
|
36
|
+
api_key=str(raw.get("api_key", canonical.api_key if canonical else "EMPTY")),
|
|
37
|
+
context_window=profile_int(
|
|
38
|
+
"context_window", canonical.context_window if canonical else 128_000
|
|
39
|
+
),
|
|
40
|
+
max_files=profile_int("max_files", canonical.max_files if canonical else 25),
|
|
41
|
+
max_file_bytes=profile_int(
|
|
42
|
+
"max_file_bytes", canonical.max_file_bytes if canonical else 60_000
|
|
43
|
+
),
|
|
44
|
+
http_timeout=profile_int("http_timeout", canonical.http_timeout if canonical else 900),
|
|
45
|
+
informational=bool(
|
|
46
|
+
raw.get("informational", canonical.informational if canonical else True)
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def local_llm_lane_profiles(
|
|
52
|
+
lane: Mapping[str, Any],
|
|
53
|
+
) -> tuple[local_llm_profiles.LocalLlmProfile, ...]:
|
|
54
|
+
provider_config = lane.get("provider_config", {})
|
|
55
|
+
if not isinstance(provider_config, Mapping):
|
|
56
|
+
return ()
|
|
57
|
+
raw_profiles = provider_config.get("profiles", {})
|
|
58
|
+
profile_env = str(provider_config.get("profile_env", ""))
|
|
59
|
+
selected_profile_id = os.environ.get(profile_env) if profile_env else None
|
|
60
|
+
if selected_profile_id:
|
|
61
|
+
if isinstance(raw_profiles, Mapping):
|
|
62
|
+
raw_profile = raw_profiles.get(selected_profile_id)
|
|
63
|
+
if isinstance(raw_profile, Mapping):
|
|
64
|
+
return (profile_from_config(selected_profile_id, raw_profile),)
|
|
65
|
+
try:
|
|
66
|
+
return (local_llm_profiles.get_profile(selected_profile_id),)
|
|
67
|
+
except KeyError:
|
|
68
|
+
return ()
|
|
69
|
+
if isinstance(raw_profiles, Mapping):
|
|
70
|
+
return tuple(
|
|
71
|
+
profile_from_config(str(profile_id), profile)
|
|
72
|
+
for profile_id, profile in sorted(raw_profiles.items())
|
|
73
|
+
if isinstance(profile, Mapping)
|
|
74
|
+
)
|
|
75
|
+
if isinstance(raw_profiles, (list, tuple)):
|
|
76
|
+
profiles: list[local_llm_profiles.LocalLlmProfile] = []
|
|
77
|
+
for profile_id in raw_profiles:
|
|
78
|
+
try:
|
|
79
|
+
profiles.append(local_llm_profiles.get_profile(str(profile_id)))
|
|
80
|
+
except KeyError:
|
|
81
|
+
continue
|
|
82
|
+
return tuple(profiles)
|
|
83
|
+
return ()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def api_model_key(
|
|
87
|
+
lane: Mapping[str, Any], profile: local_llm_profiles.LocalLlmProfile
|
|
88
|
+
) -> str:
|
|
89
|
+
provider_config = lane.get("provider_config", {})
|
|
90
|
+
api_key_env = ""
|
|
91
|
+
if isinstance(provider_config, Mapping):
|
|
92
|
+
api_key_env = str(provider_config.get("api_key_env", ""))
|
|
93
|
+
token_env = lane.get("token_env", [])
|
|
94
|
+
if not api_key_env and isinstance(token_env, list):
|
|
95
|
+
for item in token_env:
|
|
96
|
+
name = str(item)
|
|
97
|
+
if name.endswith("_API_KEY"):
|
|
98
|
+
api_key_env = name
|
|
99
|
+
break
|
|
100
|
+
if api_key_env:
|
|
101
|
+
value = os.environ.get(api_key_env)
|
|
102
|
+
if value:
|
|
103
|
+
return value
|
|
104
|
+
return profile.api_key
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def provider_env_override(
|
|
108
|
+
provider_config: Mapping[str, Any],
|
|
109
|
+
env_key: str,
|
|
110
|
+
fallback: str,
|
|
111
|
+
) -> str:
|
|
112
|
+
env_name = str(provider_config.get(env_key, ""))
|
|
113
|
+
if not env_name:
|
|
114
|
+
return fallback
|
|
115
|
+
return os.environ.get(env_name) or fallback
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def runtime_profile(
|
|
119
|
+
lane: Mapping[str, Any],
|
|
120
|
+
profile: local_llm_profiles.LocalLlmProfile,
|
|
121
|
+
) -> local_llm_profiles.LocalLlmProfile:
|
|
122
|
+
provider_config = lane.get("provider_config", {})
|
|
123
|
+
if not isinstance(provider_config, Mapping):
|
|
124
|
+
return profile
|
|
125
|
+
return local_llm_profiles.LocalLlmProfile(
|
|
126
|
+
profile_id=profile.profile_id,
|
|
127
|
+
description=profile.description,
|
|
128
|
+
api_base=provider_env_override(provider_config, "api_base_env", profile.api_base),
|
|
129
|
+
model=provider_env_override(provider_config, "model_env", profile.model),
|
|
130
|
+
endpoint=profile.endpoint,
|
|
131
|
+
api_key=profile.api_key,
|
|
132
|
+
context_window=profile.context_window,
|
|
133
|
+
max_files=profile.max_files,
|
|
134
|
+
max_file_bytes=profile.max_file_bytes,
|
|
135
|
+
http_timeout=profile.http_timeout,
|
|
136
|
+
informational=profile.informational,
|
|
137
|
+
)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Provider environment and token checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from .common import (
|
|
8
|
+
DoctorCheck,
|
|
9
|
+
STATUS_PASS,
|
|
10
|
+
STATUS_SKIP,
|
|
11
|
+
STATUS_WARN,
|
|
12
|
+
token_remediation,
|
|
13
|
+
)
|
|
14
|
+
from .provider_env_required import provider_required_env_status
|
|
15
|
+
from .provider_env_tokens import provider_token_status
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def check_token_env(lane_id: str, lane: Mapping[str, Any]) -> list[DoctorCheck]:
|
|
19
|
+
token_status = provider_token_status(lane)
|
|
20
|
+
checks: list[DoctorCheck] = []
|
|
21
|
+
if not token_status.declares_tokens:
|
|
22
|
+
checks.append(
|
|
23
|
+
DoctorCheck(
|
|
24
|
+
name="env.tokens",
|
|
25
|
+
status=STATUS_SKIP,
|
|
26
|
+
lane=lane_id,
|
|
27
|
+
message="lane declares no token env vars",
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
return checks
|
|
31
|
+
|
|
32
|
+
missing = list(token_status.missing)
|
|
33
|
+
missing_any = [list(group) for group in token_status.missing_any]
|
|
34
|
+
token_file_env = list(token_status.token_file_env)
|
|
35
|
+
if missing or missing_any:
|
|
36
|
+
messages = []
|
|
37
|
+
if missing:
|
|
38
|
+
messages.append(f"missing token env vars: {', '.join(missing)}")
|
|
39
|
+
for group in missing_any:
|
|
40
|
+
messages.append(f"set one of: {', '.join(group)}")
|
|
41
|
+
checks.append(
|
|
42
|
+
DoctorCheck(
|
|
43
|
+
name="env.tokens",
|
|
44
|
+
status=STATUS_WARN,
|
|
45
|
+
lane=lane_id,
|
|
46
|
+
message="; ".join(messages),
|
|
47
|
+
detail={
|
|
48
|
+
"missing": missing,
|
|
49
|
+
"missing_any": missing_any,
|
|
50
|
+
"token_file_env": token_file_env,
|
|
51
|
+
},
|
|
52
|
+
remediation=token_remediation(missing, missing_any),
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
checks.append(
|
|
57
|
+
DoctorCheck(
|
|
58
|
+
name="env.tokens",
|
|
59
|
+
status=STATUS_PASS,
|
|
60
|
+
lane=lane_id,
|
|
61
|
+
message="token env vars are set",
|
|
62
|
+
detail={
|
|
63
|
+
"token_env": list(token_status.token_env),
|
|
64
|
+
"token_env_any": [list(group) for group in token_status.token_env_any],
|
|
65
|
+
"token_file_env": token_file_env,
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
return checks
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def check_required_env(lane_id: str, lane: Mapping[str, Any]) -> list[DoctorCheck]:
|
|
73
|
+
env_status = provider_required_env_status(lane)
|
|
74
|
+
if not env_status.declares_required_env:
|
|
75
|
+
return []
|
|
76
|
+
required = list(env_status.required)
|
|
77
|
+
required_truthy = list(env_status.required_truthy)
|
|
78
|
+
missing = list(env_status.missing)
|
|
79
|
+
missing_truthy = list(env_status.missing_truthy)
|
|
80
|
+
if missing or missing_truthy:
|
|
81
|
+
return [
|
|
82
|
+
DoctorCheck(
|
|
83
|
+
name="env.required",
|
|
84
|
+
status=STATUS_WARN,
|
|
85
|
+
lane=lane_id,
|
|
86
|
+
message=(
|
|
87
|
+
"missing required env vars: "
|
|
88
|
+
+ ", ".join([*missing, *missing_truthy])
|
|
89
|
+
),
|
|
90
|
+
detail={
|
|
91
|
+
"missing": missing,
|
|
92
|
+
"missing_truthy": missing_truthy,
|
|
93
|
+
"required_env": required,
|
|
94
|
+
"required_env_truthy": required_truthy,
|
|
95
|
+
},
|
|
96
|
+
remediation=(
|
|
97
|
+
"Set the required env vars only when you accept the lane's "
|
|
98
|
+
"documented runtime trust model."
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
]
|
|
102
|
+
return [
|
|
103
|
+
DoctorCheck(
|
|
104
|
+
name="env.required",
|
|
105
|
+
status=STATUS_PASS,
|
|
106
|
+
lane=lane_id,
|
|
107
|
+
message="required env vars are set",
|
|
108
|
+
detail={
|
|
109
|
+
"required_env": required,
|
|
110
|
+
"required_env_truthy": required_truthy,
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Provider required environment status helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
from .common import TRUTHY_ENV_VALUES, as_sequence
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ProviderRequiredEnvStatus:
|
|
14
|
+
required: tuple[str, ...]
|
|
15
|
+
required_truthy: tuple[str, ...]
|
|
16
|
+
missing: tuple[str, ...]
|
|
17
|
+
missing_truthy: tuple[str, ...]
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def declares_required_env(self) -> bool:
|
|
21
|
+
return bool(self.required or self.required_truthy)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def all_present(self) -> bool:
|
|
25
|
+
return not self.missing and not self.missing_truthy
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def provider_required_env_status(lane: Mapping[str, Any]) -> ProviderRequiredEnvStatus:
|
|
29
|
+
provider_config = lane.get("provider_config", {})
|
|
30
|
+
if not isinstance(provider_config, Mapping):
|
|
31
|
+
return ProviderRequiredEnvStatus((), (), (), ())
|
|
32
|
+
required = tuple(
|
|
33
|
+
str(name)
|
|
34
|
+
for name in as_sequence(provider_config.get("required_env", []))
|
|
35
|
+
if str(name).strip()
|
|
36
|
+
)
|
|
37
|
+
required_truthy = tuple(
|
|
38
|
+
str(name)
|
|
39
|
+
for name in as_sequence(provider_config.get("required_env_truthy", []))
|
|
40
|
+
if str(name).strip()
|
|
41
|
+
)
|
|
42
|
+
missing = tuple(name for name in required if not os.environ.get(name))
|
|
43
|
+
missing_truthy = tuple(
|
|
44
|
+
name
|
|
45
|
+
for name in required_truthy
|
|
46
|
+
if os.environ.get(name, "").strip().lower() not in TRUTHY_ENV_VALUES
|
|
47
|
+
)
|
|
48
|
+
return ProviderRequiredEnvStatus(
|
|
49
|
+
required=required,
|
|
50
|
+
required_truthy=required_truthy,
|
|
51
|
+
missing=missing,
|
|
52
|
+
missing_truthy=missing_truthy,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = ["ProviderRequiredEnvStatus", "provider_required_env_status"]
|