Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## [0.2.0] - Unreleased

### Added

- Add a dry-run-default OpenRouter free-model canary. Its opt-in live mode
requires explicit request, output-token, timeout, evidence-path, and retention
bounds and selects only freshly discovered, completely zero-priced chat rows.

### Fixed

- OpenRouter discovery no longer marks the entire credential account
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,26 @@ Model-based conduct verification requires `fast-mlsirm` in the same runtime and
The agent pool is manageable at runtime: `POST`/`PATCH`/`DELETE` on `/api/v1/agent_pools/default/worker_agents[/{id}]` add, govern, and remove model-group members. Pass `--agents-db PATH` (or `CONTEXTUAL_ORCHESTRATOR_AGENTS_DB`) to persist those changes to a stdlib sqlite file — stored changes overlay the seed agents file at startup, and removals write disabled tombstones so they survive restarts; without it the pool is in-memory as before.
Beyond the local MLX/llama.cpp discovery above, `python -m contextual_orchestrator discover-models [--agents-db PATH]` discovers models from remote providers (OpenAI, OpenRouter, NVIDIA NIM ×2 keys, Bytez, and an allowlisted OpenAI-compatible gateway) for any subset of their KV-registered credentials, and can persist them into the same `--agents-db` sqlite file, added disabled by default. See [docs/kv-credentials.md](docs/kv-credentials.md#multi-provider-auto-discovery) for the credential-name table and cost-based auto-selection.

`python -m contextual_orchestrator openrouter-free-canary` is a dry-run-only
catalog check by default. It selects no pinned model: the lexically first
current OpenRouter chat row with explicit zero prompt and completion prices,
USD currency, and no incomparable unit price is reported without issuing a
completion. Live mode is deliberately verbose and unscheduled:

```bash
python -m contextual_orchestrator openrouter-free-canary --live \
--max-requests 1 --max-output-tokens 8 --timeout-seconds 10 \
--evidence-output ./openrouter-canary.json --retention-days 7
```

Every live cap and the evidence path/retention choice is mandatory. The command
uses only the KV-registered `OPENROUTER_API_KEY`, disables retries, makes one
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fixed prompt request, and writes atomic JSON evidence containing neither the
prompt, response, nor credential. Before transport it removes expired evidence
at the same output path and proves that a mode-`0600` atomic write is possible;
retention cleanup therefore runs whenever the canary is invoked. No workflow
invokes live mode automatically.

Seed the credential into the KV once at bootstrap:

```bash
Expand Down
33 changes: 33 additions & 0 deletions contextual_orchestrator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import logging
import os
from pathlib import Path
import sys
from dataclasses import replace

Expand Down Expand Up @@ -35,6 +36,7 @@
TaskOrchestrator,
load_agents,
)
from .openrouter_canary import OpenRouterCanaryLimits, run_openrouter_free_canary
from .privacy_policy_analysis import (
analyze_discovered_privacy_policies,
)
Expand Down Expand Up @@ -490,6 +492,37 @@ def main(argv: list[str] | None = None) -> None:
if arguments and arguments[0] == "discover-models":
_discover_models_command(arguments[1:])
return
if arguments and arguments[0] == "openrouter-free-canary":
parser = argparse.ArgumentParser(
prog="python -m contextual_orchestrator openrouter-free-canary"
)
parser.add_argument("--live", action="store_true")
parser.add_argument("--max-requests", type=_positive_int)
parser.add_argument("--max-output-tokens", type=_positive_int)
parser.add_argument("--timeout-seconds", type=_positive_int)
parser.add_argument("--evidence-output")
parser.add_argument("--retention-days", type=_positive_int)
args = parser.parse_args(arguments[1:])
supplied = (
args.max_requests,
args.max_output_tokens,
args.timeout_seconds,
args.retention_days,
)
if args.live and (
any(value is None for value in supplied) or not args.evidence_output
):
parser.error(
"--live requires positive request/output-token/timeout/retention caps and --evidence-output"
)
limits = OpenRouterCanaryLimits(*supplied) if args.live else None
result = run_openrouter_free_canary(
live=args.live,
limits=limits,
evidence_output=Path(args.evidence_output) if args.evidence_output else None,
)
print(json.dumps(result, sort_keys=True))
return
if arguments and arguments[0] == "check-fast-mlsirm":
_check_fast_mlsirm_command()
return
Expand Down
187 changes: 187 additions & 0 deletions contextual_orchestrator/openrouter_canary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"""Bounded, opt-in evidence for one currently free OpenRouter model."""
Comment thread
seonghobae marked this conversation as resolved.

from __future__ import annotations

from dataclasses import asdict, dataclass
import json
import os
from pathlib import Path
import stat
import tempfile
import time
from typing import Any, Callable

from .credentials import get_credential
from .model_discovery import (
DiscoveredModel,
PROVIDER_MODEL_SOURCES,
_deduplicate_discovered_models,
_requires_non_text_input,
agent_from_discovered,
discover_provider_models,
is_discovered_chat_candidate,
)
from .orchestrator import ModelClient


class OpenRouterCanaryError(RuntimeError):
"""Raised before transport when the canary contract is incomplete."""


@dataclass(frozen=True)
class OpenRouterCanaryLimits:
"""Explicit operator caps for the optional live request."""

max_requests: int
max_output_tokens: int
timeout_seconds: int
retention_days: int

def validate(self) -> None:
"""Reject absent, boolean, or non-positive cap values."""
for name, value in asdict(self).items():
if type(value) is not int or value < 1:
raise OpenRouterCanaryError(f"{name} must be a positive integer")


def _eligible(models: list[DiscoveredModel]) -> list[DiscoveredModel]:
"""Return price-complete, text-chat OpenRouter rows in stable order."""
return sorted(
(
model
for model in models
if model.provider_name == "openrouter"
and model.prompt_price_per_1k == 0.0
and model.completion_price_per_1k == 0.0
and model.is_free is True
and not model.unit_prices
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
and model.currency_code == "USD"
and not model.evidence_only
and model.spend_admitted
and is_discovered_chat_candidate(model)
Comment thread
seonghobae marked this conversation as resolved.
and not _requires_non_text_input(model)
Comment thread
seonghobae marked this conversation as resolved.
),
key=lambda model: model.model_id,
)


def _write_evidence(path: Path, evidence: dict[str, Any]) -> None:
"""Atomically publish one secret- and prompt-free JSON evidence document."""
path.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(
prefix=f".{path.name}.", dir=path.parent
)
try:
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
json.dump(evidence, stream, sort_keys=True)
stream.write("\n")
os.replace(temporary_name, path)
except BaseException:
try:
os.unlink(temporary_name)
except FileNotFoundError:
pass
raise


def _prepare_evidence_path(path: Path, current_time: int) -> None:
"""Remove expired prior evidence and prove a secure atomic write is possible."""
try:
mode = path.lstat().st_mode
except FileNotFoundError:
mode = None
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
if mode is not None and not stat.S_ISREG(mode):
raise OpenRouterCanaryError("evidence output must be a regular file path")
try:
prior = json.loads(path.read_text(encoding="utf-8"))
Comment thread
seonghobae marked this conversation as resolved.
Outdated
except (FileNotFoundError, OSError, UnicodeError, json.JSONDecodeError):
prior = None
if (
isinstance(prior, dict)
and prior.get("provider") == "openrouter"
and type(prior.get("expires_at")) is int
and prior["expires_at"] <= current_time
):
path.unlink(missing_ok=True)
path.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(
prefix=f".{path.name}.preflight.", dir=path.parent
)
os.close(descriptor)
os.unlink(temporary_name)


def run_openrouter_free_canary(
*,
live: bool,
limits: OpenRouterCanaryLimits | None = None,
evidence_output: Path | None = None,
discover: Callable[..., list[DiscoveredModel]] = discover_provider_models,
client_factory: Callable[..., ModelClient] = ModelClient,
now: Callable[[], float] = time.time,
) -> dict[str, Any]:
"""Discover a current free candidate and optionally issue one bounded request."""
source = next(
item for item in PROVIDER_MODEL_SOURCES if item.provider_name == "openrouter"
)
if not get_credential(source.credential_name):
raise OpenRouterCanaryError(
"OPENROUTER_API_KEY is unavailable in the KV registry"
)
if live:
if limits is None or evidence_output is None:
raise OpenRouterCanaryError(
"live mode requires all caps and an evidence output path"
)
limits.validate()
discovered_at = int(now())
Comment thread
seonghobae marked this conversation as resolved.
Outdated
if live:
assert evidence_output is not None
try:
_prepare_evidence_path(evidence_output, discovered_at)
except OSError as exc:
raise OpenRouterCanaryError("evidence output is not writable") from exc
candidates = _eligible(
_deduplicate_discovered_models(
discover(source, timeout=limits.timeout_seconds if limits else 10)
)
Comment thread
seonghobae marked this conversation as resolved.
)
Comment thread
seonghobae marked this conversation as resolved.
if not candidates:
raise OpenRouterCanaryError(
"current discovery has no unambiguous zero-price chat candidate"
)
selected = candidates[0]
evidence: dict[str, Any] = {
"schema_version": 1,
"mode": "live" if live else "dry_run",
"provider": "openrouter",
"model_id": selected.model_id,
"discovered_at": discovered_at,
"price_evidence": {
"prompt_price_per_1k": 0.0,
"completion_price_per_1k": 0.0,
"currency_code": "USD",
},
"request_count": 0,
}
if live:
assert limits is not None and evidence_output is not None
client = client_factory(
timeout=limits.timeout_seconds,
max_output_tokens=limits.max_output_tokens,
max_retries=0,
temperature=0.0,
)
client.chat(
agent_from_discovered(selected), [{"role": "user", "content": "Reply OK."}]
)
Comment thread
seonghobae marked this conversation as resolved.
Outdated
Comment thread
seonghobae marked this conversation as resolved.
Outdated
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated
evidence.update(
{
"request_count": 1,
"limits": asdict(limits),
"expires_at": discovered_at + limits.retention_days * 86400,
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
"outcome": "completed",
}
)
_write_evidence(evidence_output, evidence)
return evidence
9 changes: 6 additions & 3 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -2102,9 +2102,12 @@ Buyer-visible gaps now prioritized:
keyboard/native-form REST editor; DB membership is normalized and legacy JSON
membership migrates without data loss. Authenticated deployed-browser runtime
evidence remains a release/UAT gate rather than an implementation gap.
3. Free-model tests are deterministic catalog-contract tests. Add an opt-in,
spend-capped live OpenRouter canary selected from current zero-price metadata;
never pin a transient free model identifier in production or CI.
3. **Implemented in the current product-gap branch:** the OpenRouter free-model
canary is dry-run-only by default and selects a current chat row only when
prompt and completion prices are explicitly zero and comparable. Live mode
is unscheduled and requires positive request, output-token, timeout, evidence
retention, and output-path choices; it disables retries, pins no model id,
and persists neither credentials, prompts, nor responses.
4. Multi-instance routing observations remain process-local. Add a time-windowed
durable observation model with calibrated decay before horizontal scaling.
5. Protected main, not a feature-stack merge, remains the release boundary; do
Expand Down
Loading
Loading