Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions contextual_orchestrator/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2118,6 +2118,10 @@ def _require_pool_model(
answering with a different pool agent hides capacity/routing mismatches.
"""
agents = getattr(orchestrator, "agents", None) or []
if model_name == "contextual-orchestrator" and required_capability is None:
if any(not getattr(agent, "disabled", False) for agent in agents):
return model_name
raise RequestError(400, "invalid_model", "no enabled orchestration agent is available")
Comment thread
seonghobae marked this conversation as resolved.
Comment on lines +2121 to +2124

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: Alias branch correctly scoped to capability-free endpoints

The alias branch at server.py fires only when required_capability is None. Capability endpoints (embeddings server.py:5921, batch embeddings server.py:6034, media/rerank) pass a capability and stay fail-closed. Only /v1/completions and /v1/chat/completions reach it unconditionally; the /v1/responses call at server.py:6296 is gated by an AUTO/FREE check. Scope matches intent.

Open in Devin Review

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

if model_name in {TaskOrchestrator.AUTO_MODEL, TaskOrchestrator.FREE_MODEL}:
if required_capability is None:
if model_name == TaskOrchestrator.AUTO_MODEL or any(
Comment on lines 2125 to 2127

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: Alias requires an enabled agent unlike AUTO_MODEL

The alias branch raises invalid_model when every agent is disabled (server.py), while the AUTO_MODEL path at server.py:2127 returns unconditionally even with an empty enabled pool. The divergence looks deliberate but is asymmetric.

(Refers to this code)

Open in Devin Review

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

Expand Down
59 changes: 57 additions & 2 deletions tests/test_chat_orchestration_mode_http_honesty.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ def build() -> TaskOrchestrator:
)


def _post(port: int, payload: dict) -> tuple[int, dict]:
def _post(
port: int,
payload: dict,
*,
endpoint: str = "/v1/chat/completions",
) -> tuple[int, dict]:
request = urllib.request.Request(
f"http://127.0.0.1:{port}/v1/chat/completions",
f"http://127.0.0.1:{port}{endpoint}",
data=json.dumps(payload).encode("utf-8"),
headers={
"content-type": "application/json",
Expand Down Expand Up @@ -86,6 +91,54 @@ def test_http_chat_accepts_orchestration_mode_auto() -> None:
thread.join(timeout=5)


def test_http_chat_conduct_accepts_advertised_deployment_alias() -> None:
"""The listed deployment alias must reach the multi-agent conduct path."""
server = build_server(build(), port=0, security=SecurityConfig(auth_token=_TEST_AUTH_TOKEN))
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
port = server.server_address[1]
try:
status, body = _post(
port,
{
"model": "contextual-orchestrator",
"messages": [{"role": "user", "content": "analyze and verify this synthetic task"}],
"orchestration_mode": "conduct",
"include_orchestration_trace": True,
},
)
assert status == 200, body
assert body["model"] == "contextual-orchestrator"
assert body["orchestration"]["mode"] == "conduct"
assert len(body["orchestration"]["trace"]) > 1
finally:
server.shutdown()
thread.join(timeout=5)


def test_http_legacy_completions_accepts_advertised_deployment_alias() -> None:
"""The provider-neutral deployment alias also serves legacy text completions."""
server = build_server(build(), port=0, security=SecurityConfig(auth_token=_TEST_AUTH_TOKEN))
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
port = server.server_address[1]
try:
status, body = _post(
port,
{
"model": "contextual-orchestrator",
"prompt": "summarize this synthetic task",
},
endpoint="/v1/completions",
)
assert status == 200, body
assert body["object"] == "text_completion"
assert body["model"] == "contextual-orchestrator"
finally:
server.shutdown()
thread.join(timeout=5)


def test_http_chat_rejects_invalid_mode() -> None:
server = build_server(build(), port=0, security=SecurityConfig(auth_token=_TEST_AUTH_TOKEN))
thread = threading.Thread(target=server.serve_forever, daemon=True)
Expand Down Expand Up @@ -131,6 +184,8 @@ def test_http_chat_rejects_mode_non_string() -> None:
if __name__ == "__main__":
test_http_chat_accepts_mode_route()
test_http_chat_accepts_orchestration_mode_auto()
test_http_chat_conduct_accepts_advertised_deployment_alias()
test_http_legacy_completions_accepts_advertised_deployment_alias()
test_http_chat_rejects_invalid_mode()
test_http_chat_rejects_mode_non_string()
print("ok")
Loading