feat(providers): Moonshot AI provider + extract shared OpenAI-compat helpers

- Add MoonshotProvider and MoonshotCodeProvider (OpenAI-compatible)
- Extract `fetch_openai_models()` and `build_openai_llm()` shared functions,
  deduplicating model-listing and client construction across OpenAI,
  OpenRouter, DeepSeek, LM Studio, and Z.AI providers
- Add `lists_models` field to `ProviderUiMeta` — drives frontend model
  picker dynamically instead of hardcoded `type_id` list
- Port DeepSeek, LM Studio, OpenRouter model listing to `fetch_openai_models`
- Set `lists_models: true` on Z.AI provider (missed in prior pass)

feat(mcp): named key placeholders {SECRET:name}/{ENV:name} in connector URLs

- `apply_key_placeholder` now accepts an `env` map alongside `api_key`
- Named tokens resolve from the connector's described `env[]` fields,
  with `{SECRET:name}` falling back to `api_key` for backward compat
- Replace `substitute_secret_tokens` with `substitute_named_tokens`
- Unresolved tokens are left in place (visible misconfig) rather than
  silently producing a wrong URL

fix(ui): connector detail hides generic API key when schema has secret

fix(ui): model picker uses `lists_models` from provider types endpoint
This commit is contained in:
2026-07-18 15:41:53 +01:00
parent e6c4e202a4
commit 760dae06e5
15 changed files with 442 additions and 138 deletions
+59 -1
View File
@@ -1,6 +1,7 @@
pub mod anthropic;
pub mod deepseek;
pub mod lm_studio;
pub mod moonshot;
pub mod ollama;
pub mod openai;
pub mod openrouter;
@@ -10,7 +11,13 @@ pub mod zai;
pub use crate::provider::ServiceType;
pub use core_api::provider::RemoteLlmModelInfo;
use core_api::provider::{ApiProvider, LlmModelRecord};
use std::sync::Arc;
use anyhow::{anyhow, Context, Result};
use core_api::provider::{ApiProvider, BuiltLlmClient, LlmModelRecord, LlmProviderRecord};
use crate::chatbot::openai::OpenAiClient;
/// Computes the `extra_params` an OpenAI-compatible client should be built with,
/// given a model's stored `extra_params` and its selected reasoning value. The
@@ -37,3 +44,54 @@ pub(crate) fn extra_with_reasoning(
}
}
}
/// Fetches the OpenAI-style `GET {base_url}/models` catalog shared by most
/// OpenAI-compatible providers. Returns the raw per-model JSON objects from
/// the `data` envelope so each provider can map and enrich them with its own
/// heuristics. `api_key` is sent as a bearer token when present (local
/// providers pass `None`); `who` is the display name used in error messages.
pub(crate) async fn fetch_openai_models(
http: &reqwest::Client,
base_url: &str,
api_key: Option<&str>,
who: &str,
) -> Result<Vec<serde_json::Value>> {
let url = format!("{}/models", base_url.trim_end_matches('/'));
let mut req = http.get(&url);
if let Some(key) = api_key {
req = req.bearer_auth(key);
}
let resp: serde_json::Value = req
.send()
.await
.map_err(|e| anyhow!("{who} request failed: {e}"))?
.error_for_status()
.map_err(|e| anyhow!("{who} error response: {e}"))?
.json()
.await
.map_err(|e| anyhow!("{who} response parse failed: {e}"))?;
resp["data"]
.as_array()
.cloned()
.ok_or_else(|| anyhow!("unexpected {who} response shape"))
}
/// Builds an `OpenAiClient` for an OpenAI-compatible provider: requires the
/// provider record's `api_key` and merges the model's stored `extra_params`
/// with the provider-translated reasoning fragment (see `extra_with_reasoning`).
pub(crate) fn build_openai_llm(
provider: &dyn ApiProvider,
base_url: &str,
record: &LlmProviderRecord,
model: &LlmModelRecord,
prompt_cache: bool,
) -> Result<BuiltLlmClient> {
let key = record.api_key.as_deref()
.with_context(|| format!("provider '{}': api_key required for {}", record.name, provider.type_id()))?;
let extra = extra_with_reasoning(provider, model);
Ok(BuiltLlmClient {
client: Arc::new(OpenAiClient::new(base_url, key, extra, prompt_cache)),
prompt_cache,
})
}