Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,16 @@ pub struct ProvidersToml {
alias = "grok"
)]
pub xai: ProviderConfigToml,
/// Jiangsu Telecom TokenHub — OpenAI-compatible AI gateway.
#[serde(
default,
skip_serializing_if = "ProviderConfigToml::is_empty",
alias = "telecom-js",
alias = "telecom_js",
alias = "telecomjs-cn",
alias = "tokenhub"
)]
pub telecomjs: ProviderConfigToml,
/// Catch-all table for the dynamic OpenAI-compatible custom provider
/// identity (#1519). Arbitrary `[providers.<name>]` tables are handled by
/// the tui-side flatten map; this named slot keeps the canonical
Expand Down Expand Up @@ -374,6 +384,7 @@ impl ProvidersToml {
ProviderKind::LongCat => &self.longcat,
ProviderKind::Meta => &self.meta,
ProviderKind::Xai => &self.xai,
ProviderKind::Telecomjs => &self.telecomjs,
ProviderKind::Custom => &self.custom,
}
}
Expand Down Expand Up @@ -412,6 +423,7 @@ impl ProvidersToml {
ProviderKind::LongCat => &mut self.longcat,
ProviderKind::Meta => &mut self.meta,
ProviderKind::Xai => &mut self.xai,
ProviderKind::Telecomjs => &mut self.telecomjs,
ProviderKind::Custom => &mut self.custom,
}
}
Expand Down Expand Up @@ -2265,6 +2277,7 @@ impl ConfigToml {
ProviderKind::LongCat => DEFAULT_LONGCAT_BASE_URL.to_string(),
ProviderKind::Meta => DEFAULT_META_BASE_URL.to_string(),
ProviderKind::Xai => DEFAULT_XAI_BASE_URL.to_string(),
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_BASE_URL.to_string(),
// The custom provider has no built-in endpoint; fall back to its
// descriptor placeholder so the lookup is total. Real custom
// routes always supply a configured base_url before this point.
Expand Down Expand Up @@ -2846,6 +2859,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::LongCat => DEFAULT_LONGCAT_MODEL,
ProviderKind::Meta => DEFAULT_META_MODEL,
ProviderKind::Xai => DEFAULT_XAI_MODEL,
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_MODEL,
// No built-in default model; the registry placeholder keeps this total.
ProviderKind::Custom => provider.provider().default_model(),
}
Expand Down Expand Up @@ -2885,6 +2899,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::LongCat => DEFAULT_LONGCAT_BASE_URL,
ProviderKind::Meta => DEFAULT_META_BASE_URL,
ProviderKind::Xai => DEFAULT_XAI_BASE_URL,
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_BASE_URL,
// No built-in default base URL; the registry placeholder keeps this total.
ProviderKind::Custom => provider.provider().default_base_url(),
}
Expand Down Expand Up @@ -4434,6 +4449,8 @@ struct EnvRuntimeOverrides {
meta_model: Option<String>,
xai_base_url: Option<String>,
xai_model: Option<String>,
telecomjs_base_url: Option<String>,
telecomjs_model: Option<String>,
}

impl EnvRuntimeOverrides {
Expand Down Expand Up @@ -4700,6 +4717,12 @@ impl EnvRuntimeOverrides {
xai_model: std::env::var("XAI_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
telecomjs_base_url: std::env::var("TELECOMJS_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
telecomjs_model: std::env::var("TELECOMJS_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
}
}

Expand Down Expand Up @@ -4754,6 +4777,7 @@ impl EnvRuntimeOverrides {
ProviderKind::LongCat => self.longcat_base_url.clone(),
ProviderKind::Meta => self.meta_base_url.clone(),
ProviderKind::Xai => self.xai_base_url.clone(),
ProviderKind::Telecomjs => self.telecomjs_base_url.clone(),
// No dedicated CODEWHALE_CUSTOM_BASE_URL env override: a custom
// provider's base URL comes from its `[providers.<name>]` table.
ProviderKind::Custom => None,
Expand Down Expand Up @@ -4787,6 +4811,7 @@ impl EnvRuntimeOverrides {
ProviderKind::LongCat => self.longcat_model.clone(),
ProviderKind::Meta => self.meta_model.clone(),
ProviderKind::Xai => self.xai_model.clone(),
ProviderKind::Telecomjs => self.telecomjs_model.clone(),
_ => None,
}?;

Expand Down
19 changes: 17 additions & 2 deletions crates/config/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ use super::{
DEFAULT_QIANFAN_BASE_URL, DEFAULT_QIANFAN_MODEL, DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL,
DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL, DEFAULT_SILICONFLOW_BASE_URL,
DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL, DEFAULT_STEPFUN_BASE_URL,
DEFAULT_STEPFUN_MODEL, DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL,
DEFAULT_STEPFUN_MODEL, DEFAULT_TELECOMJS_BASE_URL, DEFAULT_TELECOMJS_MODEL,
DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL,
DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL,
DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL, DEFAULT_WANJIE_ARK_MODEL,
DEFAULT_XAI_BASE_URL, DEFAULT_XAI_MODEL, DEFAULT_XIAOMI_MIMO_BASE_URL,
Expand Down Expand Up @@ -648,6 +649,18 @@ provider!(
aliases: ["x-ai", "x_ai", "grok"]
);

provider!(
Telecomjs,
Telecomjs,
"telecomjs",
"Telecom JiangSu",
DEFAULT_TELECOMJS_BASE_URL,
DEFAULT_TELECOMJS_MODEL,
["TELECOMJS_API_KEY"],
"telecomjs",
aliases: ["telecom-js", "telecom_js", "telecomjs-cn", "tokenhub"]
);

/// User-defined OpenAI-compatible endpoint (#1519).
///
/// A single dynamic provider identity for arbitrary `[providers.<name>]
Expand Down Expand Up @@ -733,9 +746,10 @@ static SAKANA: Sakana = Sakana;
static LONGCAT: LongCat = LongCat;
static META: Meta = Meta;
static XAI: Xai = Xai;
static TELECOMJS: Telecomjs = Telecomjs;
static CUSTOM: Custom = Custom;

static PROVIDER_REGISTRY: [&dyn Provider; 33] = [
static PROVIDER_REGISTRY: [&dyn Provider; 34] = [
&DEEPSEEK,
&DEEPSEEK_ANTHROPIC,
&NVIDIA_NIM,
Expand Down Expand Up @@ -768,6 +782,7 @@ static PROVIDER_REGISTRY: [&dyn Provider; 33] = [
&LONGCAT,
&META,
&XAI,
&TELECOMJS,
&CUSTOM,
];

Expand Down
3 changes: 3 additions & 0 deletions crates/config/src/provider_defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,6 @@ pub(crate) const DEFAULT_META_BASE_URL: &str = "https://api.meta.ai/v1";
// xAI / Grok API-key route defaults
pub(crate) const DEFAULT_XAI_MODEL: &str = "grok-4.5";
pub(crate) const DEFAULT_XAI_BASE_URL: &str = "https://api.x.ai/v1";
// TelecomJS (Jiangsu Telecom TokenHub) defaults
pub(crate) const DEFAULT_TELECOMJS_MODEL: &str = "deepseek-v4-pro";
pub(crate) const DEFAULT_TELECOMJS_BASE_URL: &str = "https://aigw.telecomjs.com/v1";
10 changes: 9 additions & 1 deletion crates/config/src/provider_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ pub enum ProviderKind {
Meta,
#[serde(alias = "x-ai", alias = "x_ai", alias = "grok")]
Xai,
/// Jiangsu Telecom TokenHub (OpenAI-compatible).
///
/// An AI gateway operated by Jiangsu Telecom that speaks the OpenAI Chat
/// Completions wire protocol and serves a broad model catalog; each API key
/// may access a different subset of models.
#[serde(alias = "telecom-js", alias = "telecom_js", alias = "telecomjs-cn", alias = "tokenhub")]
Telecomjs,
/// User-defined OpenAI-compatible endpoint (#1519).
///
/// A single dynamic identity for arbitrary `[providers.<name>]
Expand All @@ -127,7 +134,7 @@ pub enum ProviderKind {
}

impl ProviderKind {
pub const ALL: [Self; 33] = [
pub const ALL: [Self; 34] = [
Self::Deepseek,
Self::DeepseekAnthropic,
Self::NvidiaNim,
Expand Down Expand Up @@ -160,6 +167,7 @@ impl ProviderKind {
Self::LongCat,
Self::Meta,
Self::Xai,
Self::Telecomjs,
Self::Custom,
];

Expand Down
131 changes: 128 additions & 3 deletions crates/tui/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,74 @@ impl DeepSeekClient {
openrouter_to_catalog_offering(item, &provider, &fingerprint, fetched_at)
})
.collect()
} else if provider == "telecomjs" {
// TelecomJS TokenHub returns bare model IDs (e.g. "glm-5.2",
// "deepseek-v4-flash") that correspond to models also present in the
// bundled catalog under their home providers. Match against the
// bundled catalog (case-insensitive) to inherit metadata (context
// window, reasoning, tool_call, etc.) so the model picker shows
// rich capability information instead of empty rows.
let models =
parse_models_response(&body).map_err(|_| CatalogRefreshError::InvalidResponse)?;
if models.is_empty() {
return Err(CatalogRefreshError::EmptyList);
}
let bundled = codewhale_config::catalog::bundled_catalog_offerings();
let default_model_id = codewhale_config::ProviderKind::Telecomjs
.provider()
.default_model();
models
.into_iter()
.map(|model| {
let is_default = model.id == default_model_id;
// Find a bundled offering whose wire_model_id matches
// case-insensitively, regardless of provider.
let bundled_match = bundled.iter().find(|offering| {
offering
.wire_model_id
.eq_ignore_ascii_case(&model.id)
});
if let Some(matched) = bundled_match {
CatalogOffering {
provider: provider.clone(),
wire_model_id: model.id,
canonical_model: matched.canonical_model.clone(),
endpoint_key: "chat".to_string(),
default_for_provider: is_default,
family: matched.family.clone(),
limit: matched.limit.clone(),
cost: matched.cost.clone(),
modalities: matched.modalities.clone(),
reasoning: matched.reasoning,
tool_call: matched.tool_call,
reasoning_options: matched.reasoning_options.clone(),
source: CatalogSource::Live {
base_url_fingerprint: fingerprint.clone(),
fetched_at,
},
}
} else {
CatalogOffering {
provider: provider.clone(),
wire_model_id: model.id,
canonical_model: None,
endpoint_key: "chat".to_string(),
default_for_provider: is_default,
family: None,
limit: None,
cost: None,
modalities: None,
reasoning: None,
tool_call: None,
reasoning_options: Vec::new(),
source: CatalogSource::Live {
base_url_fingerprint: fingerprint.clone(),
fetched_at,
},
}
}
})
.collect()
} else {
let models =
parse_models_response(&body).map_err(|_| CatalogRefreshError::InvalidResponse)?;
Expand Down Expand Up @@ -1333,6 +1401,60 @@ impl DeepSeekClient {
}
}

/// Best-effort background refresh of the active provider's own `/v1/models`
/// catalog, merging results into the provider lake (#3385).
///
/// Unlike [`models_dev_live::spawn_background_refresh`] (which fetches the
/// cross-provider Models.dev catalog), this calls the provider's own
/// `/v1/models` endpoint and merges the results into the existing live
/// snapshot via [`provider_lake::merge_live_offerings`], preserving rows
/// from other sources.
///
/// Currently activated for providers whose model list is not covered by the
/// Models.dev catalog (e.g. TelecomJS TokenHub). The refresh is non-fatal:
/// on failure, existing/bundled rows remain available.
pub fn spawn_active_provider_catalog_refresh(config: &Config) {
let provider = config.api_provider();
// Only refresh for providers that serve their own model list and are
// not already covered by the Models.dev catalog.
if !matches!(provider, ApiProvider::Telecomjs) {
return;
}

let client = match DeepSeekClient::new(config) {
Ok(client) => client,
Err(err) => {
tracing::debug!(
target: "provider_catalog",
error = %err,
"skipping provider catalog refresh: client creation failed"
);
return;
}
};

tokio::spawn(async move {
match client.fetch_catalog_delta().await {
Ok(delta) => {
let count = delta.offerings.len();
crate::provider_lake::merge_live_offerings(delta.offerings);
tracing::debug!(
target: "provider_catalog",
offering_count = count,
"provider catalog refresh merged {count} offerings into provider lake"
);
}
Err(err) => {
tracing::debug!(
target: "provider_catalog",
error = ?err,
"provider catalog refresh failed; keeping existing rows"
);
}
}
});
}

/// Generate speech with Xiaomi MiMo TTS models.
///
/// The spoken text is placed in an `assistant` message because Xiaomi
Expand Down Expand Up @@ -1952,7 +2074,8 @@ pub(super) fn apply_reasoning_effort(
| ApiProvider::Deepinfra
| ApiProvider::Together
| ApiProvider::Atlascloud
| ApiProvider::Zai => {
| ApiProvider::Zai
| ApiProvider::Telecomjs => {
body["thinking"] = json!({ "type": "disabled" });
}
ApiProvider::OpenaiCodex => {
Expand Down Expand Up @@ -2015,7 +2138,8 @@ pub(super) fn apply_reasoning_effort(
| ApiProvider::Sglang
| ApiProvider::Volcengine
| ApiProvider::Deepinfra
| ApiProvider::Atlascloud => {
| ApiProvider::Atlascloud
| ApiProvider::Telecomjs => {
body["reasoning_effort"] = json!("high");
body["thinking"] = json!({ "type": "enabled" });
}
Expand Down Expand Up @@ -2106,7 +2230,8 @@ pub(super) fn apply_reasoning_effort(
| ApiProvider::Sglang
| ApiProvider::Volcengine
| ApiProvider::Deepinfra
| ApiProvider::Atlascloud => {
| ApiProvider::Atlascloud
| ApiProvider::Telecomjs => {
body["reasoning_effort"] = json!("max");
body["thinking"] = json!({ "type": "enabled" });
}
Expand Down
Loading