mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 03:27:02 +08:00
Merge branch 'main' of github.com:leookun/cursor-byok
This commit is contained in:
@@ -0,0 +1,362 @@
|
|||||||
|
<script setup>
|
||||||
|
import { autoUpdate, computePosition, flip, offset, shift, size } from "@floating-ui/dom";
|
||||||
|
import { computed, onBeforeUnmount, nextTick, ref, watch, watchPostEffect } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
placeholder: { type: String, default: "请选择" },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
ariaLabel: { type: String, default: "" },
|
||||||
|
summaryFormatter: { type: Function, default: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "change"]);
|
||||||
|
|
||||||
|
const rootRef = ref(null);
|
||||||
|
const buttonRef = ref(null);
|
||||||
|
const menuRef = ref(null);
|
||||||
|
const selectAllRef = ref(null);
|
||||||
|
const optionRefs = ref([]);
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const menuStyle = ref({});
|
||||||
|
|
||||||
|
// -1 表示"全选"按钮,0..n-1 表示选项,共同组成一个可循环的键盘焦点环
|
||||||
|
const activeIndex = ref(-1);
|
||||||
|
|
||||||
|
const normalizedOptions = computed(() => props.options.map((option) => {
|
||||||
|
if (typeof option === "string") {
|
||||||
|
return { label: option, value: option };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: option?.label ?? option?.value ?? "",
|
||||||
|
value: option?.value ?? "",
|
||||||
|
icon: option?.icon ?? "",
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selectedValues = computed(() => new Set(props.modelValue ?? []));
|
||||||
|
const allSelected = computed(() =>
|
||||||
|
normalizedOptions.value.length > 0
|
||||||
|
&& normalizedOptions.value.every((option) => selectedValues.value.has(option.value)),
|
||||||
|
);
|
||||||
|
const summaryLabel = computed(() => {
|
||||||
|
const count = selectedValues.value.size;
|
||||||
|
if (count === 0) {
|
||||||
|
return props.placeholder;
|
||||||
|
}
|
||||||
|
if (props.summaryFormatter) {
|
||||||
|
return props.summaryFormatter(count, normalizedOptions.value.length);
|
||||||
|
}
|
||||||
|
return `已选择 ${count} 项`;
|
||||||
|
});
|
||||||
|
|
||||||
|
function emitSelection(values) {
|
||||||
|
emit("update:modelValue", values);
|
||||||
|
emit("change", values);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOption(option) {
|
||||||
|
const next = normalizedOptions.value
|
||||||
|
.filter((item) => (item.value === option.value
|
||||||
|
? !selectedValues.value.has(item.value)
|
||||||
|
: selectedValues.value.has(item.value)))
|
||||||
|
.map((item) => item.value);
|
||||||
|
emitSelection(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectAll() {
|
||||||
|
if (allSelected.value) {
|
||||||
|
emitSelection([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emitSelection(normalizedOptions.value.map((option) => option.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOptionRef(el, index) {
|
||||||
|
if (el) {
|
||||||
|
optionRefs.value[index] = el;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete optionRefs.value[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusActiveOption() {
|
||||||
|
nextTick(() => {
|
||||||
|
if (activeIndex.value < 0) {
|
||||||
|
selectAllRef.value?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
optionRefs.value[activeIndex.value]?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveActiveIndex(step) {
|
||||||
|
if (!isOpen.value) {
|
||||||
|
openMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const total = normalizedOptions.value.length;
|
||||||
|
if (total === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 焦点环长度为 total + 1(含全选),内部用 0..total 表示,再映射回 -1..total-1
|
||||||
|
const ringSize = total + 1;
|
||||||
|
const current = activeIndex.value + 1;
|
||||||
|
activeIndex.value = ((current + step + ringSize) % ringSize) - 1;
|
||||||
|
focusActiveOption();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMenu() {
|
||||||
|
if (props.disabled || isOpen.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isOpen.value = true;
|
||||||
|
const firstSelected = normalizedOptions.value.findIndex((option) => selectedValues.value.has(option.value));
|
||||||
|
activeIndex.value = firstSelected;
|
||||||
|
nextTick(() => {
|
||||||
|
updatePosition();
|
||||||
|
focusActiveOption();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMenu({ restoreFocus = false } = {}) {
|
||||||
|
if (!isOpen.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isOpen.value = false;
|
||||||
|
activeIndex.value = -1;
|
||||||
|
optionRefs.value = [];
|
||||||
|
menuStyle.value = {};
|
||||||
|
if (restoreFocus) {
|
||||||
|
nextTick(() => buttonRef.value?.focus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
|
if (isOpen.value) {
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleButtonKeydown(event) {
|
||||||
|
if (props.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowDown":
|
||||||
|
event.preventDefault();
|
||||||
|
moveActiveIndex(1);
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
event.preventDefault();
|
||||||
|
moveActiveIndex(-1);
|
||||||
|
break;
|
||||||
|
case "Enter":
|
||||||
|
case " ":
|
||||||
|
event.preventDefault();
|
||||||
|
toggleMenu();
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
if (isOpen.value) {
|
||||||
|
event.preventDefault();
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOptionKeydown(event, option, index) {
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowDown":
|
||||||
|
event.preventDefault();
|
||||||
|
activeIndex.value = index;
|
||||||
|
moveActiveIndex(1);
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
event.preventDefault();
|
||||||
|
activeIndex.value = index;
|
||||||
|
moveActiveIndex(-1);
|
||||||
|
break;
|
||||||
|
case "Enter":
|
||||||
|
case " ":
|
||||||
|
event.preventDefault();
|
||||||
|
if (option) {
|
||||||
|
toggleOption(option);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
toggleSelectAll();
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
event.preventDefault();
|
||||||
|
closeMenu({ restoreFocus: true });
|
||||||
|
break;
|
||||||
|
case "Tab":
|
||||||
|
closeMenu();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerDown(event) {
|
||||||
|
if (rootRef.value?.contains(event.target) || menuRef.value?.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePosition() {
|
||||||
|
if (!buttonRef.value || !menuRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
computePosition(buttonRef.value, menuRef.value, {
|
||||||
|
placement: "bottom-start",
|
||||||
|
middleware: [
|
||||||
|
offset(6),
|
||||||
|
flip({ padding: 12 }),
|
||||||
|
shift({ padding: 12 }),
|
||||||
|
size({
|
||||||
|
apply({ rects, elements, availableHeight }) {
|
||||||
|
Object.assign(elements.floating.style, {
|
||||||
|
minWidth: `${rects.reference.width}px`,
|
||||||
|
maxHeight: `${Math.max(availableHeight, 200)}px`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
padding: 12,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}).then(({ x, y }) => {
|
||||||
|
menuStyle.value = {
|
||||||
|
left: `${x}px`,
|
||||||
|
top: `${y}px`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watchPostEffect((cleanup) => {
|
||||||
|
if (!isOpen.value || !buttonRef.value || !menuRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stopAutoUpdate = autoUpdate(buttonRef.value, menuRef.value, updatePosition);
|
||||||
|
cleanup(() => {
|
||||||
|
stopAutoUpdate();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(isOpen, (open) => {
|
||||||
|
if (open) {
|
||||||
|
document.addEventListener("pointerdown", handlePointerDown);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.removeEventListener("pointerdown", handlePointerDown);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.disabled, (disabled) => {
|
||||||
|
if (disabled) {
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener("pointerdown", handlePointerDown);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="rootRef" class="relative">
|
||||||
|
<button
|
||||||
|
ref="buttonRef"
|
||||||
|
type="button"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="flex h-9 w-full items-center justify-between gap-2 rounded-[6px] border border-[#3f3f3f] bg-[#232323] px-3 text-left text-sm text-[#e5e5e5] outline-none transition-colors focus:border-[#10AD5D] disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
:aria-expanded="isOpen"
|
||||||
|
:aria-label="ariaLabel || undefined"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
@click="toggleMenu"
|
||||||
|
@keydown="handleButtonKeydown"
|
||||||
|
>
|
||||||
|
<span class="flex min-w-0 flex-1 items-center gap-2" :class="selectedValues.size ? 'text-[#e5e5e5]' : 'text-[#7b7b7b]'">
|
||||||
|
<span class="truncate">{{ summaryLabel }}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer-events-none center-row text-[#8f8f8f] transition-transform duration-200"
|
||||||
|
:class="isOpen ? 'rotate-180' : ''"
|
||||||
|
>
|
||||||
|
<span class="icon-[mdi--chevron-down] text-[18px]"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition duration-150 ease-out"
|
||||||
|
enter-from-class="translate-y-1 opacity-0"
|
||||||
|
enter-to-class="translate-y-0 opacity-100"
|
||||||
|
leave-active-class="transition duration-100 ease-in"
|
||||||
|
leave-from-class="translate-y-0 opacity-100"
|
||||||
|
leave-to-class="translate-y-1 opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
ref="menuRef"
|
||||||
|
class="fixed z-[999] flex flex-col overflow-hidden rounded-[8px] border border-[#3f3f3f] bg-[#232323] p-1 shadow-[0_16px_30px_-12px_rgba(0,0,0,0.7)]"
|
||||||
|
:style="menuStyle"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
ref="selectAllRef"
|
||||||
|
type="button"
|
||||||
|
class="flex w-full items-center gap-2 rounded-[6px] px-3 py-2 text-left text-sm text-[#d4d4d4] outline-none transition-colors hover:bg-[#303030]"
|
||||||
|
:class="activeIndex === -1 ? 'bg-[#303030]' : ''"
|
||||||
|
@click="toggleSelectAll"
|
||||||
|
@mouseenter="activeIndex = -1"
|
||||||
|
@keydown="handleOptionKeydown($event, null, -1)"
|
||||||
|
>
|
||||||
|
<span :class="[allSelected ? 'icon-[mdi--checkbox-marked]' : 'icon-[mdi--checkbox-blank-outline]', 'text-[16px] shrink-0']"></span>
|
||||||
|
<span class="truncate">{{ allSelected ? "取消全选" : "全选" }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul role="listbox" aria-multiselectable="true" class="overflow-y-auto py-1">
|
||||||
|
<li v-for="(option, index) in normalizedOptions" :key="option.value">
|
||||||
|
<button
|
||||||
|
:ref="(el) => setOptionRef(el, index)"
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
class="flex w-full items-center gap-2 rounded-[6px] px-3 py-2 text-left text-sm outline-none transition-colors"
|
||||||
|
:class="[
|
||||||
|
selectedValues.has(option.value)
|
||||||
|
? 'bg-[#10AD5D]/15 text-[#10d06f]'
|
||||||
|
: 'text-[#e5e5e5] hover:bg-[#303030]',
|
||||||
|
activeIndex === index ? 'bg-[#303030]' : '',
|
||||||
|
]"
|
||||||
|
:aria-selected="selectedValues.has(option.value)"
|
||||||
|
tabindex="0"
|
||||||
|
@click="toggleOption(option)"
|
||||||
|
@mouseenter="activeIndex = index"
|
||||||
|
@keydown="handleOptionKeydown($event, option, index)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
selectedValues.has(option.value) ? 'icon-[mdi--checkbox-marked]' : 'icon-[mdi--checkbox-blank-outline]',
|
||||||
|
'text-[16px] shrink-0',
|
||||||
|
]"
|
||||||
|
></span>
|
||||||
|
<span v-if="option.icon" :class="[option.icon, 'text-[16px] shrink-0']" aria-hidden="true"></span>
|
||||||
|
<span class="truncate">{{ option.label }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@
|
|||||||
"15d124b200ddabed": "Maximum number of context tokens the model can accept in a single request. Leave blank to use the default.",
|
"15d124b200ddabed": "Maximum number of context tokens the model can accept in a single request. Leave blank to use the default.",
|
||||||
"185aebe19c77425d": "{0} must be a JSON object",
|
"185aebe19c77425d": "{0} must be a JSON object",
|
||||||
"18b7312022cd1840": "Start Service",
|
"18b7312022cd1840": "Start Service",
|
||||||
|
"1afed6a81a2512d2": "Select model",
|
||||||
"1baddde657dd2720": "Current outbound requests use system proxy",
|
"1baddde657dd2720": "Current outbound requests use system proxy",
|
||||||
"1bc77f5ab979f4c1": "Add Model Settings",
|
"1bc77f5ab979f4c1": "Add Model Settings",
|
||||||
"1c631615c1d85c9e": "Log in to Cursor",
|
"1c631615c1d85c9e": "Log in to Cursor",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"281eb6d08c9960d0": "{0} thinking budget token must be a positive integer",
|
"281eb6d08c9960d0": "{0} thinking budget token must be a positive integer",
|
||||||
"28aeffc70ceb4267": "Change the display language for this interface. The setting takes effect immediately and is saved on this device.",
|
"28aeffc70ceb4267": "Change the display language for this interface. The setting takes effect immediately and is saved on this device.",
|
||||||
"2a24519398684ed5": "Visit Homepage",
|
"2a24519398684ed5": "Visit Homepage",
|
||||||
|
"2c18d5e2d70b45db": "Model prefix",
|
||||||
"2cd0f3be8738a86c": "Cancel",
|
"2cd0f3be8738a86c": "Cancel",
|
||||||
"2d706f7981b45a7b": "Local settings saved",
|
"2d706f7981b45a7b": "Local settings saved",
|
||||||
"2f9daa828907b93f": "Delete",
|
"2f9daa828907b93f": "Delete",
|
||||||
@@ -44,8 +46,10 @@
|
|||||||
"37d23612f78a2e63": "Restart Now to Update",
|
"37d23612f78a2e63": "Restart Now to Update",
|
||||||
"392d0dceb45998d3": "Extreme",
|
"392d0dceb45998d3": "Extreme",
|
||||||
"393df9bb13ea4900": "Hit",
|
"393df9bb13ea4900": "Hit",
|
||||||
|
"3a5040b68abf75f9": "Select all",
|
||||||
"3ab8cc15939f3b5c": "Log out",
|
"3ab8cc15939f3b5c": "Log out",
|
||||||
"3af7e5489e61ea51": "Refreshing",
|
"3af7e5489e61ea51": "Refreshing",
|
||||||
|
"3b2e5f2fba1bcbc7": "Enter the API address and access key",
|
||||||
"3c2a9f9901109e75": "{0} type only supports OpenAI or Anthropic",
|
"3c2a9f9901109e75": "{0} type only supports OpenAI or Anthropic",
|
||||||
"3d13868593ae4eeb": "Interface Language",
|
"3d13868593ae4eeb": "Interface Language",
|
||||||
"3d52574ce1500561": "Not connected",
|
"3d52574ce1500561": "Not connected",
|
||||||
@@ -69,6 +73,7 @@
|
|||||||
"58c6b0935a7216da": "Failed to open contributor profile",
|
"58c6b0935a7216da": "Failed to open contributor profile",
|
||||||
"593a972852ba0004": "Cursor Assistant | Permanently Free | Custom API",
|
"593a972852ba0004": "Cursor Assistant | Permanently Free | Custom API",
|
||||||
"59a2195a01a8b35b": "{0} must be a valid JSON object",
|
"59a2195a01a8b35b": "{0} must be a valid JSON object",
|
||||||
|
"5a5c8318ef649672": "{0} selected",
|
||||||
"5aa8f5590c940829": "Non-cache Input: {0}",
|
"5aa8f5590c940829": "Non-cache Input: {0}",
|
||||||
"5beb1206c532729f": "Maximum number of tokens allowed in a single response. Leave blank to use the default.",
|
"5beb1206c532729f": "Maximum number of tokens allowed in a single response. Leave blank to use the default.",
|
||||||
"5d1687a4a41883fd": "Stopping...",
|
"5d1687a4a41883fd": "Stopping...",
|
||||||
@@ -95,8 +100,10 @@
|
|||||||
"737225e2904673fc": "Estimated output tokens: {0}",
|
"737225e2904673fc": "Estimated output tokens: {0}",
|
||||||
"7520bd50a5ee5471": "Stop testing {0}/{1}",
|
"7520bd50a5ee5471": "Stop testing {0}/{1}",
|
||||||
"753d8bb0da9913ce": "Duplication failed",
|
"753d8bb0da9913ce": "Duplication failed",
|
||||||
|
"75b5f4c68c79322c": "Select at least one model to save",
|
||||||
"774d6e1b7cb89751": "Default Definition",
|
"774d6e1b7cb89751": "Default Definition",
|
||||||
"77c9e582e85583af": "Test failed",
|
"77c9e582e85583af": "Test failed",
|
||||||
|
"7923d007483ae04c": "No models to save",
|
||||||
"7a26bf794e9fb6bf": "Used only for display in the UI, so you can distinguish different models.",
|
"7a26bf794e9fb6bf": "Used only for display in the UI, so you can distinguish different models.",
|
||||||
"7b6187c41e88b70c": "Testing...",
|
"7b6187c41e88b70c": "Testing...",
|
||||||
"7bf8e2c07e084d09": "Model Editor",
|
"7bf8e2c07e084d09": "Model Editor",
|
||||||
@@ -106,15 +113,16 @@
|
|||||||
"80296f4aa3f4543b": "Cache Read/Write",
|
"80296f4aa3f4543b": "Cache Read/Write",
|
||||||
"81123c56d5d880d0": "API Key",
|
"81123c56d5d880d0": "API Key",
|
||||||
"8139cb3dd11f5a67": "When enabled, the JSON object will override the final request headers. Duplicate headers are determined by this field, and values must be strings.",
|
"8139cb3dd11f5a67": "When enabled, the JSON object will override the final request headers. Duplicate headers are determined by this field, and values must be strings.",
|
||||||
|
"826c0e5a4407befa": "You can select multiple models. Only selected models will be saved, with one configuration generated for each model.",
|
||||||
"83be9cac28873059": "Cursor Control Plane Account",
|
"83be9cac28873059": "Cursor Control Plane Account",
|
||||||
"8672864e90417138": "Max",
|
"8672864e90417138": "Max",
|
||||||
"86df7ec743047234": "Service running",
|
"86df7ec743047234": "Service running",
|
||||||
|
"891bfee3bbe52d3c": "Enter a model ID",
|
||||||
"899add6275682210": "Uses 200000 by default when left blank",
|
"899add6275682210": "Uses 200000 by default when left blank",
|
||||||
"8a4ef3e48e4e8a5a": "Enabled",
|
"8a4ef3e48e4e8a5a": "Enabled",
|
||||||
"8c1935935600e336": "Model Test",
|
"8c1935935600e336": "Model Test",
|
||||||
"8cbcf741e727dbf7": "Model Settings",
|
"8cbcf741e727dbf7": "Model Settings",
|
||||||
"8d1de152be6360ce": "Valid ratio: {0}",
|
"8d1de152be6360ce": "Valid ratio: {0}",
|
||||||
"8e2dc7b0d2e8f6f8": "e.g. OpenAI - GPT-4.1",
|
|
||||||
"8f6f8d979c981ced": "Copied",
|
"8f6f8d979c981ced": "Copied",
|
||||||
"8f8baf5d18dd0492": "When enabled, the JSON object will override the OpenAI request body. Duplicate fields are determined by this field. OpenAI service_tier supports auto, default, flex, scale, priority; priority can be used for high-priority/Fast scenarios.",
|
"8f8baf5d18dd0492": "When enabled, the JSON object will override the OpenAI request body. Duplicate fields are determined by this field. OpenAI service_tier supports auto, default, flex, scale, priority; priority can be used for high-priority/Fast scenarios.",
|
||||||
"8faa670b512b6b9b": "Open Model Settings",
|
"8faa670b512b6b9b": "Open Model Settings",
|
||||||
@@ -160,6 +168,7 @@
|
|||||||
"b571037dc396a00c": "Total request tokens include both prompt and model output.",
|
"b571037dc396a00c": "Total request tokens include both prompt and model output.",
|
||||||
"b765005f69fa971f": "e.g. gpt-4.1",
|
"b765005f69fa971f": "e.g. gpt-4.1",
|
||||||
"b76a22622020f849": "Select interface protocol endpoint. When selecting 'Custom Path', please enter the complete request URL in the API address bar (including the /chat/completions or /responses suffix). The system will automatically detect the protocol type based on the trailing segment.",
|
"b76a22622020f849": "Select interface protocol endpoint. When selecting 'Custom Path', please enter the complete request URL in the API address bar (including the /chat/completions or /responses suffix). The system will automatically detect the protocol type based on the trailing segment.",
|
||||||
|
"b7ceef2fbfeb4a85": "e.g. GPT-5",
|
||||||
"b870928f8f9a24c4": "API Endpoint",
|
"b870928f8f9a24c4": "API Endpoint",
|
||||||
"b90a8ac9c488ce46": "Select language",
|
"b90a8ac9c488ce46": "Select language",
|
||||||
"ba3c66f90fd11725": "System proxy identified",
|
"ba3c66f90fd11725": "System proxy identified",
|
||||||
@@ -173,6 +182,7 @@
|
|||||||
"c3d46b387eeadb23": "This only logs the Cursor account out of cursor-byok; it does not log out of the Cursor client. Continue?",
|
"c3d46b387eeadb23": "This only logs the Cursor account out of cursor-byok; it does not log out of the Cursor client. Continue?",
|
||||||
"c5af02060847d167": "Thinking effort for Anthropic adaptive thinking. Requests will consistently use the new thinking.type=adaptive.",
|
"c5af02060847d167": "Thinking effort for Anthropic adaptive thinking. Requests will consistently use the new thinking.type=adaptive.",
|
||||||
"c69f5bce63b9f14c": "Settings Folder",
|
"c69f5bce63b9f14c": "Settings Folder",
|
||||||
|
"c72d5dc20cd27118": "{0} / {1} models selected",
|
||||||
"c8a52b66651d294c": "Failed to log out",
|
"c8a52b66651d294c": "Failed to log out",
|
||||||
"c8c14507b2d37395": "Reasoning Effort",
|
"c8c14507b2d37395": "Reasoning Effort",
|
||||||
"c98e118e0a43f078": "Model",
|
"c98e118e0a43f078": "Model",
|
||||||
@@ -206,6 +216,7 @@
|
|||||||
"e4c0daa3c4bea691": "Thanks to @aike0210 for contributing the Cursor control-plane account feature.",
|
"e4c0daa3c4bea691": "Thanks to @aike0210 for contributing the Cursor control-plane account feature.",
|
||||||
"e53580f8031f13c0": "Complete login in the browser, then return to Cursor and reopen the plugin marketplace",
|
"e53580f8031f13c0": "Complete login in the browser, then return to Cursor and reopen the plugin marketplace",
|
||||||
"e552c2accdbf5178": "Add Model",
|
"e552c2accdbf5178": "Add Model",
|
||||||
|
"e6943d5cbfb863e0": "Fetching models...",
|
||||||
"e6faccfddce722e8": "Cache read tokens: {0}",
|
"e6faccfddce722e8": "Cache read tokens: {0}",
|
||||||
"e8a0a6053998ebfa": "Logged in",
|
"e8a0a6053998ebfa": "Logged in",
|
||||||
"eaffd48cd2ea9f1a": "e.g. https://api.anthropic.com",
|
"eaffd48cd2ea9f1a": "e.g. https://api.anthropic.com",
|
||||||
@@ -218,6 +229,7 @@
|
|||||||
"f3a76d896853c1df": "Miss",
|
"f3a76d896853c1df": "Miss",
|
||||||
"f3fae6cccb9004b1": "Custom header name cannot be empty",
|
"f3fae6cccb9004b1": "Custom header name cannot be empty",
|
||||||
"f474a4108aba4c4c": "Stop Service",
|
"f474a4108aba4c4c": "Stop Service",
|
||||||
|
"f4d4bae588c4c0ff": "Deselect all",
|
||||||
"f4f0ead1116b5b62": "Enable",
|
"f4f0ead1116b5b62": "Enable",
|
||||||
"f56c6c82203b33f6": "Notice",
|
"f56c6c82203b33f6": "Notice",
|
||||||
"f61e03f047b786d5": "{0} max output tokens must be a positive integer",
|
"f61e03f047b786d5": "{0} max output tokens must be a positive integer",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"15d124b200ddabed": "モデルが1回のリクエストで受け取れる最大コンテキスト Token 数。空欄の場合はデフォルト値を使用します。",
|
"15d124b200ddabed": "モデルが1回のリクエストで受け取れる最大コンテキスト Token 数。空欄の場合はデフォルト値を使用します。",
|
||||||
"185aebe19c77425d": "{0}はJSONオブジェクトである必要があります",
|
"185aebe19c77425d": "{0}はJSONオブジェクトである必要があります",
|
||||||
"18b7312022cd1840": "サービスを開始",
|
"18b7312022cd1840": "サービスを開始",
|
||||||
|
"1afed6a81a2512d2": "モデルを選択",
|
||||||
"1baddde657dd2720": "現在のアウトバウンドリクエストはシステムプロキシを使用しています",
|
"1baddde657dd2720": "現在のアウトバウンドリクエストはシステムプロキシを使用しています",
|
||||||
"1bc77f5ab979f4c1": "モデル設定を追加",
|
"1bc77f5ab979f4c1": "モデル設定を追加",
|
||||||
"1c631615c1d85c9e": "Cursor にログイン",
|
"1c631615c1d85c9e": "Cursor にログイン",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"281eb6d08c9960d0": "{0} の思考予算 Token は正の整数である必要があります",
|
"281eb6d08c9960d0": "{0} の思考予算 Token は正の整数である必要があります",
|
||||||
"28aeffc70ceb4267": "この画面の表示言語を切り替えます。設定はすぐに反映され、この端末に保存されます",
|
"28aeffc70ceb4267": "この画面の表示言語を切り替えます。設定はすぐに反映され、この端末に保存されます",
|
||||||
"2a24519398684ed5": "ホームページへ",
|
"2a24519398684ed5": "ホームページへ",
|
||||||
|
"2c18d5e2d70b45db": "モデルのプレフィックス",
|
||||||
"2cd0f3be8738a86c": "キャンセル",
|
"2cd0f3be8738a86c": "キャンセル",
|
||||||
"2d706f7981b45a7b": "ローカル設定を保存しました",
|
"2d706f7981b45a7b": "ローカル設定を保存しました",
|
||||||
"2f9daa828907b93f": "削除",
|
"2f9daa828907b93f": "削除",
|
||||||
@@ -44,8 +46,10 @@
|
|||||||
"37d23612f78a2e63": "今すぐ再起動して更新",
|
"37d23612f78a2e63": "今すぐ再起動して更新",
|
||||||
"392d0dceb45998d3": "最高",
|
"392d0dceb45998d3": "最高",
|
||||||
"393df9bb13ea4900": "ヒット",
|
"393df9bb13ea4900": "ヒット",
|
||||||
|
"3a5040b68abf75f9": "すべて選択",
|
||||||
"3ab8cc15939f3b5c": "ログアウト",
|
"3ab8cc15939f3b5c": "ログアウト",
|
||||||
"3af7e5489e61ea51": "更新中",
|
"3af7e5489e61ea51": "更新中",
|
||||||
|
"3b2e5f2fba1bcbc7": "インターフェースのアドレスとアクセスキーを入力してください",
|
||||||
"3c2a9f9901109e75": "{0} のタイプは OpenAI または Anthropic のみサポートします",
|
"3c2a9f9901109e75": "{0} のタイプは OpenAI または Anthropic のみサポートします",
|
||||||
"3d13868593ae4eeb": "表示言語",
|
"3d13868593ae4eeb": "表示言語",
|
||||||
"3d52574ce1500561": "未接続",
|
"3d52574ce1500561": "未接続",
|
||||||
@@ -69,6 +73,7 @@
|
|||||||
"58c6b0935a7216da": "コントリビューターのプロフィールを開けませんでした",
|
"58c6b0935a7216da": "コントリビューターのプロフィールを開けませんでした",
|
||||||
"593a972852ba0004": "Cursor アシスタント | 永久無料 | カスタム API",
|
"593a972852ba0004": "Cursor アシスタント | 永久無料 | カスタム API",
|
||||||
"59a2195a01a8b35b": "{0}は有効なJSONオブジェクトである必要があります",
|
"59a2195a01a8b35b": "{0}は有効なJSONオブジェクトである必要があります",
|
||||||
|
"5a5c8318ef649672": "{0}件を選択中",
|
||||||
"5aa8f5590c940829": "非キャッシュ入力:{0}",
|
"5aa8f5590c940829": "非キャッシュ入力:{0}",
|
||||||
"5beb1206c532729f": "1回の応答で生成できる最大 Token 数。空欄の場合はデフォルト値を使用します。",
|
"5beb1206c532729f": "1回の応答で生成できる最大 Token 数。空欄の場合はデフォルト値を使用します。",
|
||||||
"5d1687a4a41883fd": "停止中...",
|
"5d1687a4a41883fd": "停止中...",
|
||||||
@@ -95,9 +100,11 @@
|
|||||||
"737225e2904673fc": "推定出力 Token: {0}",
|
"737225e2904673fc": "推定出力 Token: {0}",
|
||||||
"7520bd50a5ee5471": "テスト停止 {0}/{1}",
|
"7520bd50a5ee5471": "テスト停止 {0}/{1}",
|
||||||
"753d8bb0da9913ce": "複製に失敗しました",
|
"753d8bb0da9913ce": "複製に失敗しました",
|
||||||
|
"75b5f4c68c79322c": "保存するモデルを1つ以上選択してください",
|
||||||
"774d6e1b7cb89751": "デフォルト定義",
|
"774d6e1b7cb89751": "デフォルト定義",
|
||||||
"77c9e582e85583af": "テスト失敗",
|
"77c9e582e85583af": "テスト失敗",
|
||||||
"7a26bf794e9fb6bf": "UI 上の表示専用で、異なるモデルを見分けやすくします。",
|
"7923d007483ae04c": "保存できるモデルがありません",
|
||||||
|
"7a26bf794e9fb6bf": "UIでモデルを区別するための表示専用です。",
|
||||||
"7b6187c41e88b70c": "テスト中...",
|
"7b6187c41e88b70c": "テスト中...",
|
||||||
"7bf8e2c07e084d09": "モデル編集",
|
"7bf8e2c07e084d09": "モデル編集",
|
||||||
"7df7641e5e741346": "キャッシュ読み取り / (キャッシュ読み取り + キャッシュ作成 + 非キャッシュ入力)",
|
"7df7641e5e741346": "キャッシュ読み取り / (キャッシュ読み取り + キャッシュ作成 + 非キャッシュ入力)",
|
||||||
@@ -106,15 +113,16 @@
|
|||||||
"80296f4aa3f4543b": "キャッシュ読み書き",
|
"80296f4aa3f4543b": "キャッシュ読み書き",
|
||||||
"81123c56d5d880d0": "API キー",
|
"81123c56d5d880d0": "API キー",
|
||||||
"8139cb3dd11f5a67": "有効にすると、JSONオブジェクトが最終的なリクエストヘッダーを上書きします。同名のヘッダーはこの設定が優先され、値は文字列である必要があります。",
|
"8139cb3dd11f5a67": "有効にすると、JSONオブジェクトが最終的なリクエストヘッダーを上書きします。同名のヘッダーはこの設定が優先され、値は文字列である必要があります。",
|
||||||
|
"826c0e5a4407befa": "複数のモデルを選択できます。保存されるのは選択したモデルだけで、モデルごとに1つの設定が作成されます。",
|
||||||
"83be9cac28873059": "Cursor コントロールプレーンアカウント",
|
"83be9cac28873059": "Cursor コントロールプレーンアカウント",
|
||||||
"8672864e90417138": "最大",
|
"8672864e90417138": "最大",
|
||||||
"86df7ec743047234": "サービス稼働中",
|
"86df7ec743047234": "サービス稼働中",
|
||||||
|
"891bfee3bbe52d3c": "モデルIDを入力してください",
|
||||||
"899add6275682210": "空欄で 200000",
|
"899add6275682210": "空欄で 200000",
|
||||||
"8a4ef3e48e4e8a5a": "有効",
|
"8a4ef3e48e4e8a5a": "有効",
|
||||||
"8c1935935600e336": "モデルテスト",
|
"8c1935935600e336": "モデルテスト",
|
||||||
"8cbcf741e727dbf7": "モデル設定",
|
"8cbcf741e727dbf7": "モデル設定",
|
||||||
"8d1de152be6360ce": "有効率: {0}",
|
"8d1de152be6360ce": "有効率: {0}",
|
||||||
"8e2dc7b0d2e8f6f8": "例: OpenAI - GPT-4.1",
|
|
||||||
"8f6f8d979c981ced": "コピーしました",
|
"8f6f8d979c981ced": "コピーしました",
|
||||||
"8f8baf5d18dd0492": "有効にすると、JSONオブジェクトがOpenAIのリクエストボディを上書きします。同名のフィールドはこの設定が優先されます。OpenAIのservice_tierはauto、default、flex、scale、priorityをサポートしており、priorityは高優先度/Fastのシナリオで使用できます。",
|
"8f8baf5d18dd0492": "有効にすると、JSONオブジェクトがOpenAIのリクエストボディを上書きします。同名のフィールドはこの設定が優先されます。OpenAIのservice_tierはauto、default、flex、scale、priorityをサポートしており、priorityは高優先度/Fastのシナリオで使用できます。",
|
||||||
"8faa670b512b6b9b": "モデル設定を開く",
|
"8faa670b512b6b9b": "モデル設定を開く",
|
||||||
@@ -122,8 +130,8 @@
|
|||||||
"917b1c1f18d0276b": "保存中...",
|
"917b1c1f18d0276b": "保存中...",
|
||||||
"9196835e388d2550": "すべてテスト",
|
"9196835e388d2550": "すべてテスト",
|
||||||
"91cba5c107a51892": "/ 異常",
|
"91cba5c107a51892": "/ 異常",
|
||||||
"92059fe6cd713db4": "実際にサーバーへ送信されるモデル名です。例: gpt-4.1 または claude-sonnet。",
|
"92059fe6cd713db4": "サーバーに実際に送信されるモデル名。例: gpt-4.1、claude-sonnet。",
|
||||||
"93e08803675e378b": "モデル ID",
|
"93e08803675e378b": "モデルID",
|
||||||
"93faf55cd25c8319": "このソフトウェアは完全に無料です。もし料金を請求された場合は、詐欺の可能性が高いです。\n著者のホームページ https://space.bilibili.com/311706663/upload/video にアクセスして、更新情報や利用方法などを確認してください。",
|
"93faf55cd25c8319": "このソフトウェアは完全に無料です。もし料金を請求された場合は、詐欺の可能性が高いです。\n著者のホームページ https://space.bilibili.com/311706663/upload/video にアクセスして、更新情報や利用方法などを確認してください。",
|
||||||
"942ff2d88baca0c6": "アップデートを確認中...",
|
"942ff2d88baca0c6": "アップデートを確認中...",
|
||||||
"970388573a3c88c9": "キャッシュ読み取り:{0} × ${1}/1M = {2}",
|
"970388573a3c88c9": "キャッシュ読み取り:{0} × ${1}/1M = {2}",
|
||||||
@@ -160,6 +168,7 @@
|
|||||||
"b571037dc396a00c": "総リクエスト Token には Prompt とモデル出力の両方が含まれます。",
|
"b571037dc396a00c": "総リクエスト Token には Prompt とモデル出力の両方が含まれます。",
|
||||||
"b765005f69fa971f": "例: gpt-4.1",
|
"b765005f69fa971f": "例: gpt-4.1",
|
||||||
"b76a22622020f849": "インターフェースプロトコルのエンドポイントを選択します。「カスタムパス」を選択する場合は、APIアドレスバーに完全なリクエストURL(/chat/completions または /responses のサフィックスを含む)を入力してください。システムは末尾 of セグメントに基づいてプロトコルタイプを自動的に判断します。",
|
"b76a22622020f849": "インターフェースプロトコルのエンドポイントを選択します。「カスタムパス」を選択する場合は、APIアドレスバーに完全なリクエストURL(/chat/completions または /responses のサフィックスを含む)を入力してください。システムは末尾 of セグメントに基づいてプロトコルタイプを自動的に判断します。",
|
||||||
|
"b7ceef2fbfeb4a85": "例: GPT-5",
|
||||||
"b870928f8f9a24c4": "APIエンドポイント",
|
"b870928f8f9a24c4": "APIエンドポイント",
|
||||||
"b90a8ac9c488ce46": "言語を選択",
|
"b90a8ac9c488ce46": "言語を選択",
|
||||||
"ba3c66f90fd11725": "システムプロキシを認識しました",
|
"ba3c66f90fd11725": "システムプロキシを認識しました",
|
||||||
@@ -173,6 +182,7 @@
|
|||||||
"c3d46b387eeadb23": "cursor-byok 内の Cursor アカウントからのみログアウトします。Cursor クライアントからはログアウトしません。続行しますか?",
|
"c3d46b387eeadb23": "cursor-byok 内の Cursor アカウントからのみログアウトします。Cursor クライアントからはログアウトしません。続行しますか?",
|
||||||
"c5af02060847d167": "Anthropic adaptive thinkingの思考強度。リクエストは一貫して新しいthinking.type=adaptiveを使用します。",
|
"c5af02060847d167": "Anthropic adaptive thinkingの思考強度。リクエストは一貫して新しいthinking.type=adaptiveを使用します。",
|
||||||
"c69f5bce63b9f14c": "設定フォルダー",
|
"c69f5bce63b9f14c": "設定フォルダー",
|
||||||
|
"c72d5dc20cd27118": "{0} / {1} モデルを選択中",
|
||||||
"c8a52b66651d294c": "ログアウトに失敗しました",
|
"c8a52b66651d294c": "ログアウトに失敗しました",
|
||||||
"c8c14507b2d37395": "推論強度",
|
"c8c14507b2d37395": "推論強度",
|
||||||
"c98e118e0a43f078": "モデル",
|
"c98e118e0a43f078": "モデル",
|
||||||
@@ -206,6 +216,7 @@
|
|||||||
"e4c0daa3c4bea691": "Cursor コントロールプレーンアカウント機能への @aike0210 の貢献に感謝します。",
|
"e4c0daa3c4bea691": "Cursor コントロールプレーンアカウント機能への @aike0210 の貢献に感謝します。",
|
||||||
"e53580f8031f13c0": "ブラウザでログインを完了し、Cursor に戻ってプラグインマーケットを開き直してください",
|
"e53580f8031f13c0": "ブラウザでログインを完了し、Cursor に戻ってプラグインマーケットを開き直してください",
|
||||||
"e552c2accdbf5178": "モデルを追加",
|
"e552c2accdbf5178": "モデルを追加",
|
||||||
|
"e6943d5cbfb863e0": "モデルを取得中...",
|
||||||
"e6faccfddce722e8": "キャッシュ読込 Token: {0}",
|
"e6faccfddce722e8": "キャッシュ読込 Token: {0}",
|
||||||
"e8a0a6053998ebfa": "ログイン済み",
|
"e8a0a6053998ebfa": "ログイン済み",
|
||||||
"eaffd48cd2ea9f1a": "例: https://api.anthropic.com",
|
"eaffd48cd2ea9f1a": "例: https://api.anthropic.com",
|
||||||
@@ -218,6 +229,7 @@
|
|||||||
"f3a76d896853c1df": "ミス",
|
"f3a76d896853c1df": "ミス",
|
||||||
"f3fae6cccb9004b1": "カスタムヘッダー名は空にできません",
|
"f3fae6cccb9004b1": "カスタムヘッダー名は空にできません",
|
||||||
"f474a4108aba4c4c": "サービスを停止",
|
"f474a4108aba4c4c": "サービスを停止",
|
||||||
|
"f4d4bae588c4c0ff": "すべて選択解除",
|
||||||
"f4f0ead1116b5b62": "有効化",
|
"f4f0ead1116b5b62": "有効化",
|
||||||
"f56c6c82203b33f6": "お知らせ",
|
"f56c6c82203b33f6": "お知らせ",
|
||||||
"f61e03f047b786d5": "{0} の最大出力 Token は正の整数である必要があります",
|
"f61e03f047b786d5": "{0} の最大出力 Token は正の整数である必要があります",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"15d124b200ddabed": "Максимальное число токенов контекста, которое модель может принять за один запрос. Оставьте поле пустым для значения по умолчанию.",
|
"15d124b200ddabed": "Максимальное число токенов контекста, которое модель может принять за один запрос. Оставьте поле пустым для значения по умолчанию.",
|
||||||
"185aebe19c77425d": "{0} должен быть объектом JSON",
|
"185aebe19c77425d": "{0} должен быть объектом JSON",
|
||||||
"18b7312022cd1840": "Запустить сервис",
|
"18b7312022cd1840": "Запустить сервис",
|
||||||
|
"1afed6a81a2512d2": "Выберите модель",
|
||||||
"1baddde657dd2720": "Исходящие запросы используют системный прокси",
|
"1baddde657dd2720": "Исходящие запросы используют системный прокси",
|
||||||
"1bc77f5ab979f4c1": "Добавить настройки модели",
|
"1bc77f5ab979f4c1": "Добавить настройки модели",
|
||||||
"1c631615c1d85c9e": "Войти в Cursor",
|
"1c631615c1d85c9e": "Войти в Cursor",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"281eb6d08c9960d0": "Бюджет токенов рассуждения {0} должен быть положительным целым числом",
|
"281eb6d08c9960d0": "Бюджет токенов рассуждения {0} должен быть положительным целым числом",
|
||||||
"28aeffc70ceb4267": "Измените язык интерфейса. Настройка применяется сразу и сохраняется на этом устройстве.",
|
"28aeffc70ceb4267": "Измените язык интерфейса. Настройка применяется сразу и сохраняется на этом устройстве.",
|
||||||
"2a24519398684ed5": "Перейти на домашнюю страницу",
|
"2a24519398684ed5": "Перейти на домашнюю страницу",
|
||||||
|
"2c18d5e2d70b45db": "Префикс модели",
|
||||||
"2cd0f3be8738a86c": "Отмена",
|
"2cd0f3be8738a86c": "Отмена",
|
||||||
"2d706f7981b45a7b": "Локальные настройки сохранены",
|
"2d706f7981b45a7b": "Локальные настройки сохранены",
|
||||||
"2f9daa828907b93f": "Удалить",
|
"2f9daa828907b93f": "Удалить",
|
||||||
@@ -44,8 +46,10 @@
|
|||||||
"37d23612f78a2e63": "Перезапустить и обновить",
|
"37d23612f78a2e63": "Перезапустить и обновить",
|
||||||
"392d0dceb45998d3": "Очень высокая",
|
"392d0dceb45998d3": "Очень высокая",
|
||||||
"393df9bb13ea4900": "Попадание",
|
"393df9bb13ea4900": "Попадание",
|
||||||
|
"3a5040b68abf75f9": "Выбрать все",
|
||||||
"3ab8cc15939f3b5c": "Выйти",
|
"3ab8cc15939f3b5c": "Выйти",
|
||||||
"3af7e5489e61ea51": "Обновление",
|
"3af7e5489e61ea51": "Обновление",
|
||||||
|
"3b2e5f2fba1bcbc7": "Введите адрес интерфейса и ключ доступа",
|
||||||
"3c2a9f9901109e75": "Тип {0} поддерживает только OpenAI или Anthropic",
|
"3c2a9f9901109e75": "Тип {0} поддерживает только OpenAI или Anthropic",
|
||||||
"3d13868593ae4eeb": "Язык интерфейса",
|
"3d13868593ae4eeb": "Язык интерфейса",
|
||||||
"3d52574ce1500561": "Не подключено",
|
"3d52574ce1500561": "Не подключено",
|
||||||
@@ -69,6 +73,7 @@
|
|||||||
"58c6b0935a7216da": "Не удалось открыть профиль участника",
|
"58c6b0935a7216da": "Не удалось открыть профиль участника",
|
||||||
"593a972852ba0004": "Cursor Assistant | Всегда бесплатно | Пользовательский API",
|
"593a972852ba0004": "Cursor Assistant | Всегда бесплатно | Пользовательский API",
|
||||||
"59a2195a01a8b35b": "{0} должен быть допустимым объектом JSON",
|
"59a2195a01a8b35b": "{0} должен быть допустимым объектом JSON",
|
||||||
|
"5a5c8318ef649672": "Выбрано: {0}",
|
||||||
"5aa8f5590c940829": "Ввод без кеша: {0}",
|
"5aa8f5590c940829": "Ввод без кеша: {0}",
|
||||||
"5beb1206c532729f": "Максимальное число токенов в одном ответе. Оставьте поле пустым для значения по умолчанию.",
|
"5beb1206c532729f": "Максимальное число токенов в одном ответе. Оставьте поле пустым для значения по умолчанию.",
|
||||||
"5d1687a4a41883fd": "Остановка...",
|
"5d1687a4a41883fd": "Остановка...",
|
||||||
@@ -95,9 +100,11 @@
|
|||||||
"737225e2904673fc": "Расчетные выходные токены: {0}",
|
"737225e2904673fc": "Расчетные выходные токены: {0}",
|
||||||
"7520bd50a5ee5471": "Остановить проверку {0}/{1}",
|
"7520bd50a5ee5471": "Остановить проверку {0}/{1}",
|
||||||
"753d8bb0da9913ce": "Не удалось дублировать",
|
"753d8bb0da9913ce": "Не удалось дублировать",
|
||||||
|
"75b5f4c68c79322c": "Выберите хотя бы одну модель для сохранения",
|
||||||
"774d6e1b7cb89751": "Стандартный расчет",
|
"774d6e1b7cb89751": "Стандартный расчет",
|
||||||
"77c9e582e85583af": "Проверка не пройдена",
|
"77c9e582e85583af": "Проверка не пройдена",
|
||||||
"7a26bf794e9fb6bf": "Используется только для отображения в интерфейсе и помогает различать модели.",
|
"7923d007483ae04c": "Нет моделей для сохранения",
|
||||||
|
"7a26bf794e9fb6bf": "Используется только для отображения в интерфейсе, чтобы различать модели.",
|
||||||
"7b6187c41e88b70c": "Проверка...",
|
"7b6187c41e88b70c": "Проверка...",
|
||||||
"7bf8e2c07e084d09": "Редактор модели",
|
"7bf8e2c07e084d09": "Редактор модели",
|
||||||
"7df7641e5e741346": "Чтение кеша / (Чтение кеша + Создание кеша + Ввод без кеша)",
|
"7df7641e5e741346": "Чтение кеша / (Чтение кеша + Создание кеша + Ввод без кеша)",
|
||||||
@@ -106,15 +113,16 @@
|
|||||||
"80296f4aa3f4543b": "Чтение/запись кеша",
|
"80296f4aa3f4543b": "Чтение/запись кеша",
|
||||||
"81123c56d5d880d0": "Ключ API",
|
"81123c56d5d880d0": "Ключ API",
|
||||||
"8139cb3dd11f5a67": "Если включено, объект JSON переопределит итоговые заголовки запроса. При совпадении имен используются значения отсюда; все значения должны быть строками.",
|
"8139cb3dd11f5a67": "Если включено, объект JSON переопределит итоговые заголовки запроса. При совпадении имен используются значения отсюда; все значения должны быть строками.",
|
||||||
|
"826c0e5a4407befa": "Можно выбрать несколько моделей. Будут сохранены только выбранные модели, для каждой будет создана отдельная конфигурация.",
|
||||||
"83be9cac28873059": "Аккаунт управляющего уровня Cursor",
|
"83be9cac28873059": "Аккаунт управляющего уровня Cursor",
|
||||||
"8672864e90417138": "Максимальная",
|
"8672864e90417138": "Максимальная",
|
||||||
"86df7ec743047234": "Сервис запущен",
|
"86df7ec743047234": "Сервис запущен",
|
||||||
|
"891bfee3bbe52d3c": "Введите идентификатор модели",
|
||||||
"899add6275682210": "Если оставить пустым, используется 200000",
|
"899add6275682210": "Если оставить пустым, используется 200000",
|
||||||
"8a4ef3e48e4e8a5a": "Включено",
|
"8a4ef3e48e4e8a5a": "Включено",
|
||||||
"8c1935935600e336": "Проверка модели",
|
"8c1935935600e336": "Проверка модели",
|
||||||
"8cbcf741e727dbf7": "Настройки модели",
|
"8cbcf741e727dbf7": "Настройки модели",
|
||||||
"8d1de152be6360ce": "Доля успешных: {0}",
|
"8d1de152be6360ce": "Доля успешных: {0}",
|
||||||
"8e2dc7b0d2e8f6f8": "например, OpenAI - GPT-4.1",
|
|
||||||
"8f6f8d979c981ced": "Скопировано",
|
"8f6f8d979c981ced": "Скопировано",
|
||||||
"8f8baf5d18dd0492": "Если включено, объект JSON переопределит тело запроса OpenAI. При совпадении полей используются значения отсюда. OpenAI service_tier поддерживает auto, default, flex, scale и priority; priority можно использовать для сценариев с высоким приоритетом/Fast.",
|
"8f8baf5d18dd0492": "Если включено, объект JSON переопределит тело запроса OpenAI. При совпадении полей используются значения отсюда. OpenAI service_tier поддерживает auto, default, flex, scale и priority; priority можно использовать для сценариев с высоким приоритетом/Fast.",
|
||||||
"8faa670b512b6b9b": "Открыть настройки модели",
|
"8faa670b512b6b9b": "Открыть настройки модели",
|
||||||
@@ -122,7 +130,7 @@
|
|||||||
"917b1c1f18d0276b": "Сохранение...",
|
"917b1c1f18d0276b": "Сохранение...",
|
||||||
"9196835e388d2550": "Проверить все",
|
"9196835e388d2550": "Проверить все",
|
||||||
"91cba5c107a51892": "/ Ошибочные",
|
"91cba5c107a51892": "/ Ошибочные",
|
||||||
"92059fe6cd713db4": "Имя модели, которое фактически отправляется серверу, например gpt-4.1 или claude-sonnet.",
|
"92059fe6cd713db4": "Имя модели, фактически отправляемое серверу, например gpt-4.1 или claude-sonnet.",
|
||||||
"93e08803675e378b": "Идентификатор модели",
|
"93e08803675e378b": "Идентификатор модели",
|
||||||
"93faf55cd25c8319": "Это программное обеспечение полностью бесплатно. Если с вас взяли плату, скорее всего, вас обманули.\\nПосетите страницу автора: https://space.bilibili.com/311706663/upload/video\\nТам публикуются обновления, руководства и другие материалы.",
|
"93faf55cd25c8319": "Это программное обеспечение полностью бесплатно. Если с вас взяли плату, скорее всего, вас обманули.\\nПосетите страницу автора: https://space.bilibili.com/311706663/upload/video\\nТам публикуются обновления, руководства и другие материалы.",
|
||||||
"942ff2d88baca0c6": "Проверка обновлений...",
|
"942ff2d88baca0c6": "Проверка обновлений...",
|
||||||
@@ -160,6 +168,7 @@
|
|||||||
"b571037dc396a00c": "Общее число токенов запроса включает Prompt и вывод модели.",
|
"b571037dc396a00c": "Общее число токенов запроса включает Prompt и вывод модели.",
|
||||||
"b765005f69fa971f": "например, gpt-4.1",
|
"b765005f69fa971f": "например, gpt-4.1",
|
||||||
"b76a22622020f849": "Выберите конечную точку протокола. Для варианта «Пользовательский путь» укажите полный URL запроса в поле адреса API, включая суффикс /chat/completions или /responses. Тип протокола будет определен автоматически по последнему сегменту.",
|
"b76a22622020f849": "Выберите конечную точку протокола. Для варианта «Пользовательский путь» укажите полный URL запроса в поле адреса API, включая суффикс /chat/completions или /responses. Тип протокола будет определен автоматически по последнему сегменту.",
|
||||||
|
"b7ceef2fbfeb4a85": "например, GPT-5",
|
||||||
"b870928f8f9a24c4": "Конечная точка API",
|
"b870928f8f9a24c4": "Конечная точка API",
|
||||||
"b90a8ac9c488ce46": "Выберите язык",
|
"b90a8ac9c488ce46": "Выберите язык",
|
||||||
"ba3c66f90fd11725": "Обнаружен системный прокси",
|
"ba3c66f90fd11725": "Обнаружен системный прокси",
|
||||||
@@ -173,6 +182,7 @@
|
|||||||
"c3d46b387eeadb23": "Будет выполнен выход только из аккаунта Cursor в cursor-byok. В клиенте Cursor вы останетесь в системе. Продолжить?",
|
"c3d46b387eeadb23": "Будет выполнен выход только из аккаунта Cursor в cursor-byok. В клиенте Cursor вы останетесь в системе. Продолжить?",
|
||||||
"c5af02060847d167": "Интенсивность для адаптивных рассуждений Anthropic. В запросах всегда используется новый режим thinking.type=adaptive.",
|
"c5af02060847d167": "Интенсивность для адаптивных рассуждений Anthropic. В запросах всегда используется новый режим thinking.type=adaptive.",
|
||||||
"c69f5bce63b9f14c": "Папка настроек",
|
"c69f5bce63b9f14c": "Папка настроек",
|
||||||
|
"c72d5dc20cd27118": "Выбрано моделей: {0} / {1}",
|
||||||
"c8a52b66651d294c": "Не удалось выйти",
|
"c8a52b66651d294c": "Не удалось выйти",
|
||||||
"c8c14507b2d37395": "Интенсивность рассуждений",
|
"c8c14507b2d37395": "Интенсивность рассуждений",
|
||||||
"c98e118e0a43f078": "Модель",
|
"c98e118e0a43f078": "Модель",
|
||||||
@@ -206,6 +216,7 @@
|
|||||||
"e4c0daa3c4bea691": "Спасибо @aike0210 за вклад в функцию аккаунта панели управления Cursor.",
|
"e4c0daa3c4bea691": "Спасибо @aike0210 за вклад в функцию аккаунта панели управления Cursor.",
|
||||||
"e53580f8031f13c0": "Завершите вход в браузере, затем вернитесь в Cursor и снова откройте магазин плагинов",
|
"e53580f8031f13c0": "Завершите вход в браузере, затем вернитесь в Cursor и снова откройте магазин плагинов",
|
||||||
"e552c2accdbf5178": "Добавить модель",
|
"e552c2accdbf5178": "Добавить модель",
|
||||||
|
"e6943d5cbfb863e0": "Получение моделей...",
|
||||||
"e6faccfddce722e8": "Токены чтения из кеша: {0}",
|
"e6faccfddce722e8": "Токены чтения из кеша: {0}",
|
||||||
"e8a0a6053998ebfa": "Выполнен вход",
|
"e8a0a6053998ebfa": "Выполнен вход",
|
||||||
"eaffd48cd2ea9f1a": "например, https://api.anthropic.com",
|
"eaffd48cd2ea9f1a": "например, https://api.anthropic.com",
|
||||||
@@ -218,6 +229,7 @@
|
|||||||
"f3a76d896853c1df": "Промах",
|
"f3a76d896853c1df": "Промах",
|
||||||
"f3fae6cccb9004b1": "Имя пользовательского заголовка не может быть пустым",
|
"f3fae6cccb9004b1": "Имя пользовательского заголовка не может быть пустым",
|
||||||
"f474a4108aba4c4c": "Остановить сервис",
|
"f474a4108aba4c4c": "Остановить сервис",
|
||||||
|
"f4d4bae588c4c0ff": "Снять выделение со всех",
|
||||||
"f4f0ead1116b5b62": "Включить",
|
"f4f0ead1116b5b62": "Включить",
|
||||||
"f56c6c82203b33f6": "Уведомление",
|
"f56c6c82203b33f6": "Уведомление",
|
||||||
"f61e03f047b786d5": "Максимальное число выходных токенов {0} должно быть положительным целым числом",
|
"f61e03f047b786d5": "Максимальное число выходных токенов {0} должно быть положительным целым числом",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"15d124b200ddabed": "模型单次可接受的最大上下文 Token 数。留空时使用默认值。",
|
"15d124b200ddabed": "模型单次可接受的最大上下文 Token 数。留空时使用默认值。",
|
||||||
"185aebe19c77425d": "{0}必须是 JSON 对象",
|
"185aebe19c77425d": "{0}必须是 JSON 对象",
|
||||||
"18b7312022cd1840": "启动服务",
|
"18b7312022cd1840": "启动服务",
|
||||||
|
"1afed6a81a2512d2": "选择模型",
|
||||||
"1baddde657dd2720": "当前出站请求使用系统代理",
|
"1baddde657dd2720": "当前出站请求使用系统代理",
|
||||||
"1bc77f5ab979f4c1": "新增模型配置",
|
"1bc77f5ab979f4c1": "新增模型配置",
|
||||||
"1c631615c1d85c9e": "登录 Cursor",
|
"1c631615c1d85c9e": "登录 Cursor",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"281eb6d08c9960d0": "{0} 的思考预算 Token 必须为正整数",
|
"281eb6d08c9960d0": "{0} 的思考预算 Token 必须为正整数",
|
||||||
"28aeffc70ceb4267": "切换当前界面显示语言,设置会立即生效并保存在本机",
|
"28aeffc70ceb4267": "切换当前界面显示语言,设置会立即生效并保存在本机",
|
||||||
"2a24519398684ed5": "访问主页",
|
"2a24519398684ed5": "访问主页",
|
||||||
|
"2c18d5e2d70b45db": "模型前缀",
|
||||||
"2cd0f3be8738a86c": "取消",
|
"2cd0f3be8738a86c": "取消",
|
||||||
"2d706f7981b45a7b": "本地配置已保存",
|
"2d706f7981b45a7b": "本地配置已保存",
|
||||||
"2f9daa828907b93f": "删除",
|
"2f9daa828907b93f": "删除",
|
||||||
@@ -44,8 +46,10 @@
|
|||||||
"37d23612f78a2e63": "立即重启更新",
|
"37d23612f78a2e63": "立即重启更新",
|
||||||
"392d0dceb45998d3": "极高",
|
"392d0dceb45998d3": "极高",
|
||||||
"393df9bb13ea4900": "命中",
|
"393df9bb13ea4900": "命中",
|
||||||
|
"3a5040b68abf75f9": "全选",
|
||||||
"3ab8cc15939f3b5c": "退出登录",
|
"3ab8cc15939f3b5c": "退出登录",
|
||||||
"3af7e5489e61ea51": "刷新中",
|
"3af7e5489e61ea51": "刷新中",
|
||||||
|
"3b2e5f2fba1bcbc7": "请输入接口地址和访问密钥",
|
||||||
"3c2a9f9901109e75": "{0} 的类型仅支持 OpenAI 或 Anthropic",
|
"3c2a9f9901109e75": "{0} 的类型仅支持 OpenAI 或 Anthropic",
|
||||||
"3d13868593ae4eeb": "界面语言",
|
"3d13868593ae4eeb": "界面语言",
|
||||||
"3d52574ce1500561": "未连接",
|
"3d52574ce1500561": "未连接",
|
||||||
@@ -69,6 +73,7 @@
|
|||||||
"58c6b0935a7216da": "打开贡献者主页失败",
|
"58c6b0935a7216da": "打开贡献者主页失败",
|
||||||
"593a972852ba0004": "Cursor助手|永久免费|自定义API",
|
"593a972852ba0004": "Cursor助手|永久免费|自定义API",
|
||||||
"59a2195a01a8b35b": "{0}必须是合法 JSON 对象",
|
"59a2195a01a8b35b": "{0}必须是合法 JSON 对象",
|
||||||
|
"5a5c8318ef649672": "已选择 {0} 项",
|
||||||
"5aa8f5590c940829": "非缓存输入:{0}",
|
"5aa8f5590c940829": "非缓存输入:{0}",
|
||||||
"5beb1206c532729f": "单次回复允许生成的最大 Token 数。留空时使用默认值。",
|
"5beb1206c532729f": "单次回复允许生成的最大 Token 数。留空时使用默认值。",
|
||||||
"5d1687a4a41883fd": "停止中...",
|
"5d1687a4a41883fd": "停止中...",
|
||||||
@@ -95,8 +100,10 @@
|
|||||||
"737225e2904673fc": "输出推算:{0}",
|
"737225e2904673fc": "输出推算:{0}",
|
||||||
"7520bd50a5ee5471": "停止测试 {0}/{1}",
|
"7520bd50a5ee5471": "停止测试 {0}/{1}",
|
||||||
"753d8bb0da9913ce": "复制失败",
|
"753d8bb0da9913ce": "复制失败",
|
||||||
|
"75b5f4c68c79322c": "请先选择要保存的模型",
|
||||||
"774d6e1b7cb89751": "默认口径",
|
"774d6e1b7cb89751": "默认口径",
|
||||||
"77c9e582e85583af": "测试失败",
|
"77c9e582e85583af": "测试失败",
|
||||||
|
"7923d007483ae04c": "没有可保存的模型",
|
||||||
"7a26bf794e9fb6bf": "仅用于界面展示,便于你区分不同模型。",
|
"7a26bf794e9fb6bf": "仅用于界面展示,便于你区分不同模型。",
|
||||||
"7b6187c41e88b70c": "测试中...",
|
"7b6187c41e88b70c": "测试中...",
|
||||||
"7bf8e2c07e084d09": "模型编辑",
|
"7bf8e2c07e084d09": "模型编辑",
|
||||||
@@ -106,15 +113,16 @@
|
|||||||
"80296f4aa3f4543b": "缓存读写",
|
"80296f4aa3f4543b": "缓存读写",
|
||||||
"81123c56d5d880d0": "访问密钥",
|
"81123c56d5d880d0": "访问密钥",
|
||||||
"8139cb3dd11f5a67": "开启后会把 JSON 对象覆盖到最终请求头。同名请求头以这里为准,值必须是字符串。",
|
"8139cb3dd11f5a67": "开启后会把 JSON 对象覆盖到最终请求头。同名请求头以这里为准,值必须是字符串。",
|
||||||
|
"826c0e5a4407befa": "可多选。保存时只会写入选中的模型,每个模型生成一条配置。",
|
||||||
"83be9cac28873059": "Cursor 控制面账号",
|
"83be9cac28873059": "Cursor 控制面账号",
|
||||||
"8672864e90417138": "最高",
|
"8672864e90417138": "最高",
|
||||||
"86df7ec743047234": "服务运行中",
|
"86df7ec743047234": "服务运行中",
|
||||||
|
"891bfee3bbe52d3c": "请填写模型标识",
|
||||||
"899add6275682210": "留空时默认 200000",
|
"899add6275682210": "留空时默认 200000",
|
||||||
"8a4ef3e48e4e8a5a": "已开启",
|
"8a4ef3e48e4e8a5a": "已开启",
|
||||||
"8c1935935600e336": "模型测试",
|
"8c1935935600e336": "模型测试",
|
||||||
"8cbcf741e727dbf7": "模型配置",
|
"8cbcf741e727dbf7": "模型配置",
|
||||||
"8d1de152be6360ce": "有效占比:{0}",
|
"8d1de152be6360ce": "有效占比:{0}",
|
||||||
"8e2dc7b0d2e8f6f8": "例如:OpenAI - GPT-4.1",
|
|
||||||
"8f6f8d979c981ced": "已复制",
|
"8f6f8d979c981ced": "已复制",
|
||||||
"8f8baf5d18dd0492": "开启后会把 JSON 对象覆盖到 OpenAI 请求体。同名字段以这里为准。OpenAI service_tier 支持 auto、default、flex、scale、priority;priority 可用于高优先级/Fast 类场景。",
|
"8f8baf5d18dd0492": "开启后会把 JSON 对象覆盖到 OpenAI 请求体。同名字段以这里为准。OpenAI service_tier 支持 auto、default、flex、scale、priority;priority 可用于高优先级/Fast 类场景。",
|
||||||
"8faa670b512b6b9b": "打开模型配置",
|
"8faa670b512b6b9b": "打开模型配置",
|
||||||
@@ -160,6 +168,7 @@
|
|||||||
"b571037dc396a00c": "总请求 Token 包含 Prompt 和模型输出。",
|
"b571037dc396a00c": "总请求 Token 包含 Prompt 和模型输出。",
|
||||||
"b765005f69fa971f": "例如:gpt-4.1",
|
"b765005f69fa971f": "例如:gpt-4.1",
|
||||||
"b76a22622020f849": "选择接口协议端点。选“自定义路径”时,请在接口地址栏填写完整请求地址(含 /chat/completions 或 /responses 路径后缀),系统会根据末段自动判断协议形态。",
|
"b76a22622020f849": "选择接口协议端点。选“自定义路径”时,请在接口地址栏填写完整请求地址(含 /chat/completions 或 /responses 路径后缀),系统会根据末段自动判断协议形态。",
|
||||||
|
"b7ceef2fbfeb4a85": "例如:GPT-5",
|
||||||
"b870928f8f9a24c4": "接口端点",
|
"b870928f8f9a24c4": "接口端点",
|
||||||
"b90a8ac9c488ce46": "选择语言",
|
"b90a8ac9c488ce46": "选择语言",
|
||||||
"ba3c66f90fd11725": "已识别系统代理",
|
"ba3c66f90fd11725": "已识别系统代理",
|
||||||
@@ -173,6 +182,7 @@
|
|||||||
"c3d46b387eeadb23": "只会退出 cursor-byok 中的 Cursor 账号,不会退出 Cursor 客户端。是否继续?",
|
"c3d46b387eeadb23": "只会退出 cursor-byok 中的 Cursor 账号,不会退出 Cursor 客户端。是否继续?",
|
||||||
"c5af02060847d167": "Anthropic adaptive thinking 的思考强度。请求会固定使用新版 thinking.type=adaptive。",
|
"c5af02060847d167": "Anthropic adaptive thinking 的思考强度。请求会固定使用新版 thinking.type=adaptive。",
|
||||||
"c69f5bce63b9f14c": "设置文件夹",
|
"c69f5bce63b9f14c": "设置文件夹",
|
||||||
|
"c72d5dc20cd27118": "已选择 {0} / {1} 个模型",
|
||||||
"c8a52b66651d294c": "退出登录失败",
|
"c8a52b66651d294c": "退出登录失败",
|
||||||
"c8c14507b2d37395": "推理强度",
|
"c8c14507b2d37395": "推理强度",
|
||||||
"c98e118e0a43f078": "模型",
|
"c98e118e0a43f078": "模型",
|
||||||
@@ -206,6 +216,7 @@
|
|||||||
"e4c0daa3c4bea691": "感谢 @aike0210 对 Cursor 控制面账号功能的贡献。",
|
"e4c0daa3c4bea691": "感谢 @aike0210 对 Cursor 控制面账号功能的贡献。",
|
||||||
"e53580f8031f13c0": "请在浏览器完成登录,完成后返回 Cursor 重新打开插件市场",
|
"e53580f8031f13c0": "请在浏览器完成登录,完成后返回 Cursor 重新打开插件市场",
|
||||||
"e552c2accdbf5178": "新增模型",
|
"e552c2accdbf5178": "新增模型",
|
||||||
|
"e6943d5cbfb863e0": "正在获取模型...",
|
||||||
"e6faccfddce722e8": "缓存读取:{0}",
|
"e6faccfddce722e8": "缓存读取:{0}",
|
||||||
"e8a0a6053998ebfa": "已经登录",
|
"e8a0a6053998ebfa": "已经登录",
|
||||||
"eaffd48cd2ea9f1a": "例如:https://api.anthropic.com",
|
"eaffd48cd2ea9f1a": "例如:https://api.anthropic.com",
|
||||||
@@ -218,6 +229,7 @@
|
|||||||
"f3a76d896853c1df": "未命中",
|
"f3a76d896853c1df": "未命中",
|
||||||
"f3fae6cccb9004b1": "自定义请求头名称不能为空",
|
"f3fae6cccb9004b1": "自定义请求头名称不能为空",
|
||||||
"f474a4108aba4c4c": "关闭服务",
|
"f474a4108aba4c4c": "关闭服务",
|
||||||
|
"f4d4bae588c4c0ff": "取消全选",
|
||||||
"f4f0ead1116b5b62": "启用",
|
"f4f0ead1116b5b62": "启用",
|
||||||
"f56c6c82203b33f6": "提示",
|
"f56c6c82203b33f6": "提示",
|
||||||
"f61e03f047b786d5": "{0} 的最大输出 Token 必须为正整数",
|
"f61e03f047b786d5": "{0} 的最大输出 Token 必须为正整数",
|
||||||
|
|||||||
@@ -161,3 +161,9 @@ export function getModelAdapterTestResults() {
|
|||||||
Call.ByName(`${PROXY_SERVICE_NAME}.GetModelAdapterTestResults`),
|
Call.ByName(`${PROXY_SERVICE_NAME}.GetModelAdapterTestResults`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchModelAdapterModels(payload) {
|
||||||
|
return withApiLogging("FetchModelAdapterModels", payload, () =>
|
||||||
|
Call.ByName(`${PROXY_SERVICE_NAME}.FetchModelAdapterModels`, payload),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
startProxyService,
|
startProxyService,
|
||||||
stopProxyService,
|
stopProxyService,
|
||||||
testModelAdapter,
|
testModelAdapter,
|
||||||
|
fetchModelAdapterModels,
|
||||||
} from "@/services/clientApi";
|
} from "@/services/clientApi";
|
||||||
|
|
||||||
const APP_STATE_STORAGE_KEY = "cursor-client:runtime-state:v2";
|
const APP_STATE_STORAGE_KEY = "cursor-client:runtime-state:v2";
|
||||||
@@ -1143,6 +1144,100 @@ export async function saveModelAdapterAt(index, adapter) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchAvailableModelIDs(payload) {
|
||||||
|
const result = await fetchModelAdapterModels(payload);
|
||||||
|
return asArray(result?.models)
|
||||||
|
.map((item) => asString(item))
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPrefixedModelDisplayName(prefix, modelID) {
|
||||||
|
const normalizedPrefix = asString(prefix) || "模型";
|
||||||
|
return `${normalizedPrefix}-${asString(modelID)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildModelAdaptersFromModelIDs(source, modelIDs, prefix) {
|
||||||
|
const base = normalizeModelAdapter(source);
|
||||||
|
const seen = new Set();
|
||||||
|
return asArray(modelIDs)
|
||||||
|
.map((item) => asString(item))
|
||||||
|
.filter((modelID) => {
|
||||||
|
if (!modelID || seen.has(modelID)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seen.add(modelID);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((modelID) => normalizeModelAdapter({
|
||||||
|
...base,
|
||||||
|
id: "",
|
||||||
|
modelID,
|
||||||
|
displayName: buildPrefixedModelDisplayName(prefix, modelID),
|
||||||
|
tooltipData: base.tooltipData || "备注",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findModelAdapterUpsertIndex(adapters, target) {
|
||||||
|
return adapters.findIndex((adapter) => {
|
||||||
|
const current = normalizeModelAdapter(adapter);
|
||||||
|
return current.type === target.type
|
||||||
|
&& normalizeBaseURL(current.baseURL) === normalizeBaseURL(target.baseURL)
|
||||||
|
&& current.apiKey === target.apiKey
|
||||||
|
&& current.modelID === target.modelID
|
||||||
|
&& current.displayName === target.displayName
|
||||||
|
&& (current.type !== "openai" || current.openAIEndpoint === target.openAIEndpoint);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveModelAdaptersFromModelIDs(source, modelIDs, prefix, selectedModelID = "") {
|
||||||
|
const generatedAdapters = buildModelAdaptersFromModelIDs(source, modelIDs, prefix);
|
||||||
|
if (generatedAdapters.length === 0) {
|
||||||
|
return { ok: false, error: "没有可保存的模型" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatedError = validateModelAdapters(generatedAdapters);
|
||||||
|
if (generatedError) {
|
||||||
|
return { ok: false, error: generatedError };
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentConfig = await loadPersistedUserConfig();
|
||||||
|
const nextAdapters = normalizeModelAdapters(currentConfig.modelAdapters);
|
||||||
|
const targetModelID = asString(selectedModelID) || generatedAdapters[0]?.modelID || "";
|
||||||
|
let selectedIndex = -1;
|
||||||
|
|
||||||
|
for (const adapter of generatedAdapters) {
|
||||||
|
const index = findModelAdapterUpsertIndex(nextAdapters, adapter);
|
||||||
|
if (index >= 0) {
|
||||||
|
nextAdapters.splice(index, 1, adapter);
|
||||||
|
if (selectedIndex < 0 && adapter.modelID === targetModelID) {
|
||||||
|
selectedIndex = index;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nextAdapters.push(adapter);
|
||||||
|
if (selectedIndex < 0 && adapter.modelID === targetModelID) {
|
||||||
|
selectedIndex = nextAdapters.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await persistConfigPayload(
|
||||||
|
{
|
||||||
|
...currentConfig,
|
||||||
|
modelAdapters: nextAdapters,
|
||||||
|
},
|
||||||
|
{ modelAdaptersOnly: true },
|
||||||
|
);
|
||||||
|
if (!result.ok) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
index: selectedIndex,
|
||||||
|
adapter: selectedIndex >= 0 ? appState.modelAdapters[selectedIndex] ?? null : null,
|
||||||
|
count: generatedAdapters.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteModelAdapterAt(index) {
|
export async function deleteModelAdapterAt(index) {
|
||||||
const currentConfig = await loadPersistedUserConfig();
|
const currentConfig = await loadPersistedUserConfig();
|
||||||
const nextAdapters = normalizeModelAdapters(currentConfig.modelAdapters);
|
const nextAdapters = normalizeModelAdapters(currentConfig.modelAdapters);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import Button from "@/components/ui/Button.vue";
|
import Button from "@/components/ui/Button.vue";
|
||||||
import Input from "@/components/ui/Input.vue";
|
import Input from "@/components/ui/Input.vue";
|
||||||
import ModelAdapterTestCard from "@/components/ModelAdapterTestCard.vue";
|
import ModelAdapterTestCard from "@/components/ModelAdapterTestCard.vue";
|
||||||
|
import MultiSelect from "@/components/ui/MultiSelect.vue";
|
||||||
import Select from "@/components/ui/Select.vue";
|
import Select from "@/components/ui/Select.vue";
|
||||||
import Tooltip from "@/components/ui/Tooltip.vue";
|
import Tooltip from "@/components/ui/Tooltip.vue";
|
||||||
import { getModelEditorContext } from "@/services/clientApi";
|
import { getModelEditorContext } from "@/services/clientApi";
|
||||||
@@ -9,9 +10,11 @@ import {
|
|||||||
ANTHROPIC_THINKING_EFFORT_DEFAULT,
|
ANTHROPIC_THINKING_EFFORT_DEFAULT,
|
||||||
appState,
|
appState,
|
||||||
buildModelAdapterTestRequestHash,
|
buildModelAdapterTestRequestHash,
|
||||||
|
buildModelAdaptersFromModelIDs,
|
||||||
createEmptyModelAdapter,
|
createEmptyModelAdapter,
|
||||||
CUSTOM_HEADERS_DEFAULT_JSON,
|
CUSTOM_HEADERS_DEFAULT_JSON,
|
||||||
EXTRA_PARAMS_DEFAULT_JSON,
|
EXTRA_PARAMS_DEFAULT_JSON,
|
||||||
|
fetchAvailableModelIDs,
|
||||||
getModelAdapterTestResult,
|
getModelAdapterTestResult,
|
||||||
getModelAdapterTestResultByID,
|
getModelAdapterTestResultByID,
|
||||||
isModelAdapterTestResultStale,
|
isModelAdapterTestResultStale,
|
||||||
@@ -22,11 +25,12 @@ import {
|
|||||||
OPENAI_EXTRA_PARAMS_DEFAULT_JSON,
|
OPENAI_EXTRA_PARAMS_DEFAULT_JSON,
|
||||||
runModelAdapterTest,
|
runModelAdapterTest,
|
||||||
saveModelAdapterAt,
|
saveModelAdapterAt,
|
||||||
|
saveModelAdaptersFromModelIDs,
|
||||||
toUserError,
|
toUserError,
|
||||||
validateModelAdapters,
|
validateModelAdapters,
|
||||||
} from "@/state/appState";
|
} from "@/state/appState";
|
||||||
import { Window } from "@wailsio/runtime";
|
import { Window } from "@wailsio/runtime";
|
||||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||||
|
|
||||||
const modelTypeTabs = [
|
const modelTypeTabs = [
|
||||||
{ label: "OpenAI", value: "openai", icon: "icon-[bxl--openai]" },
|
{ label: "OpenAI", value: "openai", icon: "icon-[bxl--openai]" },
|
||||||
@@ -61,6 +65,14 @@ const errorMessage = ref("");
|
|||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const lastTestAdapterID = ref("");
|
const lastTestAdapterID = ref("");
|
||||||
const localTestFailure = 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) {
|
function createOptionalPositiveIntegerModel(key) {
|
||||||
return computed({
|
return computed({
|
||||||
@@ -80,14 +92,44 @@ const contextWindowTokensInput = createOptionalPositiveIntegerModel("contextWind
|
|||||||
const interfacePlaceholder = computed(() =>
|
const interfacePlaceholder = computed(() =>
|
||||||
draft.type === "anthropic" ? "例如:https://api.anthropic.com" : "例如:https://api.openai.com/v1",
|
draft.type === "anthropic" ? "例如:https://api.anthropic.com" : "例如:https://api.openai.com/v1",
|
||||||
);
|
);
|
||||||
const currentRequestHash = computed(() => buildModelAdapterTestRequestHash(draft));
|
const modelOptions = computed(() => availableModelIDs.value.map((modelID) => ({
|
||||||
const directModelTestResult = computed(() => getModelAdapterTestResult(draft));
|
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(() =>
|
const rememberedModelTestResult = computed(() =>
|
||||||
lastTestAdapterID.value ? getModelAdapterTestResultByID(lastTestAdapterID.value) : null,
|
lastTestAdapterID.value ? getModelAdapterTestResultByID(lastTestAdapterID.value) : null,
|
||||||
);
|
);
|
||||||
const activeModelTestResult = computed(() => directModelTestResult.value || rememberedModelTestResult.value);
|
const activeModelTestResult = computed(() => directModelTestResult.value || rememberedModelTestResult.value);
|
||||||
const modelTestResultStale = computed(() =>
|
const modelTestResultStale = computed(() =>
|
||||||
isModelAdapterTestResultStale(draft, activeModelTestResult.value),
|
isModelAdapterTestResultStale(selectedTestAdapter.value, activeModelTestResult.value),
|
||||||
);
|
);
|
||||||
const isCurrentConfigTesting = computed(() => directModelTestResult.value?.status === "running");
|
const isCurrentConfigTesting = computed(() => directModelTestResult.value?.status === "running");
|
||||||
const modelTestSummary = computed(() => {
|
const modelTestSummary = computed(() => {
|
||||||
@@ -125,7 +167,8 @@ function ensureAnthropicThinkingEffort() {
|
|||||||
|
|
||||||
const fieldTips = {
|
const fieldTips = {
|
||||||
displayName: "仅用于界面展示,便于你区分不同模型。",
|
displayName: "仅用于界面展示,便于你区分不同模型。",
|
||||||
modelID: "请求实际发送给服务端的模型名称,例如 gpt-4.1 或 claude-sonnet。",
|
modelID: "可多选。保存时只会写入选中的模型,每个模型生成一条配置。",
|
||||||
|
manualModelID: "请求实际发送给服务端的模型名称,例如 gpt-4.1 或 claude-sonnet。",
|
||||||
baseURL: "模型服务的 API 根地址,通常为兼容 OpenAI 或 Anthropic 的接口入口。",
|
baseURL: "模型服务的 API 根地址,通常为兼容 OpenAI 或 Anthropic 的接口入口。",
|
||||||
apiKey: "调用该模型服务需要使用的访问密钥。",
|
apiKey: "调用该模型服务需要使用的访问密钥。",
|
||||||
contextWindowTokens: "模型单次可接受的最大上下文 Token 数。留空时使用默认值。",
|
contextWindowTokens: "模型单次可接受的最大上下文 Token 数。留空时使用默认值。",
|
||||||
@@ -146,6 +189,11 @@ async function loadContext() {
|
|||||||
editorIndex.value = typeof ctx.index === "number" ? ctx.index : -1;
|
editorIndex.value = typeof ctx.index === "number" ? ctx.index : -1;
|
||||||
const parsed = JSON.parse(ctx.adapterJSON || "{}");
|
const parsed = JSON.parse(ctx.adapterJSON || "{}");
|
||||||
Object.assign(draft, normalizeModelAdapter(parsed));
|
Object.assign(draft, normalizeModelAdapter(parsed));
|
||||||
|
if (draft.modelID) {
|
||||||
|
availableModelIDs.value = [draft.modelID];
|
||||||
|
selectedModelIDs.value = [draft.modelID];
|
||||||
|
}
|
||||||
|
modelPrefix.value = draft.displayName || "";
|
||||||
if (!draft.type) {
|
if (!draft.type) {
|
||||||
draft.type = "openai";
|
draft.type = "openai";
|
||||||
}
|
}
|
||||||
@@ -157,8 +205,81 @@ async function loadContext() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
async function persistDraft() {
|
||||||
const adapter = normalizeModelAdapter(draft);
|
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]);
|
const singleCheck = validateModelAdapters([adapter]);
|
||||||
if (singleCheck) {
|
if (singleCheck) {
|
||||||
@@ -166,7 +287,9 @@ async function persistDraft() {
|
|||||||
return { ok: false, error: singleCheck, adapter: null };
|
return { ok: false, error: singleCheck, adapter: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await saveModelAdapterAt(editorIndex.value, adapter);
|
const result = isManualModelInput.value
|
||||||
|
? await saveModelAdapterAt(editorIndex.value, adapter)
|
||||||
|
: await saveModelAdaptersFromModelIDs(adapter, models, modelPrefix.value, selectedModelID);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
errorMessage.value = result.error;
|
errorMessage.value = result.error;
|
||||||
return { ok: false, error: result.error, adapter: null };
|
return { ok: false, error: result.error, adapter: null };
|
||||||
@@ -177,12 +300,14 @@ async function persistDraft() {
|
|||||||
}
|
}
|
||||||
if (result.adapter) {
|
if (result.adapter) {
|
||||||
Object.assign(draft, normalizeModelAdapter(result.adapter));
|
Object.assign(draft, normalizeModelAdapter(result.adapter));
|
||||||
|
} else {
|
||||||
|
Object.assign(draft, adapter);
|
||||||
}
|
}
|
||||||
errorMessage.value = "";
|
errorMessage.value = "";
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
error: "",
|
error: "",
|
||||||
adapter: result.adapter ? normalizeModelAdapter(result.adapter) : normalizeModelAdapter(draft),
|
adapter: result.adapter ? normalizeModelAdapter(result.adapter) : normalizeModelAdapter(adapter),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +325,10 @@ async function handleCancel() {
|
|||||||
|
|
||||||
function handleModelTypeChange(type) {
|
function handleModelTypeChange(type) {
|
||||||
draft.type = type;
|
draft.type = type;
|
||||||
|
modelSelectionMode.value = "auto";
|
||||||
|
availableModelIDs.value = [];
|
||||||
|
selectedModelIDs.value = [];
|
||||||
|
draft.modelID = "";
|
||||||
if (type === "openai" && !draft.openAIEndpoint) {
|
if (type === "openai" && !draft.openAIEndpoint) {
|
||||||
draft.openAIEndpoint = OPENAI_ENDPOINT_RESPONSES;
|
draft.openAIEndpoint = OPENAI_ENDPOINT_RESPONSES;
|
||||||
} else if (type === "anthropic") {
|
} else if (type === "anthropic") {
|
||||||
@@ -273,9 +402,32 @@ watch(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 () => {
|
onMounted(async () => {
|
||||||
await loadContext();
|
await loadContext();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.clearTimeout(modelListDebounceTimer);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -318,26 +470,13 @@ onMounted(async () => {
|
|||||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
|
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
|
||||||
<Tooltip :content="fieldTips.displayName" />
|
<Tooltip :content="fieldTips.baseURL" />
|
||||||
<span>显示名称</span>
|
<span>接口地址</span>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
v-model="draft.displayName"
|
v-model="draft.baseURL"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="例如:OpenAI - GPT-4.1"
|
: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.modelID" />
|
|
||||||
<span>模型标识</span>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
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]"
|
class="h-9 rounded-[6px] border border-[#3f3f3f] bg-[#232323] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -358,17 +497,41 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<label class="flex flex-col gap-1">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
|
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
|
||||||
<Tooltip :content="fieldTips.baseURL" />
|
<Tooltip :content="isManualModelInput ? fieldTips.displayName : fieldTips.modelID" />
|
||||||
<span>接口地址</span>
|
<span>{{ isManualModelInput ? "显示名称" : "模型前缀" }}</span>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
v-model="draft.baseURL"
|
v-model="modelPrefix"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="interfacePlaceholder"
|
placeholder="例如:GPT-5"
|
||||||
class="h-9 rounded-[6px] border border-[#3f3f3f] bg-[#232323] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
|
class="h-9 rounded-[6px] border border-[#3f3f3f] bg-[#232323] px-3 text-sm text-[#e5e5e5] outline-none focus:border-[#10AD5D]"
|
||||||
/>
|
/>
|
||||||
</label>
|
</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">
|
<label class="flex flex-col gap-1">
|
||||||
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
|
<span class="center-row justify-start gap-1.5 text-sm text-[#d4d4d4]">
|
||||||
<Tooltip :content="fieldTips.contextWindowTokens" />
|
<Tooltip :content="fieldTips.contextWindowTokens" />
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ type ModelAdapterTestResult = client.ModelAdapterTestResult
|
|||||||
// ModelAdapterTestResultsPayload 定义测速结果事件载荷。
|
// ModelAdapterTestResultsPayload 定义测速结果事件载荷。
|
||||||
type ModelAdapterTestResultsPayload = client.ModelAdapterTestResultsPayload
|
type ModelAdapterTestResultsPayload = client.ModelAdapterTestResultsPayload
|
||||||
|
|
||||||
|
// ModelAdapterModelsRequest 定义模型列表查询请求。
|
||||||
|
type ModelAdapterModelsRequest = client.ModelAdapterModelsRequest
|
||||||
|
|
||||||
|
// ModelAdapterModelsResult 定义模型列表查询结果。
|
||||||
|
type ModelAdapterModelsResult = client.ModelAdapterModelsResult
|
||||||
|
|
||||||
// CursorAccountStatus 是可安全展示给桌面前端的独立 Cursor 账号状态。
|
// CursorAccountStatus 是可安全展示给桌面前端的独立 Cursor 账号状态。
|
||||||
type CursorAccountStatus = client.CursorAccountStatus
|
type CursorAccountStatus = client.CursorAccountStatus
|
||||||
|
|
||||||
@@ -119,6 +125,11 @@ func (s *ProxyService) GetModelAdapterTestResults() []ModelAdapterTestResult {
|
|||||||
return s.core.GetModelAdapterTestResults()
|
return s.core.GetModelAdapterTestResults()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchModelAdapterModels 用于从模型服务读取可用模型列表。
|
||||||
|
func (s *ProxyService) FetchModelAdapterModels(input ModelAdapterModelsRequest) (ModelAdapterModelsResult, error) {
|
||||||
|
return s.core.FetchModelAdapterModels(input)
|
||||||
|
}
|
||||||
|
|
||||||
// GetDeviceID 用于处理与 GetDeviceID 相关的逻辑。
|
// GetDeviceID 用于处理与 GetDeviceID 相关的逻辑。
|
||||||
func (s *ProxyService) GetDeviceID() (string, error) {
|
func (s *ProxyService) GetDeviceID() (string, error) {
|
||||||
return s.core.GetDeviceID()
|
return s.core.GetDeviceID()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -31,8 +32,48 @@ const (
|
|||||||
modelAdapterTestDefaultMaxTokens = 65_536
|
modelAdapterTestDefaultMaxTokens = 65_536
|
||||||
modelAdapterTestEmptyTextError = "未收到文本输出,无法计算测速结果"
|
modelAdapterTestEmptyTextError = "未收到文本输出,无法计算测速结果"
|
||||||
modelAdapterTestMaxErrorBodyBytes = 8192
|
modelAdapterTestMaxErrorBodyBytes = 8192
|
||||||
|
modelAdapterListTimeout = 20 * time.Second
|
||||||
|
modelAdapterListMaxBodyBytes = 8 << 20
|
||||||
|
modelAdapterListPageSize = 1000
|
||||||
|
modelAdapterListMaxPages = 50
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// modelListProviderRule 收敛各家模型列表接口的协议差异,避免判断散落到多个函数。
|
||||||
|
type modelListProviderRule struct {
|
||||||
|
// paths 按优先级排列,逐个尝试直到某个返回可用模型
|
||||||
|
paths []string
|
||||||
|
authHeader string
|
||||||
|
authPrefix string
|
||||||
|
extraHeader map[string]string
|
||||||
|
// paginated 为真时按 limit + after_id 游标翻页,直到 has_more 为 false
|
||||||
|
paginated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelListProviderRules = map[string]modelListProviderRule{
|
||||||
|
"openai": {
|
||||||
|
paths: []string{"/models"},
|
||||||
|
authHeader: "Authorization",
|
||||||
|
authPrefix: "Bearer ",
|
||||||
|
},
|
||||||
|
"anthropic": {
|
||||||
|
paths: []string{"/models"},
|
||||||
|
authHeader: "x-api-key",
|
||||||
|
extraHeader: map[string]string{"anthropic-version": "2023-06-01"},
|
||||||
|
paginated: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// modelListVersionSegments 用于判断 base url 是否已带版本前缀,带了就不再补 /v1。
|
||||||
|
var modelListVersionSegments = map[string]bool{
|
||||||
|
"v1": true,
|
||||||
|
"v1beta": true,
|
||||||
|
"v2": true,
|
||||||
|
"beta": true,
|
||||||
|
"openai": true,
|
||||||
|
"compat": true,
|
||||||
|
"compatible": true,
|
||||||
|
}
|
||||||
|
|
||||||
type ModelAdapterTestStatus string
|
type ModelAdapterTestStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -58,6 +99,20 @@ type ModelAdapterTestResult struct {
|
|||||||
TestedAt string `json:"testedAt"`
|
TestedAt string `json:"testedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ModelAdapterModelsRequest 定义从兼容接口读取模型列表所需的最小配置。
|
||||||
|
type ModelAdapterModelsRequest struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
BaseURL string `json:"baseURL"`
|
||||||
|
APIKey string `json:"apiKey"`
|
||||||
|
CustomHeadersEnabled bool `json:"customHeadersEnabled"`
|
||||||
|
CustomHeadersJSON string `json:"customHeadersJSON"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelAdapterModelsResult 定义可供前端下拉选择的模型列表。
|
||||||
|
type ModelAdapterModelsResult struct {
|
||||||
|
Models []string `json:"models"`
|
||||||
|
}
|
||||||
|
|
||||||
// ModelAdapterTestResultsPayload 用于向前端广播当前测速结果快照。
|
// ModelAdapterTestResultsPayload 用于向前端广播当前测速结果快照。
|
||||||
type ModelAdapterTestResultsPayload struct {
|
type ModelAdapterTestResultsPayload struct {
|
||||||
Results []ModelAdapterTestResult `json:"results"`
|
Results []ModelAdapterTestResult `json:"results"`
|
||||||
@@ -108,6 +163,254 @@ func (s *ProxyService) GetModelAdapterTestResults() []ModelAdapterTestResult {
|
|||||||
return s.snapshotModelAdapterTestResults()
|
return s.snapshotModelAdapterTestResults()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ProxyService) FetchModelAdapterModels(input ModelAdapterModelsRequest) (ModelAdapterModelsResult, error) {
|
||||||
|
_ = s
|
||||||
|
provider := strings.ToLower(strings.TrimSpace(input.Type))
|
||||||
|
baseURL := strings.TrimSpace(input.BaseURL)
|
||||||
|
apiKey := strings.TrimSpace(input.APIKey)
|
||||||
|
rule, supported := modelListProviderRules[provider]
|
||||||
|
if !supported {
|
||||||
|
return ModelAdapterModelsResult{}, errors.New("模型类型仅支持 OpenAI 或 Anthropic")
|
||||||
|
}
|
||||||
|
if baseURL == "" {
|
||||||
|
return ModelAdapterModelsResult{}, errors.New("接口地址不能为空")
|
||||||
|
}
|
||||||
|
if apiKey == "" {
|
||||||
|
return ModelAdapterModelsResult{}, errors.New("访问密钥不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), modelAdapterListTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for _, endpoint := range buildModelListEndpointCandidates(rule, baseURL) {
|
||||||
|
models, err := fetchModelListEndpoint(ctx, rule, endpoint, apiKey, input)
|
||||||
|
if err == nil {
|
||||||
|
return ModelAdapterModelsResult{Models: models}, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
if lastErr != nil {
|
||||||
|
return ModelAdapterModelsResult{}, lastErr
|
||||||
|
}
|
||||||
|
return ModelAdapterModelsResult{}, errors.New("未找到可用的模型列表接口")
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildModelListEndpointCandidates(rule modelListProviderRule, rawBaseURL string) []string {
|
||||||
|
base := strings.TrimRight(strings.TrimSpace(rawBaseURL), "/")
|
||||||
|
for _, suffix := range []string{"/chat/completions", "/responses", "/messages"} {
|
||||||
|
if strings.HasSuffix(strings.ToLower(base), suffix) {
|
||||||
|
base = base[:len(base)-len(suffix)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base = strings.TrimRight(base, "/")
|
||||||
|
|
||||||
|
tail := strings.ToLower(base[strings.LastIndex(base, "/")+1:])
|
||||||
|
var candidates []string
|
||||||
|
switch {
|
||||||
|
case tail == "models" || tail == "model":
|
||||||
|
// 用户已经填到模型列表地址本身,直接用
|
||||||
|
candidates = []string{base}
|
||||||
|
case modelListVersionSegments[tail]:
|
||||||
|
candidates = prefixModelListPaths(base, "", rule.paths)
|
||||||
|
default:
|
||||||
|
// base 没带版本段,优先试 /v1,再退回裸路径
|
||||||
|
candidates = append(
|
||||||
|
prefixModelListPaths(base, "/v1", rule.paths),
|
||||||
|
prefixModelListPaths(base, "", rule.paths)...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
endpoints := make([]string, 0, len(candidates))
|
||||||
|
for _, endpoint := range candidates {
|
||||||
|
if _, err := url.ParseRequestURI(endpoint); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[endpoint]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[endpoint] = struct{}{}
|
||||||
|
endpoints = append(endpoints, endpoint)
|
||||||
|
}
|
||||||
|
return endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefixModelListPaths(base string, version string, paths []string) []string {
|
||||||
|
endpoints := make([]string, 0, len(paths))
|
||||||
|
for _, path := range paths {
|
||||||
|
endpoints = append(endpoints, base+version+path)
|
||||||
|
}
|
||||||
|
return endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchModelListEndpoint(ctx context.Context, rule modelListProviderRule, endpoint string, apiKey string, input ModelAdapterModelsRequest) ([]string, error) {
|
||||||
|
collected := []string{}
|
||||||
|
cursor := ""
|
||||||
|
for page := 0; page < modelAdapterListMaxPages; page++ {
|
||||||
|
requestURL := endpoint
|
||||||
|
if rule.paginated {
|
||||||
|
requestURL = appendModelListCursor(endpoint, cursor)
|
||||||
|
}
|
||||||
|
payload, err := requestModelListPayload(ctx, rule, requestURL, apiKey, input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
collected = append(collected, extractModelIDs(payload)...)
|
||||||
|
if !rule.paginated {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cursor = nextModelListCursor(payload)
|
||||||
|
if cursor == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if page == modelAdapterListMaxPages-1 {
|
||||||
|
return nil, fmt.Errorf("模型列表分页超过 %d 页,结果可能不完整", modelAdapterListMaxPages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
models := normalizeFetchedModelIDs(collected)
|
||||||
|
if len(models) == 0 {
|
||||||
|
return nil, errors.New("模型列表响应中没有可用模型")
|
||||||
|
}
|
||||||
|
return models, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestModelListPayload(
|
||||||
|
ctx context.Context,
|
||||||
|
rule modelListProviderRule,
|
||||||
|
requestURL string,
|
||||||
|
apiKey string,
|
||||||
|
input ModelAdapterModelsRequest,
|
||||||
|
) (any, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set(rule.authHeader, rule.authPrefix+apiKey)
|
||||||
|
for key, value := range rule.extraHeader {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
applyModelListCustomHeaders(req.Header, input)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, readErr := io.ReadAll(io.LimitReader(resp.Body, modelAdapterListMaxBodyBytes))
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, readErr
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
message := strings.TrimSpace(string(body))
|
||||||
|
if len(message) > modelAdapterTestMaxErrorBodyBytes {
|
||||||
|
message = message[:modelAdapterTestMaxErrorBodyBytes]
|
||||||
|
}
|
||||||
|
if message == "" {
|
||||||
|
message = resp.Status
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("读取模型列表失败:%s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload any
|
||||||
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
|
return nil, fmt.Errorf("模型列表响应不是合法 JSON:%w", err)
|
||||||
|
}
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendModelListCursor(endpoint string, cursor string) string {
|
||||||
|
query := url.Values{}
|
||||||
|
query.Set("limit", strconv.Itoa(modelAdapterListPageSize))
|
||||||
|
if cursor != "" {
|
||||||
|
query.Set("after_id", cursor)
|
||||||
|
}
|
||||||
|
separator := "?"
|
||||||
|
if strings.Contains(endpoint, "?") {
|
||||||
|
separator = "&"
|
||||||
|
}
|
||||||
|
return endpoint + separator + query.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextModelListCursor(payload any) string {
|
||||||
|
object, ok := payload.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if hasMore, _ := object["has_more"].(bool); !hasMore {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
cursor, _ := object["last_id"].(string)
|
||||||
|
return strings.TrimSpace(cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyModelListCustomHeaders(header http.Header, input ModelAdapterModelsRequest) {
|
||||||
|
if !input.CustomHeadersEnabled || strings.TrimSpace(input.CustomHeadersJSON) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var parsed map[string]string
|
||||||
|
if err := json.Unmarshal([]byte(input.CustomHeadersJSON), &parsed); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for key, value := range parsed {
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
header.Set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractModelIDs(value any) []string {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case string:
|
||||||
|
if strings.TrimSpace(typed) == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
return []string{typed}
|
||||||
|
case []any:
|
||||||
|
models := make([]string, 0, len(typed))
|
||||||
|
for _, item := range typed {
|
||||||
|
models = append(models, extractModelIDs(item)...)
|
||||||
|
}
|
||||||
|
return models
|
||||||
|
case map[string]any:
|
||||||
|
for _, key := range []string{"id", "name"} {
|
||||||
|
if text, ok := typed[key].(string); ok && strings.TrimSpace(text) != "" {
|
||||||
|
return []string{text}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
models := []string{}
|
||||||
|
for _, key := range []string{"data", "models"} {
|
||||||
|
if child, ok := typed[key]; ok {
|
||||||
|
models = append(models, extractModelIDs(child)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return models
|
||||||
|
default:
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeFetchedModelIDs(input []string) []string {
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
models := make([]string, 0, len(input))
|
||||||
|
for _, item := range input {
|
||||||
|
model := strings.TrimSpace(item)
|
||||||
|
if model == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[model]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[model] = struct{}{}
|
||||||
|
models = append(models, model)
|
||||||
|
}
|
||||||
|
sort.Strings(models)
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ProxyService) TestModelAdapter(adapter serverconfig.ModelAdapterConfig) (ModelAdapterTestResult, error) {
|
func (s *ProxyService) TestModelAdapter(adapter serverconfig.ModelAdapterConfig) (ModelAdapterTestResult, error) {
|
||||||
requestHash := buildModelAdapterTestRequestHash(adapter)
|
requestHash := buildModelAdapterTestRequestHash(adapter)
|
||||||
adapterID := buildModelAdapterTestCacheKey(adapter, requestHash)
|
adapterID := buildModelAdapterTestCacheKey(adapter, requestHash)
|
||||||
|
|||||||
@@ -0,0 +1,351 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildModelListEndpointCandidates(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
provider string
|
||||||
|
baseURL string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "openai 带版本段不再补 v1",
|
||||||
|
provider: "openai",
|
||||||
|
baseURL: "https://api.openai.com/v1",
|
||||||
|
want: []string{"https://api.openai.com/v1/models"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "openai 裸域名优先试 v1",
|
||||||
|
provider: "openai",
|
||||||
|
baseURL: "https://api.openai.com",
|
||||||
|
want: []string{"https://api.openai.com/v1/models", "https://api.openai.com/models"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "anthropic 裸域名优先试 v1",
|
||||||
|
provider: "anthropic",
|
||||||
|
baseURL: "https://api.anthropic.com",
|
||||||
|
want: []string{"https://api.anthropic.com/v1/models", "https://api.anthropic.com/models"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "anthropic 带版本段不再补 v1",
|
||||||
|
provider: "anthropic",
|
||||||
|
baseURL: "https://api.anthropic.com/v1",
|
||||||
|
want: []string{"https://api.anthropic.com/v1/models"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "剥离 chat completions 后缀",
|
||||||
|
provider: "openai",
|
||||||
|
baseURL: "https://api.example.com/v1/chat/completions",
|
||||||
|
want: []string{"https://api.example.com/v1/models"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "剥离 responses 后缀",
|
||||||
|
provider: "openai",
|
||||||
|
baseURL: "https://api.example.com/v1/responses",
|
||||||
|
want: []string{"https://api.example.com/v1/models"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "剥离 anthropic messages 后缀",
|
||||||
|
provider: "anthropic",
|
||||||
|
baseURL: "https://api.example.com/v1/messages",
|
||||||
|
want: []string{"https://api.example.com/v1/models"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "已填到 models 地址本身则原样使用",
|
||||||
|
provider: "openai",
|
||||||
|
baseURL: "https://api.example.com/openai/v1/models",
|
||||||
|
want: []string{"https://api.example.com/openai/v1/models"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "自定义网关前缀会补 v1",
|
||||||
|
provider: "openai",
|
||||||
|
baseURL: "https://gateway.example.com/proxy",
|
||||||
|
want: []string{
|
||||||
|
"https://gateway.example.com/proxy/v1/models",
|
||||||
|
"https://gateway.example.com/proxy/models",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "尾部斜杠不影响推导",
|
||||||
|
provider: "anthropic",
|
||||||
|
baseURL: " https://api.anthropic.com/v1/ ",
|
||||||
|
want: []string{"https://api.anthropic.com/v1/models"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
rule, ok := modelListProviderRules[test.provider]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("provider %q 没有对应规则", test.provider)
|
||||||
|
}
|
||||||
|
got := buildModelListEndpointCandidates(rule, test.baseURL)
|
||||||
|
if !reflect.DeepEqual(got, test.want) {
|
||||||
|
t.Fatalf("buildModelListEndpointCandidates(%q) = %v, want %v", test.baseURL, got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchModelAdapterModelsOpenAIUsesBearer(t *testing.T) {
|
||||||
|
var gotPath string
|
||||||
|
var gotAuth string
|
||||||
|
var gotAnthropicVersion string
|
||||||
|
var gotAPIKeyHeader string
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotPath = r.URL.Path
|
||||||
|
gotAuth = r.Header.Get("Authorization")
|
||||||
|
gotAnthropicVersion = r.Header.Get("anthropic-version")
|
||||||
|
gotAPIKeyHeader = r.Header.Get("x-api-key")
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"data":[{"id":"gpt-5"},{"id":"gpt-4o"}]}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
service := &ProxyService{}
|
||||||
|
result, err := service.FetchModelAdapterModels(ModelAdapterModelsRequest{
|
||||||
|
Type: "openai",
|
||||||
|
BaseURL: server.URL + "/v1",
|
||||||
|
APIKey: "sk-test",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchModelAdapterModels 返回错误:%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotPath != "/v1/models" {
|
||||||
|
t.Fatalf("请求路径 = %q, want /v1/models", gotPath)
|
||||||
|
}
|
||||||
|
if gotAuth != "Bearer sk-test" {
|
||||||
|
t.Fatalf("Authorization = %q, want Bearer sk-test", gotAuth)
|
||||||
|
}
|
||||||
|
if gotAPIKeyHeader != "" {
|
||||||
|
t.Fatalf("openai 不应发送 x-api-key,实际 = %q", gotAPIKeyHeader)
|
||||||
|
}
|
||||||
|
if gotAnthropicVersion != "" {
|
||||||
|
t.Fatalf("openai 不应发送 anthropic-version,实际 = %q", gotAnthropicVersion)
|
||||||
|
}
|
||||||
|
want := []string{"gpt-4o", "gpt-5"}
|
||||||
|
if !reflect.DeepEqual(result.Models, want) {
|
||||||
|
t.Fatalf("Models = %v, want %v", result.Models, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchModelAdapterModelsAnthropicUsesAPIKeyHeader(t *testing.T) {
|
||||||
|
var gotAuth string
|
||||||
|
var gotAPIKeyHeader string
|
||||||
|
var gotAnthropicVersion string
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotAuth = r.Header.Get("Authorization")
|
||||||
|
gotAPIKeyHeader = r.Header.Get("x-api-key")
|
||||||
|
gotAnthropicVersion = r.Header.Get("anthropic-version")
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"data":[{"id":"claude-sonnet-4"}],"has_more":false}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
service := &ProxyService{}
|
||||||
|
result, err := service.FetchModelAdapterModels(ModelAdapterModelsRequest{
|
||||||
|
Type: "anthropic",
|
||||||
|
BaseURL: server.URL + "/v1",
|
||||||
|
APIKey: "sk-ant-test",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchModelAdapterModels 返回错误:%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotAPIKeyHeader != "sk-ant-test" {
|
||||||
|
t.Fatalf("x-api-key = %q, want sk-ant-test", gotAPIKeyHeader)
|
||||||
|
}
|
||||||
|
if gotAnthropicVersion != "2023-06-01" {
|
||||||
|
t.Fatalf("anthropic-version = %q, want 2023-06-01", gotAnthropicVersion)
|
||||||
|
}
|
||||||
|
if gotAuth != "" {
|
||||||
|
t.Fatalf("anthropic 不应发送 Authorization,实际 = %q", gotAuth)
|
||||||
|
}
|
||||||
|
want := []string{"claude-sonnet-4"}
|
||||||
|
if !reflect.DeepEqual(result.Models, want) {
|
||||||
|
t.Fatalf("Models = %v, want %v", result.Models, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchModelAdapterModelsAnthropicFollowsCursor(t *testing.T) {
|
||||||
|
var requestedQueries []string
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestedQueries = append(requestedQueries, r.URL.RawQuery)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
switch r.URL.Query().Get("after_id") {
|
||||||
|
case "":
|
||||||
|
_, _ = w.Write([]byte(`{"data":[{"id":"claude-a"}],"has_more":true,"last_id":"claude-a"}`))
|
||||||
|
case "claude-a":
|
||||||
|
_, _ = w.Write([]byte(`{"data":[{"id":"claude-b"}],"has_more":true,"last_id":"claude-b"}`))
|
||||||
|
default:
|
||||||
|
_, _ = w.Write([]byte(`{"data":[{"id":"claude-c"}],"has_more":false}`))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
service := &ProxyService{}
|
||||||
|
result, err := service.FetchModelAdapterModels(ModelAdapterModelsRequest{
|
||||||
|
Type: "anthropic",
|
||||||
|
BaseURL: server.URL + "/v1",
|
||||||
|
APIKey: "sk-ant-test",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchModelAdapterModels 返回错误:%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{"claude-a", "claude-b", "claude-c"}
|
||||||
|
if !reflect.DeepEqual(result.Models, want) {
|
||||||
|
t.Fatalf("Models = %v, want %v", result.Models, want)
|
||||||
|
}
|
||||||
|
if len(requestedQueries) != 3 {
|
||||||
|
t.Fatalf("请求次数 = %d, want 3(两次翻页后停止)", len(requestedQueries))
|
||||||
|
}
|
||||||
|
for _, query := range requestedQueries {
|
||||||
|
if !strings.Contains(query, "limit=1000") {
|
||||||
|
t.Fatalf("翻页请求缺少 limit 参数:%q", query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(requestedQueries[1], "after_id=claude-a") {
|
||||||
|
t.Fatalf("第二页未带上游标:%q", requestedQueries[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchModelAdapterModelsOpenAIDoesNotPaginate(t *testing.T) {
|
||||||
|
requestCount := 0
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestCount++
|
||||||
|
if r.URL.RawQuery != "" {
|
||||||
|
t.Errorf("openai 不应附加分页参数,实际 = %q", r.URL.RawQuery)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"data":[{"id":"gpt-5"}],"has_more":true,"last_id":"gpt-5"}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
service := &ProxyService{}
|
||||||
|
if _, err := service.FetchModelAdapterModels(ModelAdapterModelsRequest{
|
||||||
|
Type: "openai",
|
||||||
|
BaseURL: server.URL + "/v1",
|
||||||
|
APIKey: "sk-test",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("FetchModelAdapterModels 返回错误:%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestCount != 1 {
|
||||||
|
t.Fatalf("请求次数 = %d, want 1(openai 忽略 has_more)", requestCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchModelAdapterModelsReadsLargeBody(t *testing.T) {
|
||||||
|
models := make([]map[string]string, 0, 400)
|
||||||
|
for index := 0; index < 400; index++ {
|
||||||
|
models = append(models, map[string]string{
|
||||||
|
"id": "vendor/model-with-a-fairly-long-identifier-" + strings.Repeat("x", 40) + "-" + string(rune('a'+index%26)) + strconv.Itoa(index),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(map[string]any{"data": models})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("构造响应失败:%v", err)
|
||||||
|
}
|
||||||
|
if len(body) <= modelAdapterTestMaxErrorBodyBytes {
|
||||||
|
t.Fatalf("测试响应体只有 %d 字节,需要大于 %d 才能覆盖截断场景", len(body), modelAdapterTestMaxErrorBodyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write(body)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
service := &ProxyService{}
|
||||||
|
result, err := service.FetchModelAdapterModels(ModelAdapterModelsRequest{
|
||||||
|
Type: "openai",
|
||||||
|
BaseURL: server.URL + "/v1",
|
||||||
|
APIKey: "sk-test",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchModelAdapterModels 返回错误:%v", err)
|
||||||
|
}
|
||||||
|
if len(result.Models) != len(models) {
|
||||||
|
t.Fatalf("Models 数量 = %d, want %d", len(result.Models), len(models))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchModelAdapterModelsSupportsStringItems(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"data":["gpt-4o","gpt-4.1"]}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
service := &ProxyService{}
|
||||||
|
result, err := service.FetchModelAdapterModels(ModelAdapterModelsRequest{
|
||||||
|
Type: "openai",
|
||||||
|
BaseURL: server.URL + "/v1",
|
||||||
|
APIKey: "sk-test",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchModelAdapterModels 返回错误:%v", err)
|
||||||
|
}
|
||||||
|
want := []string{"gpt-4.1", "gpt-4o"}
|
||||||
|
if !reflect.DeepEqual(result.Models, want) {
|
||||||
|
t.Fatalf("Models = %v, want %v", result.Models, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchModelAdapterModelsRejectsPaginationTruncation(t *testing.T) {
|
||||||
|
requestCount := 0
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
requestCount++
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"data":[{"id":"claude-model"}],"has_more":true,"last_id":"next"}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
service := &ProxyService{}
|
||||||
|
_, err := service.FetchModelAdapterModels(ModelAdapterModelsRequest{
|
||||||
|
Type: "anthropic",
|
||||||
|
BaseURL: server.URL + "/v1",
|
||||||
|
APIKey: "sk-ant-test",
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "结果可能不完整") {
|
||||||
|
t.Fatalf("期望分页截断错误,实际 = %v", err)
|
||||||
|
}
|
||||||
|
if requestCount != modelAdapterListMaxPages {
|
||||||
|
t.Fatalf("请求次数 = %d, want %d", requestCount, modelAdapterListMaxPages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchModelAdapterModelsRejectsInvalidInput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
request ModelAdapterModelsRequest
|
||||||
|
}{
|
||||||
|
{name: "未知类型", request: ModelAdapterModelsRequest{Type: "gemini", BaseURL: "https://x.com", APIKey: "k"}},
|
||||||
|
{name: "缺少地址", request: ModelAdapterModelsRequest{Type: "openai", APIKey: "k"}},
|
||||||
|
{name: "缺少密钥", request: ModelAdapterModelsRequest{Type: "openai", BaseURL: "https://x.com"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
service := &ProxyService{}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if _, err := service.FetchModelAdapterModels(test.request); err == nil {
|
||||||
|
t.Fatal("期望返回错误,实际为 nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user