Skip to content

fix(pricing): resolve custom:<name> providers against models.dev - #60023

Open
robust-WJL wants to merge 3 commits into
NousResearch:mainfrom
robust-WJL:fix/friendli-per-million-pricing
Open

fix(pricing): resolve custom:<name> providers against models.dev#60023
robust-WJL wants to merge 3 commits into
NousResearch:mainfrom
robust-WJL:fix/friendli-per-million-pricing

Conversation

@robust-WJL

Copy link
Copy Markdown

Problem

Selecting a custom_providers model backed by Friendli (e.g. zai-org/GLM-5.2) trips the expensive-model guard with absurd pricing:

zai-org/GLM-5.2 has known pricing above Hermes' safety threshold.
Input tokens:  $1400000.00/M
Output tokens: $4400000.00/M

Root cause

A custom_providers entry is slugged custom:<name> (via custom_provider_slug), so target_provider becomes e.g. custom:friendli. get_model_info("custom:friendli", ...) never matches the models.dev catalog key friendli, so the guard's preferred (correct) models.dev lookup misses and falls through to the per-endpoint pricing path.

That path (_pricing_entry_from_metadata) assumes OpenRouter's per-token convention and multiplies by 1e6. Friendli's /v1/models reports pricing per million tokens (prompt: 1.4), so 1.4 x 1_000_000 = $1,400,000/M — affecting every Friendli model.

Fix

In get_model_info, when a custom:<name> slug misses, retry the lookup with the bare <name>. Custom endpoints that mirror a known models.dev provider (Friendli, etc.) now resolve to real pricing and short-circuit before the unreliable endpoint path is ever reached. Minimal, additive, no behavior change for non-custom: providers.

Verification

Reproduced end-to-end against a real config + live Friendli endpoint:

Switching to zai-org/GLM-5.2 via custom:friendli Result
Before WARNING FIRED — $1400000.00/M in, $4400000.00/M out
After no warning; pricing resolves to 1.4 / 4.4 from models.dev

Tests: scripts/run_tests.sh tests/agent/test_models_dev.py tests/hermes_cli/test_model_cost_guard.py tests/agent/test_usage_pricing.py52 passed, 0 failed. Adds 2 invariant tests asserting custom:friendli yields identical pricing to friendli, and that an unknown custom: slug still returns None.

Note / follow-up

The underlying units bug in _pricing_entry_from_metadata (per-million treated as per-token) is still latent for custom endpoints models.dev does not know about. This PR fixes the reported symptom by the smallest change; a magnitude-guard on that function would be a sensible defense-in-depth follow-up.

A custom_providers entry is slugged "custom:<name>" (e.g. "custom:friendli"),
which never matches a models.dev provider id. get_model_info() therefore
missed the catalog and the expensive-model guard fell through to the
per-endpoint pricing path. Friendli's /models reports pricing per *million*
tokens while that path assumes per-token, so 1.4 -> $1,400,000/M tripped the
guard on every Friendli model.

Retry the lookup with the bare name when a "custom:" slug misses, so custom
endpoints that mirror a known models.dev provider resolve to real pricing and
short-circuit before the unreliable endpoint path.
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/billing Account usage, credit usage, billing (cross-cutting) P2 Medium — degraded but workaround exists labels Jul 7, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to the custom-provider cost-guard cluster — but a different mechanism. #54536 / #54422 skip the expensive-model guard for custom: providers (fix #54348); this PR instead fixes the root models.dev lookup so custom:<name> slugs (e.g. custom:friendli) resolve to real pricing rather than the mis-scaled per-endpoint path. Not a duplicate; complementary. Also related to #53305 (upstream-provider billing inference).

@robust-WJL

Copy link
Copy Markdown
Author

Hi Hermes team. Any progress on this PR? Let me know if you need any support from my side.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for isolating the catalog-miss path; current main does take it: agent/models_dev.py:703-708 returns no metadata for custom:<name>, and hermes_cli/model_cost_guard.py:75-99 then falls back to endpoint pricing.

Problems

  • The retry infers provider identity from a custom endpoint's display name alone. custom_provider_slug() is derived only from that name (hermes_cli/providers.py:635-642), while resolve_custom_provider() accepts an independent user-configured URL (hermes_cli/providers.py:667-695). An unrelated endpoint named friendli would receive Friendli pricing, limits, and capabilities.
  • Catalog misses still reach agent/usage_pricing.py:813-821, where _pricing_entry_from_metadata() unconditionally multiplies pricing by one million (agent/usage_pricing.py:776-785). The added tests cover metadata lookup but not the guard/fallback path.

Suggested changes

  • Use an explicit, validated upstream-provider mapping (or endpoint validation) rather than name-only matching.
  • Add a guard-level regression test covering both the mapped custom route and a catalog miss with endpoint pricing metadata.

Automated hermes-sweeper review.

Comment thread agent/models_dev.py Outdated

data = fetch_models_dev()
pdata = data.get(mdev_id)
if not isinstance(pdata, dict) and provider_id.startswith("custom:"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

custom:<name> is derived from an arbitrary user-configured display name, not verified vendor identity (hermes_cli/providers.py:635-695). Retrying the bare name can assign catalog pricing and capabilities to an unrelated endpoint named friendli; please use an explicit/validated upstream-provider mapping instead.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
…lay name

The previous commit (7eb5703) resolved ``custom:<name>`` providers by
retrying the catalog lookup with the bare name. That is name-only matching:
``custom_provider_slug`` is derived from the user-chosen display name while
``resolve_custom_provider`` accepts an independent base_url, so an unrelated
endpoint named "friendli" pointed elsewhere would inherit Friendli pricing,
limits and capabilities (NousResearch#60023 review).

Replace it with host-validated resolution. A custom endpoint is mapped to a
known models.dev provider only when its base_url host matches that vendor's
API host (api.friendli.ai -> friendli), via upstream_provider_id_for_base_url;
the cost guard consults that instead of the slug. A genuine Friendli endpoint
thus resolves to the per-million models.dev catalog and short-circuits the
unit-blind per-endpoint pricing path, so $1.4/M no longer becomes
$1,400,000/M and trips the guard - the original integration error. An
unrelated same-named endpoint does not validate and falls through to its own
endpoint pricing as before.

Tests cover both routes the review asked for: the mapped custom route
(catalog per-million pricing, no spurious trip) and a catalog miss with
endpoint pricing metadata (per-token -> per-million scaling preserved).

Co-Authored-By: Claude <noreply@anthropic.com>
@robust-WJL

robust-WJL commented Jul 20, 2026

Copy link
Copy Markdown
Author

Hi @teknium1 , thanks for the review. Just pushed a commit to address both of problems.

  • Changed vendor identity to be derived and resolved from registry itself, not display name (upstream_provider_id_for_base_url(base_url) in agent/models_dev.py resolves vendor identity from the endpoint's host, derived from the registry itself: each provider's registered api URL is canonicalized to a hostname and indexed to that provider)
  • A validated Friendli endpoint now resolves to the friendli models.dev catalog (per-million) and short-circuits before the unit-blind _pricing_entry_from_metadata path -> no pricing issue by now.
  • Modified test_model_cost_guard.py to do guard-level regression tests - covering host-validated custom route and catalog miss with endpoint pricing metadata.

…a hardcoded host map

upstream_provider_id_for_base_url previously matched against a hand-maintained
{_UPSTREAM_PROVIDER_HOSTS} dict (api.friendli.ai -> friendli). That special-cased
Friendli and required a code change + release for every other per-million vendor.

Replace it with a host -> provider id index built lazily from
fetch_models_dev(): each provider's registered ``api`` base URL is canonicalized
to a hostname and, when that host uniquely identifies one provider, mapped to it.
Hosts shared by multiple providers (aggregators exposing many vendors) are left
unindexed so no single upstream identity is mis-assigned. The index tracks the
catalog automatically; the cost guard consults it unchanged.

A genuine Friendli endpoint (https://api.friendli.ai/serverless/v1) still
resolves to the per-million ``friendli`` catalog short-circuiting the unit-blind
endpoint path; an unrelated host and a shared aggregator host return None and
fall through as before. Verified against the live 167-provider registry.

Tests add the shared-host (aggregator) exclusion case and reset the lazy cache
between tests via _reset_upstream_provider_host_index_cache.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/billing Account usage, credit usage, billing (cross-cutting) comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants