feat(control-ui): add model presets
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
- Onboarding/Docs: add Moonshot AI (Kimi K2) auth choice + config example.
|
- Onboarding/Docs: add Moonshot AI (Kimi K2) auth choice + config example.
|
||||||
- CLI/Onboarding: prompt to reuse detected API keys for Moonshot/MiniMax/Z.AI/Gemini/Anthropic/OpenCode.
|
- CLI/Onboarding: prompt to reuse detected API keys for Moonshot/MiniMax/Z.AI/Gemini/Anthropic/OpenCode.
|
||||||
- Auto-reply: add compact `/model` picker (models + available providers) and show provider endpoints in `/model status`.
|
- Auto-reply: add compact `/model` picker (models + available providers) and show provider endpoints in `/model status`.
|
||||||
|
- Control UI: add Config tab model presets (MiniMax M2.1, GLM 4.7, Kimi) for one-click setup.
|
||||||
- Plugins: add extension loader (tools/RPC/CLI/services), discovery paths, and config schema + Control UI labels (uiHints).
|
- Plugins: add extension loader (tools/RPC/CLI/services), discovery paths, and config schema + Control UI labels (uiHints).
|
||||||
- Plugins: add `clawdbot plugins install` (path/tgz/npm), plus `list|info|enable|disable|doctor` UX.
|
- Plugins: add `clawdbot plugins install` (path/tgz/npm), plus `list|info|enable|disable|doctor` UX.
|
||||||
- Plugins: voice-call plugin now real (Twilio/log), adds start/status RPC/CLI/tool + tests.
|
- Plugins: voice-call plugin now real (Twilio/log), adds start/status RPC/CLI/tool + tests.
|
||||||
|
|||||||
@@ -43,6 +43,18 @@ The onboarding wizard generates a gateway token by default, so paste it here on
|
|||||||
- Logs: live tail of gateway file logs with filter/export (`logs.tail`)
|
- Logs: live tail of gateway file logs with filter/export (`logs.tail`)
|
||||||
- Update: run a package/git update + restart (`update.run`) with a restart report
|
- Update: run a package/git update + restart (`update.run`) with a restart report
|
||||||
|
|
||||||
|
## Model presets (Config tab)
|
||||||
|
|
||||||
|
The Config tab includes **Model presets**: one-click inserts to add common model providers and set a default model:
|
||||||
|
|
||||||
|
- **MiniMax M2.1 (Anthropic)** → configures MiniMax via `https://api.minimax.io/anthropic` and `anthropic-messages` (see [/providers/minimax](/providers/minimax))
|
||||||
|
- **GLM 4.7 (Z.AI)** → adds `ZAI_API_KEY` + sets `zai/glm-4.7` (see [/providers/zai](/providers/zai))
|
||||||
|
- **Kimi (Moonshot)** → configures Moonshot + sets `moonshot/kimi-k2-0905-preview` (see [/providers/moonshot](/providers/moonshot))
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Presets **keep existing API keys and per-model params** when present.
|
||||||
|
- Use `/model` (see [/tools/slash-commands](/tools/slash-commands)) to switch models from chat without editing config.
|
||||||
|
|
||||||
## Chat behavior
|
## Chat behavior
|
||||||
|
|
||||||
- `chat.send` is **non-blocking**: it acks immediately with `{ runId, status: "started" }` and the response streams via `chat` events.
|
- `chat.send` is **non-blocking**: it acks immediately with `{ runId, status: "started" }` and the response streams via `chat` events.
|
||||||
|
|||||||
@@ -648,7 +648,10 @@ describe("trigger handling", () => {
|
|||||||
makeCfg(home),
|
makeCfg(home),
|
||||||
);
|
);
|
||||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
||||||
expect(blockReplies.length).toBe(0);
|
// Allowlisted senders: inline /status runs immediately (like /help) and is
|
||||||
|
// stripped from the prompt; the remaining text continues through the agent.
|
||||||
|
expect(blockReplies.length).toBe(1);
|
||||||
|
expect(String(blockReplies[0]?.text ?? "").length).toBeGreaterThan(0);
|
||||||
const prompt =
|
const prompt =
|
||||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
expect(prompt).not.toContain("/status");
|
expect(prompt).not.toContain("/status");
|
||||||
@@ -818,7 +821,7 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("strips inline /status for unauthorized senders", async () => {
|
it("keeps inline /status for unauthorized senders", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||||
payloads: [{ text: "ok" }],
|
payloads: [{ text: "ok" }],
|
||||||
@@ -855,7 +858,8 @@ describe("trigger handling", () => {
|
|||||||
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).toHaveBeenCalled();
|
||||||
const prompt =
|
const prompt =
|
||||||
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? "";
|
||||||
expect(prompt).not.toContain("/status");
|
// Not allowlisted: inline /status is treated as plain text and is not stripped.
|
||||||
|
expect(prompt).toContain("/status");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,37 @@ import { describe, expect, it, vi } from "vitest";
|
|||||||
import { renderConfig } from "./config";
|
import { renderConfig } from "./config";
|
||||||
|
|
||||||
describe("config view", () => {
|
describe("config view", () => {
|
||||||
|
const baseProps = () => ({
|
||||||
|
raw: "{\n}\n",
|
||||||
|
valid: true,
|
||||||
|
issues: [],
|
||||||
|
loading: false,
|
||||||
|
saving: false,
|
||||||
|
applying: false,
|
||||||
|
updating: false,
|
||||||
|
connected: true,
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
schemaLoading: false,
|
||||||
|
uiHints: {},
|
||||||
|
formMode: "form" as const,
|
||||||
|
formValue: {},
|
||||||
|
onRawChange: vi.fn(),
|
||||||
|
onFormModeChange: vi.fn(),
|
||||||
|
onFormPatch: vi.fn(),
|
||||||
|
onReload: vi.fn(),
|
||||||
|
onSave: vi.fn(),
|
||||||
|
onApply: vi.fn(),
|
||||||
|
onUpdate: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
it("disables save when form is unsafe", () => {
|
it("disables save when form is unsafe", () => {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
render(
|
render(
|
||||||
renderConfig({
|
renderConfig({
|
||||||
raw: "{\n}\n",
|
...baseProps(),
|
||||||
valid: true,
|
|
||||||
issues: [],
|
|
||||||
loading: false,
|
|
||||||
saving: false,
|
|
||||||
connected: true,
|
|
||||||
schema: {
|
schema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -26,11 +47,6 @@ describe("config view", () => {
|
|||||||
uiHints: {},
|
uiHints: {},
|
||||||
formMode: "form",
|
formMode: "form",
|
||||||
formValue: { mixed: "x" },
|
formValue: { mixed: "x" },
|
||||||
onRawChange: vi.fn(),
|
|
||||||
onFormModeChange: vi.fn(),
|
|
||||||
onFormPatch: vi.fn(),
|
|
||||||
onReload: vi.fn(),
|
|
||||||
onSave: vi.fn(),
|
|
||||||
}),
|
}),
|
||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
@@ -43,4 +59,124 @@ describe("config view", () => {
|
|||||||
expect(saveButton).not.toBeUndefined();
|
expect(saveButton).not.toBeUndefined();
|
||||||
expect(saveButton?.disabled).toBe(true);
|
expect(saveButton?.disabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("applies MiniMax preset via onRawChange + onFormPatch", () => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
const onRawChange = vi.fn();
|
||||||
|
const onFormPatch = vi.fn();
|
||||||
|
render(
|
||||||
|
renderConfig({
|
||||||
|
...baseProps(),
|
||||||
|
onRawChange,
|
||||||
|
onFormPatch,
|
||||||
|
}),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
|
const btn = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||||
|
b.textContent?.includes("MiniMax M2.1"),
|
||||||
|
) as HTMLButtonElement | undefined;
|
||||||
|
expect(btn).toBeTruthy();
|
||||||
|
btn?.click();
|
||||||
|
|
||||||
|
expect(onRawChange).toHaveBeenCalled();
|
||||||
|
const raw = String(onRawChange.mock.calls.at(-1)?.[0] ?? "");
|
||||||
|
expect(raw).toContain("https://api.minimax.io/anthropic");
|
||||||
|
expect(raw).toContain("anthropic-messages");
|
||||||
|
expect(raw).toContain("minimax/MiniMax-M2.1");
|
||||||
|
expect(raw).toContain("MINIMAX_API_KEY");
|
||||||
|
|
||||||
|
expect(onFormPatch).toHaveBeenCalledWith(
|
||||||
|
["agents", "defaults", "model", "primary"],
|
||||||
|
"minimax/MiniMax-M2.1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not clobber existing MiniMax apiKey when applying preset", () => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
const onRawChange = vi.fn();
|
||||||
|
render(
|
||||||
|
renderConfig({
|
||||||
|
...baseProps(),
|
||||||
|
onRawChange,
|
||||||
|
formValue: {
|
||||||
|
models: {
|
||||||
|
mode: "merge",
|
||||||
|
providers: {
|
||||||
|
minimax: {
|
||||||
|
apiKey: "EXISTING_KEY",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
|
const btn = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||||
|
b.textContent?.includes("MiniMax M2.1"),
|
||||||
|
) as HTMLButtonElement | undefined;
|
||||||
|
expect(btn).toBeTruthy();
|
||||||
|
btn?.click();
|
||||||
|
|
||||||
|
const raw = String(onRawChange.mock.calls.at(-1)?.[0] ?? "");
|
||||||
|
expect(raw).toContain("EXISTING_KEY");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies Z.AI (GLM 4.7) preset", () => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
const onRawChange = vi.fn();
|
||||||
|
const onFormPatch = vi.fn();
|
||||||
|
render(
|
||||||
|
renderConfig({
|
||||||
|
...baseProps(),
|
||||||
|
onRawChange,
|
||||||
|
onFormPatch,
|
||||||
|
}),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
|
const btn = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||||
|
b.textContent?.includes("GLM 4.7"),
|
||||||
|
) as HTMLButtonElement | undefined;
|
||||||
|
expect(btn).toBeTruthy();
|
||||||
|
btn?.click();
|
||||||
|
|
||||||
|
const raw = String(onRawChange.mock.calls.at(-1)?.[0] ?? "");
|
||||||
|
expect(raw).toContain("zai/glm-4.7");
|
||||||
|
expect(raw).toContain("ZAI_API_KEY");
|
||||||
|
expect(onFormPatch).toHaveBeenCalledWith(
|
||||||
|
["agents", "defaults", "model", "primary"],
|
||||||
|
"zai/glm-4.7",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies Moonshot (Kimi) preset", () => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
const onRawChange = vi.fn();
|
||||||
|
const onFormPatch = vi.fn();
|
||||||
|
render(
|
||||||
|
renderConfig({
|
||||||
|
...baseProps(),
|
||||||
|
onRawChange,
|
||||||
|
onFormPatch,
|
||||||
|
}),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
|
const btn = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||||
|
b.textContent?.includes("Kimi"),
|
||||||
|
) as HTMLButtonElement | undefined;
|
||||||
|
expect(btn).toBeTruthy();
|
||||||
|
btn?.click();
|
||||||
|
|
||||||
|
const raw = String(onRawChange.mock.calls.at(-1)?.[0] ?? "");
|
||||||
|
expect(raw).toContain("https://api.moonshot.ai/v1");
|
||||||
|
expect(raw).toContain("moonshot/kimi-k2-0905-preview");
|
||||||
|
expect(raw).toContain("MOONSHOT_API_KEY");
|
||||||
|
expect(onFormPatch).toHaveBeenCalledWith(
|
||||||
|
["agents", "defaults", "model", "primary"],
|
||||||
|
"moonshot/kimi-k2-0905-preview",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { html, nothing } from "lit";
|
|||||||
import type { ConfigUiHints } from "../types";
|
import type { ConfigUiHints } from "../types";
|
||||||
import { analyzeConfigSchema, renderConfigForm } from "./config-form";
|
import { analyzeConfigSchema, renderConfigForm } from "./config-form";
|
||||||
|
|
||||||
|
type ConfigPatch = {
|
||||||
|
path: Array<string | number>;
|
||||||
|
value: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
export type ConfigProps = {
|
export type ConfigProps = {
|
||||||
raw: string;
|
raw: string;
|
||||||
valid: boolean | null;
|
valid: boolean | null;
|
||||||
@@ -25,6 +30,284 @@ export type ConfigProps = {
|
|||||||
onUpdate: () => void;
|
onUpdate: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function cloneConfigObject<T>(value: T): T {
|
||||||
|
if (typeof structuredClone === "function") return structuredClone(value);
|
||||||
|
return JSON.parse(JSON.stringify(value)) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryParseJsonObject(raw: string): Record<string, unknown> | null {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as unknown;
|
||||||
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||||
|
return parsed as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPathValue(
|
||||||
|
obj: Record<string, unknown> | unknown[],
|
||||||
|
path: Array<string | number>,
|
||||||
|
value: unknown,
|
||||||
|
) {
|
||||||
|
if (path.length === 0) return;
|
||||||
|
let current: Record<string, unknown> | unknown[] = obj;
|
||||||
|
for (let i = 0; i < path.length - 1; i += 1) {
|
||||||
|
const key = path[i];
|
||||||
|
const nextKey = path[i + 1];
|
||||||
|
if (typeof key === "number") {
|
||||||
|
if (!Array.isArray(current)) return;
|
||||||
|
if (current[key] == null) {
|
||||||
|
current[key] =
|
||||||
|
typeof nextKey === "number" ? [] : ({} as Record<string, unknown>);
|
||||||
|
}
|
||||||
|
current = current[key] as Record<string, unknown> | unknown[];
|
||||||
|
} else {
|
||||||
|
if (typeof current !== "object" || current == null) return;
|
||||||
|
const record = current as Record<string, unknown>;
|
||||||
|
if (record[key] == null) {
|
||||||
|
record[key] =
|
||||||
|
typeof nextKey === "number" ? [] : ({} as Record<string, unknown>);
|
||||||
|
}
|
||||||
|
current = record[key] as Record<string, unknown> | unknown[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const lastKey = path[path.length - 1];
|
||||||
|
if (typeof lastKey === "number") {
|
||||||
|
if (Array.isArray(current)) current[lastKey] = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof current === "object" && current != null) {
|
||||||
|
(current as Record<string, unknown>)[lastKey] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPathValue(
|
||||||
|
obj: unknown,
|
||||||
|
path: Array<string | number>,
|
||||||
|
): unknown | undefined {
|
||||||
|
let current: unknown = obj;
|
||||||
|
for (const key of path) {
|
||||||
|
if (typeof key === "number") {
|
||||||
|
if (!Array.isArray(current)) return undefined;
|
||||||
|
current = current[key];
|
||||||
|
} else {
|
||||||
|
if (!current || typeof current !== "object") return undefined;
|
||||||
|
current = (current as Record<string, unknown>)[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildModelPresetPatches(base: Record<string, unknown>): Array<{
|
||||||
|
id: "minimax" | "zai" | "moonshot";
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
patches: ConfigPatch[];
|
||||||
|
}> {
|
||||||
|
const setPrimary = (modelRef: string) => ({
|
||||||
|
path: ["agents", "defaults", "model", "primary"],
|
||||||
|
value: modelRef,
|
||||||
|
});
|
||||||
|
const safeAlias = (modelRef: string, alias: string): ConfigPatch | null => {
|
||||||
|
const existingAlias = getPathValue(base, [
|
||||||
|
"agents",
|
||||||
|
"defaults",
|
||||||
|
"models",
|
||||||
|
modelRef,
|
||||||
|
"alias",
|
||||||
|
]);
|
||||||
|
if (typeof existingAlias === "string" && existingAlias.trim().length > 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: ["agents", "defaults", "models", modelRef, "alias"],
|
||||||
|
value: alias,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const minimaxModelsPath = ["models", "providers", "minimax", "models"] satisfies Array<
|
||||||
|
string | number
|
||||||
|
>;
|
||||||
|
const moonshotModelsPath = [
|
||||||
|
"models",
|
||||||
|
"providers",
|
||||||
|
"moonshot",
|
||||||
|
"models",
|
||||||
|
] satisfies Array<string | number>;
|
||||||
|
|
||||||
|
const hasNonEmptyString = (value: unknown) =>
|
||||||
|
typeof value === "string" && value.trim().length > 0;
|
||||||
|
|
||||||
|
const envMinimax = getPathValue(base, ["env", "MINIMAX_API_KEY"]);
|
||||||
|
const envZai = getPathValue(base, ["env", "ZAI_API_KEY"]);
|
||||||
|
const envMoonshot = getPathValue(base, ["env", "MOONSHOT_API_KEY"]);
|
||||||
|
|
||||||
|
const minimaxHasModels = Array.isArray(getPathValue(base, minimaxModelsPath));
|
||||||
|
const moonshotHasModels = Array.isArray(getPathValue(base, moonshotModelsPath));
|
||||||
|
|
||||||
|
const minimaxProviderBaseUrl = getPathValue(base, [
|
||||||
|
"models",
|
||||||
|
"providers",
|
||||||
|
"minimax",
|
||||||
|
"baseUrl",
|
||||||
|
]);
|
||||||
|
const minimaxProviderApiKey = getPathValue(base, [
|
||||||
|
"models",
|
||||||
|
"providers",
|
||||||
|
"minimax",
|
||||||
|
"apiKey",
|
||||||
|
]);
|
||||||
|
const minimaxProviderApi = getPathValue(base, [
|
||||||
|
"models",
|
||||||
|
"providers",
|
||||||
|
"minimax",
|
||||||
|
"api",
|
||||||
|
]);
|
||||||
|
const moonshotProviderBaseUrl = getPathValue(base, [
|
||||||
|
"models",
|
||||||
|
"providers",
|
||||||
|
"moonshot",
|
||||||
|
"baseUrl",
|
||||||
|
]);
|
||||||
|
const moonshotProviderApiKey = getPathValue(base, [
|
||||||
|
"models",
|
||||||
|
"providers",
|
||||||
|
"moonshot",
|
||||||
|
"apiKey",
|
||||||
|
]);
|
||||||
|
const moonshotProviderApi = getPathValue(base, [
|
||||||
|
"models",
|
||||||
|
"providers",
|
||||||
|
"moonshot",
|
||||||
|
"api",
|
||||||
|
]);
|
||||||
|
const modelsMode = getPathValue(base, ["models", "mode"]);
|
||||||
|
|
||||||
|
const minimax: ConfigPatch[] = [];
|
||||||
|
if (!hasNonEmptyString(envMinimax)) {
|
||||||
|
minimax.push({ path: ["env", "MINIMAX_API_KEY"], value: "sk-..." });
|
||||||
|
}
|
||||||
|
if (modelsMode == null) {
|
||||||
|
minimax.push({ path: ["models", "mode"], value: "merge" });
|
||||||
|
}
|
||||||
|
// Intentional: enforce the preferred MiniMax endpoint/mode.
|
||||||
|
if (minimaxProviderBaseUrl !== "https://api.minimax.io/anthropic") {
|
||||||
|
minimax.push({
|
||||||
|
path: ["models", "providers", "minimax", "baseUrl"],
|
||||||
|
value: "https://api.minimax.io/anthropic",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!hasNonEmptyString(minimaxProviderApiKey)) {
|
||||||
|
minimax.push({
|
||||||
|
path: ["models", "providers", "minimax", "apiKey"],
|
||||||
|
value: "${MINIMAX_API_KEY}",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (minimaxProviderApi !== "anthropic-messages") {
|
||||||
|
minimax.push({
|
||||||
|
path: ["models", "providers", "minimax", "api"],
|
||||||
|
value: "anthropic-messages",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!minimaxHasModels) {
|
||||||
|
minimax.push({
|
||||||
|
path: minimaxModelsPath as Array<string | number>,
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
id: "MiniMax-M2.1",
|
||||||
|
name: "MiniMax M2.1",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
||||||
|
contextWindow: 200000,
|
||||||
|
maxTokens: 8192,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
minimax.push(setPrimary("minimax/MiniMax-M2.1"));
|
||||||
|
const minimaxAlias = safeAlias("minimax/MiniMax-M2.1", "Minimax");
|
||||||
|
if (minimaxAlias) minimax.push(minimaxAlias);
|
||||||
|
|
||||||
|
const zai: ConfigPatch[] = [];
|
||||||
|
if (!hasNonEmptyString(envZai)) {
|
||||||
|
zai.push({ path: ["env", "ZAI_API_KEY"], value: "sk-..." });
|
||||||
|
}
|
||||||
|
zai.push(setPrimary("zai/glm-4.7"));
|
||||||
|
const zaiAlias = safeAlias("zai/glm-4.7", "GLM 4.7");
|
||||||
|
if (zaiAlias) zai.push(zaiAlias);
|
||||||
|
|
||||||
|
const moonshot: ConfigPatch[] = [];
|
||||||
|
if (!hasNonEmptyString(envMoonshot)) {
|
||||||
|
moonshot.push({ path: ["env", "MOONSHOT_API_KEY"], value: "sk-..." });
|
||||||
|
}
|
||||||
|
if (modelsMode == null) {
|
||||||
|
moonshot.push({ path: ["models", "mode"], value: "merge" });
|
||||||
|
}
|
||||||
|
if (!hasNonEmptyString(moonshotProviderBaseUrl)) {
|
||||||
|
moonshot.push({
|
||||||
|
path: ["models", "providers", "moonshot", "baseUrl"],
|
||||||
|
value: "https://api.moonshot.ai/v1",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!hasNonEmptyString(moonshotProviderApiKey)) {
|
||||||
|
moonshot.push({
|
||||||
|
path: ["models", "providers", "moonshot", "apiKey"],
|
||||||
|
value: "${MOONSHOT_API_KEY}",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!hasNonEmptyString(moonshotProviderApi)) {
|
||||||
|
moonshot.push({
|
||||||
|
path: ["models", "providers", "moonshot", "api"],
|
||||||
|
value: "openai-completions",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!moonshotHasModels) {
|
||||||
|
moonshot.push({
|
||||||
|
path: moonshotModelsPath as Array<string | number>,
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
id: "kimi-k2-0905-preview",
|
||||||
|
name: "Kimi K2 0905 Preview",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: 256000,
|
||||||
|
maxTokens: 8192,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
moonshot.push(setPrimary("moonshot/kimi-k2-0905-preview"));
|
||||||
|
const moonshotAlias = safeAlias("moonshot/kimi-k2-0905-preview", "Kimi K2");
|
||||||
|
if (moonshotAlias) moonshot.push(moonshotAlias);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "minimax",
|
||||||
|
title: "MiniMax M2.1 (Anthropic)",
|
||||||
|
description:
|
||||||
|
"Adds provider config for MiniMax’s /anthropic endpoint and sets it as the default model.",
|
||||||
|
patches: minimax,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "zai",
|
||||||
|
title: "GLM 4.7 (Z.AI)",
|
||||||
|
description: "Adds ZAI_API_KEY placeholder + sets default model to zai/glm-4.7.",
|
||||||
|
patches: zai,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "moonshot",
|
||||||
|
title: "Kimi (Moonshot)",
|
||||||
|
description: "Adds Moonshot provider config + sets default model to kimi-k2-0905-preview.",
|
||||||
|
patches: moonshot,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export function renderConfig(props: ConfigProps) {
|
export function renderConfig(props: ConfigProps) {
|
||||||
const validity =
|
const validity =
|
||||||
props.valid == null ? "unknown" : props.valid ? "valid" : "invalid";
|
props.valid == null ? "unknown" : props.valid ? "valid" : "invalid";
|
||||||
@@ -44,6 +327,26 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
!props.updating &&
|
!props.updating &&
|
||||||
(props.formMode === "raw" ? true : canSaveForm);
|
(props.formMode === "raw" ? true : canSaveForm);
|
||||||
const canUpdate = props.connected && !props.applying && !props.updating;
|
const canUpdate = props.connected && !props.applying && !props.updating;
|
||||||
|
|
||||||
|
const applyPreset = (patches: ConfigPatch[]) => {
|
||||||
|
const base =
|
||||||
|
props.formValue ??
|
||||||
|
tryParseJsonObject(props.raw) ??
|
||||||
|
({} as Record<string, unknown>);
|
||||||
|
const next = cloneConfigObject(base);
|
||||||
|
for (const patch of patches) {
|
||||||
|
setPathValue(next, patch.path, patch.value);
|
||||||
|
}
|
||||||
|
props.onRawChange(`${JSON.stringify(next, null, 2).trimEnd()}\n`);
|
||||||
|
for (const patch of patches) props.onFormPatch(patch.path, patch.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const presetBase =
|
||||||
|
props.formValue ??
|
||||||
|
tryParseJsonObject(props.raw) ??
|
||||||
|
({} as Record<string, unknown>);
|
||||||
|
const modelPresets = buildModelPresetPatches(presetBase);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="row" style="justify-content: space-between;">
|
<div class="row" style="justify-content: space-between;">
|
||||||
@@ -100,6 +403,32 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
comes back.
|
comes back.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="callout" style="margin-top: 12px;">
|
||||||
|
<div style="font-weight: 600;">Model presets</div>
|
||||||
|
<div class="muted" style="margin-top: 6px;">
|
||||||
|
One-click inserts for MiniMax, GLM 4.7 (Z.AI), and Kimi (Moonshot). Keeps
|
||||||
|
existing API keys and per-model params when present.
|
||||||
|
</div>
|
||||||
|
<div class="row" style="margin-top: 10px; flex-wrap: wrap;">
|
||||||
|
${modelPresets.map(
|
||||||
|
(preset) => html`
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
?disabled=${props.loading || props.saving || !props.connected}
|
||||||
|
title=${preset.description}
|
||||||
|
@click=${() => applyPreset(preset.patches)}
|
||||||
|
>
|
||||||
|
${preset.title}
|
||||||
|
</button>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="muted" style="margin-top: 8px;">
|
||||||
|
Tip: use <span class="mono">/model</span> to switch models without editing
|
||||||
|
config.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
${props.formMode === "form"
|
${props.formMode === "form"
|
||||||
? html`<div style="margin-top: 12px;">
|
? html`<div style="margin-top: 12px;">
|
||||||
${props.schemaLoading
|
${props.schemaLoading
|
||||||
|
|||||||
Reference in New Issue
Block a user