Skip to content

Commit 972a36e

Browse files
committed
feat(desktop): add LiteLLM Gateway provider preset
Add a LiteLLM Gateway entry to PROXY_PRESETS and the Settings add-provider menu. It pre-fills the OpenAI-compatible default http://localhost:4000/v1, starts in keyless mode via the existing requiresApiKey flag, and shows setup copy in all locales. LiteLLM is not bundled. Closes #208
1 parent 5bc24f0 commit 972a36e

10 files changed

Lines changed: 98 additions & 1 deletion

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@open-codesign/shared": patch
3+
"@open-codesign/i18n": patch
4+
"@open-codesign/desktop": patch
5+
---
6+
7+
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.

‎apps/desktop/src/renderer/src/components/AddCustomProviderModal.test.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,27 @@ describe('AddCustomProviderModal', () => {
2424
expect(html).not.toMatch(/type="checkbox"[^>]*checked/);
2525
});
2626

27+
it('starts keyless with the preset hint when a preset opts out of API keys', () => {
28+
const html = renderToStaticMarkup(
29+
<AddCustomProviderModal
30+
onSave={() => undefined}
31+
onClose={() => undefined}
32+
initialValues={{
33+
name: 'LiteLLM Gateway',
34+
baseUrl: 'http://localhost:4000/v1',
35+
wire: 'openai-chat',
36+
requiresApiKey: false,
37+
hint: 'litellm-hint',
38+
}}
39+
/>,
40+
);
41+
42+
expect(html).toContain('litellm-hint');
43+
expect(html).toContain('value="http://localhost:4000/v1"');
44+
expect(html).toMatch(/type="checkbox"[^>]*checked/);
45+
expect(html).toMatch(/type="password"[^>]*disabled/);
46+
});
47+
2748
it('hides the compatibility warning when editing a locked builtin endpoint', () => {
2849
const html = renderToStaticMarkup(
2950
<AddCustomProviderModal

‎apps/desktop/src/renderer/src/components/AddCustomProviderModal.tsx‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ interface Props {
1919
baseUrl?: string;
2020
wire?: WireApi;
2121
defaultModel?: string;
22+
requiresApiKey?: boolean;
23+
/** Preset-specific setup note shown above the form. */
24+
hint?: string;
2225
};
2326
/**
2427
* Edit-mode: pre-fill every field from an existing provider and save via
@@ -143,7 +146,9 @@ export function AddCustomProviderModal({
143146
const [name, setName] = useState(editTarget?.name ?? initialValues?.name ?? '');
144147
const [baseUrl, setBaseUrl] = useState(editTarget?.baseUrl ?? initialValues?.baseUrl ?? '');
145148
const [apiKey, setApiKey] = useState('');
146-
const [requiresApiKey, setRequiresApiKey] = useState(editTarget?.requiresApiKey !== false);
149+
const [requiresApiKey, setRequiresApiKey] = useState(
150+
(editTarget ?? initialValues)?.requiresApiKey !== false,
151+
);
147152
const [defaultModel, setDefaultModel] = useState(
148153
editTarget?.defaultModel ?? initialValues?.defaultModel ?? '',
149154
);
@@ -436,6 +441,12 @@ export function AddCustomProviderModal({
436441
</button>
437442
</div>
438443

444+
{!isEdit && initialValues?.hint !== undefined && (
445+
<p className="text-[var(--text-xs)] leading-5 text-[var(--color-text-secondary)]">
446+
{initialValues.hint}
447+
</p>
448+
)}
449+
439450
{!lockEndpoint && (
440451
<Field label={t('settings.providers.custom.wire')}>
441452
<div className="flex gap-3 flex-wrap">

‎apps/desktop/src/renderer/src/components/settings/ModelsTab.tsx‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useT } from '@open-codesign/i18n';
22
import type { WireApi } from '@open-codesign/shared';
33
import {
4+
getPresetById,
45
isSupportedOnboardingProvider,
56
PROVIDER_SHORTLIST as SHORTLIST,
67
} from '@open-codesign/shared';
@@ -170,6 +171,7 @@ interface AddProviderMenuProps {
170171
onAddOllama: () => void;
171172
onAddCustom: () => void;
172173
onAddCliProxyApi: () => void;
174+
onAddLiteLLM: () => void;
173175
}
174176

175177
function AddProviderMenu({
@@ -182,6 +184,7 @@ function AddProviderMenu({
182184
onAddOllama,
183185
onAddCustom,
184186
onAddCliProxyApi,
187+
onAddLiteLLM,
185188
}: AddProviderMenuProps) {
186189
const t = useT();
187190
const rootRef = useRef<HTMLDivElement>(null);
@@ -254,6 +257,13 @@ function AddProviderMenu({
254257
disabled: false,
255258
onClick: onAddCliProxyApi,
256259
},
260+
{
261+
key: 'litellm',
262+
label: t('settings.providers.litellm.presetName'),
263+
desc: t('settings.providers.litellm.presetDescription'),
264+
disabled: false,
265+
onClick: onAddLiteLLM,
266+
},
257267
];
258268

259269
return (
@@ -347,6 +357,8 @@ export function ModelsTab() {
347357
baseUrl: string;
348358
wire: WireApi;
349359
defaultModel?: string;
360+
requiresApiKey?: boolean;
361+
hint?: string;
350362
}
351363
| undefined
352364
>(undefined);
@@ -1036,6 +1048,18 @@ export function ModelsTab() {
10361048
});
10371049
setShowAddCustom(true);
10381050
}}
1051+
onAddLiteLLM={() => {
1052+
setShowAddMenu(false);
1053+
setCustomProviderPreset({
1054+
name: 'LiteLLM Gateway',
1055+
baseUrl: getPresetById('litellm')?.baseUrl ?? '',
1056+
wire: 'openai-chat',
1057+
defaultModel: '',
1058+
requiresApiKey: false,
1059+
hint: t('settings.providers.litellm.hint'),
1060+
});
1061+
setShowAddCustom(true);
1062+
}}
10391063
/>
10401064
</div>
10411065

‎packages/i18n/src/locales/en.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,11 @@
690690
"connectionOk": "Connection OK",
691691
"connectionFailed": "Connection failed"
692692
},
693+
"litellm": {
694+
"presetName": "LiteLLM Gateway",
695+
"presetDescription": "Self-hosted OpenAI-compatible gateway, API key optional",
696+
"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."
697+
},
693698
"cliProxyApi": {
694699
"presetName": "CLIProxyAPI",
695700
"presetDescription": "Local proxy that wraps Claude/Codex/Gemini OAuth subscriptions",

‎packages/i18n/src/locales/es.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,11 @@
514514
"connectionOk": "Conexión OK",
515515
"connectionFailed": "Conexión fallida"
516516
},
517+
"litellm": {
518+
"presetName": "LiteLLM Gateway",
519+
"presetDescription": "Gateway autoalojado compatible con OpenAI, clave API opcional",
520+
"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."
521+
},
517522
"cliProxyApi": {
518523
"presetName": "CLIProxyAPI",
519524
"presetDescription": "Proxy local que envuelve suscripciones OAuth de Claude/Codex/Gemini",

‎packages/i18n/src/locales/pt-BR.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,11 @@
491491
},
492492
"addKey": "Adicionar chave",
493493
"missingKey": "Chave ausente",
494+
"litellm": {
495+
"presetName": "LiteLLM Gateway",
496+
"presetDescription": "Gateway auto-hospedado compatível com OpenAI, chave de API opcional",
497+
"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."
498+
},
494499
"cliProxyApi": {
495500
"presetName": "CLIProxyAPI",
496501
"presetDescription": "Proxy local que encapsula assinaturas OAuth do Claude/Codex/Gemini",

‎packages/i18n/src/locales/zh-CN.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,11 @@
690690
"connectionOk": "连接正常",
691691
"connectionFailed": "连接失败"
692692
},
693+
"litellm": {
694+
"presetName": "LiteLLM Gateway",
695+
"presetDescription": "自托管的 OpenAI 兼容网关,API Key 可选",
696+
"hint": "请指向你已在运行的 LiteLLM 代理(应用不内置 LiteLLM)。默认地址为 http://localhost:4000/v1,远程部署请修改 URL。默认开启无 Key 模式,适用于未配置 master key 的代理;若网关需要鉴权,请关闭该选项并粘贴 master key 或 virtual key。使用 localhost 地址时,请在下方允许测试本地/内网地址。"
697+
},
693698
"cliProxyApi": {
694699
"presetName": "CLIProxyAPI",
695700
"presetDescription": "本地反代,将 Claude/Codex/Gemini 的订阅账号包装成统一 API",

‎packages/shared/src/proxy-presets.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ describe('PROXY_PRESETS', () => {
3636
expect(ids).toContain('requesty');
3737
expect(ids).toContain('siliconflow');
3838
expect(ids).toContain('one-api');
39+
expect(ids).toContain('litellm');
3940
expect(ids).toContain('custom');
4041
});
4142

@@ -50,6 +51,12 @@ describe('PROXY_PRESETS', () => {
5051
expect(preset?.baseUrl).toBe('https://api.openai.com/v1');
5152
});
5253

54+
it('LiteLLM preset targets the default local proxy over the OpenAI wire', () => {
55+
const litellm = PROXY_PRESETS.find((p) => p.id === 'litellm');
56+
expect(litellm?.provider).toBe('openai');
57+
expect(litellm?.baseUrl).toBe('http://localhost:4000/v1');
58+
});
59+
5360
it('custom preset has empty baseUrl', () => {
5461
const custom = PROXY_PRESETS.find((p) => p.id === 'custom');
5562
expect(custom?.baseUrl).toBe('');

‎packages/shared/src/proxy-presets.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ export const PROXY_PRESETS = [
5959
baseUrl: 'http://localhost:3000/v1',
6060
notes: 'Edit URL to your deployment',
6161
},
62+
{
63+
id: 'litellm',
64+
label: 'LiteLLM Gateway',
65+
provider: 'openai',
66+
baseUrl: 'http://localhost:4000/v1',
67+
notes: 'Self-hosted OpenAI-compatible gateway; key optional',
68+
},
6269
{
6370
id: 'cli-proxy-api',
6471
label: 'CLIProxyAPI',

0 commit comments

Comments
 (0)