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:
@@ -422,6 +422,11 @@ export class ConnectorDetailPage extends LightElement {
|
||||
const canManage = this._isGlobal ? this._isAdmin : true;
|
||||
const hasVerify = !!e.verify_command;
|
||||
const oauth = e.auth_kind === 'oauth';
|
||||
// An api_key connector that declares its key as a described `env[]` field (secret)
|
||||
// collects it there — the generic, label-less "API key" box would be a duplicate
|
||||
// asking for the same value. Fall back to the generic box only when the schema
|
||||
// names no secret of its own (a bare `requires:[API_KEY]` connector).
|
||||
const schemaHasSecret = this._schema.some(f => f.secret);
|
||||
|
||||
if (this._isGlobal && !this._isAdmin) return nothing;
|
||||
|
||||
@@ -452,7 +457,7 @@ export class ConnectorDetailPage extends LightElement {
|
||||
: 'Already active. Re-submitting replaces the stored credentials.'}
|
||||
</div>` : nothing}
|
||||
|
||||
${e.auth_kind === 'api_key' ? html`
|
||||
${e.auth_kind === 'api_key' && !schemaHasSecret ? html`
|
||||
<div class="mb-3">
|
||||
<label class="form-label">API key<span class="text-danger">*</span></label>
|
||||
<input class="form-control" type="password" .value=${this._form.api_key}
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user