Guide diagnose intake one question at a time

Co-authored-by: ishaan-berri <ishaan-berri@users.noreply.github.com>
This commit is contained in:
oss-agent-shin 2026-05-06 03:20:07 +00:00
parent 22ff909283
commit 5f9f4deab4
No known key found for this signature in database
3 changed files with 630 additions and 35 deletions

View file

@ -1,4 +1,5 @@
import asyncio
import base64
import copy
import importlib.metadata
import json
@ -13,7 +14,6 @@ from typing import Any, Dict, Iterable, Literal, Optional, Union, cast
import fastapi
import yaml
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from pydantic import BaseModel, Field
import litellm
from litellm._logging import verbose_logger, verbose_proxy_logger
@ -41,6 +41,7 @@ from litellm.proxy.health_check import (
perform_health_check,
run_with_timeout,
)
from litellm.proxy.health_endpoints.diagnose_types import DiagnoseRequest
from litellm.proxy.middleware.in_flight_requests_middleware import (
get_in_flight_requests,
)
@ -138,27 +139,10 @@ services = Union[
]
_DIAGNOSE_REDACTED_VALUE = "[REDACTED]"
_DIAGNOSE_LLM_TIMEOUT_SECONDS = 30
_diagnose_sensitive_masker = SensitiveDataMasker()
class DiagnoseRequest(BaseModel):
model: Optional[str] = Field(
default=None,
description=(
"Optional proxy model name to use for generating the diagnostic report. "
"When omitted, LiteLLM uses the first configured proxy deployment."
),
)
issue_description: Optional[str] = Field(
default=None,
description="Short description of the issue the admin is trying to reproduce.",
)
reproduction_steps: Optional[str] = Field(
default=None,
description="Known reproduction steps or the behavior the admin has already tried.",
)
def _redact_diagnose_payload(payload: Any, sensitive_parent: bool = False) -> Any:
if isinstance(payload, dict):
redacted: dict = {}
@ -223,19 +207,97 @@ def _build_diagnose_response(
selected_model: Optional[str],
diagnostic_report: str,
diagnostic_context: dict,
next_question: Optional[str] = None,
next_question_index: Optional[int] = None,
next_request_body: Optional[dict] = None,
next_curl: Optional[str] = None,
) -> dict:
response_context = {
key: value
for key, value in diagnostic_context.items()
if key != "diagnostic_questions"
}
return {
"used_llm": used_llm,
"selected_model": selected_model,
"litellm_version": diagnostic_context["litellm_version"],
"installation": diagnostic_context["installation"],
"redacted_config_yaml": diagnostic_context["redacted_config_yaml"],
"next_question": next_question,
"next_question_index": next_question_index,
"diagnostic_answers_received": len(
diagnostic_context.get("diagnostic_answers", [])
),
"next_request_body": next_request_body,
"next_curl": next_curl,
"redacted_config": diagnostic_context["config"],
"diagnostic_report": diagnostic_report,
"diagnostic_context": diagnostic_context,
"diagnostic_context": response_context,
}
def _encode_diagnose_questions(questions: list[str]) -> str:
serialized = json.dumps(questions, separators=(",", ":")).encode("utf-8")
return base64.urlsafe_b64encode(serialized).decode("ascii")
def _decode_diagnose_questions(session_id: Optional[str]) -> Optional[list[str]]:
if not session_id:
return None
try:
decoded = base64.urlsafe_b64decode(session_id.encode("ascii"))
questions = json.loads(decoded.decode("utf-8"))
except Exception:
return None
if not isinstance(questions, list) or not all(
isinstance(question, str) for question in questions
):
return None
return questions[:3]
def _build_diagnose_next_request(
*,
request_body: dict,
answers: list[str],
questions: list[str],
next_answer_placeholder: str,
) -> dict:
request_body = {**request_body}
request_body["diagnostic_session_id"] = _encode_diagnose_questions(questions)
request_body["diagnostic_answers"] = [*answers, next_answer_placeholder]
return request_body
def _build_diagnose_next_curl(
*,
next_request_body: dict,
next_answer_placeholder: str,
) -> str:
return (
"curl -X POST http://<your-litellm-proxy>/diagnose \\\n"
" -H 'Authorization: Bearer <admin-key>' \\\n"
" -H 'Content-Type: application/json' \\\n"
f" -d '{json.dumps(next_request_body)}'"
)
def _diagnose_answer_count(diagnose_request: DiagnoseRequest) -> int:
return len(diagnose_request.diagnostic_answers or [])
def _build_diagnose_question_report(
*, next_question_index: int, next_question: str, next_curl: str
) -> str:
return (
"# LiteLLM Diagnostic Intake\n\n"
f"Question {next_question_index} of 3:\n\n"
f"{next_question}\n\n"
"Answer it by calling `/diagnose` again with this shape:\n\n"
f"```bash\n{next_curl}\n```"
)
def _get_diagnose_model_list(llm_router: Optional[Any]) -> list:
if llm_router is None:
return []
@ -273,6 +335,10 @@ def _build_diagnose_prompt(
f"Model selected for this diagnostic LLM call: {selected_model}\n"
f"Issue description from admin: {request.issue_description or 'Not provided'}\n"
f"Known reproduction steps from admin: {request.reproduction_steps or 'Not provided'}\n\n"
"Diagnostic questions asked:\n"
f"{json.dumps(diagnostic_context['diagnostic_questions'], indent=2)}\n\n"
"Admin answers:\n"
f"{json.dumps(request.diagnostic_answers or [], indent=2)}\n\n"
"Use only this redacted diagnostic context; never invent secrets:\n"
f"```json\n{serialized_context}\n```\n\n"
"Return Markdown with these headings: Summary, Environment, Config and Models, "
@ -280,6 +346,86 @@ def _build_diagnose_prompt(
)
def _build_diagnose_questions_prompt(
*,
request: DiagnoseRequest,
selected_model: str,
diagnostic_context: dict,
) -> str:
serialized_context = json.dumps(
diagnostic_context, indent=2, sort_keys=True, default=str
)
return (
"You are debugging a LiteLLM proxy issue. Generate exactly three concise "
"questions to ask the admin before writing a reproduction report. The first "
"question must ask what issue they are seeing. The next two questions should "
"be follow-ups based on the issue, configured models, LiteLLM version, "
"installation/runtime, and redacted config. Do not ask for secrets or API keys.\n\n"
f"Model selected for this diagnostic LLM call: {selected_model}\n"
f"Known issue description: {request.issue_description or 'Not provided'}\n"
f"Known reproduction steps: {request.reproduction_steps or 'Not provided'}\n\n"
"Redacted diagnostic context:\n"
f"```json\n{serialized_context}\n```\n\n"
"Return only a numbered list with exactly three questions."
)
def _parse_diagnose_questions(raw: str) -> list[str]:
questions: list[str] = []
for line in raw.splitlines():
cleaned = line.strip().lstrip("-*").strip()
if "." in cleaned[:4]:
_, _, cleaned = cleaned.partition(".")
cleaned = cleaned.strip()
if cleaned:
questions.append(cleaned)
if len(questions) == 3:
break
return questions
def _fallback_diagnose_questions(request: DiagnoseRequest) -> list[str]:
return [
"What issue are you seeing in LiteLLM, and what did you expect to happen instead?",
(
"Which model, provider, route, and request payload reproduces the issue?"
if not request.issue_description
else "Which model, provider, route, and request payload reproduces this issue?"
),
"What errors, logs, status codes, or traces do you see when it fails?",
]
async def _generate_diagnose_questions(
*,
llm_router: Any,
selected_model: str,
request: DiagnoseRequest,
diagnostic_context: dict,
) -> list[str]:
response = await run_with_timeout(
llm_router.acompletion(
model=selected_model,
messages=[
{
"role": "user",
"content": _build_diagnose_questions_prompt(
request=request,
selected_model=selected_model,
diagnostic_context=diagnostic_context,
),
}
],
temperature=0.0,
),
_DIAGNOSE_LLM_TIMEOUT_SECONDS,
)
questions = _parse_diagnose_questions(_extract_diagnose_response_text(response))
if len(questions) < 3:
return _fallback_diagnose_questions(request=request)
return questions
def _extract_diagnose_response_text(response: Any) -> str:
try:
content = response.choices[0].message.content
@ -288,6 +434,19 @@ def _extract_diagnose_response_text(response: Any) -> str:
return content or ""
def _build_diagnose_llm_error_report(
*, diagnostic_context: dict, error: Exception
) -> str:
return (
"# LiteLLM Diagnostic Report\n\n"
"LiteLLM collected the redacted diagnostic context, but the selected "
"LLM failed while generating the final report. Use the context below "
"to reproduce the issue manually.\n\n"
f"LLM error: {str(error)}\n\n"
f"```json\n{json.dumps(diagnostic_context, indent=2, sort_keys=True, default=str)}\n```"
)
@router.get(
"/test",
tags=["health"],
@ -1757,6 +1916,8 @@ async def diagnose_endpoint(
redacted_config = _redact_diagnose_payload(proxy_config.get_config_state())
redacted_models = _redact_diagnose_payload(configured_models)
redacted_config_yaml = _dump_diagnose_config_yaml(redacted_config)
request_dict = diagnose_request.model_dump(exclude_none=True)
answers = list(diagnose_request.diagnostic_answers or [])
diagnostic_context = {
"litellm_version": _get_litellm_package_version(),
"installation": _get_litellm_installation_info(),
@ -1764,6 +1925,8 @@ async def diagnose_endpoint(
"configured_models": redacted_models,
"config": redacted_config,
"redacted_config_yaml": redacted_config_yaml,
"diagnostic_questions": [],
"diagnostic_answers": answers,
"admin_user": {
"user_id": user_api_key_dict.user_id,
"user_role": (
@ -1775,18 +1938,91 @@ async def diagnose_endpoint(
}
if selected_model is None or llm_router is None:
diagnostic_context["diagnostic_questions"] = _fallback_diagnose_questions(
request=diagnose_request
)
next_question = diagnostic_context["diagnostic_questions"][0]
next_answer_placeholder = "<answer question 1 here>"
next_request_body = _build_diagnose_next_request(
request_body=request_dict,
answers=answers,
questions=diagnostic_context["diagnostic_questions"],
next_answer_placeholder=next_answer_placeholder,
)
next_curl = _build_diagnose_next_curl(
next_request_body=next_request_body,
next_answer_placeholder=next_answer_placeholder,
)
return _build_diagnose_response(
used_llm=False,
selected_model=selected_model,
diagnostic_report=(
"# LiteLLM Diagnostic Report\n\n"
"No proxy model is configured for the diagnostic LLM call. "
"Call `/diagnose` again with a configured `model`, or add a "
"model to the proxy config, so LiteLLM can generate the full "
"Markdown reproduction report.\n\n"
f"```json\n{json.dumps(diagnostic_context, indent=2, sort_keys=True, default=str)}\n```"
"# LiteLLM Diagnostic Intake\n\n"
"No proxy model is configured for the diagnostic LLM call, so "
"LiteLLM is using built-in fallback questions.\n\n"
+ _build_diagnose_question_report(
next_question_index=1,
next_question=next_question,
next_curl=next_curl,
)
),
diagnostic_context=diagnostic_context,
next_question=next_question,
next_question_index=1,
next_request_body=next_request_body,
next_curl=next_curl,
)
diagnostic_questions = _decode_diagnose_questions(
diagnose_request.diagnostic_session_id
)
if diagnostic_questions is None:
try:
diagnostic_questions = await _generate_diagnose_questions(
llm_router=llm_router,
selected_model=selected_model,
request=diagnose_request,
diagnostic_context=diagnostic_context,
)
except Exception as e:
verbose_proxy_logger.warning(
"Failed to generate /diagnose questions with LLM: %s", e
)
diagnostic_questions = _fallback_diagnose_questions(
request=diagnose_request
)
diagnostic_context["diagnostic_questions"] = diagnostic_questions
diagnostic_answers_count = len(answers)
if diagnostic_answers_count < 3:
next_question_index = diagnostic_answers_count + 1
next_question = diagnostic_questions[diagnostic_answers_count]
next_answer_placeholder = f"<answer question {next_question_index} here>"
next_request_body = _build_diagnose_next_request(
request_body=request_dict,
answers=answers,
questions=diagnostic_questions,
next_answer_placeholder=next_answer_placeholder,
)
return _build_diagnose_response(
used_llm=True,
selected_model=selected_model,
diagnostic_report=_build_diagnose_question_report(
next_question_index=next_question_index,
next_question=next_question,
next_curl=_build_diagnose_next_curl(
next_request_body=next_request_body,
next_answer_placeholder=next_answer_placeholder,
),
),
diagnostic_context=diagnostic_context,
next_question=next_question,
next_question_index=next_question_index,
next_request_body=next_request_body,
next_curl=_build_diagnose_next_curl(
next_request_body=next_request_body,
next_answer_placeholder=next_answer_placeholder,
),
)
prompt = _build_diagnose_prompt(
@ -1794,11 +2030,31 @@ async def diagnose_endpoint(
selected_model=selected_model,
diagnostic_context=diagnostic_context,
)
response = await llm_router.acompletion(
model=selected_model,
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
)
try:
response = await run_with_timeout(
llm_router.acompletion(
model=selected_model,
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
),
_DIAGNOSE_LLM_TIMEOUT_SECONDS,
)
except Exception as e:
verbose_proxy_logger.warning(
"Failed to generate /diagnose report with LLM: %s", e
)
return _build_diagnose_response(
used_llm=False,
selected_model=selected_model,
diagnostic_report=(
"# LiteLLM Diagnostic Report\n\n"
"The diagnostic LLM call failed while generating the final report, "
"so LiteLLM is returning the redacted diagnostic context and answers "
"for support to review.\n\n"
f"```json\n{json.dumps(diagnostic_context, indent=2, sort_keys=True, default=str)}\n```"
),
diagnostic_context=diagnostic_context,
)
return _build_diagnose_response(
used_llm=True,
selected_model=selected_model,

View file

@ -0,0 +1,38 @@
from typing import Optional
from pydantic import BaseModel, Field
class DiagnoseRequest(BaseModel):
model: Optional[str] = Field(
default=None,
description=(
"Optional proxy model name to use for generating the diagnostic report. "
"When omitted, LiteLLM uses the first configured proxy deployment."
),
)
issue_description: Optional[str] = Field(
default=None,
description="Short description of the issue the admin is trying to reproduce.",
)
reproduction_steps: Optional[str] = Field(
default=None,
description="Known reproduction steps or the behavior the admin has already tried.",
)
diagnostic_answers: Optional[list[str]] = Field(
default=None,
description=(
"Answers collected so far. /diagnose returns one next_question at a time "
"plus next_request_body/next_curl showing how to send the next answer. "
"When at least three answers are provided, LiteLLM generates the final "
"support report."
),
)
diagnostic_session_id: Optional[str] = Field(
default=None,
description=(
"Opaque question-state token returned by /diagnose. Pass it back from "
"next_request_body/next_curl so LiteLLM can ask one question per request "
"without exposing future questions early."
),
)

View file

@ -659,7 +659,7 @@ def test_diagnose_requires_admin_view():
assert response.status_code == 403, response.text
def test_diagnose_generates_redacted_llm_report(monkeypatch):
def test_diagnose_asks_llm_generated_questions(monkeypatch):
"""
/diagnose should prompt a configured LLM with redacted proxy state and
return the generated Markdown reproduction report.
@ -689,7 +689,11 @@ def test_diagnose_generates_redacted_llm_report(monkeypatch):
choices=[
SimpleNamespace(
message=SimpleNamespace(
content="# Reproduction report\n\nUse this report."
content=(
"1. What issue are you seeing?\n"
"2. Which Vertex model and route fail?\n"
"3. What status code or error appears?"
)
)
)
]
@ -739,7 +743,19 @@ def test_diagnose_generates_redacted_llm_report(monkeypatch):
"[REDACTED]"
)
assert "master_key: '[REDACTED]'" in response_data["redacted_config_yaml"]
assert response_data["diagnostic_report"].startswith("# Reproduction report")
assert "diagnostic_questions" not in response_data
assert response_data["next_question"] == "What issue are you seeing?"
assert response_data["next_question_index"] == 1
assert response_data["next_request_body"]["diagnostic_answers"] == [
"<answer question 1 here>"
]
assert "diagnostic_questions" not in response_data["next_request_body"]
assert response_data["next_request_body"]["diagnostic_session_id"]
assert (
"curl -X POST http://<your-litellm-proxy>/diagnose"
in response_data["next_curl"]
)
assert response_data["diagnostic_report"].startswith("# LiteLLM Diagnostic Intake")
assert captured_call["model"] == "gpt-4o"
prompt_text = captured_call["messages"][0]["content"]
@ -749,6 +765,287 @@ def test_diagnose_generates_redacted_llm_report(monkeypatch):
assert "REDACTED" in prompt_text
def test_diagnose_generates_report_from_answers(monkeypatch):
"""
Once the admin provides answers to the three LLM-generated questions,
/diagnose should ask the LLM for the final support report.
"""
captured_calls = []
class FakeRouter:
def get_model_list(self, model_name=None):
deployments = [
{
"model_name": "gpt-4o",
"litellm_params": {"model": "openai/gpt-4o"},
"model_info": {"id": "deployment-1"},
}
]
if model_name is None:
return deployments
return [d for d in deployments if d["model_name"] == model_name]
async def acompletion(self, **kwargs):
captured_calls.append(kwargs)
content = (
"1. What issue are you seeing?\n"
"2. Which Vertex model and route fail?\n"
"3. What status code or error appears?"
if len(captured_calls) == 1
else "# Reproduction report\n\nCloud Core fails with Vertex."
)
return SimpleNamespace(
choices=[SimpleNamespace(message=SimpleNamespace(content=content))]
)
app = FastAPI()
app.include_router(_health_endpoints_module.router)
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
user_role=LitellmUserRoles.PROXY_ADMIN
)
client = TestClient(app)
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", FakeRouter())
monkeypatch.setattr(
"litellm.proxy.proxy_server.proxy_config",
SimpleNamespace(
get_config_state=lambda: {
"model_list": [{"model_name": "gpt-4o"}],
}
),
)
response = client.post(
"/diagnose",
json={
"issue_description": "Cloud Core doesn't work with Vertex.",
"diagnostic_answers": [
"Cloud Core requests through LiteLLM fail for Vertex.",
"The failing route is /chat/completions with vertex_ai/gemini-1.5-pro.",
"The proxy returns a 400 saying project/location are missing.",
],
},
)
assert response.status_code == 200, response.text
response_data = response.json()
assert response_data["diagnostic_report"].startswith("# Reproduction report")
assert "Cloud Core fails with Vertex" in response_data["diagnostic_report"]
assert response_data["next_question"] is None
assert response_data["next_request_body"] is None
assert response_data["next_curl"] is None
assert len(captured_calls) == 2
report_prompt = captured_calls[1]["messages"][0]["content"]
assert "Cloud Core doesn't work with Vertex." in report_prompt
assert "project/location are missing" in report_prompt
def test_diagnose_walks_questions_one_at_a_time(monkeypatch):
"""
/diagnose should guide admins through one question per request and show the
next request body they can copy back into curl.
"""
class FakeRouter:
def get_model_list(self, model_name=None):
deployments = [
{
"model_name": "gpt-4o",
"litellm_params": {"model": "openai/gpt-4o"},
"model_info": {"id": "deployment-1"},
}
]
if model_name is None:
return deployments
return [d for d in deployments if d["model_name"] == model_name]
async def acompletion(self, **kwargs):
return SimpleNamespace(
choices=[
SimpleNamespace(
message=SimpleNamespace(
content=(
"1. What issue are you seeing?\n"
"2. Which Vertex model and route fail?\n"
"3. What status code or error appears?"
)
)
)
]
)
app = FastAPI()
app.include_router(_health_endpoints_module.router)
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
user_role=LitellmUserRoles.PROXY_ADMIN
)
client = TestClient(app)
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", FakeRouter())
monkeypatch.setattr(
"litellm.proxy.proxy_server.proxy_config",
SimpleNamespace(
get_config_state=lambda: {"model_list": [{"model_name": "gpt-4o"}]}
),
)
response = client.post(
"/diagnose",
json={
"issue_description": "Cloud Core doesn't work with Vertex.",
"diagnostic_answers": ["Cloud Core fails on Vertex requests."],
},
)
assert response.status_code == 200, response.text
response_data = response.json()
assert response_data["next_question"] == "Which Vertex model and route fail?"
assert response_data["next_question_index"] == 2
assert response_data["diagnostic_answers_received"] == 1
assert response_data["next_request_body"]["diagnostic_answers"] == [
"Cloud Core fails on Vertex requests.",
"<answer question 2 here>",
]
assert "Cloud Core fails on Vertex requests." in response_data["next_curl"]
def test_diagnose_report_falls_back_when_llm_errors(monkeypatch):
"""
Provider failures during report generation should return a clean fallback
instead of propagating a raw 500 during an incident.
"""
captured_calls = []
class FakeRouter:
def get_model_list(self, model_name=None):
deployments = [
{
"model_name": "gpt-4o",
"litellm_params": {"model": "openai/gpt-4o"},
"model_info": {"id": "deployment-1"},
}
]
if model_name is None:
return deployments
return [d for d in deployments if d["model_name"] == model_name]
async def acompletion(self, **kwargs):
captured_calls.append(kwargs)
if len(captured_calls) == 1:
return SimpleNamespace(
choices=[
SimpleNamespace(
message=SimpleNamespace(
content=(
"1. What issue are you seeing?\n"
"2. Which Vertex model and route fail?\n"
"3. What status code or error appears?"
)
)
)
]
)
raise RuntimeError("provider unavailable")
app = FastAPI()
app.include_router(_health_endpoints_module.router)
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
user_role=LitellmUserRoles.PROXY_ADMIN
)
client = TestClient(app)
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", FakeRouter())
monkeypatch.setattr(
"litellm.proxy.proxy_server.proxy_config",
SimpleNamespace(
get_config_state=lambda: {"model_list": [{"model_name": "gpt-4o"}]}
),
)
response = client.post(
"/diagnose",
json={
"issue_description": "Cloud Core doesn't work with Vertex.",
"diagnostic_answers": [
"Cloud Core requests through LiteLLM fail for Vertex.",
"The failing route is /chat/completions.",
"The proxy returns a 400.",
],
},
)
assert response.status_code == 200, response.text
response_data = response.json()
assert response_data["used_llm"] is False
assert response_data["diagnostic_report"].startswith("# LiteLLM Diagnostic Report")
assert "provider unavailable" not in response_data["diagnostic_report"]
assert len(captured_calls) == 2
def test_diagnose_advances_to_next_question(monkeypatch):
"""
Each intake call should ask only the next unanswered question and return a
concrete request body the admin can paste back into curl.
"""
class FakeRouter:
def get_model_list(self, model_name=None):
deployments = [
{"model_name": "gpt-4o", "litellm_params": {"model": "openai/gpt-4o"}}
]
if model_name is None:
return deployments
return [d for d in deployments if d["model_name"] == model_name]
async def acompletion(self, **kwargs):
return SimpleNamespace(
choices=[
SimpleNamespace(
message=SimpleNamespace(
content=(
"1. What issue are you seeing?\n"
"2. Which Vertex model and route fail?\n"
"3. What status code or error appears?"
)
)
)
]
)
app = FastAPI()
app.include_router(_health_endpoints_module.router)
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
user_role=LitellmUserRoles.PROXY_ADMIN
)
client = TestClient(app)
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", FakeRouter())
monkeypatch.setattr(
"litellm.proxy.proxy_server.proxy_config",
SimpleNamespace(
get_config_state=lambda: {"model_list": [{"model_name": "gpt-4o"}]}
),
)
response = client.post(
"/diagnose",
json={
"issue_description": "Cloud Core doesn't work with Vertex.",
"diagnostic_answers": ["Cloud Core fails when routed to Vertex."],
},
)
assert response.status_code == 200, response.text
response_data = response.json()
assert response_data["next_question"] == "Which Vertex model and route fail?"
assert response_data["next_question_index"] == 2
assert response_data["next_request_body"]["diagnostic_answers"] == [
"Cloud Core fails when routed to Vertex.",
"<answer question 2 here>",
]
assert response_data["diagnostic_answers_received"] == 1
def test_diagnose_prompts_for_model_when_no_llm_is_configured(monkeypatch):
"""
/diagnose should return redacted context plus next-step instructions when
@ -780,7 +1077,11 @@ def test_diagnose_prompts_for_model_when_no_llm_is_configured(monkeypatch):
assert response_data["litellm_version"]
assert response_data["installation"]["install_source"]
assert "master_key: '[REDACTED]'" in response_data["redacted_config_yaml"]
assert "configured `model`" in response_data["diagnostic_report"]
assert response_data["next_question"]
assert response_data["next_request_body"]["diagnostic_answers"] == [
"<answer question 1 here>"
]
assert "No proxy model is configured" in response_data["diagnostic_report"]
assert "sk-master-secret" not in response_data["diagnostic_report"]