Skip to content

fix(provider): fallback to config.api_key when auth-profiles.json has no key#2724

Merged
senamakel merged 3 commits into
tinyhumansai:mainfrom
ccpty:fix/provider-api-key-fallback
May 29, 2026
Merged

fix(provider): fallback to config.api_key when auth-profiles.json has no key#2724
senamakel merged 3 commits into
tinyhumansai:mainfrom
ccpty:fix/provider-api-key-fallback

Conversation

@ccpty
Copy link
Copy Markdown
Contributor

@ccpty ccpty commented May 27, 2026

When a custom provider's API key is set in config.toml but not saved through the UI into auth-profiles.json, the HTTP request would be sent without an Authorization header, causing authentication failures.

This change adds a fallback in lookup_key_for_slug() that reads from the top-level config.api_key field (which is auto-decrypted by the config loader from the enc2:-prefixed value in config.toml).

Before: api_key bytes=0 (key not found, requests fail)
After: api_key bytes=51 (key found via fallback, requests succeed)

Summary by CodeRabbit

  • New Features
    • Enhanced API key configuration resolution with additional fallback support, allowing authentication credentials to be sourced from multiple configuration locations.

Review Change Stack

… no key

When a custom provider's API key is set in config.toml but not saved
through the UI into auth-profiles.json, the HTTP request would be sent
without an Authorization header, causing authentication failures.

This change adds a fallback in lookup_key_for_slug() that reads from
the top-level config.api_key field (which is auto-decrypted by the
config loader from the enc2:-prefixed value in config.toml).

Fixes: API key bug where config.toml has api_key but HTTP requests
lack Authorization header for custom providers.
@ccpty ccpty requested a review from a team May 27, 2026 02:08
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

The PR adds a fallback authentication-key resolution mechanism in lookup_key_for_slug: when the auth-profiles.json lookup returns an empty key, the function now checks config.api_key from config.toml and returns it if non-empty, before falling back to an empty string.

Changes

Auth Key Resolution Fallback

Layer / File(s) Summary
Config fallback in key lookup
src/openhuman/inference/provider/factory.rs
The lookup_key_for_slug function now checks config.api_key when auth-profiles.json lookup yields an empty key, returning the trimmed value if non-empty.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~5 minutes

Possibly related PRs

  • tinyhumansai/openhuman#2372: Both PRs adjust how provider API keys are resolved—main PR adds a config.api_key fallback in lookup_key_for_slug, while the retrieved PR trims and validates the resolved key via OpenRouter /key before model probing.
  • tinyhumansai/openhuman#2265: Both PRs modify lookup_key_for_slug in src/openhuman/inference/provider/factory.rs—the retrieved PR routes the openai slug to lookup_openai_bearer_token, and the main PR adds a config.api_key fallback when the prior auth-profiles lookup yields an empty key.

Suggested labels

working

Poem

🐰 A fallback path, so neat and fine,
When profiles fail, config will shine,
One trim, one check, the key is found,
Authentication logic, safe and sound! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding a fallback mechanism to use config.api_key when auth-profiles.json lacks an authentication key for a provider.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 27, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/inference/provider/factory.rs`:
- Around line 641-651: The fallback returning config.api_key is currently
unconditional and can leak a global key to any provider; restrict this to only
the legacy direct-inference provider by checking that the provider endpoint for
the given slug matches config.inference_url before returning the key. In
practical terms, in the code around factory.rs where you read config.api_key and
have access to slug, resolve or compute the provider endpoint for slug (the same
logic used elsewhere in this function) and only use
config.api_key.trim().to_string() when endpoint == config.inference_url (and the
trimmed key is non-empty); otherwise skip this fallback and continue with normal
auth-profile lookup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a98ef126-9f54-4de8-a118-15e9685bb58a

📥 Commits

Reviewing files that changed from the base of the PR and between 0fddf11 and acb96fa.

📒 Files selected for processing (1)
  • src/openhuman/inference/provider/factory.rs

Comment thread src/openhuman/inference/provider/factory.rs
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Summary

PR adds a fallback in lookup_key_for_slug() to read from config.api_key when a custom provider's API key isn't found in auth-profiles.json. Tests pass, CI is green.

Findings

CodeRabbit caught a major security issue — credential leakage. The fallback applies config.api_key to any provider slug when profile lookup fails. If you have multiple cloud_providers configured, they all get the same credential, even if config.api_key was only intended for the legacy inference_url provider.

Scoping required: Only apply the fallback when the provider's endpoint matches config.inference_url. CodeRabbit's suggested fix at line 651 is the right approach — gate the fallback to check if slug points to the provider entry whose endpoint matches the configured inference_url.

This is a blocker — the credential leakage needs to be closed before merge.

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

I see a REQUEST_CHANGES is already in place for the credential leakage issue flagged by CodeRabbit. Before I can approve this, that concern needs to be addressed — the fallback should be scoped to only the legacy inference provider (where the endpoint matches config.inference_url) to avoid sending the global config.api_key to unintended providers.

Also, there's a failing CI check (test / Rust Core Tests (Windows — secrets ACL)). Once the credential leakage issue is fixed and CI passes, I'll do a full review.

@graycyrus
Copy link
Copy Markdown
Contributor

@ccpty unresolved review feedback — please address before we review.

@graycyrus
Copy link
Copy Markdown
Contributor

Unresolved review feedback from coderabbitai[bot] — please address before we review.

@graycyrus
Copy link
Copy Markdown
Contributor

@ccpty — heads up. I've re-reviewed this and the credential leakage issue from my prior REQUEST_CHANGES hasn't been addressed, and there's a CI failure on Windows (test / Rust Core Tests) that needs to be sorted out first.

The problem: the new fallback at line 651 returns config.api_key for any provider slug without checking if it's the right provider. If you have multiple cloud providers configured (openai, azure, custom), all would incorrectly receive the same credential.

The fix: scope the fallback to only apply when the slug corresponds to the provider using config.inference_url. Something like:

  • Find the cloud provider entry that matches config.inference_url (by endpoint)
  • Only apply the fallback if that entry's slug matches the incoming slug parameter

Once you fix the credential scoping AND resolve the Windows CI failure, I'll review the updated code and we can move forward. Let me know if you need clarification on the fix.

@oxoxDev oxoxDev assigned oxoxDev and unassigned oxoxDev May 28, 2026
@oxoxDev oxoxDev self-requested a review May 28, 2026 18:08
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev left a comment

Choose a reason for hiding this comment

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

Confirming CodeRabbit + @graycyrus's CHANGES_REQUESTED — the credential-leakage concern is still valid against the current single-commit diff. Adding my flag (3rd maintainer) to reinforce.

The leak

Fallback runs after standard auth resolution exhausts, gated on nothing about the slug. Consequence:

  • User has cloud_providers = [openai, anthropic, deepseek] and config.api_key = <openai-key>.
  • Anthropic / DeepSeek factory call: auth-profiles lookup empty for those slugs → config.api_key falls through → OpenAI key gets sent as Bearer to Anthropic's endpoint.
  • Cross-provider credential leak; the wrong key sits in the wrong provider's audit log.

The OpenAI OAuth fallback at line 624-642 sets the precedent for slug-gated fallbacks (if slug == "openai"). The config.api_key arm should follow the same shape — gate to the legacy direct-inference slug (the only one config.api_key was historically scoped to).

Suggested fix (inline below as a one-click suggestion block)

Determine the legacy slug by matching the provider whose endpoint equals config.inference_url, then gate the fallback on Some(slug) == legacy_inference_slug. Add a regression test that asserts the negative — config.api_key is NOT returned for a non-matching slug.

Suggested regression test

#[test]
fn fallback_to_config_api_key_only_for_legacy_inference_url_slug() {
    let config = Config {
        api_key: Some("global-key".into()),
        inference_url: Some("https://inference.example.com".into()),
        cloud_providers: vec![
            CloudProviderCreds { slug: "anthropic".into(), endpoint: "https://api.anthropic.com".into(), ..Default::default() },
            CloudProviderCreds { slug: "custom".into(),    endpoint: "https://inference.example.com".into(), ..Default::default() },
        ],
        ..Default::default()
    };
    // Legacy slug — gets the global key
    assert_eq!(lookup_key_for_slug("custom", &config).unwrap(), "global-key");
    // Non-matching slug — must NOT inherit the global key
    assert_eq!(lookup_key_for_slug("anthropic", &config).unwrap(), "");
}

The negative assertion is the load-bearing one — locks the no-leak guarantee against future refactors.

CI

1 fail: test / Rust Core Tests (Windows — secrets ACL) — same infra flake (~20min timeout) hitting multiple PRs this week. Not PR-caused. Re-run will likely clear once the scoping fix lands.

Question for the author

Was the unscoped fallback intentional (you want config.api_key to apply project-wide regardless of provider), or did the cross-provider implication just not surface in your testing? If intentional, please add a UI warning + docs note about the implication. If accidental, applying the scoping above closes the gap.

Comment on lines +641 to +651
// Fallback: read from top-level config.api_key (direct config.toml api_key).
// This handles the case where a key was set in config.toml but not saved
// through the UI into auth-profiles.json.
if let Some(config_key) = config.api_key.as_ref() {
if !config_key.trim().is_empty() {
log::debug!(
"[providers][chat-factory] auth lookup slug={} key_present=true (config.toml fallback)",
slug
);
return Ok(config_key.trim().to_string());
}
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.

Blocker (3rd maintainer flag — confirming CodeRabbit + @graycyrus) — unscoped fallback leaks config.api_key to any provider whose auth-profile lookup returned empty. One-click suggestion below gates to the legacy direct-inference slug (the only one this key was historically scoped to).

Suggested change
// Fallback: read from top-level config.api_key (direct config.toml api_key).
// This handles the case where a key was set in config.toml but not saved
// through the UI into auth-profiles.json.
if let Some(config_key) = config.api_key.as_ref() {
if !config_key.trim().is_empty() {
log::debug!(
"[providers][chat-factory] auth lookup slug={} key_present=true (config.toml fallback)",
slug
);
return Ok(config_key.trim().to_string());
}
// Fallback: read from top-level config.api_key (direct config.toml api_key).
// Only applies to the legacy direct-inference provider — the slug whose
// endpoint matches `config.inference_url`. Unscoped fallback would leak
// the global key to any provider whose auth-profile lookup returned empty
// (CodeRabbit + @graycyrus flagged this on initial review).
let legacy_inference_slug = config.inference_url.as_deref().and_then(|url| {
let norm = url.trim_end_matches('/');
config
.cloud_providers
.iter()
.find(|p| p.endpoint.trim_end_matches('/') == norm)
.map(|p| p.slug.as_str())
});
if Some(slug) == legacy_inference_slug {
if let Some(config_key) = config.api_key.as_ref() {
if !config_key.trim().is_empty() {
log::debug!(
"[providers][chat-factory] auth lookup slug={} key_present=true (config.toml fallback for legacy inference_url)",
slug
);
return Ok(config_key.trim().to_string());
}
}
}

Also add the negative-assertion regression test in factory_test.rs (see PR body for the test code). Without the negative assertion any future refactor could silently re-open the leak.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fixed in 83e0b2e. The cross-provider implication was an oversight, not intentional — applied the scoping rather than a project-wide warning. The config.api_key fallback now only fires for the legacy direct-inference slug (the cloud_providers entry whose endpoint matches config.inference_url) via a new legacy_inference_slug() helper that reuses the existing normalize_endpoint_for_compare. Any other slug with an empty auth-profile lookup still resolves to "", so no global key leaks across providers. Added your suggested negative-assertion test (config_api_key_fallback_does_not_leak_to_other_slugs) plus positive and inert-without-inference_url cases.

@senamakel senamakel self-assigned this May 29, 2026
The config.api_key fallback in lookup_key_for_slug() applied to any
provider slug whose auth-profiles.json lookup returned empty, leaking
the global key to unintended provider endpoints (e.g. an OpenAI key
sent as Bearer to Anthropic's endpoint).

Scope the fallback to the legacy direct-inference provider only — the
cloud_providers entry whose endpoint matches config.inference_url, the
single provider config.api_key was historically paired with. Add a
legacy_inference_slug() helper reusing the existing endpoint-normalize
comparison, and regression tests covering the positive case, the
load-bearing negative (no cross-provider leak), and the inert case
when inference_url is unset.

Addresses CodeRabbit, @graycyrus, and @oxoxDev CHANGES_REQUESTED on
src/openhuman/inference/provider/factory.rs:651 (PR tinyhumansai#2724).
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

All prior CHANGES_REQUESTED have been addressed in 83e0b2e.

Review Summary:

  • Credential leakage vulnerability fixed: config.api_key fallback now scoped to legacy direct-inference provider (via legacy_inference_slug() matching against config.inference_url)
  • legacy_inference_slug() properly reuses normalize_endpoint_for_compare() for consistency and filters openhuman backend
  • Regression tests cover all three cases: positive (legacy slug gets the key), negative (cross-provider leak prevention), inert (no inference_url)
  • CI all green (all checks pass)

The fix addresses @oxoxDev's and CodeRabbit's security concerns. Ready to merge once you resolve your reviews.

Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev left a comment

Choose a reason for hiding this comment

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

Walkthrough

Converting my prior CHANGES_REQUESTED (2026-05-28T18:38) to APPROVED. senamakel's 83e0b2e4 lands the exact fix shape I + CR + graycyrus asked for: a new legacy_inference_slug(config) helper that returns Some(slug) only when a cloud_providers entry's normalized endpoint matches config.inference_url AND is not the OpenHuman backend. lookup_key_for_slug then gates the config.api_key fallback on legacy_inference_slug(config) == Some(slug). No cross-provider leak. All CI green.

Verified

  • Gating semantics correct: legacy_inference_slug short-circuits to None on empty/missing inference_url, on OpenHuman-backend endpoint via looks_like_openhuman_backend, and on cloud entries via is_openhuman_cloud_entry. The fallback can fire for at most one slug per Config, and never for the OpenHuman backend slug ✓
  • Endpoint comparison reuses normalize_endpoint_for_compare — consistent with sibling matchers, no drift risk ✓
  • Doc comment in lookup_key_for_slug calls out the historical config.api_keyinference_url pairing and explicitly references the cross-provider leak concern (flagged by CodeRabbit + maintainers on #2724). Future reviewers won't re-discover the same hazard ✓
  • Tests cover all three paths:
    • Positive: config_api_key_fallback_applies_to_legacy_inference_slug — matching custom slug inherits "global-key"
    • Negative (the load-bearing assertion I asked for): config_api_key_fallback_does_not_leak_to_other_slugsanthropic slug returns "", not "global-key"
    • Inert: config_api_key_fallback_inert_without_inference_url — no inference_url → no fallback ✓
  • CI fully green across all matrices (Rust Core / Quality / Tauri / Frontend / E2E / Coverage / Smoke).

Nits

  • Optional: a 4th test covering the OpenHuman-backend short-circuit (inference_url set to backend URL → legacy_inference_slug returns None → no slug inherits the key) would pin the explicit defense-in-depth path. Today this is covered transitively by the looks_like_openhuman_backend filter being unit-tested elsewhere, so don't block — but worth a 1-liner in the next cleanup.

Questions

  • @graycyrus stale CHANGES_REQUESTED (2026-05-27T11:04) plus my prior CHANGES_REQUESTED (2026-05-28T18:38) — both predate 83e0b2e4. This APPROVE supersedes mine; re-requested you for a fresh pass to formally approve and clear yours.

@oxoxDev oxoxDev requested a review from graycyrus May 29, 2026 16:40
@senamakel senamakel merged commit fb708b9 into tinyhumansai:main May 29, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants