docs: align Hugging Face provider docs, errors, and tests with shipped route
This commit is contained in:
+3
-1
@@ -248,7 +248,8 @@ max_subagents = 10 # optional (1-20)
|
||||
# SGLang: SGLANG_BASE_URL, SGLANG_MODEL, optional SGLANG_API_KEY
|
||||
# vLLM: VLLM_BASE_URL, VLLM_MODEL, optional VLLM_API_KEY
|
||||
# Ollama: OLLAMA_BASE_URL, OLLAMA_MODEL, optional OLLAMA_API_KEY
|
||||
# Hugging Face: HUGGINGFACE_API_KEY (or HF_TOKEN), HUGGINGFACE_BASE_URL, HUGGINGFACE_MODEL
|
||||
# Hugging Face: HUGGINGFACE_API_KEY (or HF_TOKEN), HUGGINGFACE_BASE_URL (or HF_BASE_URL),
|
||||
# HUGGINGFACE_MODEL (or HF_MODEL)
|
||||
#
|
||||
# Custom DeepSeek-compatible APIs usually do not need a new provider table:
|
||||
# set `provider = "deepseek"` and override [providers.deepseek].base_url/model.
|
||||
@@ -385,6 +386,7 @@ max_subagents = 10 # optional (1-20)
|
||||
# model = "deepseek-coder:1.3b" # or any local Ollama tag
|
||||
|
||||
# Hugging Face Inference Providers (https://huggingface.co/docs/api-inference)
|
||||
# Provider aliases: huggingface, hugging-face, hugging_face, hf
|
||||
[providers.huggingface]
|
||||
# api_key = "YOUR_HF_TOKEN"
|
||||
# base_url = "https://router.huggingface.co/v1"
|
||||
|
||||
+142
-4
@@ -131,6 +131,39 @@ pub enum ProviderKind {
|
||||
}
|
||||
|
||||
impl ProviderKind {
|
||||
#[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,
|
||||
]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn names_hint() -> String {
|
||||
Self::all()
|
||||
.iter()
|
||||
.map(|provider| provider.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
@@ -698,8 +731,12 @@ impl ConfigToml {
|
||||
pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
|
||||
match key {
|
||||
"provider" => {
|
||||
self.provider = ProviderKind::parse(value)
|
||||
.with_context(|| format!("unknown provider '{value}'"))?;
|
||||
self.provider = ProviderKind::parse(value).with_context(|| {
|
||||
format!(
|
||||
"unknown provider '{value}': expected {}",
|
||||
ProviderKind::names_hint()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
"api_key" => self.api_key = Some(value.to_string()),
|
||||
"base_url" => self.base_url = Some(value.to_string()),
|
||||
@@ -1418,7 +1455,7 @@ impl ConfigToml {
|
||||
} else if let Some(value) = from_file.clone().filter(|v| !v.trim().is_empty()) {
|
||||
(Some(value), Some(RuntimeApiKeySource::ConfigFile))
|
||||
} else if should_skip_secret_store_for_provider(provider, &base_url, auth_mode.as_deref()) {
|
||||
match codewhale_secrets::env_for(provider.as_str()) {
|
||||
match env_api_key_for_provider(provider) {
|
||||
Some(value) => (Some(value), Some(RuntimeApiKeySource::Env)),
|
||||
None => (None, None),
|
||||
}
|
||||
@@ -1431,7 +1468,10 @@ impl ConfigToml {
|
||||
};
|
||||
(Some(value), Some(source))
|
||||
}
|
||||
None => (None, None),
|
||||
None => match env_api_key_for_provider(provider) {
|
||||
Some(value) => (Some(value), Some(RuntimeApiKeySource::Env)),
|
||||
None => (None, None),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1909,6 +1949,17 @@ fn should_skip_secret_store_for_provider(
|
||||
) || base_url_uses_local_host(base_url)
|
||||
}
|
||||
|
||||
fn env_api_key_for_provider(provider: ProviderKind) -> Option<String> {
|
||||
if provider == ProviderKind::Huggingface {
|
||||
return std::env::var("HUGGINGFACE_API_KEY")
|
||||
.or_else(|_| std::env::var("HF_TOKEN"))
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty());
|
||||
}
|
||||
|
||||
codewhale_secrets::env_for(provider.as_str())
|
||||
}
|
||||
|
||||
fn auth_mode_requires_api_key(auth_mode: Option<&str>) -> bool {
|
||||
matches!(
|
||||
auth_mode
|
||||
@@ -2855,6 +2906,12 @@ mod tests {
|
||||
vllm_base_url: Option<OsString>,
|
||||
ollama_api_key: Option<OsString>,
|
||||
ollama_base_url: Option<OsString>,
|
||||
huggingface_api_key: Option<OsString>,
|
||||
huggingface_token: Option<OsString>,
|
||||
huggingface_base_url: Option<OsString>,
|
||||
hf_base_url: Option<OsString>,
|
||||
huggingface_model: Option<OsString>,
|
||||
hf_model: Option<OsString>,
|
||||
codewhale_provider: Option<OsString>,
|
||||
codewhale_model: Option<OsString>,
|
||||
codewhale_base_url: Option<OsString>,
|
||||
@@ -2928,6 +2985,12 @@ mod tests {
|
||||
vllm_base_url: env::var_os("VLLM_BASE_URL"),
|
||||
ollama_api_key: env::var_os("OLLAMA_API_KEY"),
|
||||
ollama_base_url: env::var_os("OLLAMA_BASE_URL"),
|
||||
huggingface_api_key: env::var_os("HUGGINGFACE_API_KEY"),
|
||||
huggingface_token: env::var_os("HF_TOKEN"),
|
||||
huggingface_base_url: env::var_os("HUGGINGFACE_BASE_URL"),
|
||||
hf_base_url: env::var_os("HF_BASE_URL"),
|
||||
huggingface_model: env::var_os("HUGGINGFACE_MODEL"),
|
||||
hf_model: env::var_os("HF_MODEL"),
|
||||
};
|
||||
// Safety: test-only environment mutation guarded by a module mutex.
|
||||
unsafe {
|
||||
@@ -2996,6 +3059,12 @@ mod tests {
|
||||
env::remove_var("VLLM_BASE_URL");
|
||||
env::remove_var("OLLAMA_API_KEY");
|
||||
env::remove_var("OLLAMA_BASE_URL");
|
||||
env::remove_var("HUGGINGFACE_API_KEY");
|
||||
env::remove_var("HF_TOKEN");
|
||||
env::remove_var("HUGGINGFACE_BASE_URL");
|
||||
env::remove_var("HF_BASE_URL");
|
||||
env::remove_var("HUGGINGFACE_MODEL");
|
||||
env::remove_var("HF_MODEL");
|
||||
}
|
||||
guard
|
||||
}
|
||||
@@ -3084,6 +3153,12 @@ mod tests {
|
||||
Self::restore_var("VLLM_BASE_URL", self.vllm_base_url.take());
|
||||
Self::restore_var("OLLAMA_API_KEY", self.ollama_api_key.take());
|
||||
Self::restore_var("OLLAMA_BASE_URL", self.ollama_base_url.take());
|
||||
Self::restore_var("HUGGINGFACE_API_KEY", self.huggingface_api_key.take());
|
||||
Self::restore_var("HF_TOKEN", self.huggingface_token.take());
|
||||
Self::restore_var("HUGGINGFACE_BASE_URL", self.huggingface_base_url.take());
|
||||
Self::restore_var("HF_BASE_URL", self.hf_base_url.take());
|
||||
Self::restore_var("HUGGINGFACE_MODEL", self.huggingface_model.take());
|
||||
Self::restore_var("HF_MODEL", self.hf_model.take());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3987,6 +4062,13 @@ unix_socket_path = "/tmp/cw-hooks.sock"
|
||||
ProviderKind::parse("ark_wanjie"),
|
||||
Some(ProviderKind::WanjieArk)
|
||||
);
|
||||
for alias in ["huggingface", "hugging-face", "hugging_face", "hf"] {
|
||||
assert_eq!(ProviderKind::parse(alias), Some(ProviderKind::Huggingface));
|
||||
|
||||
let parsed: ConfigToml =
|
||||
toml::from_str(&format!("provider = \"{alias}\"")).expect("huggingface alias");
|
||||
assert_eq!(parsed.provider, ProviderKind::Huggingface);
|
||||
}
|
||||
|
||||
let parsed: ConfigToml =
|
||||
toml::from_str("provider = \"ark-wanjie\"").expect("wanjie provider alias");
|
||||
@@ -3997,6 +4079,17 @@ unix_socket_path = "/tmp/cw-hooks.sock"
|
||||
assert_eq!(parsed.provider, ProviderKind::Siliconflow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_provider_error_lists_huggingface() {
|
||||
let mut config = ConfigToml::default();
|
||||
let err = config
|
||||
.set_value("provider", "not-a-provider")
|
||||
.expect_err("unknown provider should fail");
|
||||
let message = err.to_string();
|
||||
assert!(message.contains("unknown provider 'not-a-provider'"));
|
||||
assert!(message.contains("huggingface"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_kind_accepts_legacy_deepseek_cn_aliases() {
|
||||
for alias in [
|
||||
@@ -4687,6 +4780,51 @@ model = "mimo-v2.5-pro"
|
||||
assert_eq!(resolved.model, ARCEE_TRINITY_LARGE_PREVIEW_MODEL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huggingface_env_precedence_prefers_documented_names() {
|
||||
let _lock = env_lock();
|
||||
let _env = EnvGuard::without_deepseek_runtime_overrides();
|
||||
// Safety: test-only environment mutation guarded by a module mutex.
|
||||
unsafe {
|
||||
env::set_var("CODEWHALE_PROVIDER", "hf");
|
||||
env::set_var("HUGGINGFACE_API_KEY", "hf-full-key");
|
||||
env::set_var("HF_TOKEN", "hf-token-fallback");
|
||||
env::set_var("HUGGINGFACE_BASE_URL", "https://hf-full.example/v1");
|
||||
env::set_var("HF_BASE_URL", "https://hf-short.example/v1");
|
||||
env::set_var("HUGGINGFACE_MODEL", "org/full-model");
|
||||
env::set_var("HF_MODEL", "org/short-model");
|
||||
}
|
||||
|
||||
let resolved =
|
||||
ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
|
||||
|
||||
assert_eq!(resolved.provider, ProviderKind::Huggingface);
|
||||
assert_eq!(resolved.api_key.as_deref(), Some("hf-full-key"));
|
||||
assert_eq!(resolved.base_url, "https://hf-full.example/v1");
|
||||
assert_eq!(resolved.model, "org/full-model");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huggingface_short_env_fallbacks_resolve_when_primary_names_are_absent() {
|
||||
let _lock = env_lock();
|
||||
let _env = EnvGuard::without_deepseek_runtime_overrides();
|
||||
// Safety: test-only environment mutation guarded by a module mutex.
|
||||
unsafe {
|
||||
env::set_var("CODEWHALE_PROVIDER", "huggingface");
|
||||
env::set_var("HF_TOKEN", "hf-token-fallback");
|
||||
env::set_var("HF_BASE_URL", "https://hf-short.example/v1");
|
||||
env::set_var("HF_MODEL", "org/short-model");
|
||||
}
|
||||
|
||||
let resolved =
|
||||
ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
|
||||
|
||||
assert_eq!(resolved.provider, ProviderKind::Huggingface);
|
||||
assert_eq!(resolved.api_key.as_deref(), Some("hf-token-fallback"));
|
||||
assert_eq!(resolved.base_url, "https://hf-short.example/v1");
|
||||
assert_eq!(resolved.model, "org/short-model");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn siliconflow_cn_base_url_env_normalizes_model_aliases() {
|
||||
let _lock = env_lock();
|
||||
|
||||
@@ -3461,7 +3461,8 @@ fn apply_env_overrides(config: &mut Config) {
|
||||
.base_url = Some(value);
|
||||
}
|
||||
if matches!(config.api_provider(), ApiProvider::Huggingface)
|
||||
&& let Ok(value) = std::env::var("HUGGINGFACE_BASE_URL")
|
||||
&& let Ok(value) =
|
||||
std::env::var("HUGGINGFACE_BASE_URL").or_else(|_| std::env::var("HF_BASE_URL"))
|
||||
&& !value.trim().is_empty()
|
||||
{
|
||||
config
|
||||
@@ -3674,7 +3675,7 @@ fn apply_env_overrides(config: &mut Config) {
|
||||
.model = Some(value);
|
||||
}
|
||||
if matches!(config.api_provider(), ApiProvider::Huggingface)
|
||||
&& let Ok(value) = std::env::var("HUGGINGFACE_MODEL")
|
||||
&& let Ok(value) = std::env::var("HUGGINGFACE_MODEL").or_else(|_| std::env::var("HF_MODEL"))
|
||||
&& !value.trim().is_empty()
|
||||
{
|
||||
config
|
||||
@@ -5763,7 +5764,9 @@ mod tests {
|
||||
huggingface_api_key: Option<OsString>,
|
||||
huggingface_token: Option<OsString>,
|
||||
huggingface_base_url: Option<OsString>,
|
||||
hf_base_url: Option<OsString>,
|
||||
huggingface_model: Option<OsString>,
|
||||
hf_model: Option<OsString>,
|
||||
}
|
||||
|
||||
impl EnvGuard {
|
||||
@@ -5860,7 +5863,9 @@ mod tests {
|
||||
let huggingface_api_key_prev = env::var_os("HUGGINGFACE_API_KEY");
|
||||
let huggingface_token_prev = env::var_os("HF_TOKEN");
|
||||
let huggingface_base_url_prev = env::var_os("HUGGINGFACE_BASE_URL");
|
||||
let hf_base_url_prev = env::var_os("HF_BASE_URL");
|
||||
let huggingface_model_prev = env::var_os("HUGGINGFACE_MODEL");
|
||||
let hf_model_prev = env::var_os("HF_MODEL");
|
||||
// Safety: test-only environment mutation guarded by a global mutex.
|
||||
unsafe {
|
||||
env::set_var("HOME", &home_str);
|
||||
@@ -5952,7 +5957,9 @@ mod tests {
|
||||
env::remove_var("HUGGINGFACE_API_KEY");
|
||||
env::remove_var("HF_TOKEN");
|
||||
env::remove_var("HUGGINGFACE_BASE_URL");
|
||||
env::remove_var("HF_BASE_URL");
|
||||
env::remove_var("HUGGINGFACE_MODEL");
|
||||
env::remove_var("HF_MODEL");
|
||||
}
|
||||
Self {
|
||||
home: home_prev,
|
||||
@@ -6044,7 +6051,9 @@ mod tests {
|
||||
huggingface_api_key: huggingface_api_key_prev,
|
||||
huggingface_token: huggingface_token_prev,
|
||||
huggingface_base_url: huggingface_base_url_prev,
|
||||
hf_base_url: hf_base_url_prev,
|
||||
huggingface_model: huggingface_model_prev,
|
||||
hf_model: hf_model_prev,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6154,7 +6163,9 @@ mod tests {
|
||||
Self::restore_var("HUGGINGFACE_API_KEY", self.huggingface_api_key.take());
|
||||
Self::restore_var("HF_TOKEN", self.huggingface_token.take());
|
||||
Self::restore_var("HUGGINGFACE_BASE_URL", self.huggingface_base_url.take());
|
||||
Self::restore_var("HF_BASE_URL", self.hf_base_url.take());
|
||||
Self::restore_var("HUGGINGFACE_MODEL", self.huggingface_model.take());
|
||||
Self::restore_var("HF_MODEL", self.hf_model.take());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10038,6 +10049,25 @@ model = "deepseek-ai/deepseek-v4-pro"
|
||||
assert_eq!(tui.status_items, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huggingface_provider_aliases_parse() {
|
||||
for alias in ["huggingface", "hugging-face", "hugging_face", "hf"] {
|
||||
assert_eq!(ApiProvider::parse(alias), Some(ApiProvider::Huggingface));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_provider_error_lists_huggingface() {
|
||||
let config = Config {
|
||||
provider: Some("not-a-provider".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let err = config.validate().expect_err("unknown provider should fail");
|
||||
let message = err.to_string();
|
||||
assert!(message.contains("Invalid provider 'not-a-provider'"));
|
||||
assert!(message.contains("huggingface"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huggingface_provider_uses_direct_defaults() -> Result<()> {
|
||||
let _lock = lock_test_env();
|
||||
@@ -10092,6 +10122,35 @@ model = "deepseek-ai/deepseek-v4-pro"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huggingface_missing_key_error_mentions_env_fallbacks() -> Result<()> {
|
||||
let _lock = lock_test_env();
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let temp_root = env::temp_dir().join(format!(
|
||||
"codewhale-tui-huggingface-missing-key-test-{}-{}",
|
||||
std::process::id(),
|
||||
nanos
|
||||
));
|
||||
fs::create_dir_all(&temp_root)?;
|
||||
let _guard = EnvGuard::new(&temp_root);
|
||||
|
||||
let config = Config {
|
||||
provider: Some("huggingface".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
config.validate()?;
|
||||
let err = config.deepseek_api_key().expect_err("missing key");
|
||||
let message = err.to_string();
|
||||
assert!(message.contains("Hugging Face API key not found"));
|
||||
assert!(message.contains("HUGGINGFACE_API_KEY"));
|
||||
assert!(message.contains("HF_TOKEN"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huggingface_env_overrides_key_base_url_and_model() -> Result<()> {
|
||||
let _lock = lock_test_env();
|
||||
@@ -10110,8 +10169,11 @@ model = "deepseek-ai/deepseek-v4-pro"
|
||||
unsafe {
|
||||
env::set_var("CODEWHALE_PROVIDER", "huggingface");
|
||||
env::set_var("HUGGINGFACE_API_KEY", "hf-env-key");
|
||||
env::set_var("HF_TOKEN", "hf-token-fallback");
|
||||
env::set_var("HUGGINGFACE_BASE_URL", "https://custom-hf.example/v1");
|
||||
env::set_var("HF_BASE_URL", "https://fallback-hf.example/v1");
|
||||
env::set_var("HUGGINGFACE_MODEL", "meta-llama/Llama-3-70B");
|
||||
env::set_var("HF_MODEL", "fallback/model");
|
||||
}
|
||||
|
||||
let config = Config::load(None, None)?;
|
||||
@@ -10121,4 +10183,34 @@ model = "deepseek-ai/deepseek-v4-pro"
|
||||
assert_eq!(config.default_model(), "meta-llama/Llama-3-70B");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huggingface_short_env_fallbacks_configure_route() -> Result<()> {
|
||||
let _lock = lock_test_env();
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let temp_root = env::temp_dir().join(format!(
|
||||
"codewhale-tui-huggingface-short-env-test-{}-{}",
|
||||
std::process::id(),
|
||||
nanos
|
||||
));
|
||||
fs::create_dir_all(&temp_root)?;
|
||||
let _guard = EnvGuard::new(&temp_root);
|
||||
|
||||
unsafe {
|
||||
env::set_var("CODEWHALE_PROVIDER", "hf");
|
||||
env::set_var("HF_TOKEN", "hf-token-value");
|
||||
env::set_var("HF_BASE_URL", "https://short-hf.example/v1");
|
||||
env::set_var("HF_MODEL", "org/short-model");
|
||||
}
|
||||
|
||||
let config = Config::load(None, None)?;
|
||||
assert_eq!(config.api_provider(), ApiProvider::Huggingface);
|
||||
assert_eq!(config.deepseek_api_key()?, "hf-token-value");
|
||||
assert_eq!(config.deepseek_base_url(), "https://short-hf.example/v1");
|
||||
assert_eq!(config.default_model(), "org/short-model");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+21
-12
@@ -7,8 +7,7 @@ those models become discoverable, evaluable, routable, servable, and exportable
|
||||
without weakening the current terminal-agent contract: local workspace control,
|
||||
explicit provider auth, approval gates, and clear privacy boundaries.
|
||||
|
||||
This document is roadmap language. It does not mean every workset below is
|
||||
implemented today.
|
||||
This document is roadmap language. Some worksets below are roadmap-only.
|
||||
|
||||
## Implemented Today
|
||||
|
||||
@@ -19,6 +18,10 @@ implemented today.
|
||||
OpenAI-compatible endpoints, SGLang, vLLM, and Ollama are supported provider
|
||||
paths where their IDs appear in `/provider`, `codewhale --provider`, or
|
||||
`codewhale models`.
|
||||
- Hugging Face Inference Providers are available through the
|
||||
OpenAI-compatible router at `https://router.huggingface.co/v1`. Select the
|
||||
route with `huggingface`, `hugging-face`, `hugging_face`, or `hf`; configure
|
||||
`HUGGINGFACE_API_KEY` or `HF_TOKEN` for auth.
|
||||
- Model auto-routing chooses a concrete DeepSeek model and thinking level per
|
||||
turn. It is not a TUI mode.
|
||||
- Fin is the fast `deepseek-v4-flash` thinking-off path for routing,
|
||||
@@ -27,11 +30,10 @@ implemented today.
|
||||
- Self-hosted OpenAI-compatible endpoints can be used through SGLang, vLLM,
|
||||
Ollama, or the generic `openai` provider configuration.
|
||||
|
||||
## Not Implemented Yet
|
||||
## Still Planned
|
||||
|
||||
- A native Hugging Face provider or Hub browser.
|
||||
- Built-in Hugging Face model card, dataset, adapter, safetensors, or Jobs
|
||||
workflows.
|
||||
- Hugging Face Hub browsing, upload/export, model card, dataset, adapter,
|
||||
safetensors, or Jobs workflows.
|
||||
- Native Unsloth, NeMo, or Arcee integrations.
|
||||
- A dedicated Model Lab UI tab.
|
||||
- Built-in benchmark suites, eval leaderboards, hosted observability, or
|
||||
@@ -57,18 +59,24 @@ describe a model as available before CodeWhale can actually route to it.
|
||||
|
||||
## Hugging Face Workset
|
||||
|
||||
Implemented today:
|
||||
|
||||
- Hugging Face Inference Providers as an explicit OpenAI-compatible router
|
||||
provider, selected with `huggingface`, `hugging-face`, `hugging_face`, or
|
||||
`hf`.
|
||||
- Model IDs are sent to the router exactly as selected, including
|
||||
org-prefixed Hugging Face model IDs.
|
||||
|
||||
Planned scope:
|
||||
|
||||
- Hub API auth and model discovery.
|
||||
- Model cards, licenses, tags, safetensors metadata, adapters, and dataset
|
||||
links surfaced in a terminal-friendly way.
|
||||
- Inference Providers as explicit provider choices when the user configures
|
||||
them.
|
||||
- Hugging Face Jobs as an optional remote execution path for user-approved
|
||||
experiments.
|
||||
|
||||
Non-goal for now: claiming a native Hugging Face provider exists before it is
|
||||
implemented in code.
|
||||
Non-goal for now: treating the router route as Hub browsing/export, or
|
||||
inferring Hub upload/export auth from the inference-provider API key.
|
||||
|
||||
## Unsloth Workset
|
||||
|
||||
@@ -138,8 +146,9 @@ Planned scope:
|
||||
- Local files, prompts, transcripts, traces, model outputs, eval results,
|
||||
adapters, datasets, and checkpoints should remain local unless the user
|
||||
explicitly chooses a provider or export destination.
|
||||
- Provider auth must remain explicit. `DEEPSEEK_*`, OpenRouter, Hugging Face,
|
||||
and self-hosted credentials should not be inferred from unrelated config.
|
||||
- Provider auth must remain explicit. `DEEPSEEK_*`, OpenRouter,
|
||||
`HUGGINGFACE_API_KEY` / `HF_TOKEN`, and self-hosted credentials should not be
|
||||
inferred from unrelated config.
|
||||
- Exportable artifacts should include provenance: source model, provider,
|
||||
route, tool policy, eval inputs, and redaction status.
|
||||
- Public sharing, hosted telemetry, sponsorship badges, and external branding
|
||||
|
||||
+18
-1
@@ -44,6 +44,11 @@ Use any of these surfaces to select a provider:
|
||||
as legacy aliases for `deepseek`. They do not select a different official host;
|
||||
DeepSeek uses the same official API host worldwide.
|
||||
|
||||
`huggingface`, `hugging-face`, `hugging_face`, and `hf` all select the
|
||||
Hugging Face Inference Providers route. This is the OpenAI-compatible router
|
||||
path for chat/inference, not Hub browsing, model-card inspection, uploads, or
|
||||
artifact export.
|
||||
|
||||
Fresh shared config writes to `~/.codewhale/config.toml`. Existing
|
||||
`~/.deepseek/config.toml` files are still read for compatibility.
|
||||
|
||||
@@ -128,7 +133,7 @@ endpoint.
|
||||
| `sglang` | `[providers.sglang]` | Optional `SGLANG_API_KEY` | `SGLANG_BASE_URL`; default `http://localhost:30000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Self-hosted OpenAI-compatible route. Localhost deployments commonly omit auth. `SGLANG_MODEL` is accepted. |
|
||||
| `vllm` | `[providers.vllm]` | Optional `VLLM_API_KEY` | `VLLM_BASE_URL`; default `http://localhost:8000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Self-hosted vLLM OpenAI-compatible route. Localhost deployments commonly omit auth. `VLLM_MODEL` is accepted. |
|
||||
| `ollama` | `[providers.ollama]` | Optional `OLLAMA_API_KEY` | `OLLAMA_BASE_URL`; default `http://localhost:11434/v1` | `deepseek-coder:1.3b`; provider-hinted custom tags pass through | Self-hosted Ollama OpenAI-compatible route. Localhost deployments commonly omit auth. `OLLAMA_MODEL` is accepted. |
|
||||
| `huggingface` | `[providers.huggingface]` | `HUGGINGFACE_API_KEY`, `HF_TOKEN` | `HUGGINGFACE_BASE_URL`; default `https://router.huggingface.co/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Hugging Face Inference Providers OpenAI-compatible route. Org-prefixed model IDs pass through. |
|
||||
| `huggingface` | `[providers.huggingface]` | `HUGGINGFACE_API_KEY`, `HF_TOKEN` | `HUGGINGFACE_BASE_URL`, `HF_BASE_URL`; default `https://router.huggingface.co/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Hugging Face Inference Providers OpenAI-compatible router route. Accepted aliases: `huggingface`, `hugging-face`, `hugging_face`, `hf`. Org-prefixed model IDs pass through. `HUGGINGFACE_MODEL` and `HF_MODEL` are accepted. Hub browsing/export are separate future features. |
|
||||
|
||||
### Xiaomi MiMo Notes
|
||||
|
||||
@@ -223,6 +228,18 @@ the endpoint's ability to accept OpenAI-compatible `tools` payloads. A custom
|
||||
OpenAI-compatible or local endpoint can still reject tool calls even if
|
||||
CodeWhale can send the schema.
|
||||
|
||||
### Hugging Face Inference Providers Notes
|
||||
|
||||
The shipped Hugging Face route targets the OpenAI-compatible Inference Providers
|
||||
router at `https://router.huggingface.co/v1`. Configure auth with
|
||||
`HUGGINGFACE_API_KEY` first, or `HF_TOKEN` as a fallback. Configure the endpoint
|
||||
with `HUGGINGFACE_BASE_URL` first, or `HF_BASE_URL` as a fallback; configure the
|
||||
model with `HUGGINGFACE_MODEL` first, or `HF_MODEL` as a fallback.
|
||||
|
||||
This route does not imply Hub browsing, model-card metadata, dataset access,
|
||||
Jobs, uploads, or export. Those remain explicit Model Lab work items so
|
||||
provider auth and artifact movement stay separate.
|
||||
|
||||
### When a Local Model Prints Tool JSON
|
||||
|
||||
CodeWhale only executes tools when the provider returns Chat Completions
|
||||
|
||||
@@ -30,6 +30,10 @@ API_PROVIDER_ONLY_IDS = {"deepseek-cn"}
|
||||
SHARED_PROVIDER_TABLES = {
|
||||
"siliconflow-CN": "siliconflow",
|
||||
}
|
||||
HUGGINGFACE_ALIASES = {"huggingface", "hugging-face", "hugging_face", "hf"}
|
||||
HUGGINGFACE_API_KEY_ENV_ORDER = ["HUGGINGFACE_API_KEY", "HF_TOKEN"]
|
||||
HUGGINGFACE_BASE_URL_ENV_ORDER = ["HUGGINGFACE_BASE_URL", "HF_BASE_URL"]
|
||||
HUGGINGFACE_MODEL_ENV_ORDER = ["HUGGINGFACE_MODEL", "HF_MODEL"]
|
||||
|
||||
|
||||
def read(path: Path) -> str:
|
||||
@@ -68,6 +72,23 @@ def extract_match_block(
|
||||
raise ValueError(f"could not parse match block after {signature!r}")
|
||||
|
||||
|
||||
def parse_aliases_for_variant(source: str, enum_name: str, variant: str, context: str) -> set[str]:
|
||||
impl_start = require_index(source, f"impl {enum_name}", context)
|
||||
block = extract_match_block(
|
||||
source,
|
||||
"pub fn parse(value: &str) -> Option<Self>",
|
||||
context,
|
||||
impl_start,
|
||||
)
|
||||
match_arm = re.search(
|
||||
rf'((?:"[^"]+"\s*\|\s*)*"[^"]+")\s*=>\s*Some\(Self::{variant}\)',
|
||||
block,
|
||||
)
|
||||
if not match_arm:
|
||||
raise ValueError(f"{context}: missing parse arm for {variant}")
|
||||
return set(re.findall(r'"([^"]+)"', match_arm.group(1)))
|
||||
|
||||
|
||||
def provider_kind_ids(config_rs: str) -> dict[str, str]:
|
||||
impl_start = require_index(
|
||||
config_rs, "impl ProviderKind", "crates/config/src/lib.rs"
|
||||
@@ -198,6 +219,76 @@ def report_provider_enum_drift(
|
||||
return errors
|
||||
|
||||
|
||||
def report_huggingface_coverage(
|
||||
config_rs: str, tui_config_rs: str, providers_md: str
|
||||
) -> list[str]:
|
||||
errors = []
|
||||
|
||||
config_aliases = parse_aliases_for_variant(
|
||||
config_rs, "ProviderKind", "Huggingface", "crates/config/src/lib.rs"
|
||||
)
|
||||
tui_aliases = parse_aliases_for_variant(
|
||||
tui_config_rs, "ApiProvider", "Huggingface", "crates/tui/src/config.rs"
|
||||
)
|
||||
errors += report_set(
|
||||
"ProviderKind Hugging Face aliases",
|
||||
HUGGINGFACE_ALIASES,
|
||||
config_aliases & HUGGINGFACE_ALIASES,
|
||||
)
|
||||
errors += report_set(
|
||||
"ApiProvider Hugging Face aliases",
|
||||
HUGGINGFACE_ALIASES,
|
||||
tui_aliases & HUGGINGFACE_ALIASES,
|
||||
)
|
||||
|
||||
inline_source = re.sub(r"```.*?```", "", providers_md, flags=re.DOTALL)
|
||||
code_spans = set(re.findall(r"`([^`]+)`", inline_source))
|
||||
errors += report_set(
|
||||
"documented Hugging Face aliases",
|
||||
HUGGINGFACE_ALIASES,
|
||||
code_spans & HUGGINGFACE_ALIASES,
|
||||
)
|
||||
|
||||
for label, env_order in [
|
||||
("Hugging Face API key env precedence", HUGGINGFACE_API_KEY_ENV_ORDER),
|
||||
("Hugging Face base URL env precedence", HUGGINGFACE_BASE_URL_ENV_ORDER),
|
||||
("Hugging Face model env precedence", HUGGINGFACE_MODEL_ENV_ORDER),
|
||||
]:
|
||||
errors += report_env_lookup_order(
|
||||
label, config_rs, env_order, "crates/config/src/lib.rs"
|
||||
)
|
||||
errors += report_env_lookup_order(
|
||||
label, tui_config_rs, env_order, "crates/tui/src/config.rs"
|
||||
)
|
||||
errors += report_string_order(label, providers_md, env_order, "docs/PROVIDERS.md")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def report_env_lookup_order(
|
||||
label: str, source: str, expected_order: list[str], context: str
|
||||
) -> list[str]:
|
||||
lookup_needles = [f'std::env::var("{name}")' for name in expected_order]
|
||||
return report_string_order(label, source, lookup_needles, context)
|
||||
|
||||
|
||||
def report_string_order(
|
||||
label: str, source: str, expected_order: list[str], context: str
|
||||
) -> list[str]:
|
||||
positions = []
|
||||
for needle in expected_order:
|
||||
index = source.find(needle)
|
||||
if index == -1:
|
||||
return [f"{label} missing {needle!r} in {context}"]
|
||||
positions.append(index)
|
||||
if positions != sorted(positions):
|
||||
return [
|
||||
f"{label} has wrong order in {context}: expected "
|
||||
+ " before ".join(expected_order)
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
def provider_table_name(provider_id: str) -> str:
|
||||
return SHARED_PROVIDER_TABLES.get(provider_id, provider_id.replace("-", "_"))
|
||||
|
||||
@@ -216,6 +307,7 @@ def main() -> int:
|
||||
|
||||
errors: list[str] = []
|
||||
errors += report_provider_enum_drift(canonical_ids, live_api_provider_ids)
|
||||
errors += report_huggingface_coverage(config_rs, tui_config_rs, providers_md)
|
||||
errors += report_set(
|
||||
"shipped provider rows",
|
||||
canonical_ids,
|
||||
|
||||
Reference in New Issue
Block a user