Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/litellm-gateway-preset.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<AddCustomProviderModal
onSave={() => 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(
<AddCustomProviderModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ interface Props {
baseUrl?: string;
wire?: WireApi;
defaultModel?: string;
requiresApiKey?: boolean;
/** Preset-specific setup note shown above the form. */
hint?: string;
};
/**
* Edit-mode: pre-fill every field from an existing provider and save via
Expand Down Expand Up @@ -143,7 +146,9 @@ export function AddCustomProviderModal({
const [name, setName] = useState(editTarget?.name ?? initialValues?.name ?? '');
const [baseUrl, setBaseUrl] = useState(editTarget?.baseUrl ?? initialValues?.baseUrl ?? '');
const [apiKey, setApiKey] = useState('');
const [requiresApiKey, setRequiresApiKey] = useState(editTarget?.requiresApiKey !== false);
const [requiresApiKey, setRequiresApiKey] = useState(
(editTarget ?? initialValues)?.requiresApiKey !== false,
);
const [defaultModel, setDefaultModel] = useState(
editTarget?.defaultModel ?? initialValues?.defaultModel ?? '',
);
Expand Down Expand Up @@ -436,6 +441,12 @@ export function AddCustomProviderModal({
</button>
</div>

{!isEdit && initialValues?.hint !== undefined && (
<p className="text-[var(--text-xs)] leading-5 text-[var(--color-text-secondary)]">
{initialValues.hint}
</p>
)}

{!lockEndpoint && (
<Field label={t('settings.providers.custom.wire')}>
<div className="flex gap-3 flex-wrap">
Expand Down
24 changes: 24 additions & 0 deletions apps/desktop/src/renderer/src/components/settings/ModelsTab.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -170,6 +171,7 @@ interface AddProviderMenuProps {
onAddOllama: () => void;
onAddCustom: () => void;
onAddCliProxyApi: () => void;
onAddLiteLLM: () => void;
}

function AddProviderMenu({
Expand All @@ -182,6 +184,7 @@ function AddProviderMenu({
onAddOllama,
onAddCustom,
onAddCliProxyApi,
onAddLiteLLM,
}: AddProviderMenuProps) {
const t = useT();
const rootRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -347,6 +357,8 @@ export function ModelsTab() {
baseUrl: string;
wire: WireApi;
defaultModel?: string;
requiresApiKey?: boolean;
hint?: string;
}
| undefined
>(undefined);
Expand Down Expand Up @@ -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);
}}
/>
</div>

Expand Down
5 changes: 5 additions & 0 deletions packages/i18n/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions packages/i18n/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions packages/i18n/src/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions packages/i18n/src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/src/proxy-presets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand All @@ -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('');
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/src/proxy-presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading