Skip to content

docs: align Hugging Face provider docs, errors, and tests with shipped route#2879

Open
mvanhorn wants to merge 2 commits into
Hmbown:mainfrom
mvanhorn:fix/2706-huggingface-provider-polish
Open

docs: align Hugging Face provider docs, errors, and tests with shipped route#2879
mvanhorn wants to merge 2 commits into
Hmbown:mainfrom
mvanhorn:fix/2706-huggingface-provider-polish

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Aligns the Hugging Face provider surface with the shipped route, per the polish list in #2706: documented env aliases now actually resolve (HUGGINGFACE_API_KEY primary, HF_TOKEN fallback, blank-primary handled so the fallback still fires), provider errors list huggingface among known providers, config.example.toml and docs/PROVIDERS.md / docs/MODEL_LAB.md match the code, and a new scripts/check-provider-registry.py drift check keeps the registry, docs, and example config from diverging again. TUI env handling in crates/tui/src/config.rs mirrors the config-crate resolution so both paths agree.

Testing

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features
  • cargo test --workspace --all-features (one unrelated TUI test flaked in a single run and passed on re-run; the huggingface tests passed in every run)

Checklist

  • Updated docs or comments as needed
  • Added or updated tests where relevant
  • Verified TUI behavior manually if UI changes (N/A: no rendered UI change; env resolution only)

Fixes #2706

Greptile Summary

This PR aligns the Hugging Face provider surface — env var resolution, error messages, docs, example config, and the drift-check script — with the route that was already shipped. The code changes are small and targeted: two new env-var fallbacks in the TUI path (HF_BASE_URL, HF_MODEL), a new env_api_key_for_provider helper in the config crate that correctly prioritises HUGGINGFACE_API_KEY over HF_TOKEN (including blank-primary handling), and an expanded check-provider-registry.py that keeps aliases and env-var order in sync across both crates and docs.

  • Config crate — adds ProviderKind::all() / names_hint() for richer "unknown provider" errors, and env_api_key_for_provider() so the HUGGINGFACE_API_KEYHF_TOKEN fallback chain fires in both the "skip secret store" and "secret store returned nothing" paths.
  • TUI crate — extends apply_env_overrides with HF_BASE_URL and HF_MODEL fallbacks to mirror the config-crate resolution; five new tests cover aliases, error text, short-name fallbacks, and combined-env precedence.
  • Docs / example configPROVIDERS.md, MODEL_LAB.md, and config.example.toml now document all four aliases, both base-URL env names, both model env names, and correctly place the HF inference-providers route in the "Implemented Today" section.

Confidence Score: 4/5

Safe to merge; all three resolution paths (config crate skip-secret-store, config crate secret-store fallback, TUI deepseek_api_key) now agree on HUGGINGFACE_API_KEY → HF_TOKEN priority, blank-primary handling is correct, and the new tests exercise the key scenarios end-to-end.

The functional changes are small and well-tested. The one structural concern is that ProviderKind::all() is a manually maintained list — a future enum variant added without updating it would silently disappear from error-message hints without any compiler warning. The drift-check script also has a first-occurrence limitation that could let an out-of-order resolution path go undetected. Neither issue affects current correctness.

crates/config/src/lib.rs — specifically the new ProviderKind::all() list, which must be kept in sync with the enum manually.

Important Files Changed

Filename Overview
crates/config/src/lib.rs Adds ProviderKind::all() + names_hint() for richer error messages, env_api_key_for_provider() for HUGGINGFACE_API_KEY/HF_TOKEN priority logic, and a fallback secrets path; tests cover blank-primary, short-name fallbacks, and full-name precedence. all() is manually maintained with no compile-time exhaustiveness guard.
crates/tui/src/config.rs Adds HF_BASE_URL and HF_MODEL fallbacks in apply_env_overrides to mirror the config-crate resolution; updates test EnvGuard to save/restore those new vars; adds five new Hugging Face tests covering aliases, error messages, short-name fallbacks, and combined overrides.
scripts/check-provider-registry.py Adds report_huggingface_coverage() which checks alias parity across ProviderKind/ApiProvider/docs, and report_env_lookup_order() which verifies HUGGINGFACE_API_KEY/HF_TOKEN ordering using source.find() (first occurrence); this approach can miss ordering violations in newly added code paths if an earlier correct-order site still exists in the file.
docs/PROVIDERS.md Adds huggingface aliases note, updates table row to document HF_BASE_URL and HF_MODEL fallbacks, adds dedicated Hugging Face Inference Providers Notes section with correct precedence order; aligns with shipped code.
docs/MODEL_LAB.md Moves Hugging Face inference route to Implemented Today list, restructures the HF workset section to separate shipped vs planned scope, and removes the stale native HF provider disclaimer.
config.example.toml Updates HF env var comments to document HF_BASE_URL and HF_MODEL fallbacks; adds alias list comment above [providers.huggingface] table.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User sets provider to huggingface / hf / hugging-face / hugging_face] --> B[ProviderKind::parse / ApiProvider::parse]
    B --> C{Recognized?}
    C -- No --> D[Error: unknown provider expected deepseek ... huggingface]
    C -- Yes --> E[Resolve API key]
    E --> F{CLI flag set?}
    F -- Yes --> G[Use CLI key]
    F -- No --> H{Config file has api_key?}
    H -- Yes --> I[Use config file key]
    H -- No --> J{Skip secret store?}
    J -- Yes --> K[env_api_key_for_provider]
    J -- No --> L[secrets.resolve_with_source huggingface]
    L -- Found --> M[Return value]
    L -- None --> K
    K --> N{HUGGINGFACE_API_KEY set and non-blank?}
    N -- Yes --> O[Use HUGGINGFACE_API_KEY]
    N -- No --> P{HF_TOKEN set and non-blank?}
    P -- Yes --> Q[Use HF_TOKEN]
    P -- No --> R[Error: Hugging Face API key not found]
    subgraph TUI apply_env_overrides
    S[HUGGINGFACE_BASE_URL set?] -->|Yes| T[Use HUGGINGFACE_BASE_URL]
    S -->|No| U[HF_BASE_URL set?]
    U -->|Yes| V[Use HF_BASE_URL]
    U -->|No| W[Use default router.huggingface.co/v1]
    X[HUGGINGFACE_MODEL set?] -->|Yes| Y[Use HUGGINGFACE_MODEL]
    X -->|No| Z[HF_MODEL set?]
    Z -->|Yes| AA[Use HF_MODEL]
    Z -->|No| AB[Use default model]
    end
Loading

Comments Outside Diff (1)

  1. scripts/check-provider-registry.py, line 682-696 (link)

    P2 report_string_order anchors on the first occurrence of each needle via str.find(). In crates/tui/src/config.rs the correct-priority occurrence of std::env::var("HUGGINGFACE_API_KEY") (line ~2506) and std::env::var("HF_TOKEN") (line ~2511) appear before any of the HF_TOKEN-only "is auth present?" checks later in the file. So the ordering check passes today. However, if a new resolution code path is added that consults HF_TOKEN before HUGGINGFACE_API_KEY, the check would still pass because find() returns the first (correctly ordered) site and ignores the new one. Tightening the check to scan within the specific resolution function would make the drift guard more reliable.

    Fix in Codex Fix in Claude Code Fix in Cursor

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (1): Last reviewed commit: "fix: address self-review findings" | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown

Thanks @mvanhorn for taking the time to contribute.

This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this pull request is staying open. When enforcement is enabled, pull requests from contributors who are not listed in .github/APPROVED_CONTRIBUTORS will be closed automatically.

Please read CONTRIBUTING.md for the expected contribution shape. A maintainer can grant PR access by commenting /lgtm on a pull request.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request adds support for Hugging Face Inference Providers as an OpenAI-compatible router provider, introducing aliases (huggingface, hugging-face, hugging_face, hf) and environment variable fallbacks for API keys, base URLs, and models. The changes also update documentation, add unit tests, and extend the provider registry validation script. The reviewer identified two high-severity issues in crates/tui/src/config.rs where empty environment variables (such as HUGGINGFACE_BASE_URL or HUGGINGFACE_MODEL set to empty strings) would return Ok("") and bypass the intended fallback logic to HF_BASE_URL or HF_MODEL. Using .ok().filter(...) was suggested to ensure empty strings are treated as None and correctly trigger the fallbacks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/tui/src/config.rs
Comment on lines +3464 to 3466
&& let Ok(value) =
std::env::var("HUGGINGFACE_BASE_URL").or_else(|_| std::env::var("HF_BASE_URL"))
&& !value.trim().is_empty()

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.

high

If HUGGINGFACE_BASE_URL is set to an empty string, std::env::var returns Ok(""). Because it is Ok, the or_else fallback is not evaluated, and the empty string is rejected by the subsequent !value.trim().is_empty() check, completely skipping the fallback to HF_BASE_URL.\n\nUsing .ok().filter(...) ensures that an empty primary variable is treated as None, correctly triggering the fallback.

        && let Some(value) = std::env::var("HUGGINGFACE_BASE_URL")\n            .ok()\n            .filter(|v| !v.trim().is_empty())\n            .or_else(|| std::env::var("HF_BASE_URL").ok().filter(|v| !v.trim().is_empty()))

Comment thread crates/tui/src/config.rs
Comment on lines +3678 to 3679
&& let Ok(value) = std::env::var("HUGGINGFACE_MODEL").or_else(|_| std::env::var("HF_MODEL"))
&& !value.trim().is_empty()

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.

high

Similar to the base URL fallback, if HUGGINGFACE_MODEL is set to an empty string, the or_else fallback to HF_MODEL will not be evaluated.\n\nUsing .ok().filter(...) ensures that an empty primary variable is treated as None, correctly triggering the fallback.

        && let Some(value) = std::env::var("HUGGINGFACE_MODEL")\n            .ok()\n            .filter(|v| !v.trim().is_empty())\n            .or_else(|| std::env::var("HF_MODEL").ok().filter(|v| !v.trim().is_empty()))

Comment thread crates/config/src/lib.rs
Comment on lines +134 to +156
#[must_use]
pub fn all() -> &'static [Self] {
&[
Self::Deepseek,
Self::NvidiaNim,
Self::Openai,
Self::Atlascloud,
Self::WanjieArk,
Self::Volcengine,
Self::Openrouter,
Self::XiaomiMimo,
Self::Novita,
Self::Fireworks,
Self::Siliconflow,
Self::SiliconflowCN,
Self::Arcee,
Self::Moonshot,
Self::Sglang,
Self::Vllm,
Self::Ollama,
Self::Huggingface,
]
}

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.

P2 ProviderKind::all() is a manually maintained list with no compile-time guarantee that it matches the enum's variants. If a new variant is added to ProviderKind but forgotten here, it will silently disappear from error-message hints. Consider using a crate like strum with #[derive(EnumIter)], or at minimum a compile-time const array alongside the enum so the mismatch is caught at build time.

Suggested change
#[must_use]
pub fn all() -> &'static [Self] {
&[
Self::Deepseek,
Self::NvidiaNim,
Self::Openai,
Self::Atlascloud,
Self::WanjieArk,
Self::Volcengine,
Self::Openrouter,
Self::XiaomiMimo,
Self::Novita,
Self::Fireworks,
Self::Siliconflow,
Self::SiliconflowCN,
Self::Arcee,
Self::Moonshot,
Self::Sglang,
Self::Vllm,
Self::Ollama,
Self::Huggingface,
]
}
// NOTE: Keep this list in sync with the ProviderKind enum. There is no
// compile-time check; adding a variant without updating this array will
// silently omit it from error-message hints produced by names_hint().
#[must_use]
pub fn all() -> &'static [Self] {
&[
Self::Deepseek,
Self::NvidiaNim,
Self::Openai,
Self::Atlascloud,
Self::WanjieArk,
Self::Volcengine,
Self::Openrouter,
Self::XiaomiMimo,
Self::Novita,
Self::Fireworks,
Self::Siliconflow,
Self::SiliconflowCN,
Self::Arcee,
Self::Moonshot,
Self::Sglang,
Self::Vllm,
Self::Ollama,
Self::Huggingface,
]
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code Fix in Cursor

@Hmbown

Hmbown commented Jun 10, 2026

Copy link
Copy Markdown
Owner

@mvanhorn — still wanted; it's conflicting after this week's merges. Could you rebase onto current main? Low-risk docs/tests alignment, we'll merge promptly once green.

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.

2 participants