Spaces:
Sleeping
Sleeping
Download src/elbench/dashboard/plan.py from adelevett/ELBench-live: direct link, hf CLI and curl.
- Browser
- Download file 35.7 kB
-
https://huggingface.co/spaces/adelevett/ELBench-live/resolve/main/src/elbench/dashboard/plan.py
- Command line
-
hf download hf://spaces/adelevett/ELBench-live/src/elbench/dashboard/plan.py
-
curl -L -o plan.py https://huggingface.co/spaces/adelevett/ELBench-live/resolve/main/src/elbench/dashboard/plan.py
35.7 kB
| """Selection caveats and a cost range for one visitor run. | |
| Sample order, prompt text, and the token floor follow the runner: | |
| ``FileRegistry.resolve``, ``LoaderFactory``, ``augment_prompt_for_format``, | |
| and ``len(prompt) // 4``. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from elbench.execution.basic_education import ( | |
| DEFAULT_BASIC_MODULE_NAME, | |
| DEFAULT_MULTI_TURN_RECURSION_LIMIT, | |
| BasicEducationExecutor, | |
| count_runtime_tasks, | |
| ) | |
| from elbench.execution.preflight import PreflightRunner | |
| from elbench.execution.response_format import augment_prompt_for_format | |
| from elbench.execution.runner import BenchmarkRunner | |
| from elbench.execution.selection import resolve_module_selection | |
| from elbench.loaders import LoaderFactory | |
| from elbench.registry import FileRegistry | |
| from elbench.schemas.config import ProjectConfig | |
| from elbench.schemas.evaluation import Sample | |
| from elbench.dashboard.catalog import Catalog, CatalogFile | |
| from elbench.dashboard.english import english_file, english_module, english_preflight_message | |
| from elbench.dashboard.leaderboard import CHINA_SPECIFIC_FILES, compute_plan_fingerprint | |
| from elbench.dashboard.protocol import ( | |
| VISITOR_KEY_ENV, | |
| Caveat, | |
| VisitorProtocol, | |
| build_judge_model_config, | |
| build_multiturn_chat_model_config, | |
| build_visitor_protocol, | |
| ) | |
| from elbench.dashboard.routing import Route | |
| from elbench.schemas.config import ModelConfig | |
| from elbench.utils.secrets import get_api_key | |
| # High-end assumption for one judge grade, thinking included. Not sent as a limit. | |
| JUDGE_HIGH_OUTPUT_TOKENS = 8192 | |
| class RunSelection: | |
| modules: frozenset[str] | |
| source_files: frozenset[str] | |
| dimensions: frozenset[str] | |
| max_samples: int | None | |
| max_samples_per_file: int | None = None | |
| class Plan: | |
| protocol: VisitorProtocol | |
| selection: RunSelection | |
| caveats: list[Caveat] | |
| standard_requests: int | |
| standard_low_input_tokens: int | |
| standard_high_input_tokens: int | |
| standard_output_cap_tokens: int | |
| judge_calls: int | |
| judge_model_id: str | None | |
| judge_api_base: str | None | |
| basic_tasks: int | |
| basic_target_calls_low: int | |
| basic_target_calls_high: int | |
| max_tokens: int | |
| prompt_usd_per_token: float | None | |
| completion_usd_per_token: float | None | |
| low_usd: float | None | |
| high_usd: float | None | |
| preflight_ok: bool | |
| blocked: bool | |
| needs_price_ack: bool | |
| file_counts: list[tuple[str, int]] = field(default_factory=list) | |
| judge_model_config: ModelConfig | None = None | |
| use_system_one_judge: bool = False | |
| multiturn_model_config: ModelConfig | None = None | |
| use_multiturn_chat_model: bool = False | |
| config_hash: str = "" | |
| is_full_benchmark: bool = False | |
| # Every question outside the region-specific sets is selected (the default "core" scope). | |
| covers_core: bool = False | |
| judge_prices: tuple[float, float] | None = None | |
| student_prices: tuple[float, float] | None = None | |
| # (low, high) USD per component: "target", "judge". Missing means no estimate. | |
| component_usd: dict[str, tuple[float, float]] = field(default_factory=dict) | |
| # Host choice per role ("target", "judge", "student") that the request bodies carry. | |
| routes: dict[str, Route] = field(default_factory=dict) | |
| def error_messages(self) -> list[str]: | |
| return [caveat.message for caveat in self.caveats if caveat.level == "error"] | |
| def build_plan( | |
| catalog: Catalog, | |
| *, | |
| openrouter_model: str, | |
| modules: set[str], | |
| source_files: set[str], | |
| dimensions: set[str] | None, | |
| max_samples: int | None, | |
| prompt_usd_per_token: float | None, | |
| completion_usd_per_token: float | None, | |
| max_concurrency: int, | |
| max_samples_per_file: int | None = None, | |
| judge_openrouter_model: str | None = None, | |
| use_system_one_judge: bool = False, | |
| judge_prompt_usd_per_token: float | None = None, | |
| judge_completion_usd_per_token: float | None = None, | |
| use_multiturn_chat_model: bool = False, | |
| multiturn_openrouter_model: str | None = None, | |
| multiturn_prompt_usd_per_token: float | None = None, | |
| multiturn_completion_usd_per_token: float | None = None, | |
| routes: dict[str, Route] | None = None, | |
| ) -> Plan: | |
| protocol = build_visitor_protocol(catalog.config, openrouter_model) | |
| config = catalog.config.model_copy(deep=True) | |
| config.models[protocol.model_config.model_id] = protocol.model_config | |
| selection = RunSelection( | |
| modules=frozenset(modules), | |
| source_files=frozenset(source_files), | |
| dimensions=frozenset(dimensions or ()), | |
| max_samples=max_samples, | |
| max_samples_per_file=max_samples_per_file, | |
| ) | |
| caveats = list(protocol.caveats) | |
| judge_model_config: ModelConfig | None = None | |
| cleaned_judge = (judge_openrouter_model or "").strip() | |
| if cleaned_judge: | |
| judge_model_config = build_judge_model_config( | |
| config, | |
| cleaned_judge, | |
| system_one=bool(use_system_one_judge), | |
| ) | |
| config.models[judge_model_config.model_id] = judge_model_config | |
| config.judges.default_judge_model_id = judge_model_config.model_id | |
| config.judges.basic_education_evaluator_model_id = judge_model_config.model_id | |
| config.judges.basic_education_test_endpoint_model_id = judge_model_config.model_id | |
| multiturn_model_config: ModelConfig | None = None | |
| cleaned_multiturn = (multiturn_openrouter_model or "").strip() | |
| if use_multiturn_chat_model: | |
| if not cleaned_multiturn: | |
| caveats.append( | |
| Caveat( | |
| level="error", | |
| code="multiturn_model_missing", | |
| message=( | |
| "Multi-turn chat model endpoint is enabled, but no OpenRouter model id " | |
| "was selected for the multi-turn endpoint." | |
| ), | |
| ) | |
| ) | |
| else: | |
| multiturn_model_config = build_multiturn_chat_model_config(config, cleaned_multiturn) | |
| config.models[multiturn_model_config.model_id] = multiturn_model_config | |
| config.judges.basic_education_test_endpoint_model_id = multiturn_model_config.model_id | |
| caveats.append( | |
| Caveat( | |
| level="info", | |
| code="multiturn_chat_on_visitor_key", | |
| message=( | |
| f"Multi-turn student dialogue (`stu`) uses OpenRouter chat model " | |
| f"`{multiturn_model_config.model_name}` via the visitor's single OpenRouter key." | |
| ), | |
| ) | |
| ) | |
| elif ( | |
| use_system_one_judge | |
| and DEFAULT_BASIC_MODULE_NAME in selection.modules | |
| and "config_guided_task.yaml" in selection.source_files | |
| ): | |
| caveats.append( | |
| Caveat( | |
| level="error", | |
| code="multiturn_endpoint_required_for_system_one", | |
| message=( | |
| "Scenario `config_guided_task.yaml` is multi-turn and requires a conversational " | |
| "student model (`stu`). System 1 decision models have 0 output tokens and cannot " | |
| "generate student dialogue turns. Check 'Opt into multi-turn chat model endpoint' " | |
| "and select an OpenRouter chat model." | |
| ), | |
| ) | |
| ) | |
| if protocol.openrouter_model.endswith(":free"): | |
| caveats.append( | |
| Caveat( | |
| level="warning", | |
| code="free_model_rate_limit", | |
| message=( | |
| "This id ends in :free. OpenRouter caps those variants at 20 requests per minute, " | |
| "and at 50 requests per UTC day until the account has purchased at least 10 credits, " | |
| "then 1,000 per day. Paid slugs are not under that cap. They are limited by credit " | |
| "balance, the in-flight spending budget, the upstream provider, and DDoS protection. " | |
| "GET /api/v1/key reports limit_remaining and free_model_daily_requests." | |
| ), | |
| ) | |
| ) | |
| applied_routes = _apply_routes( | |
| routes or {}, | |
| target=protocol.model_config, | |
| judge=None if use_system_one_judge else judge_model_config, | |
| student=multiturn_model_config, | |
| ) | |
| if applied_routes: | |
| caveats.append( | |
| Caveat( | |
| level="info", | |
| code="openrouter_host_choice", | |
| message=" ".join(route.describe() for route in applied_routes.values()), | |
| ) | |
| ) | |
| caveats.extend(_selection_caveats(catalog, selection, max_concurrency)) | |
| samples = iter_standard_samples(config, selection) | |
| file_counts: dict[str, int] = {} | |
| low_input = 0 | |
| high_input = 0 | |
| judge_calls = 0 | |
| for sample in samples: | |
| file_counts[sample.source_file] = file_counts.get(sample.source_file, 0) + 1 | |
| prompt = augment_prompt_for_format(sample, sample.prompt) | |
| chars = len(prompt) | |
| low_input += max(1, chars // 4) | |
| high_input += max(1, chars) | |
| if _task_mode(config, sample.task) == "llm": | |
| judge_calls += 1 | |
| basic_tasks, basic_low, basic_high, basic_caveats = _basic_education_bounds( | |
| config, | |
| protocol.model_config, | |
| selection, | |
| standard_loaded=len(samples), | |
| ) | |
| caveats.extend(basic_caveats) | |
| max_tokens = int(protocol.model_config.max_tokens or 0) | |
| output_cap = len(samples) * max_tokens | |
| judge_model_id, judge_api_base = _judge_endpoint(config) | |
| price_ok = ( | |
| prompt_usd_per_token is not None | |
| and completion_usd_per_token is not None | |
| and prompt_usd_per_token >= 0 | |
| and completion_usd_per_token >= 0 | |
| ) | |
| low_usd = None | |
| high_usd = None | |
| component_usd: dict[str, tuple[float, float]] = {} | |
| if price_ok: | |
| low_usd = (low_input * prompt_usd_per_token) + (basic_low * max_tokens * completion_usd_per_token) | |
| high_usd = (high_input * prompt_usd_per_token) + ( | |
| (output_cap + (basic_high * max_tokens)) * completion_usd_per_token | |
| ) | |
| component_usd["target"] = (low_usd, high_usd) | |
| caveats.append( | |
| Caveat( | |
| level="info", | |
| code="cost_range", | |
| message=( | |
| "The low dollar figure prices input tokens with the runner's estimator, " | |
| "len(prompt) // 4, and prices no standard-module output. " | |
| "The high figure prices one token per prompt character and output at the " | |
| "request max_tokens cap. OpenRouter's usage object counts the model's native tokenizer, " | |
| "not this estimator. Paid requests in flight are also held against an in-flight spending " | |
| "budget estimated from max_tokens. " | |
| "Format retries, answer-length retries, and HTTP retries are extra and are not included. " | |
| "Basic-education input tokens are not included, because that runtime assembles its own prompts." | |
| ), | |
| ) | |
| ) | |
| else: | |
| caveats.append( | |
| Caveat( | |
| level="warning", | |
| code="price_unknown", | |
| message=( | |
| f"OpenRouter did not return a token price for {protocol.openrouter_model}. " | |
| "There is no dollar estimate. Running requires an explicit confirmation." | |
| ), | |
| ) | |
| ) | |
| if judge_calls: | |
| if judge_model_config is not None: | |
| endpoint_suffix = "/systemone" if use_system_one_judge else "/chat/completions" | |
| judge_kind = "System 1 decision-model" if use_system_one_judge else "OpenRouter chat" | |
| caveats.append( | |
| Caveat( | |
| level="info", | |
| code="judge_on_visitor_key", | |
| message=( | |
| f"This selection makes {judge_calls} {judge_kind} judge calls through JudgeRouter " | |
| f"to `{judge_model_config.model_name}` at `{judge_model_config.api_base}{endpoint_suffix}` " | |
| "using the visitor's single OpenRouter API key." | |
| ), | |
| ) | |
| ) | |
| if ( | |
| low_usd is not None | |
| and high_usd is not None | |
| and judge_prompt_usd_per_token is not None | |
| and judge_prompt_usd_per_token >= 0 | |
| ): | |
| # Each judge call sends state + rubric (~300 tokens low, ~1024 tokens high). The judge has no | |
| # length limit, so the high end assumes JUDGE_HIGH_OUTPUT_TOKENS of output, thinking included. | |
| judge_low = judge_calls * 300 * judge_prompt_usd_per_token | |
| judge_out_cap = 0 if use_system_one_judge else JUDGE_HIGH_OUTPUT_TOKENS | |
| judge_comp_price = max(0.0, float(judge_completion_usd_per_token or 0.0)) | |
| judge_high = (judge_calls * 1024 * judge_prompt_usd_per_token) + ( | |
| judge_calls * judge_out_cap * judge_comp_price | |
| ) | |
| component_usd["judge"] = (judge_low, judge_high) | |
| low_usd += judge_low | |
| high_usd += judge_high | |
| else: | |
| caveats.append( | |
| Caveat( | |
| level="info", | |
| code="judge_calls_not_on_visitor_key", | |
| message=( | |
| f"This selection makes {judge_calls} LLM-judge calls through JudgeRouter. " | |
| f"The judge model is {judge_model_id} at {judge_api_base}. " | |
| "Those calls are not billed to the visitor OpenRouter key, and the judge model " | |
| "is not replaced." | |
| ), | |
| ) | |
| ) | |
| caveats.extend(_preflight_caveats(config, protocol, selection)) | |
| blocked = any(caveat.level == "error" for caveat in caveats) | |
| preflight_ok = not any(caveat.code.startswith("preflight_") and caveat.level == "error" for caveat in caveats) | |
| is_full = any(caveat.code == "full_item_set" for caveat in caveats) | |
| covers_core = _covers_core(catalog, selection) | |
| cfg_hash = compute_plan_fingerprint( | |
| openrouter_model=protocol.openrouter_model, | |
| judge_openrouter_model=cleaned_judge, | |
| use_system_one_judge=bool(use_system_one_judge), | |
| use_multiturn_chat_model=bool(use_multiturn_chat_model), | |
| multiturn_openrouter_model=cleaned_multiturn, | |
| modules=selection.modules, | |
| source_files=selection.source_files, | |
| dimensions=selection.dimensions, | |
| max_samples=selection.max_samples, | |
| max_samples_per_file=selection.max_samples_per_file, | |
| ) | |
| return Plan( | |
| protocol=protocol, | |
| selection=selection, | |
| caveats=caveats, | |
| standard_requests=len(samples), | |
| standard_low_input_tokens=low_input, | |
| standard_high_input_tokens=high_input, | |
| standard_output_cap_tokens=output_cap, | |
| judge_calls=judge_calls, | |
| judge_model_id=judge_model_id, | |
| judge_api_base=judge_api_base, | |
| basic_tasks=basic_tasks, | |
| basic_target_calls_low=basic_low, | |
| basic_target_calls_high=basic_high, | |
| max_tokens=max_tokens, | |
| prompt_usd_per_token=prompt_usd_per_token if price_ok else None, | |
| completion_usd_per_token=completion_usd_per_token if price_ok else None, | |
| low_usd=low_usd, | |
| high_usd=high_usd, | |
| preflight_ok=preflight_ok and not blocked, | |
| blocked=blocked, | |
| needs_price_ack=not price_ok, | |
| file_counts=sorted(file_counts.items()), | |
| judge_model_config=judge_model_config, | |
| use_system_one_judge=bool(use_system_one_judge), | |
| multiturn_model_config=multiturn_model_config, | |
| use_multiturn_chat_model=bool(use_multiturn_chat_model), | |
| config_hash=cfg_hash, | |
| is_full_benchmark=is_full, | |
| covers_core=covers_core, | |
| judge_prices=_price_tuple(judge_prompt_usd_per_token, judge_completion_usd_per_token), | |
| student_prices=( | |
| _price_tuple(multiturn_prompt_usd_per_token, multiturn_completion_usd_per_token) | |
| if multiturn_model_config is not None | |
| else None | |
| ), | |
| component_usd=component_usd, | |
| routes=applied_routes, | |
| ) | |
| def _apply_routes(routes: dict[str, Route], **configs: ModelConfig | None) -> dict[str, Route]: | |
| """Put each role's OpenRouter ``provider`` object into its request body. Decision models are not routed.""" | |
| applied: dict[str, Route] = {} | |
| for role, model_config in configs.items(): | |
| route = routes.get(role) | |
| if model_config is None or route is None or not route.provider: | |
| continue | |
| model_config.provider_kwargs = {**model_config.provider_kwargs, "provider": dict(route.provider)} | |
| applied[role] = route | |
| return applied | |
| def _covers_core(catalog: Catalog, selection: RunSelection) -> bool: | |
| """True when every question outside the region-specific sets is selected, uncapped and unfiltered.""" | |
| active = [ | |
| name | |
| for name in catalog.config.app.benchmark_modules.active | |
| if name in catalog.config.modules and catalog.config.modules[name].enabled | |
| ] | |
| core = {entry.canonical_name for entry in catalog.files} - set(CHINA_SPECIFIC_FILES) | |
| return ( | |
| set(selection.modules) == set(active) | |
| and core <= set(selection.source_files) | |
| and not selection.dimensions | |
| and selection.max_samples is None | |
| and selection.max_samples_per_file is None | |
| ) | |
| def _price_tuple(prompt: float | None, completion: float | None) -> tuple[float, float] | None: | |
| if prompt is None or prompt < 0: | |
| return None | |
| return float(prompt), max(0.0, float(completion or 0.0)) | |
| def iter_standard_samples(config: ProjectConfig, selection: RunSelection) -> list[Sample]: | |
| """Same sample sequence ``BenchmarkRunner._load_samples`` produces.""" | |
| if not selection.modules or not selection.source_files: | |
| return [] | |
| module_selection = resolve_module_selection(config, set(selection.modules)) | |
| if not module_selection.standard_modules: | |
| return [] | |
| resolved = FileRegistry(config).resolve( | |
| modules=set(module_selection.standard_modules), | |
| subsets=None, | |
| source_files=set(selection.source_files), | |
| ) | |
| dimensions = set(selection.dimensions) or None | |
| runner = BenchmarkRunner(config) | |
| return list(runner._load_samples(resolved, dimensions, selection.max_samples, selection.max_samples_per_file)) | |
| def render_plan(plan: Plan) -> str: | |
| lines = ["### Protocol", ""] | |
| model = plan.protocol.model_config | |
| lines.append(f"- Model field: `{model.model_name}`") | |
| lines.append(f"- Endpoint: `{model.api_base}` + the provider `endpoint_path`") | |
| lines.append( | |
| f"- temperature `{model.temperature}`, max_tokens `{model.max_tokens}`, " | |
| f"stream `{str(bool(model.supports_stream)).lower()}`, " | |
| f"responses API `{str(plan.protocol.responses_api).lower()}`" | |
| ) | |
| if plan.protocol.matched_card_id: | |
| lines.append(f"- Copied card: `{plan.protocol.matched_card_id}`") | |
| else: | |
| lines.append("- Copied card: none") | |
| if plan.judge_model_config is not None: | |
| judge_mode = "System 1 (`/systemone`)" if plan.use_system_one_judge else "OpenRouter Chat (`/chat/completions`)" | |
| lines.append(f"- Judge endpoint: `{plan.judge_model_config.model_name}` ({judge_mode})") | |
| if plan.multiturn_model_config is not None: | |
| lines.append(f"- Multi-turn chat endpoint (`stu`): `{plan.multiturn_model_config.model_name}`") | |
| lines.append("") | |
| lines.append("### Selection") | |
| lines.append("") | |
| lines.append(f"- Standard requests: {plan.standard_requests}") | |
| if plan.file_counts: | |
| for name, count in plan.file_counts: | |
| lines.append(f" - `{english_file(name)}`: {count}") | |
| lines.append( | |
| f"- Input tokens, low / high: {plan.standard_low_input_tokens} / {plan.standard_high_input_tokens}" | |
| ) | |
| lines.append(f"- Standard output cap: {plan.standard_output_cap_tokens} tokens") | |
| lines.append(f"- LLM-judge calls: {plan.judge_calls}") | |
| lines.append(f"- Basic-education tasks: {plan.basic_tasks}") | |
| lines.append( | |
| "- Basic-education target-model calls, low / high: " | |
| f"{plan.basic_target_calls_low} / {plan.basic_target_calls_high}" | |
| ) | |
| if plan.config_hash: | |
| lines.append(f"- Configuration checkpoint fingerprint: `{plan.config_hash}`") | |
| lines.append("") | |
| lines.append("### Cost range charged to the OpenRouter key") | |
| lines.append("") | |
| if plan.low_usd is None or plan.high_usd is None: | |
| lines.append("No price is available for this model id.") | |
| else: | |
| lines.append(f"- Low: {_usd(plan.low_usd)}") | |
| lines.append(f"- High: {_usd(plan.high_usd)}") | |
| lines.append( | |
| f"- Prices: prompt {_per_million(plan.prompt_usd_per_token)} / 1M, " | |
| f"completion {_per_million(plan.completion_usd_per_token)} / 1M" | |
| ) | |
| lines.append("") | |
| lines.append("### Caveats") | |
| lines.append("") | |
| if not plan.caveats: | |
| lines.append("None.") | |
| else: | |
| for caveat in plan.caveats: | |
| lines.append(f"- **{caveat.level}** `{caveat.code}`: {caveat.message}") | |
| if plan.blocked: | |
| lines.append("") | |
| lines.append("The run is blocked until every error above is cleared.") | |
| return "\n".join(lines) | |
| def _selection_caveats(catalog: Catalog, selection: RunSelection, max_concurrency: int) -> list[Caveat]: | |
| caveats: list[Caveat] = [] | |
| known = catalog.by_name() | |
| if not selection.modules: | |
| caveats.append(Caveat("error", "no_modules", "Select at least one module.")) | |
| if not selection.source_files: | |
| caveats.append(Caveat("error", "no_files", "Select at least one source file.")) | |
| unknown = sorted(name for name in selection.source_files if name not in known) | |
| if unknown: | |
| caveats.append( | |
| Caveat( | |
| "error", | |
| "unknown_files", | |
| "These source files are not in the registry: " + ", ".join(unknown), | |
| ) | |
| ) | |
| if selection.max_samples is not None and selection.max_samples <= 0: | |
| caveats.append( | |
| Caveat( | |
| "error", | |
| "bad_max_samples", | |
| "Max samples must be empty (every selected item) or a positive integer.", | |
| ) | |
| ) | |
| if max_concurrency < 1 or max_concurrency > 100: | |
| caveats.append( | |
| Caveat( | |
| "error", | |
| "bad_concurrency", | |
| "Max concurrency must be from 1 to 100. 100 is the cap in configs/app.yaml.", | |
| ) | |
| ) | |
| active = [ | |
| name | |
| for name in catalog.config.app.benchmark_modules.active | |
| if name in catalog.config.modules and catalog.config.modules[name].enabled | |
| ] | |
| all_files = {entry.canonical_name for entry in catalog.files} | |
| full_modules = set(selection.modules) == set(active) | |
| full_files = set(selection.source_files) == all_files | |
| full_items = ( | |
| full_modules | |
| and full_files | |
| and not selection.dimensions | |
| and selection.max_samples is None | |
| and selection.max_samples_per_file is None | |
| ) | |
| if not full_items: | |
| reasons: list[str] = [] | |
| if not full_modules: | |
| missing = [english_module(name) for name in active if name not in selection.modules] | |
| reasons.append("modules omitted: " + ", ".join(missing)) | |
| if not full_files: | |
| missing_files = [name for name in catalog.files if name.canonical_name not in selection.source_files] | |
| reasons.append(f"{len(missing_files)} registry files omitted") | |
| if selection.dimensions: | |
| reasons.append("dimension filter is set") | |
| if selection.max_samples is not None: | |
| reasons.append( | |
| f"max_samples={selection.max_samples} keeps a prefix in registry order, " | |
| "the same order BenchmarkRunner uses, not a random or stratified sample" | |
| ) | |
| if selection.max_samples_per_file is not None: | |
| reasons.append(f"max_samples_per_file={selection.max_samples_per_file} keeps a prefix of each file") | |
| caveats.append( | |
| Caveat( | |
| "warning", | |
| "not_full_benchmark", | |
| "This selection is not the full 2,939-item benchmark (" | |
| + "; ".join(reasons) | |
| + "). Module averages from build_summary describe only the items that ran. " | |
| "They are not comparable to the published leaderboard, and this repository " | |
| "does not compute that table's bootstrap interval.", | |
| ) | |
| ) | |
| else: | |
| caveats.append( | |
| Caveat( | |
| "info", | |
| "full_item_set", | |
| "Every active module and registry file is selected, with no dimension filter " | |
| "and no max_samples cap. build_summary will average the full item set. " | |
| "That average is still not the published 0–100 leaderboard: the host is OpenRouter, " | |
| "and the bootstrap interval is not implemented in this repository.", | |
| ) | |
| ) | |
| if selection.max_samples is not None and selection.max_samples > 0: | |
| caveats.append( | |
| Caveat( | |
| "warning", | |
| "max_samples_prefix", | |
| "max_samples stops after that many standard samples in file-registry order. " | |
| "When basic education is also selected, standard modules consume the budget first " | |
| "and basic education receives the remainder. That is the runner's rule.", | |
| ) | |
| ) | |
| orphan_files = [ | |
| name | |
| for name in selection.source_files | |
| if name in known and known[name].module not in selection.modules | |
| ] | |
| if orphan_files: | |
| caveats.append( | |
| Caveat( | |
| "warning", | |
| "file_module_mismatch", | |
| "These files are checked, but their module is not selected, so the runner will skip them: " | |
| + ", ".join(sorted(orphan_files)), | |
| ) | |
| ) | |
| selected_entries = [ | |
| known[name] | |
| for name in selection.source_files | |
| if name in known and known[name].module in selection.modules | |
| ] | |
| for entry in selected_entries: | |
| if entry.scoring_mode == "rule" and entry.preflight_llm_task: | |
| caveats.append( | |
| Caveat( | |
| "warning", | |
| "rule_task_preflight_hold", | |
| f"{english_file(entry.canonical_name)} is scored by the rule judge " | |
| f"(configs/judges.yaml task {entry.task}, mode rule). " | |
| "Preflight still lists this task with the LLM-judge tasks and will not " | |
| "start the run without that judge model's API key. The dashboard keeps that check.", | |
| ) | |
| ) | |
| _scale_caveats(caveats, selected_entries) | |
| if max_concurrency != 100: | |
| caveats.append( | |
| Caveat( | |
| "info", | |
| "concurrency", | |
| f"max_concurrency is {max_concurrency}. The source default in configs/app.yaml is 100. " | |
| "Concurrency changes scheduling only. Each request is still built by OpenAICompatibleClient " | |
| "and scored by JudgeRouter. The hosted default is 8 so a 2-vCPU Space does not turn " | |
| "timeouts into failed samples.", | |
| ) | |
| ) | |
| if not get_api_key(VISITOR_KEY_ENV): | |
| caveats.append( | |
| Caveat( | |
| "error", | |
| "visitor_key_missing", | |
| "Enter an OpenRouter API key. It is read from the process environment for the run " | |
| "and is not written to configs or key.txt.", | |
| ) | |
| ) | |
| return caveats | |
| def _scale_caveats(caveats: list[Caveat], entries: list[CatalogFile]) -> None: | |
| if any(entry.module == DEFAULT_BASIC_MODULE_NAME for entry in entries): | |
| caveats.append( | |
| Caveat( | |
| "warning", | |
| "basic_education_score_scale", | |
| "Basic-education scores are the runtime's mean of numeric rubric fields. " | |
| "The pass threshold in configs/basic_education.yaml is 3.0. " | |
| "Other modules' rule and LLM judges write scores on a 0–1 scale " | |
| "(teaching-harm can also be 0.5). build_summary averages each group separately. " | |
| "This dashboard does not rescale either group onto the published 0–100 leaderboard.", | |
| ) | |
| ) | |
| if any(entry.scoring_mode == "placeholder" for entry in entries): | |
| caveats.append( | |
| Caveat( | |
| "error", | |
| "placeholder_judge", | |
| "A selected task has no judge in configs/judges.yaml. The placeholder judge does not emit a score.", | |
| ) | |
| ) | |
| def _basic_education_bounds( | |
| config: ProjectConfig, | |
| model_config, | |
| selection: RunSelection, | |
| *, | |
| standard_loaded: int, | |
| ) -> tuple[int, int, int, list[Caveat]]: | |
| caveats: list[Caveat] = [] | |
| if DEFAULT_BASIC_MODULE_NAME not in selection.modules: | |
| return 0, 0, 0, caveats | |
| if not selection.source_files: | |
| return 0, 0, 0, caveats | |
| executor = BasicEducationExecutor(config, model_config, _SilentLogger()) | |
| scenarios = executor._select_scenarios( | |
| subsets=None, | |
| source_files=set(selection.source_files), | |
| dimensions=set(selection.dimensions) or None, | |
| ) | |
| remaining = selection.max_samples | |
| if remaining is not None: | |
| remaining = max(0, remaining - standard_loaded) | |
| if standard_loaded and DEFAULT_BASIC_MODULE_NAME in selection.modules: | |
| caveats.append( | |
| Caveat( | |
| "warning", | |
| "mixed_max_samples", | |
| "Standard modules and basic education share one max_samples budget. " | |
| "Standard samples are counted first. Basic education is skipped when nothing remains.", | |
| ) | |
| ) | |
| tasks = 0 | |
| low_calls = 0 | |
| high_calls = 0 | |
| for scenario in scenarios: | |
| if remaining is not None and remaining <= 0: | |
| break | |
| template = executor._read_yaml_file(scenario.template_path) | |
| declared = count_runtime_tasks(template) | |
| if scenario.expected_tasks is not None and declared != scenario.expected_tasks: | |
| caveats.append( | |
| Caveat( | |
| "warning", | |
| "basic_education_task_count_mismatch", | |
| f"{scenario.scenario_id} declares {declared} runtime tasks but " | |
| f"configs/basic_education.yaml expected_tasks is {scenario.expected_tasks}. " | |
| "The executor logs this and runs the template.", | |
| ) | |
| ) | |
| take = declared if remaining is None else min(declared, remaining) | |
| if selection.max_samples_per_file is not None: | |
| take = min(take, selection.max_samples_per_file) | |
| globals_section = template.get("globals") if isinstance(template, dict) else {} | |
| raw_limit = globals_section.get("recursion_limit") if isinstance(globals_section, dict) else None | |
| try: | |
| recursion_limit = int(raw_limit) | |
| except (TypeError, ValueError): | |
| recursion_limit = 25 | |
| caveats.append( | |
| Caveat( | |
| "info", | |
| "recursion_limit_default", | |
| f"{scenario.scenario_id} has no globals.recursion_limit. " | |
| "The runtime entity default is 25.", | |
| ) | |
| ) | |
| if scenario.multi_turn: | |
| recursion_limit = min(recursion_limit, DEFAULT_MULTI_TURN_RECURSION_LIMIT) | |
| tasks += take | |
| low_calls += take | |
| high_calls += take * max(1, recursion_limit) | |
| if remaining is not None: | |
| remaining -= take | |
| if scenarios: | |
| caveats.append( | |
| Caveat( | |
| "warning", | |
| "basic_education_call_bound", | |
| "Basic-education prompts are assembled by basic_education_runtime, not by the " | |
| "standard prompt loader. The low call bound is one target-model call per task. " | |
| "The high bound is one target-model call per graph step, capped by the recursion " | |
| "limit the executor writes (20 on multi-turn scenarios, otherwise the template value). " | |
| "Basic-education judge calls and the guided-teaching simulated student are not in the " | |
| "dollar range; with a visitor judge they are billed to the visitor key and count toward " | |
| "the judge and student spending limits. Input tokens for these calls are also omitted.", | |
| ) | |
| ) | |
| caveats.append( | |
| Caveat( | |
| "warning", | |
| "basic_education_key_on_disk", | |
| "The basic-education executor writes the target API key into " | |
| "outputs/basic_education/<run_id>/<model_id>/<scenario>/basic_education_runtime.yaml " | |
| "because the runtime reads it from that file. The dashboard deletes that run directory " | |
| "when the run finishes. A killed process can leave the file behind.", | |
| ) | |
| ) | |
| return tasks, low_calls, high_calls, caveats | |
| def _preflight_caveats(config: ProjectConfig, protocol: VisitorProtocol, selection: RunSelection) -> list[Caveat]: | |
| if not selection.modules or not selection.source_files: | |
| return [] | |
| if selection.max_samples is not None and selection.max_samples <= 0: | |
| return [] | |
| report = PreflightRunner(config).run( | |
| model_id=protocol.model_config.model_id, | |
| modules=set(selection.modules), | |
| subsets=None, | |
| source_files=set(selection.source_files), | |
| dimensions=set(selection.dimensions) or None, | |
| max_samples=selection.max_samples, | |
| judge_enabled=True, | |
| require_real_judges=True, | |
| ) | |
| caveats: list[Caveat] = [] | |
| for issue in report.issues: | |
| caveats.append( | |
| Caveat( | |
| level=issue.level, | |
| code=f"preflight_{issue.code}", | |
| message=english_preflight_message(issue), | |
| ) | |
| ) | |
| return caveats | |
| def _task_mode(config: ProjectConfig, task: str | None) -> str: | |
| if not task: | |
| return "placeholder" | |
| task_config = config.judges.tasks.get(task) | |
| if task_config is None: | |
| return "placeholder" | |
| return task_config.mode | |
| def _judge_endpoint(config: ProjectConfig) -> tuple[str | None, str | None]: | |
| model_id = config.judges.default_judge_model_id | |
| if not model_id or model_id not in config.models: | |
| return model_id, None | |
| return model_id, config.models[model_id].api_base | |
| def _usd(value: float) -> str: | |
| if value >= 0.01: | |
| return f"${value:,.2f}" | |
| if value == 0: | |
| return "$0" | |
| return f"${value:.4f}" | |
| def _per_million(price_per_token: float | None) -> str: | |
| if price_per_token is None: | |
| return "n/a" | |
| return f"${price_per_token * 1_000_000:.4f}" | |
| class _SilentLogger: | |
| def info(self, *args, **kwargs) -> None: | |
| return None | |
| def warning(self, *args, **kwargs) -> None: | |
| return None | |
| def error(self, *args, **kwargs) -> None: | |
| return None | |
| def exception(self, *args, **kwargs) -> None: | |
| return None | |