diff --git a/.changeset/litellm-gateway-preset.md b/.changeset/litellm-gateway-preset.md new file mode 100644 index 00000000..6a94a8e5 --- /dev/null +++ b/.changeset/litellm-gateway-preset.md @@ -0,0 +1,7 @@ +--- +"@open-codesign/shared": patch +"@open-codesign/i18n": patch +"@open-codesign/desktop": patch +--- + +Add a LiteLLM Gateway preset to Settings → Add provider. It pre-fills an OpenAI-compatible endpoint at `http://localhost:4000/v1`, starts in keyless mode for proxies without a master key, and explains how to switch to a master/virtual key. LiteLLM is not bundled. diff --git a/apps/desktop/src/renderer/src/components/AddCustomProviderModal.test.tsx b/apps/desktop/src/renderer/src/components/AddCustomProviderModal.test.tsx index 58162e69..dbbf19be 100644 --- a/apps/desktop/src/renderer/src/components/AddCustomProviderModal.test.tsx +++ b/apps/desktop/src/renderer/src/components/AddCustomProviderModal.test.tsx @@ -24,6 +24,27 @@ describe('AddCustomProviderModal', () => { expect(html).not.toMatch(/type="checkbox"[^>]*checked/); }); + it('starts keyless with the preset hint when a preset opts out of API keys', () => { + const html = renderToStaticMarkup( + undefined} + onClose={() => undefined} + initialValues={{ + name: 'LiteLLM Gateway', + baseUrl: 'http://localhost:4000/v1', + wire: 'openai-chat', + requiresApiKey: false, + hint: 'litellm-hint', + }} + />, + ); + + expect(html).toContain('litellm-hint'); + expect(html).toContain('value="http://localhost:4000/v1"'); + expect(html).toMatch(/type="checkbox"[^>]*checked/); + expect(html).toMatch(/type="password"[^>]*disabled/); + }); + it('hides the compatibility warning when editing a locked builtin endpoint', () => { const html = renderToStaticMarkup( + {!isEdit && initialValues?.hint !== undefined && ( +

+ {initialValues.hint} +

+ )} + {!lockEndpoint && (
diff --git a/apps/desktop/src/renderer/src/components/settings/ModelsTab.tsx b/apps/desktop/src/renderer/src/components/settings/ModelsTab.tsx index aee2e286..93ec2bf3 100644 --- a/apps/desktop/src/renderer/src/components/settings/ModelsTab.tsx +++ b/apps/desktop/src/renderer/src/components/settings/ModelsTab.tsx @@ -1,6 +1,7 @@ import { useT } from '@open-codesign/i18n'; import type { WireApi } from '@open-codesign/shared'; import { + getPresetById, isSupportedOnboardingProvider, PROVIDER_SHORTLIST as SHORTLIST, } from '@open-codesign/shared'; @@ -170,6 +171,7 @@ interface AddProviderMenuProps { onAddOllama: () => void; onAddCustom: () => void; onAddCliProxyApi: () => void; + onAddLiteLLM: () => void; } function AddProviderMenu({ @@ -182,6 +184,7 @@ function AddProviderMenu({ onAddOllama, onAddCustom, onAddCliProxyApi, + onAddLiteLLM, }: AddProviderMenuProps) { const t = useT(); const rootRef = useRef(null); @@ -254,6 +257,13 @@ function AddProviderMenu({ disabled: false, onClick: onAddCliProxyApi, }, + { + key: 'litellm', + label: t('settings.providers.litellm.presetName'), + desc: t('settings.providers.litellm.presetDescription'), + disabled: false, + onClick: onAddLiteLLM, + }, ]; return ( @@ -347,6 +357,8 @@ export function ModelsTab() { baseUrl: string; wire: WireApi; defaultModel?: string; + requiresApiKey?: boolean; + hint?: string; } | undefined >(undefined); @@ -1036,6 +1048,18 @@ export function ModelsTab() { }); setShowAddCustom(true); }} + onAddLiteLLM={() => { + setShowAddMenu(false); + setCustomProviderPreset({ + name: 'LiteLLM Gateway', + baseUrl: getPresetById('litellm')?.baseUrl ?? '', + wire: 'openai-chat', + defaultModel: '', + requiresApiKey: false, + hint: t('settings.providers.litellm.hint'), + }); + setShowAddCustom(true); + }} />
diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index 2ee9630d..e542dc9a 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -690,6 +690,11 @@ "connectionOk": "Connection OK", "connectionFailed": "Connection failed" }, + "litellm": { + "presetName": "LiteLLM Gateway", + "presetDescription": "Self-hosted OpenAI-compatible gateway, API key optional", + "hint": "Point this at a LiteLLM proxy you already run (LiteLLM is not bundled). The default URL is http://localhost:4000/v1; edit it for remote deployments. Keyless mode is on for proxies without a master key; turn it off and paste your master or virtual key if the gateway requires auth. For a localhost URL, allow private-network testing below." + }, "cliProxyApi": { "presetName": "CLIProxyAPI", "presetDescription": "Local proxy that wraps Claude/Codex/Gemini OAuth subscriptions", diff --git a/packages/i18n/src/locales/es.json b/packages/i18n/src/locales/es.json index b9140674..a75616f8 100644 --- a/packages/i18n/src/locales/es.json +++ b/packages/i18n/src/locales/es.json @@ -514,6 +514,11 @@ "connectionOk": "Conexión OK", "connectionFailed": "Conexión fallida" }, + "litellm": { + "presetName": "LiteLLM Gateway", + "presetDescription": "Gateway autoalojado compatible con OpenAI, clave API opcional", + "hint": "Apunta a un proxy LiteLLM que ya tengas en ejecución (LiteLLM no viene incluido). La URL predeterminada es http://localhost:4000/v1; edítala para despliegues remotos. El modo sin clave está activado para proxies sin master key; desactívalo y pega tu master key o virtual key si el gateway requiere autenticación. Para una URL localhost, permite abajo las pruebas en red privada." + }, "cliProxyApi": { "presetName": "CLIProxyAPI", "presetDescription": "Proxy local que envuelve suscripciones OAuth de Claude/Codex/Gemini", diff --git a/packages/i18n/src/locales/pt-BR.json b/packages/i18n/src/locales/pt-BR.json index 9beab78f..78f95280 100644 --- a/packages/i18n/src/locales/pt-BR.json +++ b/packages/i18n/src/locales/pt-BR.json @@ -491,6 +491,11 @@ }, "addKey": "Adicionar chave", "missingKey": "Chave ausente", + "litellm": { + "presetName": "LiteLLM Gateway", + "presetDescription": "Gateway auto-hospedado compatível com OpenAI, chave de API opcional", + "hint": "Aponte para um proxy LiteLLM que você já executa (o LiteLLM não vem incluído). A URL padrão é http://localhost:4000/v1; edite-a para implantações remotas. O modo sem chave vem ativado para proxies sem master key; desative-o e cole sua master key ou virtual key se o gateway exigir autenticação. Para uma URL localhost, permita abaixo os testes em rede privada." + }, "cliProxyApi": { "presetName": "CLIProxyAPI", "presetDescription": "Proxy local que encapsula assinaturas OAuth do Claude/Codex/Gemini", diff --git a/packages/i18n/src/locales/zh-CN.json b/packages/i18n/src/locales/zh-CN.json index 05ab4582..85d03569 100644 --- a/packages/i18n/src/locales/zh-CN.json +++ b/packages/i18n/src/locales/zh-CN.json @@ -690,6 +690,11 @@ "connectionOk": "连接正常", "connectionFailed": "连接失败" }, + "litellm": { + "presetName": "LiteLLM Gateway", + "presetDescription": "自托管的 OpenAI 兼容网关,API Key 可选", + "hint": "请指向你已在运行的 LiteLLM 代理(应用不内置 LiteLLM)。默认地址为 http://localhost:4000/v1,远程部署请修改 URL。默认开启无 Key 模式,适用于未配置 master key 的代理;若网关需要鉴权,请关闭该选项并粘贴 master key 或 virtual key。使用 localhost 地址时,请在下方允许测试本地/内网地址。" + }, "cliProxyApi": { "presetName": "CLIProxyAPI", "presetDescription": "本地反代,将 Claude/Codex/Gemini 的订阅账号包装成统一 API", diff --git a/packages/shared/src/proxy-presets.test.ts b/packages/shared/src/proxy-presets.test.ts index b19cdab2..cd524ed9 100644 --- a/packages/shared/src/proxy-presets.test.ts +++ b/packages/shared/src/proxy-presets.test.ts @@ -36,6 +36,7 @@ describe('PROXY_PRESETS', () => { expect(ids).toContain('requesty'); expect(ids).toContain('siliconflow'); expect(ids).toContain('one-api'); + expect(ids).toContain('litellm'); expect(ids).toContain('custom'); }); @@ -50,6 +51,12 @@ describe('PROXY_PRESETS', () => { expect(preset?.baseUrl).toBe('https://api.openai.com/v1'); }); + it('LiteLLM preset targets the default local proxy over the OpenAI wire', () => { + const litellm = PROXY_PRESETS.find((p) => p.id === 'litellm'); + expect(litellm?.provider).toBe('openai'); + expect(litellm?.baseUrl).toBe('http://localhost:4000/v1'); + }); + it('custom preset has empty baseUrl', () => { const custom = PROXY_PRESETS.find((p) => p.id === 'custom'); expect(custom?.baseUrl).toBe(''); diff --git a/packages/shared/src/proxy-presets.ts b/packages/shared/src/proxy-presets.ts index a0b483f4..41f5d47c 100644 --- a/packages/shared/src/proxy-presets.ts +++ b/packages/shared/src/proxy-presets.ts @@ -59,6 +59,13 @@ export const PROXY_PRESETS = [ baseUrl: 'http://localhost:3000/v1', notes: 'Edit URL to your deployment', }, + { + id: 'litellm', + label: 'LiteLLM Gateway', + provider: 'openai', + baseUrl: 'http://localhost:4000/v1', + notes: 'Self-hosted OpenAI-compatible gateway; key optional', + }, { id: 'cli-proxy-api', label: 'CLIProxyAPI',