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
+10 -5
View File
@@ -44,6 +44,7 @@ export class ModelsLlmSection extends LightElement {
onback: { attribute: false },
_models: { state: true },
_providers: { state: true },
_providerTypes: { state: true },
_modal: { state: true },
_saving: { state: true },
_error: { state: true },
@@ -61,6 +62,7 @@ export class ModelsLlmSection extends LightElement {
this.onback = null;
this._models = [];
this._providers = [];
this._providerTypes = [];
this._modal = null;
this._saving = false;
this._error = null;
@@ -80,14 +82,17 @@ export class ModelsLlmSection extends LightElement {
async _load() {
try {
const [modelsRes, providersRes] = await Promise.all([
const [modelsRes, providersRes, typesRes] = await Promise.all([
fetch('/api/llm/models'),
fetch('/api/llm/providers'),
fetch('/api/llm/providers/types'),
]);
if (!modelsRes.ok) throw new Error(`models: HTTP ${modelsRes.status}`);
if (!providersRes.ok) throw new Error(`providers: HTTP ${providersRes.status}`);
this._models = await modelsRes.json();
this._providers = await providersRes.json();
if (!typesRes.ok) throw new Error(`provider types: HTTP ${typesRes.status}`);
this._models = await modelsRes.json();
this._providers = await providersRes.json();
this._providerTypes = await typesRes.json();
} catch (e) {
this._error = e.message;
}
@@ -157,9 +162,9 @@ export class ModelsLlmSection extends LightElement {
this._pickedProvider = provider;
this._reasoningMode = null;
const hasModelPicker = ['openrouter', 'ollama', 'lm_studio', 'deepseek', 'zai'].includes(provider.type);
const typeMeta = this._providerTypes.find(t => t.type_id === provider.type);
if (hasModelPicker) {
if (typeMeta?.lists_models) {
this._orForm = emptyOrForm();
this._orSearch = '';
this._orModels = [];