diff --git a/apps/desktop/src/renderer/src/components/AddCustomProviderModal.test.tsx b/apps/desktop/src/renderer/src/components/AddCustomProviderModal.test.tsx
new file mode 100644
index 00000000..7698ad33
--- /dev/null
+++ b/apps/desktop/src/renderer/src/components/AddCustomProviderModal.test.tsx
@@ -0,0 +1,39 @@
+import { renderToStaticMarkup } from 'react-dom/server';
+import { describe, expect, it, vi } from 'vitest';
+import { AddCustomProviderModal } from './AddCustomProviderModal';
+
+vi.mock('@open-codesign/i18n', () => ({
+ useT: () => (key: string) => key,
+}));
+
+describe('AddCustomProviderModal', () => {
+ it('shows the compatibility warning for editable custom endpoints', () => {
+ const html = renderToStaticMarkup(
+ undefined} onClose={() => undefined} />,
+ );
+
+ expect(html).toContain('settings.providers.custom.compatibilityHintTitle');
+ expect(html).toContain('settings.providers.custom.compatibilityHintBody');
+ });
+
+ it('hides the compatibility warning when editing a locked builtin endpoint', () => {
+ const html = renderToStaticMarkup(
+ undefined}
+ onClose={() => undefined}
+ editTarget={{
+ id: 'anthropic',
+ name: 'Anthropic',
+ baseUrl: 'https://api.anthropic.com',
+ wire: 'anthropic',
+ defaultModel: 'claude-sonnet-4-5',
+ builtin: true,
+ lockEndpoint: true,
+ }}
+ />,
+ );
+
+ expect(html).not.toContain('settings.providers.custom.compatibilityHintTitle');
+ expect(html).not.toContain('settings.providers.custom.compatibilityHintBody');
+ });
+});
diff --git a/apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx b/apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx
index 32eb7abe..f706e907 100644
--- a/apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx
+++ b/apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx
@@ -326,6 +326,17 @@ export function AddCustomProviderModal({
placeholder="https://api.example.com/v1"
disabled={lockEndpoint}
/>
+ {!lockEndpoint && (
+
+
+
+
{t('settings.providers.custom.compatibilityHintTitle')}
+
+
+ {t('settings.providers.custom.compatibilityHintBody')}
+
+
+ )}
diff --git a/apps/desktop/src/renderer/src/components/ConnectionDiagnosticPanel.test.ts b/apps/desktop/src/renderer/src/components/ConnectionDiagnosticPanel.test.ts
index e18b7153..b9a15681 100644
--- a/apps/desktop/src/renderer/src/components/ConnectionDiagnosticPanel.test.ts
+++ b/apps/desktop/src/renderer/src/components/ConnectionDiagnosticPanel.test.ts
@@ -8,7 +8,7 @@ vi.mock('../store', () => ({
useCodesignStore: () => vi.fn(),
}));
-import { isAbsoluteHttpUrl } from './ConnectionDiagnosticPanel';
+import { isAbsoluteHttpUrl, shouldShowGatewayAllowlistHint } from './ConnectionDiagnosticPanel';
describe('isAbsoluteHttpUrl', () => {
it('rejects an empty string so /v1 quick-fix cannot produce a bare "/v1"', () => {
@@ -28,3 +28,33 @@ describe('isAbsoluteHttpUrl', () => {
expect(isAbsoluteHttpUrl(' https://api.example.com ')).toBe(true);
});
});
+
+describe('shouldShowGatewayAllowlistHint', () => {
+ it('shows the hint for 400-class compatibility failures on third-party gateways', () => {
+ expect(shouldShowGatewayAllowlistHint('400', 'https://relay.example.com/v1', undefined)).toBe(
+ true,
+ );
+ expect(
+ shouldShowGatewayAllowlistHint(
+ '403',
+ 'https://relay.example.com/v1',
+ 'https://relay.example.com/v1/chat/completions',
+ ),
+ ).toBe(true);
+ expect(shouldShowGatewayAllowlistHint('PARSE', 'https://relay.example.com/v1')).toBe(true);
+ });
+
+ it('suppresses the hint for official providers and localhost proxies', () => {
+ expect(shouldShowGatewayAllowlistHint('400', 'https://api.openai.com/v1')).toBe(false);
+ expect(shouldShowGatewayAllowlistHint('403', 'https://api.anthropic.com')).toBe(false);
+ expect(shouldShowGatewayAllowlistHint('400', 'http://127.0.0.1:8317')).toBe(false);
+ });
+
+ it('suppresses the hint for unrelated error classes', () => {
+ expect(shouldShowGatewayAllowlistHint('404', 'https://relay.example.com/v1')).toBe(false);
+ expect(shouldShowGatewayAllowlistHint('429', 'https://relay.example.com/v1')).toBe(false);
+ expect(shouldShowGatewayAllowlistHint('ECONNREFUSED', 'https://relay.example.com/v1')).toBe(
+ false,
+ );
+ });
+});
diff --git a/apps/desktop/src/renderer/src/components/ConnectionDiagnosticPanel.tsx b/apps/desktop/src/renderer/src/components/ConnectionDiagnosticPanel.tsx
index 57b5b077..e834d254 100644
--- a/apps/desktop/src/renderer/src/components/ConnectionDiagnosticPanel.tsx
+++ b/apps/desktop/src/renderer/src/components/ConnectionDiagnosticPanel.tsx
@@ -11,6 +11,49 @@ export function isAbsoluteHttpUrl(value: string): boolean {
return /^https?:\/\/\S+/i.test(value.trim());
}
+const OFFICIAL_PROVIDER_HOST_SUFFIXES = [
+ 'openai.com',
+ 'anthropic.com',
+ 'openrouter.ai',
+ 'deepseek.com',
+ 'googleapis.com',
+ 'google.com',
+ 'x.ai',
+ 'mistral.ai',
+ 'groq.com',
+ 'cerebras.ai',
+ 'amazonaws.com',
+ 'azure.com',
+] as const;
+
+function hostnameFromUrl(value: string | undefined): string | null {
+ if (!value) return null;
+ try {
+ return new URL(value).hostname.toLowerCase();
+ } catch {
+ return null;
+ }
+}
+
+function isOfficialProviderHost(hostname: string | null): boolean {
+ if (!hostname) return false;
+ return OFFICIAL_PROVIDER_HOST_SUFFIXES.some(
+ (suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`),
+ );
+}
+
+export function shouldShowGatewayAllowlistHint(
+ errorCode: ErrorCode,
+ baseUrl: string,
+ attemptedUrl?: string,
+): boolean {
+ const normalised = String(errorCode).toUpperCase();
+ if (!['400', '401', '403', 'PARSE'].includes(normalised)) return false;
+ const hostname = hostnameFromUrl(attemptedUrl) ?? hostnameFromUrl(baseUrl);
+ if (!hostname || hostname === 'localhost' || hostname === '127.0.0.1') return false;
+ return !isOfficialProviderHost(hostname);
+}
+
export interface ConnectionDiagnosticPanelProps {
/** The error code returned by connection.test or generate */
errorCode: ErrorCode;
@@ -58,6 +101,7 @@ export function ConnectionDiagnosticPanel({
? fix.baseUrlTransform(baseUrl)
: undefined;
const canApplyFix = suggestedUrl !== undefined || fix?.externalUrl !== undefined;
+ const showGatewayAllowlistHint = shouldShowGatewayAllowlistHint(errorCode, baseUrl, attemptedUrl);
function handleApplyFix() {
if (suggestedUrl !== undefined) {
@@ -137,6 +181,16 @@ export function ConnectionDiagnosticPanel({
{t('diagnostics.fix.addV1')}: {suggestedUrl}
)}
+ {showGatewayAllowlistHint && (
+
+
+ {t('diagnostics.gatewayAllowlistHintTitle')}
+
+
+ {t('diagnostics.gatewayAllowlistHintBody')}
+
+
+ )}
{/* Actions */}
diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json
index 0f4ed923..d86773e6 100644
--- a/packages/i18n/src/locales/en.json
+++ b/packages/i18n/src/locales/en.json
@@ -216,6 +216,8 @@
"apiKey": "API Key",
"apiKeyEditPlaceholder": "Leave empty to keep {{mask}}",
"defaultModel": "Default model",
+ "compatibilityHintTitle": "Compatibility note",
+ "compatibilityHintBody": "Some coding plans, relay services, or OpenAI-compatible gateways only allow specific clients such as Claude Code, openclaw, or Hermes. Even if the API looks compatible, Open CoDesign may still be blocked by an app allowlist.",
"switchToManual": "Enter manually",
"switchToDropdown": "Pick from list",
"discoveringModels": "Discovering models...",
@@ -824,6 +826,8 @@
"testAgain": "Test again",
"showLog": "Show full log",
"showLogFailed": "Failed to open logs folder",
+ "gatewayAllowlistHintTitle": "This endpoint may restrict which apps can connect",
+ "gatewayAllowlistHintBody": "Some coding plans and protocol-conversion gateways keep an app allowlist. Even when they advertise an OpenAI-compatible API, they may still reject Open CoDesign unless this app is explicitly allowed.",
"dismiss": "Dismiss",
"report": {
"title": "Report a bug",
diff --git a/packages/i18n/src/locales/pt-BR.json b/packages/i18n/src/locales/pt-BR.json
index 7eb0cae0..744236ae 100644
--- a/packages/i18n/src/locales/pt-BR.json
+++ b/packages/i18n/src/locales/pt-BR.json
@@ -213,6 +213,8 @@
"apiKey": "Chave de API",
"apiKeyEditPlaceholder": "Deixe vazio para manter {{mask}}",
"defaultModel": "Modelo padrão",
+ "compatibilityHintTitle": "Aviso de compatibilidade",
+ "compatibilityHintBody": "Alguns coding plans, relays e gateways compatíveis com OpenAI só permitem clientes específicos, como Claude Code, openclaw ou Hermes. Mesmo que a API pareça compatível, o Open CoDesign ainda pode ser bloqueado por uma lista de apps permitidos.",
"test": "Testar conexão",
"testOk": "OK — {{count}} modelos disponíveis",
"save": "Salvar e continuar",
@@ -806,6 +808,8 @@
"testAgain": "Testar novamente",
"showLog": "Mostrar log completo",
"showLogFailed": "Falha ao abrir a pasta de logs",
+ "gatewayAllowlistHintTitle": "Este endpoint pode restringir quais apps podem se conectar",
+ "gatewayAllowlistHintBody": "Alguns coding plans e gateways de conversão de protocolo mantêm uma lista de apps permitidos. Mesmo anunciando uma API compatível com OpenAI, eles ainda podem recusar o Open CoDesign se este app não estiver explicitamente liberado.",
"dismiss": "Dispensar",
"report": {
"title": "Reportar um bug",
diff --git a/packages/i18n/src/locales/zh-CN.json b/packages/i18n/src/locales/zh-CN.json
index a0bdf354..2ab06b4b 100644
--- a/packages/i18n/src/locales/zh-CN.json
+++ b/packages/i18n/src/locales/zh-CN.json
@@ -216,6 +216,8 @@
"apiKey": "API Key",
"apiKeyEditPlaceholder": "留空则保留 {{mask}}",
"defaultModel": "默认模型",
+ "compatibilityHintTitle": "\u517c\u5bb9\u6027\u63d0\u793a",
+ "compatibilityHintBody": "\u90e8\u5206 coding plan\uff0c\u534f\u8bae\u8f6c\u6362\u670d\u52a1\u6216 OpenAI \u517c\u5bb9\u7f51\u5173\u53ea\u5141\u8bb8\u7279\u5b9a\u5ba2\u6237\u7aef\uff08\u5982 Claude Code\uff0copenclaw\uff0cHermes\uff09\u8bbf\u95ee\u3002\u5373\u4f7f API \u770b\u8d77\u6765\u517c\u5bb9\uff0cOpen CoDesign \u4e5f\u53ef\u80fd\u56e0\u4e3a\u5e94\u7528\u767d\u540d\u5355\u800c\u65e0\u6cd5\u4f7f\u7528\u3002",
"switchToManual": "手动输入",
"switchToDropdown": "从列表选择",
"discoveringModels": "正在发现模型…",
@@ -820,6 +822,8 @@
"testAgain": "再次测试",
"showLog": "查看完整日志",
"showLogFailed": "无法打开日志文件夹",
+ "gatewayAllowlistHintTitle": "\u8fd9\u4e2a\u7aef\u70b9\u53ef\u80fd\u9650\u5236\u53ef\u63a5\u5165\u7684\u5e94\u7528",
+ "gatewayAllowlistHintBody": "\u90e8\u5206 coding plan \u548c\u534f\u8bae\u8f6c\u6362\u7f51\u5173\u4f1a\u914d\u7f6e\u5e94\u7528\u767d\u540d\u5355\u3002\u5b83\u4eec\u5373\u4f7f\u58f0\u79f0\u63d0\u4f9b OpenAI \u517c\u5bb9 API\uff0c\u4e5f\u53ef\u80fd\u5728\u6ca1\u6709\u663e\u5f0f\u5141\u8bb8 Open CoDesign \u7684\u60c5\u51b5\u4e0b\u62d2\u7edd\u8bf7\u6c42\u3002",
"dismiss": "关闭",
"report": {
"title": "上报问题",