infospace-bench/src/infospace_bench/routing.py

227 lines
8.4 KiB
Python
Raw Normal View History

IB-WP-0018-T01+T02+T05: routing bridge to llm-connect T01 — task-type taxonomy. docs/routing-task-types.md names the five generation stages as the default identity-mapped task types (summarize-source, extract-entities, extract-relations, evaluate-entity, synthesize-report) and records the recommended quality floors per stage. The taxonomy explicitly does not decide which adapter ships per task type, where the ledger lives, or what a quality score means — those stay with the caller per the LLM-WP-0004 scope guardrail. T02 — RoutingAssistedGenerationAdapter bridge in src/infospace_bench/routing.py. Wraps any llm-connect RoutingPolicy or AdaptiveRoutingPolicy as an infospace-bench AssistedGenerationAdapter: maps stage_id -> task_type (overridable), resolves an LLMAdapter, delegates execute_prompt with a configurable RunConfig, and surfaces the resolved adapter id, task type, model, usage, and finish_reason back on AssistedGenerationResult.metadata. Provider tag stays back-compatible with the strings already used in run records and the budget rollup (openrouter / claude_code / openai / gemini / mock / routing). T05 — eight tests in tests/test_routing_adapter.py cover: static-policy per-stage resolution, stage_to_task_type overrides, default-mapping completeness, fall-through for unmapped stage ids, the adaptive path selecting the cheaper qualifying adapter when a quality_floor is set, adaptive policy falling back to static when no floor is set, response metadata round-trip with provider tagging, and estimated_cost_per_1k pass-through. Adds llm-connect as a path dependency on pyproject.toml and to the pytest pythonpath. Static OpenRouter and fixture paths are unchanged; this commit only adds the option of routing. 139 tests pass, 1 skipped (the OpenRouter live smoke, gated as before). T03 (shadow-mode integration) and T04 (CLI + per-stage chosen-adapter in the generation report) follow next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:33:58 +02:00
"""
Bridge between infospace-bench's ``AssistedGenerationAdapter`` protocol and
llm-connect's ``RoutingPolicy`` / ``AdaptiveRoutingPolicy`` primitives
(LLM-WP-0004). Lets a generation run delegate each stage to a task-typed
route without touching ``workflow.py``.
The mapping from infospace-bench workflow stage ids to llm-connect task
types is the consumer side of LLM-WP-0004's scope guardrail: llm-connect
ships the routing primitives, infospace-bench names the tasks.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from llm_connect.adapter import LLMAdapter
IB-WP-0018-T03+T04: shadow sampling + report/CLI surfacing; close IB-WP-0018 T03 — wrap_with_shadow_sampling() helper in routing.py: builds a llm-connect ShadowingAdapter around any candidate LLMAdapter with a caller-supplied baseline, grader, and QualityLedger. async_shadow=True by default so production load is not doubled; on_shadow_error escape hatch keeps caller logs informed when a baseline outage swallows the shadow path. The returned adapter is still an LLMAdapter so it slots into a RoutingPolicy rule without further code change. T04 — generation report enrichment plus a small CLI helper: - _collect_adapter_choices walks artifact provenance, groups by (stage_id, adapter_id), and surfaces calls + prompt/completion tokens per (stage, adapter) pair in a new ## Per-stage adapter choices section. Runs that did not go through the bridge have no provider_metadata.adapter_id and emit an empty list, so fixture-only reports stay terse. - summarise_quality_ledger() rolls a llm-connect QualityLedger up by (task_type, adapter_id) with mean quality, mean cost, observations, and cumulative tokens. - infospace-bench routing ledger <path> CLI prints the rollup as JSON. Five new tests cover shadow happy-path, shadow failure isolation, ledger rollup, the routing CLI, and the report's adapter-choice aggregation. Closes IB-WP-0018: T01-T05 are all done and the workplan status flips from blocked to done now that LLM-WP-0004's primitives have shipped. 144 tests pass, 1 skipped (the OpenRouter live smoke, gated as before). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:52:05 +02:00
from llm_connect.grading import BaselineGrader
IB-WP-0018-T01+T02+T05: routing bridge to llm-connect T01 — task-type taxonomy. docs/routing-task-types.md names the five generation stages as the default identity-mapped task types (summarize-source, extract-entities, extract-relations, evaluate-entity, synthesize-report) and records the recommended quality floors per stage. The taxonomy explicitly does not decide which adapter ships per task type, where the ledger lives, or what a quality score means — those stay with the caller per the LLM-WP-0004 scope guardrail. T02 — RoutingAssistedGenerationAdapter bridge in src/infospace_bench/routing.py. Wraps any llm-connect RoutingPolicy or AdaptiveRoutingPolicy as an infospace-bench AssistedGenerationAdapter: maps stage_id -> task_type (overridable), resolves an LLMAdapter, delegates execute_prompt with a configurable RunConfig, and surfaces the resolved adapter id, task type, model, usage, and finish_reason back on AssistedGenerationResult.metadata. Provider tag stays back-compatible with the strings already used in run records and the budget rollup (openrouter / claude_code / openai / gemini / mock / routing). T05 — eight tests in tests/test_routing_adapter.py cover: static-policy per-stage resolution, stage_to_task_type overrides, default-mapping completeness, fall-through for unmapped stage ids, the adaptive path selecting the cheaper qualifying adapter when a quality_floor is set, adaptive policy falling back to static when no floor is set, response metadata round-trip with provider tagging, and estimated_cost_per_1k pass-through. Adds llm-connect as a path dependency on pyproject.toml and to the pytest pythonpath. Static OpenRouter and fixture paths are unchanged; this commit only adds the option of routing. 139 tests pass, 1 skipped (the OpenRouter live smoke, gated as before). T03 (shadow-mode integration) and T04 (CLI + per-stage chosen-adapter in the generation report) follow next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:33:58 +02:00
from llm_connect.models import RunConfig
IB-WP-0018-T03+T04: shadow sampling + report/CLI surfacing; close IB-WP-0018 T03 — wrap_with_shadow_sampling() helper in routing.py: builds a llm-connect ShadowingAdapter around any candidate LLMAdapter with a caller-supplied baseline, grader, and QualityLedger. async_shadow=True by default so production load is not doubled; on_shadow_error escape hatch keeps caller logs informed when a baseline outage swallows the shadow path. The returned adapter is still an LLMAdapter so it slots into a RoutingPolicy rule without further code change. T04 — generation report enrichment plus a small CLI helper: - _collect_adapter_choices walks artifact provenance, groups by (stage_id, adapter_id), and surfaces calls + prompt/completion tokens per (stage, adapter) pair in a new ## Per-stage adapter choices section. Runs that did not go through the bridge have no provider_metadata.adapter_id and emit an empty list, so fixture-only reports stay terse. - summarise_quality_ledger() rolls a llm-connect QualityLedger up by (task_type, adapter_id) with mean quality, mean cost, observations, and cumulative tokens. - infospace-bench routing ledger <path> CLI prints the rollup as JSON. Five new tests cover shadow happy-path, shadow failure isolation, ledger rollup, the routing CLI, and the report's adapter-choice aggregation. Closes IB-WP-0018: T01-T05 are all done and the workplan status flips from blocked to done now that LLM-WP-0004's primitives have shipped. 144 tests pass, 1 skipped (the OpenRouter live smoke, gated as before). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:52:05 +02:00
from llm_connect.quality import QualityLedger
IB-WP-0018-T01+T02+T05: routing bridge to llm-connect T01 — task-type taxonomy. docs/routing-task-types.md names the five generation stages as the default identity-mapped task types (summarize-source, extract-entities, extract-relations, evaluate-entity, synthesize-report) and records the recommended quality floors per stage. The taxonomy explicitly does not decide which adapter ships per task type, where the ledger lives, or what a quality score means — those stay with the caller per the LLM-WP-0004 scope guardrail. T02 — RoutingAssistedGenerationAdapter bridge in src/infospace_bench/routing.py. Wraps any llm-connect RoutingPolicy or AdaptiveRoutingPolicy as an infospace-bench AssistedGenerationAdapter: maps stage_id -> task_type (overridable), resolves an LLMAdapter, delegates execute_prompt with a configurable RunConfig, and surfaces the resolved adapter id, task type, model, usage, and finish_reason back on AssistedGenerationResult.metadata. Provider tag stays back-compatible with the strings already used in run records and the budget rollup (openrouter / claude_code / openai / gemini / mock / routing). T05 — eight tests in tests/test_routing_adapter.py cover: static-policy per-stage resolution, stage_to_task_type overrides, default-mapping completeness, fall-through for unmapped stage ids, the adaptive path selecting the cheaper qualifying adapter when a quality_floor is set, adaptive policy falling back to static when no floor is set, response metadata round-trip with provider tagging, and estimated_cost_per_1k pass-through. Adds llm-connect as a path dependency on pyproject.toml and to the pytest pythonpath. Static OpenRouter and fixture paths are unchanged; this commit only adds the option of routing. 139 tests pass, 1 skipped (the OpenRouter live smoke, gated as before). T03 (shadow-mode integration) and T04 (CLI + per-stage chosen-adapter in the generation report) follow next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:33:58 +02:00
from llm_connect.routing import AdaptiveRoutingPolicy, RoutingPolicy
IB-WP-0018-T03+T04: shadow sampling + report/CLI surfacing; close IB-WP-0018 T03 — wrap_with_shadow_sampling() helper in routing.py: builds a llm-connect ShadowingAdapter around any candidate LLMAdapter with a caller-supplied baseline, grader, and QualityLedger. async_shadow=True by default so production load is not doubled; on_shadow_error escape hatch keeps caller logs informed when a baseline outage swallows the shadow path. The returned adapter is still an LLMAdapter so it slots into a RoutingPolicy rule without further code change. T04 — generation report enrichment plus a small CLI helper: - _collect_adapter_choices walks artifact provenance, groups by (stage_id, adapter_id), and surfaces calls + prompt/completion tokens per (stage, adapter) pair in a new ## Per-stage adapter choices section. Runs that did not go through the bridge have no provider_metadata.adapter_id and emit an empty list, so fixture-only reports stay terse. - summarise_quality_ledger() rolls a llm-connect QualityLedger up by (task_type, adapter_id) with mean quality, mean cost, observations, and cumulative tokens. - infospace-bench routing ledger <path> CLI prints the rollup as JSON. Five new tests cover shadow happy-path, shadow failure isolation, ledger rollup, the routing CLI, and the report's adapter-choice aggregation. Closes IB-WP-0018: T01-T05 are all done and the workplan status flips from blocked to done now that LLM-WP-0004's primitives have shipped. 144 tests pass, 1 skipped (the OpenRouter live smoke, gated as before). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:52:05 +02:00
from llm_connect.shadowing import ShadowingAdapter
IB-WP-0018-T01+T02+T05: routing bridge to llm-connect T01 — task-type taxonomy. docs/routing-task-types.md names the five generation stages as the default identity-mapped task types (summarize-source, extract-entities, extract-relations, evaluate-entity, synthesize-report) and records the recommended quality floors per stage. The taxonomy explicitly does not decide which adapter ships per task type, where the ledger lives, or what a quality score means — those stay with the caller per the LLM-WP-0004 scope guardrail. T02 — RoutingAssistedGenerationAdapter bridge in src/infospace_bench/routing.py. Wraps any llm-connect RoutingPolicy or AdaptiveRoutingPolicy as an infospace-bench AssistedGenerationAdapter: maps stage_id -> task_type (overridable), resolves an LLMAdapter, delegates execute_prompt with a configurable RunConfig, and surfaces the resolved adapter id, task type, model, usage, and finish_reason back on AssistedGenerationResult.metadata. Provider tag stays back-compatible with the strings already used in run records and the budget rollup (openrouter / claude_code / openai / gemini / mock / routing). T05 — eight tests in tests/test_routing_adapter.py cover: static-policy per-stage resolution, stage_to_task_type overrides, default-mapping completeness, fall-through for unmapped stage ids, the adaptive path selecting the cheaper qualifying adapter when a quality_floor is set, adaptive policy falling back to static when no floor is set, response metadata round-trip with provider tagging, and estimated_cost_per_1k pass-through. Adds llm-connect as a path dependency on pyproject.toml and to the pytest pythonpath. Static OpenRouter and fixture paths are unchanged; this commit only adds the option of routing. 139 tests pass, 1 skipped (the OpenRouter live smoke, gated as before). T03 (shadow-mode integration) and T04 (CLI + per-stage chosen-adapter in the generation report) follow next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:33:58 +02:00
from .workflow import AssistedGenerationRequest, AssistedGenerationResult
# Default identity mapping: every generation stage shipped by the
# generic-source profile is its own task type. Callers can override
# individual stages via the ``stage_to_task_type`` field — for example to
# collapse ``extract-entities`` and ``extract-relations`` into a single
# ``extraction`` route, or to widen ``evaluate-entity`` to ``judge``.
STAGE_TO_TASK_TYPE_DEFAULT: dict[str, str] = {
"summarize-source": "summarize-source",
"extract-entities": "extract-entities",
"extract-relations": "extract-relations",
"evaluate-entity": "evaluate-entity",
"synthesize-report": "synthesize-report",
}
@dataclass(frozen=True)
class RoutingAssistedGenerationAdapter:
"""Route assisted-generation requests through an llm-connect policy.
On each ``generate(request)`` call:
1. Resolves ``task_type`` from ``request.stage_id`` (overridable via
``stage_to_task_type``; default falls back to the stage id itself).
2. Asks the policy for an adapter. When the policy is an
``AdaptiveRoutingPolicy`` and ``quality_floor`` is set, the
adaptive path is used; otherwise the policy resolves statically.
3. Calls the resolved llm-connect ``LLMAdapter.execute_prompt`` with a
``RunConfig`` built from ``default_run_config``.
4. Maps the ``LLMResponse`` back to an ``AssistedGenerationResult``
and preserves model, usage, finish_reason, and the resolved
task_type / adapter_id in ``metadata``.
"""
policy: RoutingPolicy
stage_to_task_type: dict[str, str] = field(default_factory=dict)
default_run_config: RunConfig = field(default_factory=RunConfig)
quality_floor: float | None = None
estimated_cost_per_1k: float | None = None
def generate(
self, request: AssistedGenerationRequest
) -> AssistedGenerationResult:
task_type = self._task_type_for(request.stage_id)
adapter = self._resolve(task_type)
response = adapter.execute_prompt(request.prompt, self.default_run_config)
adapter_id = _identify_adapter(adapter)
metadata: dict[str, Any] = {
"task_type": task_type,
"adapter_id": adapter_id,
"model": response.model or self.default_run_config.model_name,
"usage": dict(response.usage or {}),
"finish_reason": response.finish_reason,
}
if response.metadata:
metadata.update(response.metadata)
return AssistedGenerationResult(
markdown=response.content,
provider=_provider_tag(adapter),
metadata=metadata,
)
def _resolve(self, task_type: str) -> LLMAdapter:
if isinstance(self.policy, AdaptiveRoutingPolicy) and self.quality_floor is not None:
return self.policy.resolve(
task_type,
estimated_cost_per_1k=self.estimated_cost_per_1k,
quality_floor=self.quality_floor,
)
return self.policy.resolve(
task_type,
estimated_cost_per_1k=self.estimated_cost_per_1k,
)
def _task_type_for(self, stage_id: str) -> str:
merged = dict(STAGE_TO_TASK_TYPE_DEFAULT)
merged.update(self.stage_to_task_type)
return merged.get(stage_id, stage_id)
def _identify_adapter(adapter: LLMAdapter) -> str:
"""Best-effort stable id for an llm-connect adapter instance.
Prefers an explicit ``adapter_id`` attribute (some adapters set it),
falls back to ``{class_name}:{model_attr}`` when a model attribute is
present, otherwise just the class name.
"""
adapter_id = getattr(adapter, "adapter_id", "")
if adapter_id:
return str(adapter_id)
IB-WP-0020-T03: routing CLI flags Add --provider routing, --routing-config <yaml>, and --quality-floor <float> to generate run, generate resume, and generate from-source. The CLI flag wiring constructs a RoutingAssistedGenerationAdapter from the parsed config, with the workspace handed in so any ledger_path in the config resolves relative to it. --quality-floor overrides the config-level default_quality_floor for a single invocation. run_generation gains routing_config + quality_floor kwargs and _adapter_for grew a "routing" branch. Missing --routing-config with --provider routing fails fast with InfospaceError("missing_routing_config"); missing API key for any candidate fails fast with InfospaceError("missing_routing_api_key"). Two small bug fixes surfaced while writing T03: - routing._identify_adapter now also reads ``_model`` from llm-connect adapters (their public attribute is private), so the per-stage adapter-choice line shows the model id rather than just the class name. - budget.TOKEN_EVENTS_PATH corrected from /state/token-events to the state-hub HTTP endpoint /token-events/ that actually exists; the failure-isolation in emit_token_event already kept the prior typo from breaking runs, but the hub never saw the events. Five new tests cover: _adapter_for refusal on missing config, _adapter_for happy path, run_generation end-to-end through routing with a stubbed OpenRouterAdapter.execute_prompt (no network), workspace-relative ledger resolution, and a CLI subprocess smoke asserting fast-fail on missing API key. 173 tests pass, 1 skipped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:08:51 +02:00
model = (
getattr(adapter, "model", "")
or getattr(adapter, "model_name", "")
or getattr(adapter, "_model", "")
)
IB-WP-0018-T01+T02+T05: routing bridge to llm-connect T01 — task-type taxonomy. docs/routing-task-types.md names the five generation stages as the default identity-mapped task types (summarize-source, extract-entities, extract-relations, evaluate-entity, synthesize-report) and records the recommended quality floors per stage. The taxonomy explicitly does not decide which adapter ships per task type, where the ledger lives, or what a quality score means — those stay with the caller per the LLM-WP-0004 scope guardrail. T02 — RoutingAssistedGenerationAdapter bridge in src/infospace_bench/routing.py. Wraps any llm-connect RoutingPolicy or AdaptiveRoutingPolicy as an infospace-bench AssistedGenerationAdapter: maps stage_id -> task_type (overridable), resolves an LLMAdapter, delegates execute_prompt with a configurable RunConfig, and surfaces the resolved adapter id, task type, model, usage, and finish_reason back on AssistedGenerationResult.metadata. Provider tag stays back-compatible with the strings already used in run records and the budget rollup (openrouter / claude_code / openai / gemini / mock / routing). T05 — eight tests in tests/test_routing_adapter.py cover: static-policy per-stage resolution, stage_to_task_type overrides, default-mapping completeness, fall-through for unmapped stage ids, the adaptive path selecting the cheaper qualifying adapter when a quality_floor is set, adaptive policy falling back to static when no floor is set, response metadata round-trip with provider tagging, and estimated_cost_per_1k pass-through. Adds llm-connect as a path dependency on pyproject.toml and to the pytest pythonpath. Static OpenRouter and fixture paths are unchanged; this commit only adds the option of routing. 139 tests pass, 1 skipped (the OpenRouter live smoke, gated as before). T03 (shadow-mode integration) and T04 (CLI + per-stage chosen-adapter in the generation report) follow next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:33:58 +02:00
name = type(adapter).__name__
if model:
return f"{name}:{model}"
return name
IB-WP-0018-T03+T04: shadow sampling + report/CLI surfacing; close IB-WP-0018 T03 — wrap_with_shadow_sampling() helper in routing.py: builds a llm-connect ShadowingAdapter around any candidate LLMAdapter with a caller-supplied baseline, grader, and QualityLedger. async_shadow=True by default so production load is not doubled; on_shadow_error escape hatch keeps caller logs informed when a baseline outage swallows the shadow path. The returned adapter is still an LLMAdapter so it slots into a RoutingPolicy rule without further code change. T04 — generation report enrichment plus a small CLI helper: - _collect_adapter_choices walks artifact provenance, groups by (stage_id, adapter_id), and surfaces calls + prompt/completion tokens per (stage, adapter) pair in a new ## Per-stage adapter choices section. Runs that did not go through the bridge have no provider_metadata.adapter_id and emit an empty list, so fixture-only reports stay terse. - summarise_quality_ledger() rolls a llm-connect QualityLedger up by (task_type, adapter_id) with mean quality, mean cost, observations, and cumulative tokens. - infospace-bench routing ledger <path> CLI prints the rollup as JSON. Five new tests cover shadow happy-path, shadow failure isolation, ledger rollup, the routing CLI, and the report's adapter-choice aggregation. Closes IB-WP-0018: T01-T05 are all done and the workplan status flips from blocked to done now that LLM-WP-0004's primitives have shipped. 144 tests pass, 1 skipped (the OpenRouter live smoke, gated as before). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:52:05 +02:00
def wrap_with_shadow_sampling(
*,
candidate: LLMAdapter,
baseline: LLMAdapter,
grader: BaselineGrader,
ledger: QualityLedger,
task_type: str,
adapter_id: str | None = None,
baseline_adapter_id: str | None = None,
shadow_rate: float = 0.1,
async_shadow: bool = True,
on_shadow_error: Any | None = None,
) -> ShadowingAdapter:
"""Wrap ``candidate`` with llm-connect's ``ShadowingAdapter``.
Sampled baseline grading collects QualityLedger observations without
changing the response the caller sees. Errors in the shadow path
(baseline outage, grader failure, ledger write error) never alter the
candidate response failures land on ``on_shadow_error`` when
provided, else are silently swallowed by the underlying adapter.
The returned ``ShadowingAdapter`` is still an ``LLMAdapter``, so it
can be slotted into a ``RoutingPolicy`` rule and used through
``RoutingAssistedGenerationAdapter`` without further changes.
"""
return ShadowingAdapter(
candidate_adapter=candidate,
baseline_adapter=baseline,
grader=grader,
ledger=ledger,
task_type=task_type,
adapter_id=adapter_id or _identify_adapter(candidate),
baseline_adapter_id=baseline_adapter_id or _identify_adapter(baseline),
shadow_rate=shadow_rate,
async_shadow=async_shadow,
on_shadow_error=on_shadow_error,
)
def summarise_quality_ledger(
ledger_path: str | Any,
) -> list[dict[str, Any]]:
"""Roll up a QualityLedger into one row per (task_type, adapter_id).
Useful as a CLI helper or a quick budget-style inspection without
loading llm-connect's full ledger API at the call site.
"""
from pathlib import Path
ledger = QualityLedger(path=Path(ledger_path))
observations = ledger.read_all()
grouped: dict[tuple[str, str], dict[str, Any]] = {}
for obs in observations:
key = (obs.task_type, obs.adapter_id)
bucket = grouped.setdefault(
key,
{
"task_type": obs.task_type,
"adapter_id": obs.adapter_id,
"observations": 0,
"mean_quality": 0.0,
"mean_cost_usd": 0.0,
"total_tokens_in": 0,
"total_tokens_out": 0,
},
)
bucket["observations"] += 1
bucket["mean_quality"] += float(obs.quality_score)
bucket["mean_cost_usd"] += float(obs.cost_usd)
bucket["total_tokens_in"] += int(getattr(obs, "tokens_in", 0) or 0)
bucket["total_tokens_out"] += int(getattr(obs, "tokens_out", 0) or 0)
rows: list[dict[str, Any]] = []
for bucket in grouped.values():
count = bucket["observations"]
if count:
bucket["mean_quality"] = round(bucket["mean_quality"] / count, 4)
bucket["mean_cost_usd"] = round(bucket["mean_cost_usd"] / count, 6)
rows.append(bucket)
rows.sort(key=lambda row: (row["task_type"], row["adapter_id"]))
return rows
IB-WP-0018-T01+T02+T05: routing bridge to llm-connect T01 — task-type taxonomy. docs/routing-task-types.md names the five generation stages as the default identity-mapped task types (summarize-source, extract-entities, extract-relations, evaluate-entity, synthesize-report) and records the recommended quality floors per stage. The taxonomy explicitly does not decide which adapter ships per task type, where the ledger lives, or what a quality score means — those stay with the caller per the LLM-WP-0004 scope guardrail. T02 — RoutingAssistedGenerationAdapter bridge in src/infospace_bench/routing.py. Wraps any llm-connect RoutingPolicy or AdaptiveRoutingPolicy as an infospace-bench AssistedGenerationAdapter: maps stage_id -> task_type (overridable), resolves an LLMAdapter, delegates execute_prompt with a configurable RunConfig, and surfaces the resolved adapter id, task type, model, usage, and finish_reason back on AssistedGenerationResult.metadata. Provider tag stays back-compatible with the strings already used in run records and the budget rollup (openrouter / claude_code / openai / gemini / mock / routing). T05 — eight tests in tests/test_routing_adapter.py cover: static-policy per-stage resolution, stage_to_task_type overrides, default-mapping completeness, fall-through for unmapped stage ids, the adaptive path selecting the cheaper qualifying adapter when a quality_floor is set, adaptive policy falling back to static when no floor is set, response metadata round-trip with provider tagging, and estimated_cost_per_1k pass-through. Adds llm-connect as a path dependency on pyproject.toml and to the pytest pythonpath. Static OpenRouter and fixture paths are unchanged; this commit only adds the option of routing. 139 tests pass, 1 skipped (the OpenRouter live smoke, gated as before). T03 (shadow-mode integration) and T04 (CLI + per-stage chosen-adapter in the generation report) follow next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 11:33:58 +02:00
def _provider_tag(adapter: LLMAdapter) -> str:
"""Coarse provider tag matching the strings already used in run records.
Returns ``openrouter`` / ``claude_code`` / ``openai`` / ``gemini`` /
``routing`` so existing tooling (budget rollup buckets, archive
metadata) keeps its bucket keys stable.
"""
name = type(adapter).__name__.lower()
if "openrouter" in name:
return "openrouter"
if "claudecode" in name or "claude_code" in name:
return "claude_code"
if "openai" in name:
return "openai"
if "gemini" in name:
return "gemini"
if "mock" in name or "static" in name:
return "mock"
return "routing"