Files
cursor-byok/frontend/src/views/ModelEditor.vue
T
xiaopuandCursor 320b9fb7b9 feat(model-adapter): add remote model selection
- Fetch and normalize OpenAI and Anthropic model lists through the proxy service.
- Add searchable multi-model selection with localized UI and persisted adapter mappings.
- Cover endpoint resolution, authentication, pagination, and response parsing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 15:23:51 +08:00

715 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import Button from "@/components/ui/Button.vue";
import Input from "@/components/ui/Input.vue";
import ModelAdapterTestCard from "@/components/ModelAdapterTestCard.vue";
import MultiSelect from "@/components/ui/MultiSelect.vue";
import Select from "@/components/ui/Select.vue";
import Tooltip from "@/components/ui/Tooltip.vue";
import { getModelEditorContext } from "@/services/clientApi";
import {
ANTHROPIC_THINKING_EFFORT_DEFAULT,
appState,
buildModelAdapterTestRequestHash,
buildModelAdaptersFromModelIDs,
createEmptyModelAdapter,
CUSTOM_HEADERS_DEFAULT_JSON,
EXTRA_PARAMS_DEFAULT_JSON,
fetchAvailableModelIDs,
getModelAdapterTestResult,
getModelAdapterTestResultByID,
isModelAdapterTestResultStale,
normalizeModelAdapter,
OPENAI_ENDPOINT_CHAT_COMPLETIONS,
OPENAI_ENDPOINT_CUSTOM,
OPENAI_ENDPOINT_RESPONSES,
OPENAI_EXTRA_PARAMS_DEFAULT_JSON,
runModelAdapterTest,
saveModelAdapterAt,
saveModelAdaptersFromModelIDs,
toUserError,
validateModelAdapters,
} from "@/state/appState";
import { Window } from "@wailsio/runtime";
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
const modelTypeTabs = [
{ label: "OpenAI", value: "openai", icon: "icon-[bxl--openai]" },
{ label: "Anthropic", value: "anthropic", icon: "icon-[logos--claude-icon]" },
];
const reasoningEffortOptions = [
{ label: "低", value: "low", icon: "icon-[mdi--head-outline]" },
{ label: "中", value: "medium", icon: "icon-[mdi--head-lightbulb-outline]" },
{ label: "高", value: "high", icon: "icon-[mdi--brain]" },
{ label: "极高", value: "xhigh", icon: "icon-[mdi--head-cog-outline]" },
{ label: "最高", value: "max", icon: "icon-[mdi--brain]" },
];
const anthropicThinkingEffortOptions = [
{ label: "低", value: "low", icon: "icon-[mdi--head-outline]" },
{ label: "中", value: "medium", icon: "icon-[mdi--head-lightbulb-outline]" },
{ label: "高", value: "high", icon: "icon-[mdi--brain]" },
{ label: "极高", value: "xhigh", icon: "icon-[mdi--head-cog-outline]" },
{ label: "Max", value: "max", icon: "icon-[mdi--brain]" },
];
const openAIEndpointOptions = [
{ label: "/v1/responses", value: OPENAI_ENDPOINT_RESPONSES, icon: "icon-[mdi--api]" },
{ label: "/v1/chat/completions", value: OPENAI_ENDPOINT_CHAT_COMPLETIONS, icon: "icon-[mdi--message-text-outline]" },
{ label: "自定义路径(请输入完整请求地址)", value: OPENAI_ENDPOINT_CUSTOM, icon: "icon-[mdi--pencil-outline]" },
];
const editorIndex = ref(-1);
const draft = reactive(createEmptyModelAdapter());
const errorMessage = ref("");
const loading = ref(true);
const lastTestAdapterID = ref("");
const localTestFailure = ref("");
const modelPrefix = ref("");
const existingModelPrefixCleared = ref(false);
const availableModelIDs = ref([]);
const selectedModelIDs = ref([]);
const modelSelectionMode = ref("auto");
const modelListLoading = ref(false);
const modelListRequestSeq = ref(0);
let modelListDebounceTimer = 0;
function createOptionalPositiveIntegerModel(key) {
return computed({
get() {
return draft[key] > 0 ? String(draft[key]) : "";
},
set(value) {
const text = String(value || "").trim();
draft[key] = /^\d+$/.test(text) && Number(text) > 0 ? Number(text) : 0;
},
});
}
const maxCompletionTokensInput = createOptionalPositiveIntegerModel("maxCompletionTokens");
const anthropicMaxTokensInput = createOptionalPositiveIntegerModel("anthropicMaxTokens");
const contextWindowTokensInput = createOptionalPositiveIntegerModel("contextWindowTokens");
const interfacePlaceholder = computed(() =>
draft.type === "anthropic" ? "例如:https://api.anthropic.com" : "例如:https://api.openai.com/v1",
);
const modelOptions = computed(() => availableModelIDs.value.map((modelID) => ({
label: modelID,
value: modelID,
icon: "icon-[mdi--cube-outline]",
})));
const isManualModelInput = computed(() => modelSelectionMode.value === "manual");
const activeModelIDs = computed(() => (
isManualModelInput.value
? [String(draft.modelID || "").trim()].filter(Boolean)
: selectedModelIDs.value
));
const primaryModelID = computed(() => (
isManualModelInput.value
? String(draft.modelID || "").trim()
: selectedModelIDs.value.includes(draft.modelID) ? draft.modelID : selectedModelIDs.value[0] || ""
));
const selectedTestAdapter = computed(() => {
if (isManualModelInput.value) {
const modelID = primaryModelID.value;
return normalizeModelAdapter({
...draft,
modelID,
displayName: modelID,
});
}
const adapters = buildModelAdaptersFromModelIDs(draft, selectedModelIDs.value, modelPrefix.value);
return adapters.find((adapter) => adapter.modelID === primaryModelID.value)
?? adapters[0]
?? normalizeModelAdapter(draft);
});
const currentRequestHash = computed(() => buildModelAdapterTestRequestHash(selectedTestAdapter.value));
const directModelTestResult = computed(() => getModelAdapterTestResult(selectedTestAdapter.value));
const rememberedModelTestResult = computed(() =>
lastTestAdapterID.value ? getModelAdapterTestResultByID(lastTestAdapterID.value) : null,
);
const activeModelTestResult = computed(() => directModelTestResult.value || rememberedModelTestResult.value);
const modelTestResultStale = computed(() =>
isModelAdapterTestResultStale(selectedTestAdapter.value, activeModelTestResult.value),
);
const isCurrentConfigTesting = computed(() => directModelTestResult.value?.status === "running");
const modelTestSummary = computed(() => {
if (localTestFailure.value) {
return localTestFailure.value;
}
return activeModelTestResult.value?.summaryText || "尚未测试";
});
const title = computed(() => (editorIndex.value >= 0 ? "编辑模型配置" : "新增模型配置"));
function ensureOpenAIExtraParamsJSON() {
if (!String(draft.openAIExtraParamsJSON || "").trim()) {
draft.openAIExtraParamsJSON = OPENAI_EXTRA_PARAMS_DEFAULT_JSON;
}
}
function ensureCustomHeadersJSON() {
if (!String(draft.customHeadersJSON || "").trim()) {
draft.customHeadersJSON = CUSTOM_HEADERS_DEFAULT_JSON;
}
}
function ensureAnthropicExtraParamsJSON() {
if (!String(draft.anthropicExtraParamsJSON || "").trim()) {
draft.anthropicExtraParamsJSON = EXTRA_PARAMS_DEFAULT_JSON;
}
}
function ensureAnthropicThinkingEffort() {
if (!String(draft.anthropicThinkingEffort || "").trim()) {
draft.anthropicThinkingEffort = ANTHROPIC_THINKING_EFFORT_DEFAULT;
}
}
const fieldTips = {
displayName: "仅用于界面展示,便于你区分不同模型。",
modelID: "可多选。保存时只会写入选中的模型,每个模型生成一条配置。",
manualModelID: "请求实际发送给服务端的模型名称,例如 gpt-4.1 或 claude-sonnet。",
baseURL: "模型服务的 API 根地址,通常为兼容 OpenAI 或 Anthropic 的接口入口。",
apiKey: "调用该模型服务需要使用的访问密钥。",
contextWindowTokens: "模型单次可接受的最大上下文 Token 数。留空时使用默认值。",
reasoningEffort: "推理强度仅对部分支持 reasoning_effort 的模型生效,并不是所有模型都支持。越高通常越稳,但也可能更慢。",
maxCompletionTokens: "单次回复允许生成的最大 Token 数。留空时使用默认值。",
openAIEndpoint: "选择接口协议端点。选“自定义路径”时,请在接口地址栏填写完整请求地址(含 /chat/completions 或 /responses 路径后缀),系统会根据末段自动判断协议形态。",
openAIExtraParams: "开启后会把 JSON 对象覆盖到 OpenAI 请求体。同名字段以这里为准。OpenAI service_tier 支持 auto、default、flex、scale、priority。",
customHeaders: "开启后会把 JSON 对象覆盖到最终请求头。同名请求头以这里为准,值必须是字符串。",
anthropicExtraParams: "开启后会把 JSON 对象覆盖到 Anthropic 请求体。同名字段以这里为准。",
anthropicMaxTokens: "Anthropic 模型单次回复允许生成的最大 Token 数。留空时使用默认值。",
anthropicThinkingEffort: "Anthropic adaptive thinking 的思考强度。请求会固定使用新版 thinking.type=adaptive。",
tooltipData: "模型列表 hover 时显示的备注说明。",
};
async function loadContext() {
try {
const ctx = await getModelEditorContext();
editorIndex.value = typeof ctx.index === "number" ? ctx.index : -1;
const parsed = JSON.parse(ctx.adapterJSON || "{}");
Object.assign(draft, normalizeModelAdapter(parsed));
if (draft.modelID) {
availableModelIDs.value = [draft.modelID];
selectedModelIDs.value = [draft.modelID];
}
modelPrefix.value = draft.displayName || "";
if (!draft.type) {
draft.type = "openai";
}
} catch (_error) {
Object.assign(draft, createEmptyModelAdapter());
draft.type = "openai";
} finally {
loading.value = false;
}
}
function syncSelectionWithAvailable() {
const available = availableModelIDs.value;
const kept = selectedModelIDs.value.filter((modelID) => available.includes(modelID));
selectedModelIDs.value = kept;
draft.modelID = kept.includes(draft.modelID) ? draft.modelID : kept[0] || "";
}
function handleModelSelectionChange(values) {
selectedModelIDs.value = values;
draft.modelID = values.includes(draft.modelID) ? draft.modelID : values[0] || "";
}
async function refreshModelList() {
const baseURL = String(draft.baseURL || "").trim();
const apiKey = String(draft.apiKey || "").trim();
if (!baseURL || !apiKey || !draft.type) {
modelSelectionMode.value = "auto";
availableModelIDs.value = draft.modelID ? [draft.modelID] : [];
syncSelectionWithAvailable();
return [];
}
const requestSeq = modelListRequestSeq.value + 1;
modelListRequestSeq.value = requestSeq;
modelSelectionMode.value = "auto";
modelListLoading.value = true;
try {
const models = await fetchAvailableModelIDs({
type: draft.type,
baseURL,
apiKey,
customHeadersEnabled: draft.customHeadersEnabled,
customHeadersJSON: draft.customHeadersJSON,
});
if (requestSeq !== modelListRequestSeq.value) {
return availableModelIDs.value;
}
if (editorIndex.value >= 0 && !existingModelPrefixCleared.value) {
modelPrefix.value = "";
existingModelPrefixCleared.value = true;
}
modelSelectionMode.value = "auto";
availableModelIDs.value = models;
syncSelectionWithAvailable();
return models;
} catch (_error) {
if (requestSeq === modelListRequestSeq.value) {
modelSelectionMode.value = "manual";
availableModelIDs.value = [];
selectedModelIDs.value = [];
}
return availableModelIDs.value;
} finally {
if (requestSeq === modelListRequestSeq.value) {
modelListLoading.value = false;
}
}
}
async function persistDraft() {
const models = activeModelIDs.value;
if (models.length === 0) {
const error = isManualModelInput.value ? "请填写模型标识" : "请先选择要保存的模型";
errorMessage.value = error;
return { ok: false, error, adapter: null };
}
const selectedModelID = primaryModelID.value || models[0];
const adapter = normalizeModelAdapter({
...draft,
modelID: selectedModelID,
displayName: isManualModelInput.value
? String(modelPrefix.value || selectedModelID).trim()
: `${String(modelPrefix.value || "模型").trim()}-${selectedModelID}`,
});
const singleCheck = validateModelAdapters([adapter]);
if (singleCheck) {
errorMessage.value = singleCheck;
return { ok: false, error: singleCheck, adapter: null };
}
const result = isManualModelInput.value
? await saveModelAdapterAt(editorIndex.value, adapter)
: await saveModelAdaptersFromModelIDs(adapter, models, modelPrefix.value, selectedModelID);
if (!result.ok) {
errorMessage.value = result.error;
return { ok: false, error: result.error, adapter: null };
}
if (typeof result.index === "number") {
editorIndex.value = result.index;
}
if (result.adapter) {
Object.assign(draft, normalizeModelAdapter(result.adapter));
} else {
Object.assign(draft, adapter);
}
errorMessage.value = "";
return {
ok: true,
error: "",
adapter: result.adapter ? normalizeModelAdapter(result.adapter) : normalizeModelAdapter(adapter),
};
}
async function handleSave() {
const result = await persistDraft();
if (!result.ok) {
return;
}
await Window.Close();
}
async function handleCancel() {
await Window.Close();
}
function handleModelTypeChange(type) {
draft.type = type;
modelSelectionMode.value = "auto";
availableModelIDs.value = [];
selectedModelIDs.value = [];
draft.modelID = "";
if (type === "openai" && !draft.openAIEndpoint) {
draft.openAIEndpoint = OPENAI_ENDPOINT_RESPONSES;
} else if (type === "anthropic") {
ensureAnthropicThinkingEffort();
}
}
async function handleTest() {
localTestFailure.value = "";
try {
const saved = await persistDraft();
if (!saved.ok || !saved.adapter) {
return;
}
const result = await runModelAdapterTest(saved.adapter);
if (result?.adapterID) {
lastTestAdapterID.value = result.adapterID;
}
} catch (error) {
const latest = getModelAdapterTestResult(draft);
if (latest?.adapterID) {
lastTestAdapterID.value = latest.adapterID;
return;
}
localTestFailure.value = toUserError(error);
}
}
watch(
directModelTestResult,
(result) => {
if (!result?.adapterID) {
return;
}
lastTestAdapterID.value = result.adapterID;
if (result.status !== "running") {
localTestFailure.value = "";
}
},
{ immediate: true },
);
watch(currentRequestHash, () => {
localTestFailure.value = "";
});
watch(
() => draft.openAIExtraParamsEnabled,
(enabled) => {
if (enabled) {
ensureOpenAIExtraParamsJSON();
}
},
);
watch(
() => draft.customHeadersEnabled,
(enabled) => {
if (enabled) {
ensureCustomHeadersJSON();
}
},
);
watch(
() => draft.anthropicExtraParamsEnabled,
(enabled) => {
if (enabled) {
ensureAnthropicExtraParamsJSON();
}
},
);
watch(
() => [draft.type, draft.baseURL, draft.apiKey, draft.customHeadersEnabled, draft.customHeadersJSON],
() => {
window.clearTimeout(modelListDebounceTimer);
const baseURL = String(draft.baseURL || "").trim();
const apiKey = String(draft.apiKey || "").trim();
if (!baseURL || !apiKey) {
modelSelectionMode.value = "auto";
modelListLoading.value = false;
availableModelIDs.value = draft.modelID ? [draft.modelID] : [];
syncSelectionWithAvailable();
return;
}
modelListDebounceTimer = window.setTimeout(() => {
void refreshModelList();
}, 600);
},
);
onMounted(async () => {
await loadContext();
});
onBeforeUnmount(() => {
window.clearTimeout(modelListDebounceTimer);
});
</script>
<template>
<div class="flex h-full flex-col text-[#e5e5e5]">
<div class="flex shrink-0 items-center justify-between px-4 pb-2">
<h2 class="text-base font-medium text-white">{{ title }}</h2>
<div class="flex items-center gap-2">
<Button variant="default" @click="handleCancel">取消</Button>
<Button variant="default" :disabled="isCurrentConfigTesting || appState.configSaving" @click="handleTest">
{{ isCurrentConfigTesting ? "测试中..." : "保存并测试" }}
</Button>
<Button variant="primary" :disabled="appState.configSaving" @click="handleSave">
{{ appState.configSaving ? "保存中..." : "保存" }}
</Button>
</div>
</div>
<div v-if="loading" class="flex flex-1 items-center justify-center text-sm text-[#a3a3a3]">
加载中...
</div>
<div v-else class="flex-1 overflow-y-auto min-h-0 px-4 pb-4">
<div class="flex flex-col gap-4">
<div class="center-row gap-2">
<button
v-for="tab in modelTypeTabs"
:key="tab.value"
type="button"
class="center-row gap-2 rounded-[8px] border px-3 py-2 text-sm transition-colors duration-150"
:class="draft.type === tab.value
? 'border-[#1ca35a] bg-[#123322] text-white'
: 'border-[#343434] bg-[#252525] text-[#a3a3a3] hover:border-[#4a4a4a] hover:text-[#e5e5e5]'"
@click="handleModelTypeChange(tab.value)"
>
<span :class="[tab.icon, 'text-[16px]']"></span>
<span>{{ tab.label }}</span>
</button>
</div>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
<label class="flex flex-col gap-1">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="fieldTips.baseURL" />
<span>接口地址</span>
</span>
<input
v-model="draft.baseURL"
type="text"
:placeholder="interfacePlaceholder"
class="h-9 rounded-[6px] border border-[#3f3f3f] bg-[#232323] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
/>
</label>
<label class="flex flex-col gap-1">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="fieldTips.apiKey" />
<span>访问密钥</span>
</span>
<Input
v-model="draft.apiKey"
type="password"
allow-visibility-toggle
placeholder="例如:sk-xxxxxx"
autocomplete="off"
/>
</label>
<label class="flex flex-col gap-1">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="isManualModelInput ? fieldTips.displayName : fieldTips.modelID" />
<span>{{ isManualModelInput ? "显示名称" : "模型前缀" }}</span>
</span>
<input
v-model="modelPrefix"
type="text"
placeholder="例如:GPT-5"
class="h-9 rounded-[6px] border border-[#3f3f3f] bg-[#232323] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
/>
</label>
<div class="flex flex-col gap-1">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="isManualModelInput ? fieldTips.manualModelID : fieldTips.modelID" />
<span>{{ isManualModelInput ? "模型标识" : "选择模型" }}</span>
</span>
<input
v-if="isManualModelInput"
v-model="draft.modelID"
type="text"
placeholder="例如:gpt-4.1"
class="h-9 rounded-[6px] border border-[#3f3f3f] bg-[#232323] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
/>
<MultiSelect
v-else
:model-value="selectedModelIDs"
:options="modelOptions"
:disabled="modelListLoading || modelOptions.length === 0"
:placeholder="modelListLoading ? '正在获取模型...' : '请输入接口地址和访问密钥'"
:summary-formatter="(count, total) => `已选择 ${count} / ${total} 个模型`"
aria-label="选择模型"
@update:model-value="handleModelSelectionChange"
/>
</div>
<label class="flex flex-col gap-1">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="fieldTips.contextWindowTokens" />
<span>上下文窗口</span>
</span>
<input
v-model="contextWindowTokensInput"
type="text"
inputmode="numeric"
placeholder="例如200000留空用默认值"
class="h-9 rounded-[6px] border border-[#3f3f3f] bg-[#232323] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
/>
</label>
<label v-if="draft.type === 'openai'" class="flex flex-col gap-1">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="fieldTips.reasoningEffort" />
<span>推理强度</span>
</span>
<Select
v-model="draft.reasoningEffort"
:options="reasoningEffortOptions"
/>
</label>
<label v-if="draft.type === 'anthropic'" class="flex flex-col gap-1">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="fieldTips.anthropicMaxTokens" />
<span>最大输出 Token</span>
</span>
<input
v-model="anthropicMaxTokensInput"
type="text"
inputmode="numeric"
placeholder="例如65536留空用默认值"
class="h-9 rounded-[6px] border border-[#3f3f3f] bg-[#232323] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
/>
</label>
<label v-if="draft.type === 'anthropic'" class="flex flex-col gap-1">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="fieldTips.anthropicThinkingEffort" />
<span>思考强度</span>
</span>
<Select
v-model="draft.anthropicThinkingEffort"
:options="anthropicThinkingEffortOptions"
/>
</label>
</div>
<div v-if="draft.type === 'openai'" class="grid grid-cols-1 gap-3 md:grid-cols-2">
<label class="flex flex-col gap-1">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="fieldTips.maxCompletionTokens" />
<span>最大输出 Token</span>
</span>
<input
v-model="maxCompletionTokensInput"
type="text"
inputmode="numeric"
placeholder="例如65536留空用默认值"
class="h-9 rounded-[6px] border border-[#3f3f3f] bg-[#232323] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
/>
</label>
<label class="flex flex-col gap-1">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="fieldTips.openAIEndpoint" />
<span>接口端点</span>
</span>
<Select
v-model="draft.openAIEndpoint"
:options="openAIEndpointOptions"
/>
</label>
</div>
<div v-if="draft.type === 'openai'" class="rounded-[8px] border border-[#343434] bg-[#252525] p-3">
<div class="flex items-center justify-between gap-3">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="fieldTips.openAIExtraParams" />
<span>额外参数 JSON</span>
</span>
<label class="center-row gap-2 text-xs text-[#d4d4d4]">
<input
v-model="draft.openAIExtraParamsEnabled"
type="checkbox"
class="size-4 accent-[#10AD5D]"
/>
<span>启用</span>
</label>
</div>
<textarea
v-if="draft.openAIExtraParamsEnabled"
v-model="draft.openAIExtraParamsJSON"
rows="5"
spellcheck="false"
class="mt-3 min-h-[120px] w-full resize-none rounded-[6px] border border-[#3f3f3f] bg-[#1f1f1f] px-3 py-2 font-mono text-xs text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
/>
</div>
<div v-if="draft.type === 'anthropic'" class="rounded-[8px] border border-[#343434] bg-[#252525] p-3">
<div class="flex items-center justify-between gap-3">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="fieldTips.anthropicExtraParams" />
<span>Anthropic 额外参数 JSON</span>
</span>
<label class="center-row gap-2 text-xs text-[#d4d4d4]">
<input
v-model="draft.anthropicExtraParamsEnabled"
type="checkbox"
class="size-4 accent-[#10AD5D]"
/>
<span>启用</span>
</label>
</div>
<textarea
v-if="draft.anthropicExtraParamsEnabled"
v-model="draft.anthropicExtraParamsJSON"
rows="5"
spellcheck="false"
class="mt-3 min-h-[120px] w-full resize-none rounded-[6px] border border-[#3f3f3f] bg-[#1f1f1f] px-3 py-2 font-mono text-xs text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
/>
</div>
<div class="rounded-[8px] border border-[#343434] bg-[#252525] p-3">
<div class="flex items-center justify-between gap-3">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="fieldTips.customHeaders" />
<span>自定义请求头 JSON</span>
</span>
<label class="center-row gap-2 text-xs text-[#d4d4d4]">
<input
v-model="draft.customHeadersEnabled"
type="checkbox"
class="size-4 accent-[#10AD5D]"
/>
<span>启用</span>
</label>
</div>
<textarea
v-if="draft.customHeadersEnabled"
v-model="draft.customHeadersJSON"
rows="5"
spellcheck="false"
class="mt-3 min-h-[120px] w-full resize-none rounded-[6px] border border-[#3f3f3f] bg-[#1f1f1f] px-3 py-2 font-mono text-xs text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
/>
</div>
<label class="flex flex-col gap-1">
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
<Tooltip :content="fieldTips.tooltipData" />
<span>备注</span>
</span>
<textarea
v-model="draft.tooltipData"
rows="3"
placeholder="例如用于日常代码补全与问答"
class="min-h-[96px] resize-none rounded-[6px] border border-[#3f3f3f] bg-[#232323] px-3 py-2 text-sm text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
/>
</label>
<ModelAdapterTestCard
:result="localTestFailure ? { status: 'error', error: '测试失败', summaryText: '测试失败', rawResponse: modelTestSummary } : activeModelTestResult"
:stale="modelTestResultStale"
:show-metrics="true"
/>
<div
v-if="errorMessage"
class="rounded-[8px] border border-[#4b1d1d] bg-[#2a1313] px-3 py-2 text-sm text-[#fca5a5]"
>
{{ errorMessage }}
</div>
</div>
</div>
</div>
</template>