Skip to content

fix: fail over current-main passthrough providers - #763

Merged
seonghobae merged 94 commits into
fix/auto-reasoning-effort-contract-rebasedfrom
fix/current-main-passthrough-failover
Aug 21, 2026
Merged

fix: fail over current-main passthrough providers#763
seonghobae merged 94 commits into
fix/auto-reasoning-effort-contract-rebasedfrom
fix/current-main-passthrough-failover

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Why

Protected fix/auto-reasoning-effort-contract-rebased@ba8b4b8ab181bcd2c64b871d0984006452e4c753 contains the integrated HTTP-honesty stack and OpenAI-compatible front door. One buyer-visible reliability gap remains: raw tool, structured-output, and Responses requests can consume same-model retries on a saturated or retired provider candidate without advancing to another configured model.

Current reliability slice

  • Virtual contextual-orchestrator and omitted-model passthrough may advance across capability-ranked agents.
  • Every built-in provider candidate receives exactly one raw attempt, so HTTP 429/5xx never replays the same large request against one provider.
  • A dynamically discovered candidate that has disappeared or retired (HTTP 404/410) may advance only for the virtual/adaptive model contract.
  • Provider SDK wrappers are inspected through a bounded, cycle-safe cause/context chain.
  • Explicit raise ... from cause is authoritative; context deliberately suppressed with raise ... from None is not traversed.
  • Explicit concrete-model requests remain sticky and preserve the original provider error, including HTTP 429/404/410.
  • Provider 400/401/403 and other caller, authentication, policy, or configuration failures return immediately and are not replayed.
  • tools, tool_choice, response_format, Responses input, endpoint choice, and caller payload immutability are preserved.
  • Adaptive failover failures open the existing circuit breaker immediately; exhausting candidates fails closed and chains the final provider error.
  • The production python -m contextual_orchestrator --serve path constructs the failover-capable orchestrator.

Local-provider semantic repair

A first one-shot implementation called _send_raw directly. That removed retries but also bypassed two behaviors owned by the built-in ModelClient.proxy_send path:

  1. local Responses-to-Chat translation and Chat-to-Responses reconstruction; and
  2. the loopback model-switch/concurrency slot.

The current implementation publishes ModelClient.proxy_send_once() at the owning transport layer. It preserves those semantics while replacing only _send_raw_with_retry with one _send_raw call. Focused regressions lock local Responses translation, local model-switch coordination, caller immutability, and proof that the retry wrapper is never entered.

Test-first evidence

The branch records RED regressions before the corresponding bounded production repairs for:

  • 429 one-attempt-per-candidate behavior;
  • raw tool and response-format preservation;
  • caller request immutability;
  • 404/410 stale-discovery fallback for the virtual model;
  • wrapped 429/410 provider causes;
  • explicit context suppression;
  • concrete-model stickiness and original-error preservation;
  • immediate stop on non-transient provider 400;
  • fail-closed exhaustion with final-cause chaining;
  • production CLI/server composition;
  • local Responses adapter preservation;
  • local model-switch/concurrency preservation; and
  • direct one-shot transport without same-agent retry amplification.

Exact current identity

  • Base: fix/auto-reasoning-effort-contract-rebased@ba8b4b8ab181bcd2c64b871d0984006452e4c753
  • Head: 531c74f49f228929425b485838f18e355aaa0cdf
  • State: ready for review; merge blocked until required Checks and protected independent approval.
  • Changed files: 16 permanent files
  • Active unresolved review threads: zero
  • Branch-local repair workflow: zero

Exact-head repository Tests, Fuzz, Security, Security Scan, Semgrep, semantic review, and protected integration remain authoritative.

Dependencies and reconciliation

This PR owns passthrough failover only. PR #768 owns ordinary-chat capability isolation, PR #769 owns the runtime/test dependency boundary, and PR #770 owns honest provider-diverse discovery bootstrap. After #768 lands, this branch must rebase and ensure proxy_send_once() applies the shared chat-transport gate before mock or provider I/O and that every adaptive candidate is ordinary-role eligible. The branch is now reconciled with #765 at ba8b4b8ab181bcd2c64b871d0984006452e4c753 in merge commit 531c74f49f228929425b485838f18e355aaa0cdf; future parent movement requires another exact-head refresh.

Keep Draft until prerequisite integration/reconciliation, exact-head verification, and a qualifying current-head formal review complete. No bypass, self-approval, or predecessor-head evidence transfer.

Review repair: preserve embedding auto-selection

  • _ranked_agents keeps the general-chat model-id filter for chat roles and failover.
  • select_capability_agent opts out of that chat-only filter, so real embedding identifiers such as text-embedding-3-large remain selectable by the embeddings endpoint.
  • Regression uses text-embedding-3-large instead of a chat-shaped mock model.

Exact validation on head 531c74f49f228929425b485838f18e355aaa0cdf:

  • reconciliation-focused capability/sampling/passthrough tests: 150 passed
  • full suite: 1589 passed in 534.34s
  • compileall and git diff --check: passed

2026-08-21 parent reconciliation

Head 615f0b64078781cc7fa7d7f747737d50657971a2 is reconciled with parent #765 at 39072a654261c3570496849bb4da1e2c340e2fbc. A live gpt-5.6-sol Azure rejection exposed that the one-shot passthrough path bypassed ADR 0016 temperature capability negotiation. The current head reuses the shared raw transport with transient retries disabled: HTTP 429 still makes exactly one provider call and preserves the original error, while an explicit unsupported temperature may be removed once on the same endpoint. Focused passthrough/provider tests passed (28); the exact combined repository suite passed 1,600 tests in 541.73 seconds. Hosted checks and independent protected approval remain authoritative.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 520e939e-7bfe-48c4-b62a-ef5b61013ca2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/current-main-passthrough-failover

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Contributor Author

@opencode-agent Review exact current head f06ba9199854c090a149059b36639260e9b622d8. This is the current-main successor for passthrough provider failover only: verify one-attempt-per-candidate 429 behavior, explicit-model stickiness, tool/structured-response preservation, caller immutability, circuit-breaker state, and final-cause fail-closed behavior.

Copy link
Copy Markdown
Contributor Author

@opencode-agent Review exact head f06ba9199854c090a149059b36639260e9b622d8 as the current-main successor for passthrough provider failover. Verify one raw attempt per candidate, 429/circuit-breaker failover, explicit concrete-model stickiness, payload/tool/response-format immutability, and fail-closed exhaustion. Do not import stale #744/#751 scope. Submit an exact-head formal review once current checks are authoritative.

@seonghobae

Copy link
Copy Markdown
Contributor Author

@opencode-agent Review exact current head SHA f06ba91. Use only same-head required Checks and changed-file evidence; publish a fresh formal verdict. Approve only when all required evidence and protected review conditions are satisfied.

Copy link
Copy Markdown
Contributor Author

@opencode-agent review the exact current head f06ba9199854c090a149059b36639260e9b622d8. Verify the bounded passthrough provider failover: virtual/omitted-model requests may advance across capability-ranked candidates after 429/transient failure, each candidate gets at most one raw attempt, explicit concrete-model requests stay sticky, OpenAI tools/response-format/Responses payload fields remain immutable, circuit-breaker state is updated, and exhausted candidates fail closed. Submit a formal current-head review rather than a status-only comment.

Copy link
Copy Markdown
Contributor Author

@opencode-agent fix this exact-head defect on the existing branch before reviewing: the new failover class is exported only from contextual_orchestrator.__init__, but the production CLI in contextual_orchestrator/__main__.py still imports TaskOrchestrator directly from .orchestrator. Therefore python -m contextual_orchestrator --serve—the path used by the central sidecar—still constructs the old single-provider class and never executes this PR's failover. Add a RED regression proving the CLI-created/runtime server orchestrator is passthrough_failover.TaskOrchestrator, then make the narrow import/wiring fix (and update server.py's type binding if needed), run the focused test plus full Tests/Security/Fuzz/SAST, and push the same branch. Do not approve until the live CLI path is proven.

@seonghobae
seonghobae marked this pull request as draft August 20, 2026 05:03
auto-merge was automatically disabled August 20, 2026 05:03

Pull request was converted to draft

…ct-rebased' into HEAD

# Conflicts:
#	contextual_orchestrator/__init__.py
…bedding-models' into HEAD

# Conflicts:
#	README.md
#	contextual_orchestrator/orchestrator.py
#	tests/test_local_gateway.py
@seonghobae
seonghobae changed the base branch from main to fix/auto-reasoning-effort-contract-rebased August 20, 2026 20:44
@seonghobae
seonghobae marked this pull request as ready for review August 20, 2026 20:44
@seonghobae

Copy link
Copy Markdown
Contributor Author

Current HEAD 4a2a9ab27b0961011fc299debc78ec052f73cd54 integrates the #765 reasoning/gateway contract and #768 chat-capability boundary on the passthrough-failover branch.

Exact-head verification:

  • focused failover/capability/local-gateway regression: 140 passed
  • full integrated suite: 1589 passed
  • changed-area Ruff, compileall, and diff checks passed
  • one-shot Responses transport preserves local gateway translation/concurrency while rejecting non-chat model identifiers before mock/provider I/O
  • provider discovery tests use the current validated ModelClient transport seam

Please review this exact HEAD with @opencode-agent. No self-approval or bypass is used.

devin-ai-integration[bot]

This comment was marked as resolved.

@opencode-agent
opencode-agent Bot disabled auto-merge August 20, 2026 21:21
@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact current HEAD was revalidated in its existing stack: focused capability-isolation, one-shot passthrough, failover, local-gateway, and embedding honesty suite passed 133 tests; Ruff, compileall, and diff checks passed. The branch remains based on the current reasoning-contract parent; no force push or self-approval was used. @opencode-agent review this exact SHA only and publish a formal verdict.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact current HEAD f72fd043e01d57656b4d08bf9c03eb95d834501d was revalidated in its existing stack: focused capability-isolation, one-shot passthrough, failover, local-gateway, and embedding honesty suite passed 133 tests; Ruff, compileall, and diff checks passed. The branch remains based on the current reasoning-contract parent; no force push or self-approval was used. @opencode-agent review this exact SHA only and publish a formal verdict.

@seonghobae
seonghobae enabled auto-merge (squash) August 20, 2026 21:33

Copy link
Copy Markdown
Contributor Author

Exact-head revalidation for PR #763

Verified f72fd043e01d57656b4d08bf9c03eb95d834501d in a clean CodeGraph-indexed worktree:

  • passthrough failover, one-shot local semantics, capability isolation, embedding selection, gateway seed/local gateway, provider reliability/TLS, API, OpenAI passthrough, and PR refactor: publish gateway-only paper-grounded reasoning contract #765 regression suites: 186 passed
  • package-wide compileall and git diff --check: passed
  • current review threads are resolved; no independent approval is present
  • the branch is stacked on the current refactor: publish gateway-only paper-grounded reasoning contract #765 head 2ab9b9f011403bcea7ee812c5b5b389ddebe1181; rebase/reconciliation remains required before any protected-main merge
  • live Security Scan and SAST Semgrep runs for this SHA are still queued

Please independently review exactly this HEAD, including one-attempt-per-candidate behavior, explicit-model stickiness, cause-chain classification, local Responses translation/model switching, and preservation of embedding auto-selection. Do not merge until the current stack and all protected gates are terminal and approved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Remote concurrent work advanced the branch to 531c74f49f228929425b485838f18e355aaa0cdf with the same resulting tree as the locally validated merge resolution (50a551f3); no force push was used. Exact resulting tree verification: pytest -q = 1589 passed in 597.16s; focused passthrough/capability/sampling regressions = 85 passed; Ruff, compileall, and diff checks passed. The parent reasoning-contract removal and passthrough failover export are both preserved. Re-dispatching review and protected merge scheduling for this exact remote HEAD.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact current HEAD 531c74f49f228929425b485838f18e355aaa0cdf was revalidated after the parent reasoning-contract merge. Current capability-isolation, one-shot passthrough, failover, embedding auto-selection, and local-gateway suite passed 133 tests; Ruff, compileall, and diff checks passed. The prior embedding auto-selection finding is resolved in this current lineage. @opencode-agent review this exact SHA only.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact-head validation — PR #763

  • Current HEAD: 531c74f49f228929425b485838f18e355aaa0cdf
  • Passthrough/failover plus inherited refactor: publish gateway-only paper-grounded reasoning contract #765/fix: isolate embedding deployments from chat synthesis #768 gateway, capability, discovery, sampling, security, and cost contracts: 168 passed.
  • Ruff, Python compilation, and git diff --check: passed.
  • The current stack preserves one-shot local semantics, bounded provider failover, gateway-owned control negotiation, and the shared chat-capability boundary; no provider key or merge-authority change was introduced.
  • Current live PR remains BLOCKED/REVIEW_REQUIRED; no merge or release authority is claimed.

@opencode-agent please review only exact current HEAD 531c74f49f228929425b485838f18e355aaa0cdf, including stack ancestry, failover attempt truth, capability-filter reuse, and explicit provider-control handling. Do not approve or merge.

@seonghobae
seonghobae merged commit 5b43ebf into fix/auto-reasoning-effort-contract-rebased Aug 21, 2026
0 of 2 checks passed
@seonghobae
seonghobae deleted the fix/current-main-passthrough-failover branch August 21, 2026 13:13
@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact-head reconciliation completed on the existing branch.

  • Remote HEAD: 1ebb47cd15a663f634ee46caf436fea76ee51407
  • Parent/base HEAD: 537915715c4b050d4b5fa18ef83ad462e46e2c0 (PR base remains 537915715c4b050d4b5fa18ce2b7559080c675ba)
  • Normal merge of the parent into the PR branch resolved the prior DIRTY state; no force push or bypass was used.
  • The provider failover is now a transport seam used by both ordinary passthrough and the parent structured-output workflow. Adaptive candidates use one-shot transport; explicit concrete models stay sticky on the normal provider contract.
  • Focused provider/passthrough/structured/telemetry suite: 188 passed.
  • Full exact-tree suite after reconciliation: 1719 passed in 546.43s.
  • compileall and git diff --check: passed.

Hosted required Checks and an independent formal approval are still authoritative. No merge or approval claim is made while they are pending.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Correction to the preceding evidence comment: the exact base HEAD is 537915715c4b050d4b5fa18ce2b7559080c675ba. The preceding line contained a transcription error; all validation and the remote PR head 1ebb47cd15a663f634ee46caf436fea76ee51407 are unchanged.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 13 new potential issues.

Open in Devin Review

Comment on lines +1567 to +1574
with _local_provider_slot(agent, self.local_concurrency, self.timeout):
return self._send_raw_with_retry(
agent,
normalized_endpoint,
payload,
destination,
allow_transient_retries=False,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 One-shot passthrough drops local chat max_tokens default

proxy_send defaults max_tokens for local chat/completions passthrough (orchestrator.py, locked by test_local_chat_passthrough_applies_bounded_controls_for_final_synthesis). proxy_send_once omits that branch and sends local chat/completions through the generic path at orchestrator.py:1567-1574 with no default. Adaptive passthrough failover always calls proxy_send_once (passthrough_failover.py), so a local gateway on the virtual-model path loses the bounded control and the provider default can truncate output. Only the local responses branch was carried over.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +125 to +147
for candidate_agent in candidates:
upstream = dict(payload)
upstream["model"] = candidate_agent.model
try:
result = _proxy_send_once(self.client, candidate_agent, endpoint, upstream)
except Exception as exc:
if not adaptive_request or not _is_adaptive_failover_error(exc):
raise
last_error = exc
self._record_failure(candidate_agent.id)
with self._circuit_lock:
state = self._circuit.setdefault(
candidate_agent.id,
{"failures": 0.0, "opened_at": 0.0},
)
state["failures"] = max(
state["failures"],
float(self.circuit_failure_threshold),
)
state["opened_at"] = time.monotonic()
continue
self._record_success(candidate_agent.id)
return result

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Failover attributes fallback tokens to the primary agent

When adaptive passthrough succeeds on a fallback candidate (passthrough_failover.py), only the raw dict is returned. proxy_completion records the step with agent_id = the primary agent and usage from the fallback response (orchestrator.py), so the trace and spend ledger attribute the served model's tokens to a model that did not serve. The seam cannot report which agent actually served.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

destination = self._validate_provider(agent) # pragma: no cover
if normalized_endpoint == "responses" and _is_local_provider_url(agent.base_url):
chat_payload = _responses_to_chat_payload(payload)
chat_payload.setdefault("max_tokens", self.max_output_tokens)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: One-shot local responses ignores request-scoped max_output_tokens

proxy_send reads self._request_setting("max_output_tokens", ...) for local responses translation (orchestrator.py), but proxy_send_once uses self.max_output_tokens directly (orchestrator.py:1557), ignoring a thread-local override from request_settings(). Minor divergence on the failover path.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +135 to +144
with self._circuit_lock:
state = self._circuit.setdefault(
candidate_agent.id,
{"failures": 0.0, "opened_at": 0.0},
)
state["failures"] = max(
state["failures"],
float(self.circuit_failure_threshold),
)
state["opened_at"] = time.monotonic()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Adaptive failover forces the shared circuit breaker open on a single failure

On each adaptive passthrough failure, the code both calls self._record_failure(agent.id) and then forces state["failures"] = max(failures, threshold) and state["opened_at"] = now, opening the circuit for that agent after a single 429/5xx. Because the circuit state (self._circuit) is shared with the ordinary chat/_invoke failover path, one transient passthrough failure will mark that agent circuit-open for circuit_reset_seconds across ALL paths, potentially deprioritizing an otherwise-healthy agent for normal chat routing. The description says adaptive failures "open the existing circuit breaker immediately," so this appears intentional, but the cross-path impact of a single transient blip is aggressive and worth confirming against expected routing behavior.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +52 to +62

def _is_adaptive_failover_error(error: BaseException) -> bool:
"""Classify transient or stale-candidate failures through provider wrappers."""
for candidate in _provider_error_chain(error):
if is_transient_error(candidate):
return True
if (
isinstance(candidate, urllib.error.HTTPError)
and candidate.code in _CANDIDATE_UNAVAILABLE_HTTP_STATUS
):
return True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Implicit exception context (not suppressed) is treated as a failover signal

_is_adaptive_failover_error traverses __cause__ and, when not suppressed, __context__. This means a non-transient wrapper error whose implicit __context__ incidentally holds a transient 429 (e.g. an unrelated exception was being handled when a terminal error was raised without raise ... from None) would be classified as adaptive and trigger failover. The suppressed-context case (raise ... from None) is correctly excluded and tested, but incidental (non-suppressed) transient context could over-trigger failover. This matches the documented "bounded, cycle-safe cause/context chain" design, but the reliance on implicit context is a subtle heuristic worth keeping in mind if provider SDKs raise terminal errors while handling transient ones.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +149 to +151
raise RuntimeError(
f"all {len(candidates)} candidate agents failed for passthrough endpoint={endpoint}"
) from last_error

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Adaptive single-candidate transient failures now surface as a generic RuntimeError

For an adaptive (virtual/omitted-model) request where _failover_candidates yields a single candidate that fails transiently, the loop in passthrough_failover.py exhausts and raises RuntimeError("all 1 candidate agents failed ...") from last_error, discarding the provider's original error type at the top of the stack (it is preserved only as __cause__). Callers that previously inspected the raw provider HTTPError (e.g. 429 with Retry-After) from the base path will now see a wrapped RuntimeError. This is consistent with the exhaustion test (test_passthrough_provider_failover.py) but differs from the sticky concrete-model path which re-raises the original error.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 3463 to 3468
ranked = [
agent
for agent in self._ranked_agents("", capability)
for agent in self._ranked_agents("", capability, require_chat_model=False)
if not agent.disabled
and capability in agent.tags
and capability not in agent.provider_exclusions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: select_capability_agent intentionally bypasses the chat-model gate

select_capability_agent calls _ranked_agents("", capability, require_chat_model=False) (orchestrator.py), so non-chat models (e.g. text-embedding-3-large) can still be selected for capability-tag-driven paths like embeddings. This is correct for the embeddings route (which uses embed_many, not chat), but note that the transport-level chat() guard would fail closed if such an agent were ever routed to chat(). The separation of transport-compat vs role-eligibility is the intended design per the incident doc.

(Refers to this code)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1142 to +1150
if not is_chat_compatible_model_id(agent.model):
return {
"agent_id": agent.id,
"model": agent.model,
"status": "not_ready",
"latency_ms": round((time.monotonic() - started) * 1000, 2),
"error_type": "ValueError",
"failure_code": "non_chat_model",
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Readiness probe reports embedding agents as not_ready

ModelClient.probe now returns status=not_ready, failure_code=non_chat_model before any transport for any agent whose model id is not chat-compatible (orchestrator.py:985-993). Provider readiness reports iterate enabled workers, so a correctly configured embedding agent (model id like text-embedding-3-large) will always surface as not_ready in /api/v1/provider_readiness/latest. The incident doc item 9 states this is intended for chat transport, but combined with BUG-0001 it means embedding agents are effectively invisible/unusable through both readiness and auto-selection paths. Reviewer should confirm the readiness surface for embedding-only pools is acceptable.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1542 to +1553
normalized_endpoint = endpoint.strip("/")
if normalized_endpoint.startswith("v1/"):
normalized_endpoint = normalized_endpoint[3:]
if (
normalized_endpoint in {"chat/completions", "completions", "responses"}
and not is_chat_compatible_model_id(agent.model)
):
raise ValueError(
f"model {agent.model!r} is not chat-compatible and cannot serve {agent.id!r}"
)
if agent.base_url.startswith("mock://"):
return self._mock_raw(agent, normalized_endpoint, payload)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: proxy_send endpoint normalization fixes prior double-v1 path

proxy_send/proxy_send_once now strip a leading v1/ from the endpoint before building the provider URL (orchestrator.py:1376-1378, 1402-1404). Previously passing /v1/chat/completions or /v1/responses would (a) build a double /v1/v1/... provider path via _provider_url and (b) fail the endpoint.strip("/") == "responses" mock/local-responses detection. The normalization is a genuine behavioral fix, exercised by the new parametrized test_local_responses_passthrough_adapts_to_chat_transport. No current production caller passed a v1/-prefixed endpoint, so this only adds tolerance.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 319 to +355
def select_cheapest_discovered_agent(
discovered: list[DiscoveredModel], price_book: "PriceBook"
) -> DiscoveredModel | None:
"""Pick the lowest-cost discovered model per the price book (auto-optimization).
"""Pick the lowest-cost general chat-agent model per the price book.

Reuses :func:`~contextual_orchestrator.batch_routing.cheapest_upstream`, the
existing cost-optimizing upstream selector. Call :func:`refresh_price_book`
first so discovered pricing is visible; an unpriced candidate costs ``0``
Uses the same representative request cost as the top-N selector. Call
:func:`refresh_price_book` first so discovered pricing is visible; an
unpriced candidate costs ``0``
under that selector's documented contract and is treated as free, not
unknown -- so a genuinely unpriced provider (e.g. Bytez, priced by
GPU-second rather than per token) will always look cheapest here. Fine for
"auto-pick something free to try," but callers doing real cost comparison
should refresh pricing for every candidate they care about first.
"""
if not discovered:
eligible = [model for model in discovered if is_general_chat_agent_model_id(model.model_id)]
if not eligible:
return None
candidates = [{"provider": model.provider_name, "model": model.model_id} for model in discovered]
winner = cheapest_upstream(candidates, price_book)
if winner is None:
return None
for model in discovered:
if model.provider_name == winner["provider"] and model.model_id == winner["model"]:
return model
return None # pragma: no cover - winner always comes from candidates
return min(eligible, key=lambda model: _discovered_cost(model, price_book))


def select_top_n_cheapest_discovered_agents(
discovered: list[DiscoveredModel], price_book: "PriceBook", limit: int
) -> list[DiscoveredModel]:
"""Return the ``limit`` lowest-cost discovered models, cheapest first.

For bootstrapping a CI sidecar (or any first-boot pool) with more than one
enabled agent for failover, without hand-picking which discovered models to
trust. Same pricing contract as :func:`select_cheapest_discovered_agent`.
"""
if limit <= 0 or not discovered:
"""Return the ``limit`` cheapest general chat-agent models in ascending cost."""
if limit <= 0:
return []
eligible = [model for model in discovered if is_general_chat_agent_model_id(model.model_id)]
if not eligible:
return []

return sorted(eligible, key=lambda model: _discovered_cost(model, price_book))[:limit]

def _cost(model: DiscoveredModel) -> float:
cost, _currency = price_book.compute_cost(model.provider_name, model.model_id, 1000, 1000)
return cost

return sorted(discovered, key=_cost)[:limit]
def _discovered_cost(model: DiscoveredModel, price_book: "PriceBook") -> float:
"""Price the representative discovery request used by both selectors."""
cost, _currency = price_book.compute_cost(model.provider_name, model.model_id, 1000, 1000)
return cost

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: cheapest-agent refactor keeps assumed 1000/1000 cost and input-order ties

select_cheapest_discovered_agent was rewritten from cheapest_upstream to min(eligible, key=_discovered_cost) (model_discovery.py:319-322, 338-341). _discovered_cost uses compute_cost(..., 1000, 1000), matching cheapest_upstream's default assumed shape, and min keeps the first minimum (input order) like cheapest_upstream's documented tie behavior, so selection results are equivalent aside from the new eligibility filter. No behavior regression here.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@seonghobae

Copy link
Copy Markdown
Contributor Author

@opencode-agent Review exact current HEAD 1ebb47cd15a663f634ee46caf436fea76ee51407 only. Re-check the parent-reconciled structured-output contract, one-shot adaptive passthrough failover, explicit concrete-model stickiness, temperature capability negotiation, caller-payload immutability, circuit state, and current hosted Checks. Publish a formal current-head review; do not approve from predecessor evidence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant