Compare commits

...
9 Commits
Author SHA1 Message Date
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
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
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
24 changed files with 354 additions and 97 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ info:
description: "Cursor助手" description: "Cursor助手"
copyright: "© 2026, Cursor助手" copyright: "© 2026, Cursor助手"
comments: "Cursor助手" comments: "Cursor助手"
version: "0.0.38" version: "0.0.40"
dev_mode: dev_mode:
root_path: . root_path: .
+2 -2
View File
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.0.38</string> <string>0.0.39</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.0.38</string> <string>0.0.39</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>12.0.0</string> <string>12.0.0</string>
<key>LSUIElement</key> <key>LSUIElement</key>
+2 -2
View File
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.0.38</string> <string>0.0.39</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.0.38</string> <string>0.0.39</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>12.0.0</string> <string>12.0.0</string>
<key>LSUIElement</key> <key>LSUIElement</key>
+28 -15
View File
@@ -6,7 +6,7 @@
name: "Cursor助手" name: "Cursor助手"
arch: ${GOARCH} arch: ${GOARCH}
platform: "linux" platform: "linux"
version: "0.0.38" version: "0.0.39"
section: "default" section: "default"
priority: "extra" priority: "extra"
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
@@ -24,24 +24,24 @@ contents:
- src: "./build/linux/Cursor助手.desktop" - src: "./build/linux/Cursor助手.desktop"
dst: "/usr/share/applications/Cursor助手.desktop" dst: "/usr/share/applications/Cursor助手.desktop"
# Default dependencies for Debian 12/Ubuntu 22.04+ with WebKit 4.1 # Default dependencies for the GTK4 + WebKitGTK 6.0 stack (Ubuntu 24.04+ / Debian 13+)
depends: depends:
- libgtk-3-0 - libgtk-4-1
- libwebkit2gtk-4.1-0 - libwebkitgtk-6.0-4
# Distribution-specific overrides for different package formats and WebKit versions # Distribution-specific overrides for different package formats
overrides: overrides:
# RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0) # RPM packages for Fedora / RHEL / AlmaLinux / Rocky Linux
rpm: rpm:
depends: depends:
- gtk3 - gtk4
- webkit2gtk4.1 - webkitgtk6.0
# Arch Linux packages (WebKit 4.1) # Arch Linux packages
archlinux: archlinux:
depends: depends:
- gtk3 - gtk4
- webkit2gtk-4.1 - webkitgtk-6.0
# scripts section to ensure desktop database is updated after install # scripts section to ensure desktop database is updated after install
scripts: scripts:
@@ -50,13 +50,26 @@ scripts:
# preremove: "./build/linux/nfpm/scripts/preremove.sh" # preremove: "./build/linux/nfpm/scripts/preremove.sh"
# postremove: "./build/linux/nfpm/scripts/postremove.sh" # postremove: "./build/linux/nfpm/scripts/postremove.sh"
# If you build your app with -tags gtk3 (legacy WebKit2GTK 4.1 stack — supported through v3.0.x, removed in v3.1),
# replace the depends/overrides above with these:
#
# depends:
# - libgtk-3-0
# - libwebkit2gtk-4.1-0
# overrides:
# rpm:
# depends:
# - gtk3
# - webkit2gtk4.1
# archlinux:
# depends:
# - gtk3
# - webkit2gtk-4.1
#
# replaces: # replaces:
# - foobar # - foobar
# provides: # provides:
# - bar # - bar
# depends:
# - gtk3
# - libwebkit2gtk
# recommends: # recommends:
# - whatever # - whatever
# suggests: # suggests:
+2 -2
View File
@@ -1,10 +1,10 @@
{ {
"fixed": { "fixed": {
"file_version": "0.0.38" "file_version": "0.0.39"
}, },
"info": { "info": {
"0000": { "0000": {
"ProductVersion": "0.0.38", "ProductVersion": "0.0.39",
"CompanyName": "Cursor助手", "CompanyName": "Cursor助手",
"FileDescription": "Cursor助手", "FileDescription": "Cursor助手",
"LegalCopyright": "© 2026, Cursor助手", "LegalCopyright": "© 2026, Cursor助手",
+37 -12
View File
@@ -14,7 +14,7 @@
!define INFO_PRODUCTNAME "Cursor助手" !define INFO_PRODUCTNAME "Cursor助手"
!endif !endif
!ifndef INFO_PRODUCTVERSION !ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "0.0.38" !define INFO_PRODUCTVERSION "0.0.39"
!endif !endif
!ifndef INFO_COPYRIGHT !ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "© 2026, Cursor助手" !define INFO_COPYRIGHT "© 2026, Cursor助手"
@@ -27,8 +27,16 @@
!endif !endif
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
!ifndef WAILS_INSTALL_SCOPE
!define WAILS_INSTALL_SCOPE "machine"
!endif
!ifndef REQUEST_EXECUTION_LEVEL !ifndef REQUEST_EXECUTION_LEVEL
!define REQUEST_EXECUTION_LEVEL "admin" !if "${WAILS_INSTALL_SCOPE}" == "user"
!define REQUEST_EXECUTION_LEVEL "user"
!else
!define REQUEST_EXECUTION_LEVEL "admin"
!endif
!endif !endif
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
@@ -115,23 +123,40 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
WriteUninstaller "$INSTDIR\uninstall.exe" WriteUninstaller "$INSTDIR\uninstall.exe"
SetRegView 64 SetRegView 64
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" !if "${WAILS_INSTALL_SCOPE}" == "user"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" WriteRegStr HKCU "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0 IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0"
!else
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
!endif
!macroend !macroend
!macro wails.deleteUninstaller !macro wails.deleteUninstaller
Delete "$INSTDIR\uninstall.exe" Delete "$INSTDIR\uninstall.exe"
SetRegView 64 SetRegView 64
DeleteRegKey HKLM "${UNINST_KEY}" !if "${WAILS_INSTALL_SCOPE}" == "user"
DeleteRegKey HKCU "${UNINST_KEY}"
!else
DeleteRegKey HKLM "${UNINST_KEY}"
!endif
!macroend !macroend
!macro wails.setShellContext !macro wails.setShellContext
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?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"> <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.39" processorArchitecture="*"/>
<dependency> <dependency>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/> <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
@@ -7,6 +7,7 @@ import {
createEmptyModelAdapter, createEmptyModelAdapter,
normalizeModelAdapter, normalizeModelAdapter,
OPENAI_ENDPOINT_CHAT_COMPLETIONS, OPENAI_ENDPOINT_CHAT_COMPLETIONS,
OPENAI_ENDPOINT_CUSTOM,
OPENAI_ENDPOINT_RESPONSES, OPENAI_ENDPOINT_RESPONSES,
OPENAI_EXTRA_PARAMS_DEFAULT_JSON, OPENAI_EXTRA_PARAMS_DEFAULT_JSON,
} from "@/state/appState"; } from "@/state/appState";
@@ -22,6 +23,7 @@ const reasoningEffortOptions = [
{ label: "中", value: "medium", icon: "icon-[mdi--head-lightbulb-outline]" }, { label: "中", value: "medium", icon: "icon-[mdi--head-lightbulb-outline]" },
{ label: "高", value: "high", icon: "icon-[mdi--brain]" }, { label: "高", value: "high", icon: "icon-[mdi--brain]" },
{ label: "极高", value: "xhigh", icon: "icon-[mdi--head-cog-outline]" }, { label: "极高", value: "xhigh", icon: "icon-[mdi--head-cog-outline]" },
{ label: "最高", value: "max", icon: "icon-[mdi--brain]" },
]; ];
const anthropicThinkingEffortOptions = [ const anthropicThinkingEffortOptions = [
@@ -35,10 +37,11 @@ const anthropicThinkingEffortOptions = [
const openAIEndpointOptions = [ const openAIEndpointOptions = [
{ label: "/v1/responses", value: OPENAI_ENDPOINT_RESPONSES, icon: "icon-[mdi--api]" }, { 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: "/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 = { 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({ const props = defineProps({
+29 -12
View File
@@ -274,18 +274,6 @@
} }
] ]
}, },
"1b7d24b212e52c54": {
"source": "{0} 的推理强度仅支持 low、medium、high、xhigh",
"kind": "template",
"placeholders": 1,
"refs": [
{
"file": "src/state/appState.js",
"line": 315,
"column": 14
}
]
},
"1bc77f5ab979f4c1": { "1bc77f5ab979f4c1": {
"source": "新增模型配置", "source": "新增模型配置",
"kind": "text", "kind": "text",
@@ -1175,6 +1163,23 @@
} }
] ]
}, },
"8672864e90417138": {
"source": "最高",
"kind": "text",
"placeholders": 0,
"refs": [
{
"file": "src/components/ModelAdapterModal.vue",
"line": 26,
"column": 12
},
{
"file": "src/views/ModelEditor.vue",
"line": 41,
"column": 12
}
]
},
"86df7ec743047234": { "86df7ec743047234": {
"source": "服务运行中", "source": "服务运行中",
"kind": "text", "kind": "text",
@@ -2110,6 +2115,18 @@
} }
] ]
}, },
"dbb4b5be9b5723dc": {
"source": "{0} 的推理强度仅支持 low、medium、high、xhigh、max",
"kind": "template",
"placeholders": 1,
"refs": [
{
"file": "src/state/appState.js",
"line": 435,
"column": 14
}
]
},
"dbee6e7139243362": { "dbee6e7139243362": {
"source": "{0} 的接口地址不能为空", "source": "{0} 的接口地址不能为空",
"kind": "template", "kind": "template",
+2 -1
View File
@@ -21,7 +21,6 @@
"15d124b200ddabed": "Maximum number of context tokens the model can accept in a single request. Leave blank to use the default.", "15d124b200ddabed": "Maximum number of context tokens the model can accept in a single request. Leave blank to use the default.",
"18b7312022cd1840": "Start Service", "18b7312022cd1840": "Start Service",
"1af38868896cf53d": "Routing mode only supports local or upstream", "1af38868896cf53d": "Routing mode only supports local or upstream",
"1b7d24b212e52c54": "{0} reasoning effort only supports low, medium, high, and xhigh",
"1bc77f5ab979f4c1": "Add Model Settings", "1bc77f5ab979f4c1": "Add Model Settings",
"1e238093b79b3165": "Uses 65536 by default when left blank", "1e238093b79b3165": "Uses 65536 by default when left blank",
"24343a2096988d42": "Failed to open", "24343a2096988d42": "Failed to open",
@@ -84,6 +83,7 @@
"7e9e334aeb0bdc07": "Service operation failed", "7e9e334aeb0bdc07": "Service operation failed",
"7f68ebad19ba6bcd": "Check for Updates", "7f68ebad19ba6bcd": "Check for Updates",
"81123c56d5d880d0": "API Key", "81123c56d5d880d0": "API Key",
"8672864e90417138": "Max",
"86df7ec743047234": "Service running", "86df7ec743047234": "Service running",
"87ed126f7bd1121e": "Routing Mode", "87ed126f7bd1121e": "Routing Mode",
"899add6275682210": "Uses 200000 by default when left blank", "899add6275682210": "Uses 200000 by default when left blank",
@@ -154,6 +154,7 @@
"d7da2aabd35772ec": "e.g. 200000 (leave blank to use the default)", "d7da2aabd35772ec": "e.g. 200000 (leave blank to use the default)",
"da590a8fe3ce4de0": "Please select", "da590a8fe3ce4de0": "Please select",
"daede9881787abe7": "Notes", "daede9881787abe7": "Notes",
"dbb4b5be9b5723dc": "{0} reasoning effort only supports low, medium, high, xhigh, and max",
"dbee6e7139243362": "{0} base URL cannot be empty", "dbee6e7139243362": "{0} base URL cannot be empty",
"dc82c5e8fb2ab777": "Version: v{0}", "dc82c5e8fb2ab777": "Version: v{0}",
"de8184da1ef88d03": "Configured", "de8184da1ef88d03": "Configured",
+2 -1
View File
@@ -21,7 +21,6 @@
"15d124b200ddabed": "モデルが1回のリクエストで受け取れる最大コンテキスト Token 数。空欄の場合はデフォルト値を使用します。", "15d124b200ddabed": "モデルが1回のリクエストで受け取れる最大コンテキスト Token 数。空欄の場合はデフォルト値を使用します。",
"18b7312022cd1840": "サービスを開始", "18b7312022cd1840": "サービスを開始",
"1af38868896cf53d": "ルーティングモードは local または upstream のみサポートします", "1af38868896cf53d": "ルーティングモードは local または upstream のみサポートします",
"1b7d24b212e52c54": "{0} の推論強度は low、medium、high、xhigh のみサポートします",
"1bc77f5ab979f4c1": "モデル設定を追加", "1bc77f5ab979f4c1": "モデル設定を追加",
"1e238093b79b3165": "空欄で 65536", "1e238093b79b3165": "空欄で 65536",
"24343a2096988d42": "開けませんでした", "24343a2096988d42": "開けませんでした",
@@ -84,6 +83,7 @@
"7e9e334aeb0bdc07": "サービス操作に失敗しました", "7e9e334aeb0bdc07": "サービス操作に失敗しました",
"7f68ebad19ba6bcd": "アップデートを確認", "7f68ebad19ba6bcd": "アップデートを確認",
"81123c56d5d880d0": "API キー", "81123c56d5d880d0": "API キー",
"8672864e90417138": "最大",
"86df7ec743047234": "サービス稼働中", "86df7ec743047234": "サービス稼働中",
"87ed126f7bd1121e": "ルーティングモード", "87ed126f7bd1121e": "ルーティングモード",
"899add6275682210": "空欄で 200000", "899add6275682210": "空欄で 200000",
@@ -154,6 +154,7 @@
"d7da2aabd35772ec": "例: 200000(空欄でデフォルト値)", "d7da2aabd35772ec": "例: 200000(空欄でデフォルト値)",
"da590a8fe3ce4de0": "選択してください", "da590a8fe3ce4de0": "選択してください",
"daede9881787abe7": "メモ", "daede9881787abe7": "メモ",
"dbb4b5be9b5723dc": "{0} の推論強度は low、medium、high、xhigh、max のみサポートします",
"dbee6e7139243362": "{0} のベース URL は必須です", "dbee6e7139243362": "{0} のベース URL は必須です",
"dc82c5e8fb2ab777": "バージョン: v{0}", "dc82c5e8fb2ab777": "バージョン: v{0}",
"de8184da1ef88d03": "設定済み", "de8184da1ef88d03": "設定済み",
+2 -1
View File
@@ -21,7 +21,6 @@
"15d124b200ddabed": "模型单次可接受的最大上下文 Token 数。留空时使用默认值。", "15d124b200ddabed": "模型单次可接受的最大上下文 Token 数。留空时使用默认值。",
"18b7312022cd1840": "启动服务", "18b7312022cd1840": "启动服务",
"1af38868896cf53d": "运行模式仅支持 local 或 upstream", "1af38868896cf53d": "运行模式仅支持 local 或 upstream",
"1b7d24b212e52c54": "{0} 的推理强度仅支持 low、medium、high、xhigh",
"1bc77f5ab979f4c1": "新增模型配置", "1bc77f5ab979f4c1": "新增模型配置",
"1e238093b79b3165": "留空时默认 65536", "1e238093b79b3165": "留空时默认 65536",
"24343a2096988d42": "打开失败", "24343a2096988d42": "打开失败",
@@ -84,6 +83,7 @@
"7e9e334aeb0bdc07": "服务操作失败", "7e9e334aeb0bdc07": "服务操作失败",
"7f68ebad19ba6bcd": "检查更新", "7f68ebad19ba6bcd": "检查更新",
"81123c56d5d880d0": "访问密钥", "81123c56d5d880d0": "访问密钥",
"8672864e90417138": "最高",
"86df7ec743047234": "服务运行中", "86df7ec743047234": "服务运行中",
"87ed126f7bd1121e": "运行模式", "87ed126f7bd1121e": "运行模式",
"899add6275682210": "留空时默认 200000", "899add6275682210": "留空时默认 200000",
@@ -154,6 +154,7 @@
"d7da2aabd35772ec": "例如:200000(留空用默认值)", "d7da2aabd35772ec": "例如:200000(留空用默认值)",
"da590a8fe3ce4de0": "请选择", "da590a8fe3ce4de0": "请选择",
"daede9881787abe7": "备注", "daede9881787abe7": "备注",
"dbb4b5be9b5723dc": "{0} 的推理强度仅支持 low、medium、high、xhigh、max",
"dbee6e7139243362": "{0} 的接口地址不能为空", "dbee6e7139243362": "{0} 的接口地址不能为空",
"dc82c5e8fb2ab777": "版本:v{0}", "dc82c5e8fb2ab777": "版本:v{0}",
"de8184da1ef88d03": "已配置", "de8184da1ef88d03": "已配置",
+13 -5
View File
@@ -22,11 +22,12 @@ import {
const APP_STATE_STORAGE_KEY = "cursor-client:runtime-state:v2"; const APP_STATE_STORAGE_KEY = "cursor-client:runtime-state:v2";
const GENERIC_SERVICE_ERROR = "服务错误"; const GENERIC_SERVICE_ERROR = "服务错误";
const SUPPORTED_MODEL_ADAPTER_TYPES = new Set(["openai", "anthropic"]); 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"]); const SUPPORTED_ANTHROPIC_THINKING_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
export const ANTHROPIC_THINKING_EFFORT_DEFAULT = "xhigh"; export const ANTHROPIC_THINKING_EFFORT_DEFAULT = "xhigh";
export const OPENAI_ENDPOINT_RESPONSES = "/v1/responses"; export const OPENAI_ENDPOINT_RESPONSES = "/v1/responses";
export const OPENAI_ENDPOINT_CHAT_COMPLETIONS = "/v1/chat/completions"; export const OPENAI_ENDPOINT_CHAT_COMPLETIONS = "/v1/chat/completions";
export const OPENAI_ENDPOINT_CUSTOM = "/custom";
export const OPENAI_EXTRA_PARAMS_DEFAULT_JSON = `{ export const OPENAI_EXTRA_PARAMS_DEFAULT_JSON = `{
"service_tier": "priority" "service_tier": "priority"
}`; }`;
@@ -34,7 +35,7 @@ export const EXTRA_PARAMS_DEFAULT_JSON = `{
}`; }`;
export const CUSTOM_HEADERS_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 SUPPORTED_ROUTE_MODES = new Set(["local", "upstream"]);
const PROXY_STATE_EVENT = "proxy:state"; const PROXY_STATE_EVENT = "proxy:state";
const USER_CONFIG_CHANGED_EVENT = "user-config:changed"; 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) { function normalizeOpenAIEndpoint(value) {
const text = asString(value).toLowerCase(); const text = asString(value).toLowerCase();
if (!text) { if (!text) {
@@ -290,6 +294,10 @@ function normalizeOpenAIEndpoint(value) {
return SUPPORTED_OPENAI_ENDPOINTS.has(text) ? text : ""; return SUPPORTED_OPENAI_ENDPOINTS.has(text) ? text : "";
} }
function isValidOpenAIEndpoint(value) {
return normalizeOpenAIEndpoint(value) !== "";
}
function validateJSONObject(value, label) { function validateJSONObject(value, label) {
const text = asString(value); const text = asString(value);
if (!text) { if (!text) {
@@ -424,10 +432,10 @@ export function validateModelAdapters(source) {
return `${prefix} 的模型标识不能为空`; return `${prefix} 的模型标识不能为空`;
} }
if (adapter.type === "openai" && !SUPPORTED_REASONING_EFFORTS.has(adapter.reasoningEffort)) { 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)) { if (adapter.type === "openai" && !isValidOpenAIEndpoint(adapter.openAIEndpoint)) {
return `${prefix} 的 OpenAI 端点仅支持 /v1/responses/v1/chat/completions`; return `${prefix} 的 OpenAI 端点仅支持 /v1/responses/v1/chat/completions 或以 / 开头的自定义路径`;
} }
if (adapter.type === "openai" && adapter.openAIExtraParamsEnabled) { if (adapter.type === "openai" && adapter.openAIExtraParamsEnabled) {
const extraParamsError = validateOpenAIExtraParamsJSON(adapter.openAIExtraParamsJSON); const extraParamsError = validateOpenAIExtraParamsJSON(adapter.openAIExtraParamsJSON);
+4 -1
View File
@@ -17,6 +17,7 @@ import {
isModelAdapterTestResultStale, isModelAdapterTestResultStale,
normalizeModelAdapter, normalizeModelAdapter,
OPENAI_ENDPOINT_CHAT_COMPLETIONS, OPENAI_ENDPOINT_CHAT_COMPLETIONS,
OPENAI_ENDPOINT_CUSTOM,
OPENAI_ENDPOINT_RESPONSES, OPENAI_ENDPOINT_RESPONSES,
OPENAI_EXTRA_PARAMS_DEFAULT_JSON, OPENAI_EXTRA_PARAMS_DEFAULT_JSON,
runModelAdapterTest, runModelAdapterTest,
@@ -37,6 +38,7 @@ const reasoningEffortOptions = [
{ label: "中", value: "medium", icon: "icon-[mdi--head-lightbulb-outline]" }, { label: "中", value: "medium", icon: "icon-[mdi--head-lightbulb-outline]" },
{ label: "高", value: "high", icon: "icon-[mdi--brain]" }, { label: "高", value: "high", icon: "icon-[mdi--brain]" },
{ label: "极高", value: "xhigh", icon: "icon-[mdi--head-cog-outline]" }, { label: "极高", value: "xhigh", icon: "icon-[mdi--head-cog-outline]" },
{ label: "最高", value: "max", icon: "icon-[mdi--brain]" },
]; ];
const anthropicThinkingEffortOptions = [ const anthropicThinkingEffortOptions = [
@@ -50,6 +52,7 @@ const anthropicThinkingEffortOptions = [
const openAIEndpointOptions = [ const openAIEndpointOptions = [
{ label: "/v1/responses", value: OPENAI_ENDPOINT_RESPONSES, icon: "icon-[mdi--api]" }, { 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: "/v1/chat/completions", value: OPENAI_ENDPOINT_CHAT_COMPLETIONS, icon: "icon-[mdi--message-text-outline]" },
{ label: "自定义路径(请输入完整请求地址)", value: OPENAI_ENDPOINT_CUSTOM, icon: "icon-[mdi--pencil-outline]" },
]; ];
const editorIndex = ref(-1); const editorIndex = ref(-1);
@@ -128,7 +131,7 @@ const fieldTips = {
contextWindowTokens: "模型单次可接受的最大上下文 Token 数。留空时使用默认值。", contextWindowTokens: "模型单次可接受的最大上下文 Token 数。留空时使用默认值。",
reasoningEffort: "推理强度仅对部分支持 reasoning_effort 的模型生效,并不是所有模型都支持。越高通常越稳,但也可能更慢。", reasoningEffort: "推理强度仅对部分支持 reasoning_effort 的模型生效,并不是所有模型都支持。越高通常越稳,但也可能更慢。",
maxCompletionTokens: "单次回复允许生成的最大 Token 数。留空时使用默认值。", maxCompletionTokens: "单次回复允许生成的最大 Token 数。留空时使用默认值。",
openAIEndpoint: "OpenAI 兼容接口使用的协议端点。未选择时默认使用 /v1/responses。", openAIEndpoint: "选择接口协议端点。选“自定义路径”时,请在接口地址栏填写完整请求地址(含 /chat/completions 或 /responses 路径后缀),系统会根据末段自动判断协议形态。",
openAIExtraParams: "开启后会把 JSON 对象覆盖到 OpenAI 请求体。同名字段以这里为准。OpenAI service_tier 支持 auto、default、flex、scale、priority。", openAIExtraParams: "开启后会把 JSON 对象覆盖到 OpenAI 请求体。同名字段以这里为准。OpenAI service_tier 支持 auto、default、flex、scale、priority。",
customHeaders: "开启后会把 JSON 对象覆盖到最终请求头。同名请求头以这里为准,值必须是字符串。", customHeaders: "开启后会把 JSON 对象覆盖到最终请求头。同名请求头以这里为准,值必须是字符串。",
anthropicExtraParams: "开启后会把 JSON 对象覆盖到 Anthropic 请求体。同名字段以这里为准。", anthropicExtraParams: "开启后会把 JSON 对象覆盖到 Anthropic 请求体。同名字段以这里为准。",
+78 -18
View File
@@ -29,7 +29,7 @@ type openAIRequestBody struct {
Tools []json.RawMessage `json:"tools,omitempty"` Tools []json.RawMessage `json:"tools,omitempty"`
Messages []map[string]any `json:"messages"` Messages []map[string]any `json:"messages"`
Stream bool `json:"stream"` Stream bool `json:"stream"`
MaxTokens int `json:"max_tokens"` MaxTokens int `json:"max_tokens,omitempty"`
StreamOptions map[string]any `json:"stream_options"` StreamOptions map[string]any `json:"stream_options"`
ReasoningEffort string `json:"reasoning_effort,omitempty"` ReasoningEffort string `json:"reasoning_effort,omitempty"`
PromptCacheKey string `json:"prompt_cache_key,omitempty"` PromptCacheKey string `json:"prompt_cache_key,omitempty"`
@@ -41,7 +41,7 @@ type openAIResponsesRequestBody struct {
Input []map[string]any `json:"input"` Input []map[string]any `json:"input"`
Tools []map[string]any `json:"tools,omitempty"` Tools []map[string]any `json:"tools,omitempty"`
Stream bool `json:"stream"` Stream bool `json:"stream"`
MaxOutputTokens int `json:"max_output_tokens"` MaxOutputTokens int `json:"max_output_tokens,omitempty"`
Reasoning *openAIResponsesReasoning `json:"reasoning,omitempty"` Reasoning *openAIResponsesReasoning `json:"reasoning,omitempty"`
Include []string `json:"include,omitempty"` Include []string `json:"include,omitempty"`
PromptCacheKey string `json:"prompt_cache_key,omitempty"` PromptCacheKey string `json:"prompt_cache_key,omitempty"`
@@ -304,15 +304,67 @@ func OpenAIEndpointURL(baseURL string, endpoint string) string {
if !strings.HasPrefix(normalizedEndpoint, "/") { if !strings.HasPrefix(normalizedEndpoint, "/") {
normalizedEndpoint = "/" + 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) != "" { if OpenAIEndpointFromBaseURL(base) != "" {
return base return base
} }
if strings.HasSuffix(base, "/v1") && strings.HasPrefix(normalizedEndpoint, "/v1/") { // 规则2baseURL 以 /vN 结尾时,剥离 endpoint 的版本前缀(/v1/、/v2/ 等)
return base + strings.TrimPrefix(normalizedEndpoint, "/v1") // 这样 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 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 { func ResolveOpenAIEndpoint(baseURL string, endpoint string) string {
if endpointFromURL := OpenAIEndpointFromBaseURL(baseURL); endpointFromURL != "" { if endpointFromURL := OpenAIEndpointFromBaseURL(baseURL); endpointFromURL != "" {
return endpointFromURL return endpointFromURL
@@ -377,11 +429,11 @@ func (adapter *OpenAIAdapter) Stream(ctx context.Context, req StreamRequest, sin
req.OpenAIEndpoint = endpoint req.OpenAIEndpoint = endpoint
if req.RequestKnobs != nil { if req.RequestKnobs != nil {
req.RequestKnobs["openai_endpoint"] = endpoint req.RequestKnobs["openai_endpoint"] = endpoint
if endpoint == modelchannel.OpenAIEndpointResponses { if modelchannel.OpenAIEndpointShape(endpoint) == "responses" {
req.RequestKnobs["max_output_tokens"] = req.MaxTokens 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.streamResponses(ctx, req, baseURL, apiKey, modelID, sink)
} }
return adapter.streamChatCompletions(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, Model: modelID,
Messages: normalizedMessages, Messages: normalizedMessages,
Stream: true, Stream: true,
MaxTokens: req.MaxTokens,
StreamOptions: map[string]any{"include_usage": true}, StreamOptions: map[string]any{"include_usage": true},
} }
if shouldSendOpenAIMaxOutputTokens(modelID) {
requestBody.MaxTokens = req.MaxTokens
}
if key := openAIPromptCacheKey(req, modelID); key != "" { if key := openAIPromptCacheKey(req, modelID); key != "" {
requestBody.PromptCacheKey = 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)) recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
return 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 { if err := ApplyOpenAIExtraParams(bodyMap, req.OpenAIExtraParamsEnabled, req.OpenAIExtraParamsJSON); err != nil {
finishedAt = time.Now().UTC() finishedAt = time.Now().UTC()
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err)) recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
return err return err
} }
body = bodyMap body = bodyMap
requestURL := OpenAIEndpointURL(baseURL, modelchannel.OpenAIEndpointChatCompletions) requestURL := OpenAIEndpointURL(baseURL, req.OpenAIEndpoint)
recordLLMRequestArtifact(req, "openai", modelID, "POST", requestURL, body) recordLLMRequestArtifact(req, "openai", modelID, "POST", requestURL, body)
payload, err := json.Marshal(body) payload, err := json.Marshal(body)
@@ -862,12 +916,14 @@ func (adapter *OpenAIAdapter) streamResponses(ctx context.Context, req StreamReq
return err return err
} }
requestBody := openAIResponsesRequestBody{ requestBody := openAIResponsesRequestBody{
Model: modelID, Model: modelID,
Instructions: instructions, Instructions: instructions,
Input: input, Input: input,
Stream: true, Stream: true,
MaxOutputTokens: req.MaxTokens, Store: false,
Store: false, }
if shouldSendOpenAIMaxOutputTokens(modelID) {
requestBody.MaxOutputTokens = req.MaxTokens
} }
if key := openAIPromptCacheKey(req, modelID); key != "" { if key := openAIPromptCacheKey(req, modelID); key != "" {
requestBody.PromptCacheKey = 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)) recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
return 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 { if err := ApplyOpenAIExtraParams(bodyMap, req.OpenAIExtraParamsEnabled, req.OpenAIExtraParamsJSON); err != nil {
finishedAt = time.Now().UTC() finishedAt = time.Now().UTC()
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err)) 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 body = bodyMap
requestURL := OpenAIEndpointURL(baseURL, modelchannel.OpenAIEndpointResponses) requestURL := OpenAIEndpointURL(baseURL, req.OpenAIEndpoint)
recordLLMRequestArtifact(req, "openai", modelID, "POST", requestURL, body) recordLLMRequestArtifact(req, "openai", modelID, "POST", requestURL, body)
payload, err := json.Marshal(body) payload, err := json.Marshal(body)
@@ -1792,6 +1848,10 @@ func normalizeOpenAIProviderMessages(messages []Message, thinkingEnabled bool) (
return items, nil return items, nil
} }
func shouldSendOpenAIMaxOutputTokens(modelID string) bool {
return !strings.Contains(strings.ToLower(strings.TrimSpace(modelID)), "gpt")
}
func shouldIncludeOpenAIReasoningContent(message Message, thinkingEnabled bool) bool { func shouldIncludeOpenAIReasoningContent(message Message, thinkingEnabled bool) bool {
if strings.TrimSpace(message.ReasoningContent) != "" { if strings.TrimSpace(message.ReasoningContent) != "" {
return true return true
@@ -1819,7 +1879,7 @@ func applyOpenAIThinkingDisable(body map[string]any, req StreamRequest, baseURL
delete(body, "reasoning_effort") delete(body, "reasoning_effort")
setRequestKnob(req, "thinking_disabled_provider_param", "enable_thinking") setRequestKnob(req, "thinking_disabled_provider_param", "enable_thinking")
case "reasoning_none": case "reasoning_none":
if endpoint == modelchannel.OpenAIEndpointResponses { if modelchannel.OpenAIEndpointShape(endpoint) == "responses" {
body["reasoning"] = map[string]any{"effort": "none"} body["reasoning"] = map[string]any{"effort": "none"}
} else { } else {
body["reasoning_effort"] = "none" body["reasoning_effort"] = "none"
+3 -3
View File
@@ -152,9 +152,9 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon
case next.ModelID == "": case next.ModelID == "":
return nil, errors.New("模型适配器 modelID 不能为空") return nil, errors.New("模型适配器 modelID 不能为空")
case next.Type == "openai" && next.ReasoningEffort == "": 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 == "": 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: case next.Type == "openai" && next.OpenAIExtraParamsEnabled:
if err := validateJSONMap(next.OpenAIExtraParamsJSON, "openAIExtraParamsJSON"); err != nil { if err := validateJSONMap(next.OpenAIExtraParamsJSON, "openAIExtraParamsJSON"); err != nil {
return nil, err return nil, err
@@ -216,7 +216,7 @@ func normalizeReasoningEffort(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) { switch strings.ToLower(strings.TrimSpace(value)) {
case "", "medium": case "", "medium":
return "medium" return "medium"
case "low", "high", "xhigh": case "low", "high", "xhigh", "max":
return strings.ToLower(strings.TrimSpace(value)) return strings.ToLower(strings.TrimSpace(value))
default: default:
return "" return ""
+6 -2
View File
@@ -46,6 +46,8 @@ const (
bootstrapStatsigDisableTerminalOutputUIStreaming = "disable_terminal_output_ui_streaming" bootstrapStatsigDisableTerminalOutputUIStreaming = "disable_terminal_output_ui_streaming"
bootstrapStatsigBrowserCanvas = "browser_canvas" bootstrapStatsigBrowserCanvas = "browser_canvas"
bootstrapStatsigEnableMultitaskMode = "enable_multitask_mode" bootstrapStatsigEnableMultitaskMode = "enable_multitask_mode"
bootstrapStatsigDecomposeAlwaysLocalExtHostGate = "decompose_always_local_ext_host"
bootstrapStatsigCursorExtensionsIsolationV2Gate = "cursor_extensions_isolation_v2"
bootstrapStatsigCursorAgentWorkerExtension = "enable_cursor_agent_worker_extension" bootstrapStatsigCursorAgentWorkerExtension = "enable_cursor_agent_worker_extension"
bootstrapStatsigExperimentName = "free_user_model_picker" bootstrapStatsigExperimentName = "free_user_model_picker"
bootstrapStatsigVariantParam = "variant" bootstrapStatsigVariantParam = "variant"
@@ -139,6 +141,8 @@ var bootstrapStatsigTemplate = statsigBootstrapTemplate{
bootstrapStatsigDisableTerminalOutputUIStreaming: buildEnabledStatsigGate(bootstrapStatsigDisableTerminalOutputUIStreaming), bootstrapStatsigDisableTerminalOutputUIStreaming: buildEnabledStatsigGate(bootstrapStatsigDisableTerminalOutputUIStreaming),
bootstrapStatsigBrowserCanvas: buildEnabledStatsigGate(bootstrapStatsigBrowserCanvas), bootstrapStatsigBrowserCanvas: buildEnabledStatsigGate(bootstrapStatsigBrowserCanvas),
bootstrapStatsigEnableMultitaskMode: buildEnabledStatsigGate(bootstrapStatsigEnableMultitaskMode), bootstrapStatsigEnableMultitaskMode: buildEnabledStatsigGate(bootstrapStatsigEnableMultitaskMode),
bootstrapStatsigDecomposeAlwaysLocalExtHostGate: buildDisabledStatsigGate(bootstrapStatsigDecomposeAlwaysLocalExtHostGate),
bootstrapStatsigCursorExtensionsIsolationV2Gate: buildDisabledStatsigGate(bootstrapStatsigCursorExtensionsIsolationV2Gate),
bootstrapStatsigCursorAgentWorkerExtension: buildDisabledStatsigGate(bootstrapStatsigCursorAgentWorkerExtension), bootstrapStatsigCursorAgentWorkerExtension: buildDisabledStatsigGate(bootstrapStatsigCursorAgentWorkerExtension),
}, },
DynamicConfigs: map[string]statsigDynamicConfigTemplate{ DynamicConfigs: map[string]statsigDynamicConfigTemplate{
@@ -742,7 +746,7 @@ func buildThinkingEffortVariantDisplayName(modelDisplayName string, effortValue
func thinkingEffortValuesForAdapter(adapterType string) []string { func thinkingEffortValuesForAdapter(adapterType string) []string {
values := []string{"disabled", "low", "medium", "high", "xhigh"} 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") values = append(values, "max")
} }
return values return values
@@ -769,7 +773,7 @@ func defaultThinkingEffortForAdapter(adapter legacyruntime.ModelAdapterConfig) s
if strings.EqualFold(strings.TrimSpace(adapter.Type), "anthropic") { if strings.EqualFold(strings.TrimSpace(adapter.Type), "anthropic") {
return normalizeAvailableModelThinkingEffort(adapter.AnthropicThinkingEffort, true, "xhigh") 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 { 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
@@ -668,7 +668,7 @@ func normalizeModelAdapterTestType(value string) string {
func normalizeModelAdapterTestReasoning(value string) string { func normalizeModelAdapterTestReasoning(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) { switch strings.ToLower(strings.TrimSpace(value)) {
case "low", "medium", "high", "xhigh": case "low", "medium", "high", "xhigh", "max":
return strings.ToLower(strings.TrimSpace(value)) return strings.ToLower(strings.TrimSpace(value))
default: default:
return "medium" return "medium"
+77 -1
View File
@@ -3,6 +3,7 @@ package cursor
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -23,8 +24,14 @@ const (
cursorStateDBRelativePath = "Cursor/User/globalStorage/state.vscdb" cursorStateDBRelativePath = "Cursor/User/globalStorage/state.vscdb"
cursorStateDarwinRelativePath = "Library/Application Support/Cursor/User/globalStorage/state.vscdb" cursorStateDarwinRelativePath = "Library/Application Support/Cursor/User/globalStorage/state.vscdb"
cursorStateLinuxRelativePath = ".config/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 // InjectCursorUserInfo synchronizes the Cursor user-level auth cache used by the
// Settings page. It does not modify the installed Cursor app bundle. // Settings page. It does not modify the installed Cursor app bundle.
func InjectCursorUserInfo(email, token string) error { func InjectCursorUserInfo(email, token string) error {
@@ -42,11 +49,12 @@ func InjectCursorUserInfo(email, token string) error {
} }
logger.Infof( 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, stateDBPath,
values["cursorAuth/cachedEmail"], values["cursorAuth/cachedEmail"],
values["cursorAuth/stripeMembershipType"], values["cursorAuth/stripeMembershipType"],
values["cursorAuth/stripeSubscriptionStatus"], values["cursorAuth/stripeSubscriptionStatus"],
strings.Join(cursorStateDisabledStatsigGates, ","),
) )
return nil 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 { if err := tx.Commit(); err != nil {
return err return err
} }
@@ -118,6 +130,70 @@ func syncCursorAuthStateDB(path string, values map[string]string) error {
return nil 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) { func resolveCursorStateDBPath() (string, error) {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
+22 -4
View File
@@ -13,6 +13,7 @@ const ChannelIDHexLength = 16
const ( const (
OpenAIEndpointResponses = "/v1/responses" OpenAIEndpointResponses = "/v1/responses"
OpenAIEndpointChatCompletions = "/v1/chat/completions" OpenAIEndpointChatCompletions = "/v1/chat/completions"
OpenAIEndpointCustom = "/custom"
) )
func NormalizeBaseURL(raw string) (string, error) { func NormalizeBaseURL(raw string) (string, error) {
@@ -39,20 +40,37 @@ func NormalizeBaseURL(raw string) (string, error) {
return normalized, nil return normalized, nil
} }
// NormalizeOpenAIEndpoint 归一化 OpenAI endpoint 路径。
// 支持三个预设值:/v1/responses、/v1/chat/completions、/custom(自定义路径)。
// 选 /custom 时,用户需在接口地址栏填写完整请求 URL。
func NormalizeOpenAIEndpoint(providerType string, endpoint string) string { func NormalizeOpenAIEndpoint(providerType string, endpoint string) string {
if strings.TrimSpace(strings.ToLower(providerType)) != "openai" { if strings.TrimSpace(strings.ToLower(providerType)) != "openai" {
return "" return ""
} }
switch strings.ToLower(strings.TrimSpace(endpoint)) { normalized := strings.TrimSpace(endpoint)
case "", OpenAIEndpointResponses: switch normalized {
case "":
return OpenAIEndpointResponses return OpenAIEndpointResponses
case OpenAIEndpointChatCompletions: case OpenAIEndpointResponses, OpenAIEndpointChatCompletions, OpenAIEndpointCustom:
return OpenAIEndpointChatCompletions return normalized
default: default:
return "" 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 { func BuildLegacyChannelID(baseURL string, modelID string, apiKey string, name string) string {
return buildChannelID([]string{ return buildChannelID([]string{
strings.TrimSpace(baseURL), strings.TrimSpace(baseURL),
+2 -2
View File
@@ -138,7 +138,7 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon
case next.ModelID == "": case next.ModelID == "":
return nil, errors.New("模型适配器 modelID 不能为空") return nil, errors.New("模型适配器 modelID 不能为空")
case next.Type == "openai" && next.ReasoningEffort == "": 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 == "": 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")
case next.Type == "openai" && next.OpenAIExtraParamsEnabled: case next.Type == "openai" && next.OpenAIExtraParamsEnabled:
@@ -202,7 +202,7 @@ func normalizeReasoningEffort(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) { switch strings.ToLower(strings.TrimSpace(value)) {
case "", "medium": case "", "medium":
return "medium" return "medium"
case "low", "high", "xhigh": case "low", "high", "xhigh", "max":
return strings.ToLower(strings.TrimSpace(value)) return strings.ToLower(strings.TrimSpace(value))
default: default:
return "" return ""
+3 -1
View File
@@ -1,8 +1,10 @@
你是一个极度务实且高效的软件工程师。你认真对待工程质量,并以直接、客观的陈述方式体现协作。你沟通高效,在不加入无关细节的前提下,清楚告知用户你正在进行的操作。 你是一个极度务实且高效的软件工程师。你认真对待工程质量,并以直接、客观的陈述方式体现协作。你沟通高效,在不加入无关细节的前提下,清楚告知用户你正在进行的操作。
!IMPORTANT 如非用户明确要求,禁止使用Subagent
你具有极强的架构经验和模块化经验,当用户提出泛泛的需求时,**你会不断评估架构、模块、数据流向、状态机的合理性**,你会自信的引导用户重构代码。 你具有极强的架构经验和模块化经验,当用户提出泛泛的需求时,**你会不断评估架构、模块、数据流向、状态机的合理性**,你会自信的引导用户重构代码。
不会从代码、函数等层面交流,只输出你认为用户能看懂的数据结构、演变过程、模块关系、作用域、伪代码等,**并附带注释**。 你只输出你认为普通用户能看懂的回复,再不炫技的情况下回复内容可能是数据结构、演变过程、模块关系、作用域、伪代码、mermaid等,**并附带注释**。
除非你已经确定项目是面向对象的,其他情况下你都偏好 Functional Programming. DSL化编码风格是是你喜欢的,也是用户喜欢的,也是可读的、优秀的。 除非你已经确定项目是面向对象的,其他情况下你都偏好 Functional Programming. DSL化编码风格是是你喜欢的,也是用户喜欢的,也是可读的、优秀的。
+4 -8
View File
@@ -1,10 +1,6 @@
QQ10群:1049354347 QQ10群:1049354347
---- ----
- 修复 Taking longer than expected. - 支持 GPT 5.6
- 修复gpt5.5思考内容(加密内容展示) - 修复一些bug
- 新增多任务能力,支持在会话中并行处理多个任务
- 优化 Subagent 模型选择逻辑,提升分发稳定性
- 修复 macOS 下证书信任失败导致无法正常连接的问题
- 新增 ServerTime 接口及 Mock 实现,补齐本地模式兼容性
- 优化 OpenAI / Anthropic 适配器的请求结构,提升模型请求稳定性
- 优化提示词表达,精简系统前缀,提升响应质量