Skip to content

Switch evals/benchmark to LiteLLM gateway, add dynamic model discovery - #148

Closed
saashanair wants to merge 13 commits into
mainfrom
saashanair/pro-618-switch-to-using-litellm-throughout-evalspipeline-and-allow
Closed

Switch evals/benchmark to LiteLLM gateway, add dynamic model discovery#148
saashanair wants to merge 13 commits into
mainfrom
saashanair/pro-618-switch-to-using-litellm-throughout-evalspipeline-and-allow

Conversation

@saashanair

Copy link
Copy Markdown

Why

Model access should be allowed only through the central i.AI LLM gateway (LiteLLM). This PR makes that true everywhere in this repo — evals, synthetic data generation, and the benchmark CLI — and replaces the hardcoded model list with one that updates automatically as the gateway's model list changes.

What changed

Dynamic model discovery (new)

evals/utils_gateway.py fetches the gateway's /model_group/info and /health/latest and turns them into a list of chat-capable models, each with vendor family and health resolved. Filtering is composable (split_unhealthy, filter_by_family, select_by_name) so callers — not this module — decide what to do with unhealthy or unknown matches.

benchmark.py — no more hardcoded model list

The old MODEL_REGISTRY (Azure/Vertex/locai) is gone, along with the Vertex code path (it never worked — create_llm() unconditionally raised NotImplementedError for it). Instead relevant models are found dynamically by querying the gateway.

Before After
--provider {azure,vertex,locai,all} --models <name>... / --family {gpt,claude,gemini,locai}... / --all (exactly one required)
reasoning_effort hardcoded per model name; no way to sweep effort levels Creates a separate config per effort level by sweeping --reasoning-effort {low,medium,high}... across all models whose supports_reasoning flag, read from the gateway, is true

Model selection (--models vs. --family/--all) is two small functions, _select_named_models/_select_healthy_models, each returning only the fields relevant to its own mode — --models warns on unhealthy matches but still runs them (explicit request by name); --family/--all drops unhealthy matches and reports what was dropped.

Synthetic data generation — off direct Azure

evals/synthetic/* now talks to the gateway (openai.AsyncOpenAI(base_url=LLM_GATEWAY_URL, ...)) instead of constructing an AsyncAzureOpenAI client directly. The two models it uses are now named constants (DRAFTING_MODEL, RESPONSE_GENERATION_MODEL) instead of repeated string literals.

Gateway credentials — one source of truth

LLM_GATEWAY_URL/CONSULT_EVAL_LITELLM_API_KEY were each read directly via os.getenv in 10 separate places across benchmark.py, metrics.py, all four eval_*.py files, synthetic/cli.py, and generate_synthetic.py — only utils_gateway.py's own client validated them before use; everywhere else silently passed None/None into OpenAILLM/AsyncOpenAI on a misconfigured environment. Added utils_gateway.gateway_credentials() as the single source of truth; every call site now gets the same fail-fast validation.

Config cleanup

.env.example and eval.yml no longer list AZURE_OPENAI_*/GOOGLE_CLOUD_*/LOCAI_* — confirmed unused anywhere in the repo post-migration. LLM_GATEWAY_URL/CONSULT_EVAL_LITELLM_API_KEY are documented (previously required but missing from .env.example). AUTO_EVAL_4_1_SWEDEN_DEPLOYMENT (read by the eval judge/scoring path) is now prefilled with a known-working value plus a TODO, instead of being left blank and failing with a confusing 400 partway through a run.

Tests

35 tests (test_utils_gateway.py, test_benchmark.py) covering the discovery and selection logic against mocked gateway responses.

Test plan

Verified live against the real gateway:

  • benchmark.py --quick runs end-to-end
  • benchmark.py --models gpt-4.1-nano-sweden --evals mapping runs and returns real scores
  • benchmark.py --family locai --evals mapping — selection/execution mechanism confirmed working (discovered, filtered to family, excluded the unhealthy match, ran the rest); the eval itself failed for an unrelated, pre-existing reason (locai's structured-output handling), not a regression from this PR
  • benchmark.py with no selector errors as expected (exit 2)
  • generate_synthetic.py runs end-to-end (1 question, 10 responses) — verified real output on disk
  • All 35 unit tests pass
  • pre-commit clean (ruff, ruff format, secret/IP/AWS-key scans, nbstripout)

Not yet verified:

  • benchmark.py --models <unhealthy-name> through a full run (the warn-but-still-run behavior is unit-tested; no currently-unhealthy model was available on the live gateway to exercise it end-to-end)
  • CI (eval.yml, pre-commit.yml) on this actual PR

🤖 Generated with Claude Code

saashanair and others added 12 commits August 6, 2026 11:13
The eval suite currently selects models from a static, hand-maintained
list that goes stale as the gateway's roster changes. discover_chat_models()
fetches the live chat-capable models from /model_group/info and cross-checks
/health/latest to exclude confirmed-unhealthy ones, so the model list tracks
the gateway automatically instead of needing manual updates.

httpx is added as an explicit dependency since it's used directly here,
not just transitively via openai.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
discover_chat_models() used to exclude unhealthy models itself, which
meant --models (selecting by exact name) couldn't tell "not found on
the gateway" apart from "found but flagged unhealthy" — both looked
identical (absent from the result). Discovery now returns every chat
model unfiltered, with filter_by_family, exclude_unhealthy, and
select_by_name as separate composable steps benchmark.py can apply
based on which selection mode (--family/--all/--models) is active.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reads supports_reasoning straight off /model_group/info per model (no
name-based heuristic fallback — verified live that it's reliable for
every reasoning model currently on the gateway), so benchmark.py's
model selection no longer needs a hardcoded per-model reasoning list.
Also exposes KNOWN_FAMILIES as the single source of truth for valid
--family values, and adds a shared make_gateway_model() test fixture
factory used by both test_utils_gateway.py and test_benchmark.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deletes the hardcoded LLMProvider/AZURE_MODELS/VERTEX_MODELS/
LOCAI_MODELS/MODEL_REGISTRY setup in favour of discover_chat_models(),
so the eval suite tracks whatever's actually on the gateway instead of
a manually maintained list. Replaces --provider/--location with
--models/--family/--all (exactly one required), and adds
--reasoning-effort to sweep reasoning-capable models across low/medium/
high as separate variants.

Also extracts model-selection logic out of main() into three small,
independently testable functions, with new coverage in
test_benchmark.py, and reinstates lightweight per-model-name grouping
in BenchmarkRunner so --reasoning-effort variants of the same gateway
model don't fire concurrent requests at one deployment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Routes question/context/theme/response generation through the LLM
gateway (openai.AsyncOpenAI(base_url=LLM_GATEWAY_URL, ...)) instead of
constructing an Azure client directly, and centralises the two model
names (previously duplicated as string literals across 7 call sites)
into DRAFTING_MODEL/RESPONSE_GENERATION_MODEL constants in config.py.

Verified end-to-end against the live gateway with a minimal run
(1 question, 10 responses, no Langfuse configured).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.env.example and eval.yml still listed AZURE_OPENAI_*/GOOGLE_CLOUD_*/
LOCAI_* - all unused anywhere in evals/ or src/ after the gateway
migration. Replaces them with the two vars actually required
(LLM_GATEWAY_URL, CONSULT_EVAL_LITELLM_API_KEY), which were previously
undocumented in .env.example despite being required by CI.

Also prefills AUTO_EVAL_4_1_SWEDEN_DEPLOYMENT with a known-working
value rather than leaving it blank - an empty value causes a silent
400 ("Invalid model name passed in model=") partway through
eval_*.py/metrics.py's judge-model calls. Not a secret, just a
deployment name; a proper code-level fallback is deferred to a later
config-cleanup ticket.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Whitespace/line-wrapping only, no semantic changes - caught by running
pre-commit locally before raising the PR (pre-commit.yml runs this in CI).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rename exclude_unhealthy to split_unhealthy and return (kept, unhealthy)
instead of only the kept half, mirroring select_by_name's (found, missing)
shape - callers need the unhealthy half to report it, not just drop it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
--family/--all previously dropped unhealthy models with nothing printed.
main() now reports what was excluded. Split selector resolution into
_select_named_models/_select_healthy_models (one function per mode)
instead of a single function returning a 4-tuple where two fields were
always empty depending on which selector ran.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cut sentences that just narrated design history or restated what the
code already shows (e.g. why filter_by_family takes a list, why
_to_model_configs only expands reasoning models). Kept the ones that
protect against a future mistake: split_unhealthy keeping "unknown"
models, filter_chat_models returning raw dicts, discover_chat_models
being unfiltered by design. Also removed utils_gateway.py docstring
references to specific CLI flags (--family/--models/--all) that belong
to benchmark.py, not this module.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LLM_GATEWAY_URL/CONSULT_EVAL_LITELLM_API_KEY were each read directly
via os.getenv in 10 separate places, only one of which (utils_gateway's
_gateway_client) validated them before use - everywhere else silently
passed None/None into OpenAILLM/AsyncOpenAI on a misconfigured
environment. Added utils_gateway.gateway_credentials() as the single
source of truth; every call site now goes through it and gets the same
fail-fast validation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@linear

linear Bot commented Aug 10, 2026

Copy link
Copy Markdown

PRO-618

evals/tests/ (added in this PR) was never scoped out of tests.yml's
bare pytest invocation, so collecting it imports the wider evals/
package (eval_*.py, evaluators.py, langfuse_utils.py, etc.) which
isn't unit-tested by design - that dragged repo-wide coverage from
96% to 66%, failing the 95% gate. Scoped the coverage-gated step to
tests/, and added evals/tests/ as its own uncounted step so the 35
tests we wrote actually run in CI instead of only locally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
saashanair added a commit to i-dot-ai/consult that referenced this pull request Aug 12, 2026
Ports the code from i-dot-ai/themefinder#148 ("Switch evals/benchmark to
LiteLLM gateway, add dynamic model discovery"), raised against the old
standalone themefinder repo before it was subtree-merged into consult.
Replays that PR's diff under the themefinder/ prefix; file paths are the
only thing that changed, logic is unmodified.

Replaces the static, hand-maintained model list in evals/benchmark.py
with live discovery via the gateway's /model_group/info and
/health/latest endpoints (evals/utils_gateway.py), so evals stop
targeting deployment names that have quietly gone stale on the gateway.

Adds httpx as an explicit themefinder dependency (utils_gateway.py's
gateway calls) and regenerates uv.lock accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
saashanair added a commit to i-dot-ai/consult that referenced this pull request Aug 12, 2026
Ports the code from i-dot-ai/themefinder#148 ("Switch evals/benchmark to
LiteLLM gateway, add dynamic model discovery"), raised against the old
standalone themefinder repo before it was subtree-merged into consult.
Replays that PR's diff under the themefinder/ prefix; file paths are the
only thing that changed, logic is unmodified.

Replaces the static, hand-maintained model list in evals/benchmark.py
with live discovery via the gateway's /model_group/info and
/health/latest endpoints (evals/utils_gateway.py), so evals stop
targeting deployment names that have quietly gone stale on the gateway.

Adds httpx as an explicit themefinder dependency (utils_gateway.py's
gateway calls) and regenerates uv.lock accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
saashanair added a commit to i-dot-ai/consult that referenced this pull request Aug 13, 2026
Ports the code from i-dot-ai/themefinder#148 ("Switch evals/benchmark to
LiteLLM gateway, add dynamic model discovery"), raised against the old
standalone themefinder repo before it was subtree-merged into consult.
Replays that PR's diff under the themefinder/ prefix; file paths are the
only thing that changed, logic is unmodified.

Replaces the static, hand-maintained model list in evals/benchmark.py
with live discovery via the gateway's /model_group/info and
/health/latest endpoints (evals/utils_gateway.py), so evals stop
targeting deployment names that have quietly gone stale on the gateway.

Adds httpx as an explicit themefinder dependency (utils_gateway.py's
gateway calls) and regenerates uv.lock accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
saashanair added a commit to i-dot-ai/consult that referenced this pull request Aug 13, 2026
Ports the code from i-dot-ai/themefinder#148 ("Switch evals/benchmark to
LiteLLM gateway, add dynamic model discovery"), raised against the old
standalone themefinder repo before it was subtree-merged into consult.
Replays that PR's diff under the themefinder/ prefix; file paths are the
only thing that changed, logic is unmodified.

Replaces the static, hand-maintained model list in evals/benchmark.py
with live discovery via the gateway's /model_group/info and
/health/latest endpoints (evals/utils_gateway.py), so evals stop
targeting deployment names that have quietly gone stale on the gateway.

Adds httpx as an explicit themefinder dependency (utils_gateway.py's
gateway calls) and regenerates uv.lock accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
saashanair added a commit to i-dot-ai/consult that referenced this pull request Aug 18, 2026
Ports the code from i-dot-ai/themefinder#148 ("Switch evals/benchmark to
LiteLLM gateway, add dynamic model discovery"), raised against the old
standalone themefinder repo before it was subtree-merged into consult.
Replays that PR's diff under the themefinder/ prefix; file paths are the
only thing that changed, logic is unmodified.

Replaces the static, hand-maintained model list in evals/benchmark.py
with live discovery via the gateway's /model_group/info and
/health/latest endpoints (evals/utils_gateway.py), so evals stop
targeting deployment names that have quietly gone stale on the gateway.

Adds httpx as an explicit themefinder dependency (utils_gateway.py's
gateway calls) and regenerates uv.lock accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
saashanair added a commit to i-dot-ai/consult that referenced this pull request Aug 18, 2026
Ports the code from i-dot-ai/themefinder#148 ("Switch evals/benchmark to
LiteLLM gateway, add dynamic model discovery"), raised against the old
standalone themefinder repo before it was subtree-merged into consult.
Replays that PR's diff under the themefinder/ prefix; file paths are the
only thing that changed, logic is unmodified.

Replaces the static, hand-maintained model list in evals/benchmark.py
with live discovery via the gateway's /model_group/info and
/health/latest endpoints (evals/utils_gateway.py), so evals stop
targeting deployment names that have quietly gone stale on the gateway.

Adds httpx as an explicit themefinder dependency (utils_gateway.py's
gateway calls) and regenerates uv.lock accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@saashanair

Copy link
Copy Markdown
Author

Closing this PR. These changes have been merged into consult's version of themefinder.

@saashanair saashanair closed this Aug 18, 2026
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