Compare commits

...
25 Commits
Author SHA1 Message Date
leookun 0f6b0f9484 release: 0.0.41
- 修复内存泄漏问题,该可能导致内存异常占用
- 支持俄语增加翻译范围
- 修复qwen-3.8-max中断问题(mimo也应该属于同一类问题)
- 修复claude模型可能无法识别图片问题 @GGHansome
- 支持自定义Openai端点 @Sxuan-Coder
- WebSearch 接入百度搜索,DuckDuckGo 作为兜底 @杨超
- 修复一些兼容性问题 @kael-odin
- 修复window上一些表现问题 @philau2512

🔔 如何让AI自动拉模型配置? (以下为提示词,把地址和密钥换为你的)
我的模型配置在 ~/.cursor-local-assistant-v2/config.yaml,我的API地址是:https://xxx 密钥是xxx,帮我拉所有模型配置进去,不要影响已有模型,根据models标准接口拉取。
2026-07-27 00:55:02 +08:00
leookun e401303f64 fix: 修复502导致的内存泄漏问题 2026-07-27 00:15:23 +08:00
leokunandGitHub 799dbda7e0 Merge pull request #178 from kael-odin/fix/mimo-thinking-disable
fix(openai): recognize MiMo in thinking disable
2026-07-16 10:39:50 +08:00
leokunandGitHub f3cbf1e6c3 Merge pull request #179 from kael-odin/fix/anthropic-thinking-disable
fix(anthropic): apply thinking config on override path, symmetric with openai
2026-07-16 10:39:19 +08:00
leokunandGitHub 2380a0d692 Merge pull request #187 from ProtectCookies/main
feat: WebSearch 接入百度搜索,DuckDuckGo 作为兜底
2026-07-16 10:36:01 +08:00
leokunandGitHub 1da1faa4d4 Merge pull request #186 from GGHansome/main
尝试修复使用claude模型无法识别图片问题
2026-07-16 10:35:43 +08:00
leokunandGitHub ce05b0bf7a Merge pull request #205 from philau2512/main
fix(ui): support window resizing and fully localize remaining Chinese texts to EN/JA
2026-07-16 10:35:14 +08:00
philau2512 44a47992d9 feat(build): add PowerShell and batch build scripts for convenient compilation 2026-07-15 17:53:45 +07:00
philau2512 319bd3d7d8 fix(i18n): extract new Chinese keys and implement dynamic localization for footer/updates/tray menu 2026-07-15 17:53:42 +07:00
philau2512 12c7973a06 fix(window): enable maximize button state to support native window resizing on Windows 2026-07-15 17:53:38 +07:00
leokunandGitHub f93b18740d Merge pull request #189 from leookun/release/0.0.40
release: 0.0.40
2026-07-10 11:52:27 +08:00
leokun c6ee26b12d release: 0.0.40 2026-07-10 11:50:02 +08:00
leokunandGitHub 7c1c6a134b Merge pull request #188 from leookun/feat/gpt-5.6
feat: gpt-5.6 support
2026-07-10 11:42:10 +08:00
leokun e0aa270328 feat: gpt-5.6 support 2026-07-10 11:38:45 +08:00
杨超 556ea21f38 feat: WebSearch 接入百度搜索,DuckDuckGo 作为兜底
百度搜索失败或无结果时自动回退到 DuckDuckGo,不引入 Bing。
2026-07-10 10:36:59 +08:00
GGHansome 315aba49c2 fix: claude model can not access image 2026-07-10 03:17:02 +08:00
leokunandGitHub 6ab537ad4e Merge pull request #180 from kael-odin/docs/extra-params-tooltip-wording
docs(model-adapter): fix extra-params tooltip wording (merge→override)
2026-07-09 20:26:49 +08:00
kael-odin 6d7a4e32fe docs(model-adapter): fix extra-params tooltip wording (merge→override)
ModelAdapterModal.vue described OpenAI extra params as "合并到" (merge into)
the request body, but the backend (request_override.go:applyExtraParams)
writes body[name] = value — same-name fields are overridden, not merged.
This contradicted ModelEditor.vue's tooltip ("覆盖到...同名字段以这里为准")
and risked users expecting array fields like `tools` to be appended,
which would silently replace the built-in function tools.

Align the modal tooltip with the editor view and the actual override
semantics.
2026-07-08 09:30:37 +08:00
kael-odin 50ab63de3d fix(anthropic): apply thinking config on override path, symmetric with openai
AnthropicAdapter.Stream built thinking config (buildAnthropicThinkingConfig)
and wrote it into body only inside the `if len(body)==0` normal-construction
block. The RequestBodyOverride branch skipped it entirely — a disabled effort
on the override path left whatever thinking config the override body carried,
violating user intent and diverging from openai.go, where
applyOpenAIThinkingDisable runs unconditionally after both branches.

Also, on the normal path, disabled only wrote thinking:{type:disabled} but
left a stale output_config (set by a prior adaptive turn or by
AnthropicExtraParams) in place — an explicitly disabled request could still
carry output_config.effort=high, a contradictory payload.

Fix:
- Extract applyAnthropicThinkingConfig(body, req), called unconditionally
  after the override/normal block, mirroring openai.go:1860.
- disabled: force thinking:{type:"disabled"}, delete output_config, set
  thinking_disabled_provider_param=thinking.type knob.
- adaptive (AnthropicThinkingEffort non-empty): write
  thinking:{type:adaptive,display:summarized} + output_config.
- empty effort: no-op.
- buildAnthropicThinkingConfig retained for stable-message-count / message
  normalization signals inside the normal block.

Tests: anthropic_thinking_disable_test.go covers all three branches, alias
normalization (off→disabled), disabled overriding existing adaptive config,
and the override-path symmetry scenario.
2026-07-08 09:12:03 +08:00
kael-odin 802daa8fcd fix(openai): recognize MiMo in thinking disable
openAIThinkingDisableKind only matched dashscope/qwen/aliyun,
deepseek/bigmodel/z.ai/zhipu, and gpt-5.1+/gpt-6. Xiaomi MiMo
(api.xiaomimimo.com, model mimo-*) fell through to the default
branch, so when a user set reasoning=disabled the adapter wrote
no disable field and MiMo kept thinking enabled (extra latency +
reasoning token cost contrary to user intent).

Add xiaomimimo/mimo to the thinking_type case — MiMo uses the same
thinking:{type:"disabled"} shape as deepseek/glm, confirmed in
MiMo docs.

- openai.go: +2 conditions in openAIThinkingDisableKind
- openai_thinking_disable_test.go: cover MiMo (base/model/ultraspeed),
  regress deepseek/glm/z.ai/qwen/gpt-5.1/gpt-6/unknown, and verify
  applyOpenAIThinkingDisable writes thinking:{type:disabled} +
  deletes reasoning_effort for MiMo
2026-07-07 18:13:21 +08:00
leokunandGitHub eb2c236699 Merge pull request #168 from Sxuan-Coder/main
feat(openai): 支持"自定义路径"endpoint 以兼容非标准 OpenAI 兼容服务
2026-07-06 10:38:28 +08:00
上玄 86a655fb5d refactor: remove endpoint tests and enhance URL handling
- Deleted outdated test files for OpenAI endpoint functionality.
- Improved the OpenAIEndpointURL function to better handle custom paths and version segments.
- Introduced a new helper function, stripEndpointVersionPrefix, to streamline endpoint processing.
2026-07-04 19:03:14 +08:00
上玄 c50e7d0606 feat: add support for custom OpenAI endpoint path
- Introduced a new endpoint option for OpenAI integrations, allowing users to specify a custom path.
- Updated relevant components and validation logic to accommodate the new endpoint.
- Enhanced documentation and error messages to reflect the addition of the custom endpoint option.
2026-07-04 18:51:41 +08:00
leokun 4ec7b4289f v0.0.39 2026-07-01 15:31:24 +08:00
leokun 6bebe37bfc Merge branches 'main' and 'main' of github.com:leookun/cursor-byok 2026-07-01 14:25:30 +08:00
50 changed files with 2754 additions and 601 deletions
+42
View File
@@ -0,0 +1,42 @@
---
name: i18n-requirements
description: Use when adding or changing frontend UI text, locale support, translation JSON, the static i18n scanner, language selection, or native tray labels in this repository; keeps source messages, generated catalogs, translations, and runtime locale registration consistent.
---
# I18n Requirements
## Source Messages
- Treat `zh-CN` as the only source locale.
- Write user-visible frontend text as Chinese source literals in scanned files under `frontend/src/`.
- Do not branch on locale or hard-code English, Japanese, Russian, or other translated UI text in components or state modules.
- Let `frontend/plugins/static-i18n-plugin.js` replace source literals with runtime helpers. Do not hand-write generated message IDs in application code.
- Keep internal matching tokens out of the catalog. Use a regex for Chinese protocol/error matching instead of a user-visible string literal when the text is not intended for display.
- Do not place ordinary user-visible source messages under `frontend/src/i18n/`; the scanner excludes that directory. Native language names in `LOCALE_OPTIONS` are an intentional exception.
## Generated Catalogs
- Treat `frontend/src/i18n/generated/catalog.json` and the source-locale entries as scanner output. Do not manually edit catalog references or message IDs.
- Run `npm run build` from `frontend/` after changing UI text. The build must run with `--scan` and update every locale file.
- Preserve every placeholder exactly across locales, including `{0}`, `{1}`, newlines, and formula fragments such as `${1}`.
- Provide a non-empty translation for every catalog key in every non-source locale. Do not rely on the Chinese fallback for completed locale support.
## Adding A Locale
Update all of these integration points together:
- `SUPPORTED_LOCALES` in `frontend/plugins/static-i18n-plugin.js`.
- `SUPPORTED_LOCALES` and `LOCALE_OPTIONS` in `frontend/src/i18n/config.js`.
- The locale JSON import, `localeMessages`, and primary-language mapping in `frontend/src/i18n/runtime.js`.
- `frontend/src/i18n/locales/<locale>.json` with the complete catalog key set.
- Native tray labels in `internal/app/runner.go`.
## Verification
After the scan build:
1. Confirm `npm run build` succeeds.
2. Confirm every locale JSON has the same keys as `catalog.json`.
3. Confirm non-source locale files contain no empty values.
4. Confirm translated placeholders match the source entry placeholders.
5. Run the build twice when scanner behavior changed and confirm generated files are stable.
@@ -0,0 +1,4 @@
interface:
display_name: "I18n Requirements"
short_description: "Keep UI translations and generated catalogs in sync"
default_prompt: "Use $i18n-requirements to update localized UI text safely."
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell -NoProfile -ExecutionPolicy Bypass -Command "$env:Path = [System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path','User') + ';$HOME\go\bin'; task build"
pause
+2
View File
@@ -0,0 +1,2 @@
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + ";$HOME\go\bin"
task build
+1 -1
View File
@@ -8,7 +8,7 @@ info:
description: "Cursor助手"
copyright: "© 2026, Cursor助手"
comments: "Cursor助手"
version: "0.0.38"
version: "0.0.41"
dev_mode:
root_path: .
+2 -2
View File
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.0.38</string>
<string>0.0.41</string>
<key>CFBundleVersion</key>
<string>0.0.38</string>
<string>0.0.41</string>
<key>LSMinimumSystemVersion</key>
<string>12.0.0</string>
<key>LSUIElement</key>
+2 -2
View File
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.0.38</string>
<string>0.0.41</string>
<key>CFBundleVersion</key>
<string>0.0.38</string>
<string>0.0.41</string>
<key>LSMinimumSystemVersion</key>
<string>12.0.0</string>
<key>LSUIElement</key>
+1 -1
View File
@@ -6,7 +6,7 @@
name: "Cursor助手"
arch: ${GOARCH}
platform: "linux"
version: "0.0.38"
version: "0.0.41"
section: "default"
priority: "extra"
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
+2 -2
View File
@@ -1,10 +1,10 @@
{
"fixed": {
"file_version": "0.0.38"
"file_version": "0.0.41"
},
"info": {
"0000": {
"ProductVersion": "0.0.38",
"ProductVersion": "0.0.41",
"CompanyName": "Cursor助手",
"FileDescription": "Cursor助手",
"LegalCopyright": "© 2026, Cursor助手",
+1 -1
View File
@@ -14,7 +14,7 @@
!define INFO_PRODUCTNAME "Cursor助手"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "0.0.38"
!define INFO_PRODUCTVERSION "0.0.41"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "© 2026, Cursor助手"
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.cursor.wuxianxubei" version="0.0.38" processorArchitecture="*"/>
<assemblyIdentity type="win32" name="com.cursor.wuxianxubei" version="0.0.41" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
+2 -2
View File
@@ -5,8 +5,8 @@
"type": "module",
"scripts": {
"dev": "vite",
"build:dev": "node ./scripts/run-vite-build.mjs --minify false --mode development",
"build": "node ./scripts/run-vite-build.mjs --mode production",
"build:dev": "node ./scripts/run-vite-build.mjs --scan --minify false --mode development",
"build": "node ./scripts/run-vite-build.mjs --scan --mode production",
"preview": "vite preview"
},
"dependencies": {
+26 -17
View File
@@ -11,7 +11,7 @@ import { normalizePath } from "vite";
const traverse = traverseModule.default ?? traverseModule;
const SOURCE_LANGUAGE = "zh-CN";
const SUPPORTED_LOCALES = ["zh-CN", "en-US", "ja-JP"];
const SUPPORTED_LOCALES = ["zh-CN", "en-US", "ja-JP", "ru-RU"];
const HAN_REGEX = /\p{Script=Han}/u;
const JS_HELPERS = {
localized: "__i18nLocalized",
@@ -490,22 +490,34 @@ function walkTemplateNode(node, visitor) {
}
}
function transformVueTemplate(templateCode, filePath, rootDir) {
function resolveAbsoluteLoc(sourceCode, sourceOffset, relativeOffset) {
const absoluteOffset = sourceOffset + relativeOffset;
const prefix = sourceCode.slice(0, absoluteOffset);
const lastLineBreak = prefix.lastIndexOf("\n");
return {
line: prefix.split("\n").length,
column: absoluteOffset - lastLineBreak,
};
}
function transformVueTemplate(templateCode, filePath, rootDir, options = {}) {
const ast = parseTemplate(templateCode, { comments: true });
const replacements = [];
const records = [];
const sourceCode = options.sourceCode ?? templateCode;
const sourceOffset = options.sourceOffset ?? 0;
const resolveLoc = (node, offset = 0) =>
resolveAbsoluteLoc(sourceCode, sourceOffset, node.loc.start.offset + offset);
walkTemplateNode(ast, (node, parent) => {
if (node.type === 2 && containsHan(node.content)) {
const canonical = node.content.trim();
const record = buildMessageRecord(
filePath,
rootDir,
node.content.trim(),
canonical,
0,
{
line: node.loc.start.line,
column: node.loc.start.column + 1,
},
resolveLoc(node, node.content.indexOf(canonical)),
);
const replacement = createTextNodeReplacement(node.loc.source, record);
if (!replacement) {
@@ -527,10 +539,7 @@ function transformVueTemplate(templateCode, filePath, rootDir) {
rootDir,
node.value.content,
0,
{
line: node.loc.start.line,
column: node.loc.start.column + 1,
},
resolveLoc(node),
);
records.push(record);
replacements.push({
@@ -552,10 +561,7 @@ function transformVueTemplate(templateCode, filePath, rootDir) {
node.content,
filePath,
rootDir,
{
line: node.loc.start.line,
column: node.loc.start.column + 1,
},
resolveLoc(node),
);
if (!transformed.changed) {
return;
@@ -585,7 +591,10 @@ function transformVueSFC(code, filePath, rootDir) {
let changed = false;
if (descriptor.template) {
const templateResult = transformVueTemplate(descriptor.template.content, filePath, rootDir);
const templateResult = transformVueTemplate(descriptor.template.content, filePath, rootDir, {
sourceCode: code,
sourceOffset: descriptor.template.loc.start.offset,
});
records.push(...templateResult.records);
if (templateResult.changed) {
changed = true;
@@ -651,7 +660,7 @@ function collectCatalogRecords(rootDir) {
return records;
}
function syncCatalogFiles(rootDir) {
export function syncCatalogFiles(rootDir) {
const records = collectCatalogRecords(rootDir);
const catalog = mergeMessageRecords(records);
const generatedDir = path.join(rootDir, "src/i18n/generated");
@@ -273,7 +273,6 @@ const hasHomeAd = computed(() => normalizedHomeAds.value.length > 0);
<div
class="flex-1 center-row justify-end shrink-0 gap-2 text-xs text-[#6f6f6f] pr-4 w-[200px]"
>
<span>刷新统计</span>
<button
type="button"
class="center-row justify-center h-[24px] w-[24px] rounded-[6px] border border-[#3b3b3b] bg-[#242424] text-[#9d9d9d] transition-colors duration-150 hover:border-[#4c4c4c] hover:text-white disabled:cursor-not-allowed disabled:opacity-60"
@@ -7,6 +7,7 @@ import {
createEmptyModelAdapter,
normalizeModelAdapter,
OPENAI_ENDPOINT_CHAT_COMPLETIONS,
OPENAI_ENDPOINT_CUSTOM,
OPENAI_ENDPOINT_RESPONSES,
OPENAI_EXTRA_PARAMS_DEFAULT_JSON,
} from "@/state/appState";
@@ -22,6 +23,7 @@ const reasoningEffortOptions = [
{ label: "中", value: "medium", icon: "icon-[mdi--head-lightbulb-outline]" },
{ label: "高", value: "high", icon: "icon-[mdi--brain]" },
{ label: "极高", value: "xhigh", icon: "icon-[mdi--head-cog-outline]" },
{ label: "最高", value: "max", icon: "icon-[mdi--brain]" },
];
const anthropicThinkingEffortOptions = [
@@ -35,10 +37,11 @@ const anthropicThinkingEffortOptions = [
const openAIEndpointOptions = [
{ label: "/v1/responses", value: OPENAI_ENDPOINT_RESPONSES, icon: "icon-[mdi--api]" },
{ label: "/v1/chat/completions", value: OPENAI_ENDPOINT_CHAT_COMPLETIONS, icon: "icon-[mdi--message-text-outline]" },
{ label: "自定义路径", value: OPENAI_ENDPOINT_CUSTOM, icon: "icon-[mdi--pencil-outline]" },
];
const fieldTips = {
openAIExtraParams: "开启后会把 JSON 对象合并到 OpenAI 请求体。OpenAI service_tier 支持 auto、default、flex、scale、prioritypriority 可用于高优先级/Fast 类场景。",
openAIExtraParams: "开启后会把 JSON 对象覆盖到 OpenAI 请求体。同名字段以这里为准。OpenAI service_tier 支持 auto、default、flex、scale、prioritypriority 可用于高优先级/Fast 类场景。",
};
const props = defineProps({
+2 -1
View File
@@ -2,9 +2,10 @@ export const LOCALE_STORAGE_KEY = "cursor-client:locale:v1";
export const LOCALE_STORAGE_SOURCE_KEY = "cursor-client:locale-source:v1";
export const SOURCE_LOCALE = "zh-CN";
export const DEFAULT_LOCALE = "en-US";
export const SUPPORTED_LOCALES = ["zh-CN", "en-US", "ja-JP"];
export const SUPPORTED_LOCALES = ["zh-CN", "en-US", "ja-JP", "ru-RU"];
export const LOCALE_OPTIONS = [
{ label: "简体中文", value: "zh-CN" },
{ label: "English", value: "en-US" },
{ label: "日本語", value: "ja-JP" },
{ label: "Русский", value: "ru-RU" },
];
File diff suppressed because it is too large Load Diff
+61 -10
View File
@@ -2,7 +2,6 @@
"02216368edc68816": "No release notes",
"02bc2e95bf49e587": "No",
"03b11112dc970014": "Base URL",
"045261cc748d4300": "Token budget allowed during Anthropic's thinking phase. Leave blank to use the default.",
"047ec6b71d0cec08": "Control whether requests on the whitelist main path go through the local service or return to the original Cursor upstream endpoint",
"04f632dd4f034d5e": "{0} context window must be a positive integer",
"051836569928a9f9": "Edit",
@@ -10,35 +9,39 @@
"05c8a9238c702efa": "Direct Cursor Mode",
"0647728439b5da2e": "You can configure the routing mode and model channels. Runtime logs are stored in",
"092b520558eff5f2": "Not tested",
"09ebc2643631ba25": "Cost Estimate",
"0b0e7478e41fe677": "{0} tooltip text cannot be empty",
"0c3b4cf7aa259edb": "Operation failed",
"0dde813d719dbd01": "Failed to open homepage",
"0e40a09ab4e664ab": "Duplicate model channel detected. Check the combination of url, modelID, apiKey, and displayName",
"1117a2f86030d03b": "Cache reads and writes are included in Prompt-side statistics.",
"11afd2a534395b18": "Valid",
"124be3f86f197802": "Token Usage",
"13b61c5f697b6700": "Cache Hit Rate",
"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",
"18b7312022cd1840": "Start Service",
"1af38868896cf53d": "Routing mode only supports local or upstream",
"1b7d24b212e52c54": "{0} reasoning effort only supports low, medium, high, and xhigh",
"1baddde657dd2720": "Current outbound requests use system proxy",
"1bc77f5ab979f4c1": "Add Model Settings",
"1e238093b79b3165": "Uses 65536 by default when left blank",
"21296ab18ad9af25": "Extra Params JSON",
"24343a2096988d42": "Failed to open",
"253d4a3428c648fb": "Cache write tokens: {0}",
"258e4620e2108793": "Uses 4096 by default when left blank",
"26a3855aed1d8d17": "Service not running",
"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.",
"2caeaec539e78898": "Thinking Budget Token",
"2a24519398684ed5": "Visit Homepage",
"2cd0f3be8738a86c": "Cancel",
"2d706f7981b45a7b": "Local settings saved",
"2f9daa828907b93f": "Delete",
"30bb57a50caedc38": "e.g. For everyday code completion and Q&A",
"3275d7e70bbdc3d5": "{0}'s {1}",
"32b3c9a50003f77a": "Output tokens are estimated",
"33d2d273e2bd5f88": "Failed to open user guide",
"3463c5585c246df9": "Total: {0}",
"3468b57e3edbc599": "Aggregated from turn summaries scanned from the history.",
"35076178fe79a210": "Configuration changed. Please test again.",
"358f07b2c1445ab1": "Author's Message",
"36c149a9b3e8dca0": "models configured yet.",
"37d23612f78a2e63": "Restart Now to Update",
"392d0dceb45998d3": "Extreme",
@@ -49,99 +52,137 @@
"3d13868593ae4eeb": "Interface Language",
"3ea83f9f55062582": "Release date: {0}",
"3edda85621fd03b2": "model adapters",
"3fd47edce45b3603": "Close",
"42aa8e01e98c0d8c": "Total Duration",
"438bb77c1ecdfab0": "Cache Write: {0} × ${1}/1M = {2}",
"46056425a48ef05b": "Currently displayed using the reuse rate definition",
"468adaa418ee1475": "e.g. https://api.openai.com/v1",
"472642d58d3d5a6d": "Formula: {0}",
"4923eeb7bd75cccd": "{0} model ID cannot be empty",
"497c85690c4cc0fc": "No data",
"4b8d11bf235e9213": "Current hit rate: {0}",
"4c0a929bb86ce912": "Current: {0}",
"4d2b6e53be6002e5": "Cache Statistics Strategy: {0} ({1})",
"4d8c1c5b42830791": "Unknown",
"4f0982ba1d37e51b": "Current outbound requests use environment variable proxy",
"51194c3ad014fb29": "Retest required",
"5205125c0e91d346": "Maximum tokens an Anthropic model may generate in a single response. Leave blank to use the default.",
"56627c94a9decee6": "Max Output Tokens",
"593a972852ba0004": "Cursor Assistant | Permanently Free | Custom API",
"59a2195a01a8b35b": "{0} must be a valid JSON object",
"5aa8f5590c940829": "Non-cache Input: {0}",
"5beb1206c532729f": "Maximum number of tokens allowed in a single response. Leave blank to use the default.",
"5d1687a4a41883fd": "Stopping...",
"5e709712ce012f5d": "Current outbound requests use environment variable proxy: {0}",
"6106f0a12583a334": "Refresh failed",
"62873083fcaed27d": "Session Statistics",
"6309a3bb5ba4c714": "Save failed",
"636e3deffc1e960a": "Model {0}",
"63d90d977348ab1f": "Duplicate",
"64857b4338678314": "The value of custom header {0} must be a string",
"64d2730f2ae37997": "Raw Response",
"65cc5fd2e6ce6e75": "Backend started, proxy not started",
"66af574b8948fe83": "{0} API key cannot be empty",
"6744b4c6a9aa0038": "Disabled",
"675109292da4eb36": "Not tested yet",
"699fe7ade5407687": "Direct Mode",
"6a7b96f399e58138": "e.g. sk-xxxxxx",
"6aa8f49cc992dfd7": "Test",
"6ae23d6d7cb18592": "Service error",
"6d7872164df9b36e": "Normal Input: {0} × ${1}/1M = {2}",
"6e584e3d5ce64aa0": "Save Settings",
"6ec87609a8769425": "Default {0} / Count as creation {1}",
"72f6c3525c0192a7": "When enabled, cache creation is included in the denominator",
"737225e2904673fc": "Estimated output tokens: {0}",
"7520bd50a5ee5471": "Stop testing {0}/{1}",
"753d8bb0da9913ce": "Duplication failed",
"774d6e1b7cb89751": "Default Definition",
"77c9e582e85583af": "Test failed",
"7a26bf794e9fb6bf": "Used only for display in the UI, so you can distinguish different models.",
"7b6187c41e88b70c": "Testing...",
"7bf8e2c07e084d09": "Model Editor",
"7df7641e5e741346": "Cache Read / (Cache Read + Cache Creation + Non-cache Input)",
"7e9e334aeb0bdc07": "Service operation failed",
"7f68ebad19ba6bcd": "Check for Updates",
"80296f4aa3f4543b": "Cache Read/Write",
"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.",
"8672864e90417138": "Max",
"86df7ec743047234": "Service running",
"87ed126f7bd1121e": "Routing Mode",
"899add6275682210": "Uses 200000 by default when left blank",
"8a4ef3e48e4e8a5a": "Enabled",
"8c0d84831a3c3d5b": "Currently in Local Service Mode",
"8c1935935600e336": "Model Test",
"8cbcf741e727dbf7": "Model Settings",
"8d1de152be6360ce": "Valid ratio: {0}",
"8e2dc7b0d2e8f6f8": "e.g. OpenAI - GPT-4.1",
"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.",
"8faa670b512b6b9b": "Open Model Settings",
"907395551d0f3a93": "{0} OpenAI endpoint only supports /v1/responses, /v1/chat/completions, or a custom path starting with /",
"917b1c1f18d0276b": "Saving...",
"9196835e388d2550": "Test All",
"91cba5c107a51892": "/ Invalid",
"92059fe6cd713db4": "The model name actually sent to the server, for example gpt-4.1 or claude-sonnet.",
"93e08803675e378b": "Model ID",
"93faf55cd25c8319": "This software is completely free. If you were charged, you were likely scammed.\nWelcome to visit the author's homepage at https://space.bilibili.com/311706663/upload/video\nto see more updates, sharing guides, and future content.",
"942ff2d88baca0c6": "Checking for updates...",
"970388573a3c88c9": "Cache Read: {0} × ${1}/1M = {2}",
"9730c15f3c1963a1": "Max",
"986678eccf56dc28": "Service status is being updated. Please try again later.",
"991e374fce0f4492": "Cursor Assistant",
"9970736b36ff2b68": "The base URL of the model service, usually an OpenAI- or Anthropic-compatible endpoint.",
"9a6e7d6c17471711": "Currently displayed using the default hit rate definition",
"9b17fa889b307f7f": "Valid turns: {0}",
"9c38b6e9bf94abec": "Switched to Direct Cursor Mode",
"9cd4ac17428b86e4": "Cache hit rate = cache read tokens / prompt tokens",
"9c41b3a9e12ac994": "Thinking Effort",
"9d2ca261281a158a": "Later",
"9d2fb46c0ba890b9": "Custom Path",
"9dc0825fba5422e4": "Loading...",
"9e02529bcaef36c6": "Duplicate model channel detected. Please check the combination of url, modelID, apiKey, displayName, and endpoint",
"a026f37e613cf48b": "Output Tokens",
"a0d36236c523667c": "{0} Anthropic thinking effort only supports low, medium, high, xhigh, max",
"a1a038dfa16c3ede": "You're already on the latest version (v{0}).",
"a3030bf8f16dc63c": "Save",
"a325d25c69e7256d": "Model settings not found; cannot duplicate",
"a4dd8bb7e8b6eb31": "Show API Key",
"a54d745d9a9249e2": "When enabled, the JSON object will override the Anthropic request body. Duplicate fields are determined by this field.",
"a55a88237df85d98": "Currently in Direct Mode",
"a567bdaa11367f26": "Medium",
"a5f1bd344c92e195": "The API key required to call this model service.",
"a693d69af48bfe48": "Save and Test",
"a98585871c5313ff": "Display Name",
"aa9e366f68d3d097": "Low",
"ab607d54d86dc789": "Author leookun",
"ac217e4d1ca410f1": "New version available",
"ad79540418be700a": "Open the settings folder, or manage model settings separately",
"ae5a738238463a92": "Hide API Key",
"aed55419ce62f08e": "Switching...",
"b10041a13f5c55b1": "Model Output: {0} × ${1}/1M = {2}",
"b1c27820fec23edb": "High",
"b42049dcf8a05ef7": "Switched to Local Service Mode",
"b5409d4049286061": "Custom Path (Please enter the full request URL)",
"b571037dc396a00c": "Total request tokens include both prompt and model output.",
"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.",
"b870928f8f9a24c4": "API Endpoint",
"b90a8ac9c488ce46": "Select language",
"ba3c66f90fd11725": "System proxy identified",
"ba40014ff496f64e": "Type",
"bb074b86a98f6911": "Context Window",
"bbacfde55a92869f": "A higher hit rate means more repeated context is being reused.",
"bc87a4121a0873b3": "Refresh Stats",
"bd4464ea88d3f24a": "Total turns: {0}",
"bddd504af0c92fd0": "System PAC/automatic proxy detected; current version is handled as a direct connection",
"bef280f9eb392495": "Conversation Turns",
"c228558cf257fc49": "Delete failed",
"c3e9c3c60020b8b7": "Select Mode",
"c5af02060847d167": "Thinking effort for Anthropic adaptive thinking. Requests will consistently use the new thinking.type=adaptive.",
"c69f5bce63b9f14c": "Settings Folder",
"c6f743953145f40b": "e.g. 4096 (leave blank to use the default)",
"c8c14507b2d37395": "Reasoning Effort",
"c98e118e0a43f078": "Model",
"c9dd59beefd7144f": "Cache Read / (Cache Read + Non-cache Input)",
"ca00a39fcea70dc6": "Starting...",
"ca1d1059408b3837": "Invalid turns: {0}",
"cd7ca5fb221e1c53": "{0} cannot be empty",
"ce46f23cea3bf3c5": "When enabled, Cursor connects directly to the official service. Do not enable this.",
"d0325067fed88e5a": "Cache hit rate {0}",
"d08fd4224abcd69d": "Switch failed",
@@ -151,25 +192,35 @@
"d3209b935ae86797": "Model settings not found; cannot delete",
"d373809ab86ba93b": "Copy",
"d3b1da3088ddd334": "Model test failed",
"d53d32f1a1211371": "Custom Headers JSON",
"d7889896c5b7732a": "Anthropic Extra Params JSON",
"d7da2aabd35772ec": "e.g. 200000 (leave blank to use the default)",
"d95e5cb6bdcee553": "Include Cache Creation",
"da590a8fe3ce4de0": "Please select",
"daede9881787abe7": "Notes",
"dbb4b5be9b5723dc": "{0} reasoning effort only supports low, medium, high, xhigh, and max",
"dbee6e7139243362": "{0} base URL cannot be empty",
"dc82c5e8fb2ab777": "Version: v{0}",
"de8184da1ef88d03": "Configured",
"e01c5dae36cf8c35": "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.",
"e14c41ef2b7253c9": "Total request tokens: {0}",
"e406825e0a72d2c2": "Local Settings",
"e552c2accdbf5178": "Add Model",
"e6faccfddce722e8": "Cache read tokens: {0}",
"eaffd48cd2ea9f1a": "e.g. https://api.anthropic.com",
"eb1be07f2ca6e506": "Estimated based on Claude Opus 4.7 pricing.",
"ec3b17a75db49e24": "{0} t/s | First token {1}",
"ec99e5c45d648fd6": "Update failed",
"ee95057c6b0335d2": "Current outbound requests use system proxy: {0}",
"f1e0fc261d42fe29": "Notes shown when hovering over the model list.",
"f363622480699c52": "Reasoning effort only applies to some models that support reasoning_effort. Not all models do. Higher values are usually more stable, but may also be slower.",
"f3a76d896853c1df": "Miss",
"f3fae6cccb9004b1": "Custom header name cannot be empty",
"f474a4108aba4c4c": "Stop Service",
"f4f0ead1116b5b62": "Enable",
"f56c6c82203b33f6": "Notice",
"f61e03f047b786d5": "{0} max output tokens must be a positive integer",
"f6e1c8b1a6970db5": "Current outbound requests do not use system proxy",
"fac2a67ad87807c4": "OK",
"fb7a4c81729ed0ca": "Stopping...",
"fec45092945f8790": "User Guide"
+62 -11
View File
@@ -2,7 +2,6 @@
"02216368edc68816": "更新内容はありません",
"02bc2e95bf49e587": "まだ",
"03b11112dc970014": "ベース URL",
"045261cc748d4300": "Anthropic の思考フェーズで使用できる予算 Token 数。空欄の場合はデフォルト値を使用します。",
"047ec6b71d0cec08": "ホワイトリストのメイン経路のリクエストをローカルサービス経由にするか、元の Cursor 上流アドレスに戻すかを制御します",
"04f632dd4f034d5e": "{0} のコンテキストウィンドウは正の整数である必要があります",
"051836569928a9f9": "編集",
@@ -10,35 +9,39 @@
"05c8a9238c702efa": "Cursor 直結モード",
"0647728439b5da2e": "ルーティングモードとモデルチャネルを設定できます。実行ログは次にあります",
"092b520558eff5f2": "未テスト",
"09ebc2643631ba25": "価値見積もり",
"0b0e7478e41fe677": "{0} のツールチップは必須です",
"0c3b4cf7aa259edb": "操作に失敗しました",
"0dde813d719dbd01": "ホームページを開けませんでした",
"0e40a09ab4e664ab": "モデルチャネルが重複しています。url、modelID、apiKey、displayName の組み合わせを確認してください",
"1117a2f86030d03b": "キャッシュの読み書きは Prompt 側の統計に含まれます。",
"11afd2a534395b18": "有効",
"124be3f86f197802": "Token 使用量",
"13b61c5f697b6700": "キャッシュヒット率",
"15d124b200ddabed": "モデルが1回のリクエストで受け取れる最大コンテキスト Token 数。空欄の場合はデフォルト値を使用します。",
"185aebe19c77425d": "{0}はJSONオブジェクトである必要があります",
"18b7312022cd1840": "サービスを開始",
"1af38868896cf53d": "ルーティングモードは local または upstream のみサポートします",
"1b7d24b212e52c54": "{0} の推論強度は low、medium、high、xhigh のみサポートします",
"1baddde657dd2720": "現在のアウトバウンドリクエストはシステムプロキシを使用しています",
"1bc77f5ab979f4c1": "モデル設定を追加",
"1e238093b79b3165": "空欄で 65536",
"21296ab18ad9af25": "追加パラメータ JSON",
"24343a2096988d42": "開けませんでした",
"253d4a3428c648fb": "キャッシュ書き込み Token: {0}",
"258e4620e2108793": "空欄で 4096",
"26a3855aed1d8d17": "サービスは起動していません",
"281eb6d08c9960d0": "{0} の思考予算 Token は正の整数である必要があります",
"28aeffc70ceb4267": "この画面の表示言語を切り替えます。設定はすぐに反映され、この端末に保存されます",
"2caeaec539e78898": "思考予算 Token",
"2a24519398684ed5": "ホームページへ",
"2cd0f3be8738a86c": "キャンセル",
"2d706f7981b45a7b": "ローカル設定を保存しました",
"2f9daa828907b93f": "削除",
"30bb57a50caedc38": "例: 日常的なコード補完や Q&A 用",
"3275d7e70bbdc3d5": "{0} の {1}",
"32b3c9a50003f77a": "出力 Token は推定値です",
"33d2d273e2bd5f88": "ユーザーガイドを開けませんでした",
"3463c5585c246df9": "合計:{0}",
"3468b57e3edbc599": "履歴からスキャンした各ターンの summary を集計しています。",
"35076178fe79a210": "設定が変更されました。再テストしてください",
"358f07b2c1445ab1": "著者からのメッセージ",
"36c149a9b3e8dca0": "モデルは設定されていません。",
"37d23612f78a2e63": "今すぐ再起動して更新",
"392d0dceb45998d3": "最高",
@@ -49,99 +52,137 @@
"3d13868593ae4eeb": "表示言語",
"3ea83f9f55062582": "公開日時: {0}",
"3edda85621fd03b2": "件のモデルアダプター",
"3fd47edce45b3603": "閉じる",
"42aa8e01e98c0d8c": "総所要時間",
"438bb77c1ecdfab0": "キャッシュ書き込み:{0} × ${1}/1M = {2}",
"46056425a48ef05b": "現在、再利用率の定義に従って表示されています",
"468adaa418ee1475": "例: https://api.openai.com/v1",
"472642d58d3d5a6d": "数式:{0}",
"4923eeb7bd75cccd": "{0} のモデル ID は必須です",
"497c85690c4cc0fc": "データなし",
"4b8d11bf235e9213": "現在のヒット率: {0}",
"4c0a929bb86ce912": "現在{0}",
"4d2b6e53be6002e5": "キャッシュ統計ポリシー:{0}{1})",
"4d8c1c5b42830791": "不明",
"4f0982ba1d37e51b": "現在のアウトバウンドリクエストは環境変数プロキシを使用しています",
"51194c3ad014fb29": "再テストが必要",
"5205125c0e91d346": "Anthropic モデルが1回の応答で生成できる最大 Token 数。空欄の場合はデフォルト値を使用します。",
"56627c94a9decee6": "最大出力 Token",
"593a972852ba0004": "Cursor アシスタント | 永久無料 | カスタム API",
"59a2195a01a8b35b": "{0}は有効なJSONオブジェクトである必要があります",
"5aa8f5590c940829": "非キャッシュ入力:{0}",
"5beb1206c532729f": "1回の応答で生成できる最大 Token 数。空欄の場合はデフォルト値を使用します。",
"5d1687a4a41883fd": "停止中...",
"5e709712ce012f5d": "現在のアウトバウンドリクエストは環境変数プロキシを使用しています:{0}",
"6106f0a12583a334": "再読み込みに失敗しました",
"62873083fcaed27d": "セッション統計",
"6309a3bb5ba4c714": "保存に失敗しました",
"636e3deffc1e960a": "モデル {0}",
"63d90d977348ab1f": "複製",
"64857b4338678314": "カスタムヘッダー {0} の値は文字列である必要があります",
"64d2730f2ae37997": "生のレスポンス",
"65cc5fd2e6ce6e75": "バックエンドは起動済み、プロキシは未起動です",
"66af574b8948fe83": "{0} の API キーは必須です",
"6744b4c6a9aa0038": "無効化",
"675109292da4eb36": "まだテストしていません",
"699fe7ade5407687": "直結モード",
"6a7b96f399e58138": "例: sk-xxxxxx",
"6aa8f49cc992dfd7": "テスト",
"6ae23d6d7cb18592": "サービスエラー",
"6d7872164df9b36e": "通常入力:{0} × ${1}/1M = {2}",
"6e584e3d5ce64aa0": "設定を保存",
"6ec87609a8769425": "デフォルト {0} / 作成カウント {1}",
"72f6c3525c0192a7": "有効にすると、キャッシュ作成が分母に含まれます",
"737225e2904673fc": "推定出力 Token: {0}",
"7520bd50a5ee5471": "テスト停止 {0}/{1}",
"753d8bb0da9913ce": "複製に失敗しました",
"774d6e1b7cb89751": "デフォルト定義",
"77c9e582e85583af": "テスト失敗",
"7a26bf794e9fb6bf": "UI 上の表示専用で、異なるモデルを見分けやすくします。",
"7b6187c41e88b70c": "テスト中...",
"7bf8e2c07e084d09": "モデル編集",
"7df7641e5e741346": "キャッシュ読み取り / (キャッシュ読み取り + キャッシュ作成 + 非キャッシュ入力)",
"7e9e334aeb0bdc07": "サービス操作に失敗しました",
"7f68ebad19ba6bcd": "アップデートを確認",
"80296f4aa3f4543b": "キャッシュ読み書き",
"81123c56d5d880d0": "API キー",
"8139cb3dd11f5a67": "有効にすると、JSONオブジェクトが最終的なリクエストヘッダーを上書きします。同名のヘッダーはこの設定が優先され、値は文字列である必要があります。",
"8672864e90417138": "最大",
"86df7ec743047234": "サービス稼働中",
"87ed126f7bd1121e": "ルーティングモード",
"899add6275682210": "空欄で 200000",
"8a4ef3e48e4e8a5a": "有効",
"8c0d84831a3c3d5b": "現在はローカルサービスモードです",
"8c1935935600e336": "モデルテスト",
"8cbcf741e727dbf7": "モデル設定",
"8d1de152be6360ce": "有効率: {0}",
"8e2dc7b0d2e8f6f8": "例: OpenAI - GPT-4.1",
"8f6f8d979c981ced": "コピーしました",
"8f8baf5d18dd0492": "有効にすると、JSONオブジェクトがOpenAIのリクエストボディを上書きします。同名のフィールドはこの設定が優先されます。OpenAIのservice_tierはauto、default、flex、scale、priorityをサポートしており、priorityは高優先度/Fastのシナリオで使用できます。",
"8faa670b512b6b9b": "モデル設定を開く",
"907395551d0f3a93": "{0} のOpenAIエンドポイントは、/v1/responses、/v1/chat/completions、または / で始まるカスタムパスのみをサポートしています",
"917b1c1f18d0276b": "保存中...",
"9196835e388d2550": "すべてテスト",
"91cba5c107a51892": "/ 異常",
"92059fe6cd713db4": "実際にサーバーへ送信されるモデル名です。例: gpt-4.1 または claude-sonnet。",
"93e08803675e378b": "モデル ID",
"93faf55cd25c8319": "このソフトウェアは完全に無料です。もし料金を請求された場合は、詐欺の可能性が高いです。\n著者のホームページ https://space.bilibili.com/311706663/upload/video にアクセスして、更新情報や利用方法などを確認してください。",
"942ff2d88baca0c6": "アップデートを確認中...",
"970388573a3c88c9": "キャッシュ読み取り:{0} × ${1}/1M = {2}",
"9730c15f3c1963a1": "最大",
"986678eccf56dc28": "サービス状態を更新中です。しばらくしてからもう一度お試しください",
"991e374fce0f4492": "Cursorアシスタント",
"9970736b36ff2b68": "モデルサービスの API ルート URL。通常は OpenAI または Anthropic 互換のエンドポイントです。",
"9a6e7d6c17471711": "現在、デフォルトのヒット率定義に従って表示されています",
"9b17fa889b307f7f": "有効ターン: {0}",
"9c38b6e9bf94abec": "Cursor 直結モードに切り替えました",
"9cd4ac17428b86e4": "キャッシュヒット率 = キャッシュ読込 Token / Prompt Token",
"9c41b3a9e12ac994": "思考強度",
"9d2ca261281a158a": "後で",
"9d2fb46c0ba890b9": "カスタムパス",
"9dc0825fba5422e4": "読み込み中...",
"9e02529bcaef36c6": "モデルチャネルが重複しています。url、modelID、apiKey、displayName、endpointの組み合わせを確認してください",
"a026f37e613cf48b": "出力 Token",
"a1a038dfa16c3ede": "すでに最新バージョンです(v{0})。",
"a0d36236c523667c": "{0} のAnthropic思考強度はlow、medium、high、xhigh、maxのみをサポートしています",
"a1a038dfa16c3ede": "すでに最新バージョン(v{0})です。",
"a3030bf8f16dc63c": "保存",
"a325d25c69e7256d": "モデル設定が存在しないため複製できません",
"a4dd8bb7e8b6eb31": "API キーを表示",
"a54d745d9a9249e2": "有効にすると、JSONオブジェクトがAnthropicのリクエストボディを上書きします。同名のフィールドはこの設定が優先されます。",
"a55a88237df85d98": "現在は直結モードです",
"a567bdaa11367f26": "中",
"a5f1bd344c92e195": "このモデルサービスを呼び出すために必要な API キーです。",
"a693d69af48bfe48": "保存してテスト",
"a98585871c5313ff": "表示名",
"aa9e366f68d3d097": "低",
"ab607d54d86dc789": "著者 leookun",
"ac217e4d1ca410f1": "新しいバージョンがあります",
"ad79540418be700a": "設定フォルダーを開くか、モデル設定を個別に管理できます",
"ae5a738238463a92": "API キーを隠す",
"aed55419ce62f08e": "切替中...",
"b10041a13f5c55b1": "モデル出力:{0} × ${1}/1M = {2}",
"b1c27820fec23edb": "高",
"b42049dcf8a05ef7": "ローカルサービスモードに切り替えました",
"b5409d4049286061": "カスタムパス(完全なリクエストURLを入力してください)",
"b571037dc396a00c": "総リクエスト Token には Prompt とモデル出力の両方が含まれます。",
"b765005f69fa971f": "例: gpt-4.1",
"b76a22622020f849": "インターフェースプロトコルのエンドポイントを選択します。「カスタムパス」を選択する場合は、APIアドレスバーに完全なリクエストURL/chat/completions または /responses のサフィックスを含む)を入力してください。システムは末尾 of セグメントに基づいてプロトコルタイプを自動的に判断します。",
"b870928f8f9a24c4": "APIエンドポイント",
"b90a8ac9c488ce46": "言語を選択",
"ba3c66f90fd11725": "システムプロキシを認識しました",
"ba40014ff496f64e": "タイプ",
"bb074b86a98f6911": "コンテキストウィンドウ",
"bbacfde55a92869f": "ヒット率が高いほど、重複するコンテキストがより多く再利用されていることを示します。",
"bc87a4121a0873b3": "統計を更新",
"bd4464ea88d3f24a": "総ターン: {0}",
"bddd504af0c92fd0": "システムのPAC/自動プロキシが検出されました。現在のバージョンは直接接続として処理されます",
"bef280f9eb392495": "会話ターン",
"c228558cf257fc49": "削除に失敗しました",
"c3e9c3c60020b8b7": "モードを選択",
"c5af02060847d167": "Anthropic adaptive thinkingの思考強度。リクエストは一貫して新しいthinking.type=adaptiveを使用します。",
"c69f5bce63b9f14c": "設定フォルダー",
"c6f743953145f40b": "例: 4096(空欄でデフォルト値)",
"c8c14507b2d37395": "推論強度",
"c98e118e0a43f078": "モデル",
"c9dd59beefd7144f": "キャッシュ読み取り / (キャッシュ読み取り + 非キャッシュ入力)",
"ca00a39fcea70dc6": "起動中...",
"ca1d1059408b3837": "異常ターン: {0}",
"cd7ca5fb221e1c53": "{0}は空にできません",
"ce46f23cea3bf3c5": "有効にすると、Cursor は公式サービスへ直接接続します。オンにしないでください",
"d0325067fed88e5a": "キャッシュヒット率 {0}",
"d08fd4224abcd69d": "切替に失敗しました",
@@ -151,25 +192,35 @@
"d3209b935ae86797": "モデル設定が存在しないため削除できません",
"d373809ab86ba93b": "コピー",
"d3b1da3088ddd334": "モデルテストに失敗しました",
"d53d32f1a1211371": "カスタムヘッダー JSON",
"d7889896c5b7732a": "Anthropic 追加パラメータ JSON",
"d7da2aabd35772ec": "例: 200000(空欄でデフォルト値)",
"d95e5cb6bdcee553": "キャッシュ作成を含める",
"da590a8fe3ce4de0": "選択してください",
"daede9881787abe7": "メモ",
"dbb4b5be9b5723dc": "{0} の推論強度は low、medium、high、xhigh、max のみサポートします",
"dbee6e7139243362": "{0} のベース URL は必須です",
"dc82c5e8fb2ab777": "バージョン: v{0}",
"de8184da1ef88d03": "設定済み",
"e01c5dae36cf8c35": "有効にすると、JSONオブジェクトがOpenAIのリクエストボディを上書きします。同名のフィールドはこの設定が優先されます。OpenAIのservice_tierはauto、default、flex、scale、priorityをサポートしています。",
"e14c41ef2b7253c9": "総リクエスト Token: {0}",
"e406825e0a72d2c2": "ローカル設定",
"e552c2accdbf5178": "モデルを追加",
"e6faccfddce722e8": "キャッシュ読込 Token: {0}",
"eaffd48cd2ea9f1a": "例: https://api.anthropic.com",
"eb1be07f2ca6e506": "Claude Opus 4.7の価格に基づいて見積もられます。",
"ec3b17a75db49e24": "{0} t/s | 初回 Token {1}",
"ec99e5c45d648fd6": "アップデートに失敗しました",
"ee95057c6b0335d2": "現在のアウトバウンドリクエストはシステムプロキシを使用しています:{0}",
"f1e0fc261d42fe29": "モデル一覧にホバーしたときに表示されるメモです。",
"f363622480699c52": "推論強度は reasoning_effort をサポートする一部のモデルでのみ有効です。すべてのモデルが対応しているわけではありません。値が高いほど安定しやすい反面、遅くなることがあります。",
"f3a76d896853c1df": "ミス",
"f3fae6cccb9004b1": "カスタムヘッダー名は空にできません",
"f474a4108aba4c4c": "サービスを停止",
"f4f0ead1116b5b62": "有効化",
"f56c6c82203b33f6": "お知らせ",
"f61e03f047b786d5": "{0} の最大出力 Token は正の整数である必要があります",
"f6e1c8b1a6970db5": "現在のアウトバウンドリクエストはシステムプロキシを使用していません",
"fac2a67ad87807c4": "OK",
"fb7a4c81729ed0ca": "停止中...",
"fec45092945f8790": "ユーザーガイド"
+227
View File
@@ -0,0 +1,227 @@
{
"02216368edc68816": "Нет примечаний к выпуску",
"02bc2e95bf49e587": "Пока нет настроенных",
"03b11112dc970014": "Базовый URL",
"047ec6b71d0cec08": "Определяет, будут ли запросы основного маршрута из белого списка проходить через локальный сервис или исходный сервер Cursor",
"04f632dd4f034d5e": "Размер контекстного окна {0} должен быть положительным целым числом",
"051836569928a9f9": "Изменить",
"054d763265603305": "например, 65536 (оставьте пустым для значения по умолчанию)",
"05c8a9238c702efa": "Прямое подключение к Cursor",
"0647728439b5da2e": "Здесь можно настроить режим маршрутизации и каналы моделей. Журналы выполнения находятся в",
"092b520558eff5f2": "Не проверено",
"09ebc2643631ba25": "Оценка стоимости",
"0b0e7478e41fe677": "Текст подсказки {0} не может быть пустым",
"0c3b4cf7aa259edb": "Не удалось выполнить операцию",
"0dde813d719dbd01": "Не удалось открыть домашнюю страницу",
"1117a2f86030d03b": "Чтение и запись кеша включены в статистику Prompt.",
"11afd2a534395b18": "Успешно",
"124be3f86f197802": "Использование токенов",
"13b61c5f697b6700": "Доля попаданий в кеш",
"15d124b200ddabed": "Максимальное число токенов контекста, которое модель может принять за один запрос. Оставьте поле пустым для значения по умолчанию.",
"185aebe19c77425d": "{0} должен быть объектом JSON",
"18b7312022cd1840": "Запустить сервис",
"1af38868896cf53d": "Режим маршрутизации поддерживает только local или upstream",
"1baddde657dd2720": "Исходящие запросы используют системный прокси",
"1bc77f5ab979f4c1": "Добавить настройки модели",
"1e238093b79b3165": "Если оставить пустым, используется 65536",
"21296ab18ad9af25": "Дополнительные параметры JSON",
"24343a2096988d42": "Не удалось открыть",
"253d4a3428c648fb": "Токены записи в кеш: {0}",
"26a3855aed1d8d17": "Сервис не запущен",
"281eb6d08c9960d0": "Бюджет токенов рассуждения {0} должен быть положительным целым числом",
"28aeffc70ceb4267": "Измените язык интерфейса. Настройка применяется сразу и сохраняется на этом устройстве.",
"2a24519398684ed5": "Перейти на домашнюю страницу",
"2cd0f3be8738a86c": "Отмена",
"2d706f7981b45a7b": "Локальные настройки сохранены",
"2f9daa828907b93f": "Удалить",
"30bb57a50caedc38": "например, для ежедневного дополнения кода и ответов на вопросы",
"3275d7e70bbdc3d5": "{1} для {0}",
"32b3c9a50003f77a": "Количество выходных токенов рассчитано приблизительно",
"33d2d273e2bd5f88": "Не удалось открыть руководство пользователя",
"3463c5585c246df9": "Итого: {0}",
"3468b57e3edbc599": "Сводка составлена по данным ходов, найденным в истории.",
"35076178fe79a210": "Конфигурация изменена. Выполните проверку снова.",
"358f07b2c1445ab1": "Сообщение автора",
"36c149a9b3e8dca0": "моделей.",
"37d23612f78a2e63": "Перезапустить и обновить",
"392d0dceb45998d3": "Очень высокая",
"393df9bb13ea4900": "Попадание",
"3af7e5489e61ea51": "Обновление",
"3bf8512aa520ed21": "Режим локального сервиса",
"3c2a9f9901109e75": "Тип {0} поддерживает только OpenAI или Anthropic",
"3d13868593ae4eeb": "Язык интерфейса",
"3ea83f9f55062582": "Дата выпуска: {0}",
"3edda85621fd03b2": "адаптеров моделей",
"3fd47edce45b3603": "Закрыть",
"42aa8e01e98c0d8c": "Общая длительность",
"438bb77c1ecdfab0": "Запись в кеш: {0} × ${1}/1M = {2}",
"46056425a48ef05b": "Сейчас используется расчет по коэффициенту повторного использования",
"468adaa418ee1475": "например, https://api.openai.com/v1",
"472642d58d3d5a6d": "Формула: {0}",
"4923eeb7bd75cccd": "Идентификатор модели {0} не может быть пустым",
"497c85690c4cc0fc": "Нет данных",
"4c0a929bb86ce912": "Сейчас: {0}",
"4d2b6e53be6002e5": "Стратегия статистики кеша: {0} ({1})",
"4d8c1c5b42830791": "Неизвестно",
"4f0982ba1d37e51b": "Исходящие запросы используют прокси из переменных окружения",
"51194c3ad014fb29": "Требуется повторная проверка",
"5205125c0e91d346": "Максимальное число токенов, которое модель Anthropic может сгенерировать за один ответ. Оставьте поле пустым для значения по умолчанию.",
"56627c94a9decee6": "Макс. выходных токенов",
"593a972852ba0004": "Cursor Assistant | Всегда бесплатно | Пользовательский API",
"59a2195a01a8b35b": "{0} должен быть допустимым объектом JSON",
"5aa8f5590c940829": "Ввод без кеша: {0}",
"5beb1206c532729f": "Максимальное число токенов в одном ответе. Оставьте поле пустым для значения по умолчанию.",
"5d1687a4a41883fd": "Остановка...",
"5e709712ce012f5d": "Исходящие запросы используют прокси из переменных окружения: {0}",
"6106f0a12583a334": "Не удалось обновить",
"62873083fcaed27d": "Статистика сеанса",
"6309a3bb5ba4c714": "Не удалось сохранить",
"636e3deffc1e960a": "Модель {0}",
"63d90d977348ab1f": "Дублировать",
"64857b4338678314": "Значение пользовательского заголовка {0} должно быть строкой",
"64d2730f2ae37997": "Исходный ответ",
"65cc5fd2e6ce6e75": "Бэкенд запущен, прокси не запущен",
"66af574b8948fe83": "Ключ API {0} не может быть пустым",
"6744b4c6a9aa0038": "Выключено",
"675109292da4eb36": "Еще не проверено",
"699fe7ade5407687": "Прямой режим",
"6a7b96f399e58138": "например, sk-xxxxxx",
"6aa8f49cc992dfd7": "Проверить",
"6ae23d6d7cb18592": "Ошибка сервиса",
"6d7872164df9b36e": "Обычный ввод: {0} × ${1}/1M = {2}",
"6e584e3d5ce64aa0": "Сохранить настройки",
"6ec87609a8769425": "По умолчанию {0} / С учетом создания {1}",
"72f6c3525c0192a7": "Если включено, создание кеша учитывается в знаменателе",
"737225e2904673fc": "Расчетные выходные токены: {0}",
"7520bd50a5ee5471": "Остановить проверку {0}/{1}",
"753d8bb0da9913ce": "Не удалось дублировать",
"774d6e1b7cb89751": "Стандартный расчет",
"77c9e582e85583af": "Проверка не пройдена",
"7a26bf794e9fb6bf": "Используется только для отображения в интерфейсе и помогает различать модели.",
"7b6187c41e88b70c": "Проверка...",
"7bf8e2c07e084d09": "Редактор модели",
"7df7641e5e741346": "Чтение кеша / (Чтение кеша + Создание кеша + Ввод без кеша)",
"7e9e334aeb0bdc07": "Не удалось выполнить операцию с сервисом",
"7f68ebad19ba6bcd": "Проверить обновления",
"80296f4aa3f4543b": "Чтение/запись кеша",
"81123c56d5d880d0": "Ключ API",
"8139cb3dd11f5a67": "Если включено, объект JSON переопределит итоговые заголовки запроса. При совпадении имен используются значения отсюда; все значения должны быть строками.",
"8672864e90417138": "Максимальная",
"86df7ec743047234": "Сервис запущен",
"87ed126f7bd1121e": "Режим маршрутизации",
"899add6275682210": "Если оставить пустым, используется 200000",
"8a4ef3e48e4e8a5a": "Включено",
"8c0d84831a3c3d5b": "Сейчас используется режим локального сервиса",
"8c1935935600e336": "Проверка модели",
"8cbcf741e727dbf7": "Настройки модели",
"8d1de152be6360ce": "Доля успешных: {0}",
"8e2dc7b0d2e8f6f8": "например, OpenAI - GPT-4.1",
"8f6f8d979c981ced": "Скопировано",
"8f8baf5d18dd0492": "Если включено, объект JSON переопределит тело запроса OpenAI. При совпадении полей используются значения отсюда. OpenAI service_tier поддерживает auto, default, flex, scale и priority; priority можно использовать для сценариев с высоким приоритетом/Fast.",
"8faa670b512b6b9b": "Открыть настройки модели",
"907395551d0f3a93": "Конечная точка OpenAI для {0} поддерживает только /v1/responses, /v1/chat/completions или пользовательский путь, начинающийся с /",
"917b1c1f18d0276b": "Сохранение...",
"9196835e388d2550": "Проверить все",
"91cba5c107a51892": "/ Ошибочные",
"92059fe6cd713db4": "Имя модели, которое фактически отправляется серверу, например gpt-4.1 или claude-sonnet.",
"93e08803675e378b": "Идентификатор модели",
"93faf55cd25c8319": "Это программное обеспечение полностью бесплатно. Если с вас взяли плату, скорее всего, вас обманули.\\nПосетите страницу автора: https://space.bilibili.com/311706663/upload/video\\nТам публикуются обновления, руководства и другие материалы.",
"942ff2d88baca0c6": "Проверка обновлений...",
"970388573a3c88c9": "Чтение кеша: {0} × ${1}/1M = {2}",
"9730c15f3c1963a1": "Макс.",
"986678eccf56dc28": "Состояние сервиса обновляется. Повторите попытку позже.",
"9970736b36ff2b68": "Корневой адрес API сервиса модели, обычно совместимый с OpenAI или Anthropic.",
"9a6e7d6c17471711": "Сейчас используется стандартный расчет доли попаданий",
"9b17fa889b307f7f": "Успешных ходов: {0}",
"9c38b6e9bf94abec": "Включено прямое подключение к Cursor",
"9c41b3a9e12ac994": "Интенсивность рассуждений",
"9d2ca261281a158a": "Позже",
"9d2fb46c0ba890b9": "Пользовательский путь",
"9dc0825fba5422e4": "Загрузка...",
"9e02529bcaef36c6": "Обнаружен повторяющийся канал модели. Проверьте сочетание url, modelID, apiKey, displayName и endpoint",
"a026f37e613cf48b": "Выходные токены",
"a0d36236c523667c": "Интенсивность рассуждений Anthropic для {0} поддерживает только low, medium, high, xhigh и max",
"a1a038dfa16c3ede": "У вас уже установлена последняя версия (v{0}).",
"a3030bf8f16dc63c": "Сохранить",
"a325d25c69e7256d": "Настройки модели не найдены; дублирование невозможно",
"a4dd8bb7e8b6eb31": "Показать ключ API",
"a54d745d9a9249e2": "Если включено, объект JSON переопределит тело запроса Anthropic. При совпадении полей используются значения отсюда.",
"a55a88237df85d98": "Сейчас используется прямой режим",
"a567bdaa11367f26": "Средняя",
"a5f1bd344c92e195": "Ключ API, необходимый для обращения к сервису модели.",
"a693d69af48bfe48": "Сохранить и проверить",
"a98585871c5313ff": "Отображаемое имя",
"aa9e366f68d3d097": "Низкая",
"ab607d54d86dc789": "Автор leookun",
"ac217e4d1ca410f1": "Доступна новая версия",
"ad79540418be700a": "Открыть папку настроек или отдельно управлять настройками моделей",
"ae5a738238463a92": "Скрыть ключ API",
"aed55419ce62f08e": "Переключение...",
"b10041a13f5c55b1": "Вывод модели: {0} × ${1}/1M = {2}",
"b1c27820fec23edb": "Высокая",
"b42049dcf8a05ef7": "Включен режим локального сервиса",
"b5409d4049286061": "Пользовательский путь (введите полный URL запроса)",
"b571037dc396a00c": "Общее число токенов запроса включает Prompt и вывод модели.",
"b765005f69fa971f": "например, gpt-4.1",
"b76a22622020f849": "Выберите конечную точку протокола. Для варианта «Пользовательский путь» укажите полный URL запроса в поле адреса API, включая суффикс /chat/completions или /responses. Тип протокола будет определен автоматически по последнему сегменту.",
"b870928f8f9a24c4": "Конечная точка API",
"b90a8ac9c488ce46": "Выберите язык",
"ba3c66f90fd11725": "Обнаружен системный прокси",
"ba40014ff496f64e": "Тип",
"bb074b86a98f6911": "Контекстное окно",
"bc87a4121a0873b3": "Обновить статистику",
"bd4464ea88d3f24a": "Всего ходов: {0}",
"bddd504af0c92fd0": "Обнаружен системный PAC/автоматический прокси; в текущей версии используется прямое подключение",
"bef280f9eb392495": "Ходы диалога",
"c228558cf257fc49": "Не удалось удалить",
"c3e9c3c60020b8b7": "Выберите режим",
"c5af02060847d167": "Интенсивность для адаптивных рассуждений Anthropic. В запросах всегда используется новый режим thinking.type=adaptive.",
"c69f5bce63b9f14c": "Папка настроек",
"c8c14507b2d37395": "Интенсивность рассуждений",
"c98e118e0a43f078": "Модель",
"c9dd59beefd7144f": "Чтение кеша / (Чтение кеша + Ввод без кеша)",
"ca00a39fcea70dc6": "Запуск...",
"ca1d1059408b3837": "Ошибочных ходов: {0}",
"cd7ca5fb221e1c53": "{0} не может быть пустым",
"ce46f23cea3bf3c5": "Если включено, Cursor подключается напрямую к официальному сервису. Не включайте этот режим.",
"d0325067fed88e5a": "Доля попаданий в кеш: {0}",
"d08fd4224abcd69d": "Не удалось переключить",
"d1bde4a4e057b2c7": "[MainLayout] Не удалось загрузить сведения об авторе",
"d20ab96566d33f25": "Отображаемое имя {0} не может быть пустым",
"d2243e1d44b2a94e": "Изменить настройки модели",
"d3209b935ae86797": "Настройки модели не найдены; удаление невозможно",
"d373809ab86ba93b": "Копировать",
"d3b1da3088ddd334": "Проверка модели не пройдена",
"d53d32f1a1211371": "Пользовательские заголовки JSON",
"d7889896c5b7732a": "Дополнительные параметры Anthropic JSON",
"d7da2aabd35772ec": "например, 200000 (оставьте пустым для значения по умолчанию)",
"d95e5cb6bdcee553": "Учитывать создание кеша",
"da590a8fe3ce4de0": "Выберите значение",
"daede9881787abe7": "Примечания",
"dbb4b5be9b5723dc": "Интенсивность рассуждений {0} поддерживает только low, medium, high, xhigh и max",
"dbee6e7139243362": "Базовый URL {0} не может быть пустым",
"dc82c5e8fb2ab777": "Версия: v{0}",
"de8184da1ef88d03": "Настроено",
"e01c5dae36cf8c35": "Если включено, объект JSON переопределит тело запроса OpenAI. При совпадении полей используются значения отсюда. OpenAI service_tier поддерживает auto, default, flex, scale и priority.",
"e14c41ef2b7253c9": "Всего токенов запроса: {0}",
"e406825e0a72d2c2": "Локальные настройки",
"e552c2accdbf5178": "Добавить модель",
"e6faccfddce722e8": "Токены чтения из кеша: {0}",
"eaffd48cd2ea9f1a": "например, https://api.anthropic.com",
"eb1be07f2ca6e506": "Расчет основан на тарифах Claude Opus 4.7.",
"ec3b17a75db49e24": "{0} т/с | Первый токен {1}",
"ec99e5c45d648fd6": "Не удалось обновить",
"ee95057c6b0335d2": "Исходящие запросы используют системный прокси: {0}",
"f1e0fc261d42fe29": "Примечание, отображаемое при наведении на модель в списке.",
"f363622480699c52": "Интенсивность рассуждений применяется только к моделям с поддержкой reasoning_effort. Чем выше значение, тем обычно стабильнее результат, но ответ может формироваться медленнее.",
"f3a76d896853c1df": "Промах",
"f3fae6cccb9004b1": "Имя пользовательского заголовка не может быть пустым",
"f474a4108aba4c4c": "Остановить сервис",
"f4f0ead1116b5b62": "Включить",
"f56c6c82203b33f6": "Уведомление",
"f61e03f047b786d5": "Максимальное число выходных токенов {0} должно быть положительным целым числом",
"f6e1c8b1a6970db5": "Исходящие запросы не используют системный прокси",
"fac2a67ad87807c4": "ОК",
"fb7a4c81729ed0ca": "Остановка...",
"fec45092945f8790": "Руководство пользователя"
}
+61 -10
View File
@@ -2,7 +2,6 @@
"02216368edc68816": "无更新说明",
"02bc2e95bf49e587": "当前还没有配置任何",
"03b11112dc970014": "接口地址",
"045261cc748d4300": "Anthropic 思考阶段允许消耗的预算 Token 数。留空时使用默认值。",
"047ec6b71d0cec08": "控制白名单主链路请求走本地服务,还是回到原始 Cursor 上游地址",
"04f632dd4f034d5e": "{0} 的上下文窗口必须为正整数",
"051836569928a9f9": "编辑",
@@ -10,35 +9,39 @@
"05c8a9238c702efa": "直连 Cursor 模式",
"0647728439b5da2e": "可配置运行模式和模型渠道;运行日志位于",
"092b520558eff5f2": "未测试",
"09ebc2643631ba25": "价值估算",
"0b0e7478e41fe677": "{0} 的悬停提示不能为空",
"0c3b4cf7aa259edb": "操作失败",
"0dde813d719dbd01": "打开主页失败",
"0e40a09ab4e664ab": "模型渠道重复,请检查 url、modelID、apiKey、displayName 组合",
"1117a2f86030d03b": "缓存读写已计入 Prompt 侧统计。",
"11afd2a534395b18": "有效",
"124be3f86f197802": "Token 消耗",
"13b61c5f697b6700": "缓存命中率",
"15d124b200ddabed": "模型单次可接受的最大上下文 Token 数。留空时使用默认值。",
"185aebe19c77425d": "{0}必须是 JSON 对象",
"18b7312022cd1840": "启动服务",
"1af38868896cf53d": "运行模式仅支持 local 或 upstream",
"1b7d24b212e52c54": "{0} 的推理强度仅支持 low、medium、high、xhigh",
"1baddde657dd2720": "当前出站请求使用系统代理",
"1bc77f5ab979f4c1": "新增模型配置",
"1e238093b79b3165": "留空时默认 65536",
"21296ab18ad9af25": "额外参数 JSON",
"24343a2096988d42": "打开失败",
"253d4a3428c648fb": "缓存写入:{0}",
"258e4620e2108793": "留空时默认 4096",
"26a3855aed1d8d17": "服务未启动",
"281eb6d08c9960d0": "{0} 的思考预算 Token 必须为正整数",
"28aeffc70ceb4267": "切换当前界面显示语言,设置会立即生效并保存在本机",
"2caeaec539e78898": "思考预算 Token",
"2a24519398684ed5": "访问主页",
"2cd0f3be8738a86c": "取消",
"2d706f7981b45a7b": "本地配置已保存",
"2f9daa828907b93f": "删除",
"30bb57a50caedc38": "例如:用于日常代码补全与问答",
"3275d7e70bbdc3d5": "{0} 的 {1}",
"32b3c9a50003f77a": "输出 Token 为估算值",
"33d2d273e2bd5f88": "打开使用教程失败",
"3463c5585c246df9": "合计:{0}",
"3468b57e3edbc599": "按历史记录里扫描到的回合 summary 汇总。",
"35076178fe79a210": "配置已变更,请重新测试",
"358f07b2c1445ab1": "作者寄语",
"36c149a9b3e8dca0": "模型。",
"37d23612f78a2e63": "立即重启更新",
"392d0dceb45998d3": "极高",
@@ -49,99 +52,137 @@
"3d13868593ae4eeb": "界面语言",
"3ea83f9f55062582": "发布时间:{0}",
"3edda85621fd03b2": "个模型适配器",
"3fd47edce45b3603": "关闭",
"42aa8e01e98c0d8c": "总耗时",
"438bb77c1ecdfab0": "缓存写入:{0} × ${1}/1M = {2}",
"46056425a48ef05b": "当前按复用率口径显示",
"468adaa418ee1475": "例如:https://api.openai.com/v1",
"472642d58d3d5a6d": "公式:{0}",
"4923eeb7bd75cccd": "{0} 的模型标识不能为空",
"497c85690c4cc0fc": "暂无数据",
"4b8d11bf235e9213": "当前命中率{0}",
"4c0a929bb86ce912": "当前:{0}",
"4d2b6e53be6002e5": "缓存统计策略:{0}{1}",
"4d8c1c5b42830791": "未知",
"4f0982ba1d37e51b": "当前出站请求使用环境变量代理",
"51194c3ad014fb29": "需重测",
"5205125c0e91d346": "Anthropic 模型单次回复允许生成的最大 Token 数。留空时使用默认值。",
"56627c94a9decee6": "最大输出 Token",
"593a972852ba0004": "Cursor助手|永久免费|自定义API",
"59a2195a01a8b35b": "{0}必须是合法 JSON 对象",
"5aa8f5590c940829": "非缓存输入:{0}",
"5beb1206c532729f": "单次回复允许生成的最大 Token 数。留空时使用默认值。",
"5d1687a4a41883fd": "停止中...",
"5e709712ce012f5d": "当前出站请求使用环境变量代理:{0}",
"6106f0a12583a334": "刷新失败",
"62873083fcaed27d": "会话统计",
"6309a3bb5ba4c714": "保存失败",
"636e3deffc1e960a": "模型 {0}",
"63d90d977348ab1f": "复制",
"64857b4338678314": "自定义请求头 {0} 的值必须是字符串",
"64d2730f2ae37997": "原始返回",
"65cc5fd2e6ce6e75": "后端已启动,代理未启动",
"66af574b8948fe83": "{0} 的访问密钥不能为空",
"6744b4c6a9aa0038": "已关闭",
"675109292da4eb36": "尚未测试",
"699fe7ade5407687": "直连模式",
"6a7b96f399e58138": "例如:sk-xxxxxx",
"6aa8f49cc992dfd7": "测试",
"6ae23d6d7cb18592": "服务错误",
"6d7872164df9b36e": "普通输入:{0} × ${1}/1M = {2}",
"6e584e3d5ce64aa0": "保存配置",
"6ec87609a8769425": "默认 {0} / 计入创建 {1}",
"72f6c3525c0192a7": "开启后把缓存创建纳入分母",
"737225e2904673fc": "输出推算:{0}",
"7520bd50a5ee5471": "停止测试 {0}/{1}",
"753d8bb0da9913ce": "复制失败",
"774d6e1b7cb89751": "默认口径",
"77c9e582e85583af": "测试失败",
"7a26bf794e9fb6bf": "仅用于界面展示,便于你区分不同模型。",
"7b6187c41e88b70c": "测试中...",
"7bf8e2c07e084d09": "模型编辑",
"7df7641e5e741346": "缓存读取 /(缓存读取 + 缓存创建 + 非缓存输入)",
"7e9e334aeb0bdc07": "服务操作失败",
"7f68ebad19ba6bcd": "检查更新",
"80296f4aa3f4543b": "缓存读写",
"81123c56d5d880d0": "访问密钥",
"8139cb3dd11f5a67": "开启后会把 JSON 对象覆盖到最终请求头。同名请求头以这里为准,值必须是字符串。",
"8672864e90417138": "最高",
"86df7ec743047234": "服务运行中",
"87ed126f7bd1121e": "运行模式",
"899add6275682210": "留空时默认 200000",
"8a4ef3e48e4e8a5a": "已开启",
"8c0d84831a3c3d5b": "当前为本地服务模式",
"8c1935935600e336": "模型测试",
"8cbcf741e727dbf7": "模型配置",
"8d1de152be6360ce": "有效占比:{0}",
"8e2dc7b0d2e8f6f8": "例如:OpenAI - GPT-4.1",
"8f6f8d979c981ced": "已复制",
"8f8baf5d18dd0492": "开启后会把 JSON 对象覆盖到 OpenAI 请求体。同名字段以这里为准。OpenAI service_tier 支持 auto、default、flex、scale、prioritypriority 可用于高优先级/Fast 类场景。",
"8faa670b512b6b9b": "打开模型配置",
"907395551d0f3a93": "{0} 的 OpenAI 端点仅支持 /v1/responses、/v1/chat/completions 或以 / 开头的自定义路径",
"917b1c1f18d0276b": "保存中...",
"9196835e388d2550": "测试全部",
"91cba5c107a51892": "/ 异常",
"92059fe6cd713db4": "请求实际发送给服务端的模型名称,例如 gpt-4.1 或 claude-sonnet。",
"93e08803675e378b": "模型标识",
"93faf55cd25c8319": "本软件是纯免费软件,如果你被收费,那大概率就是被骗了。\n欢迎点击访问作者主页 https://space.bilibili.com/311706663/upload/video\n查看更多更新动态、使用分享和后续内容。",
"942ff2d88baca0c6": "检查更新中...",
"970388573a3c88c9": "缓存读取:{0} × ${1}/1M = {2}",
"9730c15f3c1963a1": "最大",
"986678eccf56dc28": "服务状态更新中,请稍后再试",
"991e374fce0f4492": "Cursor助手",
"9970736b36ff2b68": "模型服务的 API 根地址,通常为兼容 OpenAI 或 Anthropic 的接口入口。",
"9a6e7d6c17471711": "当前按默认命中率口径显示",
"9b17fa889b307f7f": "有效轮次:{0}",
"9c38b6e9bf94abec": "已切换到直连 Cursor 模式",
"9cd4ac17428b86e4": "缓存命中率 = 缓存读取 Token / Prompt Token",
"9c41b3a9e12ac994": "思考强度",
"9d2ca261281a158a": "稍后",
"9d2fb46c0ba890b9": "自定义路径",
"9dc0825fba5422e4": "加载中...",
"9e02529bcaef36c6": "模型渠道重复,请检查 url、modelID、apiKey、displayName、endpoint 组合",
"a026f37e613cf48b": "输出 Token",
"a0d36236c523667c": "{0} 的 Anthropic 思考强度仅支持 low、medium、high、xhigh、max",
"a1a038dfa16c3ede": "当前已是最新版本(v{0})。",
"a3030bf8f16dc63c": "保存",
"a325d25c69e7256d": "模型配置不存在,无法复制",
"a4dd8bb7e8b6eb31": "显示访问密钥",
"a54d745d9a9249e2": "开启后会把 JSON 对象覆盖到 Anthropic 请求体。同名字段以这里为准。",
"a55a88237df85d98": "当前为直连模式",
"a567bdaa11367f26": "中",
"a5f1bd344c92e195": "调用该模型服务需要使用的访问密钥。",
"a693d69af48bfe48": "保存并测试",
"a98585871c5313ff": "显示名称",
"aa9e366f68d3d097": "低",
"ab607d54d86dc789": "作者 leookun",
"ac217e4d1ca410f1": "发现新版本",
"ad79540418be700a": "打开设置目录,或单独管理模型配置",
"ae5a738238463a92": "隐藏访问密钥",
"aed55419ce62f08e": "切换中...",
"b10041a13f5c55b1": "模型输出:{0} × ${1}/1M = {2}",
"b1c27820fec23edb": "高",
"b42049dcf8a05ef7": "已切换到本地服务模式",
"b5409d4049286061": "自定义路径(请输入完整请求地址)",
"b571037dc396a00c": "总请求 Token 包含 Prompt 和模型输出。",
"b765005f69fa971f": "例如:gpt-4.1",
"b76a22622020f849": "选择接口协议端点。选“自定义路径”时,请在接口地址栏填写完整请求地址(含 /chat/completions 或 /responses 路径后缀),系统会根据末段自动判断协议形态。",
"b870928f8f9a24c4": "接口端点",
"b90a8ac9c488ce46": "选择语言",
"ba3c66f90fd11725": "已识别系统代理",
"ba40014ff496f64e": "类型",
"bb074b86a98f6911": "上下文窗口",
"bbacfde55a92869f": "命中率越高,说明重复上下文复用得越多。",
"bc87a4121a0873b3": "刷新统计",
"bd4464ea88d3f24a": "总轮次:{0}",
"bddd504af0c92fd0": "检测到系统 PAC/自动代理,当前版本按直连处理",
"bef280f9eb392495": "对话轮次",
"c228558cf257fc49": "删除失败",
"c3e9c3c60020b8b7": "选择模式",
"c5af02060847d167": "Anthropic adaptive thinking 的思考强度。请求会固定使用新版 thinking.type=adaptive。",
"c69f5bce63b9f14c": "设置文件夹",
"c6f743953145f40b": "例如:4096(留空用默认值)",
"c8c14507b2d37395": "推理强度",
"c98e118e0a43f078": "模型",
"c9dd59beefd7144f": "缓存读取 /(缓存读取 + 非缓存输入)",
"ca00a39fcea70dc6": "启动中...",
"ca1d1059408b3837": "异常轮次:{0}",
"cd7ca5fb221e1c53": "{0}不能为空",
"ce46f23cea3bf3c5": "开启后,Cursor将直接接通官方,请勿开启",
"d0325067fed88e5a": "缓存命中率 {0}",
"d08fd4224abcd69d": "切换失败",
@@ -151,25 +192,35 @@
"d3209b935ae86797": "模型配置不存在,无法删除",
"d373809ab86ba93b": "拷贝",
"d3b1da3088ddd334": "模型测试失败",
"d53d32f1a1211371": "自定义请求头 JSON",
"d7889896c5b7732a": "Anthropic 额外参数 JSON",
"d7da2aabd35772ec": "例如:200000(留空用默认值)",
"d95e5cb6bdcee553": "计入缓存创建",
"da590a8fe3ce4de0": "请选择",
"daede9881787abe7": "备注",
"dbb4b5be9b5723dc": "{0} 的推理强度仅支持 low、medium、high、xhigh、max",
"dbee6e7139243362": "{0} 的接口地址不能为空",
"dc82c5e8fb2ab777": "版本:v{0}",
"de8184da1ef88d03": "已配置",
"e01c5dae36cf8c35": "开启后会把 JSON 对象覆盖到 OpenAI 请求体。同名字段以这里为准。OpenAI service_tier 支持 auto、default、flex、scale、priority。",
"e14c41ef2b7253c9": "总请求:{0}",
"e406825e0a72d2c2": "本地配置",
"e552c2accdbf5178": "新增模型",
"e6faccfddce722e8": "缓存读取:{0}",
"eaffd48cd2ea9f1a": "例如:https://api.anthropic.com",
"eb1be07f2ca6e506": "按 Claude Opus 4.7 价格估算。",
"ec3b17a75db49e24": "{0} t/s | 首字 {1}",
"ec99e5c45d648fd6": "更新失败",
"ee95057c6b0335d2": "当前出站请求使用系统代理:{0}",
"f1e0fc261d42fe29": "模型列表 hover 时显示的备注说明。",
"f363622480699c52": "推理强度仅对部分支持 reasoning_effort 的模型生效,并不是所有模型都支持。越高通常越稳,但也可能更慢。",
"f3a76d896853c1df": "未命中",
"f3fae6cccb9004b1": "自定义请求头名称不能为空",
"f474a4108aba4c4c": "关闭服务",
"f4f0ead1116b5b62": "启用",
"f56c6c82203b33f6": "提示",
"f61e03f047b786d5": "{0} 的最大输出 Token 必须为正整数",
"f6e1c8b1a6970db5": "当前出站请求未使用系统代理",
"fac2a67ad87807c4": "确定",
"fb7a4c81729ed0ca": "关闭中...",
"fec45092945f8790": "使用教程"
+6
View File
@@ -1,4 +1,5 @@
import { computed, ref } from "vue";
import { Events } from "@wailsio/runtime";
import {
DEFAULT_LOCALE,
LOCALE_OPTIONS,
@@ -10,17 +11,20 @@ import {
import zhCNMessages from "@/i18n/locales/zh-CN.json";
import enUSMessages from "@/i18n/locales/en-US.json";
import jaJPMessages from "@/i18n/locales/ja-JP.json";
import ruRUMessages from "@/i18n/locales/ru-RU.json";
const localeMessages = {
"zh-CN": zhCNMessages,
"en-US": enUSMessages,
"ja-JP": jaJPMessages,
"ru-RU": ruRUMessages,
};
const languageLocaleMap = {
zh: "zh-CN",
en: "en-US",
ja: "ja-JP",
ru: "ru-RU",
};
function isSupportedLocale(locale) {
@@ -143,6 +147,7 @@ class LocalizedText extends String {
const currentLocale = ref(resolveInitialLocale());
applyLocaleToDocument(currentLocale.value);
Events.Emit("locale:changed", currentLocale.value);
const localizedCache = new Map();
@@ -155,6 +160,7 @@ export function setLocale(locale) {
currentLocale.value = nextLocale;
persistManualLocale(nextLocale);
applyLocaleToDocument(nextLocale);
Events.Emit("locale:changed", nextLocale);
return nextLocale;
}
+18 -7
View File
@@ -25,6 +25,17 @@ const title = computed(() => route.meta.title ?? "Cursor助手|永久免费|
const directlyClose = computed(() => route.meta.directlyClose === true);
const showFooter = computed(() => route.path === "/");
const footerAuthorInfo = ref(null);
const localizedAuthorInfo = computed(() => {
if (!footerAuthorInfo.value) return null;
return {
buttonText: "作者 leookun",
dialogTitle: "作者寄语",
dialogContent: "本软件是纯免费软件,如果你被收费,那大概率就是被骗了。\n欢迎点击访问作者主页 https://space.bilibili.com/311706663/upload/video\n查看更多更新动态、使用分享和后续内容。",
dialogConfirmText: "访问主页",
dialogCancelText: "关闭",
};
});
const usageDocsURL = "https://docs.leokun.cn";
let proxyStateTimer = null;
const proxyStatePollIntervalMs = 10000;
@@ -106,14 +117,14 @@ async function showActionError(title, error) {
}
async function handleOpenAuthorHome() {
if (!footerAuthorInfo.value) {
if (!localizedAuthorInfo.value) {
return;
}
const confirmed = await showModal({
title: footerAuthorInfo.value.dialogTitle,
content: footerAuthorInfo.value.dialogContent,
confirmText: footerAuthorInfo.value.dialogConfirmText,
cancelText: footerAuthorInfo.value.dialogCancelText,
title: localizedAuthorInfo.value.dialogTitle,
content: localizedAuthorInfo.value.dialogContent,
confirmText: localizedAuthorInfo.value.dialogConfirmText,
cancelText: localizedAuthorInfo.value.dialogCancelText,
showCancel: true,
});
if (!confirmed) {
@@ -221,13 +232,13 @@ onUnmounted(() => {
<span>使用教程</span>
</button>
<button
v-if="footerAuthorInfo"
v-if="localizedAuthorInfo"
type="button"
class="center-row shrink-0 gap-[6px] cursor-pointer rounded-[6px] px-[6px] py-[3px] transition-colors duration-150 hover:bg-[#1f1f1f] hover:text-[#e5e5e5]"
@click="handleOpenAuthorHome"
>
<span class="icon-[ant-design--bilibili-outlined] text-[14px]"></span>
<span>{{ footerAuthorInfo.buttonText }}</span>
<span>{{ localizedAuthorInfo.buttonText }}</span>
</button>
<div
v-if="updateViewState.footerDownloading"
+51 -19
View File
@@ -22,11 +22,12 @@ import {
const APP_STATE_STORAGE_KEY = "cursor-client:runtime-state:v2";
const GENERIC_SERVICE_ERROR = "服务错误";
const SUPPORTED_MODEL_ADAPTER_TYPES = new Set(["openai", "anthropic"]);
const SUPPORTED_REASONING_EFFORTS = new Set(["low", "medium", "high", "xhigh"]);
const SUPPORTED_REASONING_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
const SUPPORTED_ANTHROPIC_THINKING_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
export const ANTHROPIC_THINKING_EFFORT_DEFAULT = "xhigh";
export const OPENAI_ENDPOINT_RESPONSES = "/v1/responses";
export const OPENAI_ENDPOINT_CHAT_COMPLETIONS = "/v1/chat/completions";
export const OPENAI_ENDPOINT_CUSTOM = "/custom";
export const OPENAI_EXTRA_PARAMS_DEFAULT_JSON = `{
"service_tier": "priority"
}`;
@@ -34,7 +35,7 @@ export const EXTRA_PARAMS_DEFAULT_JSON = `{
}`;
export const CUSTOM_HEADERS_DEFAULT_JSON = `{
}`;
const SUPPORTED_OPENAI_ENDPOINTS = new Set([OPENAI_ENDPOINT_RESPONSES, OPENAI_ENDPOINT_CHAT_COMPLETIONS]);
const SUPPORTED_OPENAI_ENDPOINTS = new Set([OPENAI_ENDPOINT_RESPONSES, OPENAI_ENDPOINT_CHAT_COMPLETIONS, OPENAI_ENDPOINT_CUSTOM]);
const SUPPORTED_ROUTE_MODES = new Set(["local", "upstream"]);
const PROXY_STATE_EVENT = "proxy:state";
const USER_CONFIG_CHANGED_EVENT = "user-config:changed";
@@ -282,6 +283,9 @@ export function createEmptyModelAdapter() {
};
}
// normalizeOpenAIEndpoint 归一化 endpoint 路径。
// 支持三个预设值:/v1/responses、/v1/chat/completions、/custom(自定义路径)。
// 选 /custom 时,用户需在接口地址栏填写完整请求 URL。
function normalizeOpenAIEndpoint(value) {
const text = asString(value).toLowerCase();
if (!text) {
@@ -290,6 +294,10 @@ function normalizeOpenAIEndpoint(value) {
return SUPPORTED_OPENAI_ENDPOINTS.has(text) ? text : "";
}
function isValidOpenAIEndpoint(value) {
return normalizeOpenAIEndpoint(value) !== "";
}
function validateJSONObject(value, label) {
const text = asString(value);
if (!text) {
@@ -424,10 +432,10 @@ export function validateModelAdapters(source) {
return `${prefix} 的模型标识不能为空`;
}
if (adapter.type === "openai" && !SUPPORTED_REASONING_EFFORTS.has(adapter.reasoningEffort)) {
return `${prefix} 的推理强度仅支持 low、medium、high、xhigh`;
return `${prefix} 的推理强度仅支持 low、medium、high、xhigh、max`;
}
if (adapter.type === "openai" && !SUPPORTED_OPENAI_ENDPOINTS.has(adapter.openAIEndpoint)) {
return `${prefix} 的 OpenAI 端点仅支持 /v1/responses/v1/chat/completions`;
if (adapter.type === "openai" && !isValidOpenAIEndpoint(adapter.openAIEndpoint)) {
return `${prefix} 的 OpenAI 端点仅支持 /v1/responses/v1/chat/completions 或以 / 开头的自定义路径`;
}
if (adapter.type === "openai" && adapter.openAIExtraParamsEnabled) {
const extraParamsError = validateOpenAIExtraParamsJSON(adapter.openAIExtraParamsJSON);
@@ -987,6 +995,29 @@ export const appViewState = reactive({
}),
});
function localizeUpdateMessage(msg) {
if (!msg) return "";
if (/当前已是最新版本/.test(msg)) {
const match = msg.match(/v?([0-9]+\.[0-9]+\.[0-9]+)/);
const version = match ? match[1] : appState.appVersion || "...";
return `当前已是最新版本(v${version})。`;
}
return msg;
}
function localizeReadyContent() {
const version = appState.updateVersion || appState.appVersion || "...";
const date = formatReleaseDate(appState.updateReleaseDate);
const notes = appState.updateReleaseNotes || "";
return [
`版本:v${version}`,
`发布时间:${date}`,
"",
notes || "无更新说明",
].join("\n");
}
export const updateViewState = reactive({
footerDownloading: computed(() => appState.updateState === "downloading"),
footerBusy: computed(() => ["checking", "installing"].includes(appState.updateState)),
@@ -1008,24 +1039,25 @@ export const updateViewState = reactive({
promptContent: computed(() => {
switch (appState.updatePromptKind) {
case "ready":
return [
`版本:v${appState.updateVersion || appState.appVersion || "..."}`,
`发布时间:${formatReleaseDate(appState.updateReleaseDate)}`,
"",
appState.updateReleaseNotes || "无更新说明",
].join("\n");
return localizeReadyContent();
case "error":
return appState.updateError || appState.updateMessage || GENERIC_SERVICE_ERROR;
return appState.updateError || localizeUpdateMessage(appState.updateMessage) || GENERIC_SERVICE_ERROR;
default:
return appState.updateMessage || `当前已是最新版本(v${appState.appVersion || "..."})。`;
return localizeUpdateMessage(appState.updateMessage) || localizeUpdateMessage(`当前已是最新版本(v${appState.appVersion || "..."})。`);
}
}),
promptConfirmText: computed(() =>
appState.updatePromptKind === "ready" ? "立即重启更新" : "确定",
),
promptCancelText: computed(() =>
appState.updatePromptKind === "ready" ? "稍后" : "取消",
),
promptConfirmText: computed(() => {
if (appState.updatePromptKind === "ready") {
return "立即重启更新";
}
return "确定";
}),
promptCancelText: computed(() => {
if (appState.updatePromptKind === "ready") {
return "稍后";
}
return "取消";
}),
promptShowCancel: computed(() => appState.updatePromptKind === "ready"),
});
+4 -1
View File
@@ -17,6 +17,7 @@ import {
isModelAdapterTestResultStale,
normalizeModelAdapter,
OPENAI_ENDPOINT_CHAT_COMPLETIONS,
OPENAI_ENDPOINT_CUSTOM,
OPENAI_ENDPOINT_RESPONSES,
OPENAI_EXTRA_PARAMS_DEFAULT_JSON,
runModelAdapterTest,
@@ -37,6 +38,7 @@ const reasoningEffortOptions = [
{ label: "中", value: "medium", icon: "icon-[mdi--head-lightbulb-outline]" },
{ label: "高", value: "high", icon: "icon-[mdi--brain]" },
{ label: "极高", value: "xhigh", icon: "icon-[mdi--head-cog-outline]" },
{ label: "最高", value: "max", icon: "icon-[mdi--brain]" },
];
const anthropicThinkingEffortOptions = [
@@ -50,6 +52,7 @@ const anthropicThinkingEffortOptions = [
const openAIEndpointOptions = [
{ label: "/v1/responses", value: OPENAI_ENDPOINT_RESPONSES, icon: "icon-[mdi--api]" },
{ label: "/v1/chat/completions", value: OPENAI_ENDPOINT_CHAT_COMPLETIONS, icon: "icon-[mdi--message-text-outline]" },
{ label: "自定义路径(请输入完整请求地址)", value: OPENAI_ENDPOINT_CUSTOM, icon: "icon-[mdi--pencil-outline]" },
];
const editorIndex = ref(-1);
@@ -128,7 +131,7 @@ const fieldTips = {
contextWindowTokens: "模型单次可接受的最大上下文 Token 数。留空时使用默认值。",
reasoningEffort: "推理强度仅对部分支持 reasoning_effort 的模型生效,并不是所有模型都支持。越高通常越稳,但也可能更慢。",
maxCompletionTokens: "单次回复允许生成的最大 Token 数。留空时使用默认值。",
openAIEndpoint: "OpenAI 兼容接口使用的协议端点。未选择时默认使用 /v1/responses。",
openAIEndpoint: "选择接口协议端点。选“自定义路径”时,请在接口地址栏填写完整请求地址(含 /chat/completions 或 /responses 路径后缀),系统会根据末段自动判断协议形态。",
openAIExtraParams: "开启后会把 JSON 对象覆盖到 OpenAI 请求体。同名字段以这里为准。OpenAI service_tier 支持 auto、default、flex、scale、priority。",
customHeaders: "开启后会把 JSON 对象覆盖到最终请求头。同名请求头以这里为准,值必须是字符串。",
anthropicExtraParams: "开启后会把 JSON 对象覆盖到 Anthropic 请求体。同名字段以这里为准。",
+1 -1
View File
@@ -5,6 +5,7 @@ go 1.25.0
require (
codeberg.org/readeck/go-readability/v2 v2.1.1
connectrpc.com/connect v1.19.1
github.com/PuerkitoBio/goquery v1.9.2
github.com/denisbrodbeck/machineid v1.0.1
github.com/elazarl/goproxy v1.7.2
github.com/firecrawl/html-to-markdown v0.0.0-20260312013131-1af9901a5d61
@@ -29,7 +30,6 @@ require (
dario.cat/mergo v1.0.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/PuerkitoBio/goquery v1.9.2 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
+70 -7
View File
@@ -217,7 +217,7 @@ func Run(resources EmbeddedResources) error {
Hidden: false,
HideOnEscape: false,
MinimiseButtonState: application.ButtonEnabled,
MaximiseButtonState: application.ButtonHidden,
MaximiseButtonState: application.ButtonEnabled,
CloseButtonState: application.ButtonEnabled,
BackgroundColour: application.RGBA{Red: 25, Green: 25, Blue: 25, Alpha: 255},
Mac: application.MacWindow{
@@ -268,37 +268,100 @@ func Run(resources EmbeddedResources) error {
menu.AddSeparator()
startItem := menu.Add("启动服务")
stopItem := menu.Add("停止服务")
menu.Add("检查更新").OnClick(func(ctx *application.Context) {
updateItem := menu.Add("检查更新").OnClick(func(ctx *application.Context) {
updateManager.CheckNow(true)
})
menu.AddSeparator()
menu.Add("显示窗口").OnClick(func(ctx *application.Context) {
showItem := menu.Add("显示窗口").OnClick(func(ctx *application.Context) {
showMainWindow()
})
menu.Add("隐藏窗口").OnClick(func(ctx *application.Context) {
hideItem := menu.Add("隐藏窗口").OnClick(func(ctx *application.Context) {
window.Hide()
})
menu.AddSeparator()
menu.Add("退出").OnClick(func(ctx *application.Context) {
quitItem := menu.Add("退出").OnClick(func(ctx *application.Context) {
proxyService.ShutdownForQuit()
app.Quit()
})
var currentLocale = "zh-CN"
updateTrayLabels := func(locale string) {
currentLocale = locale
state := proxyService.GetState()
if state.Running {
if locale == "en-US" {
statusItem.SetLabel("Status: Running")
} else if locale == "ja-JP" {
statusItem.SetLabel("状態:実行中")
} else if locale == "ru-RU" {
statusItem.SetLabel("Статус: запущено")
} else {
statusItem.SetLabel("状态:运行中")
}
} else {
if locale == "en-US" {
statusItem.SetLabel("Status: Not Started")
} else if locale == "ja-JP" {
statusItem.SetLabel("状態:未起動")
} else if locale == "ru-RU" {
statusItem.SetLabel("Статус: не запущено")
} else {
statusItem.SetLabel("状态:未启动")
}
}
if locale == "en-US" {
startItem.SetLabel("Start Service")
stopItem.SetLabel("Stop Service")
updateItem.SetLabel("Check for Updates")
showItem.SetLabel("Show Window")
hideItem.SetLabel("Hide Window")
quitItem.SetLabel("Exit")
} else if locale == "ja-JP" {
startItem.SetLabel("サービス起動")
stopItem.SetLabel("サービス停止")
updateItem.SetLabel("アップデートを確認")
showItem.SetLabel("ウィンドウを表示")
hideItem.SetLabel("ウィンドウを非表示")
quitItem.SetLabel("終了")
} else if locale == "ru-RU" {
startItem.SetLabel("Запустить сервис")
stopItem.SetLabel("Остановить сервис")
updateItem.SetLabel("Проверить обновления")
showItem.SetLabel("Показать окно")
hideItem.SetLabel("Скрыть окно")
quitItem.SetLabel("Выход")
} else {
startItem.SetLabel("启动服务")
stopItem.SetLabel("停止服务")
updateItem.SetLabel("检查更新")
showItem.SetLabel("显示窗口")
hideItem.SetLabel("隐藏窗口")
quitItem.SetLabel("退出")
}
}
refreshTray := func() {
state := proxyService.GetState()
if state.Running {
statusItem.SetLabel("状态:运行中")
startItem.SetEnabled(false)
stopItem.SetEnabled(true)
} else {
statusItem.SetLabel("状态:未启动")
startItem.SetEnabled(true)
stopItem.SetEnabled(false)
}
updateTrayLabels(currentLocale)
if refreshAdAssetBaseURL() {
refreshAdRuntime()
}
}
app.Event.On("locale:changed", func(e *application.CustomEvent) {
if locale, ok := e.Data.(string); ok {
updateTrayLabels(locale)
}
})
app.Event.On("proxy:state", func(event *application.CustomEvent) {
refreshTray()
})
@@ -0,0 +1,212 @@
package interaction
import (
"net/http"
neturl "net/url"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"cursor/gen/agentv1"
)
const (
baiduWebSearchBaseURL = "https://www.baidu.com/s?ie=utf-8&tn=baidu&wd="
baiduWebSearchHostURL = "https://www.baidu.com"
baiduSearchAbstractLimit = 300
baiduSearchReferenceLimit = 8
)
// extractBaiduWebSearchReferences 从百度搜索结果页 HTML 中解析出搜索结果列表。
func extractBaiduWebSearchReferences(body string) []*agentv1.WebSearchReference {
document, err := goquery.NewDocumentFromReader(strings.NewReader(body))
if err != nil {
return nil
}
references := make([]*agentv1.WebSearchReference, 0, baiduSearchReferenceLimit)
document.Find("#content_left > div").EachWithBreak(func(_ int, selection *goquery.Selection) bool {
if len(references) >= baiduSearchReferenceLimit {
return false
}
if !selection.HasClass("c-container") {
return true
}
title, resultURL, abstract := extractBaiduSearchResult(selection)
if title == "" || resultURL == "" {
return true
}
references = append(references, &agentv1.WebSearchReference{
Title: title,
Url: normalizeBaiduSearchURL(resultURL),
Chunk: truncateBaiduSearchAbstract(abstract),
})
return true
})
return references
}
// extractBaiduSearchResult 从单条百度搜索结果节点中提取标题、链接和摘要。
func extractBaiduSearchResult(selection *goquery.Selection) (string, string, string) {
title := cleanBaiduSearchText(selection.Find("h3").First().Text())
resultURL, _ := selection.Find("h3 a").First().Attr("href")
if title == "" {
title = firstBaiduSearchLine(selection.Text())
}
if resultURL == "" {
resultURL, _ = selection.Find("a").First().Attr("href")
}
abstract := cleanBaiduSearchText(selection.Find(".c-abstract").First().Text())
if abstract == "" {
abstract = cleanBaiduSearchText(selection.ChildrenFiltered("div").First().Text())
}
if abstract == "" {
abstract = baiduSearchTextAfterFirstLine(selection.Text())
}
return title, strings.TrimSpace(resultURL), abstract
}
// normalizeBaiduSearchURL 把百度返回的相对或协议省略链接归一化为绝对 URL。
func normalizeBaiduSearchURL(rawURL string) string {
rawURL = strings.TrimSpace(rawURL)
if strings.HasPrefix(rawURL, "//") {
return "https:" + rawURL
}
if strings.HasPrefix(rawURL, "/") {
return baiduWebSearchHostURL + rawURL
}
return rawURL
}
// resolveBaiduWebSearchRedirects 把百度跳转链接解析为最终目标地址,就地更新引用列表。
func resolveBaiduWebSearchRedirects(client *http.Client, references []*agentv1.WebSearchReference) {
for _, reference := range references {
if reference == nil {
continue
}
reference.Url = resolveBaiduRedirectURL(client, reference.GetUrl())
}
}
// resolveBaiduRedirectURL 判断链接是否是百度跳转链接,并尝试解析出真实目标。
func resolveBaiduRedirectURL(client *http.Client, rawURL string) string {
resultURL := normalizeBaiduSearchURL(rawURL)
if !isBaiduRedirectURL(resultURL) {
return resultURL
}
redirectClient := baiduRedirectHTTPClient(client)
if location := requestBaiduRedirectLocation(redirectClient, http.MethodHead, resultURL); location != "" {
return location
}
if location := requestBaiduRedirectLocation(redirectClient, http.MethodGet, resultURL); location != "" {
return location
}
return resultURL
}
// baiduRedirectHTTPClient 基于基础 client 构造一个不自动跟随重定向的短超时客户端。
func baiduRedirectHTTPClient(base *http.Client) *http.Client {
if base == nil {
base = http.DefaultClient
}
client := *base
if client.Timeout == 0 || client.Timeout > 6*time.Second {
client.Timeout = 6 * time.Second
}
client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
}
return &client
}
// requestBaiduRedirectLocation 发起一次请求并读取响应头中的重定向目标地址。
func requestBaiduRedirectLocation(client *http.Client, method string, rawURL string) string {
request, err := http.NewRequest(method, rawURL, nil)
if err != nil {
return ""
}
request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36")
request.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
request.Header.Set("Referer", baiduWebSearchHostURL+"/")
response, err := client.Do(request)
if err != nil {
return ""
}
defer response.Body.Close()
location := strings.TrimSpace(response.Header.Get("Location"))
if location == "" {
return ""
}
return resolveBaiduLocationURL(rawURL, location)
}
// resolveBaiduLocationURL 把响应头里的相对重定向地址解析为绝对地址。
func resolveBaiduLocationURL(baseURL string, location string) string {
parsedLocation, err := neturl.Parse(location)
if err != nil {
return location
}
if parsedLocation.IsAbs() {
return parsedLocation.String()
}
parsedBase, err := neturl.Parse(baseURL)
if err != nil {
return location
}
return parsedBase.ResolveReference(parsedLocation).String()
}
// isBaiduRedirectURL 判断给定地址是否是百度域名下的跳转链接。
func isBaiduRedirectURL(rawURL string) bool {
parsedURL, err := neturl.Parse(strings.TrimSpace(rawURL))
if err != nil {
return false
}
host := strings.ToLower(parsedURL.Hostname())
path := strings.ToLower(parsedURL.EscapedPath())
return (host == "baidu.com" || strings.HasSuffix(host, ".baidu.com")) && strings.HasPrefix(path, "/link")
}
// truncateBaiduSearchAbstract 按字符数截断摘要文本,避免结果过长。
func truncateBaiduSearchAbstract(value string) string {
value = cleanBaiduSearchText(value)
if baiduSearchAbstractLimit <= 0 {
return value
}
runes := []rune(value)
if len(runes) <= baiduSearchAbstractLimit {
return value
}
return string(runes[:baiduSearchAbstractLimit])
}
// firstBaiduSearchLine 返回文本中第一个非空行。
func firstBaiduSearchLine(value string) string {
for _, line := range strings.Split(strings.ReplaceAll(value, "\r", "\n"), "\n") {
line = cleanBaiduSearchText(line)
if line != "" {
return line
}
}
return ""
}
// baiduSearchTextAfterFirstLine 返回除第一个非空行外剩余文本的拼接结果。
func baiduSearchTextAfterFirstLine(value string) string {
nonEmpty := make([]string, 0, 8)
for _, line := range strings.Split(strings.ReplaceAll(value, "\r", "\n"), "\n") {
line = cleanBaiduSearchText(line)
if line != "" {
nonEmpty = append(nonEmpty, line)
}
}
if len(nonEmpty) <= 1 {
return ""
}
return cleanBaiduSearchText(strings.Join(nonEmpty[1:], " "))
}
// cleanBaiduSearchText 折叠多余空白并去除首尾空格。
func cleanBaiduSearchText(value string) string {
return strings.Join(strings.Fields(strings.TrimSpace(value)), " ")
}
@@ -577,13 +577,68 @@ const (
)
func (bridge *Bridge) executeWebSearch(searchTerm string) ([]*agentv1.WebSearchReference, string, error) {
if strings.TrimSpace(searchTerm) == "" {
searchTerm = strings.TrimSpace(searchTerm)
if searchTerm == "" {
return nil, "", fmt.Errorf("web search search_term is required")
}
client := bridge.httpClient
if client == nil {
client = netproxy.NewHTTPClient(15 * time.Second)
}
// 先尝试百度搜索
baiduReferences, baiduPayload, baiduErr := bridge.tryBaiduWebSearch(client, searchTerm)
if baiduErr == nil && len(baiduReferences) > 0 {
return baiduReferences, baiduPayload, nil
}
// 百度失败,回退到 DuckDuckGo
duckReferences, duckPayload, duckErr := bridge.tryDuckDuckGoWebSearch(client, searchTerm)
if duckErr == nil && len(duckReferences) > 0 {
return duckReferences, duckPayload, nil
}
// 两者都失败,返回综合错误
if baiduErr != nil && duckErr != nil {
return nil, "", fmt.Errorf("web search failed: baidu=%v, duckduckgo=%v", baiduErr, duckErr)
}
return nil, "", fmt.Errorf("web search returned no parseable results")
}
func (bridge *Bridge) tryBaiduWebSearch(client *http.Client, searchTerm string) ([]*agentv1.WebSearchReference, string, error) {
requestURL := baiduWebSearchBaseURL + neturl.QueryEscape(searchTerm)
request, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil {
return nil, "", err
}
request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36")
request.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8")
request.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
request.Header.Set("Referer", baiduWebSearchHostURL+"/")
response, err := client.Do(request)
if err != nil {
return nil, "", err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, "", fmt.Errorf("baidu http status %d", response.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(response.Body, 2*1024*1024))
if err != nil {
return nil, "", err
}
references := extractBaiduWebSearchReferences(string(body))
if len(references) == 0 {
return nil, "", fmt.Errorf("baidu returned no parseable results")
}
if len(references) > 5 {
references = references[:5]
}
resolveBaiduWebSearchRedirects(client, references)
return references, formatWebSearchPayload(searchTerm, references), nil
}
func (bridge *Bridge) tryDuckDuckGoWebSearch(client *http.Client, searchTerm string) ([]*agentv1.WebSearchReference, string, error) {
requestURL := webSearchURLOverride + neturl.QueryEscape(searchTerm)
request, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil {
+110 -9
View File
@@ -156,6 +156,18 @@ func anthropicEndpointURL(baseURL string) string {
return base + "/v1/messages"
}
// shouldRelocateAnthropicImages 判断是否需要把图片块搬运到末条 user 消息。
//
// 官方 Anthropic 端点(api.anthropic.com)可正确处理任意位置的图片,保持原样;
// 其余第三方中转站默认启用搬运,规避「非末尾图片被丢弃」的转换问题。
func shouldRelocateAnthropicImages(baseURL string) bool {
base := strings.ToLower(strings.TrimSpace(baseURL))
if base == "" {
return false
}
return !strings.Contains(base, "api.anthropic.com")
}
// ApplyAnthropicCompatibleAuthHeaders 同时兼容 Anthropic 原生 x-api-key 和 Bearer token 代理。
func ApplyAnthropicCompatibleAuthHeaders(httpReq *http.Request, apiKey string) {
if httpReq == nil {
@@ -215,8 +227,9 @@ func (adapter *AnthropicAdapter) Stream(ctx context.Context, req StreamRequest,
body := cloneRequestBodyOverride(req.RequestBodyOverride)
if len(body) == 0 {
thinkingConfig := buildAnthropicThinkingConfig(req)
relocateImages := shouldRelocateAnthropicImages(baseURL)
stableMessageCount := anthropicStableProviderMessageCount(req.Messages, req.StableMessageCount, thinkingConfig != nil)
systemParts, messages, err := normalizeAnthropicProviderMessages(req.Messages, thinkingConfig != nil)
systemParts, messages, err := normalizeAnthropicProviderMessages(req.Messages, thinkingConfig != nil, relocateImages)
if err != nil {
return err
}
@@ -261,18 +274,17 @@ func (adapter *AnthropicAdapter) Stream(ctx context.Context, req StreamRequest,
body["tools"] = tools
}
body["system"] = anthropicProviderSystemBlocks(systemParts)
if thinkingConfig != nil {
body["thinking"] = thinkingConfig
if normalizeRuntimeThinkingEffort(req.ThinkingEffort) != "disabled" {
body["output_config"] = buildAnthropicOutputConfig(req)
}
}
frontier := buildAnthropicCacheFrontier(body, stableMessageCount)
req.RequestKnobs = annotateAnthropicRequestKnobs(req.RequestKnobs, body, frontier)
body = cloneRequestBodyOverride(body)
applyAnthropicCacheBreakpoints(body, frontier.BreakpointPositions)
frontier.BreakpointCount = len(frontier.BreakpointPositions)
}
// applyAnthropicThinkingConfig 在 override 块之外无条件调用,确保 RequestBodyOverride
// 路径与正常构造路径行为一致:disabled 时强制 thinking:{type:disabled} 并清理冲突字段,
// 非 disabled 时按 AnthropicThinkingEffort 写 adaptive 配置。与 openai.go 的
// applyOpenAIThinkingDisable 对称——后者也是无条件在两条路径之后调用。
applyAnthropicThinkingConfig(body, req)
if err := ApplyAnthropicExtraParams(body, req.AnthropicExtraParamsEnabled, req.AnthropicExtraParamsJSON); err != nil {
finishedAt = time.Now().UTC()
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "anthropic", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
@@ -1066,7 +1078,7 @@ func anthropicStableProviderMessageCount(input []Message, stableReplayMessageCou
if len(stableReplayMessages) == 0 {
return 0
}
_, messages, err := normalizeAnthropicProviderMessages(stableReplayMessages, thinkingEnabled)
_, messages, err := normalizeAnthropicProviderMessages(stableReplayMessages, thinkingEnabled, false)
if err != nil {
return 0
}
@@ -1122,7 +1134,7 @@ func isAnthropicCacheableBlock(block map[string]any) bool {
}
}
func normalizeAnthropicProviderMessages(input []Message, thinkingEnabled bool) ([]string, []anthropicMessage, error) {
func normalizeAnthropicProviderMessages(input []Message, thinkingEnabled bool, relocateImages bool) ([]string, []anthropicMessage, error) {
systemParts := make([]string, 0, len(input))
messages := make([]anthropicMessage, 0, len(input))
pendingToolResults := make([]map[string]any, 0, 2)
@@ -1208,9 +1220,70 @@ func normalizeAnthropicProviderMessages(input []Message, thinkingEnabled bool) (
}
}
flushToolResults()
if relocateImages {
messages = relocateAnthropicImagesToLastUserMessage(messages)
}
return systemParts, messages, nil
}
// relocateAnthropicImagesToLastUserMessage 把所有 user 消息里的 image 块搬运到最后一条 user 消息的末尾。
//
// 背景:部分第三方中转站(如 Bedrock 代理)在 Anthropic→上游 的消息转换中,
// 会丢弃「后面还跟着大量文本/消息」的非末尾图片块。将图片统一移动到末条 user 消息
// 可规避该问题,同时保留图片信息本身。
//
// 数据流演变:
//
// [user_info] [query + IMG] [reminder] [reminder] [current_request]
// → [user_info] [query] [reminder] [reminder] [current_request + IMG]
//
// 搬运后若某条 user 消息 content 变空,则丢弃该消息,避免 Anthropic 拒绝空内容消息。
func relocateAnthropicImagesToLastUserMessage(messages []anthropicMessage) []anthropicMessage {
lastUserIndex := -1
for index := len(messages) - 1; index >= 0; index-- {
if strings.TrimSpace(messages[index].Role) == "user" {
lastUserIndex = index
break
}
}
if lastUserIndex < 0 {
return messages
}
relocated := make([]map[string]any, 0, 2)
for index := 0; index < len(messages); index++ {
if index == lastUserIndex || strings.TrimSpace(messages[index].Role) != "user" {
continue
}
kept := make([]map[string]any, 0, len(messages[index].Content))
for _, block := range messages[index].Content {
if isAnthropicImageBlock(block) {
relocated = append(relocated, block)
continue
}
kept = append(kept, block)
}
messages[index].Content = kept
}
if len(relocated) == 0 {
return messages
}
messages[lastUserIndex].Content = append(messages[lastUserIndex].Content, relocated...)
compacted := make([]anthropicMessage, 0, len(messages))
for index, message := range messages {
if index != lastUserIndex && strings.TrimSpace(message.Role) == "user" && len(message.Content) == 0 {
continue
}
compacted = append(compacted, message)
}
return compacted
}
func isAnthropicImageBlock(block map[string]any) bool {
return strings.TrimSpace(anthropicStringField(block, "type")) == "image"
}
func anthropicProviderContentBlocks(message Message, thinkingEnabled bool) ([]map[string]any, error) {
blocks, err := anthropicContentBlocks(message)
if err != nil {
@@ -1367,6 +1440,34 @@ func buildAnthropicThinkingConfig(req StreamRequest) map[string]any {
}
}
// applyAnthropicThinkingConfig 在请求体构造完成后(含 RequestBodyOverride 路径)无条件调用,
// 与 openai.go 的 applyOpenAIThinkingDisable 对称。它把 thinking 配置写入 body 并在 disabled
// 时清理与之冲突的字段,确保两条构造路径行为一致:
// - disabled: 强制 thinking:{type:"disabled"},删除 output_config / 残留 thinking adaptive 配置,
// 记录 thinking_disabled_provider_param=thinking.type knob
// - adaptive: 按 AnthropicThinkingEffort 写 thinking:{type:adaptive,display:summarized} + output_config
//
// 在 override 路径下,上层若已在 override body 里塞了 thinking/output_configdisabled 时会被正确覆盖。
func applyAnthropicThinkingConfig(body map[string]any, req StreamRequest) {
if len(body) == 0 {
return
}
if normalizeRuntimeThinkingEffort(req.ThinkingEffort) != "disabled" {
if strings.TrimSpace(req.AnthropicThinkingEffort) == "" {
return
}
body["thinking"] = map[string]any{
"type": "adaptive",
"display": "summarized",
}
body["output_config"] = buildAnthropicOutputConfig(req)
return
}
body["thinking"] = map[string]any{"type": "disabled"}
delete(body, "output_config")
setRequestKnob(req, "thinking_disabled_provider_param", "thinking.type")
}
func buildAnthropicOutputConfig(req StreamRequest) map[string]any {
return map[string]any{
"effort": anthropicThinkingEffort(req),
@@ -0,0 +1,141 @@
package modeladapter
import (
"testing"
)
// TestApplyAnthropicThinkingConfig covers the three effort branches on a body
// built by the normal construction path, plus the override path symmetry that
// was the original bug (thinking config was only applied inside the
// `if len(body)==0` block, so RequestBodyOverride skipped it entirely).
func TestApplyAnthropicThinkingConfig(t *testing.T) {
tests := []struct {
name string
body map[string]any
thinkingEffort string
adaptiveEffort string
wantThinking any
wantOutputCfg bool // expect output_config key present
}{
{
name: "disabled_writes_disabled_and_drops_output_config",
body: map[string]any{"model": "m", "output_config": map[string]any{"effort": "high"}},
thinkingEffort: "disabled",
wantThinking: map[string]any{"type": "disabled"},
wantOutputCfg: false,
},
{
name: "adaptive_writes_adaptive_and_output_config",
body: map[string]any{"model": "m"},
adaptiveEffort: "high",
wantThinking: map[string]any{"type": "adaptive", "display": "summarized"},
wantOutputCfg: true,
},
{
name: "empty_effort_no_op",
body: map[string]any{"model": "m"},
wantThinking: nil,
wantOutputCfg: false,
},
{
name: "disabled_overrides_existing_adaptive_thinking",
body: map[string]any{"thinking": map[string]any{"type": "adaptive", "display": "summarized"}, "output_config": map[string]any{"effort": "high"}},
thinkingEffort: "disabled",
wantThinking: map[string]any{"type": "disabled"},
wantOutputCfg: false,
},
{
name: "disabled_normalizes_aliases",
body: map[string]any{"model": "m"},
thinkingEffort: "off",
wantThinking: map[string]any{"type": "disabled"},
wantOutputCfg: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := StreamRequest{
ThinkingEffort: tc.thinkingEffort,
AnthropicThinkingEffort: tc.adaptiveEffort,
RequestKnobs: map[string]any{},
}
applyAnthropicThinkingConfig(tc.body, req)
gotThinking, hasThinking := tc.body["thinking"]
if tc.wantThinking == nil {
if hasThinking {
t.Fatalf("expected no thinking key, got %v", gotThinking)
}
} else {
if !hasThinking {
t.Fatalf("expected thinking=%v, key absent", tc.wantThinking)
}
if !mapEqual(gotThinking, tc.wantThinking) {
t.Fatalf("thinking mismatch\nwant: %v\ngot: %v", tc.wantThinking, gotThinking)
}
}
_, hasOutputCfg := tc.body["output_config"]
if hasOutputCfg != tc.wantOutputCfg {
t.Fatalf("output_config presence=%v, want %v", hasOutputCfg, tc.wantOutputCfg)
}
if tc.thinkingEffort == "disabled" || tc.thinkingEffort == "off" {
gotKnob := req.RequestKnobs["thinking_disabled_provider_param"]
if gotKnob != "thinking.type" {
t.Fatalf("knob thinking_disabled_provider_param=%v, want thinking.type", gotKnob)
}
}
})
}
}
// TestApplyAnthropicThinkingConfigOverridePath simulates the RequestBodyOverride
// branch: the adapter receives a body that did NOT go through the normal
// construction (no tools/messages/system written by the adapter). Before the
// fix, thinking config was skipped entirely on this path. Now
// applyAnthropicThinkingConfig runs unconditionally and disables thinking.
func TestApplyAnthropicThinkingConfigOverridePath(t *testing.T) {
overrideBody := map[string]any{
"model": "claude-x",
"messages": []any{map[string]any{"role": "user", "content": "hi"}},
"thinking": map[string]any{"type": "adaptive", "display": "summarized"},
"output_config": map[string]any{"effort": "high"},
"stream": true,
}
req := StreamRequest{
ThinkingEffort: "disabled",
RequestKnobs: map[string]any{},
}
applyAnthropicThinkingConfig(overrideBody, req)
gotThinking := overrideBody["thinking"]
if !mapEqual(gotThinking, map[string]any{"type": "disabled"}) {
t.Fatalf("override path: thinking not forced to disabled, got %v", gotThinking)
}
if _, hasOutputCfg := overrideBody["output_config"]; hasOutputCfg {
t.Fatalf("override path: output_config should be dropped on disabled, still present")
}
if gotKnob := req.RequestKnobs["thinking_disabled_provider_param"]; gotKnob != "thinking.type" {
t.Fatalf("override path: knob=%v, want thinking.type", gotKnob)
}
}
func mapEqual(a, b any) bool {
ma, okA := a.(map[string]any)
mb, okB := b.(map[string]any)
if !okA || !okB {
return a == b
}
if len(ma) != len(mb) {
return false
}
for k, va := range ma {
vb, ok := mb[k]
if !ok || !mapEqual(va, vb) {
return false
}
}
return true
}
+82 -19
View File
@@ -29,7 +29,7 @@ type openAIRequestBody struct {
Tools []json.RawMessage `json:"tools,omitempty"`
Messages []map[string]any `json:"messages"`
Stream bool `json:"stream"`
MaxTokens int `json:"max_tokens"`
MaxTokens int `json:"max_tokens,omitempty"`
StreamOptions map[string]any `json:"stream_options"`
ReasoningEffort string `json:"reasoning_effort,omitempty"`
PromptCacheKey string `json:"prompt_cache_key,omitempty"`
@@ -41,7 +41,7 @@ type openAIResponsesRequestBody struct {
Input []map[string]any `json:"input"`
Tools []map[string]any `json:"tools,omitempty"`
Stream bool `json:"stream"`
MaxOutputTokens int `json:"max_output_tokens"`
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
Reasoning *openAIResponsesReasoning `json:"reasoning,omitempty"`
Include []string `json:"include,omitempty"`
PromptCacheKey string `json:"prompt_cache_key,omitempty"`
@@ -304,15 +304,67 @@ func OpenAIEndpointURL(baseURL string, endpoint string) string {
if !strings.HasPrefix(normalizedEndpoint, "/") {
normalizedEndpoint = "/" + normalizedEndpoint
}
// 规则0:自定义路径模式
// - baseURL 已含 endpoint 后缀(/chat/completions 或 /responses)→ 直接用 base
// - 否则追加 /chat/completions(默认协议形态,覆盖 Z.AI /v4 等场景)
if normalizedEndpoint == modelchannel.OpenAIEndpointCustom {
if OpenAIEndpointFromBaseURL(base) != "" {
return base
}
return base + "/chat/completions"
}
// 规则1baseURL 已含 endpoint 后缀 → 直接用 base
if OpenAIEndpointFromBaseURL(base) != "" {
return base
}
if strings.HasSuffix(base, "/v1") && strings.HasPrefix(normalizedEndpoint, "/v1/") {
return base + strings.TrimPrefix(normalizedEndpoint, "/v1")
// 规则2baseURL 以 /vN 结尾时,剥离 endpoint 的版本前缀(/v1/、/v2/ 等)
// 这样 base=.../v4 + endpoint=/v1/chat/completions → .../v4/chat/completions
if _, ok := trailingVersionSegment(base); ok {
if rest, stripped := stripEndpointVersionPrefix(normalizedEndpoint); stripped {
return base + rest
}
}
// 规则3:兜底原样拼接
return base + normalizedEndpoint
}
// trailingVersionSegment 检测 URL 末尾是否以 /vN 形式结尾(N 为数字),
// 返回版本段(如 "v4")和是否匹配。用于通用版本段去重。
func trailingVersionSegment(base string) (string, bool) {
idx := strings.LastIndex(base, "/")
if idx < 0 {
return "", false
}
seg := base[idx+1:]
if len(seg) < 2 || seg[0] != 'v' {
return "", false
}
for i := 1; i < len(seg); i++ {
if seg[i] < '0' || seg[i] > '9' {
return "", false
}
}
return seg, true
}
// stripEndpointVersionPrefix 剥离 endpoint 路径开头的版本段前缀(/vN/),
// 返回剩余路径和是否成功剥离。
// /v1/chat/completions → ("/chat/completions", true)
// /chat/completions → ("", false)
func stripEndpointVersionPrefix(endpoint string) (string, bool) {
if len(endpoint) < 4 || endpoint[0] != '/' || endpoint[1] != 'v' {
return "", false
}
i := 2
for i < len(endpoint) && endpoint[i] >= '0' && endpoint[i] <= '9' {
i++
}
if i == 2 || i >= len(endpoint) || endpoint[i] != '/' {
return "", false
}
return endpoint[i:], true
}
func ResolveOpenAIEndpoint(baseURL string, endpoint string) string {
if endpointFromURL := OpenAIEndpointFromBaseURL(baseURL); endpointFromURL != "" {
return endpointFromURL
@@ -377,11 +429,11 @@ func (adapter *OpenAIAdapter) Stream(ctx context.Context, req StreamRequest, sin
req.OpenAIEndpoint = endpoint
if req.RequestKnobs != nil {
req.RequestKnobs["openai_endpoint"] = endpoint
if endpoint == modelchannel.OpenAIEndpointResponses {
if modelchannel.OpenAIEndpointShape(endpoint) == "responses" {
req.RequestKnobs["max_output_tokens"] = req.MaxTokens
}
}
if endpoint == modelchannel.OpenAIEndpointResponses {
if modelchannel.OpenAIEndpointShape(endpoint) == "responses" {
return adapter.streamResponses(ctx, req, baseURL, apiKey, modelID, sink)
}
return adapter.streamChatCompletions(ctx, req, baseURL, apiKey, modelID, sink)
@@ -403,9 +455,11 @@ func (adapter *OpenAIAdapter) streamChatCompletions(ctx context.Context, req Str
Model: modelID,
Messages: normalizedMessages,
Stream: true,
MaxTokens: req.MaxTokens,
StreamOptions: map[string]any{"include_usage": true},
}
if shouldSendOpenAIMaxOutputTokens(modelID) {
requestBody.MaxTokens = req.MaxTokens
}
if key := openAIPromptCacheKey(req, modelID); key != "" {
requestBody.PromptCacheKey = key
}
@@ -425,14 +479,14 @@ func (adapter *OpenAIAdapter) streamChatCompletions(ctx context.Context, req Str
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
return err
}
applyOpenAIThinkingDisable(bodyMap, req, baseURL, modelID, modelchannel.OpenAIEndpointChatCompletions)
applyOpenAIThinkingDisable(bodyMap, req, baseURL, modelID, req.OpenAIEndpoint)
if err := ApplyOpenAIExtraParams(bodyMap, req.OpenAIExtraParamsEnabled, req.OpenAIExtraParamsJSON); err != nil {
finishedAt = time.Now().UTC()
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
return err
}
body = bodyMap
requestURL := OpenAIEndpointURL(baseURL, modelchannel.OpenAIEndpointChatCompletions)
requestURL := OpenAIEndpointURL(baseURL, req.OpenAIEndpoint)
recordLLMRequestArtifact(req, "openai", modelID, "POST", requestURL, body)
payload, err := json.Marshal(body)
@@ -862,12 +916,14 @@ func (adapter *OpenAIAdapter) streamResponses(ctx context.Context, req StreamReq
return err
}
requestBody := openAIResponsesRequestBody{
Model: modelID,
Instructions: instructions,
Input: input,
Stream: true,
MaxOutputTokens: req.MaxTokens,
Store: false,
Model: modelID,
Instructions: instructions,
Input: input,
Stream: true,
Store: false,
}
if shouldSendOpenAIMaxOutputTokens(modelID) {
requestBody.MaxOutputTokens = req.MaxTokens
}
if key := openAIPromptCacheKey(req, modelID); key != "" {
requestBody.PromptCacheKey = key
@@ -901,7 +957,7 @@ func (adapter *OpenAIAdapter) streamResponses(ctx context.Context, req StreamReq
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
return err
}
applyOpenAIThinkingDisable(bodyMap, req, baseURL, modelID, modelchannel.OpenAIEndpointResponses)
applyOpenAIThinkingDisable(bodyMap, req, baseURL, modelID, req.OpenAIEndpoint)
if err := ApplyOpenAIExtraParams(bodyMap, req.OpenAIExtraParamsEnabled, req.OpenAIExtraParamsJSON); err != nil {
finishedAt = time.Now().UTC()
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
@@ -909,7 +965,7 @@ func (adapter *OpenAIAdapter) streamResponses(ctx context.Context, req StreamReq
}
body = bodyMap
requestURL := OpenAIEndpointURL(baseURL, modelchannel.OpenAIEndpointResponses)
requestURL := OpenAIEndpointURL(baseURL, req.OpenAIEndpoint)
recordLLMRequestArtifact(req, "openai", modelID, "POST", requestURL, body)
payload, err := json.Marshal(body)
@@ -1792,6 +1848,10 @@ func normalizeOpenAIProviderMessages(messages []Message, thinkingEnabled bool) (
return items, nil
}
func shouldSendOpenAIMaxOutputTokens(modelID string) bool {
return !strings.Contains(strings.ToLower(strings.TrimSpace(modelID)), "gpt")
}
func shouldIncludeOpenAIReasoningContent(message Message, thinkingEnabled bool) bool {
if strings.TrimSpace(message.ReasoningContent) != "" {
return true
@@ -1819,7 +1879,7 @@ func applyOpenAIThinkingDisable(body map[string]any, req StreamRequest, baseURL
delete(body, "reasoning_effort")
setRequestKnob(req, "thinking_disabled_provider_param", "enable_thinking")
case "reasoning_none":
if endpoint == modelchannel.OpenAIEndpointResponses {
if modelchannel.OpenAIEndpointShape(endpoint) == "responses" {
body["reasoning"] = map[string]any{"effort": "none"}
} else {
body["reasoning_effort"] = "none"
@@ -1841,10 +1901,13 @@ func openAIThinkingDisableKind(baseURL string, modelID string, endpoint string)
strings.Contains(base, "bigmodel") ||
strings.Contains(base, "z.ai") ||
strings.Contains(base, "zhipu") ||
strings.Contains(base, "xiaomimimo") ||
strings.Contains(base, "mimo") ||
strings.Contains(model, "deepseek") ||
strings.Contains(model, "glm") ||
strings.Contains(model, "zai") ||
strings.Contains(model, "zhipu"):
strings.Contains(model, "zhipu") ||
strings.Contains(model, "mimo"):
return "thinking_type"
case openAIModelSupportsReasoningNone(model):
return "reasoning_none"
@@ -0,0 +1,84 @@
package modeladapter
import (
"testing"
)
// TestOpenAIThinkingDisableKindMiMo 验证小米 MiMobase_url 含 xiaomimimo/mimo
// model 含 mimo)被识别为 thinking_type 分支,从而在用户禁用思考时正确写入
// thinking:{type:"disabled"}。回归覆盖 deepseek/glm/qwen/gpt-5 等既有分支。
func TestOpenAIThinkingDisableKindMiMo(t *testing.T) {
tests := []struct {
name string
baseURL string
modelID string
endpoint string
want string
}{
// MiMo —— 修复目标
{name: "mimo official base + pro model", baseURL: "https://api.xiaomimimo.com/v1", modelID: "mimo-v2.5-pro", endpoint: "/chat/completions", want: "thinking_type"},
{name: "mimo base by host keyword", baseURL: "https://api.xiaomimimo.com/v1", modelID: "mimo-v2.5", endpoint: "/chat/completions", want: "thinking_type"},
{name: "mimo model only (custom base)", baseURL: "https://custom.proxy.example.com/v1", modelID: "mimo-v2.5-pro", endpoint: "/chat/completions", want: "thinking_type"},
{name: "mimo ultraspeed variant", baseURL: "https://api.xiaomimimo.com/v1", modelID: "mimo-v2.5-pro-ultraspeed", endpoint: "/chat/completions", want: "thinking_type"},
// 回归:既有 thinking_type 分支不受影响
{name: "deepseek base", baseURL: "https://api.deepseek.com/v1", modelID: "deepseek-chat", endpoint: "/chat/completions", want: "thinking_type"},
{name: "glm model via zhipu base", baseURL: "https://open.bigmodel.cn/api/paas/v4", modelID: "glm-4.6", endpoint: "/chat/completions", want: "thinking_type"},
{name: "z.ai base", baseURL: "https://api.z.ai/api/paas/v4", modelID: "glm-4.5", endpoint: "/chat/completions", want: "thinking_type"},
// 回归:enable_thinking 分支(qwen 系)
{name: "qwen via dashscope", baseURL: "https://dashscope.aliyuncs.com/v1", modelID: "qwen-max", endpoint: "/chat/completions", want: "enable_thinking"},
{name: "qwen model keyword", baseURL: "https://custom.example.com/v1", modelID: "qwen3-coder", endpoint: "/chat/completions", want: "enable_thinking"},
// 回归:reasoning_none 分支(gpt-5.1+/gpt-6
{name: "gpt-5.1", baseURL: "https://api.openai.com/v1", modelID: "gpt-5.1", endpoint: "/chat/completions", want: "reasoning_none"},
{name: "gpt-6", baseURL: "https://api.openai.com/v1", modelID: "gpt-6", endpoint: "/chat/completions", want: "reasoning_none"},
// 回归:未知 provider 不做 disable
{name: "unknown provider", baseURL: "https://api.unknown-llm.com/v1", modelID: "some-model", endpoint: "/chat/completions", want: ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := openAIThinkingDisableKind(tc.baseURL, tc.modelID, tc.endpoint)
if got != tc.want {
t.Fatalf("openAIThinkingDisableKind(%q, %q, %q) = %q, want %q", tc.baseURL, tc.modelID, tc.endpoint, got, tc.want)
}
})
}
}
// TestApplyOpenAIThinkingDisableMiMo 验证当 ThinkingEffort=disabled 且 provider
// 为 MiMo 时,applyOpenAIThinkingDisable 会写入 thinking:{type:"disabled"} 并删除
// reasoning_effort,与 deepseek/glm 行为一致。
func TestApplyOpenAIThinkingDisableMiMo(t *testing.T) {
req := StreamRequest{ThinkingEffort: "disabled", RequestKnobs: map[string]any{}}
body := map[string]any{
"model": "mimo-v2.5-pro",
"messages": []map[string]any{{"role": "user", "content": "hi"}},
"reasoning_effort": "high",
}
applyOpenAIThinkingDisable(body, req, "https://api.xiaomimimo.com/v1", "mimo-v2.5-pro", "/chat/completions")
thinking, ok := body["thinking"].(map[string]any)
if !ok {
t.Fatalf("expected body[thinking] to be map[string]any, got %T (%v)", body["thinking"], body["thinking"])
}
if thinking["type"] != "disabled" {
t.Fatalf("expected thinking.type=disabled, got %v", thinking["type"])
}
if _, stillPresent := body["reasoning_effort"]; stillPresent {
t.Fatalf("reasoning_effort should be deleted when thinking disabled for mimo, got %v", body["reasoning_effort"])
}
if got := req.RequestKnobs["thinking_disabled_provider_param"]; got != "thinking.type" {
t.Fatalf("expected request knob thinking_disabled_provider_param=thinking.type, got %v", got)
}
}
// TestApplyOpenAIThinkingDisableMiMoNotTriggered 验证非 disabled 时不会误写 disable 字段。
func TestApplyOpenAIThinkingDisableMiMoNotTriggered(t *testing.T) {
req := StreamRequest{ThinkingEffort: "high", RequestKnobs: map[string]any{}}
body := map[string]any{"model": "mimo-v2.5-pro", "reasoning_effort": "high"}
applyOpenAIThinkingDisable(body, req, "https://api.xiaomimimo.com/v1", "mimo-v2.5-pro", "/chat/completions")
if _, present := body["thinking"]; present {
t.Fatalf("thinking should not be injected when ThinkingEffort != disabled, got %v", body["thinking"])
}
if body["reasoning_effort"] != "high" {
t.Fatalf("reasoning_effort should be preserved when not disabled, got %v", body["reasoning_effort"])
}
}
+1 -1
View File
@@ -249,7 +249,7 @@ func canMergeProviderAssistantToolCalls(last Message, current Message) bool {
if strings.TrimSpace(last.Role) != "assistant" || strings.TrimSpace(current.Role) != "assistant" {
return false
}
if len(last.ToolCalls) == 0 || len(current.ToolCalls) == 0 {
if len(current.ToolCalls) == 0 {
return false
}
if strings.TrimSpace(last.ToolCallID) != "" || strings.TrimSpace(last.Name) != "" {
@@ -0,0 +1,58 @@
package modeladapter
import (
"reflect"
"testing"
)
func TestSanitizeProviderMessagesMergesLegacyAssistantTextAndToolCallTurnsIdempotently(t *testing.T) {
input := []Message{
{
Role: "assistant",
Content: "Now let me pass stream.Mode in service.go.",
ReasoningContent: "I need to update service.go.",
},
{
Role: "assistant",
Content: "",
ReasoningContent: "I need to update service.go.",
ToolCalls: []ToolCallDescriptor{
{
ID: "call_1",
Type: "function",
Function: ToolCallFunctionShape{
Name: "PatchEdit",
Arguments: `{"path":"/workspace/service.go"}`,
},
},
},
},
{
Role: "tool",
Content: `{"success":{"path":"/workspace/service.go"}}`,
ToolCallID: "call_1",
Name: "PatchEdit",
},
}
first := sanitizeProviderMessages(input)
if len(first) != 2 {
t.Fatalf("message count = %d, want 2: %#v", len(first), first)
}
assistant := first[0]
if assistant.Content != input[0].Content {
t.Fatalf("assistant content = %q", assistant.Content)
}
if assistant.ReasoningContent != input[0].ReasoningContent {
t.Fatalf("assistant reasoning = %q, want one copy of %q", assistant.ReasoningContent, input[0].ReasoningContent)
}
if len(assistant.ToolCalls) != 1 || assistant.ToolCalls[0].ID != "call_1" {
t.Fatalf("assistant tool calls = %#v", assistant.ToolCalls)
}
second := sanitizeProviderMessages(first)
if !reflect.DeepEqual(second, first) {
t.Fatalf("sanitizing normalized messages changed them:\nfirst: %#v\nsecond: %#v", first, second)
}
}
+4 -23
View File
@@ -897,8 +897,8 @@ func buildMessagesFromPendingAssistantRaw(raw string) []Message {
reasoningSignature := ""
pendingToolCalls := make([]ToolCallDescriptor, 0, len(payload.Content))
pendingToolResults := make([]Message, 0, len(payload.Content))
flushAssistantText := func(forceReasoningOnly bool) {
if len(textParts) == 0 && (!forceReasoningOnly || len(reasoningParts) == 0) {
flushAssistantTurn := func() {
if len(textParts) == 0 && len(reasoningParts) == 0 && len(pendingToolCalls) == 0 {
return
}
messages = append(messages, Message{
@@ -906,23 +906,10 @@ func buildMessagesFromPendingAssistantRaw(raw string) []Message {
Content: formatMessageText(strings.Join(textParts, "")),
ReasoningContent: strings.Join(reasoningParts, ""),
ReasoningSignature: reasoningSignature,
})
textParts = textParts[:0]
reasoningParts = reasoningParts[:0]
reasoningSignature = ""
}
flushToolCalls := func() {
if len(pendingToolCalls) == 0 {
return
}
messages = append(messages, Message{
Role: "assistant",
Content: "",
ReasoningContent: strings.Join(reasoningParts, ""),
ReasoningSignature: reasoningSignature,
ToolCalls: append([]ToolCallDescriptor(nil), pendingToolCalls...),
})
messages = append(messages, pendingToolResults...)
textParts = textParts[:0]
reasoningParts = reasoningParts[:0]
reasoningSignature = ""
pendingToolCalls = pendingToolCalls[:0]
@@ -932,8 +919,6 @@ func buildMessagesFromPendingAssistantRaw(raw string) []Message {
for _, item := range payload.Content {
switch strings.TrimSpace(item.Type) {
case "reasoning":
flushToolCalls()
flushAssistantText(false)
if item.Text != "" {
reasoningParts = append(reasoningParts, item.Text)
}
@@ -941,12 +926,10 @@ func buildMessagesFromPendingAssistantRaw(raw string) []Message {
reasoningSignature = strings.TrimSpace(item.Signature)
}
case "text":
flushToolCalls()
if item.Text != "" {
textParts = append(textParts, item.Text)
}
case "tool-call":
flushAssistantText(false)
pendingToolCalls = append(pendingToolCalls, ToolCallDescriptor{
ID: strings.TrimSpace(item.ToolCallID),
Index: len(pendingToolCalls),
@@ -966,9 +949,7 @@ func buildMessagesFromPendingAssistantRaw(raw string) []Message {
}
}
}
flushAssistantText(false)
flushToolCalls()
flushAssistantText(true)
flushAssistantTurn()
return messages
}
@@ -0,0 +1,53 @@
package promptengine
import (
"reflect"
"testing"
)
func TestBuildReplayMessagesFromPendingAssistantOutputsKeepsTextAndToolCallInOneAssistantTurn(t *testing.T) {
raw := `{
"id":"1",
"role":"assistant",
"content":[
{"type":"reasoning","text":"I need to update service.go.","signature":"reasoning-signature"},
{"type":"text","text":"Now let me pass stream.Mode in service.go."},
{"type":"tool-call","toolCallId":"call_1","toolName":"PatchEdit","args":{"path":"/workspace/service.go","old_string":"old","new_string":"new"},"result":{"success":{"path":"/workspace/service.go"}}}
]
}`
first := BuildReplayMessagesFromPendingAssistantOutputs([]string{raw})
if len(first) != 2 {
t.Fatalf("message count = %d, want 2 (assistant tool-call turn plus tool result): %#v", len(first), first)
}
assistant := first[0]
if assistant.Role != "assistant" {
t.Fatalf("assistant role = %q, want assistant", assistant.Role)
}
if assistant.Content != "Now let me pass stream.Mode in service.go." {
t.Fatalf("assistant content = %q", assistant.Content)
}
if assistant.ReasoningContent != "I need to update service.go." {
t.Fatalf("assistant reasoning = %q", assistant.ReasoningContent)
}
if assistant.ReasoningSignature != "reasoning-signature" {
t.Fatalf("assistant reasoning signature = %q", assistant.ReasoningSignature)
}
if len(assistant.ToolCalls) != 1 {
t.Fatalf("assistant tool calls = %d, want 1", len(assistant.ToolCalls))
}
if assistant.ToolCalls[0].ID != "call_1" || assistant.ToolCalls[0].Function.Name != "PatchEdit" {
t.Fatalf("assistant tool call = %#v", assistant.ToolCalls[0])
}
toolResult := first[1]
if toolResult.Role != "tool" || toolResult.ToolCallID != "call_1" || toolResult.Name != "PatchEdit" {
t.Fatalf("tool result = %#v", toolResult)
}
second := BuildReplayMessagesFromPendingAssistantOutputs([]string{raw})
if !reflect.DeepEqual(second, first) {
t.Fatalf("rebuilding the same pending output changed messages:\nfirst: %#v\nsecond: %#v", first, second)
}
}
+23 -16
View File
@@ -23,10 +23,10 @@ type artifactRecorder struct {
}
type artifactSession struct {
conversationID string
turnSeq int64
requestPayload map[string]any
summaryPayload map[string]any
conversationID string
turnSeq int64
requestPrefix requestArtifactPrefix
hasRequestPrefix bool
}
type requestArtifactPrefix struct {
@@ -57,14 +57,22 @@ func (recorder *artifactRecorder) RecordLLMRequest(requestID string, _ string, m
if err != nil {
return "", err
}
session.requestPayload = cloneStringAnyMap(payload)
prefix, hasPrefix, prefixErr := decodeRequestPrefixPayload(payload)
session.requestPrefix = requestArtifactPrefix{}
session.hasRequestPrefix = false
if prefixErr == nil && hasPrefix && prefix != nil {
session.requestPrefix = *prefix
session.hasRequestPrefix = true
}
recorder.mu.Lock()
recorder.sessions[artifactSessionKey(requestID, modelCallID)] = session
recorder.mu.Unlock()
if prefix, ok, err := decodeRequestPrefixPayload(session.requestPayload); err == nil && ok && prefix != nil {
if prefixErr == nil && hasPrefix && prefix != nil {
recorder.persistLatestRequestPrefix(session.conversationID, requestID, modelCallID, prefix)
}
recorder.debug.LogProviderArtifact(context.Background(), requestID, session.conversationID, modelCallID, "llm_request", session.requestPayload)
// The payload is serialized synchronously for debug output and is deliberately
// not retained in sessions: it can contain the entire replayed conversation.
recorder.debug.LogProviderArtifact(context.Background(), requestID, session.conversationID, modelCallID, "llm_request", payload)
return "", nil
}
@@ -86,17 +94,16 @@ func (recorder *artifactRecorder) RecordLLMSummary(requestID string, _ string, m
if err != nil {
return "", err
}
session.summaryPayload = cloneStringAnyMap(payload)
recorder.mu.Lock()
recorder.sessions[artifactSessionKey(requestID, modelCallID)] = session
recorder.mu.Unlock()
if prefix, ok, err := decodeRequestPrefixPayload(session.requestPayload); err == nil && ok && prefix != nil {
if tokens := readInt64Value(session.summaryPayload["prompt_tokens_total"]); tokens > 0 {
prefix.PromptTokensTotal = tokens
if session.hasRequestPrefix {
if tokens := readInt64Value(payload["prompt_tokens_total"]); tokens > 0 {
session.requestPrefix.PromptTokensTotal = tokens
}
recorder.persistLatestRequestPrefix(session.conversationID, requestID, modelCallID, prefix)
recorder.mu.Lock()
recorder.sessions[artifactSessionKey(requestID, modelCallID)] = session
recorder.mu.Unlock()
recorder.persistLatestRequestPrefix(session.conversationID, requestID, modelCallID, &session.requestPrefix)
}
recorder.debug.LogProviderArtifact(context.Background(), requestID, session.conversationID, modelCallID, "llm_summary", session.summaryPayload)
recorder.debug.LogProviderArtifact(context.Background(), requestID, session.conversationID, modelCallID, "llm_summary", payload)
return "", nil
}
@@ -10,21 +10,6 @@ import (
modeladapter "cursor/internal/backend/agent/model"
)
func cloneStringAnyMap(value map[string]any) map[string]any {
if len(value) == 0 {
return nil
}
payload, err := json.Marshal(value)
if err != nil {
return nil
}
var decoded map[string]any
if err := json.Unmarshal(payload, &decoded); err != nil {
return nil
}
return decoded
}
func parseRFC3339Time(value string) time.Time {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
+13
View File
@@ -25,6 +25,7 @@ func (gateway *DefaultProviderGateway) StartStream(ctx context.Context, req Prov
if ctx == nil {
ctx = context.Background()
}
defer releaseArtifactSession(req.Observer, req.RequestID, req.ModelCallID)
requestKnobs := make(map[string]any, len(req.RequestKnobs)+2)
for key, value := range req.RequestKnobs {
requestKnobs[key] = value
@@ -60,3 +61,15 @@ func (gateway *DefaultProviderGateway) StartStream(ctx context.Context, req Prov
}
return nil
}
type artifactSessionCleaner interface {
ClearActiveArtifacts(requestID string, modelCallID string)
}
func releaseArtifactSession(observer modeladapter.LLMArtifactObserver, requestID string, modelCallID string) {
cleaner, ok := observer.(artifactSessionCleaner)
if !ok {
return
}
cleaner.ClearActiveArtifacts(requestID, modelCallID)
}
+3 -3
View File
@@ -152,9 +152,9 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon
case next.ModelID == "":
return nil, errors.New("模型适配器 modelID 不能为空")
case next.Type == "openai" && next.ReasoningEffort == "":
return nil, errors.New("模型适配器 reasoningEffort 仅支持 low、medium、high、xhigh")
return nil, errors.New("模型适配器 reasoningEffort 仅支持 low、medium、high、xhigh、max")
case next.Type == "openai" && next.OpenAIEndpoint == "":
return nil, errors.New("模型适配器 openAIEndpoint 仅支持 /v1/responses/v1/chat/completions")
return nil, errors.New("模型适配器 openAIEndpoint 仅支持 /v1/responses/v1/chat/completions 或 /custom(自定义路径)")
case next.Type == "openai" && next.OpenAIExtraParamsEnabled:
if err := validateJSONMap(next.OpenAIExtraParamsJSON, "openAIExtraParamsJSON"); err != nil {
return nil, err
@@ -216,7 +216,7 @@ func normalizeReasoningEffort(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "medium":
return "medium"
case "low", "high", "xhigh":
case "low", "high", "xhigh", "max":
return strings.ToLower(strings.TrimSpace(value))
default:
return ""
+6 -2
View File
@@ -46,6 +46,8 @@ const (
bootstrapStatsigDisableTerminalOutputUIStreaming = "disable_terminal_output_ui_streaming"
bootstrapStatsigBrowserCanvas = "browser_canvas"
bootstrapStatsigEnableMultitaskMode = "enable_multitask_mode"
bootstrapStatsigDecomposeAlwaysLocalExtHostGate = "decompose_always_local_ext_host"
bootstrapStatsigCursorExtensionsIsolationV2Gate = "cursor_extensions_isolation_v2"
bootstrapStatsigCursorAgentWorkerExtension = "enable_cursor_agent_worker_extension"
bootstrapStatsigExperimentName = "free_user_model_picker"
bootstrapStatsigVariantParam = "variant"
@@ -139,6 +141,8 @@ var bootstrapStatsigTemplate = statsigBootstrapTemplate{
bootstrapStatsigDisableTerminalOutputUIStreaming: buildEnabledStatsigGate(bootstrapStatsigDisableTerminalOutputUIStreaming),
bootstrapStatsigBrowserCanvas: buildEnabledStatsigGate(bootstrapStatsigBrowserCanvas),
bootstrapStatsigEnableMultitaskMode: buildEnabledStatsigGate(bootstrapStatsigEnableMultitaskMode),
bootstrapStatsigDecomposeAlwaysLocalExtHostGate: buildDisabledStatsigGate(bootstrapStatsigDecomposeAlwaysLocalExtHostGate),
bootstrapStatsigCursorExtensionsIsolationV2Gate: buildDisabledStatsigGate(bootstrapStatsigCursorExtensionsIsolationV2Gate),
bootstrapStatsigCursorAgentWorkerExtension: buildDisabledStatsigGate(bootstrapStatsigCursorAgentWorkerExtension),
},
DynamicConfigs: map[string]statsigDynamicConfigTemplate{
@@ -742,7 +746,7 @@ func buildThinkingEffortVariantDisplayName(modelDisplayName string, effortValue
func thinkingEffortValuesForAdapter(adapterType string) []string {
values := []string{"disabled", "low", "medium", "high", "xhigh"}
if strings.EqualFold(strings.TrimSpace(adapterType), "anthropic") {
if adapterType := strings.ToLower(strings.TrimSpace(adapterType)); adapterType == "openai" || adapterType == "anthropic" {
values = append(values, "max")
}
return values
@@ -769,7 +773,7 @@ func defaultThinkingEffortForAdapter(adapter legacyruntime.ModelAdapterConfig) s
if strings.EqualFold(strings.TrimSpace(adapter.Type), "anthropic") {
return normalizeAvailableModelThinkingEffort(adapter.AnthropicThinkingEffort, true, "xhigh")
}
return normalizeAvailableModelThinkingEffort(adapter.ReasoningEffort, false, "medium")
return normalizeAvailableModelThinkingEffort(adapter.ReasoningEffort, true, "medium")
}
func normalizeAvailableModelThinkingEffort(raw string, allowMax bool, fallback string) string {
@@ -0,0 +1,29 @@
package upstream
import (
"encoding/json"
"testing"
)
func TestBuildBootstrapStatsigConfigJSONDisablesAlwaysLocalDecompositionGate(t *testing.T) {
payload, err := buildBootstrapStatsigConfigJSON(12345, "test-auth-id")
if err != nil {
t.Fatalf("build bootstrap statsig config: %v", err)
}
var decoded statsigBootstrapTemplate
if err := json.Unmarshal(payload, &decoded); err != nil {
t.Fatalf("decode bootstrap statsig config: %v", err)
}
gate, ok := decoded.FeatureGates[bootstrapStatsigDecomposeAlwaysLocalExtHostGate]
if !ok {
t.Fatalf("missing feature gate %q", bootstrapStatsigDecomposeAlwaysLocalExtHostGate)
}
if value, _ := gate["value"].(bool); value {
t.Fatalf("expected %q to be disabled", bootstrapStatsigDecomposeAlwaysLocalExtHostGate)
}
if ruleID, _ := gate["rule_id"].(string); ruleID != "local_disabled" {
t.Fatalf("unexpected rule_id: %q", ruleID)
}
}
+1 -1
View File
@@ -183,7 +183,7 @@ func (s *WindowService) OpenModelEditorWindow(index int, adapterJSON string) {
Hidden: false,
HideOnEscape: false,
MinimiseButtonState: application.ButtonEnabled,
MaximiseButtonState: application.ButtonHidden,
MaximiseButtonState: application.ButtonEnabled,
CloseButtonState: application.ButtonEnabled,
BackgroundColour: application.RGBA{Red: 25, Green: 25, Blue: 25, Alpha: 255},
Mac: application.MacWindow{
+1 -1
View File
@@ -668,7 +668,7 @@ func normalizeModelAdapterTestType(value string) string {
func normalizeModelAdapterTestReasoning(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "low", "medium", "high", "xhigh":
case "low", "medium", "high", "xhigh", "max":
return strings.ToLower(strings.TrimSpace(value))
default:
return "medium"
+77 -1
View File
@@ -3,6 +3,7 @@ package cursor
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"os"
"path/filepath"
@@ -23,8 +24,14 @@ const (
cursorStateDBRelativePath = "Cursor/User/globalStorage/state.vscdb"
cursorStateDarwinRelativePath = "Library/Application Support/Cursor/User/globalStorage/state.vscdb"
cursorStateLinuxRelativePath = ".config/Cursor/User/globalStorage/state.vscdb"
cursorStateStatsigBootstrapKey = "workbench.experiments.statsigBootstrap"
)
var cursorStateDisabledStatsigGates = []string{
"decompose_always_local_ext_host",
"cursor_extensions_isolation_v2",
}
// InjectCursorUserInfo synchronizes the Cursor user-level auth cache used by the
// Settings page. It does not modify the installed Cursor app bundle.
func InjectCursorUserInfo(email, token string) error {
@@ -42,11 +49,12 @@ func InjectCursorUserInfo(email, token string) error {
}
logger.Infof(
"injectCursorUserInfo synced path=%s email=%s membership=%s subscription=%s",
"injectCursorUserInfo synced path=%s email=%s membership=%s subscription=%s disabled_statsig_gates=%s",
stateDBPath,
values["cursorAuth/cachedEmail"],
values["cursorAuth/stripeMembershipType"],
values["cursorAuth/stripeSubscriptionStatus"],
strings.Join(cursorStateDisabledStatsigGates, ","),
)
return nil
}
@@ -111,6 +119,10 @@ func syncCursorAuthStateDB(path string, values map[string]string) error {
}
}
if err := disableCursorStatsigGates(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
@@ -118,6 +130,70 @@ func syncCursorAuthStateDB(path string, values map[string]string) error {
return nil
}
func disableCursorStatsigGates(ctx context.Context, tx *sql.Tx) error {
var raw []byte
err := tx.QueryRowContext(ctx, "SELECT value FROM ItemTable WHERE key = ?", cursorStateStatsigBootstrapKey).Scan(&raw)
if err != nil {
if err == sql.ErrNoRows {
return nil
}
return err
}
var payload map[string]any
if err := json.Unmarshal(raw, &payload); err != nil {
return fmt.Errorf("解析 Cursor Statsig bootstrap 失败: %w", err)
}
featureGates, _ := payload["feature_gates"].(map[string]any)
if featureGates == nil {
featureGates = map[string]any{}
payload["feature_gates"] = featureGates
}
hashUsed, _ := payload["hash_used"].(string)
for _, gate := range cursorStateDisabledStatsigGates {
disableCursorStatsigGate(featureGates, gate)
if strings.EqualFold(hashUsed, "djb2") {
disableCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate))
}
}
updated, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("编码 Cursor Statsig bootstrap 失败: %w", err)
}
if _, err := tx.ExecContext(ctx, "UPDATE ItemTable SET value = ? WHERE key = ?", updated, cursorStateStatsigBootstrapKey); err != nil {
return err
}
return nil
}
func disableCursorStatsigGate(featureGates map[string]any, key string) {
gate, _ := featureGates[key].(map[string]any)
if gate == nil {
gate = map[string]any{
"name": key,
"rule_id": "local_disabled",
"ruleID": "local_disabled",
"group_name": "local_disabled",
"groupName": "local_disabled",
"id_type": "userID",
"idType": "userID",
}
featureGates[key] = gate
}
gate["value"] = false
}
func cursorStateDJB2Hash(value string) string {
var hash uint32
for _, b := range []byte(value) {
hash = hash*31 + uint32(b)
}
return fmt.Sprintf("%d", hash)
}
func resolveCursorStateDBPath() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
+22 -4
View File
@@ -13,6 +13,7 @@ const ChannelIDHexLength = 16
const (
OpenAIEndpointResponses = "/v1/responses"
OpenAIEndpointChatCompletions = "/v1/chat/completions"
OpenAIEndpointCustom = "/custom"
)
func NormalizeBaseURL(raw string) (string, error) {
@@ -39,20 +40,37 @@ func NormalizeBaseURL(raw string) (string, error) {
return normalized, nil
}
// NormalizeOpenAIEndpoint 归一化 OpenAI endpoint 路径。
// 支持三个预设值:/v1/responses、/v1/chat/completions、/custom(自定义路径)。
// 选 /custom 时,用户需在接口地址栏填写完整请求 URL。
func NormalizeOpenAIEndpoint(providerType string, endpoint string) string {
if strings.TrimSpace(strings.ToLower(providerType)) != "openai" {
return ""
}
switch strings.ToLower(strings.TrimSpace(endpoint)) {
case "", OpenAIEndpointResponses:
normalized := strings.TrimSpace(endpoint)
switch normalized {
case "":
return OpenAIEndpointResponses
case OpenAIEndpointChatCompletions:
return OpenAIEndpointChatCompletions
case OpenAIEndpointResponses, OpenAIEndpointChatCompletions, OpenAIEndpointCustom:
return normalized
default:
return ""
}
}
// OpenAIEndpointShape 根据 endpoint 路径末段推断协议形态。
// 返回 "responses"Responses API)或 "chat/completions"Chat Completions API)。
// 这样 /v1/chat/completions、/v4/chat/completions、/chat/completions 都走同一协议分支。
func OpenAIEndpointShape(endpoint string) string {
lower := strings.ToLower(strings.TrimSpace(endpoint))
switch {
case strings.HasSuffix(lower, "/responses"):
return "responses"
default:
return "chat/completions"
}
}
func BuildLegacyChannelID(baseURL string, modelID string, apiKey string, name string) string {
return buildChannelID([]string{
strings.TrimSpace(baseURL),
+2 -2
View File
@@ -138,7 +138,7 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon
case next.ModelID == "":
return nil, errors.New("模型适配器 modelID 不能为空")
case next.Type == "openai" && next.ReasoningEffort == "":
return nil, errors.New("模型适配器 reasoningEffort 仅支持 low、medium、high、xhigh")
return nil, errors.New("模型适配器 reasoningEffort 仅支持 low、medium、high、xhigh、max")
case next.Type == "openai" && next.OpenAIEndpoint == "":
return nil, errors.New("模型适配器 openAIEndpoint 仅支持 /v1/responses 或 /v1/chat/completions")
case next.Type == "openai" && next.OpenAIExtraParamsEnabled:
@@ -202,7 +202,7 @@ func normalizeReasoningEffort(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "medium":
return "medium"
case "low", "high", "xhigh":
case "low", "high", "xhigh", "max":
return strings.ToLower(strings.TrimSpace(value))
default:
return ""
+3 -1
View File
@@ -1,8 +1,10 @@
你是一个极度务实且高效的软件工程师。你认真对待工程质量,并以直接、客观的陈述方式体现协作。你沟通高效,在不加入无关细节的前提下,清楚告知用户你正在进行的操作。
!IMPORTANT 如非用户明确要求,禁止使用Subagent
你具有极强的架构经验和模块化经验,当用户提出泛泛的需求时,**你会不断评估架构、模块、数据流向、状态机的合理性**,你会自信的引导用户重构代码。
不会从代码、函数等层面交流,只输出你认为用户能看懂的数据结构、演变过程、模块关系、作用域、伪代码等,**并附带注释**。
你只输出你认为普通用户能看懂的回复,再不炫技的情况下回复内容可能是数据结构、演变过程、模块关系、作用域、伪代码、mermaid等,**并附带注释**。
除非你已经确定项目是面向对象的,其他情况下你都偏好 Functional Programming. DSL化编码风格是是你喜欢的,也是用户喜欢的,也是可读的、优秀的。
+11 -10
View File
@@ -1,10 +1,11 @@
QQ10群:1049354347
----
- 修复 Taking longer than expected.
- 修复gpt5.5思考内容(加密内容展示)
- 新增多任务能力,支持在会话中并行处理多个任务
- 优化 Subagent 模型选择逻辑,提升分发稳定性
- 修复 macOS 下证书信任失败导致无法正常连接的问题
- 新增 ServerTime 接口及 Mock 实现,补齐本地模式兼容性
- 优化 OpenAI / Anthropic 适配器的请求结构,提升模型请求稳定性
- 优化提示词表达,精简系统前缀,提升响应质量
- 修复内存泄漏问题,该可能导致内存异常占用
- 支持俄语增加翻译范围
- 修复qwen-3.8-max中断问题(mimo也应该属于同一类问题)
- 修复claude模型可能无法识别图片问题 @GGHansome
- 支持自定义Openai端点 @Sxuan-Coder
- WebSearch 接入百度搜索,DuckDuckGo 作为兜底 @杨超
- 修复一些兼容性问题 @kael-odin
- 修复window上一些表现问题 @philau2512
🔔 如何让AI自动拉模型配置? (以下为提示词,把地址和密钥换为你的)
我的模型配置在 /.cursor-local-assistant-v2/config.yaml,我的API地址是:https://xxx 密钥是xxx,帮我拉所有模型配置进去,不要影响已有模型,根据models标准接口拉取。