diff --git a/agent/copilot_acp_client.py b/agent/copilot_acp_client.py
index 575f6fd170..dc03a00f60 100644
--- a/agent/copilot_acp_client.py
+++ b/agent/copilot_acp_client.py
@@ -199,8 +199,11 @@ def _format_messages_as_prompt(
"IMPORTANT: If you take an action with a tool, you MUST output tool calls using {...} blocks with JSON exactly in OpenAI function-call shape.",
"If no tool is needed, answer normally.",
]
- if model:
- sections.append(f"Hermes requested model hint: {model}")
+ # Deliberately no "requested model" line in the prompt: the model is
+ # applied for real via ACP session/set_model, and when the backend can't
+ # honor it (org-policy-disabled id) a prompt-text mention makes the
+ # serving model FALSELY self-identify as the requested one. Identity
+ # must come from the backend, not from prompt suggestion.
# Copilot has no tools of its own that would collide with Hermes', so it
# forwards the whole toolset (no allowlist).
@@ -585,12 +588,22 @@ class CopilotACPClient:
# and never fail the whole turn over model selection.
if requested_model and requested_model != "copilot-acp":
try:
- available = {
- str(m.get("modelId") or "").strip()
+ advertised = [
+ m
for m in (
(session.get("models") or {}).get("availableModels") or []
)
if isinstance(m, dict)
+ ]
+ available = {
+ str(m.get("modelId") or "").strip()
+ for m in advertised
+ # Org-policy-disabled ids can still appear in the list;
+ # selecting one silently serves the default model, so
+ # treat them as not offered.
+ if str(
+ ((m.get("_meta") or {}).get("copilotEnablement")) or ""
+ ).strip().lower() != "disabled"
}
if not available or requested_model in available:
_request(
diff --git a/tests/agent/test_acp_openai_bridge.py b/tests/agent/test_acp_openai_bridge.py
index d1c0402604..f687461818 100644
--- a/tests/agent/test_acp_openai_bridge.py
+++ b/tests/agent/test_acp_openai_bridge.py
@@ -200,7 +200,10 @@ def test_copilot_prompt_still_carries_the_contract_and_the_tools():
assert "{...}" in prompt
assert '"name": "memory"' in prompt
assert '"name": "read_file"' in prompt # copilot forwards everything
- assert "Hermes requested model hint: gpt-5" in prompt
+ # No prompt-text model mention: the model is applied via ACP
+ # session/set_model, and a prompt hint makes a substituted backend
+ # falsely self-identify as the requested model.
+ assert "model hint" not in prompt
assert "hi" in prompt
diff --git a/tests/agent/test_copilot_acp_client.py b/tests/agent/test_copilot_acp_client.py
index 2c5cdc5e03..dbca05e95c 100644
--- a/tests/agent/test_copilot_acp_client.py
+++ b/tests/agent/test_copilot_acp_client.py
@@ -356,6 +356,9 @@ def _run_prompt_with_scripted_wire(model, session_result):
str(m.get("modelId") or "").strip()
for m in ((session.get("models") or {}).get("availableModels") or [])
if isinstance(m, dict)
+ and str(
+ ((m.get("_meta") or {}).get("copilotEnablement")) or ""
+ ).strip().lower() != "disabled"
}
if not available or requested_model in available:
wire.request(
@@ -405,6 +408,25 @@ def test_set_model_skipped_for_provider_virtual_slug():
assert all(m != "session/set_model" for m, _ in reqs)
+def test_set_model_skipped_for_policy_disabled_model():
+ # A policy-disabled id may still be advertised; selecting it silently
+ # serves the default model, so it must not be treated as offered.
+ session = {
+ "sessionId": "s1",
+ "models": {
+ "availableModels": [
+ {"modelId": "claude-sonnet-5"},
+ {
+ "modelId": "claude-fable-5",
+ "_meta": {"copilotEnablement": "disabled"},
+ },
+ ]
+ },
+ }
+ reqs = _run_prompt_with_scripted_wire("claude-fable-5", session)
+ assert all(m != "session/set_model" for m, _ in reqs)
+
+
def test_run_prompt_receives_picker_model():
# _create_chat_completion must forward `model` into _run_prompt — the
# original wiring dropped it, reducing the selection to prompt text.