From ec3b3c85d44257c32a4022dea531f0a2e9111bbd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:34:55 +0900 Subject: [PATCH 01/18] feat: add purpose-limited PII event protection --- contextual_orchestrator/orchestrator.py | 94 ++++- contextual_orchestrator/pii_protection.py | 180 ++++++++++ contextual_orchestrator/server.py | 68 +++- docs/library_research.md | 18 +- .../0024-purpose-limited-pii-protection.md | 81 +++++ pyproject.toml | 1 + requirements.lock | 330 +++++++++++++----- tests/test_pii_protection.py | 160 +++++++++ 8 files changed, 825 insertions(+), 107 deletions(-) create mode 100644 contextual_orchestrator/pii_protection.py create mode 100644 docs/planning/adrs/0024-purpose-limited-pii-protection.md create mode 100644 tests/test_pii_protection.py diff --git a/contextual_orchestrator/orchestrator.py b/contextual_orchestrator/orchestrator.py index f353bae5e..936ef89b8 100644 --- a/contextual_orchestrator/orchestrator.py +++ b/contextual_orchestrator/orchestrator.py @@ -33,6 +33,12 @@ from .conventions import require_object_name from .credentials import NotConfigured, get_credential +from .pii_protection import ( + DEFAULT_PII_KEY_NAME, + ENCRYPTED_FIELDS_KEY, + is_encrypted_detail, + load_pii_encryptor, +) # content is usually str; multimodal vision messages use OpenAI content-parts lists. @@ -1667,6 +1673,7 @@ def __init__( agents_db: str | None = None, cache_ttl: float = 0.0, cache_max_entries: int = 256, + pii_key_name: str = DEFAULT_PII_KEY_NAME, ) -> None: # Optional durable model-group management: stored operator changes overlay the # seed agents file at startup (stored rows win by id; stored-new rows append). @@ -1702,6 +1709,9 @@ def __init__( # Optional durable persistence: default None keeps all state purely in-memory # (zero behavior change). When set, runs/audit/analytics survive restart. self._store = _StateStore(state_db) if state_db else None + if not isinstance(pii_key_name, str) or not pii_key_name: + raise ValueError("pii_key_name must be a non-empty string") + self._pii_key_name = pii_key_name self._commercial_report_cache_local = threading.local() if self._store is not None: self._reload_state() @@ -2708,16 +2718,48 @@ def _judge_verifier_output(self, verifier_output: str, thinker_output: str, work "verifier_output": verifier_output, } - def _append_audit_event(self, event_type: str, detail: dict[str, Any]) -> None: + def _protected_event_detail(self, detail: dict[str, Any], pii_fields: Iterable[str]) -> dict[str, Any]: + """Encrypt explicitly declared PII fields before an event enters memory or storage.""" + fields = tuple(pii_fields) + if not fields: + return detail + return load_pii_encryptor(self._pii_key_name).encrypt_fields(detail, fields) + + def _append_audit_event( + self, + event_type: str, + detail: dict[str, Any], + *, + pii_fields: Iterable[str] = (), + ) -> None: event = { "created_at": int(time.time()), "event_type": event_type, - "event_detail": detail, + "event_detail": self._protected_event_detail(detail, pii_fields), } self._audit_events.append(event) if self._store is not None: self._store.save("audit", None, event) + def record_authorization_decision( + self, + *, + scope: str, + purpose: str, + allowed: bool, + reason: str, + ) -> None: + """Record a secret-free role/purpose authorization decision.""" + self._append_audit_event( + "authorization_decision", + { + "scope": scope, + "purpose": purpose, + "allowed": bool(allowed), + "reason": reason, + }, + ) + def _infer_provider_name(self, base_url: str) -> str: if base_url.startswith("mock://"): return f"mock-{base_url.removeprefix('mock://')}" @@ -2805,8 +2847,15 @@ def list_recent_runs(self, page_number: int = 1, page_size: int = 10) -> list[di run_ids = list(self._run_order)[start:end] return [self._workflow_runs[run_id] for run_id in run_ids] - def list_recent_audit_events(self, page_number: int = 1, page_size: int = 25) -> list[dict[str, Any]]: - """Return recent audit events in newest-first order.""" + def list_recent_audit_events( + self, + page_number: int = 1, + page_size: int = 25, + *, + role: str | None = None, + purpose: str | None = None, + ) -> list[dict[str, Any]]: + """Return recent audit events, decrypting PII only for authorized replay.""" if page_number < 1 or page_size < 1: # pragma: no cover raise ValueError("page_number/page_size must be >= 1") events = list(self._audit_events) @@ -2815,15 +2864,40 @@ def list_recent_audit_events(self, page_number: int = 1, page_size: int = 25) -> total = len(events) left = max(0, total - end) right = max(0, total - start) - return list(reversed(events[left:right])) - - def record_analytics_event(self, event_name: str, detail: dict[str, Any]) -> None: + selected = list(reversed(events[left:right])) + if role != "admin" or purpose != "audit_replay": + return selected + restored: list[dict[str, Any]] = [] + encryptors: dict[str, Any] = {} + for event in selected: + detail = event.get("event_detail") + if not is_encrypted_detail(detail): + restored.append(event) + continue + restored_event = dict(event) + metadata = detail.get(ENCRYPTED_FIELDS_KEY) + key_name = metadata.get("key_name") if isinstance(metadata, dict) else self._pii_key_name + encryptor = encryptors.get(key_name) + if encryptor is None: + encryptor = load_pii_encryptor(key_name) + encryptors[key_name] = encryptor + restored_event["event_detail"] = encryptor.decrypt_fields(detail) + restored.append(restored_event) + return restored + + def record_analytics_event( + self, + event_name: str, + detail: dict[str, Any], + *, + pii_fields: Iterable[str] = (), + ) -> None: """Record a compact in-memory analytics event without prompt or output text.""" require_object_name(event_name, "analytics.event_name") event = { "event_time": int(time.time()), "event_name": event_name, - "event_detail": redact_value(detail), + "event_detail": redact_value(self._protected_event_detail(detail, pii_fields)), } self._analytics_events.append(event) if self._store is not None: @@ -9006,7 +9080,7 @@ def section( }, } - def admin_state(self) -> dict[str, Any]: + def admin_state(self, *, role: str | None = None, purpose: str | None = None) -> dict[str, Any]: """Build the admin console state payload from agents, policy, and audit data.""" agent_page_size = max(1, len(self.candidates)) return { @@ -9017,7 +9091,7 @@ def admin_state(self) -> dict[str, Any]: "complex_hints": list(self.COMPLEX_HINTS), }, "recent_workflow_runs": [self._shorten_run(run) for run in self.list_recent_runs(page_size=max(1, len(self._run_order)))], - "recent_audit_events": self.list_recent_audit_events(), + "recent_audit_events": self.list_recent_audit_events(role=role, purpose=purpose), "spend": self.spend_analytics(), } diff --git a/contextual_orchestrator/pii_protection.py b/contextual_orchestrator/pii_protection.py new file mode 100644 index 000000000..28becdc6f --- /dev/null +++ b/contextual_orchestrator/pii_protection.py @@ -0,0 +1,180 @@ +"""Purpose-limited access and field-level protection for stored event data.""" + +from __future__ import annotations + +import base64 +import binascii +import json +import os +from collections.abc import Iterable +from dataclasses import dataclass +from typing import Any + +from cryptography.exceptions import InvalidTag +from cryptography.hazmat.primitives.ciphers.aead import AESGCM + +from .credentials import get_credential + + +ENCRYPTED_FIELDS_KEY = "__encrypted_fields__" +ENCRYPTED_FIELDS_VERSION = 1 +ENCRYPTED_FIELDS_ALGORITHM = "AES-256-GCM" +DEFAULT_PII_KEY_NAME = "CONTEXTUAL_ORCHESTRATOR_PII_ENCRYPTION_KEY" +PURPOSES_BY_SCOPE = { + "inference": frozenset({"message_delivery"}), + "admin": frozenset({"operator_read", "audit_replay"}), +} +DEFAULT_PURPOSE_BY_SCOPE = { + "inference": "message_delivery", + "admin": "operator_read", +} + + +class PiiProtectionError(ValueError): + """Raised when marked PII cannot be safely protected or restored.""" + + +def _decode_secret(secret: str) -> bytes: + """Decode a 256-bit key supplied as base64, hex, or exactly 32 bytes.""" + if not isinstance(secret, str) or not secret: + raise PiiProtectionError("PII encryption key is empty") + if secret.startswith("hex:"): + try: + decoded = bytes.fromhex(secret[4:]) + except ValueError as exc: + raise PiiProtectionError("PII encryption key is not valid hex") from exc + elif secret.startswith("base64:"): + try: + decoded = base64.urlsafe_b64decode(secret[7:] + "=" * (-len(secret[7:]) % 4)) + except (binascii.Error, ValueError) as exc: + raise PiiProtectionError("PII encryption key is not valid base64") from exc + else: + decoded = secret.encode("utf-8") + if len(decoded) != 32: + try: + decoded = base64.urlsafe_b64decode(secret + "=" * (-len(secret) % 4)) + except (binascii.Error, ValueError): + decoded = b"" + if len(decoded) != 32: + raise PiiProtectionError("PII encryption key must decode to 32 bytes") + return decoded + + +def _b64encode(value: bytes) -> str: + """Encode binary ciphertext metadata as URL-safe base64.""" + return base64.urlsafe_b64encode(value).decode("ascii") + + +def _b64decode(value: Any) -> bytes: + """Decode strict URL-safe base64 metadata.""" + if not isinstance(value, str): + raise PiiProtectionError("encrypted field metadata is invalid") + try: + return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) + except (binascii.Error, ValueError) as exc: + raise PiiProtectionError("encrypted field metadata is invalid") from exc + + +def _field_names(fields: Iterable[str]) -> tuple[str, ...]: + """Validate and de-duplicate declared top-level field names.""" + names: list[str] = [] + for field in fields: + if not isinstance(field, str) or not field or field == ENCRYPTED_FIELDS_KEY: + raise PiiProtectionError("PII field names must be non-empty strings") + if field not in names: + names.append(field) + return tuple(names) + + +@dataclass(frozen=True) +class PiiFieldEncryptor: + """Encrypt and decrypt explicitly declared event fields with AES-GCM.""" + + key_name: str + key: bytes + + @classmethod + def from_secret(cls, key_name: str, secret: str) -> PiiFieldEncryptor: + """Build an encryptor from a KV secret without retaining its text form.""" + if not key_name: + raise PiiProtectionError("PII encryption key name is empty") + return cls(key_name, _decode_secret(secret)) + + def encrypt_fields(self, detail: dict[str, Any], fields: Iterable[str]) -> dict[str, Any]: + """Return a copy with declared top-level fields replaced by AES-GCM envelopes.""" + if not isinstance(detail, dict): + raise PiiProtectionError("event detail must be an object") + names = _field_names(fields) + if not names: + return dict(detail) + if ENCRYPTED_FIELDS_KEY in detail: + raise PiiProtectionError("reserved encrypted field metadata key") + missing = [field for field in names if field not in detail] + if missing: + raise PiiProtectionError("declared PII field is missing") + result = dict(detail) + encrypted: dict[str, dict[str, str]] = {} + cipher = AESGCM(self.key) + for field in names: + try: + plaintext = json.dumps( + detail[field], ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False + ).encode("utf-8") + except (TypeError, ValueError) as exc: + raise PiiProtectionError("PII field is not JSON serializable") from exc + nonce = os.urandom(12) + aad = f"contextual-orchestrator:event-detail:{self.key_name}:{field}".encode() + encrypted[field] = { + "nonce": _b64encode(nonce), + "ciphertext": _b64encode(cipher.encrypt(nonce, plaintext, aad)), + } + del result[field] + result[ENCRYPTED_FIELDS_KEY] = { + "version": ENCRYPTED_FIELDS_VERSION, + "algorithm": ENCRYPTED_FIELDS_ALGORITHM, + "key_name": self.key_name, + "fields": encrypted, + } + return result + + def decrypt_fields(self, detail: dict[str, Any]) -> dict[str, Any]: + """Restore an encrypted event detail or return an unchanged plain detail.""" + if not isinstance(detail, dict): + raise PiiProtectionError("event detail must be an object") + metadata = detail.get(ENCRYPTED_FIELDS_KEY) + if metadata is None: + return dict(detail) + if not isinstance(metadata, dict) or metadata.get("version") != ENCRYPTED_FIELDS_VERSION: + raise PiiProtectionError("unsupported encrypted field version") + if metadata.get("algorithm") != ENCRYPTED_FIELDS_ALGORITHM or metadata.get("key_name") != self.key_name: + raise PiiProtectionError("encrypted field metadata does not match the configured key") + encrypted = metadata.get("fields") + if not isinstance(encrypted, dict): + raise PiiProtectionError("encrypted field metadata is invalid") + result = {key: value for key, value in detail.items() if key != ENCRYPTED_FIELDS_KEY} + cipher = AESGCM(self.key) + for field, envelope in encrypted.items(): + if not isinstance(field, str) or not isinstance(envelope, dict): + raise PiiProtectionError("encrypted field metadata is invalid") + nonce = _b64decode(envelope.get("nonce")) + ciphertext = _b64decode(envelope.get("ciphertext")) + aad = f"contextual-orchestrator:event-detail:{self.key_name}:{field}".encode() + try: + value = cipher.decrypt(nonce, ciphertext, aad) + result[field] = json.loads(value.decode("utf-8")) + except (InvalidTag, ValueError, TypeError, UnicodeDecodeError, json.JSONDecodeError) as exc: + raise PiiProtectionError("encrypted PII field failed authentication") from exc + return result + + +def load_pii_encryptor(key_name: str = DEFAULT_PII_KEY_NAME) -> PiiFieldEncryptor: + """Resolve the PII key from the KV credential registry and fail closed.""" + secret = get_credential(key_name) + if not secret: + raise PiiProtectionError(f"KV credential {key_name!r} is not configured") + return PiiFieldEncryptor.from_secret(key_name, secret) + + +def is_encrypted_detail(detail: Any) -> bool: + """Return whether an event detail carries the protected-field envelope.""" + return isinstance(detail, dict) and ENCRYPTED_FIELDS_KEY in detail diff --git a/contextual_orchestrator/server.py b/contextual_orchestrator/server.py index d75870530..b4b55c8c7 100644 --- a/contextual_orchestrator/server.py +++ b/contextual_orchestrator/server.py @@ -30,6 +30,7 @@ redact_value, sse_stream_body, ) +from .pii_protection import DEFAULT_PURPOSE_BY_SCOPE, PURPOSES_BY_SCOPE # OpenAI request params forwarded verbatim to the provider on passthrough. OPENAI_PASSTHROUGH_PARAM_KEYS = { @@ -180,8 +181,24 @@ def check_bind(self, host: str) -> None: if host in {"0.0.0.0", "::", ""} and not self.allow_public_bind: # nosec B104 - comparison rejects public bind unless explicitly opted in. raise ValueError("public bind requires --allow-public-bind") - def authorize(self, headers: Any, scope: str, client_address: str) -> None: - """Validate bearer token for admin or inference scope.""" + def resolve_purpose(self, scope: str, purpose: str | None = None) -> str: + """Resolve and validate the route-owned purpose for an authenticated role.""" + if scope not in PURPOSES_BY_SCOPE: + raise RequestError(403, "invalid_scope", "authorization scope is not supported") + effective = purpose or DEFAULT_PURPOSE_BY_SCOPE[scope] + if effective not in PURPOSES_BY_SCOPE[scope]: + raise RequestError(403, "purpose_not_allowed", "purpose is not allowed for this scope") + return effective + + def authorize( + self, + headers: Any, + scope: str, + client_address: str, + purpose: str | None = None, + ) -> str: + """Validate bearer token and return its authorized purpose.""" + effective_purpose = self.resolve_purpose(scope, purpose) if not (self.auth_token or self.admin_token or self.inference_token or self.bearer_verifier): raise RequestError(401, "unauthorized", "bearer token is required") raw = headers.get("authorization", "") @@ -203,6 +220,7 @@ def authorize(self, headers: Any, scope: str, client_address: str) -> None: valid = bool(expected) and secrets.compare_digest(token, expected) if not valid: raise RequestError(401, "unauthorized", "bearer token is invalid for this scope") + return effective_purpose def check_rate_limit(self, key: str) -> None: """Apply a simple per-client fixed-window request budget.""" @@ -4465,7 +4483,7 @@ def do_GET(self) -> None: # noqa: N802 except KeyError: self._send_error(404, "embeddings_batch_not_found", f"embeddings batch {batch_id} not found") return - self._authorize("admin") + self._authorize("admin", purpose=self._admin_purpose(path)) if path == "/api/v1/cost_attribution_dimensions": self._send({"items": dimension_catalog(), "total_count": len(ATTRIBUTION_DIMENSIONS)}) return @@ -4502,7 +4520,10 @@ def do_GET(self) -> None: # noqa: N802 self._send_text(ADMIN_HTML, "text/html; charset=utf-8") return if path == "/admin/state": - state = orchestrator.admin_state() + state = orchestrator.admin_state( + role=getattr(self, "_authorized_role", None), + purpose=getattr(self, "_authorized_purpose", None), + ) state["document_viewer"] = ( {"provider": "clearfolio", "url": clearfolio_url} if clearfolio_url else None ) @@ -5610,9 +5631,42 @@ def do_POST(self) -> None: # noqa: N802 except Exception: self._send_error(500, "internal_error", "internal server error") - def _authorize(self, scope: str) -> None: - security.check_rate_limit(self.client_address[0]) - security.authorize(self.headers, scope, self.client_address[0]) + @staticmethod + def _admin_purpose(path: str) -> str: + """Select the least-privileged purpose for an admin GET route.""" + if ( + path == "/admin/state" + or path == "/api/v1/workflow_runs" + or path.startswith("/api/v1/workflow_runs/") + or path.startswith("/api/v1/access_reports/") + or path.startswith("/api/v1/evaluation_runs/") + ): + return "audit_replay" + return "operator_read" + + def _authorize(self, scope: str, *, purpose: str | None = None) -> None: + effective_purpose = purpose or DEFAULT_PURPOSE_BY_SCOPE.get(scope, "") + try: + security.check_rate_limit(self.client_address[0]) + effective_purpose = security.authorize( + self.headers, scope, self.client_address[0], purpose=purpose + ) + except RequestError as exc: + orchestrator.record_authorization_decision( + scope=scope, + purpose=effective_purpose, + allowed=False, + reason=exc.code, + ) + raise + orchestrator.record_authorization_decision( + scope=scope, + purpose=effective_purpose, + allowed=True, + reason="authorized", + ) + self._authorized_role = scope + self._authorized_purpose = effective_purpose def _run(self, callback: Any) -> dict[str, Any]: security.acquire_run_slot() diff --git a/docs/library_research.md b/docs/library_research.md index 42c7fa95c..4e840cbd8 100644 --- a/docs/library_research.md +++ b/docs/library_research.md @@ -1,6 +1,8 @@ # Library Research -The design researched existing libraries before adding code. The repository keeps the runtime dependency-free for the current lab, but the enterprise implementation target is explicit. +The design researches existing libraries before adding code. The repository keeps +the runtime dependency-light for the current lab, while security-critical +primitives use maintained libraries when the enterprise target requires them. ## Selected Stack @@ -56,3 +58,17 @@ single-repo product instead of splitting it. ## Required For New Designs Every new subsystem design must update this file before implementation starts. The entry must name the existing libraries researched, the selected library or stdlib alternative, and the custom code that was deliberately skipped. + +## Purpose-limited PII protection + +| Area | Library/pattern | Decision | Evidence | +|---|---|---|---| +| Field encryption | `cryptography.hazmat.primitives.ciphers.aead.AESGCM` | Use the maintained AEAD primitive already available in the Python ecosystem; resolve the 256-bit key from the existing KV credential registry. | [OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html) recommends authenticated encryption such as GCM; [RFC 5116](https://datatracker.ietf.org/doc/html/rfc5116) defines the AEAD interface. | +| Key management | Existing `credentials.get_credential` | Reuse the repository's KV seam; no runtime environment lookup and no second secret store. | [NIST SP 800-57 Part 1 Rev. 5](https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final) covers key protection, inventory, access control, and rotation. | +| Purpose control | Existing bearer scopes plus fixed route purposes | Map authenticated `inference` and `admin` roles to explicit `message_delivery`, `operator_read`, and `audit_replay` purposes; audit every decision. | Wolf, Pallas, and Tai (2021) describe purpose limitation for data-in-transit and access decisions in event-driven systems ([arXiv:2110.15150](https://arxiv.org/abs/2110.15150)). | + +The implementation deliberately skips custom cryptography, automatic PII +detectors, blanket masking, and a new policy framework. Callers explicitly +declare the top-level event fields that contain PII; undeclared fields retain +the existing behavior, while marked fields fail closed when the KV key is +missing or invalid. diff --git a/docs/planning/adrs/0024-purpose-limited-pii-protection.md b/docs/planning/adrs/0024-purpose-limited-pii-protection.md new file mode 100644 index 000000000..dc36f8512 --- /dev/null +++ b/docs/planning/adrs/0024-purpose-limited-pii-protection.md @@ -0,0 +1,81 @@ +--- +id: "0024" +title: "Protect marked PII with purpose-limited access and field encryption" +status: accepted +proposed_date: "2026-08-21" +accepted_date: "2026-08-21" +deciders: + - "repository maintainer" +consulted: + - "governance-risk-compliance (org PII policy owner)" +informed: + - "downstream consumers (naruon, gyeot, scopeweave)" +affected_components: + - "contextual_orchestrator/pii_protection.py" + - "contextual_orchestrator/orchestrator.py" + - "contextual_orchestrator/server.py" + - "tests/test_pii_protection.py" +related: + - path: "docs/planning/adrs/0010-pii-audit-not-mask.md" + relation: follows +--- + +# Protect marked PII with purpose-limited access and field encryption + +## Context + +ADR 0010 correctly stopped destructive email/PII masking, but left its two +explicit follow-ups open: the server had no purpose-and-role policy for raw +content, and stored event fields had no encryption boundary. The gateway must +preserve usable content for authorized consumers without making every caller a +raw-data reader. + +## Decision + +Use the existing bearer roles as the authenticated role and assign fixed, +route-owned purposes: + +| Role | Purpose | Surface | +|---|---|---| +| `inference` | `message_delivery` | OpenAI-compatible inference responses | +| `admin` | `operator_read` | Aggregate/operator endpoints | +| `admin` | `audit_replay` | Admin state and workflow/access/evaluation traces | + +Every authorization result is recorded without client IPs, tokens, or raw +content. The route chooses the purpose; a caller cannot escalate by declaring a +different purpose in request data. Invalid role-purpose combinations fail +closed. + +Callers that place personal data in audit or analytics details must declare the +top-level fields through `pii_fields`. Those fields are encrypted with +AES-256-GCM using a 32-byte key resolved from the existing KV credential +registry (`CONTEXTUAL_ORCHESTRATOR_PII_ENCRYPTION_KEY` by default). Ciphertext, +nonce, algorithm, version, and key name are stored; the plaintext field is not. +Missing/invalid keys, malformed envelopes, missing fields, and authentication +failures raise an error rather than storing or returning plaintext. Unmarked +fields keep the existing behavior so the gateway does not guess at PII or mask +usable content. + +## Consequences + +* The old OpenAI-compatible request and response shapes remain unchanged. +* Direct Python callers must pass `pii_fields` when recording PII-bearing + events; this explicit declaration is the trust boundary and avoids an + unreliable PII detector. +* Authorized admin replay decrypts protected audit fields; ordinary internal + reads see the ciphertext envelope. +* Key rotation is represented by the stored key name; old keys must remain in + the KV registry until their protected records expire or are re-encrypted. + +## Evidence + +* OWASP. (n.d.). *Cryptographic storage cheat sheet*. + https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html +* Barker, E. (2020). *Recommendation for key management: Part 1—General* + (NIST SP 800-57 Pt. 1 Rev. 5). National Institute of Standards and + Technology. https://doi.org/10.6028/NIST.SP.800-57pt1r5 +* Wolf, K., Pallas, F., & Tai, S. (2021). Messaging with purpose limitation— + Privacy-compliant publish-subscribe systems. arXiv. https://arxiv.org/abs/2110.15150 + +The cited paper is linked rather than vendored because redistribution rights +for the downloaded copy were not independently established in this run. diff --git a/pyproject.toml b/pyproject.toml index 355d3e4cf..ca2a30373 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "Paper-grounded model orchestration lab with an enterprise admin c readme = "README.md" requires-python = ">=3.10" dependencies = [ + "cryptography>=43.0", "hypothesis>=6.100", ] diff --git a/requirements.lock b/requirements.lock index dce5c956a..2e615dcb9 100644 --- a/requirements.lock +++ b/requirements.lock @@ -20,103 +20,251 @@ anyio==4.14.1 \ --hash=sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72 \ --hash=sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e # via starlette +cffi==2.1.1 \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ + --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 + # via cryptography click==8.4.2 \ --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via uvicorn -colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via click +cryptography==50.0.0 \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ + --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ + --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ + --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ + --hash=sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c \ + --hash=sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 + # via contextual-orchestrator (pyproject.toml) fastapi==0.138.2 \ --hash=sha256:6432359d067a432134620e7c5e4c6e5063e7f37815bbbbf20acef14b0d2e3fc8 \ --hash=sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615 # via contextual-orchestrator (pyproject.toml) -greenlet==3.5.3 \ - --hash=sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db \ - --hash=sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7 \ - --hash=sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8 \ - --hash=sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc \ - --hash=sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da \ - --hash=sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8 \ - --hash=sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91 \ - --hash=sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c \ - --hash=sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310 \ - --hash=sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3 \ - --hash=sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce \ - --hash=sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149 \ - --hash=sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d \ - --hash=sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34 \ - --hash=sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be \ - --hash=sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2 \ - --hash=sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b \ - --hash=sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0 \ - --hash=sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71 \ - --hash=sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d \ - --hash=sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04 \ - --hash=sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227 \ - --hash=sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c \ - --hash=sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6 \ - --hash=sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8 \ - --hash=sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21 \ - --hash=sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23 \ - --hash=sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605 \ - --hash=sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128 \ - --hash=sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f \ - --hash=sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea \ - --hash=sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81 \ - --hash=sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2 \ - --hash=sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8 \ - --hash=sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e \ - --hash=sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb \ - --hash=sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47 \ - --hash=sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8 \ - --hash=sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab \ - --hash=sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7 \ - --hash=sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861 \ - --hash=sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702 \ - --hash=sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814 \ - --hash=sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf \ - --hash=sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a \ - --hash=sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260 \ - --hash=sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1 \ - --hash=sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec \ - --hash=sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a \ - --hash=sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5 \ - --hash=sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1 \ - --hash=sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4 \ - --hash=sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c \ - --hash=sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda \ - --hash=sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb \ - --hash=sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31 \ - --hash=sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d \ - --hash=sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c \ - --hash=sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d \ - --hash=sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b \ - --hash=sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550 \ - --hash=sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c \ - --hash=sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357 \ - --hash=sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c \ - --hash=sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4 \ - --hash=sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930 \ - --hash=sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8 \ - --hash=sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b \ - --hash=sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb \ - --hash=sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16 \ - --hash=sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608 \ - --hash=sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f \ - --hash=sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc \ - --hash=sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d \ - --hash=sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44 \ - --hash=sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b \ - --hash=sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3 \ - --hash=sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154 \ - --hash=sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117 - # via sqlalchemy h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via uvicorn +hypothesis==6.165.10 \ + --hash=sha256:00de0abdcf8c05c9d0eab735a3c49a276376b55151e6fcb903c2b39a90e5e5c3 \ + --hash=sha256:057d0232f1224dcd0b7698902551a4341a7399f90670b036db6c4376715fe889 \ + --hash=sha256:09772e328a26e50486ac572be34f9887f9aa185efe7ebb16bde4e8f6038db1f4 \ + --hash=sha256:0c4e6869817c3cfdf5a2b4d348497b95159bdecb3365be732c9b8570e36a4eef \ + --hash=sha256:10d9a650a4666b0914831f769703d36140ed8039fd19bf9b71f615b8541eccf2 \ + --hash=sha256:18a3ea838ddea183388f8788750afa8494d79abb5358823be9782585f34445d3 \ + --hash=sha256:1a380bc99aa3b035e6a95a2201bf792d4082a04ca75babcc21849c2d0914bb28 \ + --hash=sha256:1d305448e9bd8e2f4f3cea0eafd809efdaab4e998a0019bc615650c8463e42f1 \ + --hash=sha256:1ec53f08732e3cfd0342cbbd75dbd1b193c8f19390660466e536a748bb81f757 \ + --hash=sha256:1f2c4db25fb8ec1a16a8dba580666337b8ffb1887c4cf1750cc954313897cef7 \ + --hash=sha256:20f6236cfb90b7817bb1a6a087589ca4aa46d73170f0dd62963952ed5dadc589 \ + --hash=sha256:22cf19388f0ff6ced8eb3e49c903d14938e4ed909d93bf28383eef451511e424 \ + --hash=sha256:277f41801e88dad2eba082f91a75632b7584ff64044ba2cf9dadf511b0d19cd0 \ + --hash=sha256:2a2567b3a03a4a5a7c575c191cfcce321a967df3727803817e75bffbbeaecabe \ + --hash=sha256:2abb50cf1cf77d721de0a24c3f99d9c4ffdeb2cbd1e12aebb5a7a93e2b6b6d1f \ + --hash=sha256:2b112768cfb67f2b683e53e58c1a33d27811aacf60c942b8eb74635e469a73f6 \ + --hash=sha256:2b36aaffc88625a44f91074c5bbedfdefb9b376c38d1b3c342edcd2e4c8ed16c \ + --hash=sha256:2d0e0f8263d34dd8fa3b39eaa9a50bba56a8470b3dd9ebf6672d10840abe063e \ + --hash=sha256:30797f20ca45e57f526d2df872f63ba453cb4e1091ad542184a7a951af8da79d \ + --hash=sha256:3376f2594763aef14faa519b0fb27cae7ce9eeaab4c69efa07777499110306c9 \ + --hash=sha256:34ee6402df6f31274d89119f1561b5f7489c97866afc5b7a3ed3a13d7e762802 \ + --hash=sha256:37a7ac3d34220800e1107871cc391bca1b00439875925d7d821878b8b791f245 \ + --hash=sha256:3de69aa8b924b400291a3cc42aaf78e6ab65c905a3e7e1a5dc39d95ef1b428cb \ + --hash=sha256:4334058033e0214475f019e15492a50f3854fe8728cf51fe25c6191a2c3f8e52 \ + --hash=sha256:490c56b830772b0eca3b4b2cecb3741a1ed26b1d7206a279e1525dbf0aa95ee4 \ + --hash=sha256:4c68e983d0007d014bb01ad4bcbba78bc432c73a1755ff36d5102ceefa18299a \ + --hash=sha256:5671d2b2bf83bd4b6f02e55b32d432506eff5358c82f39b460a849ce19a2666e \ + --hash=sha256:56cb8c9055e50545fe6e3e5a560ec25a724673b2e4051f3c24d44e3ebc35dd72 \ + --hash=sha256:5841331c504e02d7c334591681cb8587cdd59dee7e149db6d3db8e3f9e9f02eb \ + --hash=sha256:592107a0faf6c9c3a63a8dbf13dfb1cbda1cf599b0bc11c953221b00204b9ce1 \ + --hash=sha256:5cf3b612542ba174c9da4000b59a4f4c81e8d66f87509be85d3a1b71b5c36413 \ + --hash=sha256:60cab3ab4ea468d31a33739ffd7e94ec3e37dea891d65a6582ecc8a477175191 \ + --hash=sha256:637445c1593a2a9d1024fda50082f07bb56baedda78d90a25f64b8111727ef94 \ + --hash=sha256:68b45e09834cd80523cb1eb274463073c7a9af4e4ef7cff34d9615f355572d32 \ + --hash=sha256:6caadcd1afb62630ff5c5ff353626eaa616553a5971295ad6dc2b19ca8a39620 \ + --hash=sha256:6e20a02775eb3cf0ffb4f0219b6d7c1f240336663d4e5d7028675ec247c790c4 \ + --hash=sha256:713f4ce4e82c26b53031f139de959bc9e8b54d3995aa824b89bbdf8229df2a45 \ + --hash=sha256:717aea574e0e5edba2868aa66b1caae335d8f1ad3fb29f01dd6502953fa823a1 \ + --hash=sha256:72df95fb1db41755b155c5f02106e0036a339250555c8d351d488704fd112cf9 \ + --hash=sha256:73e6df02a6a62f8045b511c272f894d08e56d174504c793c9effcbc6778051a8 \ + --hash=sha256:76a7be86d986223b9f1bdb7e7cbcdb048649901fdb956c598ef73bdab1786cd5 \ + --hash=sha256:7730d8197086f65d8969a991d6728a1d420a51b19fea06535c896cb43a1e05d0 \ + --hash=sha256:79900a9920a0b1d3a626c03a90ac6bf7042e78d46906a565b86a0dbe926f1d96 \ + --hash=sha256:7a7980a898a3e6ebe4de1896a0507e3d519edb53fb9b4bda478c9fbeb6514558 \ + --hash=sha256:8001925fa3dde51cb574e4c9de4c7efe77c4e4d64bd2fd2ef61d5651f9d04f3d \ + --hash=sha256:8660572b2d424bf5369ea8990985225f70bd1615b76ecd9c25588a3b9307009f \ + --hash=sha256:8b20f44773a9ab84400465e318712d8c2ca16418d35b9f80aa27fdf2d690ad10 \ + --hash=sha256:90915635b9648071129b0f72c0673cf8eac9eb84cfd445c5bedef30c714b1ec2 \ + --hash=sha256:9ccac776b2ca93b324806facd526ccb45da0fd035001c899a35b02c44431e209 \ + --hash=sha256:9d77c3be7b429875036ad0f0597c6e5cc6bb17894a4da005e3807de64d2673ad \ + --hash=sha256:9f07ae36c3b093e13687a894e79fe69e98a94c0b67fef656c575247682218143 \ + --hash=sha256:ab0f2e9d7d7d4db257f7cf53de3706c2baf124269571f20ffc2bcd6781f03063 \ + --hash=sha256:ad0764730e8e3421601c2cc7e1f054a9206c60ea0917165d8d9193dc453f34f1 \ + --hash=sha256:aff1f584c9538e8979cd180b1d70bf99bc16be19d4666414f49e5942b21a4f2c \ + --hash=sha256:b33dc30170a7402e03c180f2c5ef69dc077152f35b91621e9cebcde9c7d71746 \ + --hash=sha256:b5820d009aedb7ae9cfd32f98b1ab0c0bbd6268379c4fab042218b6b655c63f8 \ + --hash=sha256:bb8c7d05ea27a093a92b250904095d71d924b6b44e5795a415c1b20c265f0c65 \ + --hash=sha256:c01dd04044c472e47193b54f68e84e08d6ebf4f29551885aa959b015f7cd9747 \ + --hash=sha256:c53e9b1c36350df9965ec44d6c0d4e0bbbb38f720dd2b0e1256dc6524d411015 \ + --hash=sha256:c6559380469295c4009215fe1cab561301591a3bee2e2fb3f4f96d2273a3affc \ + --hash=sha256:cc2da5aa4edf14743fa9257e5ba3513963999f01211635702479d8e92b8207c8 \ + --hash=sha256:d1ea02fa8ab3d33eb1125eade81f7136341eb429152c6dbe2ae6f8bc33b3fbdd \ + --hash=sha256:d623801ae3dcd97b77b983400ef3d48bf976648e4efff19929175322eaae074d \ + --hash=sha256:d9145fe43ebb22e66672967c3fab411793b226ed776e4fe282271bca6ad3c0bb \ + --hash=sha256:dafa7c9dbe3d802f9bcdf261b29c8a70700fb22839947f06e471f62c46b6257f \ + --hash=sha256:dd207497bb985918409a1bb5db85d1875f74e1269487332113b73d1ee7c77647 \ + --hash=sha256:e10858f57ed0e74baa04393845f469fe8ad502c16ece4499bef7700c575611bd \ + --hash=sha256:e1bbeb7c506b07ee0422cf9b2f7212fefa4240957f03526d38d27bc6743a0a48 \ + --hash=sha256:e5f95f7b622e4171096d92175dda0a560f0955ade9b8a3a07bdcf151f7359611 \ + --hash=sha256:e9acb2c4d9cb532c3fedea74159f7b923c8c036328c9239b4049e7aa073bdd81 \ + --hash=sha256:e9f924aa610c0618445e1e8738c822c3190ce2a2699a0cb48ec3a351a96761f2 \ + --hash=sha256:ed1a5891e59472884a03cb9875483e8fc131c80a275c60967f8afc5458a0c8ff \ + --hash=sha256:ed68e27b8a61e57a3ccdc7c5a14499e00b54dfe223087204d5d40b3b5ef58b6d \ + --hash=sha256:eeab73050ea58c13dd56e329f594c1dfe32ebd7bb169bbdf4f8ceefbc31ec6b5 \ + --hash=sha256:f4dafd6d6ababfa3b14dd6e5f0378cb7c7d291895a31a40abcbb7cc74f396131 \ + --hash=sha256:f69ec5be85ef508e206153bed8eafd03f7995dc464356c8bbb279a1e2b7d56f3 \ + --hash=sha256:f76d1562643693b8a40066f1f96af795b93fd9bcfc9690a1af2ff4c5867ee29e \ + --hash=sha256:f839d29d0cc12048cf073d88ca4fdf94d420bc2b8afd69641ff6d496422ccd4f \ + --hash=sha256:f9180c362bde06fd05380298ded4e234fbc0d6ede0a864835bfd91c1e24283d5 \ + --hash=sha256:f9ff356e97e3ab09db07c8b675efa67340103874a0bae7465acb83dad7a35f7f \ + --hash=sha256:fa74636a49fc8077413ce8db3e85f1c4aff880788bb55bda56253118e036fe5b + # via contextual-orchestrator (pyproject.toml) idna==3.18 \ --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 @@ -277,6 +425,10 @@ psycopg-binary==3.3.4 \ --hash=sha256:fa1cbc10768a796c96d3243656016bf4e337c81c71097270bb7b0ad6210d9765 \ --hash=sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7 # via psycopg +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + # via cffi pydantic==2.13.4 \ --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 @@ -403,6 +555,10 @@ pydantic-core==2.46.4 \ --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic +sortedcontainers==2.4.0 \ + --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ + --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 + # via hypothesis sqlalchemy==2.0.51 \ --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ @@ -488,10 +644,6 @@ typing-inspection==0.4.2 \ # via # fastapi # pydantic -tzdata==2026.2 \ - --hash=sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10 \ - --hash=sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7 - # via psycopg uvicorn==0.49.0 \ --hash=sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f \ --hash=sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3 diff --git a/tests/test_pii_protection.py b/tests/test_pii_protection.py new file mode 100644 index 000000000..4db0dedd0 --- /dev/null +++ b/tests/test_pii_protection.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +import base64 +import json +from pathlib import Path +import sys + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402 +from contextual_orchestrator.credentials import InMemoryCredentialBackend, set_backend # noqa: E402 +from contextual_orchestrator.pii_protection import ( # noqa: E402 + DEFAULT_PII_KEY_NAME, + ENCRYPTED_FIELDS_KEY, + PiiFieldEncryptor, + PiiProtectionError, + is_encrypted_detail, + load_pii_encryptor, +) +from contextual_orchestrator.server import RequestError, SecurityConfig # noqa: E402 + + +KEY_BYTES = b"0123456789abcdef0123456789abcdef" +KEY_BASE64 = "base64:" + base64.urlsafe_b64encode(KEY_BYTES).decode("ascii") + + +@pytest.fixture(autouse=True) +def memory_credentials() -> InMemoryCredentialBackend: + backend = InMemoryCredentialBackend() + backend.set(DEFAULT_PII_KEY_NAME, KEY_BASE64) + set_backend(backend) + yield backend + set_backend(None) + + +def test_field_encryption_round_trip_and_key_formats() -> None: + encryptor = PiiFieldEncryptor.from_secret(DEFAULT_PII_KEY_NAME, KEY_BASE64) + detail = {"email": "alice@example.com", "count": 2, "nested": {"ok": True}} + protected = encryptor.encrypt_fields(detail, ["email", "email"]) + + assert protected["count"] == 2 + assert protected[ENCRYPTED_FIELDS_KEY]["algorithm"] == "AES-256-GCM" + assert "alice@example.com" not in json.dumps(protected) + assert encryptor.decrypt_fields(protected) == detail + assert PiiFieldEncryptor.from_secret("k", "hex:" + KEY_BYTES.hex()).key == KEY_BYTES + assert PiiFieldEncryptor.from_secret("k", KEY_BYTES.decode("ascii")).key == KEY_BYTES + assert is_encrypted_detail(protected) + assert not is_encrypted_detail(detail) + + +def test_empty_field_set_and_plain_decrypt_are_copy_operations() -> None: + encryptor = PiiFieldEncryptor.from_secret("k", KEY_BYTES.decode("ascii")) + detail = {"email": "alice@example.com"} + assert encryptor.encrypt_fields(detail, ()) == detail + assert encryptor.encrypt_fields(detail, ()) is not detail + assert encryptor.decrypt_fields(detail) == detail + assert encryptor.decrypt_fields(detail) is not detail + + +@pytest.mark.parametrize( + "secret", + ["", "hex:bad", "base64:not@@base64", "not-a-32-byte-key"], +) +def test_invalid_keys_fail_closed(secret: str) -> None: + with pytest.raises(PiiProtectionError): + PiiFieldEncryptor.from_secret("k", secret) + with pytest.raises(PiiProtectionError): + PiiFieldEncryptor.from_secret("", KEY_BASE64) + + +def test_kv_key_resolution_and_marked_event_storage() -> None: + assert load_pii_encryptor().key == KEY_BYTES + orchestrator = TaskOrchestrator([ModelAgent("general_agent", "mock")]) + orchestrator.record_analytics_event( + "pii_event", + {"email": "alice@example.com", "status": "ok"}, + pii_fields=("email",), + ) + stored = orchestrator._analytics_events[-1] + assert "alice@example.com" not in json.dumps(stored) + assert stored["event_detail"]["status"] == "ok" + assert "email" in stored["event_detail"][ENCRYPTED_FIELDS_KEY]["fields"] + + +def test_missing_kv_key_and_invalid_event_declarations_fail_closed(memory_credentials: InMemoryCredentialBackend) -> None: + memory_credentials._store.pop(DEFAULT_PII_KEY_NAME) + with pytest.raises(PiiProtectionError): + load_pii_encryptor() + memory_credentials.set(DEFAULT_PII_KEY_NAME, "bad") + orchestrator = TaskOrchestrator([ModelAgent("general_agent", "mock")]) + with pytest.raises(PiiProtectionError): + orchestrator.record_analytics_event("pii_event", {"email": "alice@example.com"}, pii_fields=("email",)) + + memory_credentials.set(DEFAULT_PII_KEY_NAME, KEY_BASE64) + encryptor = load_pii_encryptor() + with pytest.raises(PiiProtectionError): + encryptor.encrypt_fields({"email": "x", ENCRYPTED_FIELDS_KEY: {}}, ("email",)) + with pytest.raises(PiiProtectionError): + encryptor.encrypt_fields({"email": "x"}, ("missing",)) + with pytest.raises(PiiProtectionError): + encryptor.encrypt_fields({"email": float("nan")}, ("email",)) + with pytest.raises(PiiProtectionError): + encryptor.encrypt_fields({"email": "x"}, ("",)) + with pytest.raises(PiiProtectionError): + encryptor.encrypt_fields([], ("email",)) # type: ignore[arg-type] + with pytest.raises(PiiProtectionError): + encryptor.decrypt_fields([]) # type: ignore[arg-type] + + +def test_tampered_and_malformed_envelopes_fail_closed() -> None: + encryptor = load_pii_encryptor() + protected = encryptor.encrypt_fields({"email": "alice@example.com"}, ("email",)) + tampered = json.loads(json.dumps(protected)) + tampered[ENCRYPTED_FIELDS_KEY]["fields"]["email"]["ciphertext"] = "AA" + with pytest.raises(PiiProtectionError): + encryptor.decrypt_fields(tampered) + for metadata in ( + {"version": 2}, + {"version": 1, "algorithm": "AES-256-GCM", "key_name": "wrong", "fields": {}}, + {"version": 1, "algorithm": "AES-256-GCM", "key_name": DEFAULT_PII_KEY_NAME, "fields": []}, + ): + with pytest.raises(PiiProtectionError): + encryptor.decrypt_fields({ENCRYPTED_FIELDS_KEY: metadata}) + with pytest.raises(PiiProtectionError): + encryptor.decrypt_fields({ENCRYPTED_FIELDS_KEY: {"version": 1, "algorithm": "AES-256-GCM", "key_name": DEFAULT_PII_KEY_NAME, "fields": {"email": {"nonce": 1, "ciphertext": "AA"}}}}) + with pytest.raises(PiiProtectionError): + encryptor.decrypt_fields({ENCRYPTED_FIELDS_KEY: {"version": 1, "algorithm": "AES-256-GCM", "key_name": DEFAULT_PII_KEY_NAME, "fields": {"email": {"nonce": "a", "ciphertext": "AA"}}}}) + with pytest.raises(PiiProtectionError): + encryptor.decrypt_fields({ENCRYPTED_FIELDS_KEY: {"version": 1, "algorithm": "AES-256-GCM", "key_name": DEFAULT_PII_KEY_NAME, "fields": {1: {}}}}) # type: ignore[dict-item] + + +def test_audit_replay_is_the_only_plaintext_read_path(memory_credentials: InMemoryCredentialBackend) -> None: + memory_credentials.set("old_pii_key", KEY_BASE64) + orchestrator = TaskOrchestrator([ModelAgent("general_agent", "mock")], pii_key_name="old_pii_key") + orchestrator._append_audit_event( + "message_received", {"email": "alice@example.com", "source": "naruon"}, pii_fields=("email",) + ) + encrypted = orchestrator.list_recent_audit_events() + assert "alice@example.com" not in json.dumps(encrypted) + orchestrator._pii_key_name = DEFAULT_PII_KEY_NAME + restored = orchestrator.list_recent_audit_events(role="admin", purpose="audit_replay") + assert restored[0]["event_detail"]["email"] == "alice@example.com" + + +def test_purpose_policy_is_role_scoped() -> None: + security = SecurityConfig(auth_token="secret") + assert security.authorize({"authorization": "Bearer secret"}, "inference", "127.0.0.1") == "message_delivery" + assert security.authorize({"authorization": "Bearer secret"}, "admin", "127.0.0.1", "audit_replay") == "audit_replay" + with pytest.raises(RequestError) as error: + security.authorize({"authorization": "Bearer secret"}, "inference", "127.0.0.1", "audit_replay") + assert error.value.code == "purpose_not_allowed" + with pytest.raises(RequestError) as error: + security.resolve_purpose("unknown") + assert error.value.code == "invalid_scope" + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(pytest.main([__file__, "-q"])) From 5e0611439972f93e86c8047ab239c0a35310fe21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 11:59:28 +0900 Subject: [PATCH 02/18] fix: harden purpose-limited PII protection --- contextual_orchestrator/orchestrator.py | 8 ++++- contextual_orchestrator/pii_protection.py | 4 +-- contextual_orchestrator/server.py | 36 ++++++++++++------- .../0024-purpose-limited-pii-protection.md | 9 ++--- tests/test_pii_protection.py | 4 +++ 5 files changed, 42 insertions(+), 19 deletions(-) diff --git a/contextual_orchestrator/orchestrator.py b/contextual_orchestrator/orchestrator.py index 936ef89b8..e7c58ecf1 100644 --- a/contextual_orchestrator/orchestrator.py +++ b/contextual_orchestrator/orchestrator.py @@ -36,6 +36,7 @@ from .pii_protection import ( DEFAULT_PII_KEY_NAME, ENCRYPTED_FIELDS_KEY, + PiiFieldEncryptor, is_encrypted_detail, load_pii_encryptor, ) @@ -1712,6 +1713,7 @@ def __init__( if not isinstance(pii_key_name, str) or not pii_key_name: raise ValueError("pii_key_name must be a non-empty string") self._pii_key_name = pii_key_name + self._pii_encryptors: dict[str, PiiFieldEncryptor] = {} self._commercial_report_cache_local = threading.local() if self._store is not None: self._reload_state() @@ -2723,7 +2725,11 @@ def _protected_event_detail(self, detail: dict[str, Any], pii_fields: Iterable[s fields = tuple(pii_fields) if not fields: return detail - return load_pii_encryptor(self._pii_key_name).encrypt_fields(detail, fields) + encryptor = self._pii_encryptors.get(self._pii_key_name) + if encryptor is None: + encryptor = load_pii_encryptor(self._pii_key_name) + self._pii_encryptors[self._pii_key_name] = encryptor + return encryptor.encrypt_fields(detail, fields) def _append_audit_event( self, diff --git a/contextual_orchestrator/pii_protection.py b/contextual_orchestrator/pii_protection.py index 28becdc6f..e506c6c4b 100644 --- a/contextual_orchestrator/pii_protection.py +++ b/contextual_orchestrator/pii_protection.py @@ -7,7 +7,7 @@ import json import os from collections.abc import Iterable -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any from cryptography.exceptions import InvalidTag @@ -91,7 +91,7 @@ class PiiFieldEncryptor: """Encrypt and decrypt explicitly declared event fields with AES-GCM.""" key_name: str - key: bytes + key: bytes = field(repr=False) @classmethod def from_secret(cls, key_name: str, secret: str) -> PiiFieldEncryptor: diff --git a/contextual_orchestrator/server.py b/contextual_orchestrator/server.py index b4b55c8c7..7e66f44e6 100644 --- a/contextual_orchestrator/server.py +++ b/contextual_orchestrator/server.py @@ -5645,6 +5645,7 @@ def _admin_purpose(path: str) -> str: return "operator_read" def _authorize(self, scope: str, *, purpose: str | None = None) -> None: + """Authorize the request and audit denials or sensitive replay access.""" effective_purpose = purpose or DEFAULT_PURPOSE_BY_SCOPE.get(scope, "") try: security.check_rate_limit(self.client_address[0]) @@ -5652,19 +5653,30 @@ def _authorize(self, scope: str, *, purpose: str | None = None) -> None: self.headers, scope, self.client_address[0], purpose=purpose ) except RequestError as exc: - orchestrator.record_authorization_decision( - scope=scope, - purpose=effective_purpose, - allowed=False, - reason=exc.code, - ) + try: + orchestrator.record_authorization_decision( + scope=scope, + purpose=effective_purpose, + allowed=False, + reason=exc.code, + ) + except Exception: + pass raise - orchestrator.record_authorization_decision( - scope=scope, - purpose=effective_purpose, - allowed=True, - reason="authorized", - ) + if effective_purpose == "audit_replay": + try: + orchestrator.record_authorization_decision( + scope=scope, + purpose=effective_purpose, + allowed=True, + reason="authorized", + ) + except Exception as exc: + raise RequestError( + 503, + "authorization_audit_unavailable", + "authorization audit unavailable", + ) from exc self._authorized_role = scope self._authorized_purpose = effective_purpose diff --git a/docs/planning/adrs/0024-purpose-limited-pii-protection.md b/docs/planning/adrs/0024-purpose-limited-pii-protection.md index dc36f8512..9e13f7a88 100644 --- a/docs/planning/adrs/0024-purpose-limited-pii-protection.md +++ b/docs/planning/adrs/0024-purpose-limited-pii-protection.md @@ -41,10 +41,11 @@ route-owned purposes: | `admin` | `operator_read` | Aggregate/operator endpoints | | `admin` | `audit_replay` | Admin state and workflow/access/evaluation traces | -Every authorization result is recorded without client IPs, tokens, or raw -content. The route chooses the purpose; a caller cannot escalate by declaring a -different purpose in request data. Invalid role-purpose combinations fail -closed. +Denied authorization results and successful raw-PII replay decisions are +recorded without client IPs, tokens, or raw content. Routine successful +inference/operator traffic keeps using the existing analytics path. The route +chooses the purpose; a caller cannot escalate by declaring a different purpose +in request data. Invalid role-purpose combinations fail closed. Callers that place personal data in audit or analytics details must declare the top-level fields through `pii_fields`. Those fields are encrypted with diff --git a/tests/test_pii_protection.py b/tests/test_pii_protection.py index 4db0dedd0..ae3b4b213 100644 --- a/tests/test_pii_protection.py +++ b/tests/test_pii_protection.py @@ -50,6 +50,10 @@ def test_field_encryption_round_trip_and_key_formats() -> None: assert not is_encrypted_detail(detail) +def test_encryptor_repr_does_not_expose_key() -> None: + assert KEY_BYTES.decode("ascii") not in repr(PiiFieldEncryptor("test", KEY_BYTES)) + + def test_empty_field_set_and_plain_decrypt_are_copy_operations() -> None: encryptor = PiiFieldEncryptor.from_secret("k", KEY_BYTES.decode("ascii")) detail = {"email": "alice@example.com"} From 33f312c7782b07285b782c87bf6214d73a8a6975 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 16:48:25 +0900 Subject: [PATCH 03/18] fix: bound durable audit retention and CI runtime deps --- .github/workflows/tests.yml | 4 +- contextual_orchestrator/orchestrator.py | 12 +- fuzz/requirements-atheris.in | 1 + fuzz/requirements-atheris.txt | 158 +++++++++++++++++ fuzz/requirements-property.in | 1 + fuzz/requirements-property.txt | 221 +++++++++++++++++------- requirements.lock | 103 ++++++++++- tests/test_persistence.py | 12 ++ 8 files changed, 442 insertions(+), 70 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 02605dd71..88bc0c447 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,8 +30,8 @@ jobs: - name: Install test dependencies # Hash-pinned per OpenSSF Scorecard Pinned-Dependencies. Reuses the - # property-test lockfile (pytest + hypothesis), which covers the full - # suite's requirements: the package itself is stdlib-only. + # property-test lockfile (pytest, hypothesis, and the package runtime + # dependencies), which covers the full suite without unpinned installs. run: python -m pip install --require-hashes -r fuzz/requirements-property.txt - name: Run full test suite diff --git a/contextual_orchestrator/orchestrator.py b/contextual_orchestrator/orchestrator.py index e7c58ecf1..7c180e13f 100644 --- a/contextual_orchestrator/orchestrator.py +++ b/contextual_orchestrator/orchestrator.py @@ -1549,13 +1549,14 @@ class _StateStore: """Minimal write-through sqlite persistence for orchestrator runtime state. ponytail: one generic table, no ORM. Keyed kinds (workflow_run, evaluation_run) - upsert by key; stream kinds (analytics, audit) append. Stream rows grow unbounded - on disk while the in-memory deques stay capped — add pruning if db size matters. + upsert by key; stream kinds append. Durable audit rows use the same bounded + retention as the in-memory audit deque so denial traffic cannot grow the DB forever. Runtime values (kind, key, payload, limit) are always bound through SQLite placeholders so persisted prompts and identifiers cannot become SQL syntax. """ _KEYED = {"workflow_run", "evaluation_run"} + _STREAM_LIMITS = {"audit": 256} _CREATE_RECORDS_SQL = ( "CREATE TABLE IF NOT EXISTS records (" "seq INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, key TEXT, payload TEXT NOT NULL)" @@ -1563,6 +1564,10 @@ class _StateStore: _CREATE_RECORDS_KIND_SEQ_INDEX_SQL = "CREATE INDEX IF NOT EXISTS records_kind_seq ON records(kind, seq)" _DELETE_KEYED_SQL = "DELETE FROM records WHERE kind = ? AND key = ?" _INSERT_SQL = "INSERT INTO records (kind, key, payload) VALUES (?, ?, ?)" + _PRUNE_STREAM_SQL = ( + "DELETE FROM records WHERE kind = ? AND seq NOT IN (" + "SELECT seq FROM records WHERE kind = ? ORDER BY seq DESC LIMIT ?)" + ) _SELECT_ALL_SQL = "SELECT payload FROM records WHERE kind = ? ORDER BY seq" _SELECT_LIMIT_SQL = "SELECT payload FROM records WHERE kind = ? ORDER BY seq DESC LIMIT ?" @@ -1579,6 +1584,9 @@ def save(self, kind: str, key: str | None, payload: dict[str, Any]) -> None: if kind in self._KEYED: self._conn.execute(self._DELETE_KEYED_SQL, (kind, key)) self._conn.execute(self._INSERT_SQL, (kind, key, blob)) + if kind in self._STREAM_LIMITS: + limit = self._STREAM_LIMITS[kind] + self._conn.execute(self._PRUNE_STREAM_SQL, (kind, kind, limit)) self._conn.commit() def load(self, kind: str, limit: int | None = None) -> list[dict[str, Any]]: diff --git a/fuzz/requirements-atheris.in b/fuzz/requirements-atheris.in index 5fa77c8bc..fe725a6c9 100644 --- a/fuzz/requirements-atheris.in +++ b/fuzz/requirements-atheris.in @@ -3,3 +3,4 @@ # coverage-evidence image; 3.0.0 is no longer available to that image. pip atheris==3.1.0 +cryptography>=43.0 diff --git a/fuzz/requirements-atheris.txt b/fuzz/requirements-atheris.txt index 2297ed12e..eec2942ff 100644 --- a/fuzz/requirements-atheris.txt +++ b/fuzz/requirements-atheris.txt @@ -5,3 +5,161 @@ atheris==3.1.0 \ --hash=sha256:ec5e11f21a4c197fe91f7aea2b2de88e623c73a21fc07b105ac6329a1588457b \ --hash=sha256:f8a9f51ce8369026e8eb7b7174835e8c4c85a1a6db5d9add36c15100779d2a39 # via -r fuzz/requirements-atheris.in +cffi==2.1.1 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ + --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 + # via cryptography +cryptography==50.0.0 \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ + --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ + --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ + --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ + --hash=sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c \ + --hash=sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 + # via -r fuzz/requirements-atheris.in +pip==26.2.1 \ + --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ + --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f + # via -r fuzz/requirements-atheris.in +pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + # via cffi diff --git a/fuzz/requirements-property.in b/fuzz/requirements-property.in index 47776861f..ac6654a96 100644 --- a/fuzz/requirements-property.in +++ b/fuzz/requirements-property.in @@ -2,3 +2,4 @@ pip hypothesis>=6.100 pytest +cryptography>=43.0 diff --git a/fuzz/requirements-property.txt b/fuzz/requirements-property.txt index 611a56e10..afdb43a2f 100644 --- a/fuzz/requirements-property.txt +++ b/fuzz/requirements-property.txt @@ -1,11 +1,159 @@ # This file was autogenerated by uv via the following command: # uv pip compile fuzz/requirements-property.in --generate-hashes --python-version 3.12 --universal -o fuzz/requirements-property.txt -exceptiongroup==1.3.1 \ - --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ - --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 - # via - # hypothesis - # pytest +cffi==2.1.1 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ + --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 + # via cryptography +colorama==0.4.6 ; sys_platform == 'win32' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via pytest +cryptography==50.0.0 \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ + --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ + --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ + --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ + --hash=sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c \ + --hash=sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 + # via -r fuzz/requirements-property.in hypothesis==6.165.3 \ --hash=sha256:00c63ce0d532368edcb7c201c2a29dc7cbef81ac15b96d7b97179de6390739ca \ --hash=sha256:084e113e0f5b70902d5872d2fbd27b6acb4481cd431f3fae6a54f4dac4601738 \ @@ -78,10 +226,18 @@ packaging==26.2 \ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via pytest +pip==26.2.1 \ + --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ + --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f + # via -r fuzz/requirements-property.in pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via pytest +pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + # via cffi pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 @@ -94,56 +250,3 @@ sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via hypothesis -tomli==2.4.1 \ - --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ - --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ - --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ - --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ - --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ - --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ - --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ - --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ - --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ - --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ - --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ - --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ - --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ - --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ - --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ - --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ - --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ - --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ - --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ - --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ - --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ - --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ - --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ - --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ - --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ - --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ - --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ - --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ - --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ - --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ - --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ - --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ - --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ - --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ - --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ - --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ - --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ - --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ - --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ - --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ - --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ - --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ - --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ - --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ - --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ - --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ - --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 - # via pytest -typing-extensions==4.16.0 \ - --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ - --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 - # via exceptiongroup diff --git a/requirements.lock b/requirements.lock index 2e615dcb9..34bc708ba 100644 --- a/requirements.lock +++ b/requirements.lock @@ -1,8 +1,8 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by uv in pip-compile-compatible format with Python 3.12 # by the following command: # -# pip-compile --extra=api --extra=db --generate-hashes --output-file=requirements.lock pyproject.toml +# uv pip compile --extra api --extra db --generate-hashes --python-version 3.12 --universal --output-file=requirements.lock pyproject.toml # alembic==1.18.5 \ --hash=sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc \ @@ -20,7 +20,7 @@ anyio==4.14.1 \ --hash=sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72 \ --hash=sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e # via starlette -cffi==2.1.1 \ +cffi==2.1.1 ; platform_python_implementation != 'PyPy' \ --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ @@ -126,6 +126,10 @@ click==8.4.2 \ --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via uvicorn +colorama==0.4.6 ; sys_platform == 'win32' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via click cryptography==50.0.0 \ --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ @@ -178,6 +182,87 @@ fastapi==0.138.2 \ --hash=sha256:6432359d067a432134620e7c5e4c6e5063e7f37815bbbbf20acef14b0d2e3fc8 \ --hash=sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615 # via contextual-orchestrator (pyproject.toml) +greenlet==3.5.3 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' \ + --hash=sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db \ + --hash=sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7 \ + --hash=sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8 \ + --hash=sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc \ + --hash=sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da \ + --hash=sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8 \ + --hash=sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91 \ + --hash=sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c \ + --hash=sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310 \ + --hash=sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3 \ + --hash=sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce \ + --hash=sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149 \ + --hash=sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d \ + --hash=sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34 \ + --hash=sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be \ + --hash=sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2 \ + --hash=sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b \ + --hash=sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0 \ + --hash=sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71 \ + --hash=sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d \ + --hash=sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04 \ + --hash=sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227 \ + --hash=sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c \ + --hash=sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6 \ + --hash=sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8 \ + --hash=sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21 \ + --hash=sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23 \ + --hash=sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605 \ + --hash=sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128 \ + --hash=sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f \ + --hash=sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea \ + --hash=sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81 \ + --hash=sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2 \ + --hash=sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8 \ + --hash=sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e \ + --hash=sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb \ + --hash=sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47 \ + --hash=sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8 \ + --hash=sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab \ + --hash=sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7 \ + --hash=sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861 \ + --hash=sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702 \ + --hash=sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814 \ + --hash=sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf \ + --hash=sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a \ + --hash=sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260 \ + --hash=sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1 \ + --hash=sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec \ + --hash=sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a \ + --hash=sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5 \ + --hash=sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1 \ + --hash=sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4 \ + --hash=sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c \ + --hash=sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda \ + --hash=sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb \ + --hash=sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31 \ + --hash=sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d \ + --hash=sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c \ + --hash=sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d \ + --hash=sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b \ + --hash=sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550 \ + --hash=sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c \ + --hash=sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357 \ + --hash=sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c \ + --hash=sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4 \ + --hash=sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930 \ + --hash=sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8 \ + --hash=sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b \ + --hash=sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb \ + --hash=sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16 \ + --hash=sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608 \ + --hash=sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f \ + --hash=sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc \ + --hash=sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d \ + --hash=sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44 \ + --hash=sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b \ + --hash=sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3 \ + --hash=sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154 \ + --hash=sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117 + # via sqlalchemy h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 @@ -364,11 +449,11 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via mako -psycopg[binary]==3.3.4 \ +psycopg==3.3.4 \ --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \ --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc # via contextual-orchestrator (pyproject.toml) -psycopg-binary==3.3.4 \ +psycopg-binary==3.3.4 ; implementation_name != 'pypy' \ --hash=sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070 \ --hash=sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c \ --hash=sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc \ @@ -425,7 +510,7 @@ psycopg-binary==3.3.4 \ --hash=sha256:fa1cbc10768a796c96d3243656016bf4e337c81c71097270bb7b0ad6210d9765 \ --hash=sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7 # via psycopg -pycparser==3.0 \ +pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \ --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi @@ -619,8 +704,8 @@ sqlalchemy==2.0.51 \ --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 # via - # alembic # contextual-orchestrator (pyproject.toml) + # alembic starlette==1.3.1 \ --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 @@ -644,6 +729,10 @@ typing-inspection==0.4.2 \ # via # fastapi # pydantic +tzdata==2026.2 ; sys_platform == 'win32' \ + --hash=sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10 \ + --hash=sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7 + # via psycopg uvicorn==0.49.0 \ --hash=sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f \ --hash=sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3 diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 30bd92cd0..c93b9fbe0 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -99,6 +99,18 @@ def test_store_treats_kind_key_and_limit_as_sql_parameters() -> None: store.close() +def test_durable_audit_retention_is_bounded() -> None: + with tempfile.TemporaryDirectory() as directory: + store = _StateStore(os.path.join(directory, "s.db")) + limit = store._STREAM_LIMITS["audit"] + for index in range(limit + 3): + store.save("audit", None, {"index": index}) + + assert len(store.load("audit")) == limit + assert store.load("audit", 1) == [{"index": limit + 2}] + store.close() + + def test_stream_reload_respects_deque_maxlen() -> None: with tempfile.TemporaryDirectory() as directory: db = os.path.join(directory, "state.db") From 5c51c3a93bbd1779745f94502ca4d702b2e051d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 22:35:37 +0900 Subject: [PATCH 04/18] fix: isolate authorization audit churn --- CLAUDE.md | 4 +- conductor/tech-stack.md | 5 ++- contextual_orchestrator/orchestrator.py | 53 +++++++++++++++++++------ docs/code_conventions.md | 3 +- tests/test_persistence.py | 18 +++++++++ tests/test_pii_protection.py | 31 +++++++++++++++ 6 files changed, 97 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index df04f6be6..3991300c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,7 +88,7 @@ A stdlib-Python lab implementing a single OpenAI-compatible API that routes, del - `orchestrator.py` — the domain heart: `ModelAgent`, `WorkflowStep`, `OrchestrationPolicy`, `ModelClient`, `TaskOrchestrator`, secret/PII redaction, budget enforcement, spend analytics, and the commercial-readiness report generators behind `/api/v1/*`. Domain code stays here until a second implementation forces extraction (see `docs/code_conventions.md`). - `server.py` — HTTP delivery adapter and `SecurityConfig`; all request validation lives here. -- `admin.py` — static HTML/CSS/JS for the `/admin` operator console (stays inline while the product is dependency-free). +- `admin.py` — static HTML/CSS/JS for the `/admin` operator console (stays inline while the stdlib HTTP/admin surface remains sufficient). - `credentials.py` / `kv_config.py` — the KV seam: `get_credential`/`register_credential` over pluggable backends (`InMemoryCredentialBackend` default; pgcrypto-encrypted `PostgresCredentialBackend`, selected via `CONTEXTUAL_ORCHESTRATOR_KV_BACKEND`). - `cost_ledger.py` / `cost_router.py` / `batch_routing.py` / `token_counting.py` — the cost-review + routing hub: prompt-safe usage ledger with seven attribution dimensions, `RoutingPolicy` (sync vs batch from request hints + KV thresholds), and the [pg-llm-batch](https://github.com/ContextualWisdomLab/pg-llm-batch) batch/embeddings backends (a local in-process backend keeps the standalone path working with no external service). - `api_contract.py` / `conventions.py` — API-shape and naming-rule enforcement helpers. @@ -98,7 +98,7 @@ Agent pools are **data, not code**: `examples/agents.mock.json` and `examples/ag ### `conductor/` — context, not code -`conductor/` is the CDD (context-driven development) directory, not a Python package: `product.md` (intent and non-goals), `tech-stack.md` (stdlib-only rationale), `workflow.md` (the TDD/DDD/CDD method and the Ponytail design gate), `tracks.md` (active tracks). Update it when scope, dependencies, workflow, or domain terms change. +`conductor/` is the CDD (context-driven development) directory, not a Python package: `product.md` (intent and non-goals), `tech-stack.md` (stdlib HTTP/core rationale plus selected runtime dependencies), `workflow.md` (the TDD/DDD/CDD method and the Ponytail design gate), `tracks.md` (active tracks). Update it when scope, dependencies, workflow, or domain terms change. ## Key conventions diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md index 18bbff6d6..a1057e334 100644 --- a/conductor/tech-stack.md +++ b/conductor/tech-stack.md @@ -6,7 +6,10 @@ Python 3.11+. ## Dependencies -Runtime dependencies: none beyond the Python standard library. +Runtime dependencies: + +- `cryptography` for AES-256-GCM protection of explicitly marked PII fields. +- The HTTP server, persistence, and orchestration core remain Python standard-library based. Production target dependencies after this lab hardens: diff --git a/contextual_orchestrator/orchestrator.py b/contextual_orchestrator/orchestrator.py index 7c180e13f..13171d147 100644 --- a/contextual_orchestrator/orchestrator.py +++ b/contextual_orchestrator/orchestrator.py @@ -37,6 +37,7 @@ DEFAULT_PII_KEY_NAME, ENCRYPTED_FIELDS_KEY, PiiFieldEncryptor, + PiiProtectionError, is_encrypted_detail, load_pii_encryptor, ) @@ -1549,14 +1550,14 @@ class _StateStore: """Minimal write-through sqlite persistence for orchestrator runtime state. ponytail: one generic table, no ORM. Keyed kinds (workflow_run, evaluation_run) - upsert by key; stream kinds append. Durable audit rows use the same bounded - retention as the in-memory audit deque so denial traffic cannot grow the DB forever. + upsert by key; stream kinds append. Durable audit streams use the same bounded + retention as their in-memory deques so request traffic cannot grow the DB forever. Runtime values (kind, key, payload, limit) are always bound through SQLite placeholders so persisted prompts and identifiers cannot become SQL syntax. """ _KEYED = {"workflow_run", "evaluation_run"} - _STREAM_LIMITS = {"audit": 256} + _STREAM_LIMITS = {"audit": 256, "authorization": 256} _CREATE_RECORDS_SQL = ( "CREATE TABLE IF NOT EXISTS records (" "seq INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, key TEXT, payload TEXT NOT NULL)" @@ -1705,6 +1706,7 @@ def __init__( self._evaluation_runs: dict[str, dict[str, Any]] = {} self._analytics_events: deque[dict[str, Any]] = deque(maxlen=512) self._audit_events: deque[dict[str, Any]] = deque(maxlen=256) + self._authorization_events: deque[dict[str, Any]] = deque(maxlen=256) self._run_order: deque[str] = deque(maxlen=128) # Per-agent circuit breaker: consecutive failures trip an agent "open" # so a persistently failing provider is skipped until it cools down. @@ -1790,6 +1792,8 @@ def _reload_state(self) -> None: self._analytics_events.append(event) for event in self._store.load("audit", self._audit_events.maxlen): self._audit_events.append(event) + for event in self._store.load("authorization", self._authorization_events.maxlen): + self._authorization_events.append(event) # Orchestration-only body keys that must not be forwarded to the provider. _ORCHESTRATION_ONLY_KEYS = frozenset( @@ -2745,15 +2749,18 @@ def _append_audit_event( detail: dict[str, Any], *, pii_fields: Iterable[str] = (), + stream: str = "audit", ) -> None: + """Append an event to a bounded audit stream.""" event = { "created_at": int(time.time()), "event_type": event_type, "event_detail": self._protected_event_detail(detail, pii_fields), } - self._audit_events.append(event) + events = self._authorization_events if stream == "authorization" else self._audit_events + events.append(event) if self._store is not None: - self._store.save("audit", None, event) + self._store.save(stream, None, event) def record_authorization_decision( self, @@ -2772,6 +2779,7 @@ def record_authorization_decision( "allowed": bool(allowed), "reason": reason, }, + stream="authorization", ) def _infer_provider_name(self, base_url: str) -> str: @@ -2889,16 +2897,36 @@ def list_recent_audit_events( restored.append(event) continue restored_event = dict(event) - metadata = detail.get(ENCRYPTED_FIELDS_KEY) - key_name = metadata.get("key_name") if isinstance(metadata, dict) else self._pii_key_name - encryptor = encryptors.get(key_name) - if encryptor is None: - encryptor = load_pii_encryptor(key_name) - encryptors[key_name] = encryptor - restored_event["event_detail"] = encryptor.decrypt_fields(detail) + try: + metadata = detail.get(ENCRYPTED_FIELDS_KEY) + key_name = metadata.get("key_name") if isinstance(metadata, dict) else self._pii_key_name + if not isinstance(key_name, str) or not key_name: + raise PiiProtectionError("encrypted field metadata has no valid key name") + encryptor = encryptors.get(key_name) + if encryptor is None: + encryptor = load_pii_encryptor(key_name) + encryptors[key_name] = encryptor + restored_event["event_detail"] = encryptor.decrypt_fields(detail) + except PiiProtectionError: + restored_event["event_detail"] = { + **detail, + "__pii_protection_error__": "unavailable", + } restored.append(restored_event) return restored + def list_recent_authorization_decisions(self, page_number: int = 1, page_size: int = 25) -> list[dict[str, Any]]: + """Return recent secret-free authorization decisions in newest-first order.""" + if page_number < 1 or page_size < 1: # pragma: no cover + raise ValueError("page_number/page_size must be >= 1") + events = list(self._authorization_events) + start = (page_number - 1) * page_size + end = start + page_size + total = len(events) + left = max(0, total - end) + right = max(0, total - start) + return list(reversed(events[left:right])) + def record_analytics_event( self, event_name: str, @@ -9106,6 +9134,7 @@ def admin_state(self, *, role: str | None = None, purpose: str | None = None) -> }, "recent_workflow_runs": [self._shorten_run(run) for run in self.list_recent_runs(page_size=max(1, len(self._run_order)))], "recent_audit_events": self.list_recent_audit_events(role=role, purpose=purpose), + "recent_authorization_decisions": self.list_recent_authorization_decisions(), "spend": self.spend_analytics(), } diff --git a/docs/code_conventions.md b/docs/code_conventions.md index c53e5a6f0..10be94d31 100644 --- a/docs/code_conventions.md +++ b/docs/code_conventions.md @@ -33,6 +33,5 @@ Paper role values are deliberate exceptions because they are source terminology: - Domain code stays in `contextual_orchestrator/orchestrator.py` until a second implementation forces extraction. - Delivery adapters live in `server.py`. -- UI static assets live in `admin.py` only while the product remains dependency-free. +- UI static assets live in `admin.py` while the stdlib HTTP/admin surface remains sufficient. - Do not introduce provider SDKs unless OpenAI-compatible HTTP falls short. - diff --git a/tests/test_persistence.py b/tests/test_persistence.py index c93b9fbe0..28d48c96f 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -111,6 +111,24 @@ def test_durable_audit_retention_is_bounded() -> None: store.close() +def test_authorization_stream_persists_separately_from_audit() -> None: + with tempfile.TemporaryDirectory() as directory: + db = os.path.join(directory, "state.db") + first = _orch(db) + first._append_audit_event("substantive_event", {"value": "keep"}) + first.record_authorization_decision( + scope="inference", purpose="message_delivery", allowed=False, reason="unauthorized" + ) + first.close() + + second = _orch(db) + try: + assert [event["event_type"] for event in second._audit_events] == ["substantive_event"] + assert [event["event_type"] for event in second._authorization_events] == ["authorization_decision"] + finally: + second.close() + + def test_stream_reload_respects_deque_maxlen() -> None: with tempfile.TemporaryDirectory() as directory: db = os.path.join(directory, "state.db") diff --git a/tests/test_pii_protection.py b/tests/test_pii_protection.py index ae3b4b213..fbe94d979 100644 --- a/tests/test_pii_protection.py +++ b/tests/test_pii_protection.py @@ -148,6 +148,37 @@ def test_audit_replay_is_the_only_plaintext_read_path(memory_credentials: InMemo assert restored[0]["event_detail"]["email"] == "alice@example.com" +def test_authorization_decisions_cannot_evict_substantive_audit_events() -> None: + orchestrator = TaskOrchestrator([ModelAgent("general_agent", "mock")]) + orchestrator._append_audit_event( + "message_received", {"email": "alice@example.com"}, pii_fields=("email",) + ) + for index in range(orchestrator._authorization_events.maxlen + 3): + orchestrator.record_authorization_decision( + scope="inference", purpose="message_delivery", allowed=False, reason=f"denial_{index}" + ) + + replay = orchestrator.list_recent_audit_events(role="admin", purpose="audit_replay") + assert [event["event_type"] for event in replay] == ["message_received"] + assert replay[0]["event_detail"]["email"] == "alice@example.com" + assert len(orchestrator.list_recent_authorization_decisions(page_size=orchestrator._authorization_events.maxlen)) == orchestrator._authorization_events.maxlen + + +def test_audit_replay_isolates_undecryptable_event() -> None: + orchestrator = TaskOrchestrator([ModelAgent("general_agent", "mock")]) + orchestrator._append_audit_event( + "message_received", {"email": "alice@example.com"}, pii_fields=("email",) + ) + tampered = json.loads(json.dumps(orchestrator._audit_events[-1])) + tampered["event_detail"][ENCRYPTED_FIELDS_KEY]["fields"]["email"]["ciphertext"] = "AA" + orchestrator._audit_events.append(tampered) + + replay = orchestrator.list_recent_audit_events(role="admin", purpose="audit_replay") + assert replay[0]["event_detail"]["__pii_protection_error__"] == "unavailable" + assert "alice@example.com" not in json.dumps(replay[0]) + assert replay[1]["event_detail"]["email"] == "alice@example.com" + + def test_purpose_policy_is_role_scoped() -> None: security = SecurityConfig(auth_token="secret") assert security.authorize({"authorization": "Bearer secret"}, "inference", "127.0.0.1") == "message_delivery" From f13c2241da410e4605f301364b87abd1aea52f7a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 22:46:15 +0900 Subject: [PATCH 05/18] fix: bound durable analytics retention --- contextual_orchestrator/orchestrator.py | 2 +- tests/test_persistence.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/contextual_orchestrator/orchestrator.py b/contextual_orchestrator/orchestrator.py index 13171d147..e53789bd4 100644 --- a/contextual_orchestrator/orchestrator.py +++ b/contextual_orchestrator/orchestrator.py @@ -1557,7 +1557,7 @@ class _StateStore: """ _KEYED = {"workflow_run", "evaluation_run"} - _STREAM_LIMITS = {"audit": 256, "authorization": 256} + _STREAM_LIMITS = {"audit": 256, "authorization": 256, "analytics": 512} _CREATE_RECORDS_SQL = ( "CREATE TABLE IF NOT EXISTS records (" "seq INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, key TEXT, payload TEXT NOT NULL)" diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 28d48c96f..fc2a05245 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -111,6 +111,18 @@ def test_durable_audit_retention_is_bounded() -> None: store.close() +def test_durable_analytics_retention_is_bounded() -> None: + with tempfile.TemporaryDirectory() as directory: + store = _StateStore(os.path.join(directory, "s.db")) + limit = store._STREAM_LIMITS["analytics"] + for index in range(limit + 3): + store.save("analytics", None, {"index": index}) + + assert len(store.load("analytics")) == limit + assert store.load("analytics", 1) == [{"index": limit + 2}] + store.close() + + def test_authorization_stream_persists_separately_from_audit() -> None: with tempfile.TemporaryDirectory() as directory: db = os.path.join(directory, "state.db") From 606eb3788681bf04928c5be9325f2ca499412069 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 23:02:30 +0900 Subject: [PATCH 06/18] test: prove authorization retention is bounded --- tests/test_persistence.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index fc2a05245..5c719b15a 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -111,6 +111,18 @@ def test_durable_audit_retention_is_bounded() -> None: store.close() +def test_durable_authorization_retention_is_bounded() -> None: + with tempfile.TemporaryDirectory() as directory: + store = _StateStore(os.path.join(directory, "s.db")) + limit = store._STREAM_LIMITS["authorization"] + for index in range(limit + 3): + store.save("authorization", None, {"index": index}) + + assert len(store.load("authorization")) == limit + assert store.load("authorization", 1) == [{"index": limit + 2}] + store.close() + + def test_durable_analytics_retention_is_bounded() -> None: with tempfile.TemporaryDirectory() as directory: store = _StateStore(os.path.join(directory, "s.db")) From 9f8b094b46a3714d5b5881014d1100bea8c75b31 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 00:14:28 +0900 Subject: [PATCH 07/18] test: restore security test lintability --- tests/test_security_hardening.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_security_hardening.py b/tests/test_security_hardening.py index 6aa57bf5c..18471581c 100644 --- a/tests/test_security_hardening.py +++ b/tests/test_security_hardening.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -import os import socket import threading import urllib.error From 1f836527a718374585c5fde7838148cfa22765b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 01:23:23 +0900 Subject: [PATCH 08/18] docs: describe cryptography runtime dependency --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3991300c8..345e088c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,7 +71,7 @@ CI gates: `.github/workflows/security.yml` (CodeQL + pip-audit on `requirements. ## What this is -A stdlib-Python lab implementing a single OpenAI-compatible API that routes, delegates, verifies, and synthesizes work across a configurable pool of model agents — plus the org's cost-review and sync-vs-batch routing hub. Runtime dependencies are the Python standard library only (Hypothesis is the sole listed dependency, for the property tests); FastAPI/SQLAlchemy/psycopg exist as *optional* extras for the hardened production target, not the current runtime. +A stdlib-Python lab implementing a single OpenAI-compatible API that routes, delegates, verifies, and synthesizes work across a configurable pool of model agents — plus the org's cost-review and sync-vs-batch routing hub. The core runtime uses the Python standard library plus the selected `cryptography` dependency for field-level PII protection; Hypothesis is used for property tests, while FastAPI/SQLAlchemy/psycopg exist as *optional* extras for the hardened production target. ## Architecture From 35692d59462d17640c3805bf4459f16ed97a8b5b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 05:54:00 +0900 Subject: [PATCH 09/18] fix: harden pii key derivation and retention --- contextual_orchestrator/orchestrator.py | 4 +-- contextual_orchestrator/pii_protection.py | 33 +++++++++++++++++-- docs/library_research.md | 2 +- .../0024-purpose-limited-pii-protection.md | 11 ++++++- tests/test_persistence.py | 4 ++- tests/test_pii_protection.py | 16 +++++++-- 6 files changed, 59 insertions(+), 11 deletions(-) diff --git a/contextual_orchestrator/orchestrator.py b/contextual_orchestrator/orchestrator.py index e53789bd4..bdc1ddcc4 100644 --- a/contextual_orchestrator/orchestrator.py +++ b/contextual_orchestrator/orchestrator.py @@ -1557,7 +1557,7 @@ class _StateStore: """ _KEYED = {"workflow_run", "evaluation_run"} - _STREAM_LIMITS = {"audit": 256, "authorization": 256, "analytics": 512} + _STREAM_LIMITS = {"audit": 256, "authorization": 256, "analytics": 256} _CREATE_RECORDS_SQL = ( "CREATE TABLE IF NOT EXISTS records (" "seq INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, key TEXT, payload TEXT NOT NULL)" @@ -1704,7 +1704,7 @@ def __init__( self.budget_max_cost_usd = budget_max_cost_usd self._workflow_runs: dict[str, dict[str, Any]] = {} self._evaluation_runs: dict[str, dict[str, Any]] = {} - self._analytics_events: deque[dict[str, Any]] = deque(maxlen=512) + self._analytics_events: deque[dict[str, Any]] = deque(maxlen=256) self._audit_events: deque[dict[str, Any]] = deque(maxlen=256) self._authorization_events: deque[dict[str, Any]] = deque(maxlen=256) self._run_order: deque[str] = deque(maxlen=128) diff --git a/contextual_orchestrator/pii_protection.py b/contextual_orchestrator/pii_protection.py index e506c6c4b..ad8ea641c 100644 --- a/contextual_orchestrator/pii_protection.py +++ b/contextual_orchestrator/pii_protection.py @@ -4,6 +4,7 @@ import base64 import binascii +import hashlib import json import os from collections.abc import Iterable @@ -20,6 +21,7 @@ ENCRYPTED_FIELDS_VERSION = 1 ENCRYPTED_FIELDS_ALGORITHM = "AES-256-GCM" DEFAULT_PII_KEY_NAME = "CONTEXTUAL_ORCHESTRATOR_PII_ENCRYPTION_KEY" +PASSPHRASE_PREFIX = "passphrase:" PURPOSES_BY_SCOPE = { "inference": frozenset({"message_delivery"}), "admin": frozenset({"operator_read", "audit_replay"}), @@ -34,10 +36,31 @@ class PiiProtectionError(ValueError): """Raised when marked PII cannot be safely protected or restored.""" -def _decode_secret(secret: str) -> bytes: - """Decode a 256-bit key supplied as base64, hex, or exactly 32 bytes.""" +def _decode_secret(secret: str, *, key_name: str = "") -> bytes: + """Decode an explicit key encoding or derive a key from a marked passphrase. + + Raw unprefixed 32-byte strings are rejected because a human passphrase can + otherwise be mistaken for a uniformly random AES key. Operators may use + ``base64:`` or ``hex:`` for generated key bytes, or ``passphrase:`` for a + password-derived key. + """ if not isinstance(secret, str) or not secret: raise PiiProtectionError("PII encryption key is empty") + if secret.startswith(PASSPHRASE_PREFIX): + passphrase = secret[len(PASSPHRASE_PREFIX) :] + if not passphrase: + raise PiiProtectionError("PII encryption passphrase is empty") + try: + return hashlib.scrypt( + passphrase.encode("utf-8"), + salt=f"contextual-orchestrator:pii-key:{key_name}".encode("utf-8"), + n=2**14, + r=8, + p=1, + dklen=32, + ) + except (TypeError, ValueError): + raise PiiProtectionError("PII encryption passphrase could not be derived") from None if secret.startswith("hex:"): try: decoded = bytes.fromhex(secret[4:]) @@ -55,6 +78,10 @@ def _decode_secret(secret: str) -> bytes: decoded = base64.urlsafe_b64decode(secret + "=" * (-len(secret) % 4)) except (binascii.Error, ValueError): decoded = b"" + else: + raise PiiProtectionError( + "PII encryption key must use base64:, hex:, or passphrase:" + ) if len(decoded) != 32: raise PiiProtectionError("PII encryption key must decode to 32 bytes") return decoded @@ -98,7 +125,7 @@ def from_secret(cls, key_name: str, secret: str) -> PiiFieldEncryptor: """Build an encryptor from a KV secret without retaining its text form.""" if not key_name: raise PiiProtectionError("PII encryption key name is empty") - return cls(key_name, _decode_secret(secret)) + return cls(key_name, _decode_secret(secret, key_name=key_name)) def encrypt_fields(self, detail: dict[str, Any], fields: Iterable[str]) -> dict[str, Any]: """Return a copy with declared top-level fields replaced by AES-GCM envelopes.""" diff --git a/docs/library_research.md b/docs/library_research.md index 4e840cbd8..e78731110 100644 --- a/docs/library_research.md +++ b/docs/library_research.md @@ -63,7 +63,7 @@ Every new subsystem design must update this file before implementation starts. T | Area | Library/pattern | Decision | Evidence | |---|---|---|---| -| Field encryption | `cryptography.hazmat.primitives.ciphers.aead.AESGCM` | Use the maintained AEAD primitive already available in the Python ecosystem; resolve the 256-bit key from the existing KV credential registry. | [OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html) recommends authenticated encryption such as GCM; [RFC 5116](https://datatracker.ietf.org/doc/html/rfc5116) defines the AEAD interface. | +| Field encryption | `cryptography.hazmat.primitives.ciphers.aead.AESGCM` | Use the maintained AEAD primitive already available in the Python ecosystem; resolve the 256-bit key from the existing KV credential registry. Generated key bytes use explicit `base64:`/`hex:` encodings; marked passphrases use stdlib scrypt. | [OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html) recommends authenticated encryption such as GCM; [RFC 5116](https://datatracker.ietf.org/doc/html/rfc5116) defines the AEAD interface; Percival and Josefsson (2016), [RFC 7914](https://www.rfc-editor.org/rfc/rfc7914), specifies the memory-hard scrypt derivation function. | | Key management | Existing `credentials.get_credential` | Reuse the repository's KV seam; no runtime environment lookup and no second secret store. | [NIST SP 800-57 Part 1 Rev. 5](https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final) covers key protection, inventory, access control, and rotation. | | Purpose control | Existing bearer scopes plus fixed route purposes | Map authenticated `inference` and `admin` roles to explicit `message_delivery`, `operator_read`, and `audit_replay` purposes; audit every decision. | Wolf, Pallas, and Tai (2021) describe purpose limitation for data-in-transit and access decisions in event-driven systems ([arXiv:2110.15150](https://arxiv.org/abs/2110.15150)). | diff --git a/docs/planning/adrs/0024-purpose-limited-pii-protection.md b/docs/planning/adrs/0024-purpose-limited-pii-protection.md index 9e13f7a88..0d2f9c834 100644 --- a/docs/planning/adrs/0024-purpose-limited-pii-protection.md +++ b/docs/planning/adrs/0024-purpose-limited-pii-protection.md @@ -50,7 +50,11 @@ in request data. Invalid role-purpose combinations fail closed. Callers that place personal data in audit or analytics details must declare the top-level fields through `pii_fields`. Those fields are encrypted with AES-256-GCM using a 32-byte key resolved from the existing KV credential -registry (`CONTEXTUAL_ORCHESTRATOR_PII_ENCRYPTION_KEY` by default). Ciphertext, +registry (`CONTEXTUAL_ORCHESTRATOR_PII_ENCRYPTION_KEY` by default). Generated +key bytes must use an explicit `base64:` or `hex:` encoding; an operator +passphrase must use `passphrase:` and is derived with memory-hard scrypt. +Unprefixed raw 32-byte strings are rejected so a human passphrase cannot be +mistaken for uniformly random key material. Ciphertext, nonce, algorithm, version, and key name are stored; the plaintext field is not. Missing/invalid keys, malformed envelopes, missing fields, and authentication failures raise an error rather than storing or returning plaintext. Unmarked @@ -67,6 +71,9 @@ usable content. reads see the ciphertext envelope. * Key rotation is represented by the stored key name; old keys must remain in the KV registry until their protected records expire or are re-encrypted. +* Passphrase-derived keys use a stable key-name salt, so changing the key name + remains the deliberate rotation boundary and old records require the old KV + key name during replay. ## Evidence @@ -75,6 +82,8 @@ usable content. * Barker, E. (2020). *Recommendation for key management: Part 1—General* (NIST SP 800-57 Pt. 1 Rev. 5). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-57pt1r5 +* Percival, C., & Josefsson, S. (2016). *The scrypt password-based key + derivation function* (RFC 7914). RFC Editor. https://www.rfc-editor.org/rfc/rfc7914 * Wolf, K., Pallas, F., & Tai, S. (2021). Messaging with purpose limitation— Privacy-compliant publish-subscribe systems. arXiv. https://arxiv.org/abs/2110.15150 diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 5c719b15a..da25075eb 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -126,7 +126,8 @@ def test_durable_authorization_retention_is_bounded() -> None: def test_durable_analytics_retention_is_bounded() -> None: with tempfile.TemporaryDirectory() as directory: store = _StateStore(os.path.join(directory, "s.db")) - limit = store._STREAM_LIMITS["analytics"] + assert store._STREAM_LIMITS["analytics"] == 256 + limit = 256 for index in range(limit + 3): store.save("analytics", None, {"index": index}) @@ -157,6 +158,7 @@ def test_stream_reload_respects_deque_maxlen() -> None: with tempfile.TemporaryDirectory() as directory: db = os.path.join(directory, "state.db") first = _orch(db) + assert first._analytics_events.maxlen == 256 maxlen = first._analytics_events.maxlen # Drive more analytics events than the deque can hold. for i in range(maxlen + 25): diff --git a/tests/test_pii_protection.py b/tests/test_pii_protection.py index fbe94d979..15c685ffb 100644 --- a/tests/test_pii_protection.py +++ b/tests/test_pii_protection.py @@ -45,7 +45,10 @@ def test_field_encryption_round_trip_and_key_formats() -> None: assert "alice@example.com" not in json.dumps(protected) assert encryptor.decrypt_fields(protected) == detail assert PiiFieldEncryptor.from_secret("k", "hex:" + KEY_BYTES.hex()).key == KEY_BYTES - assert PiiFieldEncryptor.from_secret("k", KEY_BYTES.decode("ascii")).key == KEY_BYTES + passphrase_key = PiiFieldEncryptor.from_secret("k", "passphrase:human-readable-secret") + assert len(passphrase_key.key) == 32 + assert passphrase_key.key != b"human-readable-secret" + assert PiiFieldEncryptor.from_secret("k", "passphrase:human-readable-secret").key == passphrase_key.key assert is_encrypted_detail(protected) assert not is_encrypted_detail(detail) @@ -55,7 +58,7 @@ def test_encryptor_repr_does_not_expose_key() -> None: def test_empty_field_set_and_plain_decrypt_are_copy_operations() -> None: - encryptor = PiiFieldEncryptor.from_secret("k", KEY_BYTES.decode("ascii")) + encryptor = PiiFieldEncryptor.from_secret("k", KEY_BASE64) detail = {"email": "alice@example.com"} assert encryptor.encrypt_fields(detail, ()) == detail assert encryptor.encrypt_fields(detail, ()) is not detail @@ -65,7 +68,14 @@ def test_empty_field_set_and_plain_decrypt_are_copy_operations() -> None: @pytest.mark.parametrize( "secret", - ["", "hex:bad", "base64:not@@base64", "not-a-32-byte-key"], + [ + "", + "hex:bad", + "base64:not@@base64", + "passphrase:", + "0123456789abcdef0123456789abcdef", + "not-a-32-byte-key", + ], ) def test_invalid_keys_fail_closed(secret: str) -> None: with pytest.raises(PiiProtectionError): From 26d1d30e13ec835e6dfdba620be71b644a86a3a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 06:12:14 +0900 Subject: [PATCH 10/18] fix: reject unprefixed PII encryption keys --- .github/workflows/fuzz.yml | 3 +++ contextual_orchestrator/pii_protection.py | 11 +-------- docs/fuzzing.md | 3 +++ fuzz/corpus/pii_key/unprefixed_base64.txt | 1 + fuzz/fuzz_pii_key.py | 28 +++++++++++++++++++++++ fuzz/targets.py | 14 ++++++++++++ tests/fuzz/test_fuzz_properties.py | 7 ++++++ tests/test_pii_protection.py | 1 + 8 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 fuzz/corpus/pii_key/unprefixed_base64.txt create mode 100644 fuzz/fuzz_pii_key.py diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 79e479384..dcc45ea64 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -87,6 +87,9 @@ jobs: - name: Fuzz model-judge response parser run: python fuzz/fuzz_model_judge.py -max_total_time="${FUZZ_SECONDS}" -artifact_prefix=crash- fuzz/corpus/judge + - name: Fuzz PII encryption key boundary + run: python fuzz/fuzz_pii_key.py -max_total_time="${FUZZ_SECONDS}" -artifact_prefix=crash- fuzz/corpus/pii_key + - name: Upload crash artifacts if: failure() uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # actions/upload-artifact@v5 diff --git a/contextual_orchestrator/pii_protection.py b/contextual_orchestrator/pii_protection.py index ad8ea641c..a07daa1e9 100644 --- a/contextual_orchestrator/pii_protection.py +++ b/contextual_orchestrator/pii_protection.py @@ -72,16 +72,7 @@ def _decode_secret(secret: str, *, key_name: str = "") -> bytes: except (binascii.Error, ValueError) as exc: raise PiiProtectionError("PII encryption key is not valid base64") from exc else: - decoded = secret.encode("utf-8") - if len(decoded) != 32: - try: - decoded = base64.urlsafe_b64decode(secret + "=" * (-len(secret) % 4)) - except (binascii.Error, ValueError): - decoded = b"" - else: - raise PiiProtectionError( - "PII encryption key must use base64:, hex:, or passphrase:" - ) + raise PiiProtectionError("PII encryption key must use base64:, hex:, or passphrase:") if len(decoded) != 32: raise PiiProtectionError("PII encryption key must decode to 32 bytes") return decoded diff --git a/docs/fuzzing.md b/docs/fuzzing.md index 9897b2bd2..ce4ea8363 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -31,6 +31,8 @@ deserialize request config validate untrusted input"`): 4. **End-to-end orchestration** — `orchestrator.TaskOrchestrator.run` against `mock://` providers (fully offline). Arbitrary prompt text and mode must produce a JSON-serialisable record whose SSE framing round-trips. +5. **PII key boundary** — unprefixed encryption-key text must be rejected; + accepted key material must declare `base64:`, `hex:`, or `passphrase:`. ## Running locally @@ -49,6 +51,7 @@ python fuzz/fuzz_request_body.py -max_total_time=60 fuzz/corpus/request_body python fuzz/fuzz_agent_config.py -max_total_time=60 fuzz/corpus/agent_config python fuzz/fuzz_redaction.py -max_total_time=60 fuzz/corpus/redaction python fuzz/fuzz_orchestration.py -max_total_time=60 fuzz/corpus/orchestration +python fuzz/fuzz_pii_key.py -max_total_time=60 fuzz/corpus/pii_key ``` Seed corpora live in `fuzz/corpus//`. diff --git a/fuzz/corpus/pii_key/unprefixed_base64.txt b/fuzz/corpus/pii_key/unprefixed_base64.txt new file mode 100644 index 000000000..f5454b843 --- /dev/null +++ b/fuzz/corpus/pii_key/unprefixed_base64.txt @@ -0,0 +1 @@ +MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY= diff --git a/fuzz/fuzz_pii_key.py b/fuzz/fuzz_pii_key.py new file mode 100644 index 000000000..4d8e259c5 --- /dev/null +++ b/fuzz/fuzz_pii_key.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Atheris harness for the explicit PII encryption key prefix boundary.""" + +import sys +from pathlib import Path + +import atheris + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +with atheris.instrument_imports(): + from fuzz.targets import exercise_pii_key + + +def one_input(data: bytes) -> None: + """Feed arbitrary Unicode key text through the shared invariant.""" + fdp = atheris.FuzzedDataProvider(data) + exercise_pii_key(fdp.ConsumeUnicodeNoSurrogates(fdp.remaining_bytes())) + + +def main() -> None: + """Start the bounded libFuzzer harness.""" + atheris.Setup(sys.argv, one_input) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/fuzz/targets.py b/fuzz/targets.py index 805b4f630..44e827a42 100644 --- a/fuzz/targets.py +++ b/fuzz/targets.py @@ -23,6 +23,8 @@ 6. ``model_discovery._parse_openai_compatible`` / ``_parse_bytez`` -- parsing of a remote provider's model-list HTTP response (attacker/compromised -provider-controlled JSON). +7. ``pii_protection._decode_secret`` -- explicit key-encoding enforcement at + the field-encryption boundary. No network, no secrets, no filesystem: every target runs fully offline. """ @@ -47,6 +49,7 @@ redact_value, sse_stream_body, ) +from contextual_orchestrator.pii_protection import PiiProtectionError, _decode_secret # ``RequestError`` is the only *domain* exception the request layer is allowed to # raise; everything else below is a legitimate stdlib decode/parse failure. @@ -68,6 +71,17 @@ ) +def exercise_pii_key(value: str) -> None: + """Verify arbitrary unprefixed key text cannot cross the key boundary.""" + if value.startswith(("base64:", "hex:", "passphrase:")): + return + try: + _decode_secret(value, key_name="fuzz_key") + except PiiProtectionError: + return + raise AssertionError("unprefixed PII encryption key was accepted") + + def exercise_request_body(raw: bytes) -> None: """Drive the HTTP request-body parser + validators over arbitrary bytes. diff --git a/tests/fuzz/test_fuzz_properties.py b/tests/fuzz/test_fuzz_properties.py index 170b9d307..945693cd0 100644 --- a/tests/fuzz/test_fuzz_properties.py +++ b/tests/fuzz/test_fuzz_properties.py @@ -20,6 +20,7 @@ exercise_agent_config, exercise_model_judge_reply, exercise_orchestration, + exercise_pii_key, exercise_provider_model_payload, exercise_redaction, exercise_request_body, @@ -103,6 +104,12 @@ def test_provider_model_payload_parser_never_crashes(value: object) -> None: exercise_provider_model_payload(value) +@_SETTINGS +@given(st.text(max_size=4096)) +def test_unprefixed_pii_keys_are_rejected(value: str) -> None: + exercise_pii_key(value) + + @_SETTINGS @given(st.text(max_size=4096)) def test_redaction_never_crashes_and_is_idempotent(text: str) -> None: diff --git a/tests/test_pii_protection.py b/tests/test_pii_protection.py index 15c685ffb..eb62bb170 100644 --- a/tests/test_pii_protection.py +++ b/tests/test_pii_protection.py @@ -72,6 +72,7 @@ def test_empty_field_set_and_plain_decrypt_are_copy_operations() -> None: "", "hex:bad", "base64:not@@base64", + base64.urlsafe_b64encode(KEY_BYTES).decode("ascii"), "passphrase:", "0123456789abcdef0123456789abcdef", "not-a-32-byte-key", From f646b0418818b14285e2c6aaa29f72f43d54e5c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 23:04:53 +0900 Subject: [PATCH 11/18] fix: keep denied-request auditing off the durable-persistence hot path record_authorization_decision now fires on every pre-auth denial (401/429), which is reachable with zero credentials. With --state-db configured, that routed through _StateStore.save()'s synchronous, lock-serialized sqlite commit shared with workflow_run/evaluation_run persistence -- letting an unauthenticated flood (rate-limit evaded by rotating source IPs) contend with legitimate authenticated traffic's durable writes. Split _StateStore.save() by durability need: keyed kinds (workflow_run, evaluation_run) stay synchronous, stream kinds (audit, authorization, analytics -- already bounded/best-effort by the existing 256-row prune) now queue through a bounded background worker, reusing the NonBlockingLedgerStore pattern already established in cost_ledger.py. load()/close() drain the queue first so read-after-write and restart durability are unchanged. Co-Authored-By: Claude Sonnet 5 --- contextual_orchestrator/orchestrator.py | 33 +++++++++++++++++++++++++ tests/test_persistence.py | 25 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/contextual_orchestrator/orchestrator.py b/contextual_orchestrator/orchestrator.py index bdc1ddcc4..bb48b4615 100644 --- a/contextual_orchestrator/orchestrator.py +++ b/contextual_orchestrator/orchestrator.py @@ -18,6 +18,7 @@ import math import os from pathlib import Path +import queue import random import re import socket @@ -1578,8 +1579,30 @@ def __init__(self, path: str) -> None: self._conn.execute(self._CREATE_RECORDS_SQL) self._conn.execute(self._CREATE_RECORDS_KIND_SEQ_INDEX_SQL) self._conn.commit() + # Stream kinds (audit/authorization/analytics) are already bounded and + # best-effort by design (see class docstring). Route them through a + # background queue so an unauthenticated request flood — e.g. denied + # authorizations — cannot force synchronous, lock-serialized disk + # commits on the request-handling thread. Keyed kinds (workflow_run, + # evaluation_run) need durability and stay synchronous. + self._stream_queue: queue.Queue[tuple[str, str | None, dict[str, Any]]] = queue.Queue(maxsize=2048) + self._stream_worker = threading.Thread( + target=self._drain_stream_queue, + name="contextual-orchestrator-state-store", + daemon=True, + ) + self._stream_worker.start() def save(self, kind: str, key: str | None, payload: dict[str, Any]) -> None: + if kind in self._STREAM_LIMITS: + try: + self._stream_queue.put_nowait((kind, key, payload)) + except queue.Full: + pass + return + self._save_sync(kind, key, payload) + + def _save_sync(self, kind: str, key: str | None, payload: dict[str, Any]) -> None: blob = json.dumps(payload, ensure_ascii=False) with self._lock: if kind in self._KEYED: @@ -1590,7 +1613,16 @@ def save(self, kind: str, key: str | None, payload: dict[str, Any]) -> None: self._conn.execute(self._PRUNE_STREAM_SQL, (kind, kind, limit)) self._conn.commit() + def _drain_stream_queue(self) -> None: + while True: + kind, key, payload = self._stream_queue.get() + try: + self._save_sync(kind, key, payload) + finally: + self._stream_queue.task_done() + def load(self, kind: str, limit: int | None = None) -> list[dict[str, Any]]: + self._stream_queue.join() with self._lock: if limit is None: rows = self._conn.execute(self._SELECT_ALL_SQL, (kind,)).fetchall() @@ -1601,6 +1633,7 @@ def load(self, kind: str, limit: int | None = None) -> list[dict[str, Any]]: def close(self) -> None: """Close the sqlite handle so Windows can release the database file.""" + self._stream_queue.join() with self._lock: self._conn.close() diff --git a/tests/test_persistence.py b/tests/test_persistence.py index da25075eb..d7460fb3b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -12,6 +12,7 @@ import sqlite3 import sys import tempfile +import time sys.path.insert(0, str(Path(__file__).resolve().parents[1])) @@ -174,6 +175,30 @@ def test_stream_reload_respects_deque_maxlen() -> None: second.close() +def test_stream_save_does_not_block_on_a_held_lock() -> None: + """Unauthenticated denial recording must not force a synchronous, lock-serialized + disk commit on the request thread (the hot path any caller can trigger pre-auth).""" + with tempfile.TemporaryDirectory() as directory: + store = _StateStore(os.path.join(directory, "s.db")) + with store._lock: # simulate a keyed write already holding the store lock + started = time.monotonic() + store.save("authorization", None, {"denied": True}) + elapsed = time.monotonic() - started + assert elapsed < 0.5 # queued without waiting for the held lock + assert store.load("authorization") == [{"denied": True}] + store.close() + + +def test_keyed_save_remains_synchronous() -> None: + with tempfile.TemporaryDirectory() as directory: + store = _StateStore(os.path.join(directory, "s.db")) + store.save("workflow_run", "run_1", {"workflow_run_id": "run_1"}) + # No queue drain needed: readable through the raw connection immediately. + rows = store._conn.execute("SELECT kind FROM records WHERE key = ?", ("run_1",)).fetchall() + assert rows == [("workflow_run",)] + store.close() + + if __name__ == "__main__": for name, fn in sorted(globals().items()): if name.startswith("test_") and callable(fn): From 3e682c4ee6d11d74548ee8873c7132d2092bd80d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 16:18:53 +0900 Subject: [PATCH 12/18] fix: preserve bounded PII audit streams --- contextual_orchestrator/orchestrator.py | 76 ++++++++++++++----- contextual_orchestrator/pii_protection.py | 17 +++-- contextual_orchestrator/server.py | 1 + docs/fuzzing.md | 2 +- .../0024-purpose-limited-pii-protection.md | 9 ++- tests/test_persistence.py | 38 ++++++++++ tests/test_pii_protection.py | 43 ++++++++++- 7 files changed, 154 insertions(+), 32 deletions(-) diff --git a/contextual_orchestrator/orchestrator.py b/contextual_orchestrator/orchestrator.py index bb48b4615..807c55c52 100644 --- a/contextual_orchestrator/orchestrator.py +++ b/contextual_orchestrator/orchestrator.py @@ -18,7 +18,6 @@ import math import os from pathlib import Path -import queue import random import re import socket @@ -1579,13 +1578,16 @@ def __init__(self, path: str) -> None: self._conn.execute(self._CREATE_RECORDS_SQL) self._conn.execute(self._CREATE_RECORDS_KIND_SEQ_INDEX_SQL) self._conn.commit() - # Stream kinds (audit/authorization/analytics) are already bounded and - # best-effort by design (see class docstring). Route them through a - # background queue so an unauthenticated request flood — e.g. denied - # authorizations — cannot force synchronous, lock-serialized disk - # commits on the request-handling thread. Keyed kinds (workflow_run, - # evaluation_run) need durability and stay synchronous. - self._stream_queue: queue.Queue[tuple[str, str | None, dict[str, Any]]] = queue.Queue(maxsize=2048) + # Stream kinds are best-effort by design, but each keeps its own newest + # retention window so an authorization flood cannot evict audit data. + # The worker keeps unauthenticated denial writes off the request thread. + self._stream_events: dict[str, deque[tuple[str | None, dict[str, Any]]]] = { + kind: deque(maxlen=limit) for kind, limit in self._STREAM_LIMITS.items() + } + self._stream_condition = threading.Condition() + self._stream_closing = False + self._stream_writing = False + self._next_stream_index = 0 self._stream_worker = threading.Thread( target=self._drain_stream_queue, name="contextual-orchestrator-state-store", @@ -1593,12 +1595,13 @@ def __init__(self, path: str) -> None: ) self._stream_worker.start() - def save(self, kind: str, key: str | None, payload: dict[str, Any]) -> None: - if kind in self._STREAM_LIMITS: - try: - self._stream_queue.put_nowait((kind, key, payload)) - except queue.Full: - pass + def save(self, kind: str, key: str | None, payload: dict[str, Any], *, durable: bool = False) -> None: + if kind in self._STREAM_LIMITS and not durable: + with self._stream_condition: + if self._stream_closing: + raise RuntimeError("state store is closed") + self._stream_events[kind].append((key, payload)) + self._stream_condition.notify() return self._save_sync(kind, key, payload) @@ -1615,14 +1618,42 @@ def _save_sync(self, kind: str, key: str | None, payload: dict[str, Any]) -> Non def _drain_stream_queue(self) -> None: while True: - kind, key, payload = self._stream_queue.get() + with self._stream_condition: + while not self._stream_closing and not any(self._stream_events.values()): + self._stream_condition.wait() + event = self._next_stream_event() + if event is None: + return + kind, key, payload = event + self._stream_writing = True try: self._save_sync(kind, key, payload) + except Exception: # noqa: BLE001 - a best-effort stream write must not stop later persistence. + pass finally: - self._stream_queue.task_done() + with self._stream_condition: + self._stream_writing = False + self._stream_condition.notify_all() + + def _next_stream_event(self) -> tuple[str, str | None, dict[str, Any]] | None: + """Return one pending event fairly; caller holds ``_stream_condition``.""" + kinds = tuple(self._STREAM_LIMITS) + for offset in range(len(kinds)): + index = (self._next_stream_index + offset) % len(kinds) + kind = kinds[index] + if self._stream_events[kind]: + self._next_stream_index = (index + 1) % len(kinds) + key, payload = self._stream_events[kind].popleft() + return kind, key, payload + return None + + def _flush_streams(self) -> None: + with self._stream_condition: + while self._stream_writing or any(self._stream_events.values()): + self._stream_condition.wait() def load(self, kind: str, limit: int | None = None) -> list[dict[str, Any]]: - self._stream_queue.join() + self._flush_streams() with self._lock: if limit is None: rows = self._conn.execute(self._SELECT_ALL_SQL, (kind,)).fetchall() @@ -1633,7 +1664,11 @@ def load(self, kind: str, limit: int | None = None) -> list[dict[str, Any]]: def close(self) -> None: """Close the sqlite handle so Windows can release the database file.""" - self._stream_queue.join() + self._flush_streams() + with self._stream_condition: + self._stream_closing = True + self._stream_condition.notify_all() + self._stream_worker.join() with self._lock: self._conn.close() @@ -2783,6 +2818,7 @@ def _append_audit_event( *, pii_fields: Iterable[str] = (), stream: str = "audit", + durable: bool = False, ) -> None: """Append an event to a bounded audit stream.""" event = { @@ -2793,7 +2829,7 @@ def _append_audit_event( events = self._authorization_events if stream == "authorization" else self._audit_events events.append(event) if self._store is not None: - self._store.save(stream, None, event) + self._store.save(stream, None, event, durable=durable) def record_authorization_decision( self, @@ -2802,6 +2838,7 @@ def record_authorization_decision( purpose: str, allowed: bool, reason: str, + durable: bool = False, ) -> None: """Record a secret-free role/purpose authorization decision.""" self._append_audit_event( @@ -2813,6 +2850,7 @@ def record_authorization_decision( "reason": reason, }, stream="authorization", + durable=durable, ) def _infer_provider_name(self, base_url: str) -> str: diff --git a/contextual_orchestrator/pii_protection.py b/contextual_orchestrator/pii_protection.py index a07daa1e9..5ec84ec8f 100644 --- a/contextual_orchestrator/pii_protection.py +++ b/contextual_orchestrator/pii_protection.py @@ -41,19 +41,25 @@ def _decode_secret(secret: str, *, key_name: str = "") -> bytes: Raw unprefixed 32-byte strings are rejected because a human passphrase can otherwise be mistaken for a uniformly random AES key. Operators may use - ``base64:`` or ``hex:`` for generated key bytes, or ``passphrase:`` for a - password-derived key. + ``base64:`` or ``hex:`` for generated key bytes, or + ``passphrase::`` for a password-derived key. """ if not isinstance(secret, str) or not secret: raise PiiProtectionError("PII encryption key is empty") if secret.startswith(PASSPHRASE_PREFIX): - passphrase = secret[len(PASSPHRASE_PREFIX) :] + try: + salt_text, passphrase = secret[len(PASSPHRASE_PREFIX) :].split(":", 1) + salt = base64.b64decode(salt_text + "=" * (-len(salt_text) % 4), altchars=b"-_", validate=True) + except (ValueError, binascii.Error): + raise PiiProtectionError("PII passphrase must include a valid base64 salt") from None + if len(salt) < 16: + raise PiiProtectionError("PII passphrase salt must decode to at least 16 bytes") if not passphrase: raise PiiProtectionError("PII encryption passphrase is empty") try: return hashlib.scrypt( passphrase.encode("utf-8"), - salt=f"contextual-orchestrator:pii-key:{key_name}".encode("utf-8"), + salt=salt, n=2**14, r=8, p=1, @@ -68,7 +74,8 @@ def _decode_secret(secret: str, *, key_name: str = "") -> bytes: raise PiiProtectionError("PII encryption key is not valid hex") from exc elif secret.startswith("base64:"): try: - decoded = base64.urlsafe_b64decode(secret[7:] + "=" * (-len(secret[7:]) % 4)) + encoded = secret[7:] + decoded = base64.b64decode(encoded + "=" * (-len(encoded) % 4), altchars=b"-_", validate=True) except (binascii.Error, ValueError) as exc: raise PiiProtectionError("PII encryption key is not valid base64") from exc else: diff --git a/contextual_orchestrator/server.py b/contextual_orchestrator/server.py index 7e66f44e6..8e33e49f5 100644 --- a/contextual_orchestrator/server.py +++ b/contextual_orchestrator/server.py @@ -5670,6 +5670,7 @@ def _authorize(self, scope: str, *, purpose: str | None = None) -> None: purpose=effective_purpose, allowed=True, reason="authorized", + durable=True, ) except Exception as exc: raise RequestError( diff --git a/docs/fuzzing.md b/docs/fuzzing.md index ce4ea8363..45e905143 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -8,7 +8,7 @@ complementary, permissively licensed tools. | Tool | License | Role | | --- | --- | --- | | [Hypothesis](https://hypothesis.readthedocs.io/) | MPL-2.0 | Always-on property tests in the normal `pytest` suite (`tests/fuzz/`). Deterministic, cross-platform, shrinks any counterexample to a minimal repro. | -| [Atheris](https://github.com/google/atheris) | Apache-2.0 | Coverage-guided (libFuzzer) harnesses in `fuzz/`, run in a bounded CI job on Python 3.11. | +| [Atheris](https://github.com/google/atheris) | Apache-2.0 | Coverage-guided (libFuzzer) harnesses in `fuzz/`, run in a bounded CI job on Python 3.12. | Both drivers call the same invariant checks in [`fuzz/targets.py`](../fuzz/targets.py), so a bug found by either tool reproduces under the other. diff --git a/docs/planning/adrs/0024-purpose-limited-pii-protection.md b/docs/planning/adrs/0024-purpose-limited-pii-protection.md index 0d2f9c834..babe443b6 100644 --- a/docs/planning/adrs/0024-purpose-limited-pii-protection.md +++ b/docs/planning/adrs/0024-purpose-limited-pii-protection.md @@ -52,7 +52,9 @@ top-level fields through `pii_fields`. Those fields are encrypted with AES-256-GCM using a 32-byte key resolved from the existing KV credential registry (`CONTEXTUAL_ORCHESTRATOR_PII_ENCRYPTION_KEY` by default). Generated key bytes must use an explicit `base64:` or `hex:` encoding; an operator -passphrase must use `passphrase:` and is derived with memory-hard scrypt. +passphrase must use `passphrase::` and is derived +with memory-hard scrypt. The salt must be a generated, unique 16-byte-or-longer +value retained with the passphrase in the KV credential. Unprefixed raw 32-byte strings are rejected so a human passphrase cannot be mistaken for uniformly random key material. Ciphertext, nonce, algorithm, version, and key name are stored; the plaintext field is not. @@ -71,9 +73,8 @@ usable content. reads see the ciphertext envelope. * Key rotation is represented by the stored key name; old keys must remain in the KV registry until their protected records expire or are re-encrypted. -* Passphrase-derived keys use a stable key-name salt, so changing the key name - remains the deliberate rotation boundary and old records require the old KV - key name during replay. +* Changing a KV key name or passphrase/salt rotates the derived key; old + records therefore require the prior KV credential during replay. ## Evidence diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d7460fb3b..f364fb084 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -12,6 +12,7 @@ import sqlite3 import sys import tempfile +import threading import time sys.path.insert(0, str(Path(__file__).resolve().parents[1])) @@ -189,6 +190,43 @@ def test_stream_save_does_not_block_on_a_held_lock() -> None: store.close() +def test_saturated_authorization_stream_keeps_newest_audit_event() -> None: + with tempfile.TemporaryDirectory() as directory: + store = _StateStore(os.path.join(directory, "s.db")) + authorization_limit = store._STREAM_LIMITS["authorization"] + with store._lock: + for index in range(authorization_limit * 9): + store.save("authorization", None, {"index": index}) + store.save("audit", None, {"event": "must-survive"}) + + assert store.load("authorization", 1) == [{"index": authorization_limit * 9 - 1}] + assert store.load("audit") == [{"event": "must-survive"}] + store.close() + + +def test_stream_worker_survives_a_failed_best_effort_write() -> None: + with tempfile.TemporaryDirectory() as directory: + store = _StateStore(os.path.join(directory, "s.db")) + worker = store._stream_worker + original_save = store._save_sync + failed = threading.Event() + + def fail_once(kind: str, key: str | None, payload: dict[str, object]) -> None: + failed.set() + raise TypeError("deliberate persistence failure") + + store._save_sync = fail_once # type: ignore[method-assign] + store.save("audit", None, {"discarded": True}) + assert failed.wait(timeout=1) + assert worker.is_alive() + + store._save_sync = original_save # type: ignore[method-assign] + store.save("audit", None, {"saved": True}) + assert store.load("audit") == [{"saved": True}] + store.close() + assert not worker.is_alive() + + def test_keyed_save_remains_synchronous() -> None: with tempfile.TemporaryDirectory() as directory: store = _StateStore(os.path.join(directory, "s.db")) diff --git a/tests/test_pii_protection.py b/tests/test_pii_protection.py index eb62bb170..1b349e8d2 100644 --- a/tests/test_pii_protection.py +++ b/tests/test_pii_protection.py @@ -2,8 +2,15 @@ import base64 import json +import os from pathlib import Path +import sqlite3 import sys +import tempfile +import threading +from unittest.mock import patch +import urllib.error +import urllib.request import pytest @@ -19,11 +26,14 @@ is_encrypted_detail, load_pii_encryptor, ) -from contextual_orchestrator.server import RequestError, SecurityConfig # noqa: E402 +from contextual_orchestrator.server import RequestError, SecurityConfig, build_server # noqa: E402 KEY_BYTES = b"0123456789abcdef0123456789abcdef" KEY_BASE64 = "base64:" + base64.urlsafe_b64encode(KEY_BYTES).decode("ascii") +PASSPHRASE_SALT = base64.urlsafe_b64encode(b"passphrase-salt!").decode("ascii") +OTHER_PASSPHRASE_SALT = base64.urlsafe_b64encode(b"other-passphrase").decode("ascii") +PASSPHRASE_SECRET = f"passphrase:{PASSPHRASE_SALT}:human-readable-secret" @pytest.fixture(autouse=True) @@ -45,10 +55,11 @@ def test_field_encryption_round_trip_and_key_formats() -> None: assert "alice@example.com" not in json.dumps(protected) assert encryptor.decrypt_fields(protected) == detail assert PiiFieldEncryptor.from_secret("k", "hex:" + KEY_BYTES.hex()).key == KEY_BYTES - passphrase_key = PiiFieldEncryptor.from_secret("k", "passphrase:human-readable-secret") + passphrase_key = PiiFieldEncryptor.from_secret("k", PASSPHRASE_SECRET) assert len(passphrase_key.key) == 32 assert passphrase_key.key != b"human-readable-secret" - assert PiiFieldEncryptor.from_secret("k", "passphrase:human-readable-secret").key == passphrase_key.key + assert PiiFieldEncryptor.from_secret("k", PASSPHRASE_SECRET).key == passphrase_key.key + assert PiiFieldEncryptor.from_secret("k", f"passphrase:{OTHER_PASSPHRASE_SALT}:human-readable-secret").key != passphrase_key.key assert is_encrypted_detail(protected) assert not is_encrypted_detail(detail) @@ -72,8 +83,10 @@ def test_empty_field_set_and_plain_decrypt_are_copy_operations() -> None: "", "hex:bad", "base64:not@@base64", + "base64:!" + base64.urlsafe_b64encode(KEY_BYTES).decode("ascii"), base64.urlsafe_b64encode(KEY_BYTES).decode("ascii"), "passphrase:", + "passphrase:human-readable-secret", "0123456789abcdef0123456789abcdef", "not-a-32-byte-key", ], @@ -190,6 +203,30 @@ def test_audit_replay_isolates_undecryptable_event() -> None: assert replay[1]["event_detail"]["email"] == "alice@example.com" +def test_audit_replay_rejects_when_durable_audit_write_fails() -> None: + with tempfile.TemporaryDirectory() as directory: + orchestrator = TaskOrchestrator( + [ModelAgent("general_agent", "mock")], state_db=os.path.join(directory, "state.db") + ) + assert orchestrator._store is not None + server = build_server(orchestrator, port=0, security=SecurityConfig(auth_token="test-token")) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + request = urllib.request.Request( + f"http://127.0.0.1:{server.server_address[1]}/admin/state", + headers={"authorization": "Bearer test-token", "connection": "close"}, + ) + try: + with patch.object(orchestrator._store, "_save_sync", side_effect=sqlite3.OperationalError("disk unavailable")): + with pytest.raises(urllib.error.HTTPError) as error: + urllib.request.urlopen(request, timeout=5) + assert error.value.code == 503 + assert json.loads(error.value.read().decode("utf-8"))["error"]["code"] == "authorization_audit_unavailable" + finally: + server.shutdown() + orchestrator.close() + + def test_purpose_policy_is_role_scoped() -> None: security = SecurityConfig(auth_token="secret") assert security.authorize({"authorization": "Bearer secret"}, "inference", "127.0.0.1") == "message_delivery" From 7233f64eb607b8606883854a093e1d7585217505 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 17:38:04 +0900 Subject: [PATCH 13/18] fix: bind PII field encryption context --- contextual_orchestrator/pii_protection.py | 29 ++++++++++-- docs/library_research.md | 2 +- .../0024-purpose-limited-pii-protection.md | 7 +++ tests/test_pii_protection.py | 47 +++++++++++++++++++ 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/contextual_orchestrator/pii_protection.py b/contextual_orchestrator/pii_protection.py index 5ec84ec8f..8210831ba 100644 --- a/contextual_orchestrator/pii_protection.py +++ b/contextual_orchestrator/pii_protection.py @@ -18,10 +18,12 @@ ENCRYPTED_FIELDS_KEY = "__encrypted_fields__" -ENCRYPTED_FIELDS_VERSION = 1 +LEGACY_ENCRYPTED_FIELDS_VERSION = 1 +ENCRYPTED_FIELDS_VERSION = 2 ENCRYPTED_FIELDS_ALGORITHM = "AES-256-GCM" DEFAULT_PII_KEY_NAME = "CONTEXTUAL_ORCHESTRATOR_PII_ENCRYPTION_KEY" PASSPHRASE_PREFIX = "passphrase:" +_FIELD_AAD_CONTEXT = "contextual-orchestrator:event-detail" PURPOSES_BY_SCOPE = { "inference": frozenset({"message_delivery"}), "admin": frozenset({"operator_read", "audit_replay"}), @@ -111,6 +113,21 @@ def _field_names(fields: Iterable[str]) -> tuple[str, ...]: return tuple(names) +def _field_aad(key_name: str, field: str, version: int) -> bytes: + """Bind an encrypted field to an unambiguous key context and field label.""" + if not isinstance(key_name, str) or not key_name or not isinstance(field, str) or not field: + raise PiiProtectionError("PII encryption context is invalid") + if version == LEGACY_ENCRYPTED_FIELDS_VERSION: + if ":" in key_name or ":" in field: + raise PiiProtectionError("legacy encrypted PII context is ambiguous") + return f"{_FIELD_AAD_CONTEXT}:{key_name}:{field}".encode("utf-8") + if version == ENCRYPTED_FIELDS_VERSION: + return json.dumps( + [_FIELD_AAD_CONTEXT, key_name, field], ensure_ascii=False, separators=(",", ":") + ).encode("utf-8") + raise PiiProtectionError("unsupported encrypted field version") + + @dataclass(frozen=True) class PiiFieldEncryptor: """Encrypt and decrypt explicitly declared event fields with AES-GCM.""" @@ -148,7 +165,7 @@ def encrypt_fields(self, detail: dict[str, Any], fields: Iterable[str]) -> dict[ except (TypeError, ValueError) as exc: raise PiiProtectionError("PII field is not JSON serializable") from exc nonce = os.urandom(12) - aad = f"contextual-orchestrator:event-detail:{self.key_name}:{field}".encode() + aad = _field_aad(self.key_name, field, ENCRYPTED_FIELDS_VERSION) encrypted[field] = { "nonce": _b64encode(nonce), "ciphertext": _b64encode(cipher.encrypt(nonce, plaintext, aad)), @@ -169,7 +186,11 @@ def decrypt_fields(self, detail: dict[str, Any]) -> dict[str, Any]: metadata = detail.get(ENCRYPTED_FIELDS_KEY) if metadata is None: return dict(detail) - if not isinstance(metadata, dict) or metadata.get("version") != ENCRYPTED_FIELDS_VERSION: + version = metadata.get("version") if isinstance(metadata, dict) else None + if type(version) is not int or version not in { + LEGACY_ENCRYPTED_FIELDS_VERSION, + ENCRYPTED_FIELDS_VERSION, + }: raise PiiProtectionError("unsupported encrypted field version") if metadata.get("algorithm") != ENCRYPTED_FIELDS_ALGORITHM or metadata.get("key_name") != self.key_name: raise PiiProtectionError("encrypted field metadata does not match the configured key") @@ -183,7 +204,7 @@ def decrypt_fields(self, detail: dict[str, Any]) -> dict[str, Any]: raise PiiProtectionError("encrypted field metadata is invalid") nonce = _b64decode(envelope.get("nonce")) ciphertext = _b64decode(envelope.get("ciphertext")) - aad = f"contextual-orchestrator:event-detail:{self.key_name}:{field}".encode() + aad = _field_aad(self.key_name, field, version) try: value = cipher.decrypt(nonce, ciphertext, aad) result[field] = json.loads(value.decode("utf-8")) diff --git a/docs/library_research.md b/docs/library_research.md index e78731110..c6d564ed6 100644 --- a/docs/library_research.md +++ b/docs/library_research.md @@ -63,7 +63,7 @@ Every new subsystem design must update this file before implementation starts. T | Area | Library/pattern | Decision | Evidence | |---|---|---|---| -| Field encryption | `cryptography.hazmat.primitives.ciphers.aead.AESGCM` | Use the maintained AEAD primitive already available in the Python ecosystem; resolve the 256-bit key from the existing KV credential registry. Generated key bytes use explicit `base64:`/`hex:` encodings; marked passphrases use stdlib scrypt. | [OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html) recommends authenticated encryption such as GCM; [RFC 5116](https://datatracker.ietf.org/doc/html/rfc5116) defines the AEAD interface; Percival and Josefsson (2016), [RFC 7914](https://www.rfc-editor.org/rfc/rfc7914), specifies the memory-hard scrypt derivation function. | +| Field encryption | `cryptography.hazmat.primitives.ciphers.aead.AESGCM` | Use the maintained AEAD primitive already available in the Python ecosystem; resolve the 256-bit key from the existing KV credential registry. Generated key bytes use explicit `base64:`/`hex:` encodings; marked passphrases use stdlib scrypt. Versioned ciphertext binds event context, key name, and field label as a canonical JSON AEAD associated-data array. | [OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html) recommends authenticated encryption such as GCM; [RFC 5116](https://datatracker.ietf.org/doc/html/rfc5116) defines the AEAD interface; Percival and Josefsson (2016), [RFC 7914](https://www.rfc-editor.org/rfc/rfc7914), specifies the memory-hard scrypt derivation function. | | Key management | Existing `credentials.get_credential` | Reuse the repository's KV seam; no runtime environment lookup and no second secret store. | [NIST SP 800-57 Part 1 Rev. 5](https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final) covers key protection, inventory, access control, and rotation. | | Purpose control | Existing bearer scopes plus fixed route purposes | Map authenticated `inference` and `admin` roles to explicit `message_delivery`, `operator_read`, and `audit_replay` purposes; audit every decision. | Wolf, Pallas, and Tai (2021) describe purpose limitation for data-in-transit and access decisions in event-driven systems ([arXiv:2110.15150](https://arxiv.org/abs/2110.15150)). | diff --git a/docs/planning/adrs/0024-purpose-limited-pii-protection.md b/docs/planning/adrs/0024-purpose-limited-pii-protection.md index babe443b6..80e13b185 100644 --- a/docs/planning/adrs/0024-purpose-limited-pii-protection.md +++ b/docs/planning/adrs/0024-purpose-limited-pii-protection.md @@ -63,6 +63,13 @@ failures raise an error rather than storing or returning plaintext. Unmarked fields keep the existing behavior so the gateway does not guess at PII or mask usable content. +Version 2 binds the AEAD associated data to a canonical JSON array containing +the event context, key name, and field label, rather than joining these values +with a delimiter. This prevents a key name and field label containing colons +from being recombined into the same authenticated context. Safe version 1 +records remain readable for migration; a version 1 key name or field label with +a colon is rejected because its legacy context is ambiguous. + ## Consequences * The old OpenAI-compatible request and response shapes remain unchanged. diff --git a/tests/test_pii_protection.py b/tests/test_pii_protection.py index 1b349e8d2..6eac56a72 100644 --- a/tests/test_pii_protection.py +++ b/tests/test_pii_protection.py @@ -12,6 +12,7 @@ import urllib.error import urllib.request +from cryptography.hazmat.primitives.ciphers.aead import AESGCM import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1])) @@ -64,6 +65,51 @@ def test_field_encryption_round_trip_and_key_formats() -> None: assert not is_encrypted_detail(detail) +def test_field_encryption_aad_keeps_colon_containing_contexts_distinct() -> None: + """Key and field labels cannot be recombined into another valid AEAD context.""" + source = PiiFieldEncryptor("tenant:scope", KEY_BYTES) + target = PiiFieldEncryptor("tenant", KEY_BYTES) + protected = source.encrypt_fields({"email": "alice@example.com"}, ("email",)) + metadata = protected[ENCRYPTED_FIELDS_KEY] + metadata["key_name"] = "tenant" + metadata["fields"]["scope:email"] = metadata["fields"].pop("email") + + with pytest.raises(PiiProtectionError): + target.decrypt_fields(protected) + + metadata["version"] = 1 + with pytest.raises(PiiProtectionError): + target.decrypt_fields(protected) + + +def test_safe_legacy_field_envelope_remains_readable() -> None: + """Version 1 records with unambiguous labels remain available during migration.""" + nonce = b"123456789012" + plaintext = json.dumps("alice@example.com", separators=(",", ":")).encode("utf-8") + ciphertext = AESGCM(KEY_BYTES).encrypt( + nonce, + plaintext, + b"contextual-orchestrator:event-detail:legacy-key:email", + ) + protected = { + ENCRYPTED_FIELDS_KEY: { + "version": 1, + "algorithm": "AES-256-GCM", + "key_name": "legacy-key", + "fields": { + "email": { + "nonce": base64.urlsafe_b64encode(nonce).decode("ascii"), + "ciphertext": base64.urlsafe_b64encode(ciphertext).decode("ascii"), + } + }, + } + } + + assert PiiFieldEncryptor("legacy-key", KEY_BYTES).decrypt_fields(protected) == { + "email": "alice@example.com" + } + + def test_encryptor_repr_does_not_expose_key() -> None: assert KEY_BYTES.decode("ascii") not in repr(PiiFieldEncryptor("test", KEY_BYTES)) @@ -148,6 +194,7 @@ def test_tampered_and_malformed_envelopes_fail_closed() -> None: {"version": 2}, {"version": 1, "algorithm": "AES-256-GCM", "key_name": "wrong", "fields": {}}, {"version": 1, "algorithm": "AES-256-GCM", "key_name": DEFAULT_PII_KEY_NAME, "fields": []}, + {"version": True, "algorithm": "AES-256-GCM", "key_name": DEFAULT_PII_KEY_NAME, "fields": {}}, ): with pytest.raises(PiiProtectionError): encryptor.decrypt_fields({ENCRYPTED_FIELDS_KEY: metadata}) From 5c6670cb24632c3b587cf386a7b97258d54385e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:00:35 +0900 Subject: [PATCH 14/18] fix: persist governance audit events synchronously --- contextual_orchestrator/orchestrator.py | 10 ++++---- .../0024-purpose-limited-pii-protection.md | 4 +++ tests/test_pii_protection.py | 25 +++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/contextual_orchestrator/orchestrator.py b/contextual_orchestrator/orchestrator.py index 807c55c52..756239e8c 100644 --- a/contextual_orchestrator/orchestrator.py +++ b/contextual_orchestrator/orchestrator.py @@ -1550,7 +1550,7 @@ class _StateStore: """Minimal write-through sqlite persistence for orchestrator runtime state. ponytail: one generic table, no ORM. Keyed kinds (workflow_run, evaluation_run) - upsert by key; stream kinds append. Durable audit streams use the same bounded + upsert by key; stream kinds append. Streams saved as durable commit synchronously and use the same bounded retention as their in-memory deques so request traffic cannot grow the DB forever. Runtime values (kind, key, payload, limit) are always bound through SQLite placeholders so persisted prompts and identifiers cannot become SQL syntax. @@ -1578,9 +1578,9 @@ def __init__(self, path: str) -> None: self._conn.execute(self._CREATE_RECORDS_SQL) self._conn.execute(self._CREATE_RECORDS_KIND_SEQ_INDEX_SQL) self._conn.commit() - # Stream kinds are best-effort by design, but each keeps its own newest + # Non-durable streams are best-effort, but each keeps its own newest # retention window so an authorization flood cannot evict audit data. - # The worker keeps unauthenticated denial writes off the request thread. + # The worker keeps best-effort denial writes off the request thread. self._stream_events: dict[str, deque[tuple[str | None, dict[str, Any]]]] = { kind: deque(maxlen=limit) for kind, limit in self._STREAM_LIMITS.items() } @@ -2818,9 +2818,9 @@ def _append_audit_event( *, pii_fields: Iterable[str] = (), stream: str = "audit", - durable: bool = False, + durable: bool = True, ) -> None: - """Append an event to a bounded audit stream.""" + """Append a durable event to a bounded audit stream by default.""" event = { "created_at": int(time.time()), "event_type": event_type, diff --git a/docs/planning/adrs/0024-purpose-limited-pii-protection.md b/docs/planning/adrs/0024-purpose-limited-pii-protection.md index 80e13b185..fe2b24dc9 100644 --- a/docs/planning/adrs/0024-purpose-limited-pii-protection.md +++ b/docs/planning/adrs/0024-purpose-limited-pii-protection.md @@ -47,6 +47,10 @@ inference/operator traffic keeps using the existing analytics path. The route chooses the purpose; a caller cannot escalate by declaring a different purpose in request data. Invalid role-purpose combinations fail closed. +Governance audit records commit synchronously with their bounded retention; +only explicitly non-durable authorization denials and routine analytics use +the best-effort background stream. + Callers that place personal data in audit or analytics details must declare the top-level fields through `pii_fields`. Those fields are encrypted with AES-256-GCM using a 32-byte key resolved from the existing KV credential diff --git a/tests/test_pii_protection.py b/tests/test_pii_protection.py index 6eac56a72..7a06e2fde 100644 --- a/tests/test_pii_protection.py +++ b/tests/test_pii_protection.py @@ -235,6 +235,31 @@ def test_authorization_decisions_cannot_evict_substantive_audit_events() -> None assert len(orchestrator.list_recent_authorization_decisions(page_size=orchestrator._authorization_events.maxlen)) == orchestrator._authorization_events.maxlen +def test_substantive_audit_events_are_durable_while_denials_remain_best_effort() -> None: + """A persisted governance change cannot outlive its audit record.""" + with tempfile.TemporaryDirectory() as directory: + orchestrator = TaskOrchestrator( + [ModelAgent("general_agent", "mock")], state_db=os.path.join(directory, "state.db") + ) + assert orchestrator._store is not None + writes: list[tuple[str, str, bool]] = [] + + def capture(kind, _key, payload, *, durable=False): + writes.append((kind, payload.get("event_type", payload.get("event_name")), durable)) + + try: + with patch.object(orchestrator._store, "save", side_effect=capture): + orchestrator.add_agent("default", {"id": "coding_agent", "model": "mock"}) + orchestrator.record_authorization_decision( + scope="inference", purpose="message_delivery", allowed=False, reason="denied" + ) + finally: + orchestrator.close() + + assert ("audit", "agent_added", True) in writes + assert ("authorization", "authorization_decision", False) in writes + + def test_audit_replay_isolates_undecryptable_event() -> None: orchestrator = TaskOrchestrator([ModelAgent("general_agent", "mock")]) orchestrator._append_audit_event( From ef375cc3b61be144e960b1b40f074cc289aa224e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 19:22:06 +0900 Subject: [PATCH 15/18] fix(deps): keep cryptography as the sole runtime dependency Rebase onto #769's runtime boundary: hypothesis moves out of runtime deps (stays in the test extra), cryptography joins as the only production dependency for PII field encryption. Regenerated requirements.lock via the canonical pip-compile command (hypothesis-free, hash-locked) and both fuzz lockfiles; hand-preserved the CPython 3.12 Atheris marker per its contract test. --- fuzz/requirements-atheris.txt | 156 ++++++++++++++++++++++++++- requirements.lock | 193 ++-------------------------------- 2 files changed, 161 insertions(+), 188 deletions(-) diff --git a/fuzz/requirements-atheris.txt b/fuzz/requirements-atheris.txt index b297d263f..2994afef8 100644 --- a/fuzz/requirements-atheris.txt +++ b/fuzz/requirements-atheris.txt @@ -1,13 +1,165 @@ # This file was autogenerated by uv via the following command: # uv pip compile fuzz/requirements-atheris.in --generate-hashes --python-version 3.12 --universal -o fuzz/requirements-atheris.txt -# uv's universal resolver widens the source marker to <3.13; preserve the -# exact CPython 3.12 boundary so Dependabot on Python 3.10 skips Atheris. atheris==3.1.0 ; python_full_version == '3.12.*' \ --hash=sha256:315a0b5c819852b1ffe1ca72efc389c7724881f2c33e4aacb8c6bcec49bd5011 \ --hash=sha256:ec5e11f21a4c197fe91f7aea2b2de88e623c73a21fc07b105ac6329a1588457b \ --hash=sha256:f8a9f51ce8369026e8eb7b7174835e8c4c85a1a6db5d9add36c15100779d2a39 # via -r fuzz/requirements-atheris.in +cffi==2.1.1 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ + --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 + # via cryptography +cryptography==50.0.0 \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ + --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ + --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ + --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ + --hash=sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c \ + --hash=sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 + # via -r fuzz/requirements-atheris.in pip==26.2.1 \ --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f # via -r fuzz/requirements-atheris.in +pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + # via cffi diff --git a/requirements.lock b/requirements.lock index 34bc708ba..a86197c53 100644 --- a/requirements.lock +++ b/requirements.lock @@ -1,8 +1,8 @@ # -# This file is autogenerated by uv in pip-compile-compatible format with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # -# uv pip compile --extra api --extra db --generate-hashes --python-version 3.12 --universal --output-file=requirements.lock pyproject.toml +# pip-compile --extra=api --extra=db --generate-hashes --output-file=requirements.lock pyproject.toml # alembic==1.18.5 \ --hash=sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc \ @@ -20,7 +20,7 @@ anyio==4.14.1 \ --hash=sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72 \ --hash=sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e # via starlette -cffi==2.1.1 ; platform_python_implementation != 'PyPy' \ +cffi==2.1.1 \ --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ @@ -126,10 +126,6 @@ click==8.4.2 \ --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via uvicorn -colorama==0.4.6 ; sys_platform == 'win32' \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via click cryptography==50.0.0 \ --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ @@ -182,174 +178,10 @@ fastapi==0.138.2 \ --hash=sha256:6432359d067a432134620e7c5e4c6e5063e7f37815bbbbf20acef14b0d2e3fc8 \ --hash=sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615 # via contextual-orchestrator (pyproject.toml) -greenlet==3.5.3 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' \ - --hash=sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db \ - --hash=sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7 \ - --hash=sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8 \ - --hash=sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc \ - --hash=sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da \ - --hash=sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8 \ - --hash=sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91 \ - --hash=sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c \ - --hash=sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310 \ - --hash=sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3 \ - --hash=sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce \ - --hash=sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149 \ - --hash=sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d \ - --hash=sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34 \ - --hash=sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be \ - --hash=sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2 \ - --hash=sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b \ - --hash=sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0 \ - --hash=sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71 \ - --hash=sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d \ - --hash=sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04 \ - --hash=sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227 \ - --hash=sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c \ - --hash=sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6 \ - --hash=sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8 \ - --hash=sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21 \ - --hash=sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23 \ - --hash=sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605 \ - --hash=sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128 \ - --hash=sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f \ - --hash=sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea \ - --hash=sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81 \ - --hash=sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2 \ - --hash=sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8 \ - --hash=sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e \ - --hash=sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb \ - --hash=sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47 \ - --hash=sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8 \ - --hash=sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab \ - --hash=sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7 \ - --hash=sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861 \ - --hash=sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702 \ - --hash=sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814 \ - --hash=sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf \ - --hash=sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a \ - --hash=sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260 \ - --hash=sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1 \ - --hash=sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec \ - --hash=sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a \ - --hash=sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5 \ - --hash=sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1 \ - --hash=sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4 \ - --hash=sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c \ - --hash=sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda \ - --hash=sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb \ - --hash=sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31 \ - --hash=sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d \ - --hash=sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c \ - --hash=sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d \ - --hash=sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b \ - --hash=sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550 \ - --hash=sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c \ - --hash=sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357 \ - --hash=sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c \ - --hash=sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4 \ - --hash=sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930 \ - --hash=sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8 \ - --hash=sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b \ - --hash=sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb \ - --hash=sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16 \ - --hash=sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608 \ - --hash=sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f \ - --hash=sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc \ - --hash=sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d \ - --hash=sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44 \ - --hash=sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b \ - --hash=sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3 \ - --hash=sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154 \ - --hash=sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117 - # via sqlalchemy h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via uvicorn -hypothesis==6.165.10 \ - --hash=sha256:00de0abdcf8c05c9d0eab735a3c49a276376b55151e6fcb903c2b39a90e5e5c3 \ - --hash=sha256:057d0232f1224dcd0b7698902551a4341a7399f90670b036db6c4376715fe889 \ - --hash=sha256:09772e328a26e50486ac572be34f9887f9aa185efe7ebb16bde4e8f6038db1f4 \ - --hash=sha256:0c4e6869817c3cfdf5a2b4d348497b95159bdecb3365be732c9b8570e36a4eef \ - --hash=sha256:10d9a650a4666b0914831f769703d36140ed8039fd19bf9b71f615b8541eccf2 \ - --hash=sha256:18a3ea838ddea183388f8788750afa8494d79abb5358823be9782585f34445d3 \ - --hash=sha256:1a380bc99aa3b035e6a95a2201bf792d4082a04ca75babcc21849c2d0914bb28 \ - --hash=sha256:1d305448e9bd8e2f4f3cea0eafd809efdaab4e998a0019bc615650c8463e42f1 \ - --hash=sha256:1ec53f08732e3cfd0342cbbd75dbd1b193c8f19390660466e536a748bb81f757 \ - --hash=sha256:1f2c4db25fb8ec1a16a8dba580666337b8ffb1887c4cf1750cc954313897cef7 \ - --hash=sha256:20f6236cfb90b7817bb1a6a087589ca4aa46d73170f0dd62963952ed5dadc589 \ - --hash=sha256:22cf19388f0ff6ced8eb3e49c903d14938e4ed909d93bf28383eef451511e424 \ - --hash=sha256:277f41801e88dad2eba082f91a75632b7584ff64044ba2cf9dadf511b0d19cd0 \ - --hash=sha256:2a2567b3a03a4a5a7c575c191cfcce321a967df3727803817e75bffbbeaecabe \ - --hash=sha256:2abb50cf1cf77d721de0a24c3f99d9c4ffdeb2cbd1e12aebb5a7a93e2b6b6d1f \ - --hash=sha256:2b112768cfb67f2b683e53e58c1a33d27811aacf60c942b8eb74635e469a73f6 \ - --hash=sha256:2b36aaffc88625a44f91074c5bbedfdefb9b376c38d1b3c342edcd2e4c8ed16c \ - --hash=sha256:2d0e0f8263d34dd8fa3b39eaa9a50bba56a8470b3dd9ebf6672d10840abe063e \ - --hash=sha256:30797f20ca45e57f526d2df872f63ba453cb4e1091ad542184a7a951af8da79d \ - --hash=sha256:3376f2594763aef14faa519b0fb27cae7ce9eeaab4c69efa07777499110306c9 \ - --hash=sha256:34ee6402df6f31274d89119f1561b5f7489c97866afc5b7a3ed3a13d7e762802 \ - --hash=sha256:37a7ac3d34220800e1107871cc391bca1b00439875925d7d821878b8b791f245 \ - --hash=sha256:3de69aa8b924b400291a3cc42aaf78e6ab65c905a3e7e1a5dc39d95ef1b428cb \ - --hash=sha256:4334058033e0214475f019e15492a50f3854fe8728cf51fe25c6191a2c3f8e52 \ - --hash=sha256:490c56b830772b0eca3b4b2cecb3741a1ed26b1d7206a279e1525dbf0aa95ee4 \ - --hash=sha256:4c68e983d0007d014bb01ad4bcbba78bc432c73a1755ff36d5102ceefa18299a \ - --hash=sha256:5671d2b2bf83bd4b6f02e55b32d432506eff5358c82f39b460a849ce19a2666e \ - --hash=sha256:56cb8c9055e50545fe6e3e5a560ec25a724673b2e4051f3c24d44e3ebc35dd72 \ - --hash=sha256:5841331c504e02d7c334591681cb8587cdd59dee7e149db6d3db8e3f9e9f02eb \ - --hash=sha256:592107a0faf6c9c3a63a8dbf13dfb1cbda1cf599b0bc11c953221b00204b9ce1 \ - --hash=sha256:5cf3b612542ba174c9da4000b59a4f4c81e8d66f87509be85d3a1b71b5c36413 \ - --hash=sha256:60cab3ab4ea468d31a33739ffd7e94ec3e37dea891d65a6582ecc8a477175191 \ - --hash=sha256:637445c1593a2a9d1024fda50082f07bb56baedda78d90a25f64b8111727ef94 \ - --hash=sha256:68b45e09834cd80523cb1eb274463073c7a9af4e4ef7cff34d9615f355572d32 \ - --hash=sha256:6caadcd1afb62630ff5c5ff353626eaa616553a5971295ad6dc2b19ca8a39620 \ - --hash=sha256:6e20a02775eb3cf0ffb4f0219b6d7c1f240336663d4e5d7028675ec247c790c4 \ - --hash=sha256:713f4ce4e82c26b53031f139de959bc9e8b54d3995aa824b89bbdf8229df2a45 \ - --hash=sha256:717aea574e0e5edba2868aa66b1caae335d8f1ad3fb29f01dd6502953fa823a1 \ - --hash=sha256:72df95fb1db41755b155c5f02106e0036a339250555c8d351d488704fd112cf9 \ - --hash=sha256:73e6df02a6a62f8045b511c272f894d08e56d174504c793c9effcbc6778051a8 \ - --hash=sha256:76a7be86d986223b9f1bdb7e7cbcdb048649901fdb956c598ef73bdab1786cd5 \ - --hash=sha256:7730d8197086f65d8969a991d6728a1d420a51b19fea06535c896cb43a1e05d0 \ - --hash=sha256:79900a9920a0b1d3a626c03a90ac6bf7042e78d46906a565b86a0dbe926f1d96 \ - --hash=sha256:7a7980a898a3e6ebe4de1896a0507e3d519edb53fb9b4bda478c9fbeb6514558 \ - --hash=sha256:8001925fa3dde51cb574e4c9de4c7efe77c4e4d64bd2fd2ef61d5651f9d04f3d \ - --hash=sha256:8660572b2d424bf5369ea8990985225f70bd1615b76ecd9c25588a3b9307009f \ - --hash=sha256:8b20f44773a9ab84400465e318712d8c2ca16418d35b9f80aa27fdf2d690ad10 \ - --hash=sha256:90915635b9648071129b0f72c0673cf8eac9eb84cfd445c5bedef30c714b1ec2 \ - --hash=sha256:9ccac776b2ca93b324806facd526ccb45da0fd035001c899a35b02c44431e209 \ - --hash=sha256:9d77c3be7b429875036ad0f0597c6e5cc6bb17894a4da005e3807de64d2673ad \ - --hash=sha256:9f07ae36c3b093e13687a894e79fe69e98a94c0b67fef656c575247682218143 \ - --hash=sha256:ab0f2e9d7d7d4db257f7cf53de3706c2baf124269571f20ffc2bcd6781f03063 \ - --hash=sha256:ad0764730e8e3421601c2cc7e1f054a9206c60ea0917165d8d9193dc453f34f1 \ - --hash=sha256:aff1f584c9538e8979cd180b1d70bf99bc16be19d4666414f49e5942b21a4f2c \ - --hash=sha256:b33dc30170a7402e03c180f2c5ef69dc077152f35b91621e9cebcde9c7d71746 \ - --hash=sha256:b5820d009aedb7ae9cfd32f98b1ab0c0bbd6268379c4fab042218b6b655c63f8 \ - --hash=sha256:bb8c7d05ea27a093a92b250904095d71d924b6b44e5795a415c1b20c265f0c65 \ - --hash=sha256:c01dd04044c472e47193b54f68e84e08d6ebf4f29551885aa959b015f7cd9747 \ - --hash=sha256:c53e9b1c36350df9965ec44d6c0d4e0bbbb38f720dd2b0e1256dc6524d411015 \ - --hash=sha256:c6559380469295c4009215fe1cab561301591a3bee2e2fb3f4f96d2273a3affc \ - --hash=sha256:cc2da5aa4edf14743fa9257e5ba3513963999f01211635702479d8e92b8207c8 \ - --hash=sha256:d1ea02fa8ab3d33eb1125eade81f7136341eb429152c6dbe2ae6f8bc33b3fbdd \ - --hash=sha256:d623801ae3dcd97b77b983400ef3d48bf976648e4efff19929175322eaae074d \ - --hash=sha256:d9145fe43ebb22e66672967c3fab411793b226ed776e4fe282271bca6ad3c0bb \ - --hash=sha256:dafa7c9dbe3d802f9bcdf261b29c8a70700fb22839947f06e471f62c46b6257f \ - --hash=sha256:dd207497bb985918409a1bb5db85d1875f74e1269487332113b73d1ee7c77647 \ - --hash=sha256:e10858f57ed0e74baa04393845f469fe8ad502c16ece4499bef7700c575611bd \ - --hash=sha256:e1bbeb7c506b07ee0422cf9b2f7212fefa4240957f03526d38d27bc6743a0a48 \ - --hash=sha256:e5f95f7b622e4171096d92175dda0a560f0955ade9b8a3a07bdcf151f7359611 \ - --hash=sha256:e9acb2c4d9cb532c3fedea74159f7b923c8c036328c9239b4049e7aa073bdd81 \ - --hash=sha256:e9f924aa610c0618445e1e8738c822c3190ce2a2699a0cb48ec3a351a96761f2 \ - --hash=sha256:ed1a5891e59472884a03cb9875483e8fc131c80a275c60967f8afc5458a0c8ff \ - --hash=sha256:ed68e27b8a61e57a3ccdc7c5a14499e00b54dfe223087204d5d40b3b5ef58b6d \ - --hash=sha256:eeab73050ea58c13dd56e329f594c1dfe32ebd7bb169bbdf4f8ceefbc31ec6b5 \ - --hash=sha256:f4dafd6d6ababfa3b14dd6e5f0378cb7c7d291895a31a40abcbb7cc74f396131 \ - --hash=sha256:f69ec5be85ef508e206153bed8eafd03f7995dc464356c8bbb279a1e2b7d56f3 \ - --hash=sha256:f76d1562643693b8a40066f1f96af795b93fd9bcfc9690a1af2ff4c5867ee29e \ - --hash=sha256:f839d29d0cc12048cf073d88ca4fdf94d420bc2b8afd69641ff6d496422ccd4f \ - --hash=sha256:f9180c362bde06fd05380298ded4e234fbc0d6ede0a864835bfd91c1e24283d5 \ - --hash=sha256:f9ff356e97e3ab09db07c8b675efa67340103874a0bae7465acb83dad7a35f7f \ - --hash=sha256:fa74636a49fc8077413ce8db3e85f1c4aff880788bb55bda56253118e036fe5b - # via contextual-orchestrator (pyproject.toml) idna==3.18 \ --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 @@ -449,11 +281,11 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via mako -psycopg==3.3.4 \ +psycopg[binary]==3.3.4 \ --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \ --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc # via contextual-orchestrator (pyproject.toml) -psycopg-binary==3.3.4 ; implementation_name != 'pypy' \ +psycopg-binary==3.3.4 \ --hash=sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070 \ --hash=sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c \ --hash=sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc \ @@ -510,7 +342,7 @@ psycopg-binary==3.3.4 ; implementation_name != 'pypy' \ --hash=sha256:fa1cbc10768a796c96d3243656016bf4e337c81c71097270bb7b0ad6210d9765 \ --hash=sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7 # via psycopg -pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \ +pycparser==3.0 \ --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi @@ -640,10 +472,6 @@ pydantic-core==2.46.4 \ --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -sortedcontainers==2.4.0 \ - --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ - --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 - # via hypothesis sqlalchemy==2.0.51 \ --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ @@ -704,8 +532,8 @@ sqlalchemy==2.0.51 \ --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 # via - # contextual-orchestrator (pyproject.toml) # alembic + # contextual-orchestrator (pyproject.toml) starlette==1.3.1 \ --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 @@ -715,13 +543,10 @@ typing-extensions==4.15.0 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 # via # alembic - # anyio # fastapi - # psycopg # pydantic # pydantic-core # sqlalchemy - # starlette # typing-inspection typing-inspection==0.4.2 \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ @@ -729,10 +554,6 @@ typing-inspection==0.4.2 \ # via # fastapi # pydantic -tzdata==2026.2 ; sys_platform == 'win32' \ - --hash=sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10 \ - --hash=sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7 - # via psycopg uvicorn==0.49.0 \ --hash=sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f \ --hash=sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3 From 519dd9d813623d647794ea447b19620fa938a72a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 09:56:24 +0900 Subject: [PATCH 16/18] fix(deps): lock SQLAlchemy greenlet runtime --- pyproject.toml | 1 + requirements.lock | 81 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d32279f81..7abe9acce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ db = [ "SQLAlchemy>=2.0", "alembic>=1.17", "psycopg[binary]>=3.2", + "greenlet>=3.2", ] fuzz = [ "atheris==3.1.0; python_version >= '3.12'", diff --git a/requirements.lock b/requirements.lock index 0997bce3e..27f5c32cc 100644 --- a/requirements.lock +++ b/requirements.lock @@ -178,6 +178,87 @@ fastapi==0.138.2 \ --hash=sha256:6432359d067a432134620e7c5e4c6e5063e7f37815bbbbf20acef14b0d2e3fc8 \ --hash=sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615 # via contextual-orchestrator (pyproject.toml) +greenlet==3.5.5 \ + --hash=sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537 \ + --hash=sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39 \ + --hash=sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277 \ + --hash=sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41 \ + --hash=sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2 \ + --hash=sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d \ + --hash=sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53 \ + --hash=sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e \ + --hash=sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502 \ + --hash=sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5 \ + --hash=sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc \ + --hash=sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759 \ + --hash=sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f \ + --hash=sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b \ + --hash=sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1 \ + --hash=sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5 \ + --hash=sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769 \ + --hash=sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0 \ + --hash=sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f \ + --hash=sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da \ + --hash=sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76 \ + --hash=sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3 \ + --hash=sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e \ + --hash=sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476 \ + --hash=sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e \ + --hash=sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380 \ + --hash=sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef \ + --hash=sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18 \ + --hash=sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b \ + --hash=sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272 \ + --hash=sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0 \ + --hash=sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053 \ + --hash=sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07 \ + --hash=sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387 \ + --hash=sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52 \ + --hash=sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed \ + --hash=sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95 \ + --hash=sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c \ + --hash=sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad \ + --hash=sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f \ + --hash=sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db \ + --hash=sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328 \ + --hash=sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8 \ + --hash=sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71 \ + --hash=sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc \ + --hash=sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864 \ + --hash=sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0 \ + --hash=sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1 \ + --hash=sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b \ + --hash=sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667 \ + --hash=sha256:86c5113d698cb8d927b2750bb1f1d59eefe3a37e0e0217491aee29a7f84ef52c \ + --hash=sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c \ + --hash=sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926 \ + --hash=sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc \ + --hash=sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd \ + --hash=sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007 \ + --hash=sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6 \ + --hash=sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56 \ + --hash=sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0 \ + --hash=sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b \ + --hash=sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53 \ + --hash=sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c \ + --hash=sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c \ + --hash=sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474 \ + --hash=sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa \ + --hash=sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61 \ + --hash=sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206 \ + --hash=sha256:c69bed34470abfcd456984fdadaa18e62169af4480335c45f3c32d1d9c12e638 \ + --hash=sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9 \ + --hash=sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874 \ + --hash=sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d \ + --hash=sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8 \ + --hash=sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae \ + --hash=sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0 \ + --hash=sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773 \ + --hash=sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4 \ + --hash=sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552 \ + --hash=sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42 \ + --hash=sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b + # via contextual-orchestrator (pyproject.toml) h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 From 6b6bd783272033d7de93d584b7618b4445b5f391 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 09:57:58 +0900 Subject: [PATCH 17/18] fix(pii): harden derivation and remove scratch artifacts --- .playwright-mcp/page-2026-08-24T07-42-00-222Z.yml | 1 - .playwright-mcp/page-2026-08-24T07-42-26-068Z.yml | 1 - .playwright-mcp/page-2026-08-24T07-42-47-401Z.yml | 1 - .playwright-mcp/page-2026-08-24T07-43-55-791Z.yml | 1 - .playwright-mcp/page-2026-08-24T07-48-05-833Z.yml | 1 - contextual_orchestrator/pii_protection.py | 3 ++- fuzz/requirements-atheris.in | 5 ----- fuzz/requirements-atheris.txt | 2 +- pr768_chat_capability_and_orchestrator.patch | 1 - registered_agents.json | 1 - task_agent_mapping.json | 1 - 11 files changed, 3 insertions(+), 15 deletions(-) delete mode 100644 .playwright-mcp/page-2026-08-24T07-42-00-222Z.yml delete mode 100644 .playwright-mcp/page-2026-08-24T07-42-26-068Z.yml delete mode 100644 .playwright-mcp/page-2026-08-24T07-42-47-401Z.yml delete mode 100644 .playwright-mcp/page-2026-08-24T07-43-55-791Z.yml delete mode 100644 .playwright-mcp/page-2026-08-24T07-48-05-833Z.yml delete mode 100644 pr768_chat_capability_and_orchestrator.patch delete mode 100644 registered_agents.json delete mode 100644 task_agent_mapping.json diff --git a/.playwright-mcp/page-2026-08-24T07-42-00-222Z.yml b/.playwright-mcp/page-2026-08-24T07-42-00-222Z.yml deleted file mode 100644 index 0152f1bc7..000000000 --- a/.playwright-mcp/page-2026-08-24T07-42-00-222Z.yml +++ /dev/null @@ -1 +0,0 @@ -- generic [active] [ref=e1]: "[ { \"sha\": \"b19e63e9d9c3c8e8b4e849356b1968ff7ea00986\", \"filename\": \".github/workflows/tests.yml\", \"status\": \"modified\", \"additions\": 6, \"deletions\": 4, \"changes\": 10, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/.github%2Fworkflows%2Ftests.yml\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/.github%2Fworkflows%2Ftests.yml\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/.github%2Fworkflows%2Ftests.yml?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -29,10 +29,12 @@ jobs:\\n python-version: \\\"3.12\\\"\\n \\n - name: Install test dependencies\\n- # Hash-pinned per OpenSSF Scorecard Pinned-Dependencies. Reuses the\\n- # property-test lockfile (pytest + hypothesis), which covers the full\\n- # suite's requirements: the package itself is stdlib-only.\\n- run: python -m pip install --require-hashes -r fuzz/requirements-property.txt\\n+ # Hash-pinned per OpenSSF Scorecard Pinned-Dependencies. Install the\\n+ # runtime lock before the property-test lock so optional integrations\\n+ # such as OpenTelemetry are exercised instead of silently disabled.\\n+ run: |\\n+ python -m pip install --require-hashes -r requirements.lock\\n+ python -m pip install --require-hashes -r fuzz/requirements-property.txt\\n \\n - name: Run full test suite\\n run: python -m pytest -q\" }, { \"sha\": \"9f198bb2fb55479e52ac1977bbb2ce33eb7715c8\", \"filename\": \"AGENTS.md\", \"status\": \"modified\", \"additions\": 23, \"deletions\": 0, \"changes\": 23, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/AGENTS.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/AGENTS.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/AGENTS.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -119,4 +119,27 @@ push or open a PR.\\n cost-optimal LLM routing, upstream load balancing, and latency/throughput\\n scheduling (e.g. LLM-cascade / model-routing and queueing/load-balancing\\n papers).\\n+\\n+### Model and reasoning policy\\n+\\n+- Model selection, reasoning-effort allocation, orchestration topology, and\\n+ claims about quality/cost trade-offs must be grounded in cited academic\\n+ papers and current capability evidence. Do not introduce a model policy from\\n+ a vendor blog, benchmark marketing claim, or an implementation convention.\\n+ The governing decision is [ADR 0013](docs/planning/adrs/0013-paper-grounded-adaptive-reasoning-policy.md).\\n+- `auto` is an orchestrator policy, not a provider `reasoning_effort` value. It\\n+ may select a provider-supported value or an orchestrated multi-agent path,\\n+ but the trace must retain the requested policy and the effective strategy.\\n+- Provider values are capability-negotiated. Do not send `none`, `minimal`,\\n+ `low`, `medium`, `high`, or `xhigh` unless the selected provider advertises\\n+ that value; omit the field for a non-reasoning provider. Never infer support\\n+ from a model name.\\n+- `high` and `xhigh` may require multiple independent attempts, verification,\\n+ and synthesis when one worker cannot provide the requested capability. That\\n+ is an orchestrator strategy, not a claim that a non-reasoning worker became a\\n+ reasoning model.\\n+- MLX is not a public provider contract. Keep runtime-specific local model\\n+ behavior behind an authenticated provider-neutral gateway as specified by\\n+ ADR 0012; do not add direct `mlx://` configuration, transport, or model\\n+ selection logic.\\n \" }, { \"sha\": \"defc3f2e94e7d1f0e210a5b30f888798e9f0c353\", \"filename\": \"README.md\", \"status\": \"modified\", \"additions\": 14, \"deletions\": 9, \"changes\": 23, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/README.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/README.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/README.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -66,38 +66,37 @@ Use real workers by replacing `mock://` agents with OpenAI-compatible endpoints.\\n }\\n ```\\n \\n-For a local `mlx-lm` OpenAI-compatible server, use the explicit `mlx://` scheme. It is loopback-only, does not require a credential, and is translated to HTTP only after the loopback check:\\n+For a local OpenAI-compatible gateway, use the explicit `local://` loopback scheme and name its separate KV credential. Provider-specific runtime settings stay behind that gateway:\\n \\n ```json\\n {\\n \\\"agents\\\": [\\n {\\n- \\\"id\\\": \\\"local_fast_agent\\\",\\n- \\\"model\\\": \\\"mlx-community/llama-3.2-3b-instruct-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n+ \\\"id\\\": \\\"local_gateway_agent\\\",\\n+ \\\"model\\\": \\\"gateway-selected-model\\\",\\n+ \\\"base_url\\\": \\\"local://127.0.0.1:8080/v1\\\",\\n+ \\\"local_credential_key\\\": \\\"LOCAL_GATEWAY_TOKEN\\\",\\n \\\"tags\\\": [\\\"reasoning\\\", \\\"coding\\\", \\\"verification\\\"]\\n }\\n ]\\n }\\n ```\\n \\n The full local candidate registry is [examples/agents.local.json](examples/agents.local.json).\\n-It contains the public `contextual-orchestrator` candidate, discovered MLX\\n-worker models, and every discovered llama.cpp/LM Studio candidate. Discovery\\n+It contains the public `contextual-orchestrator` candidate and generic local\\n+gateway/llama.cpp/LM Studio candidates. Discovery\\n does not decide governance state: seed candidates are enabled by default, while\\n `disabled` is reserved for an explicit operator/admin quarantine or a persisted\\n removal tombstone. The contextual-orchestrator record is excluded from internal\\n roles because this implementation has no bounded recursive self-call protocol;\\n that is a routing safety constraint, not a disabled candidate. The registry is\\n explicit; runtime discovery does not silently change the pool.\\n \\n-Run an evaluation against that server with `--temperature 0` for repeatable judging. For reasoning-capable mlx models, pass `--chat-template-args '{\\\"enable_thinking\\\":false}'` when a short structured judge response is required. `--local-concurrency N` enables bounded concurrent local batch requests (`1..64`; the current measured starting point for this server is `8`); when serving HTTP, set `--max-concurrent-runs N` explicitly as well if the measured batch concurrency exceeds the secure default of `8`. Keep interactive route/conduct requests on the default sequential path.\\n+Run an evaluation against that gateway with the normal provider capability contract. `--local-concurrency N` enables bounded concurrent local batch requests (`1..64`); when serving HTTP, set `--max-concurrent-runs N` explicitly as well if the measured batch concurrency exceeds the secure default of `8`. Keep interactive route/conduct requests on the default sequential path.\\n \\n Model-based conduct verification requires `fast-mlsirm` in the same runtime and fails closed when it is absent or broken; fast-mlsirm sends its judge completion through this contextual-orchestrator gateway, so no direct provider fallback is used. “Same runtime” means that the exact interpreter used for the live run can import both packages: install both checkouts into one environment (prefer editable installs), or expose both source roots with `PYTHONPATH` during a source run. Before a live judge benchmark, run `python -m contextual_orchestrator check-fast-mlsirm` with that exact interpreter. It prints the interpreter, package version, transitive-import status, and contextual contract check, and exits nonzero on a missing dependency or contract mismatch. Do not run the preflight in one virtual environment and the judge in another. See [ADR 0001](docs/planning/adrs/0001-fail-closed-model-judgment.md).\\n \\n 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.\\n-\\n 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) 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.\\n \\n Seed the credential into the KV once at bootstrap:\\n@@ -304,6 +303,12 @@ python tests/test_admin_contract.py\\n python tests/test_conventions.py\\n python tests/test_api_contract.py\\n python tests/test_security_hardening.py\\n+python tests/test_chat_model_capability_isolation.py\\n+python tests/test_chat_transport_role_separation.py\\n+python tests/test_chat_capability_unknown_identifiers.py\\n+python tests/test_chat_passthrough_capability_isolation.py\\n+python tests/test_inbound_request_framing.py\\n+python tests/test_inbound_request_total_deadline.py\\n python tests/test_repository_security_metadata.py\\n python tests/test_product_planning_contract.py\\n python tests/test_plugin_driven_artifacts.py\" }, { \"sha\": \"d00cff4624b7daf9946e5ccd793dd30ed4fefffc\", \"filename\": \"conductor/tracks/001-paper-grounded-orchestrator/spec.md\", \"status\": \"modified\", \"additions\": 2, \"deletions\": 0, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/conductor%2Ftracks%2F001-paper-grounded-orchestrator%2Fspec.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/conductor%2Ftracks%2F001-paper-grounded-orchestrator%2Fspec.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/conductor%2Ftracks%2F001-paper-grounded-orchestrator%2Fspec.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -5,6 +5,8 @@\\n - The public API accepts chat messages through one OpenAI-compatible endpoint.\\n - Simple prompts use one selected worker.\\n - Complex prompts use a workflow with thinker, worker, verifier, and synthesizer steps.\\n+- Image-bearing prompts keep their validated source image blocks in every\\n+ evidence-bearing step and use only explicitly VISION-capable agents.\\n - Workflow steps expose only the prior outputs listed in their access list.\\n - Operators can open a management console to inspect agent pool, policy, trace, and audit state.\\n - Tests encode the Fugu, TRINITY, and Conductor contracts.\" }, { \"sha\": \"f7e43c2a82e5e818a2b7460c9791b378ec49e3e4\", \"filename\": \"contextual_orchestrator/__init__.py\", \"status\": \"modified\", \"additions\": 6, \"deletions\": 1, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2F__init__.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2F__init__.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2F__init__.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -37,7 +37,12 @@\\n from .cost_router import CostRoutingCoordinator\\n from .credentials import NotConfigured, get_credential, register_credential\\n from .kv_config import InMemoryConfigStore, get_config_store\\n-from .orchestrator import ModelAgent, TaskOrchestrator, WorkflowStep, load_agents\\n+from .orchestrator import (\\n+ ModelAgent,\\n+ WorkflowStep,\\n+ load_agents,\\n+)\\n+from .passthrough_failover import TaskOrchestrator\\n from .token_counting import HeuristicTokenCounter, build_token_counter\\n \\n __all__ = [\" }, { \"sha\": \"850aa27dab97ca4fc2faf3cdfc64f33a6b7edd23\", \"filename\": \"contextual_orchestrator/__main__.py\", \"status\": \"modified\", \"additions\": 116, \"deletions\": 22, \"changes\": 138, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2F__main__.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2F__main__.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2F__main__.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -4,17 +4,22 @@\\n \\n import argparse\\n import json\\n+import math\\n import os\\n import sys\\n from dataclasses import replace\\n \\n from .cost_ledger import PriceBook\\n+from .cost_router import CostRoutingCoordinator\\n from .credentials import get_credential, register_credential\\n from .kv_config import InMemoryConfigStore\\n from .model_discovery import (\\n+ ProviderDiscoveryError,\\n+ ProviderModelSource,\\n agent_from_discovered,\\n agent_id_for,\\n discover_all_models,\\n+ discover_provider_models,\\n refresh_price_book,\\n select_top_n_cheapest_discovered_agents,\\n )\\n@@ -23,16 +28,30 @@\\n MAX_LOCAL_CONCURRENCY,\\n ModelAgent,\\n ModelClient,\\n- TaskOrchestrator,\\n load_agents,\\n )\\n+from .passthrough_failover import TaskOrchestrator\\n from .server import SecurityConfig, serve\\n \\n DEFAULT_AUTH_TOKEN_KEY = \\\"CONTEXTUAL_ORCHESTRATOR_TOKEN\\\"\\n DEFAULT_ADMIN_TOKEN_KEY = \\\"CONTEXTUAL_ORCHESTRATOR_ADMIN_TOKEN\\\"\\n DEFAULT_INFERENCE_TOKEN_KEY = \\\"CONTEXTUAL_ORCHESTRATOR_INFERENCE_TOKEN\\\"\\n \\n \\n+def _bootstrap_telemetry_config() -> InMemoryConfigStore:\\n+ \\\"\\\"\\\"Load non-secret OTEL deployment settings into the process KV at startup.\\\"\\\"\\\"\\n+ config = InMemoryConfigStore()\\n+ for environment_name, key in (\\n+ (\\\"OTEL_EXPORTER_OTLP_ENDPOINT\\\", \\\"exporter_otlp_endpoint\\\"),\\n+ (\\\"OTEL_SERVICE_NAME\\\", \\\"service_name\\\"),\\n+ (\\\"OTEL_SDK_DISABLED\\\", \\\"sdk_disabled\\\"),\\n+ ):\\n+ value = os.environ.get(environment_name, \\\"\\\").strip()\\n+ if value:\\n+ config.set(\\\"telemetry\\\", key, value)\\n+ return config\\n+\\n+\\n def _positive_int(value: str) -> int:\\n \\\"\\\"\\\"Parse a strictly positive integer for an argparse option.\\\"\\\"\\\"\\n try:\\n@@ -65,14 +84,14 @@ def _local_concurrency(value: str) -> int:\\n return parsed\\n \\n \\n-def _json_object(value: str) -> dict[str, object]:\\n- \\\"\\\"\\\"Parse a JSON object for an argparse option, rejecting other JSON values.\\\"\\\"\\\"\\n+def _request_read_timeout(value: str) -> float:\\n+ \\\"\\\"\\\"Parse a finite request-body deadline in the server-supported range.\\\"\\\"\\\"\\n try:\\n- parsed = json.loads(value)\\n- except json.JSONDecodeError as exc:\\n- raise argparse.ArgumentTypeError(\\\"valid JSON object required\\\") from exc\\n- if not isinstance(parsed, dict):\\n- raise argparse.ArgumentTypeError(\\\"JSON object required\\\")\\n+ parsed = float(value)\\n+ except ValueError as exc:\\n+ raise argparse.ArgumentTypeError(\\\"number in 0.1..120 required\\\") from exc\\n+ if not math.isfinite(parsed) or not 0.1 <= parsed <= 120.0:\\n+ raise argparse.ArgumentTypeError(\\\"number in 0.1..120 required\\\")\\n return parsed\\n \\n \\n@@ -249,15 +268,59 @@ def _discover_models_command(argv: list[str]) -> None:\\n raise SystemExit(1)\\n \\n \\n-def main() -> None:\\n+def _auto_discover_seed_agents(\\n+ agents: list[ModelAgent], *, allow_failures: bool\\n+) -> list[ModelAgent]:\\n+ \\\"\\\"\\\"Expand empty-model gateway seeds without selecting a model in the consumer.\\\"\\\"\\\"\\n+ expanded: list[ModelAgent] = []\\n+ for seed in agents:\\n+ if seed.model:\\n+ expanded.append(seed)\\n+ continue\\n+ source = ProviderModelSource(\\n+ provider_name=seed.provider_name or seed.id,\\n+ credential_name=seed.credential_name,\\n+ list_url=f\\\"{seed.base_url.rstrip('/')}/models\\\",\\n+ chat_base_url=seed.base_url,\\n+ auth_scheme=seed.auth_scheme,\\n+ )\\n+ try:\\n+ discovered = discover_provider_models(source)\\n+ except ProviderDiscoveryError:\\n+ if not allow_failures:\\n+ raise\\n+ expanded.append(replace(seed, disabled=True))\\n+ continue\\n+ # OpenAI-compatible registries do not always declare task capabilities;\\n+ # embedding deployments cannot serve the chat worker pool.\\n+ chat_models = [model for model in discovered if \\\"embedding\\\" not in model.model_id.casefold()]\\n+ if not chat_models:\\n+ expanded.append(replace(seed, disabled=True))\\n+ continue\\n+ for model in chat_models:\\n+ discovered_agent = agent_from_discovered(model, priority=seed.priority)\\n+ expanded.append(\\n+ replace(\\n+ discovered_agent,\\n+ id=f\\\"{seed.id}_{agent_id_for(model)}\\\",\\n+ tags=seed.tags,\\n+ disabled=seed.disabled,\\n+ provider_exclusions=seed.provider_exclusions,\\n+ )\\n+ )\\n+ return expanded\\n+\\n+\\n+def main(argv: list[str] | None = None) -> None:\\n \\\"\\\"\\\"Parse CLI options and run bootstrap, prompt completion, or the HTTP server.\\\"\\\"\\\"\\n- if len(sys.argv) > 1 and sys.argv[1] == \\\"register-credential\\\":\\n- _register_credential_command(sys.argv[2:])\\n+ arguments = list(sys.argv[1:] if argv is None else argv)\\n+ if arguments and arguments[0] == \\\"register-credential\\\":\\n+ _register_credential_command(arguments[1:])\\n return\\n- if len(sys.argv) > 1 and sys.argv[1] == \\\"discover-models\\\":\\n- _discover_models_command(sys.argv[2:])\\n+ if arguments and arguments[0] == \\\"discover-models\\\":\\n+ _discover_models_command(arguments[1:])\\n return\\n- if len(sys.argv) > 1 and sys.argv[1] == \\\"check-fast-mlsirm\\\":\\n+ if arguments and arguments[0] == \\\"check-fast-mlsirm\\\":\\n _check_fast_mlsirm_command()\\n return\\n \\n@@ -268,6 +331,16 @@ def main() -> None:\\n help=\\\"Optional sqlite path to persist runs/audit/analytics across restarts (default: in-memory).\\\")\\n parser.add_argument(\\\"--mode\\\", choices=[\\\"auto\\\", \\\"route\\\", \\\"conduct\\\"], default=\\\"auto\\\")\\n parser.add_argument(\\\"--serve\\\", action=\\\"store_true\\\", help=\\\"Run the chat completions HTTP server.\\\")\\n+ parser.add_argument(\\n+ \\\"--auto-discover-model-agents\\\",\\n+ action=\\\"store_true\\\",\\n+ help=\\\"Expand empty-model agents from their configured OpenAI-compatible /models endpoint.\\\",\\n+ )\\n+ parser.add_argument(\\n+ \\\"--allow-discovery-failures\\\",\\n+ action=\\\"store_true\\\",\\n+ help=\\\"Keep failed discovery seeds disabled instead of aborting startup.\\\",\\n+ )\\n parser.add_argument(\\\"--host\\\", default=\\\"127.0.0.1\\\")\\n parser.add_argument(\\\"--port\\\", type=int, default=8000)\\n parser.add_argument(\\\"--auth-token\\\", default=\\\"\\\", help=\\\"Explicit local-development bearer token; prefer a KV token name.\\\")\\n@@ -295,21 +368,31 @@ def main() -> None:\\n \\\"--temperature\\\",\\n dest=\\\"sampling_temperature\\\",\\n type=float,\\n- default=0.2,\\n- help=\\\"Default provider sampling temperature (default: 0.2; --temperature is a compatibility alias).\\\",\\n+ default=None,\\n+ help=\\\"Optional provider sampling temperature; omitted by default (--temperature is an alias).\\\",\\n )\\n parser.add_argument(\\\"--max-output-tokens\\\", type=int, default=2048,\\n help=\\\"Default provider output token cap (default: 2048).\\\")\\n+ parser.add_argument(\\n+ \\\"--max-body-bytes\\\",\\n+ type=_positive_int,\\n+ default=64 * 1024,\\n+ help=\\\"Maximum JSON request body size in bytes (default: 65536).\\\",\\n+ )\\n+ parser.add_argument(\\n+ \\\"--request-read-timeout-seconds\\\",\\n+ type=_request_read_timeout,\\n+ default=10.0,\\n+ help=\\\"Maximum time to read one fixed-length JSON body (default: 10 seconds).\\\",\\n+ )\\n parser.add_argument(\\\"--local-concurrency\\\", type=_local_concurrency, default=1,\\n- help=f\\\"Concurrent requests for explicit mlx:// local batch work (default: 1; maximum: {MAX_LOCAL_CONCURRENCY}).\\\")\\n+ help=f\\\"Concurrent requests for local gateway batch work (default: 1; maximum: {MAX_LOCAL_CONCURRENCY}).\\\")\\n parser.add_argument(\\\"--max-concurrent-runs\\\", type=_local_concurrency, default=8,\\n help=f\\\"Maximum simultaneous HTTP orchestration runs (default: 8; maximum: {MAX_LOCAL_CONCURRENCY}).\\\")\\n parser.add_argument(\\\"--route-text-length-threshold\\\", type=_positive_int, default=None,\\n help=\\\"Auto-mode minimum prompt length that can trigger conduct instead of route.\\\")\\n parser.add_argument(\\\"--conduct-hint-threshold\\\", type=_positive_int, default=None,\\n help=\\\"Auto-mode hint-count minimum that can trigger conduct instead of route.\\\")\\n- parser.add_argument(\\\"--chat-template-args\\\", type=_json_object, default={},\\n- help=\\\"JSON kwargs forwarded to local mlx-lm chat templates, e.g. '{\\\\\\\"enable_thinking\\\\\\\":false}'.\\\")\\n parser.add_argument(\\\"--budget-max-output-tokens\\\", type=int, default=None,\\n help=\\\"Refuse new runs once estimated/reported output tokens reach this cap (default: no cap).\\\")\\n parser.add_argument(\\\"--budget-max-cost-usd\\\", type=float, default=None,\\n@@ -318,18 +401,23 @@ def main() -> None:\\n help=\\\"Seconds to cache identical requests (default 0 = disabled).\\\")\\n parser.add_argument(\\\"--eval\\\", nargs=\\\"+\\\", metavar=\\\"PROMPT\\\",\\n help=\\\"Measure orchestration vs a single-worker baseline on these prompts and print the report.\\\")\\n- args = parser.parse_args()\\n+ args = parser.parse_args(arguments)\\n \\n client = ModelClient(\\n ca_bundle=args.provider_ca_bundle,\\n temperature=args.sampling_temperature,\\n max_output_tokens=args.max_output_tokens,\\n local_concurrency=args.local_concurrency,\\n- chat_template_args=args.chat_template_args,\\n allowed_provider_hosts=args.allowed_provider_hosts,\\n )\\n+ agents = load_agents(args.agents)\\n+ if args.auto_discover_model_agents:\\n+ try:\\n+ agents = _auto_discover_seed_agents(agents, allow_failures=args.allow_discovery_failures)\\n+ except (ProviderDiscoveryError, ValueError) as exc:\\n+ parser.error(str(exc))\\n orchestrator = TaskOrchestrator(\\n- load_agents(args.agents),\\n+ agents,\\n client=client,\\n state_db=args.state_db,\\n agents_db=args.agents_db,\\n@@ -396,8 +484,14 @@ def main() -> None:\\n max_concurrent_runs=args.max_concurrent_runs,\\n allow_public_bind=args.allow_public_bind,\\n expose_trace_by_default=args.expose_trace_by_default,\\n+ max_body_bytes=args.max_body_bytes,\\n+ request_read_timeout_seconds=args.request_read_timeout_seconds,\\n ),\\n clearfolio_url=args.clearfolio_url,\\n+ coordinator=CostRoutingCoordinator(\\n+ orchestrator,\\n+ config_store=_bootstrap_telemetry_config(),\\n+ ),\\n )\\n return\\n \" }, { \"sha\": \"454cd7620e28ccbec1f95da9990f7327ba9121ae\", \"filename\": \"contextual_orchestrator/api_contract.py\", \"status\": \"modified\", \"additions\": 16, \"deletions\": 5, \"changes\": 21, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fapi_contract.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fapi_contract.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fapi_contract.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -111,17 +111,20 @@\\n \\\"/v1/embeddings\\\": {\\n \\\"post\\\": {\\n \\\"operationId\\\": \\\"create_embedding\\\",\\n- \\\"summary\\\": \\\"Create embeddings for semantic input\\\",\\n+ \\\"summary\\\": \\\"Create embeddings with optional orchestrator-owned model selection\\\",\\n \\\"security\\\": [{\\\"inference_bearer_auth\\\": []}],\\n \\\"requestBody\\\": {\\n \\\"required\\\": True,\\n \\\"content\\\": {\\n \\\"application/json\\\": {\\n \\\"schema\\\": {\\n \\\"type\\\": \\\"object\\\",\\n- \\\"required\\\": [\\\"model\\\", \\\"input\\\"],\\n+ \\\"required\\\": [\\\"input\\\"],\\n \\\"properties\\\": {\\n- \\\"model\\\": {\\\"type\\\": \\\"string\\\"},\\n+ \\\"model\\\": {\\n+ \\\"type\\\": \\\"string\\\",\\n+ \\\"description\\\": \\\"Optional enabled embedding-capable pool model; omitted selects one.\\\",\\n+ },\\n \\\"input\\\": {\\n \\\"oneOf\\\": [\\n {\\\"type\\\": \\\"string\\\"},\\n@@ -136,6 +139,7 @@\\n \\\"responses\\\": {\\n \\\"200\\\": {\\\"description\\\": \\\"Embedding response\\\"},\\n \\\"400\\\": {\\\"description\\\": \\\"Invalid request\\\"},\\n+ \\\"503\\\": {\\\"description\\\": \\\"No enabled embedding-capable agent is available\\\"},\\n },\\n }\\n },\\n@@ -163,6 +167,7 @@\\n \\\"responses\\\": {\\n \\\"200\\\": {\\\"description\\\": \\\"Responses API result\\\"},\\n \\\"400\\\": {\\\"description\\\": \\\"Invalid request\\\"},\\n+ \\\"422\\\": {\\\"description\\\": \\\"Valid request shape with unsupported orchestration controls\\\"},\\n },\\n }\\n },\\n@@ -587,9 +592,15 @@\\n \\\"application/json\\\": {\\n \\\"schema\\\": {\\n \\\"type\\\": \\\"object\\\",\\n- \\\"required\\\": [\\\"model\\\"],\\n+ \\\"anyOf\\\": [\\n+ {\\\"required\\\": [\\\"input\\\"]},\\n+ {\\\"required\\\": [\\\"inputs\\\"]},\\n+ ],\\n \\\"properties\\\": {\\n- \\\"model\\\": {\\\"type\\\": \\\"string\\\"},\\n+ \\\"model\\\": {\\n+ \\\"type\\\": \\\"string\\\",\\n+ \\\"description\\\": \\\"Optional enabled embedding-capable pool model; omitted selects one.\\\",\\n+ },\\n \\\"input\\\": {\\n \\\"oneOf\\\": [\\n {\\\"type\\\": \\\"string\\\"},\" }, { \"sha\": \"68a2af371d7925b096d1d9b5278c428a9c4bbd5d\", \"filename\": \"contextual_orchestrator/batch_routing.py\", \"status\": \"modified\", \"additions\": 23, \"deletions\": 2, \"changes\": 25, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fbatch_routing.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fbatch_routing.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fbatch_routing.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -26,6 +26,7 @@\\n import time\\n import uuid\\n from concurrent.futures import ThreadPoolExecutor\\n+from contextvars import copy_context\\n from dataclasses import dataclass, field\\n from typing import Any, Callable, Dict, List, Optional, Protocol\\n \\n@@ -253,8 +254,13 @@ def run(request: BatchRequest) -> BatchResultItem:\\n if self.max_concurrency == 1 or len(requests) <= 1:\\n items = [run(request) for request in requests]\\n else:\\n+ def run_with_context(item: tuple[Any, BatchRequest]) -> BatchResultItem:\\n+ context, request = item\\n+ return context.run(run, request)\\n+\\n+ contexts_and_requests = [(copy_context(), request) for request in requests]\\n with ThreadPoolExecutor(max_workers=min(self.max_concurrency, len(requests))) as pool:\\n- items = list(pool.map(run, requests))\\n+ items = list(pool.map(run_with_context, contexts_and_requests))\\n self._results[job_id] = items\\n return BatchJob(job_id=job_id, backend=self.name, status=\\\"completed\\\", request_count=len(requests))\\n \\n@@ -411,6 +417,7 @@ class EmbeddingBatchRequest:\\n \\n input_text: str\\n model: str = \\\"contextual-orchestrator\\\"\\n+ provider_name: str = \\\"unknown\\\"\\n custom_id: str = field(default_factory=lambda: f\\\"emb_{uuid.uuid4().hex}\\\")\\n attribution: Dict[str, Any] = field(default_factory=dict)\\n source_index: int = 0\\n@@ -437,6 +444,7 @@ class EmbeddingBatchResultItem:\\n embedding: List[float]\\n prompt_tokens: int = 0\\n model: str = \\\"contextual-orchestrator\\\"\\n+ provider_name: str = \\\"unknown\\\"\\n \\n \\n class EmbeddingBatchBackend(Protocol):\\n@@ -493,10 +501,12 @@ def __init__(\\n self,\\n embedder: Optional[Callable[[str], List[float]]] = None,\\n *,\\n+ batch_embedder: Optional[Callable[[List[EmbeddingBatchRequest]], List[List[float]]]] = None,\\n token_counter: Any = None,\\n dimension: int = _DEFAULT_EMBEDDING_DIMENSION,\\n ) -> None:\\n self._embedder = embedder or (lambda text: heuristic_embedding(text, dimension))\\n+ self._batch_embedder = batch_embedder\\n self._token_counter = token_counter\\n self._results: Dict[str, List[EmbeddingBatchResultItem]] = {}\\n \\n@@ -511,15 +521,23 @@ def submit(\\n ) -> BatchJob:\\n \\\"\\\"\\\"Embed every input in-process and stash the results under a job id.\\\"\\\"\\\"\\n job_id = f\\\"localembed_{uuid.uuid4().hex}\\\"\\n+ vectors = (\\n+ self._batch_embedder(requests)\\n+ if self._batch_embedder is not None\\n+ else [self._embedder(request.input_text) for request in requests]\\n+ )\\n+ if len(vectors) != len(requests):\\n+ raise RuntimeError(\\\"embedding provider returned an incomplete vector batch\\\")\\n items: List[EmbeddingBatchResultItem] = []\\n for index, request in enumerate(requests):\\n items.append(\\n EmbeddingBatchResultItem(\\n custom_id=request.custom_id,\\n index=index,\\n- embedding=list(self._embedder(request.input_text)),\\n+ embedding=list(vectors[index]),\\n prompt_tokens=self._count_tokens(request.input_text, request.model),\\n model=request.model,\\n+ provider_name=request.provider_name,\\n )\\n )\\n self._results[job_id] = items\\n@@ -639,6 +657,9 @@ async def _download() -> Dict[str, Any]:\\n embedding=embedding,\\n prompt_tokens=int(usage.get(\\\"prompt_tokens\\\", 0)),\\n model=tracked_request.model if tracked_request else \\\"contextual-orchestrator\\\",\\n+ provider_name=(\\n+ tracked_request.provider_name if tracked_request else \\\"unknown\\\"\\n+ ),\\n )\\n )\\n items.sort(key=lambda item: item.index)\" }, { \"sha\": \"4a2a96cd560621fcc8099010ec759a2f4f75be40\", \"filename\": \"contextual_orchestrator/chat_capability.py\", \"status\": \"added\", \"additions\": 96, \"deletions\": 0, \"changes\": 96, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fchat_capability.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fchat_capability.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fchat_capability.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,96 @@\\n+\\\"\\\"\\\"Classify chat transport compatibility and ordinary agent-role eligibility.\\n+\\n+Provider catalogs mix endpoint-only models with models served through an\\n+OpenAI-compatible chat transport. Transport compatibility is not the same as\\n+fitness for an ordinary thinker, worker, verifier, or synthesizer role: audio\\n+and policy-classification models can use chat transport, while embedding,\\n+reranking, transcription, moderation-endpoint, image-generation, realtime, and\\n+speech-only models cannot.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import re\\n+\\n+_MODEL_TOKEN_RE = re.compile(r\\\"[a-z0-9]+\\\")\\n+_TRANSPORT_INCOMPATIBLE_EXACT_TOKENS = frozenset(\\n+ {\\n+ \\\"bge\\\",\\n+ \\\"clip\\\",\\n+ \\\"dall\\\",\\n+ \\\"e5\\\",\\n+ \\\"embed\\\",\\n+ \\\"embedding\\\",\\n+ \\\"embeddings\\\",\\n+ \\\"gte\\\",\\n+ \\\"image\\\",\\n+ \\\"images\\\",\\n+ \\\"moderation\\\",\\n+ \\\"realtime\\\",\\n+ \\\"rerank\\\",\\n+ \\\"reranker\\\",\\n+ \\\"siglip\\\",\\n+ \\\"sora\\\",\\n+ \\\"speech\\\",\\n+ \\\"transcribe\\\",\\n+ \\\"transcription\\\",\\n+ \\\"tts\\\",\\n+ \\\"whisper\\\",\\n+ }\\n+)\\n+_TRANSPORT_INCOMPATIBLE_PREFIXES = (\\n+ \\\"embed\\\",\\n+ \\\"moderat\\\",\\n+ \\\"rerank\\\",\\n+ \\\"transcrib\\\",\\n+)\\n+\\n+\\n+def is_chat_compatible_model_id(model_id: str) -> bool:\\n+ \\\"\\\"\\\"Return whether an identifier can use the ordinary chat transport.\\n+\\n+ The classifier rejects only identifiers that clearly advertise an endpoint\\n+ family incompatible with chat messages. Audio-capable and safety-classifier\\n+ models remain transport-compatible because providers serve some of them over\\n+ ``/chat/completions``.\\n+ \\\"\\\"\\\"\\n+ tokens = _model_tokens(model_id)\\n+ return _is_transport_compatible_tokens(tokens)\\n+\\n+\\n+def _is_transport_compatible_tokens(tokens: tuple[str, ...]) -> bool:\\n+ \\\"\\\"\\\"Judge transport compatibility from already-normalized model tokens.\\\"\\\"\\\"\\n+ if not tokens:\\n+ return False\\n+ for token in tokens:\\n+ if token in _TRANSPORT_INCOMPATIBLE_EXACT_TOKENS:\\n+ return False\\n+ if token.startswith(_TRANSPORT_INCOMPATIBLE_PREFIXES):\\n+ return False\\n+ return True\\n+\\n+\\n+def _model_tokens(model_id: str) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Normalize one provider-prefixed model identifier into lowercase tokens.\\\"\\\"\\\"\\n+ if not isinstance(model_id, str):\\n+ return ()\\n+ return tuple(_MODEL_TOKEN_RE.findall(model_id.casefold()))\\n+\\n+\\n+def is_general_chat_agent_model_id(model_id: str) -> bool:\\n+ \\\"\\\"\\\"Return whether a chat model may enter ordinary orchestration roles.\\n+\\n+ Explicit guard and safety models can use chat transport but are specialized\\n+ policy classifiers, not general answer synthesizers. This negative role gate\\n+ does not infer reasoning, coding, vision, or verification capabilities.\\n+ \\\"\\\"\\\"\\n+ tokens = _model_tokens(model_id)\\n+ if not tokens or not _is_transport_compatible_tokens(tokens):\\n+ return False\\n+ return not any(\\n+ token == \\\"safety\\\"\\n+ or token == \\\"guard\\\"\\n+ or token == \\\"shieldgemma\\\"\\n+ or token.startswith(\\\"nemoguard\\\")\\n+ for token in tokens\\n+ )\" }, { \"sha\": \"4ce34b6ea106497738b8a3bec3de422a80d7484f\", \"filename\": \"contextual_orchestrator/cost_ledger.py\", \"status\": \"modified\", \"additions\": 6, \"deletions\": 1, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fcost_ledger.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fcost_ledger.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fcost_ledger.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -330,6 +330,7 @@ class NoopUsageTelemetrySink:\\n \\\"\\\"\\\"Default sink for callers that do not wire telemetry yet.\\\"\\\"\\\"\\n \\n def emit_usage(self, event: UsageTelemetryEvent) -> None:\\n+ \\\"\\\"\\\"Discard one prompt-safe usage event.\\\"\\\"\\\"\\n return None\\n \\n \\n@@ -342,12 +343,14 @@ def __init__(self, max_events: int = 512) -> None:\\n self._lock = threading.Lock()\\n \\n def emit_usage(self, event: UsageTelemetryEvent) -> None:\\n+ \\\"\\\"\\\"Append one usage event while retaining only the configured limit.\\\"\\\"\\\"\\n with self._lock:\\n self._events.append(event)\\n if len(self._events) > self._max_events:\\n del self._events[: len(self._events) - self._max_events]\\n \\n def events(self) -> List[UsageTelemetryEvent]:\\n+ \\\"\\\"\\\"Return a thread-safe snapshot of retained usage events.\\\"\\\"\\\"\\n with self._lock:\\n return list(self._events)\\n \\n@@ -363,6 +366,7 @@ class UsageTelemetryHealth:\\n last_error_type: Optional[str] = None\\n \\n def as_dict(self) -> Dict[str, Any]:\\n+ \\\"\\\"\\\"Return operator-safe counters as a serializable mapping.\\\"\\\"\\\"\\n return {\\n \\\"records_accepted\\\": self.records_accepted,\\n \\\"records_stored\\\": self.records_stored,\\n@@ -442,6 +446,7 @@ def flush(self, timeout: Optional[float] = None) -> bool:\\n return True\\n \\n def telemetry_health(self) -> Dict[str, Any]:\\n+ \\\"\\\"\\\"Return a thread-safe snapshot of persistence health counters.\\\"\\\"\\\"\\n with self._lock:\\n return self._health.as_dict()\\n \\n@@ -669,7 +674,7 @@ def query(self, start: Optional[int] = None, end: Optional[int] = None) -> List[\\n _USAGE_QUERY_SQL[(self._paramstyle, start is not None, end is not None)],\\n tuple(params),\\n )\\n- return [dict(zip(_USAGE_COLUMNS, values)) for values in cur.fetchall()]\\n+ return [dict(zip(_USAGE_COLUMNS, values, strict=True)) for values in cur.fetchall()]\\n \\n \\n def _within_window(created_at: int, start: Optional[int], end: Optional[int]) -> bool:\" }, { \"sha\": \"1120b9877f000c96483b5234a6f05742ab68bdca\", \"filename\": \"contextual_orchestrator/cost_router.py\", \"status\": \"modified\", \"additions\": 272, \"deletions\": 31, \"changes\": 303, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fcost_router.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fcost_router.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fcost_router.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -34,7 +34,7 @@\\n RoutingHints,\\n RoutingPolicy,\\n )\\n-from .cost_ledger import CostLedger, PriceBook\\n+from .cost_ledger import CostLedger, PriceBook, UsageRecord\\n from .kv_config import InMemoryConfigStore\\n from .token_counting import HeuristicTokenCounter, build_token_counter\\n \\n@@ -77,21 +77,74 @@ def __init__(\\n )\\n else:\\n self.batch_backend = batch_backend\\n- self.embedding_batch_backend: EmbeddingBatchBackend = (\\n- embedding_batch_backend\\n- or LocalEmbeddingBatchBackend(token_counter=self.token_counter)\\n- )\\n+ self._embedding_backend_is_default = embedding_batch_backend is None\\n+ self.embedding_batch_backend: EmbeddingBatchBackend = embedding_batch_backend or self._default_embedding_backend()\\n # job_id -> submitted BatchJob (so poll/retrieve can be driven by id)\\n self._batch_jobs: Dict[str, BatchJob] = {}\\n # embeddings batch state: job handle + submitted requests + cached doc,\\n # keyed by batch id so poll/retrieve is idempotent (usage recorded once).\\n self._embedding_jobs: Dict[str, BatchJob] = {}\\n+ self._embedding_job_backends: Dict[str, EmbeddingBatchBackend] = {}\\n self._embedding_requests: Dict[str, List[EmbeddingBatchRequest]] = {}\\n self._embedding_input_counts: Dict[str, int] = {}\\n self._embedding_part_counts: Dict[str, List[int]] = {}\\n self._embedding_part_limits: Dict[str, Dict[str, int]] = {}\\n self._embedding_documents: Dict[str, Dict[str, Any]] = {}\\n \\n+ def _default_embedding_backend(self) -> EmbeddingBatchBackend:\\n+ \\\"\\\"\\\"Use a provider-tagged embedding agent when one is configured.\\\"\\\"\\\"\\n+ candidates = [\\n+ agent\\n+ for agent in getattr(self.orchestrator, \\\"candidates\\\", [])\\n+ if not agent.disabled\\n+ and \\\"embedding\\\" in agent.tags\\n+ and not agent.base_url.startswith(\\\"mock://\\\")\\n+ ]\\n+ if not candidates:\\n+ return LocalEmbeddingBatchBackend(token_counter=self.token_counter)\\n+\\n+ def embed_batch(requests: List[EmbeddingBatchRequest]) -> List[List[float]]:\\n+ vectors: List[List[float] | None] = [None] * len(requests)\\n+ provider_models = {\\n+ (request.provider_name, request.model) for request in requests\\n+ }\\n+ for provider_name, model in provider_models:\\n+ matching = [\\n+ agent\\n+ for agent in candidates\\n+ if agent.model == model\\n+ and (agent.provider_name or _provider_from_base_url(agent.base_url) or \\\"unknown\\\")\\n+ == provider_name\\n+ ]\\n+ if not matching:\\n+ raise ValueError(f\\\"embedding model {model!r} is not configured in the embedding agent pool\\\")\\n+ selected = max(matching, key=lambda agent: (agent.priority, agent.id))\\n+ indexes = [\\n+ index\\n+ for index, request in enumerate(requests)\\n+ if request.model == model and request.provider_name == provider_name\\n+ ]\\n+ client = getattr(self.orchestrator, \\\"client\\\", None)\\n+ embed_many = getattr(client, \\\"embed_many\\\", None)\\n+ if not callable(embed_many):\\n+ raise RuntimeError(\\\"configured embedding agent has no provider embedding client\\\")\\n+ batch_vectors = embed_many(\\n+ selected,\\n+ [requests[index].input_text for index in indexes],\\n+ )\\n+ if len(batch_vectors) != len(indexes):\\n+ raise RuntimeError(\\\"provider returned an incomplete embedding vector batch\\\")\\n+ for index, vector in zip(indexes, batch_vectors, strict=True):\\n+ vectors[index] = vector\\n+ if any(vector is None for vector in vectors):\\n+ raise RuntimeError(\\\"provider returned an incomplete embedding vector batch\\\")\\n+ return [vector for vector in vectors if vector is not None]\\n+\\n+ return LocalEmbeddingBatchBackend(\\n+ batch_embedder=embed_batch,\\n+ token_counter=self.token_counter,\\n+ )\\n+\\n # ------------------------------------------------------------------\\n # Provider / model resolution\\n # ------------------------------------------------------------------\\n@@ -110,6 +163,63 @@ def _served_provider_model(self, result: Dict[str, Any], fallback_model: str) ->\\n pass\\n return \\\"unknown\\\", fallback_model\\n \\n+ @staticmethod\\n+ def _provider_usage_counts(usage: Any) -> tuple[int, int] | None:\\n+ \\\"\\\"\\\"Return validated provider token counts, accepting Chat and Responses names.\\\"\\\"\\\"\\n+ if not isinstance(usage, dict):\\n+ return None\\n+ prompt_tokens = usage.get(\\\"prompt_tokens\\\", usage.get(\\\"input_tokens\\\"))\\n+ completion_tokens = usage.get(\\\"completion_tokens\\\", usage.get(\\\"output_tokens\\\"))\\n+ if (\\n+ type(prompt_tokens) is not int\\n+ or prompt_tokens < 0\\n+ or type(completion_tokens) is not int\\n+ or completion_tokens < 0\\n+ ):\\n+ return None\\n+ return prompt_tokens, completion_tokens\\n+\\n+ def _record_provider_workflow_usage(\\n+ self,\\n+ result: Dict[str, Any],\\n+ *,\\n+ attribution: Optional[Dict[str, Any]],\\n+ model_name: str,\\n+ ) -> tuple[List[UsageRecord], int]:\\n+ \\\"\\\"\\\"Record every provider-reported workflow call under one run lineage.\\\"\\\"\\\"\\n+ steps = [step for step in result.get(\\\"trace\\\", []) if isinstance(step, dict)]\\n+ verification = result.get(\\\"verification\\\")\\n+ if isinstance(verification, dict) and isinstance(verification.get(\\\"judge_usage\\\"), dict):\\n+ judge_step = {\\n+ \\\"agent_id\\\": verification.get(\\\"judge_agent_id\\\"),\\n+ \\\"usage\\\": verification[\\\"judge_usage\\\"],\\n+ }\\n+ # Provider-facing synthesis is the final persisted trace step.\\n+ steps.insert(max(0, len(steps) - 1), judge_step)\\n+\\n+ records: List[UsageRecord] = []\\n+ unmetered_count = 0\\n+ for step in steps:\\n+ counts = self._provider_usage_counts(step.get(\\\"usage\\\"))\\n+ if counts is None:\\n+ unmetered_count += 1\\n+ continue\\n+ records.append(\\n+ self._record_completion(\\n+ messages=[],\\n+ answer=\\\"\\\",\\n+ route_mode=result.get(\\\"mode\\\"),\\n+ request_channel=\\\"sync\\\",\\n+ attribution=attribution,\\n+ model_name=model_name,\\n+ provider_model=self._served_provider_model({\\\"trace\\\": [step]}, model_name),\\n+ workflow_run_id=result.get(\\\"workflow_run_id\\\"),\\n+ prompt_tokens=counts[0],\\n+ completion_tokens=counts[1],\\n+ )\\n+ )\\n+ return records, unmetered_count\\n+\\n # ------------------------------------------------------------------\\n # Sync + batch completion\\n # ------------------------------------------------------------------\\n@@ -122,19 +232,28 @@ def complete(\\n hints: Optional[Dict[str, Any]] = None,\\n model_name: str = \\\"contextual-orchestrator\\\",\\n workflow_run_id: Optional[str] = None,\\n+ response_format: Optional[Dict[str, Any]] = None,\\n+ provider_request: Optional[Dict[str, Any]] = None,\\n+ provider_endpoint: str = \\\"chat/completions\\\",\\n ) -> Dict[str, Any]:\\n \\\"\\\"\\\"Route a request (sync or batch) and record its usage + cost.\\n \\n Sync requests run the orchestrator immediately and return the completion\\n augmented with ``channel``, ``routing_reason``, ``usage``, and the\\n ``usage_record_id``. Batch requests are dispatched to the batch backend\\n- and return a job envelope; their cost is recorded on retrieval.\\n+ and return a job envelope; their cost is recorded on retrieval. When a\\n+ validated ``provider_request`` is supplied, final synthesis preserves\\n+ that Chat or Responses wire contract while this coordinator still owns\\n+ the cost record.\\n \\\"\\\"\\\"\\n routing_hints = hints if isinstance(hints, RoutingHints) else RoutingHints.from_mapping(hints)\\n prompt_tokens_estimate = self.token_counter.count_messages(messages, model_name)\\n decision = self.policy.decide(routing_hints, prompt_tokens_estimate)\\n \\n- if decision.channel == \\\"batch\\\":\\n+ structured_output_forced_sync = decision.channel == \\\"batch\\\" and (\\n+ response_format is not None or provider_request is not None\\n+ )\\n+ if decision.channel == \\\"batch\\\" and not structured_output_forced_sync:\\n request = BatchRequest(\\n messages=messages,\\n model=model_name,\\n@@ -151,26 +270,100 @@ def complete(\\n \\\"request_count\\\": job.request_count,\\n }\\n \\n- result = self.orchestrator.run(messages, mode=mode, workflow_run_id=workflow_run_id)\\n- record = self._record_completion(\\n- messages=messages,\\n- answer=result.get(\\\"answer\\\", \\\"\\\"),\\n- route_mode=result.get(\\\"mode\\\"),\\n- request_channel=\\\"sync\\\",\\n- attribution=attribution,\\n- model_name=model_name,\\n- provider_model=self._served_provider_model(result, model_name),\\n- workflow_run_id=result.get(\\\"workflow_run_id\\\"),\\n- )\\n+ provider_response: Optional[Dict[str, Any]] = None\\n+ if provider_request is None:\\n+ result = self.orchestrator.run(\\n+ messages,\\n+ mode=mode,\\n+ workflow_run_id=workflow_run_id,\\n+ output_contract=response_format,\\n+ )\\n+ else:\\n+ if provider_endpoint not in {\\\"chat/completions\\\", \\\"responses\\\"}:\\n+ raise ValueError(\\\"provider_endpoint must be chat/completions or responses\\\")\\n+ provider_response = self.orchestrator.proxy_completion(\\n+ provider_request,\\n+ endpoint=provider_endpoint,\\n+ )\\n+ orchestration = provider_response.get(\\\"orchestration\\\")\\n+ if not isinstance(orchestration, dict) or not isinstance(\\n+ orchestration.get(\\\"workflow_run_id\\\"), str\\n+ ):\\n+ raise RuntimeError(\\\"provider completion omitted orchestration lineage\\\")\\n+ result = dict(\\n+ self.orchestrator.get_workflow_run(orchestration[\\\"workflow_run_id\\\"])\\n+ )\\n+ result[\\\"provider_response\\\"] = provider_response\\n+ records: List[UsageRecord] = []\\n+ unmetered_count = 0\\n+ if provider_response is not None:\\n+ records, unmetered_count = self._record_provider_workflow_usage(\\n+ result,\\n+ attribution=attribution,\\n+ model_name=model_name,\\n+ )\\n+ if not records:\\n+ provider_usage = (\\n+ provider_response.get(\\\"usage\\\")\\n+ if isinstance(provider_response, dict)\\n+ and isinstance(provider_response.get(\\\"usage\\\"), dict)\\n+ else {}\\n+ )\\n+ counts = self._provider_usage_counts(provider_usage)\\n+ record = self._record_completion(\\n+ messages=messages,\\n+ answer=result.get(\\\"answer\\\", \\\"\\\"),\\n+ route_mode=result.get(\\\"mode\\\"),\\n+ request_channel=\\\"sync\\\",\\n+ attribution=attribution,\\n+ model_name=model_name,\\n+ provider_model=self._served_provider_model(result, model_name),\\n+ workflow_run_id=result.get(\\\"workflow_run_id\\\"),\\n+ prompt_tokens=counts[0] if counts is not None else None,\\n+ completion_tokens=counts[1] if counts is not None else None,\\n+ )\\n+ records = [record]\\n+ record = records[-1]\\n+ prompt_tokens = sum(item.prompt_tokens for item in records)\\n+ completion_tokens = sum(item.completion_tokens for item in records)\\n+ currencies = {item.currency_code for item in records}\\n+ cost = {\\n+ \\\"cost_amount\\\": (\\n+ round(sum(item.cost_amount for item in records), 6)\\n+ if len(currencies) == 1\\n+ else None\\n+ ),\\n+ \\\"currency_code\\\": next(iter(currencies)) if len(currencies) == 1 else \\\"MIXED\\\",\\n+ }\\n result[\\\"channel\\\"] = \\\"sync\\\"\\n- result[\\\"routing_reason\\\"] = decision.reason\\n+ result[\\\"routing_reason\\\"] = (\\n+ f\\\"{decision.reason};structured_output_forced_sync\\\"\\n+ if structured_output_forced_sync\\n+ else decision.reason\\n+ )\\n result[\\\"usage_record_id\\\"] = record.usage_record_id\\n+ result[\\\"usage_record_ids\\\"] = [item.usage_record_id for item in records]\\n+ result[\\\"unmetered_provider_call_count\\\"] = unmetered_count\\n result[\\\"usage\\\"] = {\\n- \\\"prompt_tokens\\\": record.prompt_tokens,\\n- \\\"completion_tokens\\\": record.completion_tokens,\\n- \\\"total_tokens\\\": record.total_tokens,\\n+ \\\"prompt_tokens\\\": prompt_tokens,\\n+ \\\"completion_tokens\\\": completion_tokens,\\n+ \\\"total_tokens\\\": prompt_tokens + completion_tokens,\\n }\\n- result[\\\"cost\\\"] = {\\\"cost_amount\\\": record.cost_amount, \\\"currency_code\\\": record.currency_code}\\n+ result[\\\"cost\\\"] = cost\\n+ provider_response = result.get(\\\"provider_response\\\")\\n+ if isinstance(provider_response, dict):\\n+ orchestration = provider_response.get(\\\"orchestration\\\")\\n+ if isinstance(orchestration, dict):\\n+ orchestration.update(\\n+ {\\n+ \\\"channel\\\": result[\\\"channel\\\"],\\n+ \\\"routing_reason\\\": result[\\\"routing_reason\\\"],\\n+ \\\"usage_record_id\\\": result[\\\"usage_record_id\\\"],\\n+ \\\"usage_record_ids\\\": result[\\\"usage_record_ids\\\"],\\n+ \\\"unmetered_provider_call_count\\\": unmetered_count,\\n+ \\\"cost\\\": result[\\\"cost\\\"],\\n+ }\\n+ )\\n return result\\n \\n def _record_completion(\\n@@ -289,22 +482,63 @@ def submit_embeddings_batch(\\n and recorded cost are produced by :meth:`embeddings_batch_document`.\\n \\\"\\\"\\\"\\n shared_attribution = dict(attribution or {})\\n+ provider_name, resolved_model = self._resolve_embedding_provider_model(model)\\n requests, part_counts, part_limits = self._build_embedding_requests(\\n- inputs, model=model, attribution=shared_attribution\\n+ inputs,\\n+ model=resolved_model,\\n+ provider_name=provider_name,\\n+ attribution=shared_attribution,\\n )\\n- job = self.embedding_batch_backend.submit(requests, metadata=metadata)\\n+ backend = (\\n+ self._default_embedding_backend()\\n+ if self._embedding_backend_is_default\\n+ else self.embedding_batch_backend\\n+ )\\n+ job = backend.submit(requests, metadata=metadata)\\n self._embedding_jobs[job.job_id] = job\\n+ self._embedding_job_backends[job.job_id] = backend\\n self._embedding_requests[job.job_id] = requests\\n self._embedding_input_counts[job.job_id] = len(inputs)\\n self._embedding_part_counts[job.job_id] = part_counts\\n self._embedding_part_limits[job.job_id] = part_limits\\n return job\\n \\n+ def _resolve_embedding_provider_model(self, model: str) -> tuple[str, str]:\\n+ \\\"\\\"\\\"Resolve a server-owned embedding provider/model pair.\\\"\\\"\\\"\\n+ candidates = [\\n+ agent\\n+ for agent in getattr(self.orchestrator, \\\"candidates\\\", [])\\n+ if not agent.disabled and \\\"embedding\\\" in agent.tags\\n+ ]\\n+ if model == \\\"contextual-orchestrator\\\":\\n+ selector = getattr(self.orchestrator, \\\"select_capability_agent\\\", None)\\n+ if callable(selector):\\n+ agent = selector(\\\"embedding\\\")\\n+ elif candidates:\\n+ agent = max(\\n+ candidates,\\n+ key=lambda candidate: (candidate.priority, candidate.id),\\n+ )\\n+ else:\\n+ raise ValueError(\\\"no enabled embedding-capable agent is available\\\")\\n+ else:\\n+ matching = [agent for agent in candidates if agent.model == model]\\n+ if not matching:\\n+ if candidates:\\n+ raise ValueError(\\n+ f\\\"embedding model {model!r} is not configured in the embedding agent pool\\\"\\n+ )\\n+ return self.embedding_batch_backend.name, model\\n+ agent = max(matching, key=lambda candidate: (candidate.priority, candidate.id))\\n+ provider = agent.provider_name or _provider_from_base_url(agent.base_url) or \\\"unknown\\\"\\n+ return provider, agent.model\\n+\\n def _build_embedding_requests(\\n self,\\n inputs: List[str],\\n *,\\n model: str,\\n+ provider_name: str,\\n attribution: Dict[str, Any],\\n ) -> tuple[List[EmbeddingBatchRequest], List[int], Dict[str, int]]:\\n \\\"\\\"\\\"Map original embedding inputs into token-budgeted provider parts.\\\"\\\"\\\"\\n@@ -323,6 +557,7 @@ def _build_embedding_requests(\\n EmbeddingBatchRequest(\\n input_text=part_text,\\n model=model,\\n+ provider_name=provider_name,\\n attribution=dict(attribution),\\n source_index=source_index,\\n part_index=part_index,\\n@@ -475,7 +710,8 @@ def embeddings_batch_document(self, batch_id: str) -> Dict[str, Any]:\\n return cached\\n \\n job = self._require_embedding_job(batch_id)\\n- status = self.embedding_batch_backend.poll(job)\\n+ backend = self._embedding_job_backends.get(batch_id, self.embedding_batch_backend)\\n+ status = backend.poll(job)\\n if not status.get(\\\"is_complete\\\"):\\n return {\\n \\\"batch_id\\\": batch_id,\\n@@ -484,7 +720,7 @@ def embeddings_batch_document(self, batch_id: str) -> Dict[str, Any]:\\n \\\"embeddings\\\": None,\\n }\\n \\n- items: List[EmbeddingBatchResultItem] = self.embedding_batch_backend.retrieve(job)\\n+ items: List[EmbeddingBatchResultItem] = backend.retrieve(job)\\n requests = self._embedding_requests.get(batch_id, [])\\n request_by_custom_id = {request.custom_id: request for request in requests}\\n input_count = self._embedding_input_counts.get(batch_id, len(requests))\\n@@ -506,6 +742,11 @@ def embeddings_batch_document(self, batch_id: str) -> Dict[str, Any]:\\n \\\"embedding\\\": item.embedding,\\n \\\"prompt_tokens\\\": max(0, prompt_tokens),\\n \\\"model\\\": item.model,\\n+ \\\"provider_name\\\": (\\n+ item.provider_name\\n+ if item.provider_name != \\\"unknown\\\"\\n+ else request.provider_name if request else \\\"unknown\\\"\\n+ ),\\n \\\"attribution\\\": dict(request.attribution) if request else {},\\n }\\n )\\n@@ -515,6 +756,7 @@ def embeddings_batch_document(self, batch_id: str) -> Dict[str, Any]:\\n total_cost_amount = 0.0\\n currency_code = \\\"USD\\\"\\n model_name = \\\"contextual-orchestrator\\\"\\n+ provider_name = \\\"unknown\\\"\\n for source_index in range(input_count):\\n parts = sorted(parts_by_source.get(source_index, []), key=lambda item: item[\\\"part_index\\\"])\\n if not parts:\\n@@ -524,11 +766,9 @@ def embeddings_batch_document(self, batch_id: str) -> Dict[str, Any]:\\n attribution = dict(parts[0][\\\"attribution\\\"])\\n prompt_tokens = sum(int(part[\\\"prompt_tokens\\\"]) for part in parts)\\n model_name = str(parts[0][\\\"model\\\"])\\n- provider = str(\\n- attribution.get(\\\"provider\\\") or attribution.get(\\\"upstream_api\\\") or \\\"unknown\\\"\\n- )\\n+ provider_name = str(parts[0][\\\"provider_name\\\"])\\n record = self.ledger.record_usage(\\n- provider=provider,\\n+ provider=provider_name,\\n model=model_name,\\n prompt_tokens=prompt_tokens,\\n completion_tokens=0,\\n@@ -553,6 +793,7 @@ def embeddings_batch_document(self, batch_id: str) -> Dict[str, Any]:\\n \\\"batch_id\\\": batch_id,\\n \\\"status\\\": \\\"completed\\\",\\n \\\"backend\\\": job.backend,\\n+ \\\"provider\\\": provider_name,\\n \\\"model\\\": model_name,\\n \\\"embeddings\\\": embeddings,\\n \\\"token_counts\\\": token_counts,\" }, { \"sha\": \"edc331eb2fac4fe45720a45aec3ec94eb187dab1\", \"filename\": \"contextual_orchestrator/model_discovery.py\", \"status\": \"modified\", \"additions\": 95, \"deletions\": 57, \"changes\": 152, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fmodel_discovery.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fmodel_discovery.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fmodel_discovery.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,4 +1,4 @@\\n-\\\"\\\"\\\"Provider model-list discovery: turns registered KV credentials into agent candidates.\\n+\\\"\\\"\\\"Provider model-list discovery for chat-agent candidates.\\n \\n Queries each configured provider's model-list endpoint over its OpenAI-compatible\\n (or provider-specific) discovery API and returns :class:`DiscoveredModel` rows that\\n@@ -9,27 +9,45 @@\\n (the KV registry), and a provider with nothing registered is silently skipped so\\n registering a subset of the five supported keys still works. Stdlib only\\n (``urllib.request``), matching this repo's dependency-free transport convention.\\n+\\n+This module owns the ordinary chat-agent discovery boundary. Provider catalogs may\\n+mix chat, embedding, reranking, transcription, moderation, image, and realtime\\n+models under one ``/models`` endpoint. Clearly non-chat identifiers are rejected\\n+before they can be converted to workers, selected by cost, or persisted into the\\n+chat agent pool.\\n \\\"\\\"\\\"\\n \\n from __future__ import annotations\\n \\n-import json\\n import re\\n import urllib.error\\n-import urllib.request\\n from dataclasses import dataclass\\n from typing import TYPE_CHECKING, Any\\n+from urllib.parse import urlparse\\n \\n-from .batch_routing import cheapest_upstream\\n+from .chat_capability import is_general_chat_agent_model_id\\n from .credentials import get_credential\\n-from .orchestrator import ModelAgent\\n+from .orchestrator import ModelAgent, ModelClient\\n \\n if TYPE_CHECKING:\\n from .cost_ledger import PriceBook\\n \\n DISCOVERY_TIMEOUT_SECONDS = 15.0\\n \\n \\n+def _provider_discovery_error_code(exc: Exception) -> str:\\n+ \\\"\\\"\\\"Map provider failures to stable codes without retaining provider response text.\\\"\\\"\\\"\\n+ if isinstance(exc, urllib.error.HTTPError):\\n+ return f\\\"http_status_{exc.code}\\\"\\n+ if isinstance(exc, TimeoutError):\\n+ return \\\"timeout\\\"\\n+ if isinstance(exc, urllib.error.URLError) or isinstance(exc, OSError):\\n+ return \\\"transport_error\\\"\\n+ if isinstance(exc, ValueError):\\n+ return \\\"invalid_response\\\"\\n+ return \\\"provider_error\\\"\\n+\\n+\\n @dataclass(frozen=True)\\n class ProviderModelSource:\\n \\\"\\\"\\\"Where and how to discover one provider's models.\\\"\\\"\\\"\\n@@ -84,7 +102,7 @@ class ProviderModelSource:\\n \\n @dataclass(frozen=True)\\n class DiscoveredModel:\\n- \\\"\\\"\\\"One model found on a provider, with pricing when the provider reports it.\\\"\\\"\\\"\\n+ \\\"\\\"\\\"One general chat-agent eligible model found on a provider, with pricing.\\\"\\\"\\\"\\n \\n provider_name: str\\n model_id: str\\n@@ -99,26 +117,41 @@ class DiscoveredModel:\\n class ProviderDiscoveryError(RuntimeError):\\n \\\"\\\"\\\"Raised when a provider's model list could not be fetched (network/auth failure).\\\"\\\"\\\"\\n \\n- def __init__(self, provider_name: str, detail: str) -> None:\\n+ def __init__(self, provider_name: str, error_code: str) -> None:\\n self.provider_name = provider_name\\n- super().__init__(f\\\"model discovery failed for provider {provider_name!r}: {detail}\\\")\\n-\\n-\\n-def _fetch_json(url: str, *, api_key: str, auth_scheme: str, timeout: float) -> Any:\\n- if not url.startswith(\\\"https://\\\"):\\n- # Every caller passes one of the hardcoded PROVIDER_SOURCES chat_base_url\\n- # constants below, never external input -- but urlopen also honors\\n- # file:// and other unsafe schemes, so refuse anything not https as a\\n- # cheap invariant check rather than trusting the constant list alone.\\n- raise ValueError(f\\\"refusing non-https model discovery URL: {url!r}\\\")\\n- request = urllib.request.Request(\\n- url,\\n- headers={\\\"authorization\\\": f\\\"{auth_scheme} {api_key}\\\"},\\n- method=\\\"GET\\\",\\n+ self.error_code = error_code\\n+ super().__init__(f\\\"model discovery failed for provider {provider_name!r}: {error_code}\\\")\\n+\\n+\\n+def _fetch_json(\\n+ url: str,\\n+ *,\\n+ auth_scheme: str,\\n+ timeout: float,\\n+ credential_name: str,\\n+) -> Any:\\n+ \\\"\\\"\\\"Fetch a provider catalog through the validated, DNS-pinned transport.\\\"\\\"\\\"\\n+ parsed = urlparse(url)\\n+ if parsed.scheme != \\\"https\\\" or not parsed.hostname:\\n+ raise ValueError(\\\"model discovery requires an https provider URL\\\")\\n+ if parsed.username is not None or parsed.password is not None or \\\"#\\\" in url:\\n+ raise ValueError(\\\"model discovery URL must not contain credentials or a fragment\\\")\\n+ try:\\n+ port = parsed.port\\n+ except ValueError as exc:\\n+ raise ValueError(\\\"model discovery URL has an invalid port\\\") from exc\\n+ origin = f\\\"https://{parsed.hostname}\\\"\\n+ if port not in (None, 443):\\n+ origin = f\\\"{origin}:{port}\\\"\\n+ agent = ModelAgent(\\n+ id=\\\"model_discovery_agent\\\",\\n+ model=\\\"model_catalog\\\",\\n+ base_url=origin,\\n+ credential_key=credential_name,\\n+ auth_scheme=auth_scheme,\\n )\\n- # Scheme is enforced to https:// immediately above; url is never attacker-controlled.\\n- with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 - fixed https provider hosts # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected\\n- return json.loads(response.read().decode(\\\"utf-8\\\"))\\n+ client = ModelClient()\\n+ return client.fetch_json(agent, url, timeout=timeout)\\n \\n \\n def _price_per_1k(value: Any) -> float | None:\\n@@ -132,13 +165,14 @@ def _price_per_1k(value: Any) -> float | None:\\n \\n \\n def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Parse one OpenAI-compatible catalog into general chat-agent candidates.\\\"\\\"\\\"\\n rows = payload.get(\\\"data\\\") if isinstance(payload, dict) else None\\n discovered: list[DiscoveredModel] = []\\n for row in rows if isinstance(rows, list) else []:\\n if not isinstance(row, dict):\\n continue\\n model_id = row.get(\\\"id\\\")\\n- if type(model_id) is not str or not model_id:\\n+ if not is_general_chat_agent_model_id(model_id):\\n continue\\n pricing = row.get(\\\"pricing\\\") if isinstance(row.get(\\\"pricing\\\"), dict) else {}\\n discovered.append(\\n@@ -156,13 +190,14 @@ def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[\\n \\n \\n def _parse_bytez(payload: Any, source: ProviderModelSource) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Parse one Bytez chat catalog without admitting ineligible identifiers.\\\"\\\"\\\"\\n rows = payload.get(\\\"output\\\") if isinstance(payload, dict) else None\\n discovered: list[DiscoveredModel] = []\\n for row in rows if isinstance(rows, list) else []:\\n if not isinstance(row, dict):\\n continue\\n model_id = row.get(\\\"modelId\\\")\\n- if type(model_id) is not str or not model_id:\\n+ if not is_general_chat_agent_model_id(model_id):\\n continue\\n discovered.append(\\n DiscoveredModel(\\n@@ -181,17 +216,22 @@ def _parse_bytez(payload: Any, source: ProviderModelSource) -> list[DiscoveredMo\\n def discover_provider_models(\\n source: ProviderModelSource, *, timeout: float = DISCOVERY_TIMEOUT_SECONDS\\n ) -> list[DiscoveredModel]:\\n- \\\"\\\"\\\"Discover one provider's models, or ``[]`` if its credential is not registered.\\\"\\\"\\\"\\n+ \\\"\\\"\\\"Discover chat candidates, or ``[]`` when the credential is not registered.\\\"\\\"\\\"\\n api_key = get_credential(source.credential_name)\\n if not api_key:\\n return []\\n url = source.list_url\\n if source.task_filter:\\n url = f\\\"{url}?task={source.task_filter}\\\"\\n try:\\n- payload = _fetch_json(url, api_key=api_key, auth_scheme=source.auth_scheme, timeout=timeout)\\n- except (urllib.error.URLError, TimeoutError, ValueError) as exc: # pragma: no cover - network path\\n- raise ProviderDiscoveryError(source.provider_name, str(exc)) from exc\\n+ payload = _fetch_json(\\n+ url,\\n+ auth_scheme=source.auth_scheme,\\n+ timeout=timeout,\\n+ credential_name=source.credential_name,\\n+ )\\n+ except (urllib.error.URLError, TimeoutError, ValueError, RuntimeError, OSError) as exc: # pragma: no cover - network path\\n+ raise ProviderDiscoveryError(source.provider_name, _provider_discovery_error_code(exc)) from None\\n if source.style == \\\"bytez\\\":\\n return _parse_bytez(payload, source)\\n return _parse_openai_compatible(payload, source)\\n@@ -202,7 +242,7 @@ def discover_all_models(\\n *,\\n timeout: float = DISCOVERY_TIMEOUT_SECONDS,\\n ) -> tuple[list[DiscoveredModel], list[ProviderDiscoveryError]]:\\n- \\\"\\\"\\\"Discover models across every provider with a registered credential.\\n+ \\\"\\\"\\\"Discover chat candidates across providers with registered credentials.\\n \\n One provider's failure never blocks the others: errors are collected and\\n returned alongside whatever models were successfully discovered.\\n@@ -231,7 +271,9 @@ def agent_id_for(discovered: DiscoveredModel) -> str:\\n \\n \\n def agent_from_discovered(discovered: DiscoveredModel, *, priority: int = 0) -> ModelAgent:\\n- \\\"\\\"\\\"Build a disabled-by-default ModelAgent for a discovered model (opt-in serving).\\\"\\\"\\\"\\n+ \\\"\\\"\\\"Build a disabled general chat agent or reject an ineligible record.\\\"\\\"\\\"\\n+ if not is_general_chat_agent_model_id(discovered.model_id):\\n+ raise ValueError(\\\"model is not eligible for a general chat agent\\\")\\n return ModelAgent(\\n id=agent_id_for(discovered),\\n model=discovered.model_id,\\n@@ -246,7 +288,7 @@ def agent_from_discovered(discovered: DiscoveredModel, *, priority: int = 0) ->\\n \\n \\n def refresh_price_book(discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\\") -> int:\\n- \\\"\\\"\\\"Write every discovered model's known pricing into the price book.\\n+ \\\"\\\"\\\"Write every discovered chat model's known pricing into the price book.\\n \\n Returns the number of price rows written. A model without provider-reported\\n pricing is skipped rather than defaulted to 0 -- an unpriced model already\\n@@ -257,6 +299,8 @@ def refresh_price_book(discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\n \\n written = 0\\n for model in discovered:\\n+ if not is_general_chat_agent_model_id(model.model_id):\\n+ continue\\n if model.prompt_price_per_1k is None and model.completion_price_per_1k is None:\\n continue\\n price_book.set_price(\\n@@ -275,43 +319,37 @@ def refresh_price_book(discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\n def select_cheapest_discovered_agent(\\n discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\\"\\n ) -> DiscoveredModel | None:\\n- \\\"\\\"\\\"Pick the lowest-cost discovered model per the price book (auto-optimization).\\n+ \\\"\\\"\\\"Pick the lowest-cost general chat-agent model per the price book.\\n \\n- Reuses :func:`~contextual_orchestrator.batch_routing.cheapest_upstream`, the\\n- existing cost-optimizing upstream selector. Call :func:`refresh_price_book`\\n- first so discovered pricing is visible; an unpriced candidate costs ``0``\\n+ Uses the same representative request cost as the top-N selector. Call\\n+ :func:`refresh_price_book` first so discovered pricing is visible; an\\n+ unpriced candidate costs ``0``\\n under that selector's documented contract and is treated as free, not\\n unknown -- so a genuinely unpriced provider (e.g. Bytez, priced by\\n GPU-second rather than per token) will always look cheapest here. Fine for\\n \\\"auto-pick something free to try,\\\" but callers doing real cost comparison\\n should refresh pricing for every candidate they care about first.\\n \\\"\\\"\\\"\\n- if not discovered:\\n- return None\\n- candidates = [{\\\"provider\\\": model.provider_name, \\\"model\\\": model.model_id} for model in discovered]\\n- winner = cheapest_upstream(candidates, price_book)\\n- if winner is None:\\n+ eligible = [model for model in discovered if is_general_chat_agent_model_id(model.model_id)]\\n+ if not eligible:\\n return None\\n- for model in discovered:\\n- if model.provider_name == winner[\\\"provider\\\"] and model.model_id == winner[\\\"model\\\"]:\\n- return model\\n- return None # pragma: no cover - winner always comes from candidates\\n+ return min(eligible, key=lambda model: _discovered_cost(model, price_book))\\n \\n \\n def select_top_n_cheapest_discovered_agents(\\n discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\\", limit: int\\n ) -> list[DiscoveredModel]:\\n- \\\"\\\"\\\"Return the ``limit`` lowest-cost discovered models, cheapest first.\\n-\\n- For bootstrapping a CI sidecar (or any first-boot pool) with more than one\\n- enabled agent for failover, without hand-picking which discovered models to\\n- trust. Same pricing contract as :func:`select_cheapest_discovered_agent`.\\n- \\\"\\\"\\\"\\n- if limit <= 0 or not discovered:\\n+ \\\"\\\"\\\"Return the ``limit`` cheapest general chat-agent models in ascending cost.\\\"\\\"\\\"\\n+ if limit <= 0:\\n+ return []\\n+ eligible = [model for model in discovered if is_general_chat_agent_model_id(model.model_id)]\\n+ if not eligible:\\n return []\\n \\n- def _cost(model: DiscoveredModel) -> float:\\n- cost, _currency = price_book.compute_cost(model.provider_name, model.model_id, 1000, 1000)\\n- return cost\\n+ return sorted(eligible, key=lambda model: _discovered_cost(model, price_book))[:limit]\\n+\\n \\n- return sorted(discovered, key=_cost)[:limit]\\n+def _discovered_cost(model: DiscoveredModel, price_book: \\\"PriceBook\\\") -> float:\\n+ \\\"\\\"\\\"Price the representative discovery request used by both selectors.\\\"\\\"\\\"\\n+ cost, _currency = price_book.compute_cost(model.provider_name, model.model_id, 1000, 1000)\\n+ return cost\" }, { \"sha\": \"e68751645623a9fac0b4aec022f1829fdabe4db4\", \"filename\": \"contextual_orchestrator/orchestrator.py\", \"status\": \"modified\", \"additions\": 1414, \"deletions\": 249, \"changes\": 1663, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Forchestrator.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Forchestrator.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Forchestrator.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\" }, { \"sha\": \"328be1c492124de845dcc54798fda4b595bc0a8b\", \"filename\": \"contextual_orchestrator/passthrough_failover.py\", \"status\": \"added\", \"additions\": 151, \"deletions\": 0, \"changes\": 151, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fpassthrough_failover.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fpassthrough_failover.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fpassthrough_failover.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,151 @@\\n+\\\"\\\"\\\"Bounded cross-provider failover for OpenAI-compatible passthrough requests.\\n+\\n+Tool calls, structured responses, and Responses API calls must preserve one\\n+provider's raw response shape. This module keeps that contract while advancing\\n+to another capability-ranked model when the caller selected the virtual\\n+``contextual-orchestrator`` model and an upstream candidate becomes transiently\\n+unavailable.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import time\\n+import urllib.error\\n+from collections.abc import Iterator\\n+from typing import Any\\n+\\n+from .orchestrator import (\\n+ ModelAgent,\\n+ ModelClient,\\n+ is_transient_error,\\n+)\\n+from .orchestrator import (\\n+ TaskOrchestrator as BaseTaskOrchestrator,\\n+)\\n+\\n+_CANDIDATE_UNAVAILABLE_HTTP_STATUS = frozenset({404, 410})\\n+_MAX_PROVIDER_ERROR_CHAIN_DEPTH = 8\\n+\\n+\\n+def _provider_error_chain(error: BaseException) -> Iterator[BaseException]:\\n+ \\\"\\\"\\\"Yield a bounded, cycle-safe provider exception cause/context chain.\\n+\\n+ An explicit ``raise ... from cause`` is authoritative. An implicit context\\n+ is inspected only when it was not deliberately suppressed with\\n+ ``raise ... from None``; suppressed history must not turn a terminal wrapper\\n+ into an adaptive fallback signal.\\n+ \\\"\\\"\\\"\\n+ current: BaseException | None = error\\n+ seen: set[int] = set()\\n+ for _ in range(_MAX_PROVIDER_ERROR_CHAIN_DEPTH):\\n+ if current is None or id(current) in seen:\\n+ return\\n+ seen.add(id(current))\\n+ yield current\\n+ if current.__cause__ is not None:\\n+ current = current.__cause__\\n+ elif current.__suppress_context__:\\n+ return\\n+ else:\\n+ current = current.__context__\\n+\\n+\\n+def _is_adaptive_failover_error(error: BaseException) -> bool:\\n+ \\\"\\\"\\\"Classify transient or stale-candidate failures through provider wrappers.\\\"\\\"\\\"\\n+ for candidate in _provider_error_chain(error):\\n+ if is_transient_error(candidate):\\n+ return True\\n+ if (\\n+ isinstance(candidate, urllib.error.HTTPError)\\n+ and candidate.code in _CANDIDATE_UNAVAILABLE_HTTP_STATUS\\n+ ):\\n+ return True\\n+ return False\\n+\\n+\\n+def _proxy_send_once(\\n+ client: Any,\\n+ agent: ModelAgent,\\n+ endpoint: str,\\n+ payload: dict[str, Any],\\n+) -> dict[str, Any]:\\n+ \\\"\\\"\\\"Send one raw passthrough attempt without same-model transient retries.\\\"\\\"\\\"\\n+ one_shot = getattr(client, \\\"proxy_send_once\\\", None)\\n+ if callable(one_shot):\\n+ return one_shot(agent, endpoint, payload)\\n+ if isinstance(client, ModelClient):\\n+ return client.proxy_send_once(agent, endpoint, payload)\\n+ return client.proxy_send(agent, endpoint, payload)\\n+\\n+\\n+class TaskOrchestrator(BaseTaskOrchestrator):\\n+ \\\"\\\"\\\"Add bounded provider failover to the final OpenAI-compatible provider call.\\\"\\\"\\\"\\n+\\n+ def _proxy_provider_completion(\\n+ self,\\n+ agent: ModelAgent,\\n+ endpoint: str,\\n+ payload: dict[str, Any],\\n+ *,\\n+ requested_model: Any,\\n+ text: str,\\n+ role: str,\\n+ required_tags: tuple[str, ...] = (),\\n+ ) -> dict[str, Any]:\\n+ \\\"\\\"\\\"Preserve response shapes while failing over only adaptive requests.\\n+\\n+ An explicitly requested concrete model remains sticky and receives its\\n+ original provider error: serving another model would violate the caller's\\n+ model contract. Requests for the virtual ``contextual-orchestrator``\\n+ model, or requests that omit ``model``, may advance through\\n+ capability-ranked candidates for transient upstream failures and for a\\n+ discovered model that has become unavailable (HTTP 404/410). Provider\\n+ SDK wrapper causes are inspected through a bounded, cycle-safe chain.\\n+\\n+ Every candidate receives at most one passthrough attempt, so a 429 is\\n+ never amplified by replaying the same large tool request. Caller,\\n+ authentication, policy, and other non-transient failures are returned\\n+ immediately instead of being replayed to another provider.\\n+ \\\"\\\"\\\"\\n+ requested_agent = self._requested_agent(requested_model)\\n+ adaptive_request = requested_agent is None\\n+ if requested_agent is not None:\\n+ if requested_agent.disabled:\\n+ raise RuntimeError(f\\\"requested model {requested_model!r} is disabled\\\")\\n+ return self.client.proxy_send(requested_agent, endpoint, payload)\\n+ else:\\n+ candidates = self._failover_candidates(\\n+ agent,\\n+ text,\\n+ role,\\n+ required_tags=required_tags,\\n+ )\\n+\\n+ last_error: Exception | None = None\\n+ for candidate_agent in candidates:\\n+ upstream = dict(payload)\\n+ upstream[\\\"model\\\"] = candidate_agent.model\\n+ try:\\n+ result = _proxy_send_once(self.client, candidate_agent, endpoint, upstream)\\n+ except Exception as exc:\\n+ if not adaptive_request or not _is_adaptive_failover_error(exc):\\n+ raise\\n+ last_error = exc\\n+ self._record_failure(candidate_agent.id)\\n+ with self._circuit_lock:\\n+ state = self._circuit.setdefault(\\n+ candidate_agent.id,\\n+ {\\\"failures\\\": 0.0, \\\"opened_at\\\": 0.0},\\n+ )\\n+ state[\\\"failures\\\"] = max(\\n+ state[\\\"failures\\\"],\\n+ float(self.circuit_failure_threshold),\\n+ )\\n+ state[\\\"opened_at\\\"] = time.monotonic()\\n+ continue\\n+ self._record_success(candidate_agent.id)\\n+ return result\\n+\\n+ raise RuntimeError(\\n+ f\\\"all {len(candidates)} candidate agents failed for passthrough endpoint={endpoint}\\\"\\n+ ) from last_error\" }, { \"sha\": \"bf2230a8fee707a1d9963c3c5db1d2e514d086d3\", \"filename\": \"contextual_orchestrator/server.py\", \"status\": \"modified\", \"additions\": 531, \"deletions\": 129, \"changes\": 660, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fserver.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fserver.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fserver.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -6,7 +6,10 @@\\n from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer\\n import base64\\n import json\\n+import logging\\n+import math\\n import secrets\\n+import socket\\n import struct\\n import threading\\n import time\\n@@ -24,14 +27,28 @@\\n MAX_LOCAL_CONCURRENCY,\\n TaskOrchestrator,\\n _new_chat_completion_id,\\n+ _responses_usage,\\n chat_completion_chunks,\\n chat_completion_response,\\n+ _responses_to_chat_payload,\\n text_completion_response,\\n redact_value,\\n sse_stream_body,\\n )\\n+from .telemetry import (\\n+ attach_trace_context,\\n+ configure_telemetry,\\n+ current_session_id,\\n+ detach_trace_context,\\n+ reset_session_id,\\n+ session_id_from_headers,\\n+ session_id_from_metadata,\\n+ set_session_id,\\n+)\\n+\\n+_LOGGER = logging.getLogger(__name__)\\n \\n-# OpenAI request params forwarded verbatim to the provider on passthrough.\\n+# OpenAI request params forwarded to the final provider after orchestration.\\n OPENAI_PASSTHROUGH_PARAM_KEYS = {\\n \\\"temperature\\\", \\\"top_p\\\", \\\"max_tokens\\\", \\\"max_completion_tokens\\\", \\\"n\\\", \\\"stop\\\",\\n \\\"seed\\\", \\\"presence_penalty\\\", \\\"frequency_penalty\\\", \\\"logit_bias\\\", \\\"logprobs\\\",\\n@@ -50,8 +67,8 @@\\n # Assistants-style tool_resources — named unsupported (not unknown_fields).\\n \\\"tool_resources\\\",\\n }\\n-# Provider features the multi-agent verifier cannot merge -> single-agent passthrough.\\n-PASSTHROUGH_TRIGGER_KEYS = {\\\"response_format\\\", \\\"tools\\\", \\\"tool_choice\\\", \\\"functions\\\", \\\"function_call\\\"}\\n+# Tool calls still require provider-native loop semantics; structured JSON is\\n+# orchestrated through the conduct+synthesis path below instead of passthrough.\\n ALLOWED_CHAT_KEYS = {\\n \\\"model\\\", \\\"messages\\\", \\\"orchestration\\\", \\\"orchestration_mode\\\", \\\"mode\\\",\\n \\\"include_orchestration_trace\\\", \\\"stream\\\", \\\"attribution\\\", \\\"routing\\\",\\n@@ -66,7 +83,7 @@\\n \\\"max_output_tokens\\\",\\n # Tool-loop budget — accepted only for explicit unsupported error (no multi-step tool loop).\\n \\\"max_tool_calls\\\",\\n- # Gateway cost/routing control plane (stripped before provider passthrough).\\n+ # Gateway cost/routing control plane (stripped before final provider transport).\\n \\\"attribution\\\", \\\"routing\\\",\\n # previous_response_id / conversation / truncation / include fail closed\\n # with named unsupported errors. Official text.format is validated\\n@@ -153,6 +170,7 @@ class SecurityConfig:\\n allow_public_bind: bool = False\\n expose_trace_by_default: bool = False\\n max_body_bytes: int = 64 * 1024\\n+ request_read_timeout_seconds: float = 10.0\\n rate_limit_requests: int = 60\\n rate_limit_window_seconds: int = 60\\n max_concurrent_runs: int = 8\\n@@ -169,6 +187,15 @@ def __post_init__(self) -> None:\\n raise ValueError(\\\"single auth_token cannot be combined with split tokens\\\")\\n if (self.admin_token or self.inference_token) and not (self.admin_token and self.inference_token):\\n raise ValueError(\\\"split token mode requires both admin_token and inference_token\\\")\\n+ if type(self.max_body_bytes) is not int or self.max_body_bytes < 1:\\n+ raise ValueError(\\\"max_body_bytes must be a positive integer\\\")\\n+ if (\\n+ isinstance(self.request_read_timeout_seconds, bool)\\n+ or not isinstance(self.request_read_timeout_seconds, (int, float))\\n+ or not math.isfinite(float(self.request_read_timeout_seconds))\\n+ or not 0.1 <= float(self.request_read_timeout_seconds) <= 120.0\\n+ ):\\n+ raise ValueError(\\\"request_read_timeout_seconds must be between 0.1 and 120 seconds\\\")\\n if type(self.max_concurrent_runs) is not int or not 1 <= self.max_concurrent_runs <= MAX_LOCAL_CONCURRENCY:\\n raise ValueError(\\n f\\\"max_concurrent_runs must be an integer in 1..{MAX_LOCAL_CONCURRENCY}\\\"\\n@@ -241,6 +268,8 @@ def readiness_profile(self) -> dict[str, Any]:\\n \\\"rate_limit_requests\\\": self.rate_limit_requests,\\n \\\"rate_limit_window_seconds\\\": self.rate_limit_window_seconds,\\n \\\"max_concurrent_runs\\\": self.max_concurrent_runs,\\n+ \\\"max_body_bytes\\\": self.max_body_bytes,\\n+ \\\"request_read_timeout_seconds\\\": self.request_read_timeout_seconds,\\n }\\n \\n \\n@@ -297,6 +326,50 @@ def _coerce_json(payload: bytes) -> dict[str, Any]:\\n return value\\n \\n \\n+def _header_values(headers: Any, field_name: str) -> list[str]:\\n+ \\\"\\\"\\\"Return all raw values for one case-insensitive HTTP header field.\\\"\\\"\\\"\\n+ raw_items = getattr(headers, \\\"raw_items\\\", None)\\n+ if callable(raw_items):\\n+ return [value for name, value in raw_items() if name.casefold() == field_name.casefold()]\\n+ get_all = getattr(headers, \\\"get_all\\\", None)\\n+ if callable(get_all):\\n+ return list(get_all(field_name, []))\\n+ value = headers.get(field_name)\\n+ return [] if value is None else [value]\\n+\\n+\\n+def _parse_request_framing(headers: Any, max_body_bytes: int) -> int:\\n+ \\\"\\\"\\\"Validate fixed-length JSON framing before consuming any request bytes.\\n+\\n+ The server deliberately does not implement a chunked decoder. Requiring one\\n+ unambiguous ASCII decimal ``Content-Length`` prevents negative lengths,\\n+ duplicate disagreement, transfer-coding ambiguity, and unbounded reads from\\n+ reaching ``BufferedReader.read``.\\n+ \\\"\\\"\\\"\\n+ transfer_encoding = _header_values(headers, \\\"Transfer-Encoding\\\")\\n+ content_lengths = _header_values(headers, \\\"Content-Length\\\")\\n+ if transfer_encoding:\\n+ raise RequestError(\\n+ 400,\\n+ \\\"invalid_request_framing\\\",\\n+ \\\"transfer-encoded request bodies are unsupported\\\",\\n+ )\\n+ if not content_lengths:\\n+ raise RequestError(411, \\\"length_required\\\", \\\"content-length is required\\\")\\n+ if len(content_lengths) != 1:\\n+ raise RequestError(400, \\\"invalid_request_framing\\\", \\\"duplicate content-length is unsupported\\\")\\n+ raw_length = content_lengths[0]\\n+ if not raw_length or raw_length != raw_length.strip() or not raw_length.isascii() or not raw_length.isdigit():\\n+ raise RequestError(400, \\\"invalid_request_framing\\\", \\\"content-length must be an ASCII decimal integer\\\")\\n+ try:\\n+ body_size = int(raw_length, 10)\\n+ except (ValueError, TypeError):\\n+ raise RequestError(400, \\\"invalid_request_framing\\\", \\\"content-length is invalid\\\") from None\\n+ if body_size > max_body_bytes:\\n+ raise RequestError(413, \\\"request_too_large\\\", \\\"request body exceeds configured limit\\\")\\n+ return body_size\\n+\\n+\\n \\n \\n def _coerce_optional_bool(\\n@@ -829,6 +902,42 @@ def _validate_responses_parallel_tool_calls(body: dict[str, Any]) -> bool | None\\n return value\\n \\n \\n+def _reject_responses_orchestration_controls(body: dict[str, Any]) -> None:\\n+ \\\"\\\"\\\"Reject non-empty controls the multi-agent Responses path cannot apply.\\\"\\\"\\\"\\n+ fields = (\\n+ \\\"temperature\\\",\\n+ \\\"top_p\\\",\\n+ \\\"presence_penalty\\\",\\n+ \\\"frequency_penalty\\\",\\n+ \\\"seed\\\",\\n+ \\\"stop\\\",\\n+ \\\"logit_bias\\\",\\n+ \\\"logprobs\\\",\\n+ \\\"top_logprobs\\\",\\n+ )\\n+ unsupported: list[str] = []\\n+ for field_name in fields:\\n+ if field_name not in body:\\n+ continue\\n+ value = body[field_name]\\n+ if value is None or (isinstance(value, str) and not value.strip()):\\n+ continue\\n+ if isinstance(value, (list, dict)) and not value:\\n+ continue\\n+ if field_name == \\\"logprobs\\\" and value in (False, 0):\\n+ continue\\n+ if field_name == \\\"top_logprobs\\\" and value == 0:\\n+ continue\\n+ unsupported.append(field_name)\\n+ if unsupported:\\n+ raise RequestError(\\n+ 422,\\n+ \\\"unsupported_responses_orchestration_controls\\\",\\n+ \\\"Responses orchestration cannot apply these provider controls\\\",\\n+ {\\\"fields\\\": unsupported},\\n+ )\\n+\\n+\\n def _validate_responses_seed(body: dict[str, Any]) -> int | None:\\n \\\"\\\"\\\"Responses ``seed`` — signed int64; valid values pass through to the provider.\\n \\n@@ -1027,13 +1136,15 @@ def _validate_completions_top_p(body: dict[str, Any]) -> float | None:\\n body[\\\"top_p\\\"] = value\\n return value\\n \\n-def _validate_completions_model(body: dict[str, Any]) -> str:\\n+def _validate_completions_model(body: dict[str, Any], *, required: bool = True) -> str:\\n \\\"\\\"\\\"Legacy Completions ``model`` — required non-empty string (OpenAI parity).\\n \\n Incidental leading/trailing whitespace is stripped and written back so\\n tools/response_format passthrough (``proxy_completion``) matches the same\\n pool model id as the orchestration path. Form/JS SDKs often pad model names.\\n \\\"\\\"\\\"\\n+ if \\\"model\\\" not in body and not required:\\n+ return \\\"contextual-orchestrator\\\"\\n if \\\"model\\\" not in body:\\n raise RequestError(400, \\\"invalid_model\\\", \\\"model is required\\\")\\n model = body.get(\\\"model\\\")\\n@@ -1642,6 +1753,7 @@ def _validate_responses_text(body: dict[str, Any]) -> dict[str, Any] | None:\\n \\\"invalid_text\\\",\\n \\\"text.format.schema must be an object\\\",\\n )\\n+ _validate_json_schema_definition(schema_body, \\\"text.format.schema\\\")\\n if \\\"description\\\" in fmt:\\n description_value = fmt.get(\\\"description\\\")\\n if description_value is None or (\\n@@ -1785,17 +1897,23 @@ def _validate_mode(mode: Any) -> str:\\n \\n \\n \\n-def _require_pool_model(orchestrator: Any, model_name: str) -> None:\\n+def _require_pool_model(\\n+ orchestrator: Any, model_name: str, *, required_capability: str | None = None\\n+) -> None:\\n \\\"\\\"\\\"Fail closed when ``model_name`` is not served by any enabled agent.\\n \\n OpenAI clients treat ``model`` as the deployment they paid for. Silently\\n answering with a different pool agent hides capacity/routing mismatches.\\n \\\"\\\"\\\"\\n+ if model_name == \\\"contextual-orchestrator\\\" and required_capability is None:\\n+ return\\n agents = getattr(orchestrator, \\\"agents\\\", None) or []\\n for agent in agents:\\n if getattr(agent, \\\"disabled\\\", False):\\n continue\\n- if getattr(agent, \\\"model\\\", None) == model_name:\\n+ if getattr(agent, \\\"model\\\", None) == model_name and (\\n+ required_capability is None or required_capability in getattr(agent, \\\"tags\\\", ())\\n+ ):\\n return\\n raise RequestError(\\n 400,\\n@@ -1804,6 +1922,111 @@ def _require_pool_model(orchestrator: Any, model_name: str) -> None:\\n )\\n \\n \\n+def _validate_json_schema_definition(\\n+ schema: Any,\\n+ path: str = \\\"response_format.json_schema.schema\\\",\\n+) -> None:\\n+ \\\"\\\"\\\"Validate nested JSON Schema containers before evaluating a response.\\\"\\\"\\\"\\n+ if not isinstance(schema, dict):\\n+ raise RequestError(400, \\\"invalid_response_format\\\", f\\\"{path} must be an object\\\")\\n+ properties = schema.get(\\\"properties\\\")\\n+ if properties is not None:\\n+ if not isinstance(properties, dict):\\n+ raise RequestError(400, \\\"invalid_response_format\\\", f\\\"{path}.properties must be an object\\\")\\n+ for name, child in properties.items():\\n+ if not isinstance(name, str) or not isinstance(child, dict):\\n+ raise RequestError(\\n+ 400,\\n+ \\\"invalid_response_format\\\",\\n+ f\\\"{path}.properties entries must map string names to objects\\\",\\n+ )\\n+ _validate_json_schema_definition(child, f\\\"{path}.properties.{name}\\\")\\n+ required = schema.get(\\\"required\\\")\\n+ if required is not None and (\\n+ not isinstance(required, list) or any(not isinstance(name, str) for name in required)\\n+ ):\\n+ raise RequestError(400, \\\"invalid_response_format\\\", f\\\"{path}.required must be an array of strings\\\")\\n+ items = schema.get(\\\"items\\\")\\n+ if items is not None:\\n+ if not isinstance(items, dict):\\n+ raise RequestError(400, \\\"invalid_response_format\\\", f\\\"{path}.items must be an object\\\")\\n+ _validate_json_schema_definition(items, f\\\"{path}.items\\\")\\n+ any_of = schema.get(\\\"anyOf\\\")\\n+ if any_of is not None:\\n+ if not isinstance(any_of, list) or any(not isinstance(option, dict) for option in any_of):\\n+ raise RequestError(400, \\\"invalid_response_format\\\", f\\\"{path}.anyOf must be an array of objects\\\")\\n+ for index, option in enumerate(any_of):\\n+ _validate_json_schema_definition(option, f\\\"{path}.anyOf[{index}]\\\")\\n+\\n+\\n+def _validate_json_schema_value(value: Any, schema: dict[str, Any], path: str = \\\"$\\\") -> None:\\n+ \\\"\\\"\\\"Validate the bounded JSON Schema subset used by structured chat output.\\\"\\\"\\\"\\n+ if not isinstance(schema, dict):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", \\\"json_schema.schema must be an object\\\")\\n+ if \\\"enum\\\" in schema and value not in schema[\\\"enum\\\"]:\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} is outside the schema enum\\\")\\n+ if \\\"const\\\" in schema and value != schema[\\\"const\\\"]:\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} does not match the schema const\\\")\\n+ if \\\"anyOf\\\" in schema and not any(\\n+ _json_schema_matches(value, option) for option in schema[\\\"anyOf\\\"] if isinstance(option, dict)\\n+ ):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} matches no anyOf branch\\\")\\n+ expected = schema.get(\\\"type\\\")\\n+ if expected == \\\"object\\\":\\n+ if not isinstance(value, dict):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} must be an object\\\")\\n+ properties = schema.get(\\\"properties\\\", {})\\n+ required = schema.get(\\\"required\\\", [])\\n+ missing = [name for name in required if name not in value]\\n+ if missing:\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} is missing {missing[0]!r}\\\")\\n+ for name, child in properties.items():\\n+ if name in value and isinstance(child, dict):\\n+ _validate_json_schema_value(value[name], child, f\\\"{path}.{name}\\\")\\n+ if schema.get(\\\"additionalProperties\\\") is False:\\n+ unknown = set(value) - set(properties)\\n+ if unknown:\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} has unknown property {sorted(unknown)[0]!r}\\\")\\n+ elif expected == \\\"array\\\":\\n+ if not isinstance(value, list):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} must be an array\\\")\\n+ items = schema.get(\\\"items\\\")\\n+ if isinstance(items, dict):\\n+ for index, item in enumerate(value):\\n+ _validate_json_schema_value(item, items, f\\\"{path}[{index}]\\\")\\n+ elif expected == \\\"string\\\" and not isinstance(value, str):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} must be a string\\\")\\n+ elif expected == \\\"boolean\\\" and not isinstance(value, bool):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} must be a boolean\\\")\\n+ elif expected == \\\"integer\\\" and (isinstance(value, bool) or not isinstance(value, int)):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} must be an integer\\\")\\n+ elif expected == \\\"number\\\" and (isinstance(value, bool) or not isinstance(value, (int, float))):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} must be a number\\\")\\n+\\n+\\n+def _json_schema_matches(value: Any, schema: dict[str, Any]) -> bool:\\n+ try:\\n+ _validate_json_schema_value(value, schema)\\n+ except RequestError:\\n+ return False\\n+ return True\\n+\\n+\\n+def _validate_structured_completion_answer(answer: Any, response_format: dict[str, Any]) -> None:\\n+ \\\"\\\"\\\"Reject non-JSON synthesis instead of returning an unverified contract.\\\"\\\"\\\"\\n+ if not isinstance(answer, str):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", \\\"orchestrator returned no textual JSON\\\")\\n+ try:\\n+ value = json.loads(answer)\\n+ except json.JSONDecodeError:\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", \\\"orchestrator returned invalid JSON\\\") from None\\n+ if response_format.get(\\\"type\\\") == \\\"json_object\\\" and not isinstance(value, dict):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", \\\"json_object output must be an object\\\")\\n+ if response_format.get(\\\"type\\\") == \\\"json_schema\\\":\\n+ schema = ((response_format.get(\\\"json_schema\\\") or {}).get(\\\"schema\\\"))\\n+ _validate_json_schema_value(value, schema)\\n+\\n+\\n \\n def _validate_message_content_parts(content: list[Any]) -> list[dict[str, Any]]:\\n \\\"\\\"\\\"OpenAI multimodal content-parts array (text + image_url) for vision callers.\\n@@ -3044,7 +3267,7 @@ def _validate_responses_store(body: dict[str, Any]) -> bool | None:\\n \\\"\\\"\\\"Responses API ``store`` — strict boolean; ``true`` is not supported.\\n \\n OpenAI may persist Responses when ``store=true``. This gateway's Responses\\n- path is a single-agent passthrough without a persistence plane, so\\n+ path has no provider-response persistence plane, so\\n ``store=true`` fails closed rather than silently dropping a buyer-visible\\n storage control. ``store=false`` and omit remain valid.\\n \\\"\\\"\\\"\\n@@ -3067,20 +3290,22 @@ def _validate_responses_store(body: dict[str, Any]) -> bool | None:\\n \\n \\n \\n-# OpenAI o-series reasoning_effort levels. Without an effort plane this gateway\\n-# treats known levels as default-effort no-ops (parity with verbosity low/medium/high).\\n+# OpenAI o-series reasoning_effort levels plus the orchestrator-owned policy\\n+# value. Without an effort plane this gateway treats accepted values as\\n+# default-effort no-ops (parity with verbosity low/medium/high).\\n _OPENAI_REASONING_EFFORT_LEVELS = frozenset(\\n- {\\\"none\\\", \\\"minimal\\\", \\\"low\\\", \\\"medium\\\", \\\"high\\\"}\\n+ {\\\"auto\\\", \\\"none\\\", \\\"minimal\\\", \\\"low\\\", \\\"medium\\\", \\\"high\\\"}\\n )\\n \\n \\n def _validate_chat_reasoning_effort(body: dict[str, Any]) -> None:\\n \\\"\\\"\\\"Chat Completions ``reasoning_effort`` — known levels are default-effort no-ops.\\n \\n OpenAI o-series models accept ``reasoning_effort`` (none/minimal/low/medium/high).\\n- This gateway never threads the knob into ``ModelClient`` on the orchestration\\n- path. Known levels are accepted as default-effort no-ops (no effort plane) so\\n- o-series SDK defaults (often ``medium``) do not 400; unknown values fail closed.\\n+ ``auto`` is an orchestrator policy value, not a provider model value. This\\n+ gateway never threads the knob into ``ModelClient`` on the orchestration\\n+ path. Accepted values are default-effort no-ops when no effort plane exists;\\n+ unknown values fail closed.\\n Explicit JSON null or empty/whitespace string is treat-as-omit.\\n \\\"\\\"\\\"\\n if \\\"reasoning_effort\\\" not in body:\\n@@ -3099,7 +3324,7 @@ def _validate_chat_reasoning_effort(body: dict[str, Any]) -> None:\\n raise RequestError(\\n 400,\\n \\\"invalid_reasoning_effort\\\",\\n- \\\"reasoning_effort must be one of none, minimal, low, medium, high \\\"\\n+ \\\"reasoning_effort must be one of auto, none, minimal, low, medium, high \\\"\\n \\\"on /v1/chat/completions\\\",\\n )\\n \\n@@ -3607,6 +3832,7 @@ def _validate_chat_response_format(body: dict[str, Any]) -> dict[str, Any] | Non\\n \\\"invalid_response_format\\\",\\n \\\"response_format.json_schema.schema must be an object\\\",\\n )\\n+ _validate_json_schema_definition(schema_body)\\n # Explicit JSON null / blank description is omit-equivalent: pop so\\n # passthrough matches omit (parity with Responses text.format).\\n if \\\"description\\\" in schema:\\n@@ -4038,7 +4264,7 @@ def _validate_chat_tool_choice(body: dict[str, Any]) -> str | dict[str, Any] | N\\n \\n \\n \\n-def _validate_responses_model(body: dict[str, Any]) -> str:\\n+def _validate_responses_model(body: dict[str, Any], *, required: bool = True) -> str:\\n \\\"\\\"\\\"Responses API ``model`` — required non-empty string ≤256 chars.\\n \\n OpenAI requires model on Responses. Missing/empty/non-string values fail\\n@@ -4047,6 +4273,8 @@ def _validate_responses_model(body: dict[str, Any]) -> str:\\n ``proxy_completion`` pool match sees the same id as form/JS padded names.\\n \\\"\\\"\\\"\\n model = body.get(\\\"model\\\")\\n+ if model is None and not required:\\n+ return \\\"contextual-orchestrator\\\"\\n if model is None:\\n raise RequestError(400, \\\"invalid_model\\\", \\\"model is required on /v1/responses\\\")\\n if not isinstance(model, str) or not model.strip():\\n@@ -4089,8 +4317,9 @@ def _validate_responses_reasoning(body: dict[str, Any]) -> None:\\n \\\"\\\"\\\"Responses API ``reasoning`` — known effort levels are default-effort no-ops.\\n \\n OpenAI Responses accepts a ``reasoning`` object (effort/summary controls).\\n+ ``auto`` is an orchestrator policy value; provider calls do not receive it.\\n This gateway proxies Responses but does not interpret or enforce reasoning\\n- controls. Known ``effort`` levels (none/minimal/low/medium/high) with blank\\n+ controls. Known ``effort`` levels (auto/none/minimal/low/medium/high) with blank\\n or omit ``summary`` are accepted as default-effort no-ops (chat\\n ``reasoning_effort`` parity). Explicit JSON null, empty object, or empty\\n string is treat-as-omit. Unknown effort/summary values fail closed.\\n@@ -4134,7 +4363,7 @@ def _validate_responses_reasoning(body: dict[str, Any]) -> None:\\n raise RequestError(\\n 400,\\n \\\"invalid_reasoning\\\",\\n- \\\"reasoning.effort must be one of none, minimal, low, medium, high \\\"\\n+ \\\"reasoning.effort must be one of auto, none, minimal, low, medium, high \\\"\\n \\\"on /v1/responses\\\",\\n )\\n \\n@@ -4169,12 +4398,27 @@ def _validate_batch_embeddings_endpoint(body: dict[str, Any]) -> str | None:\\n return value\\n \\n \\n-def _validate_embeddings_model(body: dict[str, Any]) -> str:\\n- \\\"\\\"\\\"OpenAI embeddings ``model`` — required non-empty string ≤256 chars.\\n+def _validate_embeddings_model(body: dict[str, Any], orchestrator: Any | None = None) -> str:\\n+ \\\"\\\"\\\"Validate or auto-select an OpenAI embeddings model.\\n \\n Strip + write back (parity with chat/Completions/Responses) so padded\\n- form/JS model names bind to the pool id on every surface.\\n+ form/JS model names bind to the pool id on every surface. An omitted model\\n+ is resolved by the orchestrator's explicit ``embedding`` capability pool;\\n+ no consumer-side sentinel model is accepted.\\n \\\"\\\"\\\"\\n+ if \\\"model\\\" not in body:\\n+ if orchestrator is None:\\n+ raise RequestError(400, \\\"invalid_model\\\", \\\"model is required outside an orchestrator request\\\")\\n+ try:\\n+ model = orchestrator.select_capability_agent(\\\"embedding\\\").model\\n+ except (RuntimeError, ValueError) as exc:\\n+ raise RequestError(\\n+ 503,\\n+ \\\"embedding_unavailable\\\",\\n+ \\\"no enabled embedding-capable agent is available\\\",\\n+ ) from exc\\n+ body[\\\"model\\\"] = model\\n+ return model\\n model = body.get(\\\"model\\\")\\n if model is None:\\n raise RequestError(400, \\\"invalid_model\\\", \\\"model is required\\\")\\n@@ -4412,14 +4656,51 @@ def build_server(\\n security = security or SecurityConfig()\\n security.check_bind(host)\\n coordinator = coordinator or CostRoutingCoordinator(orchestrator)\\n+ configure_telemetry(config=coordinator.config)\\n if clearfolio_url is not None:\\n parsed_viewer = urllib.parse.urlparse(clearfolio_url)\\n if parsed_viewer.scheme not in {\\\"http\\\", \\\"https\\\"} or not parsed_viewer.netloc:\\n raise ValueError(\\\"clearfolio_url must be an http(s) URL\\\")\\n clearfolio_url = clearfolio_url.rstrip(\\\"/\\\")\\n \\n class Handler(BaseHTTPRequestHandler):\\n+ \\\"\\\"\\\"Serve the authenticated OpenAI-compatible and administrative routes.\\\"\\\"\\\"\\n+ _session_token = None\\n+ _trace_token = None\\n+\\n+ def _bind_session(self, session_id: str | None) -> None:\\n+ if session_id is None:\\n+ return\\n+ if self._session_token is not None:\\n+ reset_session_id(self._session_token)\\n+ self._session_token = set_session_id(session_id)\\n+\\n+ def _reset_session(self) -> None:\\n+ \\\"\\\"\\\"Release the request session in the context that bound it.\\\"\\\"\\\"\\n+ trace_token = self._trace_token\\n+ self._trace_token = None\\n+ if trace_token is not None:\\n+ detach_trace_context(trace_token)\\n+ token = self._session_token\\n+ self._session_token = None\\n+ if token is not None:\\n+ reset_session_id(token)\\n+\\n+ def handle_one_request(self) -> None:\\n+ \\\"\\\"\\\"Prevent a keep-alive connection from carrying session state.\\\"\\\"\\\"\\n+ try:\\n+ super().handle_one_request()\\n+ finally:\\n+ self._reset_session()\\n+\\n+ def finish(self) -> None:\\n+ \\\"\\\"\\\"Flush the HTTP response and release request session context.\\\"\\\"\\\"\\n+ try:\\n+ super().finish()\\n+ finally:\\n+ self._reset_session()\\n def do_GET(self) -> None: # noqa: N802\\n+ \\\"\\\"\\\"Return health, discovery, result, and administrative resources.\\\"\\\"\\\"\\n parsed = urllib.parse.urlparse(self.path)\\n path = parsed.path\\n query = urllib.parse.parse_qs(parsed.query)\\n@@ -4769,6 +5050,7 @@ def do_GET(self) -> None: # noqa: N802\\n self._send_error(500, \\\"internal_error\\\", \\\"internal server error\\\")\\n \\n def do_PATCH(self) -> None: # noqa: N802\\n+ \\\"\\\"\\\"Apply an authenticated worker-agent configuration update.\\\"\\\"\\\"\\n try:\\n self._authorize(\\\"admin\\\")\\n path = urllib.parse.urlparse(self.path).path\\n@@ -4792,6 +5074,7 @@ def do_PATCH(self) -> None: # noqa: N802\\n self._send_error(500, \\\"internal_error\\\", \\\"internal server error\\\")\\n \\n def do_DELETE(self) -> None: # noqa: N802\\n+ \\\"\\\"\\\"Remove an authenticated worker agent from its configured pool.\\\"\\\"\\\"\\n try:\\n self._authorize(\\\"admin\\\")\\n path = urllib.parse.urlparse(self.path).path\\n@@ -4812,11 +5095,16 @@ def do_DELETE(self) -> None: # noqa: N802\\n self._send_error(500, \\\"internal_error\\\", \\\"internal server error\\\")\\n \\n def do_POST(self) -> None: # noqa: N802\\n+ \\\"\\\"\\\"Validate and execute inference or administrative commands.\\\"\\\"\\\"\\n try:\\n path = urllib.parse.urlparse(self.path).path\\n scope = \\\"admin\\\" if path == \\\"/admin/simulate\\\" or path.startswith(\\\"/api/v1/agent_pools/\\\") else \\\"inference\\\"\\n self._authorize(scope)\\n body = self._read_json()\\n+ for metadata_key in (\\\"metadata\\\", \\\"client_metadata\\\"):\\n+ metadata = body.get(metadata_key)\\n+ if isinstance(metadata, dict):\\n+ self._bind_session(session_id_from_metadata(metadata))\\n \\n if path.startswith(\\\"/api/v1/agent_pools/\\\") and path.endswith(\\\"/worker_agents\\\"):\\n segments = [part for part in path.split(\\\"/\\\") if part]\\n@@ -4886,24 +5174,13 @@ def do_POST(self) -> None: # noqa: N802\\n attribution[\\\"service\\\"] = \\\"completions_api\\\"\\n routing = _validate_routing(body.get(\\\"routing\\\"))\\n started_at = time.perf_counter()\\n- # Apply request sampling knobs to the provider client for this call.\\n- model_client = orchestrator.client\\n- previous_max_tokens = model_client.max_output_tokens\\n- previous_temperature = model_client.default_temperature\\n- previous_top_p = model_client.default_top_p\\n- previous_presence = model_client.default_presence_penalty\\n- previous_frequency = model_client.default_frequency_penalty\\n- if max_tokens is not None:\\n- model_client.max_output_tokens = max_tokens\\n- if temperature is not None:\\n- model_client.default_temperature = temperature\\n- if top_p is not None:\\n- model_client.default_top_p = top_p\\n- if presence_penalty is not None:\\n- model_client.default_presence_penalty = presence_penalty\\n- if frequency_penalty is not None:\\n- model_client.default_frequency_penalty = frequency_penalty\\n- try:\\n+ with orchestrator.client.request_settings(\\n+ max_output_tokens=max_tokens,\\n+ temperature=temperature,\\n+ top_p=top_p,\\n+ presence_penalty=presence_penalty,\\n+ frequency_penalty=frequency_penalty,\\n+ ):\\n result = self._run(lambda: coordinator.complete(\\n messages,\\n mode=\\\"route\\\",\\n@@ -4912,12 +5189,6 @@ def do_POST(self) -> None: # noqa: N802\\n model_name=model_name,\\n workflow_run_id=f\\\"run_{uuid.uuid4().hex}\\\",\\n ))\\n- finally:\\n- model_client.max_output_tokens = previous_max_tokens\\n- model_client.default_temperature = previous_temperature\\n- model_client.default_top_p = previous_top_p\\n- model_client.default_presence_penalty = previous_presence\\n- model_client.default_frequency_penalty = previous_frequency\\n # Batch-channel Completions return a job handle (202), not a\\n # text_completion body — match chat Completions honesty so\\n # clients never receive a 500 on a valid batch routing hint.\\n@@ -5024,8 +5295,7 @@ def do_POST(self) -> None: # noqa: N802\\n _validate_chat_response_format(body)\\n if \\\"tools\\\" in body:\\n _validate_chat_tools(body)\\n- if \\\"tool_choice\\\" in body:\\n- _validate_chat_tool_choice(body)\\n+ tool_choice = _validate_chat_tool_choice(body) if \\\"tool_choice\\\" in body else None\\n if \\\"parallel_tool_calls\\\" in body:\\n # Always type-check. With tools, true/false both valid for\\n # provider passthrough; without tools, true fails closed.\\n@@ -5046,7 +5316,7 @@ def do_POST(self) -> None: # noqa: N802\\n body[\\\"parallel_tool_calls\\\"] = ptc\\n # Strip+writeback model before tools/response_format passthrough so\\n # proxy_completion pool match sees the same id as form/JS padded names.\\n- _validate_completions_model(body)\\n+ _validate_completions_model(body, required=False)\\n # Coerce stream early so stream_options fail-closed matches route path\\n # and tools/response_format passthrough cannot skip type checks.\\n stream = body.get(\\\"stream\\\", False)\\n@@ -5072,29 +5342,56 @@ def do_POST(self) -> None: # noqa: N802\\n frequency_penalty = sampling[\\\"frequency_penalty\\\"]\\n # Explicit JSON null on trigger keys is omit-equivalent (SDK optional\\n # defaults) — do not force single-agent passthrough for null-only keys.\\n- if any(\\n- key in body and body.get(key) is not None\\n- for key in PASSTHROUGH_TRIGGER_KEYS\\n- ):\\n- # response_format / tools cannot be merged across agents;\\n- # proxy the full request to one agent and return it verbatim.\\n+ tool_passthrough = tools_list or isinstance(tool_choice, dict) or (\\n+ isinstance(tool_choice, str) and tool_choice not in {\\\"none\\\", \\\"auto\\\"}\\n+ )\\n+ tool_loop_header = self.headers.get(\\n+ \\\"x-contextual-orchestrator-tool-loop\\\", \\\"\\\"\\n+ ).strip().lower()\\n+ if tool_passthrough and tool_loop_header == \\\"v1\\\":\\n+ # OpenCode executes the returned function calls in its own\\n+ # bounded tool loop. Preserve the full provider response;\\n+ # multi-agent synthesis cannot safely merge tool state.\\n+ if stream:\\n+ raise RequestError(\\n+ 400,\\n+ \\\"invalid_stream\\\",\\n+ \\\"tool-loop passthrough requires stream=false\\\",\\n+ )\\n started_at = time.perf_counter()\\n- proxied = self._run(\\n- lambda: orchestrator.proxy_completion(body, endpoint=\\\"chat/completions\\\")\\n+ raw_response = self._run(\\n+ lambda: orchestrator.proxy_completion(body, single_agent=True)\\n )\\n orchestrator.record_analytics_event(\\n- \\\"chat_completion_passthrough\\\",\\n+ \\\"chat_completion_tool_passthrough\\\",\\n {\\n \\\"endpoint_path\\\": \\\"/v1/chat/completions\\\",\\n \\\"actor_scope\\\": \\\"inference\\\",\\n \\\"status_code\\\": 200,\\n \\\"duration_ms\\\": round((time.perf_counter() - started_at) * 1000, 2),\\n },\\n )\\n- self._send(proxied)\\n+ self._send(raw_response)\\n return\\n+ if tool_passthrough:\\n+ raise RequestError(\\n+ 422,\\n+ \\\"multi_agent_tools_unsupported\\\",\\n+ \\\"tool execution requires the explicit v1 client-owned tool-loop contract\\\",\\n+ )\\n messages = _validate_messages(body.get(\\\"messages\\\"))\\n mode = _validate_mode(body.get(\\\"orchestration\\\") or body.get(\\\"orchestration_mode\\\") or body.get(\\\"mode\\\") or \\\"auto\\\")\\n+ response_format = body.get(\\\"response_format\\\")\\n+ structured_response_format = (\\n+ response_format\\n+ if isinstance(response_format, dict)\\n+ and response_format.get(\\\"type\\\") in {\\\"json_object\\\", \\\"json_schema\\\"}\\n+ else None\\n+ )\\n+ if structured_response_format is not None:\\n+ # Structured output is a synthesis contract, not a\\n+ # provider passthrough. Force the multi-agent workflow.\\n+ mode = \\\"conduct\\\"\\n if \\\"include_orchestration_trace\\\" in body:\\n # Null/empty omit; bool, int 0/1, and \\\"true\\\"/\\\"false\\\"/\\\"0\\\"/\\\"1\\\"\\n # strings coerce (SDK form/query parity with stream/store).\\n@@ -5112,9 +5409,8 @@ def do_POST(self) -> None: # noqa: N802\\n # stream + stream_options already coerced/validated before passthrough.\\n attribution = _validate_attribution(body.get(\\\"attribution\\\"))\\n routing = _validate_routing(body.get(\\\"routing\\\"))\\n- # Require model — silent default to contextual-orchestrator hid\\n- # which deployment the buyer selected on the chat Completions path.\\n- model_name = _validate_completions_model(body)\\n+ # Omitted model means contextual-orchestrator owns selection.\\n+ model_name = _validate_completions_model(body, required=False)\\n _require_pool_model(orchestrator, model_name)\\n attribution = dict(attribution or {})\\n # OpenAI chat ``user`` → account when unset.\\n@@ -5131,23 +5427,13 @@ def do_POST(self) -> None: # noqa: N802\\n if \\\"metadata\\\" in body:\\n _validate_openai_metadata(body)\\n started_at = time.perf_counter()\\n- model_client = orchestrator.client\\n- previous_max_tokens = model_client.max_output_tokens\\n- previous_temperature = model_client.default_temperature\\n- previous_top_p = model_client.default_top_p\\n- previous_presence = model_client.default_presence_penalty\\n- previous_frequency = model_client.default_frequency_penalty\\n- if max_tokens is not None:\\n- model_client.max_output_tokens = max_tokens\\n- if temperature is not None:\\n- model_client.default_temperature = temperature\\n- if top_p is not None:\\n- model_client.default_top_p = top_p\\n- if presence_penalty is not None:\\n- model_client.default_presence_penalty = presence_penalty\\n- if frequency_penalty is not None:\\n- model_client.default_frequency_penalty = frequency_penalty\\n- try:\\n+ with orchestrator.client.request_settings(\\n+ max_output_tokens=max_tokens,\\n+ temperature=temperature,\\n+ top_p=top_p,\\n+ presence_penalty=presence_penalty,\\n+ frequency_penalty=frequency_penalty,\\n+ ):\\n if stream and orchestrator.would_route(messages, mode):\\n self._stream_route_completion(orchestrator, security, messages, model_name)\\n orchestrator.record_analytics_event(\\n@@ -5169,13 +5455,13 @@ def do_POST(self) -> None: # noqa: N802\\n hints=routing,\\n model_name=model_name,\\n workflow_run_id=f\\\"run_{uuid.uuid4().hex}\\\",\\n+ response_format=structured_response_format,\\n+ provider_request=(\\n+ body if structured_response_format is not None else None\\n+ ),\\n ))\\n- finally:\\n- model_client.max_output_tokens = previous_max_tokens\\n- model_client.default_temperature = previous_temperature\\n- model_client.default_top_p = previous_top_p\\n- model_client.default_presence_penalty = previous_presence\\n- model_client.default_frequency_penalty = previous_frequency\\n+ if structured_response_format is not None and result.get(\\\"channel\\\") != \\\"batch\\\":\\n+ _validate_structured_completion_answer(result.get(\\\"answer\\\"), structured_response_format)\\n # Latency-tolerant requests get dispatched to the batch backend.\\n if result.get(\\\"channel\\\") == \\\"batch\\\":\\n orchestrator.record_analytics_event(\\n@@ -5216,10 +5502,10 @@ def do_POST(self) -> None: # noqa: N802\\n # synchronously) and frames an OpenAI-shaped response so\\n # SDKs that call /v1/embeddings work without the batch path.\\n _reject_unknown_keys(body, ALLOWED_EMBEDDINGS_KEYS)\\n- model_name = _validate_embeddings_model(body)\\n+ model_name = _validate_embeddings_model(body, orchestrator)\\n # Same pool honesty as chat/Completions: do not silently serve\\n # a different embedding deployment than the client requested.\\n- _require_pool_model(orchestrator, model_name)\\n+ _require_pool_model(orchestrator, model_name, required_capability=\\\"embedding\\\")\\n encoding_format = _validate_embeddings_encoding_format(body)\\n _validate_embeddings_dimensions(body)\\n end_user_id = _validate_completions_user(body)\\n@@ -5305,16 +5591,8 @@ def do_POST(self) -> None: # noqa: N802\\n if path == \\\"/v1/batch/embeddings\\\":\\n _reject_unknown_keys(body, ALLOWED_EMBEDDINGS_BATCH_KEYS)\\n inputs = _validate_embeddings_inputs(body)\\n- # Require model — silent default to contextual-orchestrator was an\\n- # honesty gap for naruon/batch clients that omit the field.\\n- if \\\"model\\\" not in body:\\n- raise RequestError(\\n- 400,\\n- \\\"invalid_model\\\",\\n- \\\"model is required on /v1/batch/embeddings\\\",\\n- )\\n- model_name = _validate_embeddings_model(body)\\n- _require_pool_model(orchestrator, model_name)\\n+ model_name = _validate_embeddings_model(body, orchestrator)\\n+ _require_pool_model(orchestrator, model_name, required_capability=\\\"embedding\\\")\\n _validate_embeddings_encoding_format(body)\\n _validate_embeddings_dimensions(body)\\n # OpenAI ``user`` end-user id — same fail-closed shape as sync embeddings.\\n@@ -5386,12 +5664,13 @@ def do_POST(self) -> None: # noqa: N802\\n self._send(_response_payload(retrieved, include_trace=True))\\n return\\n if path == \\\"/v1/responses\\\":\\n- # The Responses API has no chat-completions verifier equivalent,\\n- # so every request is proxied to one agent verbatim.\\n+ # Normalize Responses input to the same multi-agent workflow\\n+ # used by Chat Completions; never silently proxy one agent.\\n _reject_unknown_keys(body, ALLOWED_RESPONSES_KEYS)\\n # Fail-closed shape checks before passthrough so buyers never\\n # get a 200 after shipping invalid OpenAI-shaped metadata/input.\\n- _validate_responses_model(body)\\n+ model_name = _validate_responses_model(body, required=False)\\n+ _require_pool_model(orchestrator, model_name)\\n _validate_responses_conversation_controls(body)\\n if \\\"store\\\" in body:\\n _validate_responses_store(body)\\n@@ -5403,14 +5682,10 @@ def do_POST(self) -> None: # noqa: N802\\n if \\\"stream_options\\\" in body:\\n _validate_responses_stream_options(body)\\n # Sampling knobs: type/range fail-closed before provider passthrough.\\n- if \\\"temperature\\\" in body:\\n- _validate_completions_temperature(body)\\n- if \\\"top_p\\\" in body:\\n- _validate_completions_top_p(body)\\n- if \\\"presence_penalty\\\" in body:\\n- _validate_completions_presence_penalty(body)\\n- if \\\"frequency_penalty\\\" in body:\\n- _validate_completions_frequency_penalty(body)\\n+ _validate_completions_temperature(body)\\n+ _validate_completions_top_p(body)\\n+ _validate_completions_presence_penalty(body)\\n+ _validate_completions_frequency_penalty(body)\\n if \\\"n\\\" in body:\\n _validate_responses_n(body)\\n if \\\"seed\\\" in body:\\n@@ -5421,12 +5696,9 @@ def do_POST(self) -> None: # noqa: N802\\n _validate_responses_logit_bias(body)\\n if \\\"logprobs\\\" in body or \\\"top_logprobs\\\" in body:\\n _validate_responses_logprobs(body)\\n- if \\\"max_tokens\\\" in body:\\n- _validate_completions_max_tokens(body)\\n- if \\\"max_completion_tokens\\\" in body:\\n- _validate_chat_max_completion_tokens(body)\\n- if \\\"max_output_tokens\\\" in body:\\n- _validate_responses_max_output_tokens(body)\\n+ responses_max_tokens = _validate_completions_max_tokens(body)\\n+ responses_max_completion_tokens = _validate_chat_max_completion_tokens(body)\\n+ responses_max_output_tokens = _validate_responses_max_output_tokens(body)\\n if \\\"max_tool_calls\\\" in body:\\n _validate_responses_max_tool_calls(body)\\n _validate_openai_sdk_control_fields(body, endpoint_path=\\\"/v1/responses\\\")\\n@@ -5488,6 +5760,15 @@ def do_POST(self) -> None: # noqa: N802\\n _validate_chat_tool_choice(body)\\n if \\\"response_format\\\" in body:\\n _validate_chat_response_format(body)\\n+ tool_loop_header = self.headers.get(\\n+ \\\"x-contextual-orchestrator-tool-loop\\\", \\\"\\\"\\n+ ).strip().lower()\\n+ if tools_list and tool_loop_header != \\\"v1\\\":\\n+ raise RequestError(\\n+ 422,\\n+ \\\"multi_agent_tools_unsupported\\\",\\n+ \\\"tool execution requires the explicit v1 client-owned tool-loop contract\\\",\\n+ )\\n if \\\"modalities\\\" in body:\\n _validate_responses_modalities(body)\\n if \\\"prediction\\\" in body:\\n@@ -5532,8 +5813,8 @@ def do_POST(self) -> None: # noqa: N802\\n \\\"invalid_input\\\",\\n \\\"input must be a non-empty string or non-empty array on /v1/responses\\\",\\n )\\n- # stream=false / omit → non-SSE JSON response (honest no-stream path).\\n- # stream=true is not implemented for Responses passthrough.\\n+ # stream=false / omit -> non-SSE JSON response (honest no-stream path).\\n+ # stream=true is not implemented for the conducted Responses path.\\n # String/0-1 forms coerce via shared bool helper (parity with chat).\\n if \\\"stream\\\" in body:\\n stream = _coerce_optional_bool(\\n@@ -5547,23 +5828,93 @@ def do_POST(self) -> None: # noqa: N802\\n \\\"invalid_stream\\\",\\n \\\"stream is not supported on /v1/responses\\\",\\n )\\n- started_at = time.perf_counter()\\n- proxied = self._run(\\n- lambda: orchestrator.proxy_completion(body, endpoint=\\\"responses\\\")\\n+ if tools_list and tool_loop_header == \\\"v1\\\":\\n+ # Validate input and stream before passthrough so the\\n+ # client-owned contract cannot silently downgrade a\\n+ # requested stream or accept a missing input.\\n+ started_at = time.perf_counter()\\n+ raw_response = self._run(\\n+ lambda: orchestrator.proxy_completion(\\n+ body,\\n+ endpoint=\\\"responses\\\",\\n+ single_agent=True,\\n+ )\\n+ )\\n+ orchestrator.record_analytics_event(\\n+ \\\"responses_tool_passthrough\\\",\\n+ {\\n+ \\\"endpoint_path\\\": \\\"/v1/responses\\\",\\n+ \\\"actor_scope\\\": \\\"inference\\\",\\n+ \\\"status_code\\\": 200,\\n+ \\\"duration_ms\\\": round((time.perf_counter() - started_at) * 1000, 2),\\n+ },\\n+ )\\n+ self._send(raw_response)\\n+ return\\n+ response_contract: dict[str, Any] | None = None\\n+ raw_response_format = body.get(\\\"response_format\\\")\\n+ if isinstance(raw_response_format, dict) and raw_response_format.get(\\\"type\\\") in {\\n+ \\\"json_object\\\",\\n+ \\\"json_schema\\\",\\n+ }:\\n+ response_contract = raw_response_format\\n+ text_config = body.get(\\\"text\\\")\\n+ text_format = text_config.get(\\\"format\\\") if isinstance(text_config, dict) else None\\n+ if isinstance(text_format, dict) and text_format.get(\\\"type\\\") in {\\\"json_object\\\", \\\"json_schema\\\"}:\\n+ if text_format[\\\"type\\\"] == \\\"json_object\\\":\\n+ response_contract = {\\\"type\\\": \\\"json_object\\\"}\\n+ else:\\n+ response_contract = {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\n+ key: text_format[key]\\n+ for key in (\\\"name\\\", \\\"description\\\", \\\"schema\\\", \\\"strict\\\")\\n+ if key in text_format\\n+ },\\n+ }\\n+ _reject_responses_orchestration_controls(body)\\n+ chat_payload = _responses_to_chat_payload(body)\\n+ response_max_tokens = (\\n+ responses_max_output_tokens\\n+ if responses_max_output_tokens is not None\\n+ else responses_max_completion_tokens\\n+ if responses_max_completion_tokens is not None\\n+ else responses_max_tokens\\n )\\n+ started_at = time.perf_counter()\\n+ with orchestrator.client.request_settings(\\n+ max_output_tokens=response_max_tokens,\\n+ ):\\n+ result = self._run(lambda: coordinator.complete(\\n+ chat_payload[\\\"messages\\\"],\\n+ mode=\\\"conduct\\\",\\n+ attribution=_validate_attribution(body.get(\\\"attribution\\\")),\\n+ hints=_validate_routing(body.get(\\\"routing\\\")),\\n+ model_name=model_name,\\n+ workflow_run_id=f\\\"run_{uuid.uuid4().hex}\\\",\\n+ response_format=response_contract,\\n+ provider_request=body,\\n+ provider_endpoint=\\\"responses\\\",\\n+ ))\\n+ if response_contract is not None:\\n+ _validate_structured_completion_answer(result.get(\\\"answer\\\"), response_contract)\\n+ provider_response = result.get(\\\"provider_response\\\")\\n+ if not isinstance(provider_response, dict):\\n+ raise RuntimeError(\\\"Responses completion omitted provider response\\\")\\n+ orchestrated = dict(provider_response)\\n+ orchestrated[\\\"model\\\"] = model_name\\n+ if \\\"usage\\\" not in orchestrated and isinstance(result.get(\\\"usage\\\"), dict):\\n+ orchestrated[\\\"usage\\\"] = _responses_usage(result[\\\"usage\\\"])\\n orchestrator.record_analytics_event(\\n- \\\"responses_passthrough\\\",\\n+ \\\"responses_orchestrated\\\",\\n {\\n \\\"endpoint_path\\\": \\\"/v1/responses\\\",\\n \\\"actor_scope\\\": \\\"inference\\\",\\n \\\"status_code\\\": 200,\\n \\\"duration_ms\\\": round((time.perf_counter() - started_at) * 1000, 2),\\n },\\n )\\n- if body.get(\\\"stream\\\") is True:\\n- self._send_sse(responses_sse_body(proxied))\\n- else:\\n- self._send(proxied)\\n+ self._send(orchestrated)\\n return\\n \\n if path == \\\"/admin/simulate\\\":\\n@@ -5611,6 +5962,8 @@ def do_POST(self) -> None: # noqa: N802\\n self._send_error(500, \\\"internal_error\\\", \\\"internal server error\\\")\\n \\n def _authorize(self, scope: str) -> None:\\n+ self._trace_token = attach_trace_context(self.headers)\\n+ self._bind_session(session_id_from_headers(self.headers))\\n security.check_rate_limit(self.client_address[0])\\n security.authorize(self.headers, scope, self.client_address[0])\\n \\n@@ -5646,15 +5999,49 @@ def _parse_optional_int(self, query: dict[str, list[str]], field_name: str) -> i\\n return int(raw)\\n \\n def _read_json(self) -> dict[str, Any]:\\n+ \\\"\\\"\\\"Read one bounded, fixed-length JSON body and close bad frames.\\\"\\\"\\\"\\n if self.headers.get(\\\"content-type\\\", \\\"\\\").split(\\\";\\\", 1)[0].strip().lower() != \\\"application/json\\\":\\n raise RequestError(415, \\\"unsupported_media_type\\\", \\\"content-type must be application/json\\\")\\n- body_size = int(self.headers.get(\\\"content-length\\\", \\\"0\\\"))\\n- if body_size > security.max_body_bytes:\\n- raise RequestError(413, \\\"request_too_large\\\", \\\"request body exceeds configured limit\\\")\\n- raw = self.rfile.read(body_size)\\n- return _coerce_json(raw) if raw else {}\\n+ try:\\n+ body_size = _parse_request_framing(self.headers, security.max_body_bytes)\\n+ except RequestError:\\n+ self.close_connection = True\\n+ raise\\n+ if body_size == 0:\\n+ return {}\\n+ connection = getattr(self, \\\"connection\\\", None)\\n+ previous_timeout = None\\n+ timeout_supported = all(\\n+ hasattr(connection, method) for method in (\\\"gettimeout\\\", \\\"settimeout\\\")\\n+ )\\n+ if timeout_supported:\\n+ previous_timeout = connection.gettimeout()\\n+ connection.settimeout(security.request_read_timeout_seconds)\\n+ read_deadline = time.monotonic() + security.request_read_timeout_seconds\\n+ try:\\n+ chunks = bytearray()\\n+ while len(chunks) < body_size:\\n+ if time.monotonic() >= read_deadline:\\n+ self.close_connection = True\\n+ raise RequestError(408, \\\"request_read_timeout\\\", \\\"request body read timed out\\\")\\n+ chunk = self.rfile.read(body_size - len(chunks))\\n+ if not chunk:\\n+ self.close_connection = True\\n+ raise RequestError(400, \\\"invalid_request_framing\\\", \\\"request body ended before content-length\\\")\\n+ chunks.extend(chunk)\\n+ if len(chunks) < body_size and time.monotonic() >= read_deadline:\\n+ self.close_connection = True\\n+ raise RequestError(408, \\\"request_read_timeout\\\", \\\"request body read timed out\\\")\\n+ except (TimeoutError, socket.timeout):\\n+ self.close_connection = True\\n+ raise RequestError(408, \\\"request_read_timeout\\\", \\\"request body read timed out\\\") from None\\n+ finally:\\n+ if timeout_supported:\\n+ connection.settimeout(previous_timeout)\\n+ return _coerce_json(bytes(chunks))\\n \\n def log_message(self, format: str, *args: object) -> None:\\n+ \\\"\\\"\\\"Disable the base server's unaudited stderr access log.\\\"\\\"\\\"\\n return\\n \\n def _send_error(\\n@@ -5664,6 +6051,13 @@ def _send_error(\\n message: str,\\n detail: dict[str, Any] | None = None,\\n ) -> None:\\n+ _LOGGER.warning(\\n+ \\\"request_failed status=%s code=%s path=%s session_id=%s\\\",\\n+ status,\\n+ code,\\n+ urllib.parse.urlparse(self.path).path,\\n+ current_session_id() or \\\"\\\",\\n+ )\\n self._send(_error_payload(code, message, {\\\"request_id\\\": uuid.uuid4().hex, **(detail or {})}), status)\\n \\n def _send(self, payload: dict[str, Any], status: int = 200) -> None:\\n@@ -5752,8 +6146,16 @@ def serve(\\n port: int = 8000,\\n security: SecurityConfig | None = None,\\n clearfolio_url: str | None = None,\\n+ coordinator: CostRoutingCoordinator | None = None,\\n ) -> None:\\n \\\"\\\"\\\"Serve the admin console and resource-oriented orchestration API.\\\"\\\"\\\"\\n- server = build_server(orchestrator, host=host, port=port, security=security, clearfolio_url=clearfolio_url)\\n+ server = build_server(\\n+ orchestrator,\\n+ host=host,\\n+ port=port,\\n+ security=security,\\n+ clearfolio_url=clearfolio_url,\\n+ coordinator=coordinator,\\n+ )\\n print(f\\\"listening on http://{host}:{port}\\\")\\n server.serve_forever()\" }, { \"sha\": \"27424e96201cd7d326d014f78996d7a6f1b49690\", \"filename\": \"contextual_orchestrator/telemetry.py\", \"status\": \"added\", \"additions\": 207, \"deletions\": 0, \"changes\": 207, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Ftelemetry.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Ftelemetry.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Ftelemetry.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,207 @@\\n+\\\"\\\"\\\"Prompt-safe OpenTelemetry and session correlation for the gateway.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import logging\\n+from collections.abc import Iterator, Mapping\\n+from contextlib import contextmanager\\n+from contextvars import ContextVar, Token\\n+from typing import Any\\n+\\n+try:\\n+ from opentelemetry import trace\\n+ from opentelemetry.context import attach as _otel_attach\\n+ from opentelemetry.context import detach as _otel_detach\\n+ from opentelemetry.propagate import extract as _otel_extract\\n+ from opentelemetry.propagate import inject as _otel_inject\\n+ from opentelemetry.trace import SpanKind, Status, StatusCode\\n+except ImportError: # pragma: no cover - dependency is declared by the project\\n+ trace = None # type: ignore[assignment]\\n+ _otel_attach = None\\n+ _otel_detach = None\\n+ _otel_extract = None\\n+ _otel_inject = None\\n+ SpanKind = None # type: ignore[assignment,misc]\\n+ Status = None # type: ignore[assignment,misc]\\n+ StatusCode = None # type: ignore[assignment,misc]\\n+\\n+_LOGGER = logging.getLogger(__name__)\\n+_CURRENT_SESSION: ContextVar[str | None] = ContextVar(\\n+ \\\"contextual_orchestrator_session_id\\\", default=None\\n+)\\n+_CONFIGURED = False\\n+\\n+\\n+def _otlp_trace_endpoint(endpoint: str) -> str:\\n+ \\\"\\\"\\\"Turn an OTLP base endpoint into the explicit HTTP traces endpoint.\\\"\\\"\\\"\\n+ normalized = endpoint.rstrip(\\\"/\\\")\\n+ if normalized.casefold().endswith(\\\"/v1/traces\\\"):\\n+ return normalized\\n+ return f\\\"{normalized}/v1/traces\\\"\\n+\\n+\\n+def _config_value(config: Any | None, key: str, default: Any = None) -> Any:\\n+ \\\"\\\"\\\"Read one telemetry setting from the injected KV configuration.\\\"\\\"\\\"\\n+ if config is None:\\n+ return default\\n+ return config.get(\\\"telemetry\\\", key, default)\\n+\\n+\\n+def _normalize_session_id(value: object) -> str | None:\\n+ \\\"\\\"\\\"Accept a bounded correlation value without accepting a bearer token.\\\"\\\"\\\"\\n+ if not isinstance(value, str):\\n+ return None\\n+ value = value.strip()\\n+ if not value or len(value) > 128 or any(ord(char) < 0x20 for char in value):\\n+ return None\\n+ return value\\n+\\n+\\n+def session_id_from_headers(headers: Mapping[str, str]) -> str | None:\\n+ \\\"\\\"\\\"Read the LineageWeave correlation header from an HTTP request.\\\"\\\"\\\"\\n+ return _normalize_session_id(\\n+ headers.get(\\\"x-lineageweave-session-id\\\") or headers.get(\\\"x-session-id\\\")\\n+ )\\n+\\n+\\n+def session_id_from_metadata(metadata: Mapping[str, Any] | None) -> str | None:\\n+ \\\"\\\"\\\"Read a session value from compatible OpenAI metadata fields.\\\"\\\"\\\"\\n+ if metadata is None:\\n+ return None\\n+ return _normalize_session_id(\\n+ metadata.get(\\\"lineageweave_post_session_id\\\") or metadata.get(\\\"session_id\\\")\\n+ )\\n+\\n+\\n+def current_session_id() -> str | None:\\n+ \\\"\\\"\\\"Return the request-scoped correlation value, if one is bound.\\\"\\\"\\\"\\n+ return _CURRENT_SESSION.get()\\n+\\n+\\n+def set_session_id(value: object) -> Token[str | None]:\\n+ \\\"\\\"\\\"Bind one session to the current request context.\\\"\\\"\\\"\\n+ return _CURRENT_SESSION.set(_normalize_session_id(value))\\n+\\n+\\n+def reset_session_id(token: Token[str | None]) -> None:\\n+ \\\"\\\"\\\"Restore the context value that preceded a request.\\\"\\\"\\\"\\n+ _CURRENT_SESSION.reset(token)\\n+\\n+\\n+def attach_trace_context(headers: Mapping[str, str]) -> Any:\\n+ \\\"\\\"\\\"Attach an inbound W3C trace context and return its reset token.\\\"\\\"\\\"\\n+ if _otel_extract is None or _otel_attach is None:\\n+ return None\\n+ carrier = {str(key).lower(): str(value) for key, value in headers.items()}\\n+ return _otel_attach(_otel_extract(carrier))\\n+\\n+\\n+def detach_trace_context(token: Any) -> None:\\n+ \\\"\\\"\\\"Detach an inbound W3C trace context after one HTTP request.\\\"\\\"\\\"\\n+ if token is not None and _otel_detach is not None:\\n+ _otel_detach(token)\\n+\\n+\\n+def inject_trace_context(headers: dict[str, str]) -> None:\\n+ \\\"\\\"\\\"Inject the active W3C trace context into one provider request.\\\"\\\"\\\"\\n+ if _otel_inject is not None:\\n+ _otel_inject(headers)\\n+\\n+\\n+def _safe_attributes(\\n+ attributes: Mapping[str, Any] | None,\\n+) -> dict[str, str | int | float | bool]:\\n+ \\\"\\\"\\\"Keep span attributes scalar and exclude prompt, answer, and secret content.\\\"\\\"\\\"\\n+ result: dict[str, str | int | float | bool] = {}\\n+ for key, value in (attributes or {}).items():\\n+ if (\\n+ not isinstance(key, str)\\n+ or not key\\n+ or isinstance(value, (dict, list, tuple, set))\\n+ ):\\n+ continue\\n+ if isinstance(value, str):\\n+ result[key] = value[:256]\\n+ elif isinstance(value, (bool, int, float)):\\n+ result[key] = value\\n+ session_id = current_session_id()\\n+ if session_id:\\n+ result.setdefault(\\\"contextual_orchestrator.session_id\\\", session_id)\\n+ return result\\n+\\n+\\n+def configure_telemetry(\\n+ service_name: str = \\\"contextual-orchestrator\\\",\\n+ *,\\n+ config: Any | None = None,\\n+) -> None:\\n+ \\\"\\\"\\\"Configure OTLP export from the injected KV configuration only.\\\"\\\"\\\"\\n+ global _CONFIGURED\\n+ if _CONFIGURED:\\n+ return\\n+ if config is None:\\n+ _LOGGER.debug(\\\"OpenTelemetry is not configured without a KV store\\\")\\n+ return\\n+ _CONFIGURED = True\\n+ if str(_config_value(config, \\\"sdk_disabled\\\", \\\"\\\")).lower() == \\\"true\\\":\\n+ return\\n+ endpoint = str(_config_value(config, \\\"exporter_otlp_endpoint\\\", \\\"\\\")).strip()\\n+ if trace is None or not endpoint:\\n+ return\\n+ try:\\n+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (\\n+ OTLPSpanExporter,\\n+ )\\n+ from opentelemetry.sdk.resources import Resource\\n+ from opentelemetry.sdk.trace import TracerProvider\\n+ from opentelemetry.sdk.trace.export import BatchSpanProcessor\\n+ except ImportError: # pragma: no cover - guarded by the runtime dependency\\n+ _LOGGER.warning(\\\"OpenTelemetry SDK/exporter is unavailable\\\")\\n+ return\\n+\\n+ configured_service_name = str(\\n+ _config_value(config, \\\"service_name\\\", service_name)\\n+ ).strip() or service_name\\n+ resource = Resource.create({\\n+ \\\"service.name\\\": configured_service_name,\\n+ \\\"service.namespace\\\": \\\"contextualwisdomlab\\\",\\n+ })\\n+ provider = TracerProvider(resource=resource)\\n+ provider.add_span_processor(\\n+ BatchSpanProcessor(\\n+ OTLPSpanExporter(endpoint=_otlp_trace_endpoint(endpoint))\\n+ )\\n+ )\\n+ trace.set_tracer_provider(provider)\\n+\\n+\\n+@contextmanager\\n+def traced(\\n+ name: str,\\n+ attributes: Mapping[str, Any] | None = None,\\n+) -> Iterator[Any]:\\n+ \\\"\\\"\\\"Trace one provider CLIENT operation and preserve all failures.\\\"\\\"\\\"\\n+ if trace is None: # pragma: no cover - dependency is declared by the project\\n+ yield None\\n+ return\\n+ tracer = trace.get_tracer(\\\"contextual-orchestrator\\\")\\n+ safe = _safe_attributes(attributes)\\n+ with tracer.start_as_current_span(\\n+ name,\\n+ kind=SpanKind.CLIENT,\\n+ attributes=safe,\\n+ ) as span:\\n+ try:\\n+ yield span\\n+ except Exception as exc:\\n+ if Status is not None and StatusCode is not None:\\n+ span.record_exception(exc)\\n+ span.set_attribute(\\\"error.type\\\", type(exc).__name__)\\n+ span.set_status(Status(StatusCode.ERROR))\\n+ _LOGGER.warning(\\n+ \\\"telemetry.operation_failed operation=%s error_type=%s session_id=%s\\\",\\n+ name,\\n+ type(exc).__name__,\\n+ safe.get(\\\"contextual_orchestrator.session_id\\\", \\\"\\\"),\\n+ )\\n+ raise\" }, { \"sha\": \"c04f528e099aed5de2d0772646adc33af8a0f569\", \"filename\": \"docs/adr/0122-otel-session-observability.md\", \"status\": \"added\", \"additions\": 54, \"deletions\": 0, \"changes\": 54, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fadr%2F0122-otel-session-observability.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fadr%2F0122-otel-session-observability.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fadr%2F0122-otel-session-observability.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,54 @@\\n+# ADR 0122: Correlate gateway provider telemetry by caller session\\n+\\n+## Status\\n+\\n+Accepted.\\n+\\n+## Context\\n+\\n+The gateway can route one request through several workers and providers. A\\n+caller-provided post session already exists in compatible metadata, but it was\\n+not bound to the HTTP request or provider diagnostics. The organization GRC\\n+service owns the low-cardinality, secret-free telemetry control in its ADR\\n+0009; this service must emit evidence that can be consumed there without\\n+becoming a second GRC store.\\n+\\n+## Decision\\n+\\n+Telemetry deployment settings may enter the process KV during bootstrap from non-secret OTEL_* transport settings; runtime telemetry reads the injected KV only. A configured OTLP base URL is normalized to the HTTP /v1/traces signal endpoint.\\n+\\n+1. Use the OpenTelemetry Python API, SDK, and OTLP HTTP exporter. Export is\\n+ disabled unless `OTEL_EXPORTER_OTLP_ENDPOINT` is explicitly configured.\\n+2. Accept `X-LineageWeave-Session-Id` and compatible metadata fields, bind the\\n+ normalized value to the request context, and reset it when the request\\n+ handler finishes.\\n+3. Add the bounded session correlation to provider spans for chat and embedding\\n+ calls. Follow the current OpenTelemetry GenAI span convention: emit CLIENT\\n+ spans named `chat {model}` or `embeddings {model}`, include the required\\n+ `gen_ai.operation.name` and `gen_ai.provider.name` attributes, and use\\n+ `server.address` / `server.port` for the transport destination. Record\\n+ `error.type` on failure, but never prompt, answer, request body, API key, or\\n+ raw provider response.\\n+4. Keep structured-output, Responses API, VISION, embedding, and multi-agent\\n+ requests on the same orchestration path. Telemetry observes that path; it\\n+ does not introduce a single-agent fallback or a second credential source.\\n+\\n+## Consequences\\n+\\n+An operator can follow one LineageWeave post through gateway routing and\\n+provider failures while GRC receives aggregate operational evidence rather\\n+than copied product data. Session correlation is diagnostic only: it is not an\\n+identity, tenant, authorization, or evidence label.\\n+\\n+## References\\n+\\n+OpenTelemetry Authors. (n.d.). *Manual instrumentation with OpenTelemetry\\n+Python*. Retrieved August 21, 2026, from\\n+https://opentelemetry.io/docs/languages/python/instrumentation/\\n+\\n+OpenTelemetry Authors. (n.d.). *Service semantic conventions*. Retrieved\\n+August 21, 2026, from https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/\\n+\\n+OpenTelemetry Authors. (n.d.). *Semantic conventions for generative AI spans*.\\n+Retrieved August 21, 2026, from\\n+https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md\" }, { \"sha\": \"bfdafea3a1e22e34b6ba32226cf68fee3dc9f7bb\", \"filename\": \"docs/architecture.md\", \"status\": \"modified\", \"additions\": 33, \"deletions\": 2, \"changes\": 35, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Farchitecture.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Farchitecture.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Farchitecture.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -6,6 +6,10 @@\\n - Sakana Fugu Technical Report: https://github.com/SakanaAI/fugu/blob/main/Fugu_technical_report.pdf\\n - TRINITY: An Evolved LLM Coordinator: https://arxiv.org/abs/2512.04695\\n - Learning to Orchestrate Agents in Natural Language with the Conductor: https://arxiv.org/abs/2512.04388\\n+- Route to Reason: Adaptive Routing for LLM and Reasoning Strategy Selection: https://arxiv.org/abs/2505.19435\\n+- Route-and-Reason: Scaling Large Language Model Reasoning with Reinforced Model Router: https://arxiv.org/abs/2506.05901\\n+- Reasoning on a Budget: A Survey of Adaptive and Controllable Test-Time Compute in LLMs: https://arxiv.org/abs/2507.02076\\n+- Ares: Adaptive Reasoning Effort Selection for Efficient LLM Agents: https://arxiv.org/abs/2603.07915\\n \\n ## What The Architecture Is\\n \\n@@ -24,6 +28,17 @@ The useful split is quality-latency, not separate products:\\n - Low-latency routing: select one worker for the current query or turn.\\n - Deep orchestration: create a multi-step workflow when the task needs decomposition, independent attempts, verification, or synthesis.\\n \\n+Structured provider features do not create a third single-agent path. A\\n+non-null `response_format` or Responses request enters the conducted workflow\\n+and reaches one final provider only after the\\n+Thinker/Worker/Verifier/Synthesizer evidence has been assembled. The final\\n+provider call preserves the validated wire feature; it is a transport boundary,\\n+not a bypass of orchestration. JSON object and JSON schema are both covered by\\n+[ADR 0011](planning/adrs/0011-structured-provider-features-stay-orchestrated.md).\\n+The explicit client-owned tool-loop exception remains the single-worker\\n+contract defined by [ADR 0014](planning/adrs/0014-gateway-owned-model-selection.md);\\n+ordinary tool declarations without that opt-in fail closed.\\n+\\n TRINITY contributes the compact coordinator idea: a small model representation plus a lightweight head can choose agent and role over multiple turns. Its Thinker, Worker, and Verifier contracts are practical enough to implement directly.\\n \\n Conductor contributes the workflow representation: each step is a natural-language subtask, an assigned worker, and an access list of prior step outputs. This is the key piece for preventing every worker from being dragged into the same transcript while still allowing deliberate collaboration.\\n@@ -48,10 +63,23 @@ bounded, authenticated recursion protocol; it is not administratively disabled.\\n - `Orchestrator.route_once`: the low-latency routing path.\\n - `Orchestrator.conduct`: the workflow path with planner, worker, verifier, and synthesizer steps.\\n - `WorkflowStep.access`: Conductor-style visibility control.\\n+- Image-bearing Chat Completions and Responses retain their typed source image\\n+ blocks in every evidence-bearing workflow step; access lists still constrain\\n+ prior model outputs. See [ADR 0018](planning/adrs/0018-multimodal-evidence-preserving-orchestration.md).\\n - `ModelClient`: OpenAI-compatible HTTP client, with `mock://` for local checks.\\n - `contextual_orchestrator.server`: small `/v1/chat/completions` HTTP server.\\n \\n-The deliberate simplification is the policy. The paper systems learn routing and topology from rewards; this lab uses a deterministic capability-hint heuristic only for worker/role routing so the repo runs without training data, GPUs, or vendor credentials. It is never an answer-quality, verification, or accept/reject judgment: verifier decisions must use the structured model judge and fail closed (see [ADR 0001](planning/adrs/0001-fail-closed-model-judgment.md)).\\n+The deliberate simplification is the policy. The paper systems learn routing\\n+and topology from rewards; this lab uses capability evidence and a bounded\\n+orchestrator policy, with `auto` kept internal rather than sent as a provider\\n+value. This is never an answer-quality, verification, or accept/reject\\n+judgment: verifier decisions must use the structured model judge and fail\\n+closed (see [ADR 0001](planning/adrs/0001-fail-closed-model-judgment.md)).\\n+\\n+Model and reasoning changes are governed by [ADR\\n+0013](planning/adrs/0013-paper-grounded-adaptive-reasoning-policy.md). The\\n+provider-neutral gateway boundary and direct-MLX prohibition are governed by\\n+[ADR 0012](planning/adrs/0012-gateway-only-provider-contract.md).\\n \\n Add learned routing only when there is an evaluation set and logs proving the heuristic policy is the bottleneck.\\n \\n@@ -73,6 +101,9 @@ OpenAI. (n.d.-a). *Create chat completion*. OpenAI Platform. https://platform.op\\n \\n OpenAI. (n.d.-b). *Create a model response*. OpenAI Platform. https://platform.openai.com/docs/api-reference/responses/create\\n \\n+Tang, Y., et al. (2026). *Sakana Fugu technical report* (arXiv:2606.21228).\\n+arXiv. https://doi.org/10.48550/arXiv.2606.21228\\n+\\n Sakana AI. (2026, June 22). *Sakana Fugu: One model to command them all*. https://sakana.ai/fugu-release/\\n \\n Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *Trinity: An evolved LLM coordinator* (arXiv:2512.04695). https://doi.org/10.48550/arXiv.2512.04695\\n@@ -92,4 +123,4 @@ The product is not a Fugu clone. It is a control-plane prototype for the same pu\\n See [product_planning.md](product_planning.md) for the product reboot.\\n \\n \\n-OpenAI o-series `reasoning_effort` (chat/Completions) and Responses `reasoning.effort` accept known levels `none`/`minimal`/`low`/`medium`/`high` (casefold, strip) as default-effort no-ops when this gateway has no effort plane; unknown levels fail closed with named errors. Locked by `tests/test_reasoning_effort_low_medium_high_noop_http_honesty.py` on tip ≥ #738.\\n+OpenAI o-series `reasoning_effort` (chat/Completions) and Responses `reasoning.effort` accept provider levels `none`/`minimal`/`low`/`medium`/`high` plus the orchestrator-owned `auto` policy value (casefold, strip). They are default-effort no-ops when this gateway has no effort plane; unknown levels fail closed with named errors. Locked by the reasoning HTTP honesty tests on tip ≥ #738.\" }, { \"sha\": \"caca05ee5d4a65a54c8672c9f26e4bb0f0dcdf71\", \"filename\": \"docs/doctoring/OPENTELEMETRY_REFERENCES.md\", \"status\": \"added\", \"additions\": 29, \"deletions\": 0, \"changes\": 29, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fdoctoring%2FOPENTELEMETRY_REFERENCES.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fdoctoring%2FOPENTELEMETRY_REFERENCES.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdoctoring%2FOPENTELEMETRY_REFERENCES.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,29 @@\\n+# OpenTelemetry references and implementation traceability\\n+\\n+## Normative references\\n+\\n+- OpenTelemetry Authors. (n.d.). *Manual instrumentation with OpenTelemetry\\n+ Python*. Retrieved August 21, 2026, from\\n+ https://opentelemetry.io/docs/languages/python/instrumentation/\\n+- OpenTelemetry Authors. (n.d.). *Service semantic conventions*. Retrieved\\n+ August 21, 2026, from\\n+ https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/\\n+- OpenTelemetry Authors. (n.d.). *Semantic conventions for generative AI\\n+ spans*. Retrieved August 21, 2026, from\\n+ https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md\\n+- ContextualWisdomLab governance-risk-compliance. (2026). *ADR 0009:\\n+ Emit bounded OpenTelemetry request telemetry*. Retrieved August 21, 2026,\\n+ from https://github.com/ContextualWisdomLab/governance-risk-compliance/blob/develop/docs/adr/0009-opentelemetry-request-telemetry.md\\n+\\n+## Implementation mapping\\n+\\n+| Concern | Implementation | Evidence boundary |\\n+| --- | --- | --- |\\n+| Service resource | `OTEL_SERVICE_NAME`, default `contextual-orchestrator` | One logical service name per deployment |\\n+| Request correlation | `X-LineageWeave-Session-Id` and compatible metadata | Correlation only; not identity or authorization |\\n+| Provider calls | `ModelClient` chat/embedding CLIENT spans | Required GenAI operation/provider attributes, model and server destination; no prompt, answer, key, or response |\\n+| Export | Bootstrap OTEL_EXPORTER_OTLP_ENDPOINT into the process KV | Disabled by default; runtime reads KV and sends to the normalized /v1/traces signal |\\n+\\n+The GRC repository remains the organization control and evidence owner. The\\n+gateway emits operational signals and does not copy GRC tables or provider\\n+credentials.\" }, { \"sha\": \"a1bdcf76bca059234fe8bf91a7fd49671cde5437\", \"filename\": \"docs/doctoring/embedding-chat-capability-isolation.md\", \"status\": \"added\", \"additions\": 131, \"deletions\": 0, \"changes\": 131, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fdoctoring%2Fembedding-chat-capability-isolation.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fdoctoring%2Fembedding-chat-capability-isolation.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdoctoring%2Fembedding-chat-capability-isolation.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,131 @@\\n+# Embedding-to-chat capability isolation incident\\n+\\n+**Status:** Accepted incident decision\\n+**Date:** 2026-08-20\\n+**Affected consumer:** LineageWeave buyer-surface stack around PR #260\\n+\\n+## Incident\\n+\\n+A conducted workflow reached the final contextual-orchestrator synthesizer with\\n+`model_group=text-embedding-3-large` and deployment\\n+`azure/text-embedding-3-large`. The gateway rejected the chat operation as\\n+unsupported. Its configured fallback map contained chat-generation model groups,\\n+but no fallback attached to the embedding group.\\n+\\n+The missing fallback was a symptom, not the causal defect. An embedding deployment\\n+had already crossed the chat-agent capability boundary and become eligible for a\\n+worker role.\\n+\\n+## Causal boundary\\n+\\n+Provider-compatible `/models` registries can contain multiple endpoint families.\\n+The original discovery parser accepted every non-empty model identifier and exposed\\n+it to agent creation, price selection, and durable pool synchronization. A catalog\\n+row naming an embedding deployment could therefore be scored for thinker, worker,\\n+verifier, or synthesizer work even though its serving endpoint accepts embedding\\n+input rather than chat messages.\\n+\\n+The first incident fix closed discovery and price-routing boundaries, but further\\n+root-cause tracing showed an already-persisted incompatible `ModelAgent` could still\\n+survive that filter. The runtime ranking path, generated workflow assignment,\\n+cross-agent failover, readiness probe, streaming path, and direct\\n+`ModelClient.chat()` path previously trusted the persisted model identifier. That\\n+stale-state path is sufficient to reproduce the same unsupported Azure chat\\n+operation after a process restart or durable bootstrap.\\n+\\n+OpenAI documents `text-embedding-3-large` under the embeddings endpoint, separately\\n+from models supported by chat completions. Microsoft likewise demonstrates it with\\n+`client.embeddings.create`, not `client.chat.completions.create`. LiteLLM exposes\\n+chat, responses, embeddings, image, audio, rerank, and other endpoint families as\\n+distinct operations. A router fallback can choose another deployment for the same\\n+operation; it cannot make an embedding deployment execute a chat operation.\\n+\\n+## Decision\\n+\\n+Chat transport compatibility and general agent-role eligibility are separate\\n+shared runtime invariants. A provider may expose an audio-capable model or a\\n+policy classifier through Chat Completions while that model remains unsuitable\\n+for ordinary thinker, worker, verifier, or synthesizer work.\\n+\\n+1. Normalize provider prefixes and common separators in model identifiers.\\n+2. At the transport boundary, reject identifiers that clearly advertise embedding,\\n+ reranking, transcription, moderation-endpoint, image-generation, realtime, or\\n+ speech-only semantics.\\n+3. Keep provider-documented audio and policy-classifier models transport-compatible\\n+ when they are served through Chat Completions.\\n+4. At discovery and ordinary orchestration-role boundaries, additionally exclude\\n+ explicit guard, safety, and NemoGuard policy classifiers.\\n+5. Apply the general-role guard while parsing both OpenAI-compatible and Bytez\\n+ catalogs and before converting, pricing, or cost-selecting a discovery record.\\n+6. Remove stale ineligible agents from thinker, worker, verifier, and synthesizer\\n+ ranking even if a durable configuration still contains them.\\n+7. Reselect a generated workflow step that explicitly names a stale ineligible\\n+ agent and omit such agents from planner inventory.\\n+8. Remove ineligible agents from cross-agent failover candidates.\\n+9. Apply the transport guard at `ModelClient.chat()`, `stream_chat()`, and\\n+ readiness probing before mock or network transport.\\n+10. Fail closed when no general chat agent remains.\\n+11. Leave unknown identifiers eligible without fabricating reasoning, tool, vision,\\n+ or verification capabilities from their names.\\n+\\n+This is deliberately a conservative negative filter. A future capability registry\\n+may replace name-based exclusion with authenticated provider metadata, measured\\n+endpoint probes, and separate endpoint-specific pools. Until that evidence exists,\\n+a clearly incompatible model fails closed at transport boundaries and a clearly\\n+specialized policy model fails closed at general-role boundaries.\\n+\\n+## Rejected response\\n+\\n+Adding `text-embedding-3-large` to a chat fallback map is rejected. It would retain\\n+the invalid primary assignment and merely hide it when a fallback happened to be\\n+available. Repeated provider retries are also rejected because the request is\\n+structurally unsupported, not transiently unavailable.\\n+\\n+## Residual operational action\\n+\\n+Runtime containment means an already-persisted embedding agent can no longer win\\n+chat selection or failover while stale data is being cleaned up. Durable state must\\n+still converge to the correct exact set: the provider-bootstrap slice owns stale\\n+discovered-agent withdrawal and must import the same shared classifier when rebased.\\n+Runtime rejection is defense in depth, not a substitute for deleting invalid\\n+persistent configuration.\\n+\\n+## Verification evidence\\n+\\n+`tests/test_chat_model_capability_isolation.py` reproduces the exact Azure model ID\\n+and provider/separator aliases. Together with\\n+`tests/test_chat_capability_unknown_identifiers.py`,\\n+`tests/test_chat_transport_role_separation.py`, and\\n+`tests/test_chat_passthrough_capability_isolation.py`, it verifies:\\n+\\n+- OpenAI-compatible and Bytez catalog filtering;\\n+- malformed and prefix-only identifier handling;\\n+- agent-conversion rejection;\\n+- exclusion from the price book and cheapest-agent selection;\\n+- exclusion of a high-priority stale embedding agent from synthesizer selection;\\n+- fail-closed behavior when the persisted pool contains only non-chat agents;\\n+- generated-plan reassignment away from a stale embedding agent;\\n+- exclusion from cross-agent failover;\\n+- direct and streaming `ModelClient` rejection before transport;\\n+- readiness failure with a stable non-chat code before provider access;\\n+- planner inventory and generated-plan isolation;\\n+- distinction between chat-served audio/policy models and general agent roles.\\n+- conservative unknown-identifier handling, including unrelated `vanguard` names;\\n+- endpoint-family exclusions for image-generation (`dall-e`), CLIP, and SigLIP;\\n+- normalized `/v1/responses` passthrough and pre-transport rejection of embedding models.\\n+\\n+## References\\n+\\n+BerriAI. (n.d.). *LiteLLM: Call 100+ LLMs using the OpenAI input/output format*.\\n+Retrieved August 20, 2026, from https://docs.litellm.ai/\\n+\\n+Microsoft. (n.d.). *How to switch between OpenAI and Azure OpenAI endpoints*.\\n+Microsoft Learn. Retrieved August 20, 2026, from\\n+https://learn.microsoft.com/en-us/azure/developer/ai/how-to/switching-endpoints\\n+\\n+OpenAI. (n.d.). *Data controls in the OpenAI platform: Default usage policies by\\n+endpoint*. Retrieved August 20, 2026, from\\n+https://platform.openai.com/docs/models/default-usage-policies-by-endpoint\\n+\\n+OpenAI. (n.d.). *GPT-audio model*. Retrieved August 20, 2026, from\\n+https://developers.openai.com/api/docs/models/gpt-audio\" }, { \"sha\": \"6a50017d8629cfe80f8c2fe5696e5cbd43db78d6\", \"filename\": \"docs/doctoring/inbound-request-framing.md\", \"status\": \"added\", \"additions\": 28, \"deletions\": 0, \"changes\": 28, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fdoctoring%2Finbound-request-framing.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fdoctoring%2Finbound-request-framing.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdoctoring%2Finbound-request-framing.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,28 @@\\n+# Inbound request framing doctoring\\n+\\n+## Root cause\\n+\\n+`_read_json` used `int(headers[\\\"Content-Length\\\"])` and read that value without\\n+handling unsupported transfer coding, duplicate fields, premature EOF, or\\n+a read deadline. A negative value could reach an unbounded `read(-1)`.\\n+\\n+## Implemented contract\\n+\\n+- exactly one ASCII decimal `Content-Length` is required;\\n+- unsupported `Transfer-Encoding` and every ambiguous combination fail closed;\\n+- declared size is checked before reading;\\n+- the body is read exactly and a premature EOF is rejected;\\n+- a finite request-read timeout is applied and restored;\\n+- framing failures close the connection and do not echo body/header content.\\n+\\n+## Verification\\n+\\n+```bash\\n+pytest -q tests/test_inbound_request_framing.py\\n+python -m compileall -q contextual_orchestrator\\n+git diff --check\\n+```\\n+\\n+The implementation follows HTTP/1.1 message framing and connection-management\\n+requirements in RFC 9112 (Fielding et al., 2022). It deliberately does not\\n+claim support for chunked transfer coding.\" }, { \"sha\": \"6ee39042960b7d82195ddfc539ef1d74884f8aae\", \"filename\": \"docs/kv-credentials.md\", \"status\": \"modified\", \"additions\": 7, \"deletions\": 19, \"changes\": 26, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fkv-credentials.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fkv-credentials.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fkv-credentials.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -21,8 +21,6 @@ register_credential(\\\"OPENAI_API_KEY\\\", value) # writes into the KV\\n The orchestrator resolves an agent's provider key through this seam only:\\n \\n - Remote `ModelAgent` records use `get_credential(agent.credential_name)`.\\n-- Direct `mlx://` workers are intentionally keyless and never receive a\\n- provider credential.\\n - Authenticated loopback `local://` gateways may use the separate,\\n explicitly named `ModelAgent.local_credential_key`.\\n - `ModelClient._send()` resolves the transport-specific key before building\\n@@ -50,30 +48,20 @@ string is treated as the **credential name** in the KV — it is *not* read as a\\n environment variable. `ModelAgent.credential_name` returns `api_key_env` when\\n present, otherwise `credential_key`.\\n \\n-### Direct MLX versus an authenticated local gateway\\n+### Authenticated local gateway\\n \\n-These schemes have different credential contracts:\\n+A `local://` URL denotes a provider-neutral loopback gateway. When that gateway\\n+requires bearer authentication, configure only its explicit local token name:\\n \\n ```json\\n-{ \\\"id\\\": \\\"mlx_worker\\\", \\\"model\\\": \\\"mlx-community/gemma-4-e4b-it-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:18083/v1\\\" }\\n-```\\n-\\n-The direct `mlx://` transport is a loopback-only, keyless mlx-lm server. A\\n-`credential_key` or remote `OPENAI_API_KEY` is never forwarded to it. A\\n-`local://` URL instead denotes the contextual-orchestrator loopback gateway;\\n-when that gateway requires bearer authentication, configure only its explicit\\n-local token name:\\n-\\n-```json\\n-{ \\\"id\\\": \\\"mlx_gateway\\\", \\\"model\\\": \\\"mlx-community/gemma-4-e4b-it-4bit\\\",\\n+{ \\\"id\\\": \\\"local_gateway\\\", \\\"model\\\": \\\"gateway-selected-model\\\",\\n \\\"base_url\\\": \\\"local://127.0.0.1:18084/v1\\\",\\n \\\"local_credential_key\\\": \\\"LOCAL_GATEWAY_TOKEN\\\" }\\n ```\\n \\n-The gateway owns worker template settings, so `chat_template_kwargs` is sent\\n-only to direct `mlx://` workers. Missing local gateway credentials fail closed;\\n-they do not fall back to an OpenAI credential or an unauthenticated request.\\n+The gateway owns worker-specific settings. Missing local gateway credentials\\n+fail closed; they do not fall back to an OpenAI credential or an\\n+unauthenticated request.\\n \\n ## Backends\\n \" }, { \"sha\": \"e645dab9f9f6004d831c7581b94bab1a88785f9a\", \"filename\": \"docs/library_research.md\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 2, \"changes\": 5, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Flibrary_research.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Flibrary_research.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Flibrary_research.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,6 +1,6 @@\\n # Library Research\\n \\n-The design researched existing libraries before adding code. The repository keeps the runtime dependency-free for the current lab, but the enterprise implementation target is explicit.\\n+The design researched existing libraries before adding code. Runtime dependencies are kept explicit and hash-locked; optional enterprise integrations remain separate until they carry product weight.\\n \\n ## Selected Stack\\n \\n@@ -13,12 +13,13 @@ The design researched existing libraries before adding code. The repository keep\\n | Migrations | [Alembic](https://alembic.sqlalchemy.org/) | Use for schema migration lifecycle. | Alembic is the SQLAlchemy migration tool and supports autogenerated migrations from metadata. |\\n | Database | [PostgreSQL](https://www.postgresql.org/docs/current/sql-syntax-lexical.html) | Default relational store. | PostgreSQL identifiers allow letters, digits, and underscores; the project standardizes on unquoted lower snake_case. |\\n | API contract | [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0.html) | Contract format for API review and client generation. | OAS defines a language-agnostic HTTP API description for humans and machines. |\\n+| Observability | [OpenTelemetry Python](https://opentelemetry.io/docs/languages/python/) | Use the API, SDK, and OTLP HTTP exporter for prompt-safe request/provider spans. | The standard Python API/SDK separates instrumentation from export; this repository keeps export disabled unless the injected process KV supplies an OTLP endpoint. |\\n \\n ## Ponytail Decision\\n \\n No new dependency is added until it carries real product weight:\\n \\n-- Current prototype: stdlib server, handwritten OpenAPI, static admin UI.\\n+- Current prototype: stdlib server, handwritten OpenAPI, static admin UI, and bounded OpenTelemetry instrumentation.\\n - First enterprise cut: FastAPI + React-admin + i18next + PostgreSQL + SQLAlchemy + Alembic.\\n - Do not add provider SDKs until raw OpenAI-compatible HTTP is insufficient.\\n \" }, { \"sha\": \"907b333c8e99734fe20f3b434cf0ab162e2876ba\", \"filename\": \"docs/papers/README.md\", \"status\": \"modified\", \"additions\": 63, \"deletions\": 0, \"changes\": 63, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fpapers%2FREADME.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fpapers%2FREADME.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fpapers%2FREADME.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -46,3 +46,66 @@ but not vendored here so this repository remains one deployable control plane.\\n > Citations are provided for scholarly attribution. Redistribution here relies\\n > on the arXiv non-exclusive distribution license each author granted; no\\n > GPL/AGPL-licensed material is vendored anywhere in this repository.\\n+\\n+## Adaptive reasoning and orchestration\\n+\\n+These sources govern model and reasoning-policy decisions. They are cited and\\n+linked rather than vendored because this repository does not assume that every\\n+paper permits redistribution of its PDF.\\n+\\n+- **Sakana Fugu Technical Report** — Yujin Tang et al. arXiv:2606.21228,\\n+ 2026. https://arxiv.org/abs/2606.21228\\n+ Grounds query-adaptive scaffolds over specialized agent teams. It supports\\n+ preserving modality evidence for each assigned specialist; it does not\\n+ justify selecting a model from its name.\\n+- **TRINITY: An Evolved LLM Coordinator** — Jinglue Xu, Qi Sun, Peter\\n+ Schwendeman, Stefan Nielsen, Edoardo Cetin, Yujin Tang. arXiv:2512.04695,\\n+ 2025. https://arxiv.org/abs/2512.04695\\n+ Grounds explicit Thinker, Worker, and Verifier role assignment over a\\n+ heterogeneous pool.\\n+- **Learning to Orchestrate Agents in Natural Language with the Conductor** —\\n+ Stefan Nielsen, Edoardo Cetin, Peter Schwendeman, Qi Sun, Jinglue Xu, Yujin\\n+ Tang. arXiv:2512.04388, 2025. https://arxiv.org/abs/2512.04388\\n+ Grounds targeted communication topology and natural-language subtasks. The\\n+ access list controls prior agent outputs, not removal of the source image.\\n+\\n+- **Route to Reason: Adaptive Routing for LLM and Reasoning Strategy Selection**\\n+ — Zhihong Pan, Kai Zhang, Yuze Zhao, Yupeng Han. arXiv:2505.19435, 2025.\\n+ https://arxiv.org/abs/2505.19435\\n+ Grounds joint routing of models and reasoning strategies under a budget.\\n+- **Route-and-Reason: Scaling Large Language Model Reasoning with Reinforced\\n+ Model Router** — Chenyang Shao, Xinyang Liu, Yutang Lin, Fengli Xu, Yong Li.\\n+ arXiv:2506.05901, 2025. https://arxiv.org/abs/2506.05901\\n+ Grounds decomposition and allocation across heterogeneous workers.\\n+- **Reasoning on a Budget: A Survey of Adaptive and Controllable Test-Time\\n+ Compute in LLMs** — Mohammad Ali Alomrani et al. arXiv:2507.02076, 2025.\\n+ https://arxiv.org/abs/2507.02076\\n+ Grounds the distinction between fixed effort control and adaptive effort\\n+ allocation.\\n+- **Ares: Adaptive Reasoning Effort Selection for Efficient LLM Agents** —\\n+ Jingbo Yang, Bairu Hou, Wei Wei, Yujia Bao, Shiyu Chang. arXiv:2603.07915,\\n+ 2026. https://arxiv.org/abs/2603.07915\\n+ Grounds per-step selection of the minimum sufficient effort with repeated\\n+ verification rather than a fixed effort for every step.\\n+- **Improving Factuality and Reasoning in Language Models through Multiagent\\n+ Debate** — Yilun Du, Shuang Li, Antonio Torralba, Joshua B. Tenenbaum, Igor\\n+ Mordatch. arXiv:2305.14325, 2023. https://arxiv.org/abs/2305.14325\\n+ Grounds independent proposals, multi-round debate, and final synthesis as\\n+ an optional escalation path. It does not justify treating majority vote as\\n+ proof or using debate for every request.\\n+- **Adaptive Test-Time Compute Allocation for Reasoning LLMs via Constrained\\n+ Policy Optimization** — Zhiyuan Zhai, Bingcong Li, Bingnan Xiao, Ming Li,\\n+ Xin Wang. arXiv:2604.14853, 2026. https://arxiv.org/abs/2604.14853\\n+ Grounds budget-constrained, per-input compute allocation instead of a fixed\\n+ reasoning-effort-to-worker-count mapping.\\n+\\n+## Transport references (not policy sources)\\n+\\n+The provider API documentation is used only to verify request-shape and\\n+capability compatibility. It does not select models, assign reasoning effort,\\n+or establish quality claims; those decisions remain grounded in the papers\\n+above and runtime measurement.\\n+\\n+- **OpenAI Responses API reference** — reasoning effort, output limits, and\\n+ structured output format compatibility:\\n+ https://platform.openai.com/docs/api-reference/responses\" }, { \"sha\": \"5ac7ad2eab7df442e143ef45542479857ba68543\", \"filename\": \"docs/planning/adrs/0002-explicit-local-mlx-evaluation.md\", \"status\": \"modified\", \"additions\": 7, \"deletions\": 7, \"changes\": 14, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0002-explicit-local-mlx-evaluation.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0002-explicit-local-mlx-evaluation.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0002-explicit-local-mlx-evaluation.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,7 +1,7 @@\\n ---\\n id: \\\"0002\\\"\\n title: \\\"Explicit local mlx transport and evaluation adapter\\\"\\n-status: accepted\\n+status: superseded\\n proposed_date: \\\"2026-08-10\\\"\\n accepted_date: \\\"2026-08-11\\\"\\n deciders:\\n@@ -22,13 +22,13 @@ affected_components:\\n - \\\"contextual_orchestrator/cost_router.py\\\"\\n - \\\"examples/agents.mlx.json\\\"\\n - \\\"examples/agents.local.json\\\"\\n- - \\\"tests/test_local_mlx.py\\\"\\n+ - \\\"tests/test_local_gateway.py\\\"\\n - \\\"tests/test_batch_routing.py\\\"\\n - \\\"tests/test_cost_router.py\\\"\\n - \\\"tests/test_openai_passthrough.py\\\"\\n effort: M\\n supersedes: null\\n-superseded-by: null\\n+superseded-by: \\\"0012-gateway-only-provider-contract\\\"\\n related:\\n - path: \\\"docs/planning/adrs/0001-fail-closed-model-judgment.md\\\"\\n relation: informational\\n@@ -48,7 +48,7 @@ success_criteria:\\n - metric: \\\"local provider safety\\\"\\n target: \\\"loopback-only mlx/local URL, no Authorization header, remote HTTP rejected\\\"\\n measurement_window: \\\"every local transport test run\\\"\\n- source: \\\"tests/test_local_mlx.py\\\"\\n+ source: \\\"tests/test_local_gateway.py\\\"\\n - metric: \\\"judge integration\\\"\\n target: \\\"fast-mlsirm judge reaches an injected contextual-orchestrator only\\\"\\n measurement_window: \\\"every LLM-as-a-Judge run\\\"\\n@@ -173,14 +173,14 @@ a Codex profile; its credential is never sent to the loopback mlx-lm endpoint.\\n * `contextual_orchestrator/server.py`: authenticate `/v1/models` and `/v1/responses`, proxy Responses requests, and frame streamed responses with `response.completed` and `data: [DONE]`.\\n * `examples/agents.mlx.json`: keep the minimal selected MLX worker example visible in data, not code.\\n * `examples/agents.local.json`: keep the explicit candidate registry: public contextual-orchestrator and every discovered MLX, llama.cpp, and LM Studio candidate. Do not pre-disable entries as a discovery side effect.\\n-* `tests/test_local_mlx.py`: verify direct MLX template arguments, authenticated local gateway credential separation, and fail-closed missing credentials.\\n+* `tests/test_local_gateway.py`: verify direct MLX template arguments, authenticated local gateway credential separation, and fail-closed missing credentials.\\n * `tests/test_model_judge.py`: verify structured fast-mlsirm completion requests remain on the contextual gateway adapter.\\n * `tests/test_openai_passthrough.py`: verify the Responses SSE completion contract and model discovery endpoint.\\n * Local machine configuration: keep the ChatGPT login in Codex's normal auth cache, select the built-in `openai` provider through a profile when needed, and keep the local gateway bearer token in the OS credential store.\\n \\n ## Verification\\n \\n-* `PYTHONPATH=. .venv/bin/python -m pytest -q tests/test_local_mlx.py tests/test_openai_passthrough.py` passes in the repository test environment.\\n+* `PYTHONPATH=. .venv/bin/python -m pytest -q tests/test_local_gateway.py tests/test_openai_passthrough.py` passes in the repository test environment.\\n * `GET /healthz` and authenticated `GET /v1/models` succeed on the loopback control plane; model discovery includes the public orchestrator and the complete configured candidate registry.\\n * Authenticated streamed `POST /v1/responses` contains `response.completed` and `data: [DONE]` and reaches the configured mlx-lm model.\\n * A Codex local-provider smoke returns the requested exact sentinel response through contextual-orchestrator.\\n@@ -342,7 +342,7 @@ Remove the explicit local adapter and use the mock path if the local server is u\\n * contextual_orchestrator/__main__.py\\n * examples/agents.mlx.json\\n * examples/agents.local.json\\n-* tests/test_local_mlx.py\\n+* tests/test_local_gateway.py\\n * tests/test_openai_passthrough.py\\n * fast-mlsirm/python/fast_mlsirm/llm_judge.py\\n * fast-mlsirm/tests/test_llm_judge.py\" }, { \"sha\": \"b4d06485033ae5bac25c866ab52734e5bcf14c88\", \"filename\": \"docs/planning/adrs/0004-pr-review-merge-loop.md\", \"status\": \"modified\", \"additions\": 6, \"deletions\": 0, \"changes\": 6, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0004-pr-review-merge-loop.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0004-pr-review-merge-loop.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0004-pr-review-merge-loop.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -340,6 +340,12 @@ For each repository, record branch, commit, PR URL, review result, check result,\\n | Contextual PR #109 exact head `60d9cfc9be2ce0426ed37746eb9a2768b8f3455d` produced Strix run `31831835133`/job `94869100782` with a successful zero-finding report and artifact `9231362799`, but no `evidence-binding.json`; `run.json` contained only a local temporary target and no repository, PR head, job, report path, or digest binding. The report digest was `33a47fa5855600b393d92c3a1a77e0ac15adb99a55406f99e5b2efba93c17c18`, and the same-head publish step was skipped. | Keep the success as provider/content evidence only, not a clean exact-head security or Merge result. Require the central trusted workflow to publish a structured binding for repository, full PR head, run/job, report path, and digest; reject unbound success and re-run the exact head after protected-main workflow integration. | Reproduced 2026-08-15; central provenance dependency and protected Merge remain open |\\n | The contextual exact-head code-scanning checks `Trivy` and `Scorecard` both became `neutral` because the PR's `security.yml` configuration was absent from protected `main` (`trivy-filesystem` and `supply-chain/branch-protection`), while the separate required `trivy-fs`/`scorecard` jobs were still pending or successful. | Keep the organization's CodeQL-only code-scanning rule unchanged; distinguish code-scanning alert comparison from required Security job results, record the missing configuration as a warning, and never treat neutral, skipped, or absent results as a pass. Re-audit after trusted workflow/ruleset integration. | Observed 2026-08-15; no local source bypass, governance/central workflow follow-up required |\\n | Fast PR #816 exact head `355f93b27ba4a0cb141e86b0fbc9127681edb750` produced Strix run `31833030282`/job `94872991843`; NVIDIA NIM failed with `agents.exceptions.ModelBehaviorError: Tool execute not found in agent strix`, no penetration-test report was produced, and the required check failed closed after 599 seconds. | Keep this as provider/model-tool-contract evidence, not a source vulnerability or clean security result. Preserve the failure denominator, do not publish a status-only success or retry to hide it, and require central trusted workflow/provider repair plus a structured same-head artifact before normal Merge. | Observed 2026-08-15; central Strix dependency and protected Merge remain open |\\n+| Post-merge review of PR #813 found that every budget-gated provider call reached `budget_status()` through the buyer-facing `spend_analytics()` aggregate, deep-copying archived spend and rescanning up to 128 retained runs even though ADR 0014 already requires a synchronized incremental process meter. | Make `budget_status()` read only the locked incremental meter and retain full run/model aggregation in `spend_analytics()`. Preserve current token/cost rounding and fail-closed limit semantics, and regression-test that the enforcement path never calls the aggregate. Keep per-call durable meter checkpoints because ADR 0014 explicitly retains failed-workflow spend across restarts. | Issue #814 implemented locally; 1,727 tests and 100% changed-line branch coverage passed, exact-head review/check evidence remains required |\\n+| PR #765 review found `host.docker.internal` classified as a `local://` provider even though the egress validator correctly rejects its usual non-loopback Docker gateway address. | Keep authenticated local transport loopback-only under ADR 0012, remove the unreachable host classification, and reject non-loopback `local://` configuration at `ModelAgent` construction while retaining the DNS-resolution check against rebinding. | Decision recorded 2026-08-21; implementation and exact-head review/check evidence follow |\\n+| PR #765 review found that auxiliary temperature-capability inspection could let `http.client.IncompleteRead` replace the provider's original HTTP error while reading its diagnostic body. | Treat an incomplete diagnostic body as insufficient capability evidence, preserve the original HTTP error for the caller, and cover the exact truncated-body branch without broad retry or fallback changes. | Decision recorded 2026-08-21; implementation and exact-head review/check evidence follow |\\n+| PR #765 review found that the HTTP tool loop passed the orchestrator-owned literal `reasoning_effort=auto` to a selected provider. | Strip accepted reasoning-effort values at the shared provider-payload boundary while no capability plane exists; provider-native levels may be forwarded later only after the selected agent advertises support under ADR 0013. Cover structured multi-agent and explicit single-agent tool-loop calls with the same regression. | Implemented locally 2026-08-21; focused tests pass and exact-head review/check evidence follows |\\n+| PR #765 review found that one-shot local failover bypassed the request-scoped output-token cap used by the ordinary transport path. | Reuse the existing request-setting lookup for local Responses translation and add a non-mutating `setdefault` for local Chat Completions so an explicit caller cap remains authoritative. | Implemented locally 2026-08-21; focused default/explicit cap tests pass and exact-head review/check evidence follows |\\n+| PR #765 regenerated a host-specific hash lock that omitted conditional Windows and native SQLAlchemy dependencies. | Generate the existing `requirements.lock` with uv universal resolution, retain PEP 508 markers and hashes, and fail repository metadata tests if universal mode or `colorama`, `greenlet`, or `tzdata` disappears; ADR 0025 records the format decision. | Implemented locally 2026-08-21; universal solve and hash-locked dry-run pass, exact-head review/check evidence follows |\\n \\n ## Risks and Mitigations\\n \" }, { \"sha\": \"bd40526e7c1fb24376c4983b10844fcf32cbf530\", \"filename\": \"docs/planning/adrs/0007-sast-transport-and-sql-hardening.md\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 4, \"changes\": 8, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0007-sast-transport-and-sql-hardening.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0007-sast-transport-and-sql-hardening.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0007-sast-transport-and-sql-hardening.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,7 +1,7 @@\\n ---\\n id: \\\"0007\\\"\\n title: \\\"Harden provider transport and SQL ledger against scanner findings\\\"\\n-status: accepted\\n+status: superseded\\n proposed_date: \\\"2026-08-11\\\"\\n accepted_date: \\\"2026-08-11\\\"\\n deciders:\\n@@ -16,11 +16,11 @@ affected_components:\\n - \\\"contextual_orchestrator/cost_ledger.py\\\"\\n - \\\"contextual_orchestrator/__main__.py\\\"\\n - \\\"tests/test_provider_tls.py\\\"\\n- - \\\"tests/test_local_mlx.py\\\"\\n+ - \\\"tests/test_local_gateway.py\\\"\\n - \\\"tests/test_cost_ledger.py\\\"\\n effort: M\\n supersedes: null\\n-superseded-by: null\\n+superseded-by: \\\"0012-gateway-only-provider-contract\\\"\\n related:\\n - path: \\\"docs/planning/adrs/0002-explicit-local-mlx-evaluation.md\\\"\\n relation: influences\\n@@ -154,5 +154,5 @@ reintroducing general urllib URL handling.\\n * contextual_orchestrator/cost_ledger.py\\n * contextual_orchestrator/__main__.py\\n * tests/test_provider_tls.py\\n-* tests/test_local_mlx.py\\n+* tests/test_local_gateway.py\\n * tests/test_cost_ledger.py\" }, { \"sha\": \"22e82308105e18c47eb54aa881a1bc571863c73c\", \"filename\": \"docs/planning/adrs/0011-structured-provider-features-stay-orchestrated.md\", \"status\": \"added\", \"additions\": 141, \"deletions\": 0, \"changes\": 141, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0011-structured-provider-features-stay-orchestrated.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0011-structured-provider-features-stay-orchestrated.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0011-structured-provider-features-stay-orchestrated.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,141 @@\\n+---\\n+id: \\\"0011\\\"\\n+title: \\\"Keep structured provider features inside multi-agent orchestration\\\"\\n+status: accepted\\n+proposed_date: \\\"2026-08-21\\\"\\n+accepted_date: \\\"2026-08-21\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"LineageWeave integration\\\"\\n+ - \\\"contextual-orchestrator runtime\\\"\\n+informed:\\n+ - \\\"API consumers\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/orchestrator.py\\\"\\n+ - \\\"contextual_orchestrator/server.py\\\"\\n+ - \\\"tests/test_openai_passthrough.py\\\"\\n+ - \\\"tests/test_model_judge.py\\\"\\n+effort: M\\n+supersedes: null\\n+superseded-by: null\\n+related:\\n+ - path: \\\"docs/planning/adrs/0001-fail-closed-model-judgment.md\\\"\\n+ relation: constrains\\n+ - path: \\\"docs/planning/adrs/0002-explicit-local-mlx-evaluation.md\\\"\\n+ relation: extends\\n+ - path: \\\"docs/architecture.md\\\"\\n+ relation: implements\\n+ - path: \\\"docs/planning/adrs/0014-gateway-owned-model-selection.md\\\"\\n+ relation: constrained-by\\n+success_criteria:\\n+ - metric: \\\"structured requests using the multi-agent workflow\\\"\\n+ target: \\\"100% of client-facing non-null response_format and Responses requests\\\"\\n+ measurement_window: \\\"every structured-output regression run\\\"\\n+ source: \\\"tests/test_openai_passthrough.py\\\"\\n+ - metric: \\\"Responses json_schema translation\\\"\\n+ target: \\\"Responses text.format json_schema reaches the final provider as the equivalent Chat response_format without losing the schema\\\"\\n+ measurement_window: \\\"every Responses structured-output regression run\\\"\\n+ source: \\\"tests/test_openai_passthrough.py\\\"\\n+ - metric: \\\"multimodal context preservation\\\"\\n+ target: \\\"image_url input remains available to the final synthesis request\\\"\\n+ measurement_window: \\\"every multimodal structured-output regression run\\\"\\n+ source: \\\"tests/test_openai_passthrough.py\\\"\\n+---\\n+\\n+# Keep structured provider features inside multi-agent orchestration\\n+\\n+## Context\\n+\\n+The OpenAI-compatible boundary previously treated `response_format` and the\\n+Responses API as a provider passthrough. That made a request look\\n+successful while skipping the Thinker/Worker/Verifier/Synthesizer workflow.\\n+Structured output and multimodal requests are still product work, not an\\n+exception to the orchestration contract. A consumer must receive the same\\n+workflow evidence, verification boundary, session lineage, and cost accounting\\n+as a plain chat request.\\n+\\n+## Decision\\n+\\n+1. A non-null structured-output contract or Responses request is an\\n+ orchestration trigger, never a silent single-agent downgrade.\\n+2. The request enters the existing conducted workflow. Intermediate steps use\\n+ the original messages, including multimodal content, and the final\\n+ synthesizer performs the provider-facing structured completion.\\n+3. The final provider payload preserves validated tools and structured-output\\n+ fields. A Responses request remains a Responses request at the final\\n+ provider boundary when the selected provider supports it; a local provider\\n+ may perform an explicit transport-level translation when its capability\\n+ boundary requires Chat Completions. Responses `text.format` therefore stays\\n+ native for Responses-capable providers rather than being silently downgraded.\\n+4. `json_object` and `json_schema` are both first-class structured workflows.\\n+ Schema validation remains fail-closed at the HTTP boundary; a provider\\n+ success is not treated as semantic schema validity.\\n+5. The workflow response exposes bounded orchestration metadata by default.\\n+ Prompts, answers, images, tool arguments, secrets, and unbounded raw traces\\n+ are not put into telemetry or the default response.\\n+6. Tool execution loops are not fabricated by this decision. Per ADR 0014,\\n+ clients must opt into the explicit client-owned `v1` tool-loop contract;\\n+ ordinary tool declarations fail closed, while the opted-in provider-shape\\n+ call remains a single-worker exception.\\n+7. The internal fail-closed LLM-as-a-Judge call remains one bounded,\\n+ schema-constrained provider request. It uses the orchestrator's existing\\n+ provider transport but never recursively starts another conducted workflow.\\n+\\n+## Research basis\\n+\\n+This decision applies the existing research-grounded architecture rather than\\n+inventing a provider-specific exception:\\n+\\n+- Fugu distinguishes a low-latency routed call from a quality-oriented deep\\n+ workflow and keeps the worker pool configurable.\\n+- TRINITY supplies the Thinker, Worker, and Verifier role boundary used before\\n+ synthesis.\\n+- Conductor supplies explicit workflow steps and access-controlled context.\\n+\\n+The canonical references are maintained in `docs/architecture.md` and\\n+`docs/papers/README.md`. OpenAI's Chat Completions and Responses API contracts\\n+define the wire-shape translation, not the orchestration policy.\\n+\\n+## Consequences\\n+\\n+* Good: structured and multimodal requests no longer bypass verification and\\n+ orchestration evidence.\\n+* Good: JSON object and JSON schema requests share one tested policy instead of\\n+ diverging into transport-specific single-agent paths.\\n+* Good: Responses-only providers receive their native endpoint and input shape\\n+ after the multi-agent workflow.\\n+* Good: the final provider retains the capability fields it must interpret.\\n+* Good: internal verification does not recursively multiply provider calls or\\n+ replace the judge verdict with an unrelated synthesized answer.\\n+* Bad: structured requests consume more provider calls and can take longer than\\n+ a plain routed request.\\n+* Bad: tool execution remains a distinct explicit client-owned contract rather\\n+ than being implied by merely forwarding a tool declaration.\\n+\\n+## Confirmation\\n+\\n+Run the focused structured-output and orchestration tests. Confirm that the\\n+Responses JSON-schema test preserves the schema, the structured-output test\\n+reports the conducted workflow, the multimodal test retains `image_url` through final\\n+synthesis, the explicit tool-loop test preserves its single-worker response,\\n+and the internal structured judge test performs exactly one provider call. Do\\n+not claim provider-side semantic schema validity from HTTP 200.\\n+\\n+## References\\n+\\n+Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025).\\n+*Learning to orchestrate agents in natural language with the Conductor*.\\n+https://doi.org/10.48550/arXiv.2512.04388\\n+\\n+Sakana AI. (2026, June 22). *Sakana Fugu: One model to command them all*.\\n+https://sakana.ai/fugu-release/\\n+\\n+Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025).\\n+*Trinity: An evolved LLM coordinator*. https://doi.org/10.48550/arXiv.2512.04695\\n+\\n+OpenAI. (n.d.-a). *Create chat completion*. OpenAI Platform.\\n+https://platform.openai.com/docs/api-reference/chat/create\\n+\\n+OpenAI. (n.d.-b). *Create a model response*. OpenAI Platform.\\n+https://platform.openai.com/docs/api-reference/responses/create\" }, { \"sha\": \"3adc1b70801414d66160504273790d5e2a3d7275\", \"filename\": \"docs/planning/adrs/0012-gateway-only-provider-contract.md\", \"status\": \"added\", \"additions\": 61, \"deletions\": 0, \"changes\": 61, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0012-gateway-only-provider-contract.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0012-gateway-only-provider-contract.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0012-gateway-only-provider-contract.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,61 @@\\n+---\\n+id: \\\"0012\\\"\\n+title: \\\"Gateway-only provider contract; no direct MLX transport\\\"\\n+status: accepted\\n+proposed_date: \\\"2026-08-20\\\"\\n+accepted_date: \\\"2026-08-20\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"contextual-orchestrator provider capability contract\\\"\\n+ - \\\"paper-grounded model routing policy\\\"\\n+informed:\\n+ - \\\"LineageWeave\\\"\\n+ - \\\"fast-mlsirm\\\"\\n+ - \\\"contributors\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/orchestrator.py\\\"\\n+ - \\\"contextual_orchestrator/model_discovery.py\\\"\\n+ - \\\"contextual_orchestrator/__main__.py\\\"\\n+ - \\\"examples/agents.local.json\\\"\\n+ - \\\"docs/kv-credentials.md\\\"\\n+supersedes: \\\"0002-explicit-local-mlx-evaluation\\\"\\n+superseded-by: null\\n+effort: M\\n+---\\n+\\n+# Gateway-only provider contract; no direct MLX transport\\n+\\n+## Context\\n+\\n+The orchestrator is the model routing and orchestration boundary. A direct\\n+runtime-specific `mlx://` worker contract leaks one local inference runtime\\n+into the public agent schema, CLI, credential rules, and Responses-to-Chat\\n+adaptation. It also creates provider-specific controls that cannot be applied\\n+to other models or gateways.\\n+\\n+## Decision\\n+\\n+- The public worker contract is provider-neutral: `mock://` for tests,\\n+ `https://` for remote providers, and authenticated `local://` only for a\\n+ reviewed loopback gateway.\\n+- Direct `mlx://` agents are rejected at `ModelAgent` construction. No MLX\\n+ runtime, model-template setting, or keyless direct transport is part of the\\n+ orchestrator contract.\\n+- A local gateway owns downstream model selection and runtime-specific\\n+ settings. The orchestrator sends only the negotiated provider-neutral\\n+ request shape and the explicitly named local gateway credential.\\n+- Model selection and reasoning policy remain capability- and paper-driven;\\n+ they must not infer a provider from a model name or hard-code MLX behavior.\\n+\\n+## Consequences\\n+\\n+- LineageWeave and other callers can use one gateway boundary without a direct\\n+ local-model dependency or monkey patch.\\n+- Existing local gateway concurrency and Responses/Chat compatibility remain\\n+ available because they are transport capabilities, not MLX behavior.\\n+- Historical MLX benchmark artifacts remain for provenance but are not current\\n+ configuration guidance or a supported public transport.\\n+- Operators who previously configured `mlx://` must place the runtime behind\\n+ an authenticated OpenAI-compatible gateway and configure `local://` or\\n+ `https://` accordingly.\" }, { \"sha\": \"dc6402b27ae8518361bf189f2d27e975eec5b803\", \"filename\": \"docs/planning/adrs/0013-paper-grounded-adaptive-reasoning-policy.md\", \"status\": \"added\", \"additions\": 108, \"deletions\": 0, \"changes\": 108, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0013-paper-grounded-adaptive-reasoning-policy.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0013-paper-grounded-adaptive-reasoning-policy.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0013-paper-grounded-adaptive-reasoning-policy.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,108 @@\\n+---\\n+id: \\\"0013\\\"\\n+title: \\\"Paper-grounded adaptive reasoning and model capability policy\\\"\\n+status: accepted\\n+proposed_date: \\\"2026-08-20\\\"\\n+accepted_date: \\\"2026-08-20\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"Route to Reason: Adaptive Routing for LLM and Reasoning Strategy Selection\\\"\\n+ - \\\"Route-and-Reason: Scaling Large Language Model Reasoning with Reinforced Model Router\\\"\\n+ - \\\"Reasoning on a Budget: A Survey of Adaptive and Controllable Test-Time Compute in LLMs\\\"\\n+ - \\\"Ares: Adaptive Reasoning Effort Selection for Efficient LLM Agents\\\"\\n+ - \\\"Improving Factuality and Reasoning in Language Models through Multiagent Debate\\\"\\n+ - \\\"Adaptive Test-Time Compute Allocation for Reasoning LLMs via Constrained Policy Optimization\\\"\\n+informed:\\n+ - \\\"LineageWeave\\\"\\n+ - \\\"fast-mlsirm\\\"\\n+ - \\\"contributors\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/orchestrator.py\\\"\\n+ - \\\"contextual_orchestrator/server.py\\\"\\n+ - \\\"contextual_orchestrator/model_discovery.py\\\"\\n+ - \\\"tests/test_openai_passthrough.py\\\"\\n+ - \\\"tests/test_request_metadata.py\\\"\\n+ - \\\"docs/architecture.md\\\"\\n+supersedes: null\\n+superseded-by: null\\n+related:\\n+ - path: \\\"docs/planning/adrs/0012-gateway-only-provider-contract.md\\\"\\n+ relation: extends\\n+effort: M\\n+---\\n+\\n+# Paper-grounded adaptive reasoning and model capability policy\\n+\\n+## Context\\n+\\n+The public API may receive a requested reasoning level such as `auto`,\\n+`medium`, `high`, or `xhigh`, while the selected provider may support a\\n+different subset of values or no reasoning control at all. A fixed value is\\n+also a poor policy for multi-step work: easy steps can be over-computed and\\n+hard steps can be under-computed. The repository must therefore distinguish\\n+the caller's policy from the provider wire value and must not make model\\n+decisions from model-name folklore.\\n+\\n+## Decision\\n+\\n+- Model selection, reasoning-effort allocation, orchestration topology, and\\n+ quality/cost claims are decided from cited academic papers plus current\\n+ runtime capability and measurement evidence. Vendor documentation defines\\n+ wire compatibility; it does not define this repository's model policy.\\n+- `auto` is an orchestrator-only policy. The orchestrator evaluates task/step\\n+ difficulty, capability advertisements, budget, latency constraints, and\\n+ required verification, then chooses a provider-supported effort or a\\n+ multi-agent workflow. The literal value `auto` is never sent upstream.\\n+- Explicit effort values are forwarded only when the selected provider\\n+ advertises them. `none` is not a universal synonym for a non-reasoning\\n+ model; if the provider does not advertise a requested value, the\\n+ orchestrator negotiates another supported path or fails clearly.\\n+- `high` and `xhigh` are outcome policies, not promises that one worker has a\\n+ particular hidden-thinking implementation. When appropriate, the\\n+ orchestrator may use heterogeneous workers, independent attempts,\\n+ verification, and synthesis. Traces must record the effective strategy and\\n+ must not label a non-reasoning worker as a reasoning model.\\n+- Multi-agent debate is an available escalation strategy, not a mandatory\\n+ replacement for one worker. The orchestrator may select independent\\n+ proposals, debate, verification, and synthesis when task difficulty and the\\n+ budget justify it; otherwise it may use one capable worker with the same\\n+ evidence and trace contract. A debate result is not accepted by majority\\n+ vote alone: the final synthesis must retain source attribution and pass the\\n+ requested output contract.\\n+- Adaptive compute allocation is evaluated as a constrained policy. The\\n+ orchestrator must spend additional attempts or verification where the\\n+ expected quality gain justifies the cost, rather than mapping `low`,\\n+ `medium`, `high`, or `xhigh` to fixed worker counts or a vendor model name.\\n+- No direct MLX transport or MLX-specific model policy is permitted. Local\\n+ runtimes remain behind the authenticated provider-neutral gateway boundary\\n+ in ADR 0012.\\n+\\n+## Evidence contract\\n+\\n+Every change to routing or reasoning policy must cite the relevant sources in\\n+`docs/papers/README.md`, add or update a regression test for the capability\\n+boundary, and report requested versus effective effort in the trace or\\n+metadata. A provider health result alone is not evidence of reasoning quality.\\n+\\n+Transport compatibility is checked against the current provider API contract,\\n+not treated as model-policy evidence. In particular, Responses API capability\\n+checks may cover supported reasoning-effort values and `json_schema` structured\\n+outputs; Chat Completions compatibility must negotiate its system-message and\\n+structured-output equivalent separately. These checks must never turn a vendor\\n+default into this repository's reasoning policy.\\n+\\n+## Consequences\\n+\\n+- Callers can request `auto` without coupling themselves to provider-specific\\n+ effort names.\\n+- Unsupported effort values cannot leak to providers or silently become a\\n+ different model behavior.\\n+- Adaptive multi-agent execution is measurable as orchestration, rather than\\n+ being misrepresented as a provider's native reasoning capability.\\n+- Structured-output requests, including `json_schema`, stay inside the same\\n+ multi-agent workflow. Unsupported worker-format capabilities are negotiated\\n+ or rejected; they must not silently downgrade the request to a single-agent\\n+ passthrough.\\n+- Historical MLX benchmark and transport ADRs remain available as provenance,\\n+ but they are not supported configuration guidance.\" }, { \"sha\": \"d399c8b1de1b975975e48274232bc8fe83b941b5\", \"filename\": \"docs/planning/adrs/0014-gateway-owned-model-selection.md\", \"status\": \"added\", \"additions\": 104, \"deletions\": 0, \"changes\": 104, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0014-gateway-owned-model-selection.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0014-gateway-owned-model-selection.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0014-gateway-owned-model-selection.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,104 @@\\n+---\\n+id: \\\"0014\\\"\\n+title: \\\"Gateway-owned model selection and multi-agent structured output\\\"\\n+status: accepted\\n+proposed_date: \\\"2026-08-20\\\"\\n+accepted_date: \\\"2026-08-20\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"gateway-only provider contract\\\"\\n+ - \\\"paper-grounded adaptive reasoning policy\\\"\\n+informed:\\n+ - \\\"LineageWeave\\\"\\n+ - \\\"fast-mlsirm\\\"\\n+ - \\\"contributors\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/__main__.py\\\"\\n+ - \\\"contextual_orchestrator/cost_router.py\\\"\\n+ - \\\"contextual_orchestrator/orchestrator.py\\\"\\n+ - \\\"contextual_orchestrator/server.py\\\"\\n+supersedes: null\\n+superseded-by: null\\n+related:\\n+ - path: \\\"docs/planning/adrs/0012-gateway-only-provider-contract.md\\\"\\n+ relation: depends-on\\n+ - path: \\\"docs/planning/adrs/0013-paper-grounded-adaptive-reasoning-policy.md\\\"\\n+ relation: implements\\n+effort: M\\n+---\\n+\\n+# ADR 0014: Gateway-owned model selection and multi-agent structured output\\n+\\n+- Status: Accepted\\n+- Date: 2026-08-20\\n+\\n+## Context\\n+\\n+Consumers using contextual-orchestrator must not select a provider model by\\n+copying `LLM_GATEWAY_MODEL` into every application. The configured gateway\\n+exposes its model registry, and the orchestrator owns routing, reasoning effort,\\n+and cost attribution. A request carrying JSON output constraints or the\\n+Responses API must not silently downgrade to a single provider call merely\\n+because the provider response shape is richer.\\n+\\n+## Decision\\n+\\n+- An omitted chat or Responses `model` is represented by the virtual\\n+ `contextual-orchestrator` model and is resolved inside the orchestrator.\\n+- `reasoning_effort=auto` (or Responses `reasoning.effort=auto`) is an\\n+ orchestrator-only policy value and is never forwarded as a provider field.\\n+ Provider-native levels are forwarded only after the selected agent declares\\n+ that capability; support is never inferred from a model name. If no selected\\n+ provider declares the requested level, the gateway rejects the request rather\\n+ than silently falling back to a different effort.\\n+- `--auto-discover-model-agents` expands an empty seed agent from its configured\\n+ HTTPS `/models` endpoint. Embedding-only registry rows are excluded from the\\n+ chat pool. Consumers provide only the gateway URL and credential.\\n+- `json_object`, `json_schema`, and Responses text JSON formats force the\\n+ conduct workflow. The final synthesis receives the original provider-native\\n+ output contract, and the gateway independently validates the resulting JSON\\n+ locally before returning it. A Chat request therefore keeps\\n+ `response_format`, while a Responses request keeps `text.format`, at the\\n+ final provider boundary.\\n+- Tool-loop requests are explicitly passed to one selected worker agent. The\\n+ gateway preserves the provider's full tool-call response and the client owns\\n+ execution of the returned function calls; they do not claim a multi-agent\\n+ synthesis trace. Streaming tool loops are rejected until the gateway has a\\n+ provider-shape-preserving streaming relay. Clients must opt in with the\\n+ `X-Contextual-Orchestrator-Tool-Loop: v1` header; ordinary tool requests stay\\n+ fail-closed until that contract is explicitly selected.\\n+- Each provider-reported call in a conducted workflow writes its own cost-ledger\\n+ record under the shared workflow run id, including the model judge and final\\n+ provider synthesis. The response retains one last-metered-call\\n+ `usage_record_id` for compatibility and adds the complete `usage_record_ids`\\n+ list. Calls without\\n+ valid provider usage increment `unmetered_provider_call_count`; if no workflow\\n+ call reports usage, the existing request-level estimate remains the explicit\\n+ compatibility fallback.\\n+- Raw in-memory workflow records share the existing bounded recent-run capacity.\\n+ Evicted records contribute to a compact per-model spend accumulator, so budget\\n+ enforcement and spend totals remain cumulative without retaining every prompt,\\n+ answer, or trace in process memory. A configured durable state store remains\\n+ the long-term run-evidence boundary.\\n+- Every completed provider call adds reported output usage (or the bounded\\n+ estimate when unavailable) to a synchronized process budget ledger before the\\n+ next planner, worker, verifier, judge, or synthesizer call. Failed workflows\\n+ therefore retain consumed spend even when no completed run can be persisted;\\n+ the configured state store checkpoints this compact meter across restarts.\\n+\\n+## Consequences\\n+\\n+- Provider model selection remains centralized and can change with the registry\\n+ without an application rebuild.\\n+- Structured output retains the multi-agent trace and cannot bypass synthesis.\\n+- Cost reports price each metered workflow call against the model that served it\\n+ instead of attributing all conducted work to the final synthesizer.\\n+- A response never sums monetary amounts across currencies; mixed-currency\\n+ workflows expose `currency_code=MIXED` and a null aggregate amount while the\\n+ individual ledger records retain their original amounts and currencies.\\n+- High-volume passthrough traffic cannot grow raw workflow memory without bound,\\n+ while evicted usage still contributes to buyer-visible spend and budget gates.\\n+- Tool callers use an explicit single-agent passthrough contract. The gateway\\n+ remains the model-selection boundary, while tool execution stays with the\\n+ authenticated client and never becomes an implicit multi-agent fallback.\" }, { \"sha\": \"67280b86630213acb9d870a4cff3d75c9167ba84\", \"filename\": \"docs/planning/adrs/0015-auto-embedding-model-selection.md\", \"status\": \"added\", \"additions\": 110, \"deletions\": 0, \"changes\": 110, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0015-auto-embedding-model-selection.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0015-auto-embedding-model-selection.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0015-auto-embedding-model-selection.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,110 @@\\n+---\\n+id: \\\"0015\\\"\\n+title: \\\"Orchestrator-owned automatic embedding model selection\\\"\\n+status: proposed\\n+proposed_date: \\\"2026-08-20\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"contextual-orchestrator gateway runtime\\\"\\n+ - \\\"downstream embedding consumers\\\"\\n+informed:\\n+ - \\\"downstream consumers (naruon, LineageWeave)\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/orchestrator.py\\\"\\n+ - \\\"contextual_orchestrator/server.py\\\"\\n+ - \\\"contextual_orchestrator/api_contract.py\\\"\\n+ - \\\"contextual_orchestrator/batch_routing.py\\\"\\n+ - \\\"contextual_orchestrator/cost_router.py\\\"\\n+ - \\\"tests/test_provider_embeddings.py\\\"\\n+ - \\\"tests/test_embeddings_model_pool_http_honesty.py\\\"\\n+effort: S\\n+supersedes: null\\n+superseded-by: null\\n+related:\\n+ - path: \\\"docs/planning/adrs/0001-fail-closed-model-judgment.md\\\"\\n+ relation: constrains\\n+ - path: \\\"docs/planning/adrs/0012-gateway-only-provider-contract.md\\\"\\n+ relation: depends-on\\n+---\\n+\\n+# ADR 0015: Orchestrator-owned automatic embedding model selection\\n+\\n+## Context\\n+\\n+Consumers currently have to send a model name to the embeddings endpoints. A\\n+consumer that already delegates model selection to contextual-orchestrator\\n+must then invent a sentinel model name or maintain provider-specific\\n+configuration. That contradicts the gateway-owned model policy and makes the\\n+OpenAI-compatible contract less useful for downstream services.\\n+\\n+Embedding agents are already represented in the orchestrator candidate pool by\\n+the explicit `embedding` capability tag. The selection must therefore reuse\\n+the existing ranked-agent policy rather than add a provider order, model-name\\n+guess, or consumer-side fallback.\\n+\\n+## Decision\\n+\\n+1. `/v1/embeddings` and `/v1/batch/embeddings` accept an omitted `model`.\\n+2. When omitted, the gateway selects the highest-ranked enabled agent carrying\\n+ the `embedding` capability. Ranking continues to use the existing priority\\n+ and capability policy; disabled agents and provider exclusions are ignored.\\n+3. An explicitly supplied model remains supported only when it matches an\\n+ enabled embedding-capable agent. Unknown, disabled, or non-embedding models\\n+ fail closed with the existing invalid-model contract.\\n+4. If no enabled embedding-capable agent exists for an omitted model, the\\n+ gateway returns `503 embedding_unavailable`; it never invents a model or\\n+ produces a heuristic vector as a provider substitute.\\n+5. The provider and resolved model are carried into internal batch requests,\\n+ provider JSONL, response metadata, and cost attribution so the selected\\n+ deployment remains deterministic and auditable. Client attribution metadata\\n+ cannot override either server-resolved identity. The standalone in-process\\n+ backend remains a local test/development path; a configured provider path\\n+ uses its injected embeddings backend and the resolved model.\\n+6. The default batch backend resolves the current agent pool at submission time.\\n+ Runtime additions, disablement, and priority changes are therefore visible to\\n+ new jobs; every submitted job retains the backend instance that created it for\\n+ deterministic polling and retrieval.\\n+\\n+## Contract and acceptance evidence\\n+\\n+The OpenAPI contract marks `model` optional and documents the unavailable\\n+response. Loopback HTTP tests cover omitted-model selection for sync and batch\\n+requests, explicit pool validation, and the no-capability failure. Provider\\n+backend contract tests must preserve the resolved model in every serialized\\n+embedding request before this ADR moves from proposed to accepted.\\n+\\n+## Consequences\\n+\\n+LineageWeave, naruon, and other consumers can omit provider model selectors\\n+while retaining pool validation, provider routing, and cost attribution.\\n+Explicit OpenAI-compatible model requests remain backward compatible. The\\n+gateway still exposes a clear distinction between local standalone evidence\\n+and configured-provider evidence; local heuristic vectors are not production\\n+provider evidence.\\n+\\n+## Research grounding\\n+\\n+The selection is a capability-constrained routing decision, not a semantic\\n+quality judgment. It reuses the repository's vendored routing literature:\\n+\\n+* Chen, L., Zaharia, M., & Zou, J. (2023). FrugalGPT: How to use large\\n+ language models while reducing cost and improving performance. *arXiv*.\\n+ https://arxiv.org/abs/2305.05176\\n+* Ong, I., Almahairi, A., Wu, V., Chiang, W.-L., Wu, T., Gonzalez, J. E.,\\n+ Kadous, M. W., & Stoica, I. (2024). RouteLLM: Learning to route LLMs with\\n+ preference data. *arXiv*. https://arxiv.org/abs/2406.18665\\n+* Ding, D., Mallick, A., Wang, C., Sim, R., Mukherjee, S., Rühle, V.,\\n+ Lakshmanan, L. V. S., & Awadallah, A. H. (2024). Hybrid LLM:\\n+ Cost-efficient and quality-aware query routing. *International Conference\\n+ on Learning Representations*. https://arxiv.org/abs/2404.14618\\n+\\n+These papers ground cost-aware and capability-aware routing decisions; they do\\n+not provide evidence that one embedding model is universally higher quality.\\n+No such unsupported quality claim is made by this ADR.\\n+\\n+## More information\\n+\\n+* docs/papers/README.md\\n+* docs/rest_api_design.md\\n+* docs/planning/adrs/0001-fail-closed-model-judgment.md\" }, { \"sha\": \"f3572403548fad1cb22fab5bdfa3fd5151c19608\", \"filename\": \"docs/planning/adrs/0016-optional-sampling-capability-negotiation.md\", \"status\": \"added\", \"additions\": 48, \"deletions\": 0, \"changes\": 48, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0016-optional-sampling-capability-negotiation.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0016-optional-sampling-capability-negotiation.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0016-optional-sampling-capability-negotiation.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,48 @@\\n+# ADR 0016: Optional sampling capability negotiation\\n+\\n+- Status: Accepted\\n+- Date: 2026-08-20\\n+\\n+## Context\\n+\\n+Some provider deployments reject the optional `temperature` request field even\\n+when the value is valid for the public API contract. A provider response that\\n+only reports an invalid value must not be silently changed into a different\\n+request. The same transport boundary serves chat completions and raw Responses\\n+passthrough, so the behavior must be endpoint-local and provider-neutral.\\n+\\n+## Decision\\n+\\n+When a provider returns HTTP 400 or 422 and the response evidence explicitly\\n+identifies `temperature` as unsupported, the orchestrator retries once against\\n+the same endpoint with only `temperature` removed. This negotiation is\\n+available to both the normal chat transport and raw/Responses passthrough.\\n+\\n+When neither the caller nor the operator supplies a sampling temperature, the\\n+runtime leaves the field absent so the selected provider applies its published\\n+default. The CLI compatibility flags remain an explicit operator override.\\n+HTTP request sampling and output-token controls are stored in thread-local\\n+request scope; concurrent requests never mutate the shared client defaults or\\n+inherit one another's controls.\\n+\\n+All other 4xx responses, including invalid temperature values, remain\\n+non-retryable. The orchestrator does not infer capability from a model name,\\n+provider ordering, parameter count, or local benchmark, and it does not select\\n+another model as a temperature fallback.\\n+\\n+## Consequences\\n+\\n+- GPT-5-family or otherwise restricted deployments can answer when the only\\n+ incompatibility is an optional sampling field.\\n+- The original endpoint, model, authentication, and all other request fields\\n+ remain unchanged.\\n+- Concurrent Completions, Chat Completions, Responses, streaming, and batch\\n+ transports apply only their own request-scoped controls.\\n+- The provider error body is consumed only for bounded capability\\n+ classification; it is not persisted or exposed as a credential-bearing log.\\n+\\n+## Verification\\n+\\n+`tests/test_provider_integration.py` covers successful chat negotiation,\\n+invalid-value non-negotiation, and raw Responses negotiation over a real local\\n+HTTP server.\" }, { \"sha\": \"500e834e82baa2b5b2dc9f96f2509135153b944c\", \"filename\": \"docs/planning/adrs/0017-inbound-request-framing.md\", \"status\": \"added\", \"additions\": 50, \"deletions\": 0, \"changes\": 50, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0017-inbound-request-framing.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0017-inbound-request-framing.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0017-inbound-request-framing.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,50 @@\\n+---\\n+status: proposed\\n+date: 2026-08-20\\n+decision-makers:\\n+ - contextual-orchestrator maintainers\\n+---\\n+\\n+# ADR 0017: Fail-closed inbound request framing\\n+\\n+## Context\\n+\\n+The HTTP handler previously converted one `Content-Length` value and delegated\\n+all other framing behavior to `BufferedReader.read`. Missing, negative,\\n+duplicate, transfer-coded, truncated, and slow request bodies therefore did not\\n+share one bounded policy. This is an inbound trust-boundary defect, separate\\n+from provider-response framing.\\n+\\n+## Decision\\n+\\n+Accept only one ASCII decimal `Content-Length` within the configured body limit.\\n+Reject missing length, duplicate length lines, `Transfer-Encoding`, malformed\\n+or signed values, and `Transfer-Encoding` plus `Content-Length` before reading\\n+body bytes. Read exactly the declared number of bytes with a bounded socket\\n+deadline. On any framing failure, return a stable generic error and close the\\n+connection so unread bytes cannot be interpreted as another request.\\n+\\n+The server does not implement chunked decoding in this change. A future bounded\\n+decoder requires a separate design and socket-level evidence.\\n+\\n+## Consequences\\n+\\n+- Every current JSON body endpoint inherits one parser/reader policy.\\n+- Clients must send a fixed-length JSON request; the API returns `411`, `413`,\\n+ `408`, or `400` with `invalid_request_framing`/named framing codes as\\n+ appropriate.\\n+- The body deadline and byte limit are visible in the secret-free readiness\\n+ profile.\\n+- Socket-level, truncation, timeout, duplicate, transfer-coding, and boundary\\n+ tests become merge evidence.\\n+\\n+## Standards\\n+\\n+Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP/1.1* (RFC 9112).\\n+RFC Editor. https://www.rfc-editor.org/rfc/rfc9112.html\\n+\\n+## Customer next action\\n+\\n+Send JSON requests with exactly one fixed decimal `Content-Length`; retry a\\n+framing error only after correcting the request, not by replaying the same\\n+ambiguous bytes.\" }, { \"sha\": \"959c0a9911c70036ded333b68423fcecc5869001\", \"filename\": \"docs/planning/adrs/0018-multimodal-evidence-preserving-orchestration.md\", \"status\": \"added\", \"additions\": 104, \"deletions\": 0, \"changes\": 104, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0018-multimodal-evidence-preserving-orchestration.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0018-multimodal-evidence-preserving-orchestration.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0018-multimodal-evidence-preserving-orchestration.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,104 @@\\n+---\\n+id: \\\"0018\\\"\\n+title: \\\"Preserve multimodal evidence through every evidence-bearing workflow step\\\"\\n+status: accepted\\n+proposed_date: \\\"2026-08-20\\\"\\n+accepted_date: \\\"2026-08-20\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"Sakana Fugu Technical Report\\\"\\n+ - \\\"TRINITY: An Evolved LLM Coordinator\\\"\\n+ - \\\"Learning to Orchestrate Agents in Natural Language with the Conductor\\\"\\n+ - \\\"OpenAI Chat Completions and Responses API references\\\"\\n+informed:\\n+ - \\\"LineageWeave\\\"\\n+ - \\\"OpenCode\\\"\\n+ - \\\"Noema\\\"\\n+ - \\\"Strix\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/orchestrator.py\\\"\\n+ - \\\"contextual_orchestrator/server.py\\\"\\n+ - \\\"tests/test_multimodal_workflow_evidence.py\\\"\\n+related:\\n+ - path: \\\"docs/planning/adrs/0013-paper-grounded-adaptive-reasoning-policy.md\\\"\\n+ relation: extends\\n+effort: S\\n+---\\n+\\n+# Preserve multimodal evidence through every evidence-bearing workflow step\\n+\\n+## Context\\n+\\n+The OpenAI-compatible boundary accepts Chat Completions `image_url` parts and\\n+Responses `input_image` parts, but the conducted workflow reduced the original\\n+request to text before thinker, worker, verifier, and synthesizer execution.\\n+The models therefore received the literal marker `[image]`, not the pixels.\\n+An authorized, non-identifying LineageWeave runtime check exposed the product\\n+impact: a completed five-image VISION run persisted five captions but zero OCR\\n+characters. Transport completion was incorrectly stronger than evidence\\n+completion.\\n+\\n+Fugu, TRINITY, and Conductor support adaptive coordination across specialized\\n+workers; they do not support removing the task evidence needed by those\\n+workers. The official OpenAI API contracts represent image input as typed\\n+content blocks, not as prose placeholders. The orchestrator must preserve\\n+that typed evidence while retaining access-list isolation for prior model\\n+outputs.\\n+\\n+## Decision\\n+\\n+- Normalize Responses `input_image` blocks to the existing Chat Completions\\n+ `image_url` representation at the provider-neutral boundary.\\n+- Retain the original validated image blocks beside each workflow step's text\\n+ instruction. Access lists still govern prior model outputs; source evidence\\n+ is part of the original task, not another agent's hidden state.\\n+- Route image-bearing work and failover only through enabled agents that\\n+ explicitly advertise the `vision` capability tag. Do not infer VISION\\n+ support from a provider name or model identifier.\\n+- Fail closed before provider I/O when no enabled VISION-capable worker is\\n+ available. A text-only answer to an unseen image is not a valid fallback.\\n+- Keep the public request shape and provider-neutral gateway boundary. This\\n+ change adds no provider SDK, model-name ordering, or direct provider path.\\n+\\n+## Consequences\\n+\\n+- Thinker, worker, verifier, and synthesizer steps can independently inspect\\n+ the same source pixels while seeing only the prior outputs allowed by the\\n+ workflow access list.\\n+- Chat Completions and Responses use one internal multimodal representation.\\n+- Image bytes cross the same configured provider boundary more than once in a\\n+ deep workflow. That is deliberate quality-oriented test-time compute, and\\n+ the existing request-size and URL validation remain authoritative.\\n+- A wrongly tagged pool fails visibly and requires an operator to correct its\\n+ capability catalog instead of silently accepting fabricated visual work.\\n+\\n+## Verification\\n+\\n+A synthetic content-block regression must prove that every conducted step\\n+receives the source image, text-only requests remain strings, Responses image\\n+parts survive normalization, failover never enters a non-VISION agent, and a\\n+pool without a VISION agent fails before any client call.\\n+\\n+## References\\n+\\n+Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025).\\n+*Learning to orchestrate agents in natural language with the Conductor*\\n+(arXiv:2512.04388). arXiv. https://doi.org/10.48550/arXiv.2512.04388\\n+\\n+OpenAI. (n.d.-a). *Create chat completion*. OpenAI API reference. Retrieved\\n+August 20, 2026, from\\n+https://developers.openai.com/api/reference/cli/resources/chat/subresources/completions\\n+\\n+OpenAI. (n.d.-b). *Create a model response*. OpenAI API reference. Retrieved\\n+August 20, 2026, from\\n+https://developers.openai.com/api/reference/typescript/resources/beta/subresources/responses/methods/create\\n+\\n+Tang, Y., Cetin, E., Xu, J., Sun, Q., Nielsen, S., Richard, V., Goda, H.,\\n+Tymchenko, I., Nguyen, N., Lee, H., Ashiga, M., Kotyan, S., Kuroki, S., &\\n+Clanuwat, T. (2026). *Sakana Fugu technical report* (arXiv:2606.21228).\\n+arXiv. https://doi.org/10.48550/arXiv.2606.21228\\n+\\n+Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025).\\n+*TRINITY: An evolved LLM coordinator* (arXiv:2512.04695). arXiv.\\n+https://doi.org/10.48550/arXiv.2512.04695\" }, { \"sha\": \"04b01aabcd845d19a22affdf19b65787c6bd2ae9\", \"filename\": \"docs/planning/adrs/0019-no-runtime-monkey-patching.md\", \"status\": \"added\", \"additions\": 25, \"deletions\": 0, \"changes\": 25, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0019-no-runtime-monkey-patching.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0019-no-runtime-monkey-patching.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0019-no-runtime-monkey-patching.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,25 @@\\n+# ADR 0019: No runtime monkey patching for transport contracts\\n+\\n+- Status: Accepted\\n+- Date: 2026-08-21\\n+\\n+## Context\\n+\\n+Provider capability behavior must be visible in the owning transport client.\\n+Import-time mutation of `ModelClient` obscures the effective contract, creates\\n+global process state, and can change behavior for callers that did not opt in.\\n+\\n+## Decision\\n+\\n+The orchestrator must not monkey patch classes or methods at runtime. Optional\\n+sampling omission, capability negotiation, protocol translation, and retry\\n+behavior are implemented in the owning `ModelClient` transport paths and are\\n+covered by direct tests. Importing the package must not mutate a class or\\n+install a wrapper as a side effect.\\n+\\n+## Consequences\\n+\\n+- The effective request contract is inspectable in one implementation.\\n+- Chat, streaming, and batch transports share the same provider-neutral policy.\\n+- Upstream capability changes require an ordinary code review instead of a\\n+ hidden import-order dependency.\" }, { \"sha\": \"0c47c83d6946b414d4669279f03a42cd85192f8a\", \"filename\": \"docs/planning/adrs/0020-provider-error-boundary.md\", \"status\": \"added\", \"additions\": 38, \"deletions\": 0, \"changes\": 38, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0020-provider-error-boundary.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0020-provider-error-boundary.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0020-provider-error-boundary.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,38 @@\\n+# ADR 0020: Keep raw provider failures inside the gateway\\n+\\n+- Status: Accepted\\n+- Date: 2026-08-21\\n+\\n+## Context\\n+\\n+Provider HTTP bodies and exception messages can contain credentials, prompt\\n+content, personal data, internal URLs, or vendor diagnostics. Structured\\n+orchestration, embeddings, retries, and cross-provider failover must not make\\n+provider raw exceptions available through a public gateway error or an\\n+exception cause.\\n+\\n+## Decision\\n+\\n+1. Chat, embedding, and passthrough transport failures expose only\\n+ package-owned messages and never copy provider exception text.\\n+2. Model discovery reports stable diagnostic codes without copying provider\\n+ response or exception text.\\n+3. Exhausted failover and structured-output parsing do not chain provider raw\\n+ errors; deterministic local remediation remains available.\\n+4. Provider diagnostics may be counted by allowlisted type/code in internal\\n+ telemetry, but raw bodies, exception text, credentials, and prompts are not\\n+ persisted or returned.\\n+\\n+## Verification\\n+\\n+`tests/test_model_discovery.py`, `tests/test_provider_reliability.py`, and\\n+`tests/test_model_judge.py` assert that provider response text is absent from\\n+public messages and causes. The full suite must remain green before merge.\\n+\\n+## References\\n+\\n+MITRE. (n.d.). *CWE-209: Generation of error message containing sensitive\\n+information*. https://cwe.mitre.org/data/definitions/209.html\\n+\\n+OWASP Foundation. (2023). *Application Security Verification Standard 4.0.3*.\\n+https://owasp.org/www-project-application-security-verification-standard/\" }, { \"sha\": \"7a720e1b826c290d228a2637831927706923bdfb\", \"filename\": \"docs/planning/adrs/0025-universal-hash-locked-requirements.md\", \"status\": \"added\", \"additions\": 58, \"deletions\": 0, \"changes\": 58, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0025-universal-hash-locked-requirements.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0025-universal-hash-locked-requirements.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0025-universal-hash-locked-requirements.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,58 @@\\n+---\\n+id: \\\"0025\\\"\\n+title: \\\"Generate one universal hash-locked runtime requirements file\\\"\\n+status: accepted\\n+accepted_date: \\\"2026-08-21\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+affected_components:\\n+ - \\\"requirements.lock\\\"\\n+ - \\\"tests/test_repository_security_metadata.py\\\"\\n+---\\n+\\n+# Generate one universal hash-locked runtime requirements file\\n+\\n+## Context\\n+\\n+The runtime lock is installed with `pip --require-hashes`, but its previous\\n+platform-specific regeneration removed `colorama`, `greenlet`, and `tzdata`.\\n+Those packages are conditional transitive dependencies on supported Python\\n+environments, so a lock produced for one host was not complete evidence for\\n+another host.\\n+\\n+## Decision\\n+\\n+Generate `requirements.lock` with `uv pip compile --universal\\n+--generate-hashes --python-version 3.10 --extra api --extra db pyproject.toml`.\\n+Universal resolution retains PEP 508 environment markers, one exact version per\\n+resolved branch, and hashes for every artifact while preserving the existing\\n+`pip --require-hashes` installation contract. Regeneration must retain the\\n+platform-conditional `colorama`, `greenlet`, and `tzdata` records and a metadata\\n+test must fail if universal mode or those records disappear.\\n+\\n+Do not add a second lock format yet. PEP 751 standardizes `pylock.toml`, but the\\n+current CI and buyer evidence consume the existing requirements file directly.\\n+Adopt `pylock.toml` only when the production installer and security scanners can\\n+consume it without maintaining two divergent dependency authorities.\\n+\\n+## Consequences\\n+\\n+- macOS, Linux, Windows, architecture, and supported Python marker branches are\\n+ resolved together instead of inheriting the workstation that ran the tool.\\n+- Dependency versions remain auditable and installation remains resolution-free\\n+ under hash-checking mode.\\n+- A regeneration can be more constrained than a host-only solve; an\\n+ incompatible dependency must fail the universal solve rather than silently\\n+ disappear from another platform.\\n+\\n+## References\\n+\\n+Astral Software, Inc. (2026). *Resolution*. uv.\\n+https://docs.astral.sh/uv/concepts/resolution/\\n+\\n+Cannon, B. (2025). PEP 751: A file format to record Python dependencies for\\n+installation reproducibility. *Python Enhancement Proposals*. Python Software\\n+Foundation. https://peps.python.org/pep-0751/\\n+\\n+Python Packaging Authority. (2026). *Dependency specifiers*. Python Packaging\\n+User Guide. https://packaging.python.org/en/latest/specifications/dependency-specifiers/\" }, { \"sha\": \"23753f8b8097138ec68ea3b85b463a438362303e\", \"filename\": \"examples/agents.local.json\", \"status\": \"modified\", \"additions\": 6, \"deletions\": 45, \"changes\": 51, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/examples%2Fagents.local.json\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/examples%2Fagents.local.json\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/examples%2Fagents.local.json?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -4,74 +4,35 @@\\n \\\"id\\\": \\\"contextual_orchestrator\\\",\\n \\\"model\\\": \\\"contextual-orchestrator\\\",\\n \\\"base_url\\\": \\\"local://127.0.0.1:18000/v1\\\",\\n+ \\\"local_credential_key\\\": \\\"LOCAL_GATEWAY_TOKEN\\\",\\n \\\"provider_name\\\": \\\"contextual-orchestrator\\\",\\n \\\"tags\\\": [\\\"orchestration\\\", \\\"planning\\\", \\\"reasoning\\\", \\\"verification\\\", \\\"writing\\\"],\\n \\\"priority\\\": 5,\\n \\\"provider_exclusions\\\": [\\\"thinker\\\", \\\"worker\\\", \\\"verifier\\\", \\\"synthesizer\\\"]\\n },\\n- {\\n- \\\"id\\\": \\\"mlx_gemma_4_31b_it\\\",\\n- \\\"model\\\": \\\"mlx-community/gemma-4-31b-it-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n- \\\"tags\\\": [\\\"reasoning\\\", \\\"research\\\", \\\"coding\\\", \\\"writing\\\", \\\"verification\\\"],\\n- \\\"priority\\\": 4,\\n- \\\"provider_exclusions\\\": [\\\"verifier\\\"]\\n- },\\n- {\\n- \\\"id\\\": \\\"mlx_deepseek_r1_qwen_32b\\\",\\n- \\\"model\\\": \\\"outlier-ai/deepseek-r1-distill-qwen-32b-mlx-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n- \\\"tags\\\": [\\\"reasoning\\\", \\\"research\\\", \\\"coding\\\", \\\"verification\\\"],\\n- \\\"priority\\\": 4,\\n- \\\"provider_exclusions\\\": [\\\"verifier\\\"]\\n- },\\n- {\\n- \\\"id\\\": \\\"mlx_gemma_4_e4b_it\\\",\\n- \\\"model\\\": \\\"mlx-community/gemma-4-e4b-it-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n- \\\"tags\\\": [\\\"reasoning\\\", \\\"research\\\", \\\"coding\\\", \\\"writing\\\", \\\"verification\\\"],\\n- \\\"priority\\\": 3\\n- },\\n- {\\n- \\\"id\\\": \\\"mlx_llama_3_2_3b_instruct\\\",\\n- \\\"model\\\": \\\"mlx-community/llama-3.2-3b-instruct-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n- \\\"tags\\\": [\\\"fast\\\", \\\"reasoning\\\", \\\"coding\\\", \\\"writing\\\", \\\"verification\\\"],\\n- \\\"priority\\\": 2\\n- },\\n- {\\n- \\\"id\\\": \\\"mlx_llama_3_2_1b_instruct\\\",\\n- \\\"model\\\": \\\"mlx-community/llama-3.2-1b-instruct-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n- \\\"tags\\\": [\\\"fast\\\", \\\"writing\\\", \\\"coding\\\"],\\n- \\\"priority\\\": 1,\\n- \\\"provider_exclusions\\\": [\\\"verifier\\\"]\\n- },\\n {\\n \\\"id\\\": \\\"llama_cpp_embeddinggemma\\\",\\n \\\"model\\\": \\\"embeddinggemma\\\",\\n \\\"base_url\\\": \\\"local://127.0.0.1:8082/v1\\\",\\n+ \\\"local_credential_key\\\": \\\"LOCAL_GATEWAY_TOKEN\\\",\\n \\\"provider_name\\\": \\\"llama.cpp\\\",\\n \\\"tags\\\": [\\\"embedding\\\"],\\n \\\"priority\\\": 0\\n },\\n {\\n \\\"id\\\": \\\"lmstudio_gemma_4_e4b_it\\\",\\n- \\\"model\\\": \\\"lmstudio-community/gemma-4-E4B-it-MLX-4bit\\\",\\n+ \\\"model\\\": \\\"gemma-4-e4b-it\\\",\\n \\\"base_url\\\": \\\"local://127.0.0.1:1234/v1\\\",\\n+ \\\"local_credential_key\\\": \\\"LOCAL_GATEWAY_TOKEN\\\",\\n \\\"provider_name\\\": \\\"lm-studio\\\",\\n \\\"tags\\\": [\\\"reasoning\\\", \\\"coding\\\", \\\"writing\\\"],\\n \\\"priority\\\": 0\\n },\\n {\\n \\\"id\\\": \\\"lmstudio_embeddinggemma\\\",\\n- \\\"model\\\": \\\"mlx-community/embeddinggemma-300m-8bit\\\",\\n+ \\\"model\\\": \\\"embeddinggemma\\\",\\n \\\"base_url\\\": \\\"local://127.0.0.1:1234/v1\\\",\\n+ \\\"local_credential_key\\\": \\\"LOCAL_GATEWAY_TOKEN\\\",\\n \\\"provider_name\\\": \\\"lm-studio\\\",\\n \\\"tags\\\": [\\\"embedding\\\"],\\n \\\"priority\\\": 0\" }, { \"sha\": \"103a31b38e5eea170ed1a9cc1edfeade74247bb2\", \"filename\": \"examples/agents.mlx.json\", \"status\": \"removed\", \"additions\": 0, \"deletions\": 12, \"changes\": 12, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/e226e1197bdfc890c9d8e5b9b648c78857d7e465/examples%2Fagents.mlx.json\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/e226e1197bdfc890c9d8e5b9b648c78857d7e465/examples%2Fagents.mlx.json\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/examples%2Fagents.mlx.json?ref=e226e1197bdfc890c9d8e5b9b648c78857d7e465\", \"patch\": \"@@ -1,12 +0,0 @@\\n-{\\n- \\\"agents\\\": [\\n- {\\n- \\\"id\\\": \\\"local_fast_agent\\\",\\n- \\\"model\\\": \\\"mlx-community/llama-3.2-3b-instruct-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n- \\\"tags\\\": [\\\"reasoning\\\", \\\"writing\\\", \\\"coding\\\", \\\"verification\\\"],\\n- \\\"priority\\\": 1\\n- }\\n- ]\\n-}\" }, { \"sha\": \"d9fc8f1ecf1c64fbf3accb13ec7f05f0708301fe\", \"filename\": \"pyproject.toml\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 0, \"changes\": 3, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/pyproject.toml\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/pyproject.toml\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/pyproject.toml?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -6,6 +6,9 @@ readme = \\\"README.md\\\"\\n requires-python = \\\">=3.10\\\"\\n dependencies = [\\n \\\"hypothesis>=6.100\\\",\\n+ \\\"opentelemetry-api>=1.30.0\\\",\\n+ \\\"opentelemetry-sdk>=1.30.0\\\",\\n+ \\\"opentelemetry-exporter-otlp-proto-http>=1.30.0\\\",\\n ]\\n \\n [project.optional-dependencies]\" }, { \"sha\": \"0f79fd6e90d24ade5ce44f017b5ba3e7c17fe790\", \"filename\": \"requirements.lock\", \"status\": \"modified\", \"additions\": 474, \"deletions\": 94, \"changes\": 568, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/requirements.lock\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/requirements.lock\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/requirements.lock?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,9 +1,5 @@\\n-#\\n-# This file is autogenerated by pip-compile with Python 3.12\\n-# by the following command:\\n-#\\n-# pip-compile --extra=api --extra=db --generate-hashes --output-file=requirements.lock pyproject.toml\\n-#\\n+# This file was autogenerated by uv via the following command:\\n+# uv pip compile --universal --generate-hashes --python-version 3.10 --extra api --extra db --output-file requirements.lock pyproject.toml\\n alembic==1.18.5 \\\\\\n --hash=sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc \\\\\\n --hash=sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e\\n@@ -20,107 +16,380 @@ anyio==4.14.1 \\\\\\n --hash=sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72 \\\\\\n --hash=sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e\\n # via starlette\\n+certifi==2026.7.22 \\\\\\n+ --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \\\\\\n+ --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55\\n+ # via requests\\n+charset-normalizer==3.5.1 \\\\\\n+ --hash=sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45 \\\\\\n+ --hash=sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d \\\\\\n+ --hash=sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5 \\\\\\n+ --hash=sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b \\\\\\n+ --hash=sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f \\\\\\n+ --hash=sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f \\\\\\n+ --hash=sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5 \\\\\\n+ --hash=sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22 \\\\\\n+ --hash=sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5 \\\\\\n+ --hash=sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac \\\\\\n+ --hash=sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838 \\\\\\n+ --hash=sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90 \\\\\\n+ --hash=sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626 \\\\\\n+ --hash=sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4 \\\\\\n+ --hash=sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369 \\\\\\n+ --hash=sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b \\\\\\n+ --hash=sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e \\\\\\n+ --hash=sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee \\\\\\n+ --hash=sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1 \\\\\\n+ --hash=sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102 \\\\\\n+ --hash=sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8 \\\\\\n+ --hash=sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8 \\\\\\n+ --hash=sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9 \\\\\\n+ --hash=sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf \\\\\\n+ --hash=sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0 \\\\\\n+ --hash=sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031 \\\\\\n+ --hash=sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e \\\\\\n+ --hash=sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235 \\\\\\n+ --hash=sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072 \\\\\\n+ --hash=sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb \\\\\\n+ --hash=sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c \\\\\\n+ --hash=sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950 \\\\\\n+ --hash=sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2 \\\\\\n+ --hash=sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb \\\\\\n+ --hash=sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e \\\\\\n+ --hash=sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6 \\\\\\n+ --hash=sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b \\\\\\n+ --hash=sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2 \\\\\\n+ --hash=sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d \\\\\\n+ --hash=sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa \\\\\\n+ --hash=sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2 \\\\\\n+ --hash=sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818 \\\\\\n+ --hash=sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032 \\\\\\n+ --hash=sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71 \\\\\\n+ --hash=sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96 \\\\\\n+ --hash=sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687 \\\\\\n+ --hash=sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8 \\\\\\n+ --hash=sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3 \\\\\\n+ --hash=sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61 \\\\\\n+ --hash=sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9 \\\\\\n+ --hash=sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1 \\\\\\n+ --hash=sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a \\\\\\n+ --hash=sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420 \\\\\\n+ --hash=sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4 \\\\\\n+ --hash=sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65 \\\\\\n+ --hash=sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663 \\\\\\n+ --hash=sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f \\\\\\n+ --hash=sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591 \\\\\\n+ --hash=sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a \\\\\\n+ --hash=sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18 \\\\\\n+ --hash=sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e \\\\\\n+ --hash=sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f \\\\\\n+ --hash=sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7 \\\\\\n+ --hash=sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3 \\\\\\n+ --hash=sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c \\\\\\n+ --hash=sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3 \\\\\\n+ --hash=sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7 \\\\\\n+ --hash=sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96 \\\\\\n+ --hash=sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486 \\\\\\n+ --hash=sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3 \\\\\\n+ --hash=sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6 \\\\\\n+ --hash=sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b \\\\\\n+ --hash=sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731 \\\\\\n+ --hash=sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959 \\\\\\n+ --hash=sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9 \\\\\\n+ --hash=sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf \\\\\\n+ --hash=sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8 \\\\\\n+ --hash=sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e \\\\\\n+ --hash=sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f \\\\\\n+ --hash=sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885 \\\\\\n+ --hash=sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0 \\\\\\n+ --hash=sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506 \\\\\\n+ --hash=sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2 \\\\\\n+ --hash=sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0 \\\\\\n+ --hash=sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e \\\\\\n+ --hash=sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f \\\\\\n+ --hash=sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e \\\\\\n+ --hash=sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491 \\\\\\n+ --hash=sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a \\\\\\n+ --hash=sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20 \\\\\\n+ --hash=sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449 \\\\\\n+ --hash=sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af \\\\\\n+ --hash=sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c \\\\\\n+ --hash=sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712 \\\\\\n+ --hash=sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7 \\\\\\n+ --hash=sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a \\\\\\n+ --hash=sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20 \\\\\\n+ --hash=sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f \\\\\\n+ --hash=sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3 \\\\\\n+ --hash=sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9 \\\\\\n+ --hash=sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e \\\\\\n+ --hash=sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5 \\\\\\n+ --hash=sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2 \\\\\\n+ --hash=sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36 \\\\\\n+ --hash=sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263 \\\\\\n+ --hash=sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4 \\\\\\n+ --hash=sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11 \\\\\\n+ --hash=sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a \\\\\\n+ --hash=sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3 \\\\\\n+ --hash=sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375 \\\\\\n+ --hash=sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa \\\\\\n+ --hash=sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d \\\\\\n+ --hash=sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5 \\\\\\n+ --hash=sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99 \\\\\\n+ --hash=sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d \\\\\\n+ --hash=sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c \\\\\\n+ --hash=sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488 \\\\\\n+ --hash=sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6 \\\\\\n+ --hash=sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc \\\\\\n+ --hash=sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b \\\\\\n+ --hash=sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f \\\\\\n+ --hash=sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00 \\\\\\n+ --hash=sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10 \\\\\\n+ --hash=sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598 \\\\\\n+ --hash=sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6 \\\\\\n+ --hash=sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962 \\\\\\n+ --hash=sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c \\\\\\n+ --hash=sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08 \\\\\\n+ --hash=sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab \\\\\\n+ --hash=sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573 \\\\\\n+ --hash=sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90 \\\\\\n+ --hash=sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5 \\\\\\n+ --hash=sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18 \\\\\\n+ --hash=sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d \\\\\\n+ --hash=sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af \\\\\\n+ --hash=sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea \\\\\\n+ --hash=sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c \\\\\\n+ --hash=sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b \\\\\\n+ --hash=sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6 \\\\\\n+ --hash=sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8 \\\\\\n+ --hash=sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774 \\\\\\n+ --hash=sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004 \\\\\\n+ --hash=sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a \\\\\\n+ --hash=sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a \\\\\\n+ --hash=sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2 \\\\\\n+ --hash=sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2 \\\\\\n+ --hash=sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa \\\\\\n+ --hash=sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe \\\\\\n+ --hash=sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3 \\\\\\n+ --hash=sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc \\\\\\n+ --hash=sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795 \\\\\\n+ --hash=sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d \\\\\\n+ --hash=sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc \\\\\\n+ --hash=sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893 \\\\\\n+ --hash=sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef \\\\\\n+ --hash=sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d \\\\\\n+ --hash=sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda \\\\\\n+ --hash=sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17 \\\\\\n+ --hash=sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30 \\\\\\n+ --hash=sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7 \\\\\\n+ --hash=sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5 \\\\\\n+ --hash=sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182 \\\\\\n+ --hash=sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f \\\\\\n+ --hash=sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9 \\\\\\n+ --hash=sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada \\\\\\n+ --hash=sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876 \\\\\\n+ --hash=sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a \\\\\\n+ --hash=sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348 \\\\\\n+ --hash=sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3 \\\\\\n+ --hash=sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f \\\\\\n+ --hash=sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0 \\\\\\n+ --hash=sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f\\n+ # via requests\\n click==8.4.2 \\\\\\n --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \\\\\\n --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76\\n # via uvicorn\\n-colorama==0.4.6 \\\\\\n+colorama==0.4.6 ; sys_platform == 'win32' \\\\\\n --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \\\\\\n --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6\\n # via click\\n+exceptiongroup==1.3.1 ; python_full_version < '3.11' \\\\\\n+ --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \\\\\\n+ --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598\\n+ # via\\n+ # anyio\\n+ # hypothesis\\n fastapi==0.138.2 \\\\\\n --hash=sha256:6432359d067a432134620e7c5e4c6e5063e7f37815bbbbf20acef14b0d2e3fc8 \\\\\\n --hash=sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615\\n # via contextual-orchestrator (pyproject.toml)\\n-greenlet==3.5.3 \\\\\\n- --hash=sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db \\\\\\n- --hash=sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7 \\\\\\n- --hash=sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8 \\\\\\n- --hash=sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc \\\\\\n- --hash=sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da \\\\\\n- --hash=sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8 \\\\\\n- --hash=sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91 \\\\\\n- --hash=sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c \\\\\\n- --hash=sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310 \\\\\\n- --hash=sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3 \\\\\\n- --hash=sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce \\\\\\n- --hash=sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149 \\\\\\n- --hash=sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d \\\\\\n- --hash=sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34 \\\\\\n- --hash=sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be \\\\\\n- --hash=sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2 \\\\\\n- --hash=sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b \\\\\\n- --hash=sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0 \\\\\\n- --hash=sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71 \\\\\\n- --hash=sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d \\\\\\n- --hash=sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04 \\\\\\n- --hash=sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227 \\\\\\n- --hash=sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c \\\\\\n- --hash=sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6 \\\\\\n- --hash=sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8 \\\\\\n- --hash=sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21 \\\\\\n- --hash=sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23 \\\\\\n- --hash=sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605 \\\\\\n- --hash=sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128 \\\\\\n- --hash=sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f \\\\\\n- --hash=sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea \\\\\\n- --hash=sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81 \\\\\\n- --hash=sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2 \\\\\\n- --hash=sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8 \\\\\\n- --hash=sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e \\\\\\n- --hash=sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb \\\\\\n- --hash=sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47 \\\\\\n- --hash=sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8 \\\\\\n- --hash=sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab \\\\\\n- --hash=sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7 \\\\\\n- --hash=sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861 \\\\\\n- --hash=sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702 \\\\\\n- --hash=sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814 \\\\\\n- --hash=sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf \\\\\\n- --hash=sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a \\\\\\n- --hash=sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260 \\\\\\n- --hash=sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1 \\\\\\n- --hash=sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec \\\\\\n- --hash=sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a \\\\\\n- --hash=sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5 \\\\\\n- --hash=sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1 \\\\\\n- --hash=sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4 \\\\\\n- --hash=sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c \\\\\\n- --hash=sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda \\\\\\n- --hash=sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb \\\\\\n- --hash=sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31 \\\\\\n- --hash=sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d \\\\\\n- --hash=sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c \\\\\\n- --hash=sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d \\\\\\n- --hash=sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b \\\\\\n- --hash=sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550 \\\\\\n- --hash=sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c \\\\\\n- --hash=sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357 \\\\\\n- --hash=sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c \\\\\\n- --hash=sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4 \\\\\\n- --hash=sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930 \\\\\\n- --hash=sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8 \\\\\\n- --hash=sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b \\\\\\n- --hash=sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb \\\\\\n- --hash=sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16 \\\\\\n- --hash=sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608 \\\\\\n- --hash=sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f \\\\\\n- --hash=sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc \\\\\\n- --hash=sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d \\\\\\n- --hash=sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44 \\\\\\n- --hash=sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b \\\\\\n- --hash=sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3 \\\\\\n- --hash=sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154 \\\\\\n- --hash=sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117\\n+googleapis-common-protos==1.75.1 \\\\\\n+ --hash=sha256:28a1934bcd33b9c9da66ac301a0a4227e3367f095a17d0375cb98f0a09d93b79 \\\\\\n+ --hash=sha256:d3042c6c5a2d4e67113104d6b6818b59b6bd92a197f2a91508e801fe815cf071\\n+ # via opentelemetry-exporter-otlp-proto-http\\n+greenlet==3.5.5 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' \\\\\\n+ --hash=sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537 \\\\\\n+ --hash=sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39 \\\\\\n+ --hash=sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277 \\\\\\n+ --hash=sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41 \\\\\\n+ --hash=sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2 \\\\\\n+ --hash=sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d \\\\\\n+ --hash=sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53 \\\\\\n+ --hash=sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e \\\\\\n+ --hash=sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502 \\\\\\n+ --hash=sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5 \\\\\\n+ --hash=sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc \\\\\\n+ --hash=sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759 \\\\\\n+ --hash=sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f \\\\\\n+ --hash=sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b \\\\\\n+ --hash=sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1 \\\\\\n+ --hash=sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5 \\\\\\n+ --hash=sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769 \\\\\\n+ --hash=sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0 \\\\\\n+ --hash=sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f \\\\\\n+ --hash=sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da \\\\\\n+ --hash=sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76 \\\\\\n+ --hash=sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3 \\\\\\n+ --hash=sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e \\\\\\n+ --hash=sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476 \\\\\\n+ --hash=sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e \\\\\\n+ --hash=sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380 \\\\\\n+ --hash=sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef \\\\\\n+ --hash=sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18 \\\\\\n+ --hash=sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b \\\\\\n+ --hash=sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272 \\\\\\n+ --hash=sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0 \\\\\\n+ --hash=sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053 \\\\\\n+ --hash=sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07 \\\\\\n+ --hash=sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387 \\\\\\n+ --hash=sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52 \\\\\\n+ --hash=sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed \\\\\\n+ --hash=sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95 \\\\\\n+ --hash=sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c \\\\\\n+ --hash=sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad \\\\\\n+ --hash=sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f \\\\\\n+ --hash=sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db \\\\\\n+ --hash=sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328 \\\\\\n+ --hash=sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8 \\\\\\n+ --hash=sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71 \\\\\\n+ --hash=sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc \\\\\\n+ --hash=sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864 \\\\\\n+ --hash=sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0 \\\\\\n+ --hash=sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1 \\\\\\n+ --hash=sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b \\\\\\n+ --hash=sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667 \\\\\\n+ --hash=sha256:86c5113d698cb8d927b2750bb1f1d59eefe3a37e0e0217491aee29a7f84ef52c \\\\\\n+ --hash=sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c \\\\\\n+ --hash=sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926 \\\\\\n+ --hash=sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc \\\\\\n+ --hash=sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd \\\\\\n+ --hash=sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007 \\\\\\n+ --hash=sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6 \\\\\\n+ --hash=sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56 \\\\\\n+ --hash=sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0 \\\\\\n+ --hash=sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b \\\\\\n+ --hash=sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53 \\\\\\n+ --hash=sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c \\\\\\n+ --hash=sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c \\\\\\n+ --hash=sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474 \\\\\\n+ --hash=sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa \\\\\\n+ --hash=sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61 \\\\\\n+ --hash=sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206 \\\\\\n+ --hash=sha256:c69bed34470abfcd456984fdadaa18e62169af4480335c45f3c32d1d9c12e638 \\\\\\n+ --hash=sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9 \\\\\\n+ --hash=sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874 \\\\\\n+ --hash=sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d \\\\\\n+ --hash=sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8 \\\\\\n+ --hash=sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae \\\\\\n+ --hash=sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0 \\\\\\n+ --hash=sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773 \\\\\\n+ --hash=sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4 \\\\\\n+ --hash=sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552 \\\\\\n+ --hash=sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42 \\\\\\n+ --hash=sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b\\n # via sqlalchemy\\n h11==0.16.0 \\\\\\n --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \\\\\\n --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86\\n # via uvicorn\\n+hypothesis==6.165.10 \\\\\\n+ --hash=sha256:00de0abdcf8c05c9d0eab735a3c49a276376b55151e6fcb903c2b39a90e5e5c3 \\\\\\n+ --hash=sha256:057d0232f1224dcd0b7698902551a4341a7399f90670b036db6c4376715fe889 \\\\\\n+ --hash=sha256:09772e328a26e50486ac572be34f9887f9aa185efe7ebb16bde4e8f6038db1f4 \\\\\\n+ --hash=sha256:0c4e6869817c3cfdf5a2b4d348497b95159bdecb3365be732c9b8570e36a4eef \\\\\\n+ --hash=sha256:10d9a650a4666b0914831f769703d36140ed8039fd19bf9b71f615b8541eccf2 \\\\\\n+ --hash=sha256:18a3ea838ddea183388f8788750afa8494d79abb5358823be9782585f34445d3 \\\\\\n+ --hash=sha256:1a380bc99aa3b035e6a95a2201bf792d4082a04ca75babcc21849c2d0914bb28 \\\\\\n+ --hash=sha256:1d305448e9bd8e2f4f3cea0eafd809efdaab4e998a0019bc615650c8463e42f1 \\\\\\n+ --hash=sha256:1ec53f08732e3cfd0342cbbd75dbd1b193c8f19390660466e536a748bb81f757 \\\\\\n+ --hash=sha256:1f2c4db25fb8ec1a16a8dba580666337b8ffb1887c4cf1750cc954313897cef7 \\\\\\n+ --hash=sha256:20f6236cfb90b7817bb1a6a087589ca4aa46d73170f0dd62963952ed5dadc589 \\\\\\n+ --hash=sha256:22cf19388f0ff6ced8eb3e49c903d14938e4ed909d93bf28383eef451511e424 \\\\\\n+ --hash=sha256:277f41801e88dad2eba082f91a75632b7584ff64044ba2cf9dadf511b0d19cd0 \\\\\\n+ --hash=sha256:2a2567b3a03a4a5a7c575c191cfcce321a967df3727803817e75bffbbeaecabe \\\\\\n+ --hash=sha256:2abb50cf1cf77d721de0a24c3f99d9c4ffdeb2cbd1e12aebb5a7a93e2b6b6d1f \\\\\\n+ --hash=sha256:2b112768cfb67f2b683e53e58c1a33d27811aacf60c942b8eb74635e469a73f6 \\\\\\n+ --hash=sha256:2b36aaffc88625a44f91074c5bbedfdefb9b376c38d1b3c342edcd2e4c8ed16c \\\\\\n+ --hash=sha256:2d0e0f8263d34dd8fa3b39eaa9a50bba56a8470b3dd9ebf6672d10840abe063e \\\\\\n+ --hash=sha256:30797f20ca45e57f526d2df872f63ba453cb4e1091ad542184a7a951af8da79d \\\\\\n+ --hash=sha256:3376f2594763aef14faa519b0fb27cae7ce9eeaab4c69efa07777499110306c9 \\\\\\n+ --hash=sha256:34ee6402df6f31274d89119f1561b5f7489c97866afc5b7a3ed3a13d7e762802 \\\\\\n+ --hash=sha256:37a7ac3d34220800e1107871cc391bca1b00439875925d7d821878b8b791f245 \\\\\\n+ --hash=sha256:3de69aa8b924b400291a3cc42aaf78e6ab65c905a3e7e1a5dc39d95ef1b428cb \\\\\\n+ --hash=sha256:4334058033e0214475f019e15492a50f3854fe8728cf51fe25c6191a2c3f8e52 \\\\\\n+ --hash=sha256:490c56b830772b0eca3b4b2cecb3741a1ed26b1d7206a279e1525dbf0aa95ee4 \\\\\\n+ --hash=sha256:4c68e983d0007d014bb01ad4bcbba78bc432c73a1755ff36d5102ceefa18299a \\\\\\n+ --hash=sha256:5671d2b2bf83bd4b6f02e55b32d432506eff5358c82f39b460a849ce19a2666e \\\\\\n+ --hash=sha256:56cb8c9055e50545fe6e3e5a560ec25a724673b2e4051f3c24d44e3ebc35dd72 \\\\\\n+ --hash=sha256:5841331c504e02d7c334591681cb8587cdd59dee7e149db6d3db8e3f9e9f02eb \\\\\\n+ --hash=sha256:592107a0faf6c9c3a63a8dbf13dfb1cbda1cf599b0bc11c953221b00204b9ce1 \\\\\\n+ --hash=sha256:5cf3b612542ba174c9da4000b59a4f4c81e8d66f87509be85d3a1b71b5c36413 \\\\\\n+ --hash=sha256:60cab3ab4ea468d31a33739ffd7e94ec3e37dea891d65a6582ecc8a477175191 \\\\\\n+ --hash=sha256:637445c1593a2a9d1024fda50082f07bb56baedda78d90a25f64b8111727ef94 \\\\\\n+ --hash=sha256:68b45e09834cd80523cb1eb274463073c7a9af4e4ef7cff34d9615f355572d32 \\\\\\n+ --hash=sha256:6caadcd1afb62630ff5c5ff353626eaa616553a5971295ad6dc2b19ca8a39620 \\\\\\n+ --hash=sha256:6e20a02775eb3cf0ffb4f0219b6d7c1f240336663d4e5d7028675ec247c790c4 \\\\\\n+ --hash=sha256:713f4ce4e82c26b53031f139de959bc9e8b54d3995aa824b89bbdf8229df2a45 \\\\\\n+ --hash=sha256:717aea574e0e5edba2868aa66b1caae335d8f1ad3fb29f01dd6502953fa823a1 \\\\\\n+ --hash=sha256:72df95fb1db41755b155c5f02106e0036a339250555c8d351d488704fd112cf9 \\\\\\n+ --hash=sha256:73e6df02a6a62f8045b511c272f894d08e56d174504c793c9effcbc6778051a8 \\\\\\n+ --hash=sha256:76a7be86d986223b9f1bdb7e7cbcdb048649901fdb956c598ef73bdab1786cd5 \\\\\\n+ --hash=sha256:7730d8197086f65d8969a991d6728a1d420a51b19fea06535c896cb43a1e05d0 \\\\\\n+ --hash=sha256:79900a9920a0b1d3a626c03a90ac6bf7042e78d46906a565b86a0dbe926f1d96 \\\\\\n+ --hash=sha256:7a7980a898a3e6ebe4de1896a0507e3d519edb53fb9b4bda478c9fbeb6514558 \\\\\\n+ --hash=sha256:8001925fa3dde51cb574e4c9de4c7efe77c4e4d64bd2fd2ef61d5651f9d04f3d \\\\\\n+ --hash=sha256:8660572b2d424bf5369ea8990985225f70bd1615b76ecd9c25588a3b9307009f \\\\\\n+ --hash=sha256:8b20f44773a9ab84400465e318712d8c2ca16418d35b9f80aa27fdf2d690ad10 \\\\\\n+ --hash=sha256:90915635b9648071129b0f72c0673cf8eac9eb84cfd445c5bedef30c714b1ec2 \\\\\\n+ --hash=sha256:9ccac776b2ca93b324806facd526ccb45da0fd035001c899a35b02c44431e209 \\\\\\n+ --hash=sha256:9d77c3be7b429875036ad0f0597c6e5cc6bb17894a4da005e3807de64d2673ad \\\\\\n+ --hash=sha256:9f07ae36c3b093e13687a894e79fe69e98a94c0b67fef656c575247682218143 \\\\\\n+ --hash=sha256:ab0f2e9d7d7d4db257f7cf53de3706c2baf124269571f20ffc2bcd6781f03063 \\\\\\n+ --hash=sha256:ad0764730e8e3421601c2cc7e1f054a9206c60ea0917165d8d9193dc453f34f1 \\\\\\n+ --hash=sha256:aff1f584c9538e8979cd180b1d70bf99bc16be19d4666414f49e5942b21a4f2c \\\\\\n+ --hash=sha256:b33dc30170a7402e03c180f2c5ef69dc077152f35b91621e9cebcde9c7d71746 \\\\\\n+ --hash=sha256:b5820d009aedb7ae9cfd32f98b1ab0c0bbd6268379c4fab042218b6b655c63f8 \\\\\\n+ --hash=sha256:bb8c7d05ea27a093a92b250904095d71d924b6b44e5795a415c1b20c265f0c65 \\\\\\n+ --hash=sha256:c01dd04044c472e47193b54f68e84e08d6ebf4f29551885aa959b015f7cd9747 \\\\\\n+ --hash=sha256:c53e9b1c36350df9965ec44d6c0d4e0bbbb38f720dd2b0e1256dc6524d411015 \\\\\\n+ --hash=sha256:c6559380469295c4009215fe1cab561301591a3bee2e2fb3f4f96d2273a3affc \\\\\\n+ --hash=sha256:cc2da5aa4edf14743fa9257e5ba3513963999f01211635702479d8e92b8207c8 \\\\\\n+ --hash=sha256:d1ea02fa8ab3d33eb1125eade81f7136341eb429152c6dbe2ae6f8bc33b3fbdd \\\\\\n+ --hash=sha256:d623801ae3dcd97b77b983400ef3d48bf976648e4efff19929175322eaae074d \\\\\\n+ --hash=sha256:d9145fe43ebb22e66672967c3fab411793b226ed776e4fe282271bca6ad3c0bb \\\\\\n+ --hash=sha256:dafa7c9dbe3d802f9bcdf261b29c8a70700fb22839947f06e471f62c46b6257f \\\\\\n+ --hash=sha256:dd207497bb985918409a1bb5db85d1875f74e1269487332113b73d1ee7c77647 \\\\\\n+ --hash=sha256:e10858f57ed0e74baa04393845f469fe8ad502c16ece4499bef7700c575611bd \\\\\\n+ --hash=sha256:e1bbeb7c506b07ee0422cf9b2f7212fefa4240957f03526d38d27bc6743a0a48 \\\\\\n+ --hash=sha256:e5f95f7b622e4171096d92175dda0a560f0955ade9b8a3a07bdcf151f7359611 \\\\\\n+ --hash=sha256:e9acb2c4d9cb532c3fedea74159f7b923c8c036328c9239b4049e7aa073bdd81 \\\\\\n+ --hash=sha256:e9f924aa610c0618445e1e8738c822c3190ce2a2699a0cb48ec3a351a96761f2 \\\\\\n+ --hash=sha256:ed1a5891e59472884a03cb9875483e8fc131c80a275c60967f8afc5458a0c8ff \\\\\\n+ --hash=sha256:ed68e27b8a61e57a3ccdc7c5a14499e00b54dfe223087204d5d40b3b5ef58b6d \\\\\\n+ --hash=sha256:eeab73050ea58c13dd56e329f594c1dfe32ebd7bb169bbdf4f8ceefbc31ec6b5 \\\\\\n+ --hash=sha256:f4dafd6d6ababfa3b14dd6e5f0378cb7c7d291895a31a40abcbb7cc74f396131 \\\\\\n+ --hash=sha256:f69ec5be85ef508e206153bed8eafd03f7995dc464356c8bbb279a1e2b7d56f3 \\\\\\n+ --hash=sha256:f76d1562643693b8a40066f1f96af795b93fd9bcfc9690a1af2ff4c5867ee29e \\\\\\n+ --hash=sha256:f839d29d0cc12048cf073d88ca4fdf94d420bc2b8afd69641ff6d496422ccd4f \\\\\\n+ --hash=sha256:f9180c362bde06fd05380298ded4e234fbc0d6ede0a864835bfd91c1e24283d5 \\\\\\n+ --hash=sha256:f9ff356e97e3ab09db07c8b675efa67340103874a0bae7465acb83dad7a35f7f \\\\\\n+ --hash=sha256:fa74636a49fc8077413ce8db3e85f1c4aff880788bb55bda56253118e036fe5b\\n+ # via contextual-orchestrator (pyproject.toml)\\n idna==3.18 \\\\\\n --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \\\\\\n --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848\\n- # via anyio\\n+ # via\\n+ # anyio\\n+ # requests\\n mako==1.3.12 \\\\\\n --hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \\\\\\n --hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a\\n@@ -216,11 +485,55 @@ markupsafe==3.0.3 \\\\\\n --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \\\\\\n --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50\\n # via mako\\n-psycopg[binary]==3.3.4 \\\\\\n+opentelemetry-api==1.44.0 \\\\\\n+ --hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \\\\\\n+ --hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef\\n+ # via\\n+ # contextual-orchestrator (pyproject.toml)\\n+ # opentelemetry-exporter-otlp-proto-http\\n+ # opentelemetry-sdk\\n+ # opentelemetry-semantic-conventions\\n+opentelemetry-exporter-otlp-proto-common==1.44.0 \\\\\\n+ --hash=sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694 \\\\\\n+ --hash=sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac\\n+ # via opentelemetry-exporter-otlp-proto-http\\n+opentelemetry-exporter-otlp-proto-http==1.44.0 \\\\\\n+ --hash=sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3 \\\\\\n+ --hash=sha256:c633d7270ad6b57cd4cfbe8b0007a9e2e7c0cb50bd6c50fe2a7b245f721a09d8\\n+ # via contextual-orchestrator (pyproject.toml)\\n+opentelemetry-proto==1.44.0 \\\\\\n+ --hash=sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56 \\\\\\n+ --hash=sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3\\n+ # via\\n+ # opentelemetry-exporter-otlp-proto-common\\n+ # opentelemetry-exporter-otlp-proto-http\\n+opentelemetry-sdk==1.44.0 \\\\\\n+ --hash=sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b \\\\\\n+ --hash=sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad\\n+ # via\\n+ # contextual-orchestrator (pyproject.toml)\\n+ # opentelemetry-exporter-otlp-proto-http\\n+opentelemetry-semantic-conventions==0.65b0 \\\\\\n+ --hash=sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb \\\\\\n+ --hash=sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60\\n+ # via opentelemetry-sdk\\n+protobuf==7.36.0 \\\\\\n+ --hash=sha256:1781cc1de61249b750848029bca452c0a8b7e990080316b9bbc2518b2117b488 \\\\\\n+ --hash=sha256:3297e60abdff301e5f74393d87f6cc59dacab5f024a89548a6e8de1d26576b16 \\\\\\n+ --hash=sha256:53374d53fc29a67f7dbbf0ade47d7526a0f0137bf0f9c90e48d8a60790ef748c \\\\\\n+ --hash=sha256:70f5ec8eb0da81a44360c0dc0beac99a0d78071d21956a7076bae8bd2051841b \\\\\\n+ --hash=sha256:7326fd717bdc419162a735938d89d4032332bcc3408804012b24ff3a37086071 \\\\\\n+ --hash=sha256:9103532dffd80c6fab7e50c65a31007680a06eb57537d437bb1b35812c138a37 \\\\\\n+ --hash=sha256:bf94a5917c71058262de683669bc0a797a7669d3de71f0b36d058e3194f47b44 \\\\\\n+ --hash=sha256:e8e09cb0d794c6687926fa558a8a6e72aa10edb997d5ca61da0765f12a3e00ea\\n+ # via\\n+ # googleapis-common-protos\\n+ # opentelemetry-proto\\n+psycopg==3.3.4 \\\\\\n --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \\\\\\n --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc\\n # via contextual-orchestrator (pyproject.toml)\\n-psycopg-binary==3.3.4 \\\\\\n+psycopg-binary==3.3.4 ; implementation_name != 'pypy' \\\\\\n --hash=sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070 \\\\\\n --hash=sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c \\\\\\n --hash=sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc \\\\\\n@@ -403,6 +716,14 @@ pydantic-core==2.46.4 \\\\\\n --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \\\\\\n --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae\\n # via pydantic\\n+requests==2.34.2 \\\\\\n+ --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \\\\\\n+ --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed\\n+ # via opentelemetry-exporter-otlp-proto-http\\n+sortedcontainers==2.4.0 \\\\\\n+ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \\\\\\n+ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0\\n+ # via hypothesis\\n sqlalchemy==2.0.51 \\\\\\n --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \\\\\\n --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \\\\\\n@@ -463,35 +784,94 @@ sqlalchemy==2.0.51 \\\\\\n --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \\\\\\n --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1\\n # via\\n- # alembic\\n # contextual-orchestrator (pyproject.toml)\\n+ # alembic\\n starlette==1.3.1 \\\\\\n --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \\\\\\n --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6\\n # via fastapi\\n+tomli==2.4.1 ; python_full_version < '3.11' \\\\\\n+ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \\\\\\n+ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \\\\\\n+ --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \\\\\\n+ --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \\\\\\n+ --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \\\\\\n+ --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \\\\\\n+ --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \\\\\\n+ --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \\\\\\n+ --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \\\\\\n+ --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \\\\\\n+ --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \\\\\\n+ --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \\\\\\n+ --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \\\\\\n+ --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \\\\\\n+ --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \\\\\\n+ --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \\\\\\n+ --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \\\\\\n+ --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \\\\\\n+ --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \\\\\\n+ --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \\\\\\n+ --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \\\\\\n+ --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \\\\\\n+ --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \\\\\\n+ --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \\\\\\n+ --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \\\\\\n+ --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \\\\\\n+ --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \\\\\\n+ --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \\\\\\n+ --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \\\\\\n+ --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \\\\\\n+ --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \\\\\\n+ --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \\\\\\n+ --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \\\\\\n+ --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \\\\\\n+ --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \\\\\\n+ --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \\\\\\n+ --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \\\\\\n+ --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \\\\\\n+ --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \\\\\\n+ --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \\\\\\n+ --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \\\\\\n+ --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \\\\\\n+ --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \\\\\\n+ --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \\\\\\n+ --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \\\\\\n+ --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \\\\\\n+ --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049\\n+ # via alembic\\n typing-extensions==4.15.0 \\\\\\n --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \\\\\\n --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548\\n # via\\n # alembic\\n # anyio\\n+ # exceptiongroup\\n # fastapi\\n+ # opentelemetry-api\\n+ # opentelemetry-exporter-otlp-proto-http\\n+ # opentelemetry-sdk\\n+ # opentelemetry-semantic-conventions\\n # psycopg\\n # pydantic\\n # pydantic-core\\n # sqlalchemy\\n # starlette\\n # typing-inspection\\n+ # uvicorn\\n typing-inspection==0.4.2 \\\\\\n --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \\\\\\n --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464\\n # via\\n # fastapi\\n # pydantic\\n-tzdata==2026.2 \\\\\\n- --hash=sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10 \\\\\\n- --hash=sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7\\n+tzdata==2026.3 ; sys_platform == 'win32' \\\\\\n+ --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \\\\\\n+ --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931\\n # via psycopg\\n+urllib3==2.7.0 \\\\\\n+ --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \\\\\\n+ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897\\n+ # via requests\\n uvicorn==0.49.0 \\\\\\n --hash=sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f \\\\\\n --hash=sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3\" }, { \"sha\": \"33706289864d0fa610ae03e7cdb83f48384cf81d\", \"filename\": \"tests/test_analytics_runtime.py\", \"status\": \"modified\", \"additions\": 21, \"deletions\": 0, \"changes\": 21, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_analytics_runtime.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_analytics_runtime.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_analytics_runtime.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -97,6 +97,27 @@ def test_analytics_snapshot_measures_runtime_kpis_and_guardrails() -> None:\\n assert guardrails[\\\"locale_key_parity\\\"][\\\"value_percent\\\"] == 100.0\\n \\n \\n+def test_analytics_snapshot_reads_runtime_collections_under_one_lock() -> None:\\n+ class CountingLock:\\n+ def __init__(self) -> None:\\n+ self.enter_count = 0\\n+\\n+ def __enter__(self):\\n+ self.enter_count += 1\\n+ return self\\n+\\n+ def __exit__(self, *_args: object) -> None:\\n+ return None\\n+\\n+ orchestrator = build()\\n+ lock = CountingLock()\\n+ orchestrator._workflow_run_lock = lock\\n+\\n+ orchestrator.analytics_snapshot()\\n+\\n+ assert lock.enter_count == 1\\n+\\n+\\n def test_analytics_endpoint_and_admin_console_use_source_backed_snapshot() -> None:\\n assert \\\"/api/v1/analytics_snapshots/latest\\\" in OPENAPI_SPEC[\\\"paths\\\"]\\n assert OPENAPI_SPEC[\\\"paths\\\"][\\\"/api/v1/analytics_snapshots/latest\\\"][\\\"get\\\"][\\\"operationId\\\"] == (\" }, { \"sha\": \"4bc90eb884c8e9d6988e5c3de95d18d161b6c3a2\", \"filename\": \"tests/test_api_contract.py\", \"status\": \"modified\", \"additions\": 27, \"deletions\": 0, \"changes\": 27, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_api_contract.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_api_contract.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_api_contract.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,34 @@ def test_openapi_documents_compatibility_front_door() -> None:\\n ]\\n \\n \\n+def test_openapi_documents_orchestrator_owned_embedding_model_selection() -> None:\\n+ embeddings_schema = OPENAPI_SPEC[\\\"paths\\\"][\\\"/v1/embeddings\\\"][\\\"post\\\"][\\\"requestBody\\\"][\\\"content\\\"][\\n+ \\\"application/json\\\"\\n+ ][\\\"schema\\\"]\\n+ batch_schema = OPENAPI_SPEC[\\\"paths\\\"][\\\"/v1/batch/embeddings\\\"][\\\"post\\\"][\\\"requestBody\\\"][\\\"content\\\"][\\n+ \\\"application/json\\\"\\n+ ][\\\"schema\\\"]\\n+\\n+ assert embeddings_schema[\\\"required\\\"] == [\\\"input\\\"]\\n+ assert \\\"model\\\" not in batch_schema.get(\\\"required\\\", [])\\n+ assert batch_schema[\\\"anyOf\\\"] == [\\n+ {\\\"required\\\": [\\\"input\\\"]},\\n+ {\\\"required\\\": [\\\"inputs\\\"]},\\n+ ]\\n+ assert \\\"Optional enabled embedding-capable pool model\\\" in embeddings_schema[\\\"properties\\\"][\\\"model\\\"][\\n+ \\\"description\\\"\\n+ ]\\n+\\n+\\n+def test_openapi_documents_unsupported_responses_controls() -> None:\\n+ responses = OPENAPI_SPEC[\\\"paths\\\"][\\\"/v1/responses\\\"][\\\"post\\\"][\\\"responses\\\"]\\n+\\n+ assert \\\"422\\\" in responses\\n+\\n+\\n if __name__ == \\\"__main__\\\": # pragma: no cover\\n test_rest_resource_paths_use_two_word_snake_case()\\n test_openapi_uses_resource_oriented_operation_ids()\\n+ test_openapi_documents_orchestrator_owned_embedding_model_selection()\\n+ test_openapi_documents_unsupported_responses_controls()\\n print(\\\"ok\\\")\" }, { \"sha\": \"e2aafa131862da0390ed13890731c589815e23c5\", \"filename\": \"tests/test_batch_optimizer.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_batch_optimizer.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_batch_optimizer.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_batch_optimizer.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -129,7 +129,7 @@ def test_batch_route_rejects_incomplete_or_empty_provider_results(kind: str) ->\\n \\n def test_batch_chat_rejects_incomplete_local_result_set() -> None:\\n client = ModelClient()\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"model-x\\\", base_url=\\\"local://127.0.0.1:1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"model-x\\\", base_url=\\\"local://127.0.0.1:1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n requests = {\\n \\\"task_0\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"one\\\"}],\\n \\\"task_1\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"two\\\"}],\" }, { \"sha\": \"9a84e5179abc2b0b65ad620ebc4b3d1ee28a5f44\", \"filename\": \"tests/test_batch_routing.py\", \"status\": \"modified\", \"additions\": 27, \"deletions\": 0, \"changes\": 27, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_batch_routing.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_batch_routing.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_batch_routing.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -19,6 +19,11 @@\\n )\\n from contextual_orchestrator.cost_ledger import PriceBook, PriceEntry # noqa: E402\\n from contextual_orchestrator.kv_config import InMemoryConfigStore # noqa: E402\\n+from contextual_orchestrator.telemetry import ( # noqa: E402\\n+ current_session_id,\\n+ reset_session_id,\\n+ set_session_id,\\n+)\\n \\n # ---------------------------------------------------------------------------\\n # Sync-vs-batch decision\\n@@ -122,6 +127,28 @@ def runner(messages, mode):\\n assert [item.custom_id for item in backend.retrieve(job)] == [\\\"a\\\", \\\"b\\\"]\\n \\n \\n+def test_local_backend_workers_inherit_session_id() -> None:\\n+ \\\"\\\"\\\"Batch workers retain the caller session for provider telemetry.\\\"\\\"\\\"\\n+ observed: list[str | None] = []\\n+\\n+ def runner(messages, mode):\\n+ observed.append(current_session_id())\\n+ return {\\\"answer\\\": messages[-1][\\\"content\\\"], \\\"mode\\\": mode}\\n+\\n+ backend = LocalBatchBackend(runner, max_concurrency=2)\\n+ requests = [\\n+ BatchRequest(messages=[{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"one\\\"}], custom_id=\\\"a\\\"),\\n+ BatchRequest(messages=[{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"two\\\"}], custom_id=\\\"b\\\"),\\n+ ]\\n+ token = set_session_id(\\\"post-session\\\")\\n+ try:\\n+ backend.submit(requests)\\n+ finally:\\n+ reset_session_id(token)\\n+\\n+ assert observed == [\\\"post-session\\\", \\\"post-session\\\"]\\n+\\n+\\n # ---------------------------------------------------------------------------\\n # pg-llm-batch backend (mocked async client mirroring BatchAPIClient)\\n # ---------------------------------------------------------------------------\" }, { \"sha\": \"30628947dad405f6c732f62f2713f886df95dc8c\", \"filename\": \"tests/test_bool_01_seed_str_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 3, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_bool_01_seed_str_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_bool_01_seed_str_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_bool_01_seed_str_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -144,15 +144,16 @@ def test_http_chat_parallel_tool_calls_one_requires_tools() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_seed_digit_string() -> None:\\n+def test_http_responses_rejects_unapplied_seed_digit_string() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n port,\\n \\\"/v1/responses\\\",\\n {\\\"model\\\": \\\"mock-planner\\\", \\\"input\\\": \\\"seed str\\\", \\\"seed\\\": \\\"42\\\"},\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -182,6 +183,6 @@ def test_http_completions_rejects_seed_digit_string_as_unsupported() -> None:\\n test_http_chat_accepts_stream_zero_as_false()\\n test_http_chat_accepts_parallel_tool_calls_zero()\\n test_http_chat_parallel_tool_calls_one_requires_tools()\\n- test_http_responses_accepts_seed_digit_string()\\n+ test_http_responses_rejects_unapplied_seed_digit_string()\\n test_http_completions_rejects_seed_digit_string_as_unsupported()\\n print(\\\"ok\\\")\" }, { \"sha\": \"2a151cb99bcda15642298b30dec9cea6cf916e0c\", \"filename\": \"tests/test_budget_enforcement.py\", \"status\": \"modified\", \"additions\": 69, \"deletions\": 0, \"changes\": 69, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_budget_enforcement.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_budget_enforcement.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_budget_enforcement.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -10,10 +10,13 @@\\n import json\\n from pathlib import Path\\n import sys\\n+import tempfile\\n import threading\\n import urllib.error\\n import urllib.request\\n \\n+import pytest\\n+\\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n@@ -75,6 +78,72 @@ def test_cost_budget_blocks() -> None:\\n assert raised\\n \\n \\n+def test_unpersisted_provider_usage_remains_in_the_budget_ledger() -> None:\\n+ agent = ModelAgent(\\\"general_agent\\\", \\\"priced-model\\\", tags=(\\\"reasoning\\\",))\\n+ orchestrator = TaskOrchestrator(\\n+ [agent],\\n+ price_per_million={\\\"priced-model\\\": 1_000_000.0},\\n+ budget_max_cost_usd=1.0,\\n+ )\\n+\\n+ orchestrator._record_in_flight_provider_usage(\\n+ agent,\\n+ {\\\"completion_tokens\\\": 1},\\n+ \\\"\\\",\\n+ )\\n+\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orchestrator._raise_if_spend_budget_exceeded()\\n+\\n+ assert orchestrator.budget_status()[\\\"spent_cost_usd\\\"] == 1.0\\n+\\n+\\n+def test_per_call_budget_gate_does_not_rescan_workflow_runs(monkeypatch) -> None:\\n+ \\\"\\\"\\\"Read the synchronized meter instead of rebuilding buyer spend analytics.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator([_agent()], budget_max_output_tokens=1)\\n+ orchestrator._record_in_flight_provider_usage(\\n+ _agent(),\\n+ {\\\"completion_tokens\\\": 1},\\n+ \\\"\\\",\\n+ )\\n+ monkeypatch.setattr(\\n+ orchestrator,\\n+ \\\"spend_analytics\\\",\\n+ lambda: pytest.fail(\\\"budget gate must use the incremental meter\\\"),\\n+ )\\n+\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orchestrator._raise_if_spend_budget_exceeded()\\n+\\n+\\n+def test_provider_budget_meter_survives_restart() -> None:\\n+ with tempfile.TemporaryDirectory() as directory:\\n+ state_db = str(Path(directory) / \\\"state.db\\\")\\n+ first = TaskOrchestrator(\\n+ [_agent()],\\n+ state_db=state_db,\\n+ budget_max_output_tokens=2,\\n+ )\\n+ first._record_in_flight_provider_usage(\\n+ _agent(),\\n+ {\\\"completion_tokens\\\": 2},\\n+ \\\"\\\",\\n+ )\\n+ first.close()\\n+\\n+ second = TaskOrchestrator(\\n+ [_agent()],\\n+ state_db=state_db,\\n+ budget_max_output_tokens=2,\\n+ )\\n+ try:\\n+ assert second.budget_status()[\\\"spent_output_tokens\\\"] == 2\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ second._raise_if_spend_budget_exceeded()\\n+ finally:\\n+ second.close()\\n+\\n+\\n def test_http_over_budget_returns_429() -> None:\\n token = \\\"budget_token\\\"\\n orchestrator = TaskOrchestrator([_agent()], budget_max_output_tokens=1)\" }, { \"sha\": \"d9e72b6ca5738f5f3f4005cd3ca767157f610097\", \"filename\": \"tests/test_chat_capability_unknown_identifiers.py\", \"status\": \"added\", \"additions\": 47, \"deletions\": 0, \"changes\": 47, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_capability_unknown_identifiers.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_capability_unknown_identifiers.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_capability_unknown_identifiers.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,47 @@\\n+\\\"\\\"\\\"Regressions for conservative treatment of unknown model identifiers.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import sys\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator.chat_capability import ( # noqa: E402\\n+ is_chat_compatible_model_id,\\n+ is_general_chat_agent_model_id,\\n+)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"vendor/vanguard-7b\\\",\\n+ \\\"vendor/vanguard-instruct\\\",\\n+ ],\\n+)\\n+def test_unknown_names_that_merely_end_with_guard_remain_eligible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Do not fabricate a policy-classifier capability from an unrelated word suffix.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id)\\n+ assert is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"meta-llama/llama-guard-4-12b\\\",\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-topic-control\\\",\\n+ \\\"google/shieldgemma-2b-it\\\",\\n+ ],\\n+)\\n+def test_explicit_policy_classifier_markers_remain_role_ineligible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Keep exact guard, safety, and NemoGuard markers out of general synthesis roles.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id)\\n+ assert not is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"7d5fd8ea1f6e8356edd4ebe0ccb4513e5d49942c\", \"filename\": \"tests/test_chat_developer_multimodal_content_http_honesty.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_developer_multimodal_content_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_developer_multimodal_content_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_developer_multimodal_content_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"vision\\\"))]\\n )\\n \\n \" }, { \"sha\": \"4b7e93b91692f6bd6f6d090aec6bface96efd2f1\", \"filename\": \"tests/test_chat_model_capability_isolation.py\", \"status\": \"added\", \"additions\": 391, \"deletions\": 0, \"changes\": 391, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_model_capability_isolation.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_model_capability_isolation.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_model_capability_isolation.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,391 @@\\n+\\\"\\\"\\\"Regression coverage for isolating non-chat models from chat agent discovery.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import json\\n+import sys\\n+from pathlib import Path\\n+from unittest.mock import patch\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator.chat_capability import ( # noqa: E402\\n+ is_chat_compatible_model_id,\\n+)\\n+from contextual_orchestrator.credentials import ( # noqa: E402\\n+ InMemoryCredentialBackend,\\n+ register_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.cost_ledger import PriceBook, PriceEntry # noqa: E402\\n+from contextual_orchestrator.kv_config import InMemoryConfigStore # noqa: E402\\n+from contextual_orchestrator.model_discovery import ( # noqa: E402\\n+ DiscoveredModel,\\n+ ProviderModelSource,\\n+ agent_from_discovered,\\n+ discover_provider_models,\\n+ refresh_price_book,\\n+ select_cheapest_discovered_agent,\\n+ select_top_n_cheapest_discovered_agents,\\n+)\\n+from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n+\\n+\\n+class _Response:\\n+ \\\"\\\"\\\"Small context-managed HTTP response used by the offline regression.\\\"\\\"\\\"\\n+\\n+ def __init__(self, payload: dict[str, object]) -> None:\\n+ self._body = json.dumps(payload).encode(\\\"utf-8\\\")\\n+\\n+ def __enter__(self) -> \\\"_Response\\\":\\n+ return self\\n+\\n+ def __exit__(self, *_args: object) -> bool:\\n+ return False\\n+\\n+ def read(self, _size: int = -1) -> bytes:\\n+ return self._body\\n+\\n+\\n+@pytest.fixture(autouse=True)\\n+def _fresh_credential_backend():\\n+ \\\"\\\"\\\"Keep the provider credential registry isolated between tests.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ yield\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+def _model(model_id: str, *, priced: bool = False) -> DiscoveredModel:\\n+ \\\"\\\"\\\"Build one synthetic discovered model for capability-boundary tests.\\\"\\\"\\\"\\n+ return DiscoveredModel(\\n+ provider_name=\\\"enterprise_gateway\\\",\\n+ model_id=model_id,\\n+ credential_name=\\\"GATEWAY_API_KEY\\\",\\n+ chat_base_url=\\\"https://gateway.example.test/v1\\\",\\n+ auth_scheme=\\\"Bearer\\\",\\n+ prompt_price_per_1k=1.0 if priced else None,\\n+ completion_price_per_1k=1.0 if priced else None,\\n+ )\\n+\\n+\\n+def _agent(\\n+ agent_id: str,\\n+ model_id: str,\\n+ *,\\n+ priority: int = 0,\\n+ tags: tuple[str, ...] = (\\\"writing\\\",),\\n+) -> ModelAgent:\\n+ \\\"\\\"\\\"Build one mock-backed runtime agent for selection-path regressions.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ id=agent_id,\\n+ model=model_id,\\n+ base_url=\\\"mock://local\\\",\\n+ priority=priority,\\n+ tags=tags,\\n+ )\\n+\\n+\\n+def test_embedding_deployments_never_enter_chat_agent_discovery() -> None:\\n+ \\\"\\\"\\\"Exclude the exact Azure embedding deployment seen in synthesis alerts.\\\"\\\"\\\"\\n+ register_credential(\\\"GATEWAY_API_KEY\\\", \\\"gateway-secret\\\")\\n+ source = ProviderModelSource(\\n+ provider_name=\\\"enterprise_gateway\\\",\\n+ credential_name=\\\"GATEWAY_API_KEY\\\",\\n+ list_url=\\\"https://gateway.example.test/v1/models\\\",\\n+ chat_base_url=\\\"https://gateway.example.test/v1\\\",\\n+ )\\n+ payload = {\\n+ \\\"data\\\": [\\n+ {\\\"id\\\": \\\"azure/text-embedding-3-large\\\"},\\n+ {\\\"id\\\": \\\"text_embedding_3_large\\\"},\\n+ {\\\"id\\\": \\\"BAAI/bge-m3\\\"},\\n+ {\\\"id\\\": \\\"openai/whisper-1\\\"},\\n+ {\\\"id\\\": \\\"gpt-4o-mini-transcribe\\\"},\\n+ {\\\"id\\\": \\\"text-moderation-latest\\\"},\\n+ {\\\"id\\\": \\\"company/reranker-v2\\\"},\\n+ {\\\"id\\\": \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\"},\\n+ {\\\"id\\\": \\\"gpt-audio\\\"},\\n+ {\\\"id\\\": \\\"gpt-5.2\\\"},\\n+ {\\\"id\\\": \\\"qwen/qwen3-235b-a22b-instruct\\\"},\\n+ ]\\n+ }\\n+\\n+ with patch(\\n+ \\\"contextual_orchestrator.model_discovery._fetch_json\\\",\\n+ return_value=payload,\\n+ ):\\n+ discovered = discover_provider_models(source)\\n+\\n+ assert [model.model_id for model in discovered] == [\\n+ \\\"gpt-audio\\\",\\n+ \\\"gpt-5.2\\\",\\n+ \\\"qwen/qwen3-235b-a22b-instruct\\\",\\n+ ]\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"model_id\\\", \\\"expected\\\"),\\n+ [\\n+ (None, False),\\n+ (\\\"\\\", False),\\n+ (\\\"---\\\", False),\\n+ (\\\"vendor/embeddingv2\\\", False),\\n+ (\\\"vendor/reranking-v2\\\", False),\\n+ (\\\"vendor/transcriber-v2\\\", False),\\n+ (\\\"gpt-5.2\\\", True),\\n+ (\\\"qwen/qwen3-instruct\\\", True),\\n+ ],\\n+)\\n+def test_chat_compatibility_normalizes_identifiers(\\n+ model_id: object, expected: bool\\n+) -> None:\\n+ \\\"\\\"\\\"Normalize provider prefixes and separators without guessing chat features.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id) is expected # type: ignore[arg-type]\\n+\\n+\\n+def test_bytez_chat_catalog_still_rejects_non_chat_identifiers() -> None:\\n+ \\\"\\\"\\\"Apply the same boundary even when a provider accepts a chat task filter.\\\"\\\"\\\"\\n+ register_credential(\\\"BYTEZ_API_KEY\\\", \\\"bytez-secret\\\")\\n+ source = ProviderModelSource(\\n+ provider_name=\\\"bytez\\\",\\n+ credential_name=\\\"BYTEZ_API_KEY\\\",\\n+ list_url=\\\"https://api.bytez.com/models/v2/list/models\\\",\\n+ chat_base_url=\\\"https://api.bytez.com/models/v2/openai/v1\\\",\\n+ auth_scheme=\\\"Key\\\",\\n+ style=\\\"bytez\\\",\\n+ task_filter=\\\"chat\\\",\\n+ )\\n+ payload = {\\n+ \\\"output\\\": [\\n+ {\\\"modelId\\\": \\\"vendor/embeddingv2\\\"},\\n+ {\\\"modelId\\\": \\\"vendor/chat-instruct\\\"},\\n+ ]\\n+ }\\n+\\n+ with patch(\\n+ \\\"contextual_orchestrator.model_discovery._fetch_json\\\",\\n+ return_value=payload,\\n+ ):\\n+ discovered = discover_provider_models(source)\\n+\\n+ assert [model.model_id for model in discovered] == [\\\"vendor/chat-instruct\\\"]\\n+\\n+\\n+def test_non_chat_discovery_cannot_be_converted_to_agent() -> None:\\n+ \\\"\\\"\\\"Keep manually constructed discovery rows from bypassing the parser filter.\\\"\\\"\\\"\\n+ with pytest.raises(ValueError, match=\\\"general chat agent\\\"):\\n+ agent_from_discovered(_model(\\\"azure/text-embedding-3-large\\\"))\\n+\\n+\\n+def test_non_chat_discovery_is_not_priced_or_selected_for_chat() -> None:\\n+ \\\"\\\"\\\"Keep price routing from reintroducing an incompatible endpoint model.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ embedding_model = _model(\\\"azure/text-embedding-3-large\\\", priced=True)\\n+ chat_model = _model(\\\"gpt-5.2\\\", priced=True)\\n+ price_book.set_price(PriceEntry(\\\"enterprise_gateway\\\", \\\"gpt-5.2\\\", 1.0, 1.0))\\n+\\n+ assert refresh_price_book([embedding_model, chat_model], price_book) == 1\\n+ assert price_book.get_price(\\n+ \\\"enterprise_gateway\\\", \\\"azure/text-embedding-3-large\\\"\\n+ ) is None\\n+ assert select_cheapest_discovered_agent([embedding_model], price_book) is None\\n+ assert select_top_n_cheapest_discovered_agents(\\n+ [embedding_model], price_book, 1\\n+ ) == []\\n+\\n+\\n+def test_stale_embedding_agent_cannot_win_synthesizer_selection() -> None:\\n+ \\\"\\\"\\\"Exclude an already-persisted embedding row even when it has high priority.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ priority=10_000,\\n+ )\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent])\\n+\\n+ assert orchestrator._select_agent(\\\"Produce the final answer.\\\", \\\"synthesizer\\\") is chat_agent\\n+\\n+\\n+def test_all_non_chat_agents_fail_before_synthesis() -> None:\\n+ \\\"\\\"\\\"Fail closed when a stale pool contains no chat-compatible worker.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator(\\n+ [_agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")]\\n+ )\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"chat-compatible\\\"):\\n+ orchestrator._select_agent(\\\"Produce the final answer.\\\", \\\"synthesizer\\\")\\n+\\n+\\n+def test_generated_plan_reselects_non_chat_agent_assignment() -> None:\\n+ \\\"\\\"\\\"Do not trust a generated plan that names a stale embedding agent directly.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ priority=10_000,\\n+ )\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent])\\n+ raw_plan = json.dumps(\\n+ {\\n+ \\\"steps\\\": [\\n+ {\\n+ \\\"id\\\": 0,\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"chat_agent\\\",\\n+ \\\"subtask\\\": \\\"Execute the task.\\\",\\n+ \\\"access\\\": [],\\n+ },\\n+ {\\n+ \\\"id\\\": 1,\\n+ \\\"role\\\": \\\"synthesizer\\\",\\n+ \\\"agent_id\\\": \\\"embedding_agent\\\",\\n+ \\\"subtask\\\": \\\"Produce the final answer.\\\",\\n+ \\\"access\\\": [0],\\n+ },\\n+ ]\\n+ }\\n+ )\\n+\\n+ steps = orchestrator._parse_workflow_plan(raw_plan)\\n+\\n+ assert steps[-1].agent_id == \\\"chat_agent\\\"\\n+\\n+\\n+def test_failover_candidates_exclude_stale_embedding_agents() -> None:\\n+ \\\"\\\"\\\"Keep cross-agent retry from falling through to an incompatible endpoint.\\\"\\\"\\\"\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ embedding_agent = _agent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ priority=10_000,\\n+ )\\n+ orchestrator = TaskOrchestrator([chat_agent, embedding_agent])\\n+\\n+ candidates = orchestrator._failover_candidates(\\n+ chat_agent,\\n+ \\\"Produce the final answer.\\\",\\n+ \\\"synthesizer\\\",\\n+ )\\n+\\n+ assert candidates == [chat_agent]\\n+\\n+\\n+def test_invoke_fails_clearly_when_no_general_chat_agent_remains() -> None:\\n+ \\\"\\\"\\\"Report the role boundary instead of claiming that zero candidates failed.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent])\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"no chat-compatible agent available\\\"):\\n+ orchestrator._invoke(\\n+ embedding_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Produce the final answer.\\\"}],\\n+ text=\\\"Produce the final answer.\\\",\\n+ role=\\\"worker\\\",\\n+ )\\n+\\n+\\n+def test_model_client_rejects_non_chat_model_before_mock_or_network_call() -> None:\\n+ \\\"\\\"\\\"Keep the provider boundary fail-closed even when selection is bypassed.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ client.chat(\\n+ embedding_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Produce the final answer.\\\"}],\\n+ )\\n+\\n+\\n+def test_non_chat_primary_fails_over_only_to_chat_agents() -> None:\\n+ \\\"\\\"\\\"Drop an incompatible primary while retaining a compatible fallback.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent])\\n+\\n+ candidates = orchestrator._failover_candidates(\\n+ embedding_agent,\\n+ \\\"Produce the final answer.\\\",\\n+ \\\"synthesizer\\\",\\n+ )\\n+\\n+ assert candidates == [chat_agent]\\n+\\n+\\n+def test_streaming_client_rejects_non_chat_model_before_transport() -> None:\\n+ \\\"\\\"\\\"Apply the same endpoint boundary to streaming chat requests.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ next(\\n+ client.stream_chat(\\n+ embedding_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Produce the final answer.\\\"}],\\n+ )\\n+ )\\n+\\n+\\n+def test_probe_reports_non_chat_model_without_provider_transport(monkeypatch) -> None:\\n+ \\\"\\\"\\\"Readiness must fail closed with a stable code before network access.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ monkeypatch.setattr(\\n+ client,\\n+ \\\"_validate_provider\\\",\\n+ lambda _agent: (_ for _ in ()).throw(AssertionError(\\\"transport reached\\\")),\\n+ )\\n+\\n+ assert client.probe(embedding_agent)[\\\"failure_code\\\"] == \\\"non_chat_model\\\"\\n+\\n+\\n+def test_generated_planner_inventory_excludes_non_chat_agents() -> None:\\n+ \\\"\\\"\\\"Do not advertise stale endpoint-incompatible agents to the planner.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))\\n+\\n+ class PlannerClient:\\n+ def __init__(self) -> None:\\n+ self.system_prompt = \\\"\\\"\\n+\\n+ def chat(self, _agent, messages, **_kwargs):\\n+ self.system_prompt = messages[0][\\\"content\\\"]\\n+ return json.dumps(\\n+ {\\n+ \\\"steps\\\": [\\n+ {\\n+ \\\"id\\\": 0,\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"chat_agent\\\",\\n+ \\\"subtask\\\": \\\"Execute the task.\\\",\\n+ \\\"access\\\": [],\\n+ },\\n+ {\\n+ \\\"id\\\": 1,\\n+ \\\"role\\\": \\\"synthesizer\\\",\\n+ \\\"agent_id\\\": \\\"chat_agent\\\",\\n+ \\\"subtask\\\": \\\"Produce the answer.\\\",\\n+ \\\"access\\\": [0],\\n+ },\\n+ ]\\n+ }\\n+ )\\n+\\n+ client = PlannerClient()\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent], client=client)\\n+\\n+ steps = orchestrator._plan_generated(\\\"Produce the final answer.\\\")\\n+\\n+ assert steps[-1].agent_id == \\\"chat_agent\\\"\\n+ assert \\\"embedding_agent\\\" not in client.system_prompt\\n+ assert \\\"azure/text-embedding-3-large\\\" not in client.system_prompt\\n+ assert \\\"chat_agent\\\" in client.system_prompt\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"9760da0f429d04c4ebc3735d0a23efc49b8787cc\", \"filename\": \"tests/test_chat_parallel_tool_calls_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 13, \"changes\": 17, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_parallel_tool_calls_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_parallel_tool_calls_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_parallel_tool_calls_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -117,8 +117,8 @@ def test_http_chat_parallel_tool_calls_non_boolean_fail_closed() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_parallel_tool_calls_true_with_tools_passthrough() -> None:\\n- \\\"\\\"\\\"With tools, parallel_tool_calls triggers single-agent passthrough path.\\\"\\\"\\\"\\n+def test_http_chat_parallel_tool_calls_true_rejects_single_agent_fallback() -> None:\\n+ \\\"\\\"\\\"With tools, the gateway does not silently downgrade to one agent.\\\"\\\"\\\"\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -130,17 +130,8 @@ def test_http_chat_parallel_tool_calls_true_with_tools_passthrough() -> None:\\n \\\"parallel_tool_calls\\\": True,\\n },\\n )\\n- # Mock passthrough returns chat-shaped body\\n- assert status == 200, body\\n- assert \\\"choices\\\" in body or \\\"id\\\" in body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n-\\n-\\n-if __name__ == \\\"__main__\\\":\\n- test_http_chat_parallel_tool_calls_false_without_tools_ok()\\n- test_http_chat_parallel_tool_calls_true_without_tools_fail_closed()\\n- test_http_chat_parallel_tool_calls_non_boolean_fail_closed()\\n- test_http_chat_parallel_tool_calls_true_with_tools_passthrough()\\n- print(\\\"ok\\\")\" }, { \"sha\": \"927c213243303d7343bc0b9feb2d0ab58f3ea9c5\", \"filename\": \"tests/test_chat_passthrough_capability_isolation.py\", \"status\": \"added\", \"additions\": 127, \"deletions\": 0, \"changes\": 127, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_passthrough_capability_isolation.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_passthrough_capability_isolation.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_passthrough_capability_isolation.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,127 @@\\n+\\\"\\\"\\\"Regression tests for chat-capability checks on passthrough and batch paths.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import sys\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n+\\n+\\n+def _embedding_agent() -> ModelAgent:\\n+ \\\"\\\"\\\"Build the stale embedding agent from the production incident.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ base_url=\\\"mock://local\\\",\\n+ )\\n+\\n+\\n+def _chat_agent() -> ModelAgent:\\n+ \\\"\\\"\\\"Build one compatible fallback for explicit-model passthrough tests.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ \\\"general_chat_agent\\\",\\n+ \\\"gpt-5.2\\\",\\n+ base_url=\\\"mock://local\\\",\\n+ tags=(\\\"writing\\\",),\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"endpoint\\\",\\n+ [\\n+ \\\"chat/completions\\\",\\n+ \\\"/v1/chat/completions\\\",\\n+ \\\"completions\\\",\\n+ \\\"/v1/completions\\\",\\n+ \\\"responses\\\",\\n+ \\\"/v1/responses\\\",\\n+ ],\\n+)\\n+def test_proxy_send_rejects_embedding_before_mock_or_network_transport(endpoint: str) -> None:\\n+ \\\"\\\"\\\"Keep raw OpenAI passthrough from bypassing the chat transport invariant.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ client.proxy_send(\\n+ _embedding_agent(),\\n+ endpoint,\\n+ {\\n+ \\\"model\\\": \\\"azure/text-embedding-3-large\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Return JSON.\\\"}],\\n+ \\\"input\\\": \\\"Return JSON.\\\",\\n+ },\\n+ )\\n+\\n+\\n+def test_explicit_embedding_model_cannot_bypass_through_structured_passthrough() -> None:\\n+ \\\"\\\"\\\"Reject an explicitly requested stale embedding agent before raw proxy transport.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator([_embedding_agent(), _chat_agent()])\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"azure/text-embedding-3-large\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Return JSON.\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+\\n+def test_explicit_embedding_model_cannot_bypass_through_responses_passthrough() -> None:\\n+ \\\"\\\"\\\"Apply the same transport contract to the Responses passthrough path.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator([_embedding_agent(), _chat_agent()])\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"azure/text-embedding-3-large\\\",\\n+ \\\"input\\\": \\\"Return JSON.\\\",\\n+ },\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+\\n+def test_batch_chat_rejects_embedding_before_mock_or_network_transport() -> None:\\n+ \\\"\\\"\\\"Prevent direct batch callers from submitting embedding models as chat jobs.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ client.batch_chat(\\n+ _embedding_agent(),\\n+ {\\\"task_0\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Return JSON.\\\"}]},\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"gpt-audio\\\",\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ ],\\n+)\\n+def test_chat_served_specialized_models_remain_valid_passthrough_transports(model_id: str) -> None:\\n+ \\\"\\\"\\\"Do not turn ordinary-role exclusion into a false transport rejection.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ agent = ModelAgent(\\\"specialized_chat_agent\\\", model_id, base_url=\\\"mock://local\\\")\\n+\\n+ response = client.proxy_send(\\n+ agent,\\n+ \\\"chat/completions\\\",\\n+ {\\n+ \\\"model\\\": model_id,\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Classify this.\\\"}],\\n+ },\\n+ )\\n+\\n+ assert response[\\\"object\\\"] == \\\"chat.completion\\\"\\n+ assert response[\\\"model\\\"] == model_id\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"7bfe5199e4c3c3788edf1a021039266572cbde5f\", \"filename\": \"tests/test_chat_reasoning_effort_http_honesty.py\", \"status\": \"modified\", \"additions\": 17, \"deletions\": 0, \"changes\": 17, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_reasoning_effort_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_reasoning_effort_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_reasoning_effort_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -68,6 +68,23 @@ def test_http_chat_accepts_reasoning_effort_known_levels() -> None:\\n thread.join(timeout=5)\\n \\n \\n+def test_http_chat_accepts_orchestrator_owned_reasoning_effort_auto() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"think automatically\\\"}],\\n+ \\\"reasoning_effort\\\": \\\"auto\\\",\\n+ },\\n+ )\\n+ assert status == 200, body\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n def test_http_chat_still_rejects_unknown_reasoning_effort() -> None:\\n server, thread, port = _server()\\n try:\" }, { \"sha\": \"598b8118774bd35ab3bad5102c8b9c74d1ce8e31\", \"filename\": \"tests/test_chat_response_format_json_schema_omit_real_http_honesty.py\", \"status\": \"modified\", \"additions\": 85, \"deletions\": 0, \"changes\": 85, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_response_format_json_schema_omit_real_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_response_format_json_schema_omit_real_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_response_format_json_schema_omit_real_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -9,6 +9,8 @@\\n from pathlib import Path\\n import sys\\n \\n+import pytest\\n+\\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n@@ -45,6 +47,24 @@ def _post(port: int, payload: dict) -> tuple[int, dict]:\\n return exc.code, json.loads(exc.read().decode(\\\"utf-8\\\"))\\n \\n \\n+def _post_responses(port: int, payload: dict) -> tuple[int, dict]:\\n+ request = urllib.request.Request(\\n+ f\\\"http://127.0.0.1:{port}/v1/responses\\\",\\n+ data=json.dumps(payload).encode(\\\"utf-8\\\"),\\n+ headers={\\n+ \\\"content-type\\\": \\\"application/json\\\",\\n+ \\\"authorization\\\": f\\\"Bearer {_TEST_AUTH_TOKEN}\\\",\\n+ \\\"connection\\\": \\\"close\\\",\\n+ },\\n+ method=\\\"POST\\\",\\n+ )\\n+ try:\\n+ with urllib.request.urlopen(request, timeout=10) as response:\\n+ return response.status, json.loads(response.read().decode(\\\"utf-8\\\"))\\n+ except urllib.error.HTTPError as exc:\\n+ return exc.code, json.loads(exc.read().decode(\\\"utf-8\\\"))\\n+\\n+\\n def _server():\\n server = build_server(build(), port=0, security=SecurityConfig(auth_token=_TEST_AUTH_TOKEN))\\n thread = threading.Thread(target=server.serve_forever, daemon=True)\\n@@ -141,6 +161,71 @@ def test_http_chat_omits_json_schema_null_optionals_on_response_format() -> None\\n thread.join(timeout=5)\\n \\n \\n+@pytest.mark.parametrize(\\n+ \\\"response_format\\\",\\n+ [\\n+ {\\\"type\\\": \\\"json_object\\\"},\\n+ {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\n+ \\\"name\\\": \\\"receipt_line\\\",\\n+ \\\"schema\\\": {\\n+ \\\"type\\\": \\\"object\\\",\\n+ \\\"properties\\\": {\\\"amount\\\": {\\\"type\\\": \\\"number\\\"}},\\n+ },\\n+ },\\n+ },\\n+ ],\\n+)\\n+def test_http_chat_structured_output_keeps_multi_agent_workflow(response_format: dict) -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"structured workflow\\\"}],\\n+ \\\"response_format\\\": response_format,\\n+ },\\n+ )\\n+ assert status == 200, body\\n+ assert body[\\\"orchestration\\\"][\\\"mode\\\"] == \\\"conduct\\\"\\n+ assert body[\\\"orchestration\\\"][\\\"channel\\\"] == \\\"sync\\\"\\n+ assert body[\\\"orchestration\\\"][\\\"workflow_run_id\\\"]\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_responses_json_schema_keeps_multi_agent_workflow() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post_responses(\\n+ port,\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"structured responses workflow\\\",\\n+ \\\"text\\\": {\\n+ \\\"format\\\": {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"name\\\": \\\"receipt_line\\\",\\n+ \\\"schema\\\": {\\n+ \\\"type\\\": \\\"object\\\",\\n+ \\\"properties\\\": {\\\"amount\\\": {\\\"type\\\": \\\"number\\\"}},\\n+ },\\n+ }\\n+ },\\n+ },\\n+ )\\n+ assert status == 200, body\\n+ assert body[\\\"orchestration\\\"][\\\"mode\\\"] == \\\"conduct\\\"\\n+ assert body[\\\"orchestration\\\"][\\\"channel\\\"] == \\\"sync\\\"\\n+ assert body[\\\"orchestration\\\"][\\\"workflow_run_id\\\"]\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n def test_http_chat_rejects_unknown_json_schema_nested_key() -> None:\\n server, thread, port = _server()\\n try:\" }, { \"sha\": \"aea93ad4c792c8adf6f533ed304d44eb13e55221\", \"filename\": \"tests/test_chat_tool_choice_functions_http_honesty.py\", \"status\": \"modified\", \"additions\": 5, \"deletions\": 5, \"changes\": 10, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_tool_choice_functions_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_tool_choice_functions_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_tool_choice_functions_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,4 +1,4 @@\\n-\\\"\\\"\\\"Chat tools honesty: functions/function_call rejected; tool_choice required/named requires tools; auto/none without tools are no-ops.\\\"\\\"\\\"\\n+\\\"\\\"\\\"Chat tools honesty: unsupported legacy and multi-agent tool surfaces fail closed.\\\"\\\"\\\"\\n \\n from __future__ import annotations\\n \\n@@ -143,7 +143,7 @@ def test_http_chat_accepts_tool_choice_auto_without_tools_as_omit() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_tools_with_tool_choice_passthrough_ok() -> None:\\n+def test_http_chat_rejects_tools_with_tool_choice_passthrough() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -155,8 +155,8 @@ def test_http_chat_tools_with_tool_choice_passthrough_ok() -> None:\\n \\\"tool_choice\\\": \\\"auto\\\",\\n },\\n )\\n- assert status == 200, body\\n- assert \\\"choices\\\" in body or \\\"id\\\" in body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -167,5 +167,5 @@ def test_http_chat_tools_with_tool_choice_passthrough_ok() -> None:\\n test_http_chat_accepts_function_call_auto_without_functions_as_omit()\\n test_http_chat_rejects_function_call_named_without_tools_migration()\\n test_http_chat_accepts_tool_choice_auto_without_tools_as_omit()\\n- test_http_chat_tools_with_tool_choice_passthrough_ok()\\n+ test_http_chat_rejects_tools_with_tool_choice_passthrough()\\n print(\\\"ok\\\")\" }, { \"sha\": \"70cffc0120984b751c8896b09bed1a1b9ba9bef8\", \"filename\": \"tests/test_chat_tools_passthrough_controls_http_honesty.py\", \"status\": \"modified\", \"additions\": 44, \"deletions\": 10, \"changes\": 54, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_tools_passthrough_controls_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_tools_passthrough_controls_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_tools_passthrough_controls_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -50,15 +50,18 @@ def build() -> TaskOrchestrator:\\n )\\n \\n \\n-def _post(port: int, payload: dict) -> tuple[int, dict]:\\n+def _post(port: int, payload: dict, *, tool_loop: bool = False) -> tuple[int, dict]:\\n+ headers = {\\n+ \\\"content-type\\\": \\\"application/json\\\",\\n+ \\\"authorization\\\": f\\\"Bearer {_TEST_AUTH_TOKEN}\\\",\\n+ \\\"connection\\\": \\\"close\\\",\\n+ }\\n+ if tool_loop:\\n+ headers[\\\"x-contextual-orchestrator-tool-loop\\\"] = \\\"v1\\\"\\n request = urllib.request.Request(\\n f\\\"http://127.0.0.1:{port}/v1/chat/completions\\\",\\n data=json.dumps(payload).encode(\\\"utf-8\\\"),\\n- headers={\\n- \\\"content-type\\\": \\\"application/json\\\",\\n- \\\"authorization\\\": f\\\"Bearer {_TEST_AUTH_TOKEN}\\\",\\n- \\\"connection\\\": \\\"close\\\",\\n- },\\n+ headers=headers,\\n method=\\\"POST\\\",\\n )\\n try:\\n@@ -123,7 +126,8 @@ def test_http_tools_passthrough_rejects_unsupported_seed_store_stop_n() -> None:\\n assert status == 400, (payload, body)\\n assert code in json.dumps(body), (code, body)\\n status, body = _post(port, _base(service_tier=\\\"flex\\\"))\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -146,15 +150,45 @@ def test_http_tools_passthrough_rejects_invalid_user_and_stream_options() -> Non\\n thread.join(timeout=5)\\n \\n \\n-def test_http_tools_passthrough_accepts_coerced_sampling() -> None:\\n+def test_http_tools_rejects_valid_tool_request_without_explicit_loop_header() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n port,\\n _base(temperature=\\\"0.7\\\", top_p=\\\"0.95\\\", max_tokens=\\\"64\\\"),\\n )\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_tools_preserves_valid_tool_request_with_explicit_loop_header() -> None:\\n+ \\\"\\\"\\\"The opt-in contract preserves provider tool state for OpenCode.\\\"\\\"\\\"\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ _base(temperature=\\\"0.7\\\", top_p=\\\"0.95\\\", max_tokens=\\\"64\\\"),\\n+ tool_loop=True,\\n+ )\\n assert status == 200, body\\n- assert body.get(\\\"object\\\") == \\\"chat.completion\\\" or \\\"choices\\\" in body\\n+ assert body[\\\"echo\\\"][\\\"temperature\\\"] == 0.7\\n+ assert body[\\\"echo\\\"][\\\"top_p\\\"] == 0.95\\n+ assert body[\\\"echo\\\"][\\\"max_tokens\\\"] == 64\\n+ assert body[\\\"echo\\\"][\\\"tools\\\"] == _TOOLS\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_tool_loop_rejects_streaming() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(port, _base(stream=True), tool_loop=True)\\n+ assert status == 400, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"invalid_stream\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -185,6 +219,6 @@ def test_http_response_format_passthrough_rejects_seed() -> None:\\n test_http_tools_passthrough_rejects_invalid_temperature()\\n test_http_tools_passthrough_rejects_unsupported_seed_store_stop_n()\\n test_http_tools_passthrough_rejects_invalid_user_and_stream_options()\\n- test_http_tools_passthrough_accepts_coerced_sampling()\\n+ test_unit_sampling_writeback_coerced_numbers()\\n test_http_response_format_passthrough_rejects_seed()\\n print(\\\"ok\\\")\" }, { \"sha\": \"8c054a0446b27bbccb2da4085fe39ec47c8425bd\", \"filename\": \"tests/test_chat_tools_shape_http_honesty.py\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 15, \"changes\": 18, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_tools_shape_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_tools_shape_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_tools_shape_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,7 @@ def _base_messages():\\n return [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"use a tool\\\"}]\\n \\n \\n-def test_http_chat_accepts_valid_function_tools() -> None:\\n+def test_http_chat_rejects_valid_function_tools_without_single_agent_fallback() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -75,8 +75,8 @@ def test_http_chat_accepts_valid_function_tools() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n- assert \\\"choices\\\" in body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -231,15 +231,3 @@ def test_http_chat_accepts_tools_omitted() -> None:\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n-\\n-\\n-if __name__ == \\\"__main__\\\":\\n- test_http_chat_accepts_valid_function_tools()\\n- test_http_chat_rejects_empty_tools_array()\\n- test_http_chat_rejects_tool_type_not_function()\\n- test_http_chat_rejects_tool_missing_function_name()\\n- test_http_chat_rejects_tool_function_name_bad_charset()\\n- test_http_chat_rejects_tool_sibling_unknown_fields()\\n- test_http_chat_rejects_parameters_non_object()\\n- test_http_chat_accepts_tools_omitted()\\n- print(\\\"ok\\\")\" }, { \"sha\": \"17f5b7277e09d9d5e07e8380f7adadfea4477e8e\", \"filename\": \"tests/test_chat_transport_role_separation.py\", \"status\": \"added\", \"additions\": 84, \"deletions\": 0, \"changes\": 84, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_transport_role_separation.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_transport_role_separation.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_transport_role_separation.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,84 @@\\n+\\\"\\\"\\\"Regression coverage for chat transport versus ordinary agent-role eligibility.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import sys\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator.chat_capability import ( # noqa: E402\\n+ is_chat_compatible_model_id,\\n+ is_general_chat_agent_model_id,\\n+)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"gpt-audio\\\",\\n+ \\\"gpt-audio-mini\\\",\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-content-safety\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-topic-control\\\",\\n+ ],\\n+)\\n+def test_chat_served_models_remain_transport_compatible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Do not pre-reject models that provider contracts serve through chat completions.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-content-safety\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-topic-control\\\",\\n+ ],\\n+)\\n+def test_policy_classifiers_do_not_enter_general_agent_roles(model_id: str) -> None:\\n+ \\\"\\\"\\\"Keep chat-served policy classifiers out of ordinary synthesis roles.\\\"\\\"\\\"\\n+ assert not is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"gpt-audio\\\",\\n+ \\\"gpt-audio-mini\\\",\\n+ \\\"gpt-5.2\\\",\\n+ \\\"qwen/qwen3-235b-a22b-instruct\\\",\\n+ ],\\n+)\\n+def test_general_generation_models_remain_agent_eligible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Preserve chat generation models for ordinary agent selection.\\\"\\\"\\\"\\n+ assert is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ \\\"text_embedding_3_large\\\",\\n+ \\\"company/reranker-v2\\\",\\n+ \\\"gpt-4o-mini-transcribe\\\",\\n+ \\\"omni-moderation-latest\\\",\\n+ \\\"gpt-image-1\\\",\\n+ \\\"dall-e-3\\\",\\n+ \\\"openai/clip-vit-large-patch14\\\",\\n+ \\\"google/siglip-so400m-patch14-384\\\",\\n+ \\\"sora-2\\\",\\n+ \\\"gpt-realtime\\\",\\n+ \\\"tts-1\\\",\\n+ ],\\n+)\\n+def test_endpoint_only_models_fail_both_boundaries(model_id: str) -> None:\\n+ \\\"\\\"\\\"Reject endpoint-only model families before transport or ordinary role routing.\\\"\\\"\\\"\\n+ assert not is_chat_compatible_model_id(model_id)\\n+ assert not is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"4be612a9bb2f1e4f68da779ae9b2cce807bc3810\", \"filename\": \"tests/test_cli_auth.py\", \"status\": \"modified\", \"additions\": 62, \"deletions\": 6, \"changes\": 68, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_cli_auth.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_cli_auth.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_cli_auth.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -8,9 +8,11 @@\\n from pathlib import Path\\n from unittest.mock import patch\\n \\n+import pytest\\n+\\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n-from contextual_orchestrator.__main__ import _resolve_auth_token, main\\n+from contextual_orchestrator.__main__ import _request_read_timeout, _resolve_auth_token, main\\n from contextual_orchestrator.credentials import (\\n InMemoryCredentialBackend,\\n set_backend,\\n@@ -75,22 +77,33 @@ def test_key_only_split_tokens_select_split_mode() -> None:\\n main()\\n security = serve.call_args.kwargs[\\\"security\\\"]\\n assert security.auth_token == \\\"\\\"\\n- assert security.admin_token == \\\"admin-from-kv\\\"\\n- assert security.inference_token == \\\"inference-from-kv\\\"\\n+ assert security.admin_token == \\\"admin-from-kv\\\" # noqa: S105\\n+ assert security.inference_token == \\\"inference-from-kv\\\" # noqa: S105\\n finally:\\n set_backend(None)\\n \\n \\n+def test_main_accepts_explicit_argv_without_mutating_process_arguments() -> None:\\n+ original_argv = sys.argv[:]\\n+ with patch(\\\"contextual_orchestrator.__main__.serve\\\") as serve:\\n+ main([\\\"--serve\\\", \\\"--auth-token\\\", \\\"argv-token\\\"])\\n+\\n+ assert sys.argv == original_argv\\n+ security = serve.call_args.kwargs[\\\"security\\\"]\\n+ assert security.auth_token == \\\"argv-token\\\"\\n+\\n+\\n def test_invalid_local_provider_options_fail_at_parser_boundary() -> None:\\n invalid_options = (\\n ([\\\"--local-concurrency\\\", \\\"0\\\"], \\\"positive integer\\\"),\\n ([\\\"--local-concurrency\\\", \\\"-1\\\"], \\\"positive integer\\\"),\\n ([\\\"--local-concurrency\\\", \\\"65\\\"], \\\"1..64\\\"),\\n ([\\\"--max-concurrent-runs\\\", \\\"0\\\"], \\\"positive integer\\\"),\\n ([\\\"--max-concurrent-runs\\\", \\\"65\\\"], \\\"1..64\\\"),\\n- ([\\\"--chat-template-args\\\", \\\"[]\\\"], \\\"JSON object\\\"),\\n- ([\\\"--chat-template-args\\\", \\\"null\\\"], \\\"JSON object\\\"),\\n- ([\\\"--chat-template-args\\\", \\\"{\\\"], \\\"valid JSON object\\\"),\\n+ ([\\\"--request-read-timeout-seconds\\\", \\\"0\\\"], \\\"0.1..120\\\"),\\n+ ([\\\"--request-read-timeout-seconds\\\", \\\"121\\\"], \\\"0.1..120\\\"),\\n+ ([\\\"--request-read-timeout-seconds\\\", \\\"inf\\\"], \\\"0.1..120\\\"),\\n+ ([\\\"--request-read-timeout-seconds\\\", \\\"not-a-number\\\"], \\\"0.1..120\\\"),\\n )\\n \\n for options, expected_message in invalid_options:\\n@@ -111,6 +124,26 @@ def test_invalid_local_provider_options_fail_at_parser_boundary() -> None:\\n else: # pragma: no cover\\n raise AssertionError(\\\"invalid local provider option was accepted\\\")\\n \\n+ assert _request_read_timeout(\\\"0.1\\\") == 0.1\\n+\\n+\\n+def test_serve_rejects_empty_resolved_auth_configuration() -> None:\\n+ stderr = StringIO()\\n+ with (\\n+ patch.object(sys, \\\"argv\\\", [\\\"contextual-orchestrator\\\", \\\"--serve\\\"]),\\n+ patch.object(sys, \\\"stderr\\\", stderr),\\n+ patch(\\\"contextual_orchestrator.__main__._resolve_auth_token\\\", return_value=\\\"\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.load_agents\\\", return_value=[]),\\n+ patch(\\\"contextual_orchestrator.__main__.ModelClient\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.TaskOrchestrator\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.serve\\\") as serve,\\n+ ):\\n+ with pytest.raises(SystemExit) as captured:\\n+ main()\\n+ assert captured.value.code == 2\\n+ assert \\\"requires a KV auth credential\\\" in stderr.getvalue()\\n+ serve.assert_not_called()\\n+\\n \\n def test_server_concurrency_is_explicit_and_bounded() -> None:\\n with (\\n@@ -131,6 +164,7 @@ def test_server_concurrency_is_explicit_and_bounded() -> None:\\n patch(\\\"contextual_orchestrator.__main__.load_agents\\\", return_value=[]),\\n patch(\\\"contextual_orchestrator.__main__.ModelClient\\\"),\\n patch(\\\"contextual_orchestrator.__main__.TaskOrchestrator\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.CostRoutingCoordinator\\\"),\\n patch(\\\"contextual_orchestrator.__main__.serve\\\") as serve,\\n ):\\n main()\\n@@ -148,12 +182,33 @@ def test_sampling_temperature_uses_descriptive_name_and_legacy_alias() -> None:\\n patch(\\\"contextual_orchestrator.__main__.load_agents\\\", return_value=[]),\\n patch(\\\"contextual_orchestrator.__main__.ModelClient\\\") as model_client,\\n patch(\\\"contextual_orchestrator.__main__.TaskOrchestrator\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.CostRoutingCoordinator\\\"),\\n patch(\\\"contextual_orchestrator.__main__.serve\\\"),\\n ):\\n main()\\n assert model_client.call_args.kwargs[\\\"temperature\\\"] == 0.7\\n \\n \\n+def test_sampling_temperature_is_omitted_by_default() -> None:\\n+ \\\"\\\"\\\"Startup must not invent a sampling value unsupported by the selected model.\\\"\\\"\\\"\\n+\\n+ with (\\n+ patch.object(\\n+ sys,\\n+ \\\"argv\\\",\\n+ [\\\"contextual-orchestrator\\\", \\\"--serve\\\", \\\"--auth-token\\\", \\\"token\\\"],\\n+ ),\\n+ patch(\\\"contextual_orchestrator.__main__.load_agents\\\", return_value=[]),\\n+ patch(\\\"contextual_orchestrator.__main__.ModelClient\\\") as model_client,\\n+ patch(\\\"contextual_orchestrator.__main__.TaskOrchestrator\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.CostRoutingCoordinator\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.serve\\\"),\\n+ ):\\n+ main()\\n+\\n+ assert model_client.call_args.kwargs[\\\"temperature\\\"] is None\\n+\\n+\\n def test_fast_mlsirm_preflight_reports_missing_transitive_dependency() -> None:\\n stderr = StringIO()\\n real_import = __import__\\n@@ -201,4 +256,5 @@ def test_fast_mlsirm_preflight_accepts_the_versioned_contract() -> None:\\n test_key_only_split_tokens_select_split_mode()\\n test_invalid_local_provider_options_fail_at_parser_boundary()\\n test_sampling_temperature_uses_descriptive_name_and_legacy_alias()\\n+ test_sampling_temperature_is_omitted_by_default()\\n print(\\\"ok\\\")\" }, { \"sha\": \"0a08e5e13cbe3b585f10ae29cf0986f3e488a863\", \"filename\": \"tests/test_content_part_aliases_web_search_omit_http_honesty.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_content_part_aliases_web_search_omit_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_content_part_aliases_web_search_omit_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_content_part_aliases_web_search_omit_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"vision\\\"))]\\n )\\n \\n \" }, { \"sha\": \"990470cc7492425027388fef765006d65d0ef8d4\", \"filename\": \"tests/test_content_part_type_casefold_http_honesty.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_content_part_type_casefold_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_content_part_type_casefold_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_content_part_type_casefold_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"vision\\\"))]\\n )\\n \\n \" }, { \"sha\": \"04f8dd1941594d80e29c0732be956ef4ac5c6442\", \"filename\": \"tests/test_cost_review_server.py\", \"status\": \"modified\", \"additions\": 19, \"deletions\": 1, \"changes\": 20, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_cost_review_server.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_cost_review_server.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_cost_review_server.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -73,7 +73,11 @@ def test_chat_completion_reports_real_usage_and_records_cost() -> None:\\n assert body[\\\"usage\\\"][\\\"total_tokens\\\"] > 0\\n assert body[\\\"orchestration\\\"][\\\"channel\\\"] == \\\"sync\\\"\\n \\n- status, report = _request(\\\"GET\\\", f\\\"{base}/api/v1/cost_reports/rollup?dimension=team\\\", token)\\n+ status, report = _request(\\n+ \\\"GET\\\",\\n+ f\\\"{base}/api/v1/cost_reports/rollup?dimension=team&start=0&end=9999999999\\\",\\n+ token,\\n+ )\\n assert status == 200\\n values = {item[\\\"dimension_value\\\"]: item for item in report[\\\"items\\\"]}\\n assert \\\"alpha\\\" in values\\n@@ -132,6 +136,20 @@ def test_batch_routing_jobs_endpoint_submits_multiple_requests() -> None:\\n server.shutdown()\\n \\n \\n+def test_unknown_batch_results_return_not_found() -> None:\\n+ server, port, token = _serve()\\n+ try:\\n+ status, body = _request(\\n+ \\\"POST\\\",\\n+ f\\\"http://127.0.0.1:{port}/api/v1/batch_routing_jobs/missing_job/results\\\",\\n+ token,\\n+ )\\n+ finally:\\n+ server.shutdown()\\n+ assert status == 404\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"batch_job_not_found\\\"\\n+\\n+\\n def test_cost_report_rejects_unknown_dimension() -> None:\\n server, port, token = _serve()\\n base = f\\\"http://127.0.0.1:{port}\\\"\" }, { \"sha\": \"a9c9fee460d039c0102fa1a1e158f4de82826fbe\", \"filename\": \"tests/test_cost_router.py\", \"status\": \"modified\", \"additions\": 198, \"deletions\": 0, \"changes\": 198, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_cost_router.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_cost_router.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_cost_router.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -4,6 +4,9 @@\\n \\n import sys\\n from pathlib import Path\\n+from unittest.mock import patch\\n+\\n+import pytest\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n@@ -113,6 +116,201 @@ def test_batch_completion_records_on_retrieve() -> None:\\n assert records[0][\\\"team_name\\\"] == \\\"beta\\\"\\n \\n \\n+def test_structured_output_forces_sync_when_batch_is_selected() -> None:\\n+ coordinator = _coordinator()\\n+ result = coordinator.complete(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"return one JSON object\\\"}],\\n+ hints={\\\"channel\\\": \\\"batch\\\"},\\n+ response_format={\\\"type\\\": \\\"json_object\\\"},\\n+ )\\n+ assert result[\\\"channel\\\"] == \\\"sync\\\"\\n+ assert result[\\\"routing_reason\\\"].endswith(\\\"structured_output_forced_sync\\\")\\n+\\n+\\n+def test_provider_native_structured_output_keeps_cost_and_lineage() -> None:\\n+ coordinator = _coordinator()\\n+ provider_request = {\\n+ \\\"model\\\": \\\"mock-a\\\",\\n+ \\\"input\\\": \\\"return one JSON object\\\",\\n+ \\\"text\\\": {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}},\\n+ }\\n+ messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"return one JSON object\\\"}]\\n+ provider_response = {\\n+ \\\"object\\\": \\\"response\\\",\\n+ \\\"output_text\\\": \\\"{}\\\",\\n+ \\\"output\\\": [],\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 7, \\\"output_tokens\\\": 11, \\\"total_tokens\\\": 18},\\n+ }\\n+\\n+ with patch.object(\\n+ coordinator.orchestrator.client,\\n+ \\\"proxy_send\\\",\\n+ return_value=provider_response,\\n+ ):\\n+ result = coordinator.complete(\\n+ messages,\\n+ hints={\\\"channel\\\": \\\"batch\\\"},\\n+ response_format={\\\"type\\\": \\\"json_object\\\"},\\n+ provider_request=provider_request,\\n+ provider_endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ assert result[\\\"channel\\\"] == \\\"sync\\\"\\n+ assert result[\\\"answer\\\"] == \\\"{}\\\"\\n+ assert result[\\\"provider_response\\\"][\\\"orchestration\\\"][\\\"channel\\\"] == \\\"sync\\\"\\n+ assert result[\\\"provider_response\\\"][\\\"orchestration\\\"][\\\"usage_record_id\\\"] == result[\\n+ \\\"usage_record_id\\\"\\n+ ]\\n+ assert result[\\\"provider_response\\\"][\\\"orchestration\\\"][\\\"usage_record_ids\\\"] == [\\n+ result[\\\"usage_record_id\\\"]\\n+ ]\\n+ record = coordinator.ledger.records()[0]\\n+ assert record[\\\"prompt_tokens\\\"] == 7\\n+ assert record[\\\"completion_tokens\\\"] == 11\\n+\\n+\\n+def test_provider_native_workflow_records_each_metered_provider_call() -> None:\\n+ coordinator = _coordinator()\\n+ provider_response = {\\n+ \\\"object\\\": \\\"response\\\",\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 5, \\\"output_tokens\\\": 7, \\\"total_tokens\\\": 12},\\n+ \\\"orchestration\\\": {\\\"workflow_run_id\\\": \\\"run_metered\\\"},\\n+ }\\n+ workflow_run = {\\n+ \\\"workflow_run_id\\\": \\\"run_metered\\\",\\n+ \\\"mode\\\": \\\"conduct\\\",\\n+ \\\"answer\\\": \\\"{}\\\",\\n+ \\\"trace\\\": [\\n+ {\\n+ \\\"agent_id\\\": \\\"mock_worker\\\",\\n+ \\\"output\\\": \\\"evidence\\\",\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 2, \\\"completion_tokens\\\": 3},\\n+ },\\n+ {\\n+ \\\"agent_id\\\": \\\"mock_worker\\\",\\n+ \\\"subtask\\\": \\\"Provider-facing structured synthesis\\\",\\n+ \\\"output\\\": \\\"{}\\\",\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 5, \\\"output_tokens\\\": 7},\\n+ },\\n+ ],\\n+ \\\"verification\\\": {\\n+ \\\"judge_agent_id\\\": \\\"mock_worker\\\",\\n+ \\\"judge_usage\\\": {\\\"prompt_tokens\\\": 1, \\\"completion_tokens\\\": 2},\\n+ },\\n+ }\\n+\\n+ with patch.object(\\n+ coordinator.orchestrator,\\n+ \\\"proxy_completion\\\",\\n+ return_value=provider_response,\\n+ ), patch.object(\\n+ coordinator.orchestrator,\\n+ \\\"get_workflow_run\\\",\\n+ return_value=workflow_run,\\n+ ):\\n+ result = coordinator.complete(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"return JSON\\\"}],\\n+ response_format={\\\"type\\\": \\\"json_object\\\"},\\n+ provider_request={\\\"input\\\": \\\"return JSON\\\"},\\n+ provider_endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ records = coordinator.ledger.records()\\n+ assert [(row[\\\"prompt_tokens\\\"], row[\\\"completion_tokens\\\"]) for row in records] == [\\n+ (2, 3),\\n+ (1, 2),\\n+ (5, 7),\\n+ ]\\n+ assert result[\\\"usage\\\"] == {\\n+ \\\"prompt_tokens\\\": 8,\\n+ \\\"completion_tokens\\\": 12,\\n+ \\\"total_tokens\\\": 20,\\n+ }\\n+ assert result[\\\"cost\\\"] == {\\\"cost_amount\\\": 0.032, \\\"currency_code\\\": \\\"USD\\\"}\\n+ assert result[\\\"usage_record_ids\\\"] == [row[\\\"usage_record_id\\\"] for row in records]\\n+ assert result[\\\"usage_record_id\\\"] == records[-1][\\\"usage_record_id\\\"]\\n+ assert result[\\\"unmetered_provider_call_count\\\"] == 0\\n+\\n+\\n+def test_provider_native_workflow_does_not_sum_mixed_currencies() -> None:\\n+ coordinator = _coordinator()\\n+ judge_agent = ModelAgent(\\n+ id=\\\"judge_worker\\\",\\n+ model=\\\"mock-judge\\\",\\n+ base_url=\\\"mock://judge\\\",\\n+ provider_name=\\\"mock\\\",\\n+ )\\n+ coordinator.orchestrator.candidates.append(judge_agent)\\n+ coordinator.orchestrator.agents.append(judge_agent)\\n+ coordinator.price_book.set_price(\\n+ PriceEntry(\\n+ \\\"mock\\\",\\n+ \\\"mock-judge\\\",\\n+ prompt_price_per_1k=1.0,\\n+ completion_price_per_1k=1.0,\\n+ currency_code=\\\"KRW\\\",\\n+ )\\n+ )\\n+ provider_response = {\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 5, \\\"output_tokens\\\": 7},\\n+ \\\"orchestration\\\": {\\\"workflow_run_id\\\": \\\"run_mixed_currency\\\"},\\n+ }\\n+ workflow_run = {\\n+ \\\"workflow_run_id\\\": \\\"run_mixed_currency\\\",\\n+ \\\"mode\\\": \\\"conduct\\\",\\n+ \\\"answer\\\": \\\"{}\\\",\\n+ \\\"trace\\\": [\\n+ {\\n+ \\\"agent_id\\\": \\\"mock_worker\\\",\\n+ \\\"subtask\\\": \\\"Provider-facing structured synthesis\\\",\\n+ \\\"output\\\": \\\"{}\\\",\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 5, \\\"output_tokens\\\": 7},\\n+ }\\n+ ],\\n+ \\\"verification\\\": {\\n+ \\\"judge_agent_id\\\": \\\"judge_worker\\\",\\n+ \\\"judge_usage\\\": {\\\"prompt_tokens\\\": 1, \\\"completion_tokens\\\": 2},\\n+ },\\n+ }\\n+\\n+ with patch.object(\\n+ coordinator.orchestrator, \\\"proxy_completion\\\", return_value=provider_response\\n+ ), patch.object(\\n+ coordinator.orchestrator, \\\"get_workflow_run\\\", return_value=workflow_run\\n+ ):\\n+ result = coordinator.complete(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"return JSON\\\"}],\\n+ response_format={\\\"type\\\": \\\"json_object\\\"},\\n+ provider_request={\\\"input\\\": \\\"return JSON\\\"},\\n+ provider_endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ assert result[\\\"cost\\\"] == {\\\"cost_amount\\\": None, \\\"currency_code\\\": \\\"MIXED\\\"}\\n+ assert [row[\\\"currency_code\\\"] for row in coordinator.ledger.records()] == [\\\"KRW\\\", \\\"USD\\\"]\\n+\\n+\\n+def test_provider_native_completion_rejects_unknown_endpoint() -> None:\\n+ coordinator = _coordinator()\\n+\\n+ with pytest.raises(ValueError, match=\\\"provider_endpoint must be\\\"):\\n+ coordinator.complete(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"hello\\\"}],\\n+ provider_request={\\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"hello\\\"}]},\\n+ provider_endpoint=\\\"images\\\",\\n+ )\\n+\\n+\\n+def test_provider_native_completion_requires_workflow_lineage() -> None:\\n+ coordinator = _coordinator()\\n+\\n+ with patch.object(coordinator.orchestrator, \\\"proxy_completion\\\", return_value={}):\\n+ with pytest.raises(RuntimeError, match=\\\"omitted orchestration lineage\\\"):\\n+ coordinator.complete(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"hello\\\"}],\\n+ provider_request={\\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"hello\\\"}]},\\n+ )\\n+\\n+\\n def test_default_local_batch_backend_reuses_orchestrator_concurrency() -> None:\\n class _Client:\\n local_concurrency = 3\" }, { \"sha\": \"e2183032ad5cc3cf1551f92184a9fa6e3a4de381\", \"filename\": \"tests/test_digit_n_bool01_echo_logprobs_http_honesty.py\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 3, \"changes\": 6, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_digit_n_bool01_echo_logprobs_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_digit_n_bool01_echo_logprobs_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_digit_n_bool01_echo_logprobs_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -277,9 +277,9 @@ def test_http_responses_logprobs_zero_one() -> None:\\n \\\"logprobs\\\": 1,\\n },\\n )\\n- # responses allows logprobs=true shape (boolean); 1 coerces to true and is accepted\\n- # unless top_logprobs missing - logprobs true alone is ok for responses\\n- assert status == 200, body\\n+ # The value is valid OpenAI shape but cannot be applied by conduct.\\n+ assert status == 422, body\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\" }, { \"sha\": \"c7c6bdce1f33385fac830ba429c6fd6e8205428d\", \"filename\": \"tests/test_discover_models_cli.py\", \"status\": \"modified\", \"additions\": 25, \"deletions\": 5, \"changes\": 30, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_discover_models_cli.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_discover_models_cli.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_discover_models_cli.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,6 +3,8 @@\\n from __future__ import annotations\\n \\n import json\\n+from contextlib import contextmanager\\n+import socket\\n import sys\\n import urllib.parse\\n from io import StringIO\\n@@ -17,6 +19,7 @@\\n register_credential,\\n set_backend,\\n )\\n+from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n \\n \\n class _Response:\\n@@ -29,8 +32,25 @@ def __enter__(self):\\n def __exit__(self, *_args):\\n return False\\n \\n- def read(self) -> bytes:\\n- return self._body\\n+ def read(self, _size: int = -1) -> bytes:\\n+ return self._body if _size < 0 else self._body[:_size]\\n+\\n+\\n+@contextmanager\\n+def _patched_provider_transport(urlopen):\\n+ \\\"\\\"\\\"Keep CLI discovery tests offline while exercising the validated transport seam.\\\"\\\"\\\"\\n+ def open_provider(request, _destination=None, *, timeout=None):\\n+ return urlopen(request, timeout=timeout)\\n+\\n+ with (\\n+ patch.object(\\n+ ModelClient,\\n+ \\\"_validate_provider\\\",\\n+ return_value=(socket.AF_INET, (\\\"93.184.216.34\\\", 443)),\\n+ ),\\n+ patch.object(ModelClient, \\\"_open_provider\\\", side_effect=open_provider),\\n+ ):\\n+ yield\\n \\n \\n def test_discover_models_with_no_credentials_reports_zero_and_succeeds() -> None:\\n@@ -68,7 +88,7 @@ def urlopen(request, timeout=None):\\n with (\\n patch.object(sys, \\\"argv\\\", [\\\"contextual-orchestrator\\\", \\\"discover-models\\\"]),\\n patch.object(sys, \\\"stdout\\\", stdout),\\n- patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen),\\n+ _patched_provider_transport(urlopen),\\n ):\\n main()\\n finally:\\n@@ -96,7 +116,7 @@ def urlopen(request, timeout=None):\\n with (\\n patch.object(sys, \\\"argv\\\", [\\\"contextual-orchestrator\\\", \\\"discover-models\\\", \\\"--agents-db\\\", db_path]),\\n patch.object(sys, \\\"stdout\\\", stdout),\\n- patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen),\\n+ _patched_provider_transport(urlopen),\\n ):\\n main()\\n finally:\\n@@ -151,7 +171,7 @@ def urlopen(request, timeout=None):\\n [\\\"contextual-orchestrator\\\", \\\"discover-models\\\", \\\"--agents-db\\\", db_path, \\\"--enable-cheapest\\\", \\\"1\\\"],\\n ),\\n patch.object(sys, \\\"stdout\\\", stdout),\\n- patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen),\\n+ _patched_provider_transport(urlopen),\\n ):\\n main()\\n finally:\" }, { \"sha\": \"1f55cc420d03bbec7f15c93badbaa0d6d749984c\", \"filename\": \"tests/test_embeddings_encoding_format_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 4, \"changes\": 8, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_embeddings_encoding_format_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_embeddings_encoding_format_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_embeddings_encoding_format_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,23 +3,23 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n-from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n-from contextual_orchestrator.server import SecurityConfig, build_server # noqa: E402\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+from contextual_orchestrator.server import SecurityConfig, build_server\\n \\n _TEST_AUTH_TOKEN = \\\"embeddings_encoding_format_http_honesty_token\\\" # noqa: S105\\n \\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"embedding\\\"))]\\n )\\n \\n \" }, { \"sha\": \"20be91ae1937f95c350a6b3eb481168e6c34769a\", \"filename\": \"tests/test_embeddings_model_pool_http_honesty.py\", \"status\": \"modified\", \"additions\": 111, \"deletions\": 3, \"changes\": 114, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_embeddings_model_pool_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_embeddings_model_pool_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_embeddings_model_pool_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,16 +3,16 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n-from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n-from contextual_orchestrator.server import SecurityConfig, build_server # noqa: E402\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+from contextual_orchestrator.server import SecurityConfig, build_server\\n \\n _TEST_AUTH_TOKEN = \\\"embeddings_model_pool_http_honesty_token\\\" # noqa: S105\\n \\n@@ -48,6 +48,61 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n+def _server_without_embedding():\\n+ server = build_server(\\n+ TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\",))]),\\n+ port=0,\\n+ security=SecurityConfig(auth_token=_TEST_AUTH_TOKEN),\\n+ )\\n+ thread = threading.Thread(target=server.serve_forever, daemon=True)\\n+ thread.start()\\n+ return server, thread, server.server_address[1]\\n+\\n+\\n+def test_select_capability_agent_normalizes_and_rejects_empty_capability() -> None:\\n+ \\\"\\\"\\\"Capability selection normalizes names and rejects an empty capability.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator(\\n+ [ModelAgent(\\\"embedding_agent\\\", \\\"text-embedding-3-large\\\", tags=(\\\"embedding\\\",))]\\n+ )\\n+ assert orchestrator.select_capability_agent(\\\" EMBEDDING \\\").id == \\\"embedding_agent\\\"\\n+ try:\\n+ orchestrator.select_capability_agent(\\\" \\\")\\n+ except ValueError as exc:\\n+ assert str(exc) == \\\"capability must be a non-empty string\\\"\\n+ else:\\n+ raise AssertionError(\\\"empty capability must fail closed\\\")\\n+\\n+\\n+def test_select_capability_agent_skips_disabled_and_excluded_agents() -> None:\\n+ \\\"\\\"\\\"Capability selection skips disabled and provider-excluded candidates.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator(\\n+ [\\n+ ModelAgent(\\\"disabled_embedding\\\", \\\"disabled\\\", tags=(\\\"embedding\\\",), disabled=True),\\n+ ModelAgent(\\n+ \\\"excluded_embedding\\\",\\n+ \\\"excluded\\\",\\n+ tags=(\\\"embedding\\\",),\\n+ provider_exclusions=(\\\"embedding\\\",),\\n+ ),\\n+ ModelAgent(\\\"eligible_embedding\\\", \\\"eligible\\\", tags=(\\\"embedding\\\",)),\\n+ ]\\n+ )\\n+ assert orchestrator.select_capability_agent(\\\"embedding\\\").id == \\\"eligible_embedding\\\"\\n+\\n+ unavailable = TaskOrchestrator(\\n+ [\\n+ ModelAgent(\\\"disabled_embedding\\\", \\\"disabled\\\", tags=(\\\"embedding\\\",), disabled=True),\\n+ ModelAgent(\\\"reasoning_agent\\\", \\\"reasoning\\\", tags=(\\\"reasoning\\\",)),\\n+ ]\\n+ )\\n+ try:\\n+ unavailable.select_capability_agent(\\\"embedding\\\")\\n+ except RuntimeError as exc:\\n+ assert str(exc) == \\\"no enabled agent available for capability=embedding\\\"\\n+ else:\\n+ raise AssertionError(\\\"an unavailable capability must fail closed\\\")\\n+\\n+\\n def test_http_embeddings_rejects_model_outside_agent_pool() -> None:\\n server, thread, port = _server()\\n try:\\n@@ -83,6 +138,44 @@ def test_http_embeddings_accepts_model_in_agent_pool() -> None:\\n thread.join(timeout=5)\\n \\n \\n+def test_http_embeddings_auto_selects_enabled_embedding_agent() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(port, \\\"/v1/embeddings\\\", {\\\"input\\\": \\\"invoice search chunk\\\"})\\n+ assert status == 200, body\\n+ assert body.get(\\\"model\\\") == \\\"mock-planner\\\"\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_embeddings_auto_selection_fails_when_capability_is_missing() -> None:\\n+ server, thread, port = _server_without_embedding()\\n+ try:\\n+ status, body = _post(port, \\\"/v1/embeddings\\\", {\\\"input\\\": \\\"invoice search chunk\\\"})\\n+ assert status == 503, body\\n+ assert \\\"embedding_unavailable\\\" in json.dumps(body)\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_embeddings_reject_virtual_orchestrator_model() -> None:\\n+ \\\"\\\"\\\"An explicit virtual chat model cannot bypass the embedding pool gate.\\\"\\\"\\\"\\n+ server, thread, port = _server()\\n+ try:\\n+ for path, payload in (\\n+ (\\\"/v1/embeddings\\\", {\\\"model\\\": \\\"contextual-orchestrator\\\", \\\"input\\\": \\\"invoice search chunk\\\"}),\\n+ (\\\"/v1/batch/embeddings\\\", {\\\"model\\\": \\\"contextual-orchestrator\\\", \\\"inputs\\\": [\\\"alpha\\\", \\\"beta\\\"]}),\\n+ ):\\n+ status, body = _post(port, path, payload)\\n+ assert status == 400, (path, body)\\n+ assert \\\"invalid_model\\\" in json.dumps(body)\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n def test_http_batch_embeddings_rejects_model_outside_agent_pool() -> None:\\n server, thread, port = _server()\\n try:\\n@@ -116,9 +209,24 @@ def test_http_batch_embeddings_accepts_model_in_agent_pool() -> None:\\n thread.join(timeout=5)\\n \\n \\n+def test_http_batch_embeddings_auto_selects_enabled_embedding_agent() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(port, \\\"/v1/batch/embeddings\\\", {\\\"inputs\\\": [\\\"alpha\\\", \\\"beta\\\"]})\\n+ assert status == 200, body\\n+ assert body.get(\\\"model\\\") == \\\"mock-planner\\\"\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n if __name__ == \\\"__main__\\\":\\n test_http_embeddings_rejects_model_outside_agent_pool()\\n test_http_embeddings_accepts_model_in_agent_pool()\\n+ test_http_embeddings_auto_selects_enabled_embedding_agent()\\n+ test_http_embeddings_auto_selection_fails_when_capability_is_missing()\\n+ test_http_embeddings_reject_virtual_orchestrator_model()\\n test_http_batch_embeddings_rejects_model_outside_agent_pool()\\n test_http_batch_embeddings_accepts_model_in_agent_pool()\\n+ test_http_batch_embeddings_auto_selects_enabled_embedding_agent()\\n print(\\\"ok\\\")\" }, { \"sha\": \"dc994ec6112f8fdd6cf13ebfa4d892a426be03cc\", \"filename\": \"tests/test_encoding_stream_logprobs_http_honesty.py\", \"status\": \"modified\", \"additions\": 2, \"deletions\": 2, \"changes\": 4, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_encoding_stream_logprobs_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_encoding_stream_logprobs_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_encoding_stream_logprobs_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,11 +3,11 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"embedding\\\"))]\\n )\\n \\n \" }, { \"sha\": \"6df84664587f6dee684d1ddc874d37d67fc8f8c0\", \"filename\": \"tests/test_functions_null_max_tool_calls_null_http_honesty.py\", \"status\": \"modified\", \"additions\": 0, \"deletions\": 2, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_functions_null_max_tool_calls_null_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_functions_null_max_tool_calls_null_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_functions_null_max_tool_calls_null_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -101,9 +101,7 @@ def test_http_responses_accepts_null_max_tool_calls_and_functions() -> None:\\n \\\"model\\\": \\\"mock-planner\\\",\\n \\\"input\\\": \\\"max tool null\\\",\\n \\\"max_tool_calls\\\": None,\\n- \\\"functions\\\": None,\\n \\\"function_call\\\": None,\\n- \\\"functions\\\": [],\\n },\\n )\\n assert status == 200, body\" }, { \"sha\": \"aaaf8219096d6b4099b2f8052d764d446fd36650\", \"filename\": \"tests/test_gateway_seed_discovery.py\", \"status\": \"added\", \"additions\": 106, \"deletions\": 0, \"changes\": 106, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_gateway_seed_discovery.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_gateway_seed_discovery.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_gateway_seed_discovery.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,106 @@\\n+from __future__ import annotations\\n+\\n+from types import SimpleNamespace\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+from contextual_orchestrator import __main__ as cli\\n+from contextual_orchestrator.model_discovery import (\\n+ DiscoveredModel,\\n+ ProviderDiscoveryError,\\n+)\\n+\\n+\\n+def test_empty_gateway_seed_expands_from_its_registry(monkeypatch) -> None:\\n+ seed = ModelAgent(\\n+ \\\"gateway_seed\\\",\\n+ \\\"\\\",\\n+ base_url=\\\"https://gateway.example/v1\\\",\\n+ credential_key=\\\"LLM_GATEWAY_API_KEY\\\",\\n+ tags=(\\\"reasoning\\\", \\\"writing\\\"),\\n+ )\\n+\\n+ monkeypatch.setattr(\\n+ cli,\\n+ \\\"discover_provider_models\\\",\\n+ lambda source: [\\n+ DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=\\\"chat-model\\\",\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ ),\\n+ DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=\\\"text-embedding-model\\\",\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ ),\\n+ ],\\n+ )\\n+\\n+ agents = cli._auto_discover_seed_agents([seed], allow_failures=False)\\n+\\n+ assert [agent.model for agent in agents] == [\\\"chat-model\\\"]\\n+ assert agents[0].disabled is False\\n+ assert agents[0].base_url == \\\"https://gateway.example/v1\\\"\\n+\\n+\\n+def test_gateway_seed_discovery_preserves_configured_and_disables_unusable_seeds(\\n+ monkeypatch,\\n+) -> None:\\n+ configured = ModelAgent(\\\"configured_agent\\\", \\\"chat-model\\\")\\n+ assert cli._auto_discover_seed_agents([configured], allow_failures=False) == [configured]\\n+\\n+ seed = ModelAgent(\\\"gateway_seed\\\", \\\"\\\", base_url=\\\"https://gateway.example/v1\\\")\\n+ failure = ProviderDiscoveryError(\\\"gateway\\\", \\\"unavailable\\\")\\n+ monkeypatch.setattr(cli, \\\"discover_provider_models\\\", lambda _source: (_ for _ in ()).throw(failure))\\n+ with pytest.raises(ProviderDiscoveryError):\\n+ cli._auto_discover_seed_agents([seed], allow_failures=False)\\n+ assert cli._auto_discover_seed_agents([seed], allow_failures=True)[0].disabled is True\\n+\\n+ monkeypatch.setattr(\\n+ cli,\\n+ \\\"discover_provider_models\\\",\\n+ lambda source: [\\n+ DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=\\\"text-embedding-model\\\",\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ )\\n+ ],\\n+ )\\n+ assert cli._auto_discover_seed_agents([seed], allow_failures=False)[0].disabled is True\\n+\\n+\\n+def test_discover_models_command_fails_when_every_provider_fails(monkeypatch) -> None:\\n+ monkeypatch.setattr(\\n+ cli,\\n+ \\\"discover_all_models\\\",\\n+ lambda: ([], [SimpleNamespace(provider_name=\\\"gateway\\\")]),\\n+ )\\n+ monkeypatch.setattr(cli, \\\"refresh_price_book\\\", lambda _models, _prices: 0)\\n+ with pytest.raises(SystemExit) as captured:\\n+ cli._discover_models_command([])\\n+ assert captured.value.code == 1\\n+\\n+\\n+def test_structured_output_forces_conduct_even_when_auto_would_route() -> None:\\n+ orchestrator = TaskOrchestrator(\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-model\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ )\\n+\\n+ result = orchestrator.complete(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"short structured request\\\"}],\\n+ mode=\\\"auto\\\",\\n+ output_contract={\\\"type\\\": \\\"json_object\\\"},\\n+ )\\n+\\n+ assert result[\\\"mode\\\"] == \\\"conduct\\\"\\n+ assert len(result[\\\"trace\\\"]) == 4\\n+ assert result[\\\"answer\\\"] == \\\"{}\\\"\" }, { \"sha\": \"9f2d009bbabe6312f9b84bf2c8ed84a038e537e2\", \"filename\": \"tests/test_generated_workflow.py\", \"status\": \"modified\", \"additions\": 14, \"deletions\": 1, \"changes\": 15, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_generated_workflow.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_generated_workflow.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_generated_workflow.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -18,7 +18,7 @@\\n \\n from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n import contextual_orchestrator.orchestrator as orchestrator_module # noqa: E402\\n-from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n+from contextual_orchestrator.orchestrator import BudgetExceededError, ModelClient # noqa: E402\\n \\n \\n PLAN = {\\n@@ -96,6 +96,19 @@ def test_invalid_plan_falls_back_to_template() -> None:\\n assert len(result[\\\"trace\\\"]) == 4 # fixed thinker/worker/verifier/synthesizer template\\n \\n \\n+def test_generated_planner_budget_exhaustion_does_not_fallback() -> None:\\n+ orchestrator, _ = _orch(json.dumps(PLAN))\\n+ budget_error = BudgetExceededError(\\\"spend budget exceeded\\\")\\n+ with patch.object(orchestrator, \\\"_plan\\\", side_effect=AssertionError(\\\"budget must not fall back\\\")):\\n+ with patch.object(orchestrator, \\\"_raise_if_spend_budget_exceeded\\\", side_effect=budget_error):\\n+ try:\\n+ orchestrator.conduct([{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"solve the hard problem\\\"}])\\n+ except BudgetExceededError as exc:\\n+ assert exc is budget_error\\n+ else: # pragma: no cover\\n+ raise AssertionError(\\\"budget exhaustion must stop generated planning\\\")\\n+\\n+\\n def test_default_template_unchanged() -> None:\\n orchestrator = TaskOrchestrator(\\n [ModelAgent(\\\"general_agent\\\", \\\"model-x\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"planning\\\", \\\"research\\\"))]\" }, { \"sha\": \"5b9cad401157d9129dc66d68ca80c3dcc0cdd671\", \"filename\": \"tests/test_inbound_request_framing.py\", \"status\": \"added\", \"additions\": 223, \"deletions\": 0, \"changes\": 223, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_inbound_request_framing.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_inbound_request_framing.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_inbound_request_framing.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,223 @@\\n+\\\"\\\"\\\"Fail-closed inbound HTTP request framing regressions.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from email.message import Message\\n+import io\\n+import socket\\n+from pathlib import Path\\n+import sys\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator.server import ( # noqa: E402\\n+ RequestError,\\n+ SecurityConfig,\\n+ _parse_request_framing,\\n+ build_server,\\n+)\\n+\\n+\\n+class _FakeConnection:\\n+ \\\"\\\"\\\"Record socket timeout changes made by the bounded body reader.\\\"\\\"\\\"\\n+\\n+ def __init__(self) -> None:\\n+ self.timeout: float | None = None\\n+\\n+ def gettimeout(self) -> float | None:\\n+ \\\"\\\"\\\"Return the current synthetic socket timeout.\\\"\\\"\\\"\\n+ return self.timeout\\n+\\n+ def settimeout(self, value: float | None) -> None:\\n+ \\\"\\\"\\\"Record a synthetic socket timeout.\\\"\\\"\\\"\\n+ self.timeout = value\\n+\\n+\\n+class _TimeoutReader:\\n+ \\\"\\\"\\\"Raise a real socket timeout to exercise request deadline handling.\\\"\\\"\\\"\\n+\\n+ def read(self, _size: int) -> bytes:\\n+ \\\"\\\"\\\"Raise a bounded-read timeout.\\\"\\\"\\\"\\n+ raise socket.timeout(\\\"test timeout\\\")\\n+\\n+\\n+class _ExplodingReader:\\n+ \\\"\\\"\\\"Fail if framing validation accidentally consumes bytes.\\\"\\\"\\\"\\n+\\n+ def read(self, _size: int) -> bytes:\\n+ \\\"\\\"\\\"Report an invalid unbounded read.\\\"\\\"\\\"\\n+ raise AssertionError(\\\"request bytes were consumed before framing validation\\\")\\n+\\n+\\n+class _GetAllHeaders:\\n+ \\\"\\\"\\\"Expose only the standard get_all header API.\\\"\\\"\\\"\\n+\\n+ def get_all(self, field_name: str, _default: list[str]) -> list[str]:\\n+ \\\"\\\"\\\"Return one valid fixed-length field.\\\"\\\"\\\"\\n+ return [] if field_name.casefold() == \\\"transfer-encoding\\\" else [\\\"1\\\"]\\n+\\n+\\n+class _GetHeaders:\\n+ \\\"\\\"\\\"Expose only a mapping-like get API.\\\"\\\"\\\"\\n+\\n+ def get(self, field_name: str) -> str | None:\\n+ \\\"\\\"\\\"Return one valid fixed-length field.\\\"\\\"\\\"\\n+ return None if field_name.casefold() == \\\"transfer-encoding\\\" else \\\"1\\\"\\n+\\n+\\n+def _headers(content_length: str | None = None, *, transfer_encoding: str | None = None) -> Message:\\n+ \\\"\\\"\\\"Build raw-like headers for the pure framing and handler tests.\\\"\\\"\\\"\\n+ headers = Message()\\n+ headers[\\\"content-type\\\"] = \\\"application/json\\\"\\n+ if content_length is not None:\\n+ headers[\\\"content-length\\\"] = content_length\\n+ if transfer_encoding is not None:\\n+ headers[\\\"transfer-encoding\\\"] = transfer_encoding\\n+ return headers\\n+\\n+\\n+def _handler(headers: Message, body: bytes | object, *, timeout: float = 1.0):\\n+ \\\"\\\"\\\"Create the real nested request handler without opening a listening socket.\\\"\\\"\\\"\\n+ server = build_server(\\n+ TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"mock-generalist\\\")]),\\n+ port=0,\\n+ security=SecurityConfig(\\n+ auth_token=\\\"test_token\\\", # noqa: S106\\n+ request_read_timeout_seconds=timeout,\\n+ ),\\n+ )\\n+ handler = server.RequestHandlerClass.__new__(server.RequestHandlerClass)\\n+ handler.headers = headers\\n+ handler.rfile = body if hasattr(body, \\\"read\\\") else io.BytesIO(body)\\n+ handler.connection = _FakeConnection()\\n+ handler.close_connection = False\\n+ return server, handler\\n+\\n+\\n+@pytest.mark.parametrize(\\\"value\\\", [\\\"-1\\\", \\\"+1\\\", \\\" 1\\\", \\\"1 \\\", \\\"1.0\\\", \\\"1,1\\\", \\\"\\\"])\\n+def test_invalid_content_length_is_rejected_before_read(value: str) -> None:\\n+ \\\"\\\"\\\"Reject signed, padded, non-decimal, and comma-ambiguous lengths.\\\"\\\"\\\"\\n+ with pytest.raises(RequestError, match=\\\"content-length\\\"):\\n+ _parse_request_framing(_headers(value), 64)\\n+\\n+\\n+def test_missing_length_and_transfer_encoding_fail_closed() -> None:\\n+ \\\"\\\"\\\"Require fixed-length framing and reject unsupported transfer coding.\\\"\\\"\\\"\\n+ with pytest.raises(RequestError, match=\\\"required\\\") as missing:\\n+ _parse_request_framing(_headers(), 64)\\n+ assert missing.value.status == 411\\n+ with pytest.raises(RequestError, match=\\\"transfer-encoded\\\"):\\n+ _parse_request_framing(_headers(\\\"1\\\", transfer_encoding=\\\"chunked\\\"), 64)\\n+\\n+\\n+def test_request_reader_rejects_non_json_media_type() -> None:\\n+ headers = _headers(\\\"2\\\")\\n+ headers.replace_header(\\\"content-type\\\", \\\"text/plain\\\")\\n+ server, handler = _handler(headers, b\\\"{}\\\")\\n+ try:\\n+ with pytest.raises(RequestError) as captured:\\n+ handler._read_json()\\n+ assert captured.value.status == 415\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_header_value_fallbacks_and_integer_overflow_are_safe() -> None:\\n+ \\\"\\\"\\\"Support ordinary header mappings without weakening strict parsing.\\\"\\\"\\\"\\n+ assert _parse_request_framing(_GetAllHeaders(), 64) == 1\\n+ assert _parse_request_framing(_GetHeaders(), 64) == 1\\n+ with pytest.raises(RequestError, match=\\\"invalid\\\"):\\n+ _parse_request_framing(_headers(\\\"9\\\" * 5000), 64)\\n+\\n+\\n+def test_duplicate_content_length_is_rejected_even_when_equal() -> None:\\n+ \\\"\\\"\\\"Do not choose a value when duplicate header lines are present.\\\"\\\"\\\"\\n+ headers = _headers(\\\"1\\\")\\n+ headers.add_header(\\\"Content-Length\\\", \\\"1\\\")\\n+ with pytest.raises(RequestError, match=\\\"duplicate\\\"):\\n+ _parse_request_framing(headers, 64)\\n+\\n+\\n+def test_oversized_content_length_is_rejected_before_read() -> None:\\n+ \\\"\\\"\\\"Enforce the configured byte limit before touching the body stream.\\\"\\\"\\\"\\n+ with pytest.raises(RequestError) as error:\\n+ _parse_request_framing(_headers(\\\"65\\\"), 64)\\n+ assert error.value.status == 413\\n+\\n+\\n+def test_read_json_requires_exact_body_and_restores_timeout() -> None:\\n+ \\\"\\\"\\\"Read exactly the declared bytes and restore the connection timeout.\\\"\\\"\\\"\\n+ server, handler = _handler(_headers(\\\"7\\\"), b'{\\\"x\\\":1}')\\n+ try:\\n+ assert handler._read_json() == {\\\"x\\\": 1}\\n+ assert handler.connection.timeout is None\\n+ assert handler.close_connection is False\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_zero_length_json_body_is_framing_valid_and_returns_empty_object() -> None:\\n+ \\\"\\\"\\\"Leave endpoint-level required-field validation to the existing caller.\\\"\\\"\\\"\\n+ server, handler = _handler(_headers(\\\"0\\\"), b\\\"\\\")\\n+ try:\\n+ assert handler._read_json() == {}\\n+ assert handler.close_connection is False\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_read_json_rejects_truncated_body_and_closes_connection() -> None:\\n+ \\\"\\\"\\\"Reject premature EOF rather than decoding a partial request.\\\"\\\"\\\"\\n+ server, handler = _handler(_headers(\\\"7\\\"), b'{\\\"x\\\":')\\n+ try:\\n+ with pytest.raises(RequestError, match=\\\"ended before\\\"):\\n+ handler._read_json()\\n+ assert handler.close_connection is True\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_read_json_rejects_invalid_framing_without_consuming_body() -> None:\\n+ \\\"\\\"\\\"Mark the connection closed when framing fails before the first read.\\\"\\\"\\\"\\n+ server, handler = _handler(_headers(\\\"-1\\\"), _ExplodingReader())\\n+ try:\\n+ with pytest.raises(RequestError, match=\\\"content-length\\\"):\\n+ handler._read_json()\\n+ assert handler.close_connection is True\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_read_json_times_out_slow_body_and_closes_connection() -> None:\\n+ \\\"\\\"\\\"Release a handler blocked on an incomplete declared body.\\\"\\\"\\\"\\n+ server, handler = _handler(_headers(\\\"1\\\"), _TimeoutReader(), timeout=0.1)\\n+ try:\\n+ with pytest.raises(RequestError, match=\\\"timed out\\\") as error:\\n+ handler._read_json()\\n+ assert error.value.status == 408\\n+ assert handler.close_connection is True\\n+ assert handler.connection.timeout is None\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_security_config_rejects_unbounded_body_read_timeout() -> None:\\n+ \\\"\\\"\\\"Keep deployment-provided body deadlines finite and bounded.\\\"\\\"\\\"\\n+ with pytest.raises(ValueError, match=\\\"request_read_timeout_seconds\\\"):\\n+ SecurityConfig(request_read_timeout_seconds=float(\\\"inf\\\"))\\n+ with pytest.raises(ValueError, match=\\\"max_body_bytes\\\"):\\n+ SecurityConfig(max_body_bytes=0)\\n+\\n+\\n+def test_security_readiness_exposes_bounded_request_controls() -> None:\\n+ \\\"\\\"\\\"Let operators verify the active body limit and deadline without secrets.\\\"\\\"\\\"\\n+ profile = SecurityConfig(max_body_bytes=128, request_read_timeout_seconds=2.0).readiness_profile()\\n+ assert profile[\\\"max_body_bytes\\\"] == 128\\n+ assert profile[\\\"request_read_timeout_seconds\\\"] == 2.0\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"379920f83e5abcdf3120175ee82ab0370c3bacde\", \"filename\": \"tests/test_inbound_request_total_deadline.py\", \"status\": \"added\", \"additions\": 175, \"deletions\": 0, \"changes\": 175, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_inbound_request_total_deadline.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_inbound_request_total_deadline.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_inbound_request_total_deadline.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,175 @@\\n+\\\"\\\"\\\"Regression for a total inbound-body deadline, not only idle-socket timeout.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from email.message import Message\\n+from pathlib import Path\\n+import sys\\n+from types import SimpleNamespace\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator import server as server_module # noqa: E402\\n+from contextual_orchestrator.server import ( # noqa: E402\\n+ RequestError,\\n+ SecurityConfig,\\n+ build_server,\\n+)\\n+\\n+\\n+class _Clock:\\n+ \\\"\\\"\\\"Deterministic monotonic clock advanced by the synthetic slow reader.\\\"\\\"\\\"\\n+\\n+ def __init__(self) -> None:\\n+ self.now = 0.0\\n+\\n+ def monotonic(self) -> float:\\n+ return self.now\\n+\\n+\\n+class _AdvancingClock:\\n+ \\\"\\\"\\\"Advance beyond the deadline before the first body read.\\\"\\\"\\\"\\n+\\n+ def __init__(self) -> None:\\n+ self.now = 0.0\\n+\\n+ def monotonic(self) -> float:\\n+ self.now += 0.2\\n+ return self.now\\n+\\n+\\n+class _SlowProgressReader:\\n+ \\\"\\\"\\\"Return progress before every idle timeout while exceeding the total deadline.\\\"\\\"\\\"\\n+\\n+ def __init__(self, payload: bytes, clock: _Clock, step_seconds: float) -> None:\\n+ self.payload = payload\\n+ self.clock = clock\\n+ self.step_seconds = step_seconds\\n+ self.offset = 0\\n+\\n+ def read(self, _size: int) -> bytes:\\n+ if self.offset >= len(self.payload):\\n+ return b\\\"\\\"\\n+ self.clock.now += self.step_seconds\\n+ result = self.payload[self.offset : self.offset + 1]\\n+ self.offset += 1\\n+ return result\\n+\\n+\\n+class _FakeConnection:\\n+ \\\"\\\"\\\"Expose the socket timeout methods used by the request reader.\\\"\\\"\\\"\\n+\\n+ def __init__(self) -> None:\\n+ self.timeout: float | None = None\\n+\\n+ def gettimeout(self) -> float | None:\\n+ return self.timeout\\n+\\n+ def settimeout(self, value: float | None) -> None:\\n+ self.timeout = value\\n+\\n+\\n+def _headers(body_size: int) -> Message:\\n+ headers = Message()\\n+ headers[\\\"content-type\\\"] = \\\"application/json\\\"\\n+ headers[\\\"content-length\\\"] = str(body_size)\\n+ return headers\\n+\\n+\\n+def test_slow_progress_cannot_extend_request_past_total_deadline(monkeypatch) -> None:\\n+ \\\"\\\"\\\"A byte trickle below the idle timeout must still terminate at the deadline.\\\"\\\"\\\"\\n+ payload = b'{\\\"x\\\":1}'\\n+ clock = _Clock()\\n+ server = build_server(\\n+ TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"mock-generalist\\\")]),\\n+ port=0,\\n+ security=SecurityConfig(\\n+ auth_token=\\\"test_token\\\", # noqa: S106\\n+ request_read_timeout_seconds=0.1,\\n+ ),\\n+ )\\n+ handler = server.RequestHandlerClass.__new__(server.RequestHandlerClass)\\n+ handler.headers = _headers(len(payload))\\n+ handler.rfile = _SlowProgressReader(payload, clock, step_seconds=0.06)\\n+ handler.connection = _FakeConnection()\\n+ handler.close_connection = False\\n+ monkeypatch.setattr(\\n+ server_module,\\n+ \\\"time\\\",\\n+ SimpleNamespace(monotonic=clock.monotonic),\\n+ )\\n+\\n+ try:\\n+ with pytest.raises(RequestError, match=\\\"timed out\\\") as error:\\n+ handler._read_json()\\n+ assert error.value.status == 408\\n+ assert handler.close_connection is True\\n+ assert handler.connection.timeout is None\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_complete_body_at_total_deadline_is_accepted(monkeypatch) -> None:\\n+ \\\"\\\"\\\"Accept the final byte when it completes the body at the deadline.\\\"\\\"\\\"\\n+ payload = b\\\"{}\\\"\\n+ clock = _Clock()\\n+ server = build_server(\\n+ TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"mock-generalist\\\")]),\\n+ port=0,\\n+ security=SecurityConfig(\\n+ auth_token=\\\"test_token\\\", # noqa: S106\\n+ request_read_timeout_seconds=0.1,\\n+ ),\\n+ )\\n+ handler = server.RequestHandlerClass.__new__(server.RequestHandlerClass)\\n+ handler.headers = _headers(len(payload))\\n+ handler.rfile = _SlowProgressReader(payload, clock, step_seconds=0.05)\\n+ handler.connection = _FakeConnection()\\n+ handler.close_connection = False\\n+ monkeypatch.setattr(\\n+ server_module,\\n+ \\\"time\\\",\\n+ SimpleNamespace(monotonic=clock.monotonic),\\n+ )\\n+\\n+ try:\\n+ assert handler._read_json() == {}\\n+ assert handler.close_connection is False\\n+ assert handler.connection.timeout is None\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_expired_total_deadline_rejects_before_first_read(monkeypatch) -> None:\\n+ payload = b\\\"{}\\\"\\n+ clock = _AdvancingClock()\\n+ server = build_server(\\n+ TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"mock-generalist\\\")]),\\n+ port=0,\\n+ security=SecurityConfig(\\n+ auth_token=\\\"test_token\\\", # noqa: S106\\n+ request_read_timeout_seconds=0.1,\\n+ ),\\n+ )\\n+ handler = server.RequestHandlerClass.__new__(server.RequestHandlerClass)\\n+ handler.headers = _headers(len(payload))\\n+ handler.rfile = SimpleNamespace(\\n+ read=lambda _size: (_ for _ in ()).throw(AssertionError(\\\"body was read\\\"))\\n+ )\\n+ handler.connection = _FakeConnection()\\n+ handler.close_connection = False\\n+ monkeypatch.setattr(server_module, \\\"time\\\", SimpleNamespace(monotonic=clock.monotonic))\\n+\\n+ try:\\n+ with pytest.raises(RequestError, match=\\\"timed out\\\"):\\n+ handler._read_json()\\n+ assert handler.close_connection is True\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"4f9c8e2c7d84a335db0185fc32605498e168a56f\", \"filename\": \"tests/test_int_float_max_output_stop_ws_http_honesty.py\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 2, \"changes\": 5, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_int_float_max_output_stop_ws_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_int_float_max_output_stop_ws_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_int_float_max_output_stop_ws_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -220,7 +220,7 @@ def test_http_responses_accepts_digit_and_float_max_output_tokens() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_whole_float_n_and_seed() -> None:\\n+def test_http_responses_rejects_unapplied_whole_float_seed() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -233,7 +233,8 @@ def test_http_responses_accepts_whole_float_n_and_seed() -> None:\\n \\\"seed\\\": 7.0,\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\" }, { \"sha\": \"68713aa570f8f4785aa0065f10b4a77dd46f5163\", \"filename\": \"tests/test_json_schema_name_charset_http_honesty.py\", \"status\": \"modified\", \"additions\": 2, \"deletions\": 2, \"changes\": 4, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_json_schema_name_charset_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_json_schema_name_charset_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_json_schema_name_charset_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -291,7 +291,7 @@ def test_http_chat_keeps_legal_json_schema_name() -> None:\\n },\\n )\\n assert status == 200, body\\n- assert _echo_schema(body).get(\\\"name\\\") == legal_name\\n+ assert body[\\\"object\\\"] == \\\"chat.completion\\\"\\n \\n max_name = \\\"A\\\" * 64\\n status_max, body_max = _post(\\n@@ -309,7 +309,7 @@ def test_http_chat_keeps_legal_json_schema_name() -> None:\\n },\\n )\\n assert status_max == 200, body_max\\n- assert _echo_schema(body_max).get(\\\"name\\\") == max_name\\n+ assert body_max[\\\"object\\\"] == \\\"chat.completion\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\" }, { \"sha\": \"ed7c6ae017faed868d402e66149db1c2d324f516\", \"filename\": \"tests/test_ledger_execution_identity_http_honesty.py\", \"status\": \"modified\", \"additions\": 5, \"deletions\": 5, \"changes\": 10, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_ledger_execution_identity_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_ledger_execution_identity_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_ledger_execution_identity_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,15 +3,15 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n-from contextual_orchestrator import ( # noqa: E402\\n+from contextual_orchestrator import (\\n CostLedger,\\n CostRoutingCoordinator,\\n InMemoryConfigStore,\\n@@ -20,9 +20,9 @@\\n PriceEntry,\\n TaskOrchestrator,\\n )\\n-from contextual_orchestrator.server import SecurityConfig, build_server # noqa: E402\\n+from contextual_orchestrator.server import SecurityConfig, build_server\\n \\n-_TEST_AUTH_TOKEN = \\\"ledger_execution_identity_http_honesty_token\\\" # noqa: S105\\n+_TEST_AUTH_TOKEN = \\\"ledger_execution_identity_http_honesty_token\\\"\\n \\n \\n def _serve():\\n@@ -32,7 +32,7 @@ def _serve():\\n model=\\\"mock-a\\\",\\n base_url=\\\"mock://a\\\",\\n provider_name=\\\"mock\\\",\\n- tags=(\\\"reasoning\\\", \\\"coding\\\", \\\"writing\\\"),\\n+ tags=(\\\"reasoning\\\", \\\"coding\\\", \\\"writing\\\", \\\"embedding\\\"),\\n priority=1,\\n )\\n ]\" }, { \"sha\": \"00491dd96688b307d8a93efbcc61afd317747df9\", \"filename\": \"tests/test_local_gateway.py\", \"status\": \"renamed\", \"additions\": 209, \"deletions\": 87, \"changes\": 296, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_local_gateway.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_local_gateway.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_local_gateway.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,4 +1,4 @@\\n-\\\"\\\"\\\"Explicit loopback mlx-lm transport without weakening remote egress rules.\\\"\\\"\\\"\\n+\\\"\\\"\\\"Provider-neutral loopback gateway transport without weakening egress rules.\\\"\\\"\\\"\\n \\n from __future__ import annotations\\n \\n@@ -20,39 +20,28 @@\\n _chat_to_responses_payload,\\n _is_local_provider_url,\\n _responses_to_chat_payload,\\n+ _responses_usage,\\n )\\n \\n \\n+@pytest.fixture(autouse=True)\\n+def _local_gateway_credentials():\\n+ with patch(\\\"contextual_orchestrator.orchestrator.get_credential\\\", return_value=\\\"local-secret\\\"):\\n+ yield\\n+\\n+\\n def test_local_candidate_registry_keeps_all_discovered_entries() -> None:\\n agents = load_agents(str(Path(__file__).resolve().parents[1] / \\\"examples/agents.local.json\\\"))\\n orchestrator = TaskOrchestrator(agents)\\n \\n assert {agent.model for agent in orchestrator.candidates} >= {\\n \\\"contextual-orchestrator\\\",\\n- \\\"mlx-community/gemma-4-31b-it-4bit\\\",\\n- \\\"mlx-community/llama-3.2-3b-instruct-4bit\\\",\\n- \\\"outlier-ai/deepseek-r1-distill-qwen-32b-mlx-4bit\\\",\\n+ \\\"gemma-4-e4b-it\\\",\\n \\\"embeddinggemma\\\",\\n }\\n assert all(not agent.disabled for agent in orchestrator.candidates)\\n assert len(orchestrator.candidates) == len(orchestrator.agents)\\n- verifier_exclusions = {\\n- agent.model: agent.provider_exclusions\\n- for agent in orchestrator.candidates\\n- if agent.model in {\\n- \\\"mlx-community/llama-3.2-1b-instruct-4bit\\\",\\n- \\\"mlx-community/gemma-4-31b-it-4bit\\\",\\n- \\\"outlier-ai/deepseek-r1-distill-qwen-32b-mlx-4bit\\\",\\n- }\\n- }\\n- assert verifier_exclusions == {\\n- \\\"mlx-community/llama-3.2-1b-instruct-4bit\\\": (\\\"verifier\\\",),\\n- \\\"mlx-community/gemma-4-31b-it-4bit\\\": (\\\"verifier\\\",),\\n- \\\"outlier-ai/deepseek-r1-distill-qwen-32b-mlx-4bit\\\": (\\\"verifier\\\",),\\n- }\\n- assert orchestrator._select_agent(\\n- \\\"Evaluate this answer for evidence and risk.\\\", \\\"verifier\\\"\\n- ).model == \\\"mlx-community/gemma-4-e4b-it-4bit\\\"\\n+ assert all(not agent.base_url.startswith(\\\"mlx://\\\") for agent in orchestrator.candidates)\\n assert any(\\n agent.model == \\\"contextual-orchestrator\\\"\\n and set(agent.provider_exclusions) == {\\\"thinker\\\", \\\"worker\\\", \\\"verifier\\\", \\\"synthesizer\\\"}\\n@@ -76,26 +65,9 @@ def read(self) -> bytes:\\n return json.dumps(self.payload).encode(\\\"utf-8\\\")\\n \\n \\n-def test_mlx_loopback_uses_http_without_a_credential() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n- client = ModelClient(max_retries=0, temperature=0.0, chat_template_args={\\\"enable_thinking\\\": False})\\n- seen = []\\n-\\n- def open_provider(request, _destination=None):\\n- seen.append(request)\\n- return _Response({\\n- \\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": \\\"local-ok\\\"}}],\\n- \\\"usage\\\": {\\\"prompt_tokens\\\": 2, \\\"completion_tokens\\\": 1, \\\"total_tokens\\\": 3},\\n- })\\n-\\n- with patch.object(client, \\\"_open_provider\\\", side_effect=open_provider):\\n- assert client.chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"ping\\\"}]) == \\\"local-ok\\\"\\n- assert seen[0].full_url == \\\"http://127.0.0.1:8080/v1/chat/completions\\\"\\n- assert \\\"Authorization\\\" not in seen[0].headers\\n- import json\\n-\\n- assert json.loads(seen[0].data)[\\\"chat_template_kwargs\\\"] == {\\\"enable_thinking\\\": False}\\n- assert client.take_usage()[\\\"total_tokens\\\"] == 3\\n+def test_local_gateway_requires_explicit_authentication() -> None:\\n+ with pytest.raises(ValueError, match=\\\"local_credential_key\\\"):\\n+ ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\")\\n \\n \\n def test_authenticated_local_gateway_uses_only_its_explicit_kv_credential() -> None:\\n@@ -109,7 +81,6 @@ def test_authenticated_local_gateway_uses_only_its_explicit_kv_credential() -> N\\n client = ModelClient(\\n max_retries=0,\\n temperature=0.0,\\n- chat_template_args={\\\"enable_thinking\\\": False},\\n )\\n seen = []\\n \\n@@ -142,19 +113,14 @@ def test_authenticated_local_gateway_requires_its_kv_credential() -> None:\\n ModelClient(max_retries=0).chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"ping\\\"}])\\n \\n \\n-def test_local_gateway_credential_cannot_be_attached_to_mlx_worker() -> None:\\n- with pytest.raises(ValueError, match=\\\"local:// gateway\\\"):\\n- ModelAgent(\\n- \\\"local_agent\\\",\\n- \\\"local-model\\\",\\n- base_url=\\\"mlx://127.0.0.1:8080/v1\\\",\\n- local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n- )\\n+def test_direct_mlx_provider_scheme_is_rejected() -> None:\\n+ with pytest.raises(ValueError, match=\\\"direct mlx:// provider URLs are unsupported\\\"):\\n+ ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n \\n \\n def test_provider_probe_verifies_registry_then_uses_one_bounded_completion_without_retry() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n- client = ModelClient(max_retries=2, local_max_retries=2, chat_template_args={\\\"enable_thinking\\\": False})\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n+ client = ModelClient(max_retries=2, local_max_retries=2)\\n seen: list[tuple[object, float | None]] = []\\n \\n def open_provider(request, _destination=None, *, timeout=None):\\n@@ -180,12 +146,11 @@ def open_provider(request, _destination=None, *, timeout=None):\\n \\n payload = json.loads(seen[1][0].data)\\n assert payload[\\\"max_tokens\\\"] == 1\\n- assert payload[\\\"temperature\\\"] == 0.0\\n- assert payload[\\\"chat_template_kwargs\\\"] == {\\\"enable_thinking\\\": False}\\n+ assert \\\"temperature\\\" not in payload\\n \\n \\n def test_provider_probe_rejects_a_local_model_registry_mismatch() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"requested-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"requested-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient(max_retries=0)\\n with patch.object(\\n client,\\n@@ -202,7 +167,7 @@ def test_provider_probe_rejects_a_local_model_registry_mismatch() -> None:\\n \\n \\n def test_provider_probe_reports_timeout_without_retry() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient(max_retries=2, local_max_retries=2)\\n with patch.object(client, \\\"_open_provider\\\", side_effect=TimeoutError(\\\"probe timeout\\\")) as open_provider:\\n report = client.probe(agent, timeout=0.5)\\n@@ -215,7 +180,7 @@ def test_provider_probe_reports_timeout_without_retry() -> None:\\n \\n \\n def test_provider_probe_does_not_serialize_provider_exception_text() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient(max_retries=0)\\n with patch.object(client, \\\"_open_provider\\\", side_effect=RuntimeError(\\\"provider-output-secret\\\")):\\n report = client.probe(agent, timeout=0.5)\\n@@ -281,8 +246,8 @@ def probe(_agent, *, timeout):\\n def test_local_provider_serializes_model_switches_and_bounds_waiters() -> None:\\n import threading\\n \\n- first_agent = ModelAgent(\\\"first_agent\\\", \\\"model-a\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n- second_agent = ModelAgent(\\\"second_agent\\\", \\\"model-b\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ first_agent = ModelAgent(\\\"first_agent\\\", \\\"model-a\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n+ second_agent = ModelAgent(\\\"second_agent\\\", \\\"model-b\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n first_client = ModelClient(max_retries=0, timeout=1.0)\\n second_client = ModelClient(max_retries=0, timeout=0.05)\\n entered = threading.Event()\\n@@ -328,8 +293,8 @@ def call(client, agent):\\n assert isinstance(errors[0], TimeoutError)\\n \\n \\n-def test_reasoning_only_response_explains_local_template_fix() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+def test_reasoning_only_response_explains_missing_content() -> None:\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient(max_retries=0)\\n with patch.object(\\n client,\\n@@ -339,20 +304,26 @@ def test_reasoning_only_response_explains_local_template_fix() -> None:\\n try:\\n client.chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"ping\\\"}])\\n except RuntimeError as exc:\\n- assert \\\"enable_thinking\\\" in str(exc)\\n+ assert \\\"assistant content\\\" in str(exc)\\n else: # pragma: no cover\\n raise AssertionError(\\\"reasoning-only provider response must fail clearly\\\")\\n \\n \\n def test_response_without_content_or_reasoning_fails_clearly() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n with pytest.raises(RuntimeError, match=\\\"assistant content\\\"):\\n ModelClient()._response_content(agent, {\\\"choices\\\": [{\\\"message\\\": {}}]})\\n \\n \\n-def test_local_responses_passthrough_adapts_to_chat_transport() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n- client = ModelClient(max_retries=0, chat_template_args={\\\"enable_thinking\\\": False})\\n+@pytest.mark.parametrize(\\\"endpoint\\\", [\\\"responses\\\", \\\"/v1/responses\\\"])\\n+def test_local_responses_passthrough_adapts_to_chat_transport(endpoint: str) -> None:\\n+ agent = ModelAgent(\\n+ \\\"local_agent\\\",\\n+ \\\"local-model\\\",\\n+ base_url=\\\"local://127.0.0.1:8080/v1\\\",\\n+ local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n+ )\\n+ client = ModelClient(max_retries=0)\\n with patch.object(client, \\\"_validate_provider\\\", return_value=None), patch.object(\\n client,\\n \\\"_send_raw_with_retry\\\",\\n@@ -369,7 +340,7 @@ def test_local_responses_passthrough_adapts_to_chat_transport() -> None:\\n ) as send:\\n response = client.proxy_send(\\n agent,\\n- \\\"responses\\\",\\n+ endpoint,\\n {\\n \\\"model\\\": \\\"local-model\\\",\\n \\\"instructions\\\": \\\"Be concise.\\\",\\n@@ -379,6 +350,13 @@ def test_local_responses_passthrough_adapts_to_chat_transport() -> None:\\n \\\"content\\\": [{\\\"type\\\": \\\"input_text\\\", \\\"text\\\": \\\"ping\\\"}],\\n }],\\n \\\"stream\\\": True,\\n+ \\\"text\\\": {\\n+ \\\"format\\\": {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"name\\\": \\\"result_shape\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\"},\\n+ }\\n+ },\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"function\\\",\\n \\\"name\\\": \\\"lookup\\\",\\n@@ -397,11 +375,17 @@ def test_local_responses_passthrough_adapts_to_chat_transport() -> None:\\n \\\"type\\\": \\\"function\\\",\\n \\\"function\\\": {\\\"name\\\": \\\"lookup\\\", \\\"parameters\\\": {\\\"type\\\": \\\"object\\\"}},\\n }]\\n- assert forwarded[\\\"chat_template_kwargs\\\"] == {\\\"enable_thinking\\\": False}\\n+ assert forwarded[\\\"response_format\\\"] == {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\n+ \\\"name\\\": \\\"result_shape\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\"},\\n+ },\\n+ }\\n \\n \\n-def test_local_responses_passthrough_omits_empty_template_arguments() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+def test_local_responses_passthrough_has_no_provider_specific_fields() -> None:\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient()\\n with patch.object(client, \\\"_validate_provider\\\", return_value=None), patch.object(\\n client,\\n@@ -413,6 +397,60 @@ def test_local_responses_passthrough_omits_empty_template_arguments() -> None:\\n assert \\\"chat_template_kwargs\\\" not in send.call_args.args[2]\\n \\n \\n+def test_local_chat_passthrough_applies_bounded_controls_for_final_synthesis() -> None:\\n+ agent = ModelAgent(\\n+ \\\"local_agent\\\",\\n+ \\\"local-model\\\",\\n+ base_url=\\\"local://127.0.0.1:8080/v1\\\",\\n+ local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n+ )\\n+ client = ModelClient(max_output_tokens=321)\\n+ with patch.object(client, \\\"_validate_provider\\\", return_value=None), patch.object(\\n+ client,\\n+ \\\"_send_raw_with_retry\\\",\\n+ return_value={\\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": \\\"OK\\\"}}]},\\n+ ) as send:\\n+ client.proxy_send(\\n+ agent,\\n+ \\\"chat/completions\\\",\\n+ {\\n+ \\\"model\\\": \\\"local-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"final synthesis\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ },\\n+ )\\n+\\n+ forwarded = send.call_args.args[2]\\n+ assert forwarded[\\\"max_tokens\\\"] == 321\\n+ assert \\\"chat_template_kwargs\\\" not in forwarded\\n+\\n+\\n+def test_local_chat_passthrough_preserves_explicit_max_tokens() -> None:\\n+ agent = ModelAgent(\\n+ \\\"local_agent\\\",\\n+ \\\"local-model\\\",\\n+ base_url=\\\"local://127.0.0.1:8080/v1\\\",\\n+ local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n+ )\\n+ client = ModelClient(max_output_tokens=321)\\n+ with patch.object(client, \\\"_validate_provider\\\", return_value=None), patch.object(\\n+ client,\\n+ \\\"_send_raw_with_retry\\\",\\n+ return_value={\\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": \\\"OK\\\"}}]},\\n+ ) as send:\\n+ client.proxy_send(\\n+ agent,\\n+ \\\"chat/completions\\\",\\n+ {\\n+ \\\"model\\\": \\\"local-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"final synthesis\\\"}],\\n+ \\\"max_tokens\\\": 64,\\n+ },\\n+ )\\n+\\n+ assert send.call_args.args[2][\\\"max_tokens\\\"] == 64\\n+\\n+\\n def test_local_responses_adapter_preserves_supported_items_and_controls() -> None:\\n payload = _responses_to_chat_payload(\\n {\\n@@ -480,6 +518,79 @@ def test_local_responses_adapter_preserves_supported_items_and_controls() -> Non\\n assert payload[\\\"tool_choice\\\"] == {\\\"type\\\": \\\"function\\\", \\\"function\\\": {\\\"name\\\": \\\"lookup\\\"}}\\n \\n \\n+def test_local_responses_adapter_preserves_json_schema_contract() -> None:\\n+ payload = _responses_to_chat_payload(\\n+ {\\n+ \\\"model\\\": \\\"local-model\\\",\\n+ \\\"input\\\": \\\"extract the region\\\",\\n+ \\\"text\\\": {\\n+ \\\"format\\\": {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"name\\\": \\\"region_result\\\",\\n+ \\\"description\\\": \\\"A bounded region result\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"label\\\": {\\\"type\\\": \\\"string\\\"}}},\\n+ \\\"strict\\\": True,\\n+ }\\n+ },\\n+ }\\n+ )\\n+\\n+ assert payload[\\\"response_format\\\"] == {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\n+ \\\"name\\\": \\\"region_result\\\",\\n+ \\\"description\\\": \\\"A bounded region result\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"label\\\": {\\\"type\\\": \\\"string\\\"}}},\\n+ \\\"strict\\\": True,\\n+ },\\n+ }\\n+\\n+\\n+def test_local_responses_adapter_prefers_translated_text_format_contract() -> None:\\n+ payload = _responses_to_chat_payload(\\n+ {\\n+ \\\"input\\\": \\\"prefer the Responses contract\\\",\\n+ \\\"text\\\": {\\n+ \\\"format\\\": {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"name\\\": \\\"responses_shape\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\"},\\n+ }\\n+ },\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ assert payload[\\\"response_format\\\"] == {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\n+ \\\"name\\\": \\\"responses_shape\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\"},\\n+ },\\n+ }\\n+\\n+\\n+def test_responses_usage_normalizes_chat_aliases() -> None:\\n+ assert _responses_usage(\\n+ {\\\"prompt_tokens\\\": 4, \\\"completion_tokens\\\": 5, \\\"total_tokens\\\": 9}\\n+ ) == {\\\"input_tokens\\\": 4, \\\"output_tokens\\\": 5, \\\"total_tokens\\\": 9}\\n+\\n+\\n+def test_local_responses_adapter_preserves_bounded_metadata_for_provider() -> None:\\n+ payload = _responses_to_chat_payload(\\n+ {\\n+ \\\"input\\\": \\\"use the supplied context\\\",\\n+ \\\"metadata\\\": {\\\"pu\\\": \\\"PU_TEST\\\", \\\"corp_code\\\": \\\"CORP_TEST\\\", \\\"author_id\\\": \\\"AUTHOR_TEST\\\"},\\n+ }\\n+ )\\n+\\n+ assert payload[\\\"metadata\\\"] == {\\n+ \\\"pu\\\": \\\"PU_TEST\\\",\\n+ \\\"corp_code\\\": \\\"CORP_TEST\\\",\\n+ \\\"author_id\\\": \\\"AUTHOR_TEST\\\",\\n+ }\\n+\\n+\\n def test_local_responses_adapter_rejects_non_string_input() -> None:\\n with pytest.raises(ValueError, match=\\\"string or item list\\\"):\\n _responses_to_chat_payload({\\\"input\\\": {\\\"unexpected\\\": \\\"mapping\\\"}})\\n@@ -545,9 +656,18 @@ def test_local_responses_response_maps_reasoning_and_tool_calls() -> None:\\n \\n \\n def test_local_provider_scheme_validation_rejects_remote_and_malformed_ports() -> None:\\n- assert _is_local_provider_url(\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ assert _is_local_provider_url(\\\"local://127.0.0.1:8080/v1\\\")\\n+ assert not _is_local_provider_url(\\\"local://host.docker.internal:8080/v1\\\")\\n assert not _is_local_provider_url(\\\"mlx://example.com:8080/v1\\\")\\n- assert not _is_local_provider_url(\\\"mlx://127.0.0.1:not-a-port/v1\\\")\\n+ assert not _is_local_provider_url(\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ assert not _is_local_provider_url(\\\"local://127.0.0.1:not-a-port/v1\\\")\\n+ with pytest.raises(ValueError, match=\\\"explicit loopback endpoint\\\"):\\n+ ModelAgent(\\n+ \\\"docker_host_agent\\\",\\n+ \\\"local-model\\\",\\n+ base_url=\\\"local://host.docker.internal:8080/v1\\\",\\n+ local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n+ )\\n \\n \\n @pytest.mark.parametrize(\\n@@ -581,6 +701,19 @@ def test_provider_transport_rejects_invalid_port_and_resolution_failures() -> No\\n client._resolve_addresses(\\\"empty.example\\\", 443)\\n \\n \\n+@pytest.mark.parametrize(\\n+ \\\"userinfo_url\\\",\\n+ [\\n+ \\\"https://@provider.example/v1/chat/completions\\\",\\n+ \\\"https://:secret@provider.example/v1/chat/completions\\\",\\n+ ],\\n+)\\n+def test_provider_transport_rejects_empty_userinfo(userinfo_url: str) -> None:\\n+ \\\"\\\"\\\"The low-level transport must reject empty userinfo before opening a socket.\\\"\\\"\\\"\\n+ with pytest.raises(RuntimeError, match=\\\"without userinfo\\\"):\\n+ ModelClient()._open_provider(urllib.request.Request(userinfo_url))\\n+\\n+\\n def test_https_provider_uses_verifying_connection_and_resolved_destination() -> None:\\n class FakeResponse:\\n status = 200\\n@@ -655,7 +788,7 @@ def close(self):\\n \\n \\n def test_local_provider_url_rejects_query_data_at_transport_boundary() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1?unsafe=1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1?unsafe=1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n with pytest.raises(RuntimeError, match=\\\"query data\\\"):\\n ModelClient()._provider_url(agent, \\\"/chat/completions\\\")\\n with pytest.raises(RuntimeError, match=\\\"query data\\\"):\\n@@ -670,7 +803,7 @@ def test_provider_url_rejects_non_http_scheme_at_builder_boundary() -> None:\\n \\n def test_provider_validation_rejects_non_loopback_and_remote_query_data() -> None:\\n client = ModelClient()\\n- local = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ local = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n with patch.object(client, \\\"_resolve_addresses\\\", return_value=[(socket.AF_INET, (\\\"192.0.2.1\\\", 8080))]):\\n with pytest.raises(RuntimeError, match=\\\"non-loopback\\\"):\\n client._validate_provider(local)\\n@@ -755,7 +888,7 @@ def test_provider_transport_rejects_non_http_url_before_io() -> None:\\n \\n \\n def test_local_batch_preserves_ids_and_usage() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient(max_retries=0, local_concurrency=2)\\n calls = []\\n \\n@@ -776,7 +909,7 @@ def fake_chat(_agent, messages, temperature=None):\\n \\n \\n def test_local_batch_default_uses_sequential_path() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient()\\n with patch.object(client, \\\"chat\\\", side_effect=lambda _agent, messages, temperature=None: messages[0][\\\"content\\\"]):\\n result = client.batch_chat(\\n@@ -793,17 +926,6 @@ def test_patch_agent_rejects_disabling_last_enabled_agent() -> None:\\n orchestrator.patch_agent(\\\"default\\\", \\\"only_agent\\\", {\\\"status\\\": \\\"disabled\\\"})\\n \\n \\n-def test_stream_chat_forwards_local_template_arguments() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n- client = ModelClient(chat_template_args={\\\"enable_thinking\\\": False})\\n- with patch.object(client, \\\"_validate_provider\\\", return_value=None), patch.object(\\n- client, \\\"_stream_send\\\", return_value=iter((\\\"delta\\\",))\\n- ) as stream_send:\\n- assert list(client.stream_chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"ping\\\"}])) == [\\\"delta\\\"]\\n-\\n- assert stream_send.call_args.args[1][\\\"chat_template_kwargs\\\"] == {\\\"enable_thinking\\\": False}\\n-\\n-\\n if __name__ == \\\"__main__\\\":\\n for name, fn in sorted(globals().items()):\\n if name.startswith(\\\"test_\\\") and callable(fn):\", \"previous_filename\": \"tests/test_local_mlx.py\" }, { \"sha\": \"d24fb5148dbc9066ae94527f68841297e933248c\", \"filename\": \"tests/test_logit_bias_key_strip_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 3, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_logit_bias_key_strip_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_logit_bias_key_strip_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_logit_bias_key_strip_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,7 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_responses_accepts_logit_bias_padded_digit_keys() -> None:\\n+def test_http_responses_rejects_unapplied_logit_bias_padded_digit_keys() -> None:\\n server, thread, port = _server()\\n try:\\n for key in (\\\"100\\\", \\\" 100 \\\", \\\"\\\\t42\\\\t\\\", \\\" 7\\\"):\\n@@ -65,7 +65,8 @@ def test_http_responses_accepts_logit_bias_padded_digit_keys() -> None:\\n \\\"logit_bias\\\": {key: \\\"-5\\\", \\\"200\\\": 1},\\n },\\n )\\n- assert status == 200, (key, body)\\n+ assert status == 422, (key, body)\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -135,7 +136,7 @@ def test_http_completions_still_typechecks_padded_keys_then_rejects_nonempty() -\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_responses_accepts_logit_bias_padded_digit_keys()\\n+ test_http_responses_rejects_unapplied_logit_bias_padded_digit_keys()\\n test_http_responses_still_rejects_non_digit_logit_bias_keys()\\n test_http_chat_still_typechecks_padded_keys_then_rejects_nonempty()\\n test_http_completions_still_typechecks_padded_keys_then_rejects_nonempty()\" }, { \"sha\": \"e2df31558676ad9bc5a88ef8cbb8bd5736b6a74d\", \"filename\": \"tests/test_logit_bias_numeric_string_coerce_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 3, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_logit_bias_numeric_string_coerce_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_logit_bias_numeric_string_coerce_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_logit_bias_numeric_string_coerce_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,7 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_responses_accepts_logit_bias_numeric_string_values() -> None:\\n+def test_http_responses_rejects_unapplied_logit_bias_after_type_check() -> None:\\n server, thread, port = _server()\\n try:\\n for val in (\\\"-5\\\", \\\"0\\\", \\\"100\\\", \\\" -12.5 \\\", 0, -5.0, 100):\\n@@ -65,7 +65,8 @@ def test_http_responses_accepts_logit_bias_numeric_string_values() -> None:\\n \\\"logit_bias\\\": {\\\"100\\\": val, \\\"200\\\": 1},\\n },\\n )\\n- assert status == 200, (val, body)\\n+ assert status == 422, (val, body)\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -113,7 +114,7 @@ def test_http_chat_still_rejects_nonempty_logit_bias_after_type_check() -> None:\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_responses_accepts_logit_bias_numeric_string_values()\\n+ test_http_responses_rejects_unapplied_logit_bias_after_type_check()\\n test_http_responses_still_rejects_logit_bias_bool_and_oob()\\n test_http_chat_still_rejects_nonempty_logit_bias_after_type_check()\\n print(\\\"ok\\\")\" }, { \"sha\": \"da13fb76872989146256581e5d41d6a6dcbd6c85\", \"filename\": \"tests/test_mode_casefold_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 4, \"changes\": 8, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_mode_casefold_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_mode_casefold_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_mode_casefold_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,23 +3,23 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n-from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n-from contextual_orchestrator.server import SecurityConfig, build_server # noqa: E402\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+from contextual_orchestrator.server import SecurityConfig, build_server\\n \\n _TEST_AUTH_TOKEN = \\\"mode_casefold_http_honesty_token\\\" # noqa: S105\\n \\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"embedding\\\"))]\\n )\\n \\n \" }, { \"sha\": \"d5fd91eb8127a05a9b4bba28ff96d6151430ed5c\", \"filename\": \"tests/test_model_discovery.py\", \"status\": \"modified\", \"additions\": 140, \"deletions\": 4, \"changes\": 144, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_model_discovery.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_model_discovery.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_model_discovery.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,6 +3,8 @@\\n from __future__ import annotations\\n \\n import json\\n+from contextlib import contextmanager\\n+import socket\\n import sys\\n import urllib.error\\n import urllib.parse\\n@@ -23,7 +25,9 @@\\n from contextual_orchestrator.kv_config import InMemoryConfigStore # noqa: E402\\n from contextual_orchestrator.model_discovery import ( # noqa: E402\\n DiscoveredModel,\\n+ ProviderDiscoveryError,\\n ProviderModelSource,\\n+ _fetch_json,\\n agent_from_discovered,\\n agent_id_for,\\n discover_all_models,\\n@@ -32,6 +36,7 @@\\n select_cheapest_discovered_agent,\\n select_top_n_cheapest_discovered_agents,\\n )\\n+from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n \\n \\n @pytest.fixture(autouse=True)\\n@@ -53,10 +58,27 @@ def __enter__(self):\\n def __exit__(self, *_args):\\n return False\\n \\n- def read(self) -> bytes:\\n+ def read(self, _size: int = -1) -> bytes:\\n return self._body\\n \\n \\n+@contextmanager\\n+def _patched_provider_transport(urlopen):\\n+ \\\"\\\"\\\"Keep discovery tests offline while exercising the validated transport seam.\\\"\\\"\\\"\\n+ def open_provider(request, _destination=None, *, timeout=None):\\n+ return urlopen(request, timeout=timeout)\\n+\\n+ with (\\n+ patch.object(\\n+ ModelClient,\\n+ \\\"_validate_provider\\\",\\n+ return_value=(socket.AF_INET, (\\\"93.184.216.34\\\", 443)),\\n+ ),\\n+ patch.object(ModelClient, \\\"_open_provider\\\", side_effect=open_provider),\\n+ ):\\n+ yield\\n+\\n+\\n OPENAI_SOURCE = ProviderModelSource(\\n provider_name=\\\"openai\\\",\\n credential_name=\\\"OPENAI_API_KEY\\\",\\n@@ -101,7 +123,7 @@ def urlopen(request, timeout=None):\\n seen_requests.append(request)\\n return _Response(payload)\\n \\n- with patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen):\\n+ with _patched_provider_transport(urlopen):\\n discovered = discover_provider_models(OPENROUTER_SOURCE)\\n \\n assert seen_requests[0].get_header(\\\"Authorization\\\") == \\\"Bearer sk-router\\\"\\n@@ -113,6 +135,117 @@ def urlopen(request, timeout=None):\\n assert discovered[1].prompt_price_per_1k is None\\n \\n \\n+def test_discover_local_gateway_is_not_a_model_discovery_source() -> None:\\n+ register_credential(\\\"LOCAL_GATEWAY_KEY\\\", \\\"local-secret\\\")\\n+ source = ProviderModelSource(\\n+ provider_name=\\\"local_gateway\\\",\\n+ credential_name=\\\"LOCAL_GATEWAY_KEY\\\",\\n+ list_url=\\\"local://host.docker.internal:8080/v1/models\\\",\\n+ chat_base_url=\\\"local://host.docker.internal:8080/v1\\\",\\n+ )\\n+ with pytest.raises(ProviderDiscoveryError, match=\\\"invalid_response\\\") as error:\\n+ discover_provider_models(source)\\n+ assert error.value.__cause__ is None\\n+\\n+\\n+def test_discover_rejects_private_provider_before_authorized_transport() -> None:\\n+ register_credential(\\\"PRIVATE_PROVIDER_KEY\\\", \\\"private-provider-secret\\\")\\n+ source = ProviderModelSource(\\n+ provider_name=\\\"private_provider\\\",\\n+ credential_name=\\\"PRIVATE_PROVIDER_KEY\\\",\\n+ list_url=\\\"https://models.example.test/v1/models\\\",\\n+ chat_base_url=\\\"https://models.example.test/v1\\\",\\n+ )\\n+ with (\\n+ patch.object(\\n+ ModelClient,\\n+ \\\"_resolve_addresses\\\",\\n+ return_value=[(socket.AF_INET, (\\\"127.0.0.1\\\", 443))],\\n+ ),\\n+ patch.object(ModelClient, \\\"_open_provider\\\") as open_provider,\\n+ ):\\n+ with pytest.raises(ProviderDiscoveryError, match=\\\"provider_error\\\") as error:\\n+ discover_provider_models(source)\\n+ assert error.value.__cause__ is None\\n+ open_provider.assert_not_called()\\n+\\n+\\n+def test_fetch_json_rejects_cross_origin_before_provider_transport() -> None:\\n+ \\\"\\\"\\\"Discovery cannot reuse a validated agent to send credentials elsewhere.\\\"\\\"\\\"\\n+ register_credential(\\\"OPENAI_API_KEY\\\", \\\"openai-secret\\\")\\n+ agent = ModelAgent(\\n+ \\\"model_discovery_agent\\\",\\n+ \\\"model_catalog\\\",\\n+ \\\"https://api.openai.com/v1\\\",\\n+ credential_key=\\\"OPENAI_API_KEY\\\",\\n+ )\\n+ client = ModelClient()\\n+ with (\\n+ patch.object(client, \\\"_validate_provider\\\") as validate_provider,\\n+ patch.object(client, \\\"_open_provider\\\") as open_provider,\\n+ pytest.raises(RuntimeError, match=\\\"validated agent origin\\\"),\\n+ ):\\n+ client.fetch_json(agent, \\\"https://attacker.example/v1/models\\\")\\n+ validate_provider.assert_not_called()\\n+ open_provider.assert_not_called()\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"url\\\",\\n+ [\\n+ \\\"https://@api.openai.com/v1/models\\\",\\n+ \\\"https://user:@api.openai.com/v1/models\\\",\\n+ \\\"https://api.openai.com/v1/models#\\\",\\n+ ],\\n+)\\n+def test_model_discovery_rejects_empty_userinfo_and_fragment(url: str) -> None:\\n+ with pytest.raises(ValueError, match=\\\"credentials or a fragment\\\"):\\n+ _fetch_json(\\n+ url,\\n+ auth_scheme=\\\"Bearer\\\",\\n+ timeout=1.0,\\n+ credential_name=\\\"OPENAI_API_KEY\\\",\\n+ )\\n+\\n+\\n+def test_model_discovery_rejects_invalid_port_and_preserves_nondefault_port() -> None:\\n+ with pytest.raises(ValueError, match=\\\"invalid port\\\"):\\n+ _fetch_json(\\n+ \\\"https://api.openai.com:not-a-port/v1/models\\\",\\n+ auth_scheme=\\\"Bearer\\\",\\n+ timeout=1.0,\\n+ credential_name=\\\"\\\",\\n+ )\\n+\\n+ with patch.object(ModelClient, \\\"fetch_json\\\", return_value={}) as fetch_json:\\n+ _fetch_json(\\n+ \\\"https://api.openai.com:8443/v1/models\\\",\\n+ auth_scheme=\\\"Bearer\\\",\\n+ timeout=1.0,\\n+ credential_name=\\\"\\\",\\n+ )\\n+ assert fetch_json.call_args.args[0].base_url == \\\"https://api.openai.com:8443\\\"\\n+\\n+\\n+def test_fetch_json_rejects_empty_userinfo_and_fragment_before_transport() -> None:\\n+ register_credential(\\\"OPENAI_API_KEY\\\", \\\"openai-secret\\\")\\n+ agent = ModelAgent(\\n+ \\\"model_discovery_agent\\\",\\n+ \\\"model_catalog\\\",\\n+ \\\"https://api.openai.com/v1\\\",\\n+ credential_key=\\\"OPENAI_API_KEY\\\",\\n+ )\\n+ client = ModelClient()\\n+ with (\\n+ patch.object(client, \\\"_validate_provider\\\") as validate_provider,\\n+ patch.object(client, \\\"_open_provider\\\") as open_provider,\\n+ pytest.raises(RuntimeError, match=\\\"validated agent origin\\\"),\\n+ ):\\n+ client.fetch_json(agent, \\\"https://@api.openai.com/v1/models#\\\")\\n+ validate_provider.assert_not_called()\\n+ open_provider.assert_not_called()\\n+\\n+\\n def test_discover_bytez_parses_models_with_key_auth_scheme() -> None:\\n register_credential(\\\"BYTEZ_API_KEY\\\", \\\"bytez-secret\\\")\\n payload = {\\n@@ -127,7 +260,7 @@ def urlopen(request, timeout=None):\\n seen_requests.append(request)\\n return _Response(payload)\\n \\n- with patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen):\\n+ with _patched_provider_transport(urlopen):\\n discovered = discover_provider_models(BYTEZ_SOURCE)\\n \\n assert seen_requests[0].get_header(\\\"Authorization\\\") == \\\"Key bytez-secret\\\"\\n@@ -148,12 +281,15 @@ def urlopen(request, timeout=None):\\n raise urllib.error.URLError(\\\"connection refused\\\")\\n return _Response({\\\"data\\\": [{\\\"id\\\": \\\"meta/llama-3.3\\\"}]})\\n \\n- with patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen):\\n+ with _patched_provider_transport(urlopen):\\n discovered, errors = discover_all_models((OPENAI_SOURCE, OPENROUTER_SOURCE))\\n \\n assert [m.model_id for m in discovered] == [\\\"meta/llama-3.3\\\"]\\n assert len(errors) == 1\\n assert errors[0].provider_name == \\\"openai\\\"\\n+ assert errors[0].error_code == \\\"transport_error\\\"\\n+ assert \\\"connection refused\\\" not in str(errors[0])\\n+ assert errors[0].__cause__ is None\\n \\n \\n def test_agent_id_for_is_two_word_snake_case() -> None:\" }, { \"sha\": \"f0c079bd09b6144a6b4c358380f702eeb259c292\", \"filename\": \"tests/test_model_judge.py\", \"status\": \"modified\", \"additions\": 42, \"deletions\": 7, \"changes\": 49, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_model_judge.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_model_judge.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_model_judge.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -117,6 +117,7 @@ def test_structured_model_judge_accepts() -> None:\\n result = orchestrator.conduct(MESSAGES)\\n assert result[\\\"verification\\\"][\\\"accepted\\\"] is True\\n assert result[\\\"verification\\\"][\\\"judge\\\"] == \\\"model\\\"\\n+ assert result[\\\"verification\\\"][\\\"judge_agent_id\\\"] == \\\"general_agent\\\"\\n assert client.calls == 5\\n assert result[\\\"answer\\\"] == \\\"step-output(4)\\\"\\n \\n@@ -276,7 +277,7 @@ def test_fast_mlsirm_adapter_accepts_contextual_judge_mode_keyword() -> None:\\n assert completion[\\\"mode\\\"] == \\\"conduct\\\"\\n \\n \\n-def test_fast_mlsirm_adapter_routes_structured_completion_through_gateway() -> None:\\n+def test_fast_mlsirm_adapter_keeps_structured_completion_to_one_provider_call() -> None:\\n orchestrator, _ = _orch(\\\"unused\\\")\\n adapter = orchestrator_module._FastMLSIJudgeAdapter(\\n orchestrator,\\n@@ -289,33 +290,57 @@ def test_fast_mlsirm_adapter_routes_structured_completion_through_gateway() -> N\\n \\\"json_schema\\\": {\\\"name\\\": \\\"judge\\\", \\\"strict\\\": True, \\\"schema\\\": {\\\"type\\\": \\\"object\\\"}},\\n }\\n with patch.object(\\n- orchestrator,\\n- \\\"proxy_completion\\\",\\n+ orchestrator.client,\\n+ \\\"proxy_send\\\",\\n return_value={\\n \\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": '{\\\"meets_threshold\\\":true,\\\"rationale\\\":\\\"ok\\\"}'}}],\\n \\\"usage\\\": {\\\"prompt_tokens\\\": 3, \\\"completion_tokens\\\": 2, \\\"total_tokens\\\": 5},\\n },\\n- ) as proxy:\\n+ ) as proxy_send:\\n completion = adapter.complete_structured(\\n [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"judge\\\"}],\\n mode=\\\"conduct\\\",\\n response_format=response_format,\\n )\\n \\n- proxy.assert_called_once_with(\\n+ proxy_send.assert_called_once_with(\\n+ orchestrator._agent(\\\"general_agent\\\"),\\n+ \\\"chat/completions\\\",\\n {\\n \\\"model\\\": \\\"model-x\\\",\\n \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"judge\\\"}],\\n- \\\"temperature\\\": orchestrator.client.temperature,\\n \\\"max_tokens\\\": orchestrator.client.max_output_tokens,\\n \\\"response_format\\\": response_format,\\n- }\\n+ \\\"stream\\\": False,\\n+ },\\n )\\n assert completion[\\\"answer\\\"] == '{\\\"meets_threshold\\\":true,\\\"rationale\\\":\\\"ok\\\"}'\\n assert completion[\\\"mode\\\"] == \\\"conduct\\\"\\n assert completion[\\\"trace\\\"][0][\\\"usage\\\"][\\\"total_tokens\\\"] == 5\\n \\n \\n+def test_fast_mlsirm_structured_adapter_omits_implicit_temperature() -> None:\\n+ orchestrator, _ = _orch(\\\"unused\\\")\\n+ orchestrator.client.temperature = None\\n+ adapter = orchestrator_module._FastMLSIJudgeAdapter(\\n+ orchestrator,\\n+ \\\"task\\\",\\n+ \\\"general_agent\\\",\\n+ )\\n+\\n+ with patch.object(\\n+ orchestrator.client,\\n+ \\\"proxy_send\\\",\\n+ return_value={\\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": \\\"{}\\\"}}]},\\n+ ) as proxy:\\n+ adapter.complete_structured(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"judge\\\"}],\\n+ response_format={\\\"type\\\": \\\"json_object\\\"},\\n+ )\\n+\\n+ assert \\\"temperature\\\" not in proxy.call_args.args[2]\\n+\\n+\\n def test_fast_mlsirm_judge_contract_does_not_pass_threshold_to_judge_call() -> None:\\n class _Judge:\\n def __init__(self, _orchestrator, *, mode: str, accept_threshold: float) -> None:\\n@@ -457,6 +482,16 @@ def test_model_judge_parser_rejects_oversized_reply() -> None:\\n _parse_model_judge_reply(\\\"x\\\" * 32_001)\\n \\n \\n+def test_model_judge_parser_hides_raw_provider_response() -> None:\\n+ raw_provider_response = \\\"provider-secret-response\\\"\\n+\\n+ with pytest.raises(ValueError, match=\\\"not valid JSON\\\") as error:\\n+ _parse_model_judge_reply(raw_provider_response)\\n+\\n+ assert raw_provider_response not in str(error.value)\\n+ assert error.value.__cause__ is None\\n+\\n+\\n def test_missing_fast_mlsirm_does_not_use_a_direct_judge_fallback() -> None:\\n orchestrator, _ = _orch(\\\"unused\\\")\\n with patch.object(orchestrator_module, \\\"_resolve_fast_mlsirm_components\\\", return_value=None), patch.object(\" }, { \"sha\": \"9ccc77d51a314f50e64a960c445f15d46699f4f8\", \"filename\": \"tests/test_model_strip_writeback_http_honesty.py\", \"status\": \"modified\", \"additions\": 14, \"deletions\": 16, \"changes\": 30, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_model_strip_writeback_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_model_strip_writeback_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_model_strip_writeback_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,4 +1,4 @@\\n-\\\"\\\"\\\"Model strip writeback so tools/Responses passthrough bind padded names.\\\"\\\"\\\"\\n+\\\"\\\"\\\"Model strip writeback keeps routed and structured requests bound to the pool.\\\"\\\"\\\"\\n \\n from __future__ import annotations\\n \\n@@ -82,8 +82,8 @@ def test_unit_model_rejects_blank() -> None:\\n assert getattr(exc, \\\"code\\\", None) == \\\"invalid_model\\\"\\n \\n \\n-def test_http_chat_tools_accepts_padded_model() -> None:\\n- \\\"\\\"\\\"Tools passthrough uses body.model for pool match — must see strip writeback.\\\"\\\"\\\"\\n+def test_http_chat_tools_rejects_padded_model_after_validation() -> None:\\n+ \\\"\\\"\\\"Validated tool requests stop at the explicit multi-agent contract boundary.\\\"\\\"\\\"\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -103,7 +103,8 @@ def test_http_chat_tools_accepts_padded_model() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -142,7 +143,7 @@ def test_http_responses_accepts_padded_model() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_tools_accepts_padded_model() -> None:\\n+def test_http_responses_tools_rejects_padded_model_after_validation() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -160,7 +161,8 @@ def test_http_responses_tools_accepts_padded_model() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -180,7 +182,7 @@ def test_http_completions_accepts_padded_model() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_still_rejects_unknown_padded_model() -> None:\\n+def test_http_chat_tools_fail_closed_before_model_lookup() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -200,12 +202,8 @@ def test_http_chat_still_rejects_unknown_padded_model() -> None:\\n ],\\n },\\n )\\n- assert status == 400, body\\n- blob = json.dumps(body)\\n- assert \\\"invalid_request\\\" in blob or \\\"not available\\\" in blob\\n- assert \\\"no-such-model\\\" in blob\\n- # Must not echo leading pad after strip (buyer sees real id).\\n- assert \\\"' no-such-model '\\\" not in blob\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -214,10 +212,10 @@ def test_http_chat_still_rejects_unknown_padded_model() -> None:\\n if __name__ == \\\"__main__\\\":\\n test_unit_model_strip_writeback()\\n test_unit_model_rejects_blank()\\n- test_http_chat_tools_accepts_padded_model()\\n+ test_http_chat_tools_rejects_padded_model_after_validation()\\n test_http_chat_response_format_accepts_padded_model()\\n test_http_responses_accepts_padded_model()\\n- test_http_responses_tools_accepts_padded_model()\\n+ test_http_responses_tools_rejects_padded_model_after_validation()\\n test_http_completions_accepts_padded_model()\\n- test_http_chat_still_rejects_unknown_padded_model()\\n+ test_http_chat_tools_fail_closed_before_model_lookup()\\n print(\\\"ok\\\")\" }, { \"sha\": \"f51155b33de9d313f88fed4e0ebbd8837b66f043\", \"filename\": \"tests/test_multimodal_content_parts_shape_http_honesty.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_content_parts_shape_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_content_parts_shape_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_multimodal_content_parts_shape_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"vision\\\"))]\\n )\\n \\n \" }, { \"sha\": \"253cba1c80781f4c719f16e5e3fa7ac35500793f\", \"filename\": \"tests/test_multimodal_message_content_http_honesty.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_message_content_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_message_content_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_multimodal_message_content_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"vision\\\"))]\\n )\\n \\n \" }, { \"sha\": \"2c9153d48ef44d827ee44bc642bd5b88ca499290\", \"filename\": \"tests/test_multimodal_required_tag_boundary.py\", \"status\": \"added\", \"additions\": 55, \"deletions\": 0, \"changes\": 55, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_required_tag_boundary.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_required_tag_boundary.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_multimodal_required_tag_boundary.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,55 @@\\n+\\\"\\\"\\\"Regression for enforcing multimodal capability at the invocation boundary.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+\\n+\\n+class _RecordingClient:\\n+ \\\"\\\"\\\"Record which synthetic agent the invocation boundary actually calls.\\\"\\\"\\\"\\n+\\n+ def __init__(self) -> None:\\n+ self.calls: list[str] = []\\n+\\n+ def chat(self, agent: ModelAgent, _messages, **_kwargs) -> str:\\n+ self.calls.append(agent.id)\\n+ return agent.id\\n+\\n+\\n+def test_required_tags_filter_an_ineligible_explicit_primary() -> None:\\n+ \\\"\\\"\\\"A stale or direct caller cannot smuggle a text-only primary into image work.\\\"\\\"\\\"\\n+ text_agent = ModelAgent(\\n+ \\\"text_agent\\\",\\n+ \\\"text-model\\\",\\n+ tags=(\\\"reasoning\\\", \\\"writing\\\"),\\n+ priority=100,\\n+ )\\n+ vision_agent = ModelAgent(\\n+ \\\"vision_agent\\\",\\n+ \\\"vision-model\\\",\\n+ tags=(\\\"vision\\\", \\\"reasoning\\\", \\\"writing\\\"),\\n+ priority=1,\\n+ )\\n+ client = _RecordingClient()\\n+ orchestrator = TaskOrchestrator(\\n+ [text_agent, vision_agent],\\n+ client=client,\\n+ )\\n+\\n+ answer, served_agent_id, _usage = orchestrator._invoke(\\n+ text_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Inspect the source image.\\\"}],\\n+ text=\\\"Inspect the source image.\\\",\\n+ role=\\\"worker\\\",\\n+ required_tags=(\\\"vision\\\",),\\n+ )\\n+\\n+ assert answer == \\\"vision_agent\\\"\\n+ assert served_agent_id == \\\"vision_agent\\\"\\n+ assert client.calls == [\\\"vision_agent\\\"]\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"8c20551b4605434f0d0de65e3555f26de617dad4\", \"filename\": \"tests/test_multimodal_workflow_evidence.py\", \"status\": \"added\", \"additions\": 162, \"deletions\": 0, \"changes\": 162, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_workflow_evidence.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_workflow_evidence.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_multimodal_workflow_evidence.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,162 @@\\n+from __future__ import annotations\\n+\\n+from pathlib import Path\\n+import sys\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator.orchestrator import _responses_to_chat_payload # noqa: E402\\n+\\n+\\n+IMAGE_PART = {\\n+ \\\"type\\\": \\\"image_url\\\",\\n+ \\\"image_url\\\": {\\n+ \\\"url\\\": \\\"data:image/png;base64,c3ludGhldGljLWZpeHR1cmU=\\\",\\n+ \\\"detail\\\": \\\"high\\\",\\n+ },\\n+}\\n+\\n+\\n+class RecordingClient:\\n+ \\\"\\\"\\\"Record synthetic provider calls without contacting a model.\\\"\\\"\\\"\\n+\\n+ def __init__(self, failing_agent_id: str | None = None) -> None:\\n+ self.failing_agent_id = failing_agent_id\\n+ self.calls: list[tuple[str, list[dict[str, object]]]] = []\\n+\\n+ def chat(self, agent: ModelAgent, messages, temperature: float = 0.2) -> str:\\n+ \\\"\\\"\\\"Return deterministic output, or fail the selected synthetic agent.\\\"\\\"\\\"\\n+ self.calls.append((agent.id, messages))\\n+ if agent.id == self.failing_agent_id:\\n+ raise RuntimeError(\\\"synthetic provider failure\\\")\\n+ return f\\\"{agent.id}:{len(self.calls)}\\\"\\n+\\n+\\n+def test_conduct_preserves_source_images_for_every_evidence_step() -> None:\\n+ client = RecordingClient()\\n+ orchestrator = TaskOrchestrator(\\n+ [ModelAgent(\\\"vision_agent\\\", \\\"mock-vision\\\", tags=(\\\"vision\\\",))],\\n+ client=client,\\n+ )\\n+\\n+ result = orchestrator.conduct(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"Read the table.\\\"}, IMAGE_PART]}]\\n+ )\\n+\\n+ assert result[\\\"mode\\\"] == \\\"conduct\\\"\\n+ assert len(client.calls) == 4\\n+ for agent_id, messages in client.calls:\\n+ assert agent_id == \\\"vision_agent\\\"\\n+ content = messages[-1][\\\"content\\\"]\\n+ assert isinstance(content, list)\\n+ assert content[-1] == IMAGE_PART\\n+ assert \\\"data:image\\\" not in content[0][\\\"text\\\"]\\n+\\n+\\n+def test_image_route_failover_never_uses_a_text_only_agent() -> None:\\n+ client = RecordingClient(failing_agent_id=\\\"vision_primary\\\")\\n+ orchestrator = TaskOrchestrator(\\n+ [\\n+ ModelAgent(\\\"text_agent\\\", \\\"mock-text\\\", tags=(\\\"reasoning\\\",), priority=100),\\n+ ModelAgent(\\\"vision_primary\\\", \\\"mock-vision-primary\\\", tags=(\\\"vision\\\",), priority=20),\\n+ ModelAgent(\\\"vision_backup\\\", \\\"mock-vision-backup\\\", tags=(\\\"vision\\\",), priority=10),\\n+ ],\\n+ client=client,\\n+ )\\n+\\n+ result = orchestrator.route_once(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"Read the image.\\\"}, IMAGE_PART]}]\\n+ )\\n+\\n+ assert result[\\\"answer\\\"].startswith(\\\"vision_backup:\\\")\\n+ assert [agent_id for agent_id, _ in client.calls] == [\\\"vision_primary\\\", \\\"vision_backup\\\"]\\n+\\n+\\n+def test_image_route_fails_before_io_without_a_vision_agent() -> None:\\n+ client = RecordingClient()\\n+ orchestrator = TaskOrchestrator(\\n+ [ModelAgent(\\\"text_agent\\\", \\\"mock-text\\\", tags=(\\\"reasoning\\\",))],\\n+ client=client,\\n+ )\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"required tags.*vision\\\"):\\n+ orchestrator.route_once(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"Read it.\\\"}, IMAGE_PART]}]\\n+ )\\n+\\n+ assert client.calls == []\\n+\\n+\\n+def test_responses_input_image_is_normalized_for_chat_orchestration() -> None:\\n+ payload = _responses_to_chat_payload(\\n+ {\\n+ \\\"model\\\": \\\"contextual-orchestrator\\\",\\n+ \\\"input\\\": [\\n+ {\\n+ \\\"type\\\": \\\"message\\\",\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\\"type\\\": \\\"input_text\\\", \\\"text\\\": \\\"Read the table.\\\"},\\n+ {\\n+ \\\"type\\\": \\\"input_image\\\",\\n+ \\\"image_url\\\": \\\"data:image/png;base64,c3ludGhldGljLWZpeHR1cmU=\\\",\\n+ \\\"detail\\\": \\\"high\\\",\\n+ },\\n+ ],\\n+ }\\n+ ],\\n+ }\\n+ )\\n+\\n+ assert payload[\\\"messages\\\"] == [\\n+ {\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"Read the table.\\\"},\\n+ IMAGE_PART,\\n+ ],\\n+ }\\n+ ]\\n+\\n+\\n+def test_responses_input_image_requires_a_url() -> None:\\n+ with pytest.raises(ValueError, match=\\\"image_url\\\"):\\n+ _responses_to_chat_payload(\\n+ {\\n+ \\\"input\\\": [\\n+ {\\n+ \\\"type\\\": \\\"message\\\",\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [{\\\"type\\\": \\\"input_image\\\", \\\"file_id\\\": \\\"synthetic-file\\\"}],\\n+ }\\n+ ]\\n+ }\\n+ )\\n+\\n+\\n+def test_responses_image_detail_is_normalized_or_rejected() -> None:\\n+ request = {\\n+ \\\"input\\\": [\\n+ {\\n+ \\\"type\\\": \\\"message\\\",\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\n+ \\\"type\\\": \\\"input_image\\\",\\n+ \\\"image_url\\\": {\\\"url\\\": \\\"https://example.invalid/synthetic.png\\\", \\\"detail\\\": None},\\n+ }\\n+ ],\\n+ }\\n+ ]\\n+ }\\n+\\n+ assert _responses_to_chat_payload(request)[\\\"messages\\\"][0][\\\"content\\\"][0] == {\\n+ \\\"type\\\": \\\"image_url\\\",\\n+ \\\"image_url\\\": {\\\"url\\\": \\\"https://example.invalid/synthetic.png\\\"},\\n+ }\\n+ request[\\\"input\\\"][0][\\\"content\\\"][0][\\\"image_url\\\"][\\\"detail\\\"] = \\\"pixel-perfect\\\"\\n+ with pytest.raises(ValueError, match=\\\"detail\\\"):\\n+ _responses_to_chat_payload(request)\" }, { \"sha\": \"ea97f5f1c2b36370a0d16412b0d74a5401476a12\", \"filename\": \"tests/test_openai_passthrough.py\", \"status\": \"modified\", \"additions\": 888, \"deletions\": 25, \"changes\": 913, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_openai_passthrough.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_openai_passthrough.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_openai_passthrough.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,9 +1,4 @@\\n-\\\"\\\"\\\"Full OpenAI passthrough: response_format / tools / the Responses API.\\n-\\n-Requests carrying provider features the multi-agent verifier cannot merge are\\n-proxied to one agent so the full provider response shape survives, while plain\\n-prompts keep the orchestration (routing/verification) path.\\n-\\\"\\\"\\\"\\n+\\\"\\\"\\\"OpenAI provider features remain inside multi-agent orchestration.\\\"\\\"\\\"\\n \\n from __future__ import annotations\\n \\n@@ -12,30 +7,101 @@\\n import threading\\n import urllib.error\\n import urllib.request\\n+from concurrent.futures import ThreadPoolExecutor\\n from pathlib import Path\\n+from unittest.mock import patch\\n \\n import pytest\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n-from contextual_orchestrator.server import SecurityConfig, build_server, responses_sse_body # noqa: E402\\n+from contextual_orchestrator.orchestrator import ( # noqa: E402\\n+ BudgetExceededError,\\n+ _FastMLSIJudgeAdapter,\\n+ _responses_to_chat_payload,\\n+ _responses_text_format_to_chat_response_format,\\n+ estimate_tokens,\\n+)\\n+from contextual_orchestrator.server import ( # noqa: E402\\n+ SecurityConfig,\\n+ build_server,\\n+ responses_sse_body,\\n+)\\n \\n \\n-def _build() -> TaskOrchestrator:\\n+def _build(*, budget_max_output_tokens: int | None = None) -> TaskOrchestrator:\\n return TaskOrchestrator(\\n agents=[\\n- ModelAgent(\\\"planner_agent\\\", \\\"mock-planner\\\", tags=(\\\"planning\\\", \\\"reasoning\\\")),\\n+ ModelAgent(\\\"planner_agent\\\", \\\"mock-planner\\\", tags=(\\\"planning\\\", \\\"reasoning\\\", \\\"vision\\\")),\\n ModelAgent(\\\"disabled_builder_duplicate\\\", \\\"mock-builder\\\", disabled=True),\\n ModelAgent(\\\"builder_agent\\\", \\\"mock-builder\\\", tags=(\\\"coding\\\", \\\"implementation\\\")),\\n ModelAgent(\\\"reviewer_agent\\\", \\\"mock-reviewer\\\", tags=(\\\"verification\\\", \\\"review\\\")),\\n ModelAgent(\\\"disabled_candidate\\\", \\\"disabled-model\\\", disabled=True),\\n- ]\\n+ ],\\n+ budget_max_output_tokens=budget_max_output_tokens,\\n )\\n \\n \\n # -- orchestrator-level ------------------------------------------------------\\n \\n+def test_responses_translation_preserves_input_image_content() -> None:\\n+ translated = _responses_to_chat_payload(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": [\\n+ {\\n+ \\\"type\\\": \\\"message\\\",\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\\"type\\\": \\\"input_text\\\", \\\"text\\\": \\\"Inspect this image\\\"},\\n+ {\\n+ \\\"type\\\": \\\"input_image\\\",\\n+ \\\"image_url\\\": \\\"data:image/png;base64,AA==\\\",\\n+ \\\"detail\\\": \\\"high\\\",\\n+ },\\n+ ],\\n+ }\\n+ ],\\n+ }\\n+ )\\n+\\n+ assert translated[\\\"messages\\\"] == [\\n+ {\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"Inspect this image\\\"},\\n+ {\\n+ \\\"type\\\": \\\"image_url\\\",\\n+ \\\"image_url\\\": {\\n+ \\\"url\\\": \\\"data:image/png;base64,AA==\\\",\\n+ \\\"detail\\\": \\\"high\\\",\\n+ },\\n+ },\\n+ ],\\n+ }\\n+ ]\\n+\\n+\\n+def test_final_synthesis_attaches_private_evidence_to_latest_user_turn() -> None:\\n+ result = _build().proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [\\n+ {\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Earlier question\\\"},\\n+ {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"Earlier answer\\\"},\\n+ {\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Current task\\\"},\\n+ ],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ messages = result[\\\"echo\\\"][\\\"messages\\\"]\\n+ assert messages[0][\\\"content\\\"] == \\\"Earlier question\\\"\\n+ assert messages[2][\\\"content\\\"].startswith(\\\"Current task\\\")\\n+ assert \\\"Verified workflow evidence\\\" in messages[2][\\\"content\\\"]\\n+\\n+\\n def test_proxy_completion_forwards_response_format_and_returns_full_shape() -> None:\\n orch = _build()\\n body = {\\n@@ -55,6 +121,8 @@ def test_proxy_completion_forwards_response_format_and_returns_full_shape() -> N\\n assert \\\"mode\\\" not in result[\\\"echo\\\"]\\n # model overridden to the selected agent's model.\\n assert result[\\\"model\\\"] in {\\\"mock-planner\\\", \\\"mock-builder\\\", \\\"mock-reviewer\\\"}\\n+ assert result[\\\"orchestration\\\"][\\\"mode\\\"] == \\\"conduct\\\"\\n+ assert result[\\\"orchestration\\\"][\\\"agent_count\\\"] == 4\\n \\n \\n def test_proxy_completion_forwards_tools() -> None:\\n@@ -64,6 +132,7 @@ def test_proxy_completion_forwards_tools() -> None:\\n {\\\"model\\\": \\\"mock-planner\\\", \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"call a tool\\\"}], \\\"tools\\\": tools}\\n )\\n assert result[\\\"echo\\\"][\\\"tools\\\"] == tools\\n+ assert result[\\\"orchestration\\\"][\\\"agent_count\\\"] == 4\\n \\n \\n def test_proxy_completion_honors_an_enabled_requested_worker_model() -> None:\\n@@ -104,6 +173,582 @@ def test_proxy_completion_rejects_disabled_and_malformed_requested_models() -> N\\n })\\n \\n \\n+def test_proxy_completion_blocks_before_structured_workflow_when_budget_is_exceeded() -> None:\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ _build(budget_max_output_tokens=0).proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+\\n+def test_structured_budget_stops_before_the_next_workflow_provider_call() -> None:\\n+ orchestrator = _build(budget_max_output_tokens=1)\\n+\\n+ with (\\n+ patch.object(orchestrator.client, \\\"chat\\\", wraps=orchestrator.client.chat) as chat,\\n+ patch.object(orchestrator.client, \\\"proxy_send\\\", wraps=orchestrator.client.proxy_send) as send,\\n+ pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"),\\n+ ):\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ assert chat.call_count == 1\\n+ send.assert_not_called()\\n+\\n+\\n+def test_model_client_request_settings_are_thread_local() -> None:\\n+ client = _build().client\\n+ previous_temperature = client.default_temperature\\n+ barrier = threading.Barrier(2)\\n+\\n+ def read_settings(temperature: float, max_tokens: int) -> tuple[float, int]:\\n+ with client.request_settings(temperature=temperature, max_output_tokens=max_tokens):\\n+ barrier.wait(timeout=5)\\n+ values = (\\n+ client._request_setting(\\\"temperature\\\", client.default_temperature),\\n+ client._request_setting(\\\"max_output_tokens\\\", client.max_output_tokens),\\n+ )\\n+ barrier.wait(timeout=5)\\n+ return values\\n+\\n+ with ThreadPoolExecutor(max_workers=2) as executor:\\n+ futures = [\\n+ executor.submit(read_settings, 0.1, 11),\\n+ executor.submit(read_settings, 0.9, 29),\\n+ ]\\n+ assert {future.result() for future in futures} == {(0.1, 11), (0.9, 29)}\\n+\\n+ assert client.default_temperature == previous_temperature\\n+ assert client.max_output_tokens == 2048\\n+\\n+ with client.request_settings(temperature=0.3):\\n+ with client.request_settings(temperature=0.4):\\n+ assert client._request_setting(\\\"temperature\\\", None) == 0.4\\n+ assert client._request_setting(\\\"temperature\\\", None) == 0.3\\n+\\n+\\n+def test_plain_proxy_completion_persists_reported_usage_before_next_budget_check() -> None:\\n+ orch = _build(budget_max_output_tokens=3)\\n+ raw = {\\n+ \\\"id\\\": \\\"chatcmpl-accounted\\\",\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"choices\\\": [\\n+ {\\n+ \\\"index\\\": 0,\\n+ \\\"message\\\": {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"accounted\\\"},\\n+ \\\"finish_reason\\\": \\\"stop\\\",\\n+ }\\n+ ],\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 2, \\\"completion_tokens\\\": 3, \\\"total_tokens\\\": 5},\\n+ \\\"echo\\\": {},\\n+ }\\n+ body = {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"plain passthrough\\\"}],\\n+ }\\n+\\n+ with patch.object(orch.client, \\\"proxy_send\\\", return_value=raw) as send:\\n+ assert orch.proxy_completion(body)[\\\"id\\\"] == \\\"chatcmpl-accounted\\\"\\n+ analytics = orch.spend_analytics()\\n+ assert analytics[\\\"totals\\\"][\\\"run_count\\\"] == 1\\n+ assert analytics[\\\"budget\\\"][\\\"spent_output_tokens\\\"] == 3\\n+ assert analytics[\\\"by_model\\\"] == [\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"estimated_output_tokens\\\": 3,\\n+ \\\"output_tokens\\\": 3,\\n+ \\\"usage_source\\\": \\\"reported\\\",\\n+ \\\"step_count\\\": 1,\\n+ \\\"price_per_million_usd\\\": None,\\n+ \\\"estimated_cost_usd\\\": None,\\n+ }\\n+ ]\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orch.proxy_completion(body)\\n+\\n+ assert send.call_count == 1\\n+\\n+\\n+def test_responses_tool_loop_usage_counts_toward_the_next_budget_check() -> None:\\n+ orch = _build(budget_max_output_tokens=3)\\n+ raw = {\\n+ \\\"id\\\": \\\"resp_accounted\\\",\\n+ \\\"object\\\": \\\"response\\\",\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"output\\\": [\\n+ {\\n+ \\\"type\\\": \\\"function_call\\\",\\n+ \\\"call_id\\\": \\\"call_1\\\",\\n+ \\\"name\\\": \\\"lookup\\\",\\n+ \\\"arguments\\\": \\\"{}\\\",\\n+ }\\n+ ],\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 2, \\\"output_tokens\\\": 3, \\\"total_tokens\\\": 5},\\n+ }\\n+ body = {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"call the tool\\\",\\n+ \\\"tools\\\": [{\\\"type\\\": \\\"function\\\", \\\"name\\\": \\\"lookup\\\"}],\\n+ }\\n+\\n+ with patch.object(orch.client, \\\"proxy_send\\\", return_value=raw) as send:\\n+ assert orch.proxy_completion(body, endpoint=\\\"responses\\\", single_agent=True) is raw\\n+ assert raw[\\\"usage\\\"] == {\\\"input_tokens\\\": 2, \\\"output_tokens\\\": 3, \\\"total_tokens\\\": 5}\\n+ analytics = orch.spend_analytics()\\n+ assert analytics[\\\"totals\\\"][\\\"reported_prompt_tokens\\\"] == 2\\n+ assert analytics[\\\"budget\\\"][\\\"spent_output_tokens\\\"] == 3\\n+ assert analytics[\\\"by_model\\\"][0][\\\"usage_source\\\"] == \\\"reported\\\"\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orch.proxy_completion(body, endpoint=\\\"responses\\\", single_agent=True)\\n+\\n+ assert send.call_count == 1\\n+\\n+\\n+def test_plain_proxy_completion_accounts_a_tool_only_response() -> None:\\n+ orch = _build()\\n+ raw = {\\n+ \\\"choices\\\": [\\n+ {\\n+ \\\"message\\\": {\\n+ \\\"content\\\": None,\\n+ \\\"tool_calls\\\": [{\\\"id\\\": \\\"call_1\\\", \\\"type\\\": \\\"function\\\"}],\\n+ }\\n+ }\\n+ ]\\n+ }\\n+\\n+ with patch.object(orch.client, \\\"proxy_send\\\", return_value=raw):\\n+ assert orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"call the tool\\\"}],\\n+ },\\n+ single_agent=True,\\n+ ) is raw\\n+\\n+ assert next(iter(orch._workflow_runs.values()))[\\\"answer\\\"] == \\\"\\\"\\n+\\n+\\n+def test_structured_provider_completion_rechecks_budget_before_final_provider_call() -> None:\\n+ orch = _build()\\n+ orch.budget_max_cost_usd = 1.0\\n+ orch.price_per_million = {\\\"mock-planner\\\": 1_000_000.0}\\n+ with patch.object(\\n+ orch,\\n+ \\\"conduct\\\",\\n+ return_value={\\n+ \\\"trace\\\": [\\n+ {\\n+ \\\"id\\\": \\\"worker\\\",\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"planner_agent\\\",\\n+ \\\"output\\\": \\\"verified\\\",\\n+ }\\n+ ]\\n+ },\\n+ ), patch.object(orch.client, \\\"proxy_send\\\") as send:\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ send.assert_not_called()\\n+\\n+\\n+def test_structured_provider_completion_counts_model_judge_cost() -> None:\\n+ orch = _build()\\n+ orch.budget_max_cost_usd = 3.0\\n+ orch.price_per_million = {\\\"mock-reviewer\\\": 1_000_000.0}\\n+ workflow = {\\n+ \\\"trace\\\": [{\\\"id\\\": 0, \\\"agent_id\\\": \\\"builder_agent\\\", \\\"role\\\": \\\"worker\\\", \\\"output\\\": \\\"ok\\\"}],\\n+ \\\"verification\\\": {\\n+ \\\"accepted\\\": True,\\n+ \\\"judge_agent_id\\\": \\\"reviewer_agent\\\",\\n+ \\\"judge_usage\\\": {\\\"completion_tokens\\\": 3},\\n+ },\\n+ }\\n+ with patch.object(orch, \\\"conduct\\\", return_value=workflow), patch.object(\\n+ orch.client, \\\"proxy_send\\\"\\n+ ) as send, pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ send.assert_not_called()\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"additional_spend\\\",\\n+ [\\n+ {\\\"additional_output_tokens\\\": True},\\n+ {\\\"additional_output_tokens\\\": -1},\\n+ {\\\"additional_cost_usd\\\": True},\\n+ {\\\"additional_cost_usd\\\": float(\\\"inf\\\")},\\n+ {\\\"additional_cost_usd\\\": -0.01},\\n+ ],\\n+)\\n+def test_in_flight_budget_rejects_invalid_usage(\\n+ additional_spend: dict[str, int | float | bool],\\n+) -> None:\\n+ with pytest.raises(ValueError, match=\\\"must be a non-negative\\\"):\\n+ _build()._raise_if_spend_budget_exceeded(**additional_spend)\\n+\\n+\\n+def test_structured_provider_completion_counts_in_flight_usage_before_synthesis() -> None:\\n+ orch = _build(budget_max_output_tokens=1)\\n+ workflow = {\\n+ \\\"trace\\\": [{\\\"id\\\": 0, \\\"agent_id\\\": \\\"builder_agent\\\", \\\"role\\\": \\\"worker\\\", \\\"output\\\": \\\"verified\\\"}],\\n+ \\\"verification\\\": {\\\"accepted\\\": True},\\n+ }\\n+ with patch.object(orch, \\\"conduct\\\", return_value=workflow), patch.object(\\n+ orch.client, \\\"proxy_send\\\"\\n+ ) as send:\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ send.assert_not_called()\\n+\\n+\\n+def test_structured_provider_completion_enables_model_judge_boundary() -> None:\\n+ orch = _build()\\n+ workflow = {\\n+ \\\"trace\\\": [{\\\"id\\\": 0, \\\"agent_id\\\": \\\"builder_agent\\\", \\\"role\\\": \\\"worker\\\", \\\"output\\\": \\\"verified\\\"}],\\n+ \\\"verification\\\": {\\\"accepted\\\": True},\\n+ }\\n+ raw = {\\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": \\\"{}\\\"}}]}\\n+ with patch.object(orch, \\\"conduct\\\", return_value=workflow) as conduct, patch.object(\\n+ orch.client, \\\"proxy_send\\\", return_value=raw\\n+ ):\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ assert conduct.call_args.kwargs[\\\"judge\\\"] is True\\n+\\n+\\n+def test_native_responses_drops_chat_only_output_budget_aliases() -> None:\\n+ orch = _build()\\n+ raw = {\\\"object\\\": \\\"response\\\", \\\"output_text\\\": \\\"{}\\\", \\\"output\\\": []}\\n+ with patch.object(orch.client, \\\"proxy_send\\\", return_value=raw) as send:\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"extract JSON\\\",\\n+ \\\"max_tokens\\\": 11,\\n+ \\\"max_completion_tokens\\\": 13,\\n+ \\\"max_output_tokens\\\": 17,\\n+ \\\"text\\\": {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}},\\n+ },\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ forwarded = send.call_args.args[2]\\n+ assert \\\"max_tokens\\\" not in forwarded\\n+ assert \\\"max_completion_tokens\\\" not in forwarded\\n+ assert forwarded[\\\"max_output_tokens\\\"] == 17\\n+\\n+\\n+def test_structured_provider_completion_persists_final_synthesis_run() -> None:\\n+ orch = _build()\\n+ result = orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ run_id = result[\\\"orchestration\\\"][\\\"workflow_run_id\\\"]\\n+ run = orch.get_workflow_run(run_id)\\n+ assert run[\\\"trace\\\"][-1][\\\"role\\\"] == \\\"synthesizer\\\"\\n+ assert run[\\\"trace\\\"][-1][\\\"subtask\\\"] == \\\"Provider-facing structured synthesis\\\"\\n+ assert len(run[\\\"trace\\\"]) == 5\\n+ assert orch.spend_analytics()[\\\"totals\\\"][\\\"run_count\\\"] == 1\\n+\\n+\\n+def test_orchestrated_responses_synthesis_normalizes_provider_usage() -> None:\\n+ orch = _build()\\n+ raw = {\\n+ \\\"object\\\": \\\"response\\\",\\n+ \\\"output\\\": [{\\\"type\\\": \\\"message\\\", \\\"content\\\": [{\\\"type\\\": \\\"output_text\\\", \\\"text\\\": \\\"{}\\\"}]}],\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 11, \\\"output_tokens\\\": 7, \\\"total_tokens\\\": 18},\\n+ }\\n+ with patch.object(\\n+ orch,\\n+ \\\"conduct\\\",\\n+ return_value={\\n+ \\\"trace\\\": [{\\n+ \\\"id\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"worker_agent\\\",\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"output\\\": \\\"verified\\\",\\n+ }]\\n+ },\\n+ ), patch.object(orch.client, \\\"proxy_send\\\", return_value=raw):\\n+ result = orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"extract JSON\\\",\\n+ \\\"text\\\": {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}},\\n+ },\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ run = orch.get_workflow_run(result[\\\"orchestration\\\"][\\\"workflow_run_id\\\"])\\n+ assert run[\\\"trace\\\"][-1][\\\"usage\\\"] == {\\n+ \\\"input_tokens\\\": 11,\\n+ \\\"output_tokens\\\": 7,\\n+ \\\"total_tokens\\\": 18,\\n+ \\\"prompt_tokens\\\": 11,\\n+ \\\"completion_tokens\\\": 7,\\n+ }\\n+\\n+\\n+def test_orchestrated_responses_usage_counts_toward_spend_budget() -> None:\\n+ orch = _build(budget_max_output_tokens=100)\\n+ raw = {\\n+ \\\"object\\\": \\\"response\\\",\\n+ \\\"output_text\\\": \\\"{}\\\",\\n+ \\\"output\\\": [],\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 4, \\\"output_tokens\\\": 5, \\\"total_tokens\\\": 9},\\n+ }\\n+\\n+ with patch.object(orch.client, \\\"proxy_send\\\", return_value=raw):\\n+ result = orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"extract JSON\\\",\\n+ \\\"text\\\": {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}},\\n+ },\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ run = orch.get_workflow_run(result[\\\"orchestration\\\"][\\\"workflow_run_id\\\"])\\n+ assert run[\\\"trace\\\"][-1][\\\"usage\\\"][\\\"prompt_tokens\\\"] == 4\\n+ assert run[\\\"trace\\\"][-1][\\\"usage\\\"][\\\"completion_tokens\\\"] == 5\\n+ expected_spend = sum(\\n+ row.get(\\\"usage\\\", {}).get(\\\"completion_tokens\\\", estimate_tokens(row[\\\"output\\\"]))\\n+ for row in run[\\\"trace\\\"]\\n+ )\\n+ assert orch.spend_analytics()[\\\"budget\\\"][\\\"spent_output_tokens\\\"] == expected_spend\\n+\\n+\\n+def test_orchestrated_spend_persists_model_judge_usage() -> None:\\n+ orch = _build()\\n+ workflow = {\\n+ \\\"trace\\\": [{\\\"id\\\": 0, \\\"agent_id\\\": \\\"builder_agent\\\", \\\"role\\\": \\\"worker\\\", \\\"output\\\": \\\"ok\\\"}],\\n+ \\\"verification\\\": {\\n+ \\\"accepted\\\": True,\\n+ \\\"judge_agent_id\\\": \\\"reviewer_agent\\\",\\n+ \\\"judge_usage\\\": {\\\"prompt_tokens\\\": 4, \\\"completion_tokens\\\": 3, \\\"total_tokens\\\": 7},\\n+ },\\n+ }\\n+ raw = {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"choices\\\": [{\\\"message\\\": {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"{}\\\"}}],\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 2, \\\"completion_tokens\\\": 5, \\\"total_tokens\\\": 7},\\n+ }\\n+\\n+ with patch.object(orch, \\\"conduct\\\", return_value=workflow), patch.object(\\n+ orch.client, \\\"proxy_send\\\", return_value=raw\\n+ ):\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ by_model = {row[\\\"model\\\"]: row for row in orch.spend_analytics()[\\\"by_model\\\"]}\\n+ assert by_model[\\\"mock-reviewer\\\"][\\\"output_tokens\\\"] == 3\\n+ assert by_model[\\\"mock-reviewer\\\"][\\\"usage_source\\\"] == \\\"reported\\\"\\n+ assert by_model[\\\"mock-reviewer\\\"][\\\"step_count\\\"] == 1\\n+\\n+\\n+def test_fast_mlsirm_structured_judge_uses_one_direct_provider_call() -> None:\\n+ orch = _build()\\n+ orch.client.temperature = 0.4\\n+ adapter = _FastMLSIJudgeAdapter(orch, text=\\\"judge\\\", judge=\\\"reviewer_agent\\\")\\n+ provider_response = {\\n+ \\\"choices\\\": [{\\\"message\\\": {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": '{\\\"decision\\\":\\\"pass\\\"}'}}],\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 2, \\\"completion_tokens\\\": 3, \\\"total_tokens\\\": 5},\\n+ }\\n+ with patch.object(orch, \\\"proxy_completion\\\", side_effect=AssertionError(\\\"judge must not recurse\\\")), patch.object(\\n+ orch.client, \\\"proxy_send\\\", return_value=provider_response\\n+ ) as send:\\n+ result = adapter.complete_structured(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"judge this\\\"}],\\n+ response_format={\\\"type\\\": \\\"json_object\\\"},\\n+ )\\n+\\n+ send.assert_called_once()\\n+ assert result[\\\"answer\\\"] == '{\\\"decision\\\":\\\"pass\\\"}'\\n+ assert send.call_args.args[1] == \\\"chat/completions\\\"\\n+ assert send.call_args.args[2][\\\"response_format\\\"] == {\\\"type\\\": \\\"json_object\\\"}\\n+ assert send.call_args.args[2][\\\"temperature\\\"] == 0.4\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"text\\\", \\\"expected\\\"),\\n+ [\\n+ (None, None),\\n+ ({}, None),\\n+ ({\\\"format\\\": {\\\"type\\\": \\\"text\\\"}}, {\\\"type\\\": \\\"text\\\"}),\\n+ ({\\\"format\\\": {\\\"type\\\": \\\"xml\\\"}}, None),\\n+ ],\\n+)\\n+def test_responses_text_format_translation_handles_non_schema_shapes(\\n+ text: object,\\n+ expected: dict | None,\\n+) -> None:\\n+ assert _responses_text_format_to_chat_response_format(text) == expected\\n+\\n+\\n+def test_structured_provider_completion_rejects_empty_messages_and_disabled_model() -> None:\\n+ with pytest.raises(ValueError, match=\\\"non-empty messages\\\"):\\n+ _build().proxy_completion(\\n+ {\\\"messages\\\": [], \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"}}\\n+ )\\n+ with pytest.raises(RuntimeError, match=\\\"disabled\\\"):\\n+ _build().proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"disabled-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"messages\\\", \\\"first_role\\\"),\\n+ [\\n+ ([{\\\"role\\\": \\\"user\\\", \\\"content\\\": None}], \\\"user\\\"),\\n+ ([{\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"prior\\\"}], \\\"system\\\"),\\n+ ],\\n+)\\n+def test_structured_synthesis_injects_guidance_into_non_string_histories(\\n+ messages: list[dict],\\n+ first_role: str,\\n+) -> None:\\n+ orch = _build()\\n+ raw = {\\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": \\\"done\\\"}}]}\\n+ with patch.object(\\n+ orch,\\n+ \\\"conduct\\\",\\n+ return_value={\\\"trace\\\": [], \\\"verification\\\": {\\\"accepted\\\": True}},\\n+ ), patch.object(orch.client, \\\"proxy_send\\\", return_value=raw) as send:\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": messages,\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ sent_messages = send.call_args.args[2][\\\"messages\\\"]\\n+ assert sent_messages[0][\\\"role\\\"] == first_role\\n+ assert isinstance(sent_messages[0][\\\"content\\\"], str)\\n+\\n+\\n+def test_structured_synthesis_accounts_a_tool_only_response() -> None:\\n+ orch = _build()\\n+ raw = {\\n+ \\\"choices\\\": [\\n+ {\\n+ \\\"message\\\": {\\n+ \\\"content\\\": None,\\n+ \\\"tool_calls\\\": [{\\\"id\\\": \\\"call_1\\\", \\\"type\\\": \\\"function\\\"}],\\n+ }\\n+ }\\n+ ]\\n+ }\\n+ with patch.object(\\n+ orch,\\n+ \\\"conduct\\\",\\n+ return_value={\\\"trace\\\": [], \\\"verification\\\": {\\\"accepted\\\": True}},\\n+ ), patch.object(orch.client, \\\"proxy_send\\\", return_value=raw):\\n+ result = orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"call the tool\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ assert result[\\\"orchestration\\\"][\\\"mode\\\"] == \\\"conduct\\\"\\n+ run = orch.get_workflow_run(result[\\\"orchestration\\\"][\\\"workflow_run_id\\\"])\\n+ assert run[\\\"answer\\\"] == \\\"\\\"\\n+\\n+\\n+def test_structured_synthesis_preserves_tool_call_adjacency() -> None:\\n+ orch = _build()\\n+ intermediate_messages: list[list[dict]] = []\\n+ original_chat = orch.client.chat\\n+\\n+ def observe_chat(agent, messages, temperature=None, top_p=None):\\n+ del temperature, top_p\\n+ intermediate_messages.append(messages)\\n+ return original_chat(agent, messages)\\n+\\n+ with patch.object(orch.client, \\\"chat\\\", side_effect=observe_chat):\\n+ result = orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [\\n+ {\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"look up the value\\\"},\\n+ {\\n+ \\\"role\\\": \\\"assistant\\\",\\n+ \\\"content\\\": None,\\n+ \\\"tool_calls\\\": [\\n+ {\\n+ \\\"id\\\": \\\"call_1\\\",\\n+ \\\"type\\\": \\\"function\\\",\\n+ \\\"function\\\": {\\\"name\\\": \\\"lookup\\\", \\\"arguments\\\": \\\"{}\\\"},\\n+ }\\n+ ],\\n+ },\\n+ {\\\"role\\\": \\\"tool\\\", \\\"tool_call_id\\\": \\\"call_1\\\", \\\"content\\\": \\\"42\\\"},\\n+ ],\\n+ \\\"tools\\\": [{\\\"type\\\": \\\"function\\\", \\\"function\\\": {\\\"name\\\": \\\"lookup\\\", \\\"parameters\\\": {}}}],\\n+ }\\n+ )\\n+\\n+ final_messages = result[\\\"echo\\\"][\\\"messages\\\"]\\n+ assert final_messages[0][\\\"role\\\"] == \\\"user\\\"\\n+ assert final_messages[1][\\\"role\\\"] == \\\"assistant\\\"\\n+ assert final_messages[2][\\\"role\\\"] == \\\"tool\\\"\\n+ assert final_messages[1][\\\"tool_calls\\\"][0][\\\"id\\\"] == final_messages[2][\\\"tool_call_id\\\"]\\n+ assert intermediate_messages and all(messages[-1][\\\"role\\\"] == \\\"tool\\\" for messages in intermediate_messages)\\n+\\n+\\n def test_proxy_completion_responses_endpoint_returns_response_object() -> None:\\n orch = _build()\\n result = orch.proxy_completion(\\n@@ -115,6 +760,131 @@ def test_proxy_completion_responses_endpoint_returns_response_object() -> None:\\n assert result[\\\"echo\\\"][\\\"response_format\\\"] == {\\\"type\\\": \\\"text\\\"}\\n \\n \\n+def test_proxy_completion_responses_json_schema_is_orchestrated_and_native() -> None:\\n+ orch = _build()\\n+ body = {\\n+ \\\"input\\\": \\\"extract the visible region\\\",\\n+ \\\"instructions\\\": \\\"Keep the result concise.\\\",\\n+ \\\"metadata\\\": {\\\"tenant\\\": \\\"anonymous\\\", \\\"omitted\\\": None},\\n+ \\\"text\\\": {\\n+ \\\"format\\\": {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"name\\\": \\\"region_result\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\"},\\n+ \\\"strict\\\": True,\\n+ }\\n+ },\\n+ }\\n+ result = orch.proxy_completion(\\n+ body,\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ assert result[\\\"object\\\"] == \\\"response\\\"\\n+ assert result[\\\"echo\\\"][\\\"text\\\"] == body[\\\"text\\\"]\\n+ assert \\\"response_format\\\" not in result[\\\"echo\\\"]\\n+ assert result[\\\"echo\\\"][\\\"instructions\\\"] == \\\"Keep the result concise.\\\"\\n+ assert result[\\\"echo\\\"][\\\"metadata\\\"] == body[\\\"metadata\\\"]\\n+ assert result[\\\"orchestration\\\"][\\\"agent_count\\\"] == 4\\n+ run = orch.get_workflow_run(result[\\\"orchestration\\\"][\\\"workflow_run_id\\\"])\\n+ assert run[\\\"mode\\\"] == \\\"conduct\\\"\\n+\\n+\\n+def test_responses_structured_request_keeps_native_endpoint_and_input() -> None:\\n+ orch = _build()\\n+ calls: list[tuple[str, dict]] = []\\n+\\n+ def native_response(_agent, endpoint: str, payload: dict) -> dict:\\n+ calls.append((endpoint, payload))\\n+ return {\\\"object\\\": \\\"response\\\", \\\"output\\\": [], \\\"echo\\\": dict(payload)}\\n+\\n+ with patch.object(orch.client, \\\"proxy_send\\\", side_effect=native_response):\\n+ result = orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract the visible region\\\"}],\\n+ \\\"instructions\\\": \\\"Keep the original input order.\\\",\\n+ \\\"text\\\": {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}},\\n+ },\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ final_endpoint, final_payload = calls[-1]\\n+ assert final_endpoint == \\\"responses\\\"\\n+ assert final_payload[\\\"input\\\"] == [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract the visible region\\\"}]\\n+ assert \\\"Keep the original input order.\\\" in final_payload[\\\"instructions\\\"]\\n+ assert result[\\\"orchestration\\\"][\\\"workflow_run_id\\\"]\\n+\\n+\\n+def test_structured_chat_guidance_stays_in_original_user_turn() -> None:\\n+ result = _build().proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+ assert result[\\\"echo\\\"][\\\"messages\\\"][0][\\\"role\\\"] == \\\"user\\\"\\n+ assert \\\"You are the final synthesizer\\\" in result[\\\"echo\\\"][\\\"messages\\\"][0][\\\"content\\\"]\\n+\\n+\\n+def test_structured_workflow_preserves_multimodal_input_for_final_synthesis() -> None:\\n+ result = _build().proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [\\n+ {\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"describe this\\\"},\\n+ {\\\"type\\\": \\\"image_url\\\", \\\"image_url\\\": {\\\"url\\\": \\\"data:image/png;base64,fixture\\\"}},\\n+ ],\\n+ }\\n+ ],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ final_messages = result[\\\"echo\\\"][\\\"messages\\\"]\\n+ assert any(\\n+ isinstance(message.get(\\\"content\\\"), list)\\n+ and any(part.get(\\\"type\\\") == \\\"image_url\\\" for part in message[\\\"content\\\"])\\n+ for message in final_messages\\n+ )\\n+ assert final_messages[0][\\\"role\\\"] == \\\"user\\\"\\n+ assert any(\\n+ isinstance(part, dict) and part.get(\\\"type\\\") == \\\"text\\\"\\n+ and \\\"You are the final synthesizer\\\" in part.get(\\\"text\\\", \\\"\\\")\\n+ for part in final_messages[0][\\\"content\\\"]\\n+ )\\n+\\n+\\n+def test_structured_multimodal_rejects_an_explicit_text_only_model() -> None:\\n+ orchestrator = TaskOrchestrator(\\n+ [ModelAgent(\\\"text_agent\\\", \\\"text-model\\\", tags=(\\\"reasoning\\\",))]\\n+ )\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"lacks required tags: vision\\\"):\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"text-model\\\",\\n+ \\\"messages\\\": [\\n+ {\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"describe this\\\"},\\n+ {\\n+ \\\"type\\\": \\\"image_url\\\",\\n+ \\\"image_url\\\": {\\\"url\\\": \\\"data:image/png;base64,fixture\\\"},\\n+ },\\n+ ],\\n+ }\\n+ ],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+\\n # -- HTTP server -------------------------------------------------------------\\n \\n def _post(url: str, payload: dict, token: str) -> tuple[int, dict]:\\n@@ -132,41 +902,134 @@ def _post(url: str, payload: dict, token: str) -> tuple[int, dict]:\\n \\n \\n def _serve() -> tuple[object, int, str]:\\n- token = \\\"passthrough_token\\\"\\n+ token = \\\"passthrough_token\\\" # noqa: S105 - synthetic HTTP fixture credential\\n server = build_server(_build(), port=0, security=SecurityConfig(auth_token=token))\\n threading.Thread(target=server.serve_forever, daemon=True).start()\\n return server, server.server_address[1], token\\n \\n \\n-def test_http_chat_completions_accepts_response_format_and_passes_through() -> None:\\n+def test_http_chat_completions_orchestrates_json_object_instead_of_passthrough() -> None:\\n+ orch = _build()\\n+ provider_calls: list[tuple[str, dict]] = []\\n+ original_proxy_send = orch.client.proxy_send\\n+\\n+ def observe_provider_call(agent, endpoint: str, payload: dict) -> dict:\\n+ provider_calls.append((endpoint, dict(payload)))\\n+ return original_proxy_send(agent, endpoint, payload)\\n+\\n+ with patch.object(orch.client, \\\"proxy_send\\\", side_effect=observe_provider_call):\\n+ token = \\\"structured_http_token\\\" # noqa: S105 - synthetic HTTP fixture credential\\n+ server = build_server(orch, port=0, security=SecurityConfig(auth_token=token))\\n+ threading.Thread(target=server.serve_forever, daemon=True).start()\\n+ try:\\n+ status, body = _post(\\n+ f\\\"http://127.0.0.1:{server.server_address[1]}/v1/chat/completions\\\",\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"give me JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ },\\n+ token,\\n+ )\\n+ finally:\\n+ server.shutdown()\\n+ assert status == 200\\n+ assert body[\\\"object\\\"] == \\\"chat.completion\\\"\\n+ assert json.loads(body[\\\"choices\\\"][0][\\\"message\\\"][\\\"content\\\"]) == {}\\n+ assert provider_calls[-1][0] == \\\"chat/completions\\\"\\n+ assert provider_calls[-1][1][\\\"response_format\\\"] == {\\\"type\\\": \\\"json_object\\\"}\\n+\\n+\\n+def test_http_chat_completions_omits_model_for_orchestrator_selection() -> None:\\n server, port, token = _serve()\\n- url = f\\\"http://127.0.0.1:{port}/v1/chat/completions\\\"\\n try:\\n status, body = _post(\\n- url,\\n+ f\\\"http://127.0.0.1:{port}/v1/chat/completions\\\",\\n {\\n- \\\"model\\\": \\\"mock-planner\\\",\\n \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"give me JSON\\\"}],\\n \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n },\\n token,\\n )\\n finally:\\n server.shutdown()\\n- assert status == 200 # previously rejected 400 'unknown_fields'\\n- assert body[\\\"object\\\"] == \\\"chat.completion\\\"\\n- assert body[\\\"echo\\\"][\\\"response_format\\\"] == {\\\"type\\\": \\\"json_object\\\"}\\n+ assert status == 200, body\\n+ assert body[\\\"model\\\"] == \\\"contextual-orchestrator\\\"\\n+ assert json.loads(body[\\\"choices\\\"][0][\\\"message\\\"][\\\"content\\\"]) == {}\\n+\\n+\\n+def test_http_structured_workflow_applies_sampling_and_records_orchestration() -> None:\\n+ orch = _build()\\n+ previous_temperature = orch.client.default_temperature\\n+ seen_sampling: list[tuple[float, int]] = []\\n+ original_chat = orch.client.chat\\n+\\n+ def observe_chat(agent, messages, temperature=None, top_p=None):\\n+ del top_p\\n+ seen_sampling.append((\\n+ orch.client._request_setting(\\\"temperature\\\", orch.client.default_temperature),\\n+ orch.client._request_setting(\\\"max_output_tokens\\\", orch.client.max_output_tokens),\\n+ ))\\n+ return original_chat(agent, messages, temperature=temperature)\\n+\\n+ with patch.object(orch.client, \\\"chat\\\", side_effect=observe_chat):\\n+ token = \\\"passthrough_sampling_token\\\" # noqa: S105 - synthetic HTTP fixture credential\\n+ server = build_server(orch, port=0, security=SecurityConfig(auth_token=token))\\n+ threading.Thread(target=server.serve_forever, daemon=True).start()\\n+ try:\\n+ status, body = _post(\\n+ f\\\"http://127.0.0.1:{server.server_address[1]}/v1/chat/completions\\\",\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"give me JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ \\\"temperature\\\": 0.7,\\n+ \\\"max_tokens\\\": 17,\\n+ },\\n+ token,\\n+ )\\n+ finally:\\n+ server.shutdown()\\n+\\n+ assert status == 200, body\\n+ assert seen_sampling and all(item == (0.7, 17) for item in seen_sampling)\\n+ assert orch.client.default_temperature == previous_temperature\\n+ event_names = {event[\\\"event_name\\\"] for event in orch._analytics_events}\\n+ assert \\\"chat_completion_requested\\\" in event_names\\n+ assert \\\"chat_completion_passthrough\\\" not in event_names\\n \\n \\n def test_http_responses_endpoint_passes_through() -> None:\\n- server, port, token = _serve()\\n- url = f\\\"http://127.0.0.1:{port}/v1/responses\\\"\\n- try:\\n- status, body = _post(url, {\\\"model\\\": \\\"mock-planner\\\", \\\"input\\\": \\\"hello\\\"}, token)\\n- finally:\\n- server.shutdown()\\n+ orch = _build()\\n+ provider_calls: list[tuple[str, dict]] = []\\n+ original_proxy_send = orch.client.proxy_send\\n+\\n+ def observe_provider_call(agent, endpoint: str, payload: dict) -> dict:\\n+ provider_calls.append((endpoint, dict(payload)))\\n+ return original_proxy_send(agent, endpoint, payload)\\n+\\n+ request_body = {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"hello\\\",\\n+ \\\"text\\\": {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}},\\n+ }\\n+ with patch.object(orch.client, \\\"proxy_send\\\", side_effect=observe_provider_call):\\n+ token = \\\"responses_http_token\\\" # noqa: S105 - synthetic HTTP fixture credential\\n+ server = build_server(orch, port=0, security=SecurityConfig(auth_token=token))\\n+ threading.Thread(target=server.serve_forever, daemon=True).start()\\n+ try:\\n+ status, body = _post(\\n+ f\\\"http://127.0.0.1:{server.server_address[1]}/v1/responses\\\",\\n+ request_body,\\n+ token,\\n+ )\\n+ finally:\\n+ server.shutdown()\\n assert status == 200\\n assert body[\\\"object\\\"] == \\\"response\\\"\\n+ assert provider_calls[-1][0] == \\\"responses\\\"\\n+ assert provider_calls[-1][1][\\\"input\\\"] == \\\"hello\\\"\\n+ assert provider_calls[-1][1][\\\"text\\\"] == request_body[\\\"text\\\"]\\n \\n \\n def test_http_models_endpoint_lists_configured_models() -> None:\\n@@ -220,5 +1083,5 @@ def test_http_plain_prompt_still_uses_orchestration_path() -> None:\\n server.shutdown()\\n assert status == 200\\n assert body[\\\"object\\\"] == \\\"chat.completion\\\"\\n- assert \\\"echo\\\" not in body # orchestration path, not passthrough\\n+ assert \\\"echo\\\" not in body # ordinary orchestration path\\n assert \\\"orchestration\\\" in body\" }, { \"sha\": \"bb2cddb3431eabb2ca69f8588298db0b64bd81af\", \"filename\": \"tests/test_openai_user_field_http_honesty.py\", \"status\": \"modified\", \"additions\": 2, \"deletions\": 2, \"changes\": 4, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_openai_user_field_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_openai_user_field_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_openai_user_field_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,11 +3,11 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"embedding\\\"))]\\n )\\n \\n \" }, { \"sha\": \"84d9391fbfa5208a10b735e15d11fd0559e13ab8\", \"filename\": \"tests/test_passthrough_one_shot_local_semantics.py\", \"status\": \"added\", \"additions\": 266, \"deletions\": 0, \"changes\": 266, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_passthrough_one_shot_local_semantics.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_passthrough_one_shot_local_semantics.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_passthrough_one_shot_local_semantics.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,266 @@\\n+\\\"\\\"\\\"Regression coverage for one-shot passthrough transport semantics.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from contextlib import contextmanager\\n+from copy import deepcopy\\n+import io\\n+import socket\\n+from unittest.mock import patch\\n+import urllib.error\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import ModelAgent\\n+from contextual_orchestrator.orchestrator import ModelClient\\n+\\n+\\n+def _local_agent() -> ModelAgent:\\n+ \\\"\\\"\\\"Build one authenticated loopback gateway agent.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ \\\"local_gateway_agent\\\",\\n+ \\\"local-model\\\",\\n+ base_url=\\\"local://127.0.0.1:8080/v1\\\",\\n+ local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n+ )\\n+\\n+\\n+def _chat_response(content: str = \\\"local answer\\\") -> dict[str, object]:\\n+ \\\"\\\"\\\"Return one minimal OpenAI-compatible chat response.\\\"\\\"\\\"\\n+ return {\\n+ \\\"id\\\": \\\"chatcmpl-one-shot\\\",\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"created\\\": 1,\\n+ \\\"model\\\": \\\"local-model\\\",\\n+ \\\"choices\\\": [\\n+ {\\n+ \\\"index\\\": 0,\\n+ \\\"message\\\": {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": content},\\n+ \\\"finish_reason\\\": \\\"stop\\\",\\n+ }\\n+ ],\\n+ \\\"usage\\\": {\\n+ \\\"prompt_tokens\\\": 1,\\n+ \\\"completion_tokens\\\": 2,\\n+ \\\"total_tokens\\\": 3,\\n+ },\\n+ }\\n+\\n+\\n+def test_one_shot_local_responses_preserves_translation_and_concurrency_slot() -> None:\\n+ \\\"\\\"\\\"One-shot failover must retain the provider-neutral local Responses adapter.\\\"\\\"\\\"\\n+ client = ModelClient(max_retries=4, local_concurrency=3)\\n+ agent = _local_agent()\\n+ request = {\\n+ \\\"model\\\": \\\"local-model\\\",\\n+ \\\"input\\\": \\\"summarize the incident\\\",\\n+ \\\"metadata\\\": {\\\"tenant\\\": \\\"tenant-one\\\"},\\n+ }\\n+ original = deepcopy(request)\\n+ sent: list[tuple[str, dict[str, object]]] = []\\n+ slots: list[tuple[str, int, int]] = []\\n+\\n+ @contextmanager\\n+ def local_slot(\\n+ slot_agent: ModelAgent,\\n+ capacity: int,\\n+ timeout: int,\\n+ ):\\n+ slots.append((slot_agent.id, capacity, timeout))\\n+ yield\\n+\\n+ def send_raw(\\n+ sent_agent: ModelAgent,\\n+ endpoint: str,\\n+ payload: dict[str, object],\\n+ _destination: object,\\n+ ) -> dict[str, object]:\\n+ assert sent_agent is agent\\n+ sent.append((endpoint, deepcopy(payload)))\\n+ return _chat_response()\\n+\\n+ with (\\n+ patch.object(\\n+ client,\\n+ \\\"_validate_provider\\\",\\n+ return_value=(socket.AF_INET, (\\\"127.0.0.1\\\", 8080)),\\n+ ),\\n+ patch.object(client, \\\"_send_raw\\\", side_effect=send_raw),\\n+ patch(\\n+ \\\"contextual_orchestrator.orchestrator._local_provider_slot\\\",\\n+ side_effect=local_slot,\\n+ ),\\n+ client.request_settings(max_output_tokens=73),\\n+ ):\\n+ result = client.proxy_send_once(agent, \\\"responses\\\", request)\\n+\\n+ assert request == original\\n+ assert slots == [(agent.id, 3, client.timeout)]\\n+ assert len(sent) == 1\\n+ endpoint, payload = sent[0]\\n+ assert endpoint == \\\"chat/completions\\\"\\n+ assert payload[\\\"model\\\"] == \\\"local-model\\\"\\n+ assert payload[\\\"stream\\\"] is False\\n+ assert payload[\\\"max_tokens\\\"] == 73\\n+ assert payload[\\\"messages\\\"] == [\\n+ {\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"summarize the incident\\\"}\\n+ ]\\n+ assert result[\\\"object\\\"] == \\\"response\\\"\\n+ assert result[\\\"output_text\\\"] == \\\"local answer\\\"\\n+ assert result[\\\"metadata\\\"] == {\\\"tenant\\\": \\\"tenant-one\\\"}\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"requested_max_tokens\\\", \\\"expected_max_tokens\\\"), [(None, 57), (11, 11)]\\n+)\\n+def test_one_shot_local_chat_still_uses_model_switch_concurrency_slot(\\n+ requested_max_tokens: int | None,\\n+ expected_max_tokens: int,\\n+) -> None:\\n+ \\\"\\\"\\\"Removing same-model retries must not bypass local model-switch coordination.\\\"\\\"\\\"\\n+ client = ModelClient(max_retries=5, local_concurrency=2, max_output_tokens=57)\\n+ agent = _local_agent()\\n+ payload = {\\n+ \\\"model\\\": \\\"local-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"hello\\\"}],\\n+ \\\"stream\\\": False,\\n+ }\\n+ if requested_max_tokens is not None:\\n+ payload[\\\"max_tokens\\\"] = requested_max_tokens\\n+ original = deepcopy(payload)\\n+ slots: list[tuple[str, int, int]] = []\\n+ sends: list[tuple[str, dict[str, object]]] = []\\n+\\n+ @contextmanager\\n+ def local_slot(\\n+ slot_agent: ModelAgent,\\n+ capacity: int,\\n+ timeout: int,\\n+ ):\\n+ slots.append((slot_agent.id, capacity, timeout))\\n+ yield\\n+\\n+ def send_raw(\\n+ _agent: ModelAgent,\\n+ endpoint: str,\\n+ sent_payload: dict[str, object],\\n+ _destination: object,\\n+ ) -> dict[str, object]:\\n+ sends.append((endpoint, deepcopy(sent_payload)))\\n+ return _chat_response(\\\"hello\\\")\\n+\\n+ with (\\n+ patch.object(\\n+ client,\\n+ \\\"_validate_provider\\\",\\n+ return_value=(socket.AF_INET, (\\\"127.0.0.1\\\", 8080)),\\n+ ),\\n+ patch.object(client, \\\"_send_raw\\\", side_effect=send_raw),\\n+ patch(\\n+ \\\"contextual_orchestrator.orchestrator._local_provider_slot\\\",\\n+ side_effect=local_slot,\\n+ ),\\n+ ):\\n+ result = client.proxy_send_once(agent, \\\"chat/completions\\\", payload)\\n+\\n+ assert result[\\\"object\\\"] == \\\"chat.completion\\\"\\n+ assert slots == [(agent.id, 2, client.timeout)]\\n+ assert sends == [\\n+ (\\\"chat/completions\\\", {**payload, \\\"max_tokens\\\": expected_max_tokens})\\n+ ]\\n+ assert payload == original\\n+\\n+\\n+def test_one_shot_remote_passthrough_never_enters_same_agent_retry_wrapper() -> None:\\n+ \\\"\\\"\\\"A candidate attempt is exactly one raw provider request.\\\"\\\"\\\"\\n+ client = ModelClient(max_retries=7)\\n+ agent = ModelAgent(\\n+ \\\"remote_provider_agent\\\",\\n+ \\\"remote-model\\\",\\n+ base_url=\\\"https://provider.example/v1\\\",\\n+ credential_key=\\\"REMOTE_PROVIDER_KEY\\\",\\n+ )\\n+ rate_limit = urllib.error.HTTPError(\\n+ \\\"https://provider.example/v1/chat/completions\\\",\\n+ 429,\\n+ \\\"rate limited\\\",\\n+ None,\\n+ None,\\n+ )\\n+\\n+ with (\\n+ patch.object(\\n+ client,\\n+ \\\"_validate_provider\\\",\\n+ return_value=(socket.AF_INET, (\\\"93.184.216.34\\\", 443)),\\n+ ),\\n+ patch.object(client, \\\"_send_raw\\\", side_effect=rate_limit) as send_raw,\\n+ ):\\n+ with pytest.raises(urllib.error.HTTPError) as caught:\\n+ client.proxy_send_once(\\n+ agent,\\n+ \\\"chat/completions\\\",\\n+ {\\n+ \\\"model\\\": \\\"remote-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"hello\\\"}],\\n+ \\\"stream\\\": False,\\n+ },\\n+ )\\n+\\n+ assert caught.value is rate_limit\\n+ send_raw.assert_called_once()\\n+\\n+\\n+def test_one_shot_passthrough_keeps_optional_temperature_negotiation() -> None:\\n+ \\\"\\\"\\\"Capability negotiation removes only temperature without transient replay.\\\"\\\"\\\"\\n+\\n+ client = ModelClient(max_retries=7)\\n+ agent = ModelAgent(\\n+ \\\"remote_provider_agent\\\",\\n+ \\\"remote-model\\\",\\n+ base_url=\\\"https://provider.example/v1\\\",\\n+ credential_key=\\\"REMOTE_PROVIDER_KEY\\\",\\n+ )\\n+ unsupported = urllib.error.HTTPError(\\n+ \\\"https://provider.example/v1/responses\\\",\\n+ 400,\\n+ \\\"bad request\\\",\\n+ None,\\n+ io.BytesIO(\\n+ b\\\"Unsupported value: 'temperature' does not support 0.2; only the default is supported\\\"\\n+ ),\\n+ )\\n+ sent: list[dict[str, object]] = []\\n+\\n+ def send_raw(\\n+ _agent: ModelAgent,\\n+ _endpoint: str,\\n+ payload: dict[str, object],\\n+ _destination: object,\\n+ ) -> dict[str, object]:\\n+ sent.append(deepcopy(payload))\\n+ if len(sent) == 1:\\n+ raise unsupported\\n+ return _chat_response(\\\"negotiated\\\")\\n+\\n+ with (\\n+ patch.object(\\n+ client,\\n+ \\\"_validate_provider\\\",\\n+ return_value=(socket.AF_INET, (\\\"93.184.216.34\\\", 443)),\\n+ ),\\n+ patch.object(client, \\\"_send_raw\\\", side_effect=send_raw),\\n+ ):\\n+ result = client.proxy_send_once(\\n+ agent,\\n+ \\\"responses\\\",\\n+ {\\n+ \\\"model\\\": \\\"remote-model\\\",\\n+ \\\"input\\\": \\\"hello\\\",\\n+ \\\"temperature\\\": 0.2,\\n+ },\\n+ )\\n+\\n+ assert result[\\\"object\\\"] == \\\"chat.completion\\\"\\n+ assert sent[0][\\\"temperature\\\"] == 0.2\\n+ assert \\\"temperature\\\" not in sent[1]\" }, { \"sha\": \"7c858ef527ec0e31680407b462504903358500d0\", \"filename\": \"tests/test_passthrough_provider_failover.py\", \"status\": \"added\", \"additions\": 364, \"deletions\": 0, \"changes\": 364, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_passthrough_provider_failover.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_passthrough_provider_failover.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_passthrough_provider_failover.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,364 @@\\n+\\\"\\\"\\\"Regression coverage for cross-provider failover on raw OpenAI passthrough.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import urllib.error\\n+from copy import deepcopy\\n+from typing import Any\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+\\n+\\n+class SequencedProxyClient:\\n+ \\\"\\\"\\\"Record passthrough calls and return configured outcomes by agent id.\\\"\\\"\\\"\\n+\\n+ def __init__(self, outcomes: dict[str, dict[str, Any] | BaseException]) -> None:\\n+ self.outcomes = outcomes\\n+ self.calls: list[tuple[str, str, dict[str, Any]]] = []\\n+\\n+ def chat(\\n+ self,\\n+ agent: ModelAgent,\\n+ messages: list[dict[str, Any]],\\n+ temperature: float | None = None,\\n+ top_p: float | None = None,\\n+ ) -> str:\\n+ \\\"\\\"\\\"Supply deterministic workflow evidence before final passthrough.\\\"\\\"\\\"\\n+ del messages, temperature, top_p\\n+ return f\\\"verified evidence from {agent.id}\\\"\\n+\\n+ def take_usage(self) -> None:\\n+ \\\"\\\"\\\"Expose the client usage seam used by workflow accounting.\\\"\\\"\\\"\\n+ return\\n+\\n+ def proxy_send_once(\\n+ self,\\n+ agent: ModelAgent,\\n+ endpoint: str,\\n+ payload: dict[str, Any],\\n+ ) -> dict[str, Any]:\\n+ \\\"\\\"\\\"Perform one deterministic provider attempt for the requested agent.\\\"\\\"\\\"\\n+ self.calls.append((agent.id, endpoint, deepcopy(payload)))\\n+ outcome = self.outcomes[agent.id]\\n+ if isinstance(outcome, BaseException):\\n+ raise outcome\\n+ return deepcopy(outcome)\\n+\\n+ def proxy_send(\\n+ self,\\n+ agent: ModelAgent,\\n+ endpoint: str,\\n+ payload: dict[str, Any],\\n+ ) -> dict[str, Any]:\\n+ \\\"\\\"\\\"Expose the ordinary explicit-model transport seam.\\\"\\\"\\\"\\n+ return self.proxy_send_once(agent, endpoint, payload)\\n+\\n+\\n+def _http_error(status: int, message: str) -> urllib.error.HTTPError:\\n+ \\\"\\\"\\\"Return a realistic provider HTTP error for passthrough routing tests.\\\"\\\"\\\"\\n+ return urllib.error.HTTPError(\\n+ \\\"https://provider.example/v1/chat/completions\\\",\\n+ status,\\n+ message,\\n+ None,\\n+ None,\\n+ )\\n+\\n+\\n+def _rate_limit() -> urllib.error.HTTPError:\\n+ \\\"\\\"\\\"Return a realistic transient provider HTTP 429 error.\\\"\\\"\\\"\\n+ return _http_error(429, \\\"rate limited\\\")\\n+\\n+\\n+def _wrapped(error: BaseException) -> RuntimeError:\\n+ \\\"\\\"\\\"Return a provider-style wrapper with the original failure as its cause.\\\"\\\"\\\"\\n+ try:\\n+ raise RuntimeError(\\\"provider wrapper\\\") from error\\n+ except RuntimeError as wrapper:\\n+ return wrapper\\n+\\n+\\n+def _suppressed_wrapper(error: BaseException) -> RuntimeError:\\n+ \\\"\\\"\\\"Return a terminal wrapper whose incidental context is explicitly hidden.\\\"\\\"\\\"\\n+ try:\\n+ raise error\\n+ except BaseException:\\n+ try:\\n+ raise RuntimeError(\\\"terminal provider wrapper\\\") from None\\n+ except RuntimeError as wrapper:\\n+ return wrapper\\n+\\n+\\n+def _build(client: SequencedProxyClient) -> TaskOrchestrator:\\n+ \\\"\\\"\\\"Build a deterministic two-provider pool for passthrough tests.\\\"\\\"\\\"\\n+ return TaskOrchestrator(\\n+ [\\n+ ModelAgent(\\n+ \\\"primary_agent\\\",\\n+ \\\"primary-model\\\",\\n+ tags=(\\\"coding\\\", \\\"implementation\\\", \\\"security\\\", \\\"review\\\"),\\n+ priority=10,\\n+ ),\\n+ ModelAgent(\\n+ \\\"fallback_agent\\\",\\n+ \\\"fallback-model\\\",\\n+ tags=(\\\"coding\\\", \\\"implementation\\\", \\\"security\\\", \\\"review\\\"),\\n+ priority=1,\\n+ ),\\n+ ],\\n+ client=client,\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\\"single_agent\\\", [False, True])\\n+def test_429_advances_immediately_and_preserves_tool_request(\\n+ single_agent: bool,\\n+) -> None:\\n+ \\\"\\\"\\\"A 429 must advance to another model without replaying the saturated one.\\\"\\\"\\\"\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": _rate_limit(),\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+ tools = [\\n+ {\\n+ \\\"type\\\": \\\"function\\\",\\n+ \\\"function\\\": {\\\"name\\\": \\\"inspect\\\", \\\"parameters\\\": {\\\"type\\\": \\\"object\\\"}},\\n+ }\\n+ ]\\n+ body = {\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review this security-sensitive code\\\"}],\\n+ \\\"tools\\\": tools,\\n+ \\\"tool_choice\\\": \\\"auto\\\",\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ \\\"reasoning_effort\\\": \\\"auto\\\",\\n+ \\\"mode\\\": \\\"auto\\\",\\n+ \\\"stream\\\": True,\\n+ }\\n+ original = deepcopy(body)\\n+\\n+ result = orchestrator.proxy_completion(body, single_agent=single_agent)\\n+\\n+ assert result[\\\"model\\\"] == \\\"fallback-model\\\"\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\", \\\"fallback_agent\\\"]\\n+ assert client.calls[0][2][\\\"model\\\"] == \\\"primary-model\\\"\\n+ assert client.calls[1][2][\\\"model\\\"] == \\\"fallback-model\\\"\\n+ assert client.calls[1][2][\\\"tools\\\"] == tools\\n+ assert client.calls[1][2][\\\"tool_choice\\\"] == \\\"auto\\\"\\n+ assert client.calls[1][2][\\\"response_format\\\"] == {\\\"type\\\": \\\"json_object\\\"}\\n+ assert \\\"reasoning_effort\\\" not in client.calls[1][2]\\n+ assert client.calls[1][2][\\\"stream\\\"] is False\\n+ assert \\\"mode\\\" not in client.calls[1][2]\\n+ assert body == original\\n+\\n+\\n+@pytest.mark.parametrize(\\\"status\\\", [404, 410])\\n+def test_virtual_request_advances_when_discovered_candidate_disappears(\\n+ status: int,\\n+) -> None:\\n+ \\\"\\\"\\\"A stale discovered candidate must not block another compatible worker.\\\"\\\"\\\"\\n+ unavailable = _http_error(status, \\\"model unavailable\\\")\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": unavailable,\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ result = orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"contextual-orchestrator\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review code\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert result[\\\"model\\\"] == \\\"fallback-model\\\"\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\", \\\"fallback_agent\\\"]\\n+\\n+\\n+@pytest.mark.parametrize(\\\"provider_error\\\", [_rate_limit(), _http_error(410, \\\"gone\\\")])\\n+def test_virtual_request_unwraps_provider_failure_causes(\\n+ provider_error: BaseException,\\n+) -> None:\\n+ \\\"\\\"\\\"Provider SDK wrappers must not hide a bounded fallback signal.\\\"\\\"\\\"\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": _wrapped(provider_error),\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ result = orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"contextual-orchestrator\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review code\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert result[\\\"model\\\"] == \\\"fallback-model\\\"\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\", \\\"fallback_agent\\\"]\\n+\\n+\\n+def test_suppressed_provider_context_does_not_authorize_failover() -> None:\\n+ \\\"\\\"\\\"An explicitly suppressed prior 429 must not override a terminal wrapper.\\\"\\\"\\\"\\n+ terminal = _suppressed_wrapper(_rate_limit())\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": terminal,\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"terminal provider wrapper\\\") as caught:\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"contextual-orchestrator\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review code\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert caught.value is terminal\\n+ assert terminal.__suppress_context__ is True\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\"]\\n+\\n+\\n+@pytest.mark.parametrize(\\\"status\\\", [404, 410])\\n+def test_explicit_concrete_model_remains_sticky_when_unavailable(status: int) -> None:\\n+ \\\"\\\"\\\"An explicit concrete model must never be silently replaced.\\\"\\\"\\\"\\n+ unavailable = _http_error(status, \\\"model unavailable\\\")\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": unavailable,\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ with pytest.raises(urllib.error.HTTPError) as caught:\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"primary-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review code\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert caught.value is unavailable\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\"]\\n+\\n+\\n+def test_explicit_concrete_model_preserves_rate_limit_error() -> None:\\n+ \\\"\\\"\\\"A concrete-model 429 must remain the provider's original error.\\\"\\\"\\\"\\n+ rate_limit = _rate_limit()\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": rate_limit,\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ with pytest.raises(urllib.error.HTTPError) as caught:\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"primary-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review code\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert caught.value is rate_limit\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\"]\\n+\\n+\\n+def test_non_transient_request_error_is_not_replayed_to_another_provider() -> None:\\n+ \\\"\\\"\\\"A provider 400 is caller/configuration evidence, not a failover signal.\\\"\\\"\\\"\\n+ bad_request = _http_error(400, \\\"unsupported request\\\")\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": bad_request,\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ with pytest.raises(urllib.error.HTTPError) as caught:\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"invalid request\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert caught.value is bad_request\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\"]\\n+ assert not orchestrator._circuit_open(\\\"primary_agent\\\")\\n+\\n+\\n+def test_all_transient_candidate_failures_chain_final_provider_error() -> None:\\n+ \\\"\\\"\\\"Exhausted transient candidates fail closed with the final provider cause.\\\"\\\"\\\"\\n+ first = _rate_limit()\\n+ final = _http_error(503, \\\"fallback unavailable\\\")\\n+ client = SequencedProxyClient(\\n+ {\\\"primary_agent\\\": first, \\\"fallback_agent\\\": final}\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"all 2 candidate agents failed\\\") as caught:\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review code\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert caught.value.__cause__ is final\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\", \\\"fallback_agent\\\"]\\n+\\n+\\n+def test_cli_server_constructs_the_failover_orchestrator() -> None:\\n+ \\\"\\\"\\\"The production ``python -m`` server path must use provider failover.\\\"\\\"\\\"\\n+ from contextual_orchestrator import __main__ as cli\\n+ from contextual_orchestrator.passthrough_failover import (\\n+ TaskOrchestrator as FailoverTaskOrchestrator,\\n+ )\\n+\\n+ assert cli.TaskOrchestrator is FailoverTaskOrchestrator\" }, { \"sha\": \"e58c64b610b13cf66a224859aedf64f01140803a\", \"filename\": \"tests/test_persistence.py\", \"status\": \"modified\", \"additions\": 49, \"deletions\": 0, \"changes\": 49, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_persistence.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_persistence.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_persistence.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -23,6 +23,30 @@ def _orch(state_db: str | None = None) -> TaskOrchestrator:\\n return TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"mock\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))], state_db=state_db)\\n \\n \\n+def _run_record(index: int) -> dict:\\n+ return {\\n+ \\\"workflow_run_id\\\": f\\\"run_retention_{index}\\\",\\n+ \\\"created_at\\\": index,\\n+ \\\"mode\\\": \\\"route\\\",\\n+ \\\"policy_mode\\\": \\\"route\\\",\\n+ \\\"prompt_text\\\": \\\"abcd\\\",\\n+ \\\"answer\\\": \\\"done\\\",\\n+ \\\"trace\\\": [\\n+ {\\n+ \\\"id\\\": 0,\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"general_agent\\\",\\n+ \\\"subtask\\\": \\\"Direct route\\\",\\n+ \\\"access\\\": [],\\n+ \\\"output\\\": \\\"done\\\",\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 1, \\\"completion_tokens\\\": 1},\\n+ }\\n+ ],\\n+ \\\"policy_snapshot\\\": {},\\n+ \\\"verification\\\": {\\\"accepted\\\": True},\\n+ }\\n+\\n+\\n def test_runs_audit_analytics_survive_restart() -> None:\\n with tempfile.TemporaryDirectory() as directory:\\n db = os.path.join(directory, \\\"state.db\\\")\\n@@ -118,6 +142,31 @@ def test_stream_reload_respects_deque_maxlen() -> None:\\n second.close()\\n \\n \\n+def test_workflow_reload_bounds_raw_records_and_preserves_spend() -> None:\\n+ with tempfile.TemporaryDirectory() as directory:\\n+ db = os.path.join(directory, \\\"state.db\\\")\\n+ first = _orch(db)\\n+ run_count = first._run_order.maxlen + 2\\n+ for index in range(run_count):\\n+ first._persist_workflow_run(_run_record(index))\\n+ assert len(first._workflow_runs) == first._run_order.maxlen\\n+ assert first.spend_analytics()[\\\"totals\\\"][\\\"run_count\\\"] == run_count\\n+ first.close()\\n+\\n+ second = _orch(db)\\n+ try:\\n+ assert len(second._workflow_runs) == second._run_order.maxlen\\n+ assert \\\"run_retention_0\\\" not in second._workflow_runs\\n+ assert \\\"run_retention_129\\\" in second._workflow_runs\\n+ report = second.spend_analytics()\\n+ assert report[\\\"totals\\\"][\\\"run_count\\\"] == run_count\\n+ assert report[\\\"totals\\\"][\\\"estimated_output_tokens\\\"] == run_count\\n+ assert report[\\\"totals\\\"][\\\"reported_prompt_tokens\\\"] == run_count\\n+ assert len(second._store.load(\\\"workflow_run\\\")) == run_count\\n+ finally:\\n+ second.close()\\n+\\n+\\n if __name__ == \\\"__main__\\\":\\n for name, fn in sorted(globals().items()):\\n if name.startswith(\\\"test_\\\") and callable(fn):\" }, { \"sha\": \"e99335437d547ef6a2d7a934efb6433eed52128d\", \"filename\": \"tests/test_pr765_review_regressions.py\", \"status\": \"added\", \"additions\": 320, \"deletions\": 0, \"changes\": 320, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_pr765_review_regressions.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_pr765_review_regressions.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_pr765_review_regressions.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,320 @@\\n+\\\"\\\"\\\"Regression contracts for the PR 765 review findings.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from concurrent.futures import ThreadPoolExecutor\\n+import json\\n+import threading\\n+import urllib.error\\n+from unittest.mock import patch\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import orchestrator as orchestration\\n+from contextual_orchestrator import server\\n+from contextual_orchestrator.orchestrator import ModelAgent, ModelClient, TaskOrchestrator\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"schema\\\",\\n+ [\\n+ {\\\"type\\\": \\\"object\\\", \\\"properties\\\": []},\\n+ {\\\"type\\\": \\\"object\\\", \\\"required\\\": [\\\"answer\\\", 7]},\\n+ {\\\"type\\\": \\\"array\\\", \\\"items\\\": []},\\n+ ],\\n+)\\n+def test_malformed_json_schema_is_rejected_before_response_validation(schema) -> None:\\n+ with pytest.raises(server.RequestError) as captured:\\n+ server._validate_chat_response_format(\\n+ {\\n+ \\\"response_format\\\": {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\\"name\\\": \\\"answer\\\", \\\"schema\\\": schema},\\n+ }\\n+ }\\n+ )\\n+ assert captured.value.status == 400\\n+ assert captured.value.code == \\\"invalid_response_format\\\"\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"field\\\",\\n+ [\\n+ \\\"temperature\\\",\\n+ \\\"top_p\\\",\\n+ \\\"presence_penalty\\\",\\n+ \\\"frequency_penalty\\\",\\n+ \\\"seed\\\",\\n+ \\\"stop\\\",\\n+ \\\"logit_bias\\\",\\n+ \\\"logprobs\\\",\\n+ \\\"top_logprobs\\\",\\n+ ],\\n+)\\n+def test_unapplied_responses_controls_fail_closed(field) -> None:\\n+ values = {\\n+ \\\"temperature\\\": 0.2,\\n+ \\\"top_p\\\": 0.9,\\n+ \\\"presence_penalty\\\": 0.1,\\n+ \\\"frequency_penalty\\\": -0.1,\\n+ \\\"seed\\\": 42,\\n+ \\\"stop\\\": [\\\"END\\\"],\\n+ \\\"logit_bias\\\": {\\\"1\\\": 1},\\n+ \\\"logprobs\\\": True,\\n+ \\\"top_logprobs\\\": 3,\\n+ }\\n+ with pytest.raises(server.RequestError) as captured:\\n+ server._reject_responses_orchestration_controls({field: values[field]})\\n+ assert captured.value.status == 422\\n+ assert captured.value.code == \\\"unsupported_responses_orchestration_controls\\\"\\n+ assert captured.value.detail == {\\\"fields\\\": [field]}\\n+\\n+\\n+def test_empty_responses_controls_remain_omit_equivalent() -> None:\\n+ server._reject_responses_orchestration_controls(\\n+ {\\n+ \\\"temperature\\\": None,\\n+ \\\"stop\\\": \\\"\\\",\\n+ \\\"logit_bias\\\": {},\\n+ \\\"logprobs\\\": False,\\n+ \\\"top_logprobs\\\": 0,\\n+ }\\n+ )\\n+\\n+\\n+def test_internal_chat_preserves_explicit_temperature(monkeypatch) -> None:\\n+ \\\"\\\"\\\"An explicit caller sampling control remains an honest provider passthrough.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ agent = ModelAgent(\\n+ id=\\\"chat_worker\\\",\\n+ model=\\\"provider/model\\\",\\n+ base_url=\\\"https://gateway.example.com\\\",\\n+ credential_key=\\\"\\\",\\n+ )\\n+ captured: dict[str, object] = {}\\n+\\n+ monkeypatch.setattr(client, \\\"_validate_provider\\\", lambda _agent: None)\\n+\\n+ def capture_payload(_agent, payload, _destination=None, *, timeout=None):\\n+ del timeout\\n+ captured.update(payload)\\n+ return \\\"OK\\\"\\n+\\n+ monkeypatch.setattr(client, \\\"_send\\\", capture_payload)\\n+\\n+ assert client.chat(\\n+ agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Sample.\\\"}],\\n+ temperature=0.2,\\n+ ) == \\\"OK\\\"\\n+ assert captured[\\\"temperature\\\"] == 0.2\\n+\\n+\\n+def test_request_sampling_settings_are_isolated_between_threads(monkeypatch) -> None:\\n+ client = ModelClient(max_retries=0)\\n+ agent = ModelAgent(\\n+ id=\\\"concurrent_worker\\\",\\n+ model=\\\"provider/model\\\",\\n+ base_url=\\\"https://gateway.example.com\\\",\\n+ credential_key=\\\"\\\",\\n+ )\\n+ barrier = threading.Barrier(2)\\n+ observed: list[tuple[float, int]] = []\\n+ monkeypatch.setattr(client, \\\"_validate_provider\\\", lambda _agent: None)\\n+\\n+ def capture_payload(_agent, payload, _destination=None, *, timeout=None):\\n+ del timeout\\n+ observed.append((payload[\\\"temperature\\\"], payload[\\\"max_tokens\\\"]))\\n+ barrier.wait(timeout=5)\\n+ return \\\"OK\\\"\\n+\\n+ monkeypatch.setattr(client, \\\"_send\\\", capture_payload)\\n+\\n+ def call(temperature: float, max_tokens: int) -> str:\\n+ with client.request_settings(\\n+ temperature=temperature,\\n+ max_output_tokens=max_tokens,\\n+ ):\\n+ return client.chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Sample.\\\"}])\\n+\\n+ with ThreadPoolExecutor(max_workers=2) as pool:\\n+ replies = list(pool.map(lambda row: call(*row), [(0.1, 11), (0.9, 29)]))\\n+\\n+ assert replies == [\\\"OK\\\", \\\"OK\\\"]\\n+ assert set(observed) == {(0.1, 11), (0.9, 29)}\\n+ assert client.default_temperature is None\\n+ assert client.max_output_tokens == 2048\\n+\\n+ with client.request_settings(temperature=0.3):\\n+ with client.request_settings(temperature=0.4):\\n+ assert client._request_setting(\\\"temperature\\\", None) == 0.4\\n+ assert client._request_setting(\\\"temperature\\\", None) == 0.3\\n+\\n+ streamed: list[tuple[float, int]] = []\\n+\\n+ def capture_stream(_agent, payload, _destination=None):\\n+ streamed.append((payload[\\\"temperature\\\"], payload[\\\"max_tokens\\\"]))\\n+ yield \\\"chunk\\\"\\n+\\n+ monkeypatch.setattr(client, \\\"_stream_send\\\", capture_stream)\\n+ with client.request_settings(temperature=0.6, max_output_tokens=19):\\n+ assert list(\\n+ client.stream_chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Stream.\\\"}])\\n+ ) == [\\\"chunk\\\"]\\n+ assert streamed == [(0.6, 19)]\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"userinfo_url\\\",\\n+ [\\n+ \\\"https://@gateway.example.com/v1/models\\\",\\n+ \\\"https://:secret@gateway.example.com/v1/models\\\",\\n+ ],\\n+)\\n+def test_provider_json_rejects_empty_userinfo_before_provider_transport(userinfo_url: str) -> None:\\n+ \\\"\\\"\\\"An empty username or password is still userinfo and cannot bypass origin checks.\\\"\\\"\\\"\\n+ agent = ModelAgent(\\n+ id=\\\"model_discovery_agent\\\",\\n+ model=\\\"model_catalog\\\",\\n+ base_url=\\\"https://gateway.example.com/v1\\\",\\n+ credential_key=\\\"\\\",\\n+ )\\n+ client = ModelClient()\\n+ with (\\n+ patch.object(client, \\\"_validate_provider\\\") as validate_provider,\\n+ patch.object(client, \\\"_open_provider\\\") as open_provider,\\n+ pytest.raises(RuntimeError, match=\\\"validated agent origin\\\"),\\n+ ):\\n+ client.fetch_json(agent, userinfo_url)\\n+ validate_provider.assert_not_called()\\n+ open_provider.assert_not_called()\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"schema\\\",\\n+ [\\n+ [],\\n+ {\\\"properties\\\": {\\\"answer\\\": []}},\\n+ {\\\"anyOf\\\": {}},\\n+ ],\\n+)\\n+def test_json_schema_definition_rejects_invalid_nested_containers(schema) -> None:\\n+ with pytest.raises(server.RequestError, match=\\\"must\\\") as captured:\\n+ server._validate_json_schema_definition(schema)\\n+ assert captured.value.status == 400\\n+\\n+\\n+def test_json_schema_definition_accepts_recursive_items_and_any_of() -> None:\\n+ server._validate_json_schema_definition(\\n+ {\\n+ \\\"type\\\": \\\"array\\\",\\n+ \\\"items\\\": {\\\"anyOf\\\": [{\\\"type\\\": \\\"string\\\"}, {\\\"type\\\": \\\"integer\\\"}]},\\n+ }\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"value\\\", \\\"schema\\\"),\\n+ [\\n+ (\\\"answer\\\", []),\\n+ (\\\"answer\\\", {\\\"enum\\\": [\\\"other\\\"]}),\\n+ (\\\"answer\\\", {\\\"const\\\": \\\"other\\\"}),\\n+ (\\\"answer\\\", {\\\"anyOf\\\": [{\\\"type\\\": \\\"integer\\\"}]}),\\n+ (\\\"answer\\\", {\\\"type\\\": \\\"object\\\"}),\\n+ ({}, {\\\"type\\\": \\\"object\\\", \\\"required\\\": [\\\"answer\\\"]}),\\n+ ({\\\"other\\\": 1}, {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}, \\\"additionalProperties\\\": False}),\\n+ (\\\"answer\\\", {\\\"type\\\": \\\"array\\\"}),\\n+ (1, {\\\"type\\\": \\\"string\\\"}),\\n+ (\\\"true\\\", {\\\"type\\\": \\\"boolean\\\"}),\\n+ (True, {\\\"type\\\": \\\"integer\\\"}),\\n+ (True, {\\\"type\\\": \\\"number\\\"}),\\n+ ],\\n+)\\n+def test_structured_value_validation_rejects_every_supported_mismatch(value, schema) -> None:\\n+ with pytest.raises(server.RequestError) as captured:\\n+ server._validate_json_schema_value(value, schema)\\n+ assert captured.value.status == 502\\n+\\n+\\n+def test_structured_value_validation_recurses_through_objects_and_arrays() -> None:\\n+ schema = {\\n+ \\\"type\\\": \\\"object\\\",\\n+ \\\"required\\\": [\\\"answers\\\"],\\n+ \\\"properties\\\": {\\\"answers\\\": {\\\"type\\\": \\\"array\\\", \\\"items\\\": {\\\"type\\\": \\\"string\\\"}}},\\n+ \\\"additionalProperties\\\": False,\\n+ }\\n+ server._validate_json_schema_value({\\\"answers\\\": [\\\"yes\\\"]}, schema)\\n+ assert server._json_schema_matches(\\\"yes\\\", {\\\"type\\\": \\\"string\\\"}) is True\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"answer\\\", \\\"response_format\\\"),\\n+ [\\n+ (None, {\\\"type\\\": \\\"json_object\\\"}),\\n+ (\\\"not-json\\\", {\\\"type\\\": \\\"json_object\\\"}),\\n+ (\\\"[]\\\", {\\\"type\\\": \\\"json_object\\\"}),\\n+ ],\\n+)\\n+def test_structured_completion_rejects_non_contract_answers(answer, response_format) -> None:\\n+ with pytest.raises(server.RequestError) as captured:\\n+ server._validate_structured_completion_answer(answer, response_format)\\n+ assert captured.value.status == 502\\n+\\n+\\n+def test_structured_completion_applies_json_schema() -> None:\\n+ server._validate_structured_completion_answer(\\n+ json.dumps({\\\"answer\\\": \\\"yes\\\"}),\\n+ {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\n+ \\\"schema\\\": {\\n+ \\\"type\\\": \\\"object\\\",\\n+ \\\"properties\\\": {\\\"answer\\\": {\\\"type\\\": \\\"string\\\"}},\\n+ \\\"required\\\": [\\\"answer\\\"],\\n+ }\\n+ },\\n+ },\\n+ )\\n+\\n+\\n+def test_responses_chat_content_ignores_non_content_items() -> None:\\n+ assert orchestration._responses_chat_content(None) == \\\"\\\"\\n+ assert orchestration._responses_chat_content([\\\"one\\\", 2, {\\\"text\\\": \\\"two\\\"}]) == \\\"onetwo\\\"\\n+\\n+\\n+def test_temperature_capability_rejection_preserves_http_error_body() -> None:\\n+ class _UnreadableBody:\\n+ def read(self) -> bytes:\\n+ raise OSError(\\\"closed\\\")\\n+\\n+ def close(self) -> None:\\n+ return None\\n+\\n+ assert orchestration._temperature_capability_rejection(ValueError(\\\"temperature\\\")) is False\\n+ error = urllib.error.HTTPError(\\n+ \\\"https://gateway.example/v1/chat/completions\\\",\\n+ 400,\\n+ \\\"temperature is unsupported; only the default value is accepted\\\",\\n+ {},\\n+ _UnreadableBody(),\\n+ )\\n+ assert orchestration._temperature_capability_rejection(error) is True\\n+ assert error.read() == b\\\"\\\"\\n+\\n+\\n+def test_embedding_model_requires_an_orchestrator_when_omitted() -> None:\\n+ with pytest.raises(server.RequestError) as captured:\\n+ server._validate_embeddings_model({})\\n+ assert captured.value.code == \\\"invalid_model\\\"\\n+\\n+\\n+def test_response_content_and_capability_selection_fail_closed() -> None:\\n+ agent = ModelAgent(\\\"general_agent\\\", \\\"mock-model\\\")\\n+ with pytest.raises(RuntimeError, match=\\\"assistant content\\\"):\\n+ ModelClient._response_content(agent, {\\\"choices\\\": [{\\\"message\\\": {}}]})\\n+\\n+ orchestrator = TaskOrchestrator([agent])\\n+ with pytest.raises(ValueError, match=\\\"non-empty\\\"):\\n+ orchestrator.select_capability_agent(\\\" \\\")\\n+ with pytest.raises(RuntimeError, match=\\\"capability=embedding\\\"):\\n+ orchestrator.select_capability_agent(\\\"embedding\\\")\" }, { \"sha\": \"69c33d5d2f18f30d89c64d01e59e768b38d978eb\", \"filename\": \"tests/test_provider_embeddings.py\", \"status\": \"added\", \"additions\": 289, \"deletions\": 0, \"changes\": 289, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_provider_embeddings.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_provider_embeddings.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_embeddings.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,289 @@\\n+\\\"\\\"\\\"Provider-backed embeddings stay inside contextual-orchestrator.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import json\\n+from contextlib import contextmanager\\n+from pathlib import Path\\n+import sys\\n+from types import SimpleNamespace\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator.credentials import ( # noqa: E402\\n+ InMemoryCredentialBackend,\\n+ register_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.batch_routing import EmbeddingBatchRequest # noqa: E402\\n+from contextual_orchestrator.cost_router import CostRoutingCoordinator # noqa: E402\\n+from contextual_orchestrator.orchestrator import ( # noqa: E402\\n+ ModelAgent,\\n+ ModelClient,\\n+ NotConfigured,\\n+)\\n+\\n+\\n+class _Response:\\n+ def __init__(self, payload: dict) -> None:\\n+ self._payload = json.dumps(payload).encode()\\n+\\n+ def read(self, *_args: object) -> bytes:\\n+ return self._payload\\n+\\n+ def __enter__(self) -> \\\"_Response\\\":\\n+ return self\\n+\\n+ def __exit__(self, *args: object) -> None:\\n+ return None\\n+\\n+\\n+def test_model_client_embed_many_calls_provider_embeddings_endpoint(monkeypatch) -> None:\\n+ backend = InMemoryCredentialBackend()\\n+ set_backend(backend)\\n+ register_credential(\\\"EMBEDDING_KEY\\\", \\\"provider-secret\\\")\\n+ seen: list[tuple[str, dict, str]] = []\\n+ agent = ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"text-embedding-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ credential_key=\\\"EMBEDDING_KEY\\\",\\n+ tags=(\\\"embedding\\\",),\\n+ )\\n+ client = ModelClient(max_retries=0)\\n+ monkeypatch.setattr(client, \\\"_validate_provider\\\", lambda _agent: object())\\n+\\n+ @contextmanager\\n+ def open_provider(request, _destination=None, **_kwargs):\\n+ seen.append((request.full_url, json.loads(request.data), request.headers[\\\"Authorization\\\"]))\\n+ yield _Response(\\n+ {\\n+ \\\"data\\\": [\\n+ {\\\"index\\\": 1, \\\"embedding\\\": [2.0, 3.0]},\\n+ {\\\"index\\\": 0, \\\"embedding\\\": [0.0, 1.0]},\\n+ ],\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 2},\\n+ }\\n+ )\\n+\\n+ monkeypatch.setattr(client, \\\"_open_provider\\\", open_provider)\\n+ try:\\n+ assert client.embed_many(agent, [\\\"one\\\", \\\"two\\\"]) == [[0.0, 1.0], [2.0, 3.0]]\\n+ finally:\\n+ set_backend(None)\\n+ assert seen == [\\n+ (\\n+ \\\"https://gateway.example/v1/embeddings\\\",\\n+ {\\\"model\\\": \\\"text-embedding-model\\\", \\\"input\\\": [\\\"one\\\", \\\"two\\\"]},\\n+ \\\"Bearer provider-secret\\\",\\n+ )\\n+ ]\\n+\\n+\\n+def test_cost_router_uses_embedding_agent_instead_of_heuristic() -> None:\\n+ agent = ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"text-embedding-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ tags=(\\\"embedding\\\",),\\n+ )\\n+ calls: list[tuple[str, list[str]]] = []\\n+\\n+ def embed_many(selected: ModelAgent, inputs: list[str]) -> list[list[float]]:\\n+ calls.append((selected.model, inputs))\\n+ return [[float(index)] for index, _input in enumerate(inputs)]\\n+\\n+ orchestrator = SimpleNamespace(\\n+ candidates=[agent],\\n+ client=SimpleNamespace(embed_many=embed_many),\\n+ )\\n+ coordinator = CostRoutingCoordinator(orchestrator)\\n+\\n+ result = coordinator.complete_embeddings_batch(\\n+ [\\\"one\\\", \\\"two\\\"],\\n+ attribution={\\\"provider\\\": \\\"caller-spoof\\\"},\\n+ )\\n+\\n+ assert [item[\\\"embedding\\\"] for item in result[\\\"embeddings\\\"]] == [[0.0], [1.0]]\\n+ assert calls == [(\\\"text-embedding-model\\\", [\\\"one\\\", \\\"two\\\"])]\\n+ assert result[\\\"model\\\"] == \\\"text-embedding-model\\\"\\n+ assert result[\\\"provider\\\"] == \\\"gateway.example\\\"\\n+ assert {record[\\\"provider_name\\\"] for record in coordinator.ledger.records()} == {\\n+ \\\"gateway.example\\\"\\n+ }\\n+\\n+\\n+def test_default_embedding_backend_observes_runtime_agent_addition() -> None:\\n+ calls: list[tuple[str, list[str]]] = []\\n+\\n+ def embed_many(selected: ModelAgent, inputs: list[str]) -> list[list[float]]:\\n+ calls.append((selected.model, inputs))\\n+ return [[1.0] for _input in inputs]\\n+\\n+ orchestrator = SimpleNamespace(\\n+ candidates=[],\\n+ client=SimpleNamespace(embed_many=embed_many),\\n+ )\\n+ coordinator = CostRoutingCoordinator(orchestrator)\\n+ orchestrator.candidates.append(\\n+ ModelAgent(\\n+ \\\"runtime_embedding_agent\\\",\\n+ \\\"runtime-embedding-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ tags=(\\\"embedding\\\",),\\n+ )\\n+ )\\n+\\n+ result = coordinator.complete_embeddings_batch([\\\"added later\\\"])\\n+\\n+ assert calls == [(\\\"runtime-embedding-model\\\", [\\\"added later\\\"])]\\n+ assert result[\\\"embeddings\\\"][0][\\\"embedding\\\"] == [1.0]\\n+ assert result[\\\"provider\\\"] == \\\"gateway.example\\\"\\n+\\n+\\n+def test_embedding_client_fails_closed_before_or_after_transport(monkeypatch) -> None:\\n+ client = ModelClient(max_retries=0)\\n+ mock_agent = ModelAgent(\\\"mock_embedding\\\", \\\"embedding-model\\\", \\\"mock://embedding\\\")\\n+ assert client.embed_many(mock_agent, []) == []\\n+ with pytest.raises(RuntimeError, match=\\\"mock agents\\\"):\\n+ client.embed_many(mock_agent, [\\\"one\\\"])\\n+\\n+ backend = InMemoryCredentialBackend()\\n+ set_backend(backend)\\n+ configured_agent = ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"embedding-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ credential_key=\\\"MISSING_KEY\\\",\\n+ )\\n+ monkeypatch.setattr(client, \\\"_validate_provider\\\", lambda _agent: object())\\n+ try:\\n+ with pytest.raises(NotConfigured, match=\\\"resolvable credential\\\"):\\n+ client.embed_many(configured_agent, [\\\"one\\\"])\\n+ finally:\\n+ set_backend(None)\\n+\\n+ monkeypatch.setattr(client, \\\"_send_embeddings\\\", lambda *_args: (_ for _ in ()).throw(ValueError(\\\"bad\\\")))\\n+ with pytest.raises(RuntimeError, match=\\\"embeddings request failed\\\"):\\n+ client._send_embeddings_with_retry(configured_agent, {\\\"input\\\": [\\\"one\\\"]}, object())\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"payload\\\",\\n+ [\\n+ {\\\"data\\\": []},\\n+ {\\\"data\\\": [{\\\"index\\\": \\\"0\\\", \\\"embedding\\\": [1.0]}]},\\n+ {\\\"data\\\": [{\\\"index\\\": 1, \\\"embedding\\\": [1.0]}]},\\n+ {\\\"data\\\": [{\\\"index\\\": 0, \\\"embedding\\\": [float(\\\"nan\\\")]}]},\\n+ {\\n+ \\\"data\\\": [\\n+ {\\\"index\\\": 0, \\\"embedding\\\": [1.0]},\\n+ {\\\"index\\\": 0, \\\"embedding\\\": [2.0]},\\n+ ]\\n+ },\\n+ ],\\n+)\\n+def test_embedding_client_rejects_malformed_provider_vectors(monkeypatch, payload) -> None:\\n+ client = ModelClient(max_retries=0)\\n+ agent = ModelAgent(\\\"embedding_agent\\\", \\\"embedding-model\\\", \\\"https://gateway.example/v1\\\")\\n+\\n+ @contextmanager\\n+ def open_provider(*_args, **_kwargs):\\n+ yield _Response(payload)\\n+\\n+ monkeypatch.setattr(client, \\\"_open_provider\\\", open_provider)\\n+ inputs = [\\\"one\\\", \\\"two\\\"] if len(payload.get(\\\"data\\\", [])) == 2 else [\\\"one\\\"]\\n+ with pytest.raises(RuntimeError, match=\\\"provider embeddings response\\\"):\\n+ client._send_embeddings(agent, {\\\"model\\\": agent.model, \\\"input\\\": inputs}, object())\\n+\\n+\\n+def test_embedding_model_resolution_covers_server_owned_failure_paths() -> None:\\n+ agent = ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"embedding-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ tags=(\\\"embedding\\\",),\\n+ )\\n+ selected = SimpleNamespace(\\n+ candidates=[agent],\\n+ client=SimpleNamespace(embed_many=lambda _agent, inputs: [[1.0] for _ in inputs]),\\n+ select_capability_agent=lambda capability: agent if capability == \\\"embedding\\\" else None,\\n+ )\\n+ coordinator = CostRoutingCoordinator(selected)\\n+ assert coordinator._resolve_embedding_provider_model(\\\"contextual-orchestrator\\\") == (\\n+ \\\"gateway.example\\\",\\n+ \\\"embedding-model\\\",\\n+ )\\n+ with pytest.raises(ValueError, match=\\\"not configured\\\"):\\n+ coordinator._resolve_embedding_provider_model(\\\"other-model\\\")\\n+\\n+ standalone = CostRoutingCoordinator(SimpleNamespace(candidates=[]))\\n+ with pytest.raises(ValueError, match=\\\"no enabled\\\"):\\n+ standalone._resolve_embedding_provider_model(\\\"contextual-orchestrator\\\")\\n+ assert standalone._resolve_embedding_provider_model(\\\"explicit-model\\\") == (\\n+ \\\"local\\\",\\n+ \\\"explicit-model\\\",\\n+ )\\n+\\n+\\n+def test_provider_embedding_backend_rejects_unknown_identity_and_missing_client() -> None:\\n+ agent = ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"embedding-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ tags=(\\\"embedding\\\",),\\n+ )\\n+ request = EmbeddingBatchRequest(\\n+ input_text=\\\"one\\\",\\n+ model=agent.model,\\n+ provider_name=\\\"wrong.example\\\",\\n+ )\\n+ backend = CostRoutingCoordinator(SimpleNamespace(candidates=[agent])).embedding_batch_backend\\n+ with pytest.raises(ValueError, match=\\\"not configured\\\"):\\n+ backend._batch_embedder([request])\\n+\\n+ request.provider_name = \\\"gateway.example\\\"\\n+ with pytest.raises(RuntimeError, match=\\\"no provider embedding client\\\"):\\n+ backend._batch_embedder([request])\\n+\\n+\\n+def test_provider_json_fetch_is_bounded_and_requires_configured_credentials(monkeypatch) -> None:\\n+ client = ModelClient()\\n+ agent = ModelAgent(\\\"catalog_agent\\\", \\\"catalog-model\\\", \\\"https://gateway.example/v1\\\")\\n+ with pytest.raises(ValueError, match=\\\"positive integer\\\"):\\n+ client.fetch_json(agent, \\\"https://gateway.example/v1/models\\\", max_bytes=0)\\n+\\n+ backend = InMemoryCredentialBackend()\\n+ set_backend(backend)\\n+ secured = ModelAgent(\\n+ \\\"secured_catalog_agent\\\",\\n+ \\\"catalog-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ credential_key=\\\"MISSING_KEY\\\",\\n+ )\\n+ monkeypatch.setattr(client, \\\"_validate_provider\\\", lambda _agent: object())\\n+ try:\\n+ with pytest.raises(NotConfigured, match=\\\"resolvable credential\\\"):\\n+ client.fetch_json(secured, \\\"https://gateway.example/v1/models\\\")\\n+ finally:\\n+ set_backend(None)\\n+\\n+ @contextmanager\\n+ def open_provider(*_args, **_kwargs):\\n+ yield _Response({\\\"models\\\": [\\\"too large\\\"]})\\n+\\n+ monkeypatch.setattr(client, \\\"_open_provider\\\", open_provider)\\n+ backend = InMemoryCredentialBackend()\\n+ backend.set(\\\"OPENAI_API_KEY\\\", \\\"provider-key\\\")\\n+ set_backend(backend)\\n+ try:\\n+ with pytest.raises(ValueError, match=\\\"maximum size\\\"):\\n+ client.fetch_json(agent, \\\"https://gateway.example/v1/models\\\", max_bytes=1)\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"5edaee425381d26335edef5c15f43d3f2fbb4b95\", \"filename\": \"tests/test_provider_integration.py\", \"status\": \"modified\", \"additions\": 51, \"deletions\": 3, \"changes\": 54, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_provider_integration.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_provider_integration.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_integration.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -34,12 +34,14 @@ class _FakeProvider:\\n \\n def __init__(self, responses: list[tuple[int, dict]]) -> None:\\n self.request_count = 0\\n+ self.requests: list[dict] = []\\n outer = self\\n \\n class Handler(BaseHTTPRequestHandler):\\n def do_POST(self) -> None: # noqa: N802\\n length = int(self.headers.get(\\\"content-length\\\", 0))\\n- self.rfile.read(length)\\n+ raw_request = self.rfile.read(length)\\n+ outer.requests.append(json.loads(raw_request.decode(\\\"utf-8\\\")))\\n index = min(outer.request_count, len(responses) - 1)\\n outer.request_count += 1\\n status, body = responses[index]\\n@@ -112,8 +114,54 @@ def test_permanent_4xx_is_not_retried_over_http() -> None:\\n client._send_with_retry(_agent(provider.base_url), {\\\"model\\\": \\\"gpt-x\\\"})\\n except RuntimeError:\\n raised = True\\n- assert raised\\n- assert provider.request_count == 1 # 400 is a real HTTPError classified permanent: one attempt\\n+ assert raised\\n+ assert provider.request_count == 1 # 400 is a real HTTPError classified permanent: one attempt\\n+\\n+\\n+def test_unsupported_temperature_is_negotiated_on_the_same_chat_endpoint() -> None:\\n+ with _FakeProvider([\\n+ (422, {\\\"error\\\": {\\\"message\\\": \\\"temperature is not supported for this deployment\\\"}}),\\n+ (200, _completion(\\\"negotiated\\\")),\\n+ ]) as provider:\\n+ client = ModelClient(max_retries=0)\\n+ result = client._send_with_retry(\\n+ _agent(provider.base_url), {\\\"model\\\": \\\"gpt-x\\\", \\\"temperature\\\": 0.2}\\n+ )\\n+ assert result == \\\"negotiated\\\"\\n+ assert provider.request_count == 2\\n+ assert provider.requests[0][\\\"temperature\\\"] == 0.2\\n+ assert \\\"temperature\\\" not in provider.requests[1]\\n+\\n+\\n+def test_invalid_temperature_is_not_treated_as_capability_negotiation() -> None:\\n+ with _FakeProvider([(400, {\\\"error\\\": {\\\"message\\\": \\\"invalid temperature value\\\"}})]) as provider:\\n+ client = ModelClient(max_retries=3, retry_backoff=0.0)\\n+ raised = False\\n+ try:\\n+ client._send_with_retry(\\n+ _agent(provider.base_url), {\\\"model\\\": \\\"gpt-x\\\", \\\"temperature\\\": 2.5}\\n+ )\\n+ except RuntimeError:\\n+ raised = True\\n+ assert raised\\n+ assert provider.request_count == 1\\n+\\n+\\n+def test_unsupported_temperature_is_negotiated_for_raw_responses_transport() -> None:\\n+ response = {\\\"id\\\": \\\"response-1\\\", \\\"output\\\": [{\\\"type\\\": \\\"message\\\"}]}\\n+ with _FakeProvider([\\n+ (400, {\\\"error\\\": {\\\"message\\\": \\\"unknown parameter: temperature\\\"}}),\\n+ (200, response),\\n+ ]) as provider:\\n+ client = ModelClient(max_retries=0)\\n+ result = client._send_raw_with_retry(\\n+ _agent(provider.base_url),\\n+ \\\"responses\\\",\\n+ {\\\"model\\\": \\\"gpt-x\\\", \\\"input\\\": \\\"hello\\\", \\\"temperature\\\": 0.2},\\n+ )\\n+ assert result == response\\n+ assert provider.request_count == 2\\n+ assert \\\"temperature\\\" not in provider.requests[1]\\n \\n \\n def test_connection_error_is_transient_and_exhausts() -> None:\" }, { \"sha\": \"00f1b4b2d33f78216e749a9ea19e5a9e7ae55984\", \"filename\": \"tests/test_provider_reliability.py\", \"status\": \"modified\", \"additions\": 40, \"deletions\": 4, \"changes\": 44, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_provider_reliability.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_provider_reliability.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_reliability.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -14,6 +14,8 @@\\n from concurrent.futures import ThreadPoolExecutor\\n from pathlib import Path\\n \\n+import pytest\\n+\\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n@@ -78,7 +80,7 @@ def _send(self, agent: ModelAgent, payload: dict, destination=None) -> str: # t\\n raise urllib.error.URLError(\\\"local server is busy\\\")\\n \\n client = LocalDownClient()\\n- agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n try:\\n client._send_with_retry(agent, {\\\"model\\\": agent.model})\\n except RuntimeError:\\n@@ -101,7 +103,7 @@ def _send(self, agent: ModelAgent, payload: dict, destination=None) -> str: # t\\n return \\\"recovered\\\"\\n \\n client = LocalFlakyClient()\\n- agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n assert client._send_with_retry(agent, {\\\"model\\\": agent.model}) == \\\"recovered\\\"\\n assert client.attempts == 2\\n \\n@@ -119,7 +121,7 @@ def _send(self, agent: ModelAgent, payload: dict, destination=None) -> str: # t\\n return \\\"recovered\\\"\\n \\n client = LocalFlakyClient()\\n- agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n assert client._send_with_retry(agent, {\\\"model\\\": agent.model}) == \\\"recovered\\\"\\n assert client.attempts == 3\\n \\n@@ -137,7 +139,7 @@ def _send_raw(self, agent: ModelAgent, endpoint: str, payload: dict, destination\\n return {\\\"ok\\\": True}\\n \\n client = LocalRawFlakyClient()\\n- agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n assert client._send_raw_with_retry(agent, \\\"chat/completions\\\", {}) == {\\\"ok\\\": True}\\n assert client.attempts == 3\\n \\n@@ -163,6 +165,38 @@ def _send(self, agent: ModelAgent, payload: dict, destination=None) -> str: # t\\n assert client.attempts == 1 # 400 is a caller error: exactly one attempt, no retry\\n \\n \\n+def test_provider_request_hides_raw_error_text_and_cause() -> None:\\n+ class RawProviderFailureClient(ModelClient):\\n+ def _send(self, agent: ModelAgent, payload: dict, destination=None) -> str: # type: ignore[override]\\n+ raise RuntimeError(\\\"provider-secret-response\\\")\\n+\\n+ client = RawProviderFailureClient(max_retries=0)\\n+ agent = ModelAgent(\\\"worker_agent\\\", \\\"gpt\\\", base_url=\\\"https://provider.example/v1\\\")\\n+\\n+ try:\\n+ client._send_with_retry(agent, {\\\"model\\\": \\\"gpt\\\"})\\n+ except RuntimeError as error:\\n+ assert \\\"provider-secret-response\\\" not in str(error)\\n+ assert error.__cause__ is None\\n+ else: # pragma: no cover\\n+ raise AssertionError(\\\"a failed provider request must raise\\\")\\n+\\n+\\n+def test_embedding_request_hides_raw_error_text_and_cause() -> None:\\n+ class RawEmbeddingFailureClient(ModelClient):\\n+ def _send_embeddings(self, agent: ModelAgent, payload: dict, destination=None) -> list[list[float]]: # type: ignore[override]\\n+ raise RuntimeError(\\\"embedding-provider-secret\\\")\\n+\\n+ client = RawEmbeddingFailureClient(max_retries=0)\\n+ agent = ModelAgent(\\\"embedding_agent\\\", \\\"embedding-model\\\", base_url=\\\"https://provider.example/v1\\\")\\n+\\n+ with pytest.raises(RuntimeError) as error:\\n+ client._send_embeddings_with_retry(agent, {\\\"model\\\": agent.model, \\\"input\\\": [\\\"text\\\"]})\\n+\\n+ assert \\\"embedding-provider-secret\\\" not in str(error.value)\\n+ assert error.value.__cause__ is None\\n+\\n+\\n class _AgentDownClient(ModelClient):\\n \\\"\\\"\\\"Fails for a chosen agent id, succeeds for the rest.\\\"\\\"\\\"\\n \\n@@ -214,6 +248,8 @@ def chat(self, agent: ModelAgent, messages: list, temperature: float = 0.2) -> s\\n except RuntimeError as exc:\\n raised = True\\n assert \\\"candidate agents failed\\\" in str(exc)\\n+ assert \\\"everything is down\\\" not in str(exc)\\n+ assert exc.__cause__ is None\\n assert raised\\n \\n \" }, { \"sha\": \"7349c730d586e5ec5f9cd4ac450437a1e677417e\", \"filename\": \"tests/test_repository_security_metadata.py\", \"status\": \"modified\", \"additions\": 15, \"deletions\": 1, \"changes\": 16, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_repository_security_metadata.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_repository_security_metadata.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_repository_security_metadata.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -152,11 +152,25 @@ def test_database_design_avoids_plaintext_prompt_output_storage():\\n def test_python_lockfile_uses_hash_pinning():\\n lock_text = read_text(\\\"requirements.lock\\\")\\n \\n- assert \\\"pip-compile\\\" in lock_text\\n+ assert \\\"uv pip compile\\\" in lock_text\\n+ assert \\\"--universal\\\" in lock_text\\n assert \\\"--hash=sha256:\\\" in lock_text\\n assert \\\"fastapi==\\\" in lock_text\\n assert \\\"uvicorn==\\\" in lock_text\\n assert \\\"sqlalchemy==\\\" in lock_text\\n+ for platform_dependency in (\\\"colorama\\\", \\\"greenlet\\\", \\\"tzdata\\\"):\\n+ assert f\\\"{platform_dependency}==\\\" in lock_text\\n+\\n+\\n+def test_unit_workflow_installs_runtime_and_test_lockfiles():\\n+ \\\"\\\"\\\"CI must exercise declared runtime integrations, not graceful no-op imports.\\\"\\\"\\\"\\n+ workflow_text = read_text(\\\".github/workflows/tests.yml\\\")\\n+\\n+ assert \\\"python -m pip install --require-hashes -r requirements.lock\\\" in workflow_text\\n+ assert (\\n+ \\\"python -m pip install --require-hashes -r fuzz/requirements-property.txt\\\"\\n+ in workflow_text\\n+ )\\n \\n \\n def test_security_tool_lockfile_uses_hash_pinning():\" }, { \"sha\": \"531e23fb7cccbce95ec86309db412b51b552aed7\", \"filename\": \"tests/test_responses_attribution_routing_http_honesty.py\", \"status\": \"modified\", \"additions\": 18, \"deletions\": 0, \"changes\": 18, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_attribution_routing_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_attribution_routing_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_attribution_routing_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -122,6 +122,24 @@ def test_http_responses_rejects_routing_latency_tolerant_true() -> None:\\n thread.join(timeout=5)\\n \\n \\n+def test_http_responses_rejects_routing_unknown_key() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"routing junk\\\",\\n+ \\\"routing\\\": {\\\"channel\\\": \\\"sync\\\", \\\"region\\\": \\\"us-east\\\"},\\n+ },\\n+ )\\n+ assert status == 400, body\\n+ assert \\\"invalid_routing\\\" in json.dumps(body)\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n def test_http_responses_rejects_attribution_unknown_dimension() -> None:\\n server, thread, port = _server()\\n try:\" }, { \"sha\": \"27352b247858a0b9b649e8e072992ac6be8e88b0\", \"filename\": \"tests/test_responses_flat_tools_http_honesty.py\", \"status\": \"modified\", \"additions\": 13, \"deletions\": 18, \"changes\": 31, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_flat_tools_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_flat_tools_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_flat_tools_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,7 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_responses_accepts_flat_function_tools() -> None:\\n+def test_http_responses_rejects_flat_function_tools_without_single_agent_fallback() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -72,13 +72,14 @@ def test_http_responses_accepts_flat_function_tools() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_flat_tools_with_tool_choice_name() -> None:\\n+def test_http_responses_rejects_flat_tools_with_tool_choice_name() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -100,13 +101,14 @@ def test_http_responses_accepts_flat_tools_with_tool_choice_name() -> None:\\n },\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_still_accepts_nested_function_tools() -> None:\\n+def test_http_chat_rejects_nested_function_tools_without_single_agent_fallback() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -126,14 +128,15 @@ def test_http_chat_still_accepts_nested_function_tools() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_accepts_flat_function_tools_too() -> None:\\n- \\\"\\\"\\\"Chat path accepts Responses-flat tools for SDK portability.\\\"\\\"\\\"\\n+def test_http_chat_rejects_flat_function_tools_without_single_agent_fallback() -> None:\\n+ \\\"\\\"\\\"Chat path does not silently downgrade tools to one agent.\\\"\\\"\\\"\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -152,7 +155,8 @@ def test_http_chat_accepts_flat_function_tools_too() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -181,12 +185,3 @@ def test_http_tools_rejects_mixed_nested_and_flat() -> None:\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n-\\n-\\n-if __name__ == \\\"__main__\\\":\\n- test_http_responses_accepts_flat_function_tools()\\n- test_http_responses_accepts_flat_tools_with_tool_choice_name()\\n- test_http_chat_still_accepts_nested_function_tools()\\n- test_http_chat_accepts_flat_function_tools_too()\\n- test_http_tools_rejects_mixed_nested_and_flat()\\n- print(\\\"ok\\\")\" } ]" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-24T07-42-26-068Z.yml b/.playwright-mcp/page-2026-08-24T07-42-26-068Z.yml deleted file mode 100644 index 3d3c41296..000000000 --- a/.playwright-mcp/page-2026-08-24T07-42-26-068Z.yml +++ /dev/null @@ -1 +0,0 @@ -- generic [active] [ref=f1e1]: "[ { \"sha\": \"275f7ff0f054d59b4171b1aa7c657ffd26f77060\", \"filename\": \"tests/test_responses_instructions_reasoning_http_honesty.py\", \"status\": \"modified\", \"additions\": 17, \"deletions\": 0, \"changes\": 17, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_instructions_reasoning_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_instructions_reasoning_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_instructions_reasoning_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -139,6 +139,23 @@ def test_http_responses_accepts_reasoning_effort_known_levels() -> None:\\n thread.join(timeout=5)\\n \\n \\n+def test_http_responses_accepts_orchestrator_owned_reasoning_effort_auto() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"think automatically\\\",\\n+ \\\"reasoning\\\": {\\\"effort\\\": \\\"auto\\\"},\\n+ },\\n+ )\\n+ assert status == 200, body\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n def test_http_responses_still_rejects_unknown_reasoning_effort() -> None:\\n server, thread, port = _server()\\n try:\" }, { \"sha\": \"a0f858ce725be9f491a41c22db9a9e795327e7db\", \"filename\": \"tests/test_responses_logit_bias_logprobs_http_honesty.py\", \"status\": \"modified\", \"additions\": 8, \"deletions\": 6, \"changes\": 14, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_logit_bias_logprobs_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_logit_bias_logprobs_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_logit_bias_logprobs_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -48,7 +48,7 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_responses_accepts_empty_and_valid_logit_bias() -> None:\\n+def test_http_responses_accepts_empty_and_rejects_unapplied_logit_bias() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -64,7 +64,8 @@ def test_http_responses_accepts_empty_and_valid_logit_bias() -> None:\\n \\\"logit_bias\\\": {\\\"50256\\\": -100, \\\"220\\\": 50},\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"unsupported_responses_orchestration_controls\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -119,7 +120,7 @@ def test_http_responses_accepts_logprobs_false() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_logprobs_true_with_top_logprobs() -> None:\\n+def test_http_responses_rejects_unapplied_logprobs_with_top_logprobs() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -131,7 +132,8 @@ def test_http_responses_accepts_logprobs_true_with_top_logprobs() -> None:\\n \\\"top_logprobs\\\": 5,\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"unsupported_responses_orchestration_controls\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -166,11 +168,11 @@ def test_http_responses_rejects_non_boolean_logprobs() -> None:\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_responses_accepts_empty_and_valid_logit_bias()\\n+ test_http_responses_accepts_empty_and_rejects_unapplied_logit_bias()\\n test_http_responses_rejects_non_digit_logit_bias_key()\\n test_http_responses_rejects_out_of_range_logit_bias_value()\\n test_http_responses_accepts_logprobs_false()\\n- test_http_responses_accepts_logprobs_true_with_top_logprobs()\\n+ test_http_responses_rejects_unapplied_logprobs_with_top_logprobs()\\n test_http_responses_rejects_top_logprobs_without_logprobs()\\n test_http_responses_rejects_non_boolean_logprobs()\\n print(\\\"ok\\\")\" }, { \"sha\": \"7c8dd1c37fc09762471c1d328932c785e5e62d9c\", \"filename\": \"tests/test_responses_model_required_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 6, \"changes\": 10, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_model_required_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_model_required_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_model_required_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -48,14 +48,12 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_responses_rejects_missing_model() -> None:\\n+def test_http_responses_allows_orchestrator_owned_model_selection() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(port, {\\\"input\\\": \\\"hello\\\"})\\n- assert status == 400, body\\n- blob = json.dumps(body)\\n- assert \\\"invalid_model\\\" in blob\\n- assert \\\"required\\\" in blob\\n+ assert status == 200, body\\n+ assert body[\\\"model\\\"] == \\\"contextual-orchestrator\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -110,7 +108,7 @@ def test_http_responses_accepts_pool_model() -> None:\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_responses_rejects_missing_model()\\n+ test_http_responses_allows_orchestrator_owned_model_selection()\\n test_http_responses_rejects_empty_model()\\n test_http_responses_rejects_non_string_model()\\n test_http_responses_rejects_overlong_model()\" }, { \"sha\": \"07d6d1914acd4131d75217905553caa0c41e11be\", \"filename\": \"tests/test_responses_parallel_tool_calls_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 3, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_parallel_tool_calls_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_parallel_tool_calls_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_parallel_tool_calls_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -75,7 +75,7 @@ def test_http_responses_accepts_false_parallel_tool_calls_without_tools() -> Non\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_true_parallel_tool_calls_with_tools() -> None:\\n+def test_http_responses_rejects_true_parallel_tool_calls_with_tools() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -95,7 +95,8 @@ def test_http_responses_accepts_true_parallel_tool_calls_with_tools() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -158,7 +159,7 @@ def test_http_responses_rejects_integer_parallel_tool_calls() -> None:\\n if __name__ == \\\"__main__\\\":\\n test_http_responses_accepts_omitted_parallel_tool_calls()\\n test_http_responses_accepts_false_parallel_tool_calls_without_tools()\\n- test_http_responses_accepts_true_parallel_tool_calls_with_tools()\\n+ test_http_responses_rejects_true_parallel_tool_calls_with_tools()\\n test_http_responses_rejects_true_parallel_tool_calls_without_tools()\\n test_http_responses_rejects_non_boolean_parallel_tool_calls()\\n test_http_responses_rejects_integer_parallel_tool_calls()\" }, { \"sha\": \"1b1c26bbfcd26cdab715e493a5658733620c846b\", \"filename\": \"tests/test_responses_penalties_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 3, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_penalties_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_penalties_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_penalties_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -48,7 +48,7 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_responses_accepts_valid_penalties() -> None:\\n+def test_http_responses_rejects_unapplied_penalties() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -60,7 +60,8 @@ def test_http_responses_accepts_valid_penalties() -> None:\\n \\\"frequency_penalty\\\": -0.5,\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"unsupported_responses_orchestration_controls\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -121,7 +122,7 @@ def test_http_responses_rejects_boolean_presence_penalty() -> None:\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_responses_accepts_valid_penalties()\\n+ test_http_responses_rejects_unapplied_penalties()\\n test_http_responses_rejects_out_of_range_presence_penalty()\\n test_http_responses_rejects_out_of_range_frequency_penalty()\\n test_http_responses_rejects_boolean_presence_penalty()\" }, { \"sha\": \"daba0a28021ffe633e028bf8924c763c906542ca\", \"filename\": \"tests/test_responses_seed_stop_http_honesty.py\", \"status\": \"modified\", \"additions\": 10, \"deletions\": 7, \"changes\": 17, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_seed_stop_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_seed_stop_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_seed_stop_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -48,14 +48,15 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_responses_accepts_valid_seed() -> None:\\n+def test_http_responses_rejects_unapplied_seed() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n port,\\n {\\\"model\\\": \\\"mock-planner\\\", \\\"input\\\": \\\"hello seed\\\", \\\"seed\\\": 42},\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"unsupported_responses_orchestration_controls\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -89,14 +90,15 @@ def test_http_responses_rejects_boolean_seed() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_stop_string_and_array() -> None:\\n+def test_http_responses_rejects_unapplied_stop_string_and_array() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n port,\\n {\\\"model\\\": \\\"mock-planner\\\", \\\"input\\\": \\\"hello stop str\\\", \\\"stop\\\": \\\"END\\\"},\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"unsupported_responses_orchestration_controls\\\"\\n status, body = _post(\\n port,\\n {\\n@@ -105,7 +107,8 @@ def test_http_responses_accepts_stop_string_and_array() -> None:\\n \\\"stop\\\": [\\\"END\\\", \\\"STOP\\\"],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"unsupported_responses_orchestration_controls\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -158,10 +161,10 @@ def test_http_responses_rejects_non_string_stop_item() -> None:\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_responses_accepts_valid_seed()\\n+ test_http_responses_rejects_unapplied_seed()\\n test_http_responses_rejects_non_integer_seed()\\n test_http_responses_rejects_boolean_seed()\\n- test_http_responses_accepts_stop_string_and_array()\\n+ test_http_responses_rejects_unapplied_stop_string_and_array()\\n test_http_responses_accepts_empty_stop_string_as_omit()\\n test_http_responses_rejects_stop_array_too_long()\\n test_http_responses_rejects_non_string_stop_item()\" }, { \"sha\": \"d0936238fe3835c604e5a8df81c624af6e0e2938\", \"filename\": \"tests/test_responses_temperature_top_p_http_honesty.py\", \"status\": \"modified\", \"additions\": 37, \"deletions\": 3, \"changes\": 40, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_temperature_top_p_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_temperature_top_p_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_temperature_top_p_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -8,6 +8,7 @@\\n import urllib.request\\n from pathlib import Path\\n import sys\\n+from unittest.mock import patch\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n@@ -48,7 +49,7 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_responses_accepts_valid_temperature_and_top_p() -> None:\\n+def test_http_responses_rejects_unapplied_temperature_and_top_p() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -60,12 +61,45 @@ def test_http_responses_accepts_valid_temperature_and_top_p() -> None:\\n \\\"top_p\\\": 0.9,\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"unsupported_responses_orchestration_controls\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n \\n \\n+def test_http_responses_prefers_native_max_output_tokens() -> None:\\n+ orch = build()\\n+ observed: list[int | None] = []\\n+ original_chat = orch.client.chat\\n+\\n+ def observe_chat(agent, messages, temperature=None, top_p=None):\\n+ observed.append(orch.client._request_setting(\\\"max_output_tokens\\\", orch.client.max_output_tokens))\\n+ return original_chat(agent, messages, temperature=temperature, top_p=top_p)\\n+\\n+ with patch.object(orch.client, \\\"chat\\\", side_effect=observe_chat):\\n+ server = build_server(orch, port=0, security=SecurityConfig(auth_token=_TEST_AUTH_TOKEN))\\n+ thread = threading.Thread(target=server.serve_forever, daemon=True)\\n+ thread.start()\\n+ try:\\n+ status, body = _post(\\n+ server.server_address[1],\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"hello budget\\\",\\n+ \\\"max_tokens\\\": 11,\\n+ \\\"max_completion_tokens\\\": 13,\\n+ \\\"max_output_tokens\\\": 17,\\n+ },\\n+ )\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+ assert status == 200, body\\n+ assert observed and set(observed) == {17}\\n+\\n+\\n def test_http_responses_rejects_out_of_range_temperature() -> None:\\n server, thread, port = _server()\\n try:\\n@@ -123,7 +157,7 @@ def test_http_responses_rejects_boolean_top_p() -> None:\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_responses_accepts_valid_temperature_and_top_p()\\n+ test_http_responses_rejects_unapplied_temperature_and_top_p()\\n test_http_responses_rejects_out_of_range_temperature()\\n test_http_responses_rejects_non_numeric_temperature()\\n test_http_responses_rejects_out_of_range_top_p()\" }, { \"sha\": \"ecd4f8b59f544ae69d1c78624f85cf269aa01bb4\", \"filename\": \"tests/test_responses_text_format_http_honesty.py\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 8, \"changes\": 11, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_text_format_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_text_format_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_text_format_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -114,7 +114,7 @@ def test_http_responses_accepts_official_text_format() -> None:\\n },\\n )\\n assert status == 200, body\\n- assert body.get(\\\"echo\\\", {}).get(\\\"text\\\") == _OFFICIAL_TEXT_FORMAT\\n+ assert body.get(\\\"object\\\") == \\\"response\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -151,7 +151,7 @@ def test_http_responses_accepts_text_format_json_object() -> None:\\n },\\n )\\n assert status == 200, body\\n- assert body.get(\\\"echo\\\", {}).get(\\\"text\\\") == {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}}\\n+ assert json.loads(body[\\\"output_text\\\"]) == {}\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -178,12 +178,7 @@ def test_http_responses_omits_json_schema_null_optionals_on_text_format() -> Non\\n },\\n )\\n assert status == 200, body\\n- fmt = (body.get(\\\"echo\\\") or {}).get(\\\"text\\\", {}).get(\\\"format\\\")\\n- assert isinstance(fmt, dict), body\\n- assert \\\"description\\\" not in fmt\\n- assert \\\"strict\\\" not in fmt\\n- assert fmt.get(\\\"name\\\") == \\\"receipt_line\\\"\\n- assert fmt.get(\\\"schema\\\") == _SCHEMA_BODY\\n+ assert json.loads(body[\\\"output_text\\\"]) == {}\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\" }, { \"sha\": \"dd04e3991b868cbff9306992b4b3b0fc1220bba1\", \"filename\": \"tests/test_responses_tools_shape_http_honesty.py\", \"status\": \"modified\", \"additions\": 89, \"deletions\": 9, \"changes\": 98, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_tools_shape_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_tools_shape_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_tools_shape_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -23,15 +23,18 @@ def build() -> TaskOrchestrator:\\n )\\n \\n \\n-def _post(port: int, payload: dict) -> tuple[int, dict]:\\n+def _post(port: int, payload: dict, *, tool_loop: bool = False) -> tuple[int, dict]:\\n+ headers = {\\n+ \\\"content-type\\\": \\\"application/json\\\",\\n+ \\\"authorization\\\": f\\\"Bearer {_TEST_AUTH_TOKEN}\\\",\\n+ \\\"connection\\\": \\\"close\\\",\\n+ }\\n+ if tool_loop:\\n+ headers[\\\"x-contextual-orchestrator-tool-loop\\\"] = \\\"v1\\\"\\n request = urllib.request.Request(\\n f\\\"http://127.0.0.1:{port}/v1/responses\\\",\\n data=json.dumps(payload).encode(\\\"utf-8\\\"),\\n- headers={\\n- \\\"content-type\\\": \\\"application/json\\\",\\n- \\\"authorization\\\": f\\\"Bearer {_TEST_AUTH_TOKEN}\\\",\\n- \\\"connection\\\": \\\"close\\\",\\n- },\\n+ headers=headers,\\n method=\\\"POST\\\",\\n )\\n try:\\n@@ -61,7 +64,7 @@ def _valid_tools() -> list[dict]:\\n ]\\n \\n \\n-def test_http_responses_accepts_valid_tools_and_auto_choice() -> None:\\n+def test_http_responses_rejects_tools_without_explicit_loop_header() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -73,7 +76,81 @@ def test_http_responses_accepts_valid_tools_and_auto_choice() -> None:\\n \\\"tool_choice\\\": \\\"auto\\\",\\n },\\n )\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_responses_preserves_tools_with_explicit_loop_header() -> None:\\n+ \\\"\\\"\\\"The opt-in Responses contract preserves the provider response shape.\\\"\\\"\\\"\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"use tools\\\",\\n+ \\\"tools\\\": _valid_tools(),\\n+ \\\"tool_choice\\\": \\\"auto\\\",\\n+ },\\n+ tool_loop=True,\\n+ )\\n assert status == 200, body\\n+ assert body[\\\"echo\\\"][\\\"tools\\\"] == _valid_tools()\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_responses_tool_loop_rejects_stream_true() -> None:\\n+ \\\"\\\"\\\"Client-owned Responses tool loops must reject unsupported streaming.\\\"\\\"\\\"\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"stream tools\\\",\\n+ \\\"tools\\\": _valid_tools(),\\n+ \\\"stream\\\": True,\\n+ },\\n+ tool_loop=True,\\n+ )\\n+ assert status == 400, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"invalid_stream\\\"\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_responses_tool_loop_requires_input() -> None:\\n+ \\\"\\\"\\\"Client-owned Responses tool loops still require a non-empty input.\\\"\\\"\\\"\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ {\\\"model\\\": \\\"mock-planner\\\", \\\"tools\\\": _valid_tools()},\\n+ tool_loop=True,\\n+ )\\n+ assert status == 400, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"invalid_input\\\"\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_responses_tool_loop_rejects_scalar_input() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ {\\\"model\\\": \\\"mock-planner\\\", \\\"input\\\": 7, \\\"tools\\\": _valid_tools()},\\n+ tool_loop=True,\\n+ )\\n+ assert status == 400, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"invalid_input\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -172,8 +249,11 @@ def test_http_responses_rejects_named_tool_choice_not_in_tools() -> None:\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_responses_accepts_valid_tools_and_auto_choice()\\n- test_http_responses_rejects_empty_tools_array()\\n+ test_http_responses_rejects_tools_without_explicit_loop_header()\\n+ test_http_responses_preserves_tools_with_explicit_loop_header()\\n+ test_http_responses_tool_loop_rejects_stream_true()\\n+ test_http_responses_tool_loop_requires_input()\\n+ test_http_responses_accepts_empty_tools_array_as_noop()\\n test_http_responses_rejects_tool_without_function_type()\\n test_http_responses_accepts_tool_choice_auto_without_tools_as_omit()\\n test_http_responses_rejects_legacy_functions_surface()\" }, { \"sha\": \"a7b800357075887e0d29215a737d1e969d6de250\", \"filename\": \"tests/test_routing_latency_stream_options_bool_coerce_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 3, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_routing_latency_stream_options_bool_coerce_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_routing_latency_stream_options_bool_coerce_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_routing_latency_stream_options_bool_coerce_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -171,7 +171,7 @@ def test_http_chat_accepts_response_format_empty_type_as_omit() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_top_logprobs_digit_string_with_logprobs() -> None:\\n+def test_http_responses_rejects_unapplied_top_logprobs_digit_string() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -184,7 +184,8 @@ def test_http_responses_accepts_top_logprobs_digit_string_with_logprobs() -> Non\\n \\\"top_logprobs\\\": \\\"5\\\",\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -197,5 +198,5 @@ def test_http_responses_accepts_top_logprobs_digit_string_with_logprobs() -> Non\\n test_http_chat_accepts_stream_options_include_usage_false_string()\\n test_http_chat_still_rejects_stream_options_include_usage_true_string_without_stream()\\n test_http_chat_accepts_response_format_empty_type_as_omit()\\n- test_http_responses_accepts_top_logprobs_digit_string_with_logprobs()\\n+ test_http_responses_rejects_unapplied_top_logprobs_digit_string()\\n print(\\\"ok\\\")\" }, { \"sha\": \"d36b7ca5ae7315f0918bcdb49d58b83022693181\", \"filename\": \"tests/test_sales_readiness.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_sales_readiness.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_sales_readiness.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_sales_readiness.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -162,7 +162,7 @@ def test_sales_readiness_warns_for_single_token_local_deployment() -> None:\\n def test_provider_egress_report_skips_local_and_checks_remote_agents() -> None:\\n orchestrator = TaskOrchestrator(\\n [\\n- ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\"),\\n+ ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\"),\\n ModelAgent(\\n \\\"remote_agent\\\",\\n \\\"remote-model\\\",\" }, { \"sha\": \"7152fc110ab0ecce9243bdd932b5051c80688914\", \"filename\": \"tests/test_sampling_contract.py\", \"status\": \"added\", \"additions\": 85, \"deletions\": 0, \"changes\": 85, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_sampling_contract.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_sampling_contract.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_sampling_contract.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,85 @@\\n+\\\"\\\"\\\"Tests for provider-neutral optional sampling request fields.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import json\\n+\\n+from contextual_orchestrator.orchestrator import ModelAgent, ModelClient\\n+\\n+\\n+def _agent() -> ModelAgent:\\n+ return ModelAgent(\\n+ id=\\\"sampling_worker\\\",\\n+ model=\\\"provider/model\\\",\\n+ base_url=\\\"https://gateway.example.com\\\",\\n+ credential_key=\\\"\\\",\\n+ )\\n+\\n+\\n+def test_model_client_owns_default_sampling_without_import_side_effects() -> None:\\n+ client = ModelClient()\\n+\\n+ assert client.default_temperature is None\\n+ assert client.temperature is None\\n+\\n+\\n+def test_stream_omits_unrequested_temperature_and_preserves_explicit_value(monkeypatch) -> None:\\n+ client = ModelClient()\\n+ captured: list[dict[str, object]] = []\\n+ monkeypatch.setattr(client, \\\"_validate_provider\\\", lambda _agent: object())\\n+\\n+ def stream_send(_agent, payload, _destination=None):\\n+ captured.append(payload)\\n+ return iter([\\\"OK\\\"])\\n+\\n+ monkeypatch.setattr(client, \\\"_stream_send\\\", stream_send)\\n+\\n+ assert list(client.stream_chat(_agent(), [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Sample.\\\"}])) == [\\\"OK\\\"]\\n+ assert \\\"temperature\\\" not in captured[0]\\n+ assert list(client.stream_chat(_agent(), [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Sample.\\\"}], temperature=0.2)) == [\\\"OK\\\"]\\n+ assert captured[1][\\\"temperature\\\"] == 0.2\\n+\\n+\\n+def test_batch_omits_unrequested_temperature_and_preserves_explicit_value(monkeypatch) -> None:\\n+ client = ModelClient()\\n+ uploaded: list[list[dict[str, object]]] = []\\n+\\n+ def batch_upload(_agent, payload, _destination=None):\\n+ uploaded.append([json.loads(line) for line in payload.decode(\\\"utf-8\\\").splitlines()])\\n+ return \\\"file_001\\\"\\n+\\n+ monkeypatch.setattr(client, \\\"_batch_upload\\\", batch_upload)\\n+\\n+ def batch_json(_agent, method, path, body=None, destination=None):\\n+ del path, body, destination\\n+ if method == \\\"POST\\\":\\n+ return {\\\"id\\\": \\\"batch_001\\\"}\\n+ return {\\\"status\\\": \\\"completed\\\", \\\"output_file_id\\\": \\\"file_002\\\"}\\n+\\n+ monkeypatch.setattr(client, \\\"_batch_json\\\", batch_json)\\n+ monkeypatch.setattr(\\n+ client,\\n+ \\\"_batch_raw\\\",\\n+ lambda *_args, **_kwargs: (\\n+ b'{\\\"custom_id\\\":\\\"request_1\\\",\\\"response\\\":{\\\"body\\\":{\\\"choices\\\":[{\\\"message\\\":'\\n+ b'{\\\"content\\\":\\\"OK\\\"}}]}}}'\\n+ ),\\n+ )\\n+\\n+ client._batch_run(\\n+ _agent(),\\n+ {\\\"request_1\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Sample.\\\"}]},\\n+ None,\\n+ 0.0,\\n+ 1.0,\\n+ )\\n+ assert \\\"temperature\\\" not in uploaded[0][0][\\\"body\\\"]\\n+\\n+ client._batch_run(\\n+ _agent(),\\n+ {\\\"request_2\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Sample.\\\"}]},\\n+ 0.2,\\n+ 0.0,\\n+ 1.0,\\n+ )\\n+ assert uploaded[1][0][\\\"body\\\"][\\\"temperature\\\"] == 0.2\" }, { \"sha\": \"49bb8c2fb4d60c97fe502be6d5e55cb6f42b674d\", \"filename\": \"tests/test_security_hardening.py\", \"status\": \"modified\", \"additions\": 50, \"deletions\": 2, \"changes\": 52, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_security_hardening.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_security_hardening.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_security_hardening.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,7 +1,6 @@\\n from __future__ import annotations\\n \\n import json\\n-import os\\n import socket\\n import threading\\n import urllib.error\\n@@ -58,6 +57,19 @@ def post_json(url: str, payload: dict[str, object], token: str | None = None) ->\\n return exc.code, json.loads(exc.read().decode(\\\"utf-8\\\"))\\n \\n \\n+def get_status(url: str, token: str) -> int:\\n+ request = urllib.request.Request(\\n+ url,\\n+ headers={\\\"authorization\\\": f\\\"Bearer {token}\\\", \\\"connection\\\": \\\"close\\\"},\\n+ method=\\\"GET\\\",\\n+ )\\n+ try:\\n+ with urllib.request.urlopen(request, timeout=5) as response:\\n+ return response.status\\n+ except urllib.error.HTTPError as exc:\\n+ return exc.code\\n+\\n+\\n def test_http_api_requires_bearer_token_and_hides_trace_by_default() -> None:\\n server = build_server(build(), port=0, security=SecurityConfig(auth_token=\\\"secret_token\\\"))\\n thread = threading.Thread(target=server.serve_forever, daemon=True)\\n@@ -88,7 +100,11 @@ def test_admin_and_inference_tokens_are_separate() -> None:\\n server = build_server(\\n build(),\\n port=0,\\n- security=SecurityConfig(auth_token=\\\"\\\", admin_token=\\\"admin_secret\\\", inference_token=\\\"inference_secret\\\"),\\n+ security=SecurityConfig(\\n+ auth_token=\\\"\\\",\\n+ admin_token=\\\"admin_secret\\\", # noqa: S106\\n+ inference_token=\\\"inference_secret\\\", # noqa: S106\\n+ ),\\n )\\n thread = threading.Thread(target=server.serve_forever, daemon=True)\\n thread.start()\\n@@ -116,6 +132,38 @@ def test_admin_and_inference_tokens_are_separate() -> None:\\n assert \\\"trace\\\" not in inference_body[\\\"orchestration\\\"]\\n \\n \\n+def test_inference_token_cannot_read_admin_get_surfaces() -> None:\\n+ server = build_server(\\n+ build(),\\n+ port=0,\\n+ security=SecurityConfig(auth_token=\\\"\\\", admin_token=\\\"admin_secret\\\", inference_token=\\\"inference_secret\\\"),\\n+ )\\n+ thread = threading.Thread(target=server.serve_forever, daemon=True)\\n+ thread.start()\\n+ port = server.server_address[1]\\n+ admin_paths = (\\n+ \\\"/api/v1/workflow_runs\\\",\\n+ \\\"/api/v1/evaluation_runs/missing\\\",\\n+ \\\"/api/v1/access_reports/missing\\\",\\n+ )\\n+\\n+ try:\\n+ inference_statuses = [\\n+ get_status(f\\\"http://127.0.0.1:{port}{path}\\\", \\\"inference_secret\\\")\\n+ for path in admin_paths\\n+ ]\\n+ admin_statuses = [\\n+ get_status(f\\\"http://127.0.0.1:{port}{path}\\\", \\\"admin_secret\\\")\\n+ for path in admin_paths\\n+ ]\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+ assert inference_statuses == [401, 401, 401]\\n+ assert admin_statuses == [200, 404, 404]\\n+\\n+\\n def test_single_and_split_token_modes_cannot_be_combined() -> None:\\n try:\\n SecurityConfig(auth_token=\\\"shared_secret\\\", admin_token=\\\"admin_secret\\\", inference_token=\\\"inference_secret\\\")\" }, { \"sha\": \"47ab4c8384fbac987f6717e98a89c8c33dadcfc4\", \"filename\": \"tests/test_spend_analytics.py\", \"status\": \"modified\", \"additions\": 49, \"deletions\": 0, \"changes\": 49, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_spend_analytics.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_spend_analytics.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_spend_analytics.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -7,6 +7,7 @@\\n \\n from __future__ import annotations\\n \\n+from concurrent.futures import ThreadPoolExecutor\\n import json\\n from pathlib import Path\\n import sys\\n@@ -79,6 +80,54 @@ def test_spend_empty_when_no_runs() -> None:\\n assert report[\\\"totals\\\"][\\\"estimated_output_tokens\\\"] == 0\\n \\n \\n+def test_raw_run_retention_is_bounded_without_resetting_spend() -> None:\\n+ orchestrator = TaskOrchestrator(\\n+ [ModelAgent(\\\"general_agent\\\", \\\"priced-model\\\")],\\n+ price_per_million={\\\"priced-model\\\": 10.0},\\n+ )\\n+ run_count = orchestrator._run_order.maxlen * 2\\n+\\n+ def persist(index: int) -> None:\\n+ orchestrator._persist_workflow_run(\\n+ {\\n+ \\\"workflow_run_id\\\": f\\\"run_retention_{index}\\\",\\n+ \\\"created_at\\\": index,\\n+ \\\"mode\\\": \\\"route\\\",\\n+ \\\"policy_mode\\\": \\\"route\\\",\\n+ \\\"prompt_text\\\": \\\"abcd\\\",\\n+ \\\"answer\\\": \\\"done\\\",\\n+ \\\"trace\\\": [\\n+ {\\n+ \\\"id\\\": 0,\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"general_agent\\\",\\n+ \\\"subtask\\\": \\\"Direct route\\\",\\n+ \\\"access\\\": [],\\n+ \\\"output\\\": \\\"done\\\",\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 1, \\\"completion_tokens\\\": 1},\\n+ }\\n+ ],\\n+ \\\"policy_snapshot\\\": {},\\n+ \\\"verification\\\": {\\\"accepted\\\": True},\\n+ }\\n+ )\\n+\\n+ with ThreadPoolExecutor(max_workers=8) as pool:\\n+ list(pool.map(persist, range(run_count)))\\n+\\n+ retained = orchestrator._workflow_runs[next(iter(orchestrator._workflow_runs))]\\n+ orchestrator._persist_workflow_run(retained)\\n+\\n+ report = orchestrator.spend_analytics()\\n+ assert len(orchestrator._workflow_runs) == orchestrator._run_order.maxlen\\n+ assert len(orchestrator._run_order) == orchestrator._run_order.maxlen\\n+ assert len(set(orchestrator._run_order)) == orchestrator._run_order.maxlen\\n+ assert report[\\\"totals\\\"][\\\"run_count\\\"] == run_count\\n+ assert report[\\\"totals\\\"][\\\"estimated_output_tokens\\\"] == run_count\\n+ assert report[\\\"totals\\\"][\\\"reported_prompt_tokens\\\"] == run_count\\n+ assert report[\\\"by_model\\\"][0][\\\"step_count\\\"] == run_count\\n+\\n+\\n def test_http_spend_endpoint_returns_report() -> None:\\n token = \\\"spend_token\\\"\\n orchestrator = TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"priced-model\\\", tags=(\\\"reasoning\\\",))])\" }, { \"sha\": \"a710ca52c59acf1e2db94b9f9bfa8d19b663f036\", \"filename\": \"tests/test_telemetry.py\", \"status\": \"added\", \"additions\": 285, \"deletions\": 0, \"changes\": 285, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_telemetry.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_telemetry.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_telemetry.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,285 @@\\n+\\\"\\\"\\\"Tests for request session binding and prompt-safe telemetry.\\\"\\\"\\\"\\n+\\n+from contextlib import contextmanager\\n+from http.server import BaseHTTPRequestHandler\\n+from types import SimpleNamespace\\n+from unittest.mock import MagicMock\\n+\\n+import contextual_orchestrator.orchestrator as orchestrator_module\\n+import contextual_orchestrator.telemetry as telemetry_module\\n+from contextual_orchestrator.__main__ import _bootstrap_telemetry_config\\n+from contextual_orchestrator.orchestrator import ModelAgent, ModelClient\\n+from contextual_orchestrator.server import build_server\\n+from contextual_orchestrator.telemetry import (\\n+ _otlp_trace_endpoint,\\n+ current_session_id,\\n+ reset_session_id,\\n+ session_id_from_headers,\\n+ session_id_from_metadata,\\n+ set_session_id,\\n+ traced,\\n+)\\n+\\n+\\n+def test_session_id_accepts_lineageweave_header_and_metadata():\\n+ \\\"\\\"\\\"The two compatible transport forms identify the same processing session.\\\"\\\"\\\"\\n+ assert (\\n+ session_id_from_headers({\\\"x-lineageweave-session-id\\\": \\\"session-1\\\"})\\n+ == \\\"session-1\\\"\\n+ )\\n+ assert (\\n+ session_id_from_metadata({\\\"lineageweave_post_session_id\\\": \\\"session-1\\\"})\\n+ == \\\"session-1\\\"\\n+ )\\n+\\n+\\n+def test_session_and_attribute_boundaries_reject_unsafe_values():\\n+ \\\"\\\"\\\"Correlation and span attributes stay bounded, scalar, and prompt-free.\\\"\\\"\\\"\\n+ assert telemetry_module._config_value(None, \\\"missing\\\", \\\"fallback\\\") == \\\"fallback\\\"\\n+ assert telemetry_module._normalize_session_id(None) is None\\n+ for value in (\\\"\\\", \\\"x\\\" * 129, \\\"line\\\\nbreak\\\"):\\n+ assert telemetry_module._normalize_session_id(value) is None\\n+ assert session_id_from_metadata(None) is None\\n+\\n+ token = set_session_id(\\\"session-safe\\\")\\n+ try:\\n+ assert telemetry_module._safe_attributes(\\n+ {\\n+ \\\"\\\": \\\"empty-key\\\",\\n+ \\\"nested\\\": {\\\"prompt\\\": \\\"excluded\\\"},\\n+ \\\"long\\\": \\\"x\\\" * 300,\\n+ \\\"enabled\\\": True,\\n+ \\\"attempt\\\": 2,\\n+ \\\"ratio\\\": 0.5,\\n+ \\\"object\\\": object(),\\n+ }\\n+ ) == {\\n+ \\\"long\\\": \\\"x\\\" * 256,\\n+ \\\"enabled\\\": True,\\n+ \\\"attempt\\\": 2,\\n+ \\\"ratio\\\": 0.5,\\n+ \\\"contextual_orchestrator.session_id\\\": \\\"session-safe\\\",\\n+ }\\n+ finally:\\n+ reset_session_id(token)\\n+\\n+\\n+def test_session_binding_is_reset():\\n+ \\\"\\\"\\\"A request cannot leak its session into a later request context.\\\"\\\"\\\"\\n+ token = set_session_id(\\\"session-2\\\")\\n+ try:\\n+ assert current_session_id() == \\\"session-2\\\"\\n+ finally:\\n+ reset_session_id(token)\\n+ assert current_session_id() is None\\n+\\n+\\n+def test_local_batch_workers_inherit_session_id(monkeypatch):\\n+ \\\"\\\"\\\"Concurrent local batch provider spans retain the caller's session.\\\"\\\"\\\"\\n+ client = ModelClient(local_concurrency=2)\\n+ agent = ModelAgent(\\n+ \\\"local_agent\\\",\\n+ \\\"local-model\\\",\\n+ base_url=\\\"local://127.0.0.1:8080/v1\\\",\\n+ local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n+ )\\n+ observed: list[str | None] = []\\n+\\n+ def fake_chat(_agent, _messages, temperature=None):\\n+ del temperature\\n+ observed.append(current_session_id())\\n+ return \\\"ok\\\"\\n+\\n+ monkeypatch.setattr(client, \\\"chat\\\", fake_chat)\\n+ token = set_session_id(\\\"post-session\\\")\\n+ try:\\n+ result = client._local_batch_chat(\\n+ agent,\\n+ {\\\"one\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"1\\\"}], \\\"two\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"2\\\"}]},\\n+ None,\\n+ )\\n+ finally:\\n+ reset_session_id(token)\\n+\\n+ assert set(observed) == {\\\"post-session\\\"}\\n+ assert {key: value[\\\"content\\\"] for key, value in result.items()} == {\\\"one\\\": \\\"ok\\\", \\\"two\\\": \\\"ok\\\"}\\n+\\n+\\n+def test_traced_preserves_provider_error():\\n+ \\\"\\\"\\\"Tracing records failure but never changes the provider contract.\\\"\\\"\\\"\\n+ try:\\n+ with traced(\\\"contextual_orchestrator.test.failure\\\"):\\n+ raise RuntimeError(\\\"provider failure\\\")\\n+ except RuntimeError as exc:\\n+ assert str(exc) == \\\"provider failure\\\"\\n+ else: # pragma: no cover\\n+ raise AssertionError(\\\"traced must preserve operation failures\\\")\\n+\\n+\\n+def test_otlp_base_endpoint_gets_trace_signal_path():\\n+ \\\"\\\"\\\"Explicit OTLP endpoints use the HTTP traces signal path exactly once.\\\"\\\"\\\"\\n+ assert _otlp_trace_endpoint(\\\"http://collector:4318\\\") == \\\"http://collector:4318/v1/traces\\\"\\n+ assert _otlp_trace_endpoint(\\\"http://collector:4318/v1/traces/\\\") == \\\"http://collector:4318/v1/traces\\\"\\n+\\n+\\n+def test_telemetry_settings_enter_the_process_kv_at_bootstrap(monkeypatch):\\n+ \\\"\\\"\\\"Runtime telemetry reads a KV populated by the deployment bootstrap.\\\"\\\"\\\"\\n+ monkeypatch.setenv(\\\"OTEL_EXPORTER_OTLP_ENDPOINT\\\", \\\"http://collector:4318\\\")\\n+ monkeypatch.setenv(\\\"OTEL_SERVICE_NAME\\\", \\\"gateway-test\\\")\\n+ config = _bootstrap_telemetry_config()\\n+ assert config.get(\\\"telemetry\\\", \\\"exporter_otlp_endpoint\\\") == \\\"http://collector:4318\\\"\\n+ assert config.get(\\\"telemetry\\\", \\\"service_name\\\") == \\\"gateway-test\\\"\\n+\\n+\\n+def test_configure_telemetry_handles_missing_and_disabled_kv(monkeypatch):\\n+ \\\"\\\"\\\"Absent configuration is retryable while an explicit disable is final.\\\"\\\"\\\"\\n+ monkeypatch.setattr(telemetry_module, \\\"_CONFIGURED\\\", False)\\n+ telemetry_module.configure_telemetry(config=None)\\n+ assert telemetry_module._CONFIGURED is False\\n+\\n+ disabled = SimpleNamespace(\\n+ get=lambda category, key, default=None: \\\"true\\\" if key == \\\"sdk_disabled\\\" else default\\n+ )\\n+ telemetry_module.configure_telemetry(config=disabled)\\n+ assert telemetry_module._CONFIGURED is True\\n+\\n+\\n+def test_configure_telemetry_wires_kv_values_to_otlp(monkeypatch):\\n+ \\\"\\\"\\\"The exporter receives only normalized values read from the injected KV.\\\"\\\"\\\"\\n+ import opentelemetry.exporter.otlp.proto.http.trace_exporter as exporter_module\\n+ import opentelemetry.sdk.resources as resources_module\\n+ import opentelemetry.sdk.trace as trace_sdk_module\\n+ import opentelemetry.sdk.trace.export as trace_export_module\\n+\\n+ exporter = MagicMock()\\n+ processor = MagicMock()\\n+ resource = MagicMock()\\n+ resource.create.return_value = {\\n+ \\\"service.name\\\": \\\"gateway-fallback\\\",\\n+ \\\"service.namespace\\\": \\\"contextualwisdomlab\\\",\\n+ }\\n+ provider_factory = MagicMock()\\n+ fake_trace = MagicMock()\\n+ monkeypatch.setattr(exporter_module, \\\"OTLPSpanExporter\\\", exporter)\\n+ monkeypatch.setattr(resources_module, \\\"Resource\\\", resource)\\n+ monkeypatch.setattr(trace_sdk_module, \\\"TracerProvider\\\", provider_factory)\\n+ monkeypatch.setattr(trace_export_module, \\\"BatchSpanProcessor\\\", processor)\\n+ monkeypatch.setattr(telemetry_module, \\\"trace\\\", fake_trace)\\n+ monkeypatch.setattr(telemetry_module, \\\"_CONFIGURED\\\", False)\\n+ values = {\\n+ \\\"exporter_otlp_endpoint\\\": \\\"http://collector:4318/\\\",\\n+ \\\"service_name\\\": \\\" \\\",\\n+ }\\n+ config = SimpleNamespace(\\n+ get=lambda category, key, default=None: values.get(key, default)\\n+ )\\n+\\n+ telemetry_module.configure_telemetry(\\\"gateway-fallback\\\", config=config)\\n+ expected_resource = {\\n+ \\\"service.name\\\": \\\"gateway-fallback\\\",\\n+ \\\"service.namespace\\\": \\\"contextualwisdomlab\\\",\\n+ }\\n+ exporter.assert_called_once_with(endpoint=\\\"http://collector:4318/v1/traces\\\")\\n+ resource.create.assert_called_once_with(expected_resource)\\n+ processor.assert_called_once_with(exporter.return_value)\\n+ provider_factory.assert_called_once_with(resource=expected_resource)\\n+ provider_factory.return_value.add_span_processor.assert_called_once_with(\\n+ processor.return_value\\n+ )\\n+ fake_trace.set_tracer_provider.assert_called_once_with(provider_factory.return_value)\\n+ telemetry_module.configure_telemetry(config=config)\\n+ exporter.assert_called_once()\\n+\\n+\\n+def test_handler_resets_session_after_each_keep_alive_request(monkeypatch):\\n+ \\\"\\\"\\\"A later request on the same connection cannot inherit the prior session.\\\"\\\"\\\"\\n+ server = build_server(SimpleNamespace(agents=[], candidates=[]), port=0)\\n+ handler = server.RequestHandlerClass.__new__(server.RequestHandlerClass)\\n+ monkeypatch.setattr(BaseHTTPRequestHandler, \\\"handle_one_request\\\", lambda self: None)\\n+ try:\\n+ handler._bind_session(\\\"first-request\\\")\\n+ assert current_session_id() == \\\"first-request\\\"\\n+ handler.handle_one_request()\\n+ assert current_session_id() is None\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_provider_calls_use_current_genai_semantic_convention(monkeypatch):\\n+ \\\"\\\"\\\"Provider spans expose the required, prompt-free GenAI attributes.\\\"\\\"\\\"\\n+ captured = []\\n+\\n+ @contextmanager\\n+ def capture(name, attributes):\\n+ captured.append({\\\"name\\\": name, \\\"attributes\\\": attributes})\\n+ yield None\\n+\\n+ client = ModelClient()\\n+ agent = ModelAgent(\\n+ \\\"provider_agent\\\",\\n+ \\\"model-x\\\",\\n+ base_url=\\\"https://provider.example/v1\\\",\\n+ credential_key=\\\"\\\",\\n+ provider_name=\\\"openai\\\",\\n+ )\\n+ monkeypatch.setattr(orchestrator_module, \\\"traced\\\", capture)\\n+ monkeypatch.setattr(client, \\\"_validate_provider\\\", lambda unused_agent: None)\\n+ monkeypatch.setattr(\\n+ client,\\n+ \\\"_send_with_retry\\\",\\n+ lambda unused_agent, unused_payload, unused_destination: \\\"ok\\\",\\n+ )\\n+ monkeypatch.setattr(\\n+ client,\\n+ \\\"_send_embeddings_with_retry\\\",\\n+ lambda unused_agent, unused_payload, unused_destination: [[1.0]],\\n+ )\\n+\\n+ assert client.chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"not telemetry\\\"}]) == \\\"ok\\\"\\n+ assert client.embed_many(agent, [\\\"not telemetry\\\"]) == [[1.0]]\\n+ assert captured == [\\n+ {\\n+ \\\"name\\\": \\\"chat model-x\\\",\\n+ \\\"attributes\\\": {\\n+ \\\"gen_ai.operation.name\\\": \\\"chat\\\",\\n+ \\\"gen_ai.provider.name\\\": \\\"openai\\\",\\n+ \\\"gen_ai.request.model\\\": \\\"model-x\\\",\\n+ \\\"contextual_orchestrator.agent_id\\\": \\\"provider_agent\\\",\\n+ \\\"server.address\\\": \\\"provider.example\\\",\\n+ \\\"server.port\\\": 443,\\n+ },\\n+ },\\n+ {\\n+ \\\"name\\\": \\\"embeddings model-x\\\",\\n+ \\\"attributes\\\": {\\n+ \\\"gen_ai.operation.name\\\": \\\"embeddings\\\",\\n+ \\\"gen_ai.provider.name\\\": \\\"openai\\\",\\n+ \\\"gen_ai.request.model\\\": \\\"model-x\\\",\\n+ \\\"contextual_orchestrator.agent_id\\\": \\\"provider_agent\\\",\\n+ \\\"server.address\\\": \\\"provider.example\\\",\\n+ \\\"server.port\\\": 443,\\n+ },\\n+ },\\n+ ]\\n+\\n+\\n+def test_traced_starts_client_span_with_attributes_and_error_type(monkeypatch):\\n+ \\\"\\\"\\\"Sampling attributes exist at span creation and failures stay classifiable.\\\"\\\"\\\"\\n+ tracer = MagicMock()\\n+ span = tracer.start_as_current_span.return_value.__enter__.return_value\\n+ monkeypatch.setattr(telemetry_module.trace, \\\"get_tracer\\\", lambda unused_name: tracer)\\n+\\n+ try:\\n+ with traced(\\\"chat model-x\\\", {\\\"gen_ai.operation.name\\\": \\\"chat\\\"}):\\n+ error = TimeoutError(\\\"provider timeout\\\")\\n+ raise error\\n+ except TimeoutError as caught:\\n+ assert caught is error\\n+\\n+ tracer.start_as_current_span.assert_called_once_with(\\n+ \\\"chat model-x\\\",\\n+ kind=telemetry_module.SpanKind.CLIENT,\\n+ attributes={\\\"gen_ai.operation.name\\\": \\\"chat\\\"},\\n+ )\\n+ span.record_exception.assert_called_once_with(error)\\n+ span.set_attribute.assert_called_once_with(\\\"error.type\\\", \\\"TimeoutError\\\")\" }, { \"sha\": \"9e9d1f83af0c1ce6e2d5cd811ecb503405e3e5a5\", \"filename\": \"tests/test_temperature_capability_negotiation_honesty.py\", \"status\": \"added\", \"additions\": 147, \"deletions\": 0, \"changes\": 147, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_temperature_capability_negotiation_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_temperature_capability_negotiation_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_temperature_capability_negotiation_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,147 @@\\n+\\\"\\\"\\\"Regressions for bounded, evidence-preserving temperature negotiation.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import http.client\\n+import io\\n+import json\\n+import socket\\n+import urllib.error\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import ModelAgent\\n+from contextual_orchestrator.orchestrator import (\\n+ ModelClient,\\n+ _temperature_capability_rejection,\\n+)\\n+\\n+\\n+def _http_error(status: int, message: str) -> urllib.error.HTTPError:\\n+ body = json.dumps({\\\"error\\\": {\\\"message\\\": message}}).encode(\\\"utf-8\\\")\\n+ return urllib.error.HTTPError(\\n+ \\\"https://provider.example/v1/chat/completions\\\",\\n+ status,\\n+ \\\"provider error\\\",\\n+ {},\\n+ io.BytesIO(body),\\n+ )\\n+\\n+\\n+def test_invalid_temperature_range_is_not_negotiated_as_missing_capability() -> None:\\n+ \\\"\\\"\\\"A bad caller value must remain a 4xx instead of being silently removed.\\\"\\\"\\\"\\n+ error = _http_error(400, \\\"temperature is not allowed to be greater than 1\\\")\\n+\\n+ assert not _temperature_capability_rejection(error)\\n+\\n+\\n+def test_azure_default_only_temperature_is_negotiated() -> None:\\n+ \\\"\\\"\\\"Azure's default-only diagnostic proves the optional field is unsupported.\\\"\\\"\\\"\\n+ error = _http_error(\\n+ 400,\\n+ \\\"AzureException BadRequestError - Unsupported value: 'temperature' does not \\\"\\n+ \\\"support 0.2 with this model. Only the default (1) value is supported.\\\",\\n+ )\\n+\\n+ assert _temperature_capability_rejection(error)\\n+\\n+\\n+def test_non_negotiated_error_body_remains_available_to_the_caller() -> None:\\n+ \\\"\\\"\\\"Capability inspection must not consume evidence from an unrelated 4xx.\\\"\\\"\\\"\\n+ expected = json.dumps(\\n+ {\\\"error\\\": {\\\"message\\\": \\\"invalid temperature value\\\"}}\\n+ ).encode(\\\"utf-8\\\")\\n+ error = urllib.error.HTTPError(\\n+ \\\"https://provider.example/v1/chat/completions\\\",\\n+ 400,\\n+ \\\"provider error\\\",\\n+ {},\\n+ io.BytesIO(expected),\\n+ )\\n+\\n+ assert not _temperature_capability_rejection(error)\\n+ assert error.read() == expected\\n+\\n+\\n+def test_incomplete_error_body_preserves_partial_capability_evidence() -> None:\\n+ \\\"\\\"\\\"A truncated diagnostic must not replace the provider's original HTTP error.\\\"\\\"\\\"\\n+ partial = b\\\"Unsupported value: temperature does not support 0.2 with this model\\\"\\n+\\n+ class _IncompleteBody:\\n+ def read(self) -> bytes:\\n+ raise http.client.IncompleteRead(partial)\\n+\\n+ def close(self) -> None:\\n+ pass\\n+\\n+ error = urllib.error.HTTPError(\\n+ \\\"https://provider.example/v1/chat/completions\\\",\\n+ 400,\\n+ \\\"provider error\\\",\\n+ {},\\n+ _IncompleteBody(),\\n+ )\\n+\\n+ assert _temperature_capability_rejection(error)\\n+ assert error.read() == partial\\n+\\n+\\n+class _Response:\\n+ \\\"\\\"\\\"Minimal context-managed JSON response for transport tests.\\\"\\\"\\\"\\n+\\n+ def __init__(self, payload: dict[str, object]) -> None:\\n+ self._body = json.dumps(payload).encode(\\\"utf-8\\\")\\n+\\n+ def __enter__(self) -> \\\"_Response\\\":\\n+ return self\\n+\\n+ def __exit__(self, *_args: object) -> bool:\\n+ return False\\n+\\n+ def read(self) -> bytes:\\n+ return self._body\\n+\\n+\\n+def test_transient_retry_after_negotiation_keeps_temperature_omitted(monkeypatch) -> None:\\n+ \\\"\\\"\\\"Once unsupported is proven, later transient retries must use the negotiated payload.\\\"\\\"\\\"\\n+ client = ModelClient(max_retries=1, retry_backoff=0.0)\\n+ agent = ModelAgent(\\n+ \\\"provider_agent\\\",\\n+ \\\"restricted-model\\\",\\n+ base_url=\\\"https://provider.example/v1\\\",\\n+ )\\n+ sent_payloads: list[dict[str, object]] = []\\n+\\n+ def open_provider(request, _destination=None, **_kwargs):\\n+ sent_payloads.append(json.loads(request.data.decode(\\\"utf-8\\\")))\\n+ if len(sent_payloads) == 1:\\n+ raise _http_error(422, \\\"temperature is not supported for this deployment\\\")\\n+ if len(sent_payloads) == 2:\\n+ raise _http_error(503, \\\"temporarily unavailable\\\")\\n+ return _Response(\\n+ {\\n+ \\\"choices\\\": [\\n+ {\\\"message\\\": {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"recovered\\\"}}\\n+ ]\\n+ }\\n+ )\\n+\\n+ monkeypatch.setattr(client, \\\"_open_provider\\\", open_provider)\\n+ monkeypatch.setattr(client, \\\"_sleep\\\", lambda _delay: None)\\n+\\n+ result = client._send_with_retry(\\n+ agent,\\n+ {\\\"model\\\": agent.model, \\\"messages\\\": [], \\\"temperature\\\": 0.2},\\n+ (socket.AF_INET, (\\\"93.184.216.34\\\", 443)),\\n+ )\\n+\\n+ assert result == \\\"recovered\\\"\\n+ assert [\\\"temperature\\\" in payload for payload in sent_payloads] == [\\n+ True,\\n+ False,\\n+ False,\\n+ ]\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"6749ab60e244ad2318dcd5eb92e4a286490344cd\", \"filename\": \"tests/test_tip_reland_sdk_omit_persist_http_honesty.py\", \"status\": \"modified\", \"additions\": 12, \"deletions\": 16, \"changes\": 28, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tip_reland_sdk_omit_persist_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tip_reland_sdk_omit_persist_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_tip_reland_sdk_omit_persist_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -122,7 +122,7 @@ def test_validate_openai_metadata_pops_when_only_null_values() -> None:\\n assert \\\"metadata\\\" not in body\\n \\n \\n-def test_http_chat_tools_persist_null_arguments_as_empty_string() -> None:\\n+def test_http_chat_rejects_tools_with_null_arguments() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -155,10 +155,8 @@ def test_http_chat_tools_persist_null_arguments_as_empty_string() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n- messages = (body.get(\\\"echo\\\") or {}).get(\\\"messages\\\") or []\\n- function = messages[0][\\\"tool_calls\\\"][0][\\\"function\\\"]\\n- assert function.get(\\\"arguments\\\") == \\\"\\\"\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -204,7 +202,7 @@ def test_http_responses_echoes_nonempty_instructions() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_tools_omits_null_metadata_value_from_echo() -> None:\\n+def test_http_chat_rejects_tools_with_null_metadata() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -225,9 +223,8 @@ def test_http_chat_tools_omits_null_metadata_value_from_echo() -> None:\\n \\\"metadata\\\": {\\\"keep\\\": \\\"v\\\", \\\"drop\\\": None},\\n },\\n )\\n- assert status == 200, body\\n- echo_meta = (body.get(\\\"echo\\\") or {}).get(\\\"metadata\\\")\\n- assert echo_meta == {\\\"keep\\\": \\\"v\\\"}, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -281,7 +278,7 @@ def test_http_chat_tools_rejects_nonzero_top_logprobs() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_tools_omits_whitespace_top_logprobs_from_echo() -> None:\\n+def test_http_chat_rejects_tools_with_blank_top_logprobs() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -302,9 +299,8 @@ def test_http_chat_tools_omits_whitespace_top_logprobs_from_echo() -> None:\\n \\\"top_logprobs\\\": \\\" \\\",\\n },\\n )\\n- assert status == 200, body\\n- echo = body.get(\\\"echo\\\") or {}\\n- assert \\\"top_logprobs\\\" not in echo, echo\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -316,11 +312,11 @@ def test_http_chat_tools_omits_whitespace_top_logprobs_from_echo() -> None:\\n test_validate_responses_instructions_keeps_nonempty()\\n test_validate_openai_metadata_writes_back_without_null_values()\\n test_validate_openai_metadata_pops_when_only_null_values()\\n- test_http_chat_tools_persist_null_arguments_as_empty_string()\\n+ test_http_chat_rejects_tools_with_null_arguments()\\n test_http_responses_omits_blank_instructions_from_echo()\\n test_http_responses_echoes_nonempty_instructions()\\n- test_http_chat_tools_omits_null_metadata_value_from_echo()\\n+ test_http_chat_rejects_tools_with_null_metadata()\\n test_http_responses_omits_null_metadata_value_from_echo()\\n test_http_chat_tools_rejects_nonzero_top_logprobs()\\n- test_http_chat_tools_omits_whitespace_top_logprobs_from_echo()\\n+ test_http_chat_rejects_tools_with_blank_top_logprobs()\\n print(\\\"ok\\\")\" }, { \"sha\": \"bc42fa995356ca7a7b23c12d5df83ac036efb1b7\", \"filename\": \"tests/test_token_id_whole_float_coerce_http_honesty.py\", \"status\": \"modified\", \"additions\": 5, \"deletions\": 5, \"changes\": 10, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_token_id_whole_float_coerce_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_token_id_whole_float_coerce_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_token_id_whole_float_coerce_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -8,16 +8,16 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n-from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n-from contextual_orchestrator.server import ( # noqa: E402\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+from contextual_orchestrator.server import (\\n SecurityConfig,\\n _coerce_embedding_token_sequence,\\n _coerce_token_id,\\n@@ -27,12 +27,12 @@\\n build_server,\\n )\\n \\n-_TEST_AUTH_TOKEN = \\\"token_id_whole_float_coerce_http_honesty_token\\\" # noqa: S105\\n+_TEST_AUTH_TOKEN = \\\"token_id_whole_float_coerce_http_honesty_token\\\"\\n \\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"embedding\\\"))]\\n )\\n \\n \" }, { \"sha\": \"1615b3b52628e2f732c0281716247029e3f21f54\", \"filename\": \"tests/test_tool_call_id_name_strip_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 8, \"changes\": 12, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_call_id_name_strip_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_call_id_name_strip_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_tool_call_id_name_strip_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -96,7 +96,7 @@ def test_http_chat_accepts_padded_tool_calls_id_and_function_name() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_accepts_padded_tools_and_tool_choice_names() -> None:\\n+def test_http_chat_rejects_padded_tools_and_tool_choice_names() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -120,12 +120,8 @@ def test_http_chat_accepts_padded_tools_and_tool_choice_names() -> None:\\n },\\n },\\n )\\n- assert status == 200, body\\n- echo = body.get(\\\"echo\\\") or {}\\n- tools = echo.get(\\\"tools\\\") or []\\n- if tools:\\n- name = tools[0].get(\\\"function\\\", {}).get(\\\"name\\\")\\n- assert name == \\\"lookup_item\\\", body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -234,7 +230,7 @@ def test_http_chat_still_rejects_illegal_name_after_strip() -> None:\\n \\n if __name__ == \\\"__main__\\\":\\n test_http_chat_accepts_padded_tool_calls_id_and_function_name()\\n- test_http_chat_accepts_padded_tools_and_tool_choice_names()\\n+ test_http_chat_rejects_padded_tools_and_tool_choice_names()\\n test_http_chat_accepts_padded_message_name()\\n test_http_chat_accepts_padded_json_schema_name()\\n test_http_chat_still_rejects_whitespace_only_tool_call_id()\" }, { \"sha\": \"212874db250c2af7a4c6046be9367ab29baf0de7\", \"filename\": \"tests/test_tool_choice_flat_name_http_honesty.py\", \"status\": \"modified\", \"additions\": 16, \"deletions\": 12, \"changes\": 28, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_choice_flat_name_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_choice_flat_name_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_tool_choice_flat_name_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,7 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_responses_accepts_flat_tool_choice_name() -> None:\\n+def test_http_responses_rejects_flat_tool_choice_name() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -71,13 +71,14 @@ def test_http_responses_accepts_flat_tool_choice_name() -> None:\\n \\\"tool_choice\\\": {\\\"type\\\": \\\"function\\\", \\\"name\\\": \\\"lookup_item\\\"},\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_flat_tool_choice_padded_casefold() -> None:\\n+def test_http_responses_rejects_flat_tool_choice_padded_casefold() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -96,13 +97,14 @@ def test_http_responses_accepts_flat_tool_choice_padded_casefold() -> None:\\n \\\"tool_choice\\\": {\\\"type\\\": \\\" FUNCTION \\\", \\\"name\\\": \\\" lookup_item \\\"},\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_accepts_flat_tool_choice_with_nested_tools() -> None:\\n+def test_http_chat_rejects_flat_tool_choice_with_nested_tools() -> None:\\n \\\"\\\"\\\"Chat clients using Responses-flat tool_choice against nested tools.\\\"\\\"\\\"\\n server, thread, port = _server()\\n try:\\n@@ -124,13 +126,14 @@ def test_http_chat_accepts_flat_tool_choice_with_nested_tools() -> None:\\n \\\"tool_choice\\\": {\\\"type\\\": \\\"function\\\", \\\"name\\\": \\\"lookup_item\\\"},\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_still_accepts_nested_tool_choice() -> None:\\n+def test_http_chat_rejects_nested_tool_choice() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -154,7 +157,8 @@ def test_http_chat_still_accepts_nested_tool_choice() -> None:\\n },\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -217,10 +221,10 @@ def test_http_flat_tool_choice_unknown_name_fails_closed() -> None:\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_responses_accepts_flat_tool_choice_name()\\n- test_http_responses_accepts_flat_tool_choice_padded_casefold()\\n- test_http_chat_accepts_flat_tool_choice_with_nested_tools()\\n- test_http_chat_still_accepts_nested_tool_choice()\\n+ test_http_responses_rejects_flat_tool_choice_name()\\n+ test_http_responses_rejects_flat_tool_choice_padded_casefold()\\n+ test_http_chat_rejects_flat_tool_choice_with_nested_tools()\\n+ test_http_chat_rejects_nested_tool_choice()\\n test_http_tool_choice_rejects_mixed_nested_and_flat()\\n test_http_flat_tool_choice_unknown_name_fails_closed()\\n print(\\\"ok\\\")\" }, { \"sha\": \"68e538d3f6f618f1b491a399501cb00b087870d7\", \"filename\": \"tests/test_tool_choice_function_call_casefold_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 3, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_choice_function_call_casefold_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_choice_function_call_casefold_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_tool_choice_function_call_casefold_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -71,7 +71,7 @@ def test_http_chat_accepts_function_call_none_auto_padded_casefold() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_accepts_tool_choice_required_padded_casefold_with_tools() -> None:\\n+def test_http_chat_rejects_tool_choice_required_padded_casefold_with_tools() -> None:\\n server, thread, port = _server()\\n try:\\n for value in (\\\"required\\\", \\\" REQUIRED \\\", \\\"Required\\\"):\\n@@ -93,7 +93,8 @@ def test_http_chat_accepts_tool_choice_required_padded_casefold_with_tools() ->\\n \\\"tool_choice\\\": value,\\n },\\n )\\n- assert status == 200, (value, body)\\n+ assert status == 422, (value, body)\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -138,7 +139,7 @@ def test_http_completions_accepts_function_call_none_padded_casefold() -> None:\\n \\n if __name__ == \\\"__main__\\\":\\n test_http_chat_accepts_function_call_none_auto_padded_casefold()\\n- test_http_chat_accepts_tool_choice_required_padded_casefold_with_tools()\\n+ test_http_chat_rejects_tool_choice_required_padded_casefold_with_tools()\\n test_http_chat_accepts_tool_choice_none_auto_padded_casefold()\\n test_http_completions_accepts_function_call_none_padded_casefold()\\n print(\\\"ok\\\")\" }, { \"sha\": \"2a2c4578dead9c69f179bca3469c1e38ba11ec18\", \"filename\": \"tests/test_tool_choice_required_requires_tools_http_honesty.py\", \"status\": \"modified\", \"additions\": 8, \"deletions\": 6, \"changes\": 14, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_choice_required_requires_tools_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_choice_required_requires_tools_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_tool_choice_required_requires_tools_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -126,7 +126,7 @@ def test_http_chat_rejects_tool_choice_required_with_empty_tools() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_accepts_tool_choice_required_with_tools() -> None:\\n+def test_http_chat_rejects_tool_choice_required_with_tools() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -139,7 +139,8 @@ def test_http_chat_accepts_tool_choice_required_with_tools() -> None:\\n \\\"tool_choice\\\": \\\"required\\\",\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -166,7 +167,7 @@ def test_http_responses_rejects_tool_choice_required_without_tools() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_tool_choice_required_with_tools() -> None:\\n+def test_http_responses_rejects_tool_choice_required_with_tools() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -179,7 +180,8 @@ def test_http_responses_accepts_tool_choice_required_with_tools() -> None:\\n \\\"tool_choice\\\": \\\"required\\\",\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -191,7 +193,7 @@ def test_http_responses_accepts_tool_choice_required_with_tools() -> None:\\n test_validate_tool_choice_required_with_tools_ok()\\n test_http_chat_rejects_tool_choice_required_without_tools()\\n test_http_chat_rejects_tool_choice_required_with_empty_tools()\\n- test_http_chat_accepts_tool_choice_required_with_tools()\\n+ test_http_chat_rejects_tool_choice_required_with_tools()\\n test_http_responses_rejects_tool_choice_required_without_tools()\\n- test_http_responses_accepts_tool_choice_required_with_tools()\\n+ test_http_responses_rejects_tool_choice_required_with_tools()\\n print(\\\"ok\\\")\" }, { \"sha\": \"43240b2f7610e847acfcc88cc7930929abbb6e99\", \"filename\": \"tests/test_tool_description_length_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 3, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_description_length_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_description_length_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_tool_description_length_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,7 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_chat_accepts_tool_description_at_1024_chars() -> None:\\n+def test_http_chat_rejects_tool_request_with_1024_char_description() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -72,7 +72,8 @@ def test_http_chat_accepts_tool_description_at_1024_chars() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -109,6 +110,6 @@ def test_http_chat_rejects_tool_description_over_1024_chars() -> None:\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_chat_accepts_tool_description_at_1024_chars()\\n+ test_http_chat_rejects_tool_request_with_1024_char_description()\\n test_http_chat_rejects_tool_description_over_1024_chars()\\n print(\\\"ok\\\")\" }, { \"sha\": \"3be60abe361123a9233a2688dd4ddd60fd5ea575\", \"filename\": \"tests/test_tool_description_parameters_null_noop_http_honesty.py\", \"status\": \"modified\", \"additions\": 12, \"deletions\": 9, \"changes\": 21, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_description_parameters_null_noop_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_description_parameters_null_noop_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_tool_description_parameters_null_noop_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,7 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_chat_accepts_tool_description_null() -> None:\\n+def test_http_chat_rejects_tool_request_with_null_description() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -72,13 +72,14 @@ def test_http_chat_accepts_tool_description_null() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_accepts_tool_parameters_null() -> None:\\n+def test_http_chat_rejects_tool_request_with_null_parameters() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -97,13 +98,14 @@ def test_http_chat_accepts_tool_parameters_null() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_accepts_tool_description_and_parameters_null() -> None:\\n+def test_http_chat_rejects_tool_request_with_null_description_and_parameters() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -124,7 +126,8 @@ def test_http_chat_accepts_tool_description_and_parameters_null() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -186,9 +189,9 @@ def test_http_chat_rejects_tool_parameters_non_object() -> None:\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_chat_accepts_tool_description_null()\\n- test_http_chat_accepts_tool_parameters_null()\\n- test_http_chat_accepts_tool_description_and_parameters_null()\\n+ test_http_chat_rejects_tool_request_with_null_description()\\n+ test_http_chat_rejects_tool_request_with_null_parameters()\\n+ test_http_chat_rejects_tool_request_with_null_description_and_parameters()\\n test_http_chat_rejects_tool_description_non_string()\\n test_http_chat_rejects_tool_parameters_non_object()\\n print(\\\"ok\\\")\" }, { \"sha\": \"29d997f269876f77a5fa0208289ec7a91d43ddec\", \"filename\": \"tests/test_tool_function_name_charset_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 4, \"changes\": 8, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_function_name_charset_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_function_name_charset_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_tool_function_name_charset_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -155,8 +155,8 @@ def test_http_responses_keeps_legal_function_name() -> None:\\n \\\"tools\\\": [_function_tool(max_name)],\\n },\\n )\\n- assert status == 200, body\\n- assert _echo_function(body).get(\\\"name\\\") == max_name\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -175,8 +175,8 @@ def test_http_chat_keeps_legal_function_name() -> None:\\n \\\"tools\\\": [_function_tool(legal_name)],\\n },\\n )\\n- assert status == 200, body\\n- assert _echo_function(body).get(\\\"name\\\") == legal_name\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\" }, { \"sha\": \"930bdf8b530affa942eaee2043dec360e01fdc2f\", \"filename\": \"tests/test_tool_function_null_fields_pop_http_honesty.py\", \"status\": \"modified\", \"additions\": 10, \"deletions\": 23, \"changes\": 33, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_function_null_fields_pop_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_function_null_fields_pop_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_tool_function_null_fields_pop_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -125,10 +125,8 @@ def test_http_chat_omits_tool_description_null() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n- function = _echo_function(body)\\n- assert \\\"description\\\" not in function\\n- assert function.get(\\\"parameters\\\") == {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -154,9 +152,8 @@ def test_http_chat_omits_tool_parameters_null() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n- function = _echo_function(body)\\n- assert \\\"parameters\\\" not in function\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -184,12 +181,8 @@ def test_http_chat_omits_tool_description_parameters_and_strict_null() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n- function = _echo_function(body)\\n- assert \\\"description\\\" not in function\\n- assert \\\"parameters\\\" not in function\\n- assert \\\"strict\\\" not in function\\n- assert function.get(\\\"name\\\") == \\\"lookup\\\"\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -217,11 +210,8 @@ def test_http_responses_omits_tool_description_and_parameters_null() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n- function = _echo_function(body)\\n- assert \\\"description\\\" not in function\\n- assert \\\"parameters\\\" not in function\\n- assert \\\"strict\\\" not in function\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -306,11 +296,8 @@ def test_http_chat_keeps_non_null_tool_fields() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n- function = _echo_function(body)\\n- assert function.get(\\\"description\\\") == \\\"find things\\\"\\n- assert function.get(\\\"parameters\\\") == {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}}\\n- assert function.get(\\\"strict\\\") is True\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\" }, { \"sha\": \"fccbd88bd7e443c3dd2af1e933e92752d1a6b5f0\", \"filename\": \"tests/test_tool_strict_null_noop_http_honesty.py\", \"status\": \"modified\", \"additions\": 2, \"deletions\": 1, \"changes\": 3, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_strict_null_noop_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_strict_null_noop_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_tool_strict_null_noop_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -73,7 +73,8 @@ def test_http_chat_accepts_tool_function_strict_null() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\" }, { \"sha\": \"e38cfad52746a42c9b7169daf223be2aa1f2cc02\", \"filename\": \"tests/test_tool_type_role_casefold_http_honesty.py\", \"status\": \"modified\", \"additions\": 6, \"deletions\": 3, \"changes\": 9, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_type_role_casefold_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_tool_type_role_casefold_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_tool_type_role_casefold_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -133,7 +133,8 @@ def test_http_chat_accepts_tool_type_casefold() -> None:\\n ],\\n },\\n )\\n- assert status == 200, (tool_type, body)\\n+ assert status == 422, (tool_type, body)\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -164,7 +165,8 @@ def test_http_chat_accepts_tool_choice_type_casefold() -> None:\\n },\\n },\\n )\\n- assert status == 200, (choice_type, body)\\n+ assert status == 422, (choice_type, body)\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -256,7 +258,8 @@ def test_http_responses_accepts_tool_type_casefold() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\" }, { \"sha\": \"01cb8b4f540b77da804e2a488020ef592e17ec75\", \"filename\": \"tests/test_trace_context.py\", \"status\": \"added\", \"additions\": 63, \"deletions\": 0, \"changes\": 63, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_trace_context.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_trace_context.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_trace_context.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,63 @@\\n+\\\"\\\"\\\"Tests for prompt-safe inbound W3C trace-context handling.\\\"\\\"\\\"\\n+\\n+import contextual_orchestrator.telemetry as telemetry_module\\n+\\n+\\n+def test_attach_trace_context_normalizes_headers_and_detaches(monkeypatch):\\n+ \\\"\\\"\\\"HTTP header casing is normalized and the returned context is releasable.\\\"\\\"\\\"\\n+ captured = {}\\n+ context = object()\\n+ token = object()\\n+\\n+ def fake_extract(carrier):\\n+ captured[\\\"carrier\\\"] = carrier\\n+ return context\\n+\\n+ def fake_attach(value):\\n+ captured[\\\"context\\\"] = value\\n+ return token\\n+\\n+ def fake_detach(value):\\n+ captured[\\\"token\\\"] = value\\n+\\n+ monkeypatch.setattr(telemetry_module, \\\"_otel_extract\\\", fake_extract)\\n+ monkeypatch.setattr(telemetry_module, \\\"_otel_attach\\\", fake_attach)\\n+ monkeypatch.setattr(telemetry_module, \\\"_otel_detach\\\", fake_detach)\\n+\\n+ attached = telemetry_module.attach_trace_context({\\\"Traceparent\\\": \\\"00-trace\\\"})\\n+ telemetry_module.detach_trace_context(attached)\\n+\\n+ assert captured == {\\n+ \\\"carrier\\\": {\\\"traceparent\\\": \\\"00-trace\\\"},\\n+ \\\"context\\\": context,\\n+ \\\"token\\\": token,\\n+ }\\n+\\n+\\n+def test_trace_context_is_a_noop_without_the_optional_api(monkeypatch):\\n+ \\\"\\\"\\\"Missing propagation hooks never change the request contract.\\\"\\\"\\\"\\n+ monkeypatch.setattr(telemetry_module, \\\"_otel_extract\\\", None)\\n+ monkeypatch.setattr(telemetry_module, \\\"_otel_attach\\\", None)\\n+ monkeypatch.setattr(telemetry_module, \\\"_otel_detach\\\", None)\\n+ monkeypatch.setattr(telemetry_module, \\\"_otel_inject\\\", None)\\n+\\n+ assert telemetry_module.attach_trace_context({\\\"traceparent\\\": \\\"ignored\\\"}) is None\\n+ telemetry_module.detach_trace_context(object())\\n+ telemetry_module.inject_trace_context({})\\n+\\n+\\n+def test_inject_trace_context_delegates_to_w3c_propagator(monkeypatch):\\n+ \\\"\\\"\\\"Provider transport headers receive only the active W3C propagation fields.\\\"\\\"\\\"\\n+ carrier = {\\\"content-type\\\": \\\"application/json\\\"}\\n+ monkeypatch.setattr(\\n+ telemetry_module,\\n+ \\\"_otel_inject\\\",\\n+ lambda value: value.__setitem__(\\\"traceparent\\\", \\\"00-trace-span-01\\\"),\\n+ )\\n+\\n+ telemetry_module.inject_trace_context(carrier)\\n+\\n+ assert carrier == {\\n+ \\\"content-type\\\": \\\"application/json\\\",\\n+ \\\"traceparent\\\": \\\"00-trace-span-01\\\",\\n+ }\" }, { \"sha\": \"3a4be57233fae793e7ec77ac59000aa2767a386b\", \"filename\": \"tests/test_weight_strict_bool_coerce_http_honesty.py\", \"status\": \"modified\", \"additions\": 2, \"deletions\": 1, \"changes\": 3, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_weight_strict_bool_coerce_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_weight_strict_bool_coerce_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_weight_strict_bool_coerce_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -112,7 +112,8 @@ def test_http_chat_accepts_tool_strict_bool_coerce_forms() -> None:\\n ],\\n },\\n )\\n- assert status == 200, (val, body)\\n+ assert status == 422, (val, body)\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\" }, { \"sha\": \"b28241732762e35654a4e4edf2a2a610d530879c\", \"filename\": \"tests/test_whole_float_string_int_coerce_http_honesty.py\", \"status\": \"modified\", \"additions\": 6, \"deletions\": 17, \"changes\": 23, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_whole_float_string_int_coerce_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_whole_float_string_int_coerce_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_whole_float_string_int_coerce_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -71,7 +71,7 @@ def test_http_chat_accepts_n_whole_float_string() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_seed_whole_float_string() -> None:\\n+def test_http_responses_rejects_unapplied_seed_whole_float_string() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -83,7 +83,8 @@ def test_http_responses_accepts_seed_whole_float_string() -> None:\\n \\\"seed\\\": \\\"1.0\\\",\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -202,7 +203,7 @@ def test_http_chat_accepts_max_tool_calls_zero_float_string() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_top_logprobs_whole_float_string() -> None:\\n+def test_http_responses_rejects_unapplied_top_logprobs_whole_float_string() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -215,20 +216,8 @@ def test_http_responses_accepts_top_logprobs_whole_float_string() -> None:\\n \\\"top_logprobs\\\": \\\"5.0\\\",\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n-\\n-\\n-if __name__ == \\\"__main__\\\":\\n- test_http_chat_accepts_n_whole_float_string()\\n- test_http_responses_accepts_seed_whole_float_string()\\n- test_http_chat_rejects_fractional_float_string_n()\\n- test_http_chat_accepts_top_logprobs_zero_float_strings()\\n- test_http_chat_still_rejects_nonzero_top_logprobs_float_string()\\n- test_http_completions_accepts_top_logprobs_zero_float_string()\\n- test_http_completions_accepts_best_of_whole_float_string()\\n- test_http_chat_accepts_max_tool_calls_zero_float_string()\\n- test_http_responses_accepts_top_logprobs_whole_float_string()\\n- print(\\\"ok\\\")\" } ]" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-24T07-42-47-401Z.yml b/.playwright-mcp/page-2026-08-24T07-42-47-401Z.yml deleted file mode 100644 index 428f88f3b..000000000 --- a/.playwright-mcp/page-2026-08-24T07-42-47-401Z.yml +++ /dev/null @@ -1 +0,0 @@ -- generic [active] [ref=f2e1]: "[ { \"sha\": \"1b951628a44baee32306a9977aad77aae8eff7e9\", \"filename\": \".github/workflows/provider-catalog-sync.yml\", \"status\": \"added\", \"additions\": 105, \"deletions\": 0, \"changes\": 105, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/.github%2Fworkflows%2Fprovider-catalog-sync.yml\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/.github%2Fworkflows%2Fprovider-catalog-sync.yml\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/.github%2Fworkflows%2Fprovider-catalog-sync.yml?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,105 @@\\n+name: Provider catalog sync\\n+\\n+on:\\n+ workflow_dispatch:\\n+ schedule:\\n+ - cron: \\\"17 * * * *\\\"\\n+\\n+permissions:\\n+ contents: read\\n+\\n+concurrency:\\n+ group: provider-catalog-sync\\n+ cancel-in-progress: false\\n+\\n+jobs:\\n+ sync:\\n+ name: Bootstrap durable provider KV and model catalog\\n+ if: github.ref == 'refs/heads/main'\\n+ runs-on: ubuntu-latest\\n+ environment: production\\n+ timeout-minutes: 15\\n+ steps:\\n+ - name: Checkout protected default branch\\n+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7\\n+ with:\\n+ persist-credentials: false\\n+\\n+ - name: Set up Python\\n+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6\\n+ with:\\n+ python-version: \\\"3.12\\\"\\n+\\n+ - name: Install hash-pinned runtime dependencies\\n+ run: python -m pip install --disable-pip-version-check --no-input --require-hashes -r requirements.lock\\n+\\n+ - name: Register credentials and refresh normalized model catalog\\n+ shell: bash\\n+ env:\\n+ CONTEXTUAL_ORCHESTRATOR_KV_BACKEND: postgres\\n+ CONTEXTUAL_ORCHESTRATOR_KV_DSN: ${{ secrets.CONTEXTUAL_ORCHESTRATOR_KV_DSN }}\\n+ CONTEXTUAL_ORCHESTRATOR_KV_PASSPHRASE: ${{ secrets.CONTEXTUAL_ORCHESTRATOR_KV_PASSPHRASE }}\\n+ NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}\\n+ NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }}\\n+ BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }}\\n+ OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}\\n+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\\n+ run: |\\n+ set -euo pipefail\\n+ test -n \\\"${CONTEXTUAL_ORCHESTRATOR_KV_DSN}\\\"\\n+ test -n \\\"${CONTEXTUAL_ORCHESTRATOR_KV_PASSPHRASE}\\\"\\n+ python -m contextual_orchestrator.provider_catalog_bootstrap --model-limit 24 > provider-bootstrap-report.json\\n+ python - <<'PY'\\n+ import json\\n+ from pathlib import Path\\n+\\n+ report = json.loads(Path('provider-bootstrap-report.json').read_text(encoding='utf-8'))\\n+ expected = {\\n+ 'NVIDIA_NIM_API_KEY',\\n+ 'NVIDIA_NIM_API_KEY_SUB',\\n+ 'BYTEZ_API_KEY',\\n+ 'OPENROUTER_API_KEY',\\n+ 'OPENAI_API_KEY',\\n+ }\\n+ registered = set(report['registered_credentials'])\\n+ if registered != expected:\\n+ raise SystemExit(f'credential inventory mismatch: {sorted(expected - registered)}')\\n+ if report['catalog_backend'] != 'postgres':\\n+ raise SystemExit('provider catalog is not durable PostgreSQL')\\n+ if report['catalog_model_count'] < 1 or report['eligible_model_count'] < 1:\\n+ raise SystemExit('provider catalog has no compatible serving model')\\n+ if not report['selected_agent_ids']:\\n+ raise SystemExit('provider catalog produced no serving candidates')\\n+ if report['enabled_agent_ids'] or report['durable_agent_pool']:\\n+ raise SystemExit('ephemeral Actions sync must not claim agent-pool activation')\\n+ print(json.dumps({\\n+ 'registered_credentials': sorted(registered),\\n+ 'live_discovered_model_count': report['live_discovered_model_count'],\\n+ 'catalog_model_count': report['catalog_model_count'],\\n+ 'last_known_good_model_count': report['last_known_good_model_count'],\\n+ 'selected_agent_count': len(report['selected_agent_ids']),\\n+ 'catalog_refresh_failure_count': report['catalog_refresh_failure_count'],\\n+ 'providers_with_errors': report['providers_with_errors'],\\n+ }, sort_keys=True))\\n+ PY\\n+ python - <<'PY'\\n+ import os\\n+ from pathlib import Path\\n+\\n+ report = Path('provider-bootstrap-report.json').read_text(encoding='utf-8')\\n+ names = (\\n+ 'NVIDIA_NIM_API_KEY',\\n+ 'NVIDIA_NIM_API_KEY_SUB',\\n+ 'BYTEZ_API_KEY',\\n+ 'OPENROUTER_API_KEY',\\n+ 'OPENAI_API_KEY',\\n+ )\\n+ leaked = [\\n+ name\\n+ for name in names\\n+ if os.environ[name].rstrip('\\\\r\\\\n')\\n+ and os.environ[name].rstrip('\\\\r\\\\n') in report\\n+ ]\\n+ if leaked:\\n+ raise SystemExit(f'provider bootstrap report leaked secret values for: {leaked}')\\n+ PY\" }, { \"sha\": \"fb9beb151cb73656da41c7e22cdbfeb93c4bc842\", \"filename\": \"README.md\", \"status\": \"modified\", \"additions\": 10, \"deletions\": 1, \"changes\": 11, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/README.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/README.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/README.md?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -97,7 +97,6 @@ Run an evaluation against that server with `--temperature 0` for repeatable judg\\n Model-based conduct verification requires `fast-mlsirm` in the same runtime and fails closed when it is absent or broken; fast-mlsirm sends its judge completion through this contextual-orchestrator gateway, so no direct provider fallback is used. “Same runtime” means that the exact interpreter used for the live run can import both packages: install both checkouts into one environment (prefer editable installs), or expose both source roots with `PYTHONPATH` during a source run. Before a live judge benchmark, run `python -m contextual_orchestrator check-fast-mlsirm` with that exact interpreter. It prints the interpreter, package version, transitive-import status, and contextual contract check, and exits nonzero on a missing dependency or contract mismatch. Do not run the preflight in one virtual environment and the judge in another. See [ADR 0001](docs/planning/adrs/0001-fail-closed-model-judgment.md).\\n \\n 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.\\n-\\n 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) 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.\\n \\n Seed the credential into the KV once at bootstrap:\\n@@ -304,6 +303,16 @@ python tests/test_admin_contract.py\\n python tests/test_conventions.py\\n python tests/test_api_contract.py\\n python tests/test_security_hardening.py\\n+python tests/test_chat_model_capability_isolation.py\\n+python tests/test_chat_transport_role_separation.py\\n+python tests/test_chat_capability_unknown_identifiers.py\\n+python tests/test_chat_passthrough_capability_isolation.py\\n+python tests/test_discovery_bootstrap_selection.py\\n+python tests/test_provider_bootstrap.py\\n+python tests/test_provider_bootstrap_secret_normalization.py\\n+python tests/test_provider_catalog_bootstrap.py\\n+python tests/test_provider_catalog_credential_promotion.py\\n+python tests/test_provider_catalog_store.py\\n python tests/test_repository_security_metadata.py\\n python tests/test_product_planning_contract.py\\n python tests/test_plugin_driven_artifacts.py\" }, { \"sha\": \"3424629af79165eaa9878e2c0e5d176bd90a151f\", \"filename\": \"contextual_orchestrator/__main__.py\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 3, \"changes\": 6, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2F__main__.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2F__main__.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2F__main__.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -16,7 +16,7 @@\\n agent_id_for,\\n discover_all_models,\\n refresh_price_book,\\n- select_top_n_cheapest_discovered_agents,\\n+ select_bootstrap_discovered_agents,\\n )\\n from .orchestrator import (\\n CONTEXTUAL_ORCHESTRATOR_CONTRACT_V1,\\n@@ -211,7 +211,7 @@ def _discover_models_command(argv: list[str]) -> None:\\n type=_non_negative_int,\\n default=0,\\n metavar=\\\"N\\\",\\n- help=\\\"Enable the N cheapest discovered agents in --agents-db (auto-optimization bootstrap; \\\"\\n+ help=\\\"Enable a price-honest, provider-diverse discovered agent pool in --agents-db (auto-optimization bootstrap; \\\"\\n \\\"requires --agents-db; 0 disables, the default, leaving every discovered agent inert).\\\",\\n )\\n args = parser.parse_args(argv)\\n@@ -229,7 +229,7 @@ def _discover_models_command(argv: list[str]) -> None:\\n )\\n bootstrap.sync_discovered_agents([agent_from_discovered(model) for model in discovered])\\n if args.enable_cheapest:\\n- for model in select_top_n_cheapest_discovered_agents(discovered, price_book, args.enable_cheapest):\\n+ for model in select_bootstrap_discovered_agents(discovered, price_book, args.enable_cheapest):\\n agent_id = agent_id_for(model)\\n bootstrap.patch_agent(\\\"default\\\", agent_id, {\\\"status\\\": \\\"active\\\"})\\n enabled_agent_ids.append(agent_id)\" }, { \"sha\": \"4a2a96cd560621fcc8099010ec759a2f4f75be40\", \"filename\": \"contextual_orchestrator/chat_capability.py\", \"status\": \"added\", \"additions\": 96, \"deletions\": 0, \"changes\": 96, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fchat_capability.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fchat_capability.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fchat_capability.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,96 @@\\n+\\\"\\\"\\\"Classify chat transport compatibility and ordinary agent-role eligibility.\\n+\\n+Provider catalogs mix endpoint-only models with models served through an\\n+OpenAI-compatible chat transport. Transport compatibility is not the same as\\n+fitness for an ordinary thinker, worker, verifier, or synthesizer role: audio\\n+and policy-classification models can use chat transport, while embedding,\\n+reranking, transcription, moderation-endpoint, image-generation, realtime, and\\n+speech-only models cannot.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import re\\n+\\n+_MODEL_TOKEN_RE = re.compile(r\\\"[a-z0-9]+\\\")\\n+_TRANSPORT_INCOMPATIBLE_EXACT_TOKENS = frozenset(\\n+ {\\n+ \\\"bge\\\",\\n+ \\\"clip\\\",\\n+ \\\"dall\\\",\\n+ \\\"e5\\\",\\n+ \\\"embed\\\",\\n+ \\\"embedding\\\",\\n+ \\\"embeddings\\\",\\n+ \\\"gte\\\",\\n+ \\\"image\\\",\\n+ \\\"images\\\",\\n+ \\\"moderation\\\",\\n+ \\\"realtime\\\",\\n+ \\\"rerank\\\",\\n+ \\\"reranker\\\",\\n+ \\\"siglip\\\",\\n+ \\\"sora\\\",\\n+ \\\"speech\\\",\\n+ \\\"transcribe\\\",\\n+ \\\"transcription\\\",\\n+ \\\"tts\\\",\\n+ \\\"whisper\\\",\\n+ }\\n+)\\n+_TRANSPORT_INCOMPATIBLE_PREFIXES = (\\n+ \\\"embed\\\",\\n+ \\\"moderat\\\",\\n+ \\\"rerank\\\",\\n+ \\\"transcrib\\\",\\n+)\\n+\\n+\\n+def is_chat_compatible_model_id(model_id: str) -> bool:\\n+ \\\"\\\"\\\"Return whether an identifier can use the ordinary chat transport.\\n+\\n+ The classifier rejects only identifiers that clearly advertise an endpoint\\n+ family incompatible with chat messages. Audio-capable and safety-classifier\\n+ models remain transport-compatible because providers serve some of them over\\n+ ``/chat/completions``.\\n+ \\\"\\\"\\\"\\n+ tokens = _model_tokens(model_id)\\n+ return _is_transport_compatible_tokens(tokens)\\n+\\n+\\n+def _is_transport_compatible_tokens(tokens: tuple[str, ...]) -> bool:\\n+ \\\"\\\"\\\"Judge transport compatibility from already-normalized model tokens.\\\"\\\"\\\"\\n+ if not tokens:\\n+ return False\\n+ for token in tokens:\\n+ if token in _TRANSPORT_INCOMPATIBLE_EXACT_TOKENS:\\n+ return False\\n+ if token.startswith(_TRANSPORT_INCOMPATIBLE_PREFIXES):\\n+ return False\\n+ return True\\n+\\n+\\n+def _model_tokens(model_id: str) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Normalize one provider-prefixed model identifier into lowercase tokens.\\\"\\\"\\\"\\n+ if not isinstance(model_id, str):\\n+ return ()\\n+ return tuple(_MODEL_TOKEN_RE.findall(model_id.casefold()))\\n+\\n+\\n+def is_general_chat_agent_model_id(model_id: str) -> bool:\\n+ \\\"\\\"\\\"Return whether a chat model may enter ordinary orchestration roles.\\n+\\n+ Explicit guard and safety models can use chat transport but are specialized\\n+ policy classifiers, not general answer synthesizers. This negative role gate\\n+ does not infer reasoning, coding, vision, or verification capabilities.\\n+ \\\"\\\"\\\"\\n+ tokens = _model_tokens(model_id)\\n+ if not tokens or not _is_transport_compatible_tokens(tokens):\\n+ return False\\n+ return not any(\\n+ token == \\\"safety\\\"\\n+ or token == \\\"guard\\\"\\n+ or token == \\\"shieldgemma\\\"\\n+ or token.startswith(\\\"nemoguard\\\")\\n+ for token in tokens\\n+ )\" }, { \"sha\": \"254caa98548b161670fa9ce2e8fdf83928eaedd7\", \"filename\": \"contextual_orchestrator/cost_ledger.py\", \"status\": \"modified\", \"additions\": 48, \"deletions\": 7, \"changes\": 55, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fcost_ledger.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fcost_ledger.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fcost_ledger.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -31,6 +31,7 @@\\n import threading\\n from dataclasses import dataclass, field\\n from decimal import ROUND_HALF_UP, Decimal\\n+import math\\n import time\\n from typing import Any, Dict, List, Optional, Protocol\\n import uuid\\n@@ -105,6 +106,31 @@ def _price_key(provider: str, model: str) -> str:\\n return f\\\"{provider}:{model}\\\"\\n \\n \\n+def _decimal_safe_price(value: object) -> Optional[float]:\\n+ \\\"\\\"\\\"Parse one raw price component, or ``None`` when unknown, underflowed, or overflowed.\\n+\\n+ Parses through ``Decimal`` first so a nonzero price that underflows to\\n+ ``0.0`` in float (e.g. a stray ``1e-10000``) is rejected as unknown\\n+ rather than silently accepted as a legitimate free price. A ``Decimal``\\n+ can still be finite while its ``float()`` conversion overflows to\\n+ ``inf`` (e.g. ``1e10000``), so ``math.isfinite`` is checked separately\\n+ on the converted value.\\n+ \\\"\\\"\\\"\\n+ try:\\n+ decimal_value = Decimal(str(value))\\n+ price = float(decimal_value)\\n+ except (ArithmeticError, TypeError, ValueError):\\n+ return None\\n+ if (\\n+ not decimal_value.is_finite()\\n+ or not math.isfinite(price)\\n+ or decimal_value < 0\\n+ or (decimal_value != 0 and price == 0)\\n+ ):\\n+ return None\\n+ return price\\n+\\n+\\n @dataclass\\n class PriceEntry:\\n \\\"\\\"\\\"A single price-table row: per-1K-token prices for a provider+model.\\\"\\\"\\\"\\n@@ -155,18 +181,33 @@ def get_price(self, provider: str, model: str) -> Optional[PriceEntry]:\\n \\\"\\\"\\\"Return the price entry for ``provider``+``model``, if configured.\\n \\n Falls back to a provider-wildcard entry (``\\\"{provider}:*\\\"``) so a\\n- provider can set one default price for all of its models.\\n+ provider can set one default price for all of its models. A corrupt\\n+ specific row does not suppress an otherwise-valid wildcard fallback.\\n \\\"\\\"\\\"\\n- raw = self._config.get(_PRICE_CATEGORY, _price_key(provider, model), None)\\n- if raw is None:\\n- raw = self._config.get(_PRICE_CATEGORY, _price_key(provider, \\\"*\\\"), None)\\n- if raw is None:\\n+ for candidate_model in (model, \\\"*\\\"):\\n+ raw = self._config.get(_PRICE_CATEGORY, _price_key(provider, candidate_model), None)\\n+ entry = self._parse_price_entry(raw, provider, model)\\n+ if entry is not None:\\n+ return entry\\n+ return None\\n+\\n+ def _parse_price_entry(\\n+ self, raw: Any, provider: str, model: str\\n+ ) -> Optional[PriceEntry]:\\n+ \\\"\\\"\\\"Validate one raw KV row into a ``PriceEntry``, or ``None`` if it is unusable.\\\"\\\"\\\"\\n+ if not isinstance(raw, dict):\\n+ return None\\n+ if \\\"prompt_price_per_1k\\\" not in raw or \\\"completion_price_per_1k\\\" not in raw:\\n+ return None\\n+ prompt_price = _decimal_safe_price(raw[\\\"prompt_price_per_1k\\\"])\\n+ completion_price = _decimal_safe_price(raw[\\\"completion_price_per_1k\\\"])\\n+ if prompt_price is None or completion_price is None:\\n return None\\n return PriceEntry(\\n provider_name=raw.get(\\\"provider_name\\\", provider),\\n model_name=raw.get(\\\"model_name\\\", model),\\n- prompt_price_per_1k=float(raw.get(\\\"prompt_price_per_1k\\\", 0.0)),\\n- completion_price_per_1k=float(raw.get(\\\"completion_price_per_1k\\\", 0.0)),\\n+ prompt_price_per_1k=prompt_price,\\n+ completion_price_per_1k=completion_price,\\n currency_code=raw.get(\\\"currency_code\\\", self.default_currency),\\n )\\n \" }, { \"sha\": \"3daf36283e78a2dadd0bc814d761c066756fc217\", \"filename\": \"contextual_orchestrator/credentials.py\", \"status\": \"modified\", \"additions\": 34, \"deletions\": 0, \"changes\": 34, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fcredentials.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fcredentials.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fcredentials.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -50,6 +50,10 @@ def set(self, name: str, value: str) -> None:\\n \\\"\\\"\\\"Register (or replace) the secret stored under ``name``.\\\"\\\"\\\"\\n ...\\n \\n+ def delete(self, name: str) -> None:\\n+ \\\"\\\"\\\"Remove one credential after an unvalidated candidate promotion.\\\"\\\"\\\"\\n+ ...\\n+\\n \\n class InMemoryCredentialBackend:\\n \\\"\\\"\\\"Process-local credential registry for dev and tests (no Postgres needed).\\\"\\\"\\\"\\n@@ -68,6 +72,11 @@ def set(self, name: str, value: str) -> None:\\n with self._lock:\\n self._store[name] = value\\n \\n+ def delete(self, name: str) -> None:\\n+ \\\"\\\"\\\"Remove ``name`` from the in-memory credential registry if present.\\\"\\\"\\\"\\n+ with self._lock:\\n+ self._store.pop(name, None)\\n+\\n \\n # --- Postgres pgcrypto-encrypted credential registry ------------------------\\n #\\n@@ -112,6 +121,15 @@ def __init__(self, dsn: str, passphrase: str) -> None:\\n self._passphrase = passphrase\\n self._ensured = False\\n \\n+ @property\\n+ def connection_dsn(self) -> str:\\n+ \\\"\\\"\\\"Return the bootstrap DSN for a colocated metadata store.\\n+\\n+ Callers must treat this as connection material: never include it in logs,\\n+ reports, traces, or exceptions. Provider API keys remain inaccessible.\\n+ \\\"\\\"\\\"\\n+ return self._dsn\\n+\\n @classmethod\\n def from_env(cls) -> \\\"PostgresCredentialBackend\\\":\\n \\\"\\\"\\\"Build the backend from bootstrap transport env vars (the only allowed env use).\\n@@ -173,6 +191,17 @@ def set(self, name: str, value: str) -> None: # pragma: no cover - requires a l\\n )\\n conn.commit()\\n \\n+ def delete(self, name: str) -> None: # pragma: no cover - requires a live Postgres\\n+ \\\"\\\"\\\"Delete one encrypted credential after a failed candidate promotion.\\\"\\\"\\\"\\n+ with self._connect() as conn:\\n+ self._ensure_schema(conn)\\n+ with conn.cursor() as cur:\\n+ cur.execute(\\n+ \\\"DELETE FROM provider_credentials WHERE credential_name = %s\\\",\\n+ (name,),\\n+ )\\n+ conn.commit()\\n+\\n \\n _backend: CredentialBackend | None = None\\n _backend_lock = threading.Lock()\\n@@ -216,3 +245,8 @@ def get_credential(name: str) -> str | None:\\n def register_credential(name: str, value: str) -> None:\\n \\\"\\\"\\\"Register a named secret into the KV (used by the bootstrap CLI).\\\"\\\"\\\"\\n get_backend().set(name, value)\\n+\\n+\\n+def delete_credential(name: str) -> None:\\n+ \\\"\\\"\\\"Remove a named credential from the KV after an unvalidated promotion.\\\"\\\"\\\"\\n+ get_backend().delete(name)\" }, { \"sha\": \"fbc3da91c6fa8ef5bb7768cb3659a74ffc5cf205\", \"filename\": \"contextual_orchestrator/model_discovery.py\", \"status\": \"modified\", \"additions\": 221, \"deletions\": 51, \"changes\": 272, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fmodel_discovery.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fmodel_discovery.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fmodel_discovery.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -13,14 +13,16 @@\\n \\n from __future__ import annotations\\n \\n+from decimal import Decimal\\n import json\\n+import math\\n import re\\n import urllib.error\\n import urllib.request\\n-from dataclasses import dataclass\\n+from dataclasses import dataclass, replace\\n from typing import TYPE_CHECKING, Any\\n \\n-from .batch_routing import cheapest_upstream\\n+from .chat_capability import is_general_chat_agent_model_id\\n from .credentials import get_credential\\n from .orchestrator import ModelAgent\\n \\n@@ -84,7 +86,7 @@ class ProviderModelSource:\\n \\n @dataclass(frozen=True)\\n class DiscoveredModel:\\n- \\\"\\\"\\\"One model found on a provider, with pricing when the provider reports it.\\\"\\\"\\\"\\n+ \\\"\\\"\\\"One general-chat model found on a provider, with reported pricing.\\\"\\\"\\\"\\n \\n provider_name: str\\n model_id: str\\n@@ -121,14 +123,78 @@ def _fetch_json(url: str, *, api_key: str, auth_scheme: str, timeout: float) ->\\n return json.loads(response.read().decode(\\\"utf-8\\\"))\\n \\n \\n+def _valid_price_component(value: object) -> bool:\\n+ \\\"\\\"\\\"Return whether one price component is finite, numeric, and non-negative.\\\"\\\"\\\"\\n+ if isinstance(value, bool) or not isinstance(value, (int, float)):\\n+ return False\\n+ try:\\n+ numeric = float(value)\\n+ except (TypeError, ValueError, OverflowError):\\n+ return False\\n+ return math.isfinite(numeric) and numeric >= 0.0\\n+\\n+\\n def _price_per_1k(value: Any) -> float | None:\\n- \\\"\\\"\\\"OpenAI-compatible providers report USD price per single token; convert to per-1K.\\\"\\\"\\\"\\n- if value is None:\\n+ \\\"\\\"\\\"Convert a trustworthy per-token USD price to per-1K, else return unknown.\\n+\\n+ Parses through ``Decimal`` first so a nonzero price that underflows to\\n+ ``0.0`` in float (e.g. a stray ``1e-10000``) is rejected as unknown\\n+ rather than silently accepted as a legitimate free price.\\n+ \\\"\\\"\\\"\\n+ if value is None or isinstance(value, bool):\\n return None\\n try:\\n- return float(value) * 1000\\n- except (TypeError, ValueError):\\n+ decimal_per_1k = Decimal(str(value)) * 1000\\n+ per_1k = float(decimal_per_1k)\\n+ except (ArithmeticError, TypeError, ValueError):\\n+ return None\\n+ if not decimal_per_1k.is_finite() or (decimal_per_1k != 0 and per_1k == 0):\\n return None\\n+ return per_1k if _valid_price_component(per_1k) else None\\n+\\n+\\n+def _serving_identity(model: DiscoveredModel) -> tuple[str, str]:\\n+ \\\"\\\"\\\"Return the durable agent identity used by discovery synchronization.\\\"\\\"\\\"\\n+ return (model.provider_name, model.model_id)\\n+\\n+\\n+def _source_tiebreaker(model: DiscoveredModel) -> tuple[str, str, str, str]:\\n+ \\\"\\\"\\\"Choose deterministic transport metadata for an ambiguous duplicate row.\\\"\\\"\\\"\\n+ return (\\n+ model.credential_name,\\n+ model.chat_base_url,\\n+ model.auth_scheme,\\n+ model.currency_code,\\n+ )\\n+\\n+\\n+def _deduplicate_discovered_models(\\n+ discovered: list[DiscoveredModel],\\n+) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Collapse duplicate agent identities and withhold conflicting price evidence.\\n+\\n+ Exact duplicate catalog rows become one candidate. When the same provider/model\\n+ identity is repeated with conflicting metadata or prices, one deterministic\\n+ transport record is retained but its prices become unknown. Provider row order\\n+ therefore cannot fabricate a cheaper bootstrap candidate or consume failover\\n+ capacity twice.\\n+ \\\"\\\"\\\"\\n+ unique: dict[tuple[str, str], DiscoveredModel] = {}\\n+ for model in discovered:\\n+ identity = _serving_identity(model)\\n+ previous = unique.get(identity)\\n+ if previous is None:\\n+ unique[identity] = model\\n+ continue\\n+ if previous == model:\\n+ continue\\n+ chosen = min((previous, model), key=_source_tiebreaker)\\n+ unique[identity] = replace(\\n+ chosen,\\n+ prompt_price_per_1k=None,\\n+ completion_price_per_1k=None,\\n+ )\\n+ return list(unique.values())\\n \\n \\n def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[DiscoveredModel]:\\n@@ -138,7 +204,7 @@ def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[\\n if not isinstance(row, dict):\\n continue\\n model_id = row.get(\\\"id\\\")\\n- if type(model_id) is not str or not model_id:\\n+ if type(model_id) is not str or not model_id or not is_general_chat_agent_model_id(model_id):\\n continue\\n pricing = row.get(\\\"pricing\\\") if isinstance(row.get(\\\"pricing\\\"), dict) else {}\\n discovered.append(\\n@@ -152,7 +218,7 @@ def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[\\n completion_price_per_1k=_price_per_1k(pricing.get(\\\"completion\\\")),\\n )\\n )\\n- return discovered\\n+ return _deduplicate_discovered_models(discovered)\\n \\n \\n def _parse_bytez(payload: Any, source: ProviderModelSource) -> list[DiscoveredModel]:\\n@@ -162,7 +228,7 @@ def _parse_bytez(payload: Any, source: ProviderModelSource) -> list[DiscoveredMo\\n if not isinstance(row, dict):\\n continue\\n model_id = row.get(\\\"modelId\\\")\\n- if type(model_id) is not str or not model_id:\\n+ if type(model_id) is not str or not model_id or not is_general_chat_agent_model_id(model_id):\\n continue\\n discovered.append(\\n DiscoveredModel(\\n@@ -175,7 +241,7 @@ def _parse_bytez(payload: Any, source: ProviderModelSource) -> list[DiscoveredMo\\n # per-1k pricing unset is more honest than a misleading estimate.\\n )\\n )\\n- return discovered\\n+ return _deduplicate_discovered_models(discovered)\\n \\n \\n def discover_provider_models(\\n@@ -214,7 +280,7 @@ def discover_all_models(\\n discovered.extend(discover_provider_models(source, timeout=timeout))\\n except ProviderDiscoveryError as exc:\\n errors.append(exc)\\n- return discovered, errors\\n+ return _deduplicate_discovered_models(discovered), errors\\n \\n \\n _SLUG_RE = re.compile(r\\\"[^a-z0-9]+\\\")\\n@@ -231,7 +297,9 @@ def agent_id_for(discovered: DiscoveredModel) -> str:\\n \\n \\n def agent_from_discovered(discovered: DiscoveredModel, *, priority: int = 0) -> ModelAgent:\\n- \\\"\\\"\\\"Build a disabled-by-default ModelAgent for a discovered model (opt-in serving).\\\"\\\"\\\"\\n+ \\\"\\\"\\\"Build a disabled general-chat agent or reject an ineligible record.\\\"\\\"\\\"\\n+ if not is_general_chat_agent_model_id(discovered.model_id):\\n+ raise ValueError(\\\"model is not eligible for a general chat agent\\\")\\n return ModelAgent(\\n id=agent_id_for(discovered),\\n model=discovered.model_id,\\n@@ -245,73 +313,175 @@ def agent_from_discovered(discovered: DiscoveredModel, *, priority: int = 0) ->\\n )\\n \\n \\n+def _currency_is_comparable(currency_code: object, default_currency: object) -> bool:\\n+ \\\"\\\"\\\"Return whether two ISO-style currency codes can be compared directly.\\\"\\\"\\\"\\n+ return (\\n+ isinstance(currency_code, str)\\n+ and isinstance(default_currency, str)\\n+ and currency_code.strip().upper() == default_currency.strip().upper()\\n+ and bool(currency_code.strip())\\n+ )\\n+\\n+\\n def refresh_price_book(discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\\") -> int:\\n- \\\"\\\"\\\"Write every discovered model's known pricing into the price book.\\n+ \\\"\\\"\\\"Write complete, comparable provider pricing into the discovery price book.\\n \\n- Returns the number of price rows written. A model without provider-reported\\n- pricing is skipped rather than defaulted to 0 -- an unpriced model already\\n- costs 0 under ``PriceBook.compute_cost``'s \\\"explicit, not silently expensive\\\"\\n- contract, so writing a fabricated 0 row here would just hide that signal.\\n+ Both prompt and completion prices are required for the fixed 1K+1K ranking\\n+ workload. Partial, conflicting, non-finite, negative, or cross-currency\\n+ evidence remains unknown rather than acquiring an invented zero component.\\n \\\"\\\"\\\"\\n from .cost_ledger import PriceEntry\\n \\n written = 0\\n- for model in discovered:\\n- if model.prompt_price_per_1k is None and model.completion_price_per_1k is None:\\n+ for model in _deduplicate_discovered_models(discovered):\\n+ if not is_general_chat_agent_model_id(model.model_id):\\n+ continue\\n+ if not (\\n+ _valid_price_component(model.prompt_price_per_1k)\\n+ and _valid_price_component(model.completion_price_per_1k)\\n+ and _currency_is_comparable(\\n+ model.currency_code,\\n+ price_book.default_currency,\\n+ )\\n+ ):\\n continue\\n price_book.set_price(\\n PriceEntry(\\n provider_name=model.provider_name,\\n model_name=model.model_id,\\n- prompt_price_per_1k=model.prompt_price_per_1k or 0.0,\\n- completion_price_per_1k=model.completion_price_per_1k or 0.0,\\n- currency_code=model.currency_code,\\n+ prompt_price_per_1k=float(model.prompt_price_per_1k),\\n+ completion_price_per_1k=float(model.completion_price_per_1k),\\n+ currency_code=model.currency_code.strip().upper(),\\n )\\n )\\n written += 1\\n return written\\n \\n \\n+def _discovery_price_key(\\n+ model: DiscoveredModel,\\n+ price_book: \\\"PriceBook\\\",\\n+) -> tuple[int, float, str, str]:\\n+ \\\"\\\"\\\"Rank comparable trustworthy prices first, then deterministic unknowns.\\\"\\\"\\\"\\n+ unknown = (1, 0.0, model.provider_name, model.model_id)\\n+ try:\\n+ entry = price_book.get_price(model.provider_name, model.model_id)\\n+ except (TypeError, ValueError, OverflowError):\\n+ return unknown\\n+ if entry is None:\\n+ return unknown\\n+ if not (\\n+ _valid_price_component(entry.prompt_price_per_1k)\\n+ and _valid_price_component(entry.completion_price_per_1k)\\n+ and _currency_is_comparable(\\n+ entry.currency_code,\\n+ price_book.default_currency,\\n+ )\\n+ ):\\n+ return unknown\\n+ try:\\n+ cost, currency = price_book.compute_cost(\\n+ model.provider_name,\\n+ model.model_id,\\n+ 1000,\\n+ 1000,\\n+ )\\n+ except (TypeError, ValueError, OverflowError):\\n+ return unknown\\n+ if not (\\n+ _valid_price_component(cost)\\n+ and _currency_is_comparable(currency, price_book.default_currency)\\n+ ):\\n+ return unknown\\n+ return (0, cost, model.provider_name, model.model_id)\\n+\\n+\\n+def _provider_family(provider_name: str) -> str:\\n+ \\\"\\\"\\\"Collapse credentials that share one upstream provider outage domain.\\\"\\\"\\\"\\n+ if provider_name in {\\\"nvidia_nim\\\", \\\"nvidia_nim_sub\\\"}:\\n+ return \\\"nvidia_nim\\\"\\n+ return provider_name\\n+\\n+\\n def select_cheapest_discovered_agent(\\n discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\\"\\n ) -> DiscoveredModel | None:\\n- \\\"\\\"\\\"Pick the lowest-cost discovered model per the price book (auto-optimization).\\n-\\n- Reuses :func:`~contextual_orchestrator.batch_routing.cheapest_upstream`, the\\n- existing cost-optimizing upstream selector. Call :func:`refresh_price_book`\\n- first so discovered pricing is visible; an unpriced candidate costs ``0``\\n- under that selector's documented contract and is treated as free, not\\n- unknown -- so a genuinely unpriced provider (e.g. Bytez, priced by\\n- GPU-second rather than per token) will always look cheapest here. Fine for\\n- \\\"auto-pick something free to try,\\\" but callers doing real cost comparison\\n- should refresh pricing for every candidate they care about first.\\n+ \\\"\\\"\\\"Pick the cheapest candidate with trustworthy price evidence.\\n+\\n+ A candidate without a price row is unknown, not free. Known prices therefore\\n+ sort first; when every candidate is unpriced, provider and model identifiers\\n+ provide deterministic fallback ordering without inventing a monetary value.\\n \\\"\\\"\\\"\\n- if not discovered:\\n- return None\\n- candidates = [{\\\"provider\\\": model.provider_name, \\\"model\\\": model.model_id} for model in discovered]\\n- winner = cheapest_upstream(candidates, price_book)\\n- if winner is None:\\n+ eligible = [\\n+ model\\n+ for model in _deduplicate_discovered_models(discovered)\\n+ if is_general_chat_agent_model_id(model.model_id)\\n+ ]\\n+ if not eligible:\\n return None\\n- for model in discovered:\\n- if model.provider_name == winner[\\\"provider\\\"] and model.model_id == winner[\\\"model\\\"]:\\n- return model\\n- return None # pragma: no cover - winner always comes from candidates\\n+ return min(eligible, key=lambda model: _discovery_price_key(model, price_book))\\n \\n \\n def select_top_n_cheapest_discovered_agents(\\n discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\\", limit: int\\n ) -> list[DiscoveredModel]:\\n- \\\"\\\"\\\"Return the ``limit`` lowest-cost discovered models, cheapest first.\\n+ \\\"\\\"\\\"Return up to ``limit`` unique candidates, known-priced before unknown.\\\"\\\"\\\"\\n+ if limit <= 0:\\n+ return []\\n+ eligible = [\\n+ model\\n+ for model in _deduplicate_discovered_models(discovered)\\n+ if is_general_chat_agent_model_id(model.model_id)\\n+ ]\\n+ if not eligible:\\n+ return []\\n+ return sorted(\\n+ eligible,\\n+ key=lambda model: _discovery_price_key(model, price_book),\\n+ )[:limit]\\n \\n- For bootstrapping a CI sidecar (or any first-boot pool) with more than one\\n- enabled agent for failover, without hand-picking which discovered models to\\n- trust. Same pricing contract as :func:`select_cheapest_discovered_agent`.\\n+\\n+def select_bootstrap_discovered_agents(\\n+ discovered: list[DiscoveredModel],\\n+ price_book: \\\"PriceBook\\\",\\n+ limit: int,\\n+) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Build a deterministic, price-honest, provider-diverse initial pool.\\n+\\n+ Candidates retain the known-price-first ordering above, but the first pass\\n+ takes at most one model from each independent provider family. Remaining\\n+ capacity is filled in the same deterministic cost order. NVIDIA NIM primary\\n+ and sub credentials are one outage domain, so they participate in the second\\n+ pass only after independently hosted providers have had a chance to enter.\\n+ Duplicate serving identities never consume capacity twice.\\n \\\"\\\"\\\"\\n- if limit <= 0 or not discovered:\\n+ if limit <= 0:\\n+ return []\\n+ eligible = [\\n+ model\\n+ for model in _deduplicate_discovered_models(discovered)\\n+ if is_general_chat_agent_model_id(model.model_id)\\n+ ]\\n+ if not eligible:\\n return []\\n \\n- def _cost(model: DiscoveredModel) -> float:\\n- cost, _currency = price_book.compute_cost(model.provider_name, model.model_id, 1000, 1000)\\n- return cost\\n+ ranked = sorted(\\n+ eligible,\\n+ key=lambda model: _discovery_price_key(model, price_book),\\n+ )\\n+ selected: list[DiscoveredModel] = []\\n+ deferred: list[DiscoveredModel] = []\\n+ provider_families: set[str] = set()\\n+\\n+ for model in ranked:\\n+ family = _provider_family(model.provider_name)\\n+ if family in provider_families:\\n+ deferred.append(model)\\n+ continue\\n+ provider_families.add(family)\\n+ selected.append(model)\\n+ if len(selected) == limit:\\n+ return selected\\n \\n- return sorted(discovered, key=_cost)[:limit]\\n+ selected.extend(deferred[: limit - len(selected)])\\n+ return selected\" }, { \"sha\": \"254d593bed7de2f3fc823fc9e59062e6777590a8\", \"filename\": \"contextual_orchestrator/orchestrator.py\", \"status\": \"modified\", \"additions\": 62, \"deletions\": 10, \"changes\": 72, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Forchestrator.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Forchestrator.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Forchestrator.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -31,6 +31,10 @@\\n import urllib.error\\n import urllib.request\\n \\n+from .chat_capability import (\\n+ is_chat_compatible_model_id,\\n+ is_general_chat_agent_model_id,\\n+)\\n from .conventions import require_object_name\\n from .credentials import NotConfigured, get_credential\\n \\n@@ -776,6 +780,8 @@ def chat(\\n ``default_top_p`` are used so request-scoped Completions sampling can be\\n applied without threading kwargs through every orchestrator hop.\\n \\\"\\\"\\\"\\n+ if not is_chat_compatible_model_id(agent.model):\\n+ raise ValueError(\\\"model is not chat-compatible and cannot serve a chat request\\\")\\n self._local.usage = None\\n # Expose the effective sampling knobs for request-path tests / diagnostics.\\n effective_temperature = self.default_temperature if temperature is None else temperature\\n@@ -825,6 +831,15 @@ def probe(self, agent: ModelAgent, *, timeout: float = DEFAULT_PROVIDER_PROBE_TI\\n \\\"\\\"\\\"\\n probe_timeout = _validate_provider_probe_timeout(timeout)\\n started = time.monotonic()\\n+ if not is_chat_compatible_model_id(agent.model):\\n+ return {\\n+ \\\"agent_id\\\": agent.id,\\n+ \\\"model\\\": agent.model,\\n+ \\\"status\\\": \\\"not_ready\\\",\\n+ \\\"latency_ms\\\": round((time.monotonic() - started) * 1000, 2),\\n+ \\\"error_type\\\": \\\"ValueError\\\",\\n+ \\\"failure_code\\\": \\\"non_chat_model\\\",\\n+ }\\n self._local.usage = None\\n failure_code = \\\"provider_probe_failed\\\"\\n try:\\n@@ -1070,6 +1085,10 @@ def stream_chat(self, agent: ModelAgent, messages: list[ChatMessage], temperatur\\n are yielded as they arrive (not computed-then-framed). The mock path yields its\\n answer in fixed chunks so behavior shape stays testable and unchanged.\\n \\\"\\\"\\\"\\n+ if not is_chat_compatible_model_id(agent.model):\\n+ raise ValueError(\\n+ f\\\"model {agent.model!r} is not chat-compatible and cannot serve {agent.id!r}\\\"\\n+ )\\n if agent.base_url.startswith(\\\"mock://\\\"):\\n answer = self._mock(agent, messages)\\n for start in range(0, len(answer), 24):\\n@@ -1129,10 +1148,20 @@ def proxy_send(\\n self, agent: ModelAgent, endpoint: str, payload: dict[str, Any]\\n ) -> dict[str, Any]:\\n \\\"\\\"\\\"Passthrough a full request to one agent, returning the raw provider JSON.\\\"\\\"\\\"\\n+ normalized_endpoint = endpoint.strip(\\\"/\\\")\\n+ if normalized_endpoint.startswith(\\\"v1/\\\"):\\n+ normalized_endpoint = normalized_endpoint[3:]\\n+ if (\\n+ normalized_endpoint in {\\\"chat/completions\\\", \\\"completions\\\", \\\"responses\\\"}\\n+ and not is_chat_compatible_model_id(agent.model)\\n+ ):\\n+ raise ValueError(\\n+ f\\\"model {agent.model!r} is not chat-compatible and cannot serve {agent.id!r}\\\"\\n+ )\\n if agent.base_url.startswith(\\\"mock://\\\"):\\n- return self._mock_raw(agent, endpoint, payload)\\n+ return self._mock_raw(agent, normalized_endpoint, payload)\\n destination = self._validate_provider(agent) # pragma: no cover\\n- if endpoint.strip(\\\"/\\\") == \\\"responses\\\" and _is_local_provider_url(agent.base_url):\\n+ if normalized_endpoint == \\\"responses\\\" and _is_local_provider_url(agent.base_url):\\n chat_payload = _responses_to_chat_payload(payload)\\n chat_payload.setdefault(\\\"max_tokens\\\", self.max_output_tokens)\\n if _is_direct_mlx_provider_url(agent.base_url) and self.chat_template_args:\\n@@ -1143,7 +1172,7 @@ def proxy_send(\\n )\\n return _chat_to_responses_payload(chat_response, payload)\\n with _local_provider_slot(agent, self.local_concurrency, self.timeout): # pragma: no cover\\n- return self._send_raw_with_retry(agent, endpoint, payload, destination)\\n+ return self._send_raw_with_retry(agent, normalized_endpoint, payload, destination)\\n \\n def _send_raw_with_retry(\\n self,\\n@@ -1316,6 +1345,10 @@ def batch_chat(\\n workloads (24h completion window, ~half the price); real-time chat should keep\\n using ``chat``. The mock path answers synchronously so tests and local runs work.\\n \\\"\\\"\\\"\\n+ if not is_chat_compatible_model_id(agent.model):\\n+ raise ValueError(\\n+ f\\\"model {agent.model!r} is not chat-compatible and cannot serve {agent.id!r}\\\"\\n+ )\\n if agent.base_url.startswith(\\\"mock://\\\"):\\n results = {\\n custom_id: {\\\"content\\\": self._mock(agent, messages), \\\"usage\\\": None}\\n@@ -2434,6 +2467,7 @@ def _plan_generated(self, task: str) -> list[WorkflowStep]:\\n pool = \\\"\\\\n\\\".join(\\n f\\\"- {agent.id}: model={agent.model}, tags={', '.join(agent.tags) or 'none'}\\\"\\n for agent in self.agents\\n+ if is_general_chat_agent_model_id(agent.model)\\n )\\n system = (\\n \\\"You are the workflow conductor. Decompose the user's task into a short workflow.\\\\n\\\"\\n@@ -2459,7 +2493,7 @@ def _parse_workflow_plan(self, raw: str) -> list[WorkflowStep]:\\n raw_steps = data.get(\\\"steps\\\")\\n if not isinstance(raw_steps, list) or not (2 <= len(raw_steps) <= self.policy.max_workflow_steps):\\n raise ValueError(f\\\"plan must have 2..{self.policy.max_workflow_steps} steps\\\")\\n- known_agents = {agent.id for agent in self.agents}\\n+ known_agents = {agent.id: agent for agent in self.agents}\\n steps: list[WorkflowStep] = []\\n for index, item in enumerate(raw_steps):\\n if int(item.get(\\\"id\\\", -1)) != index:\\n@@ -2474,8 +2508,9 @@ def _parse_workflow_plan(self, raw: str) -> list[WorkflowStep]:\\n if any(value < 0 or value >= index for value in access):\\n raise ValueError(\\\"access may reference only earlier steps\\\")\\n agent_id = item.get(\\\"agent_id\\\")\\n- if agent_id not in known_agents:\\n- # The planner named an unknown agent: reselect honestly instead of failing the plan.\\n+ assigned = known_agents.get(agent_id)\\n+ if assigned is None or not is_general_chat_agent_model_id(assigned.model):\\n+ # Unknown or stale ineligible assignments are reselected honestly.\\n agent_id = self._select_agent(subtask, role).id\\n steps.append(WorkflowStep(index, role, agent_id, subtask, access))\\n if steps[-1].role not in {\\\"synthesizer\\\", \\\"worker\\\"}:\\n@@ -2509,10 +2544,21 @@ def _score_agent(self, agent: ModelAgent, role: str, lowered: str) -> tuple[int,\\n def _ranked_agents(self, text: str, role: str) -> list[ModelAgent]:\\n \\\"\\\"\\\"Agents sorted best-first for a role; the head is the primary, the tail are failovers.\\\"\\\"\\\"\\n lowered = text.lower()\\n- return sorted(self.agents, key=lambda agent: self._score_agent(agent, role, lowered), reverse=True)\\n+ return [\\n+ agent\\n+ for agent in sorted(\\n+ self.agents,\\n+ key=lambda agent: self._score_agent(agent, role, lowered),\\n+ reverse=True,\\n+ )\\n+ if is_general_chat_agent_model_id(agent.model)\\n+ ]\\n \\n def _select_agent(self, text: str, role: str) -> ModelAgent:\\n- selected = self._ranked_agents(text, role)[0]\\n+ ranked = self._ranked_agents(text, role)\\n+ if not ranked:\\n+ raise RuntimeError(f\\\"no chat-compatible agent available for role={role}\\\")\\n+ selected = ranked[0]\\n if selected.disabled: # pragma: no cover\\n raise RuntimeError(f\\\"no enabled agent available for role={role}\\\")\\n if role in selected.provider_exclusions: # pragma: no cover\\n@@ -2530,6 +2576,8 @@ def _invoke(\\n usage when available (else None), so spend analytics can prefer it.\\n \\\"\\\"\\\"\\n candidates = self._failover_candidates(primary, text, role)\\n+ if not candidates:\\n+ raise RuntimeError(f\\\"no chat-compatible agent available for role={role}\\\")\\n last_error: Exception | None = None\\n for agent in candidates:\\n try:\\n@@ -2545,11 +2593,15 @@ def _invoke(\\n \\n def _failover_candidates(self, primary: ModelAgent, text: str, role: str) -> list[ModelAgent]:\\n ranked = self._ranked_agents(text, role)\\n- ordered = [primary] + [agent for agent in ranked if agent.id != primary.id]\\n+ ordered = [\\n+ agent\\n+ for agent in [primary] + [agent for agent in ranked if agent.id != primary.id]\\n+ if is_general_chat_agent_model_id(agent.model)\\n+ ]\\n eligible = [agent for agent in ordered if not agent.disabled and role not in agent.provider_exclusions]\\n healthy = [agent for agent in eligible if not self._circuit_open(agent.id)]\\n # If every eligible agent is circuit-open, still probe them rather than fail with no attempt.\\n- return healthy or eligible or [primary]\\n+ return healthy or eligible\\n \\n def _circuit_open(self, agent_id: str) -> bool:\\n with self._circuit_lock:\" }, { \"sha\": \"323c56d9df0bd0f36acf58fa641df6585a57efd9\", \"filename\": \"contextual_orchestrator/provider_bootstrap.py\", \"status\": \"added\", \"additions\": 354, \"deletions\": 0, \"changes\": 354, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fprovider_bootstrap.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fprovider_bootstrap.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fprovider_bootstrap.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,354 @@\\n+\\\"\\\"\\\"Durable bootstrap for the organization provider credential inventory.\\n+\\n+A trusted deployment process may expose the fixed provider-secret inventory to\\n+this one-shot module. Values are validated as a complete set, written to the\\n+configured credential KV, and then model discovery runs exclusively through the\\n+KV-backed runtime seam. Runtime provider calls never read provider API keys from\\n+``os.environ``.\\n+\\n+Bootstrap establishes a conservative serving candidate set. It does not infer\\n+reasoning, coding, vision, or other provider capabilities from model names;\\n+capability negotiation remains an explicit runtime/catalog responsibility.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import argparse\\n+from dataclasses import dataclass, replace\\n+import json\\n+import os\\n+from typing import Mapping, Sequence\\n+\\n+from .chat_capability import is_general_chat_agent_model_id\\n+from .cost_ledger import PriceBook\\n+from .credentials import (\\n+ InMemoryCredentialBackend,\\n+ PostgresCredentialBackend,\\n+ get_backend,\\n+)\\n+from .kv_config import InMemoryConfigStore\\n+from .model_discovery import (\\n+ DiscoveredModel,\\n+ PROVIDER_MODEL_SOURCES,\\n+ _currency_is_comparable,\\n+ _provider_family,\\n+ agent_from_discovered,\\n+ agent_id_for,\\n+ discover_all_models,\\n+ refresh_price_book,\\n+)\\n+from .orchestrator import ModelAgent, TaskOrchestrator\\n+\\n+\\n+PROVIDER_CREDENTIAL_NAMES: tuple[str, ...] = tuple(\\n+ dict.fromkeys(source.credential_name for source in PROVIDER_MODEL_SOURCES)\\n+)\\n+\\\"\\\"\\\"Fixed organization credential inventory accepted by the bootstrap boundary.\\\"\\\"\\\"\\n+\\n+_GENERIC_SERVING_TAGS = (\\n+ \\\"discovered\\\",\\n+ \\\"chat\\\",\\n+ \\\"worker\\\",\\n+ \\\"writing\\\",\\n+ \\\"synthesizer\\\",\\n+)\\n+\\n+\\n+class ProviderBootstrapError(RuntimeError):\\n+ \\\"\\\"\\\"Raised when trusted provider bootstrap cannot establish a usable catalog.\\\"\\\"\\\"\\n+\\n+\\n+@dataclass(frozen=True)\\n+class ProviderBootstrapReport:\\n+ \\\"\\\"\\\"Secret-free evidence emitted after one provider bootstrap run.\\\"\\\"\\\"\\n+\\n+ registered_credentials: tuple[str, ...]\\n+ discovered_model_count: int\\n+ eligible_model_count: int\\n+ selected_agent_ids: tuple[str, ...]\\n+ enabled_agent_ids: tuple[str, ...]\\n+ durable_agent_pool: bool\\n+ providers_with_errors: tuple[str, ...]\\n+ priced_model_count: int\\n+\\n+ def as_dict(self) -> dict[str, object]:\\n+ \\\"\\\"\\\"Return JSON-safe evidence without credential values or provider payloads.\\\"\\\"\\\"\\n+ return {\\n+ \\\"registered_credentials\\\": list(self.registered_credentials),\\n+ \\\"discovered_model_count\\\": self.discovered_model_count,\\n+ \\\"eligible_model_count\\\": self.eligible_model_count,\\n+ \\\"selected_agent_ids\\\": list(self.selected_agent_ids),\\n+ \\\"enabled_agent_ids\\\": list(self.enabled_agent_ids),\\n+ \\\"durable_agent_pool\\\": self.durable_agent_pool,\\n+ \\\"providers_with_errors\\\": list(self.providers_with_errors),\\n+ \\\"priced_model_count\\\": self.priced_model_count,\\n+ }\\n+\\n+\\n+def _strip_mounted_line_endings(value: str) -> str:\\n+ \\\"\\\"\\\"Remove only CR/LF bytes commonly appended by mounted secret files.\\\"\\\"\\\"\\n+ return value.rstrip(\\\"\\\\r\\\\n\\\")\\n+\\n+\\n+def collect_provider_credentials(\\n+ environ: Mapping[str, str], *, require_all: bool = True\\n+) -> dict[str, str]:\\n+ \\\"\\\"\\\"Collect the fixed inventory without rewriting non-line-ending bytes.\\\"\\\"\\\"\\n+ values: dict[str, str] = {}\\n+ missing: list[str] = []\\n+ for name in PROVIDER_CREDENTIAL_NAMES:\\n+ raw = environ.get(name, \\\"\\\")\\n+ value = _strip_mounted_line_endings(raw) if isinstance(raw, str) else \\\"\\\"\\n+ if value and value.strip():\\n+ values[name] = value\\n+ else:\\n+ missing.append(name)\\n+ if require_all and missing:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap requires the complete credential inventory: \\\"\\n+ + \\\", \\\".join(sorted(missing))\\n+ )\\n+ if not values:\\n+ raise ProviderBootstrapError(\\\"provider bootstrap received no credentials\\\")\\n+ return values\\n+\\n+\\n+def register_provider_credentials_atomically(\\n+ credentials: Mapping[str, str],\\n+) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Register a validated credential batch with one commit where supported.\\\"\\\"\\\"\\n+ if not credentials:\\n+ raise ProviderBootstrapError(\\\"provider bootstrap received an empty credential batch\\\")\\n+ unknown = sorted(set(credentials) - set(PROVIDER_CREDENTIAL_NAMES))\\n+ if unknown:\\n+ raise ProviderBootstrapError(\\\"provider bootstrap rejected unknown credential names\\\")\\n+\\n+ normalized: dict[str, str] = {}\\n+ for name, value in credentials.items():\\n+ if not isinstance(value, str):\\n+ raise ProviderBootstrapError(\\n+ f\\\"provider bootstrap rejected an empty value for {name}\\\"\\n+ )\\n+ normalized_value = _strip_mounted_line_endings(value)\\n+ if not normalized_value or not normalized_value.strip():\\n+ raise ProviderBootstrapError(\\n+ f\\\"provider bootstrap rejected an empty value for {name}\\\"\\n+ )\\n+ normalized[name] = normalized_value\\n+\\n+ backend = get_backend()\\n+ if isinstance(backend, InMemoryCredentialBackend):\\n+ with backend._lock: # noqa: SLF001 - package-internal atomic batch operation\\n+ backend._store.update(normalized) # noqa: SLF001\\n+ elif isinstance(backend, PostgresCredentialBackend):\\n+ with backend._connect() as connection: # noqa: SLF001 - package transaction\\n+ backend._ensure_schema(connection) # noqa: SLF001\\n+ with connection.cursor() as cursor:\\n+ for name, value in normalized.items():\\n+ cursor.execute(\\n+ \\\"INSERT INTO provider_credentials \\\"\\n+ \\\"(credential_name, encrypted_value, updated_at) \\\"\\n+ \\\"VALUES (%s, pgp_sym_encrypt(%s, %s), now()) \\\"\\n+ \\\"ON CONFLICT (credential_name) DO UPDATE SET \\\"\\n+ \\\"encrypted_value = EXCLUDED.encrypted_value, updated_at = now()\\\",\\n+ (name, value, backend._passphrase), # noqa: SLF001\\n+ )\\n+ connection.commit()\\n+ else:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap requires an atomic built-in credential backend\\\"\\n+ )\\n+ return tuple(sorted(normalized))\\n+\\n+\\n+def is_chat_serving_candidate(model: DiscoveredModel) -> bool:\\n+ \\\"\\\"\\\"Apply the shared ordinary-chat eligibility policy to a catalog row.\\n+\\n+ This is a negative compatibility filter, not positive capability inference.\\n+ Models that survive receive only generic chat-serving tags until an explicit\\n+ provider/catalog capability record or measured evidence is available.\\n+ \\\"\\\"\\\"\\n+ return is_general_chat_agent_model_id(model.model_id)\\n+\\n+\\n+def serving_tags_for_discovered(_model: DiscoveredModel) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Return capability-neutral tags safe for any compatible chat candidate.\\\"\\\"\\\"\\n+ return _GENERIC_SERVING_TAGS\\n+\\n+\\n+def _known_cost_sort_key(\\n+ model: DiscoveredModel,\\n+) -> tuple[int, float, str, str]:\\n+ \\\"\\\"\\\"Sort known-price, comparable-currency models before unknown/incomparable ones.\\n+\\n+ Mirrors ``model_discovery._discovery_price_key``'s currency gate so a\\n+ cheap non-USD price can never outrank a USD one on face value alone.\\n+ \\\"\\\"\\\"\\n+ prices = (model.prompt_price_per_1k, model.completion_price_per_1k)\\n+ prompt_price, completion_price = prices\\n+ if (\\n+ prompt_price is None\\n+ or completion_price is None\\n+ or not _currency_is_comparable(model.currency_code, \\\"USD\\\")\\n+ ):\\n+ return (1, float(\\\"inf\\\"), model.provider_name, model.model_id)\\n+ return (0, prompt_price + completion_price, model.provider_name, model.model_id)\\n+\\n+\\n+def select_provider_diverse_models(\\n+ discovered: Sequence[DiscoveredModel], *, limit: int\\n+) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Choose a bounded compatible pool while preserving provider diversity.\\\"\\\"\\\"\\n+ if limit < 1:\\n+ raise ValueError(\\\"provider bootstrap model limit must be positive\\\")\\n+ unique: dict[tuple[str, str, str], DiscoveredModel] = {}\\n+ for model in discovered:\\n+ if not is_chat_serving_candidate(model):\\n+ continue\\n+ unique[(model.provider_name, model.credential_name, model.model_id)] = model\\n+ ordered = sorted(unique.values(), key=_known_cost_sort_key)\\n+ selected: list[DiscoveredModel] = []\\n+ seen_providers: set[str] = set()\\n+ for model in ordered:\\n+ provider_family = _provider_family(model.provider_name)\\n+ if provider_family in seen_providers:\\n+ continue\\n+ selected.append(model)\\n+ seen_providers.add(provider_family)\\n+ if len(selected) >= limit:\\n+ return selected\\n+ selected_keys = {\\n+ (item.provider_name, item.credential_name, item.model_id)\\n+ for item in selected\\n+ }\\n+ for model in ordered:\\n+ key = (model.provider_name, model.credential_name, model.model_id)\\n+ if key in selected_keys:\\n+ continue\\n+ selected.append(model)\\n+ if len(selected) >= limit:\\n+ break\\n+ return selected\\n+\\n+\\n+def _active_agent_from_discovered(model: DiscoveredModel) -> ModelAgent:\\n+ \\\"\\\"\\\"Convert one selected chat model into an enabled capability-neutral agent.\\\"\\\"\\\"\\n+ return replace(\\n+ agent_from_discovered(model),\\n+ disabled=False,\\n+ tags=serving_tags_for_discovered(model),\\n+ )\\n+\\n+\\n+def _synchronize_durable_agent_pool(\\n+ agents_db: str,\\n+ selected: Sequence[DiscoveredModel],\\n+) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Activate exactly the selected discovered models in one durable agent pool.\\\"\\\"\\\"\\n+ bootstrap = TaskOrchestrator(\\n+ [ModelAgent(\\\"bootstrap_agent\\\", \\\"bootstrap-model\\\")],\\n+ agents_db=agents_db,\\n+ )\\n+ agents = [_active_agent_from_discovered(model) for model in selected]\\n+ selected_ids = {agent.id for agent in agents}\\n+ bootstrap.sync_discovered_agents(agents)\\n+\\n+ for candidate in list(bootstrap.candidates):\\n+ if candidate.id in selected_ids:\\n+ continue\\n+ if candidate.id == \\\"bootstrap_agent\\\" or \\\"discovered\\\" in candidate.tags:\\n+ if not candidate.disabled:\\n+ bootstrap.remove_agent(\\\"default\\\", candidate.id)\\n+\\n+ for agent in agents:\\n+ bootstrap.patch_agent(\\\"default\\\", agent.id, {\\\"status\\\": \\\"active\\\"})\\n+\\n+ enabled = tuple(\\n+ sorted(agent.id for agent in bootstrap.agents if agent.id in selected_ids)\\n+ )\\n+ if set(enabled) != selected_ids:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap could not activate the selected agent pool\\\"\\n+ )\\n+ return enabled\\n+\\n+\\n+def bootstrap_provider_runtime(\\n+ *,\\n+ environ: Mapping[str, str],\\n+ require_all_credentials: bool = True,\\n+ agents_db: str | None = None,\\n+ model_limit: int = 16,\\n+) -> ProviderBootstrapReport:\\n+ \\\"\\\"\\\"Register trusted secrets, discover chat models, and optionally activate a pool.\\\"\\\"\\\"\\n+ credentials = collect_provider_credentials(\\n+ environ, require_all=require_all_credentials\\n+ )\\n+ registered = register_provider_credentials_atomically(credentials)\\n+ discovered, errors = discover_all_models()\\n+ if not discovered:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap discovered no usable models\\\"\\n+ )\\n+\\n+ eligible = [model for model in discovered if is_chat_serving_candidate(model)]\\n+ if not eligible:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap discovered no chat-capable models\\\"\\n+ )\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ priced_count = refresh_price_book(discovered, price_book)\\n+ selected = select_provider_diverse_models(eligible, limit=model_limit)\\n+ if not selected:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap selected no chat-capable models\\\"\\n+ )\\n+ selected_ids = tuple(agent_id_for(model) for model in selected)\\n+ enabled_ids = (\\n+ _synchronize_durable_agent_pool(agents_db, selected)\\n+ if agents_db\\n+ else ()\\n+ )\\n+\\n+ return ProviderBootstrapReport(\\n+ registered_credentials=registered,\\n+ discovered_model_count=len(discovered),\\n+ eligible_model_count=len(eligible),\\n+ selected_agent_ids=selected_ids,\\n+ enabled_agent_ids=enabled_ids,\\n+ durable_agent_pool=bool(agents_db),\\n+ providers_with_errors=tuple(\\n+ sorted({error.provider_name for error in errors})\\n+ ),\\n+ priced_model_count=priced_count,\\n+ )\\n+\\n+\\n+def main(argv: Sequence[str] | None = None) -> None:\\n+ \\\"\\\"\\\"Run the one-shot provider bootstrap command used by trusted deployment jobs.\\\"\\\"\\\"\\n+ parser = argparse.ArgumentParser(\\n+ description=\\\"Register provider secrets and refresh the runtime model pool.\\\"\\n+ )\\n+ parser.add_argument(\\n+ \\\"--agents-db\\\",\\n+ default=os.environ.get(\\\"CONTEXTUAL_ORCHESTRATOR_AGENTS_DB\\\") or None,\\n+ )\\n+ parser.add_argument(\\\"--model-limit\\\", type=int, default=16)\\n+ parser.add_argument(\\n+ \\\"--allow-partial-credentials\\\",\\n+ action=\\\"store_true\\\",\\n+ help=\\\"Permit a subset of the fixed provider inventory (development only).\\\",\\n+ )\\n+ args = parser.parse_args(list(argv) if argv is not None else None)\\n+ report = bootstrap_provider_runtime(\\n+ environ=os.environ,\\n+ require_all_credentials=not args.allow_partial_credentials,\\n+ agents_db=args.agents_db,\\n+ model_limit=args.model_limit,\\n+ )\\n+ print(json.dumps(report.as_dict(), ensure_ascii=False, sort_keys=True))\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover - subprocess/CLI coverage\\n+ main()\" }, { \"sha\": \"2253caef562a6275371ebcb9700add802ee39ed9\", \"filename\": \"contextual_orchestrator/provider_catalog_bootstrap.py\", \"status\": \"added\", \"additions\": 378, \"deletions\": 0, \"changes\": 378, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fprovider_catalog_bootstrap.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fprovider_catalog_bootstrap.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fprovider_catalog_bootstrap.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,378 @@\\n+\\\"\\\"\\\"Trusted provider bootstrap with durable normalized model-catalog persistence.\\n+\\n+This command registers the complete credential inventory, performs provider-\\n+isolated discovery, persists successful model metadata in PostgreSQL, retains\\n+last-known-good models for failed providers, and constructs a bounded candidate\\n+pool from the persisted catalog.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import argparse\\n+from dataclasses import dataclass\\n+import json\\n+import os\\n+from typing import Callable, Mapping, Sequence\\n+\\n+from .cost_ledger import PriceBook\\n+from .credentials import (\\n+ InMemoryCredentialBackend,\\n+ PostgresCredentialBackend,\\n+ get_backend,\\n+ get_credential,\\n+)\\n+from .kv_config import InMemoryConfigStore\\n+from .model_discovery import (\\n+ DiscoveredModel,\\n+ PROVIDER_MODEL_SOURCES,\\n+ ProviderDiscoveryError,\\n+ ProviderModelSource,\\n+ agent_id_for,\\n+ discover_all_models,\\n+ refresh_price_book,\\n+)\\n+from .provider_bootstrap import (\\n+ ProviderBootstrapError,\\n+ _synchronize_durable_agent_pool,\\n+ collect_provider_credentials,\\n+ is_chat_serving_candidate,\\n+ register_provider_credentials_atomically,\\n+ select_provider_diverse_models,\\n+ serving_tags_for_discovered,\\n+)\\n+from .provider_catalog_store import (\\n+ InMemoryProviderCatalogStore,\\n+ PostgresProviderCatalogStore,\\n+ ProviderCatalogStore,\\n+)\\n+\\n+\\n+@dataclass(frozen=True)\\n+class ProviderCatalogSnapshot:\\n+ \\\"\\\"\\\"Effective persisted model snapshot after provider-isolated refresh.\\\"\\\"\\\"\\n+\\n+ models: tuple[DiscoveredModel, ...]\\n+ live_model_count: int\\n+ last_known_good_model_count: int\\n+ refresh_failure_count: int\\n+ providers_with_errors: tuple[str, ...]\\n+\\n+\\n+@dataclass(frozen=True)\\n+class ProviderCatalogBootstrapReport:\\n+ \\\"\\\"\\\"Secret-free evidence for one durable provider-catalog bootstrap.\\n+\\n+ ``registered_credentials`` contains the credential names that remain in the\\n+ credential registry after provider-isolated rollback has completed. It is\\n+ therefore safe for a workflow to use as durable-registration evidence.\\n+ \\\"\\\"\\\"\\n+\\n+ registered_credentials: tuple[str, ...]\\n+ restored_credentials: tuple[str, ...]\\n+ live_discovered_model_count: int\\n+ catalog_model_count: int\\n+ eligible_model_count: int\\n+ last_known_good_model_count: int\\n+ selected_agent_ids: tuple[str, ...]\\n+ enabled_agent_ids: tuple[str, ...]\\n+ durable_agent_pool: bool\\n+ catalog_backend: str\\n+ catalog_refresh_failure_count: int\\n+ providers_with_errors: tuple[str, ...]\\n+ priced_model_count: int\\n+\\n+ def as_dict(self) -> dict[str, object]:\\n+ \\\"\\\"\\\"Return the stable JSON evidence contract without secret values.\\\"\\\"\\\"\\n+ return {\\n+ \\\"registered_credentials\\\": list(self.registered_credentials),\\n+ \\\"restored_credentials\\\": list(self.restored_credentials),\\n+ \\\"live_discovered_model_count\\\": self.live_discovered_model_count,\\n+ \\\"catalog_model_count\\\": self.catalog_model_count,\\n+ \\\"eligible_model_count\\\": self.eligible_model_count,\\n+ \\\"last_known_good_model_count\\\": self.last_known_good_model_count,\\n+ \\\"selected_agent_ids\\\": list(self.selected_agent_ids),\\n+ \\\"enabled_agent_ids\\\": list(self.enabled_agent_ids),\\n+ \\\"durable_agent_pool\\\": self.durable_agent_pool,\\n+ \\\"catalog_backend\\\": self.catalog_backend,\\n+ \\\"catalog_refresh_failure_count\\\": self.catalog_refresh_failure_count,\\n+ \\\"providers_with_errors\\\": list(self.providers_with_errors),\\n+ \\\"priced_model_count\\\": self.priced_model_count,\\n+ }\\n+\\n+\\n+def build_provider_catalog_store() -> ProviderCatalogStore:\\n+ \\\"\\\"\\\"Build a catalog store colocated with the active credential backend.\\\"\\\"\\\"\\n+ backend = get_backend()\\n+ if isinstance(backend, PostgresCredentialBackend):\\n+ return PostgresProviderCatalogStore(backend.connection_dsn)\\n+ if isinstance(backend, InMemoryCredentialBackend):\\n+ return InMemoryProviderCatalogStore()\\n+ raise ProviderBootstrapError(\\n+ \\\"provider catalog requires a built-in atomic credential backend\\\"\\n+ )\\n+\\n+\\n+def _restore_provider_credentials_atomically(\\n+ previous_credentials: Mapping[str, str | None],\\n+) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Restore one credential snapshot in a single built-in backend transaction.\\\"\\\"\\\"\\n+ backend = get_backend()\\n+ ordered = tuple(sorted(previous_credentials))\\n+ if isinstance(backend, InMemoryCredentialBackend):\\n+ with backend._lock: # noqa: SLF001 - package-internal rollback transaction\\n+ for name in ordered:\\n+ previous = previous_credentials[name]\\n+ if previous is None:\\n+ backend._store.pop(name, None) # noqa: SLF001\\n+ else:\\n+ backend._store[name] = previous # noqa: SLF001\\n+ return ordered\\n+ if isinstance(backend, PostgresCredentialBackend):\\n+ with backend._connect() as connection: # noqa: SLF001 - package transaction\\n+ backend._ensure_schema(connection) # noqa: SLF001\\n+ with connection.cursor() as cursor:\\n+ for name in ordered:\\n+ previous = previous_credentials[name]\\n+ if previous is None:\\n+ cursor.execute(\\n+ \\\"DELETE FROM provider_credentials WHERE credential_name = %s\\\",\\n+ (name,),\\n+ )\\n+ else:\\n+ cursor.execute(\\n+ \\\"INSERT INTO provider_credentials \\\"\\n+ \\\"(credential_name, encrypted_value, updated_at) \\\"\\n+ \\\"VALUES (%s, pgp_sym_encrypt(%s, %s), now()) \\\"\\n+ \\\"ON CONFLICT (credential_name) DO UPDATE SET \\\"\\n+ \\\"encrypted_value = EXCLUDED.encrypted_value, updated_at = now()\\\",\\n+ (name, previous, backend._passphrase), # noqa: SLF001\\n+ )\\n+ connection.commit()\\n+ return ordered\\n+ raise ProviderBootstrapError(\\n+ \\\"provider credential rollback requires an atomic built-in backend\\\"\\n+ )\\n+\\n+\\n+def _source_key(source: ProviderModelSource) -> tuple[str, str]:\\n+ \\\"\\\"\\\"Return the provider-account key shared by sources and model rows.\\\"\\\"\\\"\\n+ return (source.provider_name, source.credential_name)\\n+\\n+\\n+def _model_key(model: DiscoveredModel) -> tuple[str, str]:\\n+ \\\"\\\"\\\"Return the provider-account key carried by one discovered model.\\\"\\\"\\\"\\n+ return (model.provider_name, model.credential_name)\\n+\\n+\\n+def refresh_persisted_provider_catalog(\\n+ store: ProviderCatalogStore,\\n+ *,\\n+ sources: Sequence[ProviderModelSource],\\n+ registered_credentials: Sequence[str],\\n+ discovered: Sequence[DiscoveredModel],\\n+ errors: Sequence[ProviderDiscoveryError],\\n+) -> ProviderCatalogSnapshot:\\n+ \\\"\\\"\\\"Persist account-local refreshes and return the effective LKG snapshot.\\\"\\\"\\\"\\n+ registered = set(registered_credentials)\\n+ live_by_account: dict[tuple[str, str], list[DiscoveredModel]] = {}\\n+ for model in discovered:\\n+ live_by_account.setdefault(_model_key(model), []).append(model)\\n+\\n+ failed_names = {error.provider_name for error in errors}\\n+ effective: list[DiscoveredModel] = []\\n+ last_known_good_count = 0\\n+ refresh_failures = 0\\n+ providers_with_errors: set[str] = set(failed_names)\\n+\\n+ for source in sources:\\n+ if source.credential_name not in registered:\\n+ continue\\n+ account_models = live_by_account.get(_source_key(source), [])\\n+ failed = source.provider_name in failed_names\\n+ if failed:\\n+ store.record_failure(source, error_code=\\\"provider_discovery_error\\\")\\n+ refresh_failures += 1\\n+ elif not account_models:\\n+ store.record_failure(source, error_code=\\\"empty_provider_catalog\\\")\\n+ refresh_failures += 1\\n+ providers_with_errors.add(source.provider_name)\\n+ else:\\n+ eligible_ids = {\\n+ model.model_id\\n+ for model in account_models\\n+ if is_chat_serving_candidate(model)\\n+ }\\n+ tags = {\\n+ model.model_id: serving_tags_for_discovered(model)\\n+ for model in account_models\\n+ if model.model_id in eligible_ids\\n+ }\\n+ store.record_success(\\n+ source,\\n+ account_models,\\n+ eligible_model_ids=eligible_ids,\\n+ serving_tags=tags,\\n+ )\\n+\\n+ persisted = store.serving_models(source)\\n+ effective.extend(persisted)\\n+ if failed or not account_models:\\n+ last_known_good_count += len(persisted)\\n+\\n+ unique: dict[tuple[str, str, str], DiscoveredModel] = {}\\n+ for model in effective:\\n+ unique[(model.provider_name, model.credential_name, model.model_id)] = model\\n+ ordered = tuple(unique[key] for key in sorted(unique))\\n+ return ProviderCatalogSnapshot(\\n+ models=ordered,\\n+ live_model_count=len(discovered),\\n+ last_known_good_model_count=last_known_good_count,\\n+ refresh_failure_count=refresh_failures,\\n+ providers_with_errors=tuple(sorted(providers_with_errors)),\\n+ )\\n+\\n+\\n+DiscoveryFunction = Callable[\\n+ [tuple[ProviderModelSource, ...]],\\n+ tuple[list[DiscoveredModel], list[ProviderDiscoveryError]],\\n+]\\n+\\n+\\n+def bootstrap_provider_catalog_runtime(\\n+ *,\\n+ environ: Mapping[str, str],\\n+ require_all_credentials: bool = True,\\n+ agents_db: str | None = None,\\n+ model_limit: int = 16,\\n+ catalog_store: ProviderCatalogStore | None = None,\\n+ sources: Sequence[ProviderModelSource] = PROVIDER_MODEL_SOURCES,\\n+ discovery: DiscoveryFunction | None = None,\\n+) -> ProviderCatalogBootstrapReport:\\n+ \\\"\\\"\\\"Register secrets, persist catalogs, and build the effective serving pool.\\\"\\\"\\\"\\n+ credentials = collect_provider_credentials(\\n+ environ,\\n+ require_all=require_all_credentials,\\n+ )\\n+ previous_credentials = {\\n+ name: get_credential(name) for name in credentials\\n+ }\\n+ registered = register_provider_credentials_atomically(credentials)\\n+ try:\\n+ store = catalog_store or build_provider_catalog_store()\\n+ source_tuple = tuple(sources)\\n+ discover = discovery or (\\n+ lambda requested_sources: discover_all_models(requested_sources)\\n+ )\\n+ live_models, errors = discover(source_tuple)\\n+ snapshot = refresh_persisted_provider_catalog(\\n+ store,\\n+ sources=source_tuple,\\n+ registered_credentials=registered,\\n+ discovered=live_models,\\n+ errors=errors,\\n+ )\\n+ failed_provider_names = {error.provider_name for error in errors}\\n+ failed_credentials = {\\n+ source.credential_name\\n+ for source in source_tuple\\n+ if source.credential_name in registered\\n+ and (\\n+ source.provider_name in failed_provider_names\\n+ or not any(\\n+ _model_key(model) == _source_key(source)\\n+ for model in live_models\\n+ )\\n+ )\\n+ }\\n+ restored_credentials = _restore_provider_credentials_atomically(\\n+ {\\n+ name: previous_credentials.get(name)\\n+ for name in failed_credentials\\n+ }\\n+ ) if failed_credentials else ()\\n+\\n+ usable_models = tuple(\\n+ model\\n+ for model in snapshot.models\\n+ if get_credential(model.credential_name)\\n+ )\\n+ if not usable_models:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap has no persisted chat-compatible model with a usable credential\\\"\\n+ )\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ priced_count = refresh_price_book(list(usable_models), price_book)\\n+ selected = select_provider_diverse_models(\\n+ usable_models,\\n+ limit=model_limit,\\n+ )\\n+ if not selected:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap selected no persisted chat-compatible model\\\"\\n+ )\\n+ selected_ids = tuple(agent_id_for(model) for model in selected)\\n+ enabled_ids = (\\n+ _synchronize_durable_agent_pool(agents_db, selected)\\n+ if agents_db\\n+ else ()\\n+ )\\n+ durable_registered_credentials = tuple(\\n+ name for name in registered if get_credential(name) is not None\\n+ )\\n+\\n+ return ProviderCatalogBootstrapReport(\\n+ registered_credentials=durable_registered_credentials,\\n+ restored_credentials=tuple(restored_credentials),\\n+ live_discovered_model_count=snapshot.live_model_count,\\n+ catalog_model_count=len(snapshot.models),\\n+ eligible_model_count=len(snapshot.models),\\n+ last_known_good_model_count=snapshot.last_known_good_model_count,\\n+ selected_agent_ids=selected_ids,\\n+ enabled_agent_ids=enabled_ids,\\n+ durable_agent_pool=bool(agents_db),\\n+ catalog_backend=store.backend_name,\\n+ catalog_refresh_failure_count=snapshot.refresh_failure_count,\\n+ providers_with_errors=snapshot.providers_with_errors,\\n+ priced_model_count=priced_count,\\n+ )\\n+ except Exception:\\n+ try:\\n+ _restore_provider_credentials_atomically(previous_credentials)\\n+ except Exception as rollback_error:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap failed and credential rollback could not complete\\\"\\n+ ) from rollback_error\\n+ raise\\n+\\n+\\n+def main(argv: Sequence[str] | None = None) -> None:\\n+ \\\"\\\"\\\"Run trusted durable provider bootstrap and print secret-free evidence.\\\"\\\"\\\"\\n+ parser = argparse.ArgumentParser(\\n+ description=(\\n+ \\\"Register provider secrets, persist provider models, and refresh \\\"\\n+ \\\"the effective serving pool.\\\"\\n+ )\\n+ )\\n+ parser.add_argument(\\n+ \\\"--agents-db\\\",\\n+ default=os.environ.get(\\\"CONTEXTUAL_ORCHESTRATOR_AGENTS_DB\\\") or None,\\n+ )\\n+ parser.add_argument(\\\"--model-limit\\\", type=int, default=16)\\n+ parser.add_argument(\\n+ \\\"--allow-partial-credentials\\\",\\n+ action=\\\"store_true\\\",\\n+ help=\\\"Permit a subset of the fixed provider inventory (development only).\\\",\\n+ )\\n+ args = parser.parse_args(list(argv) if argv is not None else None)\\n+ report = bootstrap_provider_catalog_runtime(\\n+ environ=os.environ,\\n+ require_all_credentials=not args.allow_partial_credentials,\\n+ agents_db=args.agents_db,\\n+ model_limit=args.model_limit,\\n+ )\\n+ print(json.dumps(report.as_dict(), ensure_ascii=False, sort_keys=True))\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover - subprocess/CLI boundary\\n+ main()\" }, { \"sha\": \"39511e7ceb5798643e8ab9c043498534f4ed30de\", \"filename\": \"contextual_orchestrator/provider_catalog_store.py\", \"status\": \"added\", \"additions\": 634, \"deletions\": 0, \"changes\": 634, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fprovider_catalog_store.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fprovider_catalog_store.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fprovider_catalog_store.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,634 @@\\n+\\\"\\\"\\\"Normalized durable provider-model catalog persistence.\\n+\\n+This module owns provider-account/model metadata persistence and last-known-good\\n+refresh behavior. It never performs network I/O and never stores credential\\n+values. Discovery transport remains in ``model_discovery``; runtime selection\\n+remains in the ordinary orchestrator.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from dataclasses import dataclass\\n+from datetime import datetime, timezone\\n+from decimal import Decimal\\n+import hashlib\\n+import math\\n+import re\\n+import threading\\n+import uuid\\n+from typing import Callable, Mapping, Protocol, Sequence\\n+\\n+from .model_discovery import DiscoveredModel, ProviderModelSource\\n+\\n+\\n+PROVIDER_CATALOG_SCHEMA_SQL = \\\"\\\"\\\"\\n+CREATE TABLE IF NOT EXISTS provider_account (\\n+ provider_account_id text PRIMARY KEY,\\n+ provider_name text NOT NULL,\\n+ credential_name text NOT NULL,\\n+ list_url text NOT NULL,\\n+ chat_base_url text NOT NULL,\\n+ auth_scheme text NOT NULL,\\n+ discovery_style text NOT NULL,\\n+ task_filter text NOT NULL,\\n+ enabled_flag boolean NOT NULL DEFAULT true,\\n+ created_at timestamptz NOT NULL DEFAULT now(),\\n+ updated_at timestamptz NOT NULL DEFAULT now(),\\n+ UNIQUE (provider_name, credential_name)\\n+);\\n+\\n+CREATE TABLE IF NOT EXISTS provider_model (\\n+ provider_model_id text PRIMARY KEY,\\n+ provider_account_id text NOT NULL\\n+ REFERENCES provider_account(provider_account_id) ON DELETE CASCADE,\\n+ model_name text NOT NULL,\\n+ prompt_price_per_1k numeric(20, 8),\\n+ completion_price_per_1k numeric(20, 8),\\n+ currency_code text NOT NULL,\\n+ serving_eligible_flag boolean NOT NULL DEFAULT false,\\n+ enabled_flag boolean NOT NULL DEFAULT true,\\n+ first_seen_at timestamptz NOT NULL,\\n+ last_seen_at timestamptz NOT NULL,\\n+ UNIQUE (provider_account_id, model_name)\\n+);\\n+\\n+CREATE TABLE IF NOT EXISTS model_serving_tag (\\n+ provider_model_id text NOT NULL\\n+ REFERENCES provider_model(provider_model_id) ON DELETE CASCADE,\\n+ tag_name text NOT NULL,\\n+ PRIMARY KEY (provider_model_id, tag_name)\\n+);\\n+\\n+CREATE TABLE IF NOT EXISTS catalog_refresh_run (\\n+ catalog_refresh_run_id text PRIMARY KEY,\\n+ provider_account_id text NOT NULL\\n+ REFERENCES provider_account(provider_account_id) ON DELETE CASCADE,\\n+ refresh_status text NOT NULL,\\n+ observed_model_count integer NOT NULL DEFAULT 0,\\n+ eligible_model_count integer NOT NULL DEFAULT 0,\\n+ error_code text,\\n+ started_at timestamptz NOT NULL,\\n+ finished_at timestamptz NOT NULL\\n+);\\n+\\n+CREATE INDEX IF NOT EXISTS provider_model_account_idx\\n+ ON provider_model (provider_account_id, enabled_flag, serving_eligible_flag);\\n+CREATE INDEX IF NOT EXISTS catalog_refresh_account_idx\\n+ ON catalog_refresh_run (provider_account_id, finished_at DESC);\\n+\\\"\\\"\\\"\\n+\\\"\\\"\\\"Third-normal-form schema for provider accounts, models, tags, and refreshes.\\\"\\\"\\\"\\n+\\n+\\n+class ProviderCatalogError(RuntimeError):\\n+ \\\"\\\"\\\"Raised when durable provider catalog metadata cannot be persisted or read.\\\"\\\"\\\"\\n+\\n+\\n+@dataclass(frozen=True)\\n+class CatalogRefreshEvidence:\\n+ \\\"\\\"\\\"Secret-free evidence for one provider-account catalog refresh.\\\"\\\"\\\"\\n+\\n+ provider_account_id: str\\n+ refresh_status: str\\n+ observed_model_count: int\\n+ eligible_model_count: int\\n+ error_code: str | None\\n+ started_at: datetime\\n+ finished_at: datetime\\n+\\n+\\n+class ProviderCatalogStore(Protocol):\\n+ \\\"\\\"\\\"Persistence boundary for provider model metadata and last-known-good rows.\\\"\\\"\\\"\\n+\\n+ @property\\n+ def backend_name(self) -> str:\\n+ \\\"\\\"\\\"Return a stable backend name for secret-free operator evidence.\\\"\\\"\\\"\\n+ ...\\n+\\n+ def record_success(\\n+ self,\\n+ source: ProviderModelSource,\\n+ models: Sequence[DiscoveredModel],\\n+ *,\\n+ eligible_model_ids: set[str],\\n+ serving_tags: Mapping[str, tuple[str, ...]],\\n+ ) -> None:\\n+ \\\"\\\"\\\"Replace one provider account's current catalog atomically.\\\"\\\"\\\"\\n+ ...\\n+\\n+ def record_failure(\\n+ self,\\n+ source: ProviderModelSource,\\n+ *,\\n+ error_code: str,\\n+ ) -> None:\\n+ \\\"\\\"\\\"Record failure without changing last-known-good enabled models.\\\"\\\"\\\"\\n+ ...\\n+\\n+ def serving_models(\\n+ self,\\n+ source: ProviderModelSource,\\n+ ) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Return enabled, serving-eligible last-known-good models.\\\"\\\"\\\"\\n+ ...\\n+\\n+ def refresh_evidence(self) -> tuple[CatalogRefreshEvidence, ...]:\\n+ \\\"\\\"\\\"Return refresh evidence in insertion order.\\\"\\\"\\\"\\n+ ...\\n+\\n+\\n+_SLUG_RE = re.compile(r\\\"[^a-z0-9]+\\\")\\n+_CURRENCY_RE = re.compile(r\\\"^[A-Z]{3}$\\\")\\n+_ALLOWED_REFRESH_ERROR_CODES = frozenset(\\n+ {\\\"provider_discovery_error\\\", \\\"empty_provider_catalog\\\", \\\"unknown_error\\\"}\\n+)\\n+\\n+\\n+def provider_account_id(source: ProviderModelSource) -> str:\\n+ \\\"\\\"\\\"Return a stable two-or-more-word snake-case provider account ID.\\\"\\\"\\\"\\n+ provider = _SLUG_RE.sub(\\\"_\\\", source.provider_name.casefold()).strip(\\\"_\\\")\\n+ credential = _SLUG_RE.sub(\\\"_\\\", source.credential_name.casefold()).strip(\\\"_\\\")\\n+ if not provider or not credential:\\n+ raise ProviderCatalogError(\\\"provider account identity is incomplete\\\")\\n+ return f\\\"{provider}_{credential}\\\"\\n+\\n+\\n+def provider_model_id(source: ProviderModelSource, model_name: str) -> str:\\n+ \\\"\\\"\\\"Return a stable opaque ID for one account-scoped model name.\\\"\\\"\\\"\\n+ normalized = model_name.strip()\\n+ if not normalized:\\n+ raise ProviderCatalogError(\\\"provider model name is empty\\\")\\n+ digest = hashlib.sha256(\\n+ f\\\"{provider_account_id(source)}\\\\0{normalized}\\\".encode(\\\"utf-8\\\")\\n+ ).hexdigest()\\n+ return f\\\"provider_model_{digest[:32]}\\\"\\n+\\n+\\n+def _now() -> datetime:\\n+ \\\"\\\"\\\"Return a timezone-aware UTC timestamp.\\\"\\\"\\\"\\n+ return datetime.now(timezone.utc)\\n+\\n+\\n+def _normalize_price(value: object) -> float | None:\\n+ \\\"\\\"\\\"Return one finite non-negative price, or ``None`` when unknown, underflowed, or overflowed.\\n+\\n+ Parses through ``Decimal`` first so a nonzero price that underflows to\\n+ ``0.0`` in float (e.g. a stray ``1e-10000``) is rejected as unknown\\n+ rather than silently accepted as a legitimate free price. A ``Decimal``\\n+ can still be finite while its ``float()`` conversion overflows to\\n+ ``inf`` (e.g. ``1e10000``), so ``math.isfinite`` is checked separately\\n+ on the converted value.\\n+ \\\"\\\"\\\"\\n+ if value is None or isinstance(value, bool):\\n+ return None\\n+ try:\\n+ decimal_value = Decimal(str(value))\\n+ number = float(decimal_value)\\n+ except (ArithmeticError, TypeError, ValueError):\\n+ return None\\n+ if (\\n+ not decimal_value.is_finite()\\n+ or not math.isfinite(number)\\n+ or decimal_value < 0\\n+ or (decimal_value != 0 and number == 0)\\n+ ):\\n+ return None\\n+ return number\\n+\\n+\\n+_UNKNOWN_CURRENCY = \\\"UNKNOWN\\\"\\n+\\n+\\n+def _normalize_currency(value: object) -> str:\\n+ \\\"\\\"\\\"Return an ISO-style three-letter currency code, or an explicit unknown marker.\\n+\\n+ An unrecognized currency must never collapse to ``USD`` by default: doing\\n+ so would let a priced model with an unverified currency rank as a\\n+ comparable USD cost. ``_UNKNOWN_CURRENCY`` deliberately fails\\n+ ``_currency_is_comparable`` against every real default currency.\\n+ \\\"\\\"\\\"\\n+ if not isinstance(value, str):\\n+ return _UNKNOWN_CURRENCY\\n+ normalized = value.strip().upper()\\n+ return normalized if _CURRENCY_RE.fullmatch(normalized) else _UNKNOWN_CURRENCY\\n+\\n+\\n+def _normalize_error_code(value: object) -> str:\\n+ \\\"\\\"\\\"Return one approved secret-free provider refresh failure code.\\\"\\\"\\\"\\n+ if not isinstance(value, str):\\n+ return \\\"unknown_error\\\"\\n+ normalized = value.strip().casefold()\\n+ return normalized if normalized in _ALLOWED_REFRESH_ERROR_CODES else \\\"unknown_error\\\"\\n+\\n+\\n+def _normalize_tags(tags: Sequence[str]) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Return deterministic, valid, duplicate-free serving tags.\\\"\\\"\\\"\\n+ normalized: list[str] = []\\n+ for raw in tags:\\n+ if not isinstance(raw, str):\\n+ continue\\n+ tag = raw.strip().casefold()\\n+ if not tag or not re.fullmatch(r\\\"[a-z][a-z0-9_]*\\\", tag):\\n+ continue\\n+ if tag not in normalized:\\n+ normalized.append(tag)\\n+ return tuple(normalized)\\n+\\n+\\n+def normalize_discovered_model(\\n+ source: ProviderModelSource,\\n+ model: DiscoveredModel,\\n+) -> DiscoveredModel:\\n+ \\\"\\\"\\\"Normalize one discovered row and enforce its provider-account identity.\\\"\\\"\\\"\\n+ name = model.model_id.strip() if isinstance(model.model_id, str) else \\\"\\\"\\n+ if not name:\\n+ raise ProviderCatalogError(\\\"provider model name is empty\\\")\\n+ if (\\n+ model.provider_name != source.provider_name\\n+ or model.credential_name != source.credential_name\\n+ ):\\n+ raise ProviderCatalogError(\\\"provider model belongs to a different account\\\")\\n+ return DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=name,\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ prompt_price_per_1k=_normalize_price(model.prompt_price_per_1k),\\n+ completion_price_per_1k=_normalize_price(\\n+ model.completion_price_per_1k\\n+ ),\\n+ currency_code=_normalize_currency(model.currency_code),\\n+ )\\n+\\n+\\n+def _deduplicate_models(\\n+ source: ProviderModelSource,\\n+ models: Sequence[DiscoveredModel],\\n+) -> dict[str, DiscoveredModel]:\\n+ \\\"\\\"\\\"Normalize and deterministically deduplicate account-scoped models.\\\"\\\"\\\"\\n+ result: dict[str, DiscoveredModel] = {}\\n+ for model in models:\\n+ normalized = normalize_discovered_model(source, model)\\n+ result[normalized.model_id] = normalized\\n+ return result\\n+\\n+\\n+class InMemoryProviderCatalogStore:\\n+ \\\"\\\"\\\"Thread-safe deterministic provider catalog for tests and standalone use.\\\"\\\"\\\"\\n+\\n+ def __init__(self) -> None:\\n+ self._accounts: dict[str, ProviderModelSource] = {}\\n+ self._models: dict[str, dict[str, DiscoveredModel]] = {}\\n+ self._eligible: dict[str, set[str]] = {}\\n+ self._tags: dict[tuple[str, str], tuple[str, ...]] = {}\\n+ self._refreshes: list[CatalogRefreshEvidence] = []\\n+ self._lock = threading.RLock()\\n+\\n+ @property\\n+ def backend_name(self) -> str:\\n+ \\\"\\\"\\\"Return the stable in-memory backend name.\\\"\\\"\\\"\\n+ return \\\"memory\\\"\\n+\\n+ def record_success(\\n+ self,\\n+ source: ProviderModelSource,\\n+ models: Sequence[DiscoveredModel],\\n+ *,\\n+ eligible_model_ids: set[str],\\n+ serving_tags: Mapping[str, tuple[str, ...]],\\n+ ) -> None:\\n+ \\\"\\\"\\\"Replace one in-memory account catalog.\\\"\\\"\\\"\\n+ normalized = _deduplicate_models(source, models)\\n+ if not normalized:\\n+ raise ProviderCatalogError(\\\"successful provider refresh cannot be empty\\\")\\n+ account_id = provider_account_id(source)\\n+ started_at = _now()\\n+ eligible = set(normalized).intersection(eligible_model_ids)\\n+ with self._lock:\\n+ self._accounts[account_id] = source\\n+ self._models[account_id] = normalized\\n+ self._eligible[account_id] = eligible\\n+ for key in [key for key in self._tags if key[0] == account_id]:\\n+ del self._tags[key]\\n+ for model_name in eligible:\\n+ self._tags[(account_id, model_name)] = _normalize_tags(\\n+ serving_tags.get(model_name, ())\\n+ )\\n+ self._refreshes.append(\\n+ CatalogRefreshEvidence(\\n+ account_id,\\n+ \\\"succeeded\\\",\\n+ len(normalized),\\n+ len(eligible),\\n+ None,\\n+ started_at,\\n+ _now(),\\n+ )\\n+ )\\n+\\n+ def record_failure(\\n+ self,\\n+ source: ProviderModelSource,\\n+ *,\\n+ error_code: str,\\n+ ) -> None:\\n+ \\\"\\\"\\\"Record a stable failure without mutating last-known-good models.\\\"\\\"\\\"\\n+ account_id = provider_account_id(source)\\n+ started_at = _now()\\n+ stable_code = _normalize_error_code(error_code)\\n+ with self._lock:\\n+ self._accounts[account_id] = source\\n+ self._refreshes.append(\\n+ CatalogRefreshEvidence(\\n+ account_id,\\n+ \\\"failed\\\",\\n+ 0,\\n+ 0,\\n+ stable_code,\\n+ started_at,\\n+ _now(),\\n+ )\\n+ )\\n+\\n+ def serving_models(\\n+ self,\\n+ source: ProviderModelSource,\\n+ ) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Return deterministic serving models for one account.\\\"\\\"\\\"\\n+ account_id = provider_account_id(source)\\n+ with self._lock:\\n+ models = self._models.get(account_id, {})\\n+ eligible = self._eligible.get(account_id, set())\\n+ return [models[name] for name in sorted(eligible) if name in models]\\n+\\n+ def serving_tags(\\n+ self,\\n+ source: ProviderModelSource,\\n+ model_name: str,\\n+ ) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Return persisted generic serving tags for one model.\\\"\\\"\\\"\\n+ with self._lock:\\n+ return self._tags.get((provider_account_id(source), model_name), ())\\n+\\n+ def refresh_evidence(self) -> tuple[CatalogRefreshEvidence, ...]:\\n+ \\\"\\\"\\\"Return immutable refresh evidence in insertion order.\\\"\\\"\\\"\\n+ with self._lock:\\n+ return tuple(self._refreshes)\\n+\\n+\\n+class PostgresProviderCatalogStore:\\n+ \\\"\\\"\\\"PostgreSQL provider catalog sharing the credential registry database.\\\"\\\"\\\"\\n+\\n+ def __init__(\\n+ self,\\n+ dsn: str,\\n+ *,\\n+ connection_factory: Callable[[], object] | None = None,\\n+ ) -> None:\\n+ if not isinstance(dsn, str) or not dsn.strip():\\n+ raise ProviderCatalogError(\\\"provider catalog requires a PostgreSQL DSN\\\")\\n+ self._dsn = dsn\\n+ self._connection_factory = connection_factory\\n+ self._schema_ready = False\\n+ self._schema_lock = threading.Lock()\\n+ self._evidence: list[CatalogRefreshEvidence] = []\\n+\\n+ @property\\n+ def backend_name(self) -> str:\\n+ \\\"\\\"\\\"Return the stable PostgreSQL backend name.\\\"\\\"\\\"\\n+ return \\\"postgres\\\"\\n+\\n+ def _connect(self):\\n+ \\\"\\\"\\\"Open one catalog connection through the injected or psycopg factory.\\\"\\\"\\\"\\n+ if self._connection_factory is not None:\\n+ return self._connection_factory()\\n+ try:\\n+ import psycopg\\n+ except ImportError as exc: # pragma: no cover - packaging boundary\\n+ raise ProviderCatalogError(\\n+ \\\"provider catalog requires contextual-orchestrator[db]\\\"\\n+ ) from exc\\n+ return psycopg.connect(self._dsn) # pragma: no cover - live database\\n+\\n+ def _ensure_schema(self, connection: object) -> None:\\n+ \\\"\\\"\\\"Create normalized catalog objects once per store instance.\\\"\\\"\\\"\\n+ if self._schema_ready:\\n+ return\\n+ with self._schema_lock:\\n+ if self._schema_ready:\\n+ return\\n+ with connection.cursor() as cursor:\\n+ cursor.execute(PROVIDER_CATALOG_SCHEMA_SQL)\\n+ connection.commit()\\n+ self._schema_ready = True\\n+\\n+ @staticmethod\\n+ def _upsert_account(cursor: object, source: ProviderModelSource) -> str:\\n+ \\\"\\\"\\\"Upsert one provider account without credential values.\\\"\\\"\\\"\\n+ account_id = provider_account_id(source)\\n+ cursor.execute(\\n+ \\\"INSERT INTO provider_account (\\\"\\n+ \\\"provider_account_id, provider_name, credential_name, list_url, \\\"\\n+ \\\"chat_base_url, auth_scheme, discovery_style, task_filter, \\\"\\n+ \\\"enabled_flag, created_at, updated_at\\\"\\n+ \\\") VALUES (%s, %s, %s, %s, %s, %s, %s, %s, true, now(), now()) \\\"\\n+ \\\"ON CONFLICT (provider_account_id) DO UPDATE SET \\\"\\n+ \\\"provider_name = EXCLUDED.provider_name, \\\"\\n+ \\\"credential_name = EXCLUDED.credential_name, \\\"\\n+ \\\"list_url = EXCLUDED.list_url, \\\"\\n+ \\\"chat_base_url = EXCLUDED.chat_base_url, \\\"\\n+ \\\"auth_scheme = EXCLUDED.auth_scheme, \\\"\\n+ \\\"discovery_style = EXCLUDED.discovery_style, \\\"\\n+ \\\"task_filter = EXCLUDED.task_filter, \\\"\\n+ \\\"enabled_flag = true, updated_at = now()\\\",\\n+ (\\n+ account_id,\\n+ source.provider_name,\\n+ source.credential_name,\\n+ source.list_url,\\n+ source.chat_base_url,\\n+ source.auth_scheme,\\n+ source.style,\\n+ source.task_filter,\\n+ ),\\n+ )\\n+ return account_id\\n+\\n+ def record_success(\\n+ self,\\n+ source: ProviderModelSource,\\n+ models: Sequence[DiscoveredModel],\\n+ *,\\n+ eligible_model_ids: set[str],\\n+ serving_tags: Mapping[str, tuple[str, ...]],\\n+ ) -> None:\\n+ \\\"\\\"\\\"Replace one PostgreSQL account catalog in a single transaction.\\\"\\\"\\\"\\n+ normalized = _deduplicate_models(source, models)\\n+ if not normalized:\\n+ raise ProviderCatalogError(\\\"successful provider refresh cannot be empty\\\")\\n+ started_at = _now()\\n+ eligible = set(normalized).intersection(eligible_model_ids)\\n+ with self._connect() as connection:\\n+ self._ensure_schema(connection)\\n+ with connection.cursor() as cursor:\\n+ account_id = self._upsert_account(cursor, source)\\n+ cursor.execute(\\n+ \\\"UPDATE provider_model SET enabled_flag = false \\\"\\n+ \\\"WHERE provider_account_id = %s\\\",\\n+ (account_id,),\\n+ )\\n+ cursor.execute(\\n+ \\\"DELETE FROM model_serving_tag WHERE provider_model_id IN (\\\"\\n+ \\\"SELECT provider_model_id FROM provider_model \\\"\\n+ \\\"WHERE provider_account_id = %s)\\\",\\n+ (account_id,),\\n+ )\\n+ for model_name, model in normalized.items():\\n+ model_row_id = provider_model_id(source, model_name)\\n+ cursor.execute(\\n+ \\\"INSERT INTO provider_model (\\\"\\n+ \\\"provider_model_id, provider_account_id, model_name, \\\"\\n+ \\\"prompt_price_per_1k, completion_price_per_1k, currency_code, \\\"\\n+ \\\"serving_eligible_flag, enabled_flag, first_seen_at, \\\"\\n+ \\\"last_seen_at\\\"\\n+ \\\") VALUES (%s, %s, %s, %s, %s, %s, %s, \\\"\\n+ \\\"true, %s, %s) \\\"\\n+ \\\"ON CONFLICT (provider_model_id) DO UPDATE SET \\\"\\n+ \\\"model_name = EXCLUDED.model_name, \\\"\\n+ \\\"prompt_price_per_1k = EXCLUDED.prompt_price_per_1k, \\\"\\n+ \\\"completion_price_per_1k = EXCLUDED.completion_price_per_1k, \\\"\\n+ \\\"currency_code = EXCLUDED.currency_code, \\\"\\n+ \\\"serving_eligible_flag = EXCLUDED.serving_eligible_flag, \\\"\\n+ \\\"enabled_flag = true, last_seen_at = EXCLUDED.last_seen_at\\\",\\n+ (\\n+ model_row_id,\\n+ account_id,\\n+ model_name,\\n+ model.prompt_price_per_1k,\\n+ model.completion_price_per_1k,\\n+ model.currency_code,\\n+ model_name in eligible,\\n+ started_at,\\n+ started_at,\\n+ ),\\n+ )\\n+ if model_name in eligible:\\n+ for tag in _normalize_tags(serving_tags.get(model_name, ())):\\n+ cursor.execute(\\n+ \\\"INSERT INTO model_serving_tag \\\"\\n+ \\\"(provider_model_id, tag_name) \\\"\\n+ \\\"VALUES (%s, %s) ON CONFLICT DO NOTHING\\\",\\n+ (model_row_id, tag),\\n+ )\\n+ finished_at = _now()\\n+ cursor.execute(\\n+ \\\"INSERT INTO catalog_refresh_run (\\\"\\n+ \\\"catalog_refresh_run_id, provider_account_id, refresh_status, \\\"\\n+ \\\"observed_model_count, eligible_model_count, error_code, \\\"\\n+ \\\"started_at, finished_at\\\"\\n+ \\\") VALUES (%s, %s, %s, %s, %s, %s, %s, %s)\\\",\\n+ (\\n+ f\\\"catalog_refresh_{uuid.uuid4().hex}\\\",\\n+ account_id,\\n+ \\\"succeeded\\\",\\n+ len(normalized),\\n+ len(eligible),\\n+ None,\\n+ started_at,\\n+ finished_at,\\n+ ),\\n+ )\\n+ connection.commit()\\n+ self._evidence.append(\\n+ CatalogRefreshEvidence(\\n+ provider_account_id(source),\\n+ \\\"succeeded\\\",\\n+ len(normalized),\\n+ len(eligible),\\n+ None,\\n+ started_at,\\n+ finished_at,\\n+ )\\n+ )\\n+\\n+ def record_failure(\\n+ self,\\n+ source: ProviderModelSource,\\n+ *,\\n+ error_code: str,\\n+ ) -> None:\\n+ \\\"\\\"\\\"Record a PostgreSQL failure without disabling prior models.\\\"\\\"\\\"\\n+ started_at = _now()\\n+ stable_code = _normalize_error_code(error_code)\\n+ with self._connect() as connection:\\n+ self._ensure_schema(connection)\\n+ with connection.cursor() as cursor:\\n+ account_id = self._upsert_account(cursor, source)\\n+ finished_at = _now()\\n+ cursor.execute(\\n+ \\\"INSERT INTO catalog_refresh_run (\\\"\\n+ \\\"catalog_refresh_run_id, provider_account_id, refresh_status, \\\"\\n+ \\\"observed_model_count, eligible_model_count, error_code, \\\"\\n+ \\\"started_at, finished_at\\\"\\n+ \\\") VALUES (%s, %s, %s, %s, %s, %s, %s, %s)\\\",\\n+ (\\n+ f\\\"catalog_refresh_{uuid.uuid4().hex}\\\",\\n+ account_id,\\n+ \\\"failed\\\",\\n+ 0,\\n+ 0,\\n+ stable_code,\\n+ started_at,\\n+ finished_at,\\n+ ),\\n+ )\\n+ connection.commit()\\n+ self._evidence.append(\\n+ CatalogRefreshEvidence(\\n+ provider_account_id(source),\\n+ \\\"failed\\\",\\n+ 0,\\n+ 0,\\n+ stable_code,\\n+ started_at,\\n+ finished_at,\\n+ )\\n+ )\\n+\\n+ def serving_models(\\n+ self,\\n+ source: ProviderModelSource,\\n+ ) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Read enabled last-known-good serving models for one account.\\\"\\\"\\\"\\n+ account_id = provider_account_id(source)\\n+ with self._connect() as connection:\\n+ self._ensure_schema(connection)\\n+ with connection.cursor() as cursor:\\n+ cursor.execute(\\n+ \\\"SELECT pm.model_name, pa.chat_base_url, pa.auth_scheme, \\\"\\n+ \\\"pm.prompt_price_per_1k, pm.completion_price_per_1k, \\\"\\n+ \\\"pm.currency_code FROM provider_model AS pm \\\"\\n+ \\\"JOIN provider_account AS pa ON pa.provider_account_id = pm.provider_account_id \\\"\\n+ \\\"WHERE pm.provider_account_id = %s \\\"\\n+ \\\"AND pm.enabled_flag = true AND pm.serving_eligible_flag = true \\\"\\n+ \\\"ORDER BY pm.model_name\\\",\\n+ (account_id,),\\n+ )\\n+ rows = cursor.fetchall()\\n+ return [\\n+ DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=row[0],\\n+ credential_name=source.credential_name,\\n+ chat_base_url=row[1],\\n+ auth_scheme=row[2],\\n+ prompt_price_per_1k=_normalize_price(row[3]),\\n+ completion_price_per_1k=_normalize_price(row[4]),\\n+ currency_code=_normalize_currency(row[5]),\\n+ )\\n+ for row in rows\\n+ ]\\n+\\n+ def refresh_evidence(self) -> tuple[CatalogRefreshEvidence, ...]:\\n+ \\\"\\\"\\\"Return evidence emitted by this store instance.\\\"\\\"\\\"\\n+ return tuple(self._evidence)\" }, { \"sha\": \"56c37beac22656936aa8ba54e7623365e9a1278b\", \"filename\": \"docs/database_design.sql\", \"status\": \"modified\", \"additions\": 64, \"deletions\": 0, \"changes\": 64, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdatabase_design.sql\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdatabase_design.sql\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdatabase_design.sql?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -69,6 +69,64 @@ create table audit_event (\\n created_at timestamptz not null default now()\\n );\\n \\n+create table provider_credentials (\\n+ credential_name text primary key,\\n+ encrypted_value bytea not null,\\n+ updated_at timestamptz not null default now()\\n+);\\n+\\n+create table provider_account (\\n+ provider_account_id text primary key,\\n+ provider_name text not null,\\n+ -- Credential rollback deliberately stays independent of catalog rows so a\\n+ -- failed candidate promotion cannot delete last-known-good model metadata.\\n+ -- The runtime catalog DDL uses the same application-managed relationship.\\n+ credential_name text not null,\\n+ list_url text not null,\\n+ chat_base_url text not null,\\n+ auth_scheme text not null,\\n+ discovery_style text not null,\\n+ task_filter text not null,\\n+ enabled_flag boolean not null default true,\\n+ created_at timestamptz not null default now(),\\n+ updated_at timestamptz not null default now(),\\n+ unique (provider_name, credential_name)\\n+);\\n+\\n+create table provider_model (\\n+ provider_model_id text primary key,\\n+ provider_account_id text not null\\n+ references provider_account(provider_account_id) on delete cascade,\\n+ model_name text not null,\\n+ prompt_price_per_1k numeric(20, 8),\\n+ completion_price_per_1k numeric(20, 8),\\n+ currency_code text not null,\\n+ serving_eligible_flag boolean not null default false,\\n+ enabled_flag boolean not null default true,\\n+ first_seen_at timestamptz not null,\\n+ last_seen_at timestamptz not null,\\n+ unique (provider_account_id, model_name)\\n+);\\n+\\n+create table model_serving_tag (\\n+ provider_model_id text not null\\n+ references provider_model(provider_model_id) on delete cascade,\\n+ tag_name text not null,\\n+ primary key (provider_model_id, tag_name)\\n+);\\n+\\n+create table catalog_refresh_run (\\n+ catalog_refresh_run_id text primary key,\\n+ provider_account_id text not null\\n+ references provider_account(provider_account_id) on delete cascade,\\n+ refresh_status text not null,\\n+ observed_model_count integer not null default 0,\\n+ eligible_model_count integer not null default 0,\\n+ error_code text,\\n+ started_at timestamptz not null,\\n+ finished_at timestamptz not null\\n+);\\n+\\n create index workflow_run_retention_idx\\n on workflow_run (retention_expires_at)\\n where deleted_at is null;\\n@@ -81,6 +139,12 @@ create index audit_event_retention_idx\\n on audit_event (retention_expires_at)\\n where deleted_at is null;\\n \\n+create index provider_model_account_idx\\n+ on provider_model (provider_account_id, enabled_flag, serving_eligible_flag);\\n+\\n+create index catalog_refresh_account_idx\\n+ on catalog_refresh_run (provider_account_id, finished_at desc);\\n+\\n create view workflow_run_safe_view as\\n select\\n workflow_run_id,\" }, { \"sha\": \"383868be62f1d2e560651614d6ac60ee77d6b7e4\", \"filename\": \"docs/doctoring/current-main-provider-bootstrap.md\", \"status\": \"added\", \"additions\": 119, \"deletions\": 0, \"changes\": 119, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdoctoring%2Fcurrent-main-provider-bootstrap.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdoctoring%2Fcurrent-main-provider-bootstrap.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdoctoring%2Fcurrent-main-provider-bootstrap.md?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,119 @@\\n+# Current-main provider bootstrap\\n+\\n+## Decision\\n+\\n+The durable catalog decision is recorded in\\n+[`ADR 0015`](../planning/adrs/0015-durable-provider-catalog.md), including the\\n+third-normal-form dependency boundary and the last-known-good refresh contract.\\n+\\n+Contextual Orchestrator treats the five organization provider credentials as one\\n+trusted bootstrap inventory:\\n+\\n+- `NVIDIA_NIM_API_KEY`\\n+- `NVIDIA_NIM_API_KEY_SUB`\\n+- `BYTEZ_API_KEY`\\n+- `OPENROUTER_API_KEY`\\n+- `OPENAI_API_KEY`\\n+\\n+GitHub Actions secrets are transport into a one-shot bootstrap process, not the\\n+runtime credential source. Production bootstrap requires the PostgreSQL credential\\n+backend so values are stored encrypted at rest through the existing pgcrypto\\n+registry. Runtime model discovery resolves credential names through\\n+`get_credential()` only.\\n+\\n+## Failure contract\\n+\\n+Production bootstrap fails closed when any fixed credential is missing, when the\\n+configured credential backend is not atomic, or when no usable provider model can\\n+be discovered. A provider-local discovery exception does not erase models returned\\n+by other providers; the report contains only stable provider names and counts, never\\n+raw exception strings or credential values.\\n+\\n+`registered_credentials` is the post-rollback durable inventory, not merely the\\n+set of candidate names received from Actions. If a first-ever candidate key is\\n+reverted after a failed provider refresh, that name is omitted from the report\\n+and the hourly workflow fails its complete-inventory gate. Existing keys that\\n+are restored remain listed, so a transient provider outage can preserve\\n+last-known-good serving without falsely claiming that a missing key is durable.\\n+\\n+A successful generic `/models` response is not itself evidence that every row can\\n+serve Chat Completions. OpenAI-compatible registries may mix chat models with\\n+embeddings, rerankers, speech, image generation, moderation, safety, or realtime\\n+transports. The bootstrap therefore applies a conservative negative compatibility\\n+filter before selection and reports both:\\n+\\n+- `discovered_model_count`: every syntactically valid catalog row; and\\n+- `eligible_model_count`: rows that are not clearly a non-chat transport.\\n+\\n+If no compatible row remains, bootstrap fails closed instead of activating the\\n+cheapest incompatible model. Surviving rows receive only generic serving tags:\\n+`discovered`, `chat`, `worker`, `writing`, and `synthesizer`. The bootstrap never\\n+infers reasoning, verification, coding, vision, or provider-native effort support\\n+from a model name. Those capabilities require explicit provider/catalog evidence or\\n+measured evaluation and are negotiated by the ordinary runtime policy.\\n+\\n+The bootstrap pool is provider-diverse before it is cost-ordered. Missing price is\\n+`unknown`, not zero. This avoids treating a provider such as Bytez, whose public\\n+catalog may use a non-token billing unit, as a fabricated free route.\\n+\\n+Candidate selection and durable serving activation are separate claims:\\n+\\n+- `selected_agent_ids` records the bounded chat candidates produced by discovery\\n+ and selection;\\n+- `enabled_agent_ids` is populated only when an explicit durable `--agents-db`\\n+ is supplied and the selected agents are confirmed active in that pool; and\\n+- `durable_agent_pool` states whether the activation claim is backed by a\\n+ persistent agent-pool database.\\n+\\n+When a durable pool is refreshed, the bootstrap tombstones its synthetic seed and\\n+previously discovered agents that are absent from the current bounded selection.\\n+Operator-managed agents are preserved. This prevents retired, withdrawn, or newly\\n+classified non-chat provider models from continuing to receive traffic after a\\n+later discovery run.\\n+\\n+## Operational workflow\\n+\\n+`.github/workflows/provider-catalog-sync.yml` runs hourly on protected `main` and may\\n+also be dispatched manually. It is intentionally absent from pull-request secret\\n+execution. The production environment must provide:\\n+\\n+- the five provider secrets above;\\n+- `CONTEXTUAL_ORCHESTRATOR_KV_DSN`; and\\n+- `CONTEXTUAL_ORCHESTRATOR_KV_PASSPHRASE`.\\n+\\n+The GitHub-hosted workflow has an ephemeral filesystem. It therefore registers the\\n+five credentials in the durable PostgreSQL KV and verifies discovery,\\n+`eligible_model_count`, and `selected_agent_ids`; it does not claim durable\\n+agent-pool activation. A long-running service may either use the ordinary KV-backed\\n+startup discovery path or invoke this bootstrap with a persistent `--agents-db`\\n+under its own deployment boundary.\\n+\\n+The workflow verifies that all five credential names were registered, at least one\\n+model was discovered, at least one chat-compatible model survived classification, a\\n+bounded serving candidate set was produced, and no exact provider secret appears in\\n+the emitted report.\\n+\\n+## Research and standards grounding\\n+\\n+The automatic pool remains a routing input rather than an unsupported claim that a\\n+single cheapest model is universally best. Quality/performance selection remains in\\n+the orchestrator's paper-grounded routing and orchestration layer; this bootstrap\\n+only establishes a compatible candidate set and failure isolation.\\n+\\n+National Institute of Standards and Technology. (2020). *Security and privacy\\n+controls for information systems and organizations* (NIST Special Publication\\n+800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5\\n+\\n+National Institute of Standards and Technology. (2024). *Artificial intelligence\\n+risk management framework: Generative artificial intelligence profile* (NIST AI\\n+600-1). https://doi.org/10.6028/NIST.AI.600-1\\n+\\n+Tang, Y., et al. (2026). *Sakana Fugu technical report*. Sakana AI.\\n+\\n+Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025).\\n+*Trinity: An evolved LLM coordinator* (arXiv:2512.04695).\\n+https://doi.org/10.48550/arXiv.2512.04695\\n+\\n+Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025).\\n+*Learning to orchestrate agents in natural language with the Conductor*\\n+(arXiv:2512.04388). https://doi.org/10.48550/arXiv.2512.04388\" }, { \"sha\": \"0251375d4bd4ad91ac23995d68c1fd6b300b0a88\", \"filename\": \"docs/doctoring/embedding-chat-capability-isolation.md\", \"status\": \"added\", \"additions\": 133, \"deletions\": 0, \"changes\": 133, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdoctoring%2Fembedding-chat-capability-isolation.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdoctoring%2Fembedding-chat-capability-isolation.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdoctoring%2Fembedding-chat-capability-isolation.md?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,133 @@\\n+# Embedding-to-chat capability isolation incident\\n+\\n+**Status:** Accepted incident decision\\n+**Date:** 2026-08-20\\n+**Affected consumer:** LineageWeave buyer-surface stack around PR #260\\n+\\n+## Incident\\n+\\n+A conducted workflow reached the final contextual-orchestrator synthesizer with\\n+`model_group=text-embedding-3-large` and deployment\\n+`azure/text-embedding-3-large`. The gateway rejected the chat operation as\\n+unsupported. Its configured fallback map contained chat-generation model groups,\\n+but no fallback attached to the embedding group.\\n+\\n+The missing fallback was a symptom, not the causal defect. An embedding deployment\\n+had already crossed the chat-agent capability boundary and become eligible for a\\n+worker role.\\n+\\n+## Causal boundary\\n+\\n+Provider-compatible `/models` registries can contain multiple endpoint families.\\n+The original discovery parser accepted every non-empty model identifier and exposed\\n+it to agent creation, price selection, and durable pool synchronization. A catalog\\n+row naming an embedding deployment could therefore be scored for thinker, worker,\\n+verifier, or synthesizer work even though its serving endpoint accepts embedding\\n+input rather than chat messages.\\n+\\n+The first incident fix closed discovery and price-routing boundaries, but further\\n+root-cause tracing showed an already-persisted incompatible `ModelAgent` could still\\n+survive that filter. The runtime ranking path, generated workflow assignment,\\n+cross-agent failover, readiness probe, streaming path, and direct\\n+`ModelClient.chat()` path previously trusted the persisted model identifier. That\\n+stale-state path is sufficient to reproduce the same unsupported Azure chat\\n+operation after a process restart or durable bootstrap.\\n+\\n+OpenAI documents `text-embedding-3-large` under the embeddings endpoint, separately\\n+from models supported by chat completions. Microsoft likewise demonstrates it with\\n+`client.embeddings.create`, not `client.chat.completions.create`. LiteLLM exposes\\n+chat, responses, embeddings, image, audio, rerank, and other endpoint families as\\n+distinct operations. A router fallback can choose another deployment for the same\\n+operation; it cannot make an embedding deployment execute a chat operation.\\n+\\n+## Decision\\n+\\n+Chat transport compatibility and general agent-role eligibility are separate\\n+shared runtime invariants. A provider may expose an audio-capable model or a\\n+policy classifier through Chat Completions while that model remains unsuitable\\n+for ordinary thinker, worker, verifier, or synthesizer work.\\n+\\n+1. Normalize provider prefixes and common separators in model identifiers.\\n+2. At the transport boundary, reject identifiers that clearly advertise embedding,\\n+ reranking, transcription, moderation-endpoint, image-generation, realtime, or\\n+ speech-only semantics.\\n+3. Keep provider-documented audio and policy-classifier models transport-compatible\\n+ when they are served through Chat Completions.\\n+4. At discovery and ordinary orchestration-role boundaries, additionally exclude\\n+ explicit guard, safety, and NemoGuard policy classifiers.\\n+5. Apply the general-role guard while parsing both OpenAI-compatible and Bytez\\n+ catalogs and before converting, pricing, or cost-selecting a discovery record.\\n+6. Remove stale ineligible agents from thinker, worker, verifier, and synthesizer\\n+ ranking even if a durable configuration still contains them.\\n+7. Reselect a generated workflow step that explicitly names a stale ineligible\\n+ agent and omit such agents from planner inventory.\\n+8. Remove ineligible agents from cross-agent failover candidates.\\n+9. Apply the transport guard at `ModelClient.chat()`, `stream_chat()`, and\\n+ readiness probing before mock or network transport.\\n+10. Fail closed when no general chat agent remains.\\n+11. Leave unknown identifiers eligible without fabricating reasoning, tool, vision,\\n+ or verification capabilities from their names.\\n+\\n+This is deliberately a conservative negative filter. A future capability registry\\n+may replace name-based exclusion with authenticated provider metadata, measured\\n+endpoint probes, and separate endpoint-specific pools. Until that evidence exists,\\n+a clearly incompatible model fails closed at transport boundaries and a clearly\\n+specialized policy model fails closed at general-role boundaries.\\n+\\n+## Rejected response\\n+\\n+Adding `text-embedding-3-large` to a chat fallback map is rejected. It would retain\\n+the invalid primary assignment and merely hide it when a fallback happened to be\\n+available. Repeated provider retries are also rejected because the request is\\n+structurally unsupported, not transiently unavailable.\\n+\\n+## Residual operational action\\n+\\n+Runtime containment means an already-persisted embedding agent can no longer win\\n+chat selection or failover while stale data is being cleaned up. Durable state must\\n+still converge to the correct exact set: the provider-bootstrap slice owns stale\\n+discovered-agent withdrawal and now imports the same shared classifier as the\\n+runtime, with a policy-matrix regression test covering image, embedding, safety,\\n+audio, and ordinary chat identifiers.\\n+Runtime rejection is defense in depth, not a substitute for deleting invalid\\n+persistent configuration.\\n+\\n+## Verification evidence\\n+\\n+`tests/test_chat_model_capability_isolation.py` reproduces the exact Azure model ID\\n+and provider/separator aliases. Together with\\n+`tests/test_chat_capability_unknown_identifiers.py`,\\n+`tests/test_chat_transport_role_separation.py`, and\\n+`tests/test_chat_passthrough_capability_isolation.py`, it verifies:\\n+\\n+- OpenAI-compatible and Bytez catalog filtering;\\n+- malformed and prefix-only identifier handling;\\n+- agent-conversion rejection;\\n+- exclusion from the price book and cheapest-agent selection;\\n+- exclusion of a high-priority stale embedding agent from synthesizer selection;\\n+- fail-closed behavior when the persisted pool contains only non-chat agents;\\n+- generated-plan reassignment away from a stale embedding agent;\\n+- exclusion from cross-agent failover;\\n+- direct and streaming `ModelClient` rejection before transport;\\n+- readiness failure with a stable non-chat code before provider access;\\n+- planner inventory and generated-plan isolation;\\n+- distinction between chat-served audio/policy models and general agent roles.\\n+- conservative unknown-identifier handling, including unrelated `vanguard` names;\\n+- endpoint-family exclusions for image-generation (`dall-e`), CLIP, and SigLIP;\\n+- normalized `/v1/responses` passthrough and pre-transport rejection of embedding models.\\n+\\n+## References\\n+\\n+BerriAI. (n.d.). *LiteLLM: Call 100+ LLMs using the OpenAI input/output format*.\\n+Retrieved August 20, 2026, from https://docs.litellm.ai/\\n+\\n+Microsoft. (n.d.). *How to switch between OpenAI and Azure OpenAI endpoints*.\\n+Microsoft Learn. Retrieved August 20, 2026, from\\n+https://learn.microsoft.com/en-us/azure/developer/ai/how-to/switching-endpoints\\n+\\n+OpenAI. (n.d.). *Data controls in the OpenAI platform: Default usage policies by\\n+endpoint*. Retrieved August 20, 2026, from\\n+https://platform.openai.com/docs/models/default-usage-policies-by-endpoint\\n+\\n+OpenAI. (n.d.). *GPT-audio model*. Retrieved August 20, 2026, from\\n+https://developers.openai.com/api/docs/models/gpt-audio\" }, { \"sha\": \"08c20b1eb3de1baaa8fa306bbc2ecc45397efc42\", \"filename\": \"docs/doctoring/provider-diverse-discovery-routing.md\", \"status\": \"added\", \"additions\": 49, \"deletions\": 0, \"changes\": 49, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdoctoring%2Fprovider-diverse-discovery-routing.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdoctoring%2Fprovider-diverse-discovery-routing.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdoctoring%2Fprovider-diverse-discovery-routing.md?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,49 @@\\n+---\\n+title: \\\"Provider-diverse discovery and cost-honest failover routing\\\"\\n+status: \\\"implemented\\\"\\n+date: \\\"2026-08-21\\\"\\n+scope: \\\"PR #770\\\"\\n+---\\n+\\n+# Provider-diverse discovery and cost-honest failover routing\\n+\\n+## Decision\\n+\\n+PR #770 makes model discovery fail closed for invalid catalog rows (a price\\n+that is negative, non-finite, or a nonzero value that underflows to zero),\\n+retains eligible candidates that simply have no reported price as an\\n+explicit unknown-cost fallback, and selects a provider-diverse bootstrap\\n+pool before ordinary chat routing. The selector is deterministic eligibility\\n+and cost accounting; it is not a learned answer-quality judge and does not\\n+claim to reproduce the learning systems in the cited work.\\n+\\n+## Research-to-code mapping\\n+\\n+| Implementation boundary | Evidence-informed reason | Acceptance evidence |\\n+| --- | --- | --- |\\n+| Reject malformed, negative, or non-finite price rows | A cost-aware router must not treat missing or invalid evidence as zero cost. | Discovery and persisted-price tests reject the row before selection. |\\n+| Keep unknown-price candidates only as an explicit fallback | Cost optimization must remain honest when price evidence is incomplete. | Selection tests never rank an unknown price above a valid priced candidate. |\\n+| Prefer distinct providers in the bootstrap pool | A gateway needs an upstream failover set rather than several aliases for one provider. | Provider-diversity tests assert the configured pool spans available providers. |\\n+| Leave quality judgment to evaluation/review policy | Routing signals and answer-quality judgment have different failure modes. | Existing model-judge and fail-closed routing tests remain the quality boundary. |\\n+\\n+The routing papers and OA PDFs are already committed in the prerequisite\\n+stack base under `docs/papers/` (`routellm-routing-2406.18665.pdf`,\\n+`hybrid-llm-query-routing-2404.14618.pdf`, and\\n+`frugalgpt-cost-2305.05176.pdf`). This doctoring record makes their relevance\\n+to the exact discovery selector explicit instead of treating inherited files\\n+as incidental documentation.\\n+\\n+## APA 7 references\\n+\\n+Chen, L., Zaharia, M., & Zou, J. (2023). *FrugalGPT: How to use large\\n+language models while reducing cost and improving performance*. arXiv.\\n+https://arxiv.org/abs/2305.05176\\n+\\n+Ding, D., Mallick, A., Wang, C., Sim, R., Mukherjee, S., Rühle, V.,\\n+Lakshmanan, L. V. S., & Awadallah, A. H. (2024). *Hybrid LLM:\\n+Cost-efficient and quality-aware query routing*. International Conference on\\n+Learning Representations. https://arxiv.org/abs/2404.14618\\n+\\n+Ong, I., Almahairi, A., Wu, V., Chiang, W.-L., Wu, T., Gonzalez, J. E.,\\n+Kadous, M. W., & Stoica, I. (2024). *RouteLLM: Learning to route LLMs with\\n+preference data*. arXiv. https://arxiv.org/abs/2406.18665\" }, { \"sha\": \"98e3789821249149eca5dc11fe9ce7ff6e9b3e5f\", \"filename\": \"docs/planning/adrs/0015-durable-provider-catalog.md\", \"status\": \"added\", \"additions\": 93, \"deletions\": 0, \"changes\": 93, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fplanning%2Fadrs%2F0015-durable-provider-catalog.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fplanning%2Fadrs%2F0015-durable-provider-catalog.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0015-durable-provider-catalog.md?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,93 @@\\n+---\\n+id: \\\"0015\\\"\\n+title: \\\"Durable provider catalog and last-known-good composition\\\"\\n+status: accepted\\n+proposed_date: \\\"2026-08-20\\\"\\n+accepted_date: \\\"2026-08-22\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"NIST SP 800-53 Rev. 5\\\"\\n+ - \\\"NIST AI 600-1\\\"\\n+informed:\\n+ - \\\"LineageWeave\\\"\\n+ - \\\"fast-mlsirm\\\"\\n+ - \\\"contributors\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/provider_bootstrap.py\\\"\\n+ - \\\"contextual_orchestrator/provider_catalog_bootstrap.py\\\"\\n+ - \\\"contextual_orchestrator/provider_catalog_store.py\\\"\\n+ - \\\".github/workflows/provider-catalog-sync.yml\\\"\\n+supersedes: null\\n+superseded-by: null\\n+related:\\n+ - path: \\\"docs/planning/adrs/0012-gateway-only-provider-contract.md\\\"\\n+ relation: depends-on\\n+ - path: \\\"docs/planning/adrs/0014-gateway-owned-model-selection.md\\\"\\n+ relation: extends\\n+effort: L\\n+---\\n+\\n+# Durable provider catalog and last-known-good composition\\n+\\n+## Context\\n+\\n+Registering the five organization provider secrets in PostgreSQL is necessary\\n+but not sufficient. A production process must also retain discovered provider\\n+accounts and models so a transient catalog outage does not erase the serving\\n+pool, and operators must distinguish live discovery from last-known-good\\n+metadata. GitHub-hosted scheduled jobs have ephemeral filesystems, so SQLite\\n+cannot be the authority for this catalog.\\n+\\n+## Decision\\n+\\n+Use a third-normal-form PostgreSQL catalog colocated with the encrypted\\n+credential registry. The authority contains four two-or-more-word\\n+`snake_case` objects:\\n+\\n+- `provider_account`: provider endpoint and credential name, never the value;\\n+- `provider_model`: account-scoped model identity, known prices, compatibility\\n+ and lifecycle state; endpoint and authentication fields are joined from its\\n+ owning account;\\n+- `model_serving_tag`: generic serving tags as a separate many-to-many relation;\\n+- `catalog_refresh_run`: provider-local success/failure evidence.\\n+\\n+A successful non-empty provider refresh atomically replaces that provider\\n+account's enabled current set. A failed or empty/malformed refresh records only\\n+an allowlisted stable error code and preserves the account's last-known-good\\n+models. Successful discovery of an authoritative non-chat-only catalog may\\n+withdraw earlier chat rows.\\n+\\n+Model names are used only for a conservative negative compatibility filter that\\n+excludes obvious embedding, reranking, speech, image, moderation, safety, and\\n+realtime transports. They are never used to infer reasoning, verification,\\n+coding, vision, or provider-native effort capabilities. Those require explicit\\n+catalog or measured evidence under the gateway-owned policy.\\n+\\n+## Consequences\\n+\\n+- Credentials and model metadata are durable but remain separated.\\n+- NVIDIA primary and secondary keys are independent provider accounts.\\n+- One provider outage does not erase other providers or its own last-known-good set.\\n+- Unknown price remains unknown rather than becoming fabricated zero cost.\\n+- The protected hourly workflow can persist catalog metadata without claiming\\n+ that its ephemeral runner has activated a durable agent-pool database.\\n+- Long-running deployments may separately synchronize selected catalog rows into\\n+ a persistent agent pool.\\n+\\n+## Verification\\n+\\n+The merge gate covers normalized DDL, secret-column absence, provider-account\\n+isolation, parameterized PostgreSQL statements, last-known-good retention,\\n+withdrawal after authoritative success, non-chat filtering, secret-free\\n+evidence, and end-to-end recovery when one provider fails.\\n+\\n+## References\\n+\\n+National Institute of Standards and Technology. (2020). *Security and privacy\\n+controls for information systems and organizations* (NIST Special Publication\\n+800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5\\n+\\n+National Institute of Standards and Technology. (2024). *Artificial\\n+intelligence risk management framework: Generative artificial intelligence\\n+profile* (NIST AI 600-1). https://doi.org/10.6028/NIST.AI.600-1\" }, { \"sha\": \"0f9fd25386195533e85fdcc66a4a22718c4a7c04\", \"filename\": \"docs/provider_catalog_database.sql\", \"status\": \"added\", \"additions\": 53, \"deletions\": 0, \"changes\": 53, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fprovider_catalog_database.sql\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fprovider_catalog_database.sql\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fprovider_catalog_database.sql?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,53 @@\\n+CREATE TABLE IF NOT EXISTS provider_account (\\n+ provider_account_id text PRIMARY KEY,\\n+ provider_name text NOT NULL,\\n+ credential_name text NOT NULL,\\n+ list_url text NOT NULL,\\n+ chat_base_url text NOT NULL,\\n+ auth_scheme text NOT NULL,\\n+ discovery_style text NOT NULL,\\n+ task_filter text NOT NULL,\\n+ enabled_flag boolean NOT NULL DEFAULT true,\\n+ created_at timestamptz NOT NULL DEFAULT now(),\\n+ updated_at timestamptz NOT NULL DEFAULT now(),\\n+ UNIQUE (provider_name, credential_name)\\n+);\\n+\\n+CREATE TABLE IF NOT EXISTS provider_model (\\n+ provider_model_id text PRIMARY KEY,\\n+ provider_account_id text NOT NULL\\n+ REFERENCES provider_account(provider_account_id) ON DELETE CASCADE,\\n+ model_name text NOT NULL,\\n+ prompt_price_per_1k numeric(20, 8),\\n+ completion_price_per_1k numeric(20, 8),\\n+ currency_code text NOT NULL,\\n+ serving_eligible_flag boolean NOT NULL DEFAULT false,\\n+ enabled_flag boolean NOT NULL DEFAULT true,\\n+ first_seen_at timestamptz NOT NULL,\\n+ last_seen_at timestamptz NOT NULL,\\n+ UNIQUE (provider_account_id, model_name)\\n+);\\n+\\n+CREATE TABLE IF NOT EXISTS model_serving_tag (\\n+ provider_model_id text NOT NULL\\n+ REFERENCES provider_model(provider_model_id) ON DELETE CASCADE,\\n+ tag_name text NOT NULL,\\n+ PRIMARY KEY (provider_model_id, tag_name)\\n+);\\n+\\n+CREATE TABLE IF NOT EXISTS catalog_refresh_run (\\n+ catalog_refresh_run_id text PRIMARY KEY,\\n+ provider_account_id text NOT NULL\\n+ REFERENCES provider_account(provider_account_id) ON DELETE CASCADE,\\n+ refresh_status text NOT NULL,\\n+ observed_model_count integer NOT NULL DEFAULT 0,\\n+ eligible_model_count integer NOT NULL DEFAULT 0,\\n+ error_code text,\\n+ started_at timestamptz NOT NULL,\\n+ finished_at timestamptz NOT NULL\\n+);\\n+\\n+CREATE INDEX IF NOT EXISTS provider_model_account_idx\\n+ ON provider_model (provider_account_id, enabled_flag, serving_eligible_flag);\\n+CREATE INDEX IF NOT EXISTS catalog_refresh_account_idx\\n+ ON catalog_refresh_run (provider_account_id, finished_at DESC);\" }, { \"sha\": \"d9e72b6ca5738f5f3f4005cd3ca767157f610097\", \"filename\": \"tests/test_chat_capability_unknown_identifiers.py\", \"status\": \"added\", \"additions\": 47, \"deletions\": 0, \"changes\": 47, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_capability_unknown_identifiers.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_capability_unknown_identifiers.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_capability_unknown_identifiers.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,47 @@\\n+\\\"\\\"\\\"Regressions for conservative treatment of unknown model identifiers.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import sys\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator.chat_capability import ( # noqa: E402\\n+ is_chat_compatible_model_id,\\n+ is_general_chat_agent_model_id,\\n+)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"vendor/vanguard-7b\\\",\\n+ \\\"vendor/vanguard-instruct\\\",\\n+ ],\\n+)\\n+def test_unknown_names_that_merely_end_with_guard_remain_eligible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Do not fabricate a policy-classifier capability from an unrelated word suffix.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id)\\n+ assert is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"meta-llama/llama-guard-4-12b\\\",\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-topic-control\\\",\\n+ \\\"google/shieldgemma-2b-it\\\",\\n+ ],\\n+)\\n+def test_explicit_policy_classifier_markers_remain_role_ineligible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Keep exact guard, safety, and NemoGuard markers out of general synthesis roles.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id)\\n+ assert not is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"249240e247b7b7aa27bd0e02c237c31fdbef896d\", \"filename\": \"tests/test_chat_model_capability_isolation.py\", \"status\": \"added\", \"additions\": 391, \"deletions\": 0, \"changes\": 391, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_model_capability_isolation.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_model_capability_isolation.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_model_capability_isolation.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,391 @@\\n+\\\"\\\"\\\"Regression coverage for isolating non-chat models from chat agent discovery.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import json\\n+import sys\\n+from pathlib import Path\\n+from unittest.mock import patch\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator.chat_capability import ( # noqa: E402\\n+ is_chat_compatible_model_id,\\n+)\\n+from contextual_orchestrator.credentials import ( # noqa: E402\\n+ InMemoryCredentialBackend,\\n+ register_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.cost_ledger import PriceBook, PriceEntry # noqa: E402\\n+from contextual_orchestrator.kv_config import InMemoryConfigStore # noqa: E402\\n+from contextual_orchestrator.model_discovery import ( # noqa: E402\\n+ DiscoveredModel,\\n+ ProviderModelSource,\\n+ agent_from_discovered,\\n+ discover_provider_models,\\n+ refresh_price_book,\\n+ select_cheapest_discovered_agent,\\n+ select_top_n_cheapest_discovered_agents,\\n+)\\n+from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n+\\n+\\n+class _Response:\\n+ \\\"\\\"\\\"Small context-managed HTTP response used by the offline regression.\\\"\\\"\\\"\\n+\\n+ def __init__(self, payload: dict[str, object]) -> None:\\n+ self._body = json.dumps(payload).encode(\\\"utf-8\\\")\\n+\\n+ def __enter__(self) -> \\\"_Response\\\":\\n+ return self\\n+\\n+ def __exit__(self, *_args: object) -> bool:\\n+ return False\\n+\\n+ def read(self, _size: int = -1) -> bytes:\\n+ return self._body\\n+\\n+\\n+@pytest.fixture(autouse=True)\\n+def _fresh_credential_backend():\\n+ \\\"\\\"\\\"Keep the provider credential registry isolated between tests.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ yield\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+def _model(model_id: str, *, priced: bool = False) -> DiscoveredModel:\\n+ \\\"\\\"\\\"Build one synthetic discovered model for capability-boundary tests.\\\"\\\"\\\"\\n+ return DiscoveredModel(\\n+ provider_name=\\\"enterprise_gateway\\\",\\n+ model_id=model_id,\\n+ credential_name=\\\"GATEWAY_API_KEY\\\",\\n+ chat_base_url=\\\"https://gateway.example.test/v1\\\",\\n+ auth_scheme=\\\"Bearer\\\",\\n+ prompt_price_per_1k=1.0 if priced else None,\\n+ completion_price_per_1k=1.0 if priced else None,\\n+ )\\n+\\n+\\n+def _agent(\\n+ agent_id: str,\\n+ model_id: str,\\n+ *,\\n+ priority: int = 0,\\n+ tags: tuple[str, ...] = (\\\"writing\\\",),\\n+) -> ModelAgent:\\n+ \\\"\\\"\\\"Build one mock-backed runtime agent for selection-path regressions.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ id=agent_id,\\n+ model=model_id,\\n+ base_url=\\\"mock://local\\\",\\n+ priority=priority,\\n+ tags=tags,\\n+ )\\n+\\n+\\n+def test_embedding_deployments_never_enter_chat_agent_discovery() -> None:\\n+ \\\"\\\"\\\"Exclude the exact Azure embedding deployment seen in synthesis alerts.\\\"\\\"\\\"\\n+ register_credential(\\\"GATEWAY_API_KEY\\\", \\\"gateway-secret\\\")\\n+ source = ProviderModelSource(\\n+ provider_name=\\\"enterprise_gateway\\\",\\n+ credential_name=\\\"GATEWAY_API_KEY\\\",\\n+ list_url=\\\"https://gateway.example.test/v1/models\\\",\\n+ chat_base_url=\\\"https://gateway.example.test/v1\\\",\\n+ )\\n+ payload = {\\n+ \\\"data\\\": [\\n+ {\\\"id\\\": \\\"azure/text-embedding-3-large\\\"},\\n+ {\\\"id\\\": \\\"text_embedding_3_large\\\"},\\n+ {\\\"id\\\": \\\"BAAI/bge-m3\\\"},\\n+ {\\\"id\\\": \\\"openai/whisper-1\\\"},\\n+ {\\\"id\\\": \\\"gpt-4o-mini-transcribe\\\"},\\n+ {\\\"id\\\": \\\"text-moderation-latest\\\"},\\n+ {\\\"id\\\": \\\"company/reranker-v2\\\"},\\n+ {\\\"id\\\": \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\"},\\n+ {\\\"id\\\": \\\"gpt-audio\\\"},\\n+ {\\\"id\\\": \\\"gpt-5.2\\\"},\\n+ {\\\"id\\\": \\\"qwen/qwen3-235b-a22b-instruct\\\"},\\n+ ]\\n+ }\\n+\\n+ with patch(\\n+ \\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\",\\n+ return_value=_Response(payload),\\n+ ):\\n+ discovered = discover_provider_models(source)\\n+\\n+ assert [model.model_id for model in discovered] == [\\n+ \\\"gpt-audio\\\",\\n+ \\\"gpt-5.2\\\",\\n+ \\\"qwen/qwen3-235b-a22b-instruct\\\",\\n+ ]\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"model_id\\\", \\\"expected\\\"),\\n+ [\\n+ (None, False),\\n+ (\\\"\\\", False),\\n+ (\\\"---\\\", False),\\n+ (\\\"vendor/embeddingv2\\\", False),\\n+ (\\\"vendor/reranking-v2\\\", False),\\n+ (\\\"vendor/transcriber-v2\\\", False),\\n+ (\\\"gpt-5.2\\\", True),\\n+ (\\\"qwen/qwen3-instruct\\\", True),\\n+ ],\\n+)\\n+def test_chat_compatibility_normalizes_identifiers(\\n+ model_id: object, expected: bool\\n+) -> None:\\n+ \\\"\\\"\\\"Normalize provider prefixes and separators without guessing chat features.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id) is expected # type: ignore[arg-type]\\n+\\n+\\n+def test_bytez_chat_catalog_still_rejects_non_chat_identifiers() -> None:\\n+ \\\"\\\"\\\"Apply the same boundary even when a provider accepts a chat task filter.\\\"\\\"\\\"\\n+ register_credential(\\\"BYTEZ_API_KEY\\\", \\\"bytez-secret\\\")\\n+ source = ProviderModelSource(\\n+ provider_name=\\\"bytez\\\",\\n+ credential_name=\\\"BYTEZ_API_KEY\\\",\\n+ list_url=\\\"https://api.bytez.com/models/v2/list/models\\\",\\n+ chat_base_url=\\\"https://api.bytez.com/models/v2/openai/v1\\\",\\n+ auth_scheme=\\\"Key\\\",\\n+ style=\\\"bytez\\\",\\n+ task_filter=\\\"chat\\\",\\n+ )\\n+ payload = {\\n+ \\\"output\\\": [\\n+ {\\\"modelId\\\": \\\"vendor/embeddingv2\\\"},\\n+ {\\\"modelId\\\": \\\"vendor/chat-instruct\\\"},\\n+ ]\\n+ }\\n+\\n+ with patch(\\n+ \\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\",\\n+ return_value=_Response(payload),\\n+ ):\\n+ discovered = discover_provider_models(source)\\n+\\n+ assert [model.model_id for model in discovered] == [\\\"vendor/chat-instruct\\\"]\\n+\\n+\\n+def test_non_chat_discovery_cannot_be_converted_to_agent() -> None:\\n+ \\\"\\\"\\\"Keep manually constructed discovery rows from bypassing the parser filter.\\\"\\\"\\\"\\n+ with pytest.raises(ValueError, match=\\\"general chat agent\\\"):\\n+ agent_from_discovered(_model(\\\"azure/text-embedding-3-large\\\"))\\n+\\n+\\n+def test_non_chat_discovery_is_not_priced_or_selected_for_chat() -> None:\\n+ \\\"\\\"\\\"Keep price routing from reintroducing an incompatible endpoint model.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ embedding_model = _model(\\\"azure/text-embedding-3-large\\\", priced=True)\\n+ chat_model = _model(\\\"gpt-5.2\\\", priced=True)\\n+ price_book.set_price(PriceEntry(\\\"enterprise_gateway\\\", \\\"gpt-5.2\\\", 1.0, 1.0))\\n+\\n+ assert refresh_price_book([embedding_model, chat_model], price_book) == 1\\n+ assert price_book.get_price(\\n+ \\\"enterprise_gateway\\\", \\\"azure/text-embedding-3-large\\\"\\n+ ) is None\\n+ assert select_cheapest_discovered_agent([embedding_model], price_book) is None\\n+ assert select_top_n_cheapest_discovered_agents(\\n+ [embedding_model], price_book, 1\\n+ ) == []\\n+\\n+\\n+def test_stale_embedding_agent_cannot_win_synthesizer_selection() -> None:\\n+ \\\"\\\"\\\"Exclude an already-persisted embedding row even when it has high priority.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ priority=10_000,\\n+ )\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent])\\n+\\n+ assert orchestrator._select_agent(\\\"Produce the final answer.\\\", \\\"synthesizer\\\") is chat_agent\\n+\\n+\\n+def test_all_non_chat_agents_fail_before_synthesis() -> None:\\n+ \\\"\\\"\\\"Fail closed when a stale pool contains no chat-compatible worker.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator(\\n+ [_agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")]\\n+ )\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"chat-compatible\\\"):\\n+ orchestrator._select_agent(\\\"Produce the final answer.\\\", \\\"synthesizer\\\")\\n+\\n+\\n+def test_generated_plan_reselects_non_chat_agent_assignment() -> None:\\n+ \\\"\\\"\\\"Do not trust a generated plan that names a stale embedding agent directly.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ priority=10_000,\\n+ )\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent])\\n+ raw_plan = json.dumps(\\n+ {\\n+ \\\"steps\\\": [\\n+ {\\n+ \\\"id\\\": 0,\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"chat_agent\\\",\\n+ \\\"subtask\\\": \\\"Execute the task.\\\",\\n+ \\\"access\\\": [],\\n+ },\\n+ {\\n+ \\\"id\\\": 1,\\n+ \\\"role\\\": \\\"synthesizer\\\",\\n+ \\\"agent_id\\\": \\\"embedding_agent\\\",\\n+ \\\"subtask\\\": \\\"Produce the final answer.\\\",\\n+ \\\"access\\\": [0],\\n+ },\\n+ ]\\n+ }\\n+ )\\n+\\n+ steps = orchestrator._parse_workflow_plan(raw_plan)\\n+\\n+ assert steps[-1].agent_id == \\\"chat_agent\\\"\\n+\\n+\\n+def test_failover_candidates_exclude_stale_embedding_agents() -> None:\\n+ \\\"\\\"\\\"Keep cross-agent retry from falling through to an incompatible endpoint.\\\"\\\"\\\"\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ embedding_agent = _agent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ priority=10_000,\\n+ )\\n+ orchestrator = TaskOrchestrator([chat_agent, embedding_agent])\\n+\\n+ candidates = orchestrator._failover_candidates(\\n+ chat_agent,\\n+ \\\"Produce the final answer.\\\",\\n+ \\\"synthesizer\\\",\\n+ )\\n+\\n+ assert candidates == [chat_agent]\\n+\\n+\\n+def test_invoke_fails_clearly_when_no_general_chat_agent_remains() -> None:\\n+ \\\"\\\"\\\"Report the role boundary instead of claiming that zero candidates failed.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent])\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"no chat-compatible agent available\\\"):\\n+ orchestrator._invoke(\\n+ embedding_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Produce the final answer.\\\"}],\\n+ text=\\\"Produce the final answer.\\\",\\n+ role=\\\"worker\\\",\\n+ )\\n+\\n+\\n+def test_model_client_rejects_non_chat_model_before_mock_or_network_call() -> None:\\n+ \\\"\\\"\\\"Keep the provider boundary fail-closed even when selection is bypassed.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ client.chat(\\n+ embedding_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Produce the final answer.\\\"}],\\n+ )\\n+\\n+\\n+def test_non_chat_primary_fails_over_only_to_chat_agents() -> None:\\n+ \\\"\\\"\\\"Drop an incompatible primary while retaining a compatible fallback.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent])\\n+\\n+ candidates = orchestrator._failover_candidates(\\n+ embedding_agent,\\n+ \\\"Produce the final answer.\\\",\\n+ \\\"synthesizer\\\",\\n+ )\\n+\\n+ assert candidates == [chat_agent]\\n+\\n+\\n+def test_streaming_client_rejects_non_chat_model_before_transport() -> None:\\n+ \\\"\\\"\\\"Apply the same endpoint boundary to streaming chat requests.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ next(\\n+ client.stream_chat(\\n+ embedding_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Produce the final answer.\\\"}],\\n+ )\\n+ )\\n+\\n+\\n+def test_probe_reports_non_chat_model_without_provider_transport(monkeypatch) -> None:\\n+ \\\"\\\"\\\"Readiness must fail closed with a stable code before network access.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ monkeypatch.setattr(\\n+ client,\\n+ \\\"_validate_provider\\\",\\n+ lambda _agent: (_ for _ in ()).throw(AssertionError(\\\"transport reached\\\")),\\n+ )\\n+\\n+ assert client.probe(embedding_agent)[\\\"failure_code\\\"] == \\\"non_chat_model\\\"\\n+\\n+\\n+def test_generated_planner_inventory_excludes_non_chat_agents() -> None:\\n+ \\\"\\\"\\\"Do not advertise stale endpoint-incompatible agents to the planner.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))\\n+\\n+ class PlannerClient:\\n+ def __init__(self) -> None:\\n+ self.system_prompt = \\\"\\\"\\n+\\n+ def chat(self, _agent, messages, **_kwargs):\\n+ self.system_prompt = messages[0][\\\"content\\\"]\\n+ return json.dumps(\\n+ {\\n+ \\\"steps\\\": [\\n+ {\\n+ \\\"id\\\": 0,\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"chat_agent\\\",\\n+ \\\"subtask\\\": \\\"Execute the task.\\\",\\n+ \\\"access\\\": [],\\n+ },\\n+ {\\n+ \\\"id\\\": 1,\\n+ \\\"role\\\": \\\"synthesizer\\\",\\n+ \\\"agent_id\\\": \\\"chat_agent\\\",\\n+ \\\"subtask\\\": \\\"Produce the answer.\\\",\\n+ \\\"access\\\": [0],\\n+ },\\n+ ]\\n+ }\\n+ )\\n+\\n+ client = PlannerClient()\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent], client=client)\\n+\\n+ steps = orchestrator._plan_generated(\\\"Produce the final answer.\\\")\\n+\\n+ assert steps[-1].agent_id == \\\"chat_agent\\\"\\n+ assert \\\"embedding_agent\\\" not in client.system_prompt\\n+ assert \\\"azure/text-embedding-3-large\\\" not in client.system_prompt\\n+ assert \\\"chat_agent\\\" in client.system_prompt\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"927c213243303d7343bc0b9feb2d0ab58f3ea9c5\", \"filename\": \"tests/test_chat_passthrough_capability_isolation.py\", \"status\": \"added\", \"additions\": 127, \"deletions\": 0, \"changes\": 127, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_passthrough_capability_isolation.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_passthrough_capability_isolation.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_passthrough_capability_isolation.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,127 @@\\n+\\\"\\\"\\\"Regression tests for chat-capability checks on passthrough and batch paths.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import sys\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n+\\n+\\n+def _embedding_agent() -> ModelAgent:\\n+ \\\"\\\"\\\"Build the stale embedding agent from the production incident.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ base_url=\\\"mock://local\\\",\\n+ )\\n+\\n+\\n+def _chat_agent() -> ModelAgent:\\n+ \\\"\\\"\\\"Build one compatible fallback for explicit-model passthrough tests.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ \\\"general_chat_agent\\\",\\n+ \\\"gpt-5.2\\\",\\n+ base_url=\\\"mock://local\\\",\\n+ tags=(\\\"writing\\\",),\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"endpoint\\\",\\n+ [\\n+ \\\"chat/completions\\\",\\n+ \\\"/v1/chat/completions\\\",\\n+ \\\"completions\\\",\\n+ \\\"/v1/completions\\\",\\n+ \\\"responses\\\",\\n+ \\\"/v1/responses\\\",\\n+ ],\\n+)\\n+def test_proxy_send_rejects_embedding_before_mock_or_network_transport(endpoint: str) -> None:\\n+ \\\"\\\"\\\"Keep raw OpenAI passthrough from bypassing the chat transport invariant.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ client.proxy_send(\\n+ _embedding_agent(),\\n+ endpoint,\\n+ {\\n+ \\\"model\\\": \\\"azure/text-embedding-3-large\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Return JSON.\\\"}],\\n+ \\\"input\\\": \\\"Return JSON.\\\",\\n+ },\\n+ )\\n+\\n+\\n+def test_explicit_embedding_model_cannot_bypass_through_structured_passthrough() -> None:\\n+ \\\"\\\"\\\"Reject an explicitly requested stale embedding agent before raw proxy transport.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator([_embedding_agent(), _chat_agent()])\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"azure/text-embedding-3-large\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Return JSON.\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+\\n+def test_explicit_embedding_model_cannot_bypass_through_responses_passthrough() -> None:\\n+ \\\"\\\"\\\"Apply the same transport contract to the Responses passthrough path.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator([_embedding_agent(), _chat_agent()])\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"azure/text-embedding-3-large\\\",\\n+ \\\"input\\\": \\\"Return JSON.\\\",\\n+ },\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+\\n+def test_batch_chat_rejects_embedding_before_mock_or_network_transport() -> None:\\n+ \\\"\\\"\\\"Prevent direct batch callers from submitting embedding models as chat jobs.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ client.batch_chat(\\n+ _embedding_agent(),\\n+ {\\\"task_0\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Return JSON.\\\"}]},\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"gpt-audio\\\",\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ ],\\n+)\\n+def test_chat_served_specialized_models_remain_valid_passthrough_transports(model_id: str) -> None:\\n+ \\\"\\\"\\\"Do not turn ordinary-role exclusion into a false transport rejection.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ agent = ModelAgent(\\\"specialized_chat_agent\\\", model_id, base_url=\\\"mock://local\\\")\\n+\\n+ response = client.proxy_send(\\n+ agent,\\n+ \\\"chat/completions\\\",\\n+ {\\n+ \\\"model\\\": model_id,\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Classify this.\\\"}],\\n+ },\\n+ )\\n+\\n+ assert response[\\\"object\\\"] == \\\"chat.completion\\\"\\n+ assert response[\\\"model\\\"] == model_id\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"17f5b7277e09d9d5e07e8380f7adadfea4477e8e\", \"filename\": \"tests/test_chat_transport_role_separation.py\", \"status\": \"added\", \"additions\": 84, \"deletions\": 0, \"changes\": 84, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_transport_role_separation.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_transport_role_separation.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_transport_role_separation.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,84 @@\\n+\\\"\\\"\\\"Regression coverage for chat transport versus ordinary agent-role eligibility.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import sys\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator.chat_capability import ( # noqa: E402\\n+ is_chat_compatible_model_id,\\n+ is_general_chat_agent_model_id,\\n+)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"gpt-audio\\\",\\n+ \\\"gpt-audio-mini\\\",\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-content-safety\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-topic-control\\\",\\n+ ],\\n+)\\n+def test_chat_served_models_remain_transport_compatible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Do not pre-reject models that provider contracts serve through chat completions.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-content-safety\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-topic-control\\\",\\n+ ],\\n+)\\n+def test_policy_classifiers_do_not_enter_general_agent_roles(model_id: str) -> None:\\n+ \\\"\\\"\\\"Keep chat-served policy classifiers out of ordinary synthesis roles.\\\"\\\"\\\"\\n+ assert not is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"gpt-audio\\\",\\n+ \\\"gpt-audio-mini\\\",\\n+ \\\"gpt-5.2\\\",\\n+ \\\"qwen/qwen3-235b-a22b-instruct\\\",\\n+ ],\\n+)\\n+def test_general_generation_models_remain_agent_eligible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Preserve chat generation models for ordinary agent selection.\\\"\\\"\\\"\\n+ assert is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ \\\"text_embedding_3_large\\\",\\n+ \\\"company/reranker-v2\\\",\\n+ \\\"gpt-4o-mini-transcribe\\\",\\n+ \\\"omni-moderation-latest\\\",\\n+ \\\"gpt-image-1\\\",\\n+ \\\"dall-e-3\\\",\\n+ \\\"openai/clip-vit-large-patch14\\\",\\n+ \\\"google/siglip-so400m-patch14-384\\\",\\n+ \\\"sora-2\\\",\\n+ \\\"gpt-realtime\\\",\\n+ \\\"tts-1\\\",\\n+ ],\\n+)\\n+def test_endpoint_only_models_fail_both_boundaries(model_id: str) -> None:\\n+ \\\"\\\"\\\"Reject endpoint-only model families before transport or ordinary role routing.\\\"\\\"\\\"\\n+ assert not is_chat_compatible_model_id(model_id)\\n+ assert not is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"6d378336f1109b6c262b678b28b0e88abe84fd2e\", \"filename\": \"tests/test_cost_ledger.py\", \"status\": \"modified\", \"additions\": 50, \"deletions\": 0, \"changes\": 50, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_cost_ledger.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_cost_ledger.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_cost_ledger.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -98,6 +98,56 @@ def test_provider_wildcard_price_entry() -> None:\\n assert record.cost_amount == 2.0\\n \\n \\n+def test_corrupt_specific_row_still_falls_back_to_wildcard_price() -> None:\\n+ \\\"\\\"\\\"A malformed provider:model row must not shadow a valid provider:* row.\\\"\\\"\\\"\\n+ config = InMemoryConfigStore()\\n+ price_book = PriceBook(config)\\n+ price_book.set_price(PriceEntry(\\\"openai\\\", \\\"*\\\", prompt_price_per_1k=1.0, completion_price_per_1k=1.0))\\n+ config.set(\\\"llm_price_entries\\\", \\\"openai:broken-model\\\", {\\\"prompt_price_per_1k\\\": \\\"not-a-number\\\"})\\n+\\n+ entry = price_book.get_price(\\\"openai\\\", \\\"broken-model\\\")\\n+\\n+ assert entry is not None\\n+ assert entry.prompt_price_per_1k == 1.0\\n+ assert entry.completion_price_per_1k == 1.0\\n+\\n+\\n+def test_underflowing_positive_price_row_falls_back_to_wildcard() -> None:\\n+ \\\"\\\"\\\"A nonzero KV price that underflows to 0.0 must not be treated as free.\\\"\\\"\\\"\\n+ config = InMemoryConfigStore()\\n+ price_book = PriceBook(config)\\n+ price_book.set_price(PriceEntry(\\\"openai\\\", \\\"*\\\", prompt_price_per_1k=1.0, completion_price_per_1k=1.0))\\n+ config.set(\\n+ \\\"llm_price_entries\\\",\\n+ \\\"openai:underflow-model\\\",\\n+ {\\\"prompt_price_per_1k\\\": \\\"1e-10000\\\", \\\"completion_price_per_1k\\\": \\\"1e-10000\\\"},\\n+ )\\n+\\n+ entry = price_book.get_price(\\\"openai\\\", \\\"underflow-model\\\")\\n+\\n+ assert entry is not None\\n+ assert entry.prompt_price_per_1k == 1.0\\n+ assert entry.completion_price_per_1k == 1.0\\n+\\n+\\n+def test_overflowing_price_row_falls_back_to_wildcard() -> None:\\n+ \\\"\\\"\\\"A Decimal-finite KV price whose float() conversion overflows to inf must not be treated as valid.\\\"\\\"\\\"\\n+ config = InMemoryConfigStore()\\n+ price_book = PriceBook(config)\\n+ price_book.set_price(PriceEntry(\\\"openai\\\", \\\"*\\\", prompt_price_per_1k=1.0, completion_price_per_1k=1.0))\\n+ config.set(\\n+ \\\"llm_price_entries\\\",\\n+ \\\"openai:overflow-model\\\",\\n+ {\\\"prompt_price_per_1k\\\": \\\"1e10000\\\", \\\"completion_price_per_1k\\\": \\\"1e10000\\\"},\\n+ )\\n+\\n+ entry = price_book.get_price(\\\"openai\\\", \\\"overflow-model\\\")\\n+\\n+ assert entry is not None\\n+ assert entry.prompt_price_per_1k == 1.0\\n+ assert entry.completion_price_per_1k == 1.0\\n+\\n+\\n def test_writes_carry_full_attribution() -> None:\\n ledger = _priced_ledger()\\n record = ledger.record_usage(\" }, { \"sha\": \"444557f755a4983db4ce6ce1be67724737efddf8\", \"filename\": \"tests/test_discover_models_cli.py\", \"status\": \"modified\", \"additions\": 46, \"deletions\": 0, \"changes\": 46, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_discover_models_cli.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_discover_models_cli.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_discover_models_cli.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -164,3 +164,49 @@ def urlopen(request, timeout=None):\\n by_id = {agent.id: agent for agent in reloaded.candidates}\\n assert by_id[\\\"openrouter_cheap_model\\\"].disabled is False\\n assert by_id[\\\"openai_pricey_model\\\"].disabled is True\\n+\\n+\\n+def test_enable_cheapest_bootstraps_independent_provider_families(tmp_path) -> None:\\n+ \\\"\\\"\\\"CLI bootstrap must use the provider-diverse selector, not only the cheapest vendor.\\\"\\\"\\\"\\n+ from contextual_orchestrator import TaskOrchestrator\\n+ from contextual_orchestrator.orchestrator import ModelAgent\\n+\\n+ set_backend(InMemoryCredentialBackend())\\n+ register_credential(\\\"OPENAI_API_KEY\\\", \\\"sk-openai\\\")\\n+ register_credential(\\\"OPENROUTER_API_KEY\\\", \\\"sk-router\\\")\\n+ register_credential(\\\"NVIDIA_NIM_API_KEY\\\", \\\"nv-primary\\\")\\n+ db_path = str(tmp_path / \\\"pool.db\\\")\\n+ stdout = StringIO()\\n+\\n+ def urlopen(request, timeout=None):\\n+ host = urllib.parse.urlsplit(request.full_url).hostname\\n+ payloads = {\\n+ \\\"api.openai.com\\\": {\\\"data\\\": [{\\\"id\\\": \\\"openai-model\\\", \\\"pricing\\\": {\\\"prompt\\\": \\\"0.001\\\", \\\"completion\\\": \\\"0.001\\\"}}]},\\n+ \\\"openrouter.ai\\\": {\\\"data\\\": [{\\\"id\\\": \\\"router-model\\\", \\\"pricing\\\": {\\\"prompt\\\": \\\"0.000001\\\", \\\"completion\\\": \\\"0.000001\\\"}}]},\\n+ \\\"integrate.api.nvidia.com\\\": {\\\"data\\\": [{\\\"id\\\": \\\"nim-model\\\", \\\"pricing\\\": {\\\"prompt\\\": \\\"0.000002\\\", \\\"completion\\\": \\\"0.000002\\\"}}]},\\n+ }\\n+ return _Response(payloads.get(host, {\\\"data\\\": []}))\\n+\\n+ try:\\n+ with (\\n+ patch.object(\\n+ sys,\\n+ \\\"argv\\\",\\n+ [\\\"contextual-orchestrator\\\", \\\"discover-models\\\", \\\"--agents-db\\\", db_path, \\\"--enable-cheapest\\\", \\\"3\\\"],\\n+ ),\\n+ patch.object(sys, \\\"stdout\\\", stdout),\\n+ patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen),\\n+ ):\\n+ main()\\n+ finally:\\n+ set_backend(None)\\n+\\n+ report = json.loads(stdout.getvalue())\\n+ assert report[\\\"enabled_agent_ids\\\"] == [\\n+ \\\"openrouter_router_model\\\",\\n+ \\\"nvidia_nim_nim_model\\\",\\n+ \\\"openai_openai_model\\\",\\n+ ]\\n+ reloaded = TaskOrchestrator([ModelAgent(\\\"seed_agent\\\", \\\"seed-model\\\")], agents_db=db_path)\\n+ enabled = {agent.id for agent in reloaded.candidates if not agent.disabled}\\n+ assert enabled - {\\\"seed_agent\\\"} == set(report[\\\"enabled_agent_ids\\\"])\" }, { \"sha\": \"45ca53917fadb330596eb16b217cfe54469db111\", \"filename\": \"tests/test_discovery_bootstrap_selection.py\", \"status\": \"added\", \"additions\": 364, \"deletions\": 0, \"changes\": 364, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_discovery_bootstrap_selection.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_discovery_bootstrap_selection.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_discovery_bootstrap_selection.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,364 @@\\n+\\\"\\\"\\\"Regression coverage for honest, provider-diverse discovery bootstrap.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator.cost_ledger import PriceBook, PriceEntry\\n+from contextual_orchestrator.kv_config import InMemoryConfigStore\\n+from contextual_orchestrator import model_discovery\\n+from contextual_orchestrator.model_discovery import (\\n+ DiscoveredModel,\\n+ refresh_price_book,\\n+ select_cheapest_discovered_agent,\\n+ select_top_n_cheapest_discovered_agents,\\n+)\\n+\\n+\\n+def _model(provider_name: str, model_id: str) -> DiscoveredModel:\\n+ \\\"\\\"\\\"Build one deterministic OpenAI-compatible discovery fixture.\\\"\\\"\\\"\\n+ credential_name = f\\\"{provider_name.upper()}_API_KEY\\\"\\n+ return DiscoveredModel(\\n+ provider_name=provider_name,\\n+ model_id=model_id,\\n+ credential_name=credential_name,\\n+ chat_base_url=f\\\"https://{provider_name}.example/v1\\\",\\n+ auth_scheme=\\\"Bearer\\\",\\n+ )\\n+\\n+\\n+def _priced_model(\\n+ provider_name: str,\\n+ model_id: str,\\n+ *,\\n+ prompt_price_per_1k: float | None,\\n+ completion_price_per_1k: float | None,\\n+ currency_code: str = \\\"USD\\\",\\n+) -> DiscoveredModel:\\n+ \\\"\\\"\\\"Build one discovery row carrying provider-reported price evidence.\\\"\\\"\\\"\\n+ base = _model(provider_name, model_id)\\n+ return DiscoveredModel(\\n+ provider_name=base.provider_name,\\n+ model_id=base.model_id,\\n+ credential_name=base.credential_name,\\n+ chat_base_url=base.chat_base_url,\\n+ auth_scheme=base.auth_scheme,\\n+ prompt_price_per_1k=prompt_price_per_1k,\\n+ completion_price_per_1k=completion_price_per_1k,\\n+ currency_code=currency_code,\\n+ )\\n+\\n+\\n+def _set_price(\\n+ price_book: PriceBook,\\n+ model: DiscoveredModel,\\n+ price_per_1k: float,\\n+ *,\\n+ currency_code: str = \\\"USD\\\",\\n+) -> None:\\n+ \\\"\\\"\\\"Record one known symmetric prompt/completion price.\\\"\\\"\\\"\\n+ price_book.set_price(\\n+ PriceEntry(\\n+ model.provider_name,\\n+ model.model_id,\\n+ price_per_1k,\\n+ price_per_1k,\\n+ currency_code,\\n+ )\\n+ )\\n+\\n+\\n+def test_unpriced_discovered_model_is_unknown_not_free() -> None:\\n+ \\\"\\\"\\\"Missing price evidence must not outrank a model with a known price.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ priced = _model(\\\"openrouter\\\", \\\"priced-model\\\")\\n+ unpriced = _model(\\\"bytez\\\", \\\"unpriced-model\\\")\\n+ _set_price(price_book, priced, 0.01)\\n+\\n+ assert select_cheapest_discovered_agent([unpriced, priced], price_book) is priced\\n+ assert select_top_n_cheapest_discovered_agents(\\n+ [unpriced, priced], price_book, 2\\n+ ) == [priced, unpriced]\\n+\\n+\\n+def test_partial_provider_price_is_unknown_instead_of_fabricating_a_free_component() -> None:\\n+ \\\"\\\"\\\"A missing prompt or completion price cannot become an invented zero.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ partial = _priced_model(\\n+ \\\"partial_vendor\\\",\\n+ \\\"partial-model\\\",\\n+ prompt_price_per_1k=0.001,\\n+ completion_price_per_1k=None,\\n+ )\\n+ complete = _priced_model(\\n+ \\\"openrouter\\\",\\n+ \\\"complete-model\\\",\\n+ prompt_price_per_1k=1.0,\\n+ completion_price_per_1k=1.0,\\n+ )\\n+\\n+ assert refresh_price_book([partial, complete], price_book) == 1\\n+ assert price_book.get_price(partial.provider_name, partial.model_id) is None\\n+ assert select_cheapest_discovered_agent([partial, complete], price_book) is complete\\n+\\n+\\n+def test_persisted_price_row_missing_one_component_remains_unknown() -> None:\\n+ \\\"\\\"\\\"KV corruption must not silently manufacture a zero-priced component.\\\"\\\"\\\"\\n+ store = InMemoryConfigStore()\\n+ store.set(\\n+ \\\"llm_price_entries\\\",\\n+ \\\"partial_vendor:partial-model\\\",\\n+ {\\n+ \\\"provider_name\\\": \\\"partial_vendor\\\",\\n+ \\\"model_name\\\": \\\"partial-model\\\",\\n+ \\\"prompt_price_per_1k\\\": 0.001,\\n+ \\\"currency_code\\\": \\\"USD\\\",\\n+ },\\n+ )\\n+ price_book = PriceBook(store)\\n+\\n+ assert price_book.get_price(\\\"partial_vendor\\\", \\\"partial-model\\\") is None\\n+\\n+\\n+def test_invalid_catalog_prices_are_unknown_not_trusted_cost_evidence() -> None:\\n+ \\\"\\\"\\\"Reject negative, non-finite, and boolean provider price values.\\\"\\\"\\\"\\n+ assert model_discovery._price_per_1k(\\\"-0.000001\\\") is None\\n+ assert model_discovery._price_per_1k(\\\"nan\\\") is None\\n+ assert model_discovery._price_per_1k(\\\"inf\\\") is None\\n+ assert model_discovery._price_per_1k(True) is None\\n+ assert model_discovery._price_per_1k(\\\"0\\\") == 0.0\\n+\\n+\\n+def test_huge_price_values_remain_unknown_without_crashing_discovery_or_ranking() -> None:\\n+ \\\"\\\"\\\"Unbounded JSON or KV integers must not terminate bootstrap selection.\\\"\\\"\\\"\\n+ huge_price = 10**10000\\n+ assert model_discovery._price_per_1k(huge_price) is None\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ huge = _model(\\\"huge_vendor\\\", \\\"huge-model\\\")\\n+ valid = _model(\\\"openrouter\\\", \\\"valid-model\\\")\\n+ _set_price(price_book, huge, huge_price)\\n+ _set_price(price_book, valid, 1.0)\\n+\\n+ assert select_cheapest_discovered_agent([huge, valid], price_book) is valid\\n+\\n+\\n+def test_malformed_price_book_row_is_unknown_instead_of_crashing_selection() -> None:\\n+ \\\"\\\"\\\"A corrupt persisted price row must not take down the serving bootstrap.\\\"\\\"\\\"\\n+ store = InMemoryConfigStore()\\n+ store.set(\\n+ \\\"llm_price_entries\\\",\\n+ \\\"broken_vendor:broken-model\\\",\\n+ {\\n+ \\\"provider_name\\\": \\\"broken_vendor\\\",\\n+ \\\"model_name\\\": \\\"broken-model\\\",\\n+ \\\"prompt_price_per_1k\\\": \\\"not-a-number\\\",\\n+ \\\"completion_price_per_1k\\\": 0.001,\\n+ \\\"currency_code\\\": \\\"USD\\\",\\n+ },\\n+ )\\n+ price_book = PriceBook(store)\\n+ broken = _model(\\\"broken_vendor\\\", \\\"broken-model\\\")\\n+ valid = _model(\\\"openrouter\\\", \\\"valid-model\\\")\\n+ _set_price(price_book, valid, 1.0)\\n+\\n+ assert select_cheapest_discovered_agent([broken, valid], price_book) is valid\\n+\\n+\\n+def test_refresh_counts_only_complete_prices_in_the_comparison_currency() -> None:\\n+ \\\"\\\"\\\"Cross-currency evidence is unknown until an explicit conversion exists.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore(), default_currency=\\\"USD\\\")\\n+ usd = _priced_model(\\n+ \\\"openrouter\\\",\\n+ \\\"usd-model\\\",\\n+ prompt_price_per_1k=1.0,\\n+ completion_price_per_1k=1.0,\\n+ currency_code=\\\"USD\\\",\\n+ )\\n+ eur = _priced_model(\\n+ \\\"eur_vendor\\\",\\n+ \\\"eur-model\\\",\\n+ prompt_price_per_1k=0.001,\\n+ completion_price_per_1k=0.001,\\n+ currency_code=\\\"EUR\\\",\\n+ )\\n+\\n+ assert refresh_price_book([eur, usd], price_book) == 1\\n+ assert price_book.get_price(\\\"eur_vendor\\\", \\\"eur-model\\\") is None\\n+ assert price_book.get_price(\\\"openrouter\\\", \\\"usd-model\\\") is not None\\n+\\n+\\n+def test_invalid_or_cross_currency_price_rows_do_not_outrank_comparable_usd_cost() -> None:\\n+ \\\"\\\"\\\"Only finite non-negative prices in the configured currency are comparable.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore(), default_currency=\\\"USD\\\")\\n+ valid = _model(\\\"openrouter\\\", \\\"valid-model\\\")\\n+ negative = _model(\\\"negative_vendor\\\", \\\"negative-model\\\")\\n+ non_finite = _model(\\\"nan_vendor\\\", \\\"nan-model\\\")\\n+ foreign = _model(\\\"eur_vendor\\\", \\\"eur-model\\\")\\n+\\n+ _set_price(price_book, valid, 1.0)\\n+ _set_price(price_book, negative, -100.0)\\n+ _set_price(price_book, non_finite, float(\\\"nan\\\"))\\n+ _set_price(price_book, foreign, 0.000001, currency_code=\\\"EUR\\\")\\n+\\n+ assert select_cheapest_discovered_agent(\\n+ [negative, non_finite, foreign, valid],\\n+ price_book,\\n+ ) is valid\\n+\\n+\\n+def test_duplicate_serving_identity_cannot_consume_bootstrap_capacity() -> None:\\n+ \\\"\\\"\\\"A repeated provider/model row must not masquerade as failover diversity.\\\"\\\"\\\"\\n+ selector = getattr(\\n+ model_discovery,\\n+ \\\"select_bootstrap_discovered_agents\\\",\\n+ None,\\n+ )\\n+ assert callable(selector), \\\"missing provider-diverse bootstrap selector\\\"\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ duplicate_first = _model(\\\"openrouter\\\", \\\"same-model\\\")\\n+ duplicate_second = _model(\\\"openrouter\\\", \\\"same-model\\\")\\n+ independent = _model(\\\"openai\\\", \\\"independent-model\\\")\\n+ _set_price(price_book, duplicate_first, 0.01)\\n+ _set_price(price_book, independent, 0.02)\\n+\\n+ selected = selector(\\n+ [duplicate_second, independent, duplicate_first],\\n+ price_book,\\n+ 3,\\n+ )\\n+ top_n = select_top_n_cheapest_discovered_agents(\\n+ [duplicate_second, independent, duplicate_first],\\n+ price_book,\\n+ 3,\\n+ )\\n+\\n+ assert [\\n+ (model.provider_name, model.model_id)\\n+ for model in selected\\n+ ] == [\\n+ (\\\"openrouter\\\", \\\"same-model\\\"),\\n+ (\\\"openai\\\", \\\"independent-model\\\"),\\n+ ]\\n+ assert [\\n+ (model.provider_name, model.model_id)\\n+ for model in top_n\\n+ ] == [\\n+ (\\\"openrouter\\\", \\\"same-model\\\"),\\n+ (\\\"openai\\\", \\\"independent-model\\\"),\\n+ ]\\n+\\n+\\n+def test_conflicting_duplicate_prices_are_withheld_as_ambiguous() -> None:\\n+ \\\"\\\"\\\"Do not let provider row order decide the trusted price for one agent id.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ cheap_claim = _priced_model(\\n+ \\\"openrouter\\\",\\n+ \\\"duplicate-model\\\",\\n+ prompt_price_per_1k=0.000001,\\n+ completion_price_per_1k=0.000001,\\n+ )\\n+ expensive_claim = _priced_model(\\n+ \\\"openrouter\\\",\\n+ \\\"duplicate-model\\\",\\n+ prompt_price_per_1k=100.0,\\n+ completion_price_per_1k=100.0,\\n+ )\\n+ complete = _priced_model(\\n+ \\\"openai\\\",\\n+ \\\"complete-model\\\",\\n+ prompt_price_per_1k=1.0,\\n+ completion_price_per_1k=1.0,\\n+ )\\n+\\n+ assert refresh_price_book(\\n+ [cheap_claim, expensive_claim, complete],\\n+ price_book,\\n+ ) == 1\\n+ assert price_book.get_price(\\\"openrouter\\\", \\\"duplicate-model\\\") is None\\n+ assert select_cheapest_discovered_agent(\\n+ [cheap_claim, expensive_claim, complete],\\n+ price_book,\\n+ ) is complete\\n+\\n+\\n+def test_bootstrap_selector_prefers_provider_diversity_before_duplicates() -> None:\\n+ \\\"\\\"\\\"The initial failover pool must span providers before repeating one.\\\"\\\"\\\"\\n+ selector = getattr(\\n+ model_discovery,\\n+ \\\"select_bootstrap_discovered_agents\\\",\\n+ None,\\n+ )\\n+ assert callable(selector), \\\"missing provider-diverse bootstrap selector\\\"\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ router_cheapest = _model(\\\"openrouter\\\", \\\"router-cheapest\\\")\\n+ router_second = _model(\\\"openrouter\\\", \\\"router-second\\\")\\n+ nim_model = _model(\\\"nvidia_nim\\\", \\\"nim-model\\\")\\n+ openai_model = _model(\\\"openai\\\", \\\"openai-model\\\")\\n+ _set_price(price_book, router_cheapest, 0.01)\\n+ _set_price(price_book, router_second, 0.02)\\n+ _set_price(price_book, nim_model, 0.5)\\n+ _set_price(price_book, openai_model, 1.0)\\n+\\n+ selected = selector(\\n+ [router_second, openai_model, nim_model, router_cheapest],\\n+ price_book,\\n+ 3,\\n+ )\\n+\\n+ assert selected == [router_cheapest, nim_model, openai_model]\\n+\\n+\\n+def test_bootstrap_selector_treats_nim_primary_and_sub_as_one_outage_domain() -> None:\\n+ \\\"\\\"\\\"Two NIM keys must not displace an independently hosted provider.\\\"\\\"\\\"\\n+ selector = getattr(\\n+ model_discovery,\\n+ \\\"select_bootstrap_discovered_agents\\\",\\n+ None,\\n+ )\\n+ assert callable(selector), \\\"missing provider-diverse bootstrap selector\\\"\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ nim_primary = _model(\\\"nvidia_nim\\\", \\\"primary-model\\\")\\n+ nim_sub = _model(\\\"nvidia_nim_sub\\\", \\\"sub-model\\\")\\n+ openrouter = _model(\\\"openrouter\\\", \\\"router-model\\\")\\n+ _set_price(price_book, nim_primary, 0.01)\\n+ _set_price(price_book, nim_sub, 0.02)\\n+ _set_price(price_book, openrouter, 0.5)\\n+\\n+ selected = selector(\\n+ [nim_sub, openrouter, nim_primary],\\n+ price_book,\\n+ 2,\\n+ )\\n+\\n+ assert selected == [nim_primary, openrouter]\\n+\\n+\\n+def test_bootstrap_selector_is_deterministic_when_every_model_is_unpriced() -> None:\\n+ \\\"\\\"\\\"All-unpriced discovery remains usable but never order-dependent.\\\"\\\"\\\"\\n+ selector = getattr(\\n+ model_discovery,\\n+ \\\"select_bootstrap_discovered_agents\\\",\\n+ None,\\n+ )\\n+ assert callable(selector), \\\"missing provider-diverse bootstrap selector\\\"\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ router_z = _model(\\\"openrouter\\\", \\\"z-model\\\")\\n+ router_a = _model(\\\"openrouter\\\", \\\"a-model\\\")\\n+ nim_b = _model(\\\"nvidia_nim\\\", \\\"b-model\\\")\\n+\\n+ selected = selector(\\n+ [router_z, nim_b, router_a],\\n+ price_book,\\n+ 3,\\n+ )\\n+\\n+ assert selected == [nim_b, router_a, router_z]\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"f1eca6acd721c760f601ef90b3c10d0b0419810f\", \"filename\": \"tests/test_local_mlx.py\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 2, \"changes\": 5, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_local_mlx.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_local_mlx.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_local_mlx.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -350,7 +350,8 @@ def test_response_without_content_or_reasoning_fails_clearly() -> None:\\n ModelClient()._response_content(agent, {\\\"choices\\\": [{\\\"message\\\": {}}]})\\n \\n \\n-def test_local_responses_passthrough_adapts_to_chat_transport() -> None:\\n+@pytest.mark.parametrize(\\\"endpoint\\\", [\\\"responses\\\", \\\"/v1/responses\\\"])\\n+def test_local_responses_passthrough_adapts_to_chat_transport(endpoint: str) -> None:\\n agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n client = ModelClient(max_retries=0, chat_template_args={\\\"enable_thinking\\\": False})\\n with patch.object(client, \\\"_validate_provider\\\", return_value=None), patch.object(\\n@@ -369,7 +370,7 @@ def test_local_responses_passthrough_adapts_to_chat_transport() -> None:\\n ) as send:\\n response = client.proxy_send(\\n agent,\\n- \\\"responses\\\",\\n+ endpoint,\\n {\\n \\\"model\\\": \\\"local-model\\\",\\n \\\"instructions\\\": \\\"Be concise.\\\",\" }, { \"sha\": \"d1974a88c31401c752fed7932bb4ce9bd48d6efc\", \"filename\": \"tests/test_model_discovery.py\", \"status\": \"modified\", \"additions\": 8, \"deletions\": 0, \"changes\": 8, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_model_discovery.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_model_discovery.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_model_discovery.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -24,6 +24,7 @@\\n from contextual_orchestrator.model_discovery import ( # noqa: E402\\n DiscoveredModel,\\n ProviderModelSource,\\n+ _price_per_1k,\\n agent_from_discovered,\\n agent_id_for,\\n discover_all_models,\\n@@ -113,6 +114,13 @@ def urlopen(request, timeout=None):\\n assert discovered[1].prompt_price_per_1k is None\\n \\n \\n+def test_price_per_1k_rejects_underflowing_positive_value() -> None:\\n+ \\\"\\\"\\\"A nonzero per-token price that underflows to 0.0 in float stays unknown.\\\"\\\"\\\"\\n+ assert _price_per_1k(\\\"1e-10000\\\") is None\\n+ assert _price_per_1k(0) == 0.0\\n+ assert _price_per_1k(0.000001) == pytest.approx(0.001)\\n+\\n+\\n def test_discover_bytez_parses_models_with_key_auth_scheme() -> None:\\n register_credential(\\\"BYTEZ_API_KEY\\\", \\\"bytez-secret\\\")\\n payload = {\" }, { \"sha\": \"a2ce16f9562bc7470bcef3d7a090be9ed96892be\", \"filename\": \"tests/test_provider_bootstrap.py\", \"status\": \"added\", \"additions\": 405, \"deletions\": 0, \"changes\": 405, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_bootstrap.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_bootstrap.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_bootstrap.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,405 @@\\n+\\\"\\\"\\\"Contracts for durable all-provider bootstrap and provider-diverse model activation.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from dataclasses import replace\\n+import json\\n+import os\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+from contextual_orchestrator.chat_capability import is_general_chat_agent_model_id\\n+from contextual_orchestrator.credentials import (\\n+ InMemoryCredentialBackend,\\n+ get_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.model_discovery import (\\n+ DiscoveredModel,\\n+ agent_from_discovered,\\n+)\\n+from contextual_orchestrator import provider_bootstrap\\n+\\n+\\n+@pytest.fixture(autouse=True)\\n+def isolated_credential_backend():\\n+ \\\"\\\"\\\"Give each test a fresh process-local credential registry.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ yield\\n+ set_backend(None)\\n+\\n+\\n+def _complete_environment() -> dict[str, str]:\\n+ \\\"\\\"\\\"Return one complete mounted-secret fixture with trailing newlines.\\\"\\\"\\\"\\n+ return {\\n+ name: f\\\"secret-for-{name.lower()}\\\\n\\\"\\n+ for name in provider_bootstrap.PROVIDER_CREDENTIAL_NAMES\\n+ }\\n+\\n+\\n+def _model(\\n+ provider: str,\\n+ credential: str,\\n+ model_id: str,\\n+ prompt: float | None,\\n+) -> DiscoveredModel:\\n+ \\\"\\\"\\\"Build a deterministic provider-catalog row for bootstrap tests.\\\"\\\"\\\"\\n+ return DiscoveredModel(\\n+ provider_name=provider,\\n+ model_id=model_id,\\n+ credential_name=credential,\\n+ chat_base_url=f\\\"https://{provider}.example/v1\\\",\\n+ auth_scheme=\\\"Bearer\\\",\\n+ prompt_price_per_1k=prompt,\\n+ completion_price_per_1k=prompt,\\n+ )\\n+\\n+\\n+def test_fixed_inventory_matches_all_five_organization_secrets():\\n+ \\\"\\\"\\\"The bootstrap inventory must not silently lose an organization provider key.\\\"\\\"\\\"\\n+ assert set(provider_bootstrap.PROVIDER_CREDENTIAL_NAMES) == {\\n+ \\\"NVIDIA_NIM_API_KEY\\\",\\n+ \\\"NVIDIA_NIM_API_KEY_SUB\\\",\\n+ \\\"BYTEZ_API_KEY\\\",\\n+ \\\"OPENROUTER_API_KEY\\\",\\n+ \\\"OPENAI_API_KEY\\\",\\n+ }\\n+\\n+\\n+def test_collect_requires_complete_inventory_without_leaking_values():\\n+ \\\"\\\"\\\"Production bootstrap fails before writes when one trusted secret is absent.\\\"\\\"\\\"\\n+ environment = _complete_environment()\\n+ removed = environment.pop(\\\"BYTEZ_API_KEY\\\")\\n+ with pytest.raises(provider_bootstrap.ProviderBootstrapError) as raised:\\n+ provider_bootstrap.collect_provider_credentials(environment)\\n+ assert \\\"BYTEZ_API_KEY\\\" in str(raised.value)\\n+ assert removed.strip() not in str(raised.value)\\n+ assert all(\\n+ get_credential(name) is None\\n+ for name in provider_bootstrap.PROVIDER_CREDENTIAL_NAMES\\n+ )\\n+\\n+\\n+def test_atomic_memory_registration_strips_mounted_secret_newlines():\\n+ \\\"\\\"\\\"A complete inventory becomes visible together and mounted newlines are removed.\\\"\\\"\\\"\\n+ credentials = provider_bootstrap.collect_provider_credentials(\\n+ _complete_environment()\\n+ )\\n+ registered = provider_bootstrap.register_provider_credentials_atomically(\\n+ credentials\\n+ )\\n+ assert registered == tuple(\\n+ sorted(provider_bootstrap.PROVIDER_CREDENTIAL_NAMES)\\n+ )\\n+ for name in provider_bootstrap.PROVIDER_CREDENTIAL_NAMES:\\n+ value = get_credential(name)\\n+ assert value == f\\\"secret-for-{name.lower()}\\\"\\n+ assert \\\"\\\\n\\\" not in value\\n+\\n+\\n+def test_unknown_credential_name_is_rejected_before_any_write():\\n+ \\\"\\\"\\\"The fixed bootstrap boundary cannot be expanded by untrusted names.\\\"\\\"\\\"\\n+ with pytest.raises(provider_bootstrap.ProviderBootstrapError):\\n+ provider_bootstrap.register_provider_credentials_atomically(\\n+ {\\\"EVIL_PROVIDER_KEY\\\": \\\"secret\\\"}\\n+ )\\n+ assert get_credential(\\\"EVIL_PROVIDER_KEY\\\") is None\\n+\\n+\\n+def test_diverse_selection_prefers_known_cost_without_treating_unknown_as_free():\\n+ \\\"\\\"\\\"Unknown-cost candidates stay usable but cannot win as fabricated zero cost.\\\"\\\"\\\"\\n+ models = [\\n+ _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"gpt-expensive\\\", 4.0),\\n+ _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"gpt-cheap\\\", 1.0),\\n+ _model(\\\"openrouter\\\", \\\"OPENROUTER_API_KEY\\\", \\\"mistral-router\\\", 2.0),\\n+ _model(\\\"bytez\\\", \\\"BYTEZ_API_KEY\\\", \\\"llama-unknown\\\", None),\\n+ ]\\n+ selected = provider_bootstrap.select_provider_diverse_models(models, limit=3)\\n+ assert [(item.provider_name, item.model_id) for item in selected] == [\\n+ (\\\"openai\\\", \\\"gpt-cheap\\\"),\\n+ (\\\"openrouter\\\", \\\"mistral-router\\\"),\\n+ (\\\"bytez\\\", \\\"llama-unknown\\\"),\\n+ ]\\n+\\n+\\n+def test_partial_price_is_unknown_in_provider_bootstrap_ranking():\\n+ \\\"\\\"\\\"A missing prompt or completion price cannot become an invented zero.\\\"\\\"\\\"\\n+ partial = replace(\\n+ _model(\\\"bytez\\\", \\\"BYTEZ_API_KEY\\\", \\\"partial-model\\\", None),\\n+ prompt_price_per_1k=0.001,\\n+ )\\n+ complete = _model(\\\"openrouter\\\", \\\"OPENROUTER_API_KEY\\\", \\\"complete-model\\\", 1.0)\\n+\\n+ selected = provider_bootstrap.select_provider_diverse_models(\\n+ [partial, complete], limit=2\\n+ )\\n+\\n+ assert [(item.provider_name, item.model_id) for item in selected] == [\\n+ (\\\"openrouter\\\", \\\"complete-model\\\"),\\n+ (\\\"bytez\\\", \\\"partial-model\\\"),\\n+ ]\\n+\\n+\\n+def test_non_usd_price_cannot_outrank_a_comparable_usd_price():\\n+ \\\"\\\"\\\"A cheap non-USD row must not beat a pricier USD row on face value alone.\\\"\\\"\\\"\\n+ cheap_foreign = replace(\\n+ _model(\\\"openrouter\\\", \\\"OPENROUTER_API_KEY\\\", \\\"cheap-foreign\\\", 0.001),\\n+ currency_code=\\\"EUR\\\",\\n+ )\\n+ priced_usd = _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"priced-usd\\\", 1.0)\\n+\\n+ selected = provider_bootstrap.select_provider_diverse_models(\\n+ [cheap_foreign, priced_usd], limit=2\\n+ )\\n+\\n+ assert [(item.provider_name, item.model_id) for item in selected] == [\\n+ (\\\"openai\\\", \\\"priced-usd\\\"),\\n+ (\\\"openrouter\\\", \\\"cheap-foreign\\\"),\\n+ ]\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"model_id\\\", \\\"eligible\\\"),\\n+ [\\n+ (\\\"dall-e-3\\\", False),\\n+ (\\\"openai/clip-vit-large\\\", False),\\n+ (\\\"siglip-base-patch16\\\", False),\\n+ (\\\"nvidia/guard-model\\\", False),\\n+ (\\\"provider/audio-chat-model\\\", True),\\n+ (\\\"openai/gpt-4.1-mini\\\", True),\\n+ ],\\n+)\\n+def test_provider_bootstrap_reuses_shared_chat_capability_policy(model_id, eligible):\\n+ \\\"\\\"\\\"Bootstrap and runtime must agree on ordinary chat-model eligibility.\\\"\\\"\\\"\\n+ model = _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", model_id, 1.0)\\n+ assert provider_bootstrap.is_chat_serving_candidate(model) is eligible\\n+ assert eligible is is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+def test_provider_bootstrap_collapses_nim_credentials_to_one_outage_domain():\\n+ \\\"\\\"\\\"Primary and secondary NIM credentials cannot displace an independent provider.\\\"\\\"\\\"\\n+ nim_primary = _model(\\\"nvidia_nim\\\", \\\"NVIDIA_NIM_API_KEY\\\", \\\"primary-model\\\", 0.01)\\n+ nim_secondary = _model(\\\"nvidia_nim_sub\\\", \\\"NVIDIA_NIM_API_KEY_SUB\\\", \\\"secondary-model\\\", 0.02)\\n+ openrouter = _model(\\\"openrouter\\\", \\\"OPENROUTER_API_KEY\\\", \\\"router-model\\\", 0.5)\\n+\\n+ selected = provider_bootstrap.select_provider_diverse_models(\\n+ [nim_secondary, openrouter, nim_primary], limit=2\\n+ )\\n+\\n+ assert [(item.provider_name, item.model_id) for item in selected] == [\\n+ (\\\"nvidia_nim\\\", \\\"primary-model\\\"),\\n+ (\\\"openrouter\\\", \\\"router-model\\\"),\\n+ ]\\n+\\n+\\n+def test_non_chat_catalog_rows_are_never_selected_for_chat_service():\\n+ \\\"\\\"\\\"Embeddings, rerankers, speech, image, moderation, and realtime rows stay inert.\\\"\\\"\\\"\\n+ models = [\\n+ _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"text-embedding-3-small\\\", 0.1),\\n+ _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"whisper-1\\\", 0.1),\\n+ _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"gpt-image-1\\\", 0.1),\\n+ _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"omni-moderation-latest\\\", 0.1),\\n+ _model(\\n+ \\\"nvidia_nim\\\",\\n+ \\\"NVIDIA_NIM_API_KEY\\\",\\n+ \\\"nv-rerankqa-mistral-4b-v3\\\",\\n+ 0.1,\\n+ ),\\n+ _model(\\n+ \\\"openrouter\\\",\\n+ \\\"OPENROUTER_API_KEY\\\",\\n+ \\\"openai/gpt-4.1-mini\\\",\\n+ 2.0,\\n+ ),\\n+ ]\\n+ selected = provider_bootstrap.select_provider_diverse_models(models, limit=10)\\n+ assert [(item.provider_name, item.model_id) for item in selected] == [\\n+ (\\\"openrouter\\\", \\\"openai/gpt-4.1-mini\\\")\\n+ ]\\n+\\n+\\n+def test_serving_tags_do_not_infer_capabilities_from_model_names():\\n+ \\\"\\\"\\\"Reasoning, coding, and vision-looking names receive only generic tags.\\\"\\\"\\\"\\n+ model = _model(\\n+ \\\"openrouter\\\",\\n+ \\\"OPENROUTER_API_KEY\\\",\\n+ \\\"qwen/qwen-vl-coder-reasoning\\\",\\n+ 1.0,\\n+ )\\n+ assert provider_bootstrap.serving_tags_for_discovered(model) == (\\n+ \\\"discovered\\\",\\n+ \\\"chat\\\",\\n+ \\\"worker\\\",\\n+ \\\"writing\\\",\\n+ \\\"synthesizer\\\",\\n+ )\\n+\\n+\\n+def test_bootstrap_registers_then_discovers_without_environment_runtime_reads(\\n+ monkeypatch,\\n+):\\n+ \\\"\\\"\\\"Discovery sees KV-backed credentials after one-shot environment bootstrap.\\\"\\\"\\\"\\n+ environment = _complete_environment()\\n+ observed: dict[str, str | None] = {}\\n+\\n+ def fake_discover_all_models():\\n+ \\\"\\\"\\\"Observe the KV from the mocked provider-discovery boundary.\\\"\\\"\\\"\\n+ for name in provider_bootstrap.PROVIDER_CREDENTIAL_NAMES:\\n+ observed[name] = get_credential(name)\\n+ return (\\n+ [_model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"gpt-test\\\", 1.0)],\\n+ [],\\n+ )\\n+\\n+ monkeypatch.setattr(\\n+ provider_bootstrap,\\n+ \\\"discover_all_models\\\",\\n+ fake_discover_all_models,\\n+ )\\n+ report = provider_bootstrap.bootstrap_provider_runtime(\\n+ environ=environment,\\n+ model_limit=1,\\n+ )\\n+\\n+ assert report.discovered_model_count == 1\\n+ assert report.eligible_model_count == 1\\n+ assert report.selected_agent_ids == (\\\"openai_gpt_test\\\",)\\n+ assert report.enabled_agent_ids == ()\\n+ assert report.durable_agent_pool is False\\n+ assert all(\\n+ observed[name] == environment[name].strip()\\n+ for name in observed\\n+ )\\n+\\n+\\n+def test_bootstrap_fails_closed_when_no_model_is_discovered(monkeypatch):\\n+ \\\"\\\"\\\"Credential writes without a usable catalog are not reported service-ready.\\\"\\\"\\\"\\n+ monkeypatch.setattr(\\n+ provider_bootstrap,\\n+ \\\"discover_all_models\\\",\\n+ lambda: ([], []),\\n+ )\\n+ with pytest.raises(\\n+ provider_bootstrap.ProviderBootstrapError,\\n+ match=\\\"no usable models\\\",\\n+ ):\\n+ provider_bootstrap.bootstrap_provider_runtime(\\n+ environ=_complete_environment()\\n+ )\\n+\\n+\\n+def test_bootstrap_fails_closed_when_catalog_has_only_non_chat_models(monkeypatch):\\n+ \\\"\\\"\\\"A successful catalog response is not ready without a chat candidate.\\\"\\\"\\\"\\n+ monkeypatch.setattr(\\n+ provider_bootstrap,\\n+ \\\"discover_all_models\\\",\\n+ lambda: (\\n+ [\\n+ _model(\\n+ \\\"openai\\\",\\n+ \\\"OPENAI_API_KEY\\\",\\n+ \\\"text-embedding-3-small\\\",\\n+ 0.1,\\n+ )\\n+ ],\\n+ [],\\n+ ),\\n+ )\\n+ with pytest.raises(\\n+ provider_bootstrap.ProviderBootstrapError,\\n+ match=\\\"no chat-capable models\\\",\\n+ ):\\n+ provider_bootstrap.bootstrap_provider_runtime(\\n+ environ=_complete_environment()\\n+ )\\n+\\n+\\n+def test_durable_pool_withdraws_bootstrap_and_stale_discovered_agents(\\n+ monkeypatch,\\n+ tmp_path,\\n+):\\n+ \\\"\\\"\\\"A refresh leaves exactly the current selected discovered models active.\\\"\\\"\\\"\\n+ agents_db = str(tmp_path / \\\"agents.db\\\")\\n+ old_model = _model(\\n+ \\\"openai\\\",\\n+ \\\"OPENAI_API_KEY\\\",\\n+ \\\"gpt-retired-model\\\",\\n+ 1.0,\\n+ )\\n+ old_agent = replace(agent_from_discovered(old_model), disabled=False)\\n+ seeded = TaskOrchestrator(\\n+ [ModelAgent(\\\"manual_agent\\\", \\\"manual-model\\\")],\\n+ agents_db=agents_db,\\n+ )\\n+ seeded.sync_discovered_agents([old_agent])\\n+\\n+ new_model = _model(\\n+ \\\"openrouter\\\",\\n+ \\\"OPENROUTER_API_KEY\\\",\\n+ \\\"qwen-current-coder\\\",\\n+ 2.0,\\n+ )\\n+ monkeypatch.setattr(\\n+ provider_bootstrap,\\n+ \\\"discover_all_models\\\",\\n+ lambda: ([new_model], []),\\n+ )\\n+ report = provider_bootstrap.bootstrap_provider_runtime(\\n+ environ=_complete_environment(),\\n+ agents_db=agents_db,\\n+ model_limit=1,\\n+ )\\n+\\n+ assert report.discovered_model_count == 1\\n+ assert report.eligible_model_count == 1\\n+ assert report.selected_agent_ids == (\\\"openrouter_qwen_current_coder\\\",)\\n+ assert report.enabled_agent_ids == (\\\"openrouter_qwen_current_coder\\\",)\\n+ assert report.durable_agent_pool is True\\n+\\n+ restarted = TaskOrchestrator(\\n+ [ModelAgent(\\\"bootstrap_agent\\\", \\\"bootstrap-model\\\")],\\n+ agents_db=agents_db,\\n+ )\\n+ assert {agent.id for agent in restarted.agents} == {\\n+ \\\"openrouter_qwen_current_coder\\\"\\n+ }\\n+ assert restarted.agents[0].tags == (\\n+ \\\"discovered\\\",\\n+ \\\"chat\\\",\\n+ \\\"worker\\\",\\n+ \\\"writing\\\",\\n+ \\\"synthesizer\\\",\\n+ )\\n+ assert all(\\n+ agent.id not in {\\\"bootstrap_agent\\\", \\\"openai_gpt_retired_model\\\"}\\n+ for agent in restarted.agents\\n+ )\\n+\\n+\\n+def test_cli_report_never_contains_secret_values(monkeypatch, capsys):\\n+ \\\"\\\"\\\"Operator evidence names credentials and agents but never prints secrets.\\\"\\\"\\\"\\n+ environment = _complete_environment()\\n+ monkeypatch.setattr(os, \\\"environ\\\", environment)\\n+ monkeypatch.setattr(\\n+ provider_bootstrap,\\n+ \\\"discover_all_models\\\",\\n+ lambda: (\\n+ [_model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"gpt-test\\\", 1.0)],\\n+ [],\\n+ ),\\n+ )\\n+ provider_bootstrap.main([\\\"--model-limit\\\", \\\"1\\\"])\\n+ output = capsys.readouterr().out\\n+ report = json.loads(output)\\n+ assert \\\"OPENAI_API_KEY\\\" in output\\n+ assert report[\\\"eligible_model_count\\\"] == 1\\n+ assert report[\\\"selected_agent_ids\\\"] == [\\\"openai_gpt_test\\\"]\\n+ assert report[\\\"enabled_agent_ids\\\"] == []\\n+ assert report[\\\"durable_agent_pool\\\"] is False\\n+ for value in environment.values():\\n+ assert value.strip() not in output\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"70ea8ecc7e83442b115b97847523e3c26484689a\", \"filename\": \"tests/test_provider_bootstrap_secret_normalization.py\", \"status\": \"added\", \"additions\": 71, \"deletions\": 0, \"changes\": 71, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_bootstrap_secret_normalization.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_bootstrap_secret_normalization.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_bootstrap_secret_normalization.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,71 @@\\n+\\\"\\\"\\\"Regression coverage for mounted provider-secret normalization.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator.credentials import (\\n+ InMemoryCredentialBackend,\\n+ get_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.provider_bootstrap import (\\n+ PROVIDER_CREDENTIAL_NAMES,\\n+ collect_provider_credentials,\\n+ register_provider_credentials_atomically,\\n+)\\n+\\n+\\n+@pytest.fixture(autouse=True)\\n+def isolated_credential_backend():\\n+ \\\"\\\"\\\"Give each test a fresh process-local credential registry.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ yield\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+def _complete_environment() -> dict[str, str]:\\n+ \\\"\\\"\\\"Return a complete mounted-secret inventory.\\\"\\\"\\\"\\n+ return {name: f\\\"secret-for-{name.lower()}\\\\n\\\" for name in PROVIDER_CREDENTIAL_NAMES}\\n+\\n+\\n+def test_collection_removes_only_mounted_line_endings() -> None:\\n+ \\\"\\\"\\\"Do not silently rewrite other credential bytes while removing CR/LF mounts.\\\"\\\"\\\"\\n+ environment = _complete_environment()\\n+ environment[\\\"OPENAI_API_KEY\\\"] = \\\" edge-sensitive-secret \\\\r\\\\n\\\"\\n+\\n+ collected = collect_provider_credentials(environment)\\n+\\n+ assert collected[\\\"OPENAI_API_KEY\\\"] == \\\" edge-sensitive-secret \\\"\\n+ assert collected[\\\"BYTEZ_API_KEY\\\"] == \\\"secret-for-bytez_api_key\\\"\\n+\\n+\\n+def test_atomic_registration_preserves_normalized_secret_bytes() -> None:\\n+ \\\"\\\"\\\"The atomic backend write must not perform a second broad whitespace trim.\\\"\\\"\\\"\\n+ credentials = {\\n+ name: f\\\"secret-for-{name.lower()}\\\"\\n+ for name in PROVIDER_CREDENTIAL_NAMES\\n+ }\\n+ credentials[\\\"OPENROUTER_API_KEY\\\"] = \\\" edge-sensitive-router-secret \\\"\\n+\\n+ register_provider_credentials_atomically(credentials)\\n+\\n+ assert get_credential(\\\"OPENROUTER_API_KEY\\\") == \\\" edge-sensitive-router-secret \\\"\\n+\\n+\\n+def test_catalog_sync_leak_guard_matches_secret_normalization() -> None:\\n+ \\\"\\\"\\\"The workflow checks the exact credential bytes that bootstrap handles.\\\"\\\"\\\"\\n+ workflow = Path(\\\".github/workflows/provider-catalog-sync.yml\\\").read_text(\\n+ encoding=\\\"utf-8\\\"\\n+ )\\n+\\n+ assert \\\"os.environ[name].rstrip('\\\\\\\\r\\\\\\\\n')\\\" in workflow\\n+ assert \\\"os.environ[name] and os.environ[name] in report\\\" not in workflow\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"02f93564c6bf711ca56a16f342bdbd24ddfba144\", \"filename\": \"tests/test_provider_catalog_bootstrap.py\", \"status\": \"added\", \"additions\": 175, \"deletions\": 0, \"changes\": 175, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_catalog_bootstrap.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_catalog_bootstrap.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_catalog_bootstrap.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,175 @@\\n+\\\"\\\"\\\"End-to-end durable provider catalog bootstrap contracts.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator.credentials import (\\n+ InMemoryCredentialBackend,\\n+ get_credential,\\n+ register_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.model_discovery import (\\n+ DiscoveredModel,\\n+ ProviderDiscoveryError,\\n+ ProviderModelSource,\\n+)\\n+from contextual_orchestrator.provider_bootstrap import PROVIDER_CREDENTIAL_NAMES\\n+from contextual_orchestrator.provider_catalog_bootstrap import (\\n+ bootstrap_provider_catalog_runtime,\\n+)\\n+from contextual_orchestrator.provider_catalog_store import (\\n+ InMemoryProviderCatalogStore,\\n+)\\n+\\n+\\n+def _environment() -> dict[str, str]:\\n+ return {\\n+ name: f\\\"value-for-{name.casefold()}\\\"\\n+ for name in PROVIDER_CREDENTIAL_NAMES\\n+ }\\n+\\n+\\n+def _source(provider: str, credential: str) -> ProviderModelSource:\\n+ return ProviderModelSource(\\n+ provider_name=provider,\\n+ credential_name=credential,\\n+ list_url=f\\\"https://{provider}.example/v1/models\\\",\\n+ chat_base_url=f\\\"https://{provider}.example/v1\\\",\\n+ )\\n+\\n+\\n+def _model(source: ProviderModelSource, model_id: str) -> DiscoveredModel:\\n+ return DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=model_id,\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ prompt_price_per_1k=1.0,\\n+ completion_price_per_1k=2.0,\\n+ )\\n+\\n+\\n+def test_failed_provider_uses_persisted_last_known_good_model() -> None:\\n+ \\\"\\\"\\\"A later provider outage keeps its last successful compatible model.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ openai = _source(\\\"openai\\\", \\\"OPENAI_API_KEY\\\")\\n+ openrouter = _source(\\\"openrouter\\\", \\\"OPENROUTER_API_KEY\\\")\\n+ store = InMemoryProviderCatalogStore()\\n+\\n+ first = bootstrap_provider_catalog_runtime(\\n+ environ=_environment(),\\n+ catalog_store=store,\\n+ sources=(openai, openrouter),\\n+ discovery=lambda _sources: (\\n+ [_model(openai, \\\"gpt-live\\\"), _model(openrouter, \\\"router-live\\\")],\\n+ [],\\n+ ),\\n+ model_limit=4,\\n+ )\\n+ assert first.catalog_model_count == 2\\n+ assert first.last_known_good_model_count == 0\\n+\\n+ second = bootstrap_provider_catalog_runtime(\\n+ environ=_environment(),\\n+ catalog_store=store,\\n+ sources=(openai, openrouter),\\n+ discovery=lambda _sources: (\\n+ [_model(openrouter, \\\"router-new\\\")],\\n+ [ProviderDiscoveryError(\\\"openai\\\", \\\"secret-bearing detail\\\")],\\n+ ),\\n+ model_limit=4,\\n+ )\\n+ assert second.live_discovered_model_count == 1\\n+ assert second.catalog_model_count == 2\\n+ assert second.last_known_good_model_count == 1\\n+ assert second.catalog_refresh_failure_count == 1\\n+ assert second.providers_with_errors == (\\\"openai\\\",)\\n+ assert set(second.selected_agent_ids) == {\\n+ \\\"openai_gpt_live\\\",\\n+ \\\"openrouter_router_new\\\",\\n+ }\\n+ assert \\\"secret-bearing detail\\\" not in str(second.as_dict())\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+def test_empty_catalog_preserves_lkg_but_nonchat_success_withdraws_it() -> None:\\n+ \\\"\\\"\\\"Empty refresh is failure; authoritative non-chat success is withdrawal.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ openai = _source(\\\"openai\\\", \\\"OPENAI_API_KEY\\\")\\n+ store = InMemoryProviderCatalogStore()\\n+ bootstrap_provider_catalog_runtime(\\n+ environ=_environment(),\\n+ catalog_store=store,\\n+ sources=(openai,),\\n+ discovery=lambda _sources: ([_model(openai, \\\"gpt-live\\\")], []),\\n+ model_limit=1,\\n+ )\\n+\\n+ empty = bootstrap_provider_catalog_runtime(\\n+ environ=_environment(),\\n+ catalog_store=store,\\n+ sources=(openai,),\\n+ discovery=lambda _sources: ([], []),\\n+ model_limit=1,\\n+ )\\n+ assert empty.last_known_good_model_count == 1\\n+ assert empty.catalog_model_count == 1\\n+\\n+ try:\\n+ bootstrap_provider_catalog_runtime(\\n+ environ=_environment(),\\n+ catalog_store=store,\\n+ sources=(openai,),\\n+ discovery=lambda _sources: (\\n+ [_model(openai, \\\"text-embedding-3-small\\\")],\\n+ [],\\n+ ),\\n+ model_limit=1,\\n+ )\\n+ except RuntimeError as error:\\n+ assert \\\"no persisted chat-compatible model\\\" in str(error)\\n+ else:\\n+ raise AssertionError(\\\"non-chat-only authoritative catalog must fail\\\")\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+def test_unexpected_discovery_failure_restores_entire_credential_inventory() -> None:\\n+ \\\"\\\"\\\"An unclassified bootstrap failure must not leave unvalidated secrets promoted.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ previous = {\\n+ name: f\\\"previous-value-for-{name.casefold()}\\\"\\n+ for name in PROVIDER_CREDENTIAL_NAMES\\n+ }\\n+ for name, value in previous.items():\\n+ register_credential(name, value)\\n+\\n+ def fail_discovery(_sources):\\n+ raise RuntimeError(\\\"unexpected discovery parser failure\\\")\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"unexpected discovery parser failure\\\"):\\n+ bootstrap_provider_catalog_runtime(\\n+ environ=_environment(),\\n+ catalog_store=InMemoryProviderCatalogStore(),\\n+ sources=(_source(\\\"openai\\\", \\\"OPENAI_API_KEY\\\"),),\\n+ discovery=fail_discovery,\\n+ model_limit=1,\\n+ )\\n+\\n+ assert {\\n+ name: get_credential(name)\\n+ for name in PROVIDER_CREDENTIAL_NAMES\\n+ } == previous\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"2a8e019ac52f508dc68b4a3fc1fee7350a4052fe\", \"filename\": \"tests/test_provider_catalog_credential_promotion.py\", \"status\": \"added\", \"additions\": 196, \"deletions\": 0, \"changes\": 196, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_catalog_credential_promotion.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_catalog_credential_promotion.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_catalog_credential_promotion.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,196 @@\\n+\\\"\\\"\\\"Regression coverage for provider credential promotion around catalog refresh.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator.credentials import (\\n+ InMemoryCredentialBackend,\\n+ get_credential,\\n+ register_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.model_discovery import (\\n+ DiscoveredModel,\\n+ ProviderDiscoveryError,\\n+ ProviderModelSource,\\n+)\\n+from contextual_orchestrator.provider_bootstrap import ProviderBootstrapError\\n+from contextual_orchestrator.provider_catalog_bootstrap import (\\n+ bootstrap_provider_catalog_runtime,\\n+)\\n+from contextual_orchestrator.provider_catalog_store import (\\n+ InMemoryProviderCatalogStore,\\n+)\\n+\\n+\\n+@pytest.fixture(autouse=True)\\n+def isolated_credential_backend():\\n+ \\\"\\\"\\\"Give every promotion test a fresh credential registry.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ yield\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+def _source() -> ProviderModelSource:\\n+ return ProviderModelSource(\\n+ provider_name=\\\"openai\\\",\\n+ credential_name=\\\"OPENAI_API_KEY\\\",\\n+ list_url=\\\"https://api.openai.example/v1/models\\\",\\n+ chat_base_url=\\\"https://api.openai.example/v1\\\",\\n+ )\\n+\\n+\\n+def _model(source: ProviderModelSource, model_id: str) -> DiscoveredModel:\\n+ return DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=model_id,\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ prompt_price_per_1k=1.0,\\n+ completion_price_per_1k=2.0,\\n+ )\\n+\\n+\\n+def _seed_last_known_good(\\n+ store: InMemoryProviderCatalogStore,\\n+ source: ProviderModelSource,\\n+) -> None:\\n+ model = _model(source, \\\"gpt-last-known-good\\\")\\n+ store.record_success(\\n+ source,\\n+ [model],\\n+ eligible_model_ids={model.model_id},\\n+ serving_tags={model.model_id: (\\\"discovered\\\", \\\"chat\\\", \\\"worker\\\")},\\n+ )\\n+\\n+\\n+def test_failed_refresh_restores_previous_credential_before_using_lkg() -> None:\\n+ \\\"\\\"\\\"An invalid candidate key must not replace the key paired with LKG models.\\\"\\\"\\\"\\n+ source = _source()\\n+ store = InMemoryProviderCatalogStore()\\n+ _seed_last_known_good(store, source)\\n+ register_credential(source.credential_name, \\\"old-working-secret\\\")\\n+\\n+ def failing_discovery(_sources):\\n+ assert get_credential(source.credential_name) == \\\"new-invalid-secret\\\"\\n+ return [], [ProviderDiscoveryError(source.provider_name, \\\"unauthorized\\\")]\\n+\\n+ report = bootstrap_provider_catalog_runtime(\\n+ environ={source.credential_name: \\\"new-invalid-secret\\\"},\\n+ require_all_credentials=False,\\n+ catalog_store=store,\\n+ sources=(source,),\\n+ discovery=failing_discovery,\\n+ model_limit=1,\\n+ )\\n+\\n+ assert get_credential(source.credential_name) == \\\"old-working-secret\\\"\\n+ assert report.selected_agent_ids == (\\\"openai_gpt_last_known_good\\\",)\\n+ assert report.restored_credentials == (source.credential_name,)\\n+\\n+\\n+def test_empty_refresh_restores_previous_credential_before_using_lkg() -> None:\\n+ \\\"\\\"\\\"An empty candidate-key catalog is failure, not credential promotion.\\\"\\\"\\\"\\n+ source = _source()\\n+ store = InMemoryProviderCatalogStore()\\n+ _seed_last_known_good(store, source)\\n+ register_credential(source.credential_name, \\\"old-working-secret\\\")\\n+\\n+ report = bootstrap_provider_catalog_runtime(\\n+ environ={source.credential_name: \\\"new-empty-catalog-secret\\\"},\\n+ require_all_credentials=False,\\n+ catalog_store=store,\\n+ sources=(source,),\\n+ discovery=lambda _sources: ([], []),\\n+ model_limit=1,\\n+ )\\n+\\n+ assert get_credential(source.credential_name) == \\\"old-working-secret\\\"\\n+ assert report.selected_agent_ids == (\\\"openai_gpt_last_known_good\\\",)\\n+ assert report.restored_credentials == (source.credential_name,)\\n+\\n+\\n+def test_failed_first_promotion_cannot_activate_lkg_without_a_prior_credential() -> None:\\n+ \\\"\\\"\\\"Persisted models are unusable when the candidate key failed and no old key exists.\\\"\\\"\\\"\\n+ source = _source()\\n+ store = InMemoryProviderCatalogStore()\\n+ _seed_last_known_good(store, source)\\n+\\n+ with pytest.raises(\\n+ ProviderBootstrapError,\\n+ match=\\\"no persisted chat-compatible model with a usable credential\\\",\\n+ ):\\n+ bootstrap_provider_catalog_runtime(\\n+ environ={source.credential_name: \\\"first-invalid-secret\\\"},\\n+ require_all_credentials=False,\\n+ catalog_store=store,\\n+ sources=(source,),\\n+ discovery=lambda _sources: (\\n+ [],\\n+ [ProviderDiscoveryError(source.provider_name, \\\"unauthorized\\\")],\\n+ ),\\n+ model_limit=1,\\n+ )\\n+\\n+ assert get_credential(source.credential_name) is None\\n+\\n+\\n+def test_report_excludes_first_promotion_credential_removed_by_rollback() -> None:\\n+ \\\"\\\"\\\"Durable-registration evidence cannot claim a deleted first candidate key.\\\"\\\"\\\"\\n+ openai = _source()\\n+ openrouter = ProviderModelSource(\\n+ provider_name=\\\"openrouter\\\",\\n+ credential_name=\\\"OPENROUTER_API_KEY\\\",\\n+ list_url=\\\"https://openrouter.example/v1/models\\\",\\n+ chat_base_url=\\\"https://openrouter.example/v1\\\",\\n+ )\\n+ live = _model(openrouter, \\\"router-live\\\")\\n+\\n+ report = bootstrap_provider_catalog_runtime(\\n+ environ={\\n+ openai.credential_name: \\\"first-invalid-secret\\\",\\n+ openrouter.credential_name: \\\"working-router-secret\\\",\\n+ },\\n+ require_all_credentials=False,\\n+ catalog_store=InMemoryProviderCatalogStore(),\\n+ sources=(openai, openrouter),\\n+ discovery=lambda _sources: (\\n+ [live],\\n+ [ProviderDiscoveryError(openai.provider_name, \\\"temporary discovery failure\\\")],\\n+ ),\\n+ model_limit=1,\\n+ )\\n+\\n+ assert report.restored_credentials == (openai.credential_name,)\\n+ assert report.registered_credentials == (openrouter.credential_name,)\\n+ assert get_credential(openai.credential_name) is None\\n+ assert get_credential(openrouter.credential_name) == \\\"working-router-secret\\\"\\n+\\n+\\n+def test_successful_refresh_promotes_the_candidate_credential() -> None:\\n+ \\\"\\\"\\\"A validated non-empty catalog commits the new provider credential.\\\"\\\"\\\"\\n+ source = _source()\\n+ store = InMemoryProviderCatalogStore()\\n+ register_credential(source.credential_name, \\\"old-working-secret\\\")\\n+ live = _model(source, \\\"gpt-new-live\\\")\\n+\\n+ report = bootstrap_provider_catalog_runtime(\\n+ environ={source.credential_name: \\\"new-working-secret\\\"},\\n+ require_all_credentials=False,\\n+ catalog_store=store,\\n+ sources=(source,),\\n+ discovery=lambda _sources: ([live], []),\\n+ model_limit=1,\\n+ )\\n+\\n+ assert get_credential(source.credential_name) == \\\"new-working-secret\\\"\\n+ assert report.selected_agent_ids == (\\\"openai_gpt_new_live\\\",)\\n+ assert report.restored_credentials == ()\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"35459edd4d7c559546abfbd47c8ffb98705c036b\", \"filename\": \"tests/test_provider_catalog_store.py\", \"status\": \"added\", \"additions\": 316, \"deletions\": 0, \"changes\": 316, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_catalog_store.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_catalog_store.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_catalog_store.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,316 @@\\n+\\\"\\\"\\\"Provider catalog persistence and last-known-good contracts.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from dataclasses import replace\\n+from decimal import Decimal\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator.model_discovery import (\\n+ DiscoveredModel,\\n+ ProviderModelSource,\\n+ _currency_is_comparable,\\n+)\\n+from contextual_orchestrator.provider_catalog_store import (\\n+ InMemoryProviderCatalogStore,\\n+ PostgresProviderCatalogStore,\\n+ PROVIDER_CATALOG_SCHEMA_SQL,\\n+ ProviderCatalogError,\\n+ normalize_discovered_model,\\n+ provider_account_id,\\n+)\\n+\\n+\\n+def _source(\\n+ provider: str = \\\"nvidia_nim\\\",\\n+ credential: str = \\\"NVIDIA_NIM_API_KEY\\\",\\n+) -> ProviderModelSource:\\n+ return ProviderModelSource(\\n+ provider_name=provider,\\n+ credential_name=credential,\\n+ list_url=f\\\"https://{provider}.example/v1/models\\\",\\n+ chat_base_url=f\\\"https://{provider}.example/v1\\\",\\n+ )\\n+\\n+\\n+def _model(\\n+ source: ProviderModelSource,\\n+ model_id: str,\\n+ prompt_price: object = 1.0,\\n+) -> DiscoveredModel:\\n+ return DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=model_id,\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ prompt_price_per_1k=prompt_price,\\n+ completion_price_per_1k=prompt_price,\\n+ currency_code=\\\"usd\\\",\\n+ )\\n+\\n+\\n+def test_schema_is_normalized_and_contains_no_secret_value_column() -> None:\\n+ \\\"\\\"\\\"Catalog DDL keeps accounts, models, tags, and refresh evidence separate.\\\"\\\"\\\"\\n+ for table in (\\n+ \\\"provider_account\\\",\\n+ \\\"provider_model\\\",\\n+ \\\"model_serving_tag\\\",\\n+ \\\"catalog_refresh_run\\\",\\n+ ):\\n+ assert f\\\"CREATE TABLE IF NOT EXISTS {table}\\\" in PROVIDER_CATALOG_SCHEMA_SQL\\n+ lowered = PROVIDER_CATALOG_SCHEMA_SQL.casefold()\\n+ assert \\\"api_key\\\" not in lowered\\n+ assert \\\"secret_value\\\" not in lowered\\n+ assert \\\"encrypted_value\\\" not in lowered\\n+ model_table = lowered.split(\\n+ \\\"create table if not exists provider_model (\\\", 1\\n+ )[1].split(\\\");\\\", 1)[0]\\n+ assert \\\"chat_base_url\\\" not in model_table\\n+ assert \\\"auth_scheme\\\" not in model_table\\n+\\n+\\n+def test_primary_and_secondary_nim_accounts_have_distinct_ids() -> None:\\n+ \\\"\\\"\\\"Two NIM credentials remain independent quota and failure domains.\\\"\\\"\\\"\\n+ primary = _source(credential=\\\"NVIDIA_NIM_API_KEY\\\")\\n+ secondary = _source(\\n+ provider=\\\"nvidia_nim_sub\\\",\\n+ credential=\\\"NVIDIA_NIM_API_KEY_SUB\\\",\\n+ )\\n+ assert provider_account_id(primary) != provider_account_id(secondary)\\n+\\n+\\n+def test_model_normalization_rejects_cross_account_rows_and_bad_prices() -> None:\\n+ \\\"\\\"\\\"Catalog normalization is account-bound and never stores non-finite prices.\\\"\\\"\\\"\\n+ source = _source()\\n+ wrong = _model(\\n+ _source(provider=\\\"openai\\\", credential=\\\"OPENAI_API_KEY\\\"),\\n+ \\\"gpt-test\\\",\\n+ )\\n+ with pytest.raises(ProviderCatalogError, match=\\\"different account\\\"):\\n+ normalize_discovered_model(source, wrong)\\n+\\n+ normalized = normalize_discovered_model(\\n+ source,\\n+ _model(source, \\\" model-a \\\", float(\\\"nan\\\")),\\n+ )\\n+ assert normalized.model_id == \\\"model-a\\\"\\n+ assert normalized.prompt_price_per_1k is None\\n+ assert normalized.completion_price_per_1k is None\\n+ assert normalized.currency_code == \\\"USD\\\"\\n+\\n+\\n+def test_underflowing_positive_price_is_rejected_not_treated_as_free() -> None:\\n+ \\\"\\\"\\\"A nonzero price that underflows to 0.0 in float must stay unknown.\\\"\\\"\\\"\\n+ source = _source()\\n+ normalized = normalize_discovered_model(\\n+ source, _model(source, \\\"underflow-model\\\", \\\"1e-10000\\\")\\n+ )\\n+ assert normalized.prompt_price_per_1k is None\\n+ assert normalized.completion_price_per_1k is None\\n+\\n+\\n+def test_overflowing_price_is_rejected_not_treated_as_infinite() -> None:\\n+ \\\"\\\"\\\"A Decimal-finite price whose float() conversion overflows to inf must stay unknown.\\\"\\\"\\\"\\n+ source = _source()\\n+ normalized = normalize_discovered_model(\\n+ source, _model(source, \\\"overflow-model\\\", \\\"1e10000\\\")\\n+ )\\n+ assert normalized.prompt_price_per_1k is None\\n+ assert normalized.completion_price_per_1k is None\\n+\\n+\\n+def test_unrecognized_currency_is_preserved_as_unknown_not_coerced_to_usd() -> None:\\n+ \\\"\\\"\\\"A priced model with an unverifiable currency must not rank as comparable USD.\\\"\\\"\\\"\\n+ source = _source()\\n+ garbage_currency = replace(\\n+ _model(source, \\\"mystery-currency-model\\\"), currency_code=\\\"not a currency\\\"\\n+ )\\n+ normalized = normalize_discovered_model(source, garbage_currency)\\n+ assert normalized.prompt_price_per_1k == 1.0\\n+ assert normalized.currency_code != \\\"USD\\\"\\n+ assert not _currency_is_comparable(normalized.currency_code, \\\"USD\\\")\\n+\\n+\\n+def test_success_replaces_current_rows_and_failure_keeps_last_known_good() -> None:\\n+ \\\"\\\"\\\"A failed refresh cannot erase the last successful serving model set.\\\"\\\"\\\"\\n+ store = InMemoryProviderCatalogStore()\\n+ source = _source()\\n+ store.record_success(\\n+ source,\\n+ [_model(source, \\\"model-a\\\"), _model(source, \\\"model-b\\\")],\\n+ eligible_model_ids={\\\"model-a\\\"},\\n+ serving_tags={\\\"model-a\\\": (\\\"discovered\\\", \\\"chat\\\", \\\"chat\\\")},\\n+ )\\n+ assert [model.model_id for model in store.serving_models(source)] == [\\n+ \\\"model-a\\\"\\n+ ]\\n+ assert store.serving_tags(source, \\\"model-a\\\") == (\\\"discovered\\\", \\\"chat\\\")\\n+\\n+ store.record_failure(source, error_code=\\\"provider_timeout: secret-token\\\")\\n+ assert [model.model_id for model in store.serving_models(source)] == [\\n+ \\\"model-a\\\"\\n+ ]\\n+ assert store.refresh_evidence()[-1].error_code == \\\"unknown_error\\\"\\n+\\n+ store.record_success(\\n+ source,\\n+ [_model(source, \\\"model-c\\\")],\\n+ eligible_model_ids={\\\"model-c\\\"},\\n+ serving_tags={\\\"model-c\\\": (\\\"discovered\\\", \\\"chat\\\")},\\n+ )\\n+ assert [model.model_id for model in store.serving_models(source)] == [\\n+ \\\"model-c\\\"\\n+ ]\\n+ assert [item.refresh_status for item in store.refresh_evidence()] == [\\n+ \\\"succeeded\\\",\\n+ \\\"failed\\\",\\n+ \\\"succeeded\\\",\\n+ ]\\n+\\n+\\n+class _FakeCursor:\\n+ \\\"\\\"\\\"Minimal DB-API cursor recording parameterized catalog statements.\\\"\\\"\\\"\\n+\\n+ def __init__(self, rows=None) -> None:\\n+ self.calls: list[tuple[str, object]] = []\\n+ self.rows = list(rows or [])\\n+\\n+ def __enter__(self):\\n+ return self\\n+\\n+ def __exit__(self, *_args) -> None:\\n+ return None\\n+\\n+ def execute(self, statement: str, params=None) -> None:\\n+ self.calls.append((statement, params))\\n+\\n+ def fetchall(self):\\n+ return list(self.rows)\\n+\\n+\\n+class _FakeConnection:\\n+ \\\"\\\"\\\"Minimal transaction object exercising the PostgreSQL adapter.\\\"\\\"\\\"\\n+\\n+ def __init__(self, rows=None) -> None:\\n+ self.cursor_object = _FakeCursor(rows)\\n+ self.commits = 0\\n+\\n+ def __enter__(self):\\n+ return self\\n+\\n+ def __exit__(self, *_args) -> None:\\n+ return None\\n+\\n+ def cursor(self):\\n+ return self.cursor_object\\n+\\n+ def commit(self) -> None:\\n+ self.commits += 1\\n+\\n+\\n+def test_postgres_success_is_parameterized_and_failure_does_not_disable_lkg() -> None:\\n+ \\\"\\\"\\\"PostgreSQL success replaces rows; failure records evidence only.\\\"\\\"\\\"\\n+ source = _source()\\n+ connections: list[_FakeConnection] = []\\n+\\n+ def factory():\\n+ connection = _FakeConnection()\\n+ connections.append(connection)\\n+ return connection\\n+\\n+ store = PostgresProviderCatalogStore(\\n+ \\\"postgresql://catalog.example/db\\\",\\n+ connection_factory=factory,\\n+ )\\n+ store.record_success(\\n+ source,\\n+ [_model(source, \\\"model-a\\\")],\\n+ eligible_model_ids={\\\"model-a\\\"},\\n+ serving_tags={\\\"model-a\\\": (\\\"discovered\\\", \\\"chat\\\")},\\n+ )\\n+ success_sql = \\\"\\\\n\\\".join(\\n+ statement for statement, _params in connections[-1].cursor_object.calls\\n+ )\\n+ assert \\\"UPDATE provider_model SET enabled_flag = false\\\" in success_sql\\n+ assert \\\"INSERT INTO model_serving_tag\\\" in success_sql\\n+ assert connections[-1].commits >= 1\\n+\\n+ store.record_failure(source, error_code=\\\"provider_timeout: secret-token\\\")\\n+ failure_sql = \\\"\\\\n\\\".join(\\n+ statement for statement, _params in connections[-1].cursor_object.calls\\n+ )\\n+ assert \\\"UPDATE provider_model SET enabled_flag = false\\\" not in failure_sql\\n+ assert \\\"INSERT INTO catalog_refresh_run\\\" in failure_sql\\n+ assert store.refresh_evidence()[-1].error_code == \\\"unknown_error\\\"\\n+\\n+\\n+def test_postgres_success_clears_tags_account_wide_not_per_current_model() -> None:\\n+ \\\"\\\"\\\"A model absent from a fresh refresh cannot leave orphaned serving_tag rows.\\\"\\\"\\\"\\n+ source = _source()\\n+ connection = _FakeConnection()\\n+ store = PostgresProviderCatalogStore(\\n+ \\\"postgresql://catalog.example/db\\\",\\n+ connection_factory=lambda: connection,\\n+ )\\n+ store.record_success(\\n+ source,\\n+ [_model(source, \\\"model-a\\\")],\\n+ eligible_model_ids={\\\"model-a\\\"},\\n+ serving_tags={\\\"model-a\\\": (\\\"discovered\\\", \\\"chat\\\")},\\n+ )\\n+ statements = [statement for statement, _params in connection.cursor_object.calls]\\n+ tag_delete_index = next(\\n+ i for i, s in enumerate(statements) if \\\"DELETE FROM model_serving_tag\\\" in s\\n+ )\\n+ tag_insert_index = next(\\n+ i for i, s in enumerate(statements) if \\\"INSERT INTO model_serving_tag\\\" in s\\n+ )\\n+ assert \\\"WHERE provider_model_id IN\\\" in statements[tag_delete_index]\\n+ assert \\\"WHERE provider_account_id = %s\\\" in statements[tag_delete_index]\\n+ assert tag_delete_index < tag_insert_index\\n+ assert statements.count(\\n+ \\\"DELETE FROM model_serving_tag WHERE provider_model_id = %s\\\"\\n+ ) == 0\\n+\\n+\\n+def test_postgres_serving_models_reconstructs_account_scoped_rows() -> None:\\n+ \\\"\\\"\\\"Read-side rows become normalized DiscoveredModel records.\\\"\\\"\\\"\\n+ source = _source(provider=\\\"openrouter\\\", credential=\\\"OPENROUTER_API_KEY\\\")\\n+ connection = _FakeConnection(\\n+ [\\n+ (\\n+ \\\"model-b\\\",\\n+ source.chat_base_url,\\n+ \\\"Bearer\\\",\\n+ Decimal(\\\"0.25\\\"),\\n+ Decimal(\\\"0.50\\\"),\\n+ \\\"usd\\\",\\n+ )\\n+ ]\\n+ )\\n+ store = PostgresProviderCatalogStore(\\n+ \\\"postgresql://catalog.example/db\\\",\\n+ connection_factory=lambda: connection,\\n+ )\\n+ assert store.serving_models(source) == [\\n+ DiscoveredModel(\\n+ provider_name=\\\"openrouter\\\",\\n+ model_id=\\\"model-b\\\",\\n+ credential_name=\\\"OPENROUTER_API_KEY\\\",\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=\\\"Bearer\\\",\\n+ prompt_price_per_1k=0.25,\\n+ completion_price_per_1k=0.5,\\n+ currency_code=\\\"USD\\\",\\n+ )\\n+ ]\\n+ query, params = connection.cursor_object.calls[-1]\\n+ assert \\\"JOIN provider_account AS pa\\\" in query\\n+ assert \\\"serving_eligible_flag = true\\\" in query\\n+ assert params == (provider_account_id(source),)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" } ]" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-24T07-43-55-791Z.yml b/.playwright-mcp/page-2026-08-24T07-43-55-791Z.yml deleted file mode 100644 index 9974b1810..000000000 --- a/.playwright-mcp/page-2026-08-24T07-43-55-791Z.yml +++ /dev/null @@ -1 +0,0 @@ -- generic [active] [ref=f3e1]: "[ { \"sha\": \"1b951628a44baee32306a9977aad77aae8eff7e9\", \"filename\": \".github/workflows/provider-catalog-sync.yml\", \"status\": \"added\", \"additions\": 105, \"deletions\": 0, \"changes\": 105, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/.github%2Fworkflows%2Fprovider-catalog-sync.yml\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/.github%2Fworkflows%2Fprovider-catalog-sync.yml\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/.github%2Fworkflows%2Fprovider-catalog-sync.yml?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,105 @@\\n+name: Provider catalog sync\\n+\\n+on:\\n+ workflow_dispatch:\\n+ schedule:\\n+ - cron: \\\"17 * * * *\\\"\\n+\\n+permissions:\\n+ contents: read\\n+\\n+concurrency:\\n+ group: provider-catalog-sync\\n+ cancel-in-progress: false\\n+\\n+jobs:\\n+ sync:\\n+ name: Bootstrap durable provider KV and model catalog\\n+ if: github.ref == 'refs/heads/main'\\n+ runs-on: ubuntu-latest\\n+ environment: production\\n+ timeout-minutes: 15\\n+ steps:\\n+ - name: Checkout protected default branch\\n+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7\\n+ with:\\n+ persist-credentials: false\\n+\\n+ - name: Set up Python\\n+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6\\n+ with:\\n+ python-version: \\\"3.12\\\"\\n+\\n+ - name: Install hash-pinned runtime dependencies\\n+ run: python -m pip install --disable-pip-version-check --no-input --require-hashes -r requirements.lock\\n+\\n+ - name: Register credentials and refresh normalized model catalog\\n+ shell: bash\\n+ env:\\n+ CONTEXTUAL_ORCHESTRATOR_KV_BACKEND: postgres\\n+ CONTEXTUAL_ORCHESTRATOR_KV_DSN: ${{ secrets.CONTEXTUAL_ORCHESTRATOR_KV_DSN }}\\n+ CONTEXTUAL_ORCHESTRATOR_KV_PASSPHRASE: ${{ secrets.CONTEXTUAL_ORCHESTRATOR_KV_PASSPHRASE }}\\n+ NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}\\n+ NVIDIA_NIM_API_KEY_SUB: ${{ secrets.NVIDIA_NIM_API_KEY_SUB }}\\n+ BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }}\\n+ OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}\\n+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\\n+ run: |\\n+ set -euo pipefail\\n+ test -n \\\"${CONTEXTUAL_ORCHESTRATOR_KV_DSN}\\\"\\n+ test -n \\\"${CONTEXTUAL_ORCHESTRATOR_KV_PASSPHRASE}\\\"\\n+ python -m contextual_orchestrator.provider_catalog_bootstrap --model-limit 24 > provider-bootstrap-report.json\\n+ python - <<'PY'\\n+ import json\\n+ from pathlib import Path\\n+\\n+ report = json.loads(Path('provider-bootstrap-report.json').read_text(encoding='utf-8'))\\n+ expected = {\\n+ 'NVIDIA_NIM_API_KEY',\\n+ 'NVIDIA_NIM_API_KEY_SUB',\\n+ 'BYTEZ_API_KEY',\\n+ 'OPENROUTER_API_KEY',\\n+ 'OPENAI_API_KEY',\\n+ }\\n+ registered = set(report['registered_credentials'])\\n+ if registered != expected:\\n+ raise SystemExit(f'credential inventory mismatch: {sorted(expected - registered)}')\\n+ if report['catalog_backend'] != 'postgres':\\n+ raise SystemExit('provider catalog is not durable PostgreSQL')\\n+ if report['catalog_model_count'] < 1 or report['eligible_model_count'] < 1:\\n+ raise SystemExit('provider catalog has no compatible serving model')\\n+ if not report['selected_agent_ids']:\\n+ raise SystemExit('provider catalog produced no serving candidates')\\n+ if report['enabled_agent_ids'] or report['durable_agent_pool']:\\n+ raise SystemExit('ephemeral Actions sync must not claim agent-pool activation')\\n+ print(json.dumps({\\n+ 'registered_credentials': sorted(registered),\\n+ 'live_discovered_model_count': report['live_discovered_model_count'],\\n+ 'catalog_model_count': report['catalog_model_count'],\\n+ 'last_known_good_model_count': report['last_known_good_model_count'],\\n+ 'selected_agent_count': len(report['selected_agent_ids']),\\n+ 'catalog_refresh_failure_count': report['catalog_refresh_failure_count'],\\n+ 'providers_with_errors': report['providers_with_errors'],\\n+ }, sort_keys=True))\\n+ PY\\n+ python - <<'PY'\\n+ import os\\n+ from pathlib import Path\\n+\\n+ report = Path('provider-bootstrap-report.json').read_text(encoding='utf-8')\\n+ names = (\\n+ 'NVIDIA_NIM_API_KEY',\\n+ 'NVIDIA_NIM_API_KEY_SUB',\\n+ 'BYTEZ_API_KEY',\\n+ 'OPENROUTER_API_KEY',\\n+ 'OPENAI_API_KEY',\\n+ )\\n+ leaked = [\\n+ name\\n+ for name in names\\n+ if os.environ[name].rstrip('\\\\r\\\\n')\\n+ and os.environ[name].rstrip('\\\\r\\\\n') in report\\n+ ]\\n+ if leaked:\\n+ raise SystemExit(f'provider bootstrap report leaked secret values for: {leaked}')\\n+ PY\" }, { \"sha\": \"fb9beb151cb73656da41c7e22cdbfeb93c4bc842\", \"filename\": \"README.md\", \"status\": \"modified\", \"additions\": 10, \"deletions\": 1, \"changes\": 11, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/README.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/README.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/README.md?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -97,7 +97,6 @@ Run an evaluation against that server with `--temperature 0` for repeatable judg\\n Model-based conduct verification requires `fast-mlsirm` in the same runtime and fails closed when it is absent or broken; fast-mlsirm sends its judge completion through this contextual-orchestrator gateway, so no direct provider fallback is used. “Same runtime” means that the exact interpreter used for the live run can import both packages: install both checkouts into one environment (prefer editable installs), or expose both source roots with `PYTHONPATH` during a source run. Before a live judge benchmark, run `python -m contextual_orchestrator check-fast-mlsirm` with that exact interpreter. It prints the interpreter, package version, transitive-import status, and contextual contract check, and exits nonzero on a missing dependency or contract mismatch. Do not run the preflight in one virtual environment and the judge in another. See [ADR 0001](docs/planning/adrs/0001-fail-closed-model-judgment.md).\\n \\n 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.\\n-\\n 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) 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.\\n \\n Seed the credential into the KV once at bootstrap:\\n@@ -304,6 +303,16 @@ python tests/test_admin_contract.py\\n python tests/test_conventions.py\\n python tests/test_api_contract.py\\n python tests/test_security_hardening.py\\n+python tests/test_chat_model_capability_isolation.py\\n+python tests/test_chat_transport_role_separation.py\\n+python tests/test_chat_capability_unknown_identifiers.py\\n+python tests/test_chat_passthrough_capability_isolation.py\\n+python tests/test_discovery_bootstrap_selection.py\\n+python tests/test_provider_bootstrap.py\\n+python tests/test_provider_bootstrap_secret_normalization.py\\n+python tests/test_provider_catalog_bootstrap.py\\n+python tests/test_provider_catalog_credential_promotion.py\\n+python tests/test_provider_catalog_store.py\\n python tests/test_repository_security_metadata.py\\n python tests/test_product_planning_contract.py\\n python tests/test_plugin_driven_artifacts.py\" }, { \"sha\": \"3424629af79165eaa9878e2c0e5d176bd90a151f\", \"filename\": \"contextual_orchestrator/__main__.py\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 3, \"changes\": 6, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2F__main__.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2F__main__.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2F__main__.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -16,7 +16,7 @@\\n agent_id_for,\\n discover_all_models,\\n refresh_price_book,\\n- select_top_n_cheapest_discovered_agents,\\n+ select_bootstrap_discovered_agents,\\n )\\n from .orchestrator import (\\n CONTEXTUAL_ORCHESTRATOR_CONTRACT_V1,\\n@@ -211,7 +211,7 @@ def _discover_models_command(argv: list[str]) -> None:\\n type=_non_negative_int,\\n default=0,\\n metavar=\\\"N\\\",\\n- help=\\\"Enable the N cheapest discovered agents in --agents-db (auto-optimization bootstrap; \\\"\\n+ help=\\\"Enable a price-honest, provider-diverse discovered agent pool in --agents-db (auto-optimization bootstrap; \\\"\\n \\\"requires --agents-db; 0 disables, the default, leaving every discovered agent inert).\\\",\\n )\\n args = parser.parse_args(argv)\\n@@ -229,7 +229,7 @@ def _discover_models_command(argv: list[str]) -> None:\\n )\\n bootstrap.sync_discovered_agents([agent_from_discovered(model) for model in discovered])\\n if args.enable_cheapest:\\n- for model in select_top_n_cheapest_discovered_agents(discovered, price_book, args.enable_cheapest):\\n+ for model in select_bootstrap_discovered_agents(discovered, price_book, args.enable_cheapest):\\n agent_id = agent_id_for(model)\\n bootstrap.patch_agent(\\\"default\\\", agent_id, {\\\"status\\\": \\\"active\\\"})\\n enabled_agent_ids.append(agent_id)\" }, { \"sha\": \"4a2a96cd560621fcc8099010ec759a2f4f75be40\", \"filename\": \"contextual_orchestrator/chat_capability.py\", \"status\": \"added\", \"additions\": 96, \"deletions\": 0, \"changes\": 96, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fchat_capability.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fchat_capability.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fchat_capability.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,96 @@\\n+\\\"\\\"\\\"Classify chat transport compatibility and ordinary agent-role eligibility.\\n+\\n+Provider catalogs mix endpoint-only models with models served through an\\n+OpenAI-compatible chat transport. Transport compatibility is not the same as\\n+fitness for an ordinary thinker, worker, verifier, or synthesizer role: audio\\n+and policy-classification models can use chat transport, while embedding,\\n+reranking, transcription, moderation-endpoint, image-generation, realtime, and\\n+speech-only models cannot.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import re\\n+\\n+_MODEL_TOKEN_RE = re.compile(r\\\"[a-z0-9]+\\\")\\n+_TRANSPORT_INCOMPATIBLE_EXACT_TOKENS = frozenset(\\n+ {\\n+ \\\"bge\\\",\\n+ \\\"clip\\\",\\n+ \\\"dall\\\",\\n+ \\\"e5\\\",\\n+ \\\"embed\\\",\\n+ \\\"embedding\\\",\\n+ \\\"embeddings\\\",\\n+ \\\"gte\\\",\\n+ \\\"image\\\",\\n+ \\\"images\\\",\\n+ \\\"moderation\\\",\\n+ \\\"realtime\\\",\\n+ \\\"rerank\\\",\\n+ \\\"reranker\\\",\\n+ \\\"siglip\\\",\\n+ \\\"sora\\\",\\n+ \\\"speech\\\",\\n+ \\\"transcribe\\\",\\n+ \\\"transcription\\\",\\n+ \\\"tts\\\",\\n+ \\\"whisper\\\",\\n+ }\\n+)\\n+_TRANSPORT_INCOMPATIBLE_PREFIXES = (\\n+ \\\"embed\\\",\\n+ \\\"moderat\\\",\\n+ \\\"rerank\\\",\\n+ \\\"transcrib\\\",\\n+)\\n+\\n+\\n+def is_chat_compatible_model_id(model_id: str) -> bool:\\n+ \\\"\\\"\\\"Return whether an identifier can use the ordinary chat transport.\\n+\\n+ The classifier rejects only identifiers that clearly advertise an endpoint\\n+ family incompatible with chat messages. Audio-capable and safety-classifier\\n+ models remain transport-compatible because providers serve some of them over\\n+ ``/chat/completions``.\\n+ \\\"\\\"\\\"\\n+ tokens = _model_tokens(model_id)\\n+ return _is_transport_compatible_tokens(tokens)\\n+\\n+\\n+def _is_transport_compatible_tokens(tokens: tuple[str, ...]) -> bool:\\n+ \\\"\\\"\\\"Judge transport compatibility from already-normalized model tokens.\\\"\\\"\\\"\\n+ if not tokens:\\n+ return False\\n+ for token in tokens:\\n+ if token in _TRANSPORT_INCOMPATIBLE_EXACT_TOKENS:\\n+ return False\\n+ if token.startswith(_TRANSPORT_INCOMPATIBLE_PREFIXES):\\n+ return False\\n+ return True\\n+\\n+\\n+def _model_tokens(model_id: str) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Normalize one provider-prefixed model identifier into lowercase tokens.\\\"\\\"\\\"\\n+ if not isinstance(model_id, str):\\n+ return ()\\n+ return tuple(_MODEL_TOKEN_RE.findall(model_id.casefold()))\\n+\\n+\\n+def is_general_chat_agent_model_id(model_id: str) -> bool:\\n+ \\\"\\\"\\\"Return whether a chat model may enter ordinary orchestration roles.\\n+\\n+ Explicit guard and safety models can use chat transport but are specialized\\n+ policy classifiers, not general answer synthesizers. This negative role gate\\n+ does not infer reasoning, coding, vision, or verification capabilities.\\n+ \\\"\\\"\\\"\\n+ tokens = _model_tokens(model_id)\\n+ if not tokens or not _is_transport_compatible_tokens(tokens):\\n+ return False\\n+ return not any(\\n+ token == \\\"safety\\\"\\n+ or token == \\\"guard\\\"\\n+ or token == \\\"shieldgemma\\\"\\n+ or token.startswith(\\\"nemoguard\\\")\\n+ for token in tokens\\n+ )\" }, { \"sha\": \"254caa98548b161670fa9ce2e8fdf83928eaedd7\", \"filename\": \"contextual_orchestrator/cost_ledger.py\", \"status\": \"modified\", \"additions\": 48, \"deletions\": 7, \"changes\": 55, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fcost_ledger.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fcost_ledger.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fcost_ledger.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -31,6 +31,7 @@\\n import threading\\n from dataclasses import dataclass, field\\n from decimal import ROUND_HALF_UP, Decimal\\n+import math\\n import time\\n from typing import Any, Dict, List, Optional, Protocol\\n import uuid\\n@@ -105,6 +106,31 @@ def _price_key(provider: str, model: str) -> str:\\n return f\\\"{provider}:{model}\\\"\\n \\n \\n+def _decimal_safe_price(value: object) -> Optional[float]:\\n+ \\\"\\\"\\\"Parse one raw price component, or ``None`` when unknown, underflowed, or overflowed.\\n+\\n+ Parses through ``Decimal`` first so a nonzero price that underflows to\\n+ ``0.0`` in float (e.g. a stray ``1e-10000``) is rejected as unknown\\n+ rather than silently accepted as a legitimate free price. A ``Decimal``\\n+ can still be finite while its ``float()`` conversion overflows to\\n+ ``inf`` (e.g. ``1e10000``), so ``math.isfinite`` is checked separately\\n+ on the converted value.\\n+ \\\"\\\"\\\"\\n+ try:\\n+ decimal_value = Decimal(str(value))\\n+ price = float(decimal_value)\\n+ except (ArithmeticError, TypeError, ValueError):\\n+ return None\\n+ if (\\n+ not decimal_value.is_finite()\\n+ or not math.isfinite(price)\\n+ or decimal_value < 0\\n+ or (decimal_value != 0 and price == 0)\\n+ ):\\n+ return None\\n+ return price\\n+\\n+\\n @dataclass\\n class PriceEntry:\\n \\\"\\\"\\\"A single price-table row: per-1K-token prices for a provider+model.\\\"\\\"\\\"\\n@@ -155,18 +181,33 @@ def get_price(self, provider: str, model: str) -> Optional[PriceEntry]:\\n \\\"\\\"\\\"Return the price entry for ``provider``+``model``, if configured.\\n \\n Falls back to a provider-wildcard entry (``\\\"{provider}:*\\\"``) so a\\n- provider can set one default price for all of its models.\\n+ provider can set one default price for all of its models. A corrupt\\n+ specific row does not suppress an otherwise-valid wildcard fallback.\\n \\\"\\\"\\\"\\n- raw = self._config.get(_PRICE_CATEGORY, _price_key(provider, model), None)\\n- if raw is None:\\n- raw = self._config.get(_PRICE_CATEGORY, _price_key(provider, \\\"*\\\"), None)\\n- if raw is None:\\n+ for candidate_model in (model, \\\"*\\\"):\\n+ raw = self._config.get(_PRICE_CATEGORY, _price_key(provider, candidate_model), None)\\n+ entry = self._parse_price_entry(raw, provider, model)\\n+ if entry is not None:\\n+ return entry\\n+ return None\\n+\\n+ def _parse_price_entry(\\n+ self, raw: Any, provider: str, model: str\\n+ ) -> Optional[PriceEntry]:\\n+ \\\"\\\"\\\"Validate one raw KV row into a ``PriceEntry``, or ``None`` if it is unusable.\\\"\\\"\\\"\\n+ if not isinstance(raw, dict):\\n+ return None\\n+ if \\\"prompt_price_per_1k\\\" not in raw or \\\"completion_price_per_1k\\\" not in raw:\\n+ return None\\n+ prompt_price = _decimal_safe_price(raw[\\\"prompt_price_per_1k\\\"])\\n+ completion_price = _decimal_safe_price(raw[\\\"completion_price_per_1k\\\"])\\n+ if prompt_price is None or completion_price is None:\\n return None\\n return PriceEntry(\\n provider_name=raw.get(\\\"provider_name\\\", provider),\\n model_name=raw.get(\\\"model_name\\\", model),\\n- prompt_price_per_1k=float(raw.get(\\\"prompt_price_per_1k\\\", 0.0)),\\n- completion_price_per_1k=float(raw.get(\\\"completion_price_per_1k\\\", 0.0)),\\n+ prompt_price_per_1k=prompt_price,\\n+ completion_price_per_1k=completion_price,\\n currency_code=raw.get(\\\"currency_code\\\", self.default_currency),\\n )\\n \" }, { \"sha\": \"3daf36283e78a2dadd0bc814d761c066756fc217\", \"filename\": \"contextual_orchestrator/credentials.py\", \"status\": \"modified\", \"additions\": 34, \"deletions\": 0, \"changes\": 34, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fcredentials.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fcredentials.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fcredentials.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -50,6 +50,10 @@ def set(self, name: str, value: str) -> None:\\n \\\"\\\"\\\"Register (or replace) the secret stored under ``name``.\\\"\\\"\\\"\\n ...\\n \\n+ def delete(self, name: str) -> None:\\n+ \\\"\\\"\\\"Remove one credential after an unvalidated candidate promotion.\\\"\\\"\\\"\\n+ ...\\n+\\n \\n class InMemoryCredentialBackend:\\n \\\"\\\"\\\"Process-local credential registry for dev and tests (no Postgres needed).\\\"\\\"\\\"\\n@@ -68,6 +72,11 @@ def set(self, name: str, value: str) -> None:\\n with self._lock:\\n self._store[name] = value\\n \\n+ def delete(self, name: str) -> None:\\n+ \\\"\\\"\\\"Remove ``name`` from the in-memory credential registry if present.\\\"\\\"\\\"\\n+ with self._lock:\\n+ self._store.pop(name, None)\\n+\\n \\n # --- Postgres pgcrypto-encrypted credential registry ------------------------\\n #\\n@@ -112,6 +121,15 @@ def __init__(self, dsn: str, passphrase: str) -> None:\\n self._passphrase = passphrase\\n self._ensured = False\\n \\n+ @property\\n+ def connection_dsn(self) -> str:\\n+ \\\"\\\"\\\"Return the bootstrap DSN for a colocated metadata store.\\n+\\n+ Callers must treat this as connection material: never include it in logs,\\n+ reports, traces, or exceptions. Provider API keys remain inaccessible.\\n+ \\\"\\\"\\\"\\n+ return self._dsn\\n+\\n @classmethod\\n def from_env(cls) -> \\\"PostgresCredentialBackend\\\":\\n \\\"\\\"\\\"Build the backend from bootstrap transport env vars (the only allowed env use).\\n@@ -173,6 +191,17 @@ def set(self, name: str, value: str) -> None: # pragma: no cover - requires a l\\n )\\n conn.commit()\\n \\n+ def delete(self, name: str) -> None: # pragma: no cover - requires a live Postgres\\n+ \\\"\\\"\\\"Delete one encrypted credential after a failed candidate promotion.\\\"\\\"\\\"\\n+ with self._connect() as conn:\\n+ self._ensure_schema(conn)\\n+ with conn.cursor() as cur:\\n+ cur.execute(\\n+ \\\"DELETE FROM provider_credentials WHERE credential_name = %s\\\",\\n+ (name,),\\n+ )\\n+ conn.commit()\\n+\\n \\n _backend: CredentialBackend | None = None\\n _backend_lock = threading.Lock()\\n@@ -216,3 +245,8 @@ def get_credential(name: str) -> str | None:\\n def register_credential(name: str, value: str) -> None:\\n \\\"\\\"\\\"Register a named secret into the KV (used by the bootstrap CLI).\\\"\\\"\\\"\\n get_backend().set(name, value)\\n+\\n+\\n+def delete_credential(name: str) -> None:\\n+ \\\"\\\"\\\"Remove a named credential from the KV after an unvalidated promotion.\\\"\\\"\\\"\\n+ get_backend().delete(name)\" }, { \"sha\": \"fbc3da91c6fa8ef5bb7768cb3659a74ffc5cf205\", \"filename\": \"contextual_orchestrator/model_discovery.py\", \"status\": \"modified\", \"additions\": 221, \"deletions\": 51, \"changes\": 272, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fmodel_discovery.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fmodel_discovery.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fmodel_discovery.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -13,14 +13,16 @@\\n \\n from __future__ import annotations\\n \\n+from decimal import Decimal\\n import json\\n+import math\\n import re\\n import urllib.error\\n import urllib.request\\n-from dataclasses import dataclass\\n+from dataclasses import dataclass, replace\\n from typing import TYPE_CHECKING, Any\\n \\n-from .batch_routing import cheapest_upstream\\n+from .chat_capability import is_general_chat_agent_model_id\\n from .credentials import get_credential\\n from .orchestrator import ModelAgent\\n \\n@@ -84,7 +86,7 @@ class ProviderModelSource:\\n \\n @dataclass(frozen=True)\\n class DiscoveredModel:\\n- \\\"\\\"\\\"One model found on a provider, with pricing when the provider reports it.\\\"\\\"\\\"\\n+ \\\"\\\"\\\"One general-chat model found on a provider, with reported pricing.\\\"\\\"\\\"\\n \\n provider_name: str\\n model_id: str\\n@@ -121,14 +123,78 @@ def _fetch_json(url: str, *, api_key: str, auth_scheme: str, timeout: float) ->\\n return json.loads(response.read().decode(\\\"utf-8\\\"))\\n \\n \\n+def _valid_price_component(value: object) -> bool:\\n+ \\\"\\\"\\\"Return whether one price component is finite, numeric, and non-negative.\\\"\\\"\\\"\\n+ if isinstance(value, bool) or not isinstance(value, (int, float)):\\n+ return False\\n+ try:\\n+ numeric = float(value)\\n+ except (TypeError, ValueError, OverflowError):\\n+ return False\\n+ return math.isfinite(numeric) and numeric >= 0.0\\n+\\n+\\n def _price_per_1k(value: Any) -> float | None:\\n- \\\"\\\"\\\"OpenAI-compatible providers report USD price per single token; convert to per-1K.\\\"\\\"\\\"\\n- if value is None:\\n+ \\\"\\\"\\\"Convert a trustworthy per-token USD price to per-1K, else return unknown.\\n+\\n+ Parses through ``Decimal`` first so a nonzero price that underflows to\\n+ ``0.0`` in float (e.g. a stray ``1e-10000``) is rejected as unknown\\n+ rather than silently accepted as a legitimate free price.\\n+ \\\"\\\"\\\"\\n+ if value is None or isinstance(value, bool):\\n return None\\n try:\\n- return float(value) * 1000\\n- except (TypeError, ValueError):\\n+ decimal_per_1k = Decimal(str(value)) * 1000\\n+ per_1k = float(decimal_per_1k)\\n+ except (ArithmeticError, TypeError, ValueError):\\n+ return None\\n+ if not decimal_per_1k.is_finite() or (decimal_per_1k != 0 and per_1k == 0):\\n return None\\n+ return per_1k if _valid_price_component(per_1k) else None\\n+\\n+\\n+def _serving_identity(model: DiscoveredModel) -> tuple[str, str]:\\n+ \\\"\\\"\\\"Return the durable agent identity used by discovery synchronization.\\\"\\\"\\\"\\n+ return (model.provider_name, model.model_id)\\n+\\n+\\n+def _source_tiebreaker(model: DiscoveredModel) -> tuple[str, str, str, str]:\\n+ \\\"\\\"\\\"Choose deterministic transport metadata for an ambiguous duplicate row.\\\"\\\"\\\"\\n+ return (\\n+ model.credential_name,\\n+ model.chat_base_url,\\n+ model.auth_scheme,\\n+ model.currency_code,\\n+ )\\n+\\n+\\n+def _deduplicate_discovered_models(\\n+ discovered: list[DiscoveredModel],\\n+) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Collapse duplicate agent identities and withhold conflicting price evidence.\\n+\\n+ Exact duplicate catalog rows become one candidate. When the same provider/model\\n+ identity is repeated with conflicting metadata or prices, one deterministic\\n+ transport record is retained but its prices become unknown. Provider row order\\n+ therefore cannot fabricate a cheaper bootstrap candidate or consume failover\\n+ capacity twice.\\n+ \\\"\\\"\\\"\\n+ unique: dict[tuple[str, str], DiscoveredModel] = {}\\n+ for model in discovered:\\n+ identity = _serving_identity(model)\\n+ previous = unique.get(identity)\\n+ if previous is None:\\n+ unique[identity] = model\\n+ continue\\n+ if previous == model:\\n+ continue\\n+ chosen = min((previous, model), key=_source_tiebreaker)\\n+ unique[identity] = replace(\\n+ chosen,\\n+ prompt_price_per_1k=None,\\n+ completion_price_per_1k=None,\\n+ )\\n+ return list(unique.values())\\n \\n \\n def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[DiscoveredModel]:\\n@@ -138,7 +204,7 @@ def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[\\n if not isinstance(row, dict):\\n continue\\n model_id = row.get(\\\"id\\\")\\n- if type(model_id) is not str or not model_id:\\n+ if type(model_id) is not str or not model_id or not is_general_chat_agent_model_id(model_id):\\n continue\\n pricing = row.get(\\\"pricing\\\") if isinstance(row.get(\\\"pricing\\\"), dict) else {}\\n discovered.append(\\n@@ -152,7 +218,7 @@ def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[\\n completion_price_per_1k=_price_per_1k(pricing.get(\\\"completion\\\")),\\n )\\n )\\n- return discovered\\n+ return _deduplicate_discovered_models(discovered)\\n \\n \\n def _parse_bytez(payload: Any, source: ProviderModelSource) -> list[DiscoveredModel]:\\n@@ -162,7 +228,7 @@ def _parse_bytez(payload: Any, source: ProviderModelSource) -> list[DiscoveredMo\\n if not isinstance(row, dict):\\n continue\\n model_id = row.get(\\\"modelId\\\")\\n- if type(model_id) is not str or not model_id:\\n+ if type(model_id) is not str or not model_id or not is_general_chat_agent_model_id(model_id):\\n continue\\n discovered.append(\\n DiscoveredModel(\\n@@ -175,7 +241,7 @@ def _parse_bytez(payload: Any, source: ProviderModelSource) -> list[DiscoveredMo\\n # per-1k pricing unset is more honest than a misleading estimate.\\n )\\n )\\n- return discovered\\n+ return _deduplicate_discovered_models(discovered)\\n \\n \\n def discover_provider_models(\\n@@ -214,7 +280,7 @@ def discover_all_models(\\n discovered.extend(discover_provider_models(source, timeout=timeout))\\n except ProviderDiscoveryError as exc:\\n errors.append(exc)\\n- return discovered, errors\\n+ return _deduplicate_discovered_models(discovered), errors\\n \\n \\n _SLUG_RE = re.compile(r\\\"[^a-z0-9]+\\\")\\n@@ -231,7 +297,9 @@ def agent_id_for(discovered: DiscoveredModel) -> str:\\n \\n \\n def agent_from_discovered(discovered: DiscoveredModel, *, priority: int = 0) -> ModelAgent:\\n- \\\"\\\"\\\"Build a disabled-by-default ModelAgent for a discovered model (opt-in serving).\\\"\\\"\\\"\\n+ \\\"\\\"\\\"Build a disabled general-chat agent or reject an ineligible record.\\\"\\\"\\\"\\n+ if not is_general_chat_agent_model_id(discovered.model_id):\\n+ raise ValueError(\\\"model is not eligible for a general chat agent\\\")\\n return ModelAgent(\\n id=agent_id_for(discovered),\\n model=discovered.model_id,\\n@@ -245,73 +313,175 @@ def agent_from_discovered(discovered: DiscoveredModel, *, priority: int = 0) ->\\n )\\n \\n \\n+def _currency_is_comparable(currency_code: object, default_currency: object) -> bool:\\n+ \\\"\\\"\\\"Return whether two ISO-style currency codes can be compared directly.\\\"\\\"\\\"\\n+ return (\\n+ isinstance(currency_code, str)\\n+ and isinstance(default_currency, str)\\n+ and currency_code.strip().upper() == default_currency.strip().upper()\\n+ and bool(currency_code.strip())\\n+ )\\n+\\n+\\n def refresh_price_book(discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\\") -> int:\\n- \\\"\\\"\\\"Write every discovered model's known pricing into the price book.\\n+ \\\"\\\"\\\"Write complete, comparable provider pricing into the discovery price book.\\n \\n- Returns the number of price rows written. A model without provider-reported\\n- pricing is skipped rather than defaulted to 0 -- an unpriced model already\\n- costs 0 under ``PriceBook.compute_cost``'s \\\"explicit, not silently expensive\\\"\\n- contract, so writing a fabricated 0 row here would just hide that signal.\\n+ Both prompt and completion prices are required for the fixed 1K+1K ranking\\n+ workload. Partial, conflicting, non-finite, negative, or cross-currency\\n+ evidence remains unknown rather than acquiring an invented zero component.\\n \\\"\\\"\\\"\\n from .cost_ledger import PriceEntry\\n \\n written = 0\\n- for model in discovered:\\n- if model.prompt_price_per_1k is None and model.completion_price_per_1k is None:\\n+ for model in _deduplicate_discovered_models(discovered):\\n+ if not is_general_chat_agent_model_id(model.model_id):\\n+ continue\\n+ if not (\\n+ _valid_price_component(model.prompt_price_per_1k)\\n+ and _valid_price_component(model.completion_price_per_1k)\\n+ and _currency_is_comparable(\\n+ model.currency_code,\\n+ price_book.default_currency,\\n+ )\\n+ ):\\n continue\\n price_book.set_price(\\n PriceEntry(\\n provider_name=model.provider_name,\\n model_name=model.model_id,\\n- prompt_price_per_1k=model.prompt_price_per_1k or 0.0,\\n- completion_price_per_1k=model.completion_price_per_1k or 0.0,\\n- currency_code=model.currency_code,\\n+ prompt_price_per_1k=float(model.prompt_price_per_1k),\\n+ completion_price_per_1k=float(model.completion_price_per_1k),\\n+ currency_code=model.currency_code.strip().upper(),\\n )\\n )\\n written += 1\\n return written\\n \\n \\n+def _discovery_price_key(\\n+ model: DiscoveredModel,\\n+ price_book: \\\"PriceBook\\\",\\n+) -> tuple[int, float, str, str]:\\n+ \\\"\\\"\\\"Rank comparable trustworthy prices first, then deterministic unknowns.\\\"\\\"\\\"\\n+ unknown = (1, 0.0, model.provider_name, model.model_id)\\n+ try:\\n+ entry = price_book.get_price(model.provider_name, model.model_id)\\n+ except (TypeError, ValueError, OverflowError):\\n+ return unknown\\n+ if entry is None:\\n+ return unknown\\n+ if not (\\n+ _valid_price_component(entry.prompt_price_per_1k)\\n+ and _valid_price_component(entry.completion_price_per_1k)\\n+ and _currency_is_comparable(\\n+ entry.currency_code,\\n+ price_book.default_currency,\\n+ )\\n+ ):\\n+ return unknown\\n+ try:\\n+ cost, currency = price_book.compute_cost(\\n+ model.provider_name,\\n+ model.model_id,\\n+ 1000,\\n+ 1000,\\n+ )\\n+ except (TypeError, ValueError, OverflowError):\\n+ return unknown\\n+ if not (\\n+ _valid_price_component(cost)\\n+ and _currency_is_comparable(currency, price_book.default_currency)\\n+ ):\\n+ return unknown\\n+ return (0, cost, model.provider_name, model.model_id)\\n+\\n+\\n+def _provider_family(provider_name: str) -> str:\\n+ \\\"\\\"\\\"Collapse credentials that share one upstream provider outage domain.\\\"\\\"\\\"\\n+ if provider_name in {\\\"nvidia_nim\\\", \\\"nvidia_nim_sub\\\"}:\\n+ return \\\"nvidia_nim\\\"\\n+ return provider_name\\n+\\n+\\n def select_cheapest_discovered_agent(\\n discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\\"\\n ) -> DiscoveredModel | None:\\n- \\\"\\\"\\\"Pick the lowest-cost discovered model per the price book (auto-optimization).\\n-\\n- Reuses :func:`~contextual_orchestrator.batch_routing.cheapest_upstream`, the\\n- existing cost-optimizing upstream selector. Call :func:`refresh_price_book`\\n- first so discovered pricing is visible; an unpriced candidate costs ``0``\\n- under that selector's documented contract and is treated as free, not\\n- unknown -- so a genuinely unpriced provider (e.g. Bytez, priced by\\n- GPU-second rather than per token) will always look cheapest here. Fine for\\n- \\\"auto-pick something free to try,\\\" but callers doing real cost comparison\\n- should refresh pricing for every candidate they care about first.\\n+ \\\"\\\"\\\"Pick the cheapest candidate with trustworthy price evidence.\\n+\\n+ A candidate without a price row is unknown, not free. Known prices therefore\\n+ sort first; when every candidate is unpriced, provider and model identifiers\\n+ provide deterministic fallback ordering without inventing a monetary value.\\n \\\"\\\"\\\"\\n- if not discovered:\\n- return None\\n- candidates = [{\\\"provider\\\": model.provider_name, \\\"model\\\": model.model_id} for model in discovered]\\n- winner = cheapest_upstream(candidates, price_book)\\n- if winner is None:\\n+ eligible = [\\n+ model\\n+ for model in _deduplicate_discovered_models(discovered)\\n+ if is_general_chat_agent_model_id(model.model_id)\\n+ ]\\n+ if not eligible:\\n return None\\n- for model in discovered:\\n- if model.provider_name == winner[\\\"provider\\\"] and model.model_id == winner[\\\"model\\\"]:\\n- return model\\n- return None # pragma: no cover - winner always comes from candidates\\n+ return min(eligible, key=lambda model: _discovery_price_key(model, price_book))\\n \\n \\n def select_top_n_cheapest_discovered_agents(\\n discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\\", limit: int\\n ) -> list[DiscoveredModel]:\\n- \\\"\\\"\\\"Return the ``limit`` lowest-cost discovered models, cheapest first.\\n+ \\\"\\\"\\\"Return up to ``limit`` unique candidates, known-priced before unknown.\\\"\\\"\\\"\\n+ if limit <= 0:\\n+ return []\\n+ eligible = [\\n+ model\\n+ for model in _deduplicate_discovered_models(discovered)\\n+ if is_general_chat_agent_model_id(model.model_id)\\n+ ]\\n+ if not eligible:\\n+ return []\\n+ return sorted(\\n+ eligible,\\n+ key=lambda model: _discovery_price_key(model, price_book),\\n+ )[:limit]\\n \\n- For bootstrapping a CI sidecar (or any first-boot pool) with more than one\\n- enabled agent for failover, without hand-picking which discovered models to\\n- trust. Same pricing contract as :func:`select_cheapest_discovered_agent`.\\n+\\n+def select_bootstrap_discovered_agents(\\n+ discovered: list[DiscoveredModel],\\n+ price_book: \\\"PriceBook\\\",\\n+ limit: int,\\n+) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Build a deterministic, price-honest, provider-diverse initial pool.\\n+\\n+ Candidates retain the known-price-first ordering above, but the first pass\\n+ takes at most one model from each independent provider family. Remaining\\n+ capacity is filled in the same deterministic cost order. NVIDIA NIM primary\\n+ and sub credentials are one outage domain, so they participate in the second\\n+ pass only after independently hosted providers have had a chance to enter.\\n+ Duplicate serving identities never consume capacity twice.\\n \\\"\\\"\\\"\\n- if limit <= 0 or not discovered:\\n+ if limit <= 0:\\n+ return []\\n+ eligible = [\\n+ model\\n+ for model in _deduplicate_discovered_models(discovered)\\n+ if is_general_chat_agent_model_id(model.model_id)\\n+ ]\\n+ if not eligible:\\n return []\\n \\n- def _cost(model: DiscoveredModel) -> float:\\n- cost, _currency = price_book.compute_cost(model.provider_name, model.model_id, 1000, 1000)\\n- return cost\\n+ ranked = sorted(\\n+ eligible,\\n+ key=lambda model: _discovery_price_key(model, price_book),\\n+ )\\n+ selected: list[DiscoveredModel] = []\\n+ deferred: list[DiscoveredModel] = []\\n+ provider_families: set[str] = set()\\n+\\n+ for model in ranked:\\n+ family = _provider_family(model.provider_name)\\n+ if family in provider_families:\\n+ deferred.append(model)\\n+ continue\\n+ provider_families.add(family)\\n+ selected.append(model)\\n+ if len(selected) == limit:\\n+ return selected\\n \\n- return sorted(discovered, key=_cost)[:limit]\\n+ selected.extend(deferred[: limit - len(selected)])\\n+ return selected\" }, { \"sha\": \"254d593bed7de2f3fc823fc9e59062e6777590a8\", \"filename\": \"contextual_orchestrator/orchestrator.py\", \"status\": \"modified\", \"additions\": 62, \"deletions\": 10, \"changes\": 72, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Forchestrator.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Forchestrator.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Forchestrator.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -31,6 +31,10 @@\\n import urllib.error\\n import urllib.request\\n \\n+from .chat_capability import (\\n+ is_chat_compatible_model_id,\\n+ is_general_chat_agent_model_id,\\n+)\\n from .conventions import require_object_name\\n from .credentials import NotConfigured, get_credential\\n \\n@@ -776,6 +780,8 @@ def chat(\\n ``default_top_p`` are used so request-scoped Completions sampling can be\\n applied without threading kwargs through every orchestrator hop.\\n \\\"\\\"\\\"\\n+ if not is_chat_compatible_model_id(agent.model):\\n+ raise ValueError(\\\"model is not chat-compatible and cannot serve a chat request\\\")\\n self._local.usage = None\\n # Expose the effective sampling knobs for request-path tests / diagnostics.\\n effective_temperature = self.default_temperature if temperature is None else temperature\\n@@ -825,6 +831,15 @@ def probe(self, agent: ModelAgent, *, timeout: float = DEFAULT_PROVIDER_PROBE_TI\\n \\\"\\\"\\\"\\n probe_timeout = _validate_provider_probe_timeout(timeout)\\n started = time.monotonic()\\n+ if not is_chat_compatible_model_id(agent.model):\\n+ return {\\n+ \\\"agent_id\\\": agent.id,\\n+ \\\"model\\\": agent.model,\\n+ \\\"status\\\": \\\"not_ready\\\",\\n+ \\\"latency_ms\\\": round((time.monotonic() - started) * 1000, 2),\\n+ \\\"error_type\\\": \\\"ValueError\\\",\\n+ \\\"failure_code\\\": \\\"non_chat_model\\\",\\n+ }\\n self._local.usage = None\\n failure_code = \\\"provider_probe_failed\\\"\\n try:\\n@@ -1070,6 +1085,10 @@ def stream_chat(self, agent: ModelAgent, messages: list[ChatMessage], temperatur\\n are yielded as they arrive (not computed-then-framed). The mock path yields its\\n answer in fixed chunks so behavior shape stays testable and unchanged.\\n \\\"\\\"\\\"\\n+ if not is_chat_compatible_model_id(agent.model):\\n+ raise ValueError(\\n+ f\\\"model {agent.model!r} is not chat-compatible and cannot serve {agent.id!r}\\\"\\n+ )\\n if agent.base_url.startswith(\\\"mock://\\\"):\\n answer = self._mock(agent, messages)\\n for start in range(0, len(answer), 24):\\n@@ -1129,10 +1148,20 @@ def proxy_send(\\n self, agent: ModelAgent, endpoint: str, payload: dict[str, Any]\\n ) -> dict[str, Any]:\\n \\\"\\\"\\\"Passthrough a full request to one agent, returning the raw provider JSON.\\\"\\\"\\\"\\n+ normalized_endpoint = endpoint.strip(\\\"/\\\")\\n+ if normalized_endpoint.startswith(\\\"v1/\\\"):\\n+ normalized_endpoint = normalized_endpoint[3:]\\n+ if (\\n+ normalized_endpoint in {\\\"chat/completions\\\", \\\"completions\\\", \\\"responses\\\"}\\n+ and not is_chat_compatible_model_id(agent.model)\\n+ ):\\n+ raise ValueError(\\n+ f\\\"model {agent.model!r} is not chat-compatible and cannot serve {agent.id!r}\\\"\\n+ )\\n if agent.base_url.startswith(\\\"mock://\\\"):\\n- return self._mock_raw(agent, endpoint, payload)\\n+ return self._mock_raw(agent, normalized_endpoint, payload)\\n destination = self._validate_provider(agent) # pragma: no cover\\n- if endpoint.strip(\\\"/\\\") == \\\"responses\\\" and _is_local_provider_url(agent.base_url):\\n+ if normalized_endpoint == \\\"responses\\\" and _is_local_provider_url(agent.base_url):\\n chat_payload = _responses_to_chat_payload(payload)\\n chat_payload.setdefault(\\\"max_tokens\\\", self.max_output_tokens)\\n if _is_direct_mlx_provider_url(agent.base_url) and self.chat_template_args:\\n@@ -1143,7 +1172,7 @@ def proxy_send(\\n )\\n return _chat_to_responses_payload(chat_response, payload)\\n with _local_provider_slot(agent, self.local_concurrency, self.timeout): # pragma: no cover\\n- return self._send_raw_with_retry(agent, endpoint, payload, destination)\\n+ return self._send_raw_with_retry(agent, normalized_endpoint, payload, destination)\\n \\n def _send_raw_with_retry(\\n self,\\n@@ -1316,6 +1345,10 @@ def batch_chat(\\n workloads (24h completion window, ~half the price); real-time chat should keep\\n using ``chat``. The mock path answers synchronously so tests and local runs work.\\n \\\"\\\"\\\"\\n+ if not is_chat_compatible_model_id(agent.model):\\n+ raise ValueError(\\n+ f\\\"model {agent.model!r} is not chat-compatible and cannot serve {agent.id!r}\\\"\\n+ )\\n if agent.base_url.startswith(\\\"mock://\\\"):\\n results = {\\n custom_id: {\\\"content\\\": self._mock(agent, messages), \\\"usage\\\": None}\\n@@ -2434,6 +2467,7 @@ def _plan_generated(self, task: str) -> list[WorkflowStep]:\\n pool = \\\"\\\\n\\\".join(\\n f\\\"- {agent.id}: model={agent.model}, tags={', '.join(agent.tags) or 'none'}\\\"\\n for agent in self.agents\\n+ if is_general_chat_agent_model_id(agent.model)\\n )\\n system = (\\n \\\"You are the workflow conductor. Decompose the user's task into a short workflow.\\\\n\\\"\\n@@ -2459,7 +2493,7 @@ def _parse_workflow_plan(self, raw: str) -> list[WorkflowStep]:\\n raw_steps = data.get(\\\"steps\\\")\\n if not isinstance(raw_steps, list) or not (2 <= len(raw_steps) <= self.policy.max_workflow_steps):\\n raise ValueError(f\\\"plan must have 2..{self.policy.max_workflow_steps} steps\\\")\\n- known_agents = {agent.id for agent in self.agents}\\n+ known_agents = {agent.id: agent for agent in self.agents}\\n steps: list[WorkflowStep] = []\\n for index, item in enumerate(raw_steps):\\n if int(item.get(\\\"id\\\", -1)) != index:\\n@@ -2474,8 +2508,9 @@ def _parse_workflow_plan(self, raw: str) -> list[WorkflowStep]:\\n if any(value < 0 or value >= index for value in access):\\n raise ValueError(\\\"access may reference only earlier steps\\\")\\n agent_id = item.get(\\\"agent_id\\\")\\n- if agent_id not in known_agents:\\n- # The planner named an unknown agent: reselect honestly instead of failing the plan.\\n+ assigned = known_agents.get(agent_id)\\n+ if assigned is None or not is_general_chat_agent_model_id(assigned.model):\\n+ # Unknown or stale ineligible assignments are reselected honestly.\\n agent_id = self._select_agent(subtask, role).id\\n steps.append(WorkflowStep(index, role, agent_id, subtask, access))\\n if steps[-1].role not in {\\\"synthesizer\\\", \\\"worker\\\"}:\\n@@ -2509,10 +2544,21 @@ def _score_agent(self, agent: ModelAgent, role: str, lowered: str) -> tuple[int,\\n def _ranked_agents(self, text: str, role: str) -> list[ModelAgent]:\\n \\\"\\\"\\\"Agents sorted best-first for a role; the head is the primary, the tail are failovers.\\\"\\\"\\\"\\n lowered = text.lower()\\n- return sorted(self.agents, key=lambda agent: self._score_agent(agent, role, lowered), reverse=True)\\n+ return [\\n+ agent\\n+ for agent in sorted(\\n+ self.agents,\\n+ key=lambda agent: self._score_agent(agent, role, lowered),\\n+ reverse=True,\\n+ )\\n+ if is_general_chat_agent_model_id(agent.model)\\n+ ]\\n \\n def _select_agent(self, text: str, role: str) -> ModelAgent:\\n- selected = self._ranked_agents(text, role)[0]\\n+ ranked = self._ranked_agents(text, role)\\n+ if not ranked:\\n+ raise RuntimeError(f\\\"no chat-compatible agent available for role={role}\\\")\\n+ selected = ranked[0]\\n if selected.disabled: # pragma: no cover\\n raise RuntimeError(f\\\"no enabled agent available for role={role}\\\")\\n if role in selected.provider_exclusions: # pragma: no cover\\n@@ -2530,6 +2576,8 @@ def _invoke(\\n usage when available (else None), so spend analytics can prefer it.\\n \\\"\\\"\\\"\\n candidates = self._failover_candidates(primary, text, role)\\n+ if not candidates:\\n+ raise RuntimeError(f\\\"no chat-compatible agent available for role={role}\\\")\\n last_error: Exception | None = None\\n for agent in candidates:\\n try:\\n@@ -2545,11 +2593,15 @@ def _invoke(\\n \\n def _failover_candidates(self, primary: ModelAgent, text: str, role: str) -> list[ModelAgent]:\\n ranked = self._ranked_agents(text, role)\\n- ordered = [primary] + [agent for agent in ranked if agent.id != primary.id]\\n+ ordered = [\\n+ agent\\n+ for agent in [primary] + [agent for agent in ranked if agent.id != primary.id]\\n+ if is_general_chat_agent_model_id(agent.model)\\n+ ]\\n eligible = [agent for agent in ordered if not agent.disabled and role not in agent.provider_exclusions]\\n healthy = [agent for agent in eligible if not self._circuit_open(agent.id)]\\n # If every eligible agent is circuit-open, still probe them rather than fail with no attempt.\\n- return healthy or eligible or [primary]\\n+ return healthy or eligible\\n \\n def _circuit_open(self, agent_id: str) -> bool:\\n with self._circuit_lock:\" }, { \"sha\": \"323c56d9df0bd0f36acf58fa641df6585a57efd9\", \"filename\": \"contextual_orchestrator/provider_bootstrap.py\", \"status\": \"added\", \"additions\": 354, \"deletions\": 0, \"changes\": 354, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fprovider_bootstrap.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fprovider_bootstrap.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fprovider_bootstrap.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,354 @@\\n+\\\"\\\"\\\"Durable bootstrap for the organization provider credential inventory.\\n+\\n+A trusted deployment process may expose the fixed provider-secret inventory to\\n+this one-shot module. Values are validated as a complete set, written to the\\n+configured credential KV, and then model discovery runs exclusively through the\\n+KV-backed runtime seam. Runtime provider calls never read provider API keys from\\n+``os.environ``.\\n+\\n+Bootstrap establishes a conservative serving candidate set. It does not infer\\n+reasoning, coding, vision, or other provider capabilities from model names;\\n+capability negotiation remains an explicit runtime/catalog responsibility.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import argparse\\n+from dataclasses import dataclass, replace\\n+import json\\n+import os\\n+from typing import Mapping, Sequence\\n+\\n+from .chat_capability import is_general_chat_agent_model_id\\n+from .cost_ledger import PriceBook\\n+from .credentials import (\\n+ InMemoryCredentialBackend,\\n+ PostgresCredentialBackend,\\n+ get_backend,\\n+)\\n+from .kv_config import InMemoryConfigStore\\n+from .model_discovery import (\\n+ DiscoveredModel,\\n+ PROVIDER_MODEL_SOURCES,\\n+ _currency_is_comparable,\\n+ _provider_family,\\n+ agent_from_discovered,\\n+ agent_id_for,\\n+ discover_all_models,\\n+ refresh_price_book,\\n+)\\n+from .orchestrator import ModelAgent, TaskOrchestrator\\n+\\n+\\n+PROVIDER_CREDENTIAL_NAMES: tuple[str, ...] = tuple(\\n+ dict.fromkeys(source.credential_name for source in PROVIDER_MODEL_SOURCES)\\n+)\\n+\\\"\\\"\\\"Fixed organization credential inventory accepted by the bootstrap boundary.\\\"\\\"\\\"\\n+\\n+_GENERIC_SERVING_TAGS = (\\n+ \\\"discovered\\\",\\n+ \\\"chat\\\",\\n+ \\\"worker\\\",\\n+ \\\"writing\\\",\\n+ \\\"synthesizer\\\",\\n+)\\n+\\n+\\n+class ProviderBootstrapError(RuntimeError):\\n+ \\\"\\\"\\\"Raised when trusted provider bootstrap cannot establish a usable catalog.\\\"\\\"\\\"\\n+\\n+\\n+@dataclass(frozen=True)\\n+class ProviderBootstrapReport:\\n+ \\\"\\\"\\\"Secret-free evidence emitted after one provider bootstrap run.\\\"\\\"\\\"\\n+\\n+ registered_credentials: tuple[str, ...]\\n+ discovered_model_count: int\\n+ eligible_model_count: int\\n+ selected_agent_ids: tuple[str, ...]\\n+ enabled_agent_ids: tuple[str, ...]\\n+ durable_agent_pool: bool\\n+ providers_with_errors: tuple[str, ...]\\n+ priced_model_count: int\\n+\\n+ def as_dict(self) -> dict[str, object]:\\n+ \\\"\\\"\\\"Return JSON-safe evidence without credential values or provider payloads.\\\"\\\"\\\"\\n+ return {\\n+ \\\"registered_credentials\\\": list(self.registered_credentials),\\n+ \\\"discovered_model_count\\\": self.discovered_model_count,\\n+ \\\"eligible_model_count\\\": self.eligible_model_count,\\n+ \\\"selected_agent_ids\\\": list(self.selected_agent_ids),\\n+ \\\"enabled_agent_ids\\\": list(self.enabled_agent_ids),\\n+ \\\"durable_agent_pool\\\": self.durable_agent_pool,\\n+ \\\"providers_with_errors\\\": list(self.providers_with_errors),\\n+ \\\"priced_model_count\\\": self.priced_model_count,\\n+ }\\n+\\n+\\n+def _strip_mounted_line_endings(value: str) -> str:\\n+ \\\"\\\"\\\"Remove only CR/LF bytes commonly appended by mounted secret files.\\\"\\\"\\\"\\n+ return value.rstrip(\\\"\\\\r\\\\n\\\")\\n+\\n+\\n+def collect_provider_credentials(\\n+ environ: Mapping[str, str], *, require_all: bool = True\\n+) -> dict[str, str]:\\n+ \\\"\\\"\\\"Collect the fixed inventory without rewriting non-line-ending bytes.\\\"\\\"\\\"\\n+ values: dict[str, str] = {}\\n+ missing: list[str] = []\\n+ for name in PROVIDER_CREDENTIAL_NAMES:\\n+ raw = environ.get(name, \\\"\\\")\\n+ value = _strip_mounted_line_endings(raw) if isinstance(raw, str) else \\\"\\\"\\n+ if value and value.strip():\\n+ values[name] = value\\n+ else:\\n+ missing.append(name)\\n+ if require_all and missing:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap requires the complete credential inventory: \\\"\\n+ + \\\", \\\".join(sorted(missing))\\n+ )\\n+ if not values:\\n+ raise ProviderBootstrapError(\\\"provider bootstrap received no credentials\\\")\\n+ return values\\n+\\n+\\n+def register_provider_credentials_atomically(\\n+ credentials: Mapping[str, str],\\n+) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Register a validated credential batch with one commit where supported.\\\"\\\"\\\"\\n+ if not credentials:\\n+ raise ProviderBootstrapError(\\\"provider bootstrap received an empty credential batch\\\")\\n+ unknown = sorted(set(credentials) - set(PROVIDER_CREDENTIAL_NAMES))\\n+ if unknown:\\n+ raise ProviderBootstrapError(\\\"provider bootstrap rejected unknown credential names\\\")\\n+\\n+ normalized: dict[str, str] = {}\\n+ for name, value in credentials.items():\\n+ if not isinstance(value, str):\\n+ raise ProviderBootstrapError(\\n+ f\\\"provider bootstrap rejected an empty value for {name}\\\"\\n+ )\\n+ normalized_value = _strip_mounted_line_endings(value)\\n+ if not normalized_value or not normalized_value.strip():\\n+ raise ProviderBootstrapError(\\n+ f\\\"provider bootstrap rejected an empty value for {name}\\\"\\n+ )\\n+ normalized[name] = normalized_value\\n+\\n+ backend = get_backend()\\n+ if isinstance(backend, InMemoryCredentialBackend):\\n+ with backend._lock: # noqa: SLF001 - package-internal atomic batch operation\\n+ backend._store.update(normalized) # noqa: SLF001\\n+ elif isinstance(backend, PostgresCredentialBackend):\\n+ with backend._connect() as connection: # noqa: SLF001 - package transaction\\n+ backend._ensure_schema(connection) # noqa: SLF001\\n+ with connection.cursor() as cursor:\\n+ for name, value in normalized.items():\\n+ cursor.execute(\\n+ \\\"INSERT INTO provider_credentials \\\"\\n+ \\\"(credential_name, encrypted_value, updated_at) \\\"\\n+ \\\"VALUES (%s, pgp_sym_encrypt(%s, %s), now()) \\\"\\n+ \\\"ON CONFLICT (credential_name) DO UPDATE SET \\\"\\n+ \\\"encrypted_value = EXCLUDED.encrypted_value, updated_at = now()\\\",\\n+ (name, value, backend._passphrase), # noqa: SLF001\\n+ )\\n+ connection.commit()\\n+ else:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap requires an atomic built-in credential backend\\\"\\n+ )\\n+ return tuple(sorted(normalized))\\n+\\n+\\n+def is_chat_serving_candidate(model: DiscoveredModel) -> bool:\\n+ \\\"\\\"\\\"Apply the shared ordinary-chat eligibility policy to a catalog row.\\n+\\n+ This is a negative compatibility filter, not positive capability inference.\\n+ Models that survive receive only generic chat-serving tags until an explicit\\n+ provider/catalog capability record or measured evidence is available.\\n+ \\\"\\\"\\\"\\n+ return is_general_chat_agent_model_id(model.model_id)\\n+\\n+\\n+def serving_tags_for_discovered(_model: DiscoveredModel) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Return capability-neutral tags safe for any compatible chat candidate.\\\"\\\"\\\"\\n+ return _GENERIC_SERVING_TAGS\\n+\\n+\\n+def _known_cost_sort_key(\\n+ model: DiscoveredModel,\\n+) -> tuple[int, float, str, str]:\\n+ \\\"\\\"\\\"Sort known-price, comparable-currency models before unknown/incomparable ones.\\n+\\n+ Mirrors ``model_discovery._discovery_price_key``'s currency gate so a\\n+ cheap non-USD price can never outrank a USD one on face value alone.\\n+ \\\"\\\"\\\"\\n+ prices = (model.prompt_price_per_1k, model.completion_price_per_1k)\\n+ prompt_price, completion_price = prices\\n+ if (\\n+ prompt_price is None\\n+ or completion_price is None\\n+ or not _currency_is_comparable(model.currency_code, \\\"USD\\\")\\n+ ):\\n+ return (1, float(\\\"inf\\\"), model.provider_name, model.model_id)\\n+ return (0, prompt_price + completion_price, model.provider_name, model.model_id)\\n+\\n+\\n+def select_provider_diverse_models(\\n+ discovered: Sequence[DiscoveredModel], *, limit: int\\n+) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Choose a bounded compatible pool while preserving provider diversity.\\\"\\\"\\\"\\n+ if limit < 1:\\n+ raise ValueError(\\\"provider bootstrap model limit must be positive\\\")\\n+ unique: dict[tuple[str, str, str], DiscoveredModel] = {}\\n+ for model in discovered:\\n+ if not is_chat_serving_candidate(model):\\n+ continue\\n+ unique[(model.provider_name, model.credential_name, model.model_id)] = model\\n+ ordered = sorted(unique.values(), key=_known_cost_sort_key)\\n+ selected: list[DiscoveredModel] = []\\n+ seen_providers: set[str] = set()\\n+ for model in ordered:\\n+ provider_family = _provider_family(model.provider_name)\\n+ if provider_family in seen_providers:\\n+ continue\\n+ selected.append(model)\\n+ seen_providers.add(provider_family)\\n+ if len(selected) >= limit:\\n+ return selected\\n+ selected_keys = {\\n+ (item.provider_name, item.credential_name, item.model_id)\\n+ for item in selected\\n+ }\\n+ for model in ordered:\\n+ key = (model.provider_name, model.credential_name, model.model_id)\\n+ if key in selected_keys:\\n+ continue\\n+ selected.append(model)\\n+ if len(selected) >= limit:\\n+ break\\n+ return selected\\n+\\n+\\n+def _active_agent_from_discovered(model: DiscoveredModel) -> ModelAgent:\\n+ \\\"\\\"\\\"Convert one selected chat model into an enabled capability-neutral agent.\\\"\\\"\\\"\\n+ return replace(\\n+ agent_from_discovered(model),\\n+ disabled=False,\\n+ tags=serving_tags_for_discovered(model),\\n+ )\\n+\\n+\\n+def _synchronize_durable_agent_pool(\\n+ agents_db: str,\\n+ selected: Sequence[DiscoveredModel],\\n+) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Activate exactly the selected discovered models in one durable agent pool.\\\"\\\"\\\"\\n+ bootstrap = TaskOrchestrator(\\n+ [ModelAgent(\\\"bootstrap_agent\\\", \\\"bootstrap-model\\\")],\\n+ agents_db=agents_db,\\n+ )\\n+ agents = [_active_agent_from_discovered(model) for model in selected]\\n+ selected_ids = {agent.id for agent in agents}\\n+ bootstrap.sync_discovered_agents(agents)\\n+\\n+ for candidate in list(bootstrap.candidates):\\n+ if candidate.id in selected_ids:\\n+ continue\\n+ if candidate.id == \\\"bootstrap_agent\\\" or \\\"discovered\\\" in candidate.tags:\\n+ if not candidate.disabled:\\n+ bootstrap.remove_agent(\\\"default\\\", candidate.id)\\n+\\n+ for agent in agents:\\n+ bootstrap.patch_agent(\\\"default\\\", agent.id, {\\\"status\\\": \\\"active\\\"})\\n+\\n+ enabled = tuple(\\n+ sorted(agent.id for agent in bootstrap.agents if agent.id in selected_ids)\\n+ )\\n+ if set(enabled) != selected_ids:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap could not activate the selected agent pool\\\"\\n+ )\\n+ return enabled\\n+\\n+\\n+def bootstrap_provider_runtime(\\n+ *,\\n+ environ: Mapping[str, str],\\n+ require_all_credentials: bool = True,\\n+ agents_db: str | None = None,\\n+ model_limit: int = 16,\\n+) -> ProviderBootstrapReport:\\n+ \\\"\\\"\\\"Register trusted secrets, discover chat models, and optionally activate a pool.\\\"\\\"\\\"\\n+ credentials = collect_provider_credentials(\\n+ environ, require_all=require_all_credentials\\n+ )\\n+ registered = register_provider_credentials_atomically(credentials)\\n+ discovered, errors = discover_all_models()\\n+ if not discovered:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap discovered no usable models\\\"\\n+ )\\n+\\n+ eligible = [model for model in discovered if is_chat_serving_candidate(model)]\\n+ if not eligible:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap discovered no chat-capable models\\\"\\n+ )\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ priced_count = refresh_price_book(discovered, price_book)\\n+ selected = select_provider_diverse_models(eligible, limit=model_limit)\\n+ if not selected:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap selected no chat-capable models\\\"\\n+ )\\n+ selected_ids = tuple(agent_id_for(model) for model in selected)\\n+ enabled_ids = (\\n+ _synchronize_durable_agent_pool(agents_db, selected)\\n+ if agents_db\\n+ else ()\\n+ )\\n+\\n+ return ProviderBootstrapReport(\\n+ registered_credentials=registered,\\n+ discovered_model_count=len(discovered),\\n+ eligible_model_count=len(eligible),\\n+ selected_agent_ids=selected_ids,\\n+ enabled_agent_ids=enabled_ids,\\n+ durable_agent_pool=bool(agents_db),\\n+ providers_with_errors=tuple(\\n+ sorted({error.provider_name for error in errors})\\n+ ),\\n+ priced_model_count=priced_count,\\n+ )\\n+\\n+\\n+def main(argv: Sequence[str] | None = None) -> None:\\n+ \\\"\\\"\\\"Run the one-shot provider bootstrap command used by trusted deployment jobs.\\\"\\\"\\\"\\n+ parser = argparse.ArgumentParser(\\n+ description=\\\"Register provider secrets and refresh the runtime model pool.\\\"\\n+ )\\n+ parser.add_argument(\\n+ \\\"--agents-db\\\",\\n+ default=os.environ.get(\\\"CONTEXTUAL_ORCHESTRATOR_AGENTS_DB\\\") or None,\\n+ )\\n+ parser.add_argument(\\\"--model-limit\\\", type=int, default=16)\\n+ parser.add_argument(\\n+ \\\"--allow-partial-credentials\\\",\\n+ action=\\\"store_true\\\",\\n+ help=\\\"Permit a subset of the fixed provider inventory (development only).\\\",\\n+ )\\n+ args = parser.parse_args(list(argv) if argv is not None else None)\\n+ report = bootstrap_provider_runtime(\\n+ environ=os.environ,\\n+ require_all_credentials=not args.allow_partial_credentials,\\n+ agents_db=args.agents_db,\\n+ model_limit=args.model_limit,\\n+ )\\n+ print(json.dumps(report.as_dict(), ensure_ascii=False, sort_keys=True))\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover - subprocess/CLI coverage\\n+ main()\" }, { \"sha\": \"2253caef562a6275371ebcb9700add802ee39ed9\", \"filename\": \"contextual_orchestrator/provider_catalog_bootstrap.py\", \"status\": \"added\", \"additions\": 378, \"deletions\": 0, \"changes\": 378, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fprovider_catalog_bootstrap.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fprovider_catalog_bootstrap.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fprovider_catalog_bootstrap.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,378 @@\\n+\\\"\\\"\\\"Trusted provider bootstrap with durable normalized model-catalog persistence.\\n+\\n+This command registers the complete credential inventory, performs provider-\\n+isolated discovery, persists successful model metadata in PostgreSQL, retains\\n+last-known-good models for failed providers, and constructs a bounded candidate\\n+pool from the persisted catalog.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import argparse\\n+from dataclasses import dataclass\\n+import json\\n+import os\\n+from typing import Callable, Mapping, Sequence\\n+\\n+from .cost_ledger import PriceBook\\n+from .credentials import (\\n+ InMemoryCredentialBackend,\\n+ PostgresCredentialBackend,\\n+ get_backend,\\n+ get_credential,\\n+)\\n+from .kv_config import InMemoryConfigStore\\n+from .model_discovery import (\\n+ DiscoveredModel,\\n+ PROVIDER_MODEL_SOURCES,\\n+ ProviderDiscoveryError,\\n+ ProviderModelSource,\\n+ agent_id_for,\\n+ discover_all_models,\\n+ refresh_price_book,\\n+)\\n+from .provider_bootstrap import (\\n+ ProviderBootstrapError,\\n+ _synchronize_durable_agent_pool,\\n+ collect_provider_credentials,\\n+ is_chat_serving_candidate,\\n+ register_provider_credentials_atomically,\\n+ select_provider_diverse_models,\\n+ serving_tags_for_discovered,\\n+)\\n+from .provider_catalog_store import (\\n+ InMemoryProviderCatalogStore,\\n+ PostgresProviderCatalogStore,\\n+ ProviderCatalogStore,\\n+)\\n+\\n+\\n+@dataclass(frozen=True)\\n+class ProviderCatalogSnapshot:\\n+ \\\"\\\"\\\"Effective persisted model snapshot after provider-isolated refresh.\\\"\\\"\\\"\\n+\\n+ models: tuple[DiscoveredModel, ...]\\n+ live_model_count: int\\n+ last_known_good_model_count: int\\n+ refresh_failure_count: int\\n+ providers_with_errors: tuple[str, ...]\\n+\\n+\\n+@dataclass(frozen=True)\\n+class ProviderCatalogBootstrapReport:\\n+ \\\"\\\"\\\"Secret-free evidence for one durable provider-catalog bootstrap.\\n+\\n+ ``registered_credentials`` contains the credential names that remain in the\\n+ credential registry after provider-isolated rollback has completed. It is\\n+ therefore safe for a workflow to use as durable-registration evidence.\\n+ \\\"\\\"\\\"\\n+\\n+ registered_credentials: tuple[str, ...]\\n+ restored_credentials: tuple[str, ...]\\n+ live_discovered_model_count: int\\n+ catalog_model_count: int\\n+ eligible_model_count: int\\n+ last_known_good_model_count: int\\n+ selected_agent_ids: tuple[str, ...]\\n+ enabled_agent_ids: tuple[str, ...]\\n+ durable_agent_pool: bool\\n+ catalog_backend: str\\n+ catalog_refresh_failure_count: int\\n+ providers_with_errors: tuple[str, ...]\\n+ priced_model_count: int\\n+\\n+ def as_dict(self) -> dict[str, object]:\\n+ \\\"\\\"\\\"Return the stable JSON evidence contract without secret values.\\\"\\\"\\\"\\n+ return {\\n+ \\\"registered_credentials\\\": list(self.registered_credentials),\\n+ \\\"restored_credentials\\\": list(self.restored_credentials),\\n+ \\\"live_discovered_model_count\\\": self.live_discovered_model_count,\\n+ \\\"catalog_model_count\\\": self.catalog_model_count,\\n+ \\\"eligible_model_count\\\": self.eligible_model_count,\\n+ \\\"last_known_good_model_count\\\": self.last_known_good_model_count,\\n+ \\\"selected_agent_ids\\\": list(self.selected_agent_ids),\\n+ \\\"enabled_agent_ids\\\": list(self.enabled_agent_ids),\\n+ \\\"durable_agent_pool\\\": self.durable_agent_pool,\\n+ \\\"catalog_backend\\\": self.catalog_backend,\\n+ \\\"catalog_refresh_failure_count\\\": self.catalog_refresh_failure_count,\\n+ \\\"providers_with_errors\\\": list(self.providers_with_errors),\\n+ \\\"priced_model_count\\\": self.priced_model_count,\\n+ }\\n+\\n+\\n+def build_provider_catalog_store() -> ProviderCatalogStore:\\n+ \\\"\\\"\\\"Build a catalog store colocated with the active credential backend.\\\"\\\"\\\"\\n+ backend = get_backend()\\n+ if isinstance(backend, PostgresCredentialBackend):\\n+ return PostgresProviderCatalogStore(backend.connection_dsn)\\n+ if isinstance(backend, InMemoryCredentialBackend):\\n+ return InMemoryProviderCatalogStore()\\n+ raise ProviderBootstrapError(\\n+ \\\"provider catalog requires a built-in atomic credential backend\\\"\\n+ )\\n+\\n+\\n+def _restore_provider_credentials_atomically(\\n+ previous_credentials: Mapping[str, str | None],\\n+) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Restore one credential snapshot in a single built-in backend transaction.\\\"\\\"\\\"\\n+ backend = get_backend()\\n+ ordered = tuple(sorted(previous_credentials))\\n+ if isinstance(backend, InMemoryCredentialBackend):\\n+ with backend._lock: # noqa: SLF001 - package-internal rollback transaction\\n+ for name in ordered:\\n+ previous = previous_credentials[name]\\n+ if previous is None:\\n+ backend._store.pop(name, None) # noqa: SLF001\\n+ else:\\n+ backend._store[name] = previous # noqa: SLF001\\n+ return ordered\\n+ if isinstance(backend, PostgresCredentialBackend):\\n+ with backend._connect() as connection: # noqa: SLF001 - package transaction\\n+ backend._ensure_schema(connection) # noqa: SLF001\\n+ with connection.cursor() as cursor:\\n+ for name in ordered:\\n+ previous = previous_credentials[name]\\n+ if previous is None:\\n+ cursor.execute(\\n+ \\\"DELETE FROM provider_credentials WHERE credential_name = %s\\\",\\n+ (name,),\\n+ )\\n+ else:\\n+ cursor.execute(\\n+ \\\"INSERT INTO provider_credentials \\\"\\n+ \\\"(credential_name, encrypted_value, updated_at) \\\"\\n+ \\\"VALUES (%s, pgp_sym_encrypt(%s, %s), now()) \\\"\\n+ \\\"ON CONFLICT (credential_name) DO UPDATE SET \\\"\\n+ \\\"encrypted_value = EXCLUDED.encrypted_value, updated_at = now()\\\",\\n+ (name, previous, backend._passphrase), # noqa: SLF001\\n+ )\\n+ connection.commit()\\n+ return ordered\\n+ raise ProviderBootstrapError(\\n+ \\\"provider credential rollback requires an atomic built-in backend\\\"\\n+ )\\n+\\n+\\n+def _source_key(source: ProviderModelSource) -> tuple[str, str]:\\n+ \\\"\\\"\\\"Return the provider-account key shared by sources and model rows.\\\"\\\"\\\"\\n+ return (source.provider_name, source.credential_name)\\n+\\n+\\n+def _model_key(model: DiscoveredModel) -> tuple[str, str]:\\n+ \\\"\\\"\\\"Return the provider-account key carried by one discovered model.\\\"\\\"\\\"\\n+ return (model.provider_name, model.credential_name)\\n+\\n+\\n+def refresh_persisted_provider_catalog(\\n+ store: ProviderCatalogStore,\\n+ *,\\n+ sources: Sequence[ProviderModelSource],\\n+ registered_credentials: Sequence[str],\\n+ discovered: Sequence[DiscoveredModel],\\n+ errors: Sequence[ProviderDiscoveryError],\\n+) -> ProviderCatalogSnapshot:\\n+ \\\"\\\"\\\"Persist account-local refreshes and return the effective LKG snapshot.\\\"\\\"\\\"\\n+ registered = set(registered_credentials)\\n+ live_by_account: dict[tuple[str, str], list[DiscoveredModel]] = {}\\n+ for model in discovered:\\n+ live_by_account.setdefault(_model_key(model), []).append(model)\\n+\\n+ failed_names = {error.provider_name for error in errors}\\n+ effective: list[DiscoveredModel] = []\\n+ last_known_good_count = 0\\n+ refresh_failures = 0\\n+ providers_with_errors: set[str] = set(failed_names)\\n+\\n+ for source in sources:\\n+ if source.credential_name not in registered:\\n+ continue\\n+ account_models = live_by_account.get(_source_key(source), [])\\n+ failed = source.provider_name in failed_names\\n+ if failed:\\n+ store.record_failure(source, error_code=\\\"provider_discovery_error\\\")\\n+ refresh_failures += 1\\n+ elif not account_models:\\n+ store.record_failure(source, error_code=\\\"empty_provider_catalog\\\")\\n+ refresh_failures += 1\\n+ providers_with_errors.add(source.provider_name)\\n+ else:\\n+ eligible_ids = {\\n+ model.model_id\\n+ for model in account_models\\n+ if is_chat_serving_candidate(model)\\n+ }\\n+ tags = {\\n+ model.model_id: serving_tags_for_discovered(model)\\n+ for model in account_models\\n+ if model.model_id in eligible_ids\\n+ }\\n+ store.record_success(\\n+ source,\\n+ account_models,\\n+ eligible_model_ids=eligible_ids,\\n+ serving_tags=tags,\\n+ )\\n+\\n+ persisted = store.serving_models(source)\\n+ effective.extend(persisted)\\n+ if failed or not account_models:\\n+ last_known_good_count += len(persisted)\\n+\\n+ unique: dict[tuple[str, str, str], DiscoveredModel] = {}\\n+ for model in effective:\\n+ unique[(model.provider_name, model.credential_name, model.model_id)] = model\\n+ ordered = tuple(unique[key] for key in sorted(unique))\\n+ return ProviderCatalogSnapshot(\\n+ models=ordered,\\n+ live_model_count=len(discovered),\\n+ last_known_good_model_count=last_known_good_count,\\n+ refresh_failure_count=refresh_failures,\\n+ providers_with_errors=tuple(sorted(providers_with_errors)),\\n+ )\\n+\\n+\\n+DiscoveryFunction = Callable[\\n+ [tuple[ProviderModelSource, ...]],\\n+ tuple[list[DiscoveredModel], list[ProviderDiscoveryError]],\\n+]\\n+\\n+\\n+def bootstrap_provider_catalog_runtime(\\n+ *,\\n+ environ: Mapping[str, str],\\n+ require_all_credentials: bool = True,\\n+ agents_db: str | None = None,\\n+ model_limit: int = 16,\\n+ catalog_store: ProviderCatalogStore | None = None,\\n+ sources: Sequence[ProviderModelSource] = PROVIDER_MODEL_SOURCES,\\n+ discovery: DiscoveryFunction | None = None,\\n+) -> ProviderCatalogBootstrapReport:\\n+ \\\"\\\"\\\"Register secrets, persist catalogs, and build the effective serving pool.\\\"\\\"\\\"\\n+ credentials = collect_provider_credentials(\\n+ environ,\\n+ require_all=require_all_credentials,\\n+ )\\n+ previous_credentials = {\\n+ name: get_credential(name) for name in credentials\\n+ }\\n+ registered = register_provider_credentials_atomically(credentials)\\n+ try:\\n+ store = catalog_store or build_provider_catalog_store()\\n+ source_tuple = tuple(sources)\\n+ discover = discovery or (\\n+ lambda requested_sources: discover_all_models(requested_sources)\\n+ )\\n+ live_models, errors = discover(source_tuple)\\n+ snapshot = refresh_persisted_provider_catalog(\\n+ store,\\n+ sources=source_tuple,\\n+ registered_credentials=registered,\\n+ discovered=live_models,\\n+ errors=errors,\\n+ )\\n+ failed_provider_names = {error.provider_name for error in errors}\\n+ failed_credentials = {\\n+ source.credential_name\\n+ for source in source_tuple\\n+ if source.credential_name in registered\\n+ and (\\n+ source.provider_name in failed_provider_names\\n+ or not any(\\n+ _model_key(model) == _source_key(source)\\n+ for model in live_models\\n+ )\\n+ )\\n+ }\\n+ restored_credentials = _restore_provider_credentials_atomically(\\n+ {\\n+ name: previous_credentials.get(name)\\n+ for name in failed_credentials\\n+ }\\n+ ) if failed_credentials else ()\\n+\\n+ usable_models = tuple(\\n+ model\\n+ for model in snapshot.models\\n+ if get_credential(model.credential_name)\\n+ )\\n+ if not usable_models:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap has no persisted chat-compatible model with a usable credential\\\"\\n+ )\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ priced_count = refresh_price_book(list(usable_models), price_book)\\n+ selected = select_provider_diverse_models(\\n+ usable_models,\\n+ limit=model_limit,\\n+ )\\n+ if not selected:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap selected no persisted chat-compatible model\\\"\\n+ )\\n+ selected_ids = tuple(agent_id_for(model) for model in selected)\\n+ enabled_ids = (\\n+ _synchronize_durable_agent_pool(agents_db, selected)\\n+ if agents_db\\n+ else ()\\n+ )\\n+ durable_registered_credentials = tuple(\\n+ name for name in registered if get_credential(name) is not None\\n+ )\\n+\\n+ return ProviderCatalogBootstrapReport(\\n+ registered_credentials=durable_registered_credentials,\\n+ restored_credentials=tuple(restored_credentials),\\n+ live_discovered_model_count=snapshot.live_model_count,\\n+ catalog_model_count=len(snapshot.models),\\n+ eligible_model_count=len(snapshot.models),\\n+ last_known_good_model_count=snapshot.last_known_good_model_count,\\n+ selected_agent_ids=selected_ids,\\n+ enabled_agent_ids=enabled_ids,\\n+ durable_agent_pool=bool(agents_db),\\n+ catalog_backend=store.backend_name,\\n+ catalog_refresh_failure_count=snapshot.refresh_failure_count,\\n+ providers_with_errors=snapshot.providers_with_errors,\\n+ priced_model_count=priced_count,\\n+ )\\n+ except Exception:\\n+ try:\\n+ _restore_provider_credentials_atomically(previous_credentials)\\n+ except Exception as rollback_error:\\n+ raise ProviderBootstrapError(\\n+ \\\"provider bootstrap failed and credential rollback could not complete\\\"\\n+ ) from rollback_error\\n+ raise\\n+\\n+\\n+def main(argv: Sequence[str] | None = None) -> None:\\n+ \\\"\\\"\\\"Run trusted durable provider bootstrap and print secret-free evidence.\\\"\\\"\\\"\\n+ parser = argparse.ArgumentParser(\\n+ description=(\\n+ \\\"Register provider secrets, persist provider models, and refresh \\\"\\n+ \\\"the effective serving pool.\\\"\\n+ )\\n+ )\\n+ parser.add_argument(\\n+ \\\"--agents-db\\\",\\n+ default=os.environ.get(\\\"CONTEXTUAL_ORCHESTRATOR_AGENTS_DB\\\") or None,\\n+ )\\n+ parser.add_argument(\\\"--model-limit\\\", type=int, default=16)\\n+ parser.add_argument(\\n+ \\\"--allow-partial-credentials\\\",\\n+ action=\\\"store_true\\\",\\n+ help=\\\"Permit a subset of the fixed provider inventory (development only).\\\",\\n+ )\\n+ args = parser.parse_args(list(argv) if argv is not None else None)\\n+ report = bootstrap_provider_catalog_runtime(\\n+ environ=os.environ,\\n+ require_all_credentials=not args.allow_partial_credentials,\\n+ agents_db=args.agents_db,\\n+ model_limit=args.model_limit,\\n+ )\\n+ print(json.dumps(report.as_dict(), ensure_ascii=False, sort_keys=True))\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover - subprocess/CLI boundary\\n+ main()\" }, { \"sha\": \"39511e7ceb5798643e8ab9c043498534f4ed30de\", \"filename\": \"contextual_orchestrator/provider_catalog_store.py\", \"status\": \"added\", \"additions\": 634, \"deletions\": 0, \"changes\": 634, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fprovider_catalog_store.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/contextual_orchestrator%2Fprovider_catalog_store.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fprovider_catalog_store.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,634 @@\\n+\\\"\\\"\\\"Normalized durable provider-model catalog persistence.\\n+\\n+This module owns provider-account/model metadata persistence and last-known-good\\n+refresh behavior. It never performs network I/O and never stores credential\\n+values. Discovery transport remains in ``model_discovery``; runtime selection\\n+remains in the ordinary orchestrator.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from dataclasses import dataclass\\n+from datetime import datetime, timezone\\n+from decimal import Decimal\\n+import hashlib\\n+import math\\n+import re\\n+import threading\\n+import uuid\\n+from typing import Callable, Mapping, Protocol, Sequence\\n+\\n+from .model_discovery import DiscoveredModel, ProviderModelSource\\n+\\n+\\n+PROVIDER_CATALOG_SCHEMA_SQL = \\\"\\\"\\\"\\n+CREATE TABLE IF NOT EXISTS provider_account (\\n+ provider_account_id text PRIMARY KEY,\\n+ provider_name text NOT NULL,\\n+ credential_name text NOT NULL,\\n+ list_url text NOT NULL,\\n+ chat_base_url text NOT NULL,\\n+ auth_scheme text NOT NULL,\\n+ discovery_style text NOT NULL,\\n+ task_filter text NOT NULL,\\n+ enabled_flag boolean NOT NULL DEFAULT true,\\n+ created_at timestamptz NOT NULL DEFAULT now(),\\n+ updated_at timestamptz NOT NULL DEFAULT now(),\\n+ UNIQUE (provider_name, credential_name)\\n+);\\n+\\n+CREATE TABLE IF NOT EXISTS provider_model (\\n+ provider_model_id text PRIMARY KEY,\\n+ provider_account_id text NOT NULL\\n+ REFERENCES provider_account(provider_account_id) ON DELETE CASCADE,\\n+ model_name text NOT NULL,\\n+ prompt_price_per_1k numeric(20, 8),\\n+ completion_price_per_1k numeric(20, 8),\\n+ currency_code text NOT NULL,\\n+ serving_eligible_flag boolean NOT NULL DEFAULT false,\\n+ enabled_flag boolean NOT NULL DEFAULT true,\\n+ first_seen_at timestamptz NOT NULL,\\n+ last_seen_at timestamptz NOT NULL,\\n+ UNIQUE (provider_account_id, model_name)\\n+);\\n+\\n+CREATE TABLE IF NOT EXISTS model_serving_tag (\\n+ provider_model_id text NOT NULL\\n+ REFERENCES provider_model(provider_model_id) ON DELETE CASCADE,\\n+ tag_name text NOT NULL,\\n+ PRIMARY KEY (provider_model_id, tag_name)\\n+);\\n+\\n+CREATE TABLE IF NOT EXISTS catalog_refresh_run (\\n+ catalog_refresh_run_id text PRIMARY KEY,\\n+ provider_account_id text NOT NULL\\n+ REFERENCES provider_account(provider_account_id) ON DELETE CASCADE,\\n+ refresh_status text NOT NULL,\\n+ observed_model_count integer NOT NULL DEFAULT 0,\\n+ eligible_model_count integer NOT NULL DEFAULT 0,\\n+ error_code text,\\n+ started_at timestamptz NOT NULL,\\n+ finished_at timestamptz NOT NULL\\n+);\\n+\\n+CREATE INDEX IF NOT EXISTS provider_model_account_idx\\n+ ON provider_model (provider_account_id, enabled_flag, serving_eligible_flag);\\n+CREATE INDEX IF NOT EXISTS catalog_refresh_account_idx\\n+ ON catalog_refresh_run (provider_account_id, finished_at DESC);\\n+\\\"\\\"\\\"\\n+\\\"\\\"\\\"Third-normal-form schema for provider accounts, models, tags, and refreshes.\\\"\\\"\\\"\\n+\\n+\\n+class ProviderCatalogError(RuntimeError):\\n+ \\\"\\\"\\\"Raised when durable provider catalog metadata cannot be persisted or read.\\\"\\\"\\\"\\n+\\n+\\n+@dataclass(frozen=True)\\n+class CatalogRefreshEvidence:\\n+ \\\"\\\"\\\"Secret-free evidence for one provider-account catalog refresh.\\\"\\\"\\\"\\n+\\n+ provider_account_id: str\\n+ refresh_status: str\\n+ observed_model_count: int\\n+ eligible_model_count: int\\n+ error_code: str | None\\n+ started_at: datetime\\n+ finished_at: datetime\\n+\\n+\\n+class ProviderCatalogStore(Protocol):\\n+ \\\"\\\"\\\"Persistence boundary for provider model metadata and last-known-good rows.\\\"\\\"\\\"\\n+\\n+ @property\\n+ def backend_name(self) -> str:\\n+ \\\"\\\"\\\"Return a stable backend name for secret-free operator evidence.\\\"\\\"\\\"\\n+ ...\\n+\\n+ def record_success(\\n+ self,\\n+ source: ProviderModelSource,\\n+ models: Sequence[DiscoveredModel],\\n+ *,\\n+ eligible_model_ids: set[str],\\n+ serving_tags: Mapping[str, tuple[str, ...]],\\n+ ) -> None:\\n+ \\\"\\\"\\\"Replace one provider account's current catalog atomically.\\\"\\\"\\\"\\n+ ...\\n+\\n+ def record_failure(\\n+ self,\\n+ source: ProviderModelSource,\\n+ *,\\n+ error_code: str,\\n+ ) -> None:\\n+ \\\"\\\"\\\"Record failure without changing last-known-good enabled models.\\\"\\\"\\\"\\n+ ...\\n+\\n+ def serving_models(\\n+ self,\\n+ source: ProviderModelSource,\\n+ ) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Return enabled, serving-eligible last-known-good models.\\\"\\\"\\\"\\n+ ...\\n+\\n+ def refresh_evidence(self) -> tuple[CatalogRefreshEvidence, ...]:\\n+ \\\"\\\"\\\"Return refresh evidence in insertion order.\\\"\\\"\\\"\\n+ ...\\n+\\n+\\n+_SLUG_RE = re.compile(r\\\"[^a-z0-9]+\\\")\\n+_CURRENCY_RE = re.compile(r\\\"^[A-Z]{3}$\\\")\\n+_ALLOWED_REFRESH_ERROR_CODES = frozenset(\\n+ {\\\"provider_discovery_error\\\", \\\"empty_provider_catalog\\\", \\\"unknown_error\\\"}\\n+)\\n+\\n+\\n+def provider_account_id(source: ProviderModelSource) -> str:\\n+ \\\"\\\"\\\"Return a stable two-or-more-word snake-case provider account ID.\\\"\\\"\\\"\\n+ provider = _SLUG_RE.sub(\\\"_\\\", source.provider_name.casefold()).strip(\\\"_\\\")\\n+ credential = _SLUG_RE.sub(\\\"_\\\", source.credential_name.casefold()).strip(\\\"_\\\")\\n+ if not provider or not credential:\\n+ raise ProviderCatalogError(\\\"provider account identity is incomplete\\\")\\n+ return f\\\"{provider}_{credential}\\\"\\n+\\n+\\n+def provider_model_id(source: ProviderModelSource, model_name: str) -> str:\\n+ \\\"\\\"\\\"Return a stable opaque ID for one account-scoped model name.\\\"\\\"\\\"\\n+ normalized = model_name.strip()\\n+ if not normalized:\\n+ raise ProviderCatalogError(\\\"provider model name is empty\\\")\\n+ digest = hashlib.sha256(\\n+ f\\\"{provider_account_id(source)}\\\\0{normalized}\\\".encode(\\\"utf-8\\\")\\n+ ).hexdigest()\\n+ return f\\\"provider_model_{digest[:32]}\\\"\\n+\\n+\\n+def _now() -> datetime:\\n+ \\\"\\\"\\\"Return a timezone-aware UTC timestamp.\\\"\\\"\\\"\\n+ return datetime.now(timezone.utc)\\n+\\n+\\n+def _normalize_price(value: object) -> float | None:\\n+ \\\"\\\"\\\"Return one finite non-negative price, or ``None`` when unknown, underflowed, or overflowed.\\n+\\n+ Parses through ``Decimal`` first so a nonzero price that underflows to\\n+ ``0.0`` in float (e.g. a stray ``1e-10000``) is rejected as unknown\\n+ rather than silently accepted as a legitimate free price. A ``Decimal``\\n+ can still be finite while its ``float()`` conversion overflows to\\n+ ``inf`` (e.g. ``1e10000``), so ``math.isfinite`` is checked separately\\n+ on the converted value.\\n+ \\\"\\\"\\\"\\n+ if value is None or isinstance(value, bool):\\n+ return None\\n+ try:\\n+ decimal_value = Decimal(str(value))\\n+ number = float(decimal_value)\\n+ except (ArithmeticError, TypeError, ValueError):\\n+ return None\\n+ if (\\n+ not decimal_value.is_finite()\\n+ or not math.isfinite(number)\\n+ or decimal_value < 0\\n+ or (decimal_value != 0 and number == 0)\\n+ ):\\n+ return None\\n+ return number\\n+\\n+\\n+_UNKNOWN_CURRENCY = \\\"UNKNOWN\\\"\\n+\\n+\\n+def _normalize_currency(value: object) -> str:\\n+ \\\"\\\"\\\"Return an ISO-style three-letter currency code, or an explicit unknown marker.\\n+\\n+ An unrecognized currency must never collapse to ``USD`` by default: doing\\n+ so would let a priced model with an unverified currency rank as a\\n+ comparable USD cost. ``_UNKNOWN_CURRENCY`` deliberately fails\\n+ ``_currency_is_comparable`` against every real default currency.\\n+ \\\"\\\"\\\"\\n+ if not isinstance(value, str):\\n+ return _UNKNOWN_CURRENCY\\n+ normalized = value.strip().upper()\\n+ return normalized if _CURRENCY_RE.fullmatch(normalized) else _UNKNOWN_CURRENCY\\n+\\n+\\n+def _normalize_error_code(value: object) -> str:\\n+ \\\"\\\"\\\"Return one approved secret-free provider refresh failure code.\\\"\\\"\\\"\\n+ if not isinstance(value, str):\\n+ return \\\"unknown_error\\\"\\n+ normalized = value.strip().casefold()\\n+ return normalized if normalized in _ALLOWED_REFRESH_ERROR_CODES else \\\"unknown_error\\\"\\n+\\n+\\n+def _normalize_tags(tags: Sequence[str]) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Return deterministic, valid, duplicate-free serving tags.\\\"\\\"\\\"\\n+ normalized: list[str] = []\\n+ for raw in tags:\\n+ if not isinstance(raw, str):\\n+ continue\\n+ tag = raw.strip().casefold()\\n+ if not tag or not re.fullmatch(r\\\"[a-z][a-z0-9_]*\\\", tag):\\n+ continue\\n+ if tag not in normalized:\\n+ normalized.append(tag)\\n+ return tuple(normalized)\\n+\\n+\\n+def normalize_discovered_model(\\n+ source: ProviderModelSource,\\n+ model: DiscoveredModel,\\n+) -> DiscoveredModel:\\n+ \\\"\\\"\\\"Normalize one discovered row and enforce its provider-account identity.\\\"\\\"\\\"\\n+ name = model.model_id.strip() if isinstance(model.model_id, str) else \\\"\\\"\\n+ if not name:\\n+ raise ProviderCatalogError(\\\"provider model name is empty\\\")\\n+ if (\\n+ model.provider_name != source.provider_name\\n+ or model.credential_name != source.credential_name\\n+ ):\\n+ raise ProviderCatalogError(\\\"provider model belongs to a different account\\\")\\n+ return DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=name,\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ prompt_price_per_1k=_normalize_price(model.prompt_price_per_1k),\\n+ completion_price_per_1k=_normalize_price(\\n+ model.completion_price_per_1k\\n+ ),\\n+ currency_code=_normalize_currency(model.currency_code),\\n+ )\\n+\\n+\\n+def _deduplicate_models(\\n+ source: ProviderModelSource,\\n+ models: Sequence[DiscoveredModel],\\n+) -> dict[str, DiscoveredModel]:\\n+ \\\"\\\"\\\"Normalize and deterministically deduplicate account-scoped models.\\\"\\\"\\\"\\n+ result: dict[str, DiscoveredModel] = {}\\n+ for model in models:\\n+ normalized = normalize_discovered_model(source, model)\\n+ result[normalized.model_id] = normalized\\n+ return result\\n+\\n+\\n+class InMemoryProviderCatalogStore:\\n+ \\\"\\\"\\\"Thread-safe deterministic provider catalog for tests and standalone use.\\\"\\\"\\\"\\n+\\n+ def __init__(self) -> None:\\n+ self._accounts: dict[str, ProviderModelSource] = {}\\n+ self._models: dict[str, dict[str, DiscoveredModel]] = {}\\n+ self._eligible: dict[str, set[str]] = {}\\n+ self._tags: dict[tuple[str, str], tuple[str, ...]] = {}\\n+ self._refreshes: list[CatalogRefreshEvidence] = []\\n+ self._lock = threading.RLock()\\n+\\n+ @property\\n+ def backend_name(self) -> str:\\n+ \\\"\\\"\\\"Return the stable in-memory backend name.\\\"\\\"\\\"\\n+ return \\\"memory\\\"\\n+\\n+ def record_success(\\n+ self,\\n+ source: ProviderModelSource,\\n+ models: Sequence[DiscoveredModel],\\n+ *,\\n+ eligible_model_ids: set[str],\\n+ serving_tags: Mapping[str, tuple[str, ...]],\\n+ ) -> None:\\n+ \\\"\\\"\\\"Replace one in-memory account catalog.\\\"\\\"\\\"\\n+ normalized = _deduplicate_models(source, models)\\n+ if not normalized:\\n+ raise ProviderCatalogError(\\\"successful provider refresh cannot be empty\\\")\\n+ account_id = provider_account_id(source)\\n+ started_at = _now()\\n+ eligible = set(normalized).intersection(eligible_model_ids)\\n+ with self._lock:\\n+ self._accounts[account_id] = source\\n+ self._models[account_id] = normalized\\n+ self._eligible[account_id] = eligible\\n+ for key in [key for key in self._tags if key[0] == account_id]:\\n+ del self._tags[key]\\n+ for model_name in eligible:\\n+ self._tags[(account_id, model_name)] = _normalize_tags(\\n+ serving_tags.get(model_name, ())\\n+ )\\n+ self._refreshes.append(\\n+ CatalogRefreshEvidence(\\n+ account_id,\\n+ \\\"succeeded\\\",\\n+ len(normalized),\\n+ len(eligible),\\n+ None,\\n+ started_at,\\n+ _now(),\\n+ )\\n+ )\\n+\\n+ def record_failure(\\n+ self,\\n+ source: ProviderModelSource,\\n+ *,\\n+ error_code: str,\\n+ ) -> None:\\n+ \\\"\\\"\\\"Record a stable failure without mutating last-known-good models.\\\"\\\"\\\"\\n+ account_id = provider_account_id(source)\\n+ started_at = _now()\\n+ stable_code = _normalize_error_code(error_code)\\n+ with self._lock:\\n+ self._accounts[account_id] = source\\n+ self._refreshes.append(\\n+ CatalogRefreshEvidence(\\n+ account_id,\\n+ \\\"failed\\\",\\n+ 0,\\n+ 0,\\n+ stable_code,\\n+ started_at,\\n+ _now(),\\n+ )\\n+ )\\n+\\n+ def serving_models(\\n+ self,\\n+ source: ProviderModelSource,\\n+ ) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Return deterministic serving models for one account.\\\"\\\"\\\"\\n+ account_id = provider_account_id(source)\\n+ with self._lock:\\n+ models = self._models.get(account_id, {})\\n+ eligible = self._eligible.get(account_id, set())\\n+ return [models[name] for name in sorted(eligible) if name in models]\\n+\\n+ def serving_tags(\\n+ self,\\n+ source: ProviderModelSource,\\n+ model_name: str,\\n+ ) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Return persisted generic serving tags for one model.\\\"\\\"\\\"\\n+ with self._lock:\\n+ return self._tags.get((provider_account_id(source), model_name), ())\\n+\\n+ def refresh_evidence(self) -> tuple[CatalogRefreshEvidence, ...]:\\n+ \\\"\\\"\\\"Return immutable refresh evidence in insertion order.\\\"\\\"\\\"\\n+ with self._lock:\\n+ return tuple(self._refreshes)\\n+\\n+\\n+class PostgresProviderCatalogStore:\\n+ \\\"\\\"\\\"PostgreSQL provider catalog sharing the credential registry database.\\\"\\\"\\\"\\n+\\n+ def __init__(\\n+ self,\\n+ dsn: str,\\n+ *,\\n+ connection_factory: Callable[[], object] | None = None,\\n+ ) -> None:\\n+ if not isinstance(dsn, str) or not dsn.strip():\\n+ raise ProviderCatalogError(\\\"provider catalog requires a PostgreSQL DSN\\\")\\n+ self._dsn = dsn\\n+ self._connection_factory = connection_factory\\n+ self._schema_ready = False\\n+ self._schema_lock = threading.Lock()\\n+ self._evidence: list[CatalogRefreshEvidence] = []\\n+\\n+ @property\\n+ def backend_name(self) -> str:\\n+ \\\"\\\"\\\"Return the stable PostgreSQL backend name.\\\"\\\"\\\"\\n+ return \\\"postgres\\\"\\n+\\n+ def _connect(self):\\n+ \\\"\\\"\\\"Open one catalog connection through the injected or psycopg factory.\\\"\\\"\\\"\\n+ if self._connection_factory is not None:\\n+ return self._connection_factory()\\n+ try:\\n+ import psycopg\\n+ except ImportError as exc: # pragma: no cover - packaging boundary\\n+ raise ProviderCatalogError(\\n+ \\\"provider catalog requires contextual-orchestrator[db]\\\"\\n+ ) from exc\\n+ return psycopg.connect(self._dsn) # pragma: no cover - live database\\n+\\n+ def _ensure_schema(self, connection: object) -> None:\\n+ \\\"\\\"\\\"Create normalized catalog objects once per store instance.\\\"\\\"\\\"\\n+ if self._schema_ready:\\n+ return\\n+ with self._schema_lock:\\n+ if self._schema_ready:\\n+ return\\n+ with connection.cursor() as cursor:\\n+ cursor.execute(PROVIDER_CATALOG_SCHEMA_SQL)\\n+ connection.commit()\\n+ self._schema_ready = True\\n+\\n+ @staticmethod\\n+ def _upsert_account(cursor: object, source: ProviderModelSource) -> str:\\n+ \\\"\\\"\\\"Upsert one provider account without credential values.\\\"\\\"\\\"\\n+ account_id = provider_account_id(source)\\n+ cursor.execute(\\n+ \\\"INSERT INTO provider_account (\\\"\\n+ \\\"provider_account_id, provider_name, credential_name, list_url, \\\"\\n+ \\\"chat_base_url, auth_scheme, discovery_style, task_filter, \\\"\\n+ \\\"enabled_flag, created_at, updated_at\\\"\\n+ \\\") VALUES (%s, %s, %s, %s, %s, %s, %s, %s, true, now(), now()) \\\"\\n+ \\\"ON CONFLICT (provider_account_id) DO UPDATE SET \\\"\\n+ \\\"provider_name = EXCLUDED.provider_name, \\\"\\n+ \\\"credential_name = EXCLUDED.credential_name, \\\"\\n+ \\\"list_url = EXCLUDED.list_url, \\\"\\n+ \\\"chat_base_url = EXCLUDED.chat_base_url, \\\"\\n+ \\\"auth_scheme = EXCLUDED.auth_scheme, \\\"\\n+ \\\"discovery_style = EXCLUDED.discovery_style, \\\"\\n+ \\\"task_filter = EXCLUDED.task_filter, \\\"\\n+ \\\"enabled_flag = true, updated_at = now()\\\",\\n+ (\\n+ account_id,\\n+ source.provider_name,\\n+ source.credential_name,\\n+ source.list_url,\\n+ source.chat_base_url,\\n+ source.auth_scheme,\\n+ source.style,\\n+ source.task_filter,\\n+ ),\\n+ )\\n+ return account_id\\n+\\n+ def record_success(\\n+ self,\\n+ source: ProviderModelSource,\\n+ models: Sequence[DiscoveredModel],\\n+ *,\\n+ eligible_model_ids: set[str],\\n+ serving_tags: Mapping[str, tuple[str, ...]],\\n+ ) -> None:\\n+ \\\"\\\"\\\"Replace one PostgreSQL account catalog in a single transaction.\\\"\\\"\\\"\\n+ normalized = _deduplicate_models(source, models)\\n+ if not normalized:\\n+ raise ProviderCatalogError(\\\"successful provider refresh cannot be empty\\\")\\n+ started_at = _now()\\n+ eligible = set(normalized).intersection(eligible_model_ids)\\n+ with self._connect() as connection:\\n+ self._ensure_schema(connection)\\n+ with connection.cursor() as cursor:\\n+ account_id = self._upsert_account(cursor, source)\\n+ cursor.execute(\\n+ \\\"UPDATE provider_model SET enabled_flag = false \\\"\\n+ \\\"WHERE provider_account_id = %s\\\",\\n+ (account_id,),\\n+ )\\n+ cursor.execute(\\n+ \\\"DELETE FROM model_serving_tag WHERE provider_model_id IN (\\\"\\n+ \\\"SELECT provider_model_id FROM provider_model \\\"\\n+ \\\"WHERE provider_account_id = %s)\\\",\\n+ (account_id,),\\n+ )\\n+ for model_name, model in normalized.items():\\n+ model_row_id = provider_model_id(source, model_name)\\n+ cursor.execute(\\n+ \\\"INSERT INTO provider_model (\\\"\\n+ \\\"provider_model_id, provider_account_id, model_name, \\\"\\n+ \\\"prompt_price_per_1k, completion_price_per_1k, currency_code, \\\"\\n+ \\\"serving_eligible_flag, enabled_flag, first_seen_at, \\\"\\n+ \\\"last_seen_at\\\"\\n+ \\\") VALUES (%s, %s, %s, %s, %s, %s, %s, \\\"\\n+ \\\"true, %s, %s) \\\"\\n+ \\\"ON CONFLICT (provider_model_id) DO UPDATE SET \\\"\\n+ \\\"model_name = EXCLUDED.model_name, \\\"\\n+ \\\"prompt_price_per_1k = EXCLUDED.prompt_price_per_1k, \\\"\\n+ \\\"completion_price_per_1k = EXCLUDED.completion_price_per_1k, \\\"\\n+ \\\"currency_code = EXCLUDED.currency_code, \\\"\\n+ \\\"serving_eligible_flag = EXCLUDED.serving_eligible_flag, \\\"\\n+ \\\"enabled_flag = true, last_seen_at = EXCLUDED.last_seen_at\\\",\\n+ (\\n+ model_row_id,\\n+ account_id,\\n+ model_name,\\n+ model.prompt_price_per_1k,\\n+ model.completion_price_per_1k,\\n+ model.currency_code,\\n+ model_name in eligible,\\n+ started_at,\\n+ started_at,\\n+ ),\\n+ )\\n+ if model_name in eligible:\\n+ for tag in _normalize_tags(serving_tags.get(model_name, ())):\\n+ cursor.execute(\\n+ \\\"INSERT INTO model_serving_tag \\\"\\n+ \\\"(provider_model_id, tag_name) \\\"\\n+ \\\"VALUES (%s, %s) ON CONFLICT DO NOTHING\\\",\\n+ (model_row_id, tag),\\n+ )\\n+ finished_at = _now()\\n+ cursor.execute(\\n+ \\\"INSERT INTO catalog_refresh_run (\\\"\\n+ \\\"catalog_refresh_run_id, provider_account_id, refresh_status, \\\"\\n+ \\\"observed_model_count, eligible_model_count, error_code, \\\"\\n+ \\\"started_at, finished_at\\\"\\n+ \\\") VALUES (%s, %s, %s, %s, %s, %s, %s, %s)\\\",\\n+ (\\n+ f\\\"catalog_refresh_{uuid.uuid4().hex}\\\",\\n+ account_id,\\n+ \\\"succeeded\\\",\\n+ len(normalized),\\n+ len(eligible),\\n+ None,\\n+ started_at,\\n+ finished_at,\\n+ ),\\n+ )\\n+ connection.commit()\\n+ self._evidence.append(\\n+ CatalogRefreshEvidence(\\n+ provider_account_id(source),\\n+ \\\"succeeded\\\",\\n+ len(normalized),\\n+ len(eligible),\\n+ None,\\n+ started_at,\\n+ finished_at,\\n+ )\\n+ )\\n+\\n+ def record_failure(\\n+ self,\\n+ source: ProviderModelSource,\\n+ *,\\n+ error_code: str,\\n+ ) -> None:\\n+ \\\"\\\"\\\"Record a PostgreSQL failure without disabling prior models.\\\"\\\"\\\"\\n+ started_at = _now()\\n+ stable_code = _normalize_error_code(error_code)\\n+ with self._connect() as connection:\\n+ self._ensure_schema(connection)\\n+ with connection.cursor() as cursor:\\n+ account_id = self._upsert_account(cursor, source)\\n+ finished_at = _now()\\n+ cursor.execute(\\n+ \\\"INSERT INTO catalog_refresh_run (\\\"\\n+ \\\"catalog_refresh_run_id, provider_account_id, refresh_status, \\\"\\n+ \\\"observed_model_count, eligible_model_count, error_code, \\\"\\n+ \\\"started_at, finished_at\\\"\\n+ \\\") VALUES (%s, %s, %s, %s, %s, %s, %s, %s)\\\",\\n+ (\\n+ f\\\"catalog_refresh_{uuid.uuid4().hex}\\\",\\n+ account_id,\\n+ \\\"failed\\\",\\n+ 0,\\n+ 0,\\n+ stable_code,\\n+ started_at,\\n+ finished_at,\\n+ ),\\n+ )\\n+ connection.commit()\\n+ self._evidence.append(\\n+ CatalogRefreshEvidence(\\n+ provider_account_id(source),\\n+ \\\"failed\\\",\\n+ 0,\\n+ 0,\\n+ stable_code,\\n+ started_at,\\n+ finished_at,\\n+ )\\n+ )\\n+\\n+ def serving_models(\\n+ self,\\n+ source: ProviderModelSource,\\n+ ) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Read enabled last-known-good serving models for one account.\\\"\\\"\\\"\\n+ account_id = provider_account_id(source)\\n+ with self._connect() as connection:\\n+ self._ensure_schema(connection)\\n+ with connection.cursor() as cursor:\\n+ cursor.execute(\\n+ \\\"SELECT pm.model_name, pa.chat_base_url, pa.auth_scheme, \\\"\\n+ \\\"pm.prompt_price_per_1k, pm.completion_price_per_1k, \\\"\\n+ \\\"pm.currency_code FROM provider_model AS pm \\\"\\n+ \\\"JOIN provider_account AS pa ON pa.provider_account_id = pm.provider_account_id \\\"\\n+ \\\"WHERE pm.provider_account_id = %s \\\"\\n+ \\\"AND pm.enabled_flag = true AND pm.serving_eligible_flag = true \\\"\\n+ \\\"ORDER BY pm.model_name\\\",\\n+ (account_id,),\\n+ )\\n+ rows = cursor.fetchall()\\n+ return [\\n+ DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=row[0],\\n+ credential_name=source.credential_name,\\n+ chat_base_url=row[1],\\n+ auth_scheme=row[2],\\n+ prompt_price_per_1k=_normalize_price(row[3]),\\n+ completion_price_per_1k=_normalize_price(row[4]),\\n+ currency_code=_normalize_currency(row[5]),\\n+ )\\n+ for row in rows\\n+ ]\\n+\\n+ def refresh_evidence(self) -> tuple[CatalogRefreshEvidence, ...]:\\n+ \\\"\\\"\\\"Return evidence emitted by this store instance.\\\"\\\"\\\"\\n+ return tuple(self._evidence)\" }, { \"sha\": \"56c37beac22656936aa8ba54e7623365e9a1278b\", \"filename\": \"docs/database_design.sql\", \"status\": \"modified\", \"additions\": 64, \"deletions\": 0, \"changes\": 64, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdatabase_design.sql\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdatabase_design.sql\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdatabase_design.sql?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -69,6 +69,64 @@ create table audit_event (\\n created_at timestamptz not null default now()\\n );\\n \\n+create table provider_credentials (\\n+ credential_name text primary key,\\n+ encrypted_value bytea not null,\\n+ updated_at timestamptz not null default now()\\n+);\\n+\\n+create table provider_account (\\n+ provider_account_id text primary key,\\n+ provider_name text not null,\\n+ -- Credential rollback deliberately stays independent of catalog rows so a\\n+ -- failed candidate promotion cannot delete last-known-good model metadata.\\n+ -- The runtime catalog DDL uses the same application-managed relationship.\\n+ credential_name text not null,\\n+ list_url text not null,\\n+ chat_base_url text not null,\\n+ auth_scheme text not null,\\n+ discovery_style text not null,\\n+ task_filter text not null,\\n+ enabled_flag boolean not null default true,\\n+ created_at timestamptz not null default now(),\\n+ updated_at timestamptz not null default now(),\\n+ unique (provider_name, credential_name)\\n+);\\n+\\n+create table provider_model (\\n+ provider_model_id text primary key,\\n+ provider_account_id text not null\\n+ references provider_account(provider_account_id) on delete cascade,\\n+ model_name text not null,\\n+ prompt_price_per_1k numeric(20, 8),\\n+ completion_price_per_1k numeric(20, 8),\\n+ currency_code text not null,\\n+ serving_eligible_flag boolean not null default false,\\n+ enabled_flag boolean not null default true,\\n+ first_seen_at timestamptz not null,\\n+ last_seen_at timestamptz not null,\\n+ unique (provider_account_id, model_name)\\n+);\\n+\\n+create table model_serving_tag (\\n+ provider_model_id text not null\\n+ references provider_model(provider_model_id) on delete cascade,\\n+ tag_name text not null,\\n+ primary key (provider_model_id, tag_name)\\n+);\\n+\\n+create table catalog_refresh_run (\\n+ catalog_refresh_run_id text primary key,\\n+ provider_account_id text not null\\n+ references provider_account(provider_account_id) on delete cascade,\\n+ refresh_status text not null,\\n+ observed_model_count integer not null default 0,\\n+ eligible_model_count integer not null default 0,\\n+ error_code text,\\n+ started_at timestamptz not null,\\n+ finished_at timestamptz not null\\n+);\\n+\\n create index workflow_run_retention_idx\\n on workflow_run (retention_expires_at)\\n where deleted_at is null;\\n@@ -81,6 +139,12 @@ create index audit_event_retention_idx\\n on audit_event (retention_expires_at)\\n where deleted_at is null;\\n \\n+create index provider_model_account_idx\\n+ on provider_model (provider_account_id, enabled_flag, serving_eligible_flag);\\n+\\n+create index catalog_refresh_account_idx\\n+ on catalog_refresh_run (provider_account_id, finished_at desc);\\n+\\n create view workflow_run_safe_view as\\n select\\n workflow_run_id,\" }, { \"sha\": \"383868be62f1d2e560651614d6ac60ee77d6b7e4\", \"filename\": \"docs/doctoring/current-main-provider-bootstrap.md\", \"status\": \"added\", \"additions\": 119, \"deletions\": 0, \"changes\": 119, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdoctoring%2Fcurrent-main-provider-bootstrap.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdoctoring%2Fcurrent-main-provider-bootstrap.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdoctoring%2Fcurrent-main-provider-bootstrap.md?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,119 @@\\n+# Current-main provider bootstrap\\n+\\n+## Decision\\n+\\n+The durable catalog decision is recorded in\\n+[`ADR 0015`](../planning/adrs/0015-durable-provider-catalog.md), including the\\n+third-normal-form dependency boundary and the last-known-good refresh contract.\\n+\\n+Contextual Orchestrator treats the five organization provider credentials as one\\n+trusted bootstrap inventory:\\n+\\n+- `NVIDIA_NIM_API_KEY`\\n+- `NVIDIA_NIM_API_KEY_SUB`\\n+- `BYTEZ_API_KEY`\\n+- `OPENROUTER_API_KEY`\\n+- `OPENAI_API_KEY`\\n+\\n+GitHub Actions secrets are transport into a one-shot bootstrap process, not the\\n+runtime credential source. Production bootstrap requires the PostgreSQL credential\\n+backend so values are stored encrypted at rest through the existing pgcrypto\\n+registry. Runtime model discovery resolves credential names through\\n+`get_credential()` only.\\n+\\n+## Failure contract\\n+\\n+Production bootstrap fails closed when any fixed credential is missing, when the\\n+configured credential backend is not atomic, or when no usable provider model can\\n+be discovered. A provider-local discovery exception does not erase models returned\\n+by other providers; the report contains only stable provider names and counts, never\\n+raw exception strings or credential values.\\n+\\n+`registered_credentials` is the post-rollback durable inventory, not merely the\\n+set of candidate names received from Actions. If a first-ever candidate key is\\n+reverted after a failed provider refresh, that name is omitted from the report\\n+and the hourly workflow fails its complete-inventory gate. Existing keys that\\n+are restored remain listed, so a transient provider outage can preserve\\n+last-known-good serving without falsely claiming that a missing key is durable.\\n+\\n+A successful generic `/models` response is not itself evidence that every row can\\n+serve Chat Completions. OpenAI-compatible registries may mix chat models with\\n+embeddings, rerankers, speech, image generation, moderation, safety, or realtime\\n+transports. The bootstrap therefore applies a conservative negative compatibility\\n+filter before selection and reports both:\\n+\\n+- `discovered_model_count`: every syntactically valid catalog row; and\\n+- `eligible_model_count`: rows that are not clearly a non-chat transport.\\n+\\n+If no compatible row remains, bootstrap fails closed instead of activating the\\n+cheapest incompatible model. Surviving rows receive only generic serving tags:\\n+`discovered`, `chat`, `worker`, `writing`, and `synthesizer`. The bootstrap never\\n+infers reasoning, verification, coding, vision, or provider-native effort support\\n+from a model name. Those capabilities require explicit provider/catalog evidence or\\n+measured evaluation and are negotiated by the ordinary runtime policy.\\n+\\n+The bootstrap pool is provider-diverse before it is cost-ordered. Missing price is\\n+`unknown`, not zero. This avoids treating a provider such as Bytez, whose public\\n+catalog may use a non-token billing unit, as a fabricated free route.\\n+\\n+Candidate selection and durable serving activation are separate claims:\\n+\\n+- `selected_agent_ids` records the bounded chat candidates produced by discovery\\n+ and selection;\\n+- `enabled_agent_ids` is populated only when an explicit durable `--agents-db`\\n+ is supplied and the selected agents are confirmed active in that pool; and\\n+- `durable_agent_pool` states whether the activation claim is backed by a\\n+ persistent agent-pool database.\\n+\\n+When a durable pool is refreshed, the bootstrap tombstones its synthetic seed and\\n+previously discovered agents that are absent from the current bounded selection.\\n+Operator-managed agents are preserved. This prevents retired, withdrawn, or newly\\n+classified non-chat provider models from continuing to receive traffic after a\\n+later discovery run.\\n+\\n+## Operational workflow\\n+\\n+`.github/workflows/provider-catalog-sync.yml` runs hourly on protected `main` and may\\n+also be dispatched manually. It is intentionally absent from pull-request secret\\n+execution. The production environment must provide:\\n+\\n+- the five provider secrets above;\\n+- `CONTEXTUAL_ORCHESTRATOR_KV_DSN`; and\\n+- `CONTEXTUAL_ORCHESTRATOR_KV_PASSPHRASE`.\\n+\\n+The GitHub-hosted workflow has an ephemeral filesystem. It therefore registers the\\n+five credentials in the durable PostgreSQL KV and verifies discovery,\\n+`eligible_model_count`, and `selected_agent_ids`; it does not claim durable\\n+agent-pool activation. A long-running service may either use the ordinary KV-backed\\n+startup discovery path or invoke this bootstrap with a persistent `--agents-db`\\n+under its own deployment boundary.\\n+\\n+The workflow verifies that all five credential names were registered, at least one\\n+model was discovered, at least one chat-compatible model survived classification, a\\n+bounded serving candidate set was produced, and no exact provider secret appears in\\n+the emitted report.\\n+\\n+## Research and standards grounding\\n+\\n+The automatic pool remains a routing input rather than an unsupported claim that a\\n+single cheapest model is universally best. Quality/performance selection remains in\\n+the orchestrator's paper-grounded routing and orchestration layer; this bootstrap\\n+only establishes a compatible candidate set and failure isolation.\\n+\\n+National Institute of Standards and Technology. (2020). *Security and privacy\\n+controls for information systems and organizations* (NIST Special Publication\\n+800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5\\n+\\n+National Institute of Standards and Technology. (2024). *Artificial intelligence\\n+risk management framework: Generative artificial intelligence profile* (NIST AI\\n+600-1). https://doi.org/10.6028/NIST.AI.600-1\\n+\\n+Tang, Y., et al. (2026). *Sakana Fugu technical report*. Sakana AI.\\n+\\n+Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025).\\n+*Trinity: An evolved LLM coordinator* (arXiv:2512.04695).\\n+https://doi.org/10.48550/arXiv.2512.04695\\n+\\n+Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025).\\n+*Learning to orchestrate agents in natural language with the Conductor*\\n+(arXiv:2512.04388). https://doi.org/10.48550/arXiv.2512.04388\" }, { \"sha\": \"0251375d4bd4ad91ac23995d68c1fd6b300b0a88\", \"filename\": \"docs/doctoring/embedding-chat-capability-isolation.md\", \"status\": \"added\", \"additions\": 133, \"deletions\": 0, \"changes\": 133, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdoctoring%2Fembedding-chat-capability-isolation.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdoctoring%2Fembedding-chat-capability-isolation.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdoctoring%2Fembedding-chat-capability-isolation.md?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,133 @@\\n+# Embedding-to-chat capability isolation incident\\n+\\n+**Status:** Accepted incident decision\\n+**Date:** 2026-08-20\\n+**Affected consumer:** LineageWeave buyer-surface stack around PR #260\\n+\\n+## Incident\\n+\\n+A conducted workflow reached the final contextual-orchestrator synthesizer with\\n+`model_group=text-embedding-3-large` and deployment\\n+`azure/text-embedding-3-large`. The gateway rejected the chat operation as\\n+unsupported. Its configured fallback map contained chat-generation model groups,\\n+but no fallback attached to the embedding group.\\n+\\n+The missing fallback was a symptom, not the causal defect. An embedding deployment\\n+had already crossed the chat-agent capability boundary and become eligible for a\\n+worker role.\\n+\\n+## Causal boundary\\n+\\n+Provider-compatible `/models` registries can contain multiple endpoint families.\\n+The original discovery parser accepted every non-empty model identifier and exposed\\n+it to agent creation, price selection, and durable pool synchronization. A catalog\\n+row naming an embedding deployment could therefore be scored for thinker, worker,\\n+verifier, or synthesizer work even though its serving endpoint accepts embedding\\n+input rather than chat messages.\\n+\\n+The first incident fix closed discovery and price-routing boundaries, but further\\n+root-cause tracing showed an already-persisted incompatible `ModelAgent` could still\\n+survive that filter. The runtime ranking path, generated workflow assignment,\\n+cross-agent failover, readiness probe, streaming path, and direct\\n+`ModelClient.chat()` path previously trusted the persisted model identifier. That\\n+stale-state path is sufficient to reproduce the same unsupported Azure chat\\n+operation after a process restart or durable bootstrap.\\n+\\n+OpenAI documents `text-embedding-3-large` under the embeddings endpoint, separately\\n+from models supported by chat completions. Microsoft likewise demonstrates it with\\n+`client.embeddings.create`, not `client.chat.completions.create`. LiteLLM exposes\\n+chat, responses, embeddings, image, audio, rerank, and other endpoint families as\\n+distinct operations. A router fallback can choose another deployment for the same\\n+operation; it cannot make an embedding deployment execute a chat operation.\\n+\\n+## Decision\\n+\\n+Chat transport compatibility and general agent-role eligibility are separate\\n+shared runtime invariants. A provider may expose an audio-capable model or a\\n+policy classifier through Chat Completions while that model remains unsuitable\\n+for ordinary thinker, worker, verifier, or synthesizer work.\\n+\\n+1. Normalize provider prefixes and common separators in model identifiers.\\n+2. At the transport boundary, reject identifiers that clearly advertise embedding,\\n+ reranking, transcription, moderation-endpoint, image-generation, realtime, or\\n+ speech-only semantics.\\n+3. Keep provider-documented audio and policy-classifier models transport-compatible\\n+ when they are served through Chat Completions.\\n+4. At discovery and ordinary orchestration-role boundaries, additionally exclude\\n+ explicit guard, safety, and NemoGuard policy classifiers.\\n+5. Apply the general-role guard while parsing both OpenAI-compatible and Bytez\\n+ catalogs and before converting, pricing, or cost-selecting a discovery record.\\n+6. Remove stale ineligible agents from thinker, worker, verifier, and synthesizer\\n+ ranking even if a durable configuration still contains them.\\n+7. Reselect a generated workflow step that explicitly names a stale ineligible\\n+ agent and omit such agents from planner inventory.\\n+8. Remove ineligible agents from cross-agent failover candidates.\\n+9. Apply the transport guard at `ModelClient.chat()`, `stream_chat()`, and\\n+ readiness probing before mock or network transport.\\n+10. Fail closed when no general chat agent remains.\\n+11. Leave unknown identifiers eligible without fabricating reasoning, tool, vision,\\n+ or verification capabilities from their names.\\n+\\n+This is deliberately a conservative negative filter. A future capability registry\\n+may replace name-based exclusion with authenticated provider metadata, measured\\n+endpoint probes, and separate endpoint-specific pools. Until that evidence exists,\\n+a clearly incompatible model fails closed at transport boundaries and a clearly\\n+specialized policy model fails closed at general-role boundaries.\\n+\\n+## Rejected response\\n+\\n+Adding `text-embedding-3-large` to a chat fallback map is rejected. It would retain\\n+the invalid primary assignment and merely hide it when a fallback happened to be\\n+available. Repeated provider retries are also rejected because the request is\\n+structurally unsupported, not transiently unavailable.\\n+\\n+## Residual operational action\\n+\\n+Runtime containment means an already-persisted embedding agent can no longer win\\n+chat selection or failover while stale data is being cleaned up. Durable state must\\n+still converge to the correct exact set: the provider-bootstrap slice owns stale\\n+discovered-agent withdrawal and now imports the same shared classifier as the\\n+runtime, with a policy-matrix regression test covering image, embedding, safety,\\n+audio, and ordinary chat identifiers.\\n+Runtime rejection is defense in depth, not a substitute for deleting invalid\\n+persistent configuration.\\n+\\n+## Verification evidence\\n+\\n+`tests/test_chat_model_capability_isolation.py` reproduces the exact Azure model ID\\n+and provider/separator aliases. Together with\\n+`tests/test_chat_capability_unknown_identifiers.py`,\\n+`tests/test_chat_transport_role_separation.py`, and\\n+`tests/test_chat_passthrough_capability_isolation.py`, it verifies:\\n+\\n+- OpenAI-compatible and Bytez catalog filtering;\\n+- malformed and prefix-only identifier handling;\\n+- agent-conversion rejection;\\n+- exclusion from the price book and cheapest-agent selection;\\n+- exclusion of a high-priority stale embedding agent from synthesizer selection;\\n+- fail-closed behavior when the persisted pool contains only non-chat agents;\\n+- generated-plan reassignment away from a stale embedding agent;\\n+- exclusion from cross-agent failover;\\n+- direct and streaming `ModelClient` rejection before transport;\\n+- readiness failure with a stable non-chat code before provider access;\\n+- planner inventory and generated-plan isolation;\\n+- distinction between chat-served audio/policy models and general agent roles.\\n+- conservative unknown-identifier handling, including unrelated `vanguard` names;\\n+- endpoint-family exclusions for image-generation (`dall-e`), CLIP, and SigLIP;\\n+- normalized `/v1/responses` passthrough and pre-transport rejection of embedding models.\\n+\\n+## References\\n+\\n+BerriAI. (n.d.). *LiteLLM: Call 100+ LLMs using the OpenAI input/output format*.\\n+Retrieved August 20, 2026, from https://docs.litellm.ai/\\n+\\n+Microsoft. (n.d.). *How to switch between OpenAI and Azure OpenAI endpoints*.\\n+Microsoft Learn. Retrieved August 20, 2026, from\\n+https://learn.microsoft.com/en-us/azure/developer/ai/how-to/switching-endpoints\\n+\\n+OpenAI. (n.d.). *Data controls in the OpenAI platform: Default usage policies by\\n+endpoint*. Retrieved August 20, 2026, from\\n+https://platform.openai.com/docs/models/default-usage-policies-by-endpoint\\n+\\n+OpenAI. (n.d.). *GPT-audio model*. Retrieved August 20, 2026, from\\n+https://developers.openai.com/api/docs/models/gpt-audio\" }, { \"sha\": \"08c20b1eb3de1baaa8fa306bbc2ecc45397efc42\", \"filename\": \"docs/doctoring/provider-diverse-discovery-routing.md\", \"status\": \"added\", \"additions\": 49, \"deletions\": 0, \"changes\": 49, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdoctoring%2Fprovider-diverse-discovery-routing.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fdoctoring%2Fprovider-diverse-discovery-routing.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdoctoring%2Fprovider-diverse-discovery-routing.md?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,49 @@\\n+---\\n+title: \\\"Provider-diverse discovery and cost-honest failover routing\\\"\\n+status: \\\"implemented\\\"\\n+date: \\\"2026-08-21\\\"\\n+scope: \\\"PR #770\\\"\\n+---\\n+\\n+# Provider-diverse discovery and cost-honest failover routing\\n+\\n+## Decision\\n+\\n+PR #770 makes model discovery fail closed for invalid catalog rows (a price\\n+that is negative, non-finite, or a nonzero value that underflows to zero),\\n+retains eligible candidates that simply have no reported price as an\\n+explicit unknown-cost fallback, and selects a provider-diverse bootstrap\\n+pool before ordinary chat routing. The selector is deterministic eligibility\\n+and cost accounting; it is not a learned answer-quality judge and does not\\n+claim to reproduce the learning systems in the cited work.\\n+\\n+## Research-to-code mapping\\n+\\n+| Implementation boundary | Evidence-informed reason | Acceptance evidence |\\n+| --- | --- | --- |\\n+| Reject malformed, negative, or non-finite price rows | A cost-aware router must not treat missing or invalid evidence as zero cost. | Discovery and persisted-price tests reject the row before selection. |\\n+| Keep unknown-price candidates only as an explicit fallback | Cost optimization must remain honest when price evidence is incomplete. | Selection tests never rank an unknown price above a valid priced candidate. |\\n+| Prefer distinct providers in the bootstrap pool | A gateway needs an upstream failover set rather than several aliases for one provider. | Provider-diversity tests assert the configured pool spans available providers. |\\n+| Leave quality judgment to evaluation/review policy | Routing signals and answer-quality judgment have different failure modes. | Existing model-judge and fail-closed routing tests remain the quality boundary. |\\n+\\n+The routing papers and OA PDFs are already committed in the prerequisite\\n+stack base under `docs/papers/` (`routellm-routing-2406.18665.pdf`,\\n+`hybrid-llm-query-routing-2404.14618.pdf`, and\\n+`frugalgpt-cost-2305.05176.pdf`). This doctoring record makes their relevance\\n+to the exact discovery selector explicit instead of treating inherited files\\n+as incidental documentation.\\n+\\n+## APA 7 references\\n+\\n+Chen, L., Zaharia, M., & Zou, J. (2023). *FrugalGPT: How to use large\\n+language models while reducing cost and improving performance*. arXiv.\\n+https://arxiv.org/abs/2305.05176\\n+\\n+Ding, D., Mallick, A., Wang, C., Sim, R., Mukherjee, S., Rühle, V.,\\n+Lakshmanan, L. V. S., & Awadallah, A. H. (2024). *Hybrid LLM:\\n+Cost-efficient and quality-aware query routing*. International Conference on\\n+Learning Representations. https://arxiv.org/abs/2404.14618\\n+\\n+Ong, I., Almahairi, A., Wu, V., Chiang, W.-L., Wu, T., Gonzalez, J. E.,\\n+Kadous, M. W., & Stoica, I. (2024). *RouteLLM: Learning to route LLMs with\\n+preference data*. arXiv. https://arxiv.org/abs/2406.18665\" }, { \"sha\": \"98e3789821249149eca5dc11fe9ce7ff6e9b3e5f\", \"filename\": \"docs/planning/adrs/0015-durable-provider-catalog.md\", \"status\": \"added\", \"additions\": 93, \"deletions\": 0, \"changes\": 93, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fplanning%2Fadrs%2F0015-durable-provider-catalog.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fplanning%2Fadrs%2F0015-durable-provider-catalog.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0015-durable-provider-catalog.md?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,93 @@\\n+---\\n+id: \\\"0015\\\"\\n+title: \\\"Durable provider catalog and last-known-good composition\\\"\\n+status: accepted\\n+proposed_date: \\\"2026-08-20\\\"\\n+accepted_date: \\\"2026-08-22\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"NIST SP 800-53 Rev. 5\\\"\\n+ - \\\"NIST AI 600-1\\\"\\n+informed:\\n+ - \\\"LineageWeave\\\"\\n+ - \\\"fast-mlsirm\\\"\\n+ - \\\"contributors\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/provider_bootstrap.py\\\"\\n+ - \\\"contextual_orchestrator/provider_catalog_bootstrap.py\\\"\\n+ - \\\"contextual_orchestrator/provider_catalog_store.py\\\"\\n+ - \\\".github/workflows/provider-catalog-sync.yml\\\"\\n+supersedes: null\\n+superseded-by: null\\n+related:\\n+ - path: \\\"docs/planning/adrs/0012-gateway-only-provider-contract.md\\\"\\n+ relation: depends-on\\n+ - path: \\\"docs/planning/adrs/0014-gateway-owned-model-selection.md\\\"\\n+ relation: extends\\n+effort: L\\n+---\\n+\\n+# Durable provider catalog and last-known-good composition\\n+\\n+## Context\\n+\\n+Registering the five organization provider secrets in PostgreSQL is necessary\\n+but not sufficient. A production process must also retain discovered provider\\n+accounts and models so a transient catalog outage does not erase the serving\\n+pool, and operators must distinguish live discovery from last-known-good\\n+metadata. GitHub-hosted scheduled jobs have ephemeral filesystems, so SQLite\\n+cannot be the authority for this catalog.\\n+\\n+## Decision\\n+\\n+Use a third-normal-form PostgreSQL catalog colocated with the encrypted\\n+credential registry. The authority contains four two-or-more-word\\n+`snake_case` objects:\\n+\\n+- `provider_account`: provider endpoint and credential name, never the value;\\n+- `provider_model`: account-scoped model identity, known prices, compatibility\\n+ and lifecycle state; endpoint and authentication fields are joined from its\\n+ owning account;\\n+- `model_serving_tag`: generic serving tags as a separate many-to-many relation;\\n+- `catalog_refresh_run`: provider-local success/failure evidence.\\n+\\n+A successful non-empty provider refresh atomically replaces that provider\\n+account's enabled current set. A failed or empty/malformed refresh records only\\n+an allowlisted stable error code and preserves the account's last-known-good\\n+models. Successful discovery of an authoritative non-chat-only catalog may\\n+withdraw earlier chat rows.\\n+\\n+Model names are used only for a conservative negative compatibility filter that\\n+excludes obvious embedding, reranking, speech, image, moderation, safety, and\\n+realtime transports. They are never used to infer reasoning, verification,\\n+coding, vision, or provider-native effort capabilities. Those require explicit\\n+catalog or measured evidence under the gateway-owned policy.\\n+\\n+## Consequences\\n+\\n+- Credentials and model metadata are durable but remain separated.\\n+- NVIDIA primary and secondary keys are independent provider accounts.\\n+- One provider outage does not erase other providers or its own last-known-good set.\\n+- Unknown price remains unknown rather than becoming fabricated zero cost.\\n+- The protected hourly workflow can persist catalog metadata without claiming\\n+ that its ephemeral runner has activated a durable agent-pool database.\\n+- Long-running deployments may separately synchronize selected catalog rows into\\n+ a persistent agent pool.\\n+\\n+## Verification\\n+\\n+The merge gate covers normalized DDL, secret-column absence, provider-account\\n+isolation, parameterized PostgreSQL statements, last-known-good retention,\\n+withdrawal after authoritative success, non-chat filtering, secret-free\\n+evidence, and end-to-end recovery when one provider fails.\\n+\\n+## References\\n+\\n+National Institute of Standards and Technology. (2020). *Security and privacy\\n+controls for information systems and organizations* (NIST Special Publication\\n+800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5\\n+\\n+National Institute of Standards and Technology. (2024). *Artificial\\n+intelligence risk management framework: Generative artificial intelligence\\n+profile* (NIST AI 600-1). https://doi.org/10.6028/NIST.AI.600-1\" }, { \"sha\": \"0f9fd25386195533e85fdcc66a4a22718c4a7c04\", \"filename\": \"docs/provider_catalog_database.sql\", \"status\": \"added\", \"additions\": 53, \"deletions\": 0, \"changes\": 53, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fprovider_catalog_database.sql\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/docs%2Fprovider_catalog_database.sql\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fprovider_catalog_database.sql?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,53 @@\\n+CREATE TABLE IF NOT EXISTS provider_account (\\n+ provider_account_id text PRIMARY KEY,\\n+ provider_name text NOT NULL,\\n+ credential_name text NOT NULL,\\n+ list_url text NOT NULL,\\n+ chat_base_url text NOT NULL,\\n+ auth_scheme text NOT NULL,\\n+ discovery_style text NOT NULL,\\n+ task_filter text NOT NULL,\\n+ enabled_flag boolean NOT NULL DEFAULT true,\\n+ created_at timestamptz NOT NULL DEFAULT now(),\\n+ updated_at timestamptz NOT NULL DEFAULT now(),\\n+ UNIQUE (provider_name, credential_name)\\n+);\\n+\\n+CREATE TABLE IF NOT EXISTS provider_model (\\n+ provider_model_id text PRIMARY KEY,\\n+ provider_account_id text NOT NULL\\n+ REFERENCES provider_account(provider_account_id) ON DELETE CASCADE,\\n+ model_name text NOT NULL,\\n+ prompt_price_per_1k numeric(20, 8),\\n+ completion_price_per_1k numeric(20, 8),\\n+ currency_code text NOT NULL,\\n+ serving_eligible_flag boolean NOT NULL DEFAULT false,\\n+ enabled_flag boolean NOT NULL DEFAULT true,\\n+ first_seen_at timestamptz NOT NULL,\\n+ last_seen_at timestamptz NOT NULL,\\n+ UNIQUE (provider_account_id, model_name)\\n+);\\n+\\n+CREATE TABLE IF NOT EXISTS model_serving_tag (\\n+ provider_model_id text NOT NULL\\n+ REFERENCES provider_model(provider_model_id) ON DELETE CASCADE,\\n+ tag_name text NOT NULL,\\n+ PRIMARY KEY (provider_model_id, tag_name)\\n+);\\n+\\n+CREATE TABLE IF NOT EXISTS catalog_refresh_run (\\n+ catalog_refresh_run_id text PRIMARY KEY,\\n+ provider_account_id text NOT NULL\\n+ REFERENCES provider_account(provider_account_id) ON DELETE CASCADE,\\n+ refresh_status text NOT NULL,\\n+ observed_model_count integer NOT NULL DEFAULT 0,\\n+ eligible_model_count integer NOT NULL DEFAULT 0,\\n+ error_code text,\\n+ started_at timestamptz NOT NULL,\\n+ finished_at timestamptz NOT NULL\\n+);\\n+\\n+CREATE INDEX IF NOT EXISTS provider_model_account_idx\\n+ ON provider_model (provider_account_id, enabled_flag, serving_eligible_flag);\\n+CREATE INDEX IF NOT EXISTS catalog_refresh_account_idx\\n+ ON catalog_refresh_run (provider_account_id, finished_at DESC);\" }, { \"sha\": \"d9e72b6ca5738f5f3f4005cd3ca767157f610097\", \"filename\": \"tests/test_chat_capability_unknown_identifiers.py\", \"status\": \"added\", \"additions\": 47, \"deletions\": 0, \"changes\": 47, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_capability_unknown_identifiers.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_capability_unknown_identifiers.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_capability_unknown_identifiers.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,47 @@\\n+\\\"\\\"\\\"Regressions for conservative treatment of unknown model identifiers.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import sys\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator.chat_capability import ( # noqa: E402\\n+ is_chat_compatible_model_id,\\n+ is_general_chat_agent_model_id,\\n+)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"vendor/vanguard-7b\\\",\\n+ \\\"vendor/vanguard-instruct\\\",\\n+ ],\\n+)\\n+def test_unknown_names_that_merely_end_with_guard_remain_eligible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Do not fabricate a policy-classifier capability from an unrelated word suffix.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id)\\n+ assert is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"meta-llama/llama-guard-4-12b\\\",\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-topic-control\\\",\\n+ \\\"google/shieldgemma-2b-it\\\",\\n+ ],\\n+)\\n+def test_explicit_policy_classifier_markers_remain_role_ineligible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Keep exact guard, safety, and NemoGuard markers out of general synthesis roles.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id)\\n+ assert not is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"249240e247b7b7aa27bd0e02c237c31fdbef896d\", \"filename\": \"tests/test_chat_model_capability_isolation.py\", \"status\": \"added\", \"additions\": 391, \"deletions\": 0, \"changes\": 391, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_model_capability_isolation.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_model_capability_isolation.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_model_capability_isolation.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,391 @@\\n+\\\"\\\"\\\"Regression coverage for isolating non-chat models from chat agent discovery.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import json\\n+import sys\\n+from pathlib import Path\\n+from unittest.mock import patch\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator.chat_capability import ( # noqa: E402\\n+ is_chat_compatible_model_id,\\n+)\\n+from contextual_orchestrator.credentials import ( # noqa: E402\\n+ InMemoryCredentialBackend,\\n+ register_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.cost_ledger import PriceBook, PriceEntry # noqa: E402\\n+from contextual_orchestrator.kv_config import InMemoryConfigStore # noqa: E402\\n+from contextual_orchestrator.model_discovery import ( # noqa: E402\\n+ DiscoveredModel,\\n+ ProviderModelSource,\\n+ agent_from_discovered,\\n+ discover_provider_models,\\n+ refresh_price_book,\\n+ select_cheapest_discovered_agent,\\n+ select_top_n_cheapest_discovered_agents,\\n+)\\n+from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n+\\n+\\n+class _Response:\\n+ \\\"\\\"\\\"Small context-managed HTTP response used by the offline regression.\\\"\\\"\\\"\\n+\\n+ def __init__(self, payload: dict[str, object]) -> None:\\n+ self._body = json.dumps(payload).encode(\\\"utf-8\\\")\\n+\\n+ def __enter__(self) -> \\\"_Response\\\":\\n+ return self\\n+\\n+ def __exit__(self, *_args: object) -> bool:\\n+ return False\\n+\\n+ def read(self, _size: int = -1) -> bytes:\\n+ return self._body\\n+\\n+\\n+@pytest.fixture(autouse=True)\\n+def _fresh_credential_backend():\\n+ \\\"\\\"\\\"Keep the provider credential registry isolated between tests.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ yield\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+def _model(model_id: str, *, priced: bool = False) -> DiscoveredModel:\\n+ \\\"\\\"\\\"Build one synthetic discovered model for capability-boundary tests.\\\"\\\"\\\"\\n+ return DiscoveredModel(\\n+ provider_name=\\\"enterprise_gateway\\\",\\n+ model_id=model_id,\\n+ credential_name=\\\"GATEWAY_API_KEY\\\",\\n+ chat_base_url=\\\"https://gateway.example.test/v1\\\",\\n+ auth_scheme=\\\"Bearer\\\",\\n+ prompt_price_per_1k=1.0 if priced else None,\\n+ completion_price_per_1k=1.0 if priced else None,\\n+ )\\n+\\n+\\n+def _agent(\\n+ agent_id: str,\\n+ model_id: str,\\n+ *,\\n+ priority: int = 0,\\n+ tags: tuple[str, ...] = (\\\"writing\\\",),\\n+) -> ModelAgent:\\n+ \\\"\\\"\\\"Build one mock-backed runtime agent for selection-path regressions.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ id=agent_id,\\n+ model=model_id,\\n+ base_url=\\\"mock://local\\\",\\n+ priority=priority,\\n+ tags=tags,\\n+ )\\n+\\n+\\n+def test_embedding_deployments_never_enter_chat_agent_discovery() -> None:\\n+ \\\"\\\"\\\"Exclude the exact Azure embedding deployment seen in synthesis alerts.\\\"\\\"\\\"\\n+ register_credential(\\\"GATEWAY_API_KEY\\\", \\\"gateway-secret\\\")\\n+ source = ProviderModelSource(\\n+ provider_name=\\\"enterprise_gateway\\\",\\n+ credential_name=\\\"GATEWAY_API_KEY\\\",\\n+ list_url=\\\"https://gateway.example.test/v1/models\\\",\\n+ chat_base_url=\\\"https://gateway.example.test/v1\\\",\\n+ )\\n+ payload = {\\n+ \\\"data\\\": [\\n+ {\\\"id\\\": \\\"azure/text-embedding-3-large\\\"},\\n+ {\\\"id\\\": \\\"text_embedding_3_large\\\"},\\n+ {\\\"id\\\": \\\"BAAI/bge-m3\\\"},\\n+ {\\\"id\\\": \\\"openai/whisper-1\\\"},\\n+ {\\\"id\\\": \\\"gpt-4o-mini-transcribe\\\"},\\n+ {\\\"id\\\": \\\"text-moderation-latest\\\"},\\n+ {\\\"id\\\": \\\"company/reranker-v2\\\"},\\n+ {\\\"id\\\": \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\"},\\n+ {\\\"id\\\": \\\"gpt-audio\\\"},\\n+ {\\\"id\\\": \\\"gpt-5.2\\\"},\\n+ {\\\"id\\\": \\\"qwen/qwen3-235b-a22b-instruct\\\"},\\n+ ]\\n+ }\\n+\\n+ with patch(\\n+ \\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\",\\n+ return_value=_Response(payload),\\n+ ):\\n+ discovered = discover_provider_models(source)\\n+\\n+ assert [model.model_id for model in discovered] == [\\n+ \\\"gpt-audio\\\",\\n+ \\\"gpt-5.2\\\",\\n+ \\\"qwen/qwen3-235b-a22b-instruct\\\",\\n+ ]\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"model_id\\\", \\\"expected\\\"),\\n+ [\\n+ (None, False),\\n+ (\\\"\\\", False),\\n+ (\\\"---\\\", False),\\n+ (\\\"vendor/embeddingv2\\\", False),\\n+ (\\\"vendor/reranking-v2\\\", False),\\n+ (\\\"vendor/transcriber-v2\\\", False),\\n+ (\\\"gpt-5.2\\\", True),\\n+ (\\\"qwen/qwen3-instruct\\\", True),\\n+ ],\\n+)\\n+def test_chat_compatibility_normalizes_identifiers(\\n+ model_id: object, expected: bool\\n+) -> None:\\n+ \\\"\\\"\\\"Normalize provider prefixes and separators without guessing chat features.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id) is expected # type: ignore[arg-type]\\n+\\n+\\n+def test_bytez_chat_catalog_still_rejects_non_chat_identifiers() -> None:\\n+ \\\"\\\"\\\"Apply the same boundary even when a provider accepts a chat task filter.\\\"\\\"\\\"\\n+ register_credential(\\\"BYTEZ_API_KEY\\\", \\\"bytez-secret\\\")\\n+ source = ProviderModelSource(\\n+ provider_name=\\\"bytez\\\",\\n+ credential_name=\\\"BYTEZ_API_KEY\\\",\\n+ list_url=\\\"https://api.bytez.com/models/v2/list/models\\\",\\n+ chat_base_url=\\\"https://api.bytez.com/models/v2/openai/v1\\\",\\n+ auth_scheme=\\\"Key\\\",\\n+ style=\\\"bytez\\\",\\n+ task_filter=\\\"chat\\\",\\n+ )\\n+ payload = {\\n+ \\\"output\\\": [\\n+ {\\\"modelId\\\": \\\"vendor/embeddingv2\\\"},\\n+ {\\\"modelId\\\": \\\"vendor/chat-instruct\\\"},\\n+ ]\\n+ }\\n+\\n+ with patch(\\n+ \\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\",\\n+ return_value=_Response(payload),\\n+ ):\\n+ discovered = discover_provider_models(source)\\n+\\n+ assert [model.model_id for model in discovered] == [\\\"vendor/chat-instruct\\\"]\\n+\\n+\\n+def test_non_chat_discovery_cannot_be_converted_to_agent() -> None:\\n+ \\\"\\\"\\\"Keep manually constructed discovery rows from bypassing the parser filter.\\\"\\\"\\\"\\n+ with pytest.raises(ValueError, match=\\\"general chat agent\\\"):\\n+ agent_from_discovered(_model(\\\"azure/text-embedding-3-large\\\"))\\n+\\n+\\n+def test_non_chat_discovery_is_not_priced_or_selected_for_chat() -> None:\\n+ \\\"\\\"\\\"Keep price routing from reintroducing an incompatible endpoint model.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ embedding_model = _model(\\\"azure/text-embedding-3-large\\\", priced=True)\\n+ chat_model = _model(\\\"gpt-5.2\\\", priced=True)\\n+ price_book.set_price(PriceEntry(\\\"enterprise_gateway\\\", \\\"gpt-5.2\\\", 1.0, 1.0))\\n+\\n+ assert refresh_price_book([embedding_model, chat_model], price_book) == 1\\n+ assert price_book.get_price(\\n+ \\\"enterprise_gateway\\\", \\\"azure/text-embedding-3-large\\\"\\n+ ) is None\\n+ assert select_cheapest_discovered_agent([embedding_model], price_book) is None\\n+ assert select_top_n_cheapest_discovered_agents(\\n+ [embedding_model], price_book, 1\\n+ ) == []\\n+\\n+\\n+def test_stale_embedding_agent_cannot_win_synthesizer_selection() -> None:\\n+ \\\"\\\"\\\"Exclude an already-persisted embedding row even when it has high priority.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ priority=10_000,\\n+ )\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent])\\n+\\n+ assert orchestrator._select_agent(\\\"Produce the final answer.\\\", \\\"synthesizer\\\") is chat_agent\\n+\\n+\\n+def test_all_non_chat_agents_fail_before_synthesis() -> None:\\n+ \\\"\\\"\\\"Fail closed when a stale pool contains no chat-compatible worker.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator(\\n+ [_agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")]\\n+ )\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"chat-compatible\\\"):\\n+ orchestrator._select_agent(\\\"Produce the final answer.\\\", \\\"synthesizer\\\")\\n+\\n+\\n+def test_generated_plan_reselects_non_chat_agent_assignment() -> None:\\n+ \\\"\\\"\\\"Do not trust a generated plan that names a stale embedding agent directly.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ priority=10_000,\\n+ )\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent])\\n+ raw_plan = json.dumps(\\n+ {\\n+ \\\"steps\\\": [\\n+ {\\n+ \\\"id\\\": 0,\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"chat_agent\\\",\\n+ \\\"subtask\\\": \\\"Execute the task.\\\",\\n+ \\\"access\\\": [],\\n+ },\\n+ {\\n+ \\\"id\\\": 1,\\n+ \\\"role\\\": \\\"synthesizer\\\",\\n+ \\\"agent_id\\\": \\\"embedding_agent\\\",\\n+ \\\"subtask\\\": \\\"Produce the final answer.\\\",\\n+ \\\"access\\\": [0],\\n+ },\\n+ ]\\n+ }\\n+ )\\n+\\n+ steps = orchestrator._parse_workflow_plan(raw_plan)\\n+\\n+ assert steps[-1].agent_id == \\\"chat_agent\\\"\\n+\\n+\\n+def test_failover_candidates_exclude_stale_embedding_agents() -> None:\\n+ \\\"\\\"\\\"Keep cross-agent retry from falling through to an incompatible endpoint.\\\"\\\"\\\"\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ embedding_agent = _agent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ priority=10_000,\\n+ )\\n+ orchestrator = TaskOrchestrator([chat_agent, embedding_agent])\\n+\\n+ candidates = orchestrator._failover_candidates(\\n+ chat_agent,\\n+ \\\"Produce the final answer.\\\",\\n+ \\\"synthesizer\\\",\\n+ )\\n+\\n+ assert candidates == [chat_agent]\\n+\\n+\\n+def test_invoke_fails_clearly_when_no_general_chat_agent_remains() -> None:\\n+ \\\"\\\"\\\"Report the role boundary instead of claiming that zero candidates failed.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent])\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"no chat-compatible agent available\\\"):\\n+ orchestrator._invoke(\\n+ embedding_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Produce the final answer.\\\"}],\\n+ text=\\\"Produce the final answer.\\\",\\n+ role=\\\"worker\\\",\\n+ )\\n+\\n+\\n+def test_model_client_rejects_non_chat_model_before_mock_or_network_call() -> None:\\n+ \\\"\\\"\\\"Keep the provider boundary fail-closed even when selection is bypassed.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ client.chat(\\n+ embedding_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Produce the final answer.\\\"}],\\n+ )\\n+\\n+\\n+def test_non_chat_primary_fails_over_only_to_chat_agents() -> None:\\n+ \\\"\\\"\\\"Drop an incompatible primary while retaining a compatible fallback.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent])\\n+\\n+ candidates = orchestrator._failover_candidates(\\n+ embedding_agent,\\n+ \\\"Produce the final answer.\\\",\\n+ \\\"synthesizer\\\",\\n+ )\\n+\\n+ assert candidates == [chat_agent]\\n+\\n+\\n+def test_streaming_client_rejects_non_chat_model_before_transport() -> None:\\n+ \\\"\\\"\\\"Apply the same endpoint boundary to streaming chat requests.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ next(\\n+ client.stream_chat(\\n+ embedding_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Produce the final answer.\\\"}],\\n+ )\\n+ )\\n+\\n+\\n+def test_probe_reports_non_chat_model_without_provider_transport(monkeypatch) -> None:\\n+ \\\"\\\"\\\"Readiness must fail closed with a stable code before network access.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ monkeypatch.setattr(\\n+ client,\\n+ \\\"_validate_provider\\\",\\n+ lambda _agent: (_ for _ in ()).throw(AssertionError(\\\"transport reached\\\")),\\n+ )\\n+\\n+ assert client.probe(embedding_agent)[\\\"failure_code\\\"] == \\\"non_chat_model\\\"\\n+\\n+\\n+def test_generated_planner_inventory_excludes_non_chat_agents() -> None:\\n+ \\\"\\\"\\\"Do not advertise stale endpoint-incompatible agents to the planner.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))\\n+\\n+ class PlannerClient:\\n+ def __init__(self) -> None:\\n+ self.system_prompt = \\\"\\\"\\n+\\n+ def chat(self, _agent, messages, **_kwargs):\\n+ self.system_prompt = messages[0][\\\"content\\\"]\\n+ return json.dumps(\\n+ {\\n+ \\\"steps\\\": [\\n+ {\\n+ \\\"id\\\": 0,\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"chat_agent\\\",\\n+ \\\"subtask\\\": \\\"Execute the task.\\\",\\n+ \\\"access\\\": [],\\n+ },\\n+ {\\n+ \\\"id\\\": 1,\\n+ \\\"role\\\": \\\"synthesizer\\\",\\n+ \\\"agent_id\\\": \\\"chat_agent\\\",\\n+ \\\"subtask\\\": \\\"Produce the answer.\\\",\\n+ \\\"access\\\": [0],\\n+ },\\n+ ]\\n+ }\\n+ )\\n+\\n+ client = PlannerClient()\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent], client=client)\\n+\\n+ steps = orchestrator._plan_generated(\\\"Produce the final answer.\\\")\\n+\\n+ assert steps[-1].agent_id == \\\"chat_agent\\\"\\n+ assert \\\"embedding_agent\\\" not in client.system_prompt\\n+ assert \\\"azure/text-embedding-3-large\\\" not in client.system_prompt\\n+ assert \\\"chat_agent\\\" in client.system_prompt\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"927c213243303d7343bc0b9feb2d0ab58f3ea9c5\", \"filename\": \"tests/test_chat_passthrough_capability_isolation.py\", \"status\": \"added\", \"additions\": 127, \"deletions\": 0, \"changes\": 127, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_passthrough_capability_isolation.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_passthrough_capability_isolation.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_passthrough_capability_isolation.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,127 @@\\n+\\\"\\\"\\\"Regression tests for chat-capability checks on passthrough and batch paths.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import sys\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n+\\n+\\n+def _embedding_agent() -> ModelAgent:\\n+ \\\"\\\"\\\"Build the stale embedding agent from the production incident.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ base_url=\\\"mock://local\\\",\\n+ )\\n+\\n+\\n+def _chat_agent() -> ModelAgent:\\n+ \\\"\\\"\\\"Build one compatible fallback for explicit-model passthrough tests.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ \\\"general_chat_agent\\\",\\n+ \\\"gpt-5.2\\\",\\n+ base_url=\\\"mock://local\\\",\\n+ tags=(\\\"writing\\\",),\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"endpoint\\\",\\n+ [\\n+ \\\"chat/completions\\\",\\n+ \\\"/v1/chat/completions\\\",\\n+ \\\"completions\\\",\\n+ \\\"/v1/completions\\\",\\n+ \\\"responses\\\",\\n+ \\\"/v1/responses\\\",\\n+ ],\\n+)\\n+def test_proxy_send_rejects_embedding_before_mock_or_network_transport(endpoint: str) -> None:\\n+ \\\"\\\"\\\"Keep raw OpenAI passthrough from bypassing the chat transport invariant.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ client.proxy_send(\\n+ _embedding_agent(),\\n+ endpoint,\\n+ {\\n+ \\\"model\\\": \\\"azure/text-embedding-3-large\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Return JSON.\\\"}],\\n+ \\\"input\\\": \\\"Return JSON.\\\",\\n+ },\\n+ )\\n+\\n+\\n+def test_explicit_embedding_model_cannot_bypass_through_structured_passthrough() -> None:\\n+ \\\"\\\"\\\"Reject an explicitly requested stale embedding agent before raw proxy transport.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator([_embedding_agent(), _chat_agent()])\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"azure/text-embedding-3-large\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Return JSON.\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+\\n+def test_explicit_embedding_model_cannot_bypass_through_responses_passthrough() -> None:\\n+ \\\"\\\"\\\"Apply the same transport contract to the Responses passthrough path.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator([_embedding_agent(), _chat_agent()])\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"azure/text-embedding-3-large\\\",\\n+ \\\"input\\\": \\\"Return JSON.\\\",\\n+ },\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+\\n+def test_batch_chat_rejects_embedding_before_mock_or_network_transport() -> None:\\n+ \\\"\\\"\\\"Prevent direct batch callers from submitting embedding models as chat jobs.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ client.batch_chat(\\n+ _embedding_agent(),\\n+ {\\\"task_0\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Return JSON.\\\"}]},\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"gpt-audio\\\",\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ ],\\n+)\\n+def test_chat_served_specialized_models_remain_valid_passthrough_transports(model_id: str) -> None:\\n+ \\\"\\\"\\\"Do not turn ordinary-role exclusion into a false transport rejection.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ agent = ModelAgent(\\\"specialized_chat_agent\\\", model_id, base_url=\\\"mock://local\\\")\\n+\\n+ response = client.proxy_send(\\n+ agent,\\n+ \\\"chat/completions\\\",\\n+ {\\n+ \\\"model\\\": model_id,\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Classify this.\\\"}],\\n+ },\\n+ )\\n+\\n+ assert response[\\\"object\\\"] == \\\"chat.completion\\\"\\n+ assert response[\\\"model\\\"] == model_id\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"17f5b7277e09d9d5e07e8380f7adadfea4477e8e\", \"filename\": \"tests/test_chat_transport_role_separation.py\", \"status\": \"added\", \"additions\": 84, \"deletions\": 0, \"changes\": 84, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_transport_role_separation.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_chat_transport_role_separation.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_transport_role_separation.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,84 @@\\n+\\\"\\\"\\\"Regression coverage for chat transport versus ordinary agent-role eligibility.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import sys\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator.chat_capability import ( # noqa: E402\\n+ is_chat_compatible_model_id,\\n+ is_general_chat_agent_model_id,\\n+)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"gpt-audio\\\",\\n+ \\\"gpt-audio-mini\\\",\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-content-safety\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-topic-control\\\",\\n+ ],\\n+)\\n+def test_chat_served_models_remain_transport_compatible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Do not pre-reject models that provider contracts serve through chat completions.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-content-safety\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-topic-control\\\",\\n+ ],\\n+)\\n+def test_policy_classifiers_do_not_enter_general_agent_roles(model_id: str) -> None:\\n+ \\\"\\\"\\\"Keep chat-served policy classifiers out of ordinary synthesis roles.\\\"\\\"\\\"\\n+ assert not is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"gpt-audio\\\",\\n+ \\\"gpt-audio-mini\\\",\\n+ \\\"gpt-5.2\\\",\\n+ \\\"qwen/qwen3-235b-a22b-instruct\\\",\\n+ ],\\n+)\\n+def test_general_generation_models_remain_agent_eligible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Preserve chat generation models for ordinary agent selection.\\\"\\\"\\\"\\n+ assert is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ \\\"text_embedding_3_large\\\",\\n+ \\\"company/reranker-v2\\\",\\n+ \\\"gpt-4o-mini-transcribe\\\",\\n+ \\\"omni-moderation-latest\\\",\\n+ \\\"gpt-image-1\\\",\\n+ \\\"dall-e-3\\\",\\n+ \\\"openai/clip-vit-large-patch14\\\",\\n+ \\\"google/siglip-so400m-patch14-384\\\",\\n+ \\\"sora-2\\\",\\n+ \\\"gpt-realtime\\\",\\n+ \\\"tts-1\\\",\\n+ ],\\n+)\\n+def test_endpoint_only_models_fail_both_boundaries(model_id: str) -> None:\\n+ \\\"\\\"\\\"Reject endpoint-only model families before transport or ordinary role routing.\\\"\\\"\\\"\\n+ assert not is_chat_compatible_model_id(model_id)\\n+ assert not is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"6d378336f1109b6c262b678b28b0e88abe84fd2e\", \"filename\": \"tests/test_cost_ledger.py\", \"status\": \"modified\", \"additions\": 50, \"deletions\": 0, \"changes\": 50, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_cost_ledger.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_cost_ledger.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_cost_ledger.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -98,6 +98,56 @@ def test_provider_wildcard_price_entry() -> None:\\n assert record.cost_amount == 2.0\\n \\n \\n+def test_corrupt_specific_row_still_falls_back_to_wildcard_price() -> None:\\n+ \\\"\\\"\\\"A malformed provider:model row must not shadow a valid provider:* row.\\\"\\\"\\\"\\n+ config = InMemoryConfigStore()\\n+ price_book = PriceBook(config)\\n+ price_book.set_price(PriceEntry(\\\"openai\\\", \\\"*\\\", prompt_price_per_1k=1.0, completion_price_per_1k=1.0))\\n+ config.set(\\\"llm_price_entries\\\", \\\"openai:broken-model\\\", {\\\"prompt_price_per_1k\\\": \\\"not-a-number\\\"})\\n+\\n+ entry = price_book.get_price(\\\"openai\\\", \\\"broken-model\\\")\\n+\\n+ assert entry is not None\\n+ assert entry.prompt_price_per_1k == 1.0\\n+ assert entry.completion_price_per_1k == 1.0\\n+\\n+\\n+def test_underflowing_positive_price_row_falls_back_to_wildcard() -> None:\\n+ \\\"\\\"\\\"A nonzero KV price that underflows to 0.0 must not be treated as free.\\\"\\\"\\\"\\n+ config = InMemoryConfigStore()\\n+ price_book = PriceBook(config)\\n+ price_book.set_price(PriceEntry(\\\"openai\\\", \\\"*\\\", prompt_price_per_1k=1.0, completion_price_per_1k=1.0))\\n+ config.set(\\n+ \\\"llm_price_entries\\\",\\n+ \\\"openai:underflow-model\\\",\\n+ {\\\"prompt_price_per_1k\\\": \\\"1e-10000\\\", \\\"completion_price_per_1k\\\": \\\"1e-10000\\\"},\\n+ )\\n+\\n+ entry = price_book.get_price(\\\"openai\\\", \\\"underflow-model\\\")\\n+\\n+ assert entry is not None\\n+ assert entry.prompt_price_per_1k == 1.0\\n+ assert entry.completion_price_per_1k == 1.0\\n+\\n+\\n+def test_overflowing_price_row_falls_back_to_wildcard() -> None:\\n+ \\\"\\\"\\\"A Decimal-finite KV price whose float() conversion overflows to inf must not be treated as valid.\\\"\\\"\\\"\\n+ config = InMemoryConfigStore()\\n+ price_book = PriceBook(config)\\n+ price_book.set_price(PriceEntry(\\\"openai\\\", \\\"*\\\", prompt_price_per_1k=1.0, completion_price_per_1k=1.0))\\n+ config.set(\\n+ \\\"llm_price_entries\\\",\\n+ \\\"openai:overflow-model\\\",\\n+ {\\\"prompt_price_per_1k\\\": \\\"1e10000\\\", \\\"completion_price_per_1k\\\": \\\"1e10000\\\"},\\n+ )\\n+\\n+ entry = price_book.get_price(\\\"openai\\\", \\\"overflow-model\\\")\\n+\\n+ assert entry is not None\\n+ assert entry.prompt_price_per_1k == 1.0\\n+ assert entry.completion_price_per_1k == 1.0\\n+\\n+\\n def test_writes_carry_full_attribution() -> None:\\n ledger = _priced_ledger()\\n record = ledger.record_usage(\" }, { \"sha\": \"444557f755a4983db4ce6ce1be67724737efddf8\", \"filename\": \"tests/test_discover_models_cli.py\", \"status\": \"modified\", \"additions\": 46, \"deletions\": 0, \"changes\": 46, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_discover_models_cli.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_discover_models_cli.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_discover_models_cli.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -164,3 +164,49 @@ def urlopen(request, timeout=None):\\n by_id = {agent.id: agent for agent in reloaded.candidates}\\n assert by_id[\\\"openrouter_cheap_model\\\"].disabled is False\\n assert by_id[\\\"openai_pricey_model\\\"].disabled is True\\n+\\n+\\n+def test_enable_cheapest_bootstraps_independent_provider_families(tmp_path) -> None:\\n+ \\\"\\\"\\\"CLI bootstrap must use the provider-diverse selector, not only the cheapest vendor.\\\"\\\"\\\"\\n+ from contextual_orchestrator import TaskOrchestrator\\n+ from contextual_orchestrator.orchestrator import ModelAgent\\n+\\n+ set_backend(InMemoryCredentialBackend())\\n+ register_credential(\\\"OPENAI_API_KEY\\\", \\\"sk-openai\\\")\\n+ register_credential(\\\"OPENROUTER_API_KEY\\\", \\\"sk-router\\\")\\n+ register_credential(\\\"NVIDIA_NIM_API_KEY\\\", \\\"nv-primary\\\")\\n+ db_path = str(tmp_path / \\\"pool.db\\\")\\n+ stdout = StringIO()\\n+\\n+ def urlopen(request, timeout=None):\\n+ host = urllib.parse.urlsplit(request.full_url).hostname\\n+ payloads = {\\n+ \\\"api.openai.com\\\": {\\\"data\\\": [{\\\"id\\\": \\\"openai-model\\\", \\\"pricing\\\": {\\\"prompt\\\": \\\"0.001\\\", \\\"completion\\\": \\\"0.001\\\"}}]},\\n+ \\\"openrouter.ai\\\": {\\\"data\\\": [{\\\"id\\\": \\\"router-model\\\", \\\"pricing\\\": {\\\"prompt\\\": \\\"0.000001\\\", \\\"completion\\\": \\\"0.000001\\\"}}]},\\n+ \\\"integrate.api.nvidia.com\\\": {\\\"data\\\": [{\\\"id\\\": \\\"nim-model\\\", \\\"pricing\\\": {\\\"prompt\\\": \\\"0.000002\\\", \\\"completion\\\": \\\"0.000002\\\"}}]},\\n+ }\\n+ return _Response(payloads.get(host, {\\\"data\\\": []}))\\n+\\n+ try:\\n+ with (\\n+ patch.object(\\n+ sys,\\n+ \\\"argv\\\",\\n+ [\\\"contextual-orchestrator\\\", \\\"discover-models\\\", \\\"--agents-db\\\", db_path, \\\"--enable-cheapest\\\", \\\"3\\\"],\\n+ ),\\n+ patch.object(sys, \\\"stdout\\\", stdout),\\n+ patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen),\\n+ ):\\n+ main()\\n+ finally:\\n+ set_backend(None)\\n+\\n+ report = json.loads(stdout.getvalue())\\n+ assert report[\\\"enabled_agent_ids\\\"] == [\\n+ \\\"openrouter_router_model\\\",\\n+ \\\"nvidia_nim_nim_model\\\",\\n+ \\\"openai_openai_model\\\",\\n+ ]\\n+ reloaded = TaskOrchestrator([ModelAgent(\\\"seed_agent\\\", \\\"seed-model\\\")], agents_db=db_path)\\n+ enabled = {agent.id for agent in reloaded.candidates if not agent.disabled}\\n+ assert enabled - {\\\"seed_agent\\\"} == set(report[\\\"enabled_agent_ids\\\"])\" }, { \"sha\": \"45ca53917fadb330596eb16b217cfe54469db111\", \"filename\": \"tests/test_discovery_bootstrap_selection.py\", \"status\": \"added\", \"additions\": 364, \"deletions\": 0, \"changes\": 364, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_discovery_bootstrap_selection.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_discovery_bootstrap_selection.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_discovery_bootstrap_selection.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,364 @@\\n+\\\"\\\"\\\"Regression coverage for honest, provider-diverse discovery bootstrap.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator.cost_ledger import PriceBook, PriceEntry\\n+from contextual_orchestrator.kv_config import InMemoryConfigStore\\n+from contextual_orchestrator import model_discovery\\n+from contextual_orchestrator.model_discovery import (\\n+ DiscoveredModel,\\n+ refresh_price_book,\\n+ select_cheapest_discovered_agent,\\n+ select_top_n_cheapest_discovered_agents,\\n+)\\n+\\n+\\n+def _model(provider_name: str, model_id: str) -> DiscoveredModel:\\n+ \\\"\\\"\\\"Build one deterministic OpenAI-compatible discovery fixture.\\\"\\\"\\\"\\n+ credential_name = f\\\"{provider_name.upper()}_API_KEY\\\"\\n+ return DiscoveredModel(\\n+ provider_name=provider_name,\\n+ model_id=model_id,\\n+ credential_name=credential_name,\\n+ chat_base_url=f\\\"https://{provider_name}.example/v1\\\",\\n+ auth_scheme=\\\"Bearer\\\",\\n+ )\\n+\\n+\\n+def _priced_model(\\n+ provider_name: str,\\n+ model_id: str,\\n+ *,\\n+ prompt_price_per_1k: float | None,\\n+ completion_price_per_1k: float | None,\\n+ currency_code: str = \\\"USD\\\",\\n+) -> DiscoveredModel:\\n+ \\\"\\\"\\\"Build one discovery row carrying provider-reported price evidence.\\\"\\\"\\\"\\n+ base = _model(provider_name, model_id)\\n+ return DiscoveredModel(\\n+ provider_name=base.provider_name,\\n+ model_id=base.model_id,\\n+ credential_name=base.credential_name,\\n+ chat_base_url=base.chat_base_url,\\n+ auth_scheme=base.auth_scheme,\\n+ prompt_price_per_1k=prompt_price_per_1k,\\n+ completion_price_per_1k=completion_price_per_1k,\\n+ currency_code=currency_code,\\n+ )\\n+\\n+\\n+def _set_price(\\n+ price_book: PriceBook,\\n+ model: DiscoveredModel,\\n+ price_per_1k: float,\\n+ *,\\n+ currency_code: str = \\\"USD\\\",\\n+) -> None:\\n+ \\\"\\\"\\\"Record one known symmetric prompt/completion price.\\\"\\\"\\\"\\n+ price_book.set_price(\\n+ PriceEntry(\\n+ model.provider_name,\\n+ model.model_id,\\n+ price_per_1k,\\n+ price_per_1k,\\n+ currency_code,\\n+ )\\n+ )\\n+\\n+\\n+def test_unpriced_discovered_model_is_unknown_not_free() -> None:\\n+ \\\"\\\"\\\"Missing price evidence must not outrank a model with a known price.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ priced = _model(\\\"openrouter\\\", \\\"priced-model\\\")\\n+ unpriced = _model(\\\"bytez\\\", \\\"unpriced-model\\\")\\n+ _set_price(price_book, priced, 0.01)\\n+\\n+ assert select_cheapest_discovered_agent([unpriced, priced], price_book) is priced\\n+ assert select_top_n_cheapest_discovered_agents(\\n+ [unpriced, priced], price_book, 2\\n+ ) == [priced, unpriced]\\n+\\n+\\n+def test_partial_provider_price_is_unknown_instead_of_fabricating_a_free_component() -> None:\\n+ \\\"\\\"\\\"A missing prompt or completion price cannot become an invented zero.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ partial = _priced_model(\\n+ \\\"partial_vendor\\\",\\n+ \\\"partial-model\\\",\\n+ prompt_price_per_1k=0.001,\\n+ completion_price_per_1k=None,\\n+ )\\n+ complete = _priced_model(\\n+ \\\"openrouter\\\",\\n+ \\\"complete-model\\\",\\n+ prompt_price_per_1k=1.0,\\n+ completion_price_per_1k=1.0,\\n+ )\\n+\\n+ assert refresh_price_book([partial, complete], price_book) == 1\\n+ assert price_book.get_price(partial.provider_name, partial.model_id) is None\\n+ assert select_cheapest_discovered_agent([partial, complete], price_book) is complete\\n+\\n+\\n+def test_persisted_price_row_missing_one_component_remains_unknown() -> None:\\n+ \\\"\\\"\\\"KV corruption must not silently manufacture a zero-priced component.\\\"\\\"\\\"\\n+ store = InMemoryConfigStore()\\n+ store.set(\\n+ \\\"llm_price_entries\\\",\\n+ \\\"partial_vendor:partial-model\\\",\\n+ {\\n+ \\\"provider_name\\\": \\\"partial_vendor\\\",\\n+ \\\"model_name\\\": \\\"partial-model\\\",\\n+ \\\"prompt_price_per_1k\\\": 0.001,\\n+ \\\"currency_code\\\": \\\"USD\\\",\\n+ },\\n+ )\\n+ price_book = PriceBook(store)\\n+\\n+ assert price_book.get_price(\\\"partial_vendor\\\", \\\"partial-model\\\") is None\\n+\\n+\\n+def test_invalid_catalog_prices_are_unknown_not_trusted_cost_evidence() -> None:\\n+ \\\"\\\"\\\"Reject negative, non-finite, and boolean provider price values.\\\"\\\"\\\"\\n+ assert model_discovery._price_per_1k(\\\"-0.000001\\\") is None\\n+ assert model_discovery._price_per_1k(\\\"nan\\\") is None\\n+ assert model_discovery._price_per_1k(\\\"inf\\\") is None\\n+ assert model_discovery._price_per_1k(True) is None\\n+ assert model_discovery._price_per_1k(\\\"0\\\") == 0.0\\n+\\n+\\n+def test_huge_price_values_remain_unknown_without_crashing_discovery_or_ranking() -> None:\\n+ \\\"\\\"\\\"Unbounded JSON or KV integers must not terminate bootstrap selection.\\\"\\\"\\\"\\n+ huge_price = 10**10000\\n+ assert model_discovery._price_per_1k(huge_price) is None\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ huge = _model(\\\"huge_vendor\\\", \\\"huge-model\\\")\\n+ valid = _model(\\\"openrouter\\\", \\\"valid-model\\\")\\n+ _set_price(price_book, huge, huge_price)\\n+ _set_price(price_book, valid, 1.0)\\n+\\n+ assert select_cheapest_discovered_agent([huge, valid], price_book) is valid\\n+\\n+\\n+def test_malformed_price_book_row_is_unknown_instead_of_crashing_selection() -> None:\\n+ \\\"\\\"\\\"A corrupt persisted price row must not take down the serving bootstrap.\\\"\\\"\\\"\\n+ store = InMemoryConfigStore()\\n+ store.set(\\n+ \\\"llm_price_entries\\\",\\n+ \\\"broken_vendor:broken-model\\\",\\n+ {\\n+ \\\"provider_name\\\": \\\"broken_vendor\\\",\\n+ \\\"model_name\\\": \\\"broken-model\\\",\\n+ \\\"prompt_price_per_1k\\\": \\\"not-a-number\\\",\\n+ \\\"completion_price_per_1k\\\": 0.001,\\n+ \\\"currency_code\\\": \\\"USD\\\",\\n+ },\\n+ )\\n+ price_book = PriceBook(store)\\n+ broken = _model(\\\"broken_vendor\\\", \\\"broken-model\\\")\\n+ valid = _model(\\\"openrouter\\\", \\\"valid-model\\\")\\n+ _set_price(price_book, valid, 1.0)\\n+\\n+ assert select_cheapest_discovered_agent([broken, valid], price_book) is valid\\n+\\n+\\n+def test_refresh_counts_only_complete_prices_in_the_comparison_currency() -> None:\\n+ \\\"\\\"\\\"Cross-currency evidence is unknown until an explicit conversion exists.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore(), default_currency=\\\"USD\\\")\\n+ usd = _priced_model(\\n+ \\\"openrouter\\\",\\n+ \\\"usd-model\\\",\\n+ prompt_price_per_1k=1.0,\\n+ completion_price_per_1k=1.0,\\n+ currency_code=\\\"USD\\\",\\n+ )\\n+ eur = _priced_model(\\n+ \\\"eur_vendor\\\",\\n+ \\\"eur-model\\\",\\n+ prompt_price_per_1k=0.001,\\n+ completion_price_per_1k=0.001,\\n+ currency_code=\\\"EUR\\\",\\n+ )\\n+\\n+ assert refresh_price_book([eur, usd], price_book) == 1\\n+ assert price_book.get_price(\\\"eur_vendor\\\", \\\"eur-model\\\") is None\\n+ assert price_book.get_price(\\\"openrouter\\\", \\\"usd-model\\\") is not None\\n+\\n+\\n+def test_invalid_or_cross_currency_price_rows_do_not_outrank_comparable_usd_cost() -> None:\\n+ \\\"\\\"\\\"Only finite non-negative prices in the configured currency are comparable.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore(), default_currency=\\\"USD\\\")\\n+ valid = _model(\\\"openrouter\\\", \\\"valid-model\\\")\\n+ negative = _model(\\\"negative_vendor\\\", \\\"negative-model\\\")\\n+ non_finite = _model(\\\"nan_vendor\\\", \\\"nan-model\\\")\\n+ foreign = _model(\\\"eur_vendor\\\", \\\"eur-model\\\")\\n+\\n+ _set_price(price_book, valid, 1.0)\\n+ _set_price(price_book, negative, -100.0)\\n+ _set_price(price_book, non_finite, float(\\\"nan\\\"))\\n+ _set_price(price_book, foreign, 0.000001, currency_code=\\\"EUR\\\")\\n+\\n+ assert select_cheapest_discovered_agent(\\n+ [negative, non_finite, foreign, valid],\\n+ price_book,\\n+ ) is valid\\n+\\n+\\n+def test_duplicate_serving_identity_cannot_consume_bootstrap_capacity() -> None:\\n+ \\\"\\\"\\\"A repeated provider/model row must not masquerade as failover diversity.\\\"\\\"\\\"\\n+ selector = getattr(\\n+ model_discovery,\\n+ \\\"select_bootstrap_discovered_agents\\\",\\n+ None,\\n+ )\\n+ assert callable(selector), \\\"missing provider-diverse bootstrap selector\\\"\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ duplicate_first = _model(\\\"openrouter\\\", \\\"same-model\\\")\\n+ duplicate_second = _model(\\\"openrouter\\\", \\\"same-model\\\")\\n+ independent = _model(\\\"openai\\\", \\\"independent-model\\\")\\n+ _set_price(price_book, duplicate_first, 0.01)\\n+ _set_price(price_book, independent, 0.02)\\n+\\n+ selected = selector(\\n+ [duplicate_second, independent, duplicate_first],\\n+ price_book,\\n+ 3,\\n+ )\\n+ top_n = select_top_n_cheapest_discovered_agents(\\n+ [duplicate_second, independent, duplicate_first],\\n+ price_book,\\n+ 3,\\n+ )\\n+\\n+ assert [\\n+ (model.provider_name, model.model_id)\\n+ for model in selected\\n+ ] == [\\n+ (\\\"openrouter\\\", \\\"same-model\\\"),\\n+ (\\\"openai\\\", \\\"independent-model\\\"),\\n+ ]\\n+ assert [\\n+ (model.provider_name, model.model_id)\\n+ for model in top_n\\n+ ] == [\\n+ (\\\"openrouter\\\", \\\"same-model\\\"),\\n+ (\\\"openai\\\", \\\"independent-model\\\"),\\n+ ]\\n+\\n+\\n+def test_conflicting_duplicate_prices_are_withheld_as_ambiguous() -> None:\\n+ \\\"\\\"\\\"Do not let provider row order decide the trusted price for one agent id.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ cheap_claim = _priced_model(\\n+ \\\"openrouter\\\",\\n+ \\\"duplicate-model\\\",\\n+ prompt_price_per_1k=0.000001,\\n+ completion_price_per_1k=0.000001,\\n+ )\\n+ expensive_claim = _priced_model(\\n+ \\\"openrouter\\\",\\n+ \\\"duplicate-model\\\",\\n+ prompt_price_per_1k=100.0,\\n+ completion_price_per_1k=100.0,\\n+ )\\n+ complete = _priced_model(\\n+ \\\"openai\\\",\\n+ \\\"complete-model\\\",\\n+ prompt_price_per_1k=1.0,\\n+ completion_price_per_1k=1.0,\\n+ )\\n+\\n+ assert refresh_price_book(\\n+ [cheap_claim, expensive_claim, complete],\\n+ price_book,\\n+ ) == 1\\n+ assert price_book.get_price(\\\"openrouter\\\", \\\"duplicate-model\\\") is None\\n+ assert select_cheapest_discovered_agent(\\n+ [cheap_claim, expensive_claim, complete],\\n+ price_book,\\n+ ) is complete\\n+\\n+\\n+def test_bootstrap_selector_prefers_provider_diversity_before_duplicates() -> None:\\n+ \\\"\\\"\\\"The initial failover pool must span providers before repeating one.\\\"\\\"\\\"\\n+ selector = getattr(\\n+ model_discovery,\\n+ \\\"select_bootstrap_discovered_agents\\\",\\n+ None,\\n+ )\\n+ assert callable(selector), \\\"missing provider-diverse bootstrap selector\\\"\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ router_cheapest = _model(\\\"openrouter\\\", \\\"router-cheapest\\\")\\n+ router_second = _model(\\\"openrouter\\\", \\\"router-second\\\")\\n+ nim_model = _model(\\\"nvidia_nim\\\", \\\"nim-model\\\")\\n+ openai_model = _model(\\\"openai\\\", \\\"openai-model\\\")\\n+ _set_price(price_book, router_cheapest, 0.01)\\n+ _set_price(price_book, router_second, 0.02)\\n+ _set_price(price_book, nim_model, 0.5)\\n+ _set_price(price_book, openai_model, 1.0)\\n+\\n+ selected = selector(\\n+ [router_second, openai_model, nim_model, router_cheapest],\\n+ price_book,\\n+ 3,\\n+ )\\n+\\n+ assert selected == [router_cheapest, nim_model, openai_model]\\n+\\n+\\n+def test_bootstrap_selector_treats_nim_primary_and_sub_as_one_outage_domain() -> None:\\n+ \\\"\\\"\\\"Two NIM keys must not displace an independently hosted provider.\\\"\\\"\\\"\\n+ selector = getattr(\\n+ model_discovery,\\n+ \\\"select_bootstrap_discovered_agents\\\",\\n+ None,\\n+ )\\n+ assert callable(selector), \\\"missing provider-diverse bootstrap selector\\\"\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ nim_primary = _model(\\\"nvidia_nim\\\", \\\"primary-model\\\")\\n+ nim_sub = _model(\\\"nvidia_nim_sub\\\", \\\"sub-model\\\")\\n+ openrouter = _model(\\\"openrouter\\\", \\\"router-model\\\")\\n+ _set_price(price_book, nim_primary, 0.01)\\n+ _set_price(price_book, nim_sub, 0.02)\\n+ _set_price(price_book, openrouter, 0.5)\\n+\\n+ selected = selector(\\n+ [nim_sub, openrouter, nim_primary],\\n+ price_book,\\n+ 2,\\n+ )\\n+\\n+ assert selected == [nim_primary, openrouter]\\n+\\n+\\n+def test_bootstrap_selector_is_deterministic_when_every_model_is_unpriced() -> None:\\n+ \\\"\\\"\\\"All-unpriced discovery remains usable but never order-dependent.\\\"\\\"\\\"\\n+ selector = getattr(\\n+ model_discovery,\\n+ \\\"select_bootstrap_discovered_agents\\\",\\n+ None,\\n+ )\\n+ assert callable(selector), \\\"missing provider-diverse bootstrap selector\\\"\\n+\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ router_z = _model(\\\"openrouter\\\", \\\"z-model\\\")\\n+ router_a = _model(\\\"openrouter\\\", \\\"a-model\\\")\\n+ nim_b = _model(\\\"nvidia_nim\\\", \\\"b-model\\\")\\n+\\n+ selected = selector(\\n+ [router_z, nim_b, router_a],\\n+ price_book,\\n+ 3,\\n+ )\\n+\\n+ assert selected == [nim_b, router_a, router_z]\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"f1eca6acd721c760f601ef90b3c10d0b0419810f\", \"filename\": \"tests/test_local_mlx.py\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 2, \"changes\": 5, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_local_mlx.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_local_mlx.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_local_mlx.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -350,7 +350,8 @@ def test_response_without_content_or_reasoning_fails_clearly() -> None:\\n ModelClient()._response_content(agent, {\\\"choices\\\": [{\\\"message\\\": {}}]})\\n \\n \\n-def test_local_responses_passthrough_adapts_to_chat_transport() -> None:\\n+@pytest.mark.parametrize(\\\"endpoint\\\", [\\\"responses\\\", \\\"/v1/responses\\\"])\\n+def test_local_responses_passthrough_adapts_to_chat_transport(endpoint: str) -> None:\\n agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n client = ModelClient(max_retries=0, chat_template_args={\\\"enable_thinking\\\": False})\\n with patch.object(client, \\\"_validate_provider\\\", return_value=None), patch.object(\\n@@ -369,7 +370,7 @@ def test_local_responses_passthrough_adapts_to_chat_transport() -> None:\\n ) as send:\\n response = client.proxy_send(\\n agent,\\n- \\\"responses\\\",\\n+ endpoint,\\n {\\n \\\"model\\\": \\\"local-model\\\",\\n \\\"instructions\\\": \\\"Be concise.\\\",\" }, { \"sha\": \"d1974a88c31401c752fed7932bb4ce9bd48d6efc\", \"filename\": \"tests/test_model_discovery.py\", \"status\": \"modified\", \"additions\": 8, \"deletions\": 0, \"changes\": 8, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_model_discovery.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_model_discovery.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_model_discovery.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -24,6 +24,7 @@\\n from contextual_orchestrator.model_discovery import ( # noqa: E402\\n DiscoveredModel,\\n ProviderModelSource,\\n+ _price_per_1k,\\n agent_from_discovered,\\n agent_id_for,\\n discover_all_models,\\n@@ -113,6 +114,13 @@ def urlopen(request, timeout=None):\\n assert discovered[1].prompt_price_per_1k is None\\n \\n \\n+def test_price_per_1k_rejects_underflowing_positive_value() -> None:\\n+ \\\"\\\"\\\"A nonzero per-token price that underflows to 0.0 in float stays unknown.\\\"\\\"\\\"\\n+ assert _price_per_1k(\\\"1e-10000\\\") is None\\n+ assert _price_per_1k(0) == 0.0\\n+ assert _price_per_1k(0.000001) == pytest.approx(0.001)\\n+\\n+\\n def test_discover_bytez_parses_models_with_key_auth_scheme() -> None:\\n register_credential(\\\"BYTEZ_API_KEY\\\", \\\"bytez-secret\\\")\\n payload = {\" }, { \"sha\": \"a2ce16f9562bc7470bcef3d7a090be9ed96892be\", \"filename\": \"tests/test_provider_bootstrap.py\", \"status\": \"added\", \"additions\": 405, \"deletions\": 0, \"changes\": 405, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_bootstrap.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_bootstrap.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_bootstrap.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,405 @@\\n+\\\"\\\"\\\"Contracts for durable all-provider bootstrap and provider-diverse model activation.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from dataclasses import replace\\n+import json\\n+import os\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+from contextual_orchestrator.chat_capability import is_general_chat_agent_model_id\\n+from contextual_orchestrator.credentials import (\\n+ InMemoryCredentialBackend,\\n+ get_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.model_discovery import (\\n+ DiscoveredModel,\\n+ agent_from_discovered,\\n+)\\n+from contextual_orchestrator import provider_bootstrap\\n+\\n+\\n+@pytest.fixture(autouse=True)\\n+def isolated_credential_backend():\\n+ \\\"\\\"\\\"Give each test a fresh process-local credential registry.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ yield\\n+ set_backend(None)\\n+\\n+\\n+def _complete_environment() -> dict[str, str]:\\n+ \\\"\\\"\\\"Return one complete mounted-secret fixture with trailing newlines.\\\"\\\"\\\"\\n+ return {\\n+ name: f\\\"secret-for-{name.lower()}\\\\n\\\"\\n+ for name in provider_bootstrap.PROVIDER_CREDENTIAL_NAMES\\n+ }\\n+\\n+\\n+def _model(\\n+ provider: str,\\n+ credential: str,\\n+ model_id: str,\\n+ prompt: float | None,\\n+) -> DiscoveredModel:\\n+ \\\"\\\"\\\"Build a deterministic provider-catalog row for bootstrap tests.\\\"\\\"\\\"\\n+ return DiscoveredModel(\\n+ provider_name=provider,\\n+ model_id=model_id,\\n+ credential_name=credential,\\n+ chat_base_url=f\\\"https://{provider}.example/v1\\\",\\n+ auth_scheme=\\\"Bearer\\\",\\n+ prompt_price_per_1k=prompt,\\n+ completion_price_per_1k=prompt,\\n+ )\\n+\\n+\\n+def test_fixed_inventory_matches_all_five_organization_secrets():\\n+ \\\"\\\"\\\"The bootstrap inventory must not silently lose an organization provider key.\\\"\\\"\\\"\\n+ assert set(provider_bootstrap.PROVIDER_CREDENTIAL_NAMES) == {\\n+ \\\"NVIDIA_NIM_API_KEY\\\",\\n+ \\\"NVIDIA_NIM_API_KEY_SUB\\\",\\n+ \\\"BYTEZ_API_KEY\\\",\\n+ \\\"OPENROUTER_API_KEY\\\",\\n+ \\\"OPENAI_API_KEY\\\",\\n+ }\\n+\\n+\\n+def test_collect_requires_complete_inventory_without_leaking_values():\\n+ \\\"\\\"\\\"Production bootstrap fails before writes when one trusted secret is absent.\\\"\\\"\\\"\\n+ environment = _complete_environment()\\n+ removed = environment.pop(\\\"BYTEZ_API_KEY\\\")\\n+ with pytest.raises(provider_bootstrap.ProviderBootstrapError) as raised:\\n+ provider_bootstrap.collect_provider_credentials(environment)\\n+ assert \\\"BYTEZ_API_KEY\\\" in str(raised.value)\\n+ assert removed.strip() not in str(raised.value)\\n+ assert all(\\n+ get_credential(name) is None\\n+ for name in provider_bootstrap.PROVIDER_CREDENTIAL_NAMES\\n+ )\\n+\\n+\\n+def test_atomic_memory_registration_strips_mounted_secret_newlines():\\n+ \\\"\\\"\\\"A complete inventory becomes visible together and mounted newlines are removed.\\\"\\\"\\\"\\n+ credentials = provider_bootstrap.collect_provider_credentials(\\n+ _complete_environment()\\n+ )\\n+ registered = provider_bootstrap.register_provider_credentials_atomically(\\n+ credentials\\n+ )\\n+ assert registered == tuple(\\n+ sorted(provider_bootstrap.PROVIDER_CREDENTIAL_NAMES)\\n+ )\\n+ for name in provider_bootstrap.PROVIDER_CREDENTIAL_NAMES:\\n+ value = get_credential(name)\\n+ assert value == f\\\"secret-for-{name.lower()}\\\"\\n+ assert \\\"\\\\n\\\" not in value\\n+\\n+\\n+def test_unknown_credential_name_is_rejected_before_any_write():\\n+ \\\"\\\"\\\"The fixed bootstrap boundary cannot be expanded by untrusted names.\\\"\\\"\\\"\\n+ with pytest.raises(provider_bootstrap.ProviderBootstrapError):\\n+ provider_bootstrap.register_provider_credentials_atomically(\\n+ {\\\"EVIL_PROVIDER_KEY\\\": \\\"secret\\\"}\\n+ )\\n+ assert get_credential(\\\"EVIL_PROVIDER_KEY\\\") is None\\n+\\n+\\n+def test_diverse_selection_prefers_known_cost_without_treating_unknown_as_free():\\n+ \\\"\\\"\\\"Unknown-cost candidates stay usable but cannot win as fabricated zero cost.\\\"\\\"\\\"\\n+ models = [\\n+ _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"gpt-expensive\\\", 4.0),\\n+ _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"gpt-cheap\\\", 1.0),\\n+ _model(\\\"openrouter\\\", \\\"OPENROUTER_API_KEY\\\", \\\"mistral-router\\\", 2.0),\\n+ _model(\\\"bytez\\\", \\\"BYTEZ_API_KEY\\\", \\\"llama-unknown\\\", None),\\n+ ]\\n+ selected = provider_bootstrap.select_provider_diverse_models(models, limit=3)\\n+ assert [(item.provider_name, item.model_id) for item in selected] == [\\n+ (\\\"openai\\\", \\\"gpt-cheap\\\"),\\n+ (\\\"openrouter\\\", \\\"mistral-router\\\"),\\n+ (\\\"bytez\\\", \\\"llama-unknown\\\"),\\n+ ]\\n+\\n+\\n+def test_partial_price_is_unknown_in_provider_bootstrap_ranking():\\n+ \\\"\\\"\\\"A missing prompt or completion price cannot become an invented zero.\\\"\\\"\\\"\\n+ partial = replace(\\n+ _model(\\\"bytez\\\", \\\"BYTEZ_API_KEY\\\", \\\"partial-model\\\", None),\\n+ prompt_price_per_1k=0.001,\\n+ )\\n+ complete = _model(\\\"openrouter\\\", \\\"OPENROUTER_API_KEY\\\", \\\"complete-model\\\", 1.0)\\n+\\n+ selected = provider_bootstrap.select_provider_diverse_models(\\n+ [partial, complete], limit=2\\n+ )\\n+\\n+ assert [(item.provider_name, item.model_id) for item in selected] == [\\n+ (\\\"openrouter\\\", \\\"complete-model\\\"),\\n+ (\\\"bytez\\\", \\\"partial-model\\\"),\\n+ ]\\n+\\n+\\n+def test_non_usd_price_cannot_outrank_a_comparable_usd_price():\\n+ \\\"\\\"\\\"A cheap non-USD row must not beat a pricier USD row on face value alone.\\\"\\\"\\\"\\n+ cheap_foreign = replace(\\n+ _model(\\\"openrouter\\\", \\\"OPENROUTER_API_KEY\\\", \\\"cheap-foreign\\\", 0.001),\\n+ currency_code=\\\"EUR\\\",\\n+ )\\n+ priced_usd = _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"priced-usd\\\", 1.0)\\n+\\n+ selected = provider_bootstrap.select_provider_diverse_models(\\n+ [cheap_foreign, priced_usd], limit=2\\n+ )\\n+\\n+ assert [(item.provider_name, item.model_id) for item in selected] == [\\n+ (\\\"openai\\\", \\\"priced-usd\\\"),\\n+ (\\\"openrouter\\\", \\\"cheap-foreign\\\"),\\n+ ]\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"model_id\\\", \\\"eligible\\\"),\\n+ [\\n+ (\\\"dall-e-3\\\", False),\\n+ (\\\"openai/clip-vit-large\\\", False),\\n+ (\\\"siglip-base-patch16\\\", False),\\n+ (\\\"nvidia/guard-model\\\", False),\\n+ (\\\"provider/audio-chat-model\\\", True),\\n+ (\\\"openai/gpt-4.1-mini\\\", True),\\n+ ],\\n+)\\n+def test_provider_bootstrap_reuses_shared_chat_capability_policy(model_id, eligible):\\n+ \\\"\\\"\\\"Bootstrap and runtime must agree on ordinary chat-model eligibility.\\\"\\\"\\\"\\n+ model = _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", model_id, 1.0)\\n+ assert provider_bootstrap.is_chat_serving_candidate(model) is eligible\\n+ assert eligible is is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+def test_provider_bootstrap_collapses_nim_credentials_to_one_outage_domain():\\n+ \\\"\\\"\\\"Primary and secondary NIM credentials cannot displace an independent provider.\\\"\\\"\\\"\\n+ nim_primary = _model(\\\"nvidia_nim\\\", \\\"NVIDIA_NIM_API_KEY\\\", \\\"primary-model\\\", 0.01)\\n+ nim_secondary = _model(\\\"nvidia_nim_sub\\\", \\\"NVIDIA_NIM_API_KEY_SUB\\\", \\\"secondary-model\\\", 0.02)\\n+ openrouter = _model(\\\"openrouter\\\", \\\"OPENROUTER_API_KEY\\\", \\\"router-model\\\", 0.5)\\n+\\n+ selected = provider_bootstrap.select_provider_diverse_models(\\n+ [nim_secondary, openrouter, nim_primary], limit=2\\n+ )\\n+\\n+ assert [(item.provider_name, item.model_id) for item in selected] == [\\n+ (\\\"nvidia_nim\\\", \\\"primary-model\\\"),\\n+ (\\\"openrouter\\\", \\\"router-model\\\"),\\n+ ]\\n+\\n+\\n+def test_non_chat_catalog_rows_are_never_selected_for_chat_service():\\n+ \\\"\\\"\\\"Embeddings, rerankers, speech, image, moderation, and realtime rows stay inert.\\\"\\\"\\\"\\n+ models = [\\n+ _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"text-embedding-3-small\\\", 0.1),\\n+ _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"whisper-1\\\", 0.1),\\n+ _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"gpt-image-1\\\", 0.1),\\n+ _model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"omni-moderation-latest\\\", 0.1),\\n+ _model(\\n+ \\\"nvidia_nim\\\",\\n+ \\\"NVIDIA_NIM_API_KEY\\\",\\n+ \\\"nv-rerankqa-mistral-4b-v3\\\",\\n+ 0.1,\\n+ ),\\n+ _model(\\n+ \\\"openrouter\\\",\\n+ \\\"OPENROUTER_API_KEY\\\",\\n+ \\\"openai/gpt-4.1-mini\\\",\\n+ 2.0,\\n+ ),\\n+ ]\\n+ selected = provider_bootstrap.select_provider_diverse_models(models, limit=10)\\n+ assert [(item.provider_name, item.model_id) for item in selected] == [\\n+ (\\\"openrouter\\\", \\\"openai/gpt-4.1-mini\\\")\\n+ ]\\n+\\n+\\n+def test_serving_tags_do_not_infer_capabilities_from_model_names():\\n+ \\\"\\\"\\\"Reasoning, coding, and vision-looking names receive only generic tags.\\\"\\\"\\\"\\n+ model = _model(\\n+ \\\"openrouter\\\",\\n+ \\\"OPENROUTER_API_KEY\\\",\\n+ \\\"qwen/qwen-vl-coder-reasoning\\\",\\n+ 1.0,\\n+ )\\n+ assert provider_bootstrap.serving_tags_for_discovered(model) == (\\n+ \\\"discovered\\\",\\n+ \\\"chat\\\",\\n+ \\\"worker\\\",\\n+ \\\"writing\\\",\\n+ \\\"synthesizer\\\",\\n+ )\\n+\\n+\\n+def test_bootstrap_registers_then_discovers_without_environment_runtime_reads(\\n+ monkeypatch,\\n+):\\n+ \\\"\\\"\\\"Discovery sees KV-backed credentials after one-shot environment bootstrap.\\\"\\\"\\\"\\n+ environment = _complete_environment()\\n+ observed: dict[str, str | None] = {}\\n+\\n+ def fake_discover_all_models():\\n+ \\\"\\\"\\\"Observe the KV from the mocked provider-discovery boundary.\\\"\\\"\\\"\\n+ for name in provider_bootstrap.PROVIDER_CREDENTIAL_NAMES:\\n+ observed[name] = get_credential(name)\\n+ return (\\n+ [_model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"gpt-test\\\", 1.0)],\\n+ [],\\n+ )\\n+\\n+ monkeypatch.setattr(\\n+ provider_bootstrap,\\n+ \\\"discover_all_models\\\",\\n+ fake_discover_all_models,\\n+ )\\n+ report = provider_bootstrap.bootstrap_provider_runtime(\\n+ environ=environment,\\n+ model_limit=1,\\n+ )\\n+\\n+ assert report.discovered_model_count == 1\\n+ assert report.eligible_model_count == 1\\n+ assert report.selected_agent_ids == (\\\"openai_gpt_test\\\",)\\n+ assert report.enabled_agent_ids == ()\\n+ assert report.durable_agent_pool is False\\n+ assert all(\\n+ observed[name] == environment[name].strip()\\n+ for name in observed\\n+ )\\n+\\n+\\n+def test_bootstrap_fails_closed_when_no_model_is_discovered(monkeypatch):\\n+ \\\"\\\"\\\"Credential writes without a usable catalog are not reported service-ready.\\\"\\\"\\\"\\n+ monkeypatch.setattr(\\n+ provider_bootstrap,\\n+ \\\"discover_all_models\\\",\\n+ lambda: ([], []),\\n+ )\\n+ with pytest.raises(\\n+ provider_bootstrap.ProviderBootstrapError,\\n+ match=\\\"no usable models\\\",\\n+ ):\\n+ provider_bootstrap.bootstrap_provider_runtime(\\n+ environ=_complete_environment()\\n+ )\\n+\\n+\\n+def test_bootstrap_fails_closed_when_catalog_has_only_non_chat_models(monkeypatch):\\n+ \\\"\\\"\\\"A successful catalog response is not ready without a chat candidate.\\\"\\\"\\\"\\n+ monkeypatch.setattr(\\n+ provider_bootstrap,\\n+ \\\"discover_all_models\\\",\\n+ lambda: (\\n+ [\\n+ _model(\\n+ \\\"openai\\\",\\n+ \\\"OPENAI_API_KEY\\\",\\n+ \\\"text-embedding-3-small\\\",\\n+ 0.1,\\n+ )\\n+ ],\\n+ [],\\n+ ),\\n+ )\\n+ with pytest.raises(\\n+ provider_bootstrap.ProviderBootstrapError,\\n+ match=\\\"no chat-capable models\\\",\\n+ ):\\n+ provider_bootstrap.bootstrap_provider_runtime(\\n+ environ=_complete_environment()\\n+ )\\n+\\n+\\n+def test_durable_pool_withdraws_bootstrap_and_stale_discovered_agents(\\n+ monkeypatch,\\n+ tmp_path,\\n+):\\n+ \\\"\\\"\\\"A refresh leaves exactly the current selected discovered models active.\\\"\\\"\\\"\\n+ agents_db = str(tmp_path / \\\"agents.db\\\")\\n+ old_model = _model(\\n+ \\\"openai\\\",\\n+ \\\"OPENAI_API_KEY\\\",\\n+ \\\"gpt-retired-model\\\",\\n+ 1.0,\\n+ )\\n+ old_agent = replace(agent_from_discovered(old_model), disabled=False)\\n+ seeded = TaskOrchestrator(\\n+ [ModelAgent(\\\"manual_agent\\\", \\\"manual-model\\\")],\\n+ agents_db=agents_db,\\n+ )\\n+ seeded.sync_discovered_agents([old_agent])\\n+\\n+ new_model = _model(\\n+ \\\"openrouter\\\",\\n+ \\\"OPENROUTER_API_KEY\\\",\\n+ \\\"qwen-current-coder\\\",\\n+ 2.0,\\n+ )\\n+ monkeypatch.setattr(\\n+ provider_bootstrap,\\n+ \\\"discover_all_models\\\",\\n+ lambda: ([new_model], []),\\n+ )\\n+ report = provider_bootstrap.bootstrap_provider_runtime(\\n+ environ=_complete_environment(),\\n+ agents_db=agents_db,\\n+ model_limit=1,\\n+ )\\n+\\n+ assert report.discovered_model_count == 1\\n+ assert report.eligible_model_count == 1\\n+ assert report.selected_agent_ids == (\\\"openrouter_qwen_current_coder\\\",)\\n+ assert report.enabled_agent_ids == (\\\"openrouter_qwen_current_coder\\\",)\\n+ assert report.durable_agent_pool is True\\n+\\n+ restarted = TaskOrchestrator(\\n+ [ModelAgent(\\\"bootstrap_agent\\\", \\\"bootstrap-model\\\")],\\n+ agents_db=agents_db,\\n+ )\\n+ assert {agent.id for agent in restarted.agents} == {\\n+ \\\"openrouter_qwen_current_coder\\\"\\n+ }\\n+ assert restarted.agents[0].tags == (\\n+ \\\"discovered\\\",\\n+ \\\"chat\\\",\\n+ \\\"worker\\\",\\n+ \\\"writing\\\",\\n+ \\\"synthesizer\\\",\\n+ )\\n+ assert all(\\n+ agent.id not in {\\\"bootstrap_agent\\\", \\\"openai_gpt_retired_model\\\"}\\n+ for agent in restarted.agents\\n+ )\\n+\\n+\\n+def test_cli_report_never_contains_secret_values(monkeypatch, capsys):\\n+ \\\"\\\"\\\"Operator evidence names credentials and agents but never prints secrets.\\\"\\\"\\\"\\n+ environment = _complete_environment()\\n+ monkeypatch.setattr(os, \\\"environ\\\", environment)\\n+ monkeypatch.setattr(\\n+ provider_bootstrap,\\n+ \\\"discover_all_models\\\",\\n+ lambda: (\\n+ [_model(\\\"openai\\\", \\\"OPENAI_API_KEY\\\", \\\"gpt-test\\\", 1.0)],\\n+ [],\\n+ ),\\n+ )\\n+ provider_bootstrap.main([\\\"--model-limit\\\", \\\"1\\\"])\\n+ output = capsys.readouterr().out\\n+ report = json.loads(output)\\n+ assert \\\"OPENAI_API_KEY\\\" in output\\n+ assert report[\\\"eligible_model_count\\\"] == 1\\n+ assert report[\\\"selected_agent_ids\\\"] == [\\\"openai_gpt_test\\\"]\\n+ assert report[\\\"enabled_agent_ids\\\"] == []\\n+ assert report[\\\"durable_agent_pool\\\"] is False\\n+ for value in environment.values():\\n+ assert value.strip() not in output\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"70ea8ecc7e83442b115b97847523e3c26484689a\", \"filename\": \"tests/test_provider_bootstrap_secret_normalization.py\", \"status\": \"added\", \"additions\": 71, \"deletions\": 0, \"changes\": 71, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_bootstrap_secret_normalization.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_bootstrap_secret_normalization.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_bootstrap_secret_normalization.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,71 @@\\n+\\\"\\\"\\\"Regression coverage for mounted provider-secret normalization.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator.credentials import (\\n+ InMemoryCredentialBackend,\\n+ get_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.provider_bootstrap import (\\n+ PROVIDER_CREDENTIAL_NAMES,\\n+ collect_provider_credentials,\\n+ register_provider_credentials_atomically,\\n+)\\n+\\n+\\n+@pytest.fixture(autouse=True)\\n+def isolated_credential_backend():\\n+ \\\"\\\"\\\"Give each test a fresh process-local credential registry.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ yield\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+def _complete_environment() -> dict[str, str]:\\n+ \\\"\\\"\\\"Return a complete mounted-secret inventory.\\\"\\\"\\\"\\n+ return {name: f\\\"secret-for-{name.lower()}\\\\n\\\" for name in PROVIDER_CREDENTIAL_NAMES}\\n+\\n+\\n+def test_collection_removes_only_mounted_line_endings() -> None:\\n+ \\\"\\\"\\\"Do not silently rewrite other credential bytes while removing CR/LF mounts.\\\"\\\"\\\"\\n+ environment = _complete_environment()\\n+ environment[\\\"OPENAI_API_KEY\\\"] = \\\" edge-sensitive-secret \\\\r\\\\n\\\"\\n+\\n+ collected = collect_provider_credentials(environment)\\n+\\n+ assert collected[\\\"OPENAI_API_KEY\\\"] == \\\" edge-sensitive-secret \\\"\\n+ assert collected[\\\"BYTEZ_API_KEY\\\"] == \\\"secret-for-bytez_api_key\\\"\\n+\\n+\\n+def test_atomic_registration_preserves_normalized_secret_bytes() -> None:\\n+ \\\"\\\"\\\"The atomic backend write must not perform a second broad whitespace trim.\\\"\\\"\\\"\\n+ credentials = {\\n+ name: f\\\"secret-for-{name.lower()}\\\"\\n+ for name in PROVIDER_CREDENTIAL_NAMES\\n+ }\\n+ credentials[\\\"OPENROUTER_API_KEY\\\"] = \\\" edge-sensitive-router-secret \\\"\\n+\\n+ register_provider_credentials_atomically(credentials)\\n+\\n+ assert get_credential(\\\"OPENROUTER_API_KEY\\\") == \\\" edge-sensitive-router-secret \\\"\\n+\\n+\\n+def test_catalog_sync_leak_guard_matches_secret_normalization() -> None:\\n+ \\\"\\\"\\\"The workflow checks the exact credential bytes that bootstrap handles.\\\"\\\"\\\"\\n+ workflow = Path(\\\".github/workflows/provider-catalog-sync.yml\\\").read_text(\\n+ encoding=\\\"utf-8\\\"\\n+ )\\n+\\n+ assert \\\"os.environ[name].rstrip('\\\\\\\\r\\\\\\\\n')\\\" in workflow\\n+ assert \\\"os.environ[name] and os.environ[name] in report\\\" not in workflow\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"02f93564c6bf711ca56a16f342bdbd24ddfba144\", \"filename\": \"tests/test_provider_catalog_bootstrap.py\", \"status\": \"added\", \"additions\": 175, \"deletions\": 0, \"changes\": 175, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_catalog_bootstrap.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_catalog_bootstrap.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_catalog_bootstrap.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,175 @@\\n+\\\"\\\"\\\"End-to-end durable provider catalog bootstrap contracts.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator.credentials import (\\n+ InMemoryCredentialBackend,\\n+ get_credential,\\n+ register_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.model_discovery import (\\n+ DiscoveredModel,\\n+ ProviderDiscoveryError,\\n+ ProviderModelSource,\\n+)\\n+from contextual_orchestrator.provider_bootstrap import PROVIDER_CREDENTIAL_NAMES\\n+from contextual_orchestrator.provider_catalog_bootstrap import (\\n+ bootstrap_provider_catalog_runtime,\\n+)\\n+from contextual_orchestrator.provider_catalog_store import (\\n+ InMemoryProviderCatalogStore,\\n+)\\n+\\n+\\n+def _environment() -> dict[str, str]:\\n+ return {\\n+ name: f\\\"value-for-{name.casefold()}\\\"\\n+ for name in PROVIDER_CREDENTIAL_NAMES\\n+ }\\n+\\n+\\n+def _source(provider: str, credential: str) -> ProviderModelSource:\\n+ return ProviderModelSource(\\n+ provider_name=provider,\\n+ credential_name=credential,\\n+ list_url=f\\\"https://{provider}.example/v1/models\\\",\\n+ chat_base_url=f\\\"https://{provider}.example/v1\\\",\\n+ )\\n+\\n+\\n+def _model(source: ProviderModelSource, model_id: str) -> DiscoveredModel:\\n+ return DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=model_id,\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ prompt_price_per_1k=1.0,\\n+ completion_price_per_1k=2.0,\\n+ )\\n+\\n+\\n+def test_failed_provider_uses_persisted_last_known_good_model() -> None:\\n+ \\\"\\\"\\\"A later provider outage keeps its last successful compatible model.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ openai = _source(\\\"openai\\\", \\\"OPENAI_API_KEY\\\")\\n+ openrouter = _source(\\\"openrouter\\\", \\\"OPENROUTER_API_KEY\\\")\\n+ store = InMemoryProviderCatalogStore()\\n+\\n+ first = bootstrap_provider_catalog_runtime(\\n+ environ=_environment(),\\n+ catalog_store=store,\\n+ sources=(openai, openrouter),\\n+ discovery=lambda _sources: (\\n+ [_model(openai, \\\"gpt-live\\\"), _model(openrouter, \\\"router-live\\\")],\\n+ [],\\n+ ),\\n+ model_limit=4,\\n+ )\\n+ assert first.catalog_model_count == 2\\n+ assert first.last_known_good_model_count == 0\\n+\\n+ second = bootstrap_provider_catalog_runtime(\\n+ environ=_environment(),\\n+ catalog_store=store,\\n+ sources=(openai, openrouter),\\n+ discovery=lambda _sources: (\\n+ [_model(openrouter, \\\"router-new\\\")],\\n+ [ProviderDiscoveryError(\\\"openai\\\", \\\"secret-bearing detail\\\")],\\n+ ),\\n+ model_limit=4,\\n+ )\\n+ assert second.live_discovered_model_count == 1\\n+ assert second.catalog_model_count == 2\\n+ assert second.last_known_good_model_count == 1\\n+ assert second.catalog_refresh_failure_count == 1\\n+ assert second.providers_with_errors == (\\\"openai\\\",)\\n+ assert set(second.selected_agent_ids) == {\\n+ \\\"openai_gpt_live\\\",\\n+ \\\"openrouter_router_new\\\",\\n+ }\\n+ assert \\\"secret-bearing detail\\\" not in str(second.as_dict())\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+def test_empty_catalog_preserves_lkg_but_nonchat_success_withdraws_it() -> None:\\n+ \\\"\\\"\\\"Empty refresh is failure; authoritative non-chat success is withdrawal.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ openai = _source(\\\"openai\\\", \\\"OPENAI_API_KEY\\\")\\n+ store = InMemoryProviderCatalogStore()\\n+ bootstrap_provider_catalog_runtime(\\n+ environ=_environment(),\\n+ catalog_store=store,\\n+ sources=(openai,),\\n+ discovery=lambda _sources: ([_model(openai, \\\"gpt-live\\\")], []),\\n+ model_limit=1,\\n+ )\\n+\\n+ empty = bootstrap_provider_catalog_runtime(\\n+ environ=_environment(),\\n+ catalog_store=store,\\n+ sources=(openai,),\\n+ discovery=lambda _sources: ([], []),\\n+ model_limit=1,\\n+ )\\n+ assert empty.last_known_good_model_count == 1\\n+ assert empty.catalog_model_count == 1\\n+\\n+ try:\\n+ bootstrap_provider_catalog_runtime(\\n+ environ=_environment(),\\n+ catalog_store=store,\\n+ sources=(openai,),\\n+ discovery=lambda _sources: (\\n+ [_model(openai, \\\"text-embedding-3-small\\\")],\\n+ [],\\n+ ),\\n+ model_limit=1,\\n+ )\\n+ except RuntimeError as error:\\n+ assert \\\"no persisted chat-compatible model\\\" in str(error)\\n+ else:\\n+ raise AssertionError(\\\"non-chat-only authoritative catalog must fail\\\")\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+def test_unexpected_discovery_failure_restores_entire_credential_inventory() -> None:\\n+ \\\"\\\"\\\"An unclassified bootstrap failure must not leave unvalidated secrets promoted.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ previous = {\\n+ name: f\\\"previous-value-for-{name.casefold()}\\\"\\n+ for name in PROVIDER_CREDENTIAL_NAMES\\n+ }\\n+ for name, value in previous.items():\\n+ register_credential(name, value)\\n+\\n+ def fail_discovery(_sources):\\n+ raise RuntimeError(\\\"unexpected discovery parser failure\\\")\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"unexpected discovery parser failure\\\"):\\n+ bootstrap_provider_catalog_runtime(\\n+ environ=_environment(),\\n+ catalog_store=InMemoryProviderCatalogStore(),\\n+ sources=(_source(\\\"openai\\\", \\\"OPENAI_API_KEY\\\"),),\\n+ discovery=fail_discovery,\\n+ model_limit=1,\\n+ )\\n+\\n+ assert {\\n+ name: get_credential(name)\\n+ for name in PROVIDER_CREDENTIAL_NAMES\\n+ } == previous\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"2a8e019ac52f508dc68b4a3fc1fee7350a4052fe\", \"filename\": \"tests/test_provider_catalog_credential_promotion.py\", \"status\": \"added\", \"additions\": 196, \"deletions\": 0, \"changes\": 196, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_catalog_credential_promotion.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_catalog_credential_promotion.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_catalog_credential_promotion.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,196 @@\\n+\\\"\\\"\\\"Regression coverage for provider credential promotion around catalog refresh.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator.credentials import (\\n+ InMemoryCredentialBackend,\\n+ get_credential,\\n+ register_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.model_discovery import (\\n+ DiscoveredModel,\\n+ ProviderDiscoveryError,\\n+ ProviderModelSource,\\n+)\\n+from contextual_orchestrator.provider_bootstrap import ProviderBootstrapError\\n+from contextual_orchestrator.provider_catalog_bootstrap import (\\n+ bootstrap_provider_catalog_runtime,\\n+)\\n+from contextual_orchestrator.provider_catalog_store import (\\n+ InMemoryProviderCatalogStore,\\n+)\\n+\\n+\\n+@pytest.fixture(autouse=True)\\n+def isolated_credential_backend():\\n+ \\\"\\\"\\\"Give every promotion test a fresh credential registry.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ yield\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+def _source() -> ProviderModelSource:\\n+ return ProviderModelSource(\\n+ provider_name=\\\"openai\\\",\\n+ credential_name=\\\"OPENAI_API_KEY\\\",\\n+ list_url=\\\"https://api.openai.example/v1/models\\\",\\n+ chat_base_url=\\\"https://api.openai.example/v1\\\",\\n+ )\\n+\\n+\\n+def _model(source: ProviderModelSource, model_id: str) -> DiscoveredModel:\\n+ return DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=model_id,\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ prompt_price_per_1k=1.0,\\n+ completion_price_per_1k=2.0,\\n+ )\\n+\\n+\\n+def _seed_last_known_good(\\n+ store: InMemoryProviderCatalogStore,\\n+ source: ProviderModelSource,\\n+) -> None:\\n+ model = _model(source, \\\"gpt-last-known-good\\\")\\n+ store.record_success(\\n+ source,\\n+ [model],\\n+ eligible_model_ids={model.model_id},\\n+ serving_tags={model.model_id: (\\\"discovered\\\", \\\"chat\\\", \\\"worker\\\")},\\n+ )\\n+\\n+\\n+def test_failed_refresh_restores_previous_credential_before_using_lkg() -> None:\\n+ \\\"\\\"\\\"An invalid candidate key must not replace the key paired with LKG models.\\\"\\\"\\\"\\n+ source = _source()\\n+ store = InMemoryProviderCatalogStore()\\n+ _seed_last_known_good(store, source)\\n+ register_credential(source.credential_name, \\\"old-working-secret\\\")\\n+\\n+ def failing_discovery(_sources):\\n+ assert get_credential(source.credential_name) == \\\"new-invalid-secret\\\"\\n+ return [], [ProviderDiscoveryError(source.provider_name, \\\"unauthorized\\\")]\\n+\\n+ report = bootstrap_provider_catalog_runtime(\\n+ environ={source.credential_name: \\\"new-invalid-secret\\\"},\\n+ require_all_credentials=False,\\n+ catalog_store=store,\\n+ sources=(source,),\\n+ discovery=failing_discovery,\\n+ model_limit=1,\\n+ )\\n+\\n+ assert get_credential(source.credential_name) == \\\"old-working-secret\\\"\\n+ assert report.selected_agent_ids == (\\\"openai_gpt_last_known_good\\\",)\\n+ assert report.restored_credentials == (source.credential_name,)\\n+\\n+\\n+def test_empty_refresh_restores_previous_credential_before_using_lkg() -> None:\\n+ \\\"\\\"\\\"An empty candidate-key catalog is failure, not credential promotion.\\\"\\\"\\\"\\n+ source = _source()\\n+ store = InMemoryProviderCatalogStore()\\n+ _seed_last_known_good(store, source)\\n+ register_credential(source.credential_name, \\\"old-working-secret\\\")\\n+\\n+ report = bootstrap_provider_catalog_runtime(\\n+ environ={source.credential_name: \\\"new-empty-catalog-secret\\\"},\\n+ require_all_credentials=False,\\n+ catalog_store=store,\\n+ sources=(source,),\\n+ discovery=lambda _sources: ([], []),\\n+ model_limit=1,\\n+ )\\n+\\n+ assert get_credential(source.credential_name) == \\\"old-working-secret\\\"\\n+ assert report.selected_agent_ids == (\\\"openai_gpt_last_known_good\\\",)\\n+ assert report.restored_credentials == (source.credential_name,)\\n+\\n+\\n+def test_failed_first_promotion_cannot_activate_lkg_without_a_prior_credential() -> None:\\n+ \\\"\\\"\\\"Persisted models are unusable when the candidate key failed and no old key exists.\\\"\\\"\\\"\\n+ source = _source()\\n+ store = InMemoryProviderCatalogStore()\\n+ _seed_last_known_good(store, source)\\n+\\n+ with pytest.raises(\\n+ ProviderBootstrapError,\\n+ match=\\\"no persisted chat-compatible model with a usable credential\\\",\\n+ ):\\n+ bootstrap_provider_catalog_runtime(\\n+ environ={source.credential_name: \\\"first-invalid-secret\\\"},\\n+ require_all_credentials=False,\\n+ catalog_store=store,\\n+ sources=(source,),\\n+ discovery=lambda _sources: (\\n+ [],\\n+ [ProviderDiscoveryError(source.provider_name, \\\"unauthorized\\\")],\\n+ ),\\n+ model_limit=1,\\n+ )\\n+\\n+ assert get_credential(source.credential_name) is None\\n+\\n+\\n+def test_report_excludes_first_promotion_credential_removed_by_rollback() -> None:\\n+ \\\"\\\"\\\"Durable-registration evidence cannot claim a deleted first candidate key.\\\"\\\"\\\"\\n+ openai = _source()\\n+ openrouter = ProviderModelSource(\\n+ provider_name=\\\"openrouter\\\",\\n+ credential_name=\\\"OPENROUTER_API_KEY\\\",\\n+ list_url=\\\"https://openrouter.example/v1/models\\\",\\n+ chat_base_url=\\\"https://openrouter.example/v1\\\",\\n+ )\\n+ live = _model(openrouter, \\\"router-live\\\")\\n+\\n+ report = bootstrap_provider_catalog_runtime(\\n+ environ={\\n+ openai.credential_name: \\\"first-invalid-secret\\\",\\n+ openrouter.credential_name: \\\"working-router-secret\\\",\\n+ },\\n+ require_all_credentials=False,\\n+ catalog_store=InMemoryProviderCatalogStore(),\\n+ sources=(openai, openrouter),\\n+ discovery=lambda _sources: (\\n+ [live],\\n+ [ProviderDiscoveryError(openai.provider_name, \\\"temporary discovery failure\\\")],\\n+ ),\\n+ model_limit=1,\\n+ )\\n+\\n+ assert report.restored_credentials == (openai.credential_name,)\\n+ assert report.registered_credentials == (openrouter.credential_name,)\\n+ assert get_credential(openai.credential_name) is None\\n+ assert get_credential(openrouter.credential_name) == \\\"working-router-secret\\\"\\n+\\n+\\n+def test_successful_refresh_promotes_the_candidate_credential() -> None:\\n+ \\\"\\\"\\\"A validated non-empty catalog commits the new provider credential.\\\"\\\"\\\"\\n+ source = _source()\\n+ store = InMemoryProviderCatalogStore()\\n+ register_credential(source.credential_name, \\\"old-working-secret\\\")\\n+ live = _model(source, \\\"gpt-new-live\\\")\\n+\\n+ report = bootstrap_provider_catalog_runtime(\\n+ environ={source.credential_name: \\\"new-working-secret\\\"},\\n+ require_all_credentials=False,\\n+ catalog_store=store,\\n+ sources=(source,),\\n+ discovery=lambda _sources: ([live], []),\\n+ model_limit=1,\\n+ )\\n+\\n+ assert get_credential(source.credential_name) == \\\"new-working-secret\\\"\\n+ assert report.selected_agent_ids == (\\\"openai_gpt_new_live\\\",)\\n+ assert report.restored_credentials == ()\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"35459edd4d7c559546abfbd47c8ffb98705c036b\", \"filename\": \"tests/test_provider_catalog_store.py\", \"status\": \"added\", \"additions\": 316, \"deletions\": 0, \"changes\": 316, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_catalog_store.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/9c4535e1a9503a6d65cec07a398607f6413f910a/tests%2Ftest_provider_catalog_store.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_catalog_store.py?ref=9c4535e1a9503a6d65cec07a398607f6413f910a\", \"patch\": \"@@ -0,0 +1,316 @@\\n+\\\"\\\"\\\"Provider catalog persistence and last-known-good contracts.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from dataclasses import replace\\n+from decimal import Decimal\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator.model_discovery import (\\n+ DiscoveredModel,\\n+ ProviderModelSource,\\n+ _currency_is_comparable,\\n+)\\n+from contextual_orchestrator.provider_catalog_store import (\\n+ InMemoryProviderCatalogStore,\\n+ PostgresProviderCatalogStore,\\n+ PROVIDER_CATALOG_SCHEMA_SQL,\\n+ ProviderCatalogError,\\n+ normalize_discovered_model,\\n+ provider_account_id,\\n+)\\n+\\n+\\n+def _source(\\n+ provider: str = \\\"nvidia_nim\\\",\\n+ credential: str = \\\"NVIDIA_NIM_API_KEY\\\",\\n+) -> ProviderModelSource:\\n+ return ProviderModelSource(\\n+ provider_name=provider,\\n+ credential_name=credential,\\n+ list_url=f\\\"https://{provider}.example/v1/models\\\",\\n+ chat_base_url=f\\\"https://{provider}.example/v1\\\",\\n+ )\\n+\\n+\\n+def _model(\\n+ source: ProviderModelSource,\\n+ model_id: str,\\n+ prompt_price: object = 1.0,\\n+) -> DiscoveredModel:\\n+ return DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=model_id,\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ prompt_price_per_1k=prompt_price,\\n+ completion_price_per_1k=prompt_price,\\n+ currency_code=\\\"usd\\\",\\n+ )\\n+\\n+\\n+def test_schema_is_normalized_and_contains_no_secret_value_column() -> None:\\n+ \\\"\\\"\\\"Catalog DDL keeps accounts, models, tags, and refresh evidence separate.\\\"\\\"\\\"\\n+ for table in (\\n+ \\\"provider_account\\\",\\n+ \\\"provider_model\\\",\\n+ \\\"model_serving_tag\\\",\\n+ \\\"catalog_refresh_run\\\",\\n+ ):\\n+ assert f\\\"CREATE TABLE IF NOT EXISTS {table}\\\" in PROVIDER_CATALOG_SCHEMA_SQL\\n+ lowered = PROVIDER_CATALOG_SCHEMA_SQL.casefold()\\n+ assert \\\"api_key\\\" not in lowered\\n+ assert \\\"secret_value\\\" not in lowered\\n+ assert \\\"encrypted_value\\\" not in lowered\\n+ model_table = lowered.split(\\n+ \\\"create table if not exists provider_model (\\\", 1\\n+ )[1].split(\\\");\\\", 1)[0]\\n+ assert \\\"chat_base_url\\\" not in model_table\\n+ assert \\\"auth_scheme\\\" not in model_table\\n+\\n+\\n+def test_primary_and_secondary_nim_accounts_have_distinct_ids() -> None:\\n+ \\\"\\\"\\\"Two NIM credentials remain independent quota and failure domains.\\\"\\\"\\\"\\n+ primary = _source(credential=\\\"NVIDIA_NIM_API_KEY\\\")\\n+ secondary = _source(\\n+ provider=\\\"nvidia_nim_sub\\\",\\n+ credential=\\\"NVIDIA_NIM_API_KEY_SUB\\\",\\n+ )\\n+ assert provider_account_id(primary) != provider_account_id(secondary)\\n+\\n+\\n+def test_model_normalization_rejects_cross_account_rows_and_bad_prices() -> None:\\n+ \\\"\\\"\\\"Catalog normalization is account-bound and never stores non-finite prices.\\\"\\\"\\\"\\n+ source = _source()\\n+ wrong = _model(\\n+ _source(provider=\\\"openai\\\", credential=\\\"OPENAI_API_KEY\\\"),\\n+ \\\"gpt-test\\\",\\n+ )\\n+ with pytest.raises(ProviderCatalogError, match=\\\"different account\\\"):\\n+ normalize_discovered_model(source, wrong)\\n+\\n+ normalized = normalize_discovered_model(\\n+ source,\\n+ _model(source, \\\" model-a \\\", float(\\\"nan\\\")),\\n+ )\\n+ assert normalized.model_id == \\\"model-a\\\"\\n+ assert normalized.prompt_price_per_1k is None\\n+ assert normalized.completion_price_per_1k is None\\n+ assert normalized.currency_code == \\\"USD\\\"\\n+\\n+\\n+def test_underflowing_positive_price_is_rejected_not_treated_as_free() -> None:\\n+ \\\"\\\"\\\"A nonzero price that underflows to 0.0 in float must stay unknown.\\\"\\\"\\\"\\n+ source = _source()\\n+ normalized = normalize_discovered_model(\\n+ source, _model(source, \\\"underflow-model\\\", \\\"1e-10000\\\")\\n+ )\\n+ assert normalized.prompt_price_per_1k is None\\n+ assert normalized.completion_price_per_1k is None\\n+\\n+\\n+def test_overflowing_price_is_rejected_not_treated_as_infinite() -> None:\\n+ \\\"\\\"\\\"A Decimal-finite price whose float() conversion overflows to inf must stay unknown.\\\"\\\"\\\"\\n+ source = _source()\\n+ normalized = normalize_discovered_model(\\n+ source, _model(source, \\\"overflow-model\\\", \\\"1e10000\\\")\\n+ )\\n+ assert normalized.prompt_price_per_1k is None\\n+ assert normalized.completion_price_per_1k is None\\n+\\n+\\n+def test_unrecognized_currency_is_preserved_as_unknown_not_coerced_to_usd() -> None:\\n+ \\\"\\\"\\\"A priced model with an unverifiable currency must not rank as comparable USD.\\\"\\\"\\\"\\n+ source = _source()\\n+ garbage_currency = replace(\\n+ _model(source, \\\"mystery-currency-model\\\"), currency_code=\\\"not a currency\\\"\\n+ )\\n+ normalized = normalize_discovered_model(source, garbage_currency)\\n+ assert normalized.prompt_price_per_1k == 1.0\\n+ assert normalized.currency_code != \\\"USD\\\"\\n+ assert not _currency_is_comparable(normalized.currency_code, \\\"USD\\\")\\n+\\n+\\n+def test_success_replaces_current_rows_and_failure_keeps_last_known_good() -> None:\\n+ \\\"\\\"\\\"A failed refresh cannot erase the last successful serving model set.\\\"\\\"\\\"\\n+ store = InMemoryProviderCatalogStore()\\n+ source = _source()\\n+ store.record_success(\\n+ source,\\n+ [_model(source, \\\"model-a\\\"), _model(source, \\\"model-b\\\")],\\n+ eligible_model_ids={\\\"model-a\\\"},\\n+ serving_tags={\\\"model-a\\\": (\\\"discovered\\\", \\\"chat\\\", \\\"chat\\\")},\\n+ )\\n+ assert [model.model_id for model in store.serving_models(source)] == [\\n+ \\\"model-a\\\"\\n+ ]\\n+ assert store.serving_tags(source, \\\"model-a\\\") == (\\\"discovered\\\", \\\"chat\\\")\\n+\\n+ store.record_failure(source, error_code=\\\"provider_timeout: secret-token\\\")\\n+ assert [model.model_id for model in store.serving_models(source)] == [\\n+ \\\"model-a\\\"\\n+ ]\\n+ assert store.refresh_evidence()[-1].error_code == \\\"unknown_error\\\"\\n+\\n+ store.record_success(\\n+ source,\\n+ [_model(source, \\\"model-c\\\")],\\n+ eligible_model_ids={\\\"model-c\\\"},\\n+ serving_tags={\\\"model-c\\\": (\\\"discovered\\\", \\\"chat\\\")},\\n+ )\\n+ assert [model.model_id for model in store.serving_models(source)] == [\\n+ \\\"model-c\\\"\\n+ ]\\n+ assert [item.refresh_status for item in store.refresh_evidence()] == [\\n+ \\\"succeeded\\\",\\n+ \\\"failed\\\",\\n+ \\\"succeeded\\\",\\n+ ]\\n+\\n+\\n+class _FakeCursor:\\n+ \\\"\\\"\\\"Minimal DB-API cursor recording parameterized catalog statements.\\\"\\\"\\\"\\n+\\n+ def __init__(self, rows=None) -> None:\\n+ self.calls: list[tuple[str, object]] = []\\n+ self.rows = list(rows or [])\\n+\\n+ def __enter__(self):\\n+ return self\\n+\\n+ def __exit__(self, *_args) -> None:\\n+ return None\\n+\\n+ def execute(self, statement: str, params=None) -> None:\\n+ self.calls.append((statement, params))\\n+\\n+ def fetchall(self):\\n+ return list(self.rows)\\n+\\n+\\n+class _FakeConnection:\\n+ \\\"\\\"\\\"Minimal transaction object exercising the PostgreSQL adapter.\\\"\\\"\\\"\\n+\\n+ def __init__(self, rows=None) -> None:\\n+ self.cursor_object = _FakeCursor(rows)\\n+ self.commits = 0\\n+\\n+ def __enter__(self):\\n+ return self\\n+\\n+ def __exit__(self, *_args) -> None:\\n+ return None\\n+\\n+ def cursor(self):\\n+ return self.cursor_object\\n+\\n+ def commit(self) -> None:\\n+ self.commits += 1\\n+\\n+\\n+def test_postgres_success_is_parameterized_and_failure_does_not_disable_lkg() -> None:\\n+ \\\"\\\"\\\"PostgreSQL success replaces rows; failure records evidence only.\\\"\\\"\\\"\\n+ source = _source()\\n+ connections: list[_FakeConnection] = []\\n+\\n+ def factory():\\n+ connection = _FakeConnection()\\n+ connections.append(connection)\\n+ return connection\\n+\\n+ store = PostgresProviderCatalogStore(\\n+ \\\"postgresql://catalog.example/db\\\",\\n+ connection_factory=factory,\\n+ )\\n+ store.record_success(\\n+ source,\\n+ [_model(source, \\\"model-a\\\")],\\n+ eligible_model_ids={\\\"model-a\\\"},\\n+ serving_tags={\\\"model-a\\\": (\\\"discovered\\\", \\\"chat\\\")},\\n+ )\\n+ success_sql = \\\"\\\\n\\\".join(\\n+ statement for statement, _params in connections[-1].cursor_object.calls\\n+ )\\n+ assert \\\"UPDATE provider_model SET enabled_flag = false\\\" in success_sql\\n+ assert \\\"INSERT INTO model_serving_tag\\\" in success_sql\\n+ assert connections[-1].commits >= 1\\n+\\n+ store.record_failure(source, error_code=\\\"provider_timeout: secret-token\\\")\\n+ failure_sql = \\\"\\\\n\\\".join(\\n+ statement for statement, _params in connections[-1].cursor_object.calls\\n+ )\\n+ assert \\\"UPDATE provider_model SET enabled_flag = false\\\" not in failure_sql\\n+ assert \\\"INSERT INTO catalog_refresh_run\\\" in failure_sql\\n+ assert store.refresh_evidence()[-1].error_code == \\\"unknown_error\\\"\\n+\\n+\\n+def test_postgres_success_clears_tags_account_wide_not_per_current_model() -> None:\\n+ \\\"\\\"\\\"A model absent from a fresh refresh cannot leave orphaned serving_tag rows.\\\"\\\"\\\"\\n+ source = _source()\\n+ connection = _FakeConnection()\\n+ store = PostgresProviderCatalogStore(\\n+ \\\"postgresql://catalog.example/db\\\",\\n+ connection_factory=lambda: connection,\\n+ )\\n+ store.record_success(\\n+ source,\\n+ [_model(source, \\\"model-a\\\")],\\n+ eligible_model_ids={\\\"model-a\\\"},\\n+ serving_tags={\\\"model-a\\\": (\\\"discovered\\\", \\\"chat\\\")},\\n+ )\\n+ statements = [statement for statement, _params in connection.cursor_object.calls]\\n+ tag_delete_index = next(\\n+ i for i, s in enumerate(statements) if \\\"DELETE FROM model_serving_tag\\\" in s\\n+ )\\n+ tag_insert_index = next(\\n+ i for i, s in enumerate(statements) if \\\"INSERT INTO model_serving_tag\\\" in s\\n+ )\\n+ assert \\\"WHERE provider_model_id IN\\\" in statements[tag_delete_index]\\n+ assert \\\"WHERE provider_account_id = %s\\\" in statements[tag_delete_index]\\n+ assert tag_delete_index < tag_insert_index\\n+ assert statements.count(\\n+ \\\"DELETE FROM model_serving_tag WHERE provider_model_id = %s\\\"\\n+ ) == 0\\n+\\n+\\n+def test_postgres_serving_models_reconstructs_account_scoped_rows() -> None:\\n+ \\\"\\\"\\\"Read-side rows become normalized DiscoveredModel records.\\\"\\\"\\\"\\n+ source = _source(provider=\\\"openrouter\\\", credential=\\\"OPENROUTER_API_KEY\\\")\\n+ connection = _FakeConnection(\\n+ [\\n+ (\\n+ \\\"model-b\\\",\\n+ source.chat_base_url,\\n+ \\\"Bearer\\\",\\n+ Decimal(\\\"0.25\\\"),\\n+ Decimal(\\\"0.50\\\"),\\n+ \\\"usd\\\",\\n+ )\\n+ ]\\n+ )\\n+ store = PostgresProviderCatalogStore(\\n+ \\\"postgresql://catalog.example/db\\\",\\n+ connection_factory=lambda: connection,\\n+ )\\n+ assert store.serving_models(source) == [\\n+ DiscoveredModel(\\n+ provider_name=\\\"openrouter\\\",\\n+ model_id=\\\"model-b\\\",\\n+ credential_name=\\\"OPENROUTER_API_KEY\\\",\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=\\\"Bearer\\\",\\n+ prompt_price_per_1k=0.25,\\n+ completion_price_per_1k=0.5,\\n+ currency_code=\\\"USD\\\",\\n+ )\\n+ ]\\n+ query, params = connection.cursor_object.calls[-1]\\n+ assert \\\"JOIN provider_account AS pa\\\" in query\\n+ assert \\\"serving_eligible_flag = true\\\" in query\\n+ assert params == (provider_account_id(source),)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" } ]" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-08-24T07-48-05-833Z.yml b/.playwright-mcp/page-2026-08-24T07-48-05-833Z.yml deleted file mode 100644 index 6cd64140a..000000000 --- a/.playwright-mcp/page-2026-08-24T07-48-05-833Z.yml +++ /dev/null @@ -1 +0,0 @@ -- generic [active] [ref=f4e1]: "[ { \"sha\": \"b19e63e9d9c3c8e8b4e849356b1968ff7ea00986\", \"filename\": \".github/workflows/tests.yml\", \"status\": \"modified\", \"additions\": 6, \"deletions\": 4, \"changes\": 10, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/.github%2Fworkflows%2Ftests.yml\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/.github%2Fworkflows%2Ftests.yml\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/.github%2Fworkflows%2Ftests.yml?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -29,10 +29,12 @@ jobs:\\n python-version: \\\"3.12\\\"\\n \\n - name: Install test dependencies\\n- # Hash-pinned per OpenSSF Scorecard Pinned-Dependencies. Reuses the\\n- # property-test lockfile (pytest + hypothesis), which covers the full\\n- # suite's requirements: the package itself is stdlib-only.\\n- run: python -m pip install --require-hashes -r fuzz/requirements-property.txt\\n+ # Hash-pinned per OpenSSF Scorecard Pinned-Dependencies. Install the\\n+ # runtime lock before the property-test lock so optional integrations\\n+ # such as OpenTelemetry are exercised instead of silently disabled.\\n+ run: |\\n+ python -m pip install --require-hashes -r requirements.lock\\n+ python -m pip install --require-hashes -r fuzz/requirements-property.txt\\n \\n - name: Run full test suite\\n run: python -m pytest -q\" }, { \"sha\": \"9f198bb2fb55479e52ac1977bbb2ce33eb7715c8\", \"filename\": \"AGENTS.md\", \"status\": \"modified\", \"additions\": 23, \"deletions\": 0, \"changes\": 23, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/AGENTS.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/AGENTS.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/AGENTS.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -119,4 +119,27 @@ push or open a PR.\\n cost-optimal LLM routing, upstream load balancing, and latency/throughput\\n scheduling (e.g. LLM-cascade / model-routing and queueing/load-balancing\\n papers).\\n+\\n+### Model and reasoning policy\\n+\\n+- Model selection, reasoning-effort allocation, orchestration topology, and\\n+ claims about quality/cost trade-offs must be grounded in cited academic\\n+ papers and current capability evidence. Do not introduce a model policy from\\n+ a vendor blog, benchmark marketing claim, or an implementation convention.\\n+ The governing decision is [ADR 0013](docs/planning/adrs/0013-paper-grounded-adaptive-reasoning-policy.md).\\n+- `auto` is an orchestrator policy, not a provider `reasoning_effort` value. It\\n+ may select a provider-supported value or an orchestrated multi-agent path,\\n+ but the trace must retain the requested policy and the effective strategy.\\n+- Provider values are capability-negotiated. Do not send `none`, `minimal`,\\n+ `low`, `medium`, `high`, or `xhigh` unless the selected provider advertises\\n+ that value; omit the field for a non-reasoning provider. Never infer support\\n+ from a model name.\\n+- `high` and `xhigh` may require multiple independent attempts, verification,\\n+ and synthesis when one worker cannot provide the requested capability. That\\n+ is an orchestrator strategy, not a claim that a non-reasoning worker became a\\n+ reasoning model.\\n+- MLX is not a public provider contract. Keep runtime-specific local model\\n+ behavior behind an authenticated provider-neutral gateway as specified by\\n+ ADR 0012; do not add direct `mlx://` configuration, transport, or model\\n+ selection logic.\\n \" }, { \"sha\": \"defc3f2e94e7d1f0e210a5b30f888798e9f0c353\", \"filename\": \"README.md\", \"status\": \"modified\", \"additions\": 14, \"deletions\": 9, \"changes\": 23, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/README.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/README.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/README.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -66,38 +66,37 @@ Use real workers by replacing `mock://` agents with OpenAI-compatible endpoints.\\n }\\n ```\\n \\n-For a local `mlx-lm` OpenAI-compatible server, use the explicit `mlx://` scheme. It is loopback-only, does not require a credential, and is translated to HTTP only after the loopback check:\\n+For a local OpenAI-compatible gateway, use the explicit `local://` loopback scheme and name its separate KV credential. Provider-specific runtime settings stay behind that gateway:\\n \\n ```json\\n {\\n \\\"agents\\\": [\\n {\\n- \\\"id\\\": \\\"local_fast_agent\\\",\\n- \\\"model\\\": \\\"mlx-community/llama-3.2-3b-instruct-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n+ \\\"id\\\": \\\"local_gateway_agent\\\",\\n+ \\\"model\\\": \\\"gateway-selected-model\\\",\\n+ \\\"base_url\\\": \\\"local://127.0.0.1:8080/v1\\\",\\n+ \\\"local_credential_key\\\": \\\"LOCAL_GATEWAY_TOKEN\\\",\\n \\\"tags\\\": [\\\"reasoning\\\", \\\"coding\\\", \\\"verification\\\"]\\n }\\n ]\\n }\\n ```\\n \\n The full local candidate registry is [examples/agents.local.json](examples/agents.local.json).\\n-It contains the public `contextual-orchestrator` candidate, discovered MLX\\n-worker models, and every discovered llama.cpp/LM Studio candidate. Discovery\\n+It contains the public `contextual-orchestrator` candidate and generic local\\n+gateway/llama.cpp/LM Studio candidates. Discovery\\n does not decide governance state: seed candidates are enabled by default, while\\n `disabled` is reserved for an explicit operator/admin quarantine or a persisted\\n removal tombstone. The contextual-orchestrator record is excluded from internal\\n roles because this implementation has no bounded recursive self-call protocol;\\n that is a routing safety constraint, not a disabled candidate. The registry is\\n explicit; runtime discovery does not silently change the pool.\\n \\n-Run an evaluation against that server with `--temperature 0` for repeatable judging. For reasoning-capable mlx models, pass `--chat-template-args '{\\\"enable_thinking\\\":false}'` when a short structured judge response is required. `--local-concurrency N` enables bounded concurrent local batch requests (`1..64`; the current measured starting point for this server is `8`); when serving HTTP, set `--max-concurrent-runs N` explicitly as well if the measured batch concurrency exceeds the secure default of `8`. Keep interactive route/conduct requests on the default sequential path.\\n+Run an evaluation against that gateway with the normal provider capability contract. `--local-concurrency N` enables bounded concurrent local batch requests (`1..64`); when serving HTTP, set `--max-concurrent-runs N` explicitly as well if the measured batch concurrency exceeds the secure default of `8`. Keep interactive route/conduct requests on the default sequential path.\\n \\n Model-based conduct verification requires `fast-mlsirm` in the same runtime and fails closed when it is absent or broken; fast-mlsirm sends its judge completion through this contextual-orchestrator gateway, so no direct provider fallback is used. “Same runtime” means that the exact interpreter used for the live run can import both packages: install both checkouts into one environment (prefer editable installs), or expose both source roots with `PYTHONPATH` during a source run. Before a live judge benchmark, run `python -m contextual_orchestrator check-fast-mlsirm` with that exact interpreter. It prints the interpreter, package version, transitive-import status, and contextual contract check, and exits nonzero on a missing dependency or contract mismatch. Do not run the preflight in one virtual environment and the judge in another. See [ADR 0001](docs/planning/adrs/0001-fail-closed-model-judgment.md).\\n \\n 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.\\n-\\n 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) 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.\\n \\n Seed the credential into the KV once at bootstrap:\\n@@ -304,6 +303,12 @@ python tests/test_admin_contract.py\\n python tests/test_conventions.py\\n python tests/test_api_contract.py\\n python tests/test_security_hardening.py\\n+python tests/test_chat_model_capability_isolation.py\\n+python tests/test_chat_transport_role_separation.py\\n+python tests/test_chat_capability_unknown_identifiers.py\\n+python tests/test_chat_passthrough_capability_isolation.py\\n+python tests/test_inbound_request_framing.py\\n+python tests/test_inbound_request_total_deadline.py\\n python tests/test_repository_security_metadata.py\\n python tests/test_product_planning_contract.py\\n python tests/test_plugin_driven_artifacts.py\" }, { \"sha\": \"d00cff4624b7daf9946e5ccd793dd30ed4fefffc\", \"filename\": \"conductor/tracks/001-paper-grounded-orchestrator/spec.md\", \"status\": \"modified\", \"additions\": 2, \"deletions\": 0, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/conductor%2Ftracks%2F001-paper-grounded-orchestrator%2Fspec.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/conductor%2Ftracks%2F001-paper-grounded-orchestrator%2Fspec.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/conductor%2Ftracks%2F001-paper-grounded-orchestrator%2Fspec.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -5,6 +5,8 @@\\n - The public API accepts chat messages through one OpenAI-compatible endpoint.\\n - Simple prompts use one selected worker.\\n - Complex prompts use a workflow with thinker, worker, verifier, and synthesizer steps.\\n+- Image-bearing prompts keep their validated source image blocks in every\\n+ evidence-bearing step and use only explicitly VISION-capable agents.\\n - Workflow steps expose only the prior outputs listed in their access list.\\n - Operators can open a management console to inspect agent pool, policy, trace, and audit state.\\n - Tests encode the Fugu, TRINITY, and Conductor contracts.\" }, { \"sha\": \"f7e43c2a82e5e818a2b7460c9791b378ec49e3e4\", \"filename\": \"contextual_orchestrator/__init__.py\", \"status\": \"modified\", \"additions\": 6, \"deletions\": 1, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2F__init__.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2F__init__.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2F__init__.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -37,7 +37,12 @@\\n from .cost_router import CostRoutingCoordinator\\n from .credentials import NotConfigured, get_credential, register_credential\\n from .kv_config import InMemoryConfigStore, get_config_store\\n-from .orchestrator import ModelAgent, TaskOrchestrator, WorkflowStep, load_agents\\n+from .orchestrator import (\\n+ ModelAgent,\\n+ WorkflowStep,\\n+ load_agents,\\n+)\\n+from .passthrough_failover import TaskOrchestrator\\n from .token_counting import HeuristicTokenCounter, build_token_counter\\n \\n __all__ = [\" }, { \"sha\": \"850aa27dab97ca4fc2faf3cdfc64f33a6b7edd23\", \"filename\": \"contextual_orchestrator/__main__.py\", \"status\": \"modified\", \"additions\": 116, \"deletions\": 22, \"changes\": 138, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2F__main__.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2F__main__.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2F__main__.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -4,17 +4,22 @@\\n \\n import argparse\\n import json\\n+import math\\n import os\\n import sys\\n from dataclasses import replace\\n \\n from .cost_ledger import PriceBook\\n+from .cost_router import CostRoutingCoordinator\\n from .credentials import get_credential, register_credential\\n from .kv_config import InMemoryConfigStore\\n from .model_discovery import (\\n+ ProviderDiscoveryError,\\n+ ProviderModelSource,\\n agent_from_discovered,\\n agent_id_for,\\n discover_all_models,\\n+ discover_provider_models,\\n refresh_price_book,\\n select_top_n_cheapest_discovered_agents,\\n )\\n@@ -23,16 +28,30 @@\\n MAX_LOCAL_CONCURRENCY,\\n ModelAgent,\\n ModelClient,\\n- TaskOrchestrator,\\n load_agents,\\n )\\n+from .passthrough_failover import TaskOrchestrator\\n from .server import SecurityConfig, serve\\n \\n DEFAULT_AUTH_TOKEN_KEY = \\\"CONTEXTUAL_ORCHESTRATOR_TOKEN\\\"\\n DEFAULT_ADMIN_TOKEN_KEY = \\\"CONTEXTUAL_ORCHESTRATOR_ADMIN_TOKEN\\\"\\n DEFAULT_INFERENCE_TOKEN_KEY = \\\"CONTEXTUAL_ORCHESTRATOR_INFERENCE_TOKEN\\\"\\n \\n \\n+def _bootstrap_telemetry_config() -> InMemoryConfigStore:\\n+ \\\"\\\"\\\"Load non-secret OTEL deployment settings into the process KV at startup.\\\"\\\"\\\"\\n+ config = InMemoryConfigStore()\\n+ for environment_name, key in (\\n+ (\\\"OTEL_EXPORTER_OTLP_ENDPOINT\\\", \\\"exporter_otlp_endpoint\\\"),\\n+ (\\\"OTEL_SERVICE_NAME\\\", \\\"service_name\\\"),\\n+ (\\\"OTEL_SDK_DISABLED\\\", \\\"sdk_disabled\\\"),\\n+ ):\\n+ value = os.environ.get(environment_name, \\\"\\\").strip()\\n+ if value:\\n+ config.set(\\\"telemetry\\\", key, value)\\n+ return config\\n+\\n+\\n def _positive_int(value: str) -> int:\\n \\\"\\\"\\\"Parse a strictly positive integer for an argparse option.\\\"\\\"\\\"\\n try:\\n@@ -65,14 +84,14 @@ def _local_concurrency(value: str) -> int:\\n return parsed\\n \\n \\n-def _json_object(value: str) -> dict[str, object]:\\n- \\\"\\\"\\\"Parse a JSON object for an argparse option, rejecting other JSON values.\\\"\\\"\\\"\\n+def _request_read_timeout(value: str) -> float:\\n+ \\\"\\\"\\\"Parse a finite request-body deadline in the server-supported range.\\\"\\\"\\\"\\n try:\\n- parsed = json.loads(value)\\n- except json.JSONDecodeError as exc:\\n- raise argparse.ArgumentTypeError(\\\"valid JSON object required\\\") from exc\\n- if not isinstance(parsed, dict):\\n- raise argparse.ArgumentTypeError(\\\"JSON object required\\\")\\n+ parsed = float(value)\\n+ except ValueError as exc:\\n+ raise argparse.ArgumentTypeError(\\\"number in 0.1..120 required\\\") from exc\\n+ if not math.isfinite(parsed) or not 0.1 <= parsed <= 120.0:\\n+ raise argparse.ArgumentTypeError(\\\"number in 0.1..120 required\\\")\\n return parsed\\n \\n \\n@@ -249,15 +268,59 @@ def _discover_models_command(argv: list[str]) -> None:\\n raise SystemExit(1)\\n \\n \\n-def main() -> None:\\n+def _auto_discover_seed_agents(\\n+ agents: list[ModelAgent], *, allow_failures: bool\\n+) -> list[ModelAgent]:\\n+ \\\"\\\"\\\"Expand empty-model gateway seeds without selecting a model in the consumer.\\\"\\\"\\\"\\n+ expanded: list[ModelAgent] = []\\n+ for seed in agents:\\n+ if seed.model:\\n+ expanded.append(seed)\\n+ continue\\n+ source = ProviderModelSource(\\n+ provider_name=seed.provider_name or seed.id,\\n+ credential_name=seed.credential_name,\\n+ list_url=f\\\"{seed.base_url.rstrip('/')}/models\\\",\\n+ chat_base_url=seed.base_url,\\n+ auth_scheme=seed.auth_scheme,\\n+ )\\n+ try:\\n+ discovered = discover_provider_models(source)\\n+ except ProviderDiscoveryError:\\n+ if not allow_failures:\\n+ raise\\n+ expanded.append(replace(seed, disabled=True))\\n+ continue\\n+ # OpenAI-compatible registries do not always declare task capabilities;\\n+ # embedding deployments cannot serve the chat worker pool.\\n+ chat_models = [model for model in discovered if \\\"embedding\\\" not in model.model_id.casefold()]\\n+ if not chat_models:\\n+ expanded.append(replace(seed, disabled=True))\\n+ continue\\n+ for model in chat_models:\\n+ discovered_agent = agent_from_discovered(model, priority=seed.priority)\\n+ expanded.append(\\n+ replace(\\n+ discovered_agent,\\n+ id=f\\\"{seed.id}_{agent_id_for(model)}\\\",\\n+ tags=seed.tags,\\n+ disabled=seed.disabled,\\n+ provider_exclusions=seed.provider_exclusions,\\n+ )\\n+ )\\n+ return expanded\\n+\\n+\\n+def main(argv: list[str] | None = None) -> None:\\n \\\"\\\"\\\"Parse CLI options and run bootstrap, prompt completion, or the HTTP server.\\\"\\\"\\\"\\n- if len(sys.argv) > 1 and sys.argv[1] == \\\"register-credential\\\":\\n- _register_credential_command(sys.argv[2:])\\n+ arguments = list(sys.argv[1:] if argv is None else argv)\\n+ if arguments and arguments[0] == \\\"register-credential\\\":\\n+ _register_credential_command(arguments[1:])\\n return\\n- if len(sys.argv) > 1 and sys.argv[1] == \\\"discover-models\\\":\\n- _discover_models_command(sys.argv[2:])\\n+ if arguments and arguments[0] == \\\"discover-models\\\":\\n+ _discover_models_command(arguments[1:])\\n return\\n- if len(sys.argv) > 1 and sys.argv[1] == \\\"check-fast-mlsirm\\\":\\n+ if arguments and arguments[0] == \\\"check-fast-mlsirm\\\":\\n _check_fast_mlsirm_command()\\n return\\n \\n@@ -268,6 +331,16 @@ def main() -> None:\\n help=\\\"Optional sqlite path to persist runs/audit/analytics across restarts (default: in-memory).\\\")\\n parser.add_argument(\\\"--mode\\\", choices=[\\\"auto\\\", \\\"route\\\", \\\"conduct\\\"], default=\\\"auto\\\")\\n parser.add_argument(\\\"--serve\\\", action=\\\"store_true\\\", help=\\\"Run the chat completions HTTP server.\\\")\\n+ parser.add_argument(\\n+ \\\"--auto-discover-model-agents\\\",\\n+ action=\\\"store_true\\\",\\n+ help=\\\"Expand empty-model agents from their configured OpenAI-compatible /models endpoint.\\\",\\n+ )\\n+ parser.add_argument(\\n+ \\\"--allow-discovery-failures\\\",\\n+ action=\\\"store_true\\\",\\n+ help=\\\"Keep failed discovery seeds disabled instead of aborting startup.\\\",\\n+ )\\n parser.add_argument(\\\"--host\\\", default=\\\"127.0.0.1\\\")\\n parser.add_argument(\\\"--port\\\", type=int, default=8000)\\n parser.add_argument(\\\"--auth-token\\\", default=\\\"\\\", help=\\\"Explicit local-development bearer token; prefer a KV token name.\\\")\\n@@ -295,21 +368,31 @@ def main() -> None:\\n \\\"--temperature\\\",\\n dest=\\\"sampling_temperature\\\",\\n type=float,\\n- default=0.2,\\n- help=\\\"Default provider sampling temperature (default: 0.2; --temperature is a compatibility alias).\\\",\\n+ default=None,\\n+ help=\\\"Optional provider sampling temperature; omitted by default (--temperature is an alias).\\\",\\n )\\n parser.add_argument(\\\"--max-output-tokens\\\", type=int, default=2048,\\n help=\\\"Default provider output token cap (default: 2048).\\\")\\n+ parser.add_argument(\\n+ \\\"--max-body-bytes\\\",\\n+ type=_positive_int,\\n+ default=64 * 1024,\\n+ help=\\\"Maximum JSON request body size in bytes (default: 65536).\\\",\\n+ )\\n+ parser.add_argument(\\n+ \\\"--request-read-timeout-seconds\\\",\\n+ type=_request_read_timeout,\\n+ default=10.0,\\n+ help=\\\"Maximum time to read one fixed-length JSON body (default: 10 seconds).\\\",\\n+ )\\n parser.add_argument(\\\"--local-concurrency\\\", type=_local_concurrency, default=1,\\n- help=f\\\"Concurrent requests for explicit mlx:// local batch work (default: 1; maximum: {MAX_LOCAL_CONCURRENCY}).\\\")\\n+ help=f\\\"Concurrent requests for local gateway batch work (default: 1; maximum: {MAX_LOCAL_CONCURRENCY}).\\\")\\n parser.add_argument(\\\"--max-concurrent-runs\\\", type=_local_concurrency, default=8,\\n help=f\\\"Maximum simultaneous HTTP orchestration runs (default: 8; maximum: {MAX_LOCAL_CONCURRENCY}).\\\")\\n parser.add_argument(\\\"--route-text-length-threshold\\\", type=_positive_int, default=None,\\n help=\\\"Auto-mode minimum prompt length that can trigger conduct instead of route.\\\")\\n parser.add_argument(\\\"--conduct-hint-threshold\\\", type=_positive_int, default=None,\\n help=\\\"Auto-mode hint-count minimum that can trigger conduct instead of route.\\\")\\n- parser.add_argument(\\\"--chat-template-args\\\", type=_json_object, default={},\\n- help=\\\"JSON kwargs forwarded to local mlx-lm chat templates, e.g. '{\\\\\\\"enable_thinking\\\\\\\":false}'.\\\")\\n parser.add_argument(\\\"--budget-max-output-tokens\\\", type=int, default=None,\\n help=\\\"Refuse new runs once estimated/reported output tokens reach this cap (default: no cap).\\\")\\n parser.add_argument(\\\"--budget-max-cost-usd\\\", type=float, default=None,\\n@@ -318,18 +401,23 @@ def main() -> None:\\n help=\\\"Seconds to cache identical requests (default 0 = disabled).\\\")\\n parser.add_argument(\\\"--eval\\\", nargs=\\\"+\\\", metavar=\\\"PROMPT\\\",\\n help=\\\"Measure orchestration vs a single-worker baseline on these prompts and print the report.\\\")\\n- args = parser.parse_args()\\n+ args = parser.parse_args(arguments)\\n \\n client = ModelClient(\\n ca_bundle=args.provider_ca_bundle,\\n temperature=args.sampling_temperature,\\n max_output_tokens=args.max_output_tokens,\\n local_concurrency=args.local_concurrency,\\n- chat_template_args=args.chat_template_args,\\n allowed_provider_hosts=args.allowed_provider_hosts,\\n )\\n+ agents = load_agents(args.agents)\\n+ if args.auto_discover_model_agents:\\n+ try:\\n+ agents = _auto_discover_seed_agents(agents, allow_failures=args.allow_discovery_failures)\\n+ except (ProviderDiscoveryError, ValueError) as exc:\\n+ parser.error(str(exc))\\n orchestrator = TaskOrchestrator(\\n- load_agents(args.agents),\\n+ agents,\\n client=client,\\n state_db=args.state_db,\\n agents_db=args.agents_db,\\n@@ -396,8 +484,14 @@ def main() -> None:\\n max_concurrent_runs=args.max_concurrent_runs,\\n allow_public_bind=args.allow_public_bind,\\n expose_trace_by_default=args.expose_trace_by_default,\\n+ max_body_bytes=args.max_body_bytes,\\n+ request_read_timeout_seconds=args.request_read_timeout_seconds,\\n ),\\n clearfolio_url=args.clearfolio_url,\\n+ coordinator=CostRoutingCoordinator(\\n+ orchestrator,\\n+ config_store=_bootstrap_telemetry_config(),\\n+ ),\\n )\\n return\\n \" }, { \"sha\": \"454cd7620e28ccbec1f95da9990f7327ba9121ae\", \"filename\": \"contextual_orchestrator/api_contract.py\", \"status\": \"modified\", \"additions\": 16, \"deletions\": 5, \"changes\": 21, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fapi_contract.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fapi_contract.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fapi_contract.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -111,17 +111,20 @@\\n \\\"/v1/embeddings\\\": {\\n \\\"post\\\": {\\n \\\"operationId\\\": \\\"create_embedding\\\",\\n- \\\"summary\\\": \\\"Create embeddings for semantic input\\\",\\n+ \\\"summary\\\": \\\"Create embeddings with optional orchestrator-owned model selection\\\",\\n \\\"security\\\": [{\\\"inference_bearer_auth\\\": []}],\\n \\\"requestBody\\\": {\\n \\\"required\\\": True,\\n \\\"content\\\": {\\n \\\"application/json\\\": {\\n \\\"schema\\\": {\\n \\\"type\\\": \\\"object\\\",\\n- \\\"required\\\": [\\\"model\\\", \\\"input\\\"],\\n+ \\\"required\\\": [\\\"input\\\"],\\n \\\"properties\\\": {\\n- \\\"model\\\": {\\\"type\\\": \\\"string\\\"},\\n+ \\\"model\\\": {\\n+ \\\"type\\\": \\\"string\\\",\\n+ \\\"description\\\": \\\"Optional enabled embedding-capable pool model; omitted selects one.\\\",\\n+ },\\n \\\"input\\\": {\\n \\\"oneOf\\\": [\\n {\\\"type\\\": \\\"string\\\"},\\n@@ -136,6 +139,7 @@\\n \\\"responses\\\": {\\n \\\"200\\\": {\\\"description\\\": \\\"Embedding response\\\"},\\n \\\"400\\\": {\\\"description\\\": \\\"Invalid request\\\"},\\n+ \\\"503\\\": {\\\"description\\\": \\\"No enabled embedding-capable agent is available\\\"},\\n },\\n }\\n },\\n@@ -163,6 +167,7 @@\\n \\\"responses\\\": {\\n \\\"200\\\": {\\\"description\\\": \\\"Responses API result\\\"},\\n \\\"400\\\": {\\\"description\\\": \\\"Invalid request\\\"},\\n+ \\\"422\\\": {\\\"description\\\": \\\"Valid request shape with unsupported orchestration controls\\\"},\\n },\\n }\\n },\\n@@ -587,9 +592,15 @@\\n \\\"application/json\\\": {\\n \\\"schema\\\": {\\n \\\"type\\\": \\\"object\\\",\\n- \\\"required\\\": [\\\"model\\\"],\\n+ \\\"anyOf\\\": [\\n+ {\\\"required\\\": [\\\"input\\\"]},\\n+ {\\\"required\\\": [\\\"inputs\\\"]},\\n+ ],\\n \\\"properties\\\": {\\n- \\\"model\\\": {\\\"type\\\": \\\"string\\\"},\\n+ \\\"model\\\": {\\n+ \\\"type\\\": \\\"string\\\",\\n+ \\\"description\\\": \\\"Optional enabled embedding-capable pool model; omitted selects one.\\\",\\n+ },\\n \\\"input\\\": {\\n \\\"oneOf\\\": [\\n {\\\"type\\\": \\\"string\\\"},\" }, { \"sha\": \"68a2af371d7925b096d1d9b5278c428a9c4bbd5d\", \"filename\": \"contextual_orchestrator/batch_routing.py\", \"status\": \"modified\", \"additions\": 23, \"deletions\": 2, \"changes\": 25, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fbatch_routing.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fbatch_routing.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fbatch_routing.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -26,6 +26,7 @@\\n import time\\n import uuid\\n from concurrent.futures import ThreadPoolExecutor\\n+from contextvars import copy_context\\n from dataclasses import dataclass, field\\n from typing import Any, Callable, Dict, List, Optional, Protocol\\n \\n@@ -253,8 +254,13 @@ def run(request: BatchRequest) -> BatchResultItem:\\n if self.max_concurrency == 1 or len(requests) <= 1:\\n items = [run(request) for request in requests]\\n else:\\n+ def run_with_context(item: tuple[Any, BatchRequest]) -> BatchResultItem:\\n+ context, request = item\\n+ return context.run(run, request)\\n+\\n+ contexts_and_requests = [(copy_context(), request) for request in requests]\\n with ThreadPoolExecutor(max_workers=min(self.max_concurrency, len(requests))) as pool:\\n- items = list(pool.map(run, requests))\\n+ items = list(pool.map(run_with_context, contexts_and_requests))\\n self._results[job_id] = items\\n return BatchJob(job_id=job_id, backend=self.name, status=\\\"completed\\\", request_count=len(requests))\\n \\n@@ -411,6 +417,7 @@ class EmbeddingBatchRequest:\\n \\n input_text: str\\n model: str = \\\"contextual-orchestrator\\\"\\n+ provider_name: str = \\\"unknown\\\"\\n custom_id: str = field(default_factory=lambda: f\\\"emb_{uuid.uuid4().hex}\\\")\\n attribution: Dict[str, Any] = field(default_factory=dict)\\n source_index: int = 0\\n@@ -437,6 +444,7 @@ class EmbeddingBatchResultItem:\\n embedding: List[float]\\n prompt_tokens: int = 0\\n model: str = \\\"contextual-orchestrator\\\"\\n+ provider_name: str = \\\"unknown\\\"\\n \\n \\n class EmbeddingBatchBackend(Protocol):\\n@@ -493,10 +501,12 @@ def __init__(\\n self,\\n embedder: Optional[Callable[[str], List[float]]] = None,\\n *,\\n+ batch_embedder: Optional[Callable[[List[EmbeddingBatchRequest]], List[List[float]]]] = None,\\n token_counter: Any = None,\\n dimension: int = _DEFAULT_EMBEDDING_DIMENSION,\\n ) -> None:\\n self._embedder = embedder or (lambda text: heuristic_embedding(text, dimension))\\n+ self._batch_embedder = batch_embedder\\n self._token_counter = token_counter\\n self._results: Dict[str, List[EmbeddingBatchResultItem]] = {}\\n \\n@@ -511,15 +521,23 @@ def submit(\\n ) -> BatchJob:\\n \\\"\\\"\\\"Embed every input in-process and stash the results under a job id.\\\"\\\"\\\"\\n job_id = f\\\"localembed_{uuid.uuid4().hex}\\\"\\n+ vectors = (\\n+ self._batch_embedder(requests)\\n+ if self._batch_embedder is not None\\n+ else [self._embedder(request.input_text) for request in requests]\\n+ )\\n+ if len(vectors) != len(requests):\\n+ raise RuntimeError(\\\"embedding provider returned an incomplete vector batch\\\")\\n items: List[EmbeddingBatchResultItem] = []\\n for index, request in enumerate(requests):\\n items.append(\\n EmbeddingBatchResultItem(\\n custom_id=request.custom_id,\\n index=index,\\n- embedding=list(self._embedder(request.input_text)),\\n+ embedding=list(vectors[index]),\\n prompt_tokens=self._count_tokens(request.input_text, request.model),\\n model=request.model,\\n+ provider_name=request.provider_name,\\n )\\n )\\n self._results[job_id] = items\\n@@ -639,6 +657,9 @@ async def _download() -> Dict[str, Any]:\\n embedding=embedding,\\n prompt_tokens=int(usage.get(\\\"prompt_tokens\\\", 0)),\\n model=tracked_request.model if tracked_request else \\\"contextual-orchestrator\\\",\\n+ provider_name=(\\n+ tracked_request.provider_name if tracked_request else \\\"unknown\\\"\\n+ ),\\n )\\n )\\n items.sort(key=lambda item: item.index)\" }, { \"sha\": \"4a2a96cd560621fcc8099010ec759a2f4f75be40\", \"filename\": \"contextual_orchestrator/chat_capability.py\", \"status\": \"added\", \"additions\": 96, \"deletions\": 0, \"changes\": 96, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fchat_capability.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fchat_capability.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fchat_capability.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,96 @@\\n+\\\"\\\"\\\"Classify chat transport compatibility and ordinary agent-role eligibility.\\n+\\n+Provider catalogs mix endpoint-only models with models served through an\\n+OpenAI-compatible chat transport. Transport compatibility is not the same as\\n+fitness for an ordinary thinker, worker, verifier, or synthesizer role: audio\\n+and policy-classification models can use chat transport, while embedding,\\n+reranking, transcription, moderation-endpoint, image-generation, realtime, and\\n+speech-only models cannot.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import re\\n+\\n+_MODEL_TOKEN_RE = re.compile(r\\\"[a-z0-9]+\\\")\\n+_TRANSPORT_INCOMPATIBLE_EXACT_TOKENS = frozenset(\\n+ {\\n+ \\\"bge\\\",\\n+ \\\"clip\\\",\\n+ \\\"dall\\\",\\n+ \\\"e5\\\",\\n+ \\\"embed\\\",\\n+ \\\"embedding\\\",\\n+ \\\"embeddings\\\",\\n+ \\\"gte\\\",\\n+ \\\"image\\\",\\n+ \\\"images\\\",\\n+ \\\"moderation\\\",\\n+ \\\"realtime\\\",\\n+ \\\"rerank\\\",\\n+ \\\"reranker\\\",\\n+ \\\"siglip\\\",\\n+ \\\"sora\\\",\\n+ \\\"speech\\\",\\n+ \\\"transcribe\\\",\\n+ \\\"transcription\\\",\\n+ \\\"tts\\\",\\n+ \\\"whisper\\\",\\n+ }\\n+)\\n+_TRANSPORT_INCOMPATIBLE_PREFIXES = (\\n+ \\\"embed\\\",\\n+ \\\"moderat\\\",\\n+ \\\"rerank\\\",\\n+ \\\"transcrib\\\",\\n+)\\n+\\n+\\n+def is_chat_compatible_model_id(model_id: str) -> bool:\\n+ \\\"\\\"\\\"Return whether an identifier can use the ordinary chat transport.\\n+\\n+ The classifier rejects only identifiers that clearly advertise an endpoint\\n+ family incompatible with chat messages. Audio-capable and safety-classifier\\n+ models remain transport-compatible because providers serve some of them over\\n+ ``/chat/completions``.\\n+ \\\"\\\"\\\"\\n+ tokens = _model_tokens(model_id)\\n+ return _is_transport_compatible_tokens(tokens)\\n+\\n+\\n+def _is_transport_compatible_tokens(tokens: tuple[str, ...]) -> bool:\\n+ \\\"\\\"\\\"Judge transport compatibility from already-normalized model tokens.\\\"\\\"\\\"\\n+ if not tokens:\\n+ return False\\n+ for token in tokens:\\n+ if token in _TRANSPORT_INCOMPATIBLE_EXACT_TOKENS:\\n+ return False\\n+ if token.startswith(_TRANSPORT_INCOMPATIBLE_PREFIXES):\\n+ return False\\n+ return True\\n+\\n+\\n+def _model_tokens(model_id: str) -> tuple[str, ...]:\\n+ \\\"\\\"\\\"Normalize one provider-prefixed model identifier into lowercase tokens.\\\"\\\"\\\"\\n+ if not isinstance(model_id, str):\\n+ return ()\\n+ return tuple(_MODEL_TOKEN_RE.findall(model_id.casefold()))\\n+\\n+\\n+def is_general_chat_agent_model_id(model_id: str) -> bool:\\n+ \\\"\\\"\\\"Return whether a chat model may enter ordinary orchestration roles.\\n+\\n+ Explicit guard and safety models can use chat transport but are specialized\\n+ policy classifiers, not general answer synthesizers. This negative role gate\\n+ does not infer reasoning, coding, vision, or verification capabilities.\\n+ \\\"\\\"\\\"\\n+ tokens = _model_tokens(model_id)\\n+ if not tokens or not _is_transport_compatible_tokens(tokens):\\n+ return False\\n+ return not any(\\n+ token == \\\"safety\\\"\\n+ or token == \\\"guard\\\"\\n+ or token == \\\"shieldgemma\\\"\\n+ or token.startswith(\\\"nemoguard\\\")\\n+ for token in tokens\\n+ )\" }, { \"sha\": \"4ce34b6ea106497738b8a3bec3de422a80d7484f\", \"filename\": \"contextual_orchestrator/cost_ledger.py\", \"status\": \"modified\", \"additions\": 6, \"deletions\": 1, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fcost_ledger.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fcost_ledger.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fcost_ledger.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -330,6 +330,7 @@ class NoopUsageTelemetrySink:\\n \\\"\\\"\\\"Default sink for callers that do not wire telemetry yet.\\\"\\\"\\\"\\n \\n def emit_usage(self, event: UsageTelemetryEvent) -> None:\\n+ \\\"\\\"\\\"Discard one prompt-safe usage event.\\\"\\\"\\\"\\n return None\\n \\n \\n@@ -342,12 +343,14 @@ def __init__(self, max_events: int = 512) -> None:\\n self._lock = threading.Lock()\\n \\n def emit_usage(self, event: UsageTelemetryEvent) -> None:\\n+ \\\"\\\"\\\"Append one usage event while retaining only the configured limit.\\\"\\\"\\\"\\n with self._lock:\\n self._events.append(event)\\n if len(self._events) > self._max_events:\\n del self._events[: len(self._events) - self._max_events]\\n \\n def events(self) -> List[UsageTelemetryEvent]:\\n+ \\\"\\\"\\\"Return a thread-safe snapshot of retained usage events.\\\"\\\"\\\"\\n with self._lock:\\n return list(self._events)\\n \\n@@ -363,6 +366,7 @@ class UsageTelemetryHealth:\\n last_error_type: Optional[str] = None\\n \\n def as_dict(self) -> Dict[str, Any]:\\n+ \\\"\\\"\\\"Return operator-safe counters as a serializable mapping.\\\"\\\"\\\"\\n return {\\n \\\"records_accepted\\\": self.records_accepted,\\n \\\"records_stored\\\": self.records_stored,\\n@@ -442,6 +446,7 @@ def flush(self, timeout: Optional[float] = None) -> bool:\\n return True\\n \\n def telemetry_health(self) -> Dict[str, Any]:\\n+ \\\"\\\"\\\"Return a thread-safe snapshot of persistence health counters.\\\"\\\"\\\"\\n with self._lock:\\n return self._health.as_dict()\\n \\n@@ -669,7 +674,7 @@ def query(self, start: Optional[int] = None, end: Optional[int] = None) -> List[\\n _USAGE_QUERY_SQL[(self._paramstyle, start is not None, end is not None)],\\n tuple(params),\\n )\\n- return [dict(zip(_USAGE_COLUMNS, values)) for values in cur.fetchall()]\\n+ return [dict(zip(_USAGE_COLUMNS, values, strict=True)) for values in cur.fetchall()]\\n \\n \\n def _within_window(created_at: int, start: Optional[int], end: Optional[int]) -> bool:\" }, { \"sha\": \"1120b9877f000c96483b5234a6f05742ab68bdca\", \"filename\": \"contextual_orchestrator/cost_router.py\", \"status\": \"modified\", \"additions\": 272, \"deletions\": 31, \"changes\": 303, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fcost_router.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fcost_router.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fcost_router.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -34,7 +34,7 @@\\n RoutingHints,\\n RoutingPolicy,\\n )\\n-from .cost_ledger import CostLedger, PriceBook\\n+from .cost_ledger import CostLedger, PriceBook, UsageRecord\\n from .kv_config import InMemoryConfigStore\\n from .token_counting import HeuristicTokenCounter, build_token_counter\\n \\n@@ -77,21 +77,74 @@ def __init__(\\n )\\n else:\\n self.batch_backend = batch_backend\\n- self.embedding_batch_backend: EmbeddingBatchBackend = (\\n- embedding_batch_backend\\n- or LocalEmbeddingBatchBackend(token_counter=self.token_counter)\\n- )\\n+ self._embedding_backend_is_default = embedding_batch_backend is None\\n+ self.embedding_batch_backend: EmbeddingBatchBackend = embedding_batch_backend or self._default_embedding_backend()\\n # job_id -> submitted BatchJob (so poll/retrieve can be driven by id)\\n self._batch_jobs: Dict[str, BatchJob] = {}\\n # embeddings batch state: job handle + submitted requests + cached doc,\\n # keyed by batch id so poll/retrieve is idempotent (usage recorded once).\\n self._embedding_jobs: Dict[str, BatchJob] = {}\\n+ self._embedding_job_backends: Dict[str, EmbeddingBatchBackend] = {}\\n self._embedding_requests: Dict[str, List[EmbeddingBatchRequest]] = {}\\n self._embedding_input_counts: Dict[str, int] = {}\\n self._embedding_part_counts: Dict[str, List[int]] = {}\\n self._embedding_part_limits: Dict[str, Dict[str, int]] = {}\\n self._embedding_documents: Dict[str, Dict[str, Any]] = {}\\n \\n+ def _default_embedding_backend(self) -> EmbeddingBatchBackend:\\n+ \\\"\\\"\\\"Use a provider-tagged embedding agent when one is configured.\\\"\\\"\\\"\\n+ candidates = [\\n+ agent\\n+ for agent in getattr(self.orchestrator, \\\"candidates\\\", [])\\n+ if not agent.disabled\\n+ and \\\"embedding\\\" in agent.tags\\n+ and not agent.base_url.startswith(\\\"mock://\\\")\\n+ ]\\n+ if not candidates:\\n+ return LocalEmbeddingBatchBackend(token_counter=self.token_counter)\\n+\\n+ def embed_batch(requests: List[EmbeddingBatchRequest]) -> List[List[float]]:\\n+ vectors: List[List[float] | None] = [None] * len(requests)\\n+ provider_models = {\\n+ (request.provider_name, request.model) for request in requests\\n+ }\\n+ for provider_name, model in provider_models:\\n+ matching = [\\n+ agent\\n+ for agent in candidates\\n+ if agent.model == model\\n+ and (agent.provider_name or _provider_from_base_url(agent.base_url) or \\\"unknown\\\")\\n+ == provider_name\\n+ ]\\n+ if not matching:\\n+ raise ValueError(f\\\"embedding model {model!r} is not configured in the embedding agent pool\\\")\\n+ selected = max(matching, key=lambda agent: (agent.priority, agent.id))\\n+ indexes = [\\n+ index\\n+ for index, request in enumerate(requests)\\n+ if request.model == model and request.provider_name == provider_name\\n+ ]\\n+ client = getattr(self.orchestrator, \\\"client\\\", None)\\n+ embed_many = getattr(client, \\\"embed_many\\\", None)\\n+ if not callable(embed_many):\\n+ raise RuntimeError(\\\"configured embedding agent has no provider embedding client\\\")\\n+ batch_vectors = embed_many(\\n+ selected,\\n+ [requests[index].input_text for index in indexes],\\n+ )\\n+ if len(batch_vectors) != len(indexes):\\n+ raise RuntimeError(\\\"provider returned an incomplete embedding vector batch\\\")\\n+ for index, vector in zip(indexes, batch_vectors, strict=True):\\n+ vectors[index] = vector\\n+ if any(vector is None for vector in vectors):\\n+ raise RuntimeError(\\\"provider returned an incomplete embedding vector batch\\\")\\n+ return [vector for vector in vectors if vector is not None]\\n+\\n+ return LocalEmbeddingBatchBackend(\\n+ batch_embedder=embed_batch,\\n+ token_counter=self.token_counter,\\n+ )\\n+\\n # ------------------------------------------------------------------\\n # Provider / model resolution\\n # ------------------------------------------------------------------\\n@@ -110,6 +163,63 @@ def _served_provider_model(self, result: Dict[str, Any], fallback_model: str) ->\\n pass\\n return \\\"unknown\\\", fallback_model\\n \\n+ @staticmethod\\n+ def _provider_usage_counts(usage: Any) -> tuple[int, int] | None:\\n+ \\\"\\\"\\\"Return validated provider token counts, accepting Chat and Responses names.\\\"\\\"\\\"\\n+ if not isinstance(usage, dict):\\n+ return None\\n+ prompt_tokens = usage.get(\\\"prompt_tokens\\\", usage.get(\\\"input_tokens\\\"))\\n+ completion_tokens = usage.get(\\\"completion_tokens\\\", usage.get(\\\"output_tokens\\\"))\\n+ if (\\n+ type(prompt_tokens) is not int\\n+ or prompt_tokens < 0\\n+ or type(completion_tokens) is not int\\n+ or completion_tokens < 0\\n+ ):\\n+ return None\\n+ return prompt_tokens, completion_tokens\\n+\\n+ def _record_provider_workflow_usage(\\n+ self,\\n+ result: Dict[str, Any],\\n+ *,\\n+ attribution: Optional[Dict[str, Any]],\\n+ model_name: str,\\n+ ) -> tuple[List[UsageRecord], int]:\\n+ \\\"\\\"\\\"Record every provider-reported workflow call under one run lineage.\\\"\\\"\\\"\\n+ steps = [step for step in result.get(\\\"trace\\\", []) if isinstance(step, dict)]\\n+ verification = result.get(\\\"verification\\\")\\n+ if isinstance(verification, dict) and isinstance(verification.get(\\\"judge_usage\\\"), dict):\\n+ judge_step = {\\n+ \\\"agent_id\\\": verification.get(\\\"judge_agent_id\\\"),\\n+ \\\"usage\\\": verification[\\\"judge_usage\\\"],\\n+ }\\n+ # Provider-facing synthesis is the final persisted trace step.\\n+ steps.insert(max(0, len(steps) - 1), judge_step)\\n+\\n+ records: List[UsageRecord] = []\\n+ unmetered_count = 0\\n+ for step in steps:\\n+ counts = self._provider_usage_counts(step.get(\\\"usage\\\"))\\n+ if counts is None:\\n+ unmetered_count += 1\\n+ continue\\n+ records.append(\\n+ self._record_completion(\\n+ messages=[],\\n+ answer=\\\"\\\",\\n+ route_mode=result.get(\\\"mode\\\"),\\n+ request_channel=\\\"sync\\\",\\n+ attribution=attribution,\\n+ model_name=model_name,\\n+ provider_model=self._served_provider_model({\\\"trace\\\": [step]}, model_name),\\n+ workflow_run_id=result.get(\\\"workflow_run_id\\\"),\\n+ prompt_tokens=counts[0],\\n+ completion_tokens=counts[1],\\n+ )\\n+ )\\n+ return records, unmetered_count\\n+\\n # ------------------------------------------------------------------\\n # Sync + batch completion\\n # ------------------------------------------------------------------\\n@@ -122,19 +232,28 @@ def complete(\\n hints: Optional[Dict[str, Any]] = None,\\n model_name: str = \\\"contextual-orchestrator\\\",\\n workflow_run_id: Optional[str] = None,\\n+ response_format: Optional[Dict[str, Any]] = None,\\n+ provider_request: Optional[Dict[str, Any]] = None,\\n+ provider_endpoint: str = \\\"chat/completions\\\",\\n ) -> Dict[str, Any]:\\n \\\"\\\"\\\"Route a request (sync or batch) and record its usage + cost.\\n \\n Sync requests run the orchestrator immediately and return the completion\\n augmented with ``channel``, ``routing_reason``, ``usage``, and the\\n ``usage_record_id``. Batch requests are dispatched to the batch backend\\n- and return a job envelope; their cost is recorded on retrieval.\\n+ and return a job envelope; their cost is recorded on retrieval. When a\\n+ validated ``provider_request`` is supplied, final synthesis preserves\\n+ that Chat or Responses wire contract while this coordinator still owns\\n+ the cost record.\\n \\\"\\\"\\\"\\n routing_hints = hints if isinstance(hints, RoutingHints) else RoutingHints.from_mapping(hints)\\n prompt_tokens_estimate = self.token_counter.count_messages(messages, model_name)\\n decision = self.policy.decide(routing_hints, prompt_tokens_estimate)\\n \\n- if decision.channel == \\\"batch\\\":\\n+ structured_output_forced_sync = decision.channel == \\\"batch\\\" and (\\n+ response_format is not None or provider_request is not None\\n+ )\\n+ if decision.channel == \\\"batch\\\" and not structured_output_forced_sync:\\n request = BatchRequest(\\n messages=messages,\\n model=model_name,\\n@@ -151,26 +270,100 @@ def complete(\\n \\\"request_count\\\": job.request_count,\\n }\\n \\n- result = self.orchestrator.run(messages, mode=mode, workflow_run_id=workflow_run_id)\\n- record = self._record_completion(\\n- messages=messages,\\n- answer=result.get(\\\"answer\\\", \\\"\\\"),\\n- route_mode=result.get(\\\"mode\\\"),\\n- request_channel=\\\"sync\\\",\\n- attribution=attribution,\\n- model_name=model_name,\\n- provider_model=self._served_provider_model(result, model_name),\\n- workflow_run_id=result.get(\\\"workflow_run_id\\\"),\\n- )\\n+ provider_response: Optional[Dict[str, Any]] = None\\n+ if provider_request is None:\\n+ result = self.orchestrator.run(\\n+ messages,\\n+ mode=mode,\\n+ workflow_run_id=workflow_run_id,\\n+ output_contract=response_format,\\n+ )\\n+ else:\\n+ if provider_endpoint not in {\\\"chat/completions\\\", \\\"responses\\\"}:\\n+ raise ValueError(\\\"provider_endpoint must be chat/completions or responses\\\")\\n+ provider_response = self.orchestrator.proxy_completion(\\n+ provider_request,\\n+ endpoint=provider_endpoint,\\n+ )\\n+ orchestration = provider_response.get(\\\"orchestration\\\")\\n+ if not isinstance(orchestration, dict) or not isinstance(\\n+ orchestration.get(\\\"workflow_run_id\\\"), str\\n+ ):\\n+ raise RuntimeError(\\\"provider completion omitted orchestration lineage\\\")\\n+ result = dict(\\n+ self.orchestrator.get_workflow_run(orchestration[\\\"workflow_run_id\\\"])\\n+ )\\n+ result[\\\"provider_response\\\"] = provider_response\\n+ records: List[UsageRecord] = []\\n+ unmetered_count = 0\\n+ if provider_response is not None:\\n+ records, unmetered_count = self._record_provider_workflow_usage(\\n+ result,\\n+ attribution=attribution,\\n+ model_name=model_name,\\n+ )\\n+ if not records:\\n+ provider_usage = (\\n+ provider_response.get(\\\"usage\\\")\\n+ if isinstance(provider_response, dict)\\n+ and isinstance(provider_response.get(\\\"usage\\\"), dict)\\n+ else {}\\n+ )\\n+ counts = self._provider_usage_counts(provider_usage)\\n+ record = self._record_completion(\\n+ messages=messages,\\n+ answer=result.get(\\\"answer\\\", \\\"\\\"),\\n+ route_mode=result.get(\\\"mode\\\"),\\n+ request_channel=\\\"sync\\\",\\n+ attribution=attribution,\\n+ model_name=model_name,\\n+ provider_model=self._served_provider_model(result, model_name),\\n+ workflow_run_id=result.get(\\\"workflow_run_id\\\"),\\n+ prompt_tokens=counts[0] if counts is not None else None,\\n+ completion_tokens=counts[1] if counts is not None else None,\\n+ )\\n+ records = [record]\\n+ record = records[-1]\\n+ prompt_tokens = sum(item.prompt_tokens for item in records)\\n+ completion_tokens = sum(item.completion_tokens for item in records)\\n+ currencies = {item.currency_code for item in records}\\n+ cost = {\\n+ \\\"cost_amount\\\": (\\n+ round(sum(item.cost_amount for item in records), 6)\\n+ if len(currencies) == 1\\n+ else None\\n+ ),\\n+ \\\"currency_code\\\": next(iter(currencies)) if len(currencies) == 1 else \\\"MIXED\\\",\\n+ }\\n result[\\\"channel\\\"] = \\\"sync\\\"\\n- result[\\\"routing_reason\\\"] = decision.reason\\n+ result[\\\"routing_reason\\\"] = (\\n+ f\\\"{decision.reason};structured_output_forced_sync\\\"\\n+ if structured_output_forced_sync\\n+ else decision.reason\\n+ )\\n result[\\\"usage_record_id\\\"] = record.usage_record_id\\n+ result[\\\"usage_record_ids\\\"] = [item.usage_record_id for item in records]\\n+ result[\\\"unmetered_provider_call_count\\\"] = unmetered_count\\n result[\\\"usage\\\"] = {\\n- \\\"prompt_tokens\\\": record.prompt_tokens,\\n- \\\"completion_tokens\\\": record.completion_tokens,\\n- \\\"total_tokens\\\": record.total_tokens,\\n+ \\\"prompt_tokens\\\": prompt_tokens,\\n+ \\\"completion_tokens\\\": completion_tokens,\\n+ \\\"total_tokens\\\": prompt_tokens + completion_tokens,\\n }\\n- result[\\\"cost\\\"] = {\\\"cost_amount\\\": record.cost_amount, \\\"currency_code\\\": record.currency_code}\\n+ result[\\\"cost\\\"] = cost\\n+ provider_response = result.get(\\\"provider_response\\\")\\n+ if isinstance(provider_response, dict):\\n+ orchestration = provider_response.get(\\\"orchestration\\\")\\n+ if isinstance(orchestration, dict):\\n+ orchestration.update(\\n+ {\\n+ \\\"channel\\\": result[\\\"channel\\\"],\\n+ \\\"routing_reason\\\": result[\\\"routing_reason\\\"],\\n+ \\\"usage_record_id\\\": result[\\\"usage_record_id\\\"],\\n+ \\\"usage_record_ids\\\": result[\\\"usage_record_ids\\\"],\\n+ \\\"unmetered_provider_call_count\\\": unmetered_count,\\n+ \\\"cost\\\": result[\\\"cost\\\"],\\n+ }\\n+ )\\n return result\\n \\n def _record_completion(\\n@@ -289,22 +482,63 @@ def submit_embeddings_batch(\\n and recorded cost are produced by :meth:`embeddings_batch_document`.\\n \\\"\\\"\\\"\\n shared_attribution = dict(attribution or {})\\n+ provider_name, resolved_model = self._resolve_embedding_provider_model(model)\\n requests, part_counts, part_limits = self._build_embedding_requests(\\n- inputs, model=model, attribution=shared_attribution\\n+ inputs,\\n+ model=resolved_model,\\n+ provider_name=provider_name,\\n+ attribution=shared_attribution,\\n )\\n- job = self.embedding_batch_backend.submit(requests, metadata=metadata)\\n+ backend = (\\n+ self._default_embedding_backend()\\n+ if self._embedding_backend_is_default\\n+ else self.embedding_batch_backend\\n+ )\\n+ job = backend.submit(requests, metadata=metadata)\\n self._embedding_jobs[job.job_id] = job\\n+ self._embedding_job_backends[job.job_id] = backend\\n self._embedding_requests[job.job_id] = requests\\n self._embedding_input_counts[job.job_id] = len(inputs)\\n self._embedding_part_counts[job.job_id] = part_counts\\n self._embedding_part_limits[job.job_id] = part_limits\\n return job\\n \\n+ def _resolve_embedding_provider_model(self, model: str) -> tuple[str, str]:\\n+ \\\"\\\"\\\"Resolve a server-owned embedding provider/model pair.\\\"\\\"\\\"\\n+ candidates = [\\n+ agent\\n+ for agent in getattr(self.orchestrator, \\\"candidates\\\", [])\\n+ if not agent.disabled and \\\"embedding\\\" in agent.tags\\n+ ]\\n+ if model == \\\"contextual-orchestrator\\\":\\n+ selector = getattr(self.orchestrator, \\\"select_capability_agent\\\", None)\\n+ if callable(selector):\\n+ agent = selector(\\\"embedding\\\")\\n+ elif candidates:\\n+ agent = max(\\n+ candidates,\\n+ key=lambda candidate: (candidate.priority, candidate.id),\\n+ )\\n+ else:\\n+ raise ValueError(\\\"no enabled embedding-capable agent is available\\\")\\n+ else:\\n+ matching = [agent for agent in candidates if agent.model == model]\\n+ if not matching:\\n+ if candidates:\\n+ raise ValueError(\\n+ f\\\"embedding model {model!r} is not configured in the embedding agent pool\\\"\\n+ )\\n+ return self.embedding_batch_backend.name, model\\n+ agent = max(matching, key=lambda candidate: (candidate.priority, candidate.id))\\n+ provider = agent.provider_name or _provider_from_base_url(agent.base_url) or \\\"unknown\\\"\\n+ return provider, agent.model\\n+\\n def _build_embedding_requests(\\n self,\\n inputs: List[str],\\n *,\\n model: str,\\n+ provider_name: str,\\n attribution: Dict[str, Any],\\n ) -> tuple[List[EmbeddingBatchRequest], List[int], Dict[str, int]]:\\n \\\"\\\"\\\"Map original embedding inputs into token-budgeted provider parts.\\\"\\\"\\\"\\n@@ -323,6 +557,7 @@ def _build_embedding_requests(\\n EmbeddingBatchRequest(\\n input_text=part_text,\\n model=model,\\n+ provider_name=provider_name,\\n attribution=dict(attribution),\\n source_index=source_index,\\n part_index=part_index,\\n@@ -475,7 +710,8 @@ def embeddings_batch_document(self, batch_id: str) -> Dict[str, Any]:\\n return cached\\n \\n job = self._require_embedding_job(batch_id)\\n- status = self.embedding_batch_backend.poll(job)\\n+ backend = self._embedding_job_backends.get(batch_id, self.embedding_batch_backend)\\n+ status = backend.poll(job)\\n if not status.get(\\\"is_complete\\\"):\\n return {\\n \\\"batch_id\\\": batch_id,\\n@@ -484,7 +720,7 @@ def embeddings_batch_document(self, batch_id: str) -> Dict[str, Any]:\\n \\\"embeddings\\\": None,\\n }\\n \\n- items: List[EmbeddingBatchResultItem] = self.embedding_batch_backend.retrieve(job)\\n+ items: List[EmbeddingBatchResultItem] = backend.retrieve(job)\\n requests = self._embedding_requests.get(batch_id, [])\\n request_by_custom_id = {request.custom_id: request for request in requests}\\n input_count = self._embedding_input_counts.get(batch_id, len(requests))\\n@@ -506,6 +742,11 @@ def embeddings_batch_document(self, batch_id: str) -> Dict[str, Any]:\\n \\\"embedding\\\": item.embedding,\\n \\\"prompt_tokens\\\": max(0, prompt_tokens),\\n \\\"model\\\": item.model,\\n+ \\\"provider_name\\\": (\\n+ item.provider_name\\n+ if item.provider_name != \\\"unknown\\\"\\n+ else request.provider_name if request else \\\"unknown\\\"\\n+ ),\\n \\\"attribution\\\": dict(request.attribution) if request else {},\\n }\\n )\\n@@ -515,6 +756,7 @@ def embeddings_batch_document(self, batch_id: str) -> Dict[str, Any]:\\n total_cost_amount = 0.0\\n currency_code = \\\"USD\\\"\\n model_name = \\\"contextual-orchestrator\\\"\\n+ provider_name = \\\"unknown\\\"\\n for source_index in range(input_count):\\n parts = sorted(parts_by_source.get(source_index, []), key=lambda item: item[\\\"part_index\\\"])\\n if not parts:\\n@@ -524,11 +766,9 @@ def embeddings_batch_document(self, batch_id: str) -> Dict[str, Any]:\\n attribution = dict(parts[0][\\\"attribution\\\"])\\n prompt_tokens = sum(int(part[\\\"prompt_tokens\\\"]) for part in parts)\\n model_name = str(parts[0][\\\"model\\\"])\\n- provider = str(\\n- attribution.get(\\\"provider\\\") or attribution.get(\\\"upstream_api\\\") or \\\"unknown\\\"\\n- )\\n+ provider_name = str(parts[0][\\\"provider_name\\\"])\\n record = self.ledger.record_usage(\\n- provider=provider,\\n+ provider=provider_name,\\n model=model_name,\\n prompt_tokens=prompt_tokens,\\n completion_tokens=0,\\n@@ -553,6 +793,7 @@ def embeddings_batch_document(self, batch_id: str) -> Dict[str, Any]:\\n \\\"batch_id\\\": batch_id,\\n \\\"status\\\": \\\"completed\\\",\\n \\\"backend\\\": job.backend,\\n+ \\\"provider\\\": provider_name,\\n \\\"model\\\": model_name,\\n \\\"embeddings\\\": embeddings,\\n \\\"token_counts\\\": token_counts,\" }, { \"sha\": \"edc331eb2fac4fe45720a45aec3ec94eb187dab1\", \"filename\": \"contextual_orchestrator/model_discovery.py\", \"status\": \"modified\", \"additions\": 95, \"deletions\": 57, \"changes\": 152, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fmodel_discovery.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fmodel_discovery.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fmodel_discovery.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,4 +1,4 @@\\n-\\\"\\\"\\\"Provider model-list discovery: turns registered KV credentials into agent candidates.\\n+\\\"\\\"\\\"Provider model-list discovery for chat-agent candidates.\\n \\n Queries each configured provider's model-list endpoint over its OpenAI-compatible\\n (or provider-specific) discovery API and returns :class:`DiscoveredModel` rows that\\n@@ -9,27 +9,45 @@\\n (the KV registry), and a provider with nothing registered is silently skipped so\\n registering a subset of the five supported keys still works. Stdlib only\\n (``urllib.request``), matching this repo's dependency-free transport convention.\\n+\\n+This module owns the ordinary chat-agent discovery boundary. Provider catalogs may\\n+mix chat, embedding, reranking, transcription, moderation, image, and realtime\\n+models under one ``/models`` endpoint. Clearly non-chat identifiers are rejected\\n+before they can be converted to workers, selected by cost, or persisted into the\\n+chat agent pool.\\n \\\"\\\"\\\"\\n \\n from __future__ import annotations\\n \\n-import json\\n import re\\n import urllib.error\\n-import urllib.request\\n from dataclasses import dataclass\\n from typing import TYPE_CHECKING, Any\\n+from urllib.parse import urlparse\\n \\n-from .batch_routing import cheapest_upstream\\n+from .chat_capability import is_general_chat_agent_model_id\\n from .credentials import get_credential\\n-from .orchestrator import ModelAgent\\n+from .orchestrator import ModelAgent, ModelClient\\n \\n if TYPE_CHECKING:\\n from .cost_ledger import PriceBook\\n \\n DISCOVERY_TIMEOUT_SECONDS = 15.0\\n \\n \\n+def _provider_discovery_error_code(exc: Exception) -> str:\\n+ \\\"\\\"\\\"Map provider failures to stable codes without retaining provider response text.\\\"\\\"\\\"\\n+ if isinstance(exc, urllib.error.HTTPError):\\n+ return f\\\"http_status_{exc.code}\\\"\\n+ if isinstance(exc, TimeoutError):\\n+ return \\\"timeout\\\"\\n+ if isinstance(exc, urllib.error.URLError) or isinstance(exc, OSError):\\n+ return \\\"transport_error\\\"\\n+ if isinstance(exc, ValueError):\\n+ return \\\"invalid_response\\\"\\n+ return \\\"provider_error\\\"\\n+\\n+\\n @dataclass(frozen=True)\\n class ProviderModelSource:\\n \\\"\\\"\\\"Where and how to discover one provider's models.\\\"\\\"\\\"\\n@@ -84,7 +102,7 @@ class ProviderModelSource:\\n \\n @dataclass(frozen=True)\\n class DiscoveredModel:\\n- \\\"\\\"\\\"One model found on a provider, with pricing when the provider reports it.\\\"\\\"\\\"\\n+ \\\"\\\"\\\"One general chat-agent eligible model found on a provider, with pricing.\\\"\\\"\\\"\\n \\n provider_name: str\\n model_id: str\\n@@ -99,26 +117,41 @@ class DiscoveredModel:\\n class ProviderDiscoveryError(RuntimeError):\\n \\\"\\\"\\\"Raised when a provider's model list could not be fetched (network/auth failure).\\\"\\\"\\\"\\n \\n- def __init__(self, provider_name: str, detail: str) -> None:\\n+ def __init__(self, provider_name: str, error_code: str) -> None:\\n self.provider_name = provider_name\\n- super().__init__(f\\\"model discovery failed for provider {provider_name!r}: {detail}\\\")\\n-\\n-\\n-def _fetch_json(url: str, *, api_key: str, auth_scheme: str, timeout: float) -> Any:\\n- if not url.startswith(\\\"https://\\\"):\\n- # Every caller passes one of the hardcoded PROVIDER_SOURCES chat_base_url\\n- # constants below, never external input -- but urlopen also honors\\n- # file:// and other unsafe schemes, so refuse anything not https as a\\n- # cheap invariant check rather than trusting the constant list alone.\\n- raise ValueError(f\\\"refusing non-https model discovery URL: {url!r}\\\")\\n- request = urllib.request.Request(\\n- url,\\n- headers={\\\"authorization\\\": f\\\"{auth_scheme} {api_key}\\\"},\\n- method=\\\"GET\\\",\\n+ self.error_code = error_code\\n+ super().__init__(f\\\"model discovery failed for provider {provider_name!r}: {error_code}\\\")\\n+\\n+\\n+def _fetch_json(\\n+ url: str,\\n+ *,\\n+ auth_scheme: str,\\n+ timeout: float,\\n+ credential_name: str,\\n+) -> Any:\\n+ \\\"\\\"\\\"Fetch a provider catalog through the validated, DNS-pinned transport.\\\"\\\"\\\"\\n+ parsed = urlparse(url)\\n+ if parsed.scheme != \\\"https\\\" or not parsed.hostname:\\n+ raise ValueError(\\\"model discovery requires an https provider URL\\\")\\n+ if parsed.username is not None or parsed.password is not None or \\\"#\\\" in url:\\n+ raise ValueError(\\\"model discovery URL must not contain credentials or a fragment\\\")\\n+ try:\\n+ port = parsed.port\\n+ except ValueError as exc:\\n+ raise ValueError(\\\"model discovery URL has an invalid port\\\") from exc\\n+ origin = f\\\"https://{parsed.hostname}\\\"\\n+ if port not in (None, 443):\\n+ origin = f\\\"{origin}:{port}\\\"\\n+ agent = ModelAgent(\\n+ id=\\\"model_discovery_agent\\\",\\n+ model=\\\"model_catalog\\\",\\n+ base_url=origin,\\n+ credential_key=credential_name,\\n+ auth_scheme=auth_scheme,\\n )\\n- # Scheme is enforced to https:// immediately above; url is never attacker-controlled.\\n- with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 - fixed https provider hosts # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected\\n- return json.loads(response.read().decode(\\\"utf-8\\\"))\\n+ client = ModelClient()\\n+ return client.fetch_json(agent, url, timeout=timeout)\\n \\n \\n def _price_per_1k(value: Any) -> float | None:\\n@@ -132,13 +165,14 @@ def _price_per_1k(value: Any) -> float | None:\\n \\n \\n def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Parse one OpenAI-compatible catalog into general chat-agent candidates.\\\"\\\"\\\"\\n rows = payload.get(\\\"data\\\") if isinstance(payload, dict) else None\\n discovered: list[DiscoveredModel] = []\\n for row in rows if isinstance(rows, list) else []:\\n if not isinstance(row, dict):\\n continue\\n model_id = row.get(\\\"id\\\")\\n- if type(model_id) is not str or not model_id:\\n+ if not is_general_chat_agent_model_id(model_id):\\n continue\\n pricing = row.get(\\\"pricing\\\") if isinstance(row.get(\\\"pricing\\\"), dict) else {}\\n discovered.append(\\n@@ -156,13 +190,14 @@ def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[\\n \\n \\n def _parse_bytez(payload: Any, source: ProviderModelSource) -> list[DiscoveredModel]:\\n+ \\\"\\\"\\\"Parse one Bytez chat catalog without admitting ineligible identifiers.\\\"\\\"\\\"\\n rows = payload.get(\\\"output\\\") if isinstance(payload, dict) else None\\n discovered: list[DiscoveredModel] = []\\n for row in rows if isinstance(rows, list) else []:\\n if not isinstance(row, dict):\\n continue\\n model_id = row.get(\\\"modelId\\\")\\n- if type(model_id) is not str or not model_id:\\n+ if not is_general_chat_agent_model_id(model_id):\\n continue\\n discovered.append(\\n DiscoveredModel(\\n@@ -181,17 +216,22 @@ def _parse_bytez(payload: Any, source: ProviderModelSource) -> list[DiscoveredMo\\n def discover_provider_models(\\n source: ProviderModelSource, *, timeout: float = DISCOVERY_TIMEOUT_SECONDS\\n ) -> list[DiscoveredModel]:\\n- \\\"\\\"\\\"Discover one provider's models, or ``[]`` if its credential is not registered.\\\"\\\"\\\"\\n+ \\\"\\\"\\\"Discover chat candidates, or ``[]`` when the credential is not registered.\\\"\\\"\\\"\\n api_key = get_credential(source.credential_name)\\n if not api_key:\\n return []\\n url = source.list_url\\n if source.task_filter:\\n url = f\\\"{url}?task={source.task_filter}\\\"\\n try:\\n- payload = _fetch_json(url, api_key=api_key, auth_scheme=source.auth_scheme, timeout=timeout)\\n- except (urllib.error.URLError, TimeoutError, ValueError) as exc: # pragma: no cover - network path\\n- raise ProviderDiscoveryError(source.provider_name, str(exc)) from exc\\n+ payload = _fetch_json(\\n+ url,\\n+ auth_scheme=source.auth_scheme,\\n+ timeout=timeout,\\n+ credential_name=source.credential_name,\\n+ )\\n+ except (urllib.error.URLError, TimeoutError, ValueError, RuntimeError, OSError) as exc: # pragma: no cover - network path\\n+ raise ProviderDiscoveryError(source.provider_name, _provider_discovery_error_code(exc)) from None\\n if source.style == \\\"bytez\\\":\\n return _parse_bytez(payload, source)\\n return _parse_openai_compatible(payload, source)\\n@@ -202,7 +242,7 @@ def discover_all_models(\\n *,\\n timeout: float = DISCOVERY_TIMEOUT_SECONDS,\\n ) -> tuple[list[DiscoveredModel], list[ProviderDiscoveryError]]:\\n- \\\"\\\"\\\"Discover models across every provider with a registered credential.\\n+ \\\"\\\"\\\"Discover chat candidates across providers with registered credentials.\\n \\n One provider's failure never blocks the others: errors are collected and\\n returned alongside whatever models were successfully discovered.\\n@@ -231,7 +271,9 @@ def agent_id_for(discovered: DiscoveredModel) -> str:\\n \\n \\n def agent_from_discovered(discovered: DiscoveredModel, *, priority: int = 0) -> ModelAgent:\\n- \\\"\\\"\\\"Build a disabled-by-default ModelAgent for a discovered model (opt-in serving).\\\"\\\"\\\"\\n+ \\\"\\\"\\\"Build a disabled general chat agent or reject an ineligible record.\\\"\\\"\\\"\\n+ if not is_general_chat_agent_model_id(discovered.model_id):\\n+ raise ValueError(\\\"model is not eligible for a general chat agent\\\")\\n return ModelAgent(\\n id=agent_id_for(discovered),\\n model=discovered.model_id,\\n@@ -246,7 +288,7 @@ def agent_from_discovered(discovered: DiscoveredModel, *, priority: int = 0) ->\\n \\n \\n def refresh_price_book(discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\\") -> int:\\n- \\\"\\\"\\\"Write every discovered model's known pricing into the price book.\\n+ \\\"\\\"\\\"Write every discovered chat model's known pricing into the price book.\\n \\n Returns the number of price rows written. A model without provider-reported\\n pricing is skipped rather than defaulted to 0 -- an unpriced model already\\n@@ -257,6 +299,8 @@ def refresh_price_book(discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\n \\n written = 0\\n for model in discovered:\\n+ if not is_general_chat_agent_model_id(model.model_id):\\n+ continue\\n if model.prompt_price_per_1k is None and model.completion_price_per_1k is None:\\n continue\\n price_book.set_price(\\n@@ -275,43 +319,37 @@ def refresh_price_book(discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\n def select_cheapest_discovered_agent(\\n discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\\"\\n ) -> DiscoveredModel | None:\\n- \\\"\\\"\\\"Pick the lowest-cost discovered model per the price book (auto-optimization).\\n+ \\\"\\\"\\\"Pick the lowest-cost general chat-agent model per the price book.\\n \\n- Reuses :func:`~contextual_orchestrator.batch_routing.cheapest_upstream`, the\\n- existing cost-optimizing upstream selector. Call :func:`refresh_price_book`\\n- first so discovered pricing is visible; an unpriced candidate costs ``0``\\n+ Uses the same representative request cost as the top-N selector. Call\\n+ :func:`refresh_price_book` first so discovered pricing is visible; an\\n+ unpriced candidate costs ``0``\\n under that selector's documented contract and is treated as free, not\\n unknown -- so a genuinely unpriced provider (e.g. Bytez, priced by\\n GPU-second rather than per token) will always look cheapest here. Fine for\\n \\\"auto-pick something free to try,\\\" but callers doing real cost comparison\\n should refresh pricing for every candidate they care about first.\\n \\\"\\\"\\\"\\n- if not discovered:\\n- return None\\n- candidates = [{\\\"provider\\\": model.provider_name, \\\"model\\\": model.model_id} for model in discovered]\\n- winner = cheapest_upstream(candidates, price_book)\\n- if winner is None:\\n+ eligible = [model for model in discovered if is_general_chat_agent_model_id(model.model_id)]\\n+ if not eligible:\\n return None\\n- for model in discovered:\\n- if model.provider_name == winner[\\\"provider\\\"] and model.model_id == winner[\\\"model\\\"]:\\n- return model\\n- return None # pragma: no cover - winner always comes from candidates\\n+ return min(eligible, key=lambda model: _discovered_cost(model, price_book))\\n \\n \\n def select_top_n_cheapest_discovered_agents(\\n discovered: list[DiscoveredModel], price_book: \\\"PriceBook\\\", limit: int\\n ) -> list[DiscoveredModel]:\\n- \\\"\\\"\\\"Return the ``limit`` lowest-cost discovered models, cheapest first.\\n-\\n- For bootstrapping a CI sidecar (or any first-boot pool) with more than one\\n- enabled agent for failover, without hand-picking which discovered models to\\n- trust. Same pricing contract as :func:`select_cheapest_discovered_agent`.\\n- \\\"\\\"\\\"\\n- if limit <= 0 or not discovered:\\n+ \\\"\\\"\\\"Return the ``limit`` cheapest general chat-agent models in ascending cost.\\\"\\\"\\\"\\n+ if limit <= 0:\\n+ return []\\n+ eligible = [model for model in discovered if is_general_chat_agent_model_id(model.model_id)]\\n+ if not eligible:\\n return []\\n \\n- def _cost(model: DiscoveredModel) -> float:\\n- cost, _currency = price_book.compute_cost(model.provider_name, model.model_id, 1000, 1000)\\n- return cost\\n+ return sorted(eligible, key=lambda model: _discovered_cost(model, price_book))[:limit]\\n+\\n \\n- return sorted(discovered, key=_cost)[:limit]\\n+def _discovered_cost(model: DiscoveredModel, price_book: \\\"PriceBook\\\") -> float:\\n+ \\\"\\\"\\\"Price the representative discovery request used by both selectors.\\\"\\\"\\\"\\n+ cost, _currency = price_book.compute_cost(model.provider_name, model.model_id, 1000, 1000)\\n+ return cost\" }, { \"sha\": \"e68751645623a9fac0b4aec022f1829fdabe4db4\", \"filename\": \"contextual_orchestrator/orchestrator.py\", \"status\": \"modified\", \"additions\": 1414, \"deletions\": 249, \"changes\": 1663, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Forchestrator.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Forchestrator.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Forchestrator.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\" }, { \"sha\": \"328be1c492124de845dcc54798fda4b595bc0a8b\", \"filename\": \"contextual_orchestrator/passthrough_failover.py\", \"status\": \"added\", \"additions\": 151, \"deletions\": 0, \"changes\": 151, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fpassthrough_failover.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fpassthrough_failover.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fpassthrough_failover.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,151 @@\\n+\\\"\\\"\\\"Bounded cross-provider failover for OpenAI-compatible passthrough requests.\\n+\\n+Tool calls, structured responses, and Responses API calls must preserve one\\n+provider's raw response shape. This module keeps that contract while advancing\\n+to another capability-ranked model when the caller selected the virtual\\n+``contextual-orchestrator`` model and an upstream candidate becomes transiently\\n+unavailable.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import time\\n+import urllib.error\\n+from collections.abc import Iterator\\n+from typing import Any\\n+\\n+from .orchestrator import (\\n+ ModelAgent,\\n+ ModelClient,\\n+ is_transient_error,\\n+)\\n+from .orchestrator import (\\n+ TaskOrchestrator as BaseTaskOrchestrator,\\n+)\\n+\\n+_CANDIDATE_UNAVAILABLE_HTTP_STATUS = frozenset({404, 410})\\n+_MAX_PROVIDER_ERROR_CHAIN_DEPTH = 8\\n+\\n+\\n+def _provider_error_chain(error: BaseException) -> Iterator[BaseException]:\\n+ \\\"\\\"\\\"Yield a bounded, cycle-safe provider exception cause/context chain.\\n+\\n+ An explicit ``raise ... from cause`` is authoritative. An implicit context\\n+ is inspected only when it was not deliberately suppressed with\\n+ ``raise ... from None``; suppressed history must not turn a terminal wrapper\\n+ into an adaptive fallback signal.\\n+ \\\"\\\"\\\"\\n+ current: BaseException | None = error\\n+ seen: set[int] = set()\\n+ for _ in range(_MAX_PROVIDER_ERROR_CHAIN_DEPTH):\\n+ if current is None or id(current) in seen:\\n+ return\\n+ seen.add(id(current))\\n+ yield current\\n+ if current.__cause__ is not None:\\n+ current = current.__cause__\\n+ elif current.__suppress_context__:\\n+ return\\n+ else:\\n+ current = current.__context__\\n+\\n+\\n+def _is_adaptive_failover_error(error: BaseException) -> bool:\\n+ \\\"\\\"\\\"Classify transient or stale-candidate failures through provider wrappers.\\\"\\\"\\\"\\n+ for candidate in _provider_error_chain(error):\\n+ if is_transient_error(candidate):\\n+ return True\\n+ if (\\n+ isinstance(candidate, urllib.error.HTTPError)\\n+ and candidate.code in _CANDIDATE_UNAVAILABLE_HTTP_STATUS\\n+ ):\\n+ return True\\n+ return False\\n+\\n+\\n+def _proxy_send_once(\\n+ client: Any,\\n+ agent: ModelAgent,\\n+ endpoint: str,\\n+ payload: dict[str, Any],\\n+) -> dict[str, Any]:\\n+ \\\"\\\"\\\"Send one raw passthrough attempt without same-model transient retries.\\\"\\\"\\\"\\n+ one_shot = getattr(client, \\\"proxy_send_once\\\", None)\\n+ if callable(one_shot):\\n+ return one_shot(agent, endpoint, payload)\\n+ if isinstance(client, ModelClient):\\n+ return client.proxy_send_once(agent, endpoint, payload)\\n+ return client.proxy_send(agent, endpoint, payload)\\n+\\n+\\n+class TaskOrchestrator(BaseTaskOrchestrator):\\n+ \\\"\\\"\\\"Add bounded provider failover to the final OpenAI-compatible provider call.\\\"\\\"\\\"\\n+\\n+ def _proxy_provider_completion(\\n+ self,\\n+ agent: ModelAgent,\\n+ endpoint: str,\\n+ payload: dict[str, Any],\\n+ *,\\n+ requested_model: Any,\\n+ text: str,\\n+ role: str,\\n+ required_tags: tuple[str, ...] = (),\\n+ ) -> dict[str, Any]:\\n+ \\\"\\\"\\\"Preserve response shapes while failing over only adaptive requests.\\n+\\n+ An explicitly requested concrete model remains sticky and receives its\\n+ original provider error: serving another model would violate the caller's\\n+ model contract. Requests for the virtual ``contextual-orchestrator``\\n+ model, or requests that omit ``model``, may advance through\\n+ capability-ranked candidates for transient upstream failures and for a\\n+ discovered model that has become unavailable (HTTP 404/410). Provider\\n+ SDK wrapper causes are inspected through a bounded, cycle-safe chain.\\n+\\n+ Every candidate receives at most one passthrough attempt, so a 429 is\\n+ never amplified by replaying the same large tool request. Caller,\\n+ authentication, policy, and other non-transient failures are returned\\n+ immediately instead of being replayed to another provider.\\n+ \\\"\\\"\\\"\\n+ requested_agent = self._requested_agent(requested_model)\\n+ adaptive_request = requested_agent is None\\n+ if requested_agent is not None:\\n+ if requested_agent.disabled:\\n+ raise RuntimeError(f\\\"requested model {requested_model!r} is disabled\\\")\\n+ return self.client.proxy_send(requested_agent, endpoint, payload)\\n+ else:\\n+ candidates = self._failover_candidates(\\n+ agent,\\n+ text,\\n+ role,\\n+ required_tags=required_tags,\\n+ )\\n+\\n+ last_error: Exception | None = None\\n+ for candidate_agent in candidates:\\n+ upstream = dict(payload)\\n+ upstream[\\\"model\\\"] = candidate_agent.model\\n+ try:\\n+ result = _proxy_send_once(self.client, candidate_agent, endpoint, upstream)\\n+ except Exception as exc:\\n+ if not adaptive_request or not _is_adaptive_failover_error(exc):\\n+ raise\\n+ last_error = exc\\n+ self._record_failure(candidate_agent.id)\\n+ with self._circuit_lock:\\n+ state = self._circuit.setdefault(\\n+ candidate_agent.id,\\n+ {\\\"failures\\\": 0.0, \\\"opened_at\\\": 0.0},\\n+ )\\n+ state[\\\"failures\\\"] = max(\\n+ state[\\\"failures\\\"],\\n+ float(self.circuit_failure_threshold),\\n+ )\\n+ state[\\\"opened_at\\\"] = time.monotonic()\\n+ continue\\n+ self._record_success(candidate_agent.id)\\n+ return result\\n+\\n+ raise RuntimeError(\\n+ f\\\"all {len(candidates)} candidate agents failed for passthrough endpoint={endpoint}\\\"\\n+ ) from last_error\" }, { \"sha\": \"bf2230a8fee707a1d9963c3c5db1d2e514d086d3\", \"filename\": \"contextual_orchestrator/server.py\", \"status\": \"modified\", \"additions\": 531, \"deletions\": 129, \"changes\": 660, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fserver.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Fserver.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Fserver.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -6,7 +6,10 @@\\n from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer\\n import base64\\n import json\\n+import logging\\n+import math\\n import secrets\\n+import socket\\n import struct\\n import threading\\n import time\\n@@ -24,14 +27,28 @@\\n MAX_LOCAL_CONCURRENCY,\\n TaskOrchestrator,\\n _new_chat_completion_id,\\n+ _responses_usage,\\n chat_completion_chunks,\\n chat_completion_response,\\n+ _responses_to_chat_payload,\\n text_completion_response,\\n redact_value,\\n sse_stream_body,\\n )\\n+from .telemetry import (\\n+ attach_trace_context,\\n+ configure_telemetry,\\n+ current_session_id,\\n+ detach_trace_context,\\n+ reset_session_id,\\n+ session_id_from_headers,\\n+ session_id_from_metadata,\\n+ set_session_id,\\n+)\\n+\\n+_LOGGER = logging.getLogger(__name__)\\n \\n-# OpenAI request params forwarded verbatim to the provider on passthrough.\\n+# OpenAI request params forwarded to the final provider after orchestration.\\n OPENAI_PASSTHROUGH_PARAM_KEYS = {\\n \\\"temperature\\\", \\\"top_p\\\", \\\"max_tokens\\\", \\\"max_completion_tokens\\\", \\\"n\\\", \\\"stop\\\",\\n \\\"seed\\\", \\\"presence_penalty\\\", \\\"frequency_penalty\\\", \\\"logit_bias\\\", \\\"logprobs\\\",\\n@@ -50,8 +67,8 @@\\n # Assistants-style tool_resources — named unsupported (not unknown_fields).\\n \\\"tool_resources\\\",\\n }\\n-# Provider features the multi-agent verifier cannot merge -> single-agent passthrough.\\n-PASSTHROUGH_TRIGGER_KEYS = {\\\"response_format\\\", \\\"tools\\\", \\\"tool_choice\\\", \\\"functions\\\", \\\"function_call\\\"}\\n+# Tool calls still require provider-native loop semantics; structured JSON is\\n+# orchestrated through the conduct+synthesis path below instead of passthrough.\\n ALLOWED_CHAT_KEYS = {\\n \\\"model\\\", \\\"messages\\\", \\\"orchestration\\\", \\\"orchestration_mode\\\", \\\"mode\\\",\\n \\\"include_orchestration_trace\\\", \\\"stream\\\", \\\"attribution\\\", \\\"routing\\\",\\n@@ -66,7 +83,7 @@\\n \\\"max_output_tokens\\\",\\n # Tool-loop budget — accepted only for explicit unsupported error (no multi-step tool loop).\\n \\\"max_tool_calls\\\",\\n- # Gateway cost/routing control plane (stripped before provider passthrough).\\n+ # Gateway cost/routing control plane (stripped before final provider transport).\\n \\\"attribution\\\", \\\"routing\\\",\\n # previous_response_id / conversation / truncation / include fail closed\\n # with named unsupported errors. Official text.format is validated\\n@@ -153,6 +170,7 @@ class SecurityConfig:\\n allow_public_bind: bool = False\\n expose_trace_by_default: bool = False\\n max_body_bytes: int = 64 * 1024\\n+ request_read_timeout_seconds: float = 10.0\\n rate_limit_requests: int = 60\\n rate_limit_window_seconds: int = 60\\n max_concurrent_runs: int = 8\\n@@ -169,6 +187,15 @@ def __post_init__(self) -> None:\\n raise ValueError(\\\"single auth_token cannot be combined with split tokens\\\")\\n if (self.admin_token or self.inference_token) and not (self.admin_token and self.inference_token):\\n raise ValueError(\\\"split token mode requires both admin_token and inference_token\\\")\\n+ if type(self.max_body_bytes) is not int or self.max_body_bytes < 1:\\n+ raise ValueError(\\\"max_body_bytes must be a positive integer\\\")\\n+ if (\\n+ isinstance(self.request_read_timeout_seconds, bool)\\n+ or not isinstance(self.request_read_timeout_seconds, (int, float))\\n+ or not math.isfinite(float(self.request_read_timeout_seconds))\\n+ or not 0.1 <= float(self.request_read_timeout_seconds) <= 120.0\\n+ ):\\n+ raise ValueError(\\\"request_read_timeout_seconds must be between 0.1 and 120 seconds\\\")\\n if type(self.max_concurrent_runs) is not int or not 1 <= self.max_concurrent_runs <= MAX_LOCAL_CONCURRENCY:\\n raise ValueError(\\n f\\\"max_concurrent_runs must be an integer in 1..{MAX_LOCAL_CONCURRENCY}\\\"\\n@@ -241,6 +268,8 @@ def readiness_profile(self) -> dict[str, Any]:\\n \\\"rate_limit_requests\\\": self.rate_limit_requests,\\n \\\"rate_limit_window_seconds\\\": self.rate_limit_window_seconds,\\n \\\"max_concurrent_runs\\\": self.max_concurrent_runs,\\n+ \\\"max_body_bytes\\\": self.max_body_bytes,\\n+ \\\"request_read_timeout_seconds\\\": self.request_read_timeout_seconds,\\n }\\n \\n \\n@@ -297,6 +326,50 @@ def _coerce_json(payload: bytes) -> dict[str, Any]:\\n return value\\n \\n \\n+def _header_values(headers: Any, field_name: str) -> list[str]:\\n+ \\\"\\\"\\\"Return all raw values for one case-insensitive HTTP header field.\\\"\\\"\\\"\\n+ raw_items = getattr(headers, \\\"raw_items\\\", None)\\n+ if callable(raw_items):\\n+ return [value for name, value in raw_items() if name.casefold() == field_name.casefold()]\\n+ get_all = getattr(headers, \\\"get_all\\\", None)\\n+ if callable(get_all):\\n+ return list(get_all(field_name, []))\\n+ value = headers.get(field_name)\\n+ return [] if value is None else [value]\\n+\\n+\\n+def _parse_request_framing(headers: Any, max_body_bytes: int) -> int:\\n+ \\\"\\\"\\\"Validate fixed-length JSON framing before consuming any request bytes.\\n+\\n+ The server deliberately does not implement a chunked decoder. Requiring one\\n+ unambiguous ASCII decimal ``Content-Length`` prevents negative lengths,\\n+ duplicate disagreement, transfer-coding ambiguity, and unbounded reads from\\n+ reaching ``BufferedReader.read``.\\n+ \\\"\\\"\\\"\\n+ transfer_encoding = _header_values(headers, \\\"Transfer-Encoding\\\")\\n+ content_lengths = _header_values(headers, \\\"Content-Length\\\")\\n+ if transfer_encoding:\\n+ raise RequestError(\\n+ 400,\\n+ \\\"invalid_request_framing\\\",\\n+ \\\"transfer-encoded request bodies are unsupported\\\",\\n+ )\\n+ if not content_lengths:\\n+ raise RequestError(411, \\\"length_required\\\", \\\"content-length is required\\\")\\n+ if len(content_lengths) != 1:\\n+ raise RequestError(400, \\\"invalid_request_framing\\\", \\\"duplicate content-length is unsupported\\\")\\n+ raw_length = content_lengths[0]\\n+ if not raw_length or raw_length != raw_length.strip() or not raw_length.isascii() or not raw_length.isdigit():\\n+ raise RequestError(400, \\\"invalid_request_framing\\\", \\\"content-length must be an ASCII decimal integer\\\")\\n+ try:\\n+ body_size = int(raw_length, 10)\\n+ except (ValueError, TypeError):\\n+ raise RequestError(400, \\\"invalid_request_framing\\\", \\\"content-length is invalid\\\") from None\\n+ if body_size > max_body_bytes:\\n+ raise RequestError(413, \\\"request_too_large\\\", \\\"request body exceeds configured limit\\\")\\n+ return body_size\\n+\\n+\\n \\n \\n def _coerce_optional_bool(\\n@@ -829,6 +902,42 @@ def _validate_responses_parallel_tool_calls(body: dict[str, Any]) -> bool | None\\n return value\\n \\n \\n+def _reject_responses_orchestration_controls(body: dict[str, Any]) -> None:\\n+ \\\"\\\"\\\"Reject non-empty controls the multi-agent Responses path cannot apply.\\\"\\\"\\\"\\n+ fields = (\\n+ \\\"temperature\\\",\\n+ \\\"top_p\\\",\\n+ \\\"presence_penalty\\\",\\n+ \\\"frequency_penalty\\\",\\n+ \\\"seed\\\",\\n+ \\\"stop\\\",\\n+ \\\"logit_bias\\\",\\n+ \\\"logprobs\\\",\\n+ \\\"top_logprobs\\\",\\n+ )\\n+ unsupported: list[str] = []\\n+ for field_name in fields:\\n+ if field_name not in body:\\n+ continue\\n+ value = body[field_name]\\n+ if value is None or (isinstance(value, str) and not value.strip()):\\n+ continue\\n+ if isinstance(value, (list, dict)) and not value:\\n+ continue\\n+ if field_name == \\\"logprobs\\\" and value in (False, 0):\\n+ continue\\n+ if field_name == \\\"top_logprobs\\\" and value == 0:\\n+ continue\\n+ unsupported.append(field_name)\\n+ if unsupported:\\n+ raise RequestError(\\n+ 422,\\n+ \\\"unsupported_responses_orchestration_controls\\\",\\n+ \\\"Responses orchestration cannot apply these provider controls\\\",\\n+ {\\\"fields\\\": unsupported},\\n+ )\\n+\\n+\\n def _validate_responses_seed(body: dict[str, Any]) -> int | None:\\n \\\"\\\"\\\"Responses ``seed`` — signed int64; valid values pass through to the provider.\\n \\n@@ -1027,13 +1136,15 @@ def _validate_completions_top_p(body: dict[str, Any]) -> float | None:\\n body[\\\"top_p\\\"] = value\\n return value\\n \\n-def _validate_completions_model(body: dict[str, Any]) -> str:\\n+def _validate_completions_model(body: dict[str, Any], *, required: bool = True) -> str:\\n \\\"\\\"\\\"Legacy Completions ``model`` — required non-empty string (OpenAI parity).\\n \\n Incidental leading/trailing whitespace is stripped and written back so\\n tools/response_format passthrough (``proxy_completion``) matches the same\\n pool model id as the orchestration path. Form/JS SDKs often pad model names.\\n \\\"\\\"\\\"\\n+ if \\\"model\\\" not in body and not required:\\n+ return \\\"contextual-orchestrator\\\"\\n if \\\"model\\\" not in body:\\n raise RequestError(400, \\\"invalid_model\\\", \\\"model is required\\\")\\n model = body.get(\\\"model\\\")\\n@@ -1642,6 +1753,7 @@ def _validate_responses_text(body: dict[str, Any]) -> dict[str, Any] | None:\\n \\\"invalid_text\\\",\\n \\\"text.format.schema must be an object\\\",\\n )\\n+ _validate_json_schema_definition(schema_body, \\\"text.format.schema\\\")\\n if \\\"description\\\" in fmt:\\n description_value = fmt.get(\\\"description\\\")\\n if description_value is None or (\\n@@ -1785,17 +1897,23 @@ def _validate_mode(mode: Any) -> str:\\n \\n \\n \\n-def _require_pool_model(orchestrator: Any, model_name: str) -> None:\\n+def _require_pool_model(\\n+ orchestrator: Any, model_name: str, *, required_capability: str | None = None\\n+) -> None:\\n \\\"\\\"\\\"Fail closed when ``model_name`` is not served by any enabled agent.\\n \\n OpenAI clients treat ``model`` as the deployment they paid for. Silently\\n answering with a different pool agent hides capacity/routing mismatches.\\n \\\"\\\"\\\"\\n+ if model_name == \\\"contextual-orchestrator\\\" and required_capability is None:\\n+ return\\n agents = getattr(orchestrator, \\\"agents\\\", None) or []\\n for agent in agents:\\n if getattr(agent, \\\"disabled\\\", False):\\n continue\\n- if getattr(agent, \\\"model\\\", None) == model_name:\\n+ if getattr(agent, \\\"model\\\", None) == model_name and (\\n+ required_capability is None or required_capability in getattr(agent, \\\"tags\\\", ())\\n+ ):\\n return\\n raise RequestError(\\n 400,\\n@@ -1804,6 +1922,111 @@ def _require_pool_model(orchestrator: Any, model_name: str) -> None:\\n )\\n \\n \\n+def _validate_json_schema_definition(\\n+ schema: Any,\\n+ path: str = \\\"response_format.json_schema.schema\\\",\\n+) -> None:\\n+ \\\"\\\"\\\"Validate nested JSON Schema containers before evaluating a response.\\\"\\\"\\\"\\n+ if not isinstance(schema, dict):\\n+ raise RequestError(400, \\\"invalid_response_format\\\", f\\\"{path} must be an object\\\")\\n+ properties = schema.get(\\\"properties\\\")\\n+ if properties is not None:\\n+ if not isinstance(properties, dict):\\n+ raise RequestError(400, \\\"invalid_response_format\\\", f\\\"{path}.properties must be an object\\\")\\n+ for name, child in properties.items():\\n+ if not isinstance(name, str) or not isinstance(child, dict):\\n+ raise RequestError(\\n+ 400,\\n+ \\\"invalid_response_format\\\",\\n+ f\\\"{path}.properties entries must map string names to objects\\\",\\n+ )\\n+ _validate_json_schema_definition(child, f\\\"{path}.properties.{name}\\\")\\n+ required = schema.get(\\\"required\\\")\\n+ if required is not None and (\\n+ not isinstance(required, list) or any(not isinstance(name, str) for name in required)\\n+ ):\\n+ raise RequestError(400, \\\"invalid_response_format\\\", f\\\"{path}.required must be an array of strings\\\")\\n+ items = schema.get(\\\"items\\\")\\n+ if items is not None:\\n+ if not isinstance(items, dict):\\n+ raise RequestError(400, \\\"invalid_response_format\\\", f\\\"{path}.items must be an object\\\")\\n+ _validate_json_schema_definition(items, f\\\"{path}.items\\\")\\n+ any_of = schema.get(\\\"anyOf\\\")\\n+ if any_of is not None:\\n+ if not isinstance(any_of, list) or any(not isinstance(option, dict) for option in any_of):\\n+ raise RequestError(400, \\\"invalid_response_format\\\", f\\\"{path}.anyOf must be an array of objects\\\")\\n+ for index, option in enumerate(any_of):\\n+ _validate_json_schema_definition(option, f\\\"{path}.anyOf[{index}]\\\")\\n+\\n+\\n+def _validate_json_schema_value(value: Any, schema: dict[str, Any], path: str = \\\"$\\\") -> None:\\n+ \\\"\\\"\\\"Validate the bounded JSON Schema subset used by structured chat output.\\\"\\\"\\\"\\n+ if not isinstance(schema, dict):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", \\\"json_schema.schema must be an object\\\")\\n+ if \\\"enum\\\" in schema and value not in schema[\\\"enum\\\"]:\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} is outside the schema enum\\\")\\n+ if \\\"const\\\" in schema and value != schema[\\\"const\\\"]:\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} does not match the schema const\\\")\\n+ if \\\"anyOf\\\" in schema and not any(\\n+ _json_schema_matches(value, option) for option in schema[\\\"anyOf\\\"] if isinstance(option, dict)\\n+ ):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} matches no anyOf branch\\\")\\n+ expected = schema.get(\\\"type\\\")\\n+ if expected == \\\"object\\\":\\n+ if not isinstance(value, dict):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} must be an object\\\")\\n+ properties = schema.get(\\\"properties\\\", {})\\n+ required = schema.get(\\\"required\\\", [])\\n+ missing = [name for name in required if name not in value]\\n+ if missing:\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} is missing {missing[0]!r}\\\")\\n+ for name, child in properties.items():\\n+ if name in value and isinstance(child, dict):\\n+ _validate_json_schema_value(value[name], child, f\\\"{path}.{name}\\\")\\n+ if schema.get(\\\"additionalProperties\\\") is False:\\n+ unknown = set(value) - set(properties)\\n+ if unknown:\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} has unknown property {sorted(unknown)[0]!r}\\\")\\n+ elif expected == \\\"array\\\":\\n+ if not isinstance(value, list):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} must be an array\\\")\\n+ items = schema.get(\\\"items\\\")\\n+ if isinstance(items, dict):\\n+ for index, item in enumerate(value):\\n+ _validate_json_schema_value(item, items, f\\\"{path}[{index}]\\\")\\n+ elif expected == \\\"string\\\" and not isinstance(value, str):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} must be a string\\\")\\n+ elif expected == \\\"boolean\\\" and not isinstance(value, bool):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} must be a boolean\\\")\\n+ elif expected == \\\"integer\\\" and (isinstance(value, bool) or not isinstance(value, int)):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} must be an integer\\\")\\n+ elif expected == \\\"number\\\" and (isinstance(value, bool) or not isinstance(value, (int, float))):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", f\\\"{path} must be a number\\\")\\n+\\n+\\n+def _json_schema_matches(value: Any, schema: dict[str, Any]) -> bool:\\n+ try:\\n+ _validate_json_schema_value(value, schema)\\n+ except RequestError:\\n+ return False\\n+ return True\\n+\\n+\\n+def _validate_structured_completion_answer(answer: Any, response_format: dict[str, Any]) -> None:\\n+ \\\"\\\"\\\"Reject non-JSON synthesis instead of returning an unverified contract.\\\"\\\"\\\"\\n+ if not isinstance(answer, str):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", \\\"orchestrator returned no textual JSON\\\")\\n+ try:\\n+ value = json.loads(answer)\\n+ except json.JSONDecodeError:\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", \\\"orchestrator returned invalid JSON\\\") from None\\n+ if response_format.get(\\\"type\\\") == \\\"json_object\\\" and not isinstance(value, dict):\\n+ raise RequestError(502, \\\"invalid_structured_output\\\", \\\"json_object output must be an object\\\")\\n+ if response_format.get(\\\"type\\\") == \\\"json_schema\\\":\\n+ schema = ((response_format.get(\\\"json_schema\\\") or {}).get(\\\"schema\\\"))\\n+ _validate_json_schema_value(value, schema)\\n+\\n+\\n \\n def _validate_message_content_parts(content: list[Any]) -> list[dict[str, Any]]:\\n \\\"\\\"\\\"OpenAI multimodal content-parts array (text + image_url) for vision callers.\\n@@ -3044,7 +3267,7 @@ def _validate_responses_store(body: dict[str, Any]) -> bool | None:\\n \\\"\\\"\\\"Responses API ``store`` — strict boolean; ``true`` is not supported.\\n \\n OpenAI may persist Responses when ``store=true``. This gateway's Responses\\n- path is a single-agent passthrough without a persistence plane, so\\n+ path has no provider-response persistence plane, so\\n ``store=true`` fails closed rather than silently dropping a buyer-visible\\n storage control. ``store=false`` and omit remain valid.\\n \\\"\\\"\\\"\\n@@ -3067,20 +3290,22 @@ def _validate_responses_store(body: dict[str, Any]) -> bool | None:\\n \\n \\n \\n-# OpenAI o-series reasoning_effort levels. Without an effort plane this gateway\\n-# treats known levels as default-effort no-ops (parity with verbosity low/medium/high).\\n+# OpenAI o-series reasoning_effort levels plus the orchestrator-owned policy\\n+# value. Without an effort plane this gateway treats accepted values as\\n+# default-effort no-ops (parity with verbosity low/medium/high).\\n _OPENAI_REASONING_EFFORT_LEVELS = frozenset(\\n- {\\\"none\\\", \\\"minimal\\\", \\\"low\\\", \\\"medium\\\", \\\"high\\\"}\\n+ {\\\"auto\\\", \\\"none\\\", \\\"minimal\\\", \\\"low\\\", \\\"medium\\\", \\\"high\\\"}\\n )\\n \\n \\n def _validate_chat_reasoning_effort(body: dict[str, Any]) -> None:\\n \\\"\\\"\\\"Chat Completions ``reasoning_effort`` — known levels are default-effort no-ops.\\n \\n OpenAI o-series models accept ``reasoning_effort`` (none/minimal/low/medium/high).\\n- This gateway never threads the knob into ``ModelClient`` on the orchestration\\n- path. Known levels are accepted as default-effort no-ops (no effort plane) so\\n- o-series SDK defaults (often ``medium``) do not 400; unknown values fail closed.\\n+ ``auto`` is an orchestrator policy value, not a provider model value. This\\n+ gateway never threads the knob into ``ModelClient`` on the orchestration\\n+ path. Accepted values are default-effort no-ops when no effort plane exists;\\n+ unknown values fail closed.\\n Explicit JSON null or empty/whitespace string is treat-as-omit.\\n \\\"\\\"\\\"\\n if \\\"reasoning_effort\\\" not in body:\\n@@ -3099,7 +3324,7 @@ def _validate_chat_reasoning_effort(body: dict[str, Any]) -> None:\\n raise RequestError(\\n 400,\\n \\\"invalid_reasoning_effort\\\",\\n- \\\"reasoning_effort must be one of none, minimal, low, medium, high \\\"\\n+ \\\"reasoning_effort must be one of auto, none, minimal, low, medium, high \\\"\\n \\\"on /v1/chat/completions\\\",\\n )\\n \\n@@ -3607,6 +3832,7 @@ def _validate_chat_response_format(body: dict[str, Any]) -> dict[str, Any] | Non\\n \\\"invalid_response_format\\\",\\n \\\"response_format.json_schema.schema must be an object\\\",\\n )\\n+ _validate_json_schema_definition(schema_body)\\n # Explicit JSON null / blank description is omit-equivalent: pop so\\n # passthrough matches omit (parity with Responses text.format).\\n if \\\"description\\\" in schema:\\n@@ -4038,7 +4264,7 @@ def _validate_chat_tool_choice(body: dict[str, Any]) -> str | dict[str, Any] | N\\n \\n \\n \\n-def _validate_responses_model(body: dict[str, Any]) -> str:\\n+def _validate_responses_model(body: dict[str, Any], *, required: bool = True) -> str:\\n \\\"\\\"\\\"Responses API ``model`` — required non-empty string ≤256 chars.\\n \\n OpenAI requires model on Responses. Missing/empty/non-string values fail\\n@@ -4047,6 +4273,8 @@ def _validate_responses_model(body: dict[str, Any]) -> str:\\n ``proxy_completion`` pool match sees the same id as form/JS padded names.\\n \\\"\\\"\\\"\\n model = body.get(\\\"model\\\")\\n+ if model is None and not required:\\n+ return \\\"contextual-orchestrator\\\"\\n if model is None:\\n raise RequestError(400, \\\"invalid_model\\\", \\\"model is required on /v1/responses\\\")\\n if not isinstance(model, str) or not model.strip():\\n@@ -4089,8 +4317,9 @@ def _validate_responses_reasoning(body: dict[str, Any]) -> None:\\n \\\"\\\"\\\"Responses API ``reasoning`` — known effort levels are default-effort no-ops.\\n \\n OpenAI Responses accepts a ``reasoning`` object (effort/summary controls).\\n+ ``auto`` is an orchestrator policy value; provider calls do not receive it.\\n This gateway proxies Responses but does not interpret or enforce reasoning\\n- controls. Known ``effort`` levels (none/minimal/low/medium/high) with blank\\n+ controls. Known ``effort`` levels (auto/none/minimal/low/medium/high) with blank\\n or omit ``summary`` are accepted as default-effort no-ops (chat\\n ``reasoning_effort`` parity). Explicit JSON null, empty object, or empty\\n string is treat-as-omit. Unknown effort/summary values fail closed.\\n@@ -4134,7 +4363,7 @@ def _validate_responses_reasoning(body: dict[str, Any]) -> None:\\n raise RequestError(\\n 400,\\n \\\"invalid_reasoning\\\",\\n- \\\"reasoning.effort must be one of none, minimal, low, medium, high \\\"\\n+ \\\"reasoning.effort must be one of auto, none, minimal, low, medium, high \\\"\\n \\\"on /v1/responses\\\",\\n )\\n \\n@@ -4169,12 +4398,27 @@ def _validate_batch_embeddings_endpoint(body: dict[str, Any]) -> str | None:\\n return value\\n \\n \\n-def _validate_embeddings_model(body: dict[str, Any]) -> str:\\n- \\\"\\\"\\\"OpenAI embeddings ``model`` — required non-empty string ≤256 chars.\\n+def _validate_embeddings_model(body: dict[str, Any], orchestrator: Any | None = None) -> str:\\n+ \\\"\\\"\\\"Validate or auto-select an OpenAI embeddings model.\\n \\n Strip + write back (parity with chat/Completions/Responses) so padded\\n- form/JS model names bind to the pool id on every surface.\\n+ form/JS model names bind to the pool id on every surface. An omitted model\\n+ is resolved by the orchestrator's explicit ``embedding`` capability pool;\\n+ no consumer-side sentinel model is accepted.\\n \\\"\\\"\\\"\\n+ if \\\"model\\\" not in body:\\n+ if orchestrator is None:\\n+ raise RequestError(400, \\\"invalid_model\\\", \\\"model is required outside an orchestrator request\\\")\\n+ try:\\n+ model = orchestrator.select_capability_agent(\\\"embedding\\\").model\\n+ except (RuntimeError, ValueError) as exc:\\n+ raise RequestError(\\n+ 503,\\n+ \\\"embedding_unavailable\\\",\\n+ \\\"no enabled embedding-capable agent is available\\\",\\n+ ) from exc\\n+ body[\\\"model\\\"] = model\\n+ return model\\n model = body.get(\\\"model\\\")\\n if model is None:\\n raise RequestError(400, \\\"invalid_model\\\", \\\"model is required\\\")\\n@@ -4412,14 +4656,51 @@ def build_server(\\n security = security or SecurityConfig()\\n security.check_bind(host)\\n coordinator = coordinator or CostRoutingCoordinator(orchestrator)\\n+ configure_telemetry(config=coordinator.config)\\n if clearfolio_url is not None:\\n parsed_viewer = urllib.parse.urlparse(clearfolio_url)\\n if parsed_viewer.scheme not in {\\\"http\\\", \\\"https\\\"} or not parsed_viewer.netloc:\\n raise ValueError(\\\"clearfolio_url must be an http(s) URL\\\")\\n clearfolio_url = clearfolio_url.rstrip(\\\"/\\\")\\n \\n class Handler(BaseHTTPRequestHandler):\\n+ \\\"\\\"\\\"Serve the authenticated OpenAI-compatible and administrative routes.\\\"\\\"\\\"\\n+ _session_token = None\\n+ _trace_token = None\\n+\\n+ def _bind_session(self, session_id: str | None) -> None:\\n+ if session_id is None:\\n+ return\\n+ if self._session_token is not None:\\n+ reset_session_id(self._session_token)\\n+ self._session_token = set_session_id(session_id)\\n+\\n+ def _reset_session(self) -> None:\\n+ \\\"\\\"\\\"Release the request session in the context that bound it.\\\"\\\"\\\"\\n+ trace_token = self._trace_token\\n+ self._trace_token = None\\n+ if trace_token is not None:\\n+ detach_trace_context(trace_token)\\n+ token = self._session_token\\n+ self._session_token = None\\n+ if token is not None:\\n+ reset_session_id(token)\\n+\\n+ def handle_one_request(self) -> None:\\n+ \\\"\\\"\\\"Prevent a keep-alive connection from carrying session state.\\\"\\\"\\\"\\n+ try:\\n+ super().handle_one_request()\\n+ finally:\\n+ self._reset_session()\\n+\\n+ def finish(self) -> None:\\n+ \\\"\\\"\\\"Flush the HTTP response and release request session context.\\\"\\\"\\\"\\n+ try:\\n+ super().finish()\\n+ finally:\\n+ self._reset_session()\\n def do_GET(self) -> None: # noqa: N802\\n+ \\\"\\\"\\\"Return health, discovery, result, and administrative resources.\\\"\\\"\\\"\\n parsed = urllib.parse.urlparse(self.path)\\n path = parsed.path\\n query = urllib.parse.parse_qs(parsed.query)\\n@@ -4769,6 +5050,7 @@ def do_GET(self) -> None: # noqa: N802\\n self._send_error(500, \\\"internal_error\\\", \\\"internal server error\\\")\\n \\n def do_PATCH(self) -> None: # noqa: N802\\n+ \\\"\\\"\\\"Apply an authenticated worker-agent configuration update.\\\"\\\"\\\"\\n try:\\n self._authorize(\\\"admin\\\")\\n path = urllib.parse.urlparse(self.path).path\\n@@ -4792,6 +5074,7 @@ def do_PATCH(self) -> None: # noqa: N802\\n self._send_error(500, \\\"internal_error\\\", \\\"internal server error\\\")\\n \\n def do_DELETE(self) -> None: # noqa: N802\\n+ \\\"\\\"\\\"Remove an authenticated worker agent from its configured pool.\\\"\\\"\\\"\\n try:\\n self._authorize(\\\"admin\\\")\\n path = urllib.parse.urlparse(self.path).path\\n@@ -4812,11 +5095,16 @@ def do_DELETE(self) -> None: # noqa: N802\\n self._send_error(500, \\\"internal_error\\\", \\\"internal server error\\\")\\n \\n def do_POST(self) -> None: # noqa: N802\\n+ \\\"\\\"\\\"Validate and execute inference or administrative commands.\\\"\\\"\\\"\\n try:\\n path = urllib.parse.urlparse(self.path).path\\n scope = \\\"admin\\\" if path == \\\"/admin/simulate\\\" or path.startswith(\\\"/api/v1/agent_pools/\\\") else \\\"inference\\\"\\n self._authorize(scope)\\n body = self._read_json()\\n+ for metadata_key in (\\\"metadata\\\", \\\"client_metadata\\\"):\\n+ metadata = body.get(metadata_key)\\n+ if isinstance(metadata, dict):\\n+ self._bind_session(session_id_from_metadata(metadata))\\n \\n if path.startswith(\\\"/api/v1/agent_pools/\\\") and path.endswith(\\\"/worker_agents\\\"):\\n segments = [part for part in path.split(\\\"/\\\") if part]\\n@@ -4886,24 +5174,13 @@ def do_POST(self) -> None: # noqa: N802\\n attribution[\\\"service\\\"] = \\\"completions_api\\\"\\n routing = _validate_routing(body.get(\\\"routing\\\"))\\n started_at = time.perf_counter()\\n- # Apply request sampling knobs to the provider client for this call.\\n- model_client = orchestrator.client\\n- previous_max_tokens = model_client.max_output_tokens\\n- previous_temperature = model_client.default_temperature\\n- previous_top_p = model_client.default_top_p\\n- previous_presence = model_client.default_presence_penalty\\n- previous_frequency = model_client.default_frequency_penalty\\n- if max_tokens is not None:\\n- model_client.max_output_tokens = max_tokens\\n- if temperature is not None:\\n- model_client.default_temperature = temperature\\n- if top_p is not None:\\n- model_client.default_top_p = top_p\\n- if presence_penalty is not None:\\n- model_client.default_presence_penalty = presence_penalty\\n- if frequency_penalty is not None:\\n- model_client.default_frequency_penalty = frequency_penalty\\n- try:\\n+ with orchestrator.client.request_settings(\\n+ max_output_tokens=max_tokens,\\n+ temperature=temperature,\\n+ top_p=top_p,\\n+ presence_penalty=presence_penalty,\\n+ frequency_penalty=frequency_penalty,\\n+ ):\\n result = self._run(lambda: coordinator.complete(\\n messages,\\n mode=\\\"route\\\",\\n@@ -4912,12 +5189,6 @@ def do_POST(self) -> None: # noqa: N802\\n model_name=model_name,\\n workflow_run_id=f\\\"run_{uuid.uuid4().hex}\\\",\\n ))\\n- finally:\\n- model_client.max_output_tokens = previous_max_tokens\\n- model_client.default_temperature = previous_temperature\\n- model_client.default_top_p = previous_top_p\\n- model_client.default_presence_penalty = previous_presence\\n- model_client.default_frequency_penalty = previous_frequency\\n # Batch-channel Completions return a job handle (202), not a\\n # text_completion body — match chat Completions honesty so\\n # clients never receive a 500 on a valid batch routing hint.\\n@@ -5024,8 +5295,7 @@ def do_POST(self) -> None: # noqa: N802\\n _validate_chat_response_format(body)\\n if \\\"tools\\\" in body:\\n _validate_chat_tools(body)\\n- if \\\"tool_choice\\\" in body:\\n- _validate_chat_tool_choice(body)\\n+ tool_choice = _validate_chat_tool_choice(body) if \\\"tool_choice\\\" in body else None\\n if \\\"parallel_tool_calls\\\" in body:\\n # Always type-check. With tools, true/false both valid for\\n # provider passthrough; without tools, true fails closed.\\n@@ -5046,7 +5316,7 @@ def do_POST(self) -> None: # noqa: N802\\n body[\\\"parallel_tool_calls\\\"] = ptc\\n # Strip+writeback model before tools/response_format passthrough so\\n # proxy_completion pool match sees the same id as form/JS padded names.\\n- _validate_completions_model(body)\\n+ _validate_completions_model(body, required=False)\\n # Coerce stream early so stream_options fail-closed matches route path\\n # and tools/response_format passthrough cannot skip type checks.\\n stream = body.get(\\\"stream\\\", False)\\n@@ -5072,29 +5342,56 @@ def do_POST(self) -> None: # noqa: N802\\n frequency_penalty = sampling[\\\"frequency_penalty\\\"]\\n # Explicit JSON null on trigger keys is omit-equivalent (SDK optional\\n # defaults) — do not force single-agent passthrough for null-only keys.\\n- if any(\\n- key in body and body.get(key) is not None\\n- for key in PASSTHROUGH_TRIGGER_KEYS\\n- ):\\n- # response_format / tools cannot be merged across agents;\\n- # proxy the full request to one agent and return it verbatim.\\n+ tool_passthrough = tools_list or isinstance(tool_choice, dict) or (\\n+ isinstance(tool_choice, str) and tool_choice not in {\\\"none\\\", \\\"auto\\\"}\\n+ )\\n+ tool_loop_header = self.headers.get(\\n+ \\\"x-contextual-orchestrator-tool-loop\\\", \\\"\\\"\\n+ ).strip().lower()\\n+ if tool_passthrough and tool_loop_header == \\\"v1\\\":\\n+ # OpenCode executes the returned function calls in its own\\n+ # bounded tool loop. Preserve the full provider response;\\n+ # multi-agent synthesis cannot safely merge tool state.\\n+ if stream:\\n+ raise RequestError(\\n+ 400,\\n+ \\\"invalid_stream\\\",\\n+ \\\"tool-loop passthrough requires stream=false\\\",\\n+ )\\n started_at = time.perf_counter()\\n- proxied = self._run(\\n- lambda: orchestrator.proxy_completion(body, endpoint=\\\"chat/completions\\\")\\n+ raw_response = self._run(\\n+ lambda: orchestrator.proxy_completion(body, single_agent=True)\\n )\\n orchestrator.record_analytics_event(\\n- \\\"chat_completion_passthrough\\\",\\n+ \\\"chat_completion_tool_passthrough\\\",\\n {\\n \\\"endpoint_path\\\": \\\"/v1/chat/completions\\\",\\n \\\"actor_scope\\\": \\\"inference\\\",\\n \\\"status_code\\\": 200,\\n \\\"duration_ms\\\": round((time.perf_counter() - started_at) * 1000, 2),\\n },\\n )\\n- self._send(proxied)\\n+ self._send(raw_response)\\n return\\n+ if tool_passthrough:\\n+ raise RequestError(\\n+ 422,\\n+ \\\"multi_agent_tools_unsupported\\\",\\n+ \\\"tool execution requires the explicit v1 client-owned tool-loop contract\\\",\\n+ )\\n messages = _validate_messages(body.get(\\\"messages\\\"))\\n mode = _validate_mode(body.get(\\\"orchestration\\\") or body.get(\\\"orchestration_mode\\\") or body.get(\\\"mode\\\") or \\\"auto\\\")\\n+ response_format = body.get(\\\"response_format\\\")\\n+ structured_response_format = (\\n+ response_format\\n+ if isinstance(response_format, dict)\\n+ and response_format.get(\\\"type\\\") in {\\\"json_object\\\", \\\"json_schema\\\"}\\n+ else None\\n+ )\\n+ if structured_response_format is not None:\\n+ # Structured output is a synthesis contract, not a\\n+ # provider passthrough. Force the multi-agent workflow.\\n+ mode = \\\"conduct\\\"\\n if \\\"include_orchestration_trace\\\" in body:\\n # Null/empty omit; bool, int 0/1, and \\\"true\\\"/\\\"false\\\"/\\\"0\\\"/\\\"1\\\"\\n # strings coerce (SDK form/query parity with stream/store).\\n@@ -5112,9 +5409,8 @@ def do_POST(self) -> None: # noqa: N802\\n # stream + stream_options already coerced/validated before passthrough.\\n attribution = _validate_attribution(body.get(\\\"attribution\\\"))\\n routing = _validate_routing(body.get(\\\"routing\\\"))\\n- # Require model — silent default to contextual-orchestrator hid\\n- # which deployment the buyer selected on the chat Completions path.\\n- model_name = _validate_completions_model(body)\\n+ # Omitted model means contextual-orchestrator owns selection.\\n+ model_name = _validate_completions_model(body, required=False)\\n _require_pool_model(orchestrator, model_name)\\n attribution = dict(attribution or {})\\n # OpenAI chat ``user`` → account when unset.\\n@@ -5131,23 +5427,13 @@ def do_POST(self) -> None: # noqa: N802\\n if \\\"metadata\\\" in body:\\n _validate_openai_metadata(body)\\n started_at = time.perf_counter()\\n- model_client = orchestrator.client\\n- previous_max_tokens = model_client.max_output_tokens\\n- previous_temperature = model_client.default_temperature\\n- previous_top_p = model_client.default_top_p\\n- previous_presence = model_client.default_presence_penalty\\n- previous_frequency = model_client.default_frequency_penalty\\n- if max_tokens is not None:\\n- model_client.max_output_tokens = max_tokens\\n- if temperature is not None:\\n- model_client.default_temperature = temperature\\n- if top_p is not None:\\n- model_client.default_top_p = top_p\\n- if presence_penalty is not None:\\n- model_client.default_presence_penalty = presence_penalty\\n- if frequency_penalty is not None:\\n- model_client.default_frequency_penalty = frequency_penalty\\n- try:\\n+ with orchestrator.client.request_settings(\\n+ max_output_tokens=max_tokens,\\n+ temperature=temperature,\\n+ top_p=top_p,\\n+ presence_penalty=presence_penalty,\\n+ frequency_penalty=frequency_penalty,\\n+ ):\\n if stream and orchestrator.would_route(messages, mode):\\n self._stream_route_completion(orchestrator, security, messages, model_name)\\n orchestrator.record_analytics_event(\\n@@ -5169,13 +5455,13 @@ def do_POST(self) -> None: # noqa: N802\\n hints=routing,\\n model_name=model_name,\\n workflow_run_id=f\\\"run_{uuid.uuid4().hex}\\\",\\n+ response_format=structured_response_format,\\n+ provider_request=(\\n+ body if structured_response_format is not None else None\\n+ ),\\n ))\\n- finally:\\n- model_client.max_output_tokens = previous_max_tokens\\n- model_client.default_temperature = previous_temperature\\n- model_client.default_top_p = previous_top_p\\n- model_client.default_presence_penalty = previous_presence\\n- model_client.default_frequency_penalty = previous_frequency\\n+ if structured_response_format is not None and result.get(\\\"channel\\\") != \\\"batch\\\":\\n+ _validate_structured_completion_answer(result.get(\\\"answer\\\"), structured_response_format)\\n # Latency-tolerant requests get dispatched to the batch backend.\\n if result.get(\\\"channel\\\") == \\\"batch\\\":\\n orchestrator.record_analytics_event(\\n@@ -5216,10 +5502,10 @@ def do_POST(self) -> None: # noqa: N802\\n # synchronously) and frames an OpenAI-shaped response so\\n # SDKs that call /v1/embeddings work without the batch path.\\n _reject_unknown_keys(body, ALLOWED_EMBEDDINGS_KEYS)\\n- model_name = _validate_embeddings_model(body)\\n+ model_name = _validate_embeddings_model(body, orchestrator)\\n # Same pool honesty as chat/Completions: do not silently serve\\n # a different embedding deployment than the client requested.\\n- _require_pool_model(orchestrator, model_name)\\n+ _require_pool_model(orchestrator, model_name, required_capability=\\\"embedding\\\")\\n encoding_format = _validate_embeddings_encoding_format(body)\\n _validate_embeddings_dimensions(body)\\n end_user_id = _validate_completions_user(body)\\n@@ -5305,16 +5591,8 @@ def do_POST(self) -> None: # noqa: N802\\n if path == \\\"/v1/batch/embeddings\\\":\\n _reject_unknown_keys(body, ALLOWED_EMBEDDINGS_BATCH_KEYS)\\n inputs = _validate_embeddings_inputs(body)\\n- # Require model — silent default to contextual-orchestrator was an\\n- # honesty gap for naruon/batch clients that omit the field.\\n- if \\\"model\\\" not in body:\\n- raise RequestError(\\n- 400,\\n- \\\"invalid_model\\\",\\n- \\\"model is required on /v1/batch/embeddings\\\",\\n- )\\n- model_name = _validate_embeddings_model(body)\\n- _require_pool_model(orchestrator, model_name)\\n+ model_name = _validate_embeddings_model(body, orchestrator)\\n+ _require_pool_model(orchestrator, model_name, required_capability=\\\"embedding\\\")\\n _validate_embeddings_encoding_format(body)\\n _validate_embeddings_dimensions(body)\\n # OpenAI ``user`` end-user id — same fail-closed shape as sync embeddings.\\n@@ -5386,12 +5664,13 @@ def do_POST(self) -> None: # noqa: N802\\n self._send(_response_payload(retrieved, include_trace=True))\\n return\\n if path == \\\"/v1/responses\\\":\\n- # The Responses API has no chat-completions verifier equivalent,\\n- # so every request is proxied to one agent verbatim.\\n+ # Normalize Responses input to the same multi-agent workflow\\n+ # used by Chat Completions; never silently proxy one agent.\\n _reject_unknown_keys(body, ALLOWED_RESPONSES_KEYS)\\n # Fail-closed shape checks before passthrough so buyers never\\n # get a 200 after shipping invalid OpenAI-shaped metadata/input.\\n- _validate_responses_model(body)\\n+ model_name = _validate_responses_model(body, required=False)\\n+ _require_pool_model(orchestrator, model_name)\\n _validate_responses_conversation_controls(body)\\n if \\\"store\\\" in body:\\n _validate_responses_store(body)\\n@@ -5403,14 +5682,10 @@ def do_POST(self) -> None: # noqa: N802\\n if \\\"stream_options\\\" in body:\\n _validate_responses_stream_options(body)\\n # Sampling knobs: type/range fail-closed before provider passthrough.\\n- if \\\"temperature\\\" in body:\\n- _validate_completions_temperature(body)\\n- if \\\"top_p\\\" in body:\\n- _validate_completions_top_p(body)\\n- if \\\"presence_penalty\\\" in body:\\n- _validate_completions_presence_penalty(body)\\n- if \\\"frequency_penalty\\\" in body:\\n- _validate_completions_frequency_penalty(body)\\n+ _validate_completions_temperature(body)\\n+ _validate_completions_top_p(body)\\n+ _validate_completions_presence_penalty(body)\\n+ _validate_completions_frequency_penalty(body)\\n if \\\"n\\\" in body:\\n _validate_responses_n(body)\\n if \\\"seed\\\" in body:\\n@@ -5421,12 +5696,9 @@ def do_POST(self) -> None: # noqa: N802\\n _validate_responses_logit_bias(body)\\n if \\\"logprobs\\\" in body or \\\"top_logprobs\\\" in body:\\n _validate_responses_logprobs(body)\\n- if \\\"max_tokens\\\" in body:\\n- _validate_completions_max_tokens(body)\\n- if \\\"max_completion_tokens\\\" in body:\\n- _validate_chat_max_completion_tokens(body)\\n- if \\\"max_output_tokens\\\" in body:\\n- _validate_responses_max_output_tokens(body)\\n+ responses_max_tokens = _validate_completions_max_tokens(body)\\n+ responses_max_completion_tokens = _validate_chat_max_completion_tokens(body)\\n+ responses_max_output_tokens = _validate_responses_max_output_tokens(body)\\n if \\\"max_tool_calls\\\" in body:\\n _validate_responses_max_tool_calls(body)\\n _validate_openai_sdk_control_fields(body, endpoint_path=\\\"/v1/responses\\\")\\n@@ -5488,6 +5760,15 @@ def do_POST(self) -> None: # noqa: N802\\n _validate_chat_tool_choice(body)\\n if \\\"response_format\\\" in body:\\n _validate_chat_response_format(body)\\n+ tool_loop_header = self.headers.get(\\n+ \\\"x-contextual-orchestrator-tool-loop\\\", \\\"\\\"\\n+ ).strip().lower()\\n+ if tools_list and tool_loop_header != \\\"v1\\\":\\n+ raise RequestError(\\n+ 422,\\n+ \\\"multi_agent_tools_unsupported\\\",\\n+ \\\"tool execution requires the explicit v1 client-owned tool-loop contract\\\",\\n+ )\\n if \\\"modalities\\\" in body:\\n _validate_responses_modalities(body)\\n if \\\"prediction\\\" in body:\\n@@ -5532,8 +5813,8 @@ def do_POST(self) -> None: # noqa: N802\\n \\\"invalid_input\\\",\\n \\\"input must be a non-empty string or non-empty array on /v1/responses\\\",\\n )\\n- # stream=false / omit → non-SSE JSON response (honest no-stream path).\\n- # stream=true is not implemented for Responses passthrough.\\n+ # stream=false / omit -> non-SSE JSON response (honest no-stream path).\\n+ # stream=true is not implemented for the conducted Responses path.\\n # String/0-1 forms coerce via shared bool helper (parity with chat).\\n if \\\"stream\\\" in body:\\n stream = _coerce_optional_bool(\\n@@ -5547,23 +5828,93 @@ def do_POST(self) -> None: # noqa: N802\\n \\\"invalid_stream\\\",\\n \\\"stream is not supported on /v1/responses\\\",\\n )\\n- started_at = time.perf_counter()\\n- proxied = self._run(\\n- lambda: orchestrator.proxy_completion(body, endpoint=\\\"responses\\\")\\n+ if tools_list and tool_loop_header == \\\"v1\\\":\\n+ # Validate input and stream before passthrough so the\\n+ # client-owned contract cannot silently downgrade a\\n+ # requested stream or accept a missing input.\\n+ started_at = time.perf_counter()\\n+ raw_response = self._run(\\n+ lambda: orchestrator.proxy_completion(\\n+ body,\\n+ endpoint=\\\"responses\\\",\\n+ single_agent=True,\\n+ )\\n+ )\\n+ orchestrator.record_analytics_event(\\n+ \\\"responses_tool_passthrough\\\",\\n+ {\\n+ \\\"endpoint_path\\\": \\\"/v1/responses\\\",\\n+ \\\"actor_scope\\\": \\\"inference\\\",\\n+ \\\"status_code\\\": 200,\\n+ \\\"duration_ms\\\": round((time.perf_counter() - started_at) * 1000, 2),\\n+ },\\n+ )\\n+ self._send(raw_response)\\n+ return\\n+ response_contract: dict[str, Any] | None = None\\n+ raw_response_format = body.get(\\\"response_format\\\")\\n+ if isinstance(raw_response_format, dict) and raw_response_format.get(\\\"type\\\") in {\\n+ \\\"json_object\\\",\\n+ \\\"json_schema\\\",\\n+ }:\\n+ response_contract = raw_response_format\\n+ text_config = body.get(\\\"text\\\")\\n+ text_format = text_config.get(\\\"format\\\") if isinstance(text_config, dict) else None\\n+ if isinstance(text_format, dict) and text_format.get(\\\"type\\\") in {\\\"json_object\\\", \\\"json_schema\\\"}:\\n+ if text_format[\\\"type\\\"] == \\\"json_object\\\":\\n+ response_contract = {\\\"type\\\": \\\"json_object\\\"}\\n+ else:\\n+ response_contract = {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\n+ key: text_format[key]\\n+ for key in (\\\"name\\\", \\\"description\\\", \\\"schema\\\", \\\"strict\\\")\\n+ if key in text_format\\n+ },\\n+ }\\n+ _reject_responses_orchestration_controls(body)\\n+ chat_payload = _responses_to_chat_payload(body)\\n+ response_max_tokens = (\\n+ responses_max_output_tokens\\n+ if responses_max_output_tokens is not None\\n+ else responses_max_completion_tokens\\n+ if responses_max_completion_tokens is not None\\n+ else responses_max_tokens\\n )\\n+ started_at = time.perf_counter()\\n+ with orchestrator.client.request_settings(\\n+ max_output_tokens=response_max_tokens,\\n+ ):\\n+ result = self._run(lambda: coordinator.complete(\\n+ chat_payload[\\\"messages\\\"],\\n+ mode=\\\"conduct\\\",\\n+ attribution=_validate_attribution(body.get(\\\"attribution\\\")),\\n+ hints=_validate_routing(body.get(\\\"routing\\\")),\\n+ model_name=model_name,\\n+ workflow_run_id=f\\\"run_{uuid.uuid4().hex}\\\",\\n+ response_format=response_contract,\\n+ provider_request=body,\\n+ provider_endpoint=\\\"responses\\\",\\n+ ))\\n+ if response_contract is not None:\\n+ _validate_structured_completion_answer(result.get(\\\"answer\\\"), response_contract)\\n+ provider_response = result.get(\\\"provider_response\\\")\\n+ if not isinstance(provider_response, dict):\\n+ raise RuntimeError(\\\"Responses completion omitted provider response\\\")\\n+ orchestrated = dict(provider_response)\\n+ orchestrated[\\\"model\\\"] = model_name\\n+ if \\\"usage\\\" not in orchestrated and isinstance(result.get(\\\"usage\\\"), dict):\\n+ orchestrated[\\\"usage\\\"] = _responses_usage(result[\\\"usage\\\"])\\n orchestrator.record_analytics_event(\\n- \\\"responses_passthrough\\\",\\n+ \\\"responses_orchestrated\\\",\\n {\\n \\\"endpoint_path\\\": \\\"/v1/responses\\\",\\n \\\"actor_scope\\\": \\\"inference\\\",\\n \\\"status_code\\\": 200,\\n \\\"duration_ms\\\": round((time.perf_counter() - started_at) * 1000, 2),\\n },\\n )\\n- if body.get(\\\"stream\\\") is True:\\n- self._send_sse(responses_sse_body(proxied))\\n- else:\\n- self._send(proxied)\\n+ self._send(orchestrated)\\n return\\n \\n if path == \\\"/admin/simulate\\\":\\n@@ -5611,6 +5962,8 @@ def do_POST(self) -> None: # noqa: N802\\n self._send_error(500, \\\"internal_error\\\", \\\"internal server error\\\")\\n \\n def _authorize(self, scope: str) -> None:\\n+ self._trace_token = attach_trace_context(self.headers)\\n+ self._bind_session(session_id_from_headers(self.headers))\\n security.check_rate_limit(self.client_address[0])\\n security.authorize(self.headers, scope, self.client_address[0])\\n \\n@@ -5646,15 +5999,49 @@ def _parse_optional_int(self, query: dict[str, list[str]], field_name: str) -> i\\n return int(raw)\\n \\n def _read_json(self) -> dict[str, Any]:\\n+ \\\"\\\"\\\"Read one bounded, fixed-length JSON body and close bad frames.\\\"\\\"\\\"\\n if self.headers.get(\\\"content-type\\\", \\\"\\\").split(\\\";\\\", 1)[0].strip().lower() != \\\"application/json\\\":\\n raise RequestError(415, \\\"unsupported_media_type\\\", \\\"content-type must be application/json\\\")\\n- body_size = int(self.headers.get(\\\"content-length\\\", \\\"0\\\"))\\n- if body_size > security.max_body_bytes:\\n- raise RequestError(413, \\\"request_too_large\\\", \\\"request body exceeds configured limit\\\")\\n- raw = self.rfile.read(body_size)\\n- return _coerce_json(raw) if raw else {}\\n+ try:\\n+ body_size = _parse_request_framing(self.headers, security.max_body_bytes)\\n+ except RequestError:\\n+ self.close_connection = True\\n+ raise\\n+ if body_size == 0:\\n+ return {}\\n+ connection = getattr(self, \\\"connection\\\", None)\\n+ previous_timeout = None\\n+ timeout_supported = all(\\n+ hasattr(connection, method) for method in (\\\"gettimeout\\\", \\\"settimeout\\\")\\n+ )\\n+ if timeout_supported:\\n+ previous_timeout = connection.gettimeout()\\n+ connection.settimeout(security.request_read_timeout_seconds)\\n+ read_deadline = time.monotonic() + security.request_read_timeout_seconds\\n+ try:\\n+ chunks = bytearray()\\n+ while len(chunks) < body_size:\\n+ if time.monotonic() >= read_deadline:\\n+ self.close_connection = True\\n+ raise RequestError(408, \\\"request_read_timeout\\\", \\\"request body read timed out\\\")\\n+ chunk = self.rfile.read(body_size - len(chunks))\\n+ if not chunk:\\n+ self.close_connection = True\\n+ raise RequestError(400, \\\"invalid_request_framing\\\", \\\"request body ended before content-length\\\")\\n+ chunks.extend(chunk)\\n+ if len(chunks) < body_size and time.monotonic() >= read_deadline:\\n+ self.close_connection = True\\n+ raise RequestError(408, \\\"request_read_timeout\\\", \\\"request body read timed out\\\")\\n+ except (TimeoutError, socket.timeout):\\n+ self.close_connection = True\\n+ raise RequestError(408, \\\"request_read_timeout\\\", \\\"request body read timed out\\\") from None\\n+ finally:\\n+ if timeout_supported:\\n+ connection.settimeout(previous_timeout)\\n+ return _coerce_json(bytes(chunks))\\n \\n def log_message(self, format: str, *args: object) -> None:\\n+ \\\"\\\"\\\"Disable the base server's unaudited stderr access log.\\\"\\\"\\\"\\n return\\n \\n def _send_error(\\n@@ -5664,6 +6051,13 @@ def _send_error(\\n message: str,\\n detail: dict[str, Any] | None = None,\\n ) -> None:\\n+ _LOGGER.warning(\\n+ \\\"request_failed status=%s code=%s path=%s session_id=%s\\\",\\n+ status,\\n+ code,\\n+ urllib.parse.urlparse(self.path).path,\\n+ current_session_id() or \\\"\\\",\\n+ )\\n self._send(_error_payload(code, message, {\\\"request_id\\\": uuid.uuid4().hex, **(detail or {})}), status)\\n \\n def _send(self, payload: dict[str, Any], status: int = 200) -> None:\\n@@ -5752,8 +6146,16 @@ def serve(\\n port: int = 8000,\\n security: SecurityConfig | None = None,\\n clearfolio_url: str | None = None,\\n+ coordinator: CostRoutingCoordinator | None = None,\\n ) -> None:\\n \\\"\\\"\\\"Serve the admin console and resource-oriented orchestration API.\\\"\\\"\\\"\\n- server = build_server(orchestrator, host=host, port=port, security=security, clearfolio_url=clearfolio_url)\\n+ server = build_server(\\n+ orchestrator,\\n+ host=host,\\n+ port=port,\\n+ security=security,\\n+ clearfolio_url=clearfolio_url,\\n+ coordinator=coordinator,\\n+ )\\n print(f\\\"listening on http://{host}:{port}\\\")\\n server.serve_forever()\" }, { \"sha\": \"27424e96201cd7d326d014f78996d7a6f1b49690\", \"filename\": \"contextual_orchestrator/telemetry.py\", \"status\": \"added\", \"additions\": 207, \"deletions\": 0, \"changes\": 207, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Ftelemetry.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/contextual_orchestrator%2Ftelemetry.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/contextual_orchestrator%2Ftelemetry.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,207 @@\\n+\\\"\\\"\\\"Prompt-safe OpenTelemetry and session correlation for the gateway.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import logging\\n+from collections.abc import Iterator, Mapping\\n+from contextlib import contextmanager\\n+from contextvars import ContextVar, Token\\n+from typing import Any\\n+\\n+try:\\n+ from opentelemetry import trace\\n+ from opentelemetry.context import attach as _otel_attach\\n+ from opentelemetry.context import detach as _otel_detach\\n+ from opentelemetry.propagate import extract as _otel_extract\\n+ from opentelemetry.propagate import inject as _otel_inject\\n+ from opentelemetry.trace import SpanKind, Status, StatusCode\\n+except ImportError: # pragma: no cover - dependency is declared by the project\\n+ trace = None # type: ignore[assignment]\\n+ _otel_attach = None\\n+ _otel_detach = None\\n+ _otel_extract = None\\n+ _otel_inject = None\\n+ SpanKind = None # type: ignore[assignment,misc]\\n+ Status = None # type: ignore[assignment,misc]\\n+ StatusCode = None # type: ignore[assignment,misc]\\n+\\n+_LOGGER = logging.getLogger(__name__)\\n+_CURRENT_SESSION: ContextVar[str | None] = ContextVar(\\n+ \\\"contextual_orchestrator_session_id\\\", default=None\\n+)\\n+_CONFIGURED = False\\n+\\n+\\n+def _otlp_trace_endpoint(endpoint: str) -> str:\\n+ \\\"\\\"\\\"Turn an OTLP base endpoint into the explicit HTTP traces endpoint.\\\"\\\"\\\"\\n+ normalized = endpoint.rstrip(\\\"/\\\")\\n+ if normalized.casefold().endswith(\\\"/v1/traces\\\"):\\n+ return normalized\\n+ return f\\\"{normalized}/v1/traces\\\"\\n+\\n+\\n+def _config_value(config: Any | None, key: str, default: Any = None) -> Any:\\n+ \\\"\\\"\\\"Read one telemetry setting from the injected KV configuration.\\\"\\\"\\\"\\n+ if config is None:\\n+ return default\\n+ return config.get(\\\"telemetry\\\", key, default)\\n+\\n+\\n+def _normalize_session_id(value: object) -> str | None:\\n+ \\\"\\\"\\\"Accept a bounded correlation value without accepting a bearer token.\\\"\\\"\\\"\\n+ if not isinstance(value, str):\\n+ return None\\n+ value = value.strip()\\n+ if not value or len(value) > 128 or any(ord(char) < 0x20 for char in value):\\n+ return None\\n+ return value\\n+\\n+\\n+def session_id_from_headers(headers: Mapping[str, str]) -> str | None:\\n+ \\\"\\\"\\\"Read the LineageWeave correlation header from an HTTP request.\\\"\\\"\\\"\\n+ return _normalize_session_id(\\n+ headers.get(\\\"x-lineageweave-session-id\\\") or headers.get(\\\"x-session-id\\\")\\n+ )\\n+\\n+\\n+def session_id_from_metadata(metadata: Mapping[str, Any] | None) -> str | None:\\n+ \\\"\\\"\\\"Read a session value from compatible OpenAI metadata fields.\\\"\\\"\\\"\\n+ if metadata is None:\\n+ return None\\n+ return _normalize_session_id(\\n+ metadata.get(\\\"lineageweave_post_session_id\\\") or metadata.get(\\\"session_id\\\")\\n+ )\\n+\\n+\\n+def current_session_id() -> str | None:\\n+ \\\"\\\"\\\"Return the request-scoped correlation value, if one is bound.\\\"\\\"\\\"\\n+ return _CURRENT_SESSION.get()\\n+\\n+\\n+def set_session_id(value: object) -> Token[str | None]:\\n+ \\\"\\\"\\\"Bind one session to the current request context.\\\"\\\"\\\"\\n+ return _CURRENT_SESSION.set(_normalize_session_id(value))\\n+\\n+\\n+def reset_session_id(token: Token[str | None]) -> None:\\n+ \\\"\\\"\\\"Restore the context value that preceded a request.\\\"\\\"\\\"\\n+ _CURRENT_SESSION.reset(token)\\n+\\n+\\n+def attach_trace_context(headers: Mapping[str, str]) -> Any:\\n+ \\\"\\\"\\\"Attach an inbound W3C trace context and return its reset token.\\\"\\\"\\\"\\n+ if _otel_extract is None or _otel_attach is None:\\n+ return None\\n+ carrier = {str(key).lower(): str(value) for key, value in headers.items()}\\n+ return _otel_attach(_otel_extract(carrier))\\n+\\n+\\n+def detach_trace_context(token: Any) -> None:\\n+ \\\"\\\"\\\"Detach an inbound W3C trace context after one HTTP request.\\\"\\\"\\\"\\n+ if token is not None and _otel_detach is not None:\\n+ _otel_detach(token)\\n+\\n+\\n+def inject_trace_context(headers: dict[str, str]) -> None:\\n+ \\\"\\\"\\\"Inject the active W3C trace context into one provider request.\\\"\\\"\\\"\\n+ if _otel_inject is not None:\\n+ _otel_inject(headers)\\n+\\n+\\n+def _safe_attributes(\\n+ attributes: Mapping[str, Any] | None,\\n+) -> dict[str, str | int | float | bool]:\\n+ \\\"\\\"\\\"Keep span attributes scalar and exclude prompt, answer, and secret content.\\\"\\\"\\\"\\n+ result: dict[str, str | int | float | bool] = {}\\n+ for key, value in (attributes or {}).items():\\n+ if (\\n+ not isinstance(key, str)\\n+ or not key\\n+ or isinstance(value, (dict, list, tuple, set))\\n+ ):\\n+ continue\\n+ if isinstance(value, str):\\n+ result[key] = value[:256]\\n+ elif isinstance(value, (bool, int, float)):\\n+ result[key] = value\\n+ session_id = current_session_id()\\n+ if session_id:\\n+ result.setdefault(\\\"contextual_orchestrator.session_id\\\", session_id)\\n+ return result\\n+\\n+\\n+def configure_telemetry(\\n+ service_name: str = \\\"contextual-orchestrator\\\",\\n+ *,\\n+ config: Any | None = None,\\n+) -> None:\\n+ \\\"\\\"\\\"Configure OTLP export from the injected KV configuration only.\\\"\\\"\\\"\\n+ global _CONFIGURED\\n+ if _CONFIGURED:\\n+ return\\n+ if config is None:\\n+ _LOGGER.debug(\\\"OpenTelemetry is not configured without a KV store\\\")\\n+ return\\n+ _CONFIGURED = True\\n+ if str(_config_value(config, \\\"sdk_disabled\\\", \\\"\\\")).lower() == \\\"true\\\":\\n+ return\\n+ endpoint = str(_config_value(config, \\\"exporter_otlp_endpoint\\\", \\\"\\\")).strip()\\n+ if trace is None or not endpoint:\\n+ return\\n+ try:\\n+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (\\n+ OTLPSpanExporter,\\n+ )\\n+ from opentelemetry.sdk.resources import Resource\\n+ from opentelemetry.sdk.trace import TracerProvider\\n+ from opentelemetry.sdk.trace.export import BatchSpanProcessor\\n+ except ImportError: # pragma: no cover - guarded by the runtime dependency\\n+ _LOGGER.warning(\\\"OpenTelemetry SDK/exporter is unavailable\\\")\\n+ return\\n+\\n+ configured_service_name = str(\\n+ _config_value(config, \\\"service_name\\\", service_name)\\n+ ).strip() or service_name\\n+ resource = Resource.create({\\n+ \\\"service.name\\\": configured_service_name,\\n+ \\\"service.namespace\\\": \\\"contextualwisdomlab\\\",\\n+ })\\n+ provider = TracerProvider(resource=resource)\\n+ provider.add_span_processor(\\n+ BatchSpanProcessor(\\n+ OTLPSpanExporter(endpoint=_otlp_trace_endpoint(endpoint))\\n+ )\\n+ )\\n+ trace.set_tracer_provider(provider)\\n+\\n+\\n+@contextmanager\\n+def traced(\\n+ name: str,\\n+ attributes: Mapping[str, Any] | None = None,\\n+) -> Iterator[Any]:\\n+ \\\"\\\"\\\"Trace one provider CLIENT operation and preserve all failures.\\\"\\\"\\\"\\n+ if trace is None: # pragma: no cover - dependency is declared by the project\\n+ yield None\\n+ return\\n+ tracer = trace.get_tracer(\\\"contextual-orchestrator\\\")\\n+ safe = _safe_attributes(attributes)\\n+ with tracer.start_as_current_span(\\n+ name,\\n+ kind=SpanKind.CLIENT,\\n+ attributes=safe,\\n+ ) as span:\\n+ try:\\n+ yield span\\n+ except Exception as exc:\\n+ if Status is not None and StatusCode is not None:\\n+ span.record_exception(exc)\\n+ span.set_attribute(\\\"error.type\\\", type(exc).__name__)\\n+ span.set_status(Status(StatusCode.ERROR))\\n+ _LOGGER.warning(\\n+ \\\"telemetry.operation_failed operation=%s error_type=%s session_id=%s\\\",\\n+ name,\\n+ type(exc).__name__,\\n+ safe.get(\\\"contextual_orchestrator.session_id\\\", \\\"\\\"),\\n+ )\\n+ raise\" }, { \"sha\": \"c04f528e099aed5de2d0772646adc33af8a0f569\", \"filename\": \"docs/adr/0122-otel-session-observability.md\", \"status\": \"added\", \"additions\": 54, \"deletions\": 0, \"changes\": 54, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fadr%2F0122-otel-session-observability.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fadr%2F0122-otel-session-observability.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fadr%2F0122-otel-session-observability.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,54 @@\\n+# ADR 0122: Correlate gateway provider telemetry by caller session\\n+\\n+## Status\\n+\\n+Accepted.\\n+\\n+## Context\\n+\\n+The gateway can route one request through several workers and providers. A\\n+caller-provided post session already exists in compatible metadata, but it was\\n+not bound to the HTTP request or provider diagnostics. The organization GRC\\n+service owns the low-cardinality, secret-free telemetry control in its ADR\\n+0009; this service must emit evidence that can be consumed there without\\n+becoming a second GRC store.\\n+\\n+## Decision\\n+\\n+Telemetry deployment settings may enter the process KV during bootstrap from non-secret OTEL_* transport settings; runtime telemetry reads the injected KV only. A configured OTLP base URL is normalized to the HTTP /v1/traces signal endpoint.\\n+\\n+1. Use the OpenTelemetry Python API, SDK, and OTLP HTTP exporter. Export is\\n+ disabled unless `OTEL_EXPORTER_OTLP_ENDPOINT` is explicitly configured.\\n+2. Accept `X-LineageWeave-Session-Id` and compatible metadata fields, bind the\\n+ normalized value to the request context, and reset it when the request\\n+ handler finishes.\\n+3. Add the bounded session correlation to provider spans for chat and embedding\\n+ calls. Follow the current OpenTelemetry GenAI span convention: emit CLIENT\\n+ spans named `chat {model}` or `embeddings {model}`, include the required\\n+ `gen_ai.operation.name` and `gen_ai.provider.name` attributes, and use\\n+ `server.address` / `server.port` for the transport destination. Record\\n+ `error.type` on failure, but never prompt, answer, request body, API key, or\\n+ raw provider response.\\n+4. Keep structured-output, Responses API, VISION, embedding, and multi-agent\\n+ requests on the same orchestration path. Telemetry observes that path; it\\n+ does not introduce a single-agent fallback or a second credential source.\\n+\\n+## Consequences\\n+\\n+An operator can follow one LineageWeave post through gateway routing and\\n+provider failures while GRC receives aggregate operational evidence rather\\n+than copied product data. Session correlation is diagnostic only: it is not an\\n+identity, tenant, authorization, or evidence label.\\n+\\n+## References\\n+\\n+OpenTelemetry Authors. (n.d.). *Manual instrumentation with OpenTelemetry\\n+Python*. Retrieved August 21, 2026, from\\n+https://opentelemetry.io/docs/languages/python/instrumentation/\\n+\\n+OpenTelemetry Authors. (n.d.). *Service semantic conventions*. Retrieved\\n+August 21, 2026, from https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/\\n+\\n+OpenTelemetry Authors. (n.d.). *Semantic conventions for generative AI spans*.\\n+Retrieved August 21, 2026, from\\n+https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md\" }, { \"sha\": \"bfdafea3a1e22e34b6ba32226cf68fee3dc9f7bb\", \"filename\": \"docs/architecture.md\", \"status\": \"modified\", \"additions\": 33, \"deletions\": 2, \"changes\": 35, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Farchitecture.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Farchitecture.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Farchitecture.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -6,6 +6,10 @@\\n - Sakana Fugu Technical Report: https://github.com/SakanaAI/fugu/blob/main/Fugu_technical_report.pdf\\n - TRINITY: An Evolved LLM Coordinator: https://arxiv.org/abs/2512.04695\\n - Learning to Orchestrate Agents in Natural Language with the Conductor: https://arxiv.org/abs/2512.04388\\n+- Route to Reason: Adaptive Routing for LLM and Reasoning Strategy Selection: https://arxiv.org/abs/2505.19435\\n+- Route-and-Reason: Scaling Large Language Model Reasoning with Reinforced Model Router: https://arxiv.org/abs/2506.05901\\n+- Reasoning on a Budget: A Survey of Adaptive and Controllable Test-Time Compute in LLMs: https://arxiv.org/abs/2507.02076\\n+- Ares: Adaptive Reasoning Effort Selection for Efficient LLM Agents: https://arxiv.org/abs/2603.07915\\n \\n ## What The Architecture Is\\n \\n@@ -24,6 +28,17 @@ The useful split is quality-latency, not separate products:\\n - Low-latency routing: select one worker for the current query or turn.\\n - Deep orchestration: create a multi-step workflow when the task needs decomposition, independent attempts, verification, or synthesis.\\n \\n+Structured provider features do not create a third single-agent path. A\\n+non-null `response_format` or Responses request enters the conducted workflow\\n+and reaches one final provider only after the\\n+Thinker/Worker/Verifier/Synthesizer evidence has been assembled. The final\\n+provider call preserves the validated wire feature; it is a transport boundary,\\n+not a bypass of orchestration. JSON object and JSON schema are both covered by\\n+[ADR 0011](planning/adrs/0011-structured-provider-features-stay-orchestrated.md).\\n+The explicit client-owned tool-loop exception remains the single-worker\\n+contract defined by [ADR 0014](planning/adrs/0014-gateway-owned-model-selection.md);\\n+ordinary tool declarations without that opt-in fail closed.\\n+\\n TRINITY contributes the compact coordinator idea: a small model representation plus a lightweight head can choose agent and role over multiple turns. Its Thinker, Worker, and Verifier contracts are practical enough to implement directly.\\n \\n Conductor contributes the workflow representation: each step is a natural-language subtask, an assigned worker, and an access list of prior step outputs. This is the key piece for preventing every worker from being dragged into the same transcript while still allowing deliberate collaboration.\\n@@ -48,10 +63,23 @@ bounded, authenticated recursion protocol; it is not administratively disabled.\\n - `Orchestrator.route_once`: the low-latency routing path.\\n - `Orchestrator.conduct`: the workflow path with planner, worker, verifier, and synthesizer steps.\\n - `WorkflowStep.access`: Conductor-style visibility control.\\n+- Image-bearing Chat Completions and Responses retain their typed source image\\n+ blocks in every evidence-bearing workflow step; access lists still constrain\\n+ prior model outputs. See [ADR 0018](planning/adrs/0018-multimodal-evidence-preserving-orchestration.md).\\n - `ModelClient`: OpenAI-compatible HTTP client, with `mock://` for local checks.\\n - `contextual_orchestrator.server`: small `/v1/chat/completions` HTTP server.\\n \\n-The deliberate simplification is the policy. The paper systems learn routing and topology from rewards; this lab uses a deterministic capability-hint heuristic only for worker/role routing so the repo runs without training data, GPUs, or vendor credentials. It is never an answer-quality, verification, or accept/reject judgment: verifier decisions must use the structured model judge and fail closed (see [ADR 0001](planning/adrs/0001-fail-closed-model-judgment.md)).\\n+The deliberate simplification is the policy. The paper systems learn routing\\n+and topology from rewards; this lab uses capability evidence and a bounded\\n+orchestrator policy, with `auto` kept internal rather than sent as a provider\\n+value. This is never an answer-quality, verification, or accept/reject\\n+judgment: verifier decisions must use the structured model judge and fail\\n+closed (see [ADR 0001](planning/adrs/0001-fail-closed-model-judgment.md)).\\n+\\n+Model and reasoning changes are governed by [ADR\\n+0013](planning/adrs/0013-paper-grounded-adaptive-reasoning-policy.md). The\\n+provider-neutral gateway boundary and direct-MLX prohibition are governed by\\n+[ADR 0012](planning/adrs/0012-gateway-only-provider-contract.md).\\n \\n Add learned routing only when there is an evaluation set and logs proving the heuristic policy is the bottleneck.\\n \\n@@ -73,6 +101,9 @@ OpenAI. (n.d.-a). *Create chat completion*. OpenAI Platform. https://platform.op\\n \\n OpenAI. (n.d.-b). *Create a model response*. OpenAI Platform. https://platform.openai.com/docs/api-reference/responses/create\\n \\n+Tang, Y., et al. (2026). *Sakana Fugu technical report* (arXiv:2606.21228).\\n+arXiv. https://doi.org/10.48550/arXiv.2606.21228\\n+\\n Sakana AI. (2026, June 22). *Sakana Fugu: One model to command them all*. https://sakana.ai/fugu-release/\\n \\n Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *Trinity: An evolved LLM coordinator* (arXiv:2512.04695). https://doi.org/10.48550/arXiv.2512.04695\\n@@ -92,4 +123,4 @@ The product is not a Fugu clone. It is a control-plane prototype for the same pu\\n See [product_planning.md](product_planning.md) for the product reboot.\\n \\n \\n-OpenAI o-series `reasoning_effort` (chat/Completions) and Responses `reasoning.effort` accept known levels `none`/`minimal`/`low`/`medium`/`high` (casefold, strip) as default-effort no-ops when this gateway has no effort plane; unknown levels fail closed with named errors. Locked by `tests/test_reasoning_effort_low_medium_high_noop_http_honesty.py` on tip ≥ #738.\\n+OpenAI o-series `reasoning_effort` (chat/Completions) and Responses `reasoning.effort` accept provider levels `none`/`minimal`/`low`/`medium`/`high` plus the orchestrator-owned `auto` policy value (casefold, strip). They are default-effort no-ops when this gateway has no effort plane; unknown levels fail closed with named errors. Locked by the reasoning HTTP honesty tests on tip ≥ #738.\" }, { \"sha\": \"caca05ee5d4a65a54c8672c9f26e4bb0f0dcdf71\", \"filename\": \"docs/doctoring/OPENTELEMETRY_REFERENCES.md\", \"status\": \"added\", \"additions\": 29, \"deletions\": 0, \"changes\": 29, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fdoctoring%2FOPENTELEMETRY_REFERENCES.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fdoctoring%2FOPENTELEMETRY_REFERENCES.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdoctoring%2FOPENTELEMETRY_REFERENCES.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,29 @@\\n+# OpenTelemetry references and implementation traceability\\n+\\n+## Normative references\\n+\\n+- OpenTelemetry Authors. (n.d.). *Manual instrumentation with OpenTelemetry\\n+ Python*. Retrieved August 21, 2026, from\\n+ https://opentelemetry.io/docs/languages/python/instrumentation/\\n+- OpenTelemetry Authors. (n.d.). *Service semantic conventions*. Retrieved\\n+ August 21, 2026, from\\n+ https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/\\n+- OpenTelemetry Authors. (n.d.). *Semantic conventions for generative AI\\n+ spans*. Retrieved August 21, 2026, from\\n+ https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md\\n+- ContextualWisdomLab governance-risk-compliance. (2026). *ADR 0009:\\n+ Emit bounded OpenTelemetry request telemetry*. Retrieved August 21, 2026,\\n+ from https://github.com/ContextualWisdomLab/governance-risk-compliance/blob/develop/docs/adr/0009-opentelemetry-request-telemetry.md\\n+\\n+## Implementation mapping\\n+\\n+| Concern | Implementation | Evidence boundary |\\n+| --- | --- | --- |\\n+| Service resource | `OTEL_SERVICE_NAME`, default `contextual-orchestrator` | One logical service name per deployment |\\n+| Request correlation | `X-LineageWeave-Session-Id` and compatible metadata | Correlation only; not identity or authorization |\\n+| Provider calls | `ModelClient` chat/embedding CLIENT spans | Required GenAI operation/provider attributes, model and server destination; no prompt, answer, key, or response |\\n+| Export | Bootstrap OTEL_EXPORTER_OTLP_ENDPOINT into the process KV | Disabled by default; runtime reads KV and sends to the normalized /v1/traces signal |\\n+\\n+The GRC repository remains the organization control and evidence owner. The\\n+gateway emits operational signals and does not copy GRC tables or provider\\n+credentials.\" }, { \"sha\": \"a1bdcf76bca059234fe8bf91a7fd49671cde5437\", \"filename\": \"docs/doctoring/embedding-chat-capability-isolation.md\", \"status\": \"added\", \"additions\": 131, \"deletions\": 0, \"changes\": 131, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fdoctoring%2Fembedding-chat-capability-isolation.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fdoctoring%2Fembedding-chat-capability-isolation.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdoctoring%2Fembedding-chat-capability-isolation.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,131 @@\\n+# Embedding-to-chat capability isolation incident\\n+\\n+**Status:** Accepted incident decision\\n+**Date:** 2026-08-20\\n+**Affected consumer:** LineageWeave buyer-surface stack around PR #260\\n+\\n+## Incident\\n+\\n+A conducted workflow reached the final contextual-orchestrator synthesizer with\\n+`model_group=text-embedding-3-large` and deployment\\n+`azure/text-embedding-3-large`. The gateway rejected the chat operation as\\n+unsupported. Its configured fallback map contained chat-generation model groups,\\n+but no fallback attached to the embedding group.\\n+\\n+The missing fallback was a symptom, not the causal defect. An embedding deployment\\n+had already crossed the chat-agent capability boundary and become eligible for a\\n+worker role.\\n+\\n+## Causal boundary\\n+\\n+Provider-compatible `/models` registries can contain multiple endpoint families.\\n+The original discovery parser accepted every non-empty model identifier and exposed\\n+it to agent creation, price selection, and durable pool synchronization. A catalog\\n+row naming an embedding deployment could therefore be scored for thinker, worker,\\n+verifier, or synthesizer work even though its serving endpoint accepts embedding\\n+input rather than chat messages.\\n+\\n+The first incident fix closed discovery and price-routing boundaries, but further\\n+root-cause tracing showed an already-persisted incompatible `ModelAgent` could still\\n+survive that filter. The runtime ranking path, generated workflow assignment,\\n+cross-agent failover, readiness probe, streaming path, and direct\\n+`ModelClient.chat()` path previously trusted the persisted model identifier. That\\n+stale-state path is sufficient to reproduce the same unsupported Azure chat\\n+operation after a process restart or durable bootstrap.\\n+\\n+OpenAI documents `text-embedding-3-large` under the embeddings endpoint, separately\\n+from models supported by chat completions. Microsoft likewise demonstrates it with\\n+`client.embeddings.create`, not `client.chat.completions.create`. LiteLLM exposes\\n+chat, responses, embeddings, image, audio, rerank, and other endpoint families as\\n+distinct operations. A router fallback can choose another deployment for the same\\n+operation; it cannot make an embedding deployment execute a chat operation.\\n+\\n+## Decision\\n+\\n+Chat transport compatibility and general agent-role eligibility are separate\\n+shared runtime invariants. A provider may expose an audio-capable model or a\\n+policy classifier through Chat Completions while that model remains unsuitable\\n+for ordinary thinker, worker, verifier, or synthesizer work.\\n+\\n+1. Normalize provider prefixes and common separators in model identifiers.\\n+2. At the transport boundary, reject identifiers that clearly advertise embedding,\\n+ reranking, transcription, moderation-endpoint, image-generation, realtime, or\\n+ speech-only semantics.\\n+3. Keep provider-documented audio and policy-classifier models transport-compatible\\n+ when they are served through Chat Completions.\\n+4. At discovery and ordinary orchestration-role boundaries, additionally exclude\\n+ explicit guard, safety, and NemoGuard policy classifiers.\\n+5. Apply the general-role guard while parsing both OpenAI-compatible and Bytez\\n+ catalogs and before converting, pricing, or cost-selecting a discovery record.\\n+6. Remove stale ineligible agents from thinker, worker, verifier, and synthesizer\\n+ ranking even if a durable configuration still contains them.\\n+7. Reselect a generated workflow step that explicitly names a stale ineligible\\n+ agent and omit such agents from planner inventory.\\n+8. Remove ineligible agents from cross-agent failover candidates.\\n+9. Apply the transport guard at `ModelClient.chat()`, `stream_chat()`, and\\n+ readiness probing before mock or network transport.\\n+10. Fail closed when no general chat agent remains.\\n+11. Leave unknown identifiers eligible without fabricating reasoning, tool, vision,\\n+ or verification capabilities from their names.\\n+\\n+This is deliberately a conservative negative filter. A future capability registry\\n+may replace name-based exclusion with authenticated provider metadata, measured\\n+endpoint probes, and separate endpoint-specific pools. Until that evidence exists,\\n+a clearly incompatible model fails closed at transport boundaries and a clearly\\n+specialized policy model fails closed at general-role boundaries.\\n+\\n+## Rejected response\\n+\\n+Adding `text-embedding-3-large` to a chat fallback map is rejected. It would retain\\n+the invalid primary assignment and merely hide it when a fallback happened to be\\n+available. Repeated provider retries are also rejected because the request is\\n+structurally unsupported, not transiently unavailable.\\n+\\n+## Residual operational action\\n+\\n+Runtime containment means an already-persisted embedding agent can no longer win\\n+chat selection or failover while stale data is being cleaned up. Durable state must\\n+still converge to the correct exact set: the provider-bootstrap slice owns stale\\n+discovered-agent withdrawal and must import the same shared classifier when rebased.\\n+Runtime rejection is defense in depth, not a substitute for deleting invalid\\n+persistent configuration.\\n+\\n+## Verification evidence\\n+\\n+`tests/test_chat_model_capability_isolation.py` reproduces the exact Azure model ID\\n+and provider/separator aliases. Together with\\n+`tests/test_chat_capability_unknown_identifiers.py`,\\n+`tests/test_chat_transport_role_separation.py`, and\\n+`tests/test_chat_passthrough_capability_isolation.py`, it verifies:\\n+\\n+- OpenAI-compatible and Bytez catalog filtering;\\n+- malformed and prefix-only identifier handling;\\n+- agent-conversion rejection;\\n+- exclusion from the price book and cheapest-agent selection;\\n+- exclusion of a high-priority stale embedding agent from synthesizer selection;\\n+- fail-closed behavior when the persisted pool contains only non-chat agents;\\n+- generated-plan reassignment away from a stale embedding agent;\\n+- exclusion from cross-agent failover;\\n+- direct and streaming `ModelClient` rejection before transport;\\n+- readiness failure with a stable non-chat code before provider access;\\n+- planner inventory and generated-plan isolation;\\n+- distinction between chat-served audio/policy models and general agent roles.\\n+- conservative unknown-identifier handling, including unrelated `vanguard` names;\\n+- endpoint-family exclusions for image-generation (`dall-e`), CLIP, and SigLIP;\\n+- normalized `/v1/responses` passthrough and pre-transport rejection of embedding models.\\n+\\n+## References\\n+\\n+BerriAI. (n.d.). *LiteLLM: Call 100+ LLMs using the OpenAI input/output format*.\\n+Retrieved August 20, 2026, from https://docs.litellm.ai/\\n+\\n+Microsoft. (n.d.). *How to switch between OpenAI and Azure OpenAI endpoints*.\\n+Microsoft Learn. Retrieved August 20, 2026, from\\n+https://learn.microsoft.com/en-us/azure/developer/ai/how-to/switching-endpoints\\n+\\n+OpenAI. (n.d.). *Data controls in the OpenAI platform: Default usage policies by\\n+endpoint*. Retrieved August 20, 2026, from\\n+https://platform.openai.com/docs/models/default-usage-policies-by-endpoint\\n+\\n+OpenAI. (n.d.). *GPT-audio model*. Retrieved August 20, 2026, from\\n+https://developers.openai.com/api/docs/models/gpt-audio\" }, { \"sha\": \"6a50017d8629cfe80f8c2fe5696e5cbd43db78d6\", \"filename\": \"docs/doctoring/inbound-request-framing.md\", \"status\": \"added\", \"additions\": 28, \"deletions\": 0, \"changes\": 28, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fdoctoring%2Finbound-request-framing.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fdoctoring%2Finbound-request-framing.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fdoctoring%2Finbound-request-framing.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,28 @@\\n+# Inbound request framing doctoring\\n+\\n+## Root cause\\n+\\n+`_read_json` used `int(headers[\\\"Content-Length\\\"])` and read that value without\\n+handling unsupported transfer coding, duplicate fields, premature EOF, or\\n+a read deadline. A negative value could reach an unbounded `read(-1)`.\\n+\\n+## Implemented contract\\n+\\n+- exactly one ASCII decimal `Content-Length` is required;\\n+- unsupported `Transfer-Encoding` and every ambiguous combination fail closed;\\n+- declared size is checked before reading;\\n+- the body is read exactly and a premature EOF is rejected;\\n+- a finite request-read timeout is applied and restored;\\n+- framing failures close the connection and do not echo body/header content.\\n+\\n+## Verification\\n+\\n+```bash\\n+pytest -q tests/test_inbound_request_framing.py\\n+python -m compileall -q contextual_orchestrator\\n+git diff --check\\n+```\\n+\\n+The implementation follows HTTP/1.1 message framing and connection-management\\n+requirements in RFC 9112 (Fielding et al., 2022). It deliberately does not\\n+claim support for chunked transfer coding.\" }, { \"sha\": \"6ee39042960b7d82195ddfc539ef1d74884f8aae\", \"filename\": \"docs/kv-credentials.md\", \"status\": \"modified\", \"additions\": 7, \"deletions\": 19, \"changes\": 26, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fkv-credentials.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fkv-credentials.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fkv-credentials.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -21,8 +21,6 @@ register_credential(\\\"OPENAI_API_KEY\\\", value) # writes into the KV\\n The orchestrator resolves an agent's provider key through this seam only:\\n \\n - Remote `ModelAgent` records use `get_credential(agent.credential_name)`.\\n-- Direct `mlx://` workers are intentionally keyless and never receive a\\n- provider credential.\\n - Authenticated loopback `local://` gateways may use the separate,\\n explicitly named `ModelAgent.local_credential_key`.\\n - `ModelClient._send()` resolves the transport-specific key before building\\n@@ -50,30 +48,20 @@ string is treated as the **credential name** in the KV — it is *not* read as a\\n environment variable. `ModelAgent.credential_name` returns `api_key_env` when\\n present, otherwise `credential_key`.\\n \\n-### Direct MLX versus an authenticated local gateway\\n+### Authenticated local gateway\\n \\n-These schemes have different credential contracts:\\n+A `local://` URL denotes a provider-neutral loopback gateway. When that gateway\\n+requires bearer authentication, configure only its explicit local token name:\\n \\n ```json\\n-{ \\\"id\\\": \\\"mlx_worker\\\", \\\"model\\\": \\\"mlx-community/gemma-4-e4b-it-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:18083/v1\\\" }\\n-```\\n-\\n-The direct `mlx://` transport is a loopback-only, keyless mlx-lm server. A\\n-`credential_key` or remote `OPENAI_API_KEY` is never forwarded to it. A\\n-`local://` URL instead denotes the contextual-orchestrator loopback gateway;\\n-when that gateway requires bearer authentication, configure only its explicit\\n-local token name:\\n-\\n-```json\\n-{ \\\"id\\\": \\\"mlx_gateway\\\", \\\"model\\\": \\\"mlx-community/gemma-4-e4b-it-4bit\\\",\\n+{ \\\"id\\\": \\\"local_gateway\\\", \\\"model\\\": \\\"gateway-selected-model\\\",\\n \\\"base_url\\\": \\\"local://127.0.0.1:18084/v1\\\",\\n \\\"local_credential_key\\\": \\\"LOCAL_GATEWAY_TOKEN\\\" }\\n ```\\n \\n-The gateway owns worker template settings, so `chat_template_kwargs` is sent\\n-only to direct `mlx://` workers. Missing local gateway credentials fail closed;\\n-they do not fall back to an OpenAI credential or an unauthenticated request.\\n+The gateway owns worker-specific settings. Missing local gateway credentials\\n+fail closed; they do not fall back to an OpenAI credential or an\\n+unauthenticated request.\\n \\n ## Backends\\n \" }, { \"sha\": \"e645dab9f9f6004d831c7581b94bab1a88785f9a\", \"filename\": \"docs/library_research.md\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 2, \"changes\": 5, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Flibrary_research.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Flibrary_research.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Flibrary_research.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,6 +1,6 @@\\n # Library Research\\n \\n-The design researched existing libraries before adding code. The repository keeps the runtime dependency-free for the current lab, but the enterprise implementation target is explicit.\\n+The design researched existing libraries before adding code. Runtime dependencies are kept explicit and hash-locked; optional enterprise integrations remain separate until they carry product weight.\\n \\n ## Selected Stack\\n \\n@@ -13,12 +13,13 @@ The design researched existing libraries before adding code. The repository keep\\n | Migrations | [Alembic](https://alembic.sqlalchemy.org/) | Use for schema migration lifecycle. | Alembic is the SQLAlchemy migration tool and supports autogenerated migrations from metadata. |\\n | Database | [PostgreSQL](https://www.postgresql.org/docs/current/sql-syntax-lexical.html) | Default relational store. | PostgreSQL identifiers allow letters, digits, and underscores; the project standardizes on unquoted lower snake_case. |\\n | API contract | [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0.html) | Contract format for API review and client generation. | OAS defines a language-agnostic HTTP API description for humans and machines. |\\n+| Observability | [OpenTelemetry Python](https://opentelemetry.io/docs/languages/python/) | Use the API, SDK, and OTLP HTTP exporter for prompt-safe request/provider spans. | The standard Python API/SDK separates instrumentation from export; this repository keeps export disabled unless the injected process KV supplies an OTLP endpoint. |\\n \\n ## Ponytail Decision\\n \\n No new dependency is added until it carries real product weight:\\n \\n-- Current prototype: stdlib server, handwritten OpenAPI, static admin UI.\\n+- Current prototype: stdlib server, handwritten OpenAPI, static admin UI, and bounded OpenTelemetry instrumentation.\\n - First enterprise cut: FastAPI + React-admin + i18next + PostgreSQL + SQLAlchemy + Alembic.\\n - Do not add provider SDKs until raw OpenAI-compatible HTTP is insufficient.\\n \" }, { \"sha\": \"907b333c8e99734fe20f3b434cf0ab162e2876ba\", \"filename\": \"docs/papers/README.md\", \"status\": \"modified\", \"additions\": 63, \"deletions\": 0, \"changes\": 63, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fpapers%2FREADME.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fpapers%2FREADME.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fpapers%2FREADME.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -46,3 +46,66 @@ but not vendored here so this repository remains one deployable control plane.\\n > Citations are provided for scholarly attribution. Redistribution here relies\\n > on the arXiv non-exclusive distribution license each author granted; no\\n > GPL/AGPL-licensed material is vendored anywhere in this repository.\\n+\\n+## Adaptive reasoning and orchestration\\n+\\n+These sources govern model and reasoning-policy decisions. They are cited and\\n+linked rather than vendored because this repository does not assume that every\\n+paper permits redistribution of its PDF.\\n+\\n+- **Sakana Fugu Technical Report** — Yujin Tang et al. arXiv:2606.21228,\\n+ 2026. https://arxiv.org/abs/2606.21228\\n+ Grounds query-adaptive scaffolds over specialized agent teams. It supports\\n+ preserving modality evidence for each assigned specialist; it does not\\n+ justify selecting a model from its name.\\n+- **TRINITY: An Evolved LLM Coordinator** — Jinglue Xu, Qi Sun, Peter\\n+ Schwendeman, Stefan Nielsen, Edoardo Cetin, Yujin Tang. arXiv:2512.04695,\\n+ 2025. https://arxiv.org/abs/2512.04695\\n+ Grounds explicit Thinker, Worker, and Verifier role assignment over a\\n+ heterogeneous pool.\\n+- **Learning to Orchestrate Agents in Natural Language with the Conductor** —\\n+ Stefan Nielsen, Edoardo Cetin, Peter Schwendeman, Qi Sun, Jinglue Xu, Yujin\\n+ Tang. arXiv:2512.04388, 2025. https://arxiv.org/abs/2512.04388\\n+ Grounds targeted communication topology and natural-language subtasks. The\\n+ access list controls prior agent outputs, not removal of the source image.\\n+\\n+- **Route to Reason: Adaptive Routing for LLM and Reasoning Strategy Selection**\\n+ — Zhihong Pan, Kai Zhang, Yuze Zhao, Yupeng Han. arXiv:2505.19435, 2025.\\n+ https://arxiv.org/abs/2505.19435\\n+ Grounds joint routing of models and reasoning strategies under a budget.\\n+- **Route-and-Reason: Scaling Large Language Model Reasoning with Reinforced\\n+ Model Router** — Chenyang Shao, Xinyang Liu, Yutang Lin, Fengli Xu, Yong Li.\\n+ arXiv:2506.05901, 2025. https://arxiv.org/abs/2506.05901\\n+ Grounds decomposition and allocation across heterogeneous workers.\\n+- **Reasoning on a Budget: A Survey of Adaptive and Controllable Test-Time\\n+ Compute in LLMs** — Mohammad Ali Alomrani et al. arXiv:2507.02076, 2025.\\n+ https://arxiv.org/abs/2507.02076\\n+ Grounds the distinction between fixed effort control and adaptive effort\\n+ allocation.\\n+- **Ares: Adaptive Reasoning Effort Selection for Efficient LLM Agents** —\\n+ Jingbo Yang, Bairu Hou, Wei Wei, Yujia Bao, Shiyu Chang. arXiv:2603.07915,\\n+ 2026. https://arxiv.org/abs/2603.07915\\n+ Grounds per-step selection of the minimum sufficient effort with repeated\\n+ verification rather than a fixed effort for every step.\\n+- **Improving Factuality and Reasoning in Language Models through Multiagent\\n+ Debate** — Yilun Du, Shuang Li, Antonio Torralba, Joshua B. Tenenbaum, Igor\\n+ Mordatch. arXiv:2305.14325, 2023. https://arxiv.org/abs/2305.14325\\n+ Grounds independent proposals, multi-round debate, and final synthesis as\\n+ an optional escalation path. It does not justify treating majority vote as\\n+ proof or using debate for every request.\\n+- **Adaptive Test-Time Compute Allocation for Reasoning LLMs via Constrained\\n+ Policy Optimization** — Zhiyuan Zhai, Bingcong Li, Bingnan Xiao, Ming Li,\\n+ Xin Wang. arXiv:2604.14853, 2026. https://arxiv.org/abs/2604.14853\\n+ Grounds budget-constrained, per-input compute allocation instead of a fixed\\n+ reasoning-effort-to-worker-count mapping.\\n+\\n+## Transport references (not policy sources)\\n+\\n+The provider API documentation is used only to verify request-shape and\\n+capability compatibility. It does not select models, assign reasoning effort,\\n+or establish quality claims; those decisions remain grounded in the papers\\n+above and runtime measurement.\\n+\\n+- **OpenAI Responses API reference** — reasoning effort, output limits, and\\n+ structured output format compatibility:\\n+ https://platform.openai.com/docs/api-reference/responses\" }, { \"sha\": \"5ac7ad2eab7df442e143ef45542479857ba68543\", \"filename\": \"docs/planning/adrs/0002-explicit-local-mlx-evaluation.md\", \"status\": \"modified\", \"additions\": 7, \"deletions\": 7, \"changes\": 14, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0002-explicit-local-mlx-evaluation.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0002-explicit-local-mlx-evaluation.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0002-explicit-local-mlx-evaluation.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,7 +1,7 @@\\n ---\\n id: \\\"0002\\\"\\n title: \\\"Explicit local mlx transport and evaluation adapter\\\"\\n-status: accepted\\n+status: superseded\\n proposed_date: \\\"2026-08-10\\\"\\n accepted_date: \\\"2026-08-11\\\"\\n deciders:\\n@@ -22,13 +22,13 @@ affected_components:\\n - \\\"contextual_orchestrator/cost_router.py\\\"\\n - \\\"examples/agents.mlx.json\\\"\\n - \\\"examples/agents.local.json\\\"\\n- - \\\"tests/test_local_mlx.py\\\"\\n+ - \\\"tests/test_local_gateway.py\\\"\\n - \\\"tests/test_batch_routing.py\\\"\\n - \\\"tests/test_cost_router.py\\\"\\n - \\\"tests/test_openai_passthrough.py\\\"\\n effort: M\\n supersedes: null\\n-superseded-by: null\\n+superseded-by: \\\"0012-gateway-only-provider-contract\\\"\\n related:\\n - path: \\\"docs/planning/adrs/0001-fail-closed-model-judgment.md\\\"\\n relation: informational\\n@@ -48,7 +48,7 @@ success_criteria:\\n - metric: \\\"local provider safety\\\"\\n target: \\\"loopback-only mlx/local URL, no Authorization header, remote HTTP rejected\\\"\\n measurement_window: \\\"every local transport test run\\\"\\n- source: \\\"tests/test_local_mlx.py\\\"\\n+ source: \\\"tests/test_local_gateway.py\\\"\\n - metric: \\\"judge integration\\\"\\n target: \\\"fast-mlsirm judge reaches an injected contextual-orchestrator only\\\"\\n measurement_window: \\\"every LLM-as-a-Judge run\\\"\\n@@ -173,14 +173,14 @@ a Codex profile; its credential is never sent to the loopback mlx-lm endpoint.\\n * `contextual_orchestrator/server.py`: authenticate `/v1/models` and `/v1/responses`, proxy Responses requests, and frame streamed responses with `response.completed` and `data: [DONE]`.\\n * `examples/agents.mlx.json`: keep the minimal selected MLX worker example visible in data, not code.\\n * `examples/agents.local.json`: keep the explicit candidate registry: public contextual-orchestrator and every discovered MLX, llama.cpp, and LM Studio candidate. Do not pre-disable entries as a discovery side effect.\\n-* `tests/test_local_mlx.py`: verify direct MLX template arguments, authenticated local gateway credential separation, and fail-closed missing credentials.\\n+* `tests/test_local_gateway.py`: verify direct MLX template arguments, authenticated local gateway credential separation, and fail-closed missing credentials.\\n * `tests/test_model_judge.py`: verify structured fast-mlsirm completion requests remain on the contextual gateway adapter.\\n * `tests/test_openai_passthrough.py`: verify the Responses SSE completion contract and model discovery endpoint.\\n * Local machine configuration: keep the ChatGPT login in Codex's normal auth cache, select the built-in `openai` provider through a profile when needed, and keep the local gateway bearer token in the OS credential store.\\n \\n ## Verification\\n \\n-* `PYTHONPATH=. .venv/bin/python -m pytest -q tests/test_local_mlx.py tests/test_openai_passthrough.py` passes in the repository test environment.\\n+* `PYTHONPATH=. .venv/bin/python -m pytest -q tests/test_local_gateway.py tests/test_openai_passthrough.py` passes in the repository test environment.\\n * `GET /healthz` and authenticated `GET /v1/models` succeed on the loopback control plane; model discovery includes the public orchestrator and the complete configured candidate registry.\\n * Authenticated streamed `POST /v1/responses` contains `response.completed` and `data: [DONE]` and reaches the configured mlx-lm model.\\n * A Codex local-provider smoke returns the requested exact sentinel response through contextual-orchestrator.\\n@@ -342,7 +342,7 @@ Remove the explicit local adapter and use the mock path if the local server is u\\n * contextual_orchestrator/__main__.py\\n * examples/agents.mlx.json\\n * examples/agents.local.json\\n-* tests/test_local_mlx.py\\n+* tests/test_local_gateway.py\\n * tests/test_openai_passthrough.py\\n * fast-mlsirm/python/fast_mlsirm/llm_judge.py\\n * fast-mlsirm/tests/test_llm_judge.py\" }, { \"sha\": \"b4d06485033ae5bac25c866ab52734e5bcf14c88\", \"filename\": \"docs/planning/adrs/0004-pr-review-merge-loop.md\", \"status\": \"modified\", \"additions\": 6, \"deletions\": 0, \"changes\": 6, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0004-pr-review-merge-loop.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0004-pr-review-merge-loop.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0004-pr-review-merge-loop.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -340,6 +340,12 @@ For each repository, record branch, commit, PR URL, review result, check result,\\n | Contextual PR #109 exact head `60d9cfc9be2ce0426ed37746eb9a2768b8f3455d` produced Strix run `31831835133`/job `94869100782` with a successful zero-finding report and artifact `9231362799`, but no `evidence-binding.json`; `run.json` contained only a local temporary target and no repository, PR head, job, report path, or digest binding. The report digest was `33a47fa5855600b393d92c3a1a77e0ac15adb99a55406f99e5b2efba93c17c18`, and the same-head publish step was skipped. | Keep the success as provider/content evidence only, not a clean exact-head security or Merge result. Require the central trusted workflow to publish a structured binding for repository, full PR head, run/job, report path, and digest; reject unbound success and re-run the exact head after protected-main workflow integration. | Reproduced 2026-08-15; central provenance dependency and protected Merge remain open |\\n | The contextual exact-head code-scanning checks `Trivy` and `Scorecard` both became `neutral` because the PR's `security.yml` configuration was absent from protected `main` (`trivy-filesystem` and `supply-chain/branch-protection`), while the separate required `trivy-fs`/`scorecard` jobs were still pending or successful. | Keep the organization's CodeQL-only code-scanning rule unchanged; distinguish code-scanning alert comparison from required Security job results, record the missing configuration as a warning, and never treat neutral, skipped, or absent results as a pass. Re-audit after trusted workflow/ruleset integration. | Observed 2026-08-15; no local source bypass, governance/central workflow follow-up required |\\n | Fast PR #816 exact head `355f93b27ba4a0cb141e86b0fbc9127681edb750` produced Strix run `31833030282`/job `94872991843`; NVIDIA NIM failed with `agents.exceptions.ModelBehaviorError: Tool execute not found in agent strix`, no penetration-test report was produced, and the required check failed closed after 599 seconds. | Keep this as provider/model-tool-contract evidence, not a source vulnerability or clean security result. Preserve the failure denominator, do not publish a status-only success or retry to hide it, and require central trusted workflow/provider repair plus a structured same-head artifact before normal Merge. | Observed 2026-08-15; central Strix dependency and protected Merge remain open |\\n+| Post-merge review of PR #813 found that every budget-gated provider call reached `budget_status()` through the buyer-facing `spend_analytics()` aggregate, deep-copying archived spend and rescanning up to 128 retained runs even though ADR 0014 already requires a synchronized incremental process meter. | Make `budget_status()` read only the locked incremental meter and retain full run/model aggregation in `spend_analytics()`. Preserve current token/cost rounding and fail-closed limit semantics, and regression-test that the enforcement path never calls the aggregate. Keep per-call durable meter checkpoints because ADR 0014 explicitly retains failed-workflow spend across restarts. | Issue #814 implemented locally; 1,727 tests and 100% changed-line branch coverage passed, exact-head review/check evidence remains required |\\n+| PR #765 review found `host.docker.internal` classified as a `local://` provider even though the egress validator correctly rejects its usual non-loopback Docker gateway address. | Keep authenticated local transport loopback-only under ADR 0012, remove the unreachable host classification, and reject non-loopback `local://` configuration at `ModelAgent` construction while retaining the DNS-resolution check against rebinding. | Decision recorded 2026-08-21; implementation and exact-head review/check evidence follow |\\n+| PR #765 review found that auxiliary temperature-capability inspection could let `http.client.IncompleteRead` replace the provider's original HTTP error while reading its diagnostic body. | Treat an incomplete diagnostic body as insufficient capability evidence, preserve the original HTTP error for the caller, and cover the exact truncated-body branch without broad retry or fallback changes. | Decision recorded 2026-08-21; implementation and exact-head review/check evidence follow |\\n+| PR #765 review found that the HTTP tool loop passed the orchestrator-owned literal `reasoning_effort=auto` to a selected provider. | Strip accepted reasoning-effort values at the shared provider-payload boundary while no capability plane exists; provider-native levels may be forwarded later only after the selected agent advertises support under ADR 0013. Cover structured multi-agent and explicit single-agent tool-loop calls with the same regression. | Implemented locally 2026-08-21; focused tests pass and exact-head review/check evidence follows |\\n+| PR #765 review found that one-shot local failover bypassed the request-scoped output-token cap used by the ordinary transport path. | Reuse the existing request-setting lookup for local Responses translation and add a non-mutating `setdefault` for local Chat Completions so an explicit caller cap remains authoritative. | Implemented locally 2026-08-21; focused default/explicit cap tests pass and exact-head review/check evidence follows |\\n+| PR #765 regenerated a host-specific hash lock that omitted conditional Windows and native SQLAlchemy dependencies. | Generate the existing `requirements.lock` with uv universal resolution, retain PEP 508 markers and hashes, and fail repository metadata tests if universal mode or `colorama`, `greenlet`, or `tzdata` disappears; ADR 0025 records the format decision. | Implemented locally 2026-08-21; universal solve and hash-locked dry-run pass, exact-head review/check evidence follows |\\n \\n ## Risks and Mitigations\\n \" }, { \"sha\": \"bd40526e7c1fb24376c4983b10844fcf32cbf530\", \"filename\": \"docs/planning/adrs/0007-sast-transport-and-sql-hardening.md\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 4, \"changes\": 8, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0007-sast-transport-and-sql-hardening.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0007-sast-transport-and-sql-hardening.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0007-sast-transport-and-sql-hardening.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,7 +1,7 @@\\n ---\\n id: \\\"0007\\\"\\n title: \\\"Harden provider transport and SQL ledger against scanner findings\\\"\\n-status: accepted\\n+status: superseded\\n proposed_date: \\\"2026-08-11\\\"\\n accepted_date: \\\"2026-08-11\\\"\\n deciders:\\n@@ -16,11 +16,11 @@ affected_components:\\n - \\\"contextual_orchestrator/cost_ledger.py\\\"\\n - \\\"contextual_orchestrator/__main__.py\\\"\\n - \\\"tests/test_provider_tls.py\\\"\\n- - \\\"tests/test_local_mlx.py\\\"\\n+ - \\\"tests/test_local_gateway.py\\\"\\n - \\\"tests/test_cost_ledger.py\\\"\\n effort: M\\n supersedes: null\\n-superseded-by: null\\n+superseded-by: \\\"0012-gateway-only-provider-contract\\\"\\n related:\\n - path: \\\"docs/planning/adrs/0002-explicit-local-mlx-evaluation.md\\\"\\n relation: influences\\n@@ -154,5 +154,5 @@ reintroducing general urllib URL handling.\\n * contextual_orchestrator/cost_ledger.py\\n * contextual_orchestrator/__main__.py\\n * tests/test_provider_tls.py\\n-* tests/test_local_mlx.py\\n+* tests/test_local_gateway.py\\n * tests/test_cost_ledger.py\" }, { \"sha\": \"22e82308105e18c47eb54aa881a1bc571863c73c\", \"filename\": \"docs/planning/adrs/0011-structured-provider-features-stay-orchestrated.md\", \"status\": \"added\", \"additions\": 141, \"deletions\": 0, \"changes\": 141, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0011-structured-provider-features-stay-orchestrated.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0011-structured-provider-features-stay-orchestrated.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0011-structured-provider-features-stay-orchestrated.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,141 @@\\n+---\\n+id: \\\"0011\\\"\\n+title: \\\"Keep structured provider features inside multi-agent orchestration\\\"\\n+status: accepted\\n+proposed_date: \\\"2026-08-21\\\"\\n+accepted_date: \\\"2026-08-21\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"LineageWeave integration\\\"\\n+ - \\\"contextual-orchestrator runtime\\\"\\n+informed:\\n+ - \\\"API consumers\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/orchestrator.py\\\"\\n+ - \\\"contextual_orchestrator/server.py\\\"\\n+ - \\\"tests/test_openai_passthrough.py\\\"\\n+ - \\\"tests/test_model_judge.py\\\"\\n+effort: M\\n+supersedes: null\\n+superseded-by: null\\n+related:\\n+ - path: \\\"docs/planning/adrs/0001-fail-closed-model-judgment.md\\\"\\n+ relation: constrains\\n+ - path: \\\"docs/planning/adrs/0002-explicit-local-mlx-evaluation.md\\\"\\n+ relation: extends\\n+ - path: \\\"docs/architecture.md\\\"\\n+ relation: implements\\n+ - path: \\\"docs/planning/adrs/0014-gateway-owned-model-selection.md\\\"\\n+ relation: constrained-by\\n+success_criteria:\\n+ - metric: \\\"structured requests using the multi-agent workflow\\\"\\n+ target: \\\"100% of client-facing non-null response_format and Responses requests\\\"\\n+ measurement_window: \\\"every structured-output regression run\\\"\\n+ source: \\\"tests/test_openai_passthrough.py\\\"\\n+ - metric: \\\"Responses json_schema translation\\\"\\n+ target: \\\"Responses text.format json_schema reaches the final provider as the equivalent Chat response_format without losing the schema\\\"\\n+ measurement_window: \\\"every Responses structured-output regression run\\\"\\n+ source: \\\"tests/test_openai_passthrough.py\\\"\\n+ - metric: \\\"multimodal context preservation\\\"\\n+ target: \\\"image_url input remains available to the final synthesis request\\\"\\n+ measurement_window: \\\"every multimodal structured-output regression run\\\"\\n+ source: \\\"tests/test_openai_passthrough.py\\\"\\n+---\\n+\\n+# Keep structured provider features inside multi-agent orchestration\\n+\\n+## Context\\n+\\n+The OpenAI-compatible boundary previously treated `response_format` and the\\n+Responses API as a provider passthrough. That made a request look\\n+successful while skipping the Thinker/Worker/Verifier/Synthesizer workflow.\\n+Structured output and multimodal requests are still product work, not an\\n+exception to the orchestration contract. A consumer must receive the same\\n+workflow evidence, verification boundary, session lineage, and cost accounting\\n+as a plain chat request.\\n+\\n+## Decision\\n+\\n+1. A non-null structured-output contract or Responses request is an\\n+ orchestration trigger, never a silent single-agent downgrade.\\n+2. The request enters the existing conducted workflow. Intermediate steps use\\n+ the original messages, including multimodal content, and the final\\n+ synthesizer performs the provider-facing structured completion.\\n+3. The final provider payload preserves validated tools and structured-output\\n+ fields. A Responses request remains a Responses request at the final\\n+ provider boundary when the selected provider supports it; a local provider\\n+ may perform an explicit transport-level translation when its capability\\n+ boundary requires Chat Completions. Responses `text.format` therefore stays\\n+ native for Responses-capable providers rather than being silently downgraded.\\n+4. `json_object` and `json_schema` are both first-class structured workflows.\\n+ Schema validation remains fail-closed at the HTTP boundary; a provider\\n+ success is not treated as semantic schema validity.\\n+5. The workflow response exposes bounded orchestration metadata by default.\\n+ Prompts, answers, images, tool arguments, secrets, and unbounded raw traces\\n+ are not put into telemetry or the default response.\\n+6. Tool execution loops are not fabricated by this decision. Per ADR 0014,\\n+ clients must opt into the explicit client-owned `v1` tool-loop contract;\\n+ ordinary tool declarations fail closed, while the opted-in provider-shape\\n+ call remains a single-worker exception.\\n+7. The internal fail-closed LLM-as-a-Judge call remains one bounded,\\n+ schema-constrained provider request. It uses the orchestrator's existing\\n+ provider transport but never recursively starts another conducted workflow.\\n+\\n+## Research basis\\n+\\n+This decision applies the existing research-grounded architecture rather than\\n+inventing a provider-specific exception:\\n+\\n+- Fugu distinguishes a low-latency routed call from a quality-oriented deep\\n+ workflow and keeps the worker pool configurable.\\n+- TRINITY supplies the Thinker, Worker, and Verifier role boundary used before\\n+ synthesis.\\n+- Conductor supplies explicit workflow steps and access-controlled context.\\n+\\n+The canonical references are maintained in `docs/architecture.md` and\\n+`docs/papers/README.md`. OpenAI's Chat Completions and Responses API contracts\\n+define the wire-shape translation, not the orchestration policy.\\n+\\n+## Consequences\\n+\\n+* Good: structured and multimodal requests no longer bypass verification and\\n+ orchestration evidence.\\n+* Good: JSON object and JSON schema requests share one tested policy instead of\\n+ diverging into transport-specific single-agent paths.\\n+* Good: Responses-only providers receive their native endpoint and input shape\\n+ after the multi-agent workflow.\\n+* Good: the final provider retains the capability fields it must interpret.\\n+* Good: internal verification does not recursively multiply provider calls or\\n+ replace the judge verdict with an unrelated synthesized answer.\\n+* Bad: structured requests consume more provider calls and can take longer than\\n+ a plain routed request.\\n+* Bad: tool execution remains a distinct explicit client-owned contract rather\\n+ than being implied by merely forwarding a tool declaration.\\n+\\n+## Confirmation\\n+\\n+Run the focused structured-output and orchestration tests. Confirm that the\\n+Responses JSON-schema test preserves the schema, the structured-output test\\n+reports the conducted workflow, the multimodal test retains `image_url` through final\\n+synthesis, the explicit tool-loop test preserves its single-worker response,\\n+and the internal structured judge test performs exactly one provider call. Do\\n+not claim provider-side semantic schema validity from HTTP 200.\\n+\\n+## References\\n+\\n+Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025).\\n+*Learning to orchestrate agents in natural language with the Conductor*.\\n+https://doi.org/10.48550/arXiv.2512.04388\\n+\\n+Sakana AI. (2026, June 22). *Sakana Fugu: One model to command them all*.\\n+https://sakana.ai/fugu-release/\\n+\\n+Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025).\\n+*Trinity: An evolved LLM coordinator*. https://doi.org/10.48550/arXiv.2512.04695\\n+\\n+OpenAI. (n.d.-a). *Create chat completion*. OpenAI Platform.\\n+https://platform.openai.com/docs/api-reference/chat/create\\n+\\n+OpenAI. (n.d.-b). *Create a model response*. OpenAI Platform.\\n+https://platform.openai.com/docs/api-reference/responses/create\" }, { \"sha\": \"3adc1b70801414d66160504273790d5e2a3d7275\", \"filename\": \"docs/planning/adrs/0012-gateway-only-provider-contract.md\", \"status\": \"added\", \"additions\": 61, \"deletions\": 0, \"changes\": 61, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0012-gateway-only-provider-contract.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0012-gateway-only-provider-contract.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0012-gateway-only-provider-contract.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,61 @@\\n+---\\n+id: \\\"0012\\\"\\n+title: \\\"Gateway-only provider contract; no direct MLX transport\\\"\\n+status: accepted\\n+proposed_date: \\\"2026-08-20\\\"\\n+accepted_date: \\\"2026-08-20\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"contextual-orchestrator provider capability contract\\\"\\n+ - \\\"paper-grounded model routing policy\\\"\\n+informed:\\n+ - \\\"LineageWeave\\\"\\n+ - \\\"fast-mlsirm\\\"\\n+ - \\\"contributors\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/orchestrator.py\\\"\\n+ - \\\"contextual_orchestrator/model_discovery.py\\\"\\n+ - \\\"contextual_orchestrator/__main__.py\\\"\\n+ - \\\"examples/agents.local.json\\\"\\n+ - \\\"docs/kv-credentials.md\\\"\\n+supersedes: \\\"0002-explicit-local-mlx-evaluation\\\"\\n+superseded-by: null\\n+effort: M\\n+---\\n+\\n+# Gateway-only provider contract; no direct MLX transport\\n+\\n+## Context\\n+\\n+The orchestrator is the model routing and orchestration boundary. A direct\\n+runtime-specific `mlx://` worker contract leaks one local inference runtime\\n+into the public agent schema, CLI, credential rules, and Responses-to-Chat\\n+adaptation. It also creates provider-specific controls that cannot be applied\\n+to other models or gateways.\\n+\\n+## Decision\\n+\\n+- The public worker contract is provider-neutral: `mock://` for tests,\\n+ `https://` for remote providers, and authenticated `local://` only for a\\n+ reviewed loopback gateway.\\n+- Direct `mlx://` agents are rejected at `ModelAgent` construction. No MLX\\n+ runtime, model-template setting, or keyless direct transport is part of the\\n+ orchestrator contract.\\n+- A local gateway owns downstream model selection and runtime-specific\\n+ settings. The orchestrator sends only the negotiated provider-neutral\\n+ request shape and the explicitly named local gateway credential.\\n+- Model selection and reasoning policy remain capability- and paper-driven;\\n+ they must not infer a provider from a model name or hard-code MLX behavior.\\n+\\n+## Consequences\\n+\\n+- LineageWeave and other callers can use one gateway boundary without a direct\\n+ local-model dependency or monkey patch.\\n+- Existing local gateway concurrency and Responses/Chat compatibility remain\\n+ available because they are transport capabilities, not MLX behavior.\\n+- Historical MLX benchmark artifacts remain for provenance but are not current\\n+ configuration guidance or a supported public transport.\\n+- Operators who previously configured `mlx://` must place the runtime behind\\n+ an authenticated OpenAI-compatible gateway and configure `local://` or\\n+ `https://` accordingly.\" }, { \"sha\": \"dc6402b27ae8518361bf189f2d27e975eec5b803\", \"filename\": \"docs/planning/adrs/0013-paper-grounded-adaptive-reasoning-policy.md\", \"status\": \"added\", \"additions\": 108, \"deletions\": 0, \"changes\": 108, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0013-paper-grounded-adaptive-reasoning-policy.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0013-paper-grounded-adaptive-reasoning-policy.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0013-paper-grounded-adaptive-reasoning-policy.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,108 @@\\n+---\\n+id: \\\"0013\\\"\\n+title: \\\"Paper-grounded adaptive reasoning and model capability policy\\\"\\n+status: accepted\\n+proposed_date: \\\"2026-08-20\\\"\\n+accepted_date: \\\"2026-08-20\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"Route to Reason: Adaptive Routing for LLM and Reasoning Strategy Selection\\\"\\n+ - \\\"Route-and-Reason: Scaling Large Language Model Reasoning with Reinforced Model Router\\\"\\n+ - \\\"Reasoning on a Budget: A Survey of Adaptive and Controllable Test-Time Compute in LLMs\\\"\\n+ - \\\"Ares: Adaptive Reasoning Effort Selection for Efficient LLM Agents\\\"\\n+ - \\\"Improving Factuality and Reasoning in Language Models through Multiagent Debate\\\"\\n+ - \\\"Adaptive Test-Time Compute Allocation for Reasoning LLMs via Constrained Policy Optimization\\\"\\n+informed:\\n+ - \\\"LineageWeave\\\"\\n+ - \\\"fast-mlsirm\\\"\\n+ - \\\"contributors\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/orchestrator.py\\\"\\n+ - \\\"contextual_orchestrator/server.py\\\"\\n+ - \\\"contextual_orchestrator/model_discovery.py\\\"\\n+ - \\\"tests/test_openai_passthrough.py\\\"\\n+ - \\\"tests/test_request_metadata.py\\\"\\n+ - \\\"docs/architecture.md\\\"\\n+supersedes: null\\n+superseded-by: null\\n+related:\\n+ - path: \\\"docs/planning/adrs/0012-gateway-only-provider-contract.md\\\"\\n+ relation: extends\\n+effort: M\\n+---\\n+\\n+# Paper-grounded adaptive reasoning and model capability policy\\n+\\n+## Context\\n+\\n+The public API may receive a requested reasoning level such as `auto`,\\n+`medium`, `high`, or `xhigh`, while the selected provider may support a\\n+different subset of values or no reasoning control at all. A fixed value is\\n+also a poor policy for multi-step work: easy steps can be over-computed and\\n+hard steps can be under-computed. The repository must therefore distinguish\\n+the caller's policy from the provider wire value and must not make model\\n+decisions from model-name folklore.\\n+\\n+## Decision\\n+\\n+- Model selection, reasoning-effort allocation, orchestration topology, and\\n+ quality/cost claims are decided from cited academic papers plus current\\n+ runtime capability and measurement evidence. Vendor documentation defines\\n+ wire compatibility; it does not define this repository's model policy.\\n+- `auto` is an orchestrator-only policy. The orchestrator evaluates task/step\\n+ difficulty, capability advertisements, budget, latency constraints, and\\n+ required verification, then chooses a provider-supported effort or a\\n+ multi-agent workflow. The literal value `auto` is never sent upstream.\\n+- Explicit effort values are forwarded only when the selected provider\\n+ advertises them. `none` is not a universal synonym for a non-reasoning\\n+ model; if the provider does not advertise a requested value, the\\n+ orchestrator negotiates another supported path or fails clearly.\\n+- `high` and `xhigh` are outcome policies, not promises that one worker has a\\n+ particular hidden-thinking implementation. When appropriate, the\\n+ orchestrator may use heterogeneous workers, independent attempts,\\n+ verification, and synthesis. Traces must record the effective strategy and\\n+ must not label a non-reasoning worker as a reasoning model.\\n+- Multi-agent debate is an available escalation strategy, not a mandatory\\n+ replacement for one worker. The orchestrator may select independent\\n+ proposals, debate, verification, and synthesis when task difficulty and the\\n+ budget justify it; otherwise it may use one capable worker with the same\\n+ evidence and trace contract. A debate result is not accepted by majority\\n+ vote alone: the final synthesis must retain source attribution and pass the\\n+ requested output contract.\\n+- Adaptive compute allocation is evaluated as a constrained policy. The\\n+ orchestrator must spend additional attempts or verification where the\\n+ expected quality gain justifies the cost, rather than mapping `low`,\\n+ `medium`, `high`, or `xhigh` to fixed worker counts or a vendor model name.\\n+- No direct MLX transport or MLX-specific model policy is permitted. Local\\n+ runtimes remain behind the authenticated provider-neutral gateway boundary\\n+ in ADR 0012.\\n+\\n+## Evidence contract\\n+\\n+Every change to routing or reasoning policy must cite the relevant sources in\\n+`docs/papers/README.md`, add or update a regression test for the capability\\n+boundary, and report requested versus effective effort in the trace or\\n+metadata. A provider health result alone is not evidence of reasoning quality.\\n+\\n+Transport compatibility is checked against the current provider API contract,\\n+not treated as model-policy evidence. In particular, Responses API capability\\n+checks may cover supported reasoning-effort values and `json_schema` structured\\n+outputs; Chat Completions compatibility must negotiate its system-message and\\n+structured-output equivalent separately. These checks must never turn a vendor\\n+default into this repository's reasoning policy.\\n+\\n+## Consequences\\n+\\n+- Callers can request `auto` without coupling themselves to provider-specific\\n+ effort names.\\n+- Unsupported effort values cannot leak to providers or silently become a\\n+ different model behavior.\\n+- Adaptive multi-agent execution is measurable as orchestration, rather than\\n+ being misrepresented as a provider's native reasoning capability.\\n+- Structured-output requests, including `json_schema`, stay inside the same\\n+ multi-agent workflow. Unsupported worker-format capabilities are negotiated\\n+ or rejected; they must not silently downgrade the request to a single-agent\\n+ passthrough.\\n+- Historical MLX benchmark and transport ADRs remain available as provenance,\\n+ but they are not supported configuration guidance.\" }, { \"sha\": \"d399c8b1de1b975975e48274232bc8fe83b941b5\", \"filename\": \"docs/planning/adrs/0014-gateway-owned-model-selection.md\", \"status\": \"added\", \"additions\": 104, \"deletions\": 0, \"changes\": 104, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0014-gateway-owned-model-selection.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0014-gateway-owned-model-selection.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0014-gateway-owned-model-selection.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,104 @@\\n+---\\n+id: \\\"0014\\\"\\n+title: \\\"Gateway-owned model selection and multi-agent structured output\\\"\\n+status: accepted\\n+proposed_date: \\\"2026-08-20\\\"\\n+accepted_date: \\\"2026-08-20\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"gateway-only provider contract\\\"\\n+ - \\\"paper-grounded adaptive reasoning policy\\\"\\n+informed:\\n+ - \\\"LineageWeave\\\"\\n+ - \\\"fast-mlsirm\\\"\\n+ - \\\"contributors\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/__main__.py\\\"\\n+ - \\\"contextual_orchestrator/cost_router.py\\\"\\n+ - \\\"contextual_orchestrator/orchestrator.py\\\"\\n+ - \\\"contextual_orchestrator/server.py\\\"\\n+supersedes: null\\n+superseded-by: null\\n+related:\\n+ - path: \\\"docs/planning/adrs/0012-gateway-only-provider-contract.md\\\"\\n+ relation: depends-on\\n+ - path: \\\"docs/planning/adrs/0013-paper-grounded-adaptive-reasoning-policy.md\\\"\\n+ relation: implements\\n+effort: M\\n+---\\n+\\n+# ADR 0014: Gateway-owned model selection and multi-agent structured output\\n+\\n+- Status: Accepted\\n+- Date: 2026-08-20\\n+\\n+## Context\\n+\\n+Consumers using contextual-orchestrator must not select a provider model by\\n+copying `LLM_GATEWAY_MODEL` into every application. The configured gateway\\n+exposes its model registry, and the orchestrator owns routing, reasoning effort,\\n+and cost attribution. A request carrying JSON output constraints or the\\n+Responses API must not silently downgrade to a single provider call merely\\n+because the provider response shape is richer.\\n+\\n+## Decision\\n+\\n+- An omitted chat or Responses `model` is represented by the virtual\\n+ `contextual-orchestrator` model and is resolved inside the orchestrator.\\n+- `reasoning_effort=auto` (or Responses `reasoning.effort=auto`) is an\\n+ orchestrator-only policy value and is never forwarded as a provider field.\\n+ Provider-native levels are forwarded only after the selected agent declares\\n+ that capability; support is never inferred from a model name. If no selected\\n+ provider declares the requested level, the gateway rejects the request rather\\n+ than silently falling back to a different effort.\\n+- `--auto-discover-model-agents` expands an empty seed agent from its configured\\n+ HTTPS `/models` endpoint. Embedding-only registry rows are excluded from the\\n+ chat pool. Consumers provide only the gateway URL and credential.\\n+- `json_object`, `json_schema`, and Responses text JSON formats force the\\n+ conduct workflow. The final synthesis receives the original provider-native\\n+ output contract, and the gateway independently validates the resulting JSON\\n+ locally before returning it. A Chat request therefore keeps\\n+ `response_format`, while a Responses request keeps `text.format`, at the\\n+ final provider boundary.\\n+- Tool-loop requests are explicitly passed to one selected worker agent. The\\n+ gateway preserves the provider's full tool-call response and the client owns\\n+ execution of the returned function calls; they do not claim a multi-agent\\n+ synthesis trace. Streaming tool loops are rejected until the gateway has a\\n+ provider-shape-preserving streaming relay. Clients must opt in with the\\n+ `X-Contextual-Orchestrator-Tool-Loop: v1` header; ordinary tool requests stay\\n+ fail-closed until that contract is explicitly selected.\\n+- Each provider-reported call in a conducted workflow writes its own cost-ledger\\n+ record under the shared workflow run id, including the model judge and final\\n+ provider synthesis. The response retains one last-metered-call\\n+ `usage_record_id` for compatibility and adds the complete `usage_record_ids`\\n+ list. Calls without\\n+ valid provider usage increment `unmetered_provider_call_count`; if no workflow\\n+ call reports usage, the existing request-level estimate remains the explicit\\n+ compatibility fallback.\\n+- Raw in-memory workflow records share the existing bounded recent-run capacity.\\n+ Evicted records contribute to a compact per-model spend accumulator, so budget\\n+ enforcement and spend totals remain cumulative without retaining every prompt,\\n+ answer, or trace in process memory. A configured durable state store remains\\n+ the long-term run-evidence boundary.\\n+- Every completed provider call adds reported output usage (or the bounded\\n+ estimate when unavailable) to a synchronized process budget ledger before the\\n+ next planner, worker, verifier, judge, or synthesizer call. Failed workflows\\n+ therefore retain consumed spend even when no completed run can be persisted;\\n+ the configured state store checkpoints this compact meter across restarts.\\n+\\n+## Consequences\\n+\\n+- Provider model selection remains centralized and can change with the registry\\n+ without an application rebuild.\\n+- Structured output retains the multi-agent trace and cannot bypass synthesis.\\n+- Cost reports price each metered workflow call against the model that served it\\n+ instead of attributing all conducted work to the final synthesizer.\\n+- A response never sums monetary amounts across currencies; mixed-currency\\n+ workflows expose `currency_code=MIXED` and a null aggregate amount while the\\n+ individual ledger records retain their original amounts and currencies.\\n+- High-volume passthrough traffic cannot grow raw workflow memory without bound,\\n+ while evicted usage still contributes to buyer-visible spend and budget gates.\\n+- Tool callers use an explicit single-agent passthrough contract. The gateway\\n+ remains the model-selection boundary, while tool execution stays with the\\n+ authenticated client and never becomes an implicit multi-agent fallback.\" }, { \"sha\": \"67280b86630213acb9d870a4cff3d75c9167ba84\", \"filename\": \"docs/planning/adrs/0015-auto-embedding-model-selection.md\", \"status\": \"added\", \"additions\": 110, \"deletions\": 0, \"changes\": 110, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0015-auto-embedding-model-selection.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0015-auto-embedding-model-selection.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0015-auto-embedding-model-selection.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,110 @@\\n+---\\n+id: \\\"0015\\\"\\n+title: \\\"Orchestrator-owned automatic embedding model selection\\\"\\n+status: proposed\\n+proposed_date: \\\"2026-08-20\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"contextual-orchestrator gateway runtime\\\"\\n+ - \\\"downstream embedding consumers\\\"\\n+informed:\\n+ - \\\"downstream consumers (naruon, LineageWeave)\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/orchestrator.py\\\"\\n+ - \\\"contextual_orchestrator/server.py\\\"\\n+ - \\\"contextual_orchestrator/api_contract.py\\\"\\n+ - \\\"contextual_orchestrator/batch_routing.py\\\"\\n+ - \\\"contextual_orchestrator/cost_router.py\\\"\\n+ - \\\"tests/test_provider_embeddings.py\\\"\\n+ - \\\"tests/test_embeddings_model_pool_http_honesty.py\\\"\\n+effort: S\\n+supersedes: null\\n+superseded-by: null\\n+related:\\n+ - path: \\\"docs/planning/adrs/0001-fail-closed-model-judgment.md\\\"\\n+ relation: constrains\\n+ - path: \\\"docs/planning/adrs/0012-gateway-only-provider-contract.md\\\"\\n+ relation: depends-on\\n+---\\n+\\n+# ADR 0015: Orchestrator-owned automatic embedding model selection\\n+\\n+## Context\\n+\\n+Consumers currently have to send a model name to the embeddings endpoints. A\\n+consumer that already delegates model selection to contextual-orchestrator\\n+must then invent a sentinel model name or maintain provider-specific\\n+configuration. That contradicts the gateway-owned model policy and makes the\\n+OpenAI-compatible contract less useful for downstream services.\\n+\\n+Embedding agents are already represented in the orchestrator candidate pool by\\n+the explicit `embedding` capability tag. The selection must therefore reuse\\n+the existing ranked-agent policy rather than add a provider order, model-name\\n+guess, or consumer-side fallback.\\n+\\n+## Decision\\n+\\n+1. `/v1/embeddings` and `/v1/batch/embeddings` accept an omitted `model`.\\n+2. When omitted, the gateway selects the highest-ranked enabled agent carrying\\n+ the `embedding` capability. Ranking continues to use the existing priority\\n+ and capability policy; disabled agents and provider exclusions are ignored.\\n+3. An explicitly supplied model remains supported only when it matches an\\n+ enabled embedding-capable agent. Unknown, disabled, or non-embedding models\\n+ fail closed with the existing invalid-model contract.\\n+4. If no enabled embedding-capable agent exists for an omitted model, the\\n+ gateway returns `503 embedding_unavailable`; it never invents a model or\\n+ produces a heuristic vector as a provider substitute.\\n+5. The provider and resolved model are carried into internal batch requests,\\n+ provider JSONL, response metadata, and cost attribution so the selected\\n+ deployment remains deterministic and auditable. Client attribution metadata\\n+ cannot override either server-resolved identity. The standalone in-process\\n+ backend remains a local test/development path; a configured provider path\\n+ uses its injected embeddings backend and the resolved model.\\n+6. The default batch backend resolves the current agent pool at submission time.\\n+ Runtime additions, disablement, and priority changes are therefore visible to\\n+ new jobs; every submitted job retains the backend instance that created it for\\n+ deterministic polling and retrieval.\\n+\\n+## Contract and acceptance evidence\\n+\\n+The OpenAPI contract marks `model` optional and documents the unavailable\\n+response. Loopback HTTP tests cover omitted-model selection for sync and batch\\n+requests, explicit pool validation, and the no-capability failure. Provider\\n+backend contract tests must preserve the resolved model in every serialized\\n+embedding request before this ADR moves from proposed to accepted.\\n+\\n+## Consequences\\n+\\n+LineageWeave, naruon, and other consumers can omit provider model selectors\\n+while retaining pool validation, provider routing, and cost attribution.\\n+Explicit OpenAI-compatible model requests remain backward compatible. The\\n+gateway still exposes a clear distinction between local standalone evidence\\n+and configured-provider evidence; local heuristic vectors are not production\\n+provider evidence.\\n+\\n+## Research grounding\\n+\\n+The selection is a capability-constrained routing decision, not a semantic\\n+quality judgment. It reuses the repository's vendored routing literature:\\n+\\n+* Chen, L., Zaharia, M., & Zou, J. (2023). FrugalGPT: How to use large\\n+ language models while reducing cost and improving performance. *arXiv*.\\n+ https://arxiv.org/abs/2305.05176\\n+* Ong, I., Almahairi, A., Wu, V., Chiang, W.-L., Wu, T., Gonzalez, J. E.,\\n+ Kadous, M. W., & Stoica, I. (2024). RouteLLM: Learning to route LLMs with\\n+ preference data. *arXiv*. https://arxiv.org/abs/2406.18665\\n+* Ding, D., Mallick, A., Wang, C., Sim, R., Mukherjee, S., Rühle, V.,\\n+ Lakshmanan, L. V. S., & Awadallah, A. H. (2024). Hybrid LLM:\\n+ Cost-efficient and quality-aware query routing. *International Conference\\n+ on Learning Representations*. https://arxiv.org/abs/2404.14618\\n+\\n+These papers ground cost-aware and capability-aware routing decisions; they do\\n+not provide evidence that one embedding model is universally higher quality.\\n+No such unsupported quality claim is made by this ADR.\\n+\\n+## More information\\n+\\n+* docs/papers/README.md\\n+* docs/rest_api_design.md\\n+* docs/planning/adrs/0001-fail-closed-model-judgment.md\" }, { \"sha\": \"f3572403548fad1cb22fab5bdfa3fd5151c19608\", \"filename\": \"docs/planning/adrs/0016-optional-sampling-capability-negotiation.md\", \"status\": \"added\", \"additions\": 48, \"deletions\": 0, \"changes\": 48, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0016-optional-sampling-capability-negotiation.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0016-optional-sampling-capability-negotiation.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0016-optional-sampling-capability-negotiation.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,48 @@\\n+# ADR 0016: Optional sampling capability negotiation\\n+\\n+- Status: Accepted\\n+- Date: 2026-08-20\\n+\\n+## Context\\n+\\n+Some provider deployments reject the optional `temperature` request field even\\n+when the value is valid for the public API contract. A provider response that\\n+only reports an invalid value must not be silently changed into a different\\n+request. The same transport boundary serves chat completions and raw Responses\\n+passthrough, so the behavior must be endpoint-local and provider-neutral.\\n+\\n+## Decision\\n+\\n+When a provider returns HTTP 400 or 422 and the response evidence explicitly\\n+identifies `temperature` as unsupported, the orchestrator retries once against\\n+the same endpoint with only `temperature` removed. This negotiation is\\n+available to both the normal chat transport and raw/Responses passthrough.\\n+\\n+When neither the caller nor the operator supplies a sampling temperature, the\\n+runtime leaves the field absent so the selected provider applies its published\\n+default. The CLI compatibility flags remain an explicit operator override.\\n+HTTP request sampling and output-token controls are stored in thread-local\\n+request scope; concurrent requests never mutate the shared client defaults or\\n+inherit one another's controls.\\n+\\n+All other 4xx responses, including invalid temperature values, remain\\n+non-retryable. The orchestrator does not infer capability from a model name,\\n+provider ordering, parameter count, or local benchmark, and it does not select\\n+another model as a temperature fallback.\\n+\\n+## Consequences\\n+\\n+- GPT-5-family or otherwise restricted deployments can answer when the only\\n+ incompatibility is an optional sampling field.\\n+- The original endpoint, model, authentication, and all other request fields\\n+ remain unchanged.\\n+- Concurrent Completions, Chat Completions, Responses, streaming, and batch\\n+ transports apply only their own request-scoped controls.\\n+- The provider error body is consumed only for bounded capability\\n+ classification; it is not persisted or exposed as a credential-bearing log.\\n+\\n+## Verification\\n+\\n+`tests/test_provider_integration.py` covers successful chat negotiation,\\n+invalid-value non-negotiation, and raw Responses negotiation over a real local\\n+HTTP server.\" }, { \"sha\": \"500e834e82baa2b5b2dc9f96f2509135153b944c\", \"filename\": \"docs/planning/adrs/0017-inbound-request-framing.md\", \"status\": \"added\", \"additions\": 50, \"deletions\": 0, \"changes\": 50, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0017-inbound-request-framing.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0017-inbound-request-framing.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0017-inbound-request-framing.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,50 @@\\n+---\\n+status: proposed\\n+date: 2026-08-20\\n+decision-makers:\\n+ - contextual-orchestrator maintainers\\n+---\\n+\\n+# ADR 0017: Fail-closed inbound request framing\\n+\\n+## Context\\n+\\n+The HTTP handler previously converted one `Content-Length` value and delegated\\n+all other framing behavior to `BufferedReader.read`. Missing, negative,\\n+duplicate, transfer-coded, truncated, and slow request bodies therefore did not\\n+share one bounded policy. This is an inbound trust-boundary defect, separate\\n+from provider-response framing.\\n+\\n+## Decision\\n+\\n+Accept only one ASCII decimal `Content-Length` within the configured body limit.\\n+Reject missing length, duplicate length lines, `Transfer-Encoding`, malformed\\n+or signed values, and `Transfer-Encoding` plus `Content-Length` before reading\\n+body bytes. Read exactly the declared number of bytes with a bounded socket\\n+deadline. On any framing failure, return a stable generic error and close the\\n+connection so unread bytes cannot be interpreted as another request.\\n+\\n+The server does not implement chunked decoding in this change. A future bounded\\n+decoder requires a separate design and socket-level evidence.\\n+\\n+## Consequences\\n+\\n+- Every current JSON body endpoint inherits one parser/reader policy.\\n+- Clients must send a fixed-length JSON request; the API returns `411`, `413`,\\n+ `408`, or `400` with `invalid_request_framing`/named framing codes as\\n+ appropriate.\\n+- The body deadline and byte limit are visible in the secret-free readiness\\n+ profile.\\n+- Socket-level, truncation, timeout, duplicate, transfer-coding, and boundary\\n+ tests become merge evidence.\\n+\\n+## Standards\\n+\\n+Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP/1.1* (RFC 9112).\\n+RFC Editor. https://www.rfc-editor.org/rfc/rfc9112.html\\n+\\n+## Customer next action\\n+\\n+Send JSON requests with exactly one fixed decimal `Content-Length`; retry a\\n+framing error only after correcting the request, not by replaying the same\\n+ambiguous bytes.\" }, { \"sha\": \"959c0a9911c70036ded333b68423fcecc5869001\", \"filename\": \"docs/planning/adrs/0018-multimodal-evidence-preserving-orchestration.md\", \"status\": \"added\", \"additions\": 104, \"deletions\": 0, \"changes\": 104, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0018-multimodal-evidence-preserving-orchestration.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0018-multimodal-evidence-preserving-orchestration.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0018-multimodal-evidence-preserving-orchestration.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,104 @@\\n+---\\n+id: \\\"0018\\\"\\n+title: \\\"Preserve multimodal evidence through every evidence-bearing workflow step\\\"\\n+status: accepted\\n+proposed_date: \\\"2026-08-20\\\"\\n+accepted_date: \\\"2026-08-20\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+consulted:\\n+ - \\\"Sakana Fugu Technical Report\\\"\\n+ - \\\"TRINITY: An Evolved LLM Coordinator\\\"\\n+ - \\\"Learning to Orchestrate Agents in Natural Language with the Conductor\\\"\\n+ - \\\"OpenAI Chat Completions and Responses API references\\\"\\n+informed:\\n+ - \\\"LineageWeave\\\"\\n+ - \\\"OpenCode\\\"\\n+ - \\\"Noema\\\"\\n+ - \\\"Strix\\\"\\n+affected_components:\\n+ - \\\"contextual_orchestrator/orchestrator.py\\\"\\n+ - \\\"contextual_orchestrator/server.py\\\"\\n+ - \\\"tests/test_multimodal_workflow_evidence.py\\\"\\n+related:\\n+ - path: \\\"docs/planning/adrs/0013-paper-grounded-adaptive-reasoning-policy.md\\\"\\n+ relation: extends\\n+effort: S\\n+---\\n+\\n+# Preserve multimodal evidence through every evidence-bearing workflow step\\n+\\n+## Context\\n+\\n+The OpenAI-compatible boundary accepts Chat Completions `image_url` parts and\\n+Responses `input_image` parts, but the conducted workflow reduced the original\\n+request to text before thinker, worker, verifier, and synthesizer execution.\\n+The models therefore received the literal marker `[image]`, not the pixels.\\n+An authorized, non-identifying LineageWeave runtime check exposed the product\\n+impact: a completed five-image VISION run persisted five captions but zero OCR\\n+characters. Transport completion was incorrectly stronger than evidence\\n+completion.\\n+\\n+Fugu, TRINITY, and Conductor support adaptive coordination across specialized\\n+workers; they do not support removing the task evidence needed by those\\n+workers. The official OpenAI API contracts represent image input as typed\\n+content blocks, not as prose placeholders. The orchestrator must preserve\\n+that typed evidence while retaining access-list isolation for prior model\\n+outputs.\\n+\\n+## Decision\\n+\\n+- Normalize Responses `input_image` blocks to the existing Chat Completions\\n+ `image_url` representation at the provider-neutral boundary.\\n+- Retain the original validated image blocks beside each workflow step's text\\n+ instruction. Access lists still govern prior model outputs; source evidence\\n+ is part of the original task, not another agent's hidden state.\\n+- Route image-bearing work and failover only through enabled agents that\\n+ explicitly advertise the `vision` capability tag. Do not infer VISION\\n+ support from a provider name or model identifier.\\n+- Fail closed before provider I/O when no enabled VISION-capable worker is\\n+ available. A text-only answer to an unseen image is not a valid fallback.\\n+- Keep the public request shape and provider-neutral gateway boundary. This\\n+ change adds no provider SDK, model-name ordering, or direct provider path.\\n+\\n+## Consequences\\n+\\n+- Thinker, worker, verifier, and synthesizer steps can independently inspect\\n+ the same source pixels while seeing only the prior outputs allowed by the\\n+ workflow access list.\\n+- Chat Completions and Responses use one internal multimodal representation.\\n+- Image bytes cross the same configured provider boundary more than once in a\\n+ deep workflow. That is deliberate quality-oriented test-time compute, and\\n+ the existing request-size and URL validation remain authoritative.\\n+- A wrongly tagged pool fails visibly and requires an operator to correct its\\n+ capability catalog instead of silently accepting fabricated visual work.\\n+\\n+## Verification\\n+\\n+A synthetic content-block regression must prove that every conducted step\\n+receives the source image, text-only requests remain strings, Responses image\\n+parts survive normalization, failover never enters a non-VISION agent, and a\\n+pool without a VISION agent fails before any client call.\\n+\\n+## References\\n+\\n+Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025).\\n+*Learning to orchestrate agents in natural language with the Conductor*\\n+(arXiv:2512.04388). arXiv. https://doi.org/10.48550/arXiv.2512.04388\\n+\\n+OpenAI. (n.d.-a). *Create chat completion*. OpenAI API reference. Retrieved\\n+August 20, 2026, from\\n+https://developers.openai.com/api/reference/cli/resources/chat/subresources/completions\\n+\\n+OpenAI. (n.d.-b). *Create a model response*. OpenAI API reference. Retrieved\\n+August 20, 2026, from\\n+https://developers.openai.com/api/reference/typescript/resources/beta/subresources/responses/methods/create\\n+\\n+Tang, Y., Cetin, E., Xu, J., Sun, Q., Nielsen, S., Richard, V., Goda, H.,\\n+Tymchenko, I., Nguyen, N., Lee, H., Ashiga, M., Kotyan, S., Kuroki, S., &\\n+Clanuwat, T. (2026). *Sakana Fugu technical report* (arXiv:2606.21228).\\n+arXiv. https://doi.org/10.48550/arXiv.2606.21228\\n+\\n+Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025).\\n+*TRINITY: An evolved LLM coordinator* (arXiv:2512.04695). arXiv.\\n+https://doi.org/10.48550/arXiv.2512.04695\" }, { \"sha\": \"04b01aabcd845d19a22affdf19b65787c6bd2ae9\", \"filename\": \"docs/planning/adrs/0019-no-runtime-monkey-patching.md\", \"status\": \"added\", \"additions\": 25, \"deletions\": 0, \"changes\": 25, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0019-no-runtime-monkey-patching.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0019-no-runtime-monkey-patching.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0019-no-runtime-monkey-patching.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,25 @@\\n+# ADR 0019: No runtime monkey patching for transport contracts\\n+\\n+- Status: Accepted\\n+- Date: 2026-08-21\\n+\\n+## Context\\n+\\n+Provider capability behavior must be visible in the owning transport client.\\n+Import-time mutation of `ModelClient` obscures the effective contract, creates\\n+global process state, and can change behavior for callers that did not opt in.\\n+\\n+## Decision\\n+\\n+The orchestrator must not monkey patch classes or methods at runtime. Optional\\n+sampling omission, capability negotiation, protocol translation, and retry\\n+behavior are implemented in the owning `ModelClient` transport paths and are\\n+covered by direct tests. Importing the package must not mutate a class or\\n+install a wrapper as a side effect.\\n+\\n+## Consequences\\n+\\n+- The effective request contract is inspectable in one implementation.\\n+- Chat, streaming, and batch transports share the same provider-neutral policy.\\n+- Upstream capability changes require an ordinary code review instead of a\\n+ hidden import-order dependency.\" }, { \"sha\": \"0c47c83d6946b414d4669279f03a42cd85192f8a\", \"filename\": \"docs/planning/adrs/0020-provider-error-boundary.md\", \"status\": \"added\", \"additions\": 38, \"deletions\": 0, \"changes\": 38, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0020-provider-error-boundary.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0020-provider-error-boundary.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0020-provider-error-boundary.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,38 @@\\n+# ADR 0020: Keep raw provider failures inside the gateway\\n+\\n+- Status: Accepted\\n+- Date: 2026-08-21\\n+\\n+## Context\\n+\\n+Provider HTTP bodies and exception messages can contain credentials, prompt\\n+content, personal data, internal URLs, or vendor diagnostics. Structured\\n+orchestration, embeddings, retries, and cross-provider failover must not make\\n+provider raw exceptions available through a public gateway error or an\\n+exception cause.\\n+\\n+## Decision\\n+\\n+1. Chat, embedding, and passthrough transport failures expose only\\n+ package-owned messages and never copy provider exception text.\\n+2. Model discovery reports stable diagnostic codes without copying provider\\n+ response or exception text.\\n+3. Exhausted failover and structured-output parsing do not chain provider raw\\n+ errors; deterministic local remediation remains available.\\n+4. Provider diagnostics may be counted by allowlisted type/code in internal\\n+ telemetry, but raw bodies, exception text, credentials, and prompts are not\\n+ persisted or returned.\\n+\\n+## Verification\\n+\\n+`tests/test_model_discovery.py`, `tests/test_provider_reliability.py`, and\\n+`tests/test_model_judge.py` assert that provider response text is absent from\\n+public messages and causes. The full suite must remain green before merge.\\n+\\n+## References\\n+\\n+MITRE. (n.d.). *CWE-209: Generation of error message containing sensitive\\n+information*. https://cwe.mitre.org/data/definitions/209.html\\n+\\n+OWASP Foundation. (2023). *Application Security Verification Standard 4.0.3*.\\n+https://owasp.org/www-project-application-security-verification-standard/\" }, { \"sha\": \"7a720e1b826c290d228a2637831927706923bdfb\", \"filename\": \"docs/planning/adrs/0025-universal-hash-locked-requirements.md\", \"status\": \"added\", \"additions\": 58, \"deletions\": 0, \"changes\": 58, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0025-universal-hash-locked-requirements.md\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/docs%2Fplanning%2Fadrs%2F0025-universal-hash-locked-requirements.md\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/docs%2Fplanning%2Fadrs%2F0025-universal-hash-locked-requirements.md?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,58 @@\\n+---\\n+id: \\\"0025\\\"\\n+title: \\\"Generate one universal hash-locked runtime requirements file\\\"\\n+status: accepted\\n+accepted_date: \\\"2026-08-21\\\"\\n+deciders:\\n+ - \\\"repository maintainer\\\"\\n+affected_components:\\n+ - \\\"requirements.lock\\\"\\n+ - \\\"tests/test_repository_security_metadata.py\\\"\\n+---\\n+\\n+# Generate one universal hash-locked runtime requirements file\\n+\\n+## Context\\n+\\n+The runtime lock is installed with `pip --require-hashes`, but its previous\\n+platform-specific regeneration removed `colorama`, `greenlet`, and `tzdata`.\\n+Those packages are conditional transitive dependencies on supported Python\\n+environments, so a lock produced for one host was not complete evidence for\\n+another host.\\n+\\n+## Decision\\n+\\n+Generate `requirements.lock` with `uv pip compile --universal\\n+--generate-hashes --python-version 3.10 --extra api --extra db pyproject.toml`.\\n+Universal resolution retains PEP 508 environment markers, one exact version per\\n+resolved branch, and hashes for every artifact while preserving the existing\\n+`pip --require-hashes` installation contract. Regeneration must retain the\\n+platform-conditional `colorama`, `greenlet`, and `tzdata` records and a metadata\\n+test must fail if universal mode or those records disappear.\\n+\\n+Do not add a second lock format yet. PEP 751 standardizes `pylock.toml`, but the\\n+current CI and buyer evidence consume the existing requirements file directly.\\n+Adopt `pylock.toml` only when the production installer and security scanners can\\n+consume it without maintaining two divergent dependency authorities.\\n+\\n+## Consequences\\n+\\n+- macOS, Linux, Windows, architecture, and supported Python marker branches are\\n+ resolved together instead of inheriting the workstation that ran the tool.\\n+- Dependency versions remain auditable and installation remains resolution-free\\n+ under hash-checking mode.\\n+- A regeneration can be more constrained than a host-only solve; an\\n+ incompatible dependency must fail the universal solve rather than silently\\n+ disappear from another platform.\\n+\\n+## References\\n+\\n+Astral Software, Inc. (2026). *Resolution*. uv.\\n+https://docs.astral.sh/uv/concepts/resolution/\\n+\\n+Cannon, B. (2025). PEP 751: A file format to record Python dependencies for\\n+installation reproducibility. *Python Enhancement Proposals*. Python Software\\n+Foundation. https://peps.python.org/pep-0751/\\n+\\n+Python Packaging Authority. (2026). *Dependency specifiers*. Python Packaging\\n+User Guide. https://packaging.python.org/en/latest/specifications/dependency-specifiers/\" }, { \"sha\": \"23753f8b8097138ec68ea3b85b463a438362303e\", \"filename\": \"examples/agents.local.json\", \"status\": \"modified\", \"additions\": 6, \"deletions\": 45, \"changes\": 51, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/examples%2Fagents.local.json\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/examples%2Fagents.local.json\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/examples%2Fagents.local.json?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -4,74 +4,35 @@\\n \\\"id\\\": \\\"contextual_orchestrator\\\",\\n \\\"model\\\": \\\"contextual-orchestrator\\\",\\n \\\"base_url\\\": \\\"local://127.0.0.1:18000/v1\\\",\\n+ \\\"local_credential_key\\\": \\\"LOCAL_GATEWAY_TOKEN\\\",\\n \\\"provider_name\\\": \\\"contextual-orchestrator\\\",\\n \\\"tags\\\": [\\\"orchestration\\\", \\\"planning\\\", \\\"reasoning\\\", \\\"verification\\\", \\\"writing\\\"],\\n \\\"priority\\\": 5,\\n \\\"provider_exclusions\\\": [\\\"thinker\\\", \\\"worker\\\", \\\"verifier\\\", \\\"synthesizer\\\"]\\n },\\n- {\\n- \\\"id\\\": \\\"mlx_gemma_4_31b_it\\\",\\n- \\\"model\\\": \\\"mlx-community/gemma-4-31b-it-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n- \\\"tags\\\": [\\\"reasoning\\\", \\\"research\\\", \\\"coding\\\", \\\"writing\\\", \\\"verification\\\"],\\n- \\\"priority\\\": 4,\\n- \\\"provider_exclusions\\\": [\\\"verifier\\\"]\\n- },\\n- {\\n- \\\"id\\\": \\\"mlx_deepseek_r1_qwen_32b\\\",\\n- \\\"model\\\": \\\"outlier-ai/deepseek-r1-distill-qwen-32b-mlx-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n- \\\"tags\\\": [\\\"reasoning\\\", \\\"research\\\", \\\"coding\\\", \\\"verification\\\"],\\n- \\\"priority\\\": 4,\\n- \\\"provider_exclusions\\\": [\\\"verifier\\\"]\\n- },\\n- {\\n- \\\"id\\\": \\\"mlx_gemma_4_e4b_it\\\",\\n- \\\"model\\\": \\\"mlx-community/gemma-4-e4b-it-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n- \\\"tags\\\": [\\\"reasoning\\\", \\\"research\\\", \\\"coding\\\", \\\"writing\\\", \\\"verification\\\"],\\n- \\\"priority\\\": 3\\n- },\\n- {\\n- \\\"id\\\": \\\"mlx_llama_3_2_3b_instruct\\\",\\n- \\\"model\\\": \\\"mlx-community/llama-3.2-3b-instruct-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n- \\\"tags\\\": [\\\"fast\\\", \\\"reasoning\\\", \\\"coding\\\", \\\"writing\\\", \\\"verification\\\"],\\n- \\\"priority\\\": 2\\n- },\\n- {\\n- \\\"id\\\": \\\"mlx_llama_3_2_1b_instruct\\\",\\n- \\\"model\\\": \\\"mlx-community/llama-3.2-1b-instruct-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n- \\\"tags\\\": [\\\"fast\\\", \\\"writing\\\", \\\"coding\\\"],\\n- \\\"priority\\\": 1,\\n- \\\"provider_exclusions\\\": [\\\"verifier\\\"]\\n- },\\n {\\n \\\"id\\\": \\\"llama_cpp_embeddinggemma\\\",\\n \\\"model\\\": \\\"embeddinggemma\\\",\\n \\\"base_url\\\": \\\"local://127.0.0.1:8082/v1\\\",\\n+ \\\"local_credential_key\\\": \\\"LOCAL_GATEWAY_TOKEN\\\",\\n \\\"provider_name\\\": \\\"llama.cpp\\\",\\n \\\"tags\\\": [\\\"embedding\\\"],\\n \\\"priority\\\": 0\\n },\\n {\\n \\\"id\\\": \\\"lmstudio_gemma_4_e4b_it\\\",\\n- \\\"model\\\": \\\"lmstudio-community/gemma-4-E4B-it-MLX-4bit\\\",\\n+ \\\"model\\\": \\\"gemma-4-e4b-it\\\",\\n \\\"base_url\\\": \\\"local://127.0.0.1:1234/v1\\\",\\n+ \\\"local_credential_key\\\": \\\"LOCAL_GATEWAY_TOKEN\\\",\\n \\\"provider_name\\\": \\\"lm-studio\\\",\\n \\\"tags\\\": [\\\"reasoning\\\", \\\"coding\\\", \\\"writing\\\"],\\n \\\"priority\\\": 0\\n },\\n {\\n \\\"id\\\": \\\"lmstudio_embeddinggemma\\\",\\n- \\\"model\\\": \\\"mlx-community/embeddinggemma-300m-8bit\\\",\\n+ \\\"model\\\": \\\"embeddinggemma\\\",\\n \\\"base_url\\\": \\\"local://127.0.0.1:1234/v1\\\",\\n+ \\\"local_credential_key\\\": \\\"LOCAL_GATEWAY_TOKEN\\\",\\n \\\"provider_name\\\": \\\"lm-studio\\\",\\n \\\"tags\\\": [\\\"embedding\\\"],\\n \\\"priority\\\": 0\" }, { \"sha\": \"103a31b38e5eea170ed1a9cc1edfeade74247bb2\", \"filename\": \"examples/agents.mlx.json\", \"status\": \"removed\", \"additions\": 0, \"deletions\": 12, \"changes\": 12, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/e226e1197bdfc890c9d8e5b9b648c78857d7e465/examples%2Fagents.mlx.json\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/e226e1197bdfc890c9d8e5b9b648c78857d7e465/examples%2Fagents.mlx.json\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/examples%2Fagents.mlx.json?ref=e226e1197bdfc890c9d8e5b9b648c78857d7e465\", \"patch\": \"@@ -1,12 +0,0 @@\\n-{\\n- \\\"agents\\\": [\\n- {\\n- \\\"id\\\": \\\"local_fast_agent\\\",\\n- \\\"model\\\": \\\"mlx-community/llama-3.2-3b-instruct-4bit\\\",\\n- \\\"base_url\\\": \\\"mlx://127.0.0.1:8080/v1\\\",\\n- \\\"provider_name\\\": \\\"mlx-lm\\\",\\n- \\\"tags\\\": [\\\"reasoning\\\", \\\"writing\\\", \\\"coding\\\", \\\"verification\\\"],\\n- \\\"priority\\\": 1\\n- }\\n- ]\\n-}\" }, { \"sha\": \"d9fc8f1ecf1c64fbf3accb13ec7f05f0708301fe\", \"filename\": \"pyproject.toml\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 0, \"changes\": 3, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/pyproject.toml\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/pyproject.toml\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/pyproject.toml?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -6,6 +6,9 @@ readme = \\\"README.md\\\"\\n requires-python = \\\">=3.10\\\"\\n dependencies = [\\n \\\"hypothesis>=6.100\\\",\\n+ \\\"opentelemetry-api>=1.30.0\\\",\\n+ \\\"opentelemetry-sdk>=1.30.0\\\",\\n+ \\\"opentelemetry-exporter-otlp-proto-http>=1.30.0\\\",\\n ]\\n \\n [project.optional-dependencies]\" }, { \"sha\": \"0f79fd6e90d24ade5ce44f017b5ba3e7c17fe790\", \"filename\": \"requirements.lock\", \"status\": \"modified\", \"additions\": 474, \"deletions\": 94, \"changes\": 568, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/requirements.lock\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/requirements.lock\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/requirements.lock?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,9 +1,5 @@\\n-#\\n-# This file is autogenerated by pip-compile with Python 3.12\\n-# by the following command:\\n-#\\n-# pip-compile --extra=api --extra=db --generate-hashes --output-file=requirements.lock pyproject.toml\\n-#\\n+# This file was autogenerated by uv via the following command:\\n+# uv pip compile --universal --generate-hashes --python-version 3.10 --extra api --extra db --output-file requirements.lock pyproject.toml\\n alembic==1.18.5 \\\\\\n --hash=sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc \\\\\\n --hash=sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e\\n@@ -20,107 +16,380 @@ anyio==4.14.1 \\\\\\n --hash=sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72 \\\\\\n --hash=sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e\\n # via starlette\\n+certifi==2026.7.22 \\\\\\n+ --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \\\\\\n+ --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55\\n+ # via requests\\n+charset-normalizer==3.5.1 \\\\\\n+ --hash=sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45 \\\\\\n+ --hash=sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d \\\\\\n+ --hash=sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5 \\\\\\n+ --hash=sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b \\\\\\n+ --hash=sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f \\\\\\n+ --hash=sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f \\\\\\n+ --hash=sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5 \\\\\\n+ --hash=sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22 \\\\\\n+ --hash=sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5 \\\\\\n+ --hash=sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac \\\\\\n+ --hash=sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838 \\\\\\n+ --hash=sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90 \\\\\\n+ --hash=sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626 \\\\\\n+ --hash=sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4 \\\\\\n+ --hash=sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369 \\\\\\n+ --hash=sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b \\\\\\n+ --hash=sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e \\\\\\n+ --hash=sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee \\\\\\n+ --hash=sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1 \\\\\\n+ --hash=sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102 \\\\\\n+ --hash=sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8 \\\\\\n+ --hash=sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8 \\\\\\n+ --hash=sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9 \\\\\\n+ --hash=sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf \\\\\\n+ --hash=sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0 \\\\\\n+ --hash=sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031 \\\\\\n+ --hash=sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e \\\\\\n+ --hash=sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235 \\\\\\n+ --hash=sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072 \\\\\\n+ --hash=sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb \\\\\\n+ --hash=sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c \\\\\\n+ --hash=sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950 \\\\\\n+ --hash=sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2 \\\\\\n+ --hash=sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb \\\\\\n+ --hash=sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e \\\\\\n+ --hash=sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6 \\\\\\n+ --hash=sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b \\\\\\n+ --hash=sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2 \\\\\\n+ --hash=sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d \\\\\\n+ --hash=sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa \\\\\\n+ --hash=sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2 \\\\\\n+ --hash=sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818 \\\\\\n+ --hash=sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032 \\\\\\n+ --hash=sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71 \\\\\\n+ --hash=sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96 \\\\\\n+ --hash=sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687 \\\\\\n+ --hash=sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8 \\\\\\n+ --hash=sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3 \\\\\\n+ --hash=sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61 \\\\\\n+ --hash=sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9 \\\\\\n+ --hash=sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1 \\\\\\n+ --hash=sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a \\\\\\n+ --hash=sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420 \\\\\\n+ --hash=sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4 \\\\\\n+ --hash=sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65 \\\\\\n+ --hash=sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663 \\\\\\n+ --hash=sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f \\\\\\n+ --hash=sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591 \\\\\\n+ --hash=sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a \\\\\\n+ --hash=sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18 \\\\\\n+ --hash=sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e \\\\\\n+ --hash=sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f \\\\\\n+ --hash=sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7 \\\\\\n+ --hash=sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3 \\\\\\n+ --hash=sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c \\\\\\n+ --hash=sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3 \\\\\\n+ --hash=sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7 \\\\\\n+ --hash=sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96 \\\\\\n+ --hash=sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486 \\\\\\n+ --hash=sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3 \\\\\\n+ --hash=sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6 \\\\\\n+ --hash=sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b \\\\\\n+ --hash=sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731 \\\\\\n+ --hash=sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959 \\\\\\n+ --hash=sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9 \\\\\\n+ --hash=sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf \\\\\\n+ --hash=sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8 \\\\\\n+ --hash=sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e \\\\\\n+ --hash=sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f \\\\\\n+ --hash=sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885 \\\\\\n+ --hash=sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0 \\\\\\n+ --hash=sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506 \\\\\\n+ --hash=sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2 \\\\\\n+ --hash=sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0 \\\\\\n+ --hash=sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e \\\\\\n+ --hash=sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f \\\\\\n+ --hash=sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e \\\\\\n+ --hash=sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491 \\\\\\n+ --hash=sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a \\\\\\n+ --hash=sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20 \\\\\\n+ --hash=sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449 \\\\\\n+ --hash=sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af \\\\\\n+ --hash=sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c \\\\\\n+ --hash=sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712 \\\\\\n+ --hash=sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7 \\\\\\n+ --hash=sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a \\\\\\n+ --hash=sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20 \\\\\\n+ --hash=sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f \\\\\\n+ --hash=sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3 \\\\\\n+ --hash=sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9 \\\\\\n+ --hash=sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e \\\\\\n+ --hash=sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5 \\\\\\n+ --hash=sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2 \\\\\\n+ --hash=sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36 \\\\\\n+ --hash=sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263 \\\\\\n+ --hash=sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4 \\\\\\n+ --hash=sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11 \\\\\\n+ --hash=sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a \\\\\\n+ --hash=sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3 \\\\\\n+ --hash=sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375 \\\\\\n+ --hash=sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa \\\\\\n+ --hash=sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d \\\\\\n+ --hash=sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5 \\\\\\n+ --hash=sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99 \\\\\\n+ --hash=sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d \\\\\\n+ --hash=sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c \\\\\\n+ --hash=sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488 \\\\\\n+ --hash=sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6 \\\\\\n+ --hash=sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc \\\\\\n+ --hash=sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b \\\\\\n+ --hash=sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f \\\\\\n+ --hash=sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00 \\\\\\n+ --hash=sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10 \\\\\\n+ --hash=sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598 \\\\\\n+ --hash=sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6 \\\\\\n+ --hash=sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962 \\\\\\n+ --hash=sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c \\\\\\n+ --hash=sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08 \\\\\\n+ --hash=sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab \\\\\\n+ --hash=sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573 \\\\\\n+ --hash=sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90 \\\\\\n+ --hash=sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5 \\\\\\n+ --hash=sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18 \\\\\\n+ --hash=sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d \\\\\\n+ --hash=sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af \\\\\\n+ --hash=sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea \\\\\\n+ --hash=sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c \\\\\\n+ --hash=sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b \\\\\\n+ --hash=sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6 \\\\\\n+ --hash=sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8 \\\\\\n+ --hash=sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774 \\\\\\n+ --hash=sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004 \\\\\\n+ --hash=sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a \\\\\\n+ --hash=sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a \\\\\\n+ --hash=sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2 \\\\\\n+ --hash=sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2 \\\\\\n+ --hash=sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa \\\\\\n+ --hash=sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe \\\\\\n+ --hash=sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3 \\\\\\n+ --hash=sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc \\\\\\n+ --hash=sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795 \\\\\\n+ --hash=sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d \\\\\\n+ --hash=sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc \\\\\\n+ --hash=sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893 \\\\\\n+ --hash=sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef \\\\\\n+ --hash=sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d \\\\\\n+ --hash=sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda \\\\\\n+ --hash=sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17 \\\\\\n+ --hash=sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30 \\\\\\n+ --hash=sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7 \\\\\\n+ --hash=sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5 \\\\\\n+ --hash=sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182 \\\\\\n+ --hash=sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f \\\\\\n+ --hash=sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9 \\\\\\n+ --hash=sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada \\\\\\n+ --hash=sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876 \\\\\\n+ --hash=sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a \\\\\\n+ --hash=sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348 \\\\\\n+ --hash=sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3 \\\\\\n+ --hash=sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f \\\\\\n+ --hash=sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0 \\\\\\n+ --hash=sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f\\n+ # via requests\\n click==8.4.2 \\\\\\n --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \\\\\\n --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76\\n # via uvicorn\\n-colorama==0.4.6 \\\\\\n+colorama==0.4.6 ; sys_platform == 'win32' \\\\\\n --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \\\\\\n --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6\\n # via click\\n+exceptiongroup==1.3.1 ; python_full_version < '3.11' \\\\\\n+ --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \\\\\\n+ --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598\\n+ # via\\n+ # anyio\\n+ # hypothesis\\n fastapi==0.138.2 \\\\\\n --hash=sha256:6432359d067a432134620e7c5e4c6e5063e7f37815bbbbf20acef14b0d2e3fc8 \\\\\\n --hash=sha256:db90c1ffb5517fba5d4a9f80e866daa008747e646310c9ce155c8c535f9d1615\\n # via contextual-orchestrator (pyproject.toml)\\n-greenlet==3.5.3 \\\\\\n- --hash=sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db \\\\\\n- --hash=sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7 \\\\\\n- --hash=sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8 \\\\\\n- --hash=sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc \\\\\\n- --hash=sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da \\\\\\n- --hash=sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8 \\\\\\n- --hash=sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91 \\\\\\n- --hash=sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c \\\\\\n- --hash=sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310 \\\\\\n- --hash=sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3 \\\\\\n- --hash=sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce \\\\\\n- --hash=sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149 \\\\\\n- --hash=sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d \\\\\\n- --hash=sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34 \\\\\\n- --hash=sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be \\\\\\n- --hash=sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2 \\\\\\n- --hash=sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b \\\\\\n- --hash=sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0 \\\\\\n- --hash=sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71 \\\\\\n- --hash=sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d \\\\\\n- --hash=sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04 \\\\\\n- --hash=sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227 \\\\\\n- --hash=sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c \\\\\\n- --hash=sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6 \\\\\\n- --hash=sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8 \\\\\\n- --hash=sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21 \\\\\\n- --hash=sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23 \\\\\\n- --hash=sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605 \\\\\\n- --hash=sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128 \\\\\\n- --hash=sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f \\\\\\n- --hash=sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea \\\\\\n- --hash=sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81 \\\\\\n- --hash=sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2 \\\\\\n- --hash=sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8 \\\\\\n- --hash=sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e \\\\\\n- --hash=sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb \\\\\\n- --hash=sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47 \\\\\\n- --hash=sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8 \\\\\\n- --hash=sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab \\\\\\n- --hash=sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7 \\\\\\n- --hash=sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861 \\\\\\n- --hash=sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702 \\\\\\n- --hash=sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814 \\\\\\n- --hash=sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf \\\\\\n- --hash=sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a \\\\\\n- --hash=sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260 \\\\\\n- --hash=sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1 \\\\\\n- --hash=sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec \\\\\\n- --hash=sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a \\\\\\n- --hash=sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5 \\\\\\n- --hash=sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1 \\\\\\n- --hash=sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4 \\\\\\n- --hash=sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c \\\\\\n- --hash=sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda \\\\\\n- --hash=sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb \\\\\\n- --hash=sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31 \\\\\\n- --hash=sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d \\\\\\n- --hash=sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c \\\\\\n- --hash=sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d \\\\\\n- --hash=sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b \\\\\\n- --hash=sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550 \\\\\\n- --hash=sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c \\\\\\n- --hash=sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357 \\\\\\n- --hash=sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c \\\\\\n- --hash=sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4 \\\\\\n- --hash=sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930 \\\\\\n- --hash=sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8 \\\\\\n- --hash=sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b \\\\\\n- --hash=sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb \\\\\\n- --hash=sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16 \\\\\\n- --hash=sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608 \\\\\\n- --hash=sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f \\\\\\n- --hash=sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc \\\\\\n- --hash=sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d \\\\\\n- --hash=sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44 \\\\\\n- --hash=sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b \\\\\\n- --hash=sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3 \\\\\\n- --hash=sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154 \\\\\\n- --hash=sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117\\n+googleapis-common-protos==1.75.1 \\\\\\n+ --hash=sha256:28a1934bcd33b9c9da66ac301a0a4227e3367f095a17d0375cb98f0a09d93b79 \\\\\\n+ --hash=sha256:d3042c6c5a2d4e67113104d6b6818b59b6bd92a197f2a91508e801fe815cf071\\n+ # via opentelemetry-exporter-otlp-proto-http\\n+greenlet==3.5.5 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' \\\\\\n+ --hash=sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537 \\\\\\n+ --hash=sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39 \\\\\\n+ --hash=sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277 \\\\\\n+ --hash=sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41 \\\\\\n+ --hash=sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2 \\\\\\n+ --hash=sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d \\\\\\n+ --hash=sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53 \\\\\\n+ --hash=sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e \\\\\\n+ --hash=sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502 \\\\\\n+ --hash=sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5 \\\\\\n+ --hash=sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc \\\\\\n+ --hash=sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759 \\\\\\n+ --hash=sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f \\\\\\n+ --hash=sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b \\\\\\n+ --hash=sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1 \\\\\\n+ --hash=sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5 \\\\\\n+ --hash=sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769 \\\\\\n+ --hash=sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0 \\\\\\n+ --hash=sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f \\\\\\n+ --hash=sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da \\\\\\n+ --hash=sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76 \\\\\\n+ --hash=sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3 \\\\\\n+ --hash=sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e \\\\\\n+ --hash=sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476 \\\\\\n+ --hash=sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e \\\\\\n+ --hash=sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380 \\\\\\n+ --hash=sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef \\\\\\n+ --hash=sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18 \\\\\\n+ --hash=sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b \\\\\\n+ --hash=sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272 \\\\\\n+ --hash=sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0 \\\\\\n+ --hash=sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053 \\\\\\n+ --hash=sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07 \\\\\\n+ --hash=sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387 \\\\\\n+ --hash=sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52 \\\\\\n+ --hash=sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed \\\\\\n+ --hash=sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95 \\\\\\n+ --hash=sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c \\\\\\n+ --hash=sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad \\\\\\n+ --hash=sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f \\\\\\n+ --hash=sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db \\\\\\n+ --hash=sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328 \\\\\\n+ --hash=sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8 \\\\\\n+ --hash=sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71 \\\\\\n+ --hash=sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc \\\\\\n+ --hash=sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864 \\\\\\n+ --hash=sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0 \\\\\\n+ --hash=sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1 \\\\\\n+ --hash=sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b \\\\\\n+ --hash=sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667 \\\\\\n+ --hash=sha256:86c5113d698cb8d927b2750bb1f1d59eefe3a37e0e0217491aee29a7f84ef52c \\\\\\n+ --hash=sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c \\\\\\n+ --hash=sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926 \\\\\\n+ --hash=sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc \\\\\\n+ --hash=sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd \\\\\\n+ --hash=sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007 \\\\\\n+ --hash=sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6 \\\\\\n+ --hash=sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56 \\\\\\n+ --hash=sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0 \\\\\\n+ --hash=sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b \\\\\\n+ --hash=sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53 \\\\\\n+ --hash=sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c \\\\\\n+ --hash=sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c \\\\\\n+ --hash=sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474 \\\\\\n+ --hash=sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa \\\\\\n+ --hash=sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61 \\\\\\n+ --hash=sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206 \\\\\\n+ --hash=sha256:c69bed34470abfcd456984fdadaa18e62169af4480335c45f3c32d1d9c12e638 \\\\\\n+ --hash=sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9 \\\\\\n+ --hash=sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874 \\\\\\n+ --hash=sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d \\\\\\n+ --hash=sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8 \\\\\\n+ --hash=sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae \\\\\\n+ --hash=sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0 \\\\\\n+ --hash=sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773 \\\\\\n+ --hash=sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4 \\\\\\n+ --hash=sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552 \\\\\\n+ --hash=sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42 \\\\\\n+ --hash=sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b\\n # via sqlalchemy\\n h11==0.16.0 \\\\\\n --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \\\\\\n --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86\\n # via uvicorn\\n+hypothesis==6.165.10 \\\\\\n+ --hash=sha256:00de0abdcf8c05c9d0eab735a3c49a276376b55151e6fcb903c2b39a90e5e5c3 \\\\\\n+ --hash=sha256:057d0232f1224dcd0b7698902551a4341a7399f90670b036db6c4376715fe889 \\\\\\n+ --hash=sha256:09772e328a26e50486ac572be34f9887f9aa185efe7ebb16bde4e8f6038db1f4 \\\\\\n+ --hash=sha256:0c4e6869817c3cfdf5a2b4d348497b95159bdecb3365be732c9b8570e36a4eef \\\\\\n+ --hash=sha256:10d9a650a4666b0914831f769703d36140ed8039fd19bf9b71f615b8541eccf2 \\\\\\n+ --hash=sha256:18a3ea838ddea183388f8788750afa8494d79abb5358823be9782585f34445d3 \\\\\\n+ --hash=sha256:1a380bc99aa3b035e6a95a2201bf792d4082a04ca75babcc21849c2d0914bb28 \\\\\\n+ --hash=sha256:1d305448e9bd8e2f4f3cea0eafd809efdaab4e998a0019bc615650c8463e42f1 \\\\\\n+ --hash=sha256:1ec53f08732e3cfd0342cbbd75dbd1b193c8f19390660466e536a748bb81f757 \\\\\\n+ --hash=sha256:1f2c4db25fb8ec1a16a8dba580666337b8ffb1887c4cf1750cc954313897cef7 \\\\\\n+ --hash=sha256:20f6236cfb90b7817bb1a6a087589ca4aa46d73170f0dd62963952ed5dadc589 \\\\\\n+ --hash=sha256:22cf19388f0ff6ced8eb3e49c903d14938e4ed909d93bf28383eef451511e424 \\\\\\n+ --hash=sha256:277f41801e88dad2eba082f91a75632b7584ff64044ba2cf9dadf511b0d19cd0 \\\\\\n+ --hash=sha256:2a2567b3a03a4a5a7c575c191cfcce321a967df3727803817e75bffbbeaecabe \\\\\\n+ --hash=sha256:2abb50cf1cf77d721de0a24c3f99d9c4ffdeb2cbd1e12aebb5a7a93e2b6b6d1f \\\\\\n+ --hash=sha256:2b112768cfb67f2b683e53e58c1a33d27811aacf60c942b8eb74635e469a73f6 \\\\\\n+ --hash=sha256:2b36aaffc88625a44f91074c5bbedfdefb9b376c38d1b3c342edcd2e4c8ed16c \\\\\\n+ --hash=sha256:2d0e0f8263d34dd8fa3b39eaa9a50bba56a8470b3dd9ebf6672d10840abe063e \\\\\\n+ --hash=sha256:30797f20ca45e57f526d2df872f63ba453cb4e1091ad542184a7a951af8da79d \\\\\\n+ --hash=sha256:3376f2594763aef14faa519b0fb27cae7ce9eeaab4c69efa07777499110306c9 \\\\\\n+ --hash=sha256:34ee6402df6f31274d89119f1561b5f7489c97866afc5b7a3ed3a13d7e762802 \\\\\\n+ --hash=sha256:37a7ac3d34220800e1107871cc391bca1b00439875925d7d821878b8b791f245 \\\\\\n+ --hash=sha256:3de69aa8b924b400291a3cc42aaf78e6ab65c905a3e7e1a5dc39d95ef1b428cb \\\\\\n+ --hash=sha256:4334058033e0214475f019e15492a50f3854fe8728cf51fe25c6191a2c3f8e52 \\\\\\n+ --hash=sha256:490c56b830772b0eca3b4b2cecb3741a1ed26b1d7206a279e1525dbf0aa95ee4 \\\\\\n+ --hash=sha256:4c68e983d0007d014bb01ad4bcbba78bc432c73a1755ff36d5102ceefa18299a \\\\\\n+ --hash=sha256:5671d2b2bf83bd4b6f02e55b32d432506eff5358c82f39b460a849ce19a2666e \\\\\\n+ --hash=sha256:56cb8c9055e50545fe6e3e5a560ec25a724673b2e4051f3c24d44e3ebc35dd72 \\\\\\n+ --hash=sha256:5841331c504e02d7c334591681cb8587cdd59dee7e149db6d3db8e3f9e9f02eb \\\\\\n+ --hash=sha256:592107a0faf6c9c3a63a8dbf13dfb1cbda1cf599b0bc11c953221b00204b9ce1 \\\\\\n+ --hash=sha256:5cf3b612542ba174c9da4000b59a4f4c81e8d66f87509be85d3a1b71b5c36413 \\\\\\n+ --hash=sha256:60cab3ab4ea468d31a33739ffd7e94ec3e37dea891d65a6582ecc8a477175191 \\\\\\n+ --hash=sha256:637445c1593a2a9d1024fda50082f07bb56baedda78d90a25f64b8111727ef94 \\\\\\n+ --hash=sha256:68b45e09834cd80523cb1eb274463073c7a9af4e4ef7cff34d9615f355572d32 \\\\\\n+ --hash=sha256:6caadcd1afb62630ff5c5ff353626eaa616553a5971295ad6dc2b19ca8a39620 \\\\\\n+ --hash=sha256:6e20a02775eb3cf0ffb4f0219b6d7c1f240336663d4e5d7028675ec247c790c4 \\\\\\n+ --hash=sha256:713f4ce4e82c26b53031f139de959bc9e8b54d3995aa824b89bbdf8229df2a45 \\\\\\n+ --hash=sha256:717aea574e0e5edba2868aa66b1caae335d8f1ad3fb29f01dd6502953fa823a1 \\\\\\n+ --hash=sha256:72df95fb1db41755b155c5f02106e0036a339250555c8d351d488704fd112cf9 \\\\\\n+ --hash=sha256:73e6df02a6a62f8045b511c272f894d08e56d174504c793c9effcbc6778051a8 \\\\\\n+ --hash=sha256:76a7be86d986223b9f1bdb7e7cbcdb048649901fdb956c598ef73bdab1786cd5 \\\\\\n+ --hash=sha256:7730d8197086f65d8969a991d6728a1d420a51b19fea06535c896cb43a1e05d0 \\\\\\n+ --hash=sha256:79900a9920a0b1d3a626c03a90ac6bf7042e78d46906a565b86a0dbe926f1d96 \\\\\\n+ --hash=sha256:7a7980a898a3e6ebe4de1896a0507e3d519edb53fb9b4bda478c9fbeb6514558 \\\\\\n+ --hash=sha256:8001925fa3dde51cb574e4c9de4c7efe77c4e4d64bd2fd2ef61d5651f9d04f3d \\\\\\n+ --hash=sha256:8660572b2d424bf5369ea8990985225f70bd1615b76ecd9c25588a3b9307009f \\\\\\n+ --hash=sha256:8b20f44773a9ab84400465e318712d8c2ca16418d35b9f80aa27fdf2d690ad10 \\\\\\n+ --hash=sha256:90915635b9648071129b0f72c0673cf8eac9eb84cfd445c5bedef30c714b1ec2 \\\\\\n+ --hash=sha256:9ccac776b2ca93b324806facd526ccb45da0fd035001c899a35b02c44431e209 \\\\\\n+ --hash=sha256:9d77c3be7b429875036ad0f0597c6e5cc6bb17894a4da005e3807de64d2673ad \\\\\\n+ --hash=sha256:9f07ae36c3b093e13687a894e79fe69e98a94c0b67fef656c575247682218143 \\\\\\n+ --hash=sha256:ab0f2e9d7d7d4db257f7cf53de3706c2baf124269571f20ffc2bcd6781f03063 \\\\\\n+ --hash=sha256:ad0764730e8e3421601c2cc7e1f054a9206c60ea0917165d8d9193dc453f34f1 \\\\\\n+ --hash=sha256:aff1f584c9538e8979cd180b1d70bf99bc16be19d4666414f49e5942b21a4f2c \\\\\\n+ --hash=sha256:b33dc30170a7402e03c180f2c5ef69dc077152f35b91621e9cebcde9c7d71746 \\\\\\n+ --hash=sha256:b5820d009aedb7ae9cfd32f98b1ab0c0bbd6268379c4fab042218b6b655c63f8 \\\\\\n+ --hash=sha256:bb8c7d05ea27a093a92b250904095d71d924b6b44e5795a415c1b20c265f0c65 \\\\\\n+ --hash=sha256:c01dd04044c472e47193b54f68e84e08d6ebf4f29551885aa959b015f7cd9747 \\\\\\n+ --hash=sha256:c53e9b1c36350df9965ec44d6c0d4e0bbbb38f720dd2b0e1256dc6524d411015 \\\\\\n+ --hash=sha256:c6559380469295c4009215fe1cab561301591a3bee2e2fb3f4f96d2273a3affc \\\\\\n+ --hash=sha256:cc2da5aa4edf14743fa9257e5ba3513963999f01211635702479d8e92b8207c8 \\\\\\n+ --hash=sha256:d1ea02fa8ab3d33eb1125eade81f7136341eb429152c6dbe2ae6f8bc33b3fbdd \\\\\\n+ --hash=sha256:d623801ae3dcd97b77b983400ef3d48bf976648e4efff19929175322eaae074d \\\\\\n+ --hash=sha256:d9145fe43ebb22e66672967c3fab411793b226ed776e4fe282271bca6ad3c0bb \\\\\\n+ --hash=sha256:dafa7c9dbe3d802f9bcdf261b29c8a70700fb22839947f06e471f62c46b6257f \\\\\\n+ --hash=sha256:dd207497bb985918409a1bb5db85d1875f74e1269487332113b73d1ee7c77647 \\\\\\n+ --hash=sha256:e10858f57ed0e74baa04393845f469fe8ad502c16ece4499bef7700c575611bd \\\\\\n+ --hash=sha256:e1bbeb7c506b07ee0422cf9b2f7212fefa4240957f03526d38d27bc6743a0a48 \\\\\\n+ --hash=sha256:e5f95f7b622e4171096d92175dda0a560f0955ade9b8a3a07bdcf151f7359611 \\\\\\n+ --hash=sha256:e9acb2c4d9cb532c3fedea74159f7b923c8c036328c9239b4049e7aa073bdd81 \\\\\\n+ --hash=sha256:e9f924aa610c0618445e1e8738c822c3190ce2a2699a0cb48ec3a351a96761f2 \\\\\\n+ --hash=sha256:ed1a5891e59472884a03cb9875483e8fc131c80a275c60967f8afc5458a0c8ff \\\\\\n+ --hash=sha256:ed68e27b8a61e57a3ccdc7c5a14499e00b54dfe223087204d5d40b3b5ef58b6d \\\\\\n+ --hash=sha256:eeab73050ea58c13dd56e329f594c1dfe32ebd7bb169bbdf4f8ceefbc31ec6b5 \\\\\\n+ --hash=sha256:f4dafd6d6ababfa3b14dd6e5f0378cb7c7d291895a31a40abcbb7cc74f396131 \\\\\\n+ --hash=sha256:f69ec5be85ef508e206153bed8eafd03f7995dc464356c8bbb279a1e2b7d56f3 \\\\\\n+ --hash=sha256:f76d1562643693b8a40066f1f96af795b93fd9bcfc9690a1af2ff4c5867ee29e \\\\\\n+ --hash=sha256:f839d29d0cc12048cf073d88ca4fdf94d420bc2b8afd69641ff6d496422ccd4f \\\\\\n+ --hash=sha256:f9180c362bde06fd05380298ded4e234fbc0d6ede0a864835bfd91c1e24283d5 \\\\\\n+ --hash=sha256:f9ff356e97e3ab09db07c8b675efa67340103874a0bae7465acb83dad7a35f7f \\\\\\n+ --hash=sha256:fa74636a49fc8077413ce8db3e85f1c4aff880788bb55bda56253118e036fe5b\\n+ # via contextual-orchestrator (pyproject.toml)\\n idna==3.18 \\\\\\n --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \\\\\\n --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848\\n- # via anyio\\n+ # via\\n+ # anyio\\n+ # requests\\n mako==1.3.12 \\\\\\n --hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \\\\\\n --hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a\\n@@ -216,11 +485,55 @@ markupsafe==3.0.3 \\\\\\n --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \\\\\\n --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50\\n # via mako\\n-psycopg[binary]==3.3.4 \\\\\\n+opentelemetry-api==1.44.0 \\\\\\n+ --hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \\\\\\n+ --hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef\\n+ # via\\n+ # contextual-orchestrator (pyproject.toml)\\n+ # opentelemetry-exporter-otlp-proto-http\\n+ # opentelemetry-sdk\\n+ # opentelemetry-semantic-conventions\\n+opentelemetry-exporter-otlp-proto-common==1.44.0 \\\\\\n+ --hash=sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694 \\\\\\n+ --hash=sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac\\n+ # via opentelemetry-exporter-otlp-proto-http\\n+opentelemetry-exporter-otlp-proto-http==1.44.0 \\\\\\n+ --hash=sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3 \\\\\\n+ --hash=sha256:c633d7270ad6b57cd4cfbe8b0007a9e2e7c0cb50bd6c50fe2a7b245f721a09d8\\n+ # via contextual-orchestrator (pyproject.toml)\\n+opentelemetry-proto==1.44.0 \\\\\\n+ --hash=sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56 \\\\\\n+ --hash=sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3\\n+ # via\\n+ # opentelemetry-exporter-otlp-proto-common\\n+ # opentelemetry-exporter-otlp-proto-http\\n+opentelemetry-sdk==1.44.0 \\\\\\n+ --hash=sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b \\\\\\n+ --hash=sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad\\n+ # via\\n+ # contextual-orchestrator (pyproject.toml)\\n+ # opentelemetry-exporter-otlp-proto-http\\n+opentelemetry-semantic-conventions==0.65b0 \\\\\\n+ --hash=sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb \\\\\\n+ --hash=sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60\\n+ # via opentelemetry-sdk\\n+protobuf==7.36.0 \\\\\\n+ --hash=sha256:1781cc1de61249b750848029bca452c0a8b7e990080316b9bbc2518b2117b488 \\\\\\n+ --hash=sha256:3297e60abdff301e5f74393d87f6cc59dacab5f024a89548a6e8de1d26576b16 \\\\\\n+ --hash=sha256:53374d53fc29a67f7dbbf0ade47d7526a0f0137bf0f9c90e48d8a60790ef748c \\\\\\n+ --hash=sha256:70f5ec8eb0da81a44360c0dc0beac99a0d78071d21956a7076bae8bd2051841b \\\\\\n+ --hash=sha256:7326fd717bdc419162a735938d89d4032332bcc3408804012b24ff3a37086071 \\\\\\n+ --hash=sha256:9103532dffd80c6fab7e50c65a31007680a06eb57537d437bb1b35812c138a37 \\\\\\n+ --hash=sha256:bf94a5917c71058262de683669bc0a797a7669d3de71f0b36d058e3194f47b44 \\\\\\n+ --hash=sha256:e8e09cb0d794c6687926fa558a8a6e72aa10edb997d5ca61da0765f12a3e00ea\\n+ # via\\n+ # googleapis-common-protos\\n+ # opentelemetry-proto\\n+psycopg==3.3.4 \\\\\\n --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \\\\\\n --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc\\n # via contextual-orchestrator (pyproject.toml)\\n-psycopg-binary==3.3.4 \\\\\\n+psycopg-binary==3.3.4 ; implementation_name != 'pypy' \\\\\\n --hash=sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070 \\\\\\n --hash=sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c \\\\\\n --hash=sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc \\\\\\n@@ -403,6 +716,14 @@ pydantic-core==2.46.4 \\\\\\n --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \\\\\\n --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae\\n # via pydantic\\n+requests==2.34.2 \\\\\\n+ --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \\\\\\n+ --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed\\n+ # via opentelemetry-exporter-otlp-proto-http\\n+sortedcontainers==2.4.0 \\\\\\n+ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \\\\\\n+ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0\\n+ # via hypothesis\\n sqlalchemy==2.0.51 \\\\\\n --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \\\\\\n --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \\\\\\n@@ -463,35 +784,94 @@ sqlalchemy==2.0.51 \\\\\\n --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \\\\\\n --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1\\n # via\\n- # alembic\\n # contextual-orchestrator (pyproject.toml)\\n+ # alembic\\n starlette==1.3.1 \\\\\\n --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \\\\\\n --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6\\n # via fastapi\\n+tomli==2.4.1 ; python_full_version < '3.11' \\\\\\n+ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \\\\\\n+ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \\\\\\n+ --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \\\\\\n+ --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \\\\\\n+ --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \\\\\\n+ --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \\\\\\n+ --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \\\\\\n+ --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \\\\\\n+ --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \\\\\\n+ --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \\\\\\n+ --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \\\\\\n+ --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \\\\\\n+ --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \\\\\\n+ --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \\\\\\n+ --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \\\\\\n+ --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \\\\\\n+ --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \\\\\\n+ --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \\\\\\n+ --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \\\\\\n+ --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \\\\\\n+ --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \\\\\\n+ --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \\\\\\n+ --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \\\\\\n+ --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \\\\\\n+ --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \\\\\\n+ --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \\\\\\n+ --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \\\\\\n+ --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \\\\\\n+ --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \\\\\\n+ --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \\\\\\n+ --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \\\\\\n+ --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \\\\\\n+ --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \\\\\\n+ --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \\\\\\n+ --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \\\\\\n+ --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \\\\\\n+ --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \\\\\\n+ --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \\\\\\n+ --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \\\\\\n+ --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \\\\\\n+ --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \\\\\\n+ --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \\\\\\n+ --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \\\\\\n+ --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \\\\\\n+ --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \\\\\\n+ --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \\\\\\n+ --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049\\n+ # via alembic\\n typing-extensions==4.15.0 \\\\\\n --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \\\\\\n --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548\\n # via\\n # alembic\\n # anyio\\n+ # exceptiongroup\\n # fastapi\\n+ # opentelemetry-api\\n+ # opentelemetry-exporter-otlp-proto-http\\n+ # opentelemetry-sdk\\n+ # opentelemetry-semantic-conventions\\n # psycopg\\n # pydantic\\n # pydantic-core\\n # sqlalchemy\\n # starlette\\n # typing-inspection\\n+ # uvicorn\\n typing-inspection==0.4.2 \\\\\\n --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \\\\\\n --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464\\n # via\\n # fastapi\\n # pydantic\\n-tzdata==2026.2 \\\\\\n- --hash=sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10 \\\\\\n- --hash=sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7\\n+tzdata==2026.3 ; sys_platform == 'win32' \\\\\\n+ --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \\\\\\n+ --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931\\n # via psycopg\\n+urllib3==2.7.0 \\\\\\n+ --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \\\\\\n+ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897\\n+ # via requests\\n uvicorn==0.49.0 \\\\\\n --hash=sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f \\\\\\n --hash=sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3\" }, { \"sha\": \"33706289864d0fa610ae03e7cdb83f48384cf81d\", \"filename\": \"tests/test_analytics_runtime.py\", \"status\": \"modified\", \"additions\": 21, \"deletions\": 0, \"changes\": 21, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_analytics_runtime.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_analytics_runtime.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_analytics_runtime.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -97,6 +97,27 @@ def test_analytics_snapshot_measures_runtime_kpis_and_guardrails() -> None:\\n assert guardrails[\\\"locale_key_parity\\\"][\\\"value_percent\\\"] == 100.0\\n \\n \\n+def test_analytics_snapshot_reads_runtime_collections_under_one_lock() -> None:\\n+ class CountingLock:\\n+ def __init__(self) -> None:\\n+ self.enter_count = 0\\n+\\n+ def __enter__(self):\\n+ self.enter_count += 1\\n+ return self\\n+\\n+ def __exit__(self, *_args: object) -> None:\\n+ return None\\n+\\n+ orchestrator = build()\\n+ lock = CountingLock()\\n+ orchestrator._workflow_run_lock = lock\\n+\\n+ orchestrator.analytics_snapshot()\\n+\\n+ assert lock.enter_count == 1\\n+\\n+\\n def test_analytics_endpoint_and_admin_console_use_source_backed_snapshot() -> None:\\n assert \\\"/api/v1/analytics_snapshots/latest\\\" in OPENAPI_SPEC[\\\"paths\\\"]\\n assert OPENAPI_SPEC[\\\"paths\\\"][\\\"/api/v1/analytics_snapshots/latest\\\"][\\\"get\\\"][\\\"operationId\\\"] == (\" }, { \"sha\": \"4bc90eb884c8e9d6988e5c3de95d18d161b6c3a2\", \"filename\": \"tests/test_api_contract.py\", \"status\": \"modified\", \"additions\": 27, \"deletions\": 0, \"changes\": 27, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_api_contract.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_api_contract.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_api_contract.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,34 @@ def test_openapi_documents_compatibility_front_door() -> None:\\n ]\\n \\n \\n+def test_openapi_documents_orchestrator_owned_embedding_model_selection() -> None:\\n+ embeddings_schema = OPENAPI_SPEC[\\\"paths\\\"][\\\"/v1/embeddings\\\"][\\\"post\\\"][\\\"requestBody\\\"][\\\"content\\\"][\\n+ \\\"application/json\\\"\\n+ ][\\\"schema\\\"]\\n+ batch_schema = OPENAPI_SPEC[\\\"paths\\\"][\\\"/v1/batch/embeddings\\\"][\\\"post\\\"][\\\"requestBody\\\"][\\\"content\\\"][\\n+ \\\"application/json\\\"\\n+ ][\\\"schema\\\"]\\n+\\n+ assert embeddings_schema[\\\"required\\\"] == [\\\"input\\\"]\\n+ assert \\\"model\\\" not in batch_schema.get(\\\"required\\\", [])\\n+ assert batch_schema[\\\"anyOf\\\"] == [\\n+ {\\\"required\\\": [\\\"input\\\"]},\\n+ {\\\"required\\\": [\\\"inputs\\\"]},\\n+ ]\\n+ assert \\\"Optional enabled embedding-capable pool model\\\" in embeddings_schema[\\\"properties\\\"][\\\"model\\\"][\\n+ \\\"description\\\"\\n+ ]\\n+\\n+\\n+def test_openapi_documents_unsupported_responses_controls() -> None:\\n+ responses = OPENAPI_SPEC[\\\"paths\\\"][\\\"/v1/responses\\\"][\\\"post\\\"][\\\"responses\\\"]\\n+\\n+ assert \\\"422\\\" in responses\\n+\\n+\\n if __name__ == \\\"__main__\\\": # pragma: no cover\\n test_rest_resource_paths_use_two_word_snake_case()\\n test_openapi_uses_resource_oriented_operation_ids()\\n+ test_openapi_documents_orchestrator_owned_embedding_model_selection()\\n+ test_openapi_documents_unsupported_responses_controls()\\n print(\\\"ok\\\")\" }, { \"sha\": \"e2aafa131862da0390ed13890731c589815e23c5\", \"filename\": \"tests/test_batch_optimizer.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_batch_optimizer.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_batch_optimizer.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_batch_optimizer.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -129,7 +129,7 @@ def test_batch_route_rejects_incomplete_or_empty_provider_results(kind: str) ->\\n \\n def test_batch_chat_rejects_incomplete_local_result_set() -> None:\\n client = ModelClient()\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"model-x\\\", base_url=\\\"local://127.0.0.1:1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"model-x\\\", base_url=\\\"local://127.0.0.1:1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n requests = {\\n \\\"task_0\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"one\\\"}],\\n \\\"task_1\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"two\\\"}],\" }, { \"sha\": \"9a84e5179abc2b0b65ad620ebc4b3d1ee28a5f44\", \"filename\": \"tests/test_batch_routing.py\", \"status\": \"modified\", \"additions\": 27, \"deletions\": 0, \"changes\": 27, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_batch_routing.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_batch_routing.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_batch_routing.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -19,6 +19,11 @@\\n )\\n from contextual_orchestrator.cost_ledger import PriceBook, PriceEntry # noqa: E402\\n from contextual_orchestrator.kv_config import InMemoryConfigStore # noqa: E402\\n+from contextual_orchestrator.telemetry import ( # noqa: E402\\n+ current_session_id,\\n+ reset_session_id,\\n+ set_session_id,\\n+)\\n \\n # ---------------------------------------------------------------------------\\n # Sync-vs-batch decision\\n@@ -122,6 +127,28 @@ def runner(messages, mode):\\n assert [item.custom_id for item in backend.retrieve(job)] == [\\\"a\\\", \\\"b\\\"]\\n \\n \\n+def test_local_backend_workers_inherit_session_id() -> None:\\n+ \\\"\\\"\\\"Batch workers retain the caller session for provider telemetry.\\\"\\\"\\\"\\n+ observed: list[str | None] = []\\n+\\n+ def runner(messages, mode):\\n+ observed.append(current_session_id())\\n+ return {\\\"answer\\\": messages[-1][\\\"content\\\"], \\\"mode\\\": mode}\\n+\\n+ backend = LocalBatchBackend(runner, max_concurrency=2)\\n+ requests = [\\n+ BatchRequest(messages=[{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"one\\\"}], custom_id=\\\"a\\\"),\\n+ BatchRequest(messages=[{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"two\\\"}], custom_id=\\\"b\\\"),\\n+ ]\\n+ token = set_session_id(\\\"post-session\\\")\\n+ try:\\n+ backend.submit(requests)\\n+ finally:\\n+ reset_session_id(token)\\n+\\n+ assert observed == [\\\"post-session\\\", \\\"post-session\\\"]\\n+\\n+\\n # ---------------------------------------------------------------------------\\n # pg-llm-batch backend (mocked async client mirroring BatchAPIClient)\\n # ---------------------------------------------------------------------------\" }, { \"sha\": \"30628947dad405f6c732f62f2713f886df95dc8c\", \"filename\": \"tests/test_bool_01_seed_str_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 3, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_bool_01_seed_str_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_bool_01_seed_str_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_bool_01_seed_str_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -144,15 +144,16 @@ def test_http_chat_parallel_tool_calls_one_requires_tools() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_seed_digit_string() -> None:\\n+def test_http_responses_rejects_unapplied_seed_digit_string() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n port,\\n \\\"/v1/responses\\\",\\n {\\\"model\\\": \\\"mock-planner\\\", \\\"input\\\": \\\"seed str\\\", \\\"seed\\\": \\\"42\\\"},\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -182,6 +183,6 @@ def test_http_completions_rejects_seed_digit_string_as_unsupported() -> None:\\n test_http_chat_accepts_stream_zero_as_false()\\n test_http_chat_accepts_parallel_tool_calls_zero()\\n test_http_chat_parallel_tool_calls_one_requires_tools()\\n- test_http_responses_accepts_seed_digit_string()\\n+ test_http_responses_rejects_unapplied_seed_digit_string()\\n test_http_completions_rejects_seed_digit_string_as_unsupported()\\n print(\\\"ok\\\")\" }, { \"sha\": \"2a151cb99bcda15642298b30dec9cea6cf916e0c\", \"filename\": \"tests/test_budget_enforcement.py\", \"status\": \"modified\", \"additions\": 69, \"deletions\": 0, \"changes\": 69, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_budget_enforcement.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_budget_enforcement.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_budget_enforcement.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -10,10 +10,13 @@\\n import json\\n from pathlib import Path\\n import sys\\n+import tempfile\\n import threading\\n import urllib.error\\n import urllib.request\\n \\n+import pytest\\n+\\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n@@ -75,6 +78,72 @@ def test_cost_budget_blocks() -> None:\\n assert raised\\n \\n \\n+def test_unpersisted_provider_usage_remains_in_the_budget_ledger() -> None:\\n+ agent = ModelAgent(\\\"general_agent\\\", \\\"priced-model\\\", tags=(\\\"reasoning\\\",))\\n+ orchestrator = TaskOrchestrator(\\n+ [agent],\\n+ price_per_million={\\\"priced-model\\\": 1_000_000.0},\\n+ budget_max_cost_usd=1.0,\\n+ )\\n+\\n+ orchestrator._record_in_flight_provider_usage(\\n+ agent,\\n+ {\\\"completion_tokens\\\": 1},\\n+ \\\"\\\",\\n+ )\\n+\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orchestrator._raise_if_spend_budget_exceeded()\\n+\\n+ assert orchestrator.budget_status()[\\\"spent_cost_usd\\\"] == 1.0\\n+\\n+\\n+def test_per_call_budget_gate_does_not_rescan_workflow_runs(monkeypatch) -> None:\\n+ \\\"\\\"\\\"Read the synchronized meter instead of rebuilding buyer spend analytics.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator([_agent()], budget_max_output_tokens=1)\\n+ orchestrator._record_in_flight_provider_usage(\\n+ _agent(),\\n+ {\\\"completion_tokens\\\": 1},\\n+ \\\"\\\",\\n+ )\\n+ monkeypatch.setattr(\\n+ orchestrator,\\n+ \\\"spend_analytics\\\",\\n+ lambda: pytest.fail(\\\"budget gate must use the incremental meter\\\"),\\n+ )\\n+\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orchestrator._raise_if_spend_budget_exceeded()\\n+\\n+\\n+def test_provider_budget_meter_survives_restart() -> None:\\n+ with tempfile.TemporaryDirectory() as directory:\\n+ state_db = str(Path(directory) / \\\"state.db\\\")\\n+ first = TaskOrchestrator(\\n+ [_agent()],\\n+ state_db=state_db,\\n+ budget_max_output_tokens=2,\\n+ )\\n+ first._record_in_flight_provider_usage(\\n+ _agent(),\\n+ {\\\"completion_tokens\\\": 2},\\n+ \\\"\\\",\\n+ )\\n+ first.close()\\n+\\n+ second = TaskOrchestrator(\\n+ [_agent()],\\n+ state_db=state_db,\\n+ budget_max_output_tokens=2,\\n+ )\\n+ try:\\n+ assert second.budget_status()[\\\"spent_output_tokens\\\"] == 2\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ second._raise_if_spend_budget_exceeded()\\n+ finally:\\n+ second.close()\\n+\\n+\\n def test_http_over_budget_returns_429() -> None:\\n token = \\\"budget_token\\\"\\n orchestrator = TaskOrchestrator([_agent()], budget_max_output_tokens=1)\" }, { \"sha\": \"d9e72b6ca5738f5f3f4005cd3ca767157f610097\", \"filename\": \"tests/test_chat_capability_unknown_identifiers.py\", \"status\": \"added\", \"additions\": 47, \"deletions\": 0, \"changes\": 47, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_capability_unknown_identifiers.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_capability_unknown_identifiers.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_capability_unknown_identifiers.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,47 @@\\n+\\\"\\\"\\\"Regressions for conservative treatment of unknown model identifiers.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import sys\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator.chat_capability import ( # noqa: E402\\n+ is_chat_compatible_model_id,\\n+ is_general_chat_agent_model_id,\\n+)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"vendor/vanguard-7b\\\",\\n+ \\\"vendor/vanguard-instruct\\\",\\n+ ],\\n+)\\n+def test_unknown_names_that_merely_end_with_guard_remain_eligible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Do not fabricate a policy-classifier capability from an unrelated word suffix.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id)\\n+ assert is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"meta-llama/llama-guard-4-12b\\\",\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-topic-control\\\",\\n+ \\\"google/shieldgemma-2b-it\\\",\\n+ ],\\n+)\\n+def test_explicit_policy_classifier_markers_remain_role_ineligible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Keep exact guard, safety, and NemoGuard markers out of general synthesis roles.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id)\\n+ assert not is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"7d5fd8ea1f6e8356edd4ebe0ccb4513e5d49942c\", \"filename\": \"tests/test_chat_developer_multimodal_content_http_honesty.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_developer_multimodal_content_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_developer_multimodal_content_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_developer_multimodal_content_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"vision\\\"))]\\n )\\n \\n \" }, { \"sha\": \"4b7e93b91692f6bd6f6d090aec6bface96efd2f1\", \"filename\": \"tests/test_chat_model_capability_isolation.py\", \"status\": \"added\", \"additions\": 391, \"deletions\": 0, \"changes\": 391, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_model_capability_isolation.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_model_capability_isolation.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_model_capability_isolation.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,391 @@\\n+\\\"\\\"\\\"Regression coverage for isolating non-chat models from chat agent discovery.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import json\\n+import sys\\n+from pathlib import Path\\n+from unittest.mock import patch\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator.chat_capability import ( # noqa: E402\\n+ is_chat_compatible_model_id,\\n+)\\n+from contextual_orchestrator.credentials import ( # noqa: E402\\n+ InMemoryCredentialBackend,\\n+ register_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.cost_ledger import PriceBook, PriceEntry # noqa: E402\\n+from contextual_orchestrator.kv_config import InMemoryConfigStore # noqa: E402\\n+from contextual_orchestrator.model_discovery import ( # noqa: E402\\n+ DiscoveredModel,\\n+ ProviderModelSource,\\n+ agent_from_discovered,\\n+ discover_provider_models,\\n+ refresh_price_book,\\n+ select_cheapest_discovered_agent,\\n+ select_top_n_cheapest_discovered_agents,\\n+)\\n+from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n+\\n+\\n+class _Response:\\n+ \\\"\\\"\\\"Small context-managed HTTP response used by the offline regression.\\\"\\\"\\\"\\n+\\n+ def __init__(self, payload: dict[str, object]) -> None:\\n+ self._body = json.dumps(payload).encode(\\\"utf-8\\\")\\n+\\n+ def __enter__(self) -> \\\"_Response\\\":\\n+ return self\\n+\\n+ def __exit__(self, *_args: object) -> bool:\\n+ return False\\n+\\n+ def read(self, _size: int = -1) -> bytes:\\n+ return self._body\\n+\\n+\\n+@pytest.fixture(autouse=True)\\n+def _fresh_credential_backend():\\n+ \\\"\\\"\\\"Keep the provider credential registry isolated between tests.\\\"\\\"\\\"\\n+ set_backend(InMemoryCredentialBackend())\\n+ try:\\n+ yield\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+def _model(model_id: str, *, priced: bool = False) -> DiscoveredModel:\\n+ \\\"\\\"\\\"Build one synthetic discovered model for capability-boundary tests.\\\"\\\"\\\"\\n+ return DiscoveredModel(\\n+ provider_name=\\\"enterprise_gateway\\\",\\n+ model_id=model_id,\\n+ credential_name=\\\"GATEWAY_API_KEY\\\",\\n+ chat_base_url=\\\"https://gateway.example.test/v1\\\",\\n+ auth_scheme=\\\"Bearer\\\",\\n+ prompt_price_per_1k=1.0 if priced else None,\\n+ completion_price_per_1k=1.0 if priced else None,\\n+ )\\n+\\n+\\n+def _agent(\\n+ agent_id: str,\\n+ model_id: str,\\n+ *,\\n+ priority: int = 0,\\n+ tags: tuple[str, ...] = (\\\"writing\\\",),\\n+) -> ModelAgent:\\n+ \\\"\\\"\\\"Build one mock-backed runtime agent for selection-path regressions.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ id=agent_id,\\n+ model=model_id,\\n+ base_url=\\\"mock://local\\\",\\n+ priority=priority,\\n+ tags=tags,\\n+ )\\n+\\n+\\n+def test_embedding_deployments_never_enter_chat_agent_discovery() -> None:\\n+ \\\"\\\"\\\"Exclude the exact Azure embedding deployment seen in synthesis alerts.\\\"\\\"\\\"\\n+ register_credential(\\\"GATEWAY_API_KEY\\\", \\\"gateway-secret\\\")\\n+ source = ProviderModelSource(\\n+ provider_name=\\\"enterprise_gateway\\\",\\n+ credential_name=\\\"GATEWAY_API_KEY\\\",\\n+ list_url=\\\"https://gateway.example.test/v1/models\\\",\\n+ chat_base_url=\\\"https://gateway.example.test/v1\\\",\\n+ )\\n+ payload = {\\n+ \\\"data\\\": [\\n+ {\\\"id\\\": \\\"azure/text-embedding-3-large\\\"},\\n+ {\\\"id\\\": \\\"text_embedding_3_large\\\"},\\n+ {\\\"id\\\": \\\"BAAI/bge-m3\\\"},\\n+ {\\\"id\\\": \\\"openai/whisper-1\\\"},\\n+ {\\\"id\\\": \\\"gpt-4o-mini-transcribe\\\"},\\n+ {\\\"id\\\": \\\"text-moderation-latest\\\"},\\n+ {\\\"id\\\": \\\"company/reranker-v2\\\"},\\n+ {\\\"id\\\": \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\"},\\n+ {\\\"id\\\": \\\"gpt-audio\\\"},\\n+ {\\\"id\\\": \\\"gpt-5.2\\\"},\\n+ {\\\"id\\\": \\\"qwen/qwen3-235b-a22b-instruct\\\"},\\n+ ]\\n+ }\\n+\\n+ with patch(\\n+ \\\"contextual_orchestrator.model_discovery._fetch_json\\\",\\n+ return_value=payload,\\n+ ):\\n+ discovered = discover_provider_models(source)\\n+\\n+ assert [model.model_id for model in discovered] == [\\n+ \\\"gpt-audio\\\",\\n+ \\\"gpt-5.2\\\",\\n+ \\\"qwen/qwen3-235b-a22b-instruct\\\",\\n+ ]\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"model_id\\\", \\\"expected\\\"),\\n+ [\\n+ (None, False),\\n+ (\\\"\\\", False),\\n+ (\\\"---\\\", False),\\n+ (\\\"vendor/embeddingv2\\\", False),\\n+ (\\\"vendor/reranking-v2\\\", False),\\n+ (\\\"vendor/transcriber-v2\\\", False),\\n+ (\\\"gpt-5.2\\\", True),\\n+ (\\\"qwen/qwen3-instruct\\\", True),\\n+ ],\\n+)\\n+def test_chat_compatibility_normalizes_identifiers(\\n+ model_id: object, expected: bool\\n+) -> None:\\n+ \\\"\\\"\\\"Normalize provider prefixes and separators without guessing chat features.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id) is expected # type: ignore[arg-type]\\n+\\n+\\n+def test_bytez_chat_catalog_still_rejects_non_chat_identifiers() -> None:\\n+ \\\"\\\"\\\"Apply the same boundary even when a provider accepts a chat task filter.\\\"\\\"\\\"\\n+ register_credential(\\\"BYTEZ_API_KEY\\\", \\\"bytez-secret\\\")\\n+ source = ProviderModelSource(\\n+ provider_name=\\\"bytez\\\",\\n+ credential_name=\\\"BYTEZ_API_KEY\\\",\\n+ list_url=\\\"https://api.bytez.com/models/v2/list/models\\\",\\n+ chat_base_url=\\\"https://api.bytez.com/models/v2/openai/v1\\\",\\n+ auth_scheme=\\\"Key\\\",\\n+ style=\\\"bytez\\\",\\n+ task_filter=\\\"chat\\\",\\n+ )\\n+ payload = {\\n+ \\\"output\\\": [\\n+ {\\\"modelId\\\": \\\"vendor/embeddingv2\\\"},\\n+ {\\\"modelId\\\": \\\"vendor/chat-instruct\\\"},\\n+ ]\\n+ }\\n+\\n+ with patch(\\n+ \\\"contextual_orchestrator.model_discovery._fetch_json\\\",\\n+ return_value=payload,\\n+ ):\\n+ discovered = discover_provider_models(source)\\n+\\n+ assert [model.model_id for model in discovered] == [\\\"vendor/chat-instruct\\\"]\\n+\\n+\\n+def test_non_chat_discovery_cannot_be_converted_to_agent() -> None:\\n+ \\\"\\\"\\\"Keep manually constructed discovery rows from bypassing the parser filter.\\\"\\\"\\\"\\n+ with pytest.raises(ValueError, match=\\\"general chat agent\\\"):\\n+ agent_from_discovered(_model(\\\"azure/text-embedding-3-large\\\"))\\n+\\n+\\n+def test_non_chat_discovery_is_not_priced_or_selected_for_chat() -> None:\\n+ \\\"\\\"\\\"Keep price routing from reintroducing an incompatible endpoint model.\\\"\\\"\\\"\\n+ price_book = PriceBook(InMemoryConfigStore())\\n+ embedding_model = _model(\\\"azure/text-embedding-3-large\\\", priced=True)\\n+ chat_model = _model(\\\"gpt-5.2\\\", priced=True)\\n+ price_book.set_price(PriceEntry(\\\"enterprise_gateway\\\", \\\"gpt-5.2\\\", 1.0, 1.0))\\n+\\n+ assert refresh_price_book([embedding_model, chat_model], price_book) == 1\\n+ assert price_book.get_price(\\n+ \\\"enterprise_gateway\\\", \\\"azure/text-embedding-3-large\\\"\\n+ ) is None\\n+ assert select_cheapest_discovered_agent([embedding_model], price_book) is None\\n+ assert select_top_n_cheapest_discovered_agents(\\n+ [embedding_model], price_book, 1\\n+ ) == []\\n+\\n+\\n+def test_stale_embedding_agent_cannot_win_synthesizer_selection() -> None:\\n+ \\\"\\\"\\\"Exclude an already-persisted embedding row even when it has high priority.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ priority=10_000,\\n+ )\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent])\\n+\\n+ assert orchestrator._select_agent(\\\"Produce the final answer.\\\", \\\"synthesizer\\\") is chat_agent\\n+\\n+\\n+def test_all_non_chat_agents_fail_before_synthesis() -> None:\\n+ \\\"\\\"\\\"Fail closed when a stale pool contains no chat-compatible worker.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator(\\n+ [_agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")]\\n+ )\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"chat-compatible\\\"):\\n+ orchestrator._select_agent(\\\"Produce the final answer.\\\", \\\"synthesizer\\\")\\n+\\n+\\n+def test_generated_plan_reselects_non_chat_agent_assignment() -> None:\\n+ \\\"\\\"\\\"Do not trust a generated plan that names a stale embedding agent directly.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ priority=10_000,\\n+ )\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent])\\n+ raw_plan = json.dumps(\\n+ {\\n+ \\\"steps\\\": [\\n+ {\\n+ \\\"id\\\": 0,\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"chat_agent\\\",\\n+ \\\"subtask\\\": \\\"Execute the task.\\\",\\n+ \\\"access\\\": [],\\n+ },\\n+ {\\n+ \\\"id\\\": 1,\\n+ \\\"role\\\": \\\"synthesizer\\\",\\n+ \\\"agent_id\\\": \\\"embedding_agent\\\",\\n+ \\\"subtask\\\": \\\"Produce the final answer.\\\",\\n+ \\\"access\\\": [0],\\n+ },\\n+ ]\\n+ }\\n+ )\\n+\\n+ steps = orchestrator._parse_workflow_plan(raw_plan)\\n+\\n+ assert steps[-1].agent_id == \\\"chat_agent\\\"\\n+\\n+\\n+def test_failover_candidates_exclude_stale_embedding_agents() -> None:\\n+ \\\"\\\"\\\"Keep cross-agent retry from falling through to an incompatible endpoint.\\\"\\\"\\\"\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ embedding_agent = _agent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ priority=10_000,\\n+ )\\n+ orchestrator = TaskOrchestrator([chat_agent, embedding_agent])\\n+\\n+ candidates = orchestrator._failover_candidates(\\n+ chat_agent,\\n+ \\\"Produce the final answer.\\\",\\n+ \\\"synthesizer\\\",\\n+ )\\n+\\n+ assert candidates == [chat_agent]\\n+\\n+\\n+def test_invoke_fails_clearly_when_no_general_chat_agent_remains() -> None:\\n+ \\\"\\\"\\\"Report the role boundary instead of claiming that zero candidates failed.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent])\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"no chat-compatible agent available\\\"):\\n+ orchestrator._invoke(\\n+ embedding_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Produce the final answer.\\\"}],\\n+ text=\\\"Produce the final answer.\\\",\\n+ role=\\\"worker\\\",\\n+ )\\n+\\n+\\n+def test_model_client_rejects_non_chat_model_before_mock_or_network_call() -> None:\\n+ \\\"\\\"\\\"Keep the provider boundary fail-closed even when selection is bypassed.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ client.chat(\\n+ embedding_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Produce the final answer.\\\"}],\\n+ )\\n+\\n+\\n+def test_non_chat_primary_fails_over_only_to_chat_agents() -> None:\\n+ \\\"\\\"\\\"Drop an incompatible primary while retaining a compatible fallback.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\")\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent])\\n+\\n+ candidates = orchestrator._failover_candidates(\\n+ embedding_agent,\\n+ \\\"Produce the final answer.\\\",\\n+ \\\"synthesizer\\\",\\n+ )\\n+\\n+ assert candidates == [chat_agent]\\n+\\n+\\n+def test_streaming_client_rejects_non_chat_model_before_transport() -> None:\\n+ \\\"\\\"\\\"Apply the same endpoint boundary to streaming chat requests.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ next(\\n+ client.stream_chat(\\n+ embedding_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Produce the final answer.\\\"}],\\n+ )\\n+ )\\n+\\n+\\n+def test_probe_reports_non_chat_model_without_provider_transport(monkeypatch) -> None:\\n+ \\\"\\\"\\\"Readiness must fail closed with a stable code before network access.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ monkeypatch.setattr(\\n+ client,\\n+ \\\"_validate_provider\\\",\\n+ lambda _agent: (_ for _ in ()).throw(AssertionError(\\\"transport reached\\\")),\\n+ )\\n+\\n+ assert client.probe(embedding_agent)[\\\"failure_code\\\"] == \\\"non_chat_model\\\"\\n+\\n+\\n+def test_generated_planner_inventory_excludes_non_chat_agents() -> None:\\n+ \\\"\\\"\\\"Do not advertise stale endpoint-incompatible agents to the planner.\\\"\\\"\\\"\\n+ embedding_agent = _agent(\\\"embedding_agent\\\", \\\"azure/text-embedding-3-large\\\")\\n+ chat_agent = _agent(\\\"chat_agent\\\", \\\"gpt-5.2\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))\\n+\\n+ class PlannerClient:\\n+ def __init__(self) -> None:\\n+ self.system_prompt = \\\"\\\"\\n+\\n+ def chat(self, _agent, messages, **_kwargs):\\n+ self.system_prompt = messages[0][\\\"content\\\"]\\n+ return json.dumps(\\n+ {\\n+ \\\"steps\\\": [\\n+ {\\n+ \\\"id\\\": 0,\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"chat_agent\\\",\\n+ \\\"subtask\\\": \\\"Execute the task.\\\",\\n+ \\\"access\\\": [],\\n+ },\\n+ {\\n+ \\\"id\\\": 1,\\n+ \\\"role\\\": \\\"synthesizer\\\",\\n+ \\\"agent_id\\\": \\\"chat_agent\\\",\\n+ \\\"subtask\\\": \\\"Produce the answer.\\\",\\n+ \\\"access\\\": [0],\\n+ },\\n+ ]\\n+ }\\n+ )\\n+\\n+ client = PlannerClient()\\n+ orchestrator = TaskOrchestrator([embedding_agent, chat_agent], client=client)\\n+\\n+ steps = orchestrator._plan_generated(\\\"Produce the final answer.\\\")\\n+\\n+ assert steps[-1].agent_id == \\\"chat_agent\\\"\\n+ assert \\\"embedding_agent\\\" not in client.system_prompt\\n+ assert \\\"azure/text-embedding-3-large\\\" not in client.system_prompt\\n+ assert \\\"chat_agent\\\" in client.system_prompt\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"9760da0f429d04c4ebc3735d0a23efc49b8787cc\", \"filename\": \"tests/test_chat_parallel_tool_calls_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 13, \"changes\": 17, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_parallel_tool_calls_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_parallel_tool_calls_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_parallel_tool_calls_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -117,8 +117,8 @@ def test_http_chat_parallel_tool_calls_non_boolean_fail_closed() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_parallel_tool_calls_true_with_tools_passthrough() -> None:\\n- \\\"\\\"\\\"With tools, parallel_tool_calls triggers single-agent passthrough path.\\\"\\\"\\\"\\n+def test_http_chat_parallel_tool_calls_true_rejects_single_agent_fallback() -> None:\\n+ \\\"\\\"\\\"With tools, the gateway does not silently downgrade to one agent.\\\"\\\"\\\"\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -130,17 +130,8 @@ def test_http_chat_parallel_tool_calls_true_with_tools_passthrough() -> None:\\n \\\"parallel_tool_calls\\\": True,\\n },\\n )\\n- # Mock passthrough returns chat-shaped body\\n- assert status == 200, body\\n- assert \\\"choices\\\" in body or \\\"id\\\" in body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n-\\n-\\n-if __name__ == \\\"__main__\\\":\\n- test_http_chat_parallel_tool_calls_false_without_tools_ok()\\n- test_http_chat_parallel_tool_calls_true_without_tools_fail_closed()\\n- test_http_chat_parallel_tool_calls_non_boolean_fail_closed()\\n- test_http_chat_parallel_tool_calls_true_with_tools_passthrough()\\n- print(\\\"ok\\\")\" }, { \"sha\": \"927c213243303d7343bc0b9feb2d0ab58f3ea9c5\", \"filename\": \"tests/test_chat_passthrough_capability_isolation.py\", \"status\": \"added\", \"additions\": 127, \"deletions\": 0, \"changes\": 127, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_passthrough_capability_isolation.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_passthrough_capability_isolation.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_passthrough_capability_isolation.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,127 @@\\n+\\\"\\\"\\\"Regression tests for chat-capability checks on passthrough and batch paths.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import sys\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n+\\n+\\n+def _embedding_agent() -> ModelAgent:\\n+ \\\"\\\"\\\"Build the stale embedding agent from the production incident.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ base_url=\\\"mock://local\\\",\\n+ )\\n+\\n+\\n+def _chat_agent() -> ModelAgent:\\n+ \\\"\\\"\\\"Build one compatible fallback for explicit-model passthrough tests.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ \\\"general_chat_agent\\\",\\n+ \\\"gpt-5.2\\\",\\n+ base_url=\\\"mock://local\\\",\\n+ tags=(\\\"writing\\\",),\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"endpoint\\\",\\n+ [\\n+ \\\"chat/completions\\\",\\n+ \\\"/v1/chat/completions\\\",\\n+ \\\"completions\\\",\\n+ \\\"/v1/completions\\\",\\n+ \\\"responses\\\",\\n+ \\\"/v1/responses\\\",\\n+ ],\\n+)\\n+def test_proxy_send_rejects_embedding_before_mock_or_network_transport(endpoint: str) -> None:\\n+ \\\"\\\"\\\"Keep raw OpenAI passthrough from bypassing the chat transport invariant.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ client.proxy_send(\\n+ _embedding_agent(),\\n+ endpoint,\\n+ {\\n+ \\\"model\\\": \\\"azure/text-embedding-3-large\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Return JSON.\\\"}],\\n+ \\\"input\\\": \\\"Return JSON.\\\",\\n+ },\\n+ )\\n+\\n+\\n+def test_explicit_embedding_model_cannot_bypass_through_structured_passthrough() -> None:\\n+ \\\"\\\"\\\"Reject an explicitly requested stale embedding agent before raw proxy transport.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator([_embedding_agent(), _chat_agent()])\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"azure/text-embedding-3-large\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Return JSON.\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+\\n+def test_explicit_embedding_model_cannot_bypass_through_responses_passthrough() -> None:\\n+ \\\"\\\"\\\"Apply the same transport contract to the Responses passthrough path.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator([_embedding_agent(), _chat_agent()])\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"azure/text-embedding-3-large\\\",\\n+ \\\"input\\\": \\\"Return JSON.\\\",\\n+ },\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+\\n+def test_batch_chat_rejects_embedding_before_mock_or_network_transport() -> None:\\n+ \\\"\\\"\\\"Prevent direct batch callers from submitting embedding models as chat jobs.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+\\n+ with pytest.raises(ValueError, match=\\\"chat-compatible\\\"):\\n+ client.batch_chat(\\n+ _embedding_agent(),\\n+ {\\\"task_0\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Return JSON.\\\"}]},\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"gpt-audio\\\",\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ ],\\n+)\\n+def test_chat_served_specialized_models_remain_valid_passthrough_transports(model_id: str) -> None:\\n+ \\\"\\\"\\\"Do not turn ordinary-role exclusion into a false transport rejection.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ agent = ModelAgent(\\\"specialized_chat_agent\\\", model_id, base_url=\\\"mock://local\\\")\\n+\\n+ response = client.proxy_send(\\n+ agent,\\n+ \\\"chat/completions\\\",\\n+ {\\n+ \\\"model\\\": model_id,\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Classify this.\\\"}],\\n+ },\\n+ )\\n+\\n+ assert response[\\\"object\\\"] == \\\"chat.completion\\\"\\n+ assert response[\\\"model\\\"] == model_id\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"7bfe5199e4c3c3788edf1a021039266572cbde5f\", \"filename\": \"tests/test_chat_reasoning_effort_http_honesty.py\", \"status\": \"modified\", \"additions\": 17, \"deletions\": 0, \"changes\": 17, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_reasoning_effort_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_reasoning_effort_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_reasoning_effort_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -68,6 +68,23 @@ def test_http_chat_accepts_reasoning_effort_known_levels() -> None:\\n thread.join(timeout=5)\\n \\n \\n+def test_http_chat_accepts_orchestrator_owned_reasoning_effort_auto() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"think automatically\\\"}],\\n+ \\\"reasoning_effort\\\": \\\"auto\\\",\\n+ },\\n+ )\\n+ assert status == 200, body\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n def test_http_chat_still_rejects_unknown_reasoning_effort() -> None:\\n server, thread, port = _server()\\n try:\" }, { \"sha\": \"598b8118774bd35ab3bad5102c8b9c74d1ce8e31\", \"filename\": \"tests/test_chat_response_format_json_schema_omit_real_http_honesty.py\", \"status\": \"modified\", \"additions\": 85, \"deletions\": 0, \"changes\": 85, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_response_format_json_schema_omit_real_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_response_format_json_schema_omit_real_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_response_format_json_schema_omit_real_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -9,6 +9,8 @@\\n from pathlib import Path\\n import sys\\n \\n+import pytest\\n+\\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n@@ -45,6 +47,24 @@ def _post(port: int, payload: dict) -> tuple[int, dict]:\\n return exc.code, json.loads(exc.read().decode(\\\"utf-8\\\"))\\n \\n \\n+def _post_responses(port: int, payload: dict) -> tuple[int, dict]:\\n+ request = urllib.request.Request(\\n+ f\\\"http://127.0.0.1:{port}/v1/responses\\\",\\n+ data=json.dumps(payload).encode(\\\"utf-8\\\"),\\n+ headers={\\n+ \\\"content-type\\\": \\\"application/json\\\",\\n+ \\\"authorization\\\": f\\\"Bearer {_TEST_AUTH_TOKEN}\\\",\\n+ \\\"connection\\\": \\\"close\\\",\\n+ },\\n+ method=\\\"POST\\\",\\n+ )\\n+ try:\\n+ with urllib.request.urlopen(request, timeout=10) as response:\\n+ return response.status, json.loads(response.read().decode(\\\"utf-8\\\"))\\n+ except urllib.error.HTTPError as exc:\\n+ return exc.code, json.loads(exc.read().decode(\\\"utf-8\\\"))\\n+\\n+\\n def _server():\\n server = build_server(build(), port=0, security=SecurityConfig(auth_token=_TEST_AUTH_TOKEN))\\n thread = threading.Thread(target=server.serve_forever, daemon=True)\\n@@ -141,6 +161,71 @@ def test_http_chat_omits_json_schema_null_optionals_on_response_format() -> None\\n thread.join(timeout=5)\\n \\n \\n+@pytest.mark.parametrize(\\n+ \\\"response_format\\\",\\n+ [\\n+ {\\\"type\\\": \\\"json_object\\\"},\\n+ {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\n+ \\\"name\\\": \\\"receipt_line\\\",\\n+ \\\"schema\\\": {\\n+ \\\"type\\\": \\\"object\\\",\\n+ \\\"properties\\\": {\\\"amount\\\": {\\\"type\\\": \\\"number\\\"}},\\n+ },\\n+ },\\n+ },\\n+ ],\\n+)\\n+def test_http_chat_structured_output_keeps_multi_agent_workflow(response_format: dict) -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"structured workflow\\\"}],\\n+ \\\"response_format\\\": response_format,\\n+ },\\n+ )\\n+ assert status == 200, body\\n+ assert body[\\\"orchestration\\\"][\\\"mode\\\"] == \\\"conduct\\\"\\n+ assert body[\\\"orchestration\\\"][\\\"channel\\\"] == \\\"sync\\\"\\n+ assert body[\\\"orchestration\\\"][\\\"workflow_run_id\\\"]\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_responses_json_schema_keeps_multi_agent_workflow() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post_responses(\\n+ port,\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"structured responses workflow\\\",\\n+ \\\"text\\\": {\\n+ \\\"format\\\": {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"name\\\": \\\"receipt_line\\\",\\n+ \\\"schema\\\": {\\n+ \\\"type\\\": \\\"object\\\",\\n+ \\\"properties\\\": {\\\"amount\\\": {\\\"type\\\": \\\"number\\\"}},\\n+ },\\n+ }\\n+ },\\n+ },\\n+ )\\n+ assert status == 200, body\\n+ assert body[\\\"orchestration\\\"][\\\"mode\\\"] == \\\"conduct\\\"\\n+ assert body[\\\"orchestration\\\"][\\\"channel\\\"] == \\\"sync\\\"\\n+ assert body[\\\"orchestration\\\"][\\\"workflow_run_id\\\"]\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n def test_http_chat_rejects_unknown_json_schema_nested_key() -> None:\\n server, thread, port = _server()\\n try:\" }, { \"sha\": \"aea93ad4c792c8adf6f533ed304d44eb13e55221\", \"filename\": \"tests/test_chat_tool_choice_functions_http_honesty.py\", \"status\": \"modified\", \"additions\": 5, \"deletions\": 5, \"changes\": 10, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_tool_choice_functions_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_tool_choice_functions_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_tool_choice_functions_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,4 +1,4 @@\\n-\\\"\\\"\\\"Chat tools honesty: functions/function_call rejected; tool_choice required/named requires tools; auto/none without tools are no-ops.\\\"\\\"\\\"\\n+\\\"\\\"\\\"Chat tools honesty: unsupported legacy and multi-agent tool surfaces fail closed.\\\"\\\"\\\"\\n \\n from __future__ import annotations\\n \\n@@ -143,7 +143,7 @@ def test_http_chat_accepts_tool_choice_auto_without_tools_as_omit() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_tools_with_tool_choice_passthrough_ok() -> None:\\n+def test_http_chat_rejects_tools_with_tool_choice_passthrough() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -155,8 +155,8 @@ def test_http_chat_tools_with_tool_choice_passthrough_ok() -> None:\\n \\\"tool_choice\\\": \\\"auto\\\",\\n },\\n )\\n- assert status == 200, body\\n- assert \\\"choices\\\" in body or \\\"id\\\" in body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -167,5 +167,5 @@ def test_http_chat_tools_with_tool_choice_passthrough_ok() -> None:\\n test_http_chat_accepts_function_call_auto_without_functions_as_omit()\\n test_http_chat_rejects_function_call_named_without_tools_migration()\\n test_http_chat_accepts_tool_choice_auto_without_tools_as_omit()\\n- test_http_chat_tools_with_tool_choice_passthrough_ok()\\n+ test_http_chat_rejects_tools_with_tool_choice_passthrough()\\n print(\\\"ok\\\")\" }, { \"sha\": \"70cffc0120984b751c8896b09bed1a1b9ba9bef8\", \"filename\": \"tests/test_chat_tools_passthrough_controls_http_honesty.py\", \"status\": \"modified\", \"additions\": 44, \"deletions\": 10, \"changes\": 54, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_tools_passthrough_controls_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_tools_passthrough_controls_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_tools_passthrough_controls_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -50,15 +50,18 @@ def build() -> TaskOrchestrator:\\n )\\n \\n \\n-def _post(port: int, payload: dict) -> tuple[int, dict]:\\n+def _post(port: int, payload: dict, *, tool_loop: bool = False) -> tuple[int, dict]:\\n+ headers = {\\n+ \\\"content-type\\\": \\\"application/json\\\",\\n+ \\\"authorization\\\": f\\\"Bearer {_TEST_AUTH_TOKEN}\\\",\\n+ \\\"connection\\\": \\\"close\\\",\\n+ }\\n+ if tool_loop:\\n+ headers[\\\"x-contextual-orchestrator-tool-loop\\\"] = \\\"v1\\\"\\n request = urllib.request.Request(\\n f\\\"http://127.0.0.1:{port}/v1/chat/completions\\\",\\n data=json.dumps(payload).encode(\\\"utf-8\\\"),\\n- headers={\\n- \\\"content-type\\\": \\\"application/json\\\",\\n- \\\"authorization\\\": f\\\"Bearer {_TEST_AUTH_TOKEN}\\\",\\n- \\\"connection\\\": \\\"close\\\",\\n- },\\n+ headers=headers,\\n method=\\\"POST\\\",\\n )\\n try:\\n@@ -123,7 +126,8 @@ def test_http_tools_passthrough_rejects_unsupported_seed_store_stop_n() -> None:\\n assert status == 400, (payload, body)\\n assert code in json.dumps(body), (code, body)\\n status, body = _post(port, _base(service_tier=\\\"flex\\\"))\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -146,15 +150,45 @@ def test_http_tools_passthrough_rejects_invalid_user_and_stream_options() -> Non\\n thread.join(timeout=5)\\n \\n \\n-def test_http_tools_passthrough_accepts_coerced_sampling() -> None:\\n+def test_http_tools_rejects_valid_tool_request_without_explicit_loop_header() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n port,\\n _base(temperature=\\\"0.7\\\", top_p=\\\"0.95\\\", max_tokens=\\\"64\\\"),\\n )\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_tools_preserves_valid_tool_request_with_explicit_loop_header() -> None:\\n+ \\\"\\\"\\\"The opt-in contract preserves provider tool state for OpenCode.\\\"\\\"\\\"\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ _base(temperature=\\\"0.7\\\", top_p=\\\"0.95\\\", max_tokens=\\\"64\\\"),\\n+ tool_loop=True,\\n+ )\\n assert status == 200, body\\n- assert body.get(\\\"object\\\") == \\\"chat.completion\\\" or \\\"choices\\\" in body\\n+ assert body[\\\"echo\\\"][\\\"temperature\\\"] == 0.7\\n+ assert body[\\\"echo\\\"][\\\"top_p\\\"] == 0.95\\n+ assert body[\\\"echo\\\"][\\\"max_tokens\\\"] == 64\\n+ assert body[\\\"echo\\\"][\\\"tools\\\"] == _TOOLS\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_tool_loop_rejects_streaming() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(port, _base(stream=True), tool_loop=True)\\n+ assert status == 400, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"invalid_stream\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -185,6 +219,6 @@ def test_http_response_format_passthrough_rejects_seed() -> None:\\n test_http_tools_passthrough_rejects_invalid_temperature()\\n test_http_tools_passthrough_rejects_unsupported_seed_store_stop_n()\\n test_http_tools_passthrough_rejects_invalid_user_and_stream_options()\\n- test_http_tools_passthrough_accepts_coerced_sampling()\\n+ test_unit_sampling_writeback_coerced_numbers()\\n test_http_response_format_passthrough_rejects_seed()\\n print(\\\"ok\\\")\" }, { \"sha\": \"8c054a0446b27bbccb2da4085fe39ec47c8425bd\", \"filename\": \"tests/test_chat_tools_shape_http_honesty.py\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 15, \"changes\": 18, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_tools_shape_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_tools_shape_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_tools_shape_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,7 @@ def _base_messages():\\n return [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"use a tool\\\"}]\\n \\n \\n-def test_http_chat_accepts_valid_function_tools() -> None:\\n+def test_http_chat_rejects_valid_function_tools_without_single_agent_fallback() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -75,8 +75,8 @@ def test_http_chat_accepts_valid_function_tools() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n- assert \\\"choices\\\" in body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -231,15 +231,3 @@ def test_http_chat_accepts_tools_omitted() -> None:\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n-\\n-\\n-if __name__ == \\\"__main__\\\":\\n- test_http_chat_accepts_valid_function_tools()\\n- test_http_chat_rejects_empty_tools_array()\\n- test_http_chat_rejects_tool_type_not_function()\\n- test_http_chat_rejects_tool_missing_function_name()\\n- test_http_chat_rejects_tool_function_name_bad_charset()\\n- test_http_chat_rejects_tool_sibling_unknown_fields()\\n- test_http_chat_rejects_parameters_non_object()\\n- test_http_chat_accepts_tools_omitted()\\n- print(\\\"ok\\\")\" }, { \"sha\": \"17f5b7277e09d9d5e07e8380f7adadfea4477e8e\", \"filename\": \"tests/test_chat_transport_role_separation.py\", \"status\": \"added\", \"additions\": 84, \"deletions\": 0, \"changes\": 84, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_transport_role_separation.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_chat_transport_role_separation.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_chat_transport_role_separation.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,84 @@\\n+\\\"\\\"\\\"Regression coverage for chat transport versus ordinary agent-role eligibility.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import sys\\n+from pathlib import Path\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator.chat_capability import ( # noqa: E402\\n+ is_chat_compatible_model_id,\\n+ is_general_chat_agent_model_id,\\n+)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"gpt-audio\\\",\\n+ \\\"gpt-audio-mini\\\",\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-content-safety\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-topic-control\\\",\\n+ ],\\n+)\\n+def test_chat_served_models_remain_transport_compatible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Do not pre-reject models that provider contracts serve through chat completions.\\\"\\\"\\\"\\n+ assert is_chat_compatible_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"nvidia/llama-3.1-nemotron-safety-guard-8b-v3\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-content-safety\\\",\\n+ \\\"nvidia/llama-3.1-nemoguard-8b-topic-control\\\",\\n+ ],\\n+)\\n+def test_policy_classifiers_do_not_enter_general_agent_roles(model_id: str) -> None:\\n+ \\\"\\\"\\\"Keep chat-served policy classifiers out of ordinary synthesis roles.\\\"\\\"\\\"\\n+ assert not is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"gpt-audio\\\",\\n+ \\\"gpt-audio-mini\\\",\\n+ \\\"gpt-5.2\\\",\\n+ \\\"qwen/qwen3-235b-a22b-instruct\\\",\\n+ ],\\n+)\\n+def test_general_generation_models_remain_agent_eligible(model_id: str) -> None:\\n+ \\\"\\\"\\\"Preserve chat generation models for ordinary agent selection.\\\"\\\"\\\"\\n+ assert is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"model_id\\\",\\n+ [\\n+ \\\"azure/text-embedding-3-large\\\",\\n+ \\\"text_embedding_3_large\\\",\\n+ \\\"company/reranker-v2\\\",\\n+ \\\"gpt-4o-mini-transcribe\\\",\\n+ \\\"omni-moderation-latest\\\",\\n+ \\\"gpt-image-1\\\",\\n+ \\\"dall-e-3\\\",\\n+ \\\"openai/clip-vit-large-patch14\\\",\\n+ \\\"google/siglip-so400m-patch14-384\\\",\\n+ \\\"sora-2\\\",\\n+ \\\"gpt-realtime\\\",\\n+ \\\"tts-1\\\",\\n+ ],\\n+)\\n+def test_endpoint_only_models_fail_both_boundaries(model_id: str) -> None:\\n+ \\\"\\\"\\\"Reject endpoint-only model families before transport or ordinary role routing.\\\"\\\"\\\"\\n+ assert not is_chat_compatible_model_id(model_id)\\n+ assert not is_general_chat_agent_model_id(model_id)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"4be612a9bb2f1e4f68da779ae9b2cce807bc3810\", \"filename\": \"tests/test_cli_auth.py\", \"status\": \"modified\", \"additions\": 62, \"deletions\": 6, \"changes\": 68, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_cli_auth.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_cli_auth.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_cli_auth.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -8,9 +8,11 @@\\n from pathlib import Path\\n from unittest.mock import patch\\n \\n+import pytest\\n+\\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n-from contextual_orchestrator.__main__ import _resolve_auth_token, main\\n+from contextual_orchestrator.__main__ import _request_read_timeout, _resolve_auth_token, main\\n from contextual_orchestrator.credentials import (\\n InMemoryCredentialBackend,\\n set_backend,\\n@@ -75,22 +77,33 @@ def test_key_only_split_tokens_select_split_mode() -> None:\\n main()\\n security = serve.call_args.kwargs[\\\"security\\\"]\\n assert security.auth_token == \\\"\\\"\\n- assert security.admin_token == \\\"admin-from-kv\\\"\\n- assert security.inference_token == \\\"inference-from-kv\\\"\\n+ assert security.admin_token == \\\"admin-from-kv\\\" # noqa: S105\\n+ assert security.inference_token == \\\"inference-from-kv\\\" # noqa: S105\\n finally:\\n set_backend(None)\\n \\n \\n+def test_main_accepts_explicit_argv_without_mutating_process_arguments() -> None:\\n+ original_argv = sys.argv[:]\\n+ with patch(\\\"contextual_orchestrator.__main__.serve\\\") as serve:\\n+ main([\\\"--serve\\\", \\\"--auth-token\\\", \\\"argv-token\\\"])\\n+\\n+ assert sys.argv == original_argv\\n+ security = serve.call_args.kwargs[\\\"security\\\"]\\n+ assert security.auth_token == \\\"argv-token\\\"\\n+\\n+\\n def test_invalid_local_provider_options_fail_at_parser_boundary() -> None:\\n invalid_options = (\\n ([\\\"--local-concurrency\\\", \\\"0\\\"], \\\"positive integer\\\"),\\n ([\\\"--local-concurrency\\\", \\\"-1\\\"], \\\"positive integer\\\"),\\n ([\\\"--local-concurrency\\\", \\\"65\\\"], \\\"1..64\\\"),\\n ([\\\"--max-concurrent-runs\\\", \\\"0\\\"], \\\"positive integer\\\"),\\n ([\\\"--max-concurrent-runs\\\", \\\"65\\\"], \\\"1..64\\\"),\\n- ([\\\"--chat-template-args\\\", \\\"[]\\\"], \\\"JSON object\\\"),\\n- ([\\\"--chat-template-args\\\", \\\"null\\\"], \\\"JSON object\\\"),\\n- ([\\\"--chat-template-args\\\", \\\"{\\\"], \\\"valid JSON object\\\"),\\n+ ([\\\"--request-read-timeout-seconds\\\", \\\"0\\\"], \\\"0.1..120\\\"),\\n+ ([\\\"--request-read-timeout-seconds\\\", \\\"121\\\"], \\\"0.1..120\\\"),\\n+ ([\\\"--request-read-timeout-seconds\\\", \\\"inf\\\"], \\\"0.1..120\\\"),\\n+ ([\\\"--request-read-timeout-seconds\\\", \\\"not-a-number\\\"], \\\"0.1..120\\\"),\\n )\\n \\n for options, expected_message in invalid_options:\\n@@ -111,6 +124,26 @@ def test_invalid_local_provider_options_fail_at_parser_boundary() -> None:\\n else: # pragma: no cover\\n raise AssertionError(\\\"invalid local provider option was accepted\\\")\\n \\n+ assert _request_read_timeout(\\\"0.1\\\") == 0.1\\n+\\n+\\n+def test_serve_rejects_empty_resolved_auth_configuration() -> None:\\n+ stderr = StringIO()\\n+ with (\\n+ patch.object(sys, \\\"argv\\\", [\\\"contextual-orchestrator\\\", \\\"--serve\\\"]),\\n+ patch.object(sys, \\\"stderr\\\", stderr),\\n+ patch(\\\"contextual_orchestrator.__main__._resolve_auth_token\\\", return_value=\\\"\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.load_agents\\\", return_value=[]),\\n+ patch(\\\"contextual_orchestrator.__main__.ModelClient\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.TaskOrchestrator\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.serve\\\") as serve,\\n+ ):\\n+ with pytest.raises(SystemExit) as captured:\\n+ main()\\n+ assert captured.value.code == 2\\n+ assert \\\"requires a KV auth credential\\\" in stderr.getvalue()\\n+ serve.assert_not_called()\\n+\\n \\n def test_server_concurrency_is_explicit_and_bounded() -> None:\\n with (\\n@@ -131,6 +164,7 @@ def test_server_concurrency_is_explicit_and_bounded() -> None:\\n patch(\\\"contextual_orchestrator.__main__.load_agents\\\", return_value=[]),\\n patch(\\\"contextual_orchestrator.__main__.ModelClient\\\"),\\n patch(\\\"contextual_orchestrator.__main__.TaskOrchestrator\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.CostRoutingCoordinator\\\"),\\n patch(\\\"contextual_orchestrator.__main__.serve\\\") as serve,\\n ):\\n main()\\n@@ -148,12 +182,33 @@ def test_sampling_temperature_uses_descriptive_name_and_legacy_alias() -> None:\\n patch(\\\"contextual_orchestrator.__main__.load_agents\\\", return_value=[]),\\n patch(\\\"contextual_orchestrator.__main__.ModelClient\\\") as model_client,\\n patch(\\\"contextual_orchestrator.__main__.TaskOrchestrator\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.CostRoutingCoordinator\\\"),\\n patch(\\\"contextual_orchestrator.__main__.serve\\\"),\\n ):\\n main()\\n assert model_client.call_args.kwargs[\\\"temperature\\\"] == 0.7\\n \\n \\n+def test_sampling_temperature_is_omitted_by_default() -> None:\\n+ \\\"\\\"\\\"Startup must not invent a sampling value unsupported by the selected model.\\\"\\\"\\\"\\n+\\n+ with (\\n+ patch.object(\\n+ sys,\\n+ \\\"argv\\\",\\n+ [\\\"contextual-orchestrator\\\", \\\"--serve\\\", \\\"--auth-token\\\", \\\"token\\\"],\\n+ ),\\n+ patch(\\\"contextual_orchestrator.__main__.load_agents\\\", return_value=[]),\\n+ patch(\\\"contextual_orchestrator.__main__.ModelClient\\\") as model_client,\\n+ patch(\\\"contextual_orchestrator.__main__.TaskOrchestrator\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.CostRoutingCoordinator\\\"),\\n+ patch(\\\"contextual_orchestrator.__main__.serve\\\"),\\n+ ):\\n+ main()\\n+\\n+ assert model_client.call_args.kwargs[\\\"temperature\\\"] is None\\n+\\n+\\n def test_fast_mlsirm_preflight_reports_missing_transitive_dependency() -> None:\\n stderr = StringIO()\\n real_import = __import__\\n@@ -201,4 +256,5 @@ def test_fast_mlsirm_preflight_accepts_the_versioned_contract() -> None:\\n test_key_only_split_tokens_select_split_mode()\\n test_invalid_local_provider_options_fail_at_parser_boundary()\\n test_sampling_temperature_uses_descriptive_name_and_legacy_alias()\\n+ test_sampling_temperature_is_omitted_by_default()\\n print(\\\"ok\\\")\" }, { \"sha\": \"0a08e5e13cbe3b585f10ae29cf0986f3e488a863\", \"filename\": \"tests/test_content_part_aliases_web_search_omit_http_honesty.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_content_part_aliases_web_search_omit_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_content_part_aliases_web_search_omit_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_content_part_aliases_web_search_omit_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"vision\\\"))]\\n )\\n \\n \" }, { \"sha\": \"990470cc7492425027388fef765006d65d0ef8d4\", \"filename\": \"tests/test_content_part_type_casefold_http_honesty.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_content_part_type_casefold_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_content_part_type_casefold_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_content_part_type_casefold_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"vision\\\"))]\\n )\\n \\n \" }, { \"sha\": \"04f8dd1941594d80e29c0732be956ef4ac5c6442\", \"filename\": \"tests/test_cost_review_server.py\", \"status\": \"modified\", \"additions\": 19, \"deletions\": 1, \"changes\": 20, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_cost_review_server.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_cost_review_server.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_cost_review_server.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -73,7 +73,11 @@ def test_chat_completion_reports_real_usage_and_records_cost() -> None:\\n assert body[\\\"usage\\\"][\\\"total_tokens\\\"] > 0\\n assert body[\\\"orchestration\\\"][\\\"channel\\\"] == \\\"sync\\\"\\n \\n- status, report = _request(\\\"GET\\\", f\\\"{base}/api/v1/cost_reports/rollup?dimension=team\\\", token)\\n+ status, report = _request(\\n+ \\\"GET\\\",\\n+ f\\\"{base}/api/v1/cost_reports/rollup?dimension=team&start=0&end=9999999999\\\",\\n+ token,\\n+ )\\n assert status == 200\\n values = {item[\\\"dimension_value\\\"]: item for item in report[\\\"items\\\"]}\\n assert \\\"alpha\\\" in values\\n@@ -132,6 +136,20 @@ def test_batch_routing_jobs_endpoint_submits_multiple_requests() -> None:\\n server.shutdown()\\n \\n \\n+def test_unknown_batch_results_return_not_found() -> None:\\n+ server, port, token = _serve()\\n+ try:\\n+ status, body = _request(\\n+ \\\"POST\\\",\\n+ f\\\"http://127.0.0.1:{port}/api/v1/batch_routing_jobs/missing_job/results\\\",\\n+ token,\\n+ )\\n+ finally:\\n+ server.shutdown()\\n+ assert status == 404\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"batch_job_not_found\\\"\\n+\\n+\\n def test_cost_report_rejects_unknown_dimension() -> None:\\n server, port, token = _serve()\\n base = f\\\"http://127.0.0.1:{port}\\\"\" }, { \"sha\": \"a9c9fee460d039c0102fa1a1e158f4de82826fbe\", \"filename\": \"tests/test_cost_router.py\", \"status\": \"modified\", \"additions\": 198, \"deletions\": 0, \"changes\": 198, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_cost_router.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_cost_router.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_cost_router.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -4,6 +4,9 @@\\n \\n import sys\\n from pathlib import Path\\n+from unittest.mock import patch\\n+\\n+import pytest\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n@@ -113,6 +116,201 @@ def test_batch_completion_records_on_retrieve() -> None:\\n assert records[0][\\\"team_name\\\"] == \\\"beta\\\"\\n \\n \\n+def test_structured_output_forces_sync_when_batch_is_selected() -> None:\\n+ coordinator = _coordinator()\\n+ result = coordinator.complete(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"return one JSON object\\\"}],\\n+ hints={\\\"channel\\\": \\\"batch\\\"},\\n+ response_format={\\\"type\\\": \\\"json_object\\\"},\\n+ )\\n+ assert result[\\\"channel\\\"] == \\\"sync\\\"\\n+ assert result[\\\"routing_reason\\\"].endswith(\\\"structured_output_forced_sync\\\")\\n+\\n+\\n+def test_provider_native_structured_output_keeps_cost_and_lineage() -> None:\\n+ coordinator = _coordinator()\\n+ provider_request = {\\n+ \\\"model\\\": \\\"mock-a\\\",\\n+ \\\"input\\\": \\\"return one JSON object\\\",\\n+ \\\"text\\\": {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}},\\n+ }\\n+ messages = [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"return one JSON object\\\"}]\\n+ provider_response = {\\n+ \\\"object\\\": \\\"response\\\",\\n+ \\\"output_text\\\": \\\"{}\\\",\\n+ \\\"output\\\": [],\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 7, \\\"output_tokens\\\": 11, \\\"total_tokens\\\": 18},\\n+ }\\n+\\n+ with patch.object(\\n+ coordinator.orchestrator.client,\\n+ \\\"proxy_send\\\",\\n+ return_value=provider_response,\\n+ ):\\n+ result = coordinator.complete(\\n+ messages,\\n+ hints={\\\"channel\\\": \\\"batch\\\"},\\n+ response_format={\\\"type\\\": \\\"json_object\\\"},\\n+ provider_request=provider_request,\\n+ provider_endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ assert result[\\\"channel\\\"] == \\\"sync\\\"\\n+ assert result[\\\"answer\\\"] == \\\"{}\\\"\\n+ assert result[\\\"provider_response\\\"][\\\"orchestration\\\"][\\\"channel\\\"] == \\\"sync\\\"\\n+ assert result[\\\"provider_response\\\"][\\\"orchestration\\\"][\\\"usage_record_id\\\"] == result[\\n+ \\\"usage_record_id\\\"\\n+ ]\\n+ assert result[\\\"provider_response\\\"][\\\"orchestration\\\"][\\\"usage_record_ids\\\"] == [\\n+ result[\\\"usage_record_id\\\"]\\n+ ]\\n+ record = coordinator.ledger.records()[0]\\n+ assert record[\\\"prompt_tokens\\\"] == 7\\n+ assert record[\\\"completion_tokens\\\"] == 11\\n+\\n+\\n+def test_provider_native_workflow_records_each_metered_provider_call() -> None:\\n+ coordinator = _coordinator()\\n+ provider_response = {\\n+ \\\"object\\\": \\\"response\\\",\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 5, \\\"output_tokens\\\": 7, \\\"total_tokens\\\": 12},\\n+ \\\"orchestration\\\": {\\\"workflow_run_id\\\": \\\"run_metered\\\"},\\n+ }\\n+ workflow_run = {\\n+ \\\"workflow_run_id\\\": \\\"run_metered\\\",\\n+ \\\"mode\\\": \\\"conduct\\\",\\n+ \\\"answer\\\": \\\"{}\\\",\\n+ \\\"trace\\\": [\\n+ {\\n+ \\\"agent_id\\\": \\\"mock_worker\\\",\\n+ \\\"output\\\": \\\"evidence\\\",\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 2, \\\"completion_tokens\\\": 3},\\n+ },\\n+ {\\n+ \\\"agent_id\\\": \\\"mock_worker\\\",\\n+ \\\"subtask\\\": \\\"Provider-facing structured synthesis\\\",\\n+ \\\"output\\\": \\\"{}\\\",\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 5, \\\"output_tokens\\\": 7},\\n+ },\\n+ ],\\n+ \\\"verification\\\": {\\n+ \\\"judge_agent_id\\\": \\\"mock_worker\\\",\\n+ \\\"judge_usage\\\": {\\\"prompt_tokens\\\": 1, \\\"completion_tokens\\\": 2},\\n+ },\\n+ }\\n+\\n+ with patch.object(\\n+ coordinator.orchestrator,\\n+ \\\"proxy_completion\\\",\\n+ return_value=provider_response,\\n+ ), patch.object(\\n+ coordinator.orchestrator,\\n+ \\\"get_workflow_run\\\",\\n+ return_value=workflow_run,\\n+ ):\\n+ result = coordinator.complete(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"return JSON\\\"}],\\n+ response_format={\\\"type\\\": \\\"json_object\\\"},\\n+ provider_request={\\\"input\\\": \\\"return JSON\\\"},\\n+ provider_endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ records = coordinator.ledger.records()\\n+ assert [(row[\\\"prompt_tokens\\\"], row[\\\"completion_tokens\\\"]) for row in records] == [\\n+ (2, 3),\\n+ (1, 2),\\n+ (5, 7),\\n+ ]\\n+ assert result[\\\"usage\\\"] == {\\n+ \\\"prompt_tokens\\\": 8,\\n+ \\\"completion_tokens\\\": 12,\\n+ \\\"total_tokens\\\": 20,\\n+ }\\n+ assert result[\\\"cost\\\"] == {\\\"cost_amount\\\": 0.032, \\\"currency_code\\\": \\\"USD\\\"}\\n+ assert result[\\\"usage_record_ids\\\"] == [row[\\\"usage_record_id\\\"] for row in records]\\n+ assert result[\\\"usage_record_id\\\"] == records[-1][\\\"usage_record_id\\\"]\\n+ assert result[\\\"unmetered_provider_call_count\\\"] == 0\\n+\\n+\\n+def test_provider_native_workflow_does_not_sum_mixed_currencies() -> None:\\n+ coordinator = _coordinator()\\n+ judge_agent = ModelAgent(\\n+ id=\\\"judge_worker\\\",\\n+ model=\\\"mock-judge\\\",\\n+ base_url=\\\"mock://judge\\\",\\n+ provider_name=\\\"mock\\\",\\n+ )\\n+ coordinator.orchestrator.candidates.append(judge_agent)\\n+ coordinator.orchestrator.agents.append(judge_agent)\\n+ coordinator.price_book.set_price(\\n+ PriceEntry(\\n+ \\\"mock\\\",\\n+ \\\"mock-judge\\\",\\n+ prompt_price_per_1k=1.0,\\n+ completion_price_per_1k=1.0,\\n+ currency_code=\\\"KRW\\\",\\n+ )\\n+ )\\n+ provider_response = {\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 5, \\\"output_tokens\\\": 7},\\n+ \\\"orchestration\\\": {\\\"workflow_run_id\\\": \\\"run_mixed_currency\\\"},\\n+ }\\n+ workflow_run = {\\n+ \\\"workflow_run_id\\\": \\\"run_mixed_currency\\\",\\n+ \\\"mode\\\": \\\"conduct\\\",\\n+ \\\"answer\\\": \\\"{}\\\",\\n+ \\\"trace\\\": [\\n+ {\\n+ \\\"agent_id\\\": \\\"mock_worker\\\",\\n+ \\\"subtask\\\": \\\"Provider-facing structured synthesis\\\",\\n+ \\\"output\\\": \\\"{}\\\",\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 5, \\\"output_tokens\\\": 7},\\n+ }\\n+ ],\\n+ \\\"verification\\\": {\\n+ \\\"judge_agent_id\\\": \\\"judge_worker\\\",\\n+ \\\"judge_usage\\\": {\\\"prompt_tokens\\\": 1, \\\"completion_tokens\\\": 2},\\n+ },\\n+ }\\n+\\n+ with patch.object(\\n+ coordinator.orchestrator, \\\"proxy_completion\\\", return_value=provider_response\\n+ ), patch.object(\\n+ coordinator.orchestrator, \\\"get_workflow_run\\\", return_value=workflow_run\\n+ ):\\n+ result = coordinator.complete(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"return JSON\\\"}],\\n+ response_format={\\\"type\\\": \\\"json_object\\\"},\\n+ provider_request={\\\"input\\\": \\\"return JSON\\\"},\\n+ provider_endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ assert result[\\\"cost\\\"] == {\\\"cost_amount\\\": None, \\\"currency_code\\\": \\\"MIXED\\\"}\\n+ assert [row[\\\"currency_code\\\"] for row in coordinator.ledger.records()] == [\\\"KRW\\\", \\\"USD\\\"]\\n+\\n+\\n+def test_provider_native_completion_rejects_unknown_endpoint() -> None:\\n+ coordinator = _coordinator()\\n+\\n+ with pytest.raises(ValueError, match=\\\"provider_endpoint must be\\\"):\\n+ coordinator.complete(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"hello\\\"}],\\n+ provider_request={\\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"hello\\\"}]},\\n+ provider_endpoint=\\\"images\\\",\\n+ )\\n+\\n+\\n+def test_provider_native_completion_requires_workflow_lineage() -> None:\\n+ coordinator = _coordinator()\\n+\\n+ with patch.object(coordinator.orchestrator, \\\"proxy_completion\\\", return_value={}):\\n+ with pytest.raises(RuntimeError, match=\\\"omitted orchestration lineage\\\"):\\n+ coordinator.complete(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"hello\\\"}],\\n+ provider_request={\\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"hello\\\"}]},\\n+ )\\n+\\n+\\n def test_default_local_batch_backend_reuses_orchestrator_concurrency() -> None:\\n class _Client:\\n local_concurrency = 3\" }, { \"sha\": \"e2183032ad5cc3cf1551f92184a9fa6e3a4de381\", \"filename\": \"tests/test_digit_n_bool01_echo_logprobs_http_honesty.py\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 3, \"changes\": 6, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_digit_n_bool01_echo_logprobs_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_digit_n_bool01_echo_logprobs_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_digit_n_bool01_echo_logprobs_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -277,9 +277,9 @@ def test_http_responses_logprobs_zero_one() -> None:\\n \\\"logprobs\\\": 1,\\n },\\n )\\n- # responses allows logprobs=true shape (boolean); 1 coerces to true and is accepted\\n- # unless top_logprobs missing - logprobs true alone is ok for responses\\n- assert status == 200, body\\n+ # The value is valid OpenAI shape but cannot be applied by conduct.\\n+ assert status == 422, body\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\" }, { \"sha\": \"c7c6bdce1f33385fac830ba429c6fd6e8205428d\", \"filename\": \"tests/test_discover_models_cli.py\", \"status\": \"modified\", \"additions\": 25, \"deletions\": 5, \"changes\": 30, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_discover_models_cli.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_discover_models_cli.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_discover_models_cli.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,6 +3,8 @@\\n from __future__ import annotations\\n \\n import json\\n+from contextlib import contextmanager\\n+import socket\\n import sys\\n import urllib.parse\\n from io import StringIO\\n@@ -17,6 +19,7 @@\\n register_credential,\\n set_backend,\\n )\\n+from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n \\n \\n class _Response:\\n@@ -29,8 +32,25 @@ def __enter__(self):\\n def __exit__(self, *_args):\\n return False\\n \\n- def read(self) -> bytes:\\n- return self._body\\n+ def read(self, _size: int = -1) -> bytes:\\n+ return self._body if _size < 0 else self._body[:_size]\\n+\\n+\\n+@contextmanager\\n+def _patched_provider_transport(urlopen):\\n+ \\\"\\\"\\\"Keep CLI discovery tests offline while exercising the validated transport seam.\\\"\\\"\\\"\\n+ def open_provider(request, _destination=None, *, timeout=None):\\n+ return urlopen(request, timeout=timeout)\\n+\\n+ with (\\n+ patch.object(\\n+ ModelClient,\\n+ \\\"_validate_provider\\\",\\n+ return_value=(socket.AF_INET, (\\\"93.184.216.34\\\", 443)),\\n+ ),\\n+ patch.object(ModelClient, \\\"_open_provider\\\", side_effect=open_provider),\\n+ ):\\n+ yield\\n \\n \\n def test_discover_models_with_no_credentials_reports_zero_and_succeeds() -> None:\\n@@ -68,7 +88,7 @@ def urlopen(request, timeout=None):\\n with (\\n patch.object(sys, \\\"argv\\\", [\\\"contextual-orchestrator\\\", \\\"discover-models\\\"]),\\n patch.object(sys, \\\"stdout\\\", stdout),\\n- patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen),\\n+ _patched_provider_transport(urlopen),\\n ):\\n main()\\n finally:\\n@@ -96,7 +116,7 @@ def urlopen(request, timeout=None):\\n with (\\n patch.object(sys, \\\"argv\\\", [\\\"contextual-orchestrator\\\", \\\"discover-models\\\", \\\"--agents-db\\\", db_path]),\\n patch.object(sys, \\\"stdout\\\", stdout),\\n- patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen),\\n+ _patched_provider_transport(urlopen),\\n ):\\n main()\\n finally:\\n@@ -151,7 +171,7 @@ def urlopen(request, timeout=None):\\n [\\\"contextual-orchestrator\\\", \\\"discover-models\\\", \\\"--agents-db\\\", db_path, \\\"--enable-cheapest\\\", \\\"1\\\"],\\n ),\\n patch.object(sys, \\\"stdout\\\", stdout),\\n- patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen),\\n+ _patched_provider_transport(urlopen),\\n ):\\n main()\\n finally:\" }, { \"sha\": \"1f55cc420d03bbec7f15c93badbaa0d6d749984c\", \"filename\": \"tests/test_embeddings_encoding_format_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 4, \"changes\": 8, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_embeddings_encoding_format_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_embeddings_encoding_format_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_embeddings_encoding_format_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,23 +3,23 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n-from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n-from contextual_orchestrator.server import SecurityConfig, build_server # noqa: E402\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+from contextual_orchestrator.server import SecurityConfig, build_server\\n \\n _TEST_AUTH_TOKEN = \\\"embeddings_encoding_format_http_honesty_token\\\" # noqa: S105\\n \\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"embedding\\\"))]\\n )\\n \\n \" }, { \"sha\": \"20be91ae1937f95c350a6b3eb481168e6c34769a\", \"filename\": \"tests/test_embeddings_model_pool_http_honesty.py\", \"status\": \"modified\", \"additions\": 111, \"deletions\": 3, \"changes\": 114, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_embeddings_model_pool_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_embeddings_model_pool_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_embeddings_model_pool_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,16 +3,16 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n-from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n-from contextual_orchestrator.server import SecurityConfig, build_server # noqa: E402\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+from contextual_orchestrator.server import SecurityConfig, build_server\\n \\n _TEST_AUTH_TOKEN = \\\"embeddings_model_pool_http_honesty_token\\\" # noqa: S105\\n \\n@@ -48,6 +48,61 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n+def _server_without_embedding():\\n+ server = build_server(\\n+ TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\",))]),\\n+ port=0,\\n+ security=SecurityConfig(auth_token=_TEST_AUTH_TOKEN),\\n+ )\\n+ thread = threading.Thread(target=server.serve_forever, daemon=True)\\n+ thread.start()\\n+ return server, thread, server.server_address[1]\\n+\\n+\\n+def test_select_capability_agent_normalizes_and_rejects_empty_capability() -> None:\\n+ \\\"\\\"\\\"Capability selection normalizes names and rejects an empty capability.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator(\\n+ [ModelAgent(\\\"embedding_agent\\\", \\\"text-embedding-3-large\\\", tags=(\\\"embedding\\\",))]\\n+ )\\n+ assert orchestrator.select_capability_agent(\\\" EMBEDDING \\\").id == \\\"embedding_agent\\\"\\n+ try:\\n+ orchestrator.select_capability_agent(\\\" \\\")\\n+ except ValueError as exc:\\n+ assert str(exc) == \\\"capability must be a non-empty string\\\"\\n+ else:\\n+ raise AssertionError(\\\"empty capability must fail closed\\\")\\n+\\n+\\n+def test_select_capability_agent_skips_disabled_and_excluded_agents() -> None:\\n+ \\\"\\\"\\\"Capability selection skips disabled and provider-excluded candidates.\\\"\\\"\\\"\\n+ orchestrator = TaskOrchestrator(\\n+ [\\n+ ModelAgent(\\\"disabled_embedding\\\", \\\"disabled\\\", tags=(\\\"embedding\\\",), disabled=True),\\n+ ModelAgent(\\n+ \\\"excluded_embedding\\\",\\n+ \\\"excluded\\\",\\n+ tags=(\\\"embedding\\\",),\\n+ provider_exclusions=(\\\"embedding\\\",),\\n+ ),\\n+ ModelAgent(\\\"eligible_embedding\\\", \\\"eligible\\\", tags=(\\\"embedding\\\",)),\\n+ ]\\n+ )\\n+ assert orchestrator.select_capability_agent(\\\"embedding\\\").id == \\\"eligible_embedding\\\"\\n+\\n+ unavailable = TaskOrchestrator(\\n+ [\\n+ ModelAgent(\\\"disabled_embedding\\\", \\\"disabled\\\", tags=(\\\"embedding\\\",), disabled=True),\\n+ ModelAgent(\\\"reasoning_agent\\\", \\\"reasoning\\\", tags=(\\\"reasoning\\\",)),\\n+ ]\\n+ )\\n+ try:\\n+ unavailable.select_capability_agent(\\\"embedding\\\")\\n+ except RuntimeError as exc:\\n+ assert str(exc) == \\\"no enabled agent available for capability=embedding\\\"\\n+ else:\\n+ raise AssertionError(\\\"an unavailable capability must fail closed\\\")\\n+\\n+\\n def test_http_embeddings_rejects_model_outside_agent_pool() -> None:\\n server, thread, port = _server()\\n try:\\n@@ -83,6 +138,44 @@ def test_http_embeddings_accepts_model_in_agent_pool() -> None:\\n thread.join(timeout=5)\\n \\n \\n+def test_http_embeddings_auto_selects_enabled_embedding_agent() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(port, \\\"/v1/embeddings\\\", {\\\"input\\\": \\\"invoice search chunk\\\"})\\n+ assert status == 200, body\\n+ assert body.get(\\\"model\\\") == \\\"mock-planner\\\"\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_embeddings_auto_selection_fails_when_capability_is_missing() -> None:\\n+ server, thread, port = _server_without_embedding()\\n+ try:\\n+ status, body = _post(port, \\\"/v1/embeddings\\\", {\\\"input\\\": \\\"invoice search chunk\\\"})\\n+ assert status == 503, body\\n+ assert \\\"embedding_unavailable\\\" in json.dumps(body)\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n+def test_http_embeddings_reject_virtual_orchestrator_model() -> None:\\n+ \\\"\\\"\\\"An explicit virtual chat model cannot bypass the embedding pool gate.\\\"\\\"\\\"\\n+ server, thread, port = _server()\\n+ try:\\n+ for path, payload in (\\n+ (\\\"/v1/embeddings\\\", {\\\"model\\\": \\\"contextual-orchestrator\\\", \\\"input\\\": \\\"invoice search chunk\\\"}),\\n+ (\\\"/v1/batch/embeddings\\\", {\\\"model\\\": \\\"contextual-orchestrator\\\", \\\"inputs\\\": [\\\"alpha\\\", \\\"beta\\\"]}),\\n+ ):\\n+ status, body = _post(port, path, payload)\\n+ assert status == 400, (path, body)\\n+ assert \\\"invalid_model\\\" in json.dumps(body)\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n def test_http_batch_embeddings_rejects_model_outside_agent_pool() -> None:\\n server, thread, port = _server()\\n try:\\n@@ -116,9 +209,24 @@ def test_http_batch_embeddings_accepts_model_in_agent_pool() -> None:\\n thread.join(timeout=5)\\n \\n \\n+def test_http_batch_embeddings_auto_selects_enabled_embedding_agent() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(port, \\\"/v1/batch/embeddings\\\", {\\\"inputs\\\": [\\\"alpha\\\", \\\"beta\\\"]})\\n+ assert status == 200, body\\n+ assert body.get(\\\"model\\\") == \\\"mock-planner\\\"\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n if __name__ == \\\"__main__\\\":\\n test_http_embeddings_rejects_model_outside_agent_pool()\\n test_http_embeddings_accepts_model_in_agent_pool()\\n+ test_http_embeddings_auto_selects_enabled_embedding_agent()\\n+ test_http_embeddings_auto_selection_fails_when_capability_is_missing()\\n+ test_http_embeddings_reject_virtual_orchestrator_model()\\n test_http_batch_embeddings_rejects_model_outside_agent_pool()\\n test_http_batch_embeddings_accepts_model_in_agent_pool()\\n+ test_http_batch_embeddings_auto_selects_enabled_embedding_agent()\\n print(\\\"ok\\\")\" }, { \"sha\": \"dc994ec6112f8fdd6cf13ebfa4d892a426be03cc\", \"filename\": \"tests/test_encoding_stream_logprobs_http_honesty.py\", \"status\": \"modified\", \"additions\": 2, \"deletions\": 2, \"changes\": 4, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_encoding_stream_logprobs_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_encoding_stream_logprobs_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_encoding_stream_logprobs_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,11 +3,11 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"embedding\\\"))]\\n )\\n \\n \" }, { \"sha\": \"6df84664587f6dee684d1ddc874d37d67fc8f8c0\", \"filename\": \"tests/test_functions_null_max_tool_calls_null_http_honesty.py\", \"status\": \"modified\", \"additions\": 0, \"deletions\": 2, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_functions_null_max_tool_calls_null_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_functions_null_max_tool_calls_null_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_functions_null_max_tool_calls_null_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -101,9 +101,7 @@ def test_http_responses_accepts_null_max_tool_calls_and_functions() -> None:\\n \\\"model\\\": \\\"mock-planner\\\",\\n \\\"input\\\": \\\"max tool null\\\",\\n \\\"max_tool_calls\\\": None,\\n- \\\"functions\\\": None,\\n \\\"function_call\\\": None,\\n- \\\"functions\\\": [],\\n },\\n )\\n assert status == 200, body\" }, { \"sha\": \"aaaf8219096d6b4099b2f8052d764d446fd36650\", \"filename\": \"tests/test_gateway_seed_discovery.py\", \"status\": \"added\", \"additions\": 106, \"deletions\": 0, \"changes\": 106, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_gateway_seed_discovery.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_gateway_seed_discovery.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_gateway_seed_discovery.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,106 @@\\n+from __future__ import annotations\\n+\\n+from types import SimpleNamespace\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+from contextual_orchestrator import __main__ as cli\\n+from contextual_orchestrator.model_discovery import (\\n+ DiscoveredModel,\\n+ ProviderDiscoveryError,\\n+)\\n+\\n+\\n+def test_empty_gateway_seed_expands_from_its_registry(monkeypatch) -> None:\\n+ seed = ModelAgent(\\n+ \\\"gateway_seed\\\",\\n+ \\\"\\\",\\n+ base_url=\\\"https://gateway.example/v1\\\",\\n+ credential_key=\\\"LLM_GATEWAY_API_KEY\\\",\\n+ tags=(\\\"reasoning\\\", \\\"writing\\\"),\\n+ )\\n+\\n+ monkeypatch.setattr(\\n+ cli,\\n+ \\\"discover_provider_models\\\",\\n+ lambda source: [\\n+ DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=\\\"chat-model\\\",\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ ),\\n+ DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=\\\"text-embedding-model\\\",\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ ),\\n+ ],\\n+ )\\n+\\n+ agents = cli._auto_discover_seed_agents([seed], allow_failures=False)\\n+\\n+ assert [agent.model for agent in agents] == [\\\"chat-model\\\"]\\n+ assert agents[0].disabled is False\\n+ assert agents[0].base_url == \\\"https://gateway.example/v1\\\"\\n+\\n+\\n+def test_gateway_seed_discovery_preserves_configured_and_disables_unusable_seeds(\\n+ monkeypatch,\\n+) -> None:\\n+ configured = ModelAgent(\\\"configured_agent\\\", \\\"chat-model\\\")\\n+ assert cli._auto_discover_seed_agents([configured], allow_failures=False) == [configured]\\n+\\n+ seed = ModelAgent(\\\"gateway_seed\\\", \\\"\\\", base_url=\\\"https://gateway.example/v1\\\")\\n+ failure = ProviderDiscoveryError(\\\"gateway\\\", \\\"unavailable\\\")\\n+ monkeypatch.setattr(cli, \\\"discover_provider_models\\\", lambda _source: (_ for _ in ()).throw(failure))\\n+ with pytest.raises(ProviderDiscoveryError):\\n+ cli._auto_discover_seed_agents([seed], allow_failures=False)\\n+ assert cli._auto_discover_seed_agents([seed], allow_failures=True)[0].disabled is True\\n+\\n+ monkeypatch.setattr(\\n+ cli,\\n+ \\\"discover_provider_models\\\",\\n+ lambda source: [\\n+ DiscoveredModel(\\n+ provider_name=source.provider_name,\\n+ model_id=\\\"text-embedding-model\\\",\\n+ credential_name=source.credential_name,\\n+ chat_base_url=source.chat_base_url,\\n+ auth_scheme=source.auth_scheme,\\n+ )\\n+ ],\\n+ )\\n+ assert cli._auto_discover_seed_agents([seed], allow_failures=False)[0].disabled is True\\n+\\n+\\n+def test_discover_models_command_fails_when_every_provider_fails(monkeypatch) -> None:\\n+ monkeypatch.setattr(\\n+ cli,\\n+ \\\"discover_all_models\\\",\\n+ lambda: ([], [SimpleNamespace(provider_name=\\\"gateway\\\")]),\\n+ )\\n+ monkeypatch.setattr(cli, \\\"refresh_price_book\\\", lambda _models, _prices: 0)\\n+ with pytest.raises(SystemExit) as captured:\\n+ cli._discover_models_command([])\\n+ assert captured.value.code == 1\\n+\\n+\\n+def test_structured_output_forces_conduct_even_when_auto_would_route() -> None:\\n+ orchestrator = TaskOrchestrator(\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-model\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ )\\n+\\n+ result = orchestrator.complete(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"short structured request\\\"}],\\n+ mode=\\\"auto\\\",\\n+ output_contract={\\\"type\\\": \\\"json_object\\\"},\\n+ )\\n+\\n+ assert result[\\\"mode\\\"] == \\\"conduct\\\"\\n+ assert len(result[\\\"trace\\\"]) == 4\\n+ assert result[\\\"answer\\\"] == \\\"{}\\\"\" }, { \"sha\": \"9f2d009bbabe6312f9b84bf2c8ed84a038e537e2\", \"filename\": \"tests/test_generated_workflow.py\", \"status\": \"modified\", \"additions\": 14, \"deletions\": 1, \"changes\": 15, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_generated_workflow.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_generated_workflow.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_generated_workflow.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -18,7 +18,7 @@\\n \\n from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n import contextual_orchestrator.orchestrator as orchestrator_module # noqa: E402\\n-from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n+from contextual_orchestrator.orchestrator import BudgetExceededError, ModelClient # noqa: E402\\n \\n \\n PLAN = {\\n@@ -96,6 +96,19 @@ def test_invalid_plan_falls_back_to_template() -> None:\\n assert len(result[\\\"trace\\\"]) == 4 # fixed thinker/worker/verifier/synthesizer template\\n \\n \\n+def test_generated_planner_budget_exhaustion_does_not_fallback() -> None:\\n+ orchestrator, _ = _orch(json.dumps(PLAN))\\n+ budget_error = BudgetExceededError(\\\"spend budget exceeded\\\")\\n+ with patch.object(orchestrator, \\\"_plan\\\", side_effect=AssertionError(\\\"budget must not fall back\\\")):\\n+ with patch.object(orchestrator, \\\"_raise_if_spend_budget_exceeded\\\", side_effect=budget_error):\\n+ try:\\n+ orchestrator.conduct([{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"solve the hard problem\\\"}])\\n+ except BudgetExceededError as exc:\\n+ assert exc is budget_error\\n+ else: # pragma: no cover\\n+ raise AssertionError(\\\"budget exhaustion must stop generated planning\\\")\\n+\\n+\\n def test_default_template_unchanged() -> None:\\n orchestrator = TaskOrchestrator(\\n [ModelAgent(\\\"general_agent\\\", \\\"model-x\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"planning\\\", \\\"research\\\"))]\" }, { \"sha\": \"5b9cad401157d9129dc66d68ca80c3dcc0cdd671\", \"filename\": \"tests/test_inbound_request_framing.py\", \"status\": \"added\", \"additions\": 223, \"deletions\": 0, \"changes\": 223, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_inbound_request_framing.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_inbound_request_framing.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_inbound_request_framing.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,223 @@\\n+\\\"\\\"\\\"Fail-closed inbound HTTP request framing regressions.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from email.message import Message\\n+import io\\n+import socket\\n+from pathlib import Path\\n+import sys\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator.server import ( # noqa: E402\\n+ RequestError,\\n+ SecurityConfig,\\n+ _parse_request_framing,\\n+ build_server,\\n+)\\n+\\n+\\n+class _FakeConnection:\\n+ \\\"\\\"\\\"Record socket timeout changes made by the bounded body reader.\\\"\\\"\\\"\\n+\\n+ def __init__(self) -> None:\\n+ self.timeout: float | None = None\\n+\\n+ def gettimeout(self) -> float | None:\\n+ \\\"\\\"\\\"Return the current synthetic socket timeout.\\\"\\\"\\\"\\n+ return self.timeout\\n+\\n+ def settimeout(self, value: float | None) -> None:\\n+ \\\"\\\"\\\"Record a synthetic socket timeout.\\\"\\\"\\\"\\n+ self.timeout = value\\n+\\n+\\n+class _TimeoutReader:\\n+ \\\"\\\"\\\"Raise a real socket timeout to exercise request deadline handling.\\\"\\\"\\\"\\n+\\n+ def read(self, _size: int) -> bytes:\\n+ \\\"\\\"\\\"Raise a bounded-read timeout.\\\"\\\"\\\"\\n+ raise socket.timeout(\\\"test timeout\\\")\\n+\\n+\\n+class _ExplodingReader:\\n+ \\\"\\\"\\\"Fail if framing validation accidentally consumes bytes.\\\"\\\"\\\"\\n+\\n+ def read(self, _size: int) -> bytes:\\n+ \\\"\\\"\\\"Report an invalid unbounded read.\\\"\\\"\\\"\\n+ raise AssertionError(\\\"request bytes were consumed before framing validation\\\")\\n+\\n+\\n+class _GetAllHeaders:\\n+ \\\"\\\"\\\"Expose only the standard get_all header API.\\\"\\\"\\\"\\n+\\n+ def get_all(self, field_name: str, _default: list[str]) -> list[str]:\\n+ \\\"\\\"\\\"Return one valid fixed-length field.\\\"\\\"\\\"\\n+ return [] if field_name.casefold() == \\\"transfer-encoding\\\" else [\\\"1\\\"]\\n+\\n+\\n+class _GetHeaders:\\n+ \\\"\\\"\\\"Expose only a mapping-like get API.\\\"\\\"\\\"\\n+\\n+ def get(self, field_name: str) -> str | None:\\n+ \\\"\\\"\\\"Return one valid fixed-length field.\\\"\\\"\\\"\\n+ return None if field_name.casefold() == \\\"transfer-encoding\\\" else \\\"1\\\"\\n+\\n+\\n+def _headers(content_length: str | None = None, *, transfer_encoding: str | None = None) -> Message:\\n+ \\\"\\\"\\\"Build raw-like headers for the pure framing and handler tests.\\\"\\\"\\\"\\n+ headers = Message()\\n+ headers[\\\"content-type\\\"] = \\\"application/json\\\"\\n+ if content_length is not None:\\n+ headers[\\\"content-length\\\"] = content_length\\n+ if transfer_encoding is not None:\\n+ headers[\\\"transfer-encoding\\\"] = transfer_encoding\\n+ return headers\\n+\\n+\\n+def _handler(headers: Message, body: bytes | object, *, timeout: float = 1.0):\\n+ \\\"\\\"\\\"Create the real nested request handler without opening a listening socket.\\\"\\\"\\\"\\n+ server = build_server(\\n+ TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"mock-generalist\\\")]),\\n+ port=0,\\n+ security=SecurityConfig(\\n+ auth_token=\\\"test_token\\\", # noqa: S106\\n+ request_read_timeout_seconds=timeout,\\n+ ),\\n+ )\\n+ handler = server.RequestHandlerClass.__new__(server.RequestHandlerClass)\\n+ handler.headers = headers\\n+ handler.rfile = body if hasattr(body, \\\"read\\\") else io.BytesIO(body)\\n+ handler.connection = _FakeConnection()\\n+ handler.close_connection = False\\n+ return server, handler\\n+\\n+\\n+@pytest.mark.parametrize(\\\"value\\\", [\\\"-1\\\", \\\"+1\\\", \\\" 1\\\", \\\"1 \\\", \\\"1.0\\\", \\\"1,1\\\", \\\"\\\"])\\n+def test_invalid_content_length_is_rejected_before_read(value: str) -> None:\\n+ \\\"\\\"\\\"Reject signed, padded, non-decimal, and comma-ambiguous lengths.\\\"\\\"\\\"\\n+ with pytest.raises(RequestError, match=\\\"content-length\\\"):\\n+ _parse_request_framing(_headers(value), 64)\\n+\\n+\\n+def test_missing_length_and_transfer_encoding_fail_closed() -> None:\\n+ \\\"\\\"\\\"Require fixed-length framing and reject unsupported transfer coding.\\\"\\\"\\\"\\n+ with pytest.raises(RequestError, match=\\\"required\\\") as missing:\\n+ _parse_request_framing(_headers(), 64)\\n+ assert missing.value.status == 411\\n+ with pytest.raises(RequestError, match=\\\"transfer-encoded\\\"):\\n+ _parse_request_framing(_headers(\\\"1\\\", transfer_encoding=\\\"chunked\\\"), 64)\\n+\\n+\\n+def test_request_reader_rejects_non_json_media_type() -> None:\\n+ headers = _headers(\\\"2\\\")\\n+ headers.replace_header(\\\"content-type\\\", \\\"text/plain\\\")\\n+ server, handler = _handler(headers, b\\\"{}\\\")\\n+ try:\\n+ with pytest.raises(RequestError) as captured:\\n+ handler._read_json()\\n+ assert captured.value.status == 415\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_header_value_fallbacks_and_integer_overflow_are_safe() -> None:\\n+ \\\"\\\"\\\"Support ordinary header mappings without weakening strict parsing.\\\"\\\"\\\"\\n+ assert _parse_request_framing(_GetAllHeaders(), 64) == 1\\n+ assert _parse_request_framing(_GetHeaders(), 64) == 1\\n+ with pytest.raises(RequestError, match=\\\"invalid\\\"):\\n+ _parse_request_framing(_headers(\\\"9\\\" * 5000), 64)\\n+\\n+\\n+def test_duplicate_content_length_is_rejected_even_when_equal() -> None:\\n+ \\\"\\\"\\\"Do not choose a value when duplicate header lines are present.\\\"\\\"\\\"\\n+ headers = _headers(\\\"1\\\")\\n+ headers.add_header(\\\"Content-Length\\\", \\\"1\\\")\\n+ with pytest.raises(RequestError, match=\\\"duplicate\\\"):\\n+ _parse_request_framing(headers, 64)\\n+\\n+\\n+def test_oversized_content_length_is_rejected_before_read() -> None:\\n+ \\\"\\\"\\\"Enforce the configured byte limit before touching the body stream.\\\"\\\"\\\"\\n+ with pytest.raises(RequestError) as error:\\n+ _parse_request_framing(_headers(\\\"65\\\"), 64)\\n+ assert error.value.status == 413\\n+\\n+\\n+def test_read_json_requires_exact_body_and_restores_timeout() -> None:\\n+ \\\"\\\"\\\"Read exactly the declared bytes and restore the connection timeout.\\\"\\\"\\\"\\n+ server, handler = _handler(_headers(\\\"7\\\"), b'{\\\"x\\\":1}')\\n+ try:\\n+ assert handler._read_json() == {\\\"x\\\": 1}\\n+ assert handler.connection.timeout is None\\n+ assert handler.close_connection is False\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_zero_length_json_body_is_framing_valid_and_returns_empty_object() -> None:\\n+ \\\"\\\"\\\"Leave endpoint-level required-field validation to the existing caller.\\\"\\\"\\\"\\n+ server, handler = _handler(_headers(\\\"0\\\"), b\\\"\\\")\\n+ try:\\n+ assert handler._read_json() == {}\\n+ assert handler.close_connection is False\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_read_json_rejects_truncated_body_and_closes_connection() -> None:\\n+ \\\"\\\"\\\"Reject premature EOF rather than decoding a partial request.\\\"\\\"\\\"\\n+ server, handler = _handler(_headers(\\\"7\\\"), b'{\\\"x\\\":')\\n+ try:\\n+ with pytest.raises(RequestError, match=\\\"ended before\\\"):\\n+ handler._read_json()\\n+ assert handler.close_connection is True\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_read_json_rejects_invalid_framing_without_consuming_body() -> None:\\n+ \\\"\\\"\\\"Mark the connection closed when framing fails before the first read.\\\"\\\"\\\"\\n+ server, handler = _handler(_headers(\\\"-1\\\"), _ExplodingReader())\\n+ try:\\n+ with pytest.raises(RequestError, match=\\\"content-length\\\"):\\n+ handler._read_json()\\n+ assert handler.close_connection is True\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_read_json_times_out_slow_body_and_closes_connection() -> None:\\n+ \\\"\\\"\\\"Release a handler blocked on an incomplete declared body.\\\"\\\"\\\"\\n+ server, handler = _handler(_headers(\\\"1\\\"), _TimeoutReader(), timeout=0.1)\\n+ try:\\n+ with pytest.raises(RequestError, match=\\\"timed out\\\") as error:\\n+ handler._read_json()\\n+ assert error.value.status == 408\\n+ assert handler.close_connection is True\\n+ assert handler.connection.timeout is None\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_security_config_rejects_unbounded_body_read_timeout() -> None:\\n+ \\\"\\\"\\\"Keep deployment-provided body deadlines finite and bounded.\\\"\\\"\\\"\\n+ with pytest.raises(ValueError, match=\\\"request_read_timeout_seconds\\\"):\\n+ SecurityConfig(request_read_timeout_seconds=float(\\\"inf\\\"))\\n+ with pytest.raises(ValueError, match=\\\"max_body_bytes\\\"):\\n+ SecurityConfig(max_body_bytes=0)\\n+\\n+\\n+def test_security_readiness_exposes_bounded_request_controls() -> None:\\n+ \\\"\\\"\\\"Let operators verify the active body limit and deadline without secrets.\\\"\\\"\\\"\\n+ profile = SecurityConfig(max_body_bytes=128, request_read_timeout_seconds=2.0).readiness_profile()\\n+ assert profile[\\\"max_body_bytes\\\"] == 128\\n+ assert profile[\\\"request_read_timeout_seconds\\\"] == 2.0\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"379920f83e5abcdf3120175ee82ab0370c3bacde\", \"filename\": \"tests/test_inbound_request_total_deadline.py\", \"status\": \"added\", \"additions\": 175, \"deletions\": 0, \"changes\": 175, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_inbound_request_total_deadline.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_inbound_request_total_deadline.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_inbound_request_total_deadline.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,175 @@\\n+\\\"\\\"\\\"Regression for a total inbound-body deadline, not only idle-socket timeout.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from email.message import Message\\n+from pathlib import Path\\n+import sys\\n+from types import SimpleNamespace\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator import server as server_module # noqa: E402\\n+from contextual_orchestrator.server import ( # noqa: E402\\n+ RequestError,\\n+ SecurityConfig,\\n+ build_server,\\n+)\\n+\\n+\\n+class _Clock:\\n+ \\\"\\\"\\\"Deterministic monotonic clock advanced by the synthetic slow reader.\\\"\\\"\\\"\\n+\\n+ def __init__(self) -> None:\\n+ self.now = 0.0\\n+\\n+ def monotonic(self) -> float:\\n+ return self.now\\n+\\n+\\n+class _AdvancingClock:\\n+ \\\"\\\"\\\"Advance beyond the deadline before the first body read.\\\"\\\"\\\"\\n+\\n+ def __init__(self) -> None:\\n+ self.now = 0.0\\n+\\n+ def monotonic(self) -> float:\\n+ self.now += 0.2\\n+ return self.now\\n+\\n+\\n+class _SlowProgressReader:\\n+ \\\"\\\"\\\"Return progress before every idle timeout while exceeding the total deadline.\\\"\\\"\\\"\\n+\\n+ def __init__(self, payload: bytes, clock: _Clock, step_seconds: float) -> None:\\n+ self.payload = payload\\n+ self.clock = clock\\n+ self.step_seconds = step_seconds\\n+ self.offset = 0\\n+\\n+ def read(self, _size: int) -> bytes:\\n+ if self.offset >= len(self.payload):\\n+ return b\\\"\\\"\\n+ self.clock.now += self.step_seconds\\n+ result = self.payload[self.offset : self.offset + 1]\\n+ self.offset += 1\\n+ return result\\n+\\n+\\n+class _FakeConnection:\\n+ \\\"\\\"\\\"Expose the socket timeout methods used by the request reader.\\\"\\\"\\\"\\n+\\n+ def __init__(self) -> None:\\n+ self.timeout: float | None = None\\n+\\n+ def gettimeout(self) -> float | None:\\n+ return self.timeout\\n+\\n+ def settimeout(self, value: float | None) -> None:\\n+ self.timeout = value\\n+\\n+\\n+def _headers(body_size: int) -> Message:\\n+ headers = Message()\\n+ headers[\\\"content-type\\\"] = \\\"application/json\\\"\\n+ headers[\\\"content-length\\\"] = str(body_size)\\n+ return headers\\n+\\n+\\n+def test_slow_progress_cannot_extend_request_past_total_deadline(monkeypatch) -> None:\\n+ \\\"\\\"\\\"A byte trickle below the idle timeout must still terminate at the deadline.\\\"\\\"\\\"\\n+ payload = b'{\\\"x\\\":1}'\\n+ clock = _Clock()\\n+ server = build_server(\\n+ TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"mock-generalist\\\")]),\\n+ port=0,\\n+ security=SecurityConfig(\\n+ auth_token=\\\"test_token\\\", # noqa: S106\\n+ request_read_timeout_seconds=0.1,\\n+ ),\\n+ )\\n+ handler = server.RequestHandlerClass.__new__(server.RequestHandlerClass)\\n+ handler.headers = _headers(len(payload))\\n+ handler.rfile = _SlowProgressReader(payload, clock, step_seconds=0.06)\\n+ handler.connection = _FakeConnection()\\n+ handler.close_connection = False\\n+ monkeypatch.setattr(\\n+ server_module,\\n+ \\\"time\\\",\\n+ SimpleNamespace(monotonic=clock.monotonic),\\n+ )\\n+\\n+ try:\\n+ with pytest.raises(RequestError, match=\\\"timed out\\\") as error:\\n+ handler._read_json()\\n+ assert error.value.status == 408\\n+ assert handler.close_connection is True\\n+ assert handler.connection.timeout is None\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_complete_body_at_total_deadline_is_accepted(monkeypatch) -> None:\\n+ \\\"\\\"\\\"Accept the final byte when it completes the body at the deadline.\\\"\\\"\\\"\\n+ payload = b\\\"{}\\\"\\n+ clock = _Clock()\\n+ server = build_server(\\n+ TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"mock-generalist\\\")]),\\n+ port=0,\\n+ security=SecurityConfig(\\n+ auth_token=\\\"test_token\\\", # noqa: S106\\n+ request_read_timeout_seconds=0.1,\\n+ ),\\n+ )\\n+ handler = server.RequestHandlerClass.__new__(server.RequestHandlerClass)\\n+ handler.headers = _headers(len(payload))\\n+ handler.rfile = _SlowProgressReader(payload, clock, step_seconds=0.05)\\n+ handler.connection = _FakeConnection()\\n+ handler.close_connection = False\\n+ monkeypatch.setattr(\\n+ server_module,\\n+ \\\"time\\\",\\n+ SimpleNamespace(monotonic=clock.monotonic),\\n+ )\\n+\\n+ try:\\n+ assert handler._read_json() == {}\\n+ assert handler.close_connection is False\\n+ assert handler.connection.timeout is None\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+def test_expired_total_deadline_rejects_before_first_read(monkeypatch) -> None:\\n+ payload = b\\\"{}\\\"\\n+ clock = _AdvancingClock()\\n+ server = build_server(\\n+ TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"mock-generalist\\\")]),\\n+ port=0,\\n+ security=SecurityConfig(\\n+ auth_token=\\\"test_token\\\", # noqa: S106\\n+ request_read_timeout_seconds=0.1,\\n+ ),\\n+ )\\n+ handler = server.RequestHandlerClass.__new__(server.RequestHandlerClass)\\n+ handler.headers = _headers(len(payload))\\n+ handler.rfile = SimpleNamespace(\\n+ read=lambda _size: (_ for _ in ()).throw(AssertionError(\\\"body was read\\\"))\\n+ )\\n+ handler.connection = _FakeConnection()\\n+ handler.close_connection = False\\n+ monkeypatch.setattr(server_module, \\\"time\\\", SimpleNamespace(monotonic=clock.monotonic))\\n+\\n+ try:\\n+ with pytest.raises(RequestError, match=\\\"timed out\\\"):\\n+ handler._read_json()\\n+ assert handler.close_connection is True\\n+ finally:\\n+ server.server_close()\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"4f9c8e2c7d84a335db0185fc32605498e168a56f\", \"filename\": \"tests/test_int_float_max_output_stop_ws_http_honesty.py\", \"status\": \"modified\", \"additions\": 3, \"deletions\": 2, \"changes\": 5, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_int_float_max_output_stop_ws_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_int_float_max_output_stop_ws_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_int_float_max_output_stop_ws_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -220,7 +220,7 @@ def test_http_responses_accepts_digit_and_float_max_output_tokens() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_whole_float_n_and_seed() -> None:\\n+def test_http_responses_rejects_unapplied_whole_float_seed() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -233,7 +233,8 @@ def test_http_responses_accepts_whole_float_n_and_seed() -> None:\\n \\\"seed\\\": 7.0,\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\" }, { \"sha\": \"68713aa570f8f4785aa0065f10b4a77dd46f5163\", \"filename\": \"tests/test_json_schema_name_charset_http_honesty.py\", \"status\": \"modified\", \"additions\": 2, \"deletions\": 2, \"changes\": 4, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_json_schema_name_charset_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_json_schema_name_charset_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_json_schema_name_charset_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -291,7 +291,7 @@ def test_http_chat_keeps_legal_json_schema_name() -> None:\\n },\\n )\\n assert status == 200, body\\n- assert _echo_schema(body).get(\\\"name\\\") == legal_name\\n+ assert body[\\\"object\\\"] == \\\"chat.completion\\\"\\n \\n max_name = \\\"A\\\" * 64\\n status_max, body_max = _post(\\n@@ -309,7 +309,7 @@ def test_http_chat_keeps_legal_json_schema_name() -> None:\\n },\\n )\\n assert status_max == 200, body_max\\n- assert _echo_schema(body_max).get(\\\"name\\\") == max_name\\n+ assert body_max[\\\"object\\\"] == \\\"chat.completion\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\" }, { \"sha\": \"ed7c6ae017faed868d402e66149db1c2d324f516\", \"filename\": \"tests/test_ledger_execution_identity_http_honesty.py\", \"status\": \"modified\", \"additions\": 5, \"deletions\": 5, \"changes\": 10, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_ledger_execution_identity_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_ledger_execution_identity_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_ledger_execution_identity_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,15 +3,15 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n-from contextual_orchestrator import ( # noqa: E402\\n+from contextual_orchestrator import (\\n CostLedger,\\n CostRoutingCoordinator,\\n InMemoryConfigStore,\\n@@ -20,9 +20,9 @@\\n PriceEntry,\\n TaskOrchestrator,\\n )\\n-from contextual_orchestrator.server import SecurityConfig, build_server # noqa: E402\\n+from contextual_orchestrator.server import SecurityConfig, build_server\\n \\n-_TEST_AUTH_TOKEN = \\\"ledger_execution_identity_http_honesty_token\\\" # noqa: S105\\n+_TEST_AUTH_TOKEN = \\\"ledger_execution_identity_http_honesty_token\\\"\\n \\n \\n def _serve():\\n@@ -32,7 +32,7 @@ def _serve():\\n model=\\\"mock-a\\\",\\n base_url=\\\"mock://a\\\",\\n provider_name=\\\"mock\\\",\\n- tags=(\\\"reasoning\\\", \\\"coding\\\", \\\"writing\\\"),\\n+ tags=(\\\"reasoning\\\", \\\"coding\\\", \\\"writing\\\", \\\"embedding\\\"),\\n priority=1,\\n )\\n ]\" }, { \"sha\": \"00491dd96688b307d8a93efbcc61afd317747df9\", \"filename\": \"tests/test_local_gateway.py\", \"status\": \"renamed\", \"additions\": 209, \"deletions\": 87, \"changes\": 296, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_local_gateway.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_local_gateway.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_local_gateway.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,4 +1,4 @@\\n-\\\"\\\"\\\"Explicit loopback mlx-lm transport without weakening remote egress rules.\\\"\\\"\\\"\\n+\\\"\\\"\\\"Provider-neutral loopback gateway transport without weakening egress rules.\\\"\\\"\\\"\\n \\n from __future__ import annotations\\n \\n@@ -20,39 +20,28 @@\\n _chat_to_responses_payload,\\n _is_local_provider_url,\\n _responses_to_chat_payload,\\n+ _responses_usage,\\n )\\n \\n \\n+@pytest.fixture(autouse=True)\\n+def _local_gateway_credentials():\\n+ with patch(\\\"contextual_orchestrator.orchestrator.get_credential\\\", return_value=\\\"local-secret\\\"):\\n+ yield\\n+\\n+\\n def test_local_candidate_registry_keeps_all_discovered_entries() -> None:\\n agents = load_agents(str(Path(__file__).resolve().parents[1] / \\\"examples/agents.local.json\\\"))\\n orchestrator = TaskOrchestrator(agents)\\n \\n assert {agent.model for agent in orchestrator.candidates} >= {\\n \\\"contextual-orchestrator\\\",\\n- \\\"mlx-community/gemma-4-31b-it-4bit\\\",\\n- \\\"mlx-community/llama-3.2-3b-instruct-4bit\\\",\\n- \\\"outlier-ai/deepseek-r1-distill-qwen-32b-mlx-4bit\\\",\\n+ \\\"gemma-4-e4b-it\\\",\\n \\\"embeddinggemma\\\",\\n }\\n assert all(not agent.disabled for agent in orchestrator.candidates)\\n assert len(orchestrator.candidates) == len(orchestrator.agents)\\n- verifier_exclusions = {\\n- agent.model: agent.provider_exclusions\\n- for agent in orchestrator.candidates\\n- if agent.model in {\\n- \\\"mlx-community/llama-3.2-1b-instruct-4bit\\\",\\n- \\\"mlx-community/gemma-4-31b-it-4bit\\\",\\n- \\\"outlier-ai/deepseek-r1-distill-qwen-32b-mlx-4bit\\\",\\n- }\\n- }\\n- assert verifier_exclusions == {\\n- \\\"mlx-community/llama-3.2-1b-instruct-4bit\\\": (\\\"verifier\\\",),\\n- \\\"mlx-community/gemma-4-31b-it-4bit\\\": (\\\"verifier\\\",),\\n- \\\"outlier-ai/deepseek-r1-distill-qwen-32b-mlx-4bit\\\": (\\\"verifier\\\",),\\n- }\\n- assert orchestrator._select_agent(\\n- \\\"Evaluate this answer for evidence and risk.\\\", \\\"verifier\\\"\\n- ).model == \\\"mlx-community/gemma-4-e4b-it-4bit\\\"\\n+ assert all(not agent.base_url.startswith(\\\"mlx://\\\") for agent in orchestrator.candidates)\\n assert any(\\n agent.model == \\\"contextual-orchestrator\\\"\\n and set(agent.provider_exclusions) == {\\\"thinker\\\", \\\"worker\\\", \\\"verifier\\\", \\\"synthesizer\\\"}\\n@@ -76,26 +65,9 @@ def read(self) -> bytes:\\n return json.dumps(self.payload).encode(\\\"utf-8\\\")\\n \\n \\n-def test_mlx_loopback_uses_http_without_a_credential() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n- client = ModelClient(max_retries=0, temperature=0.0, chat_template_args={\\\"enable_thinking\\\": False})\\n- seen = []\\n-\\n- def open_provider(request, _destination=None):\\n- seen.append(request)\\n- return _Response({\\n- \\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": \\\"local-ok\\\"}}],\\n- \\\"usage\\\": {\\\"prompt_tokens\\\": 2, \\\"completion_tokens\\\": 1, \\\"total_tokens\\\": 3},\\n- })\\n-\\n- with patch.object(client, \\\"_open_provider\\\", side_effect=open_provider):\\n- assert client.chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"ping\\\"}]) == \\\"local-ok\\\"\\n- assert seen[0].full_url == \\\"http://127.0.0.1:8080/v1/chat/completions\\\"\\n- assert \\\"Authorization\\\" not in seen[0].headers\\n- import json\\n-\\n- assert json.loads(seen[0].data)[\\\"chat_template_kwargs\\\"] == {\\\"enable_thinking\\\": False}\\n- assert client.take_usage()[\\\"total_tokens\\\"] == 3\\n+def test_local_gateway_requires_explicit_authentication() -> None:\\n+ with pytest.raises(ValueError, match=\\\"local_credential_key\\\"):\\n+ ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\")\\n \\n \\n def test_authenticated_local_gateway_uses_only_its_explicit_kv_credential() -> None:\\n@@ -109,7 +81,6 @@ def test_authenticated_local_gateway_uses_only_its_explicit_kv_credential() -> N\\n client = ModelClient(\\n max_retries=0,\\n temperature=0.0,\\n- chat_template_args={\\\"enable_thinking\\\": False},\\n )\\n seen = []\\n \\n@@ -142,19 +113,14 @@ def test_authenticated_local_gateway_requires_its_kv_credential() -> None:\\n ModelClient(max_retries=0).chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"ping\\\"}])\\n \\n \\n-def test_local_gateway_credential_cannot_be_attached_to_mlx_worker() -> None:\\n- with pytest.raises(ValueError, match=\\\"local:// gateway\\\"):\\n- ModelAgent(\\n- \\\"local_agent\\\",\\n- \\\"local-model\\\",\\n- base_url=\\\"mlx://127.0.0.1:8080/v1\\\",\\n- local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n- )\\n+def test_direct_mlx_provider_scheme_is_rejected() -> None:\\n+ with pytest.raises(ValueError, match=\\\"direct mlx:// provider URLs are unsupported\\\"):\\n+ ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n \\n \\n def test_provider_probe_verifies_registry_then_uses_one_bounded_completion_without_retry() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n- client = ModelClient(max_retries=2, local_max_retries=2, chat_template_args={\\\"enable_thinking\\\": False})\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n+ client = ModelClient(max_retries=2, local_max_retries=2)\\n seen: list[tuple[object, float | None]] = []\\n \\n def open_provider(request, _destination=None, *, timeout=None):\\n@@ -180,12 +146,11 @@ def open_provider(request, _destination=None, *, timeout=None):\\n \\n payload = json.loads(seen[1][0].data)\\n assert payload[\\\"max_tokens\\\"] == 1\\n- assert payload[\\\"temperature\\\"] == 0.0\\n- assert payload[\\\"chat_template_kwargs\\\"] == {\\\"enable_thinking\\\": False}\\n+ assert \\\"temperature\\\" not in payload\\n \\n \\n def test_provider_probe_rejects_a_local_model_registry_mismatch() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"requested-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"requested-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient(max_retries=0)\\n with patch.object(\\n client,\\n@@ -202,7 +167,7 @@ def test_provider_probe_rejects_a_local_model_registry_mismatch() -> None:\\n \\n \\n def test_provider_probe_reports_timeout_without_retry() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient(max_retries=2, local_max_retries=2)\\n with patch.object(client, \\\"_open_provider\\\", side_effect=TimeoutError(\\\"probe timeout\\\")) as open_provider:\\n report = client.probe(agent, timeout=0.5)\\n@@ -215,7 +180,7 @@ def test_provider_probe_reports_timeout_without_retry() -> None:\\n \\n \\n def test_provider_probe_does_not_serialize_provider_exception_text() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient(max_retries=0)\\n with patch.object(client, \\\"_open_provider\\\", side_effect=RuntimeError(\\\"provider-output-secret\\\")):\\n report = client.probe(agent, timeout=0.5)\\n@@ -281,8 +246,8 @@ def probe(_agent, *, timeout):\\n def test_local_provider_serializes_model_switches_and_bounds_waiters() -> None:\\n import threading\\n \\n- first_agent = ModelAgent(\\\"first_agent\\\", \\\"model-a\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n- second_agent = ModelAgent(\\\"second_agent\\\", \\\"model-b\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ first_agent = ModelAgent(\\\"first_agent\\\", \\\"model-a\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n+ second_agent = ModelAgent(\\\"second_agent\\\", \\\"model-b\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n first_client = ModelClient(max_retries=0, timeout=1.0)\\n second_client = ModelClient(max_retries=0, timeout=0.05)\\n entered = threading.Event()\\n@@ -328,8 +293,8 @@ def call(client, agent):\\n assert isinstance(errors[0], TimeoutError)\\n \\n \\n-def test_reasoning_only_response_explains_local_template_fix() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+def test_reasoning_only_response_explains_missing_content() -> None:\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient(max_retries=0)\\n with patch.object(\\n client,\\n@@ -339,20 +304,26 @@ def test_reasoning_only_response_explains_local_template_fix() -> None:\\n try:\\n client.chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"ping\\\"}])\\n except RuntimeError as exc:\\n- assert \\\"enable_thinking\\\" in str(exc)\\n+ assert \\\"assistant content\\\" in str(exc)\\n else: # pragma: no cover\\n raise AssertionError(\\\"reasoning-only provider response must fail clearly\\\")\\n \\n \\n def test_response_without_content_or_reasoning_fails_clearly() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n with pytest.raises(RuntimeError, match=\\\"assistant content\\\"):\\n ModelClient()._response_content(agent, {\\\"choices\\\": [{\\\"message\\\": {}}]})\\n \\n \\n-def test_local_responses_passthrough_adapts_to_chat_transport() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n- client = ModelClient(max_retries=0, chat_template_args={\\\"enable_thinking\\\": False})\\n+@pytest.mark.parametrize(\\\"endpoint\\\", [\\\"responses\\\", \\\"/v1/responses\\\"])\\n+def test_local_responses_passthrough_adapts_to_chat_transport(endpoint: str) -> None:\\n+ agent = ModelAgent(\\n+ \\\"local_agent\\\",\\n+ \\\"local-model\\\",\\n+ base_url=\\\"local://127.0.0.1:8080/v1\\\",\\n+ local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n+ )\\n+ client = ModelClient(max_retries=0)\\n with patch.object(client, \\\"_validate_provider\\\", return_value=None), patch.object(\\n client,\\n \\\"_send_raw_with_retry\\\",\\n@@ -369,7 +340,7 @@ def test_local_responses_passthrough_adapts_to_chat_transport() -> None:\\n ) as send:\\n response = client.proxy_send(\\n agent,\\n- \\\"responses\\\",\\n+ endpoint,\\n {\\n \\\"model\\\": \\\"local-model\\\",\\n \\\"instructions\\\": \\\"Be concise.\\\",\\n@@ -379,6 +350,13 @@ def test_local_responses_passthrough_adapts_to_chat_transport() -> None:\\n \\\"content\\\": [{\\\"type\\\": \\\"input_text\\\", \\\"text\\\": \\\"ping\\\"}],\\n }],\\n \\\"stream\\\": True,\\n+ \\\"text\\\": {\\n+ \\\"format\\\": {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"name\\\": \\\"result_shape\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\"},\\n+ }\\n+ },\\n \\\"tools\\\": [{\\n \\\"type\\\": \\\"function\\\",\\n \\\"name\\\": \\\"lookup\\\",\\n@@ -397,11 +375,17 @@ def test_local_responses_passthrough_adapts_to_chat_transport() -> None:\\n \\\"type\\\": \\\"function\\\",\\n \\\"function\\\": {\\\"name\\\": \\\"lookup\\\", \\\"parameters\\\": {\\\"type\\\": \\\"object\\\"}},\\n }]\\n- assert forwarded[\\\"chat_template_kwargs\\\"] == {\\\"enable_thinking\\\": False}\\n+ assert forwarded[\\\"response_format\\\"] == {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\n+ \\\"name\\\": \\\"result_shape\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\"},\\n+ },\\n+ }\\n \\n \\n-def test_local_responses_passthrough_omits_empty_template_arguments() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+def test_local_responses_passthrough_has_no_provider_specific_fields() -> None:\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient()\\n with patch.object(client, \\\"_validate_provider\\\", return_value=None), patch.object(\\n client,\\n@@ -413,6 +397,60 @@ def test_local_responses_passthrough_omits_empty_template_arguments() -> None:\\n assert \\\"chat_template_kwargs\\\" not in send.call_args.args[2]\\n \\n \\n+def test_local_chat_passthrough_applies_bounded_controls_for_final_synthesis() -> None:\\n+ agent = ModelAgent(\\n+ \\\"local_agent\\\",\\n+ \\\"local-model\\\",\\n+ base_url=\\\"local://127.0.0.1:8080/v1\\\",\\n+ local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n+ )\\n+ client = ModelClient(max_output_tokens=321)\\n+ with patch.object(client, \\\"_validate_provider\\\", return_value=None), patch.object(\\n+ client,\\n+ \\\"_send_raw_with_retry\\\",\\n+ return_value={\\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": \\\"OK\\\"}}]},\\n+ ) as send:\\n+ client.proxy_send(\\n+ agent,\\n+ \\\"chat/completions\\\",\\n+ {\\n+ \\\"model\\\": \\\"local-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"final synthesis\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ },\\n+ )\\n+\\n+ forwarded = send.call_args.args[2]\\n+ assert forwarded[\\\"max_tokens\\\"] == 321\\n+ assert \\\"chat_template_kwargs\\\" not in forwarded\\n+\\n+\\n+def test_local_chat_passthrough_preserves_explicit_max_tokens() -> None:\\n+ agent = ModelAgent(\\n+ \\\"local_agent\\\",\\n+ \\\"local-model\\\",\\n+ base_url=\\\"local://127.0.0.1:8080/v1\\\",\\n+ local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n+ )\\n+ client = ModelClient(max_output_tokens=321)\\n+ with patch.object(client, \\\"_validate_provider\\\", return_value=None), patch.object(\\n+ client,\\n+ \\\"_send_raw_with_retry\\\",\\n+ return_value={\\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": \\\"OK\\\"}}]},\\n+ ) as send:\\n+ client.proxy_send(\\n+ agent,\\n+ \\\"chat/completions\\\",\\n+ {\\n+ \\\"model\\\": \\\"local-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"final synthesis\\\"}],\\n+ \\\"max_tokens\\\": 64,\\n+ },\\n+ )\\n+\\n+ assert send.call_args.args[2][\\\"max_tokens\\\"] == 64\\n+\\n+\\n def test_local_responses_adapter_preserves_supported_items_and_controls() -> None:\\n payload = _responses_to_chat_payload(\\n {\\n@@ -480,6 +518,79 @@ def test_local_responses_adapter_preserves_supported_items_and_controls() -> Non\\n assert payload[\\\"tool_choice\\\"] == {\\\"type\\\": \\\"function\\\", \\\"function\\\": {\\\"name\\\": \\\"lookup\\\"}}\\n \\n \\n+def test_local_responses_adapter_preserves_json_schema_contract() -> None:\\n+ payload = _responses_to_chat_payload(\\n+ {\\n+ \\\"model\\\": \\\"local-model\\\",\\n+ \\\"input\\\": \\\"extract the region\\\",\\n+ \\\"text\\\": {\\n+ \\\"format\\\": {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"name\\\": \\\"region_result\\\",\\n+ \\\"description\\\": \\\"A bounded region result\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"label\\\": {\\\"type\\\": \\\"string\\\"}}},\\n+ \\\"strict\\\": True,\\n+ }\\n+ },\\n+ }\\n+ )\\n+\\n+ assert payload[\\\"response_format\\\"] == {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\n+ \\\"name\\\": \\\"region_result\\\",\\n+ \\\"description\\\": \\\"A bounded region result\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {\\\"label\\\": {\\\"type\\\": \\\"string\\\"}}},\\n+ \\\"strict\\\": True,\\n+ },\\n+ }\\n+\\n+\\n+def test_local_responses_adapter_prefers_translated_text_format_contract() -> None:\\n+ payload = _responses_to_chat_payload(\\n+ {\\n+ \\\"input\\\": \\\"prefer the Responses contract\\\",\\n+ \\\"text\\\": {\\n+ \\\"format\\\": {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"name\\\": \\\"responses_shape\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\"},\\n+ }\\n+ },\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ assert payload[\\\"response_format\\\"] == {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\n+ \\\"name\\\": \\\"responses_shape\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\"},\\n+ },\\n+ }\\n+\\n+\\n+def test_responses_usage_normalizes_chat_aliases() -> None:\\n+ assert _responses_usage(\\n+ {\\\"prompt_tokens\\\": 4, \\\"completion_tokens\\\": 5, \\\"total_tokens\\\": 9}\\n+ ) == {\\\"input_tokens\\\": 4, \\\"output_tokens\\\": 5, \\\"total_tokens\\\": 9}\\n+\\n+\\n+def test_local_responses_adapter_preserves_bounded_metadata_for_provider() -> None:\\n+ payload = _responses_to_chat_payload(\\n+ {\\n+ \\\"input\\\": \\\"use the supplied context\\\",\\n+ \\\"metadata\\\": {\\\"pu\\\": \\\"PU_TEST\\\", \\\"corp_code\\\": \\\"CORP_TEST\\\", \\\"author_id\\\": \\\"AUTHOR_TEST\\\"},\\n+ }\\n+ )\\n+\\n+ assert payload[\\\"metadata\\\"] == {\\n+ \\\"pu\\\": \\\"PU_TEST\\\",\\n+ \\\"corp_code\\\": \\\"CORP_TEST\\\",\\n+ \\\"author_id\\\": \\\"AUTHOR_TEST\\\",\\n+ }\\n+\\n+\\n def test_local_responses_adapter_rejects_non_string_input() -> None:\\n with pytest.raises(ValueError, match=\\\"string or item list\\\"):\\n _responses_to_chat_payload({\\\"input\\\": {\\\"unexpected\\\": \\\"mapping\\\"}})\\n@@ -545,9 +656,18 @@ def test_local_responses_response_maps_reasoning_and_tool_calls() -> None:\\n \\n \\n def test_local_provider_scheme_validation_rejects_remote_and_malformed_ports() -> None:\\n- assert _is_local_provider_url(\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ assert _is_local_provider_url(\\\"local://127.0.0.1:8080/v1\\\")\\n+ assert not _is_local_provider_url(\\\"local://host.docker.internal:8080/v1\\\")\\n assert not _is_local_provider_url(\\\"mlx://example.com:8080/v1\\\")\\n- assert not _is_local_provider_url(\\\"mlx://127.0.0.1:not-a-port/v1\\\")\\n+ assert not _is_local_provider_url(\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ assert not _is_local_provider_url(\\\"local://127.0.0.1:not-a-port/v1\\\")\\n+ with pytest.raises(ValueError, match=\\\"explicit loopback endpoint\\\"):\\n+ ModelAgent(\\n+ \\\"docker_host_agent\\\",\\n+ \\\"local-model\\\",\\n+ base_url=\\\"local://host.docker.internal:8080/v1\\\",\\n+ local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n+ )\\n \\n \\n @pytest.mark.parametrize(\\n@@ -581,6 +701,19 @@ def test_provider_transport_rejects_invalid_port_and_resolution_failures() -> No\\n client._resolve_addresses(\\\"empty.example\\\", 443)\\n \\n \\n+@pytest.mark.parametrize(\\n+ \\\"userinfo_url\\\",\\n+ [\\n+ \\\"https://@provider.example/v1/chat/completions\\\",\\n+ \\\"https://:secret@provider.example/v1/chat/completions\\\",\\n+ ],\\n+)\\n+def test_provider_transport_rejects_empty_userinfo(userinfo_url: str) -> None:\\n+ \\\"\\\"\\\"The low-level transport must reject empty userinfo before opening a socket.\\\"\\\"\\\"\\n+ with pytest.raises(RuntimeError, match=\\\"without userinfo\\\"):\\n+ ModelClient()._open_provider(urllib.request.Request(userinfo_url))\\n+\\n+\\n def test_https_provider_uses_verifying_connection_and_resolved_destination() -> None:\\n class FakeResponse:\\n status = 200\\n@@ -655,7 +788,7 @@ def close(self):\\n \\n \\n def test_local_provider_url_rejects_query_data_at_transport_boundary() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1?unsafe=1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1?unsafe=1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n with pytest.raises(RuntimeError, match=\\\"query data\\\"):\\n ModelClient()._provider_url(agent, \\\"/chat/completions\\\")\\n with pytest.raises(RuntimeError, match=\\\"query data\\\"):\\n@@ -670,7 +803,7 @@ def test_provider_url_rejects_non_http_scheme_at_builder_boundary() -> None:\\n \\n def test_provider_validation_rejects_non_loopback_and_remote_query_data() -> None:\\n client = ModelClient()\\n- local = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ local = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n with patch.object(client, \\\"_resolve_addresses\\\", return_value=[(socket.AF_INET, (\\\"192.0.2.1\\\", 8080))]):\\n with pytest.raises(RuntimeError, match=\\\"non-loopback\\\"):\\n client._validate_provider(local)\\n@@ -755,7 +888,7 @@ def test_provider_transport_rejects_non_http_url_before_io() -> None:\\n \\n \\n def test_local_batch_preserves_ids_and_usage() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient(max_retries=0, local_concurrency=2)\\n calls = []\\n \\n@@ -776,7 +909,7 @@ def fake_chat(_agent, messages, temperature=None):\\n \\n \\n def test_local_batch_default_uses_sequential_path() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n client = ModelClient()\\n with patch.object(client, \\\"chat\\\", side_effect=lambda _agent, messages, temperature=None: messages[0][\\\"content\\\"]):\\n result = client.batch_chat(\\n@@ -793,17 +926,6 @@ def test_patch_agent_rejects_disabling_last_enabled_agent() -> None:\\n orchestrator.patch_agent(\\\"default\\\", \\\"only_agent\\\", {\\\"status\\\": \\\"disabled\\\"})\\n \\n \\n-def test_stream_chat_forwards_local_template_arguments() -> None:\\n- agent = ModelAgent(\\\"local_agent\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n- client = ModelClient(chat_template_args={\\\"enable_thinking\\\": False})\\n- with patch.object(client, \\\"_validate_provider\\\", return_value=None), patch.object(\\n- client, \\\"_stream_send\\\", return_value=iter((\\\"delta\\\",))\\n- ) as stream_send:\\n- assert list(client.stream_chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"ping\\\"}])) == [\\\"delta\\\"]\\n-\\n- assert stream_send.call_args.args[1][\\\"chat_template_kwargs\\\"] == {\\\"enable_thinking\\\": False}\\n-\\n-\\n if __name__ == \\\"__main__\\\":\\n for name, fn in sorted(globals().items()):\\n if name.startswith(\\\"test_\\\") and callable(fn):\", \"previous_filename\": \"tests/test_local_mlx.py\" }, { \"sha\": \"d24fb5148dbc9066ae94527f68841297e933248c\", \"filename\": \"tests/test_logit_bias_key_strip_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 3, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_logit_bias_key_strip_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_logit_bias_key_strip_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_logit_bias_key_strip_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,7 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_responses_accepts_logit_bias_padded_digit_keys() -> None:\\n+def test_http_responses_rejects_unapplied_logit_bias_padded_digit_keys() -> None:\\n server, thread, port = _server()\\n try:\\n for key in (\\\"100\\\", \\\" 100 \\\", \\\"\\\\t42\\\\t\\\", \\\" 7\\\"):\\n@@ -65,7 +65,8 @@ def test_http_responses_accepts_logit_bias_padded_digit_keys() -> None:\\n \\\"logit_bias\\\": {key: \\\"-5\\\", \\\"200\\\": 1},\\n },\\n )\\n- assert status == 200, (key, body)\\n+ assert status == 422, (key, body)\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -135,7 +136,7 @@ def test_http_completions_still_typechecks_padded_keys_then_rejects_nonempty() -\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_responses_accepts_logit_bias_padded_digit_keys()\\n+ test_http_responses_rejects_unapplied_logit_bias_padded_digit_keys()\\n test_http_responses_still_rejects_non_digit_logit_bias_keys()\\n test_http_chat_still_typechecks_padded_keys_then_rejects_nonempty()\\n test_http_completions_still_typechecks_padded_keys_then_rejects_nonempty()\" }, { \"sha\": \"e2df31558676ad9bc5a88ef8cbb8bd5736b6a74d\", \"filename\": \"tests/test_logit_bias_numeric_string_coerce_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 3, \"changes\": 7, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_logit_bias_numeric_string_coerce_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_logit_bias_numeric_string_coerce_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_logit_bias_numeric_string_coerce_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,7 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_responses_accepts_logit_bias_numeric_string_values() -> None:\\n+def test_http_responses_rejects_unapplied_logit_bias_after_type_check() -> None:\\n server, thread, port = _server()\\n try:\\n for val in (\\\"-5\\\", \\\"0\\\", \\\"100\\\", \\\" -12.5 \\\", 0, -5.0, 100):\\n@@ -65,7 +65,8 @@ def test_http_responses_accepts_logit_bias_numeric_string_values() -> None:\\n \\\"logit_bias\\\": {\\\"100\\\": val, \\\"200\\\": 1},\\n },\\n )\\n- assert status == 200, (val, body)\\n+ assert status == 422, (val, body)\\n+ assert \\\"unsupported_responses_orchestration_controls\\\" in json.dumps(body)\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -113,7 +114,7 @@ def test_http_chat_still_rejects_nonempty_logit_bias_after_type_check() -> None:\\n \\n \\n if __name__ == \\\"__main__\\\":\\n- test_http_responses_accepts_logit_bias_numeric_string_values()\\n+ test_http_responses_rejects_unapplied_logit_bias_after_type_check()\\n test_http_responses_still_rejects_logit_bias_bool_and_oob()\\n test_http_chat_still_rejects_nonempty_logit_bias_after_type_check()\\n print(\\\"ok\\\")\" }, { \"sha\": \"da13fb76872989146256581e5d41d6a6dcbd6c85\", \"filename\": \"tests/test_mode_casefold_http_honesty.py\", \"status\": \"modified\", \"additions\": 4, \"deletions\": 4, \"changes\": 8, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_mode_casefold_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_mode_casefold_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_mode_casefold_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,23 +3,23 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n-from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n-from contextual_orchestrator.server import SecurityConfig, build_server # noqa: E402\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+from contextual_orchestrator.server import SecurityConfig, build_server\\n \\n _TEST_AUTH_TOKEN = \\\"mode_casefold_http_honesty_token\\\" # noqa: S105\\n \\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"embedding\\\"))]\\n )\\n \\n \" }, { \"sha\": \"d5fd91eb8127a05a9b4bba28ff96d6151430ed5c\", \"filename\": \"tests/test_model_discovery.py\", \"status\": \"modified\", \"additions\": 140, \"deletions\": 4, \"changes\": 144, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_model_discovery.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_model_discovery.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_model_discovery.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,6 +3,8 @@\\n from __future__ import annotations\\n \\n import json\\n+from contextlib import contextmanager\\n+import socket\\n import sys\\n import urllib.error\\n import urllib.parse\\n@@ -23,7 +25,9 @@\\n from contextual_orchestrator.kv_config import InMemoryConfigStore # noqa: E402\\n from contextual_orchestrator.model_discovery import ( # noqa: E402\\n DiscoveredModel,\\n+ ProviderDiscoveryError,\\n ProviderModelSource,\\n+ _fetch_json,\\n agent_from_discovered,\\n agent_id_for,\\n discover_all_models,\\n@@ -32,6 +36,7 @@\\n select_cheapest_discovered_agent,\\n select_top_n_cheapest_discovered_agents,\\n )\\n+from contextual_orchestrator.orchestrator import ModelClient # noqa: E402\\n \\n \\n @pytest.fixture(autouse=True)\\n@@ -53,10 +58,27 @@ def __enter__(self):\\n def __exit__(self, *_args):\\n return False\\n \\n- def read(self) -> bytes:\\n+ def read(self, _size: int = -1) -> bytes:\\n return self._body\\n \\n \\n+@contextmanager\\n+def _patched_provider_transport(urlopen):\\n+ \\\"\\\"\\\"Keep discovery tests offline while exercising the validated transport seam.\\\"\\\"\\\"\\n+ def open_provider(request, _destination=None, *, timeout=None):\\n+ return urlopen(request, timeout=timeout)\\n+\\n+ with (\\n+ patch.object(\\n+ ModelClient,\\n+ \\\"_validate_provider\\\",\\n+ return_value=(socket.AF_INET, (\\\"93.184.216.34\\\", 443)),\\n+ ),\\n+ patch.object(ModelClient, \\\"_open_provider\\\", side_effect=open_provider),\\n+ ):\\n+ yield\\n+\\n+\\n OPENAI_SOURCE = ProviderModelSource(\\n provider_name=\\\"openai\\\",\\n credential_name=\\\"OPENAI_API_KEY\\\",\\n@@ -101,7 +123,7 @@ def urlopen(request, timeout=None):\\n seen_requests.append(request)\\n return _Response(payload)\\n \\n- with patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen):\\n+ with _patched_provider_transport(urlopen):\\n discovered = discover_provider_models(OPENROUTER_SOURCE)\\n \\n assert seen_requests[0].get_header(\\\"Authorization\\\") == \\\"Bearer sk-router\\\"\\n@@ -113,6 +135,117 @@ def urlopen(request, timeout=None):\\n assert discovered[1].prompt_price_per_1k is None\\n \\n \\n+def test_discover_local_gateway_is_not_a_model_discovery_source() -> None:\\n+ register_credential(\\\"LOCAL_GATEWAY_KEY\\\", \\\"local-secret\\\")\\n+ source = ProviderModelSource(\\n+ provider_name=\\\"local_gateway\\\",\\n+ credential_name=\\\"LOCAL_GATEWAY_KEY\\\",\\n+ list_url=\\\"local://host.docker.internal:8080/v1/models\\\",\\n+ chat_base_url=\\\"local://host.docker.internal:8080/v1\\\",\\n+ )\\n+ with pytest.raises(ProviderDiscoveryError, match=\\\"invalid_response\\\") as error:\\n+ discover_provider_models(source)\\n+ assert error.value.__cause__ is None\\n+\\n+\\n+def test_discover_rejects_private_provider_before_authorized_transport() -> None:\\n+ register_credential(\\\"PRIVATE_PROVIDER_KEY\\\", \\\"private-provider-secret\\\")\\n+ source = ProviderModelSource(\\n+ provider_name=\\\"private_provider\\\",\\n+ credential_name=\\\"PRIVATE_PROVIDER_KEY\\\",\\n+ list_url=\\\"https://models.example.test/v1/models\\\",\\n+ chat_base_url=\\\"https://models.example.test/v1\\\",\\n+ )\\n+ with (\\n+ patch.object(\\n+ ModelClient,\\n+ \\\"_resolve_addresses\\\",\\n+ return_value=[(socket.AF_INET, (\\\"127.0.0.1\\\", 443))],\\n+ ),\\n+ patch.object(ModelClient, \\\"_open_provider\\\") as open_provider,\\n+ ):\\n+ with pytest.raises(ProviderDiscoveryError, match=\\\"provider_error\\\") as error:\\n+ discover_provider_models(source)\\n+ assert error.value.__cause__ is None\\n+ open_provider.assert_not_called()\\n+\\n+\\n+def test_fetch_json_rejects_cross_origin_before_provider_transport() -> None:\\n+ \\\"\\\"\\\"Discovery cannot reuse a validated agent to send credentials elsewhere.\\\"\\\"\\\"\\n+ register_credential(\\\"OPENAI_API_KEY\\\", \\\"openai-secret\\\")\\n+ agent = ModelAgent(\\n+ \\\"model_discovery_agent\\\",\\n+ \\\"model_catalog\\\",\\n+ \\\"https://api.openai.com/v1\\\",\\n+ credential_key=\\\"OPENAI_API_KEY\\\",\\n+ )\\n+ client = ModelClient()\\n+ with (\\n+ patch.object(client, \\\"_validate_provider\\\") as validate_provider,\\n+ patch.object(client, \\\"_open_provider\\\") as open_provider,\\n+ pytest.raises(RuntimeError, match=\\\"validated agent origin\\\"),\\n+ ):\\n+ client.fetch_json(agent, \\\"https://attacker.example/v1/models\\\")\\n+ validate_provider.assert_not_called()\\n+ open_provider.assert_not_called()\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"url\\\",\\n+ [\\n+ \\\"https://@api.openai.com/v1/models\\\",\\n+ \\\"https://user:@api.openai.com/v1/models\\\",\\n+ \\\"https://api.openai.com/v1/models#\\\",\\n+ ],\\n+)\\n+def test_model_discovery_rejects_empty_userinfo_and_fragment(url: str) -> None:\\n+ with pytest.raises(ValueError, match=\\\"credentials or a fragment\\\"):\\n+ _fetch_json(\\n+ url,\\n+ auth_scheme=\\\"Bearer\\\",\\n+ timeout=1.0,\\n+ credential_name=\\\"OPENAI_API_KEY\\\",\\n+ )\\n+\\n+\\n+def test_model_discovery_rejects_invalid_port_and_preserves_nondefault_port() -> None:\\n+ with pytest.raises(ValueError, match=\\\"invalid port\\\"):\\n+ _fetch_json(\\n+ \\\"https://api.openai.com:not-a-port/v1/models\\\",\\n+ auth_scheme=\\\"Bearer\\\",\\n+ timeout=1.0,\\n+ credential_name=\\\"\\\",\\n+ )\\n+\\n+ with patch.object(ModelClient, \\\"fetch_json\\\", return_value={}) as fetch_json:\\n+ _fetch_json(\\n+ \\\"https://api.openai.com:8443/v1/models\\\",\\n+ auth_scheme=\\\"Bearer\\\",\\n+ timeout=1.0,\\n+ credential_name=\\\"\\\",\\n+ )\\n+ assert fetch_json.call_args.args[0].base_url == \\\"https://api.openai.com:8443\\\"\\n+\\n+\\n+def test_fetch_json_rejects_empty_userinfo_and_fragment_before_transport() -> None:\\n+ register_credential(\\\"OPENAI_API_KEY\\\", \\\"openai-secret\\\")\\n+ agent = ModelAgent(\\n+ \\\"model_discovery_agent\\\",\\n+ \\\"model_catalog\\\",\\n+ \\\"https://api.openai.com/v1\\\",\\n+ credential_key=\\\"OPENAI_API_KEY\\\",\\n+ )\\n+ client = ModelClient()\\n+ with (\\n+ patch.object(client, \\\"_validate_provider\\\") as validate_provider,\\n+ patch.object(client, \\\"_open_provider\\\") as open_provider,\\n+ pytest.raises(RuntimeError, match=\\\"validated agent origin\\\"),\\n+ ):\\n+ client.fetch_json(agent, \\\"https://@api.openai.com/v1/models#\\\")\\n+ validate_provider.assert_not_called()\\n+ open_provider.assert_not_called()\\n+\\n+\\n def test_discover_bytez_parses_models_with_key_auth_scheme() -> None:\\n register_credential(\\\"BYTEZ_API_KEY\\\", \\\"bytez-secret\\\")\\n payload = {\\n@@ -127,7 +260,7 @@ def urlopen(request, timeout=None):\\n seen_requests.append(request)\\n return _Response(payload)\\n \\n- with patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen):\\n+ with _patched_provider_transport(urlopen):\\n discovered = discover_provider_models(BYTEZ_SOURCE)\\n \\n assert seen_requests[0].get_header(\\\"Authorization\\\") == \\\"Key bytez-secret\\\"\\n@@ -148,12 +281,15 @@ def urlopen(request, timeout=None):\\n raise urllib.error.URLError(\\\"connection refused\\\")\\n return _Response({\\\"data\\\": [{\\\"id\\\": \\\"meta/llama-3.3\\\"}]})\\n \\n- with patch(\\\"contextual_orchestrator.model_discovery.urllib.request.urlopen\\\", side_effect=urlopen):\\n+ with _patched_provider_transport(urlopen):\\n discovered, errors = discover_all_models((OPENAI_SOURCE, OPENROUTER_SOURCE))\\n \\n assert [m.model_id for m in discovered] == [\\\"meta/llama-3.3\\\"]\\n assert len(errors) == 1\\n assert errors[0].provider_name == \\\"openai\\\"\\n+ assert errors[0].error_code == \\\"transport_error\\\"\\n+ assert \\\"connection refused\\\" not in str(errors[0])\\n+ assert errors[0].__cause__ is None\\n \\n \\n def test_agent_id_for_is_two_word_snake_case() -> None:\" }, { \"sha\": \"f0c079bd09b6144a6b4c358380f702eeb259c292\", \"filename\": \"tests/test_model_judge.py\", \"status\": \"modified\", \"additions\": 42, \"deletions\": 7, \"changes\": 49, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_model_judge.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_model_judge.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_model_judge.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -117,6 +117,7 @@ def test_structured_model_judge_accepts() -> None:\\n result = orchestrator.conduct(MESSAGES)\\n assert result[\\\"verification\\\"][\\\"accepted\\\"] is True\\n assert result[\\\"verification\\\"][\\\"judge\\\"] == \\\"model\\\"\\n+ assert result[\\\"verification\\\"][\\\"judge_agent_id\\\"] == \\\"general_agent\\\"\\n assert client.calls == 5\\n assert result[\\\"answer\\\"] == \\\"step-output(4)\\\"\\n \\n@@ -276,7 +277,7 @@ def test_fast_mlsirm_adapter_accepts_contextual_judge_mode_keyword() -> None:\\n assert completion[\\\"mode\\\"] == \\\"conduct\\\"\\n \\n \\n-def test_fast_mlsirm_adapter_routes_structured_completion_through_gateway() -> None:\\n+def test_fast_mlsirm_adapter_keeps_structured_completion_to_one_provider_call() -> None:\\n orchestrator, _ = _orch(\\\"unused\\\")\\n adapter = orchestrator_module._FastMLSIJudgeAdapter(\\n orchestrator,\\n@@ -289,33 +290,57 @@ def test_fast_mlsirm_adapter_routes_structured_completion_through_gateway() -> N\\n \\\"json_schema\\\": {\\\"name\\\": \\\"judge\\\", \\\"strict\\\": True, \\\"schema\\\": {\\\"type\\\": \\\"object\\\"}},\\n }\\n with patch.object(\\n- orchestrator,\\n- \\\"proxy_completion\\\",\\n+ orchestrator.client,\\n+ \\\"proxy_send\\\",\\n return_value={\\n \\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": '{\\\"meets_threshold\\\":true,\\\"rationale\\\":\\\"ok\\\"}'}}],\\n \\\"usage\\\": {\\\"prompt_tokens\\\": 3, \\\"completion_tokens\\\": 2, \\\"total_tokens\\\": 5},\\n },\\n- ) as proxy:\\n+ ) as proxy_send:\\n completion = adapter.complete_structured(\\n [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"judge\\\"}],\\n mode=\\\"conduct\\\",\\n response_format=response_format,\\n )\\n \\n- proxy.assert_called_once_with(\\n+ proxy_send.assert_called_once_with(\\n+ orchestrator._agent(\\\"general_agent\\\"),\\n+ \\\"chat/completions\\\",\\n {\\n \\\"model\\\": \\\"model-x\\\",\\n \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"judge\\\"}],\\n- \\\"temperature\\\": orchestrator.client.temperature,\\n \\\"max_tokens\\\": orchestrator.client.max_output_tokens,\\n \\\"response_format\\\": response_format,\\n- }\\n+ \\\"stream\\\": False,\\n+ },\\n )\\n assert completion[\\\"answer\\\"] == '{\\\"meets_threshold\\\":true,\\\"rationale\\\":\\\"ok\\\"}'\\n assert completion[\\\"mode\\\"] == \\\"conduct\\\"\\n assert completion[\\\"trace\\\"][0][\\\"usage\\\"][\\\"total_tokens\\\"] == 5\\n \\n \\n+def test_fast_mlsirm_structured_adapter_omits_implicit_temperature() -> None:\\n+ orchestrator, _ = _orch(\\\"unused\\\")\\n+ orchestrator.client.temperature = None\\n+ adapter = orchestrator_module._FastMLSIJudgeAdapter(\\n+ orchestrator,\\n+ \\\"task\\\",\\n+ \\\"general_agent\\\",\\n+ )\\n+\\n+ with patch.object(\\n+ orchestrator.client,\\n+ \\\"proxy_send\\\",\\n+ return_value={\\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": \\\"{}\\\"}}]},\\n+ ) as proxy:\\n+ adapter.complete_structured(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"judge\\\"}],\\n+ response_format={\\\"type\\\": \\\"json_object\\\"},\\n+ )\\n+\\n+ assert \\\"temperature\\\" not in proxy.call_args.args[2]\\n+\\n+\\n def test_fast_mlsirm_judge_contract_does_not_pass_threshold_to_judge_call() -> None:\\n class _Judge:\\n def __init__(self, _orchestrator, *, mode: str, accept_threshold: float) -> None:\\n@@ -457,6 +482,16 @@ def test_model_judge_parser_rejects_oversized_reply() -> None:\\n _parse_model_judge_reply(\\\"x\\\" * 32_001)\\n \\n \\n+def test_model_judge_parser_hides_raw_provider_response() -> None:\\n+ raw_provider_response = \\\"provider-secret-response\\\"\\n+\\n+ with pytest.raises(ValueError, match=\\\"not valid JSON\\\") as error:\\n+ _parse_model_judge_reply(raw_provider_response)\\n+\\n+ assert raw_provider_response not in str(error.value)\\n+ assert error.value.__cause__ is None\\n+\\n+\\n def test_missing_fast_mlsirm_does_not_use_a_direct_judge_fallback() -> None:\\n orchestrator, _ = _orch(\\\"unused\\\")\\n with patch.object(orchestrator_module, \\\"_resolve_fast_mlsirm_components\\\", return_value=None), patch.object(\" }, { \"sha\": \"9ccc77d51a314f50e64a960c445f15d46699f4f8\", \"filename\": \"tests/test_model_strip_writeback_http_honesty.py\", \"status\": \"modified\", \"additions\": 14, \"deletions\": 16, \"changes\": 30, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_model_strip_writeback_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_model_strip_writeback_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_model_strip_writeback_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,4 +1,4 @@\\n-\\\"\\\"\\\"Model strip writeback so tools/Responses passthrough bind padded names.\\\"\\\"\\\"\\n+\\\"\\\"\\\"Model strip writeback keeps routed and structured requests bound to the pool.\\\"\\\"\\\"\\n \\n from __future__ import annotations\\n \\n@@ -82,8 +82,8 @@ def test_unit_model_rejects_blank() -> None:\\n assert getattr(exc, \\\"code\\\", None) == \\\"invalid_model\\\"\\n \\n \\n-def test_http_chat_tools_accepts_padded_model() -> None:\\n- \\\"\\\"\\\"Tools passthrough uses body.model for pool match — must see strip writeback.\\\"\\\"\\\"\\n+def test_http_chat_tools_rejects_padded_model_after_validation() -> None:\\n+ \\\"\\\"\\\"Validated tool requests stop at the explicit multi-agent contract boundary.\\\"\\\"\\\"\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -103,7 +103,8 @@ def test_http_chat_tools_accepts_padded_model() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -142,7 +143,7 @@ def test_http_responses_accepts_padded_model() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_tools_accepts_padded_model() -> None:\\n+def test_http_responses_tools_rejects_padded_model_after_validation() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -160,7 +161,8 @@ def test_http_responses_tools_accepts_padded_model() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -180,7 +182,7 @@ def test_http_completions_accepts_padded_model() -> None:\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_still_rejects_unknown_padded_model() -> None:\\n+def test_http_chat_tools_fail_closed_before_model_lookup() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -200,12 +202,8 @@ def test_http_chat_still_rejects_unknown_padded_model() -> None:\\n ],\\n },\\n )\\n- assert status == 400, body\\n- blob = json.dumps(body)\\n- assert \\\"invalid_request\\\" in blob or \\\"not available\\\" in blob\\n- assert \\\"no-such-model\\\" in blob\\n- # Must not echo leading pad after strip (buyer sees real id).\\n- assert \\\"' no-such-model '\\\" not in blob\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -214,10 +212,10 @@ def test_http_chat_still_rejects_unknown_padded_model() -> None:\\n if __name__ == \\\"__main__\\\":\\n test_unit_model_strip_writeback()\\n test_unit_model_rejects_blank()\\n- test_http_chat_tools_accepts_padded_model()\\n+ test_http_chat_tools_rejects_padded_model_after_validation()\\n test_http_chat_response_format_accepts_padded_model()\\n test_http_responses_accepts_padded_model()\\n- test_http_responses_tools_accepts_padded_model()\\n+ test_http_responses_tools_rejects_padded_model_after_validation()\\n test_http_completions_accepts_padded_model()\\n- test_http_chat_still_rejects_unknown_padded_model()\\n+ test_http_chat_tools_fail_closed_before_model_lookup()\\n print(\\\"ok\\\")\" }, { \"sha\": \"f51155b33de9d313f88fed4e0ebbd8837b66f043\", \"filename\": \"tests/test_multimodal_content_parts_shape_http_honesty.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_content_parts_shape_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_content_parts_shape_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_multimodal_content_parts_shape_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"vision\\\"))]\\n )\\n \\n \" }, { \"sha\": \"253cba1c80781f4c719f16e5e3fa7ac35500793f\", \"filename\": \"tests/test_multimodal_message_content_http_honesty.py\", \"status\": \"modified\", \"additions\": 1, \"deletions\": 1, \"changes\": 2, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_message_content_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_message_content_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_multimodal_message_content_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"vision\\\"))]\\n )\\n \\n \" }, { \"sha\": \"2c9153d48ef44d827ee44bc642bd5b88ca499290\", \"filename\": \"tests/test_multimodal_required_tag_boundary.py\", \"status\": \"added\", \"additions\": 55, \"deletions\": 0, \"changes\": 55, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_required_tag_boundary.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_required_tag_boundary.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_multimodal_required_tag_boundary.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,55 @@\\n+\\\"\\\"\\\"Regression for enforcing multimodal capability at the invocation boundary.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+\\n+\\n+class _RecordingClient:\\n+ \\\"\\\"\\\"Record which synthetic agent the invocation boundary actually calls.\\\"\\\"\\\"\\n+\\n+ def __init__(self) -> None:\\n+ self.calls: list[str] = []\\n+\\n+ def chat(self, agent: ModelAgent, _messages, **_kwargs) -> str:\\n+ self.calls.append(agent.id)\\n+ return agent.id\\n+\\n+\\n+def test_required_tags_filter_an_ineligible_explicit_primary() -> None:\\n+ \\\"\\\"\\\"A stale or direct caller cannot smuggle a text-only primary into image work.\\\"\\\"\\\"\\n+ text_agent = ModelAgent(\\n+ \\\"text_agent\\\",\\n+ \\\"text-model\\\",\\n+ tags=(\\\"reasoning\\\", \\\"writing\\\"),\\n+ priority=100,\\n+ )\\n+ vision_agent = ModelAgent(\\n+ \\\"vision_agent\\\",\\n+ \\\"vision-model\\\",\\n+ tags=(\\\"vision\\\", \\\"reasoning\\\", \\\"writing\\\"),\\n+ priority=1,\\n+ )\\n+ client = _RecordingClient()\\n+ orchestrator = TaskOrchestrator(\\n+ [text_agent, vision_agent],\\n+ client=client,\\n+ )\\n+\\n+ answer, served_agent_id, _usage = orchestrator._invoke(\\n+ text_agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Inspect the source image.\\\"}],\\n+ text=\\\"Inspect the source image.\\\",\\n+ role=\\\"worker\\\",\\n+ required_tags=(\\\"vision\\\",),\\n+ )\\n+\\n+ assert answer == \\\"vision_agent\\\"\\n+ assert served_agent_id == \\\"vision_agent\\\"\\n+ assert client.calls == [\\\"vision_agent\\\"]\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"8c20551b4605434f0d0de65e3555f26de617dad4\", \"filename\": \"tests/test_multimodal_workflow_evidence.py\", \"status\": \"added\", \"additions\": 162, \"deletions\": 0, \"changes\": 162, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_workflow_evidence.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_multimodal_workflow_evidence.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_multimodal_workflow_evidence.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,162 @@\\n+from __future__ import annotations\\n+\\n+from pathlib import Path\\n+import sys\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n+from contextual_orchestrator.orchestrator import _responses_to_chat_payload # noqa: E402\\n+\\n+\\n+IMAGE_PART = {\\n+ \\\"type\\\": \\\"image_url\\\",\\n+ \\\"image_url\\\": {\\n+ \\\"url\\\": \\\"data:image/png;base64,c3ludGhldGljLWZpeHR1cmU=\\\",\\n+ \\\"detail\\\": \\\"high\\\",\\n+ },\\n+}\\n+\\n+\\n+class RecordingClient:\\n+ \\\"\\\"\\\"Record synthetic provider calls without contacting a model.\\\"\\\"\\\"\\n+\\n+ def __init__(self, failing_agent_id: str | None = None) -> None:\\n+ self.failing_agent_id = failing_agent_id\\n+ self.calls: list[tuple[str, list[dict[str, object]]]] = []\\n+\\n+ def chat(self, agent: ModelAgent, messages, temperature: float = 0.2) -> str:\\n+ \\\"\\\"\\\"Return deterministic output, or fail the selected synthetic agent.\\\"\\\"\\\"\\n+ self.calls.append((agent.id, messages))\\n+ if agent.id == self.failing_agent_id:\\n+ raise RuntimeError(\\\"synthetic provider failure\\\")\\n+ return f\\\"{agent.id}:{len(self.calls)}\\\"\\n+\\n+\\n+def test_conduct_preserves_source_images_for_every_evidence_step() -> None:\\n+ client = RecordingClient()\\n+ orchestrator = TaskOrchestrator(\\n+ [ModelAgent(\\\"vision_agent\\\", \\\"mock-vision\\\", tags=(\\\"vision\\\",))],\\n+ client=client,\\n+ )\\n+\\n+ result = orchestrator.conduct(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"Read the table.\\\"}, IMAGE_PART]}]\\n+ )\\n+\\n+ assert result[\\\"mode\\\"] == \\\"conduct\\\"\\n+ assert len(client.calls) == 4\\n+ for agent_id, messages in client.calls:\\n+ assert agent_id == \\\"vision_agent\\\"\\n+ content = messages[-1][\\\"content\\\"]\\n+ assert isinstance(content, list)\\n+ assert content[-1] == IMAGE_PART\\n+ assert \\\"data:image\\\" not in content[0][\\\"text\\\"]\\n+\\n+\\n+def test_image_route_failover_never_uses_a_text_only_agent() -> None:\\n+ client = RecordingClient(failing_agent_id=\\\"vision_primary\\\")\\n+ orchestrator = TaskOrchestrator(\\n+ [\\n+ ModelAgent(\\\"text_agent\\\", \\\"mock-text\\\", tags=(\\\"reasoning\\\",), priority=100),\\n+ ModelAgent(\\\"vision_primary\\\", \\\"mock-vision-primary\\\", tags=(\\\"vision\\\",), priority=20),\\n+ ModelAgent(\\\"vision_backup\\\", \\\"mock-vision-backup\\\", tags=(\\\"vision\\\",), priority=10),\\n+ ],\\n+ client=client,\\n+ )\\n+\\n+ result = orchestrator.route_once(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"Read the image.\\\"}, IMAGE_PART]}]\\n+ )\\n+\\n+ assert result[\\\"answer\\\"].startswith(\\\"vision_backup:\\\")\\n+ assert [agent_id for agent_id, _ in client.calls] == [\\\"vision_primary\\\", \\\"vision_backup\\\"]\\n+\\n+\\n+def test_image_route_fails_before_io_without_a_vision_agent() -> None:\\n+ client = RecordingClient()\\n+ orchestrator = TaskOrchestrator(\\n+ [ModelAgent(\\\"text_agent\\\", \\\"mock-text\\\", tags=(\\\"reasoning\\\",))],\\n+ client=client,\\n+ )\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"required tags.*vision\\\"):\\n+ orchestrator.route_once(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"Read it.\\\"}, IMAGE_PART]}]\\n+ )\\n+\\n+ assert client.calls == []\\n+\\n+\\n+def test_responses_input_image_is_normalized_for_chat_orchestration() -> None:\\n+ payload = _responses_to_chat_payload(\\n+ {\\n+ \\\"model\\\": \\\"contextual-orchestrator\\\",\\n+ \\\"input\\\": [\\n+ {\\n+ \\\"type\\\": \\\"message\\\",\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\\"type\\\": \\\"input_text\\\", \\\"text\\\": \\\"Read the table.\\\"},\\n+ {\\n+ \\\"type\\\": \\\"input_image\\\",\\n+ \\\"image_url\\\": \\\"data:image/png;base64,c3ludGhldGljLWZpeHR1cmU=\\\",\\n+ \\\"detail\\\": \\\"high\\\",\\n+ },\\n+ ],\\n+ }\\n+ ],\\n+ }\\n+ )\\n+\\n+ assert payload[\\\"messages\\\"] == [\\n+ {\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"Read the table.\\\"},\\n+ IMAGE_PART,\\n+ ],\\n+ }\\n+ ]\\n+\\n+\\n+def test_responses_input_image_requires_a_url() -> None:\\n+ with pytest.raises(ValueError, match=\\\"image_url\\\"):\\n+ _responses_to_chat_payload(\\n+ {\\n+ \\\"input\\\": [\\n+ {\\n+ \\\"type\\\": \\\"message\\\",\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [{\\\"type\\\": \\\"input_image\\\", \\\"file_id\\\": \\\"synthetic-file\\\"}],\\n+ }\\n+ ]\\n+ }\\n+ )\\n+\\n+\\n+def test_responses_image_detail_is_normalized_or_rejected() -> None:\\n+ request = {\\n+ \\\"input\\\": [\\n+ {\\n+ \\\"type\\\": \\\"message\\\",\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\n+ \\\"type\\\": \\\"input_image\\\",\\n+ \\\"image_url\\\": {\\\"url\\\": \\\"https://example.invalid/synthetic.png\\\", \\\"detail\\\": None},\\n+ }\\n+ ],\\n+ }\\n+ ]\\n+ }\\n+\\n+ assert _responses_to_chat_payload(request)[\\\"messages\\\"][0][\\\"content\\\"][0] == {\\n+ \\\"type\\\": \\\"image_url\\\",\\n+ \\\"image_url\\\": {\\\"url\\\": \\\"https://example.invalid/synthetic.png\\\"},\\n+ }\\n+ request[\\\"input\\\"][0][\\\"content\\\"][0][\\\"image_url\\\"][\\\"detail\\\"] = \\\"pixel-perfect\\\"\\n+ with pytest.raises(ValueError, match=\\\"detail\\\"):\\n+ _responses_to_chat_payload(request)\" }, { \"sha\": \"ea97f5f1c2b36370a0d16412b0d74a5401476a12\", \"filename\": \"tests/test_openai_passthrough.py\", \"status\": \"modified\", \"additions\": 888, \"deletions\": 25, \"changes\": 913, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_openai_passthrough.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_openai_passthrough.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_openai_passthrough.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -1,9 +1,4 @@\\n-\\\"\\\"\\\"Full OpenAI passthrough: response_format / tools / the Responses API.\\n-\\n-Requests carrying provider features the multi-agent verifier cannot merge are\\n-proxied to one agent so the full provider response shape survives, while plain\\n-prompts keep the orchestration (routing/verification) path.\\n-\\\"\\\"\\\"\\n+\\\"\\\"\\\"OpenAI provider features remain inside multi-agent orchestration.\\\"\\\"\\\"\\n \\n from __future__ import annotations\\n \\n@@ -12,30 +7,101 @@\\n import threading\\n import urllib.error\\n import urllib.request\\n+from concurrent.futures import ThreadPoolExecutor\\n from pathlib import Path\\n+from unittest.mock import patch\\n \\n import pytest\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n-from contextual_orchestrator.server import SecurityConfig, build_server, responses_sse_body # noqa: E402\\n+from contextual_orchestrator.orchestrator import ( # noqa: E402\\n+ BudgetExceededError,\\n+ _FastMLSIJudgeAdapter,\\n+ _responses_to_chat_payload,\\n+ _responses_text_format_to_chat_response_format,\\n+ estimate_tokens,\\n+)\\n+from contextual_orchestrator.server import ( # noqa: E402\\n+ SecurityConfig,\\n+ build_server,\\n+ responses_sse_body,\\n+)\\n \\n \\n-def _build() -> TaskOrchestrator:\\n+def _build(*, budget_max_output_tokens: int | None = None) -> TaskOrchestrator:\\n return TaskOrchestrator(\\n agents=[\\n- ModelAgent(\\\"planner_agent\\\", \\\"mock-planner\\\", tags=(\\\"planning\\\", \\\"reasoning\\\")),\\n+ ModelAgent(\\\"planner_agent\\\", \\\"mock-planner\\\", tags=(\\\"planning\\\", \\\"reasoning\\\", \\\"vision\\\")),\\n ModelAgent(\\\"disabled_builder_duplicate\\\", \\\"mock-builder\\\", disabled=True),\\n ModelAgent(\\\"builder_agent\\\", \\\"mock-builder\\\", tags=(\\\"coding\\\", \\\"implementation\\\")),\\n ModelAgent(\\\"reviewer_agent\\\", \\\"mock-reviewer\\\", tags=(\\\"verification\\\", \\\"review\\\")),\\n ModelAgent(\\\"disabled_candidate\\\", \\\"disabled-model\\\", disabled=True),\\n- ]\\n+ ],\\n+ budget_max_output_tokens=budget_max_output_tokens,\\n )\\n \\n \\n # -- orchestrator-level ------------------------------------------------------\\n \\n+def test_responses_translation_preserves_input_image_content() -> None:\\n+ translated = _responses_to_chat_payload(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": [\\n+ {\\n+ \\\"type\\\": \\\"message\\\",\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\\"type\\\": \\\"input_text\\\", \\\"text\\\": \\\"Inspect this image\\\"},\\n+ {\\n+ \\\"type\\\": \\\"input_image\\\",\\n+ \\\"image_url\\\": \\\"data:image/png;base64,AA==\\\",\\n+ \\\"detail\\\": \\\"high\\\",\\n+ },\\n+ ],\\n+ }\\n+ ],\\n+ }\\n+ )\\n+\\n+ assert translated[\\\"messages\\\"] == [\\n+ {\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"Inspect this image\\\"},\\n+ {\\n+ \\\"type\\\": \\\"image_url\\\",\\n+ \\\"image_url\\\": {\\n+ \\\"url\\\": \\\"data:image/png;base64,AA==\\\",\\n+ \\\"detail\\\": \\\"high\\\",\\n+ },\\n+ },\\n+ ],\\n+ }\\n+ ]\\n+\\n+\\n+def test_final_synthesis_attaches_private_evidence_to_latest_user_turn() -> None:\\n+ result = _build().proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [\\n+ {\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Earlier question\\\"},\\n+ {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"Earlier answer\\\"},\\n+ {\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Current task\\\"},\\n+ ],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ messages = result[\\\"echo\\\"][\\\"messages\\\"]\\n+ assert messages[0][\\\"content\\\"] == \\\"Earlier question\\\"\\n+ assert messages[2][\\\"content\\\"].startswith(\\\"Current task\\\")\\n+ assert \\\"Verified workflow evidence\\\" in messages[2][\\\"content\\\"]\\n+\\n+\\n def test_proxy_completion_forwards_response_format_and_returns_full_shape() -> None:\\n orch = _build()\\n body = {\\n@@ -55,6 +121,8 @@ def test_proxy_completion_forwards_response_format_and_returns_full_shape() -> N\\n assert \\\"mode\\\" not in result[\\\"echo\\\"]\\n # model overridden to the selected agent's model.\\n assert result[\\\"model\\\"] in {\\\"mock-planner\\\", \\\"mock-builder\\\", \\\"mock-reviewer\\\"}\\n+ assert result[\\\"orchestration\\\"][\\\"mode\\\"] == \\\"conduct\\\"\\n+ assert result[\\\"orchestration\\\"][\\\"agent_count\\\"] == 4\\n \\n \\n def test_proxy_completion_forwards_tools() -> None:\\n@@ -64,6 +132,7 @@ def test_proxy_completion_forwards_tools() -> None:\\n {\\\"model\\\": \\\"mock-planner\\\", \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"call a tool\\\"}], \\\"tools\\\": tools}\\n )\\n assert result[\\\"echo\\\"][\\\"tools\\\"] == tools\\n+ assert result[\\\"orchestration\\\"][\\\"agent_count\\\"] == 4\\n \\n \\n def test_proxy_completion_honors_an_enabled_requested_worker_model() -> None:\\n@@ -104,6 +173,582 @@ def test_proxy_completion_rejects_disabled_and_malformed_requested_models() -> N\\n })\\n \\n \\n+def test_proxy_completion_blocks_before_structured_workflow_when_budget_is_exceeded() -> None:\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ _build(budget_max_output_tokens=0).proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+\\n+def test_structured_budget_stops_before_the_next_workflow_provider_call() -> None:\\n+ orchestrator = _build(budget_max_output_tokens=1)\\n+\\n+ with (\\n+ patch.object(orchestrator.client, \\\"chat\\\", wraps=orchestrator.client.chat) as chat,\\n+ patch.object(orchestrator.client, \\\"proxy_send\\\", wraps=orchestrator.client.proxy_send) as send,\\n+ pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"),\\n+ ):\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ assert chat.call_count == 1\\n+ send.assert_not_called()\\n+\\n+\\n+def test_model_client_request_settings_are_thread_local() -> None:\\n+ client = _build().client\\n+ previous_temperature = client.default_temperature\\n+ barrier = threading.Barrier(2)\\n+\\n+ def read_settings(temperature: float, max_tokens: int) -> tuple[float, int]:\\n+ with client.request_settings(temperature=temperature, max_output_tokens=max_tokens):\\n+ barrier.wait(timeout=5)\\n+ values = (\\n+ client._request_setting(\\\"temperature\\\", client.default_temperature),\\n+ client._request_setting(\\\"max_output_tokens\\\", client.max_output_tokens),\\n+ )\\n+ barrier.wait(timeout=5)\\n+ return values\\n+\\n+ with ThreadPoolExecutor(max_workers=2) as executor:\\n+ futures = [\\n+ executor.submit(read_settings, 0.1, 11),\\n+ executor.submit(read_settings, 0.9, 29),\\n+ ]\\n+ assert {future.result() for future in futures} == {(0.1, 11), (0.9, 29)}\\n+\\n+ assert client.default_temperature == previous_temperature\\n+ assert client.max_output_tokens == 2048\\n+\\n+ with client.request_settings(temperature=0.3):\\n+ with client.request_settings(temperature=0.4):\\n+ assert client._request_setting(\\\"temperature\\\", None) == 0.4\\n+ assert client._request_setting(\\\"temperature\\\", None) == 0.3\\n+\\n+\\n+def test_plain_proxy_completion_persists_reported_usage_before_next_budget_check() -> None:\\n+ orch = _build(budget_max_output_tokens=3)\\n+ raw = {\\n+ \\\"id\\\": \\\"chatcmpl-accounted\\\",\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"choices\\\": [\\n+ {\\n+ \\\"index\\\": 0,\\n+ \\\"message\\\": {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"accounted\\\"},\\n+ \\\"finish_reason\\\": \\\"stop\\\",\\n+ }\\n+ ],\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 2, \\\"completion_tokens\\\": 3, \\\"total_tokens\\\": 5},\\n+ \\\"echo\\\": {},\\n+ }\\n+ body = {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"plain passthrough\\\"}],\\n+ }\\n+\\n+ with patch.object(orch.client, \\\"proxy_send\\\", return_value=raw) as send:\\n+ assert orch.proxy_completion(body)[\\\"id\\\"] == \\\"chatcmpl-accounted\\\"\\n+ analytics = orch.spend_analytics()\\n+ assert analytics[\\\"totals\\\"][\\\"run_count\\\"] == 1\\n+ assert analytics[\\\"budget\\\"][\\\"spent_output_tokens\\\"] == 3\\n+ assert analytics[\\\"by_model\\\"] == [\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"estimated_output_tokens\\\": 3,\\n+ \\\"output_tokens\\\": 3,\\n+ \\\"usage_source\\\": \\\"reported\\\",\\n+ \\\"step_count\\\": 1,\\n+ \\\"price_per_million_usd\\\": None,\\n+ \\\"estimated_cost_usd\\\": None,\\n+ }\\n+ ]\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orch.proxy_completion(body)\\n+\\n+ assert send.call_count == 1\\n+\\n+\\n+def test_responses_tool_loop_usage_counts_toward_the_next_budget_check() -> None:\\n+ orch = _build(budget_max_output_tokens=3)\\n+ raw = {\\n+ \\\"id\\\": \\\"resp_accounted\\\",\\n+ \\\"object\\\": \\\"response\\\",\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"output\\\": [\\n+ {\\n+ \\\"type\\\": \\\"function_call\\\",\\n+ \\\"call_id\\\": \\\"call_1\\\",\\n+ \\\"name\\\": \\\"lookup\\\",\\n+ \\\"arguments\\\": \\\"{}\\\",\\n+ }\\n+ ],\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 2, \\\"output_tokens\\\": 3, \\\"total_tokens\\\": 5},\\n+ }\\n+ body = {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"call the tool\\\",\\n+ \\\"tools\\\": [{\\\"type\\\": \\\"function\\\", \\\"name\\\": \\\"lookup\\\"}],\\n+ }\\n+\\n+ with patch.object(orch.client, \\\"proxy_send\\\", return_value=raw) as send:\\n+ assert orch.proxy_completion(body, endpoint=\\\"responses\\\", single_agent=True) is raw\\n+ assert raw[\\\"usage\\\"] == {\\\"input_tokens\\\": 2, \\\"output_tokens\\\": 3, \\\"total_tokens\\\": 5}\\n+ analytics = orch.spend_analytics()\\n+ assert analytics[\\\"totals\\\"][\\\"reported_prompt_tokens\\\"] == 2\\n+ assert analytics[\\\"budget\\\"][\\\"spent_output_tokens\\\"] == 3\\n+ assert analytics[\\\"by_model\\\"][0][\\\"usage_source\\\"] == \\\"reported\\\"\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orch.proxy_completion(body, endpoint=\\\"responses\\\", single_agent=True)\\n+\\n+ assert send.call_count == 1\\n+\\n+\\n+def test_plain_proxy_completion_accounts_a_tool_only_response() -> None:\\n+ orch = _build()\\n+ raw = {\\n+ \\\"choices\\\": [\\n+ {\\n+ \\\"message\\\": {\\n+ \\\"content\\\": None,\\n+ \\\"tool_calls\\\": [{\\\"id\\\": \\\"call_1\\\", \\\"type\\\": \\\"function\\\"}],\\n+ }\\n+ }\\n+ ]\\n+ }\\n+\\n+ with patch.object(orch.client, \\\"proxy_send\\\", return_value=raw):\\n+ assert orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"call the tool\\\"}],\\n+ },\\n+ single_agent=True,\\n+ ) is raw\\n+\\n+ assert next(iter(orch._workflow_runs.values()))[\\\"answer\\\"] == \\\"\\\"\\n+\\n+\\n+def test_structured_provider_completion_rechecks_budget_before_final_provider_call() -> None:\\n+ orch = _build()\\n+ orch.budget_max_cost_usd = 1.0\\n+ orch.price_per_million = {\\\"mock-planner\\\": 1_000_000.0}\\n+ with patch.object(\\n+ orch,\\n+ \\\"conduct\\\",\\n+ return_value={\\n+ \\\"trace\\\": [\\n+ {\\n+ \\\"id\\\": \\\"worker\\\",\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"planner_agent\\\",\\n+ \\\"output\\\": \\\"verified\\\",\\n+ }\\n+ ]\\n+ },\\n+ ), patch.object(orch.client, \\\"proxy_send\\\") as send:\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ send.assert_not_called()\\n+\\n+\\n+def test_structured_provider_completion_counts_model_judge_cost() -> None:\\n+ orch = _build()\\n+ orch.budget_max_cost_usd = 3.0\\n+ orch.price_per_million = {\\\"mock-reviewer\\\": 1_000_000.0}\\n+ workflow = {\\n+ \\\"trace\\\": [{\\\"id\\\": 0, \\\"agent_id\\\": \\\"builder_agent\\\", \\\"role\\\": \\\"worker\\\", \\\"output\\\": \\\"ok\\\"}],\\n+ \\\"verification\\\": {\\n+ \\\"accepted\\\": True,\\n+ \\\"judge_agent_id\\\": \\\"reviewer_agent\\\",\\n+ \\\"judge_usage\\\": {\\\"completion_tokens\\\": 3},\\n+ },\\n+ }\\n+ with patch.object(orch, \\\"conduct\\\", return_value=workflow), patch.object(\\n+ orch.client, \\\"proxy_send\\\"\\n+ ) as send, pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ send.assert_not_called()\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"additional_spend\\\",\\n+ [\\n+ {\\\"additional_output_tokens\\\": True},\\n+ {\\\"additional_output_tokens\\\": -1},\\n+ {\\\"additional_cost_usd\\\": True},\\n+ {\\\"additional_cost_usd\\\": float(\\\"inf\\\")},\\n+ {\\\"additional_cost_usd\\\": -0.01},\\n+ ],\\n+)\\n+def test_in_flight_budget_rejects_invalid_usage(\\n+ additional_spend: dict[str, int | float | bool],\\n+) -> None:\\n+ with pytest.raises(ValueError, match=\\\"must be a non-negative\\\"):\\n+ _build()._raise_if_spend_budget_exceeded(**additional_spend)\\n+\\n+\\n+def test_structured_provider_completion_counts_in_flight_usage_before_synthesis() -> None:\\n+ orch = _build(budget_max_output_tokens=1)\\n+ workflow = {\\n+ \\\"trace\\\": [{\\\"id\\\": 0, \\\"agent_id\\\": \\\"builder_agent\\\", \\\"role\\\": \\\"worker\\\", \\\"output\\\": \\\"verified\\\"}],\\n+ \\\"verification\\\": {\\\"accepted\\\": True},\\n+ }\\n+ with patch.object(orch, \\\"conduct\\\", return_value=workflow), patch.object(\\n+ orch.client, \\\"proxy_send\\\"\\n+ ) as send:\\n+ with pytest.raises(BudgetExceededError, match=\\\"spend budget exceeded\\\"):\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ send.assert_not_called()\\n+\\n+\\n+def test_structured_provider_completion_enables_model_judge_boundary() -> None:\\n+ orch = _build()\\n+ workflow = {\\n+ \\\"trace\\\": [{\\\"id\\\": 0, \\\"agent_id\\\": \\\"builder_agent\\\", \\\"role\\\": \\\"worker\\\", \\\"output\\\": \\\"verified\\\"}],\\n+ \\\"verification\\\": {\\\"accepted\\\": True},\\n+ }\\n+ raw = {\\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": \\\"{}\\\"}}]}\\n+ with patch.object(orch, \\\"conduct\\\", return_value=workflow) as conduct, patch.object(\\n+ orch.client, \\\"proxy_send\\\", return_value=raw\\n+ ):\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ assert conduct.call_args.kwargs[\\\"judge\\\"] is True\\n+\\n+\\n+def test_native_responses_drops_chat_only_output_budget_aliases() -> None:\\n+ orch = _build()\\n+ raw = {\\\"object\\\": \\\"response\\\", \\\"output_text\\\": \\\"{}\\\", \\\"output\\\": []}\\n+ with patch.object(orch.client, \\\"proxy_send\\\", return_value=raw) as send:\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"extract JSON\\\",\\n+ \\\"max_tokens\\\": 11,\\n+ \\\"max_completion_tokens\\\": 13,\\n+ \\\"max_output_tokens\\\": 17,\\n+ \\\"text\\\": {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}},\\n+ },\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ forwarded = send.call_args.args[2]\\n+ assert \\\"max_tokens\\\" not in forwarded\\n+ assert \\\"max_completion_tokens\\\" not in forwarded\\n+ assert forwarded[\\\"max_output_tokens\\\"] == 17\\n+\\n+\\n+def test_structured_provider_completion_persists_final_synthesis_run() -> None:\\n+ orch = _build()\\n+ result = orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ run_id = result[\\\"orchestration\\\"][\\\"workflow_run_id\\\"]\\n+ run = orch.get_workflow_run(run_id)\\n+ assert run[\\\"trace\\\"][-1][\\\"role\\\"] == \\\"synthesizer\\\"\\n+ assert run[\\\"trace\\\"][-1][\\\"subtask\\\"] == \\\"Provider-facing structured synthesis\\\"\\n+ assert len(run[\\\"trace\\\"]) == 5\\n+ assert orch.spend_analytics()[\\\"totals\\\"][\\\"run_count\\\"] == 1\\n+\\n+\\n+def test_orchestrated_responses_synthesis_normalizes_provider_usage() -> None:\\n+ orch = _build()\\n+ raw = {\\n+ \\\"object\\\": \\\"response\\\",\\n+ \\\"output\\\": [{\\\"type\\\": \\\"message\\\", \\\"content\\\": [{\\\"type\\\": \\\"output_text\\\", \\\"text\\\": \\\"{}\\\"}]}],\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 11, \\\"output_tokens\\\": 7, \\\"total_tokens\\\": 18},\\n+ }\\n+ with patch.object(\\n+ orch,\\n+ \\\"conduct\\\",\\n+ return_value={\\n+ \\\"trace\\\": [{\\n+ \\\"id\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"worker_agent\\\",\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"output\\\": \\\"verified\\\",\\n+ }]\\n+ },\\n+ ), patch.object(orch.client, \\\"proxy_send\\\", return_value=raw):\\n+ result = orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"extract JSON\\\",\\n+ \\\"text\\\": {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}},\\n+ },\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ run = orch.get_workflow_run(result[\\\"orchestration\\\"][\\\"workflow_run_id\\\"])\\n+ assert run[\\\"trace\\\"][-1][\\\"usage\\\"] == {\\n+ \\\"input_tokens\\\": 11,\\n+ \\\"output_tokens\\\": 7,\\n+ \\\"total_tokens\\\": 18,\\n+ \\\"prompt_tokens\\\": 11,\\n+ \\\"completion_tokens\\\": 7,\\n+ }\\n+\\n+\\n+def test_orchestrated_responses_usage_counts_toward_spend_budget() -> None:\\n+ orch = _build(budget_max_output_tokens=100)\\n+ raw = {\\n+ \\\"object\\\": \\\"response\\\",\\n+ \\\"output_text\\\": \\\"{}\\\",\\n+ \\\"output\\\": [],\\n+ \\\"usage\\\": {\\\"input_tokens\\\": 4, \\\"output_tokens\\\": 5, \\\"total_tokens\\\": 9},\\n+ }\\n+\\n+ with patch.object(orch.client, \\\"proxy_send\\\", return_value=raw):\\n+ result = orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"extract JSON\\\",\\n+ \\\"text\\\": {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}},\\n+ },\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ run = orch.get_workflow_run(result[\\\"orchestration\\\"][\\\"workflow_run_id\\\"])\\n+ assert run[\\\"trace\\\"][-1][\\\"usage\\\"][\\\"prompt_tokens\\\"] == 4\\n+ assert run[\\\"trace\\\"][-1][\\\"usage\\\"][\\\"completion_tokens\\\"] == 5\\n+ expected_spend = sum(\\n+ row.get(\\\"usage\\\", {}).get(\\\"completion_tokens\\\", estimate_tokens(row[\\\"output\\\"]))\\n+ for row in run[\\\"trace\\\"]\\n+ )\\n+ assert orch.spend_analytics()[\\\"budget\\\"][\\\"spent_output_tokens\\\"] == expected_spend\\n+\\n+\\n+def test_orchestrated_spend_persists_model_judge_usage() -> None:\\n+ orch = _build()\\n+ workflow = {\\n+ \\\"trace\\\": [{\\\"id\\\": 0, \\\"agent_id\\\": \\\"builder_agent\\\", \\\"role\\\": \\\"worker\\\", \\\"output\\\": \\\"ok\\\"}],\\n+ \\\"verification\\\": {\\n+ \\\"accepted\\\": True,\\n+ \\\"judge_agent_id\\\": \\\"reviewer_agent\\\",\\n+ \\\"judge_usage\\\": {\\\"prompt_tokens\\\": 4, \\\"completion_tokens\\\": 3, \\\"total_tokens\\\": 7},\\n+ },\\n+ }\\n+ raw = {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"choices\\\": [{\\\"message\\\": {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"{}\\\"}}],\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 2, \\\"completion_tokens\\\": 5, \\\"total_tokens\\\": 7},\\n+ }\\n+\\n+ with patch.object(orch, \\\"conduct\\\", return_value=workflow), patch.object(\\n+ orch.client, \\\"proxy_send\\\", return_value=raw\\n+ ):\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ by_model = {row[\\\"model\\\"]: row for row in orch.spend_analytics()[\\\"by_model\\\"]}\\n+ assert by_model[\\\"mock-reviewer\\\"][\\\"output_tokens\\\"] == 3\\n+ assert by_model[\\\"mock-reviewer\\\"][\\\"usage_source\\\"] == \\\"reported\\\"\\n+ assert by_model[\\\"mock-reviewer\\\"][\\\"step_count\\\"] == 1\\n+\\n+\\n+def test_fast_mlsirm_structured_judge_uses_one_direct_provider_call() -> None:\\n+ orch = _build()\\n+ orch.client.temperature = 0.4\\n+ adapter = _FastMLSIJudgeAdapter(orch, text=\\\"judge\\\", judge=\\\"reviewer_agent\\\")\\n+ provider_response = {\\n+ \\\"choices\\\": [{\\\"message\\\": {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": '{\\\"decision\\\":\\\"pass\\\"}'}}],\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 2, \\\"completion_tokens\\\": 3, \\\"total_tokens\\\": 5},\\n+ }\\n+ with patch.object(orch, \\\"proxy_completion\\\", side_effect=AssertionError(\\\"judge must not recurse\\\")), patch.object(\\n+ orch.client, \\\"proxy_send\\\", return_value=provider_response\\n+ ) as send:\\n+ result = adapter.complete_structured(\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"judge this\\\"}],\\n+ response_format={\\\"type\\\": \\\"json_object\\\"},\\n+ )\\n+\\n+ send.assert_called_once()\\n+ assert result[\\\"answer\\\"] == '{\\\"decision\\\":\\\"pass\\\"}'\\n+ assert send.call_args.args[1] == \\\"chat/completions\\\"\\n+ assert send.call_args.args[2][\\\"response_format\\\"] == {\\\"type\\\": \\\"json_object\\\"}\\n+ assert send.call_args.args[2][\\\"temperature\\\"] == 0.4\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"text\\\", \\\"expected\\\"),\\n+ [\\n+ (None, None),\\n+ ({}, None),\\n+ ({\\\"format\\\": {\\\"type\\\": \\\"text\\\"}}, {\\\"type\\\": \\\"text\\\"}),\\n+ ({\\\"format\\\": {\\\"type\\\": \\\"xml\\\"}}, None),\\n+ ],\\n+)\\n+def test_responses_text_format_translation_handles_non_schema_shapes(\\n+ text: object,\\n+ expected: dict | None,\\n+) -> None:\\n+ assert _responses_text_format_to_chat_response_format(text) == expected\\n+\\n+\\n+def test_structured_provider_completion_rejects_empty_messages_and_disabled_model() -> None:\\n+ with pytest.raises(ValueError, match=\\\"non-empty messages\\\"):\\n+ _build().proxy_completion(\\n+ {\\\"messages\\\": [], \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"}}\\n+ )\\n+ with pytest.raises(RuntimeError, match=\\\"disabled\\\"):\\n+ _build().proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"disabled-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"messages\\\", \\\"first_role\\\"),\\n+ [\\n+ ([{\\\"role\\\": \\\"user\\\", \\\"content\\\": None}], \\\"user\\\"),\\n+ ([{\\\"role\\\": \\\"assistant\\\", \\\"content\\\": \\\"prior\\\"}], \\\"system\\\"),\\n+ ],\\n+)\\n+def test_structured_synthesis_injects_guidance_into_non_string_histories(\\n+ messages: list[dict],\\n+ first_role: str,\\n+) -> None:\\n+ orch = _build()\\n+ raw = {\\\"choices\\\": [{\\\"message\\\": {\\\"content\\\": \\\"done\\\"}}]}\\n+ with patch.object(\\n+ orch,\\n+ \\\"conduct\\\",\\n+ return_value={\\\"trace\\\": [], \\\"verification\\\": {\\\"accepted\\\": True}},\\n+ ), patch.object(orch.client, \\\"proxy_send\\\", return_value=raw) as send:\\n+ orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": messages,\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ sent_messages = send.call_args.args[2][\\\"messages\\\"]\\n+ assert sent_messages[0][\\\"role\\\"] == first_role\\n+ assert isinstance(sent_messages[0][\\\"content\\\"], str)\\n+\\n+\\n+def test_structured_synthesis_accounts_a_tool_only_response() -> None:\\n+ orch = _build()\\n+ raw = {\\n+ \\\"choices\\\": [\\n+ {\\n+ \\\"message\\\": {\\n+ \\\"content\\\": None,\\n+ \\\"tool_calls\\\": [{\\\"id\\\": \\\"call_1\\\", \\\"type\\\": \\\"function\\\"}],\\n+ }\\n+ }\\n+ ]\\n+ }\\n+ with patch.object(\\n+ orch,\\n+ \\\"conduct\\\",\\n+ return_value={\\\"trace\\\": [], \\\"verification\\\": {\\\"accepted\\\": True}},\\n+ ), patch.object(orch.client, \\\"proxy_send\\\", return_value=raw):\\n+ result = orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"call the tool\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ assert result[\\\"orchestration\\\"][\\\"mode\\\"] == \\\"conduct\\\"\\n+ run = orch.get_workflow_run(result[\\\"orchestration\\\"][\\\"workflow_run_id\\\"])\\n+ assert run[\\\"answer\\\"] == \\\"\\\"\\n+\\n+\\n+def test_structured_synthesis_preserves_tool_call_adjacency() -> None:\\n+ orch = _build()\\n+ intermediate_messages: list[list[dict]] = []\\n+ original_chat = orch.client.chat\\n+\\n+ def observe_chat(agent, messages, temperature=None, top_p=None):\\n+ del temperature, top_p\\n+ intermediate_messages.append(messages)\\n+ return original_chat(agent, messages)\\n+\\n+ with patch.object(orch.client, \\\"chat\\\", side_effect=observe_chat):\\n+ result = orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [\\n+ {\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"look up the value\\\"},\\n+ {\\n+ \\\"role\\\": \\\"assistant\\\",\\n+ \\\"content\\\": None,\\n+ \\\"tool_calls\\\": [\\n+ {\\n+ \\\"id\\\": \\\"call_1\\\",\\n+ \\\"type\\\": \\\"function\\\",\\n+ \\\"function\\\": {\\\"name\\\": \\\"lookup\\\", \\\"arguments\\\": \\\"{}\\\"},\\n+ }\\n+ ],\\n+ },\\n+ {\\\"role\\\": \\\"tool\\\", \\\"tool_call_id\\\": \\\"call_1\\\", \\\"content\\\": \\\"42\\\"},\\n+ ],\\n+ \\\"tools\\\": [{\\\"type\\\": \\\"function\\\", \\\"function\\\": {\\\"name\\\": \\\"lookup\\\", \\\"parameters\\\": {}}}],\\n+ }\\n+ )\\n+\\n+ final_messages = result[\\\"echo\\\"][\\\"messages\\\"]\\n+ assert final_messages[0][\\\"role\\\"] == \\\"user\\\"\\n+ assert final_messages[1][\\\"role\\\"] == \\\"assistant\\\"\\n+ assert final_messages[2][\\\"role\\\"] == \\\"tool\\\"\\n+ assert final_messages[1][\\\"tool_calls\\\"][0][\\\"id\\\"] == final_messages[2][\\\"tool_call_id\\\"]\\n+ assert intermediate_messages and all(messages[-1][\\\"role\\\"] == \\\"tool\\\" for messages in intermediate_messages)\\n+\\n+\\n def test_proxy_completion_responses_endpoint_returns_response_object() -> None:\\n orch = _build()\\n result = orch.proxy_completion(\\n@@ -115,6 +760,131 @@ def test_proxy_completion_responses_endpoint_returns_response_object() -> None:\\n assert result[\\\"echo\\\"][\\\"response_format\\\"] == {\\\"type\\\": \\\"text\\\"}\\n \\n \\n+def test_proxy_completion_responses_json_schema_is_orchestrated_and_native() -> None:\\n+ orch = _build()\\n+ body = {\\n+ \\\"input\\\": \\\"extract the visible region\\\",\\n+ \\\"instructions\\\": \\\"Keep the result concise.\\\",\\n+ \\\"metadata\\\": {\\\"tenant\\\": \\\"anonymous\\\", \\\"omitted\\\": None},\\n+ \\\"text\\\": {\\n+ \\\"format\\\": {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"name\\\": \\\"region_result\\\",\\n+ \\\"schema\\\": {\\\"type\\\": \\\"object\\\"},\\n+ \\\"strict\\\": True,\\n+ }\\n+ },\\n+ }\\n+ result = orch.proxy_completion(\\n+ body,\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ assert result[\\\"object\\\"] == \\\"response\\\"\\n+ assert result[\\\"echo\\\"][\\\"text\\\"] == body[\\\"text\\\"]\\n+ assert \\\"response_format\\\" not in result[\\\"echo\\\"]\\n+ assert result[\\\"echo\\\"][\\\"instructions\\\"] == \\\"Keep the result concise.\\\"\\n+ assert result[\\\"echo\\\"][\\\"metadata\\\"] == body[\\\"metadata\\\"]\\n+ assert result[\\\"orchestration\\\"][\\\"agent_count\\\"] == 4\\n+ run = orch.get_workflow_run(result[\\\"orchestration\\\"][\\\"workflow_run_id\\\"])\\n+ assert run[\\\"mode\\\"] == \\\"conduct\\\"\\n+\\n+\\n+def test_responses_structured_request_keeps_native_endpoint_and_input() -> None:\\n+ orch = _build()\\n+ calls: list[tuple[str, dict]] = []\\n+\\n+ def native_response(_agent, endpoint: str, payload: dict) -> dict:\\n+ calls.append((endpoint, payload))\\n+ return {\\\"object\\\": \\\"response\\\", \\\"output\\\": [], \\\"echo\\\": dict(payload)}\\n+\\n+ with patch.object(orch.client, \\\"proxy_send\\\", side_effect=native_response):\\n+ result = orch.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract the visible region\\\"}],\\n+ \\\"instructions\\\": \\\"Keep the original input order.\\\",\\n+ \\\"text\\\": {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}},\\n+ },\\n+ endpoint=\\\"responses\\\",\\n+ )\\n+\\n+ final_endpoint, final_payload = calls[-1]\\n+ assert final_endpoint == \\\"responses\\\"\\n+ assert final_payload[\\\"input\\\"] == [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract the visible region\\\"}]\\n+ assert \\\"Keep the original input order.\\\" in final_payload[\\\"instructions\\\"]\\n+ assert result[\\\"orchestration\\\"][\\\"workflow_run_id\\\"]\\n+\\n+\\n+def test_structured_chat_guidance_stays_in_original_user_turn() -> None:\\n+ result = _build().proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"extract JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+ assert result[\\\"echo\\\"][\\\"messages\\\"][0][\\\"role\\\"] == \\\"user\\\"\\n+ assert \\\"You are the final synthesizer\\\" in result[\\\"echo\\\"][\\\"messages\\\"][0][\\\"content\\\"]\\n+\\n+\\n+def test_structured_workflow_preserves_multimodal_input_for_final_synthesis() -> None:\\n+ result = _build().proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [\\n+ {\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"describe this\\\"},\\n+ {\\\"type\\\": \\\"image_url\\\", \\\"image_url\\\": {\\\"url\\\": \\\"data:image/png;base64,fixture\\\"}},\\n+ ],\\n+ }\\n+ ],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+ final_messages = result[\\\"echo\\\"][\\\"messages\\\"]\\n+ assert any(\\n+ isinstance(message.get(\\\"content\\\"), list)\\n+ and any(part.get(\\\"type\\\") == \\\"image_url\\\" for part in message[\\\"content\\\"])\\n+ for message in final_messages\\n+ )\\n+ assert final_messages[0][\\\"role\\\"] == \\\"user\\\"\\n+ assert any(\\n+ isinstance(part, dict) and part.get(\\\"type\\\") == \\\"text\\\"\\n+ and \\\"You are the final synthesizer\\\" in part.get(\\\"text\\\", \\\"\\\")\\n+ for part in final_messages[0][\\\"content\\\"]\\n+ )\\n+\\n+\\n+def test_structured_multimodal_rejects_an_explicit_text_only_model() -> None:\\n+ orchestrator = TaskOrchestrator(\\n+ [ModelAgent(\\\"text_agent\\\", \\\"text-model\\\", tags=(\\\"reasoning\\\",))]\\n+ )\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"lacks required tags: vision\\\"):\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"text-model\\\",\\n+ \\\"messages\\\": [\\n+ {\\n+ \\\"role\\\": \\\"user\\\",\\n+ \\\"content\\\": [\\n+ {\\\"type\\\": \\\"text\\\", \\\"text\\\": \\\"describe this\\\"},\\n+ {\\n+ \\\"type\\\": \\\"image_url\\\",\\n+ \\\"image_url\\\": {\\\"url\\\": \\\"data:image/png;base64,fixture\\\"},\\n+ },\\n+ ],\\n+ }\\n+ ],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ }\\n+ )\\n+\\n+\\n # -- HTTP server -------------------------------------------------------------\\n \\n def _post(url: str, payload: dict, token: str) -> tuple[int, dict]:\\n@@ -132,41 +902,134 @@ def _post(url: str, payload: dict, token: str) -> tuple[int, dict]:\\n \\n \\n def _serve() -> tuple[object, int, str]:\\n- token = \\\"passthrough_token\\\"\\n+ token = \\\"passthrough_token\\\" # noqa: S105 - synthetic HTTP fixture credential\\n server = build_server(_build(), port=0, security=SecurityConfig(auth_token=token))\\n threading.Thread(target=server.serve_forever, daemon=True).start()\\n return server, server.server_address[1], token\\n \\n \\n-def test_http_chat_completions_accepts_response_format_and_passes_through() -> None:\\n+def test_http_chat_completions_orchestrates_json_object_instead_of_passthrough() -> None:\\n+ orch = _build()\\n+ provider_calls: list[tuple[str, dict]] = []\\n+ original_proxy_send = orch.client.proxy_send\\n+\\n+ def observe_provider_call(agent, endpoint: str, payload: dict) -> dict:\\n+ provider_calls.append((endpoint, dict(payload)))\\n+ return original_proxy_send(agent, endpoint, payload)\\n+\\n+ with patch.object(orch.client, \\\"proxy_send\\\", side_effect=observe_provider_call):\\n+ token = \\\"structured_http_token\\\" # noqa: S105 - synthetic HTTP fixture credential\\n+ server = build_server(orch, port=0, security=SecurityConfig(auth_token=token))\\n+ threading.Thread(target=server.serve_forever, daemon=True).start()\\n+ try:\\n+ status, body = _post(\\n+ f\\\"http://127.0.0.1:{server.server_address[1]}/v1/chat/completions\\\",\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"give me JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ },\\n+ token,\\n+ )\\n+ finally:\\n+ server.shutdown()\\n+ assert status == 200\\n+ assert body[\\\"object\\\"] == \\\"chat.completion\\\"\\n+ assert json.loads(body[\\\"choices\\\"][0][\\\"message\\\"][\\\"content\\\"]) == {}\\n+ assert provider_calls[-1][0] == \\\"chat/completions\\\"\\n+ assert provider_calls[-1][1][\\\"response_format\\\"] == {\\\"type\\\": \\\"json_object\\\"}\\n+\\n+\\n+def test_http_chat_completions_omits_model_for_orchestrator_selection() -> None:\\n server, port, token = _serve()\\n- url = f\\\"http://127.0.0.1:{port}/v1/chat/completions\\\"\\n try:\\n status, body = _post(\\n- url,\\n+ f\\\"http://127.0.0.1:{port}/v1/chat/completions\\\",\\n {\\n- \\\"model\\\": \\\"mock-planner\\\",\\n \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"give me JSON\\\"}],\\n \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n },\\n token,\\n )\\n finally:\\n server.shutdown()\\n- assert status == 200 # previously rejected 400 'unknown_fields'\\n- assert body[\\\"object\\\"] == \\\"chat.completion\\\"\\n- assert body[\\\"echo\\\"][\\\"response_format\\\"] == {\\\"type\\\": \\\"json_object\\\"}\\n+ assert status == 200, body\\n+ assert body[\\\"model\\\"] == \\\"contextual-orchestrator\\\"\\n+ assert json.loads(body[\\\"choices\\\"][0][\\\"message\\\"][\\\"content\\\"]) == {}\\n+\\n+\\n+def test_http_structured_workflow_applies_sampling_and_records_orchestration() -> None:\\n+ orch = _build()\\n+ previous_temperature = orch.client.default_temperature\\n+ seen_sampling: list[tuple[float, int]] = []\\n+ original_chat = orch.client.chat\\n+\\n+ def observe_chat(agent, messages, temperature=None, top_p=None):\\n+ del top_p\\n+ seen_sampling.append((\\n+ orch.client._request_setting(\\\"temperature\\\", orch.client.default_temperature),\\n+ orch.client._request_setting(\\\"max_output_tokens\\\", orch.client.max_output_tokens),\\n+ ))\\n+ return original_chat(agent, messages, temperature=temperature)\\n+\\n+ with patch.object(orch.client, \\\"chat\\\", side_effect=observe_chat):\\n+ token = \\\"passthrough_sampling_token\\\" # noqa: S105 - synthetic HTTP fixture credential\\n+ server = build_server(orch, port=0, security=SecurityConfig(auth_token=token))\\n+ threading.Thread(target=server.serve_forever, daemon=True).start()\\n+ try:\\n+ status, body = _post(\\n+ f\\\"http://127.0.0.1:{server.server_address[1]}/v1/chat/completions\\\",\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"give me JSON\\\"}],\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ \\\"temperature\\\": 0.7,\\n+ \\\"max_tokens\\\": 17,\\n+ },\\n+ token,\\n+ )\\n+ finally:\\n+ server.shutdown()\\n+\\n+ assert status == 200, body\\n+ assert seen_sampling and all(item == (0.7, 17) for item in seen_sampling)\\n+ assert orch.client.default_temperature == previous_temperature\\n+ event_names = {event[\\\"event_name\\\"] for event in orch._analytics_events}\\n+ assert \\\"chat_completion_requested\\\" in event_names\\n+ assert \\\"chat_completion_passthrough\\\" not in event_names\\n \\n \\n def test_http_responses_endpoint_passes_through() -> None:\\n- server, port, token = _serve()\\n- url = f\\\"http://127.0.0.1:{port}/v1/responses\\\"\\n- try:\\n- status, body = _post(url, {\\\"model\\\": \\\"mock-planner\\\", \\\"input\\\": \\\"hello\\\"}, token)\\n- finally:\\n- server.shutdown()\\n+ orch = _build()\\n+ provider_calls: list[tuple[str, dict]] = []\\n+ original_proxy_send = orch.client.proxy_send\\n+\\n+ def observe_provider_call(agent, endpoint: str, payload: dict) -> dict:\\n+ provider_calls.append((endpoint, dict(payload)))\\n+ return original_proxy_send(agent, endpoint, payload)\\n+\\n+ request_body = {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"hello\\\",\\n+ \\\"text\\\": {\\\"format\\\": {\\\"type\\\": \\\"json_object\\\"}},\\n+ }\\n+ with patch.object(orch.client, \\\"proxy_send\\\", side_effect=observe_provider_call):\\n+ token = \\\"responses_http_token\\\" # noqa: S105 - synthetic HTTP fixture credential\\n+ server = build_server(orch, port=0, security=SecurityConfig(auth_token=token))\\n+ threading.Thread(target=server.serve_forever, daemon=True).start()\\n+ try:\\n+ status, body = _post(\\n+ f\\\"http://127.0.0.1:{server.server_address[1]}/v1/responses\\\",\\n+ request_body,\\n+ token,\\n+ )\\n+ finally:\\n+ server.shutdown()\\n assert status == 200\\n assert body[\\\"object\\\"] == \\\"response\\\"\\n+ assert provider_calls[-1][0] == \\\"responses\\\"\\n+ assert provider_calls[-1][1][\\\"input\\\"] == \\\"hello\\\"\\n+ assert provider_calls[-1][1][\\\"text\\\"] == request_body[\\\"text\\\"]\\n \\n \\n def test_http_models_endpoint_lists_configured_models() -> None:\\n@@ -220,5 +1083,5 @@ def test_http_plain_prompt_still_uses_orchestration_path() -> None:\\n server.shutdown()\\n assert status == 200\\n assert body[\\\"object\\\"] == \\\"chat.completion\\\"\\n- assert \\\"echo\\\" not in body # orchestration path, not passthrough\\n+ assert \\\"echo\\\" not in body # ordinary orchestration path\\n assert \\\"orchestration\\\" in body\" }, { \"sha\": \"bb2cddb3431eabb2ca69f8588298db0b64bd81af\", \"filename\": \"tests/test_openai_user_field_http_honesty.py\", \"status\": \"modified\", \"additions\": 2, \"deletions\": 2, \"changes\": 4, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_openai_user_field_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_openai_user_field_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_openai_user_field_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -3,11 +3,11 @@\\n from __future__ import annotations\\n \\n import json\\n+import sys\\n import threading\\n import urllib.error\\n import urllib.request\\n from pathlib import Path\\n-import sys\\n \\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n@@ -19,7 +19,7 @@\\n \\n def build() -> TaskOrchestrator:\\n return TaskOrchestrator(\\n- [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))]\\n+ [ModelAgent(\\\"general_agent\\\", \\\"mock-planner\\\", tags=(\\\"reasoning\\\", \\\"writing\\\", \\\"embedding\\\"))]\\n )\\n \\n \" }, { \"sha\": \"84d9391fbfa5208a10b735e15d11fd0559e13ab8\", \"filename\": \"tests/test_passthrough_one_shot_local_semantics.py\", \"status\": \"added\", \"additions\": 266, \"deletions\": 0, \"changes\": 266, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_passthrough_one_shot_local_semantics.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_passthrough_one_shot_local_semantics.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_passthrough_one_shot_local_semantics.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,266 @@\\n+\\\"\\\"\\\"Regression coverage for one-shot passthrough transport semantics.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from contextlib import contextmanager\\n+from copy import deepcopy\\n+import io\\n+import socket\\n+from unittest.mock import patch\\n+import urllib.error\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import ModelAgent\\n+from contextual_orchestrator.orchestrator import ModelClient\\n+\\n+\\n+def _local_agent() -> ModelAgent:\\n+ \\\"\\\"\\\"Build one authenticated loopback gateway agent.\\\"\\\"\\\"\\n+ return ModelAgent(\\n+ \\\"local_gateway_agent\\\",\\n+ \\\"local-model\\\",\\n+ base_url=\\\"local://127.0.0.1:8080/v1\\\",\\n+ local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\",\\n+ )\\n+\\n+\\n+def _chat_response(content: str = \\\"local answer\\\") -> dict[str, object]:\\n+ \\\"\\\"\\\"Return one minimal OpenAI-compatible chat response.\\\"\\\"\\\"\\n+ return {\\n+ \\\"id\\\": \\\"chatcmpl-one-shot\\\",\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"created\\\": 1,\\n+ \\\"model\\\": \\\"local-model\\\",\\n+ \\\"choices\\\": [\\n+ {\\n+ \\\"index\\\": 0,\\n+ \\\"message\\\": {\\\"role\\\": \\\"assistant\\\", \\\"content\\\": content},\\n+ \\\"finish_reason\\\": \\\"stop\\\",\\n+ }\\n+ ],\\n+ \\\"usage\\\": {\\n+ \\\"prompt_tokens\\\": 1,\\n+ \\\"completion_tokens\\\": 2,\\n+ \\\"total_tokens\\\": 3,\\n+ },\\n+ }\\n+\\n+\\n+def test_one_shot_local_responses_preserves_translation_and_concurrency_slot() -> None:\\n+ \\\"\\\"\\\"One-shot failover must retain the provider-neutral local Responses adapter.\\\"\\\"\\\"\\n+ client = ModelClient(max_retries=4, local_concurrency=3)\\n+ agent = _local_agent()\\n+ request = {\\n+ \\\"model\\\": \\\"local-model\\\",\\n+ \\\"input\\\": \\\"summarize the incident\\\",\\n+ \\\"metadata\\\": {\\\"tenant\\\": \\\"tenant-one\\\"},\\n+ }\\n+ original = deepcopy(request)\\n+ sent: list[tuple[str, dict[str, object]]] = []\\n+ slots: list[tuple[str, int, int]] = []\\n+\\n+ @contextmanager\\n+ def local_slot(\\n+ slot_agent: ModelAgent,\\n+ capacity: int,\\n+ timeout: int,\\n+ ):\\n+ slots.append((slot_agent.id, capacity, timeout))\\n+ yield\\n+\\n+ def send_raw(\\n+ sent_agent: ModelAgent,\\n+ endpoint: str,\\n+ payload: dict[str, object],\\n+ _destination: object,\\n+ ) -> dict[str, object]:\\n+ assert sent_agent is agent\\n+ sent.append((endpoint, deepcopy(payload)))\\n+ return _chat_response()\\n+\\n+ with (\\n+ patch.object(\\n+ client,\\n+ \\\"_validate_provider\\\",\\n+ return_value=(socket.AF_INET, (\\\"127.0.0.1\\\", 8080)),\\n+ ),\\n+ patch.object(client, \\\"_send_raw\\\", side_effect=send_raw),\\n+ patch(\\n+ \\\"contextual_orchestrator.orchestrator._local_provider_slot\\\",\\n+ side_effect=local_slot,\\n+ ),\\n+ client.request_settings(max_output_tokens=73),\\n+ ):\\n+ result = client.proxy_send_once(agent, \\\"responses\\\", request)\\n+\\n+ assert request == original\\n+ assert slots == [(agent.id, 3, client.timeout)]\\n+ assert len(sent) == 1\\n+ endpoint, payload = sent[0]\\n+ assert endpoint == \\\"chat/completions\\\"\\n+ assert payload[\\\"model\\\"] == \\\"local-model\\\"\\n+ assert payload[\\\"stream\\\"] is False\\n+ assert payload[\\\"max_tokens\\\"] == 73\\n+ assert payload[\\\"messages\\\"] == [\\n+ {\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"summarize the incident\\\"}\\n+ ]\\n+ assert result[\\\"object\\\"] == \\\"response\\\"\\n+ assert result[\\\"output_text\\\"] == \\\"local answer\\\"\\n+ assert result[\\\"metadata\\\"] == {\\\"tenant\\\": \\\"tenant-one\\\"}\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"requested_max_tokens\\\", \\\"expected_max_tokens\\\"), [(None, 57), (11, 11)]\\n+)\\n+def test_one_shot_local_chat_still_uses_model_switch_concurrency_slot(\\n+ requested_max_tokens: int | None,\\n+ expected_max_tokens: int,\\n+) -> None:\\n+ \\\"\\\"\\\"Removing same-model retries must not bypass local model-switch coordination.\\\"\\\"\\\"\\n+ client = ModelClient(max_retries=5, local_concurrency=2, max_output_tokens=57)\\n+ agent = _local_agent()\\n+ payload = {\\n+ \\\"model\\\": \\\"local-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"hello\\\"}],\\n+ \\\"stream\\\": False,\\n+ }\\n+ if requested_max_tokens is not None:\\n+ payload[\\\"max_tokens\\\"] = requested_max_tokens\\n+ original = deepcopy(payload)\\n+ slots: list[tuple[str, int, int]] = []\\n+ sends: list[tuple[str, dict[str, object]]] = []\\n+\\n+ @contextmanager\\n+ def local_slot(\\n+ slot_agent: ModelAgent,\\n+ capacity: int,\\n+ timeout: int,\\n+ ):\\n+ slots.append((slot_agent.id, capacity, timeout))\\n+ yield\\n+\\n+ def send_raw(\\n+ _agent: ModelAgent,\\n+ endpoint: str,\\n+ sent_payload: dict[str, object],\\n+ _destination: object,\\n+ ) -> dict[str, object]:\\n+ sends.append((endpoint, deepcopy(sent_payload)))\\n+ return _chat_response(\\\"hello\\\")\\n+\\n+ with (\\n+ patch.object(\\n+ client,\\n+ \\\"_validate_provider\\\",\\n+ return_value=(socket.AF_INET, (\\\"127.0.0.1\\\", 8080)),\\n+ ),\\n+ patch.object(client, \\\"_send_raw\\\", side_effect=send_raw),\\n+ patch(\\n+ \\\"contextual_orchestrator.orchestrator._local_provider_slot\\\",\\n+ side_effect=local_slot,\\n+ ),\\n+ ):\\n+ result = client.proxy_send_once(agent, \\\"chat/completions\\\", payload)\\n+\\n+ assert result[\\\"object\\\"] == \\\"chat.completion\\\"\\n+ assert slots == [(agent.id, 2, client.timeout)]\\n+ assert sends == [\\n+ (\\\"chat/completions\\\", {**payload, \\\"max_tokens\\\": expected_max_tokens})\\n+ ]\\n+ assert payload == original\\n+\\n+\\n+def test_one_shot_remote_passthrough_never_enters_same_agent_retry_wrapper() -> None:\\n+ \\\"\\\"\\\"A candidate attempt is exactly one raw provider request.\\\"\\\"\\\"\\n+ client = ModelClient(max_retries=7)\\n+ agent = ModelAgent(\\n+ \\\"remote_provider_agent\\\",\\n+ \\\"remote-model\\\",\\n+ base_url=\\\"https://provider.example/v1\\\",\\n+ credential_key=\\\"REMOTE_PROVIDER_KEY\\\",\\n+ )\\n+ rate_limit = urllib.error.HTTPError(\\n+ \\\"https://provider.example/v1/chat/completions\\\",\\n+ 429,\\n+ \\\"rate limited\\\",\\n+ None,\\n+ None,\\n+ )\\n+\\n+ with (\\n+ patch.object(\\n+ client,\\n+ \\\"_validate_provider\\\",\\n+ return_value=(socket.AF_INET, (\\\"93.184.216.34\\\", 443)),\\n+ ),\\n+ patch.object(client, \\\"_send_raw\\\", side_effect=rate_limit) as send_raw,\\n+ ):\\n+ with pytest.raises(urllib.error.HTTPError) as caught:\\n+ client.proxy_send_once(\\n+ agent,\\n+ \\\"chat/completions\\\",\\n+ {\\n+ \\\"model\\\": \\\"remote-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"hello\\\"}],\\n+ \\\"stream\\\": False,\\n+ },\\n+ )\\n+\\n+ assert caught.value is rate_limit\\n+ send_raw.assert_called_once()\\n+\\n+\\n+def test_one_shot_passthrough_keeps_optional_temperature_negotiation() -> None:\\n+ \\\"\\\"\\\"Capability negotiation removes only temperature without transient replay.\\\"\\\"\\\"\\n+\\n+ client = ModelClient(max_retries=7)\\n+ agent = ModelAgent(\\n+ \\\"remote_provider_agent\\\",\\n+ \\\"remote-model\\\",\\n+ base_url=\\\"https://provider.example/v1\\\",\\n+ credential_key=\\\"REMOTE_PROVIDER_KEY\\\",\\n+ )\\n+ unsupported = urllib.error.HTTPError(\\n+ \\\"https://provider.example/v1/responses\\\",\\n+ 400,\\n+ \\\"bad request\\\",\\n+ None,\\n+ io.BytesIO(\\n+ b\\\"Unsupported value: 'temperature' does not support 0.2; only the default is supported\\\"\\n+ ),\\n+ )\\n+ sent: list[dict[str, object]] = []\\n+\\n+ def send_raw(\\n+ _agent: ModelAgent,\\n+ _endpoint: str,\\n+ payload: dict[str, object],\\n+ _destination: object,\\n+ ) -> dict[str, object]:\\n+ sent.append(deepcopy(payload))\\n+ if len(sent) == 1:\\n+ raise unsupported\\n+ return _chat_response(\\\"negotiated\\\")\\n+\\n+ with (\\n+ patch.object(\\n+ client,\\n+ \\\"_validate_provider\\\",\\n+ return_value=(socket.AF_INET, (\\\"93.184.216.34\\\", 443)),\\n+ ),\\n+ patch.object(client, \\\"_send_raw\\\", side_effect=send_raw),\\n+ ):\\n+ result = client.proxy_send_once(\\n+ agent,\\n+ \\\"responses\\\",\\n+ {\\n+ \\\"model\\\": \\\"remote-model\\\",\\n+ \\\"input\\\": \\\"hello\\\",\\n+ \\\"temperature\\\": 0.2,\\n+ },\\n+ )\\n+\\n+ assert result[\\\"object\\\"] == \\\"chat.completion\\\"\\n+ assert sent[0][\\\"temperature\\\"] == 0.2\\n+ assert \\\"temperature\\\" not in sent[1]\" }, { \"sha\": \"7c858ef527ec0e31680407b462504903358500d0\", \"filename\": \"tests/test_passthrough_provider_failover.py\", \"status\": \"added\", \"additions\": 364, \"deletions\": 0, \"changes\": 364, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_passthrough_provider_failover.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_passthrough_provider_failover.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_passthrough_provider_failover.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,364 @@\\n+\\\"\\\"\\\"Regression coverage for cross-provider failover on raw OpenAI passthrough.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import urllib.error\\n+from copy import deepcopy\\n+from typing import Any\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import ModelAgent, TaskOrchestrator\\n+\\n+\\n+class SequencedProxyClient:\\n+ \\\"\\\"\\\"Record passthrough calls and return configured outcomes by agent id.\\\"\\\"\\\"\\n+\\n+ def __init__(self, outcomes: dict[str, dict[str, Any] | BaseException]) -> None:\\n+ self.outcomes = outcomes\\n+ self.calls: list[tuple[str, str, dict[str, Any]]] = []\\n+\\n+ def chat(\\n+ self,\\n+ agent: ModelAgent,\\n+ messages: list[dict[str, Any]],\\n+ temperature: float | None = None,\\n+ top_p: float | None = None,\\n+ ) -> str:\\n+ \\\"\\\"\\\"Supply deterministic workflow evidence before final passthrough.\\\"\\\"\\\"\\n+ del messages, temperature, top_p\\n+ return f\\\"verified evidence from {agent.id}\\\"\\n+\\n+ def take_usage(self) -> None:\\n+ \\\"\\\"\\\"Expose the client usage seam used by workflow accounting.\\\"\\\"\\\"\\n+ return\\n+\\n+ def proxy_send_once(\\n+ self,\\n+ agent: ModelAgent,\\n+ endpoint: str,\\n+ payload: dict[str, Any],\\n+ ) -> dict[str, Any]:\\n+ \\\"\\\"\\\"Perform one deterministic provider attempt for the requested agent.\\\"\\\"\\\"\\n+ self.calls.append((agent.id, endpoint, deepcopy(payload)))\\n+ outcome = self.outcomes[agent.id]\\n+ if isinstance(outcome, BaseException):\\n+ raise outcome\\n+ return deepcopy(outcome)\\n+\\n+ def proxy_send(\\n+ self,\\n+ agent: ModelAgent,\\n+ endpoint: str,\\n+ payload: dict[str, Any],\\n+ ) -> dict[str, Any]:\\n+ \\\"\\\"\\\"Expose the ordinary explicit-model transport seam.\\\"\\\"\\\"\\n+ return self.proxy_send_once(agent, endpoint, payload)\\n+\\n+\\n+def _http_error(status: int, message: str) -> urllib.error.HTTPError:\\n+ \\\"\\\"\\\"Return a realistic provider HTTP error for passthrough routing tests.\\\"\\\"\\\"\\n+ return urllib.error.HTTPError(\\n+ \\\"https://provider.example/v1/chat/completions\\\",\\n+ status,\\n+ message,\\n+ None,\\n+ None,\\n+ )\\n+\\n+\\n+def _rate_limit() -> urllib.error.HTTPError:\\n+ \\\"\\\"\\\"Return a realistic transient provider HTTP 429 error.\\\"\\\"\\\"\\n+ return _http_error(429, \\\"rate limited\\\")\\n+\\n+\\n+def _wrapped(error: BaseException) -> RuntimeError:\\n+ \\\"\\\"\\\"Return a provider-style wrapper with the original failure as its cause.\\\"\\\"\\\"\\n+ try:\\n+ raise RuntimeError(\\\"provider wrapper\\\") from error\\n+ except RuntimeError as wrapper:\\n+ return wrapper\\n+\\n+\\n+def _suppressed_wrapper(error: BaseException) -> RuntimeError:\\n+ \\\"\\\"\\\"Return a terminal wrapper whose incidental context is explicitly hidden.\\\"\\\"\\\"\\n+ try:\\n+ raise error\\n+ except BaseException:\\n+ try:\\n+ raise RuntimeError(\\\"terminal provider wrapper\\\") from None\\n+ except RuntimeError as wrapper:\\n+ return wrapper\\n+\\n+\\n+def _build(client: SequencedProxyClient) -> TaskOrchestrator:\\n+ \\\"\\\"\\\"Build a deterministic two-provider pool for passthrough tests.\\\"\\\"\\\"\\n+ return TaskOrchestrator(\\n+ [\\n+ ModelAgent(\\n+ \\\"primary_agent\\\",\\n+ \\\"primary-model\\\",\\n+ tags=(\\\"coding\\\", \\\"implementation\\\", \\\"security\\\", \\\"review\\\"),\\n+ priority=10,\\n+ ),\\n+ ModelAgent(\\n+ \\\"fallback_agent\\\",\\n+ \\\"fallback-model\\\",\\n+ tags=(\\\"coding\\\", \\\"implementation\\\", \\\"security\\\", \\\"review\\\"),\\n+ priority=1,\\n+ ),\\n+ ],\\n+ client=client,\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\\"single_agent\\\", [False, True])\\n+def test_429_advances_immediately_and_preserves_tool_request(\\n+ single_agent: bool,\\n+) -> None:\\n+ \\\"\\\"\\\"A 429 must advance to another model without replaying the saturated one.\\\"\\\"\\\"\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": _rate_limit(),\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+ tools = [\\n+ {\\n+ \\\"type\\\": \\\"function\\\",\\n+ \\\"function\\\": {\\\"name\\\": \\\"inspect\\\", \\\"parameters\\\": {\\\"type\\\": \\\"object\\\"}},\\n+ }\\n+ ]\\n+ body = {\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review this security-sensitive code\\\"}],\\n+ \\\"tools\\\": tools,\\n+ \\\"tool_choice\\\": \\\"auto\\\",\\n+ \\\"response_format\\\": {\\\"type\\\": \\\"json_object\\\"},\\n+ \\\"reasoning_effort\\\": \\\"auto\\\",\\n+ \\\"mode\\\": \\\"auto\\\",\\n+ \\\"stream\\\": True,\\n+ }\\n+ original = deepcopy(body)\\n+\\n+ result = orchestrator.proxy_completion(body, single_agent=single_agent)\\n+\\n+ assert result[\\\"model\\\"] == \\\"fallback-model\\\"\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\", \\\"fallback_agent\\\"]\\n+ assert client.calls[0][2][\\\"model\\\"] == \\\"primary-model\\\"\\n+ assert client.calls[1][2][\\\"model\\\"] == \\\"fallback-model\\\"\\n+ assert client.calls[1][2][\\\"tools\\\"] == tools\\n+ assert client.calls[1][2][\\\"tool_choice\\\"] == \\\"auto\\\"\\n+ assert client.calls[1][2][\\\"response_format\\\"] == {\\\"type\\\": \\\"json_object\\\"}\\n+ assert \\\"reasoning_effort\\\" not in client.calls[1][2]\\n+ assert client.calls[1][2][\\\"stream\\\"] is False\\n+ assert \\\"mode\\\" not in client.calls[1][2]\\n+ assert body == original\\n+\\n+\\n+@pytest.mark.parametrize(\\\"status\\\", [404, 410])\\n+def test_virtual_request_advances_when_discovered_candidate_disappears(\\n+ status: int,\\n+) -> None:\\n+ \\\"\\\"\\\"A stale discovered candidate must not block another compatible worker.\\\"\\\"\\\"\\n+ unavailable = _http_error(status, \\\"model unavailable\\\")\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": unavailable,\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ result = orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"contextual-orchestrator\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review code\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert result[\\\"model\\\"] == \\\"fallback-model\\\"\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\", \\\"fallback_agent\\\"]\\n+\\n+\\n+@pytest.mark.parametrize(\\\"provider_error\\\", [_rate_limit(), _http_error(410, \\\"gone\\\")])\\n+def test_virtual_request_unwraps_provider_failure_causes(\\n+ provider_error: BaseException,\\n+) -> None:\\n+ \\\"\\\"\\\"Provider SDK wrappers must not hide a bounded fallback signal.\\\"\\\"\\\"\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": _wrapped(provider_error),\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ result = orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"contextual-orchestrator\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review code\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert result[\\\"model\\\"] == \\\"fallback-model\\\"\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\", \\\"fallback_agent\\\"]\\n+\\n+\\n+def test_suppressed_provider_context_does_not_authorize_failover() -> None:\\n+ \\\"\\\"\\\"An explicitly suppressed prior 429 must not override a terminal wrapper.\\\"\\\"\\\"\\n+ terminal = _suppressed_wrapper(_rate_limit())\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": terminal,\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"terminal provider wrapper\\\") as caught:\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"contextual-orchestrator\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review code\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert caught.value is terminal\\n+ assert terminal.__suppress_context__ is True\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\"]\\n+\\n+\\n+@pytest.mark.parametrize(\\\"status\\\", [404, 410])\\n+def test_explicit_concrete_model_remains_sticky_when_unavailable(status: int) -> None:\\n+ \\\"\\\"\\\"An explicit concrete model must never be silently replaced.\\\"\\\"\\\"\\n+ unavailable = _http_error(status, \\\"model unavailable\\\")\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": unavailable,\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ with pytest.raises(urllib.error.HTTPError) as caught:\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"primary-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review code\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert caught.value is unavailable\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\"]\\n+\\n+\\n+def test_explicit_concrete_model_preserves_rate_limit_error() -> None:\\n+ \\\"\\\"\\\"A concrete-model 429 must remain the provider's original error.\\\"\\\"\\\"\\n+ rate_limit = _rate_limit()\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": rate_limit,\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ with pytest.raises(urllib.error.HTTPError) as caught:\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"model\\\": \\\"primary-model\\\",\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review code\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert caught.value is rate_limit\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\"]\\n+\\n+\\n+def test_non_transient_request_error_is_not_replayed_to_another_provider() -> None:\\n+ \\\"\\\"\\\"A provider 400 is caller/configuration evidence, not a failover signal.\\\"\\\"\\\"\\n+ bad_request = _http_error(400, \\\"unsupported request\\\")\\n+ client = SequencedProxyClient(\\n+ {\\n+ \\\"primary_agent\\\": bad_request,\\n+ \\\"fallback_agent\\\": {\\n+ \\\"object\\\": \\\"chat.completion\\\",\\n+ \\\"model\\\": \\\"fallback-model\\\",\\n+ \\\"choices\\\": [],\\n+ },\\n+ }\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ with pytest.raises(urllib.error.HTTPError) as caught:\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"invalid request\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert caught.value is bad_request\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\"]\\n+ assert not orchestrator._circuit_open(\\\"primary_agent\\\")\\n+\\n+\\n+def test_all_transient_candidate_failures_chain_final_provider_error() -> None:\\n+ \\\"\\\"\\\"Exhausted transient candidates fail closed with the final provider cause.\\\"\\\"\\\"\\n+ first = _rate_limit()\\n+ final = _http_error(503, \\\"fallback unavailable\\\")\\n+ client = SequencedProxyClient(\\n+ {\\\"primary_agent\\\": first, \\\"fallback_agent\\\": final}\\n+ )\\n+ orchestrator = _build(client)\\n+\\n+ with pytest.raises(RuntimeError, match=\\\"all 2 candidate agents failed\\\") as caught:\\n+ orchestrator.proxy_completion(\\n+ {\\n+ \\\"messages\\\": [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"review code\\\"}],\\n+ \\\"tools\\\": [],\\n+ }\\n+ )\\n+\\n+ assert caught.value.__cause__ is final\\n+ assert [call[0] for call in client.calls] == [\\\"primary_agent\\\", \\\"fallback_agent\\\"]\\n+\\n+\\n+def test_cli_server_constructs_the_failover_orchestrator() -> None:\\n+ \\\"\\\"\\\"The production ``python -m`` server path must use provider failover.\\\"\\\"\\\"\\n+ from contextual_orchestrator import __main__ as cli\\n+ from contextual_orchestrator.passthrough_failover import (\\n+ TaskOrchestrator as FailoverTaskOrchestrator,\\n+ )\\n+\\n+ assert cli.TaskOrchestrator is FailoverTaskOrchestrator\" }, { \"sha\": \"e58c64b610b13cf66a224859aedf64f01140803a\", \"filename\": \"tests/test_persistence.py\", \"status\": \"modified\", \"additions\": 49, \"deletions\": 0, \"changes\": 49, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_persistence.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_persistence.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_persistence.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -23,6 +23,30 @@ def _orch(state_db: str | None = None) -> TaskOrchestrator:\\n return TaskOrchestrator([ModelAgent(\\\"general_agent\\\", \\\"mock\\\", tags=(\\\"reasoning\\\", \\\"writing\\\"))], state_db=state_db)\\n \\n \\n+def _run_record(index: int) -> dict:\\n+ return {\\n+ \\\"workflow_run_id\\\": f\\\"run_retention_{index}\\\",\\n+ \\\"created_at\\\": index,\\n+ \\\"mode\\\": \\\"route\\\",\\n+ \\\"policy_mode\\\": \\\"route\\\",\\n+ \\\"prompt_text\\\": \\\"abcd\\\",\\n+ \\\"answer\\\": \\\"done\\\",\\n+ \\\"trace\\\": [\\n+ {\\n+ \\\"id\\\": 0,\\n+ \\\"role\\\": \\\"worker\\\",\\n+ \\\"agent_id\\\": \\\"general_agent\\\",\\n+ \\\"subtask\\\": \\\"Direct route\\\",\\n+ \\\"access\\\": [],\\n+ \\\"output\\\": \\\"done\\\",\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 1, \\\"completion_tokens\\\": 1},\\n+ }\\n+ ],\\n+ \\\"policy_snapshot\\\": {},\\n+ \\\"verification\\\": {\\\"accepted\\\": True},\\n+ }\\n+\\n+\\n def test_runs_audit_analytics_survive_restart() -> None:\\n with tempfile.TemporaryDirectory() as directory:\\n db = os.path.join(directory, \\\"state.db\\\")\\n@@ -118,6 +142,31 @@ def test_stream_reload_respects_deque_maxlen() -> None:\\n second.close()\\n \\n \\n+def test_workflow_reload_bounds_raw_records_and_preserves_spend() -> None:\\n+ with tempfile.TemporaryDirectory() as directory:\\n+ db = os.path.join(directory, \\\"state.db\\\")\\n+ first = _orch(db)\\n+ run_count = first._run_order.maxlen + 2\\n+ for index in range(run_count):\\n+ first._persist_workflow_run(_run_record(index))\\n+ assert len(first._workflow_runs) == first._run_order.maxlen\\n+ assert first.spend_analytics()[\\\"totals\\\"][\\\"run_count\\\"] == run_count\\n+ first.close()\\n+\\n+ second = _orch(db)\\n+ try:\\n+ assert len(second._workflow_runs) == second._run_order.maxlen\\n+ assert \\\"run_retention_0\\\" not in second._workflow_runs\\n+ assert \\\"run_retention_129\\\" in second._workflow_runs\\n+ report = second.spend_analytics()\\n+ assert report[\\\"totals\\\"][\\\"run_count\\\"] == run_count\\n+ assert report[\\\"totals\\\"][\\\"estimated_output_tokens\\\"] == run_count\\n+ assert report[\\\"totals\\\"][\\\"reported_prompt_tokens\\\"] == run_count\\n+ assert len(second._store.load(\\\"workflow_run\\\")) == run_count\\n+ finally:\\n+ second.close()\\n+\\n+\\n if __name__ == \\\"__main__\\\":\\n for name, fn in sorted(globals().items()):\\n if name.startswith(\\\"test_\\\") and callable(fn):\" }, { \"sha\": \"e99335437d547ef6a2d7a934efb6433eed52128d\", \"filename\": \"tests/test_pr765_review_regressions.py\", \"status\": \"added\", \"additions\": 320, \"deletions\": 0, \"changes\": 320, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_pr765_review_regressions.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_pr765_review_regressions.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_pr765_review_regressions.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,320 @@\\n+\\\"\\\"\\\"Regression contracts for the PR 765 review findings.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+from concurrent.futures import ThreadPoolExecutor\\n+import json\\n+import threading\\n+import urllib.error\\n+from unittest.mock import patch\\n+\\n+import pytest\\n+\\n+from contextual_orchestrator import orchestrator as orchestration\\n+from contextual_orchestrator import server\\n+from contextual_orchestrator.orchestrator import ModelAgent, ModelClient, TaskOrchestrator\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"schema\\\",\\n+ [\\n+ {\\\"type\\\": \\\"object\\\", \\\"properties\\\": []},\\n+ {\\\"type\\\": \\\"object\\\", \\\"required\\\": [\\\"answer\\\", 7]},\\n+ {\\\"type\\\": \\\"array\\\", \\\"items\\\": []},\\n+ ],\\n+)\\n+def test_malformed_json_schema_is_rejected_before_response_validation(schema) -> None:\\n+ with pytest.raises(server.RequestError) as captured:\\n+ server._validate_chat_response_format(\\n+ {\\n+ \\\"response_format\\\": {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\\"name\\\": \\\"answer\\\", \\\"schema\\\": schema},\\n+ }\\n+ }\\n+ )\\n+ assert captured.value.status == 400\\n+ assert captured.value.code == \\\"invalid_response_format\\\"\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"field\\\",\\n+ [\\n+ \\\"temperature\\\",\\n+ \\\"top_p\\\",\\n+ \\\"presence_penalty\\\",\\n+ \\\"frequency_penalty\\\",\\n+ \\\"seed\\\",\\n+ \\\"stop\\\",\\n+ \\\"logit_bias\\\",\\n+ \\\"logprobs\\\",\\n+ \\\"top_logprobs\\\",\\n+ ],\\n+)\\n+def test_unapplied_responses_controls_fail_closed(field) -> None:\\n+ values = {\\n+ \\\"temperature\\\": 0.2,\\n+ \\\"top_p\\\": 0.9,\\n+ \\\"presence_penalty\\\": 0.1,\\n+ \\\"frequency_penalty\\\": -0.1,\\n+ \\\"seed\\\": 42,\\n+ \\\"stop\\\": [\\\"END\\\"],\\n+ \\\"logit_bias\\\": {\\\"1\\\": 1},\\n+ \\\"logprobs\\\": True,\\n+ \\\"top_logprobs\\\": 3,\\n+ }\\n+ with pytest.raises(server.RequestError) as captured:\\n+ server._reject_responses_orchestration_controls({field: values[field]})\\n+ assert captured.value.status == 422\\n+ assert captured.value.code == \\\"unsupported_responses_orchestration_controls\\\"\\n+ assert captured.value.detail == {\\\"fields\\\": [field]}\\n+\\n+\\n+def test_empty_responses_controls_remain_omit_equivalent() -> None:\\n+ server._reject_responses_orchestration_controls(\\n+ {\\n+ \\\"temperature\\\": None,\\n+ \\\"stop\\\": \\\"\\\",\\n+ \\\"logit_bias\\\": {},\\n+ \\\"logprobs\\\": False,\\n+ \\\"top_logprobs\\\": 0,\\n+ }\\n+ )\\n+\\n+\\n+def test_internal_chat_preserves_explicit_temperature(monkeypatch) -> None:\\n+ \\\"\\\"\\\"An explicit caller sampling control remains an honest provider passthrough.\\\"\\\"\\\"\\n+ client = ModelClient()\\n+ agent = ModelAgent(\\n+ id=\\\"chat_worker\\\",\\n+ model=\\\"provider/model\\\",\\n+ base_url=\\\"https://gateway.example.com\\\",\\n+ credential_key=\\\"\\\",\\n+ )\\n+ captured: dict[str, object] = {}\\n+\\n+ monkeypatch.setattr(client, \\\"_validate_provider\\\", lambda _agent: None)\\n+\\n+ def capture_payload(_agent, payload, _destination=None, *, timeout=None):\\n+ del timeout\\n+ captured.update(payload)\\n+ return \\\"OK\\\"\\n+\\n+ monkeypatch.setattr(client, \\\"_send\\\", capture_payload)\\n+\\n+ assert client.chat(\\n+ agent,\\n+ [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Sample.\\\"}],\\n+ temperature=0.2,\\n+ ) == \\\"OK\\\"\\n+ assert captured[\\\"temperature\\\"] == 0.2\\n+\\n+\\n+def test_request_sampling_settings_are_isolated_between_threads(monkeypatch) -> None:\\n+ client = ModelClient(max_retries=0)\\n+ agent = ModelAgent(\\n+ id=\\\"concurrent_worker\\\",\\n+ model=\\\"provider/model\\\",\\n+ base_url=\\\"https://gateway.example.com\\\",\\n+ credential_key=\\\"\\\",\\n+ )\\n+ barrier = threading.Barrier(2)\\n+ observed: list[tuple[float, int]] = []\\n+ monkeypatch.setattr(client, \\\"_validate_provider\\\", lambda _agent: None)\\n+\\n+ def capture_payload(_agent, payload, _destination=None, *, timeout=None):\\n+ del timeout\\n+ observed.append((payload[\\\"temperature\\\"], payload[\\\"max_tokens\\\"]))\\n+ barrier.wait(timeout=5)\\n+ return \\\"OK\\\"\\n+\\n+ monkeypatch.setattr(client, \\\"_send\\\", capture_payload)\\n+\\n+ def call(temperature: float, max_tokens: int) -> str:\\n+ with client.request_settings(\\n+ temperature=temperature,\\n+ max_output_tokens=max_tokens,\\n+ ):\\n+ return client.chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Sample.\\\"}])\\n+\\n+ with ThreadPoolExecutor(max_workers=2) as pool:\\n+ replies = list(pool.map(lambda row: call(*row), [(0.1, 11), (0.9, 29)]))\\n+\\n+ assert replies == [\\\"OK\\\", \\\"OK\\\"]\\n+ assert set(observed) == {(0.1, 11), (0.9, 29)}\\n+ assert client.default_temperature is None\\n+ assert client.max_output_tokens == 2048\\n+\\n+ with client.request_settings(temperature=0.3):\\n+ with client.request_settings(temperature=0.4):\\n+ assert client._request_setting(\\\"temperature\\\", None) == 0.4\\n+ assert client._request_setting(\\\"temperature\\\", None) == 0.3\\n+\\n+ streamed: list[tuple[float, int]] = []\\n+\\n+ def capture_stream(_agent, payload, _destination=None):\\n+ streamed.append((payload[\\\"temperature\\\"], payload[\\\"max_tokens\\\"]))\\n+ yield \\\"chunk\\\"\\n+\\n+ monkeypatch.setattr(client, \\\"_stream_send\\\", capture_stream)\\n+ with client.request_settings(temperature=0.6, max_output_tokens=19):\\n+ assert list(\\n+ client.stream_chat(agent, [{\\\"role\\\": \\\"user\\\", \\\"content\\\": \\\"Stream.\\\"}])\\n+ ) == [\\\"chunk\\\"]\\n+ assert streamed == [(0.6, 19)]\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"userinfo_url\\\",\\n+ [\\n+ \\\"https://@gateway.example.com/v1/models\\\",\\n+ \\\"https://:secret@gateway.example.com/v1/models\\\",\\n+ ],\\n+)\\n+def test_provider_json_rejects_empty_userinfo_before_provider_transport(userinfo_url: str) -> None:\\n+ \\\"\\\"\\\"An empty username or password is still userinfo and cannot bypass origin checks.\\\"\\\"\\\"\\n+ agent = ModelAgent(\\n+ id=\\\"model_discovery_agent\\\",\\n+ model=\\\"model_catalog\\\",\\n+ base_url=\\\"https://gateway.example.com/v1\\\",\\n+ credential_key=\\\"\\\",\\n+ )\\n+ client = ModelClient()\\n+ with (\\n+ patch.object(client, \\\"_validate_provider\\\") as validate_provider,\\n+ patch.object(client, \\\"_open_provider\\\") as open_provider,\\n+ pytest.raises(RuntimeError, match=\\\"validated agent origin\\\"),\\n+ ):\\n+ client.fetch_json(agent, userinfo_url)\\n+ validate_provider.assert_not_called()\\n+ open_provider.assert_not_called()\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"schema\\\",\\n+ [\\n+ [],\\n+ {\\\"properties\\\": {\\\"answer\\\": []}},\\n+ {\\\"anyOf\\\": {}},\\n+ ],\\n+)\\n+def test_json_schema_definition_rejects_invalid_nested_containers(schema) -> None:\\n+ with pytest.raises(server.RequestError, match=\\\"must\\\") as captured:\\n+ server._validate_json_schema_definition(schema)\\n+ assert captured.value.status == 400\\n+\\n+\\n+def test_json_schema_definition_accepts_recursive_items_and_any_of() -> None:\\n+ server._validate_json_schema_definition(\\n+ {\\n+ \\\"type\\\": \\\"array\\\",\\n+ \\\"items\\\": {\\\"anyOf\\\": [{\\\"type\\\": \\\"string\\\"}, {\\\"type\\\": \\\"integer\\\"}]},\\n+ }\\n+ )\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"value\\\", \\\"schema\\\"),\\n+ [\\n+ (\\\"answer\\\", []),\\n+ (\\\"answer\\\", {\\\"enum\\\": [\\\"other\\\"]}),\\n+ (\\\"answer\\\", {\\\"const\\\": \\\"other\\\"}),\\n+ (\\\"answer\\\", {\\\"anyOf\\\": [{\\\"type\\\": \\\"integer\\\"}]}),\\n+ (\\\"answer\\\", {\\\"type\\\": \\\"object\\\"}),\\n+ ({}, {\\\"type\\\": \\\"object\\\", \\\"required\\\": [\\\"answer\\\"]}),\\n+ ({\\\"other\\\": 1}, {\\\"type\\\": \\\"object\\\", \\\"properties\\\": {}, \\\"additionalProperties\\\": False}),\\n+ (\\\"answer\\\", {\\\"type\\\": \\\"array\\\"}),\\n+ (1, {\\\"type\\\": \\\"string\\\"}),\\n+ (\\\"true\\\", {\\\"type\\\": \\\"boolean\\\"}),\\n+ (True, {\\\"type\\\": \\\"integer\\\"}),\\n+ (True, {\\\"type\\\": \\\"number\\\"}),\\n+ ],\\n+)\\n+def test_structured_value_validation_rejects_every_supported_mismatch(value, schema) -> None:\\n+ with pytest.raises(server.RequestError) as captured:\\n+ server._validate_json_schema_value(value, schema)\\n+ assert captured.value.status == 502\\n+\\n+\\n+def test_structured_value_validation_recurses_through_objects_and_arrays() -> None:\\n+ schema = {\\n+ \\\"type\\\": \\\"object\\\",\\n+ \\\"required\\\": [\\\"answers\\\"],\\n+ \\\"properties\\\": {\\\"answers\\\": {\\\"type\\\": \\\"array\\\", \\\"items\\\": {\\\"type\\\": \\\"string\\\"}}},\\n+ \\\"additionalProperties\\\": False,\\n+ }\\n+ server._validate_json_schema_value({\\\"answers\\\": [\\\"yes\\\"]}, schema)\\n+ assert server._json_schema_matches(\\\"yes\\\", {\\\"type\\\": \\\"string\\\"}) is True\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ (\\\"answer\\\", \\\"response_format\\\"),\\n+ [\\n+ (None, {\\\"type\\\": \\\"json_object\\\"}),\\n+ (\\\"not-json\\\", {\\\"type\\\": \\\"json_object\\\"}),\\n+ (\\\"[]\\\", {\\\"type\\\": \\\"json_object\\\"}),\\n+ ],\\n+)\\n+def test_structured_completion_rejects_non_contract_answers(answer, response_format) -> None:\\n+ with pytest.raises(server.RequestError) as captured:\\n+ server._validate_structured_completion_answer(answer, response_format)\\n+ assert captured.value.status == 502\\n+\\n+\\n+def test_structured_completion_applies_json_schema() -> None:\\n+ server._validate_structured_completion_answer(\\n+ json.dumps({\\\"answer\\\": \\\"yes\\\"}),\\n+ {\\n+ \\\"type\\\": \\\"json_schema\\\",\\n+ \\\"json_schema\\\": {\\n+ \\\"schema\\\": {\\n+ \\\"type\\\": \\\"object\\\",\\n+ \\\"properties\\\": {\\\"answer\\\": {\\\"type\\\": \\\"string\\\"}},\\n+ \\\"required\\\": [\\\"answer\\\"],\\n+ }\\n+ },\\n+ },\\n+ )\\n+\\n+\\n+def test_responses_chat_content_ignores_non_content_items() -> None:\\n+ assert orchestration._responses_chat_content(None) == \\\"\\\"\\n+ assert orchestration._responses_chat_content([\\\"one\\\", 2, {\\\"text\\\": \\\"two\\\"}]) == \\\"onetwo\\\"\\n+\\n+\\n+def test_temperature_capability_rejection_preserves_http_error_body() -> None:\\n+ class _UnreadableBody:\\n+ def read(self) -> bytes:\\n+ raise OSError(\\\"closed\\\")\\n+\\n+ def close(self) -> None:\\n+ return None\\n+\\n+ assert orchestration._temperature_capability_rejection(ValueError(\\\"temperature\\\")) is False\\n+ error = urllib.error.HTTPError(\\n+ \\\"https://gateway.example/v1/chat/completions\\\",\\n+ 400,\\n+ \\\"temperature is unsupported; only the default value is accepted\\\",\\n+ {},\\n+ _UnreadableBody(),\\n+ )\\n+ assert orchestration._temperature_capability_rejection(error) is True\\n+ assert error.read() == b\\\"\\\"\\n+\\n+\\n+def test_embedding_model_requires_an_orchestrator_when_omitted() -> None:\\n+ with pytest.raises(server.RequestError) as captured:\\n+ server._validate_embeddings_model({})\\n+ assert captured.value.code == \\\"invalid_model\\\"\\n+\\n+\\n+def test_response_content_and_capability_selection_fail_closed() -> None:\\n+ agent = ModelAgent(\\\"general_agent\\\", \\\"mock-model\\\")\\n+ with pytest.raises(RuntimeError, match=\\\"assistant content\\\"):\\n+ ModelClient._response_content(agent, {\\\"choices\\\": [{\\\"message\\\": {}}]})\\n+\\n+ orchestrator = TaskOrchestrator([agent])\\n+ with pytest.raises(ValueError, match=\\\"non-empty\\\"):\\n+ orchestrator.select_capability_agent(\\\" \\\")\\n+ with pytest.raises(RuntimeError, match=\\\"capability=embedding\\\"):\\n+ orchestrator.select_capability_agent(\\\"embedding\\\")\" }, { \"sha\": \"69c33d5d2f18f30d89c64d01e59e768b38d978eb\", \"filename\": \"tests/test_provider_embeddings.py\", \"status\": \"added\", \"additions\": 289, \"deletions\": 0, \"changes\": 289, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_provider_embeddings.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_provider_embeddings.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_embeddings.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -0,0 +1,289 @@\\n+\\\"\\\"\\\"Provider-backed embeddings stay inside contextual-orchestrator.\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import json\\n+from contextlib import contextmanager\\n+from pathlib import Path\\n+import sys\\n+from types import SimpleNamespace\\n+\\n+import pytest\\n+\\n+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n+\\n+from contextual_orchestrator.credentials import ( # noqa: E402\\n+ InMemoryCredentialBackend,\\n+ register_credential,\\n+ set_backend,\\n+)\\n+from contextual_orchestrator.batch_routing import EmbeddingBatchRequest # noqa: E402\\n+from contextual_orchestrator.cost_router import CostRoutingCoordinator # noqa: E402\\n+from contextual_orchestrator.orchestrator import ( # noqa: E402\\n+ ModelAgent,\\n+ ModelClient,\\n+ NotConfigured,\\n+)\\n+\\n+\\n+class _Response:\\n+ def __init__(self, payload: dict) -> None:\\n+ self._payload = json.dumps(payload).encode()\\n+\\n+ def read(self, *_args: object) -> bytes:\\n+ return self._payload\\n+\\n+ def __enter__(self) -> \\\"_Response\\\":\\n+ return self\\n+\\n+ def __exit__(self, *args: object) -> None:\\n+ return None\\n+\\n+\\n+def test_model_client_embed_many_calls_provider_embeddings_endpoint(monkeypatch) -> None:\\n+ backend = InMemoryCredentialBackend()\\n+ set_backend(backend)\\n+ register_credential(\\\"EMBEDDING_KEY\\\", \\\"provider-secret\\\")\\n+ seen: list[tuple[str, dict, str]] = []\\n+ agent = ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"text-embedding-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ credential_key=\\\"EMBEDDING_KEY\\\",\\n+ tags=(\\\"embedding\\\",),\\n+ )\\n+ client = ModelClient(max_retries=0)\\n+ monkeypatch.setattr(client, \\\"_validate_provider\\\", lambda _agent: object())\\n+\\n+ @contextmanager\\n+ def open_provider(request, _destination=None, **_kwargs):\\n+ seen.append((request.full_url, json.loads(request.data), request.headers[\\\"Authorization\\\"]))\\n+ yield _Response(\\n+ {\\n+ \\\"data\\\": [\\n+ {\\\"index\\\": 1, \\\"embedding\\\": [2.0, 3.0]},\\n+ {\\\"index\\\": 0, \\\"embedding\\\": [0.0, 1.0]},\\n+ ],\\n+ \\\"usage\\\": {\\\"prompt_tokens\\\": 2},\\n+ }\\n+ )\\n+\\n+ monkeypatch.setattr(client, \\\"_open_provider\\\", open_provider)\\n+ try:\\n+ assert client.embed_many(agent, [\\\"one\\\", \\\"two\\\"]) == [[0.0, 1.0], [2.0, 3.0]]\\n+ finally:\\n+ set_backend(None)\\n+ assert seen == [\\n+ (\\n+ \\\"https://gateway.example/v1/embeddings\\\",\\n+ {\\\"model\\\": \\\"text-embedding-model\\\", \\\"input\\\": [\\\"one\\\", \\\"two\\\"]},\\n+ \\\"Bearer provider-secret\\\",\\n+ )\\n+ ]\\n+\\n+\\n+def test_cost_router_uses_embedding_agent_instead_of_heuristic() -> None:\\n+ agent = ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"text-embedding-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ tags=(\\\"embedding\\\",),\\n+ )\\n+ calls: list[tuple[str, list[str]]] = []\\n+\\n+ def embed_many(selected: ModelAgent, inputs: list[str]) -> list[list[float]]:\\n+ calls.append((selected.model, inputs))\\n+ return [[float(index)] for index, _input in enumerate(inputs)]\\n+\\n+ orchestrator = SimpleNamespace(\\n+ candidates=[agent],\\n+ client=SimpleNamespace(embed_many=embed_many),\\n+ )\\n+ coordinator = CostRoutingCoordinator(orchestrator)\\n+\\n+ result = coordinator.complete_embeddings_batch(\\n+ [\\\"one\\\", \\\"two\\\"],\\n+ attribution={\\\"provider\\\": \\\"caller-spoof\\\"},\\n+ )\\n+\\n+ assert [item[\\\"embedding\\\"] for item in result[\\\"embeddings\\\"]] == [[0.0], [1.0]]\\n+ assert calls == [(\\\"text-embedding-model\\\", [\\\"one\\\", \\\"two\\\"])]\\n+ assert result[\\\"model\\\"] == \\\"text-embedding-model\\\"\\n+ assert result[\\\"provider\\\"] == \\\"gateway.example\\\"\\n+ assert {record[\\\"provider_name\\\"] for record in coordinator.ledger.records()} == {\\n+ \\\"gateway.example\\\"\\n+ }\\n+\\n+\\n+def test_default_embedding_backend_observes_runtime_agent_addition() -> None:\\n+ calls: list[tuple[str, list[str]]] = []\\n+\\n+ def embed_many(selected: ModelAgent, inputs: list[str]) -> list[list[float]]:\\n+ calls.append((selected.model, inputs))\\n+ return [[1.0] for _input in inputs]\\n+\\n+ orchestrator = SimpleNamespace(\\n+ candidates=[],\\n+ client=SimpleNamespace(embed_many=embed_many),\\n+ )\\n+ coordinator = CostRoutingCoordinator(orchestrator)\\n+ orchestrator.candidates.append(\\n+ ModelAgent(\\n+ \\\"runtime_embedding_agent\\\",\\n+ \\\"runtime-embedding-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ tags=(\\\"embedding\\\",),\\n+ )\\n+ )\\n+\\n+ result = coordinator.complete_embeddings_batch([\\\"added later\\\"])\\n+\\n+ assert calls == [(\\\"runtime-embedding-model\\\", [\\\"added later\\\"])]\\n+ assert result[\\\"embeddings\\\"][0][\\\"embedding\\\"] == [1.0]\\n+ assert result[\\\"provider\\\"] == \\\"gateway.example\\\"\\n+\\n+\\n+def test_embedding_client_fails_closed_before_or_after_transport(monkeypatch) -> None:\\n+ client = ModelClient(max_retries=0)\\n+ mock_agent = ModelAgent(\\\"mock_embedding\\\", \\\"embedding-model\\\", \\\"mock://embedding\\\")\\n+ assert client.embed_many(mock_agent, []) == []\\n+ with pytest.raises(RuntimeError, match=\\\"mock agents\\\"):\\n+ client.embed_many(mock_agent, [\\\"one\\\"])\\n+\\n+ backend = InMemoryCredentialBackend()\\n+ set_backend(backend)\\n+ configured_agent = ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"embedding-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ credential_key=\\\"MISSING_KEY\\\",\\n+ )\\n+ monkeypatch.setattr(client, \\\"_validate_provider\\\", lambda _agent: object())\\n+ try:\\n+ with pytest.raises(NotConfigured, match=\\\"resolvable credential\\\"):\\n+ client.embed_many(configured_agent, [\\\"one\\\"])\\n+ finally:\\n+ set_backend(None)\\n+\\n+ monkeypatch.setattr(client, \\\"_send_embeddings\\\", lambda *_args: (_ for _ in ()).throw(ValueError(\\\"bad\\\")))\\n+ with pytest.raises(RuntimeError, match=\\\"embeddings request failed\\\"):\\n+ client._send_embeddings_with_retry(configured_agent, {\\\"input\\\": [\\\"one\\\"]}, object())\\n+\\n+\\n+@pytest.mark.parametrize(\\n+ \\\"payload\\\",\\n+ [\\n+ {\\\"data\\\": []},\\n+ {\\\"data\\\": [{\\\"index\\\": \\\"0\\\", \\\"embedding\\\": [1.0]}]},\\n+ {\\\"data\\\": [{\\\"index\\\": 1, \\\"embedding\\\": [1.0]}]},\\n+ {\\\"data\\\": [{\\\"index\\\": 0, \\\"embedding\\\": [float(\\\"nan\\\")]}]},\\n+ {\\n+ \\\"data\\\": [\\n+ {\\\"index\\\": 0, \\\"embedding\\\": [1.0]},\\n+ {\\\"index\\\": 0, \\\"embedding\\\": [2.0]},\\n+ ]\\n+ },\\n+ ],\\n+)\\n+def test_embedding_client_rejects_malformed_provider_vectors(monkeypatch, payload) -> None:\\n+ client = ModelClient(max_retries=0)\\n+ agent = ModelAgent(\\\"embedding_agent\\\", \\\"embedding-model\\\", \\\"https://gateway.example/v1\\\")\\n+\\n+ @contextmanager\\n+ def open_provider(*_args, **_kwargs):\\n+ yield _Response(payload)\\n+\\n+ monkeypatch.setattr(client, \\\"_open_provider\\\", open_provider)\\n+ inputs = [\\\"one\\\", \\\"two\\\"] if len(payload.get(\\\"data\\\", [])) == 2 else [\\\"one\\\"]\\n+ with pytest.raises(RuntimeError, match=\\\"provider embeddings response\\\"):\\n+ client._send_embeddings(agent, {\\\"model\\\": agent.model, \\\"input\\\": inputs}, object())\\n+\\n+\\n+def test_embedding_model_resolution_covers_server_owned_failure_paths() -> None:\\n+ agent = ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"embedding-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ tags=(\\\"embedding\\\",),\\n+ )\\n+ selected = SimpleNamespace(\\n+ candidates=[agent],\\n+ client=SimpleNamespace(embed_many=lambda _agent, inputs: [[1.0] for _ in inputs]),\\n+ select_capability_agent=lambda capability: agent if capability == \\\"embedding\\\" else None,\\n+ )\\n+ coordinator = CostRoutingCoordinator(selected)\\n+ assert coordinator._resolve_embedding_provider_model(\\\"contextual-orchestrator\\\") == (\\n+ \\\"gateway.example\\\",\\n+ \\\"embedding-model\\\",\\n+ )\\n+ with pytest.raises(ValueError, match=\\\"not configured\\\"):\\n+ coordinator._resolve_embedding_provider_model(\\\"other-model\\\")\\n+\\n+ standalone = CostRoutingCoordinator(SimpleNamespace(candidates=[]))\\n+ with pytest.raises(ValueError, match=\\\"no enabled\\\"):\\n+ standalone._resolve_embedding_provider_model(\\\"contextual-orchestrator\\\")\\n+ assert standalone._resolve_embedding_provider_model(\\\"explicit-model\\\") == (\\n+ \\\"local\\\",\\n+ \\\"explicit-model\\\",\\n+ )\\n+\\n+\\n+def test_provider_embedding_backend_rejects_unknown_identity_and_missing_client() -> None:\\n+ agent = ModelAgent(\\n+ \\\"embedding_agent\\\",\\n+ \\\"embedding-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ tags=(\\\"embedding\\\",),\\n+ )\\n+ request = EmbeddingBatchRequest(\\n+ input_text=\\\"one\\\",\\n+ model=agent.model,\\n+ provider_name=\\\"wrong.example\\\",\\n+ )\\n+ backend = CostRoutingCoordinator(SimpleNamespace(candidates=[agent])).embedding_batch_backend\\n+ with pytest.raises(ValueError, match=\\\"not configured\\\"):\\n+ backend._batch_embedder([request])\\n+\\n+ request.provider_name = \\\"gateway.example\\\"\\n+ with pytest.raises(RuntimeError, match=\\\"no provider embedding client\\\"):\\n+ backend._batch_embedder([request])\\n+\\n+\\n+def test_provider_json_fetch_is_bounded_and_requires_configured_credentials(monkeypatch) -> None:\\n+ client = ModelClient()\\n+ agent = ModelAgent(\\\"catalog_agent\\\", \\\"catalog-model\\\", \\\"https://gateway.example/v1\\\")\\n+ with pytest.raises(ValueError, match=\\\"positive integer\\\"):\\n+ client.fetch_json(agent, \\\"https://gateway.example/v1/models\\\", max_bytes=0)\\n+\\n+ backend = InMemoryCredentialBackend()\\n+ set_backend(backend)\\n+ secured = ModelAgent(\\n+ \\\"secured_catalog_agent\\\",\\n+ \\\"catalog-model\\\",\\n+ \\\"https://gateway.example/v1\\\",\\n+ credential_key=\\\"MISSING_KEY\\\",\\n+ )\\n+ monkeypatch.setattr(client, \\\"_validate_provider\\\", lambda _agent: object())\\n+ try:\\n+ with pytest.raises(NotConfigured, match=\\\"resolvable credential\\\"):\\n+ client.fetch_json(secured, \\\"https://gateway.example/v1/models\\\")\\n+ finally:\\n+ set_backend(None)\\n+\\n+ @contextmanager\\n+ def open_provider(*_args, **_kwargs):\\n+ yield _Response({\\\"models\\\": [\\\"too large\\\"]})\\n+\\n+ monkeypatch.setattr(client, \\\"_open_provider\\\", open_provider)\\n+ backend = InMemoryCredentialBackend()\\n+ backend.set(\\\"OPENAI_API_KEY\\\", \\\"provider-key\\\")\\n+ set_backend(backend)\\n+ try:\\n+ with pytest.raises(ValueError, match=\\\"maximum size\\\"):\\n+ client.fetch_json(agent, \\\"https://gateway.example/v1/models\\\", max_bytes=1)\\n+ finally:\\n+ set_backend(None)\\n+\\n+\\n+if __name__ == \\\"__main__\\\": # pragma: no cover\\n+ raise SystemExit(pytest.main([__file__]))\" }, { \"sha\": \"5edaee425381d26335edef5c15f43d3f2fbb4b95\", \"filename\": \"tests/test_provider_integration.py\", \"status\": \"modified\", \"additions\": 51, \"deletions\": 3, \"changes\": 54, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_provider_integration.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_provider_integration.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_integration.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -34,12 +34,14 @@ class _FakeProvider:\\n \\n def __init__(self, responses: list[tuple[int, dict]]) -> None:\\n self.request_count = 0\\n+ self.requests: list[dict] = []\\n outer = self\\n \\n class Handler(BaseHTTPRequestHandler):\\n def do_POST(self) -> None: # noqa: N802\\n length = int(self.headers.get(\\\"content-length\\\", 0))\\n- self.rfile.read(length)\\n+ raw_request = self.rfile.read(length)\\n+ outer.requests.append(json.loads(raw_request.decode(\\\"utf-8\\\")))\\n index = min(outer.request_count, len(responses) - 1)\\n outer.request_count += 1\\n status, body = responses[index]\\n@@ -112,8 +114,54 @@ def test_permanent_4xx_is_not_retried_over_http() -> None:\\n client._send_with_retry(_agent(provider.base_url), {\\\"model\\\": \\\"gpt-x\\\"})\\n except RuntimeError:\\n raised = True\\n- assert raised\\n- assert provider.request_count == 1 # 400 is a real HTTPError classified permanent: one attempt\\n+ assert raised\\n+ assert provider.request_count == 1 # 400 is a real HTTPError classified permanent: one attempt\\n+\\n+\\n+def test_unsupported_temperature_is_negotiated_on_the_same_chat_endpoint() -> None:\\n+ with _FakeProvider([\\n+ (422, {\\\"error\\\": {\\\"message\\\": \\\"temperature is not supported for this deployment\\\"}}),\\n+ (200, _completion(\\\"negotiated\\\")),\\n+ ]) as provider:\\n+ client = ModelClient(max_retries=0)\\n+ result = client._send_with_retry(\\n+ _agent(provider.base_url), {\\\"model\\\": \\\"gpt-x\\\", \\\"temperature\\\": 0.2}\\n+ )\\n+ assert result == \\\"negotiated\\\"\\n+ assert provider.request_count == 2\\n+ assert provider.requests[0][\\\"temperature\\\"] == 0.2\\n+ assert \\\"temperature\\\" not in provider.requests[1]\\n+\\n+\\n+def test_invalid_temperature_is_not_treated_as_capability_negotiation() -> None:\\n+ with _FakeProvider([(400, {\\\"error\\\": {\\\"message\\\": \\\"invalid temperature value\\\"}})]) as provider:\\n+ client = ModelClient(max_retries=3, retry_backoff=0.0)\\n+ raised = False\\n+ try:\\n+ client._send_with_retry(\\n+ _agent(provider.base_url), {\\\"model\\\": \\\"gpt-x\\\", \\\"temperature\\\": 2.5}\\n+ )\\n+ except RuntimeError:\\n+ raised = True\\n+ assert raised\\n+ assert provider.request_count == 1\\n+\\n+\\n+def test_unsupported_temperature_is_negotiated_for_raw_responses_transport() -> None:\\n+ response = {\\\"id\\\": \\\"response-1\\\", \\\"output\\\": [{\\\"type\\\": \\\"message\\\"}]}\\n+ with _FakeProvider([\\n+ (400, {\\\"error\\\": {\\\"message\\\": \\\"unknown parameter: temperature\\\"}}),\\n+ (200, response),\\n+ ]) as provider:\\n+ client = ModelClient(max_retries=0)\\n+ result = client._send_raw_with_retry(\\n+ _agent(provider.base_url),\\n+ \\\"responses\\\",\\n+ {\\\"model\\\": \\\"gpt-x\\\", \\\"input\\\": \\\"hello\\\", \\\"temperature\\\": 0.2},\\n+ )\\n+ assert result == response\\n+ assert provider.request_count == 2\\n+ assert \\\"temperature\\\" not in provider.requests[1]\\n \\n \\n def test_connection_error_is_transient_and_exhausts() -> None:\" }, { \"sha\": \"00f1b4b2d33f78216e749a9ea19e5a9e7ae55984\", \"filename\": \"tests/test_provider_reliability.py\", \"status\": \"modified\", \"additions\": 40, \"deletions\": 4, \"changes\": 44, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_provider_reliability.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_provider_reliability.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_provider_reliability.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -14,6 +14,8 @@\\n from concurrent.futures import ThreadPoolExecutor\\n from pathlib import Path\\n \\n+import pytest\\n+\\n sys.path.insert(0, str(Path(__file__).resolve().parents[1]))\\n \\n from contextual_orchestrator import ModelAgent, TaskOrchestrator # noqa: E402\\n@@ -78,7 +80,7 @@ def _send(self, agent: ModelAgent, payload: dict, destination=None) -> str: # t\\n raise urllib.error.URLError(\\\"local server is busy\\\")\\n \\n client = LocalDownClient()\\n- agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n try:\\n client._send_with_retry(agent, {\\\"model\\\": agent.model})\\n except RuntimeError:\\n@@ -101,7 +103,7 @@ def _send(self, agent: ModelAgent, payload: dict, destination=None) -> str: # t\\n return \\\"recovered\\\"\\n \\n client = LocalFlakyClient()\\n- agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n assert client._send_with_retry(agent, {\\\"model\\\": agent.model}) == \\\"recovered\\\"\\n assert client.attempts == 2\\n \\n@@ -119,7 +121,7 @@ def _send(self, agent: ModelAgent, payload: dict, destination=None) -> str: # t\\n return \\\"recovered\\\"\\n \\n client = LocalFlakyClient()\\n- agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"mlx://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n assert client._send_with_retry(agent, {\\\"model\\\": agent.model}) == \\\"recovered\\\"\\n assert client.attempts == 3\\n \\n@@ -137,7 +139,7 @@ def _send_raw(self, agent: ModelAgent, endpoint: str, payload: dict, destination\\n return {\\\"ok\\\": True}\\n \\n client = LocalRawFlakyClient()\\n- agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\")\\n+ agent = ModelAgent(\\\"local_worker\\\", \\\"local-model\\\", base_url=\\\"local://127.0.0.1:8080/v1\\\", local_credential_key=\\\"LOCAL_GATEWAY_TOKEN\\\")\\n assert client._send_raw_with_retry(agent, \\\"chat/completions\\\", {}) == {\\\"ok\\\": True}\\n assert client.attempts == 3\\n \\n@@ -163,6 +165,38 @@ def _send(self, agent: ModelAgent, payload: dict, destination=None) -> str: # t\\n assert client.attempts == 1 # 400 is a caller error: exactly one attempt, no retry\\n \\n \\n+def test_provider_request_hides_raw_error_text_and_cause() -> None:\\n+ class RawProviderFailureClient(ModelClient):\\n+ def _send(self, agent: ModelAgent, payload: dict, destination=None) -> str: # type: ignore[override]\\n+ raise RuntimeError(\\\"provider-secret-response\\\")\\n+\\n+ client = RawProviderFailureClient(max_retries=0)\\n+ agent = ModelAgent(\\\"worker_agent\\\", \\\"gpt\\\", base_url=\\\"https://provider.example/v1\\\")\\n+\\n+ try:\\n+ client._send_with_retry(agent, {\\\"model\\\": \\\"gpt\\\"})\\n+ except RuntimeError as error:\\n+ assert \\\"provider-secret-response\\\" not in str(error)\\n+ assert error.__cause__ is None\\n+ else: # pragma: no cover\\n+ raise AssertionError(\\\"a failed provider request must raise\\\")\\n+\\n+\\n+def test_embedding_request_hides_raw_error_text_and_cause() -> None:\\n+ class RawEmbeddingFailureClient(ModelClient):\\n+ def _send_embeddings(self, agent: ModelAgent, payload: dict, destination=None) -> list[list[float]]: # type: ignore[override]\\n+ raise RuntimeError(\\\"embedding-provider-secret\\\")\\n+\\n+ client = RawEmbeddingFailureClient(max_retries=0)\\n+ agent = ModelAgent(\\\"embedding_agent\\\", \\\"embedding-model\\\", base_url=\\\"https://provider.example/v1\\\")\\n+\\n+ with pytest.raises(RuntimeError) as error:\\n+ client._send_embeddings_with_retry(agent, {\\\"model\\\": agent.model, \\\"input\\\": [\\\"text\\\"]})\\n+\\n+ assert \\\"embedding-provider-secret\\\" not in str(error.value)\\n+ assert error.value.__cause__ is None\\n+\\n+\\n class _AgentDownClient(ModelClient):\\n \\\"\\\"\\\"Fails for a chosen agent id, succeeds for the rest.\\\"\\\"\\\"\\n \\n@@ -214,6 +248,8 @@ def chat(self, agent: ModelAgent, messages: list, temperature: float = 0.2) -> s\\n except RuntimeError as exc:\\n raised = True\\n assert \\\"candidate agents failed\\\" in str(exc)\\n+ assert \\\"everything is down\\\" not in str(exc)\\n+ assert exc.__cause__ is None\\n assert raised\\n \\n \" }, { \"sha\": \"7349c730d586e5ec5f9cd4ac450437a1e677417e\", \"filename\": \"tests/test_repository_security_metadata.py\", \"status\": \"modified\", \"additions\": 15, \"deletions\": 1, \"changes\": 16, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_repository_security_metadata.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_repository_security_metadata.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_repository_security_metadata.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -152,11 +152,25 @@ def test_database_design_avoids_plaintext_prompt_output_storage():\\n def test_python_lockfile_uses_hash_pinning():\\n lock_text = read_text(\\\"requirements.lock\\\")\\n \\n- assert \\\"pip-compile\\\" in lock_text\\n+ assert \\\"uv pip compile\\\" in lock_text\\n+ assert \\\"--universal\\\" in lock_text\\n assert \\\"--hash=sha256:\\\" in lock_text\\n assert \\\"fastapi==\\\" in lock_text\\n assert \\\"uvicorn==\\\" in lock_text\\n assert \\\"sqlalchemy==\\\" in lock_text\\n+ for platform_dependency in (\\\"colorama\\\", \\\"greenlet\\\", \\\"tzdata\\\"):\\n+ assert f\\\"{platform_dependency}==\\\" in lock_text\\n+\\n+\\n+def test_unit_workflow_installs_runtime_and_test_lockfiles():\\n+ \\\"\\\"\\\"CI must exercise declared runtime integrations, not graceful no-op imports.\\\"\\\"\\\"\\n+ workflow_text = read_text(\\\".github/workflows/tests.yml\\\")\\n+\\n+ assert \\\"python -m pip install --require-hashes -r requirements.lock\\\" in workflow_text\\n+ assert (\\n+ \\\"python -m pip install --require-hashes -r fuzz/requirements-property.txt\\\"\\n+ in workflow_text\\n+ )\\n \\n \\n def test_security_tool_lockfile_uses_hash_pinning():\" }, { \"sha\": \"531e23fb7cccbce95ec86309db412b51b552aed7\", \"filename\": \"tests/test_responses_attribution_routing_http_honesty.py\", \"status\": \"modified\", \"additions\": 18, \"deletions\": 0, \"changes\": 18, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_attribution_routing_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_attribution_routing_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_attribution_routing_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -122,6 +122,24 @@ def test_http_responses_rejects_routing_latency_tolerant_true() -> None:\\n thread.join(timeout=5)\\n \\n \\n+def test_http_responses_rejects_routing_unknown_key() -> None:\\n+ server, thread, port = _server()\\n+ try:\\n+ status, body = _post(\\n+ port,\\n+ {\\n+ \\\"model\\\": \\\"mock-planner\\\",\\n+ \\\"input\\\": \\\"routing junk\\\",\\n+ \\\"routing\\\": {\\\"channel\\\": \\\"sync\\\", \\\"region\\\": \\\"us-east\\\"},\\n+ },\\n+ )\\n+ assert status == 400, body\\n+ assert \\\"invalid_routing\\\" in json.dumps(body)\\n+ finally:\\n+ server.shutdown()\\n+ thread.join(timeout=5)\\n+\\n+\\n def test_http_responses_rejects_attribution_unknown_dimension() -> None:\\n server, thread, port = _server()\\n try:\" }, { \"sha\": \"27352b247858a0b9b649e8e072992ac6be8e88b0\", \"filename\": \"tests/test_responses_flat_tools_http_honesty.py\", \"status\": \"modified\", \"additions\": 13, \"deletions\": 18, \"changes\": 31, \"blob_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_flat_tools_http_honesty.py\", \"raw_url\": \"https://github.com/ContextualWisdomLab/contextual-orchestrator/raw/d19e3492192e21e4a040fa3fc13a0793443731bf/tests%2Ftest_responses_flat_tools_http_honesty.py\", \"contents_url\": \"https://api.github.com/repos/ContextualWisdomLab/contextual-orchestrator/contents/tests%2Ftest_responses_flat_tools_http_honesty.py?ref=d19e3492192e21e4a040fa3fc13a0793443731bf\", \"patch\": \"@@ -52,7 +52,7 @@ def _server():\\n return server, thread, server.server_address[1]\\n \\n \\n-def test_http_responses_accepts_flat_function_tools() -> None:\\n+def test_http_responses_rejects_flat_function_tools_without_single_agent_fallback() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -72,13 +72,14 @@ def test_http_responses_accepts_flat_function_tools() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n \\n \\n-def test_http_responses_accepts_flat_tools_with_tool_choice_name() -> None:\\n+def test_http_responses_rejects_flat_tools_with_tool_choice_name() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -100,13 +101,14 @@ def test_http_responses_accepts_flat_tools_with_tool_choice_name() -> None:\\n },\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_still_accepts_nested_function_tools() -> None:\\n+def test_http_chat_rejects_nested_function_tools_without_single_agent_fallback() -> None:\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -126,14 +128,15 @@ def test_http_chat_still_accepts_nested_function_tools() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n \\n \\n-def test_http_chat_accepts_flat_function_tools_too() -> None:\\n- \\\"\\\"\\\"Chat path accepts Responses-flat tools for SDK portability.\\\"\\\"\\\"\\n+def test_http_chat_rejects_flat_function_tools_without_single_agent_fallback() -> None:\\n+ \\\"\\\"\\\"Chat path does not silently downgrade tools to one agent.\\\"\\\"\\\"\\n server, thread, port = _server()\\n try:\\n status, body = _post(\\n@@ -152,7 +155,8 @@ def test_http_chat_accepts_flat_function_tools_too() -> None:\\n ],\\n },\\n )\\n- assert status == 200, body\\n+ assert status == 422, body\\n+ assert body[\\\"error\\\"][\\\"code\\\"] == \\\"multi_agent_tools_unsupported\\\"\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n@@ -181,12 +185,3 @@ def test_http_tools_rejects_mixed_nested_and_flat() -> None:\\n finally:\\n server.shutdown()\\n thread.join(timeout=5)\\n-\\n-\\n-if __name__ == \\\"__main__\\\":\\n- test_http_responses_accepts_flat_function_tools()\\n- test_http_responses_accepts_flat_tools_with_tool_choice_name()\\n- test_http_chat_still_accepts_nested_function_tools()\\n- test_http_chat_accepts_flat_function_tools_too()\\n- test_http_tools_rejects_mixed_nested_and_flat()\\n- print(\\\"ok\\\")\" } ]" \ No newline at end of file diff --git a/contextual_orchestrator/pii_protection.py b/contextual_orchestrator/pii_protection.py index 8210831ba..6d6234533 100644 --- a/contextual_orchestrator/pii_protection.py +++ b/contextual_orchestrator/pii_protection.py @@ -62,10 +62,11 @@ def _decode_secret(secret: str, *, key_name: str = "") -> bytes: return hashlib.scrypt( passphrase.encode("utf-8"), salt=salt, - n=2**14, + n=2**17, r=8, p=1, dklen=32, + maxmem=256 * 1024 * 1024, ) except (TypeError, ValueError): raise PiiProtectionError("PII encryption passphrase could not be derived") from None diff --git a/fuzz/requirements-atheris.in b/fuzz/requirements-atheris.in index f44ec63a9..abae43678 100644 --- a/fuzz/requirements-atheris.in +++ b/fuzz/requirements-atheris.in @@ -3,10 +3,5 @@ # interpreters must skip this optional native fuzz dependency until a compatible # wheel is available. pip -# Atheris coverage-guided job deps. Compile: uv pip compile fuzz/requirements-atheris.in --generate-hashes --python-version 3.12 --universal -o fuzz/requirements-atheris.txt -# Atheris 3.1.0 is pinned to the repository's CPython 3.12 fuzz runner. Other -# interpreters must skip this optional native fuzz dependency until a compatible -# wheel is available. -pip atheris==3.1.0; python_full_version == "3.12.*" cryptography>=43.0 diff --git a/fuzz/requirements-atheris.txt b/fuzz/requirements-atheris.txt index 2994afef8..f0a9f2c95 100644 --- a/fuzz/requirements-atheris.txt +++ b/fuzz/requirements-atheris.txt @@ -1,6 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile fuzz/requirements-atheris.in --generate-hashes --python-version 3.12 --universal -o fuzz/requirements-atheris.txt -atheris==3.1.0 ; python_full_version == '3.12.*' \ +atheris==3.1.0 ; python_full_version < '3.13' \ --hash=sha256:315a0b5c819852b1ffe1ca72efc389c7724881f2c33e4aacb8c6bcec49bd5011 \ --hash=sha256:ec5e11f21a4c197fe91f7aea2b2de88e623c73a21fc07b105ac6329a1588457b \ --hash=sha256:f8a9f51ce8369026e8eb7b7174835e8c4c85a1a6db5d9add36c15100779d2a39 diff --git a/pr768_chat_capability_and_orchestrator.patch b/pr768_chat_capability_and_orchestrator.patch deleted file mode 100644 index 0f4290019..000000000 --- a/pr768_chat_capability_and_orchestrator.patch +++ /dev/null @@ -1 +0,0 @@ -"===== contextual_orchestrator/__main__.py (modified +3/-3) =====\n@@ -16,7 +16,7 @@\n agent_id_for,\n discover_all_models,\n refresh_price_book,\n- select_top_n_cheapest_discovered_agents,\n+ select_bootstrap_discovered_agents,\n )\n from .orchestrator import (\n CONTEXTUAL_ORCHESTRATOR_CONTRACT_V1,\n@@ -211,7 +211,7 @@ def _discover_models_command(argv: list[str]) -> None:\n type=_non_negative_int,\n default=0,\n metavar=\"N\",\n- help=\"Enable the N cheapest discovered agents in --agents-db (auto-optimization bootstrap; \"\n+ help=\"Enable a price-honest, provider-diverse discovered agent pool in --agents-db (auto-optimization bootstrap; \"\n \"requires --agents-db; 0 disables, the default, leaving every discovered agent inert).\",\n )\n args = parser.parse_args(argv)\n@@ -229,7 +229,7 @@ def _discover_models_command(argv: list[str]) -> None:\n )\n bootstrap.sync_discovered_agents([agent_from_discovered(model) for model in discovered])\n if args.enable_cheapest:\n- for model in select_top_n_cheapest_discovered_agents(discovered, price_book, args.enable_cheapest):\n+ for model in select_bootstrap_discovered_agents(discovered, price_book, args.enable_cheapest):\n agent_id = agent_id_for(model)\n bootstrap.patch_agent(\"default\", agent_id, {\"status\": \"active\"})\n enabled_agent_ids.append(agent_id)\n\n===== contextual_orchestrator/chat_capability.py (added +96/-0) =====\n@@ -0,0 +1,96 @@\n+\"\"\"Classify chat transport compatibility and ordinary agent-role eligibility.\n+\n+Provider catalogs mix endpoint-only models with models served through an\n+OpenAI-compatible chat transport. Transport compatibility is not the same as\n+fitness for an ordinary thinker, worker, verifier, or synthesizer role: audio\n+and policy-classification models can use chat transport, while embedding,\n+reranking, transcription, moderation-endpoint, image-generation, realtime, and\n+speech-only models cannot.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import re\n+\n+_MODEL_TOKEN_RE = re.compile(r\"[a-z0-9]+\")\n+_TRANSPORT_INCOMPATIBLE_EXACT_TOKENS = frozenset(\n+ {\n+ \"bge\",\n+ \"clip\",\n+ \"dall\",\n+ \"e5\",\n+ \"embed\",\n+ \"embedding\",\n+ \"embeddings\",\n+ \"gte\",\n+ \"image\",\n+ \"images\",\n+ \"moderation\",\n+ \"realtime\",\n+ \"rerank\",\n+ \"reranker\",\n+ \"siglip\",\n+ \"sora\",\n+ \"speech\",\n+ \"transcribe\",\n+ \"transcription\",\n+ \"tts\",\n+ \"whisper\",\n+ }\n+)\n+_TRANSPORT_INCOMPATIBLE_PREFIXES = (\n+ \"embed\",\n+ \"moderat\",\n+ \"rerank\",\n+ \"transcrib\",\n+)\n+\n+\n+def is_chat_compatible_model_id(model_id: str) -> bool:\n+ \"\"\"Return whether an identifier can use the ordinary chat transport.\n+\n+ The classifier rejects only identifiers that clearly advertise an endpoint\n+ family incompatible with chat messages. Audio-capable and safety-classifier\n+ models remain transport-compatible because providers serve some of them over\n+ ``/chat/completions``.\n+ \"\"\"\n+ tokens = _model_tokens(model_id)\n+ return _is_transport_compatible_tokens(tokens)\n+\n+\n+def _is_transport_compatible_tokens(tokens: tuple[str, ...]) -> bool:\n+ \"\"\"Judge transport compatibility from already-normalized model tokens.\"\"\"\n+ if not tokens:\n+ return False\n+ for token in tokens:\n+ if token in _TRANSPORT_INCOMPATIBLE_EXACT_TOKENS:\n+ return False\n+ if token.startswith(_TRANSPORT_INCOMPATIBLE_PREFIXES):\n+ return False\n+ return True\n+\n+\n+def _model_tokens(model_id: str) -> tuple[str, ...]:\n+ \"\"\"Normalize one provider-prefixed model identifier into lowercase tokens.\"\"\"\n+ if not isinstance(model_id, str):\n+ return ()\n+ return tuple(_MODEL_TOKEN_RE.findall(model_id.casefold()))\n+\n+\n+def is_general_chat_agent_model_id(model_id: str) -> bool:\n+ \"\"\"Return whether a chat model may enter ordinary orchestration roles.\n+\n+ Explicit guard and safety models can use chat transport but are specialized\n+ policy classifiers, not general answer synthesizers. This negative role gate\n+ does not infer reasoning, coding, vision, or verification capabilities.\n+ \"\"\"\n+ tokens = _model_tokens(model_id)\n+ if not tokens or not _is_transport_compatible_tokens(tokens):\n+ return False\n+ return not any(\n+ token == \"safety\"\n+ or token == \"guard\"\n+ or token == \"shieldgemma\"\n+ or token.startswith(\"nemoguard\")\n+ for token in tokens\n+ )\n\n===== contextual_orchestrator/cost_ledger.py (modified +48/-7) =====\n@@ -31,6 +31,7 @@\n import threading\n from dataclasses import dataclass, field\n from decimal import ROUND_HALF_UP, Decimal\n+import math\n import time\n from typing import Any, Dict, List, Optional, Protocol\n import uuid\n@@ -105,6 +106,31 @@ def _price_key(provider: str, model: str) -> str:\n return f\"{provider}:{model}\"\n \n \n+def _decimal_safe_price(value: object) -> Optional[float]:\n+ \"\"\"Parse one raw price component, or ``None`` when unknown, underflowed, or overflowed.\n+\n+ Parses through ``Decimal`` first so a nonzero price that underflows to\n+ ``0.0`` in float (e.g. a stray ``1e-10000``) is rejected as unknown\n+ rather than silently accepted as a legitimate free price. A ``Decimal``\n+ can still be finite while its ``float()`` conversion overflows to\n+ ``inf`` (e.g. ``1e10000``), so ``math.isfinite`` is checked separately\n+ on the converted value.\n+ \"\"\"\n+ try:\n+ decimal_value = Decimal(str(value))\n+ price = float(decimal_value)\n+ except (ArithmeticError, TypeError, ValueError):\n+ return None\n+ if (\n+ not decimal_value.is_finite()\n+ or not math.isfinite(price)\n+ or decimal_value < 0\n+ or (decimal_value != 0 and price == 0)\n+ ):\n+ return None\n+ return price\n+\n+\n @dataclass\n class PriceEntry:\n \"\"\"A single price-table row: per-1K-token prices for a provider+model.\"\"\"\n@@ -155,18 +181,33 @@ def get_price(self, provider: str, model: str) -> Optional[PriceEntry]:\n \"\"\"Return the price entry for ``provider``+``model``, if configured.\n \n Falls back to a provider-wildcard entry (``\"{provider}:*\"``) so a\n- provider can set one default price for all of its models.\n+ provider can set one default price for all of its models. A corrupt\n+ specific row does not suppress an otherwise-valid wildcard fallback.\n \"\"\"\n- raw = self._config.get(_PRICE_CATEGORY, _price_key(provider, model), None)\n- if raw is None:\n- raw = self._config.get(_PRICE_CATEGORY, _price_key(provider, \"*\"), None)\n- if raw is None:\n+ for candidate_model in (model, \"*\"):\n+ raw = self._config.get(_PRICE_CATEGORY, _price_key(provider, candidate_model), None)\n+ entry = self._parse_price_entry(raw, provider, model)\n+ if entry is not None:\n+ return entry\n+ return None\n+\n+ def _parse_price_entry(\n+ self, raw: Any, provider: str, model: str\n+ ) -> Optional[PriceEntry]:\n+ \"\"\"Validate one raw KV row into a ``PriceEntry``, or ``None`` if it is unusable.\"\"\"\n+ if not isinstance(raw, dict):\n+ return None\n+ if \"prompt_price_per_1k\" not in raw or \"completion_price_per_1k\" not in raw:\n+ return None\n+ prompt_price = _decimal_safe_price(raw[\"prompt_price_per_1k\"])\n+ completion_price = _decimal_safe_price(raw[\"completion_price_per_1k\"])\n+ if prompt_price is None or completion_price is None:\n return None\n return PriceEntry(\n provider_name=raw.get(\"provider_name\", provider),\n model_name=raw.get(\"model_name\", model),\n- prompt_price_per_1k=float(raw.get(\"prompt_price_per_1k\", 0.0)),\n- completion_price_per_1k=float(raw.get(\"completion_price_per_1k\", 0.0)),\n+ prompt_price_per_1k=prompt_price,\n+ completion_price_per_1k=completion_price,\n currency_code=raw.get(\"currency_code\", self.default_currency),\n )\n \n\n===== contextual_orchestrator/credentials.py (modified +34/-0) =====\n@@ -50,6 +50,10 @@ def set(self, name: str, value: str) -> None:\n \"\"\"Register (or replace) the secret stored under ``name``.\"\"\"\n ...\n \n+ def delete(self, name: str) -> None:\n+ \"\"\"Remove one credential after an unvalidated candidate promotion.\"\"\"\n+ ...\n+\n \n class InMemoryCredentialBackend:\n \"\"\"Process-local credential registry for dev and tests (no Postgres needed).\"\"\"\n@@ -68,6 +72,11 @@ def set(self, name: str, value: str) -> None:\n with self._lock:\n self._store[name] = value\n \n+ def delete(self, name: str) -> None:\n+ \"\"\"Remove ``name`` from the in-memory credential registry if present.\"\"\"\n+ with self._lock:\n+ self._store.pop(name, None)\n+\n \n # --- Postgres pgcrypto-encrypted credential registry ------------------------\n #\n@@ -112,6 +121,15 @@ def __init__(self, dsn: str, passphrase: str) -> None:\n self._passphrase = passphrase\n self._ensured = False\n \n+ @property\n+ def connection_dsn(self) -> str:\n+ \"\"\"Return the bootstrap DSN for a colocated metadata store.\n+\n+ Callers must treat this as connection material: never include it in logs,\n+ reports, traces, or exceptions. Provider API keys remain inaccessible.\n+ \"\"\"\n+ return self._dsn\n+\n @classmethod\n def from_env(cls) -> \"PostgresCredentialBackend\":\n \"\"\"Build the backend from bootstrap transport env vars (the only allowed env use).\n@@ -173,6 +191,17 @@ def set(self, name: str, value: str) -> None: # pragma: no cover - requires a l\n )\n conn.commit()\n \n+ def delete(self, name: str) -> None: # pragma: no cover - requires a live Postgres\n+ \"\"\"Delete one encrypted credential after a failed candidate promotion.\"\"\"\n+ with self._connect() as conn:\n+ self._ensure_schema(conn)\n+ with conn.cursor() as cur:\n+ cur.execute(\n+ \"DELETE FROM provider_credentials WHERE credential_name = %s\",\n+ (name,),\n+ )\n+ conn.commit()\n+\n \n _backend: CredentialBackend | None = None\n _backend_lock = threading.Lock()\n@@ -216,3 +245,8 @@ def get_credential(name: str) -> str | None:\n def register_credential(name: str, value: str) -> None:\n \"\"\"Register a named secret into the KV (used by the bootstrap CLI).\"\"\"\n get_backend().set(name, value)\n+\n+\n+def delete_credential(name: str) -> None:\n+ \"\"\"Remove a named credential from the KV after an unvalidated promotion.\"\"\"\n+ get_backend().delete(name)\n\n===== contextual_orchestrator/orchestrator.py (modified +62/-10) =====\n@@ -31,6 +31,10 @@\n import urllib.error\n import urllib.request\n \n+from .chat_capability import (\n+ is_chat_compatible_model_id,\n+ is_general_chat_agent_model_id,\n+)\n from .conventions import require_object_name\n from .credentials import NotConfigured, get_credential\n \n@@ -776,6 +780,8 @@ def chat(\n ``default_top_p`` are used so request-scoped Completions sampling can be\n applied without threading kwargs through every orchestrator hop.\n \"\"\"\n+ if not is_chat_compatible_model_id(agent.model):\n+ raise ValueError(\"model is not chat-compatible and cannot serve a chat request\")\n self._local.usage = None\n # Expose the effective sampling knobs for request-path tests / diagnostics.\n effective_temperature = self.default_temperature if temperature is None else temperature\n@@ -825,6 +831,15 @@ def probe(self, agent: ModelAgent, *, timeout: float = DEFAULT_PROVIDER_PROBE_TI\n \"\"\"\n probe_timeout = _validate_provider_probe_timeout(timeout)\n started = time.monotonic()\n+ if not is_chat_compatible_model_id(agent.model):\n+ return {\n+ \"agent_id\": agent.id,\n+ \"model\": agent.model,\n+ \"status\": \"not_ready\",\n+ \"latency_ms\": round((time.monotonic() - started) * 1000, 2),\n+ \"error_type\": \"ValueError\",\n+ \"failure_code\": \"non_chat_model\",\n+ }\n self._local.usage = None\n failure_code = \"provider_probe_failed\"\n try:\n@@ -1070,6 +1085,10 @@ def stream_chat(self, agent: ModelAgent, messages: list[ChatMessage], temperatur\n are yielded as they arrive (not computed-then-framed). The mock path yields its\n answer in fixed chunks so behavior shape stays testable and unchanged.\n \"\"\"\n+ if not is_chat_compatible_model_id(agent.model):\n+ raise ValueError(\n+ f\"model {agent.model!r} is not chat-compatible and cannot serve {agent.id!r}\"\n+ )\n if agent.base_url.startswith(\"mock://\"):\n answer = self._mock(agent, messages)\n for start in range(0, len(answer), 24):\n@@ -1129,10 +1148,20 @@ def proxy_send(\n self, agent: ModelAgent, endpoint: str, payload: dict[str, Any]\n ) -> dict[str, Any]:\n \"\"\"Passthrough a full request to one agent, returning the raw provider JSON.\"\"\"\n+ normalized_endpoint = endpoint.strip(\"/\")\n+ if normalized_endpoint.startswith(\"v1/\"):\n+ normalized_endpoint = normalized_endpoint[3:]\n+ if (\n+ normalized_endpoint in {\"chat/completions\", \"completions\", \"responses\"}\n+ and not is_chat_compatible_model_id(agent.model)\n+ ):\n+ raise ValueError(\n+ f\"model {agent.model!r} is not chat-compatible and cannot serve {agent.id!r}\"\n+ )\n if agent.base_url.startswith(\"mock://\"):\n- return self._mock_raw(agent, endpoint, payload)\n+ return self._mock_raw(agent, normalized_endpoint, payload)\n destination = self._validate_provider(agent) # pragma: no cover\n- if endpoint.strip(\"/\") == \"responses\" and _is_local_provider_url(agent.base_url):\n+ if normalized_endpoint == \"responses\" and _is_local_provider_url(agent.base_url):\n chat_payload = _responses_to_chat_payload(payload)\n chat_payload.setdefault(\"max_tokens\", self.max_output_tokens)\n if _is_direct_mlx_provider_url(agent.base_url) and self.chat_template_args:\n@@ -1143,7 +1172,7 @@ def proxy_send(\n )\n return _chat_to_responses_payload(chat_response, payload)\n with _local_provider_slot(agent, self.local_concurrency, self.timeout): # pragma: no cover\n- return self._send_raw_with_retry(agent, endpoint, payload, destination)\n+ return self._send_raw_with_retry(agent, normalized_endpoint, payload, destination)\n \n def _send_raw_with_retry(\n self,\n@@ -1316,6 +1345,10 @@ def batch_chat(\n workloads (24h completion window, ~half the price); real-time chat should keep\n using ``chat``. The mock path answers synchronously so tests and local runs work.\n \"\"\"\n+ if not is_chat_compatible_model_id(agent.model):\n+ raise ValueError(\n+ f\"model {agent.model!r} is not chat-compatible and cannot serve {agent.id!r}\"\n+ )\n if agent.base_url.startswith(\"mock://\"):\n results = {\n custom_id: {\"content\": self._mock(agent, messages), \"usage\": None}\n@@ -2434,6 +2467,7 @@ def _plan_generated(self, task: str) -> list[WorkflowStep]:\n pool = \"\\n\".join(\n f\"- {agent.id}: model={agent.model}, tags={', '.join(agent.tags) or 'none'}\"\n for agent in self.agents\n+ if is_general_chat_agent_model_id(agent.model)\n )\n system = (\n \"You are the workflow conductor. Decompose the user's task into a short workflow.\\n\"\n@@ -2459,7 +2493,7 @@ def _parse_workflow_plan(self, raw: str) -> list[WorkflowStep]:\n raw_steps = data.get(\"steps\")\n if not isinstance(raw_steps, list) or not (2 <= len(raw_steps) <= self.policy.max_workflow_steps):\n raise ValueError(f\"plan must have 2..{self.policy.max_workflow_steps} steps\")\n- known_agents = {agent.id for agent in self.agents}\n+ known_agents = {agent.id: agent for agent in self.agents}\n steps: list[WorkflowStep] = []\n for index, item in enumerate(raw_steps):\n if int(item.get(\"id\", -1)) != index:\n@@ -2474,8 +2508,9 @@ def _parse_workflow_plan(self, raw: str) -> list[WorkflowStep]:\n if any(value < 0 or value >= index for value in access):\n raise ValueError(\"access may reference only earlier steps\")\n agent_id = item.get(\"agent_id\")\n- if agent_id not in known_agents:\n- # The planner named an unknown agent: reselect honestly instead of failing the plan.\n+ assigned = known_agents.get(agent_id)\n+ if assigned is None or not is_general_chat_agent_model_id(assigned.model):\n+ # Unknown or stale ineligible assignments are reselected honestly.\n agent_id = self._select_agent(subtask, role).id\n steps.append(WorkflowStep(index, role, agent_id, subtask, access))\n if steps[-1].role not in {\"synthesizer\", \"worker\"}:\n@@ -2509,10 +2544,21 @@ def _score_agent(self, agent: ModelAgent, role: str, lowered: str) -> tuple[int,\n def _ranked_agents(self, text: str, role: str) -> list[ModelAgent]:\n \"\"\"Agents sorted best-first for a role; the head is the primary, the tail are failovers.\"\"\"\n lowered = text.lower()\n- return sorted(self.agents, key=lambda agent: self._score_agent(agent, role, lowered), reverse=True)\n+ return [\n+ agent\n+ for agent in sorted(\n+ self.agents,\n+ key=lambda agent: self._score_agent(agent, role, lowered),\n+ reverse=True,\n+ )\n+ if is_general_chat_agent_model_id(agent.model)\n+ ]\n \n def _select_agent(self, text: str, role: str) -> ModelAgent:\n- selected = self._ranked_agents(text, role)[0]\n+ ranked = self._ranked_agents(text, role)\n+ if not ranked:\n+ raise RuntimeError(f\"no chat-compatible agent available for role={role}\")\n+ selected = ranked[0]\n if selected.disabled: # pragma: no cover\n raise RuntimeError(f\"no enabled agent available for role={role}\")\n if role in selected.provider_exclusions: # pragma: no cover\n@@ -2530,6 +2576,8 @@ def _invoke(\n usage when available (else None), so spend analytics can prefer it.\n \"\"\"\n candidates = self._failover_candidates(primary, text, role)\n+ if not candidates:\n+ raise RuntimeError(f\"no chat-compatible agent available for role={role}\")\n last_error: Exception | None = None\n for agent in candidates:\n try:\n@@ -2545,11 +2593,15 @@ def _invoke(\n \n def _failover_candidates(self, primary: ModelAgent, text: str, role: str) -> list[ModelAgent]:\n ranked = self._ranked_agents(text, role)\n- ordered = [primary] + [agent for agent in ranked if agent.id != primary.id]\n+ ordered = [\n+ agent\n+ for agent in [primary] + [agent for agent in ranked if agent.id != primary.id]\n+ if is_general_chat_agent_model_id(agent.model)\n+ ]\n eligible = [agent for agent in ordered if not agent.disabled and role not in agent.provider_exclusions]\n healthy = [agent for agent in eligible if not self._circuit_open(agent.id)]\n # If every eligible agent is circuit-open, still probe them rather than fail with no attempt.\n- return healthy or eligible or [primary]\n+ return healthy or eligible\n \n def _circuit_open(self, agent_id: str) -> bool:\n with self._circuit_lock:" \ No newline at end of file diff --git a/registered_agents.json b/registered_agents.json deleted file mode 100644 index 9e26dfeeb..000000000 --- a/registered_agents.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/task_agent_mapping.json b/task_agent_mapping.json deleted file mode 100644 index 9e26dfeeb..000000000 --- a/task_agent_mapping.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file From 030b4348eb4ecdf85d1017f8891cac9faa5a0394 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 10:49:18 +0900 Subject: [PATCH 18/18] fix(ci): preserve exact Atheris Python marker --- fuzz/requirements-atheris.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fuzz/requirements-atheris.txt b/fuzz/requirements-atheris.txt index f0a9f2c95..4d15ad8bc 100644 --- a/fuzz/requirements-atheris.txt +++ b/fuzz/requirements-atheris.txt @@ -1,6 +1,8 @@ # This file was autogenerated by uv via the following command: # uv pip compile fuzz/requirements-atheris.in --generate-hashes --python-version 3.12 --universal -o fuzz/requirements-atheris.txt -atheris==3.1.0 ; python_full_version < '3.13' \ +# uv's universal resolver widens the source marker to <3.13; preserve the +# exact CPython 3.12 boundary so Dependabot on Python 3.10 skips Atheris. +atheris==3.1.0 ; python_full_version == '3.12.*' \ --hash=sha256:315a0b5c819852b1ffe1ca72efc389c7724881f2c33e4aacb8c6bcec49bd5011 \ --hash=sha256:ec5e11f21a4c197fe91f7aea2b2de88e623c73a21fc07b105ac6329a1588457b \ --hash=sha256:f8a9f51ce8369026e8eb7b7174835e8c4c85a1a6db5d9add36c15100779d2a39