From 139601c5bf71aece59a3e6851cd1912233af16c4 Mon Sep 17 00:00:00 2001 From: Sun-sunshine06 Date: Thu, 23 Apr 2026 13:32:24 +0800 Subject: [PATCH 1/4] fix: enable reasoning for third-party OpenAI-compatible proxies (issue #188) Previously, only enabled reasoning=true for official OpenAI API when the modelId matched a reasoning pattern. This commit extends the heuristic to also match reasoning-capable model IDs on any third-party OpenAI-compatible proxy (univibe, OpenRouter custom endpoint, etc). Fixes issue #188 where configured a proxy with OpenAI-compatible protocol and a Claude 4 reasoning model would return 0 artifacts because the reasoning flag was missing, causing the gateway to reject the request or return empty. Co-Authored-By: Claude Opus 4.7 --- packages/providers/src/index.test.ts | 18 ++++++++++++++++++ packages/providers/src/index.ts | 27 ++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/providers/src/index.test.ts b/packages/providers/src/index.test.ts index e05e30ae..ebdeaf80 100644 --- a/packages/providers/src/index.test.ts +++ b/packages/providers/src/index.test.ts @@ -555,4 +555,22 @@ describe('inferReasoning', () => { it('returns false when wire is undefined', () => { expect(inferReasoning(undefined, 'gpt-4o', 'https://api.openai.com/v1')).toBe(false); }); + + it('returns true for third-party openai-chat with reasoning model ID (issue #188)', () => { + // univibe/custom proxy with Claude 4 model + expect(inferReasoning('openai-chat', 'claude-opus-4-6', 'https://api.univibe.cc/openai')).toBe(true); + expect(inferReasoning('openai-chat', 'claude-sonnet-4-6', 'https://api.univibe.cc/openai')).toBe(true); + // OpenRouter-style paths on custom proxy + expect(inferReasoning('openai-chat', 'anthropic/claude-opus-4-6', 'https://my-proxy.example/v1')).toBe(true); + // o1 on custom proxy + expect(inferReasoning('openai-chat', 'o1-mini', 'https://my-proxy.example/v1')).toBe(true); + // qwen/qwq on custom proxy + expect(inferReasoning('openai-chat', 'qwen/qwq-32b-preview', 'https://my-proxy.example/v1')).toBe(true); + }); + + it('returns false for third-party openai-chat with non-reasoning model ID', () => { + expect(inferReasoning('openai-chat', 'qwen3.6-plus', 'https://dashscope.aliyuncs.com/compatible-mode/v1')).toBe(false); + expect(inferReasoning('openai-chat', 'deepseek-chat', 'https://api.deepseek.com/v1')).toBe(false); + expect(inferReasoning('openai-chat', 'glm-4.6v', 'https://open.bigmodel.cn/api/paas/v4')).toBe(false); + }); }); diff --git a/packages/providers/src/index.ts b/packages/providers/src/index.ts index 940b103e..c4e62949 100644 --- a/packages/providers/src/index.ts +++ b/packages/providers/src/index.ts @@ -189,6 +189,24 @@ function isReasoningModelId(modelId: string): boolean { return /^(o[134]|gpt-5)/i.test(modelId); } +/** + * Matches reasoning-capable model IDs commonly proxied through OpenAI-compatible + * gateways (OpenRouter, univibe, sub2api, etc). This pattern matches the same + * set that OPENROUTER_REASONING_MODEL_RE uses for OpenRouter, but applies to + * custom openai-chat wire endpoints as well. + */ +const REASONING_MODEL_ID_PATTERN = new RegExp( + [ + ':thinking$', + '(^|/)claude-(?:opus|sonnet)-4', + '^o1[-.]', '^o3[-.]', '^o4[-.]', '^gpt-5[-.]', + '^minimax/minimax-m\\d', + '^deepseek/deepseek-r\\d', + '^qwen/qwq', + ].join('|'), + 'i', +); + export function inferReasoning( wire: GenerateOptions['wire'], modelId: string, @@ -201,7 +219,14 @@ export function inferReasoning( case 'openai-codex-responses': return true; case 'openai-chat': - return isOpenAIOfficial(baseUrl) && isReasoningModelId(modelId); + // For official OpenAI, check both base URL and model ID pattern + if (isOpenAIOfficial(baseUrl)) { + return isReasoningModelId(modelId); + } + // For third-party OpenAI-compatible gateways, heuristically match + // common reasoning model IDs — many gateways still require the + // reasoning flag to get extended thinking output. + return REASONING_MODEL_ID_PATTERN.test(modelId); default: return false; } From 8abe2ae40d93d0feb0591b81bf9cc2dd0970dcac Mon Sep 17 00:00:00 2001 From: Sun-sunshine06 Date: Thu, 23 Apr 2026 13:57:12 +0800 Subject: [PATCH 2/4] fix: cover namespaced openai reasoning models on proxies Signed-off-by: Sun-sunshine06 --- packages/providers/src/index.test.ts | 39 +++++++++++++++++++++++----- packages/providers/src/index.ts | 2 +- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/providers/src/index.test.ts b/packages/providers/src/index.test.ts index ebdeaf80..dbf73877 100644 --- a/packages/providers/src/index.test.ts +++ b/packages/providers/src/index.test.ts @@ -558,19 +558,44 @@ describe('inferReasoning', () => { it('returns true for third-party openai-chat with reasoning model ID (issue #188)', () => { // univibe/custom proxy with Claude 4 model - expect(inferReasoning('openai-chat', 'claude-opus-4-6', 'https://api.univibe.cc/openai')).toBe(true); - expect(inferReasoning('openai-chat', 'claude-sonnet-4-6', 'https://api.univibe.cc/openai')).toBe(true); + expect(inferReasoning('openai-chat', 'claude-opus-4-6', 'https://api.univibe.cc/openai')).toBe( + true, + ); + expect( + inferReasoning('openai-chat', 'claude-sonnet-4-6', 'https://api.univibe.cc/openai'), + ).toBe(true); // OpenRouter-style paths on custom proxy - expect(inferReasoning('openai-chat', 'anthropic/claude-opus-4-6', 'https://my-proxy.example/v1')).toBe(true); + expect( + inferReasoning('openai-chat', 'anthropic/claude-opus-4-6', 'https://my-proxy.example/v1'), + ).toBe(true); + // OpenAI-style namespaced paths on custom proxy + expect(inferReasoning('openai-chat', 'openai/o3-mini', 'https://my-proxy.example/v1')).toBe( + true, + ); + expect(inferReasoning('openai-chat', 'openai/gpt-5.1', 'https://my-proxy.example/v1')).toBe( + true, + ); // o1 on custom proxy expect(inferReasoning('openai-chat', 'o1-mini', 'https://my-proxy.example/v1')).toBe(true); // qwen/qwq on custom proxy - expect(inferReasoning('openai-chat', 'qwen/qwq-32b-preview', 'https://my-proxy.example/v1')).toBe(true); + expect( + inferReasoning('openai-chat', 'qwen/qwq-32b-preview', 'https://my-proxy.example/v1'), + ).toBe(true); }); it('returns false for third-party openai-chat with non-reasoning model ID', () => { - expect(inferReasoning('openai-chat', 'qwen3.6-plus', 'https://dashscope.aliyuncs.com/compatible-mode/v1')).toBe(false); - expect(inferReasoning('openai-chat', 'deepseek-chat', 'https://api.deepseek.com/v1')).toBe(false); - expect(inferReasoning('openai-chat', 'glm-4.6v', 'https://open.bigmodel.cn/api/paas/v4')).toBe(false); + expect( + inferReasoning( + 'openai-chat', + 'qwen3.6-plus', + 'https://dashscope.aliyuncs.com/compatible-mode/v1', + ), + ).toBe(false); + expect(inferReasoning('openai-chat', 'deepseek-chat', 'https://api.deepseek.com/v1')).toBe( + false, + ); + expect(inferReasoning('openai-chat', 'glm-4.6v', 'https://open.bigmodel.cn/api/paas/v4')).toBe( + false, + ); }); }); diff --git a/packages/providers/src/index.ts b/packages/providers/src/index.ts index c4e62949..29c1140b 100644 --- a/packages/providers/src/index.ts +++ b/packages/providers/src/index.ts @@ -199,7 +199,7 @@ const REASONING_MODEL_ID_PATTERN = new RegExp( [ ':thinking$', '(^|/)claude-(?:opus|sonnet)-4', - '^o1[-.]', '^o3[-.]', '^o4[-.]', '^gpt-5[-.]', + '^(?:openai/)?(?:o1|o3|o4|gpt-5)(?:[-.].*)?$', '^minimax/minimax-m\\d', '^deepseek/deepseek-r\\d', '^qwen/qwq', From c0815120b3a34085b9d21c614f7d5242c2af5ffe Mon Sep 17 00:00:00 2001 From: Sun-sunshine06 Date: Thu, 23 Apr 2026 14:17:08 +0800 Subject: [PATCH 3/4] feat: warn about coding plan app allowlists Signed-off-by: Sun-sunshine06 --- .../AddCustomProviderModal.test.tsx | 39 ++++++++++++++ .../src/components/AddCustomProviderModal.tsx | 11 ++++ .../ConnectionDiagnosticPanel.test.ts | 32 ++++++++++- .../components/ConnectionDiagnosticPanel.tsx | 54 +++++++++++++++++++ packages/i18n/src/locales/en.json | 4 ++ packages/i18n/src/locales/pt-BR.json | 4 ++ packages/i18n/src/locales/zh-CN.json | 4 ++ 7 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/renderer/src/components/AddCustomProviderModal.test.tsx 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..4d1326c5 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..44b6d8fe 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": "上报问题", From 5678f5f5b2bfc9aadc1525799401991d016922d1 Mon Sep 17 00:00:00 2001 From: Sun-sunshine06 Date: Thu, 23 Apr 2026 14:29:26 +0800 Subject: [PATCH 4/4] fix: address bot review feedback --- .../src/components/AddCustomProviderModal.tsx | 4 +- .../components/ConnectionDiagnosticPanel.tsx | 2 +- packages/providers/src/index.test.ts | 43 ------------------- packages/providers/src/index.ts | 27 +----------- 4 files changed, 4 insertions(+), 72 deletions(-) diff --git a/apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx b/apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx index 4d1326c5..f706e907 100644 --- a/apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx +++ b/apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx @@ -327,9 +327,9 @@ export function AddCustomProviderModal({ disabled={lockEndpoint} /> {!lockEndpoint && ( -
+
- + {t('settings.providers.custom.compatibilityHintTitle')}

diff --git a/apps/desktop/src/renderer/src/components/ConnectionDiagnosticPanel.tsx b/apps/desktop/src/renderer/src/components/ConnectionDiagnosticPanel.tsx index 44b6d8fe..e834d254 100644 --- a/apps/desktop/src/renderer/src/components/ConnectionDiagnosticPanel.tsx +++ b/apps/desktop/src/renderer/src/components/ConnectionDiagnosticPanel.tsx @@ -182,7 +182,7 @@ export function ConnectionDiagnosticPanel({

)} {showGatewayAllowlistHint && ( -
+

{t('diagnostics.gatewayAllowlistHintTitle')}

diff --git a/packages/providers/src/index.test.ts b/packages/providers/src/index.test.ts index dbf73877..e05e30ae 100644 --- a/packages/providers/src/index.test.ts +++ b/packages/providers/src/index.test.ts @@ -555,47 +555,4 @@ describe('inferReasoning', () => { it('returns false when wire is undefined', () => { expect(inferReasoning(undefined, 'gpt-4o', 'https://api.openai.com/v1')).toBe(false); }); - - it('returns true for third-party openai-chat with reasoning model ID (issue #188)', () => { - // univibe/custom proxy with Claude 4 model - expect(inferReasoning('openai-chat', 'claude-opus-4-6', 'https://api.univibe.cc/openai')).toBe( - true, - ); - expect( - inferReasoning('openai-chat', 'claude-sonnet-4-6', 'https://api.univibe.cc/openai'), - ).toBe(true); - // OpenRouter-style paths on custom proxy - expect( - inferReasoning('openai-chat', 'anthropic/claude-opus-4-6', 'https://my-proxy.example/v1'), - ).toBe(true); - // OpenAI-style namespaced paths on custom proxy - expect(inferReasoning('openai-chat', 'openai/o3-mini', 'https://my-proxy.example/v1')).toBe( - true, - ); - expect(inferReasoning('openai-chat', 'openai/gpt-5.1', 'https://my-proxy.example/v1')).toBe( - true, - ); - // o1 on custom proxy - expect(inferReasoning('openai-chat', 'o1-mini', 'https://my-proxy.example/v1')).toBe(true); - // qwen/qwq on custom proxy - expect( - inferReasoning('openai-chat', 'qwen/qwq-32b-preview', 'https://my-proxy.example/v1'), - ).toBe(true); - }); - - it('returns false for third-party openai-chat with non-reasoning model ID', () => { - expect( - inferReasoning( - 'openai-chat', - 'qwen3.6-plus', - 'https://dashscope.aliyuncs.com/compatible-mode/v1', - ), - ).toBe(false); - expect(inferReasoning('openai-chat', 'deepseek-chat', 'https://api.deepseek.com/v1')).toBe( - false, - ); - expect(inferReasoning('openai-chat', 'glm-4.6v', 'https://open.bigmodel.cn/api/paas/v4')).toBe( - false, - ); - }); }); diff --git a/packages/providers/src/index.ts b/packages/providers/src/index.ts index 29c1140b..940b103e 100644 --- a/packages/providers/src/index.ts +++ b/packages/providers/src/index.ts @@ -189,24 +189,6 @@ function isReasoningModelId(modelId: string): boolean { return /^(o[134]|gpt-5)/i.test(modelId); } -/** - * Matches reasoning-capable model IDs commonly proxied through OpenAI-compatible - * gateways (OpenRouter, univibe, sub2api, etc). This pattern matches the same - * set that OPENROUTER_REASONING_MODEL_RE uses for OpenRouter, but applies to - * custom openai-chat wire endpoints as well. - */ -const REASONING_MODEL_ID_PATTERN = new RegExp( - [ - ':thinking$', - '(^|/)claude-(?:opus|sonnet)-4', - '^(?:openai/)?(?:o1|o3|o4|gpt-5)(?:[-.].*)?$', - '^minimax/minimax-m\\d', - '^deepseek/deepseek-r\\d', - '^qwen/qwq', - ].join('|'), - 'i', -); - export function inferReasoning( wire: GenerateOptions['wire'], modelId: string, @@ -219,14 +201,7 @@ export function inferReasoning( case 'openai-codex-responses': return true; case 'openai-chat': - // For official OpenAI, check both base URL and model ID pattern - if (isOpenAIOfficial(baseUrl)) { - return isReasoningModelId(modelId); - } - // For third-party OpenAI-compatible gateways, heuristically match - // common reasoning model IDs — many gateways still require the - // reasoning flag to get extended thinking output. - return REASONING_MODEL_ID_PATTERN.test(modelId); + return isOpenAIOfficial(baseUrl) && isReasoningModelId(modelId); default: return false; }