Switch evals/benchmark to LiteLLM gateway, add dynamic model discovery - #148
Closed
saashanair wants to merge 13 commits into
Closed
Conversation
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>
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>
10 tasks
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>
Author
|
Closing this PR. These changes have been merged into consult's version of themefinder. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.pyfetches the gateway's/model_group/infoand/health/latestand 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 listThe old
MODEL_REGISTRY(Azure/Vertex/locai) is gone, along with the Vertex code path (it never worked —create_llm()unconditionally raisedNotImplementedErrorfor it). Instead relevant models are found dynamically by querying the gateway.--provider {azure,vertex,locai,all}--models <name>.../--family {gpt,claude,gemini,locai}.../--all(exactly one required)reasoning_efforthardcoded per model name; no way to sweep effort levels--reasoning-effort {low,medium,high}...across all models whosesupports_reasoningflag, read from the gateway, is trueModel selection (
--modelsvs.--family/--all) is two small functions,_select_named_models/_select_healthy_models, each returning only the fields relevant to its own mode —--modelswarns on unhealthy matches but still runs them (explicit request by name);--family/--alldrops 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 anAsyncAzureOpenAIclient 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_KEYwere each read directly viaos.getenvin 10 separate places acrossbenchmark.py,metrics.py, all foureval_*.pyfiles,synthetic/cli.py, andgenerate_synthetic.py— onlyutils_gateway.py's own client validated them before use; everywhere else silently passedNone/NoneintoOpenAILLM/AsyncOpenAIon a misconfigured environment. Addedutils_gateway.gateway_credentials()as the single source of truth; every call site now gets the same fail-fast validation.Config cleanup
.env.exampleandeval.ymlno longer listAZURE_OPENAI_*/GOOGLE_CLOUD_*/LOCAI_*— confirmed unused anywhere in the repo post-migration.LLM_GATEWAY_URL/CONSULT_EVAL_LITELLM_API_KEYare 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 --quickruns end-to-endbenchmark.py --models gpt-4.1-nano-sweden --evals mappingruns and returns real scoresbenchmark.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 PRbenchmark.pywith no selector errors as expected (exit 2)generate_synthetic.pyruns end-to-end (1 question, 10 responses) — verified real output on diskpre-commitclean (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)eval.yml,pre-commit.yml) on this actual PR🤖 Generated with Claude Code