Skip to content

Name the broken model setting, at boot and in the 500 - #2777

Merged
harry-rhesis merged 7 commits into
mainfrom
fix/default-model-diagnostics
Sep 21, 2026
Merged

harry-rhesis merged 7 commits into
mainfrom
fix/default-model-diagnostics

Conversation

@harry-rhesis

@harry-rhesis harry-rhesis commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Purpose

A deployment that cannot build its own DEFAULT_*_MODEL gives the operator nothing to work with. Nothing at boot, and on the first execute a 500 reading {"detail": "An unexpected error occurred."}. The cause appears only in the backend logs. That is what #2671 was actually reporting, once the two questions it raised were settled: #2681 decided the 500 is correct (the model that fails is the deployment default, which an API caller cannot fix) and fixed the [Test] Java SDK failure by configuring the key on the java compose profile. What was left is the diagnostics.

What Changed

  • warn_on_unbuildable_default_models() (user_model_utils.py) tries to build each of the four DEFAULT_*_MODEL settings and logs a warning naming the setting, the model string and the reason. A warning rather than a failed boot, because a deployment that never resolves a model still has to start. It catches every exception type, not just ValueError, since huggingface raises ImportError without torch and a boot diagnostic that takes the app down would be worse than the problem it reports. Once per process: deployment settings cannot change within one, and the test suite starts a fresh app lifespan per test.
  • Called from the API lifespan and from worker_ready. A metric evaluation resolves a model on the worker, so where the API and worker environments differ that signal is the only place the worker's own gap shows up.
  • _deployment_model_error() (execution_validation.py) replaces the generic 500 body on this path with one that names the setting. Status code unchanged. It goes through internal_error's public_detail, so the exception text still stays out of the response and lands in the log with its traceback.
  • validate_execution_model loops over its two purposes so the message names the one that failed rather than both.

Only a deployment default can reach the new branch, and two commits exist to keep that true. Both change user-visible behaviour:

  • _build_configured_model now catches ImportError alongside ValueError. It caught only ValueError, so an ImportError from a provider module escaped it. huggingface raises one at import time without torch, and _require_own_credentials lets such a row through as soon as it has an endpoint. Escaping, it reached the new handler and was reported as a broken DEFAULT_*_MODEL: the deployment blamed for a model the organization picked. It now answers 400 naming their own model instead of an opaque 500.
  • _call_polyphemus_with_delegation raises ModelConfigurationError, not a bare ValueError, for an inactive or unverified account. That path is reachable: has_own_credentials exempts polyphemus rows from _require_own_credentials, with no platform key and no RHESIS_API_KEY the delegation branch runs, and nothing in the auth layer gates on is_active or is_verified. RHESIS_API_KEY is unset by design on Rhesis-hosted, so that condition holds there. It now answers 400 naming the account state rather than the deployment 500. ModelConfigurationError subclasses ValueError, so every existing handler catches it unchanged.

The handler catches (ValueError, ImportError) rather than bare Exception, so QuotaExceededError still reaches its own handler and becomes a 402.

Additional Context

  • Closes Warn at boot when a default model cannot be built, and say so in the 500 #2671. Follows Fix Java SDK execute test failing on a missing model credential #2681, which made the status-code decision this builds on and listed the startup warning as a follow-up.
  • test_validate_execution_model_generic_error asserted that the plain ValueError propagates, which is exactly what this changes, so it is replaced by tests for the status, the type, the named setting and the exception text staying out of the body. test_raises_when_construction_fails covers resolve_default_hosted_model and is untouched.
  • Not changed: handle_execution_error's ValueError branch still answers 400 with the raw message. That is a different path, inside route bodies rather than the pre-flight dependency, and out of scope here.
  • The last two commits came out of peqy's review. Its first point, that an org-configured model's failure could be misclassified as the deployment's, was right twice over: once for ImportError, and once for the ValueError paths its parenthetical mentioned.

Testing

Verified for real, not only against mocks.

With RHESIS_API_KEY unset, the boot check logs:

WARNING DEFAULT_GENERATION_MODEL=rhesis/rhesis cannot be built on this deployment: RHESIS_API_KEY is not set. Anything that resolves to it will fail when it is used.
WARNING DEFAULT_EVALUATION_MODEL=rhesis/rhesis ...
WARNING DEFAULT_EXECUTION_MODEL=rhesis/rhesis ...
WARNING DEFAULT_EMBEDDING_MODEL=rhesis/rhesis-embedding ...

Silent with the key set.

The response, through the actual http_exception_handler rather than an assertion on the exception:

500 {'detail': "This deployment's default evaluation model could not be built. Check the backend's DEFAULT_EVALUATION_MODEL setting and the credentials it needs."}

New file tests/backend/app/test_default_model_startup_check.py, 7 tests: warns per setting, survives an ImportError, silent when everything builds, runs once per process, does not burn that once-per-process flag when the settings load raises, uses the right model type per setting (the embedding default is a different SDK type), and the worker runs the same check. Three new tests in test_execution_validation.py for the 500, the purpose it names, and QuotaExceededError not being swallowed.

For the two misclassification fixes: test_model_override.py gains a case for an org's huggingface model failing on a missing optional dependency, checked to fail with the raw ImportError when the fix is reverted, and test_llm_delegation.py asserts the new type on both account states plus that it is still a ValueError subclass, which is the property that keeps existing handlers working.

cd apps/backend
uv run pytest ../../tests/backend/app ../../tests/backend/routes ../../tests/backend/security ../../tests/backend/services ../../tests/backend/utils ../../tests/backend/jobs

7074 passed, 48 skipped, 1 xfailed. All skips pre-existing. Ruff clean on every file touched.

@peqy peqy Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deployment default diagnostics are a solid improvement (startup warnings + a non-generic 500 that points to the right DEFAULT_*_MODEL).

Found 2 issues (0 critical, 2 improvements).

validate_model(db, current_user, purpose)
except ModelConfigurationError as e:
raise _convert_model_error_to_http_exception(e, "execution")
except (ValueError, ImportError) as e:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Improvement] Catching (ValueError, ImportError) here and converting it to a “deployment default … DEFAULT_*_MODEL” 500 seems risky: validate_model() can raise ImportError for org-configured models too (e.g. _build_configured_model only wraps ValueError, so an optional-dep ImportError from get_model() will escape). That would misclassify a user-selected model failure as a broken deployment default.

Fix: either wrap ImportError (and any other “SDK construction” exceptions you consider user-config errors) into ModelConfigurationError in _build_configured_model, or narrow this handler so it only triggers when you know you’re actually building the deployment default (and adjust _deployment_model_error’s docstring accordingly).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch fix 👍. With _build_configured_model now catching (ValueError, ImportError) and re-raising ModelConfigurationError, this (ValueError, ImportError) handler should only apply to deployment defaults as intended. Looks good to me now.

global _default_models_checked
if _default_models_checked:
return
_default_models_checked = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Improvement] _default_models_checked is set to True before any of the work runs. If get_model_settings() (or something unexpected in the loop) ever raises before you hit the except Exception around get_model(), the check will be permanently disabled for the process.

Fix: set _default_models_checked = True after settings = get_model_settings() (or at the end), or only flip it once the loop has started successfully.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep—moving _default_models_checked = True to after get_model_settings() (and adding the regression test) avoids permanently disabling the check on an early failure. Thanks for the update.

@harry-rhesis
harry-rhesis force-pushed the fix/default-model-diagnostics branch from d4c8289 to ccd160b Compare September 19, 2026 13:15

@peqy peqy Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main concern still open from the earlier inline threads:

  1. validate_execution_model() catching (ValueError, ImportError) and always turning it into a “deployment default DEFAULT_*_MODEL is broken” 500 can misclassify org-configured model failures (e.g. ImportError from get_model() isn’t wrapped into ModelConfigurationError, and some org/user-paths can raise ValueError). This will return the wrong status/message and misdirect operators.

Fix: ensure org-configured model construction errors (incl. ImportError, and any other “misconfig” exceptions you consider) are wrapped into ModelConfigurationError in _build_configured_model, or narrow the new handler so it only triggers when you’ve positively identified the deployment default path.

  1. warn_on_unbuildable_default_models() sets _default_models_checked = True before get_model_settings() / the loop runs; an unexpected exception there would permanently disable the check for the process.

Fix: flip the flag after successfully loading settings (or at the end / via try/finally that only marks success when the check actually ran).

Found 2 issues (0 critical, 2 improvements).

harry-rhesis added a commit that referenced this pull request Sep 19, 2026
_build_configured_model caught only ValueError, so an ImportError from a
provider module escaped it. huggingface raises one at import time when
torch is absent, and _require_own_credentials lets such a row through as
soon as it has an endpoint.

Escaping, it reached _deployment_model_error and was reported as a
broken DEFAULT_*_MODEL: the deployment blamed for a model the
organization picked, which is the misdirection this branch exists to
remove. Catching it alongside ValueError answers 400 naming their own
model instead, on the generic branch, an ImportError carrying no message
the classifier can read.

Also sets the once-per-process flag in warn_on_unbuildable_default_models
after the settings load rather than before, so a failure to read them
leaves the check able to run again.

Both found by peqy on #2777.
@peqy

peqy Bot commented Sep 19, 2026

Copy link
Copy Markdown

Looks good. The boot-time warnings + worker check and the 500’s public_detail naming the specific DEFAULT_*_MODEL should make this failure mode much easier to diagnose without leaking exception text. Ship it.

Found 0 issues (0 critical, 0 improvements).

harry-rhesis added a commit that referenced this pull request Sep 19, 2026
_call_polyphemus_with_delegation raised a bare ValueError for an
inactive or unverified account. That reached _deployment_model_error and
answered "this deployment's default evaluation model could not be
built", blaming the server for something the user can act on.

Reachable: has_own_credentials exempts polyphemus rows from
_require_own_credentials, so a keyless row builds; with no platform key
and no RHESIS_API_KEY the delegation branch runs; and nothing in the
auth layer gates on is_active or is_verified, so such a user reaches the
dependency. RHESIS_API_KEY is unset by design on Rhesis-hosted.

ModelConfigurationError instead. It subclasses ValueError, so existing
handlers catch it unchanged, but execution_validation matches it first
and answers 400 naming the account state.

Found by peqy on #2777, in the part of its review about org paths that
raise ValueError.
@harry-rhesis

Copy link
Copy Markdown
Contributor Author

Both numbered points were addressed in c8d4e77, which landed after this review was submitted. Your own follow-ups on the two inline threads agree, so I won't restate them.

The part worth answering is the parenthetical in (1): "and some org/user-paths can raise ValueError". That was right, and it stayed true after c8d4e77, so "should only apply to deployment defaults as intended" was a little too generous.

_call_polyphemus_with_delegation raised a bare ValueError for an inactive or unverified account, and it is reachable from validate_model:

  • has_own_credentials exempts polyphemus rows from _require_own_credentials, so a keyless row builds
  • with no platform key and no RHESIS_API_KEY, _fetch_and_configure_model falls through to the delegation branch
  • nothing in the auth layer gates on is_active or is_verified, so such a user reaches the dependency

RHESIS_API_KEY is unset by design on Rhesis-hosted, so that condition holds there. The result was an unverified user being told the deployment's DEFAULT_EVALUATION_MODEL was broken, which is exactly the misdirection this branch exists to remove.

Fixed in ba75413: it raises ModelConfigurationError now. That subclasses ValueError, so existing handlers catch it unchanged, but execution_validation matches it first and answers 400 naming the account state instead of the deployment 500.

@peqy peqy Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still looks good after the latest updates. The extra fixes to keep org-model ImportErrors and Polyphemus account-state failures classified as ModelConfigurationError avoid misreporting them as broken DEFAULT_*_MODEL deployment defaults. Ship it.

harry-rhesis added a commit that referenced this pull request Sep 19, 2026
_build_configured_model caught only ValueError, so an ImportError from a
provider module escaped it. huggingface raises one at import time when
torch is absent, and _require_own_credentials lets such a row through as
soon as it has an endpoint.

Escaping, it reached _deployment_model_error and was reported as a
broken DEFAULT_*_MODEL: the deployment blamed for a model the
organization picked, which is the misdirection this branch exists to
remove. Catching it alongside ValueError answers 400 naming their own
model instead, on the generic branch, an ImportError carrying no message
the classifier can read.

Also sets the once-per-process flag in warn_on_unbuildable_default_models
after the settings load rather than before, so a failure to read them
leaves the check able to run again.

Both found by peqy on #2777.
harry-rhesis added a commit that referenced this pull request Sep 19, 2026
_call_polyphemus_with_delegation raised a bare ValueError for an
inactive or unverified account. That reached _deployment_model_error and
answered "this deployment's default evaluation model could not be
built", blaming the server for something the user can act on.

Reachable: has_own_credentials exempts polyphemus rows from
_require_own_credentials, so a keyless row builds; with no platform key
and no RHESIS_API_KEY the delegation branch runs; and nothing in the
auth layer gates on is_active or is_verified, so such a user reaches the
dependency. RHESIS_API_KEY is unset by design on Rhesis-hosted.

ModelConfigurationError instead. It subclasses ValueError, so existing
handlers catch it unchanged, but execution_validation matches it first
and answers 400 naming the account state.

Found by peqy on #2777, in the part of its review about org paths that
raise ValueError.
@harry-rhesis
harry-rhesis force-pushed the fix/default-model-diagnostics branch from ba75413 to f981a98 Compare September 19, 2026 13:51

@peqy peqy Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. The follow-up fixes keep org-configured ImportError and Polyphemus account-state failures from being misreported as broken DEFAULT_*_MODEL settings, and the boot/worker warnings + 500 public_detail remain clear and non-leaky. Ship it.

A deployment whose DEFAULT_*_MODEL cannot be built gave no signal until
the first execute returned a 500, long after whoever set the environment
could act on it. Try building each of the four defaults at startup and
log a warning naming the setting, the model string and the reason.

A warning, not a failed boot: a deployment that never resolves a model
still has to start. Catches every exception type rather than ValueError,
because huggingface raises ImportError when torch is absent and a boot
diagnostic that takes the app down is worse than the problem it reports.

Runs on the worker as well as the API. A metric evaluation resolves a
model there, and where the two environments differ it is the only place
the worker's own gap shows up.

Once per process: deployment settings cannot change within one, and the
test suite starts a fresh app lifespan per test.

Refs #2671

Signed-off-by: Harry Cruz <harry@rhesis.ai>
Test execution on a backend that cannot build its own default model
answered "An unexpected error occurred", leaving the cause in the logs
alone. The body now names the DEFAULT_*_MODEL at fault.

Status code unchanged, as decided on #2681: the request was fine, the
server is not, and telling an API caller to check their model settings
would misdirect at a setting they cannot reach. Routed through
internal_error's public_detail, so the exception text still stays out of
the response and in the log with its traceback.

Only a deployment default gets here. Every failure to build an org's own
configured model is raised as ModelConfigurationError by
_build_configured_model and still answered with the existing 400.
Catches ImportError too, for a provider missing an optional dependency,
but not bare Exception: QuotaExceededError has to reach its own handler
to become a 402.

validate_execution_model loops over the two purposes so the message
names the one that failed rather than both.

test_validate_execution_model_generic_error asserted the plain
ValueError propagates, which is exactly what changes here, so it is
replaced by tests for the status, the type, the named setting and the
exception text staying out of the body.

Closes #2671

Signed-off-by: Harry Cruz <harry@rhesis.ai>
_build_configured_model caught only ValueError, so an ImportError from a
provider module escaped it. huggingface raises one at import time when
torch is absent, and _require_own_credentials lets such a row through as
soon as it has an endpoint.

Escaping, it reached _deployment_model_error and was reported as a
broken DEFAULT_*_MODEL: the deployment blamed for a model the
organization picked, which is the misdirection this branch exists to
remove. Catching it alongside ValueError answers 400 naming their own
model instead, on the generic branch, an ImportError carrying no message
the classifier can read.

Also sets the once-per-process flag in warn_on_unbuildable_default_models
after the settings load rather than before, so a failure to read them
leaves the check able to run again.

Both found by peqy on #2777.

Signed-off-by: Harry Cruz <harry@rhesis.ai>
_call_polyphemus_with_delegation raised a bare ValueError for an
inactive or unverified account. That reached _deployment_model_error and
answered "this deployment's default evaluation model could not be
built", blaming the server for something the user can act on.

Reachable: has_own_credentials exempts polyphemus rows from
_require_own_credentials, so a keyless row builds; with no platform key
and no RHESIS_API_KEY the delegation branch runs; and nothing in the
auth layer gates on is_active or is_verified, so such a user reaches the
dependency. RHESIS_API_KEY is unset by design on Rhesis-hosted.

ModelConfigurationError instead. It subclasses ValueError, so existing
handlers catch it unchanged, but execution_validation matches it first
and answers 400 naming the account state.

Found by peqy on #2777, in the part of its review about org paths that
raise ValueError.

Signed-off-by: Harry Cruz <harry@rhesis.ai>

@peqy peqy Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. The boot/worker warnings and the 500 public_detail that names the specific DEFAULT_*_MODEL should make this failure mode much easier to diagnose, and the follow-up fixes keep org/account errors from being misreported as deployment defaults. Ship it.

@peqy peqy Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. The startup/worker warnings and the 500 public_detail now point operators to the exact DEFAULT_*_MODEL setting while keeping exception text out of responses, and the follow-up fixes correctly prevent org/account-state failures from being misreported as broken deployment defaults. Ship it.

@peqy peqy Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. The follow-up fixes (wrapping provider ImportError and polyphemus account-state failures into ModelConfigurationError) keep the new (ValueError, ImportError) → deployment-default 500 path from misclassifying org/user errors. Boot + worker warnings and the named DEFAULT_*_MODEL 500 are solid. Ship it.

@harry-rhesis
harry-rhesis merged commit e436cd6 into main Sep 21, 2026
17 checks passed
@harry-rhesis
harry-rhesis deleted the fix/default-model-diagnostics branch September 21, 2026 09:21
@peqy

peqy Bot commented Sep 21, 2026

Copy link
Copy Markdown

Looks good. The follow-up fixes (wrapping org-side ImportError into ModelConfigurationError, and making delegation account-state failures raise ModelConfigurationError) close the last misclassification paths, so the new “deployment default model could not be built” 500 should now be limited to true DEFAULT_*_MODEL breakage. Ship it.

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.

Warn at boot when a default model cannot be built, and say so in the 500

1 participant