-
Notifications
You must be signed in to change notification settings - Fork 877
Add a connection test for API modes #1102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9f96e82
fc92f95
ae9e43c
232315f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -124,10 +124,18 @@ | |
| "Override provider temperature": "Override provider temperature", | ||
| "The temperature parameter is not sent. The provider or model default is used.": "The temperature parameter is not sent. The provider or model default is used.", | ||
| "The current model does not accept a custom temperature. The parameter will not be sent.": "The current model does not accept a custom temperature. The parameter will not be sent.", | ||
| "Extra Request Body (JSON)": "Extra Request Body (JSON)", | ||
| "Merged into the API request body. Must be a JSON object, other values are ignored.": "Merged into the API request body. Must be a JSON object, other values are ignored.", | ||
| "Invalid JSON object, this value is ignored.": "Invalid JSON object, this value is ignored.", | ||
| "API Url": "API Url", | ||
| "Provider": "Provider", | ||
| "Others": "Others", | ||
| "API Modes": "API Modes", | ||
| "Test": "Test", | ||
| "Testing...": "Testing...", | ||
| "Reachable": "Reachable", | ||
| "Unreachable": "Unreachable", | ||
|
Comment on lines
+134
to
+137
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Most locales omit new settings text The new request-body guidance and connection-test labels have keys in English and the two Chinese locale files, but not in the other supported locale files. When those users open the settings, the configured English fallback supplies the new text instead of a locale entry or marked placeholder. Agent Prompt
|
||
| "Not testable": "Not testable", | ||
| "Disable web mode history for better privacy protection, but it will result in unavailable conversations after a period of time": "Disable web mode history for better privacy protection, but it will result in unavailable conversations after a period of time", | ||
| "Display selection tools next to input box to avoid blocking": "Display selection tools next to input box to avoid blocking", | ||
| "Close All Chats In This Page": "Close All Chats In This Page", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -118,10 +118,18 @@ | |||||
| "Override provider temperature": "覆盖提供商的温度参数", | ||||||
| "The temperature parameter is not sent. The provider or model default is used.": "不会发送温度参数,将使用提供商或模型的默认值。", | ||||||
| "The current model does not accept a custom temperature. The parameter will not be sent.": "当前模型不接受自定义温度参数,因此不会发送该参数。", | ||||||
| "Extra Request Body (JSON)": "额外请求参数 (JSON)", | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: This label calls a JSON request body “extra request parameters,” conflicting with the adjacent explanation and the separate API Params tab. Translate it as “额外请求体 (JSON)” to identify the field correctly. Prompt for AI agents
Suggested change
|
||||||
| "Merged into the API request body. Must be a JSON object, other values are ignored.": "会合并进 API 请求体,必须是 JSON 对象,其他类型的值会被忽略。", | ||||||
| "Invalid JSON object, this value is ignored.": "不是合法的 JSON 对象,该值会被忽略。", | ||||||
| "API Url": "API地址", | ||||||
| "Provider": "提供商", | ||||||
| "Others": "其他", | ||||||
| "API Modes": "API模式", | ||||||
| "Test": "测试", | ||||||
| "Testing...": "测试中…", | ||||||
| "Reachable": "可连通", | ||||||
| "Unreachable": "无法连通", | ||||||
| "Not testable": "无法测试", | ||||||
| "Disable web mode history for better privacy protection, but it will result in unavailable conversations after a period of time": "禁用网页版模式历史记录以获得更好的隐私保护, 但会导致对话在一段时间后不可用", | ||||||
| "Display selection tools next to input box to avoid blocking": "将选择浮动工具显示在输入框旁边以避免遮挡", | ||||||
| "Close All Chats In This Page": "关闭本页所有聊天", | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import Browser from 'webextension-polyfill' | ||
|
|
||
| /** | ||
| * Messages that answer with data or reach stored credentials must come from extension | ||
| * code. A sender that reports an id is trusted only when it is this extension; extension | ||
| * pages in some browsers report no id, so their own URL is the fallback signal. | ||
| * @param {{id?: string, url?: string, documentUrl?: string, origin?: string}} sender | ||
| * @returns {boolean} | ||
| */ | ||
| export function isTrustedExtensionSender(sender) { | ||
| if (sender?.id === Browser.runtime.id) return true | ||
| if (sender?.id) return false | ||
| const senderUrl = sender?.url || sender?.documentUrl || sender?.origin | ||
| if (typeof senderUrl !== 'string') return false | ||
| return senderUrl.startsWith(Browser.runtime.getURL('/')) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ import { parseFloatWithClamp, parseIntWithClamp } from '../../utils/index.mjs' | |
| import { getModelValue } from '../../utils/model-name-convert.mjs' | ||
| import { isUsingAzureOpenAiApiModel } from '../../config/index.mjs' | ||
| import { canApplyTemperatureOverride } from '../../services/apis/temperature-params.mjs' | ||
| import { parseExtraBody } from '../../services/apis/extra-body-params.mjs' | ||
| import PropTypes from 'prop-types' | ||
| import { Tab, TabList, TabPanel, Tabs } from 'react-tabs' | ||
| import Browser from 'webextension-polyfill' | ||
|
|
@@ -22,6 +23,8 @@ function ApiParams({ config, updateConfig }) { | |
| ? config.customModelName | ||
| : getModelValue(config) | ||
| const temperatureOverrideAvailable = canApplyTemperatureOverride(selectedModel) | ||
| const extraBodyValue = typeof config.extraBody === 'string' ? config.extraBody : '' | ||
| const extraBodyInvalid = extraBodyValue.trim() !== '' && !parseExtraBody(extraBodyValue) | ||
|
|
||
| return ( | ||
| <> | ||
|
|
@@ -89,6 +92,21 @@ function ApiParams({ config, updateConfig }) { | |
| /> | ||
| </label> | ||
| )} | ||
| <label> | ||
| {t('Extra Request Body (JSON)')} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: This field is rendered unconditionally, but the extra body is only consumed by API-key providers (OpenAI-compatible, Azure, Claude). In Web API modes (ChatGPT web, Claude web, Bing, Bard, Moonshot) the value is silently ignored while the helper text claims it is "Merged into the API request body", so users get no indication the setting has no effect. Show the field only for API modes, or state its scope in the helper text. Prompt for AI agents |
||
| <textarea | ||
| value={extraBodyValue} | ||
| placeholder={'{\n "reasoning_effort": "high"\n}'} | ||
| onChange={(e) => { | ||
| updateConfig({ extraBody: e.target.value }) | ||
| }} | ||
| /> | ||
| </label> | ||
| <small> | ||
| {extraBodyInvalid | ||
| ? t('Invalid JSON object, this value is ignored.') | ||
| : t('Merged into the API request body. Must be a JSON object, other values are ignored.')} | ||
| </small> | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,12 @@ import { | |
| getCustomOpenAIProviders, | ||
| OPENAI_COMPATIBLE_GROUP_TO_PROVIDER_ID, | ||
| } from '../../services/apis/provider-registry.mjs' | ||
| import { canTestConnectionSession } from '../../services/apis/connection-test-groups.mjs' | ||
| import { | ||
| getConnectionTestButtonStyle, | ||
| getConnectionTestLabel, | ||
| getConnectionTestTitle, | ||
| } from './connection-test-status.mjs' | ||
| import { | ||
| applySelectedProviderToApiMode, | ||
| applyDeletedProviderSecrets, | ||
|
|
@@ -72,6 +78,20 @@ const defaultProviderDraftValidation = { | |
| apiUrl: false, | ||
| } | ||
|
|
||
| // Results are keyed by what the mode is, not by where it happens to sit in the list, so | ||
| // reordering or deleting a row cannot attach a result to a different provider. | ||
| function getConnectionTestKey(apiMode) { | ||
| return [ | ||
| apiMode?.groupName, | ||
| apiMode?.itemName, | ||
| apiMode?.customName, | ||
| apiMode?.providerId, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This key does not invalidate a result when the tested credential or endpoint changes, so the row can show Prompt for AI agents |
||
| apiMode?.customUrl, | ||
| ] | ||
| .map((part) => String(part ?? '').trim()) | ||
| .join('\u0000') | ||
| } | ||
|
|
||
| export function ApiModes({ config, updateConfig }) { | ||
| const { t } = useTranslation() | ||
| const [editing, setEditing] = useState(false) | ||
|
|
@@ -88,6 +108,7 @@ export function ApiModes({ config, updateConfig }) { | |
| const [providerSelector, setProviderSelector] = useState(LEGACY_CUSTOM_PROVIDER_ID) | ||
| const [isProviderEditorOpen, setIsProviderEditorOpen] = useState(false) | ||
| const [providerEditingId, setProviderEditingId] = useState('') | ||
| const [connectionTests, setConnectionTests] = useState({}) | ||
| const [providerDraft, setProviderDraft] = useState(defaultProviderDraft) | ||
| const [providerDraftValidation, setProviderDraftValidation] = useState( | ||
| defaultProviderDraftValidation, | ||
|
|
@@ -269,6 +290,25 @@ export function ApiModes({ config, updateConfig }) { | |
| setIsProviderEditorOpen(true) | ||
| } | ||
|
|
||
| const runConnectionTest = async (apiMode) => { | ||
| const key = getConnectionTestKey(apiMode) | ||
| // A probe in flight owns the row: a second click would race it for the same result. | ||
| if (connectionTests[key]?.pending) return | ||
| setConnectionTests((current) => ({ ...current, [key]: { pending: true } })) | ||
| let result | ||
| try { | ||
| result = await Browser.runtime.sendMessage({ | ||
| type: 'TEST_API_CONNECTION', | ||
| data: { session: { apiMode } }, | ||
| }) | ||
| } catch (error) { | ||
| result = { ok: false, error: error?.message ?? String(error) } | ||
| } | ||
| setConnectionTests((current) => ({ ...current, [key]: { ...result, pending: false } })) | ||
| } | ||
|
|
||
| const getConnectionTest = (apiMode) => connectionTests[getConnectionTestKey(apiMode)] | ||
|
|
||
| const onSaveProviderEditing = (event) => { | ||
| event.preventDefault() | ||
| const providerName = providerDraft.name.trim() | ||
|
|
@@ -629,7 +669,26 @@ export function ApiModes({ config, updateConfig }) { | |
| /> | ||
| {getApiModeDisplayLabel(apiMode, t, effectiveProviders)} | ||
| <div style={{ flexGrow: 1 }} /> | ||
| <div style={{ display: 'flex', gap: '12px' }}> | ||
| <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}> | ||
| {canTestConnectionSession({ apiMode }) && ( | ||
| <button | ||
| type="button" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Two new buttons use double-quoted props The new Test buttons use type="button" rather than single-quoted JSX attribute values. Both the API mode row and the custom model field introduce this convention mismatch. Agent Prompt
|
||
| title={getConnectionTestTitle(getConnectionTest(apiMode), t)} | ||
| disabled={Boolean(getConnectionTest(apiMode)?.pending)} | ||
| style={{ | ||
| cursor: 'pointer', | ||
| width: 'auto', | ||
| marginBottom: 0, | ||
| ...getConnectionTestButtonStyle(getConnectionTest(apiMode)), | ||
| }} | ||
| onClick={(e) => { | ||
| e.preventDefault() | ||
| runConnectionTest(apiMode) | ||
| }} | ||
| > | ||
| {getConnectionTestLabel(getConnectionTest(apiMode), t)} | ||
| </button> | ||
| )} | ||
| <div | ||
| style={{ cursor: 'pointer' }} | ||
| onClick={(e) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,11 @@ import PropTypes from 'prop-types' | |
| import { config as menuConfig } from '../../content-script/menu-tools' | ||
| import { PencilIcon } from '@primer/octicons-react' | ||
| import { importDataIntoStorage } from './import-data-cleanup.mjs' | ||
| import { | ||
| getConnectionTestButtonStyle, | ||
| getConnectionTestLabel, | ||
| getConnectionTestTitle, | ||
| } from './connection-test-status.mjs' | ||
| import { resolveOpenAICompatibleRequest } from '../../services/apis/provider-registry.mjs' | ||
| import { | ||
| getApiModeDisplayLabel, | ||
|
|
@@ -100,6 +105,35 @@ export function GeneralPart({ | |
| }) { | ||
| const { t, i18n } = useTranslation() | ||
| const [apiModes, setApiModes] = useState([]) | ||
| const [connectionTest, setConnectionTest] = useState(null) | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| // A result describes the endpoint it was actually sent to, so the URL, model and key it | ||
| // ran with are part of its identity; editing any of them retires the result. | ||
| const customModelTestSignature = [ | ||
| config.customModelApiUrl, | ||
| config.customModelName, | ||
| config.customApiKey, | ||
| ] | ||
| .map((part) => String(part ?? '')) | ||
| .join('\u0000') | ||
| const customModelTest = | ||
| connectionTest?.signature === customModelTestSignature ? connectionTest : null | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Allow a new test after the custom-model signature changes. When the URL or model changes during a probe, 🤖 Prompt for AI Agents |
||
|
|
||
| const runCustomModelConnectionTest = async () => { | ||
| // Ignore repeat clicks while a probe is running, so a stale result cannot win. | ||
| if (connectionTest?.pending) return | ||
| const signature = customModelTestSignature | ||
| setConnectionTest({ pending: true, signature }) | ||
| let result | ||
| try { | ||
| result = await Browser.runtime.sendMessage({ | ||
| type: 'TEST_API_CONNECTION', | ||
| data: { session: { modelName: 'customModel' } }, | ||
| }) | ||
|
Comment on lines
+128
to
+131
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 8. Immediate tests can probe the old url runCustomModelConnectionTest sends only a model selector, while the background test independently reads persisted configuration. If a user edits the custom-model URL and immediately clicks Test, the popup's queued storage write can still be pending, so the request uses the previously saved URL rather than the one in the input. Agent Prompt
|
||
| } catch (error) { | ||
| result = { ok: false, error: error?.message ?? String(error) } | ||
| } | ||
| setConnectionTest({ ...result, pending: false, signature }) | ||
| } | ||
| const [providerApiKeyDraft, setProviderApiKeyDraft] = useState('') | ||
| const [isOverrideProviderKeyActionPending, setIsOverrideProviderKeyActionPending] = | ||
| useState(false) | ||
|
|
@@ -701,15 +735,29 @@ export function GeneralPart({ | |
| </span> | ||
| )} | ||
| {isUsingSpecialCustomModel(config) && ( | ||
| <input | ||
| type="text" | ||
| value={config.customModelApiUrl} | ||
| placeholder={t('Custom Model API Url')} | ||
| onChange={(e) => { | ||
| const value = e.target.value | ||
| updateConfig({ customModelApiUrl: value }) | ||
| }} | ||
| /> | ||
| <div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}> | ||
| <input | ||
| type="text" | ||
| value={config.customModelApiUrl} | ||
| placeholder={t('Custom Model API Url')} | ||
| onChange={(e) => { | ||
| const value = e.target.value | ||
| updateConfig({ customModelApiUrl: value }) | ||
| }} | ||
| /> | ||
| <button | ||
| type="button" | ||
| title={getConnectionTestTitle(customModelTest, t)} | ||
| disabled={Boolean(customModelTest?.pending)} | ||
| style={{ | ||
| whiteSpace: 'nowrap', | ||
| ...getConnectionTestButtonStyle(customModelTest), | ||
| }} | ||
| onClick={runCustomModelConnectionTest} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Clicking Test can probe the previous API key because the key field’s blur persistence is asynchronous and this handler sends the probe immediately. Await the credential write or pass the current key/config to the probe before sending it. Prompt for AI agents |
||
| > | ||
| {getConnectionTestLabel(customModelTest, t)} | ||
| </button> | ||
| </div> | ||
| )} | ||
| {isUsingOllamaApiModel(config) && ( | ||
| <div style={{ display: 'flex', gap: '10px' }}> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: The new Test/Testing.../Reachable/Unreachable and Extra Request Body keys exist only in en, zh-hans, and zh-hant. Users of the ten other locales (de, es, fr, id, it, ja, ko, pt, ru, tr) will see English strings in the UI via the en fallback. Add the seven keys to the remaining locale files or confirm the partial-translation pattern is intentional.
Prompt for AI agents