Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const providerOptions = [
{ label: 'Claude', value: 'CLAUDE' },
{ label: 'Gemini', value: 'GEMINI' },
{ label: 'MiniMax', value: 'MINIMAX' },
{ label: 'LiteLLM', value: 'LITELLM' },
];

const emptyFormValues: IAIModelConfigSaveRequest = {
Expand Down Expand Up @@ -307,7 +308,8 @@ export default function AIModelConfigModal({ open, onClose, onChanged }: AIModel
required:
(currentProvider === 'OPENAI' ||
currentProvider === 'CLAUDE' ||
currentProvider === 'MINIMAX') &&
currentProvider === 'MINIMAX' ||
currentProvider === 'LITELLM') &&
!currentConfig?.hasApiKey,
message: i18n('setting.modelConfig.validation.apiKey'),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import {
DEFAULT_LITELLM_BASE_URL,
DEFAULT_MINIMAX_BASE_URL,
resolveBaseUrlOnProviderChange,
resolveProviderBaseUrl,
Expand All @@ -10,8 +11,16 @@ assert.equal(resolveProviderBaseUrl('MINIMAX', ' '), DEFAULT_MINIMAX_BASE_URL)
assert.equal(resolveProviderBaseUrl('MINIMAX', 'https://api.minimax.chat/v1'), 'https://api.minimax.chat/v1');
assert.equal(resolveProviderBaseUrl('OPENAI', ''), '');

// LiteLLM: self-hosted proxy default, overridable by the user.
assert.equal(resolveProviderBaseUrl('LITELLM', ''), DEFAULT_LITELLM_BASE_URL);
assert.equal(resolveProviderBaseUrl('LITELLM', ' '), DEFAULT_LITELLM_BASE_URL);
assert.equal(resolveProviderBaseUrl('LITELLM', 'http://litellm.internal:8000/v1'), 'http://litellm.internal:8000/v1');

assert.equal(resolveBaseUrlOnProviderChange('MINIMAX', ''), DEFAULT_MINIMAX_BASE_URL);
assert.equal(resolveBaseUrlOnProviderChange('LITELLM', ''), DEFAULT_LITELLM_BASE_URL);
assert.equal(resolveBaseUrlOnProviderChange('OPENAI', DEFAULT_MINIMAX_BASE_URL), '');
// Switching away from LiteLLM clears its auto-filled default too.
assert.equal(resolveBaseUrlOnProviderChange('OPENAI', DEFAULT_LITELLM_BASE_URL), '');
assert.equal(
resolveBaseUrlOnProviderChange('OPENAI', 'https://proxy.example.com/v1'),
'https://proxy.example.com/v1',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import type { AIProvider } from '@/service/aiModelConfig';

export const DEFAULT_MINIMAX_BASE_URL = 'https://api.minimax.io/v1';
// LiteLLM is self-hosted; default to the proxy's conventional local address.
export const DEFAULT_LITELLM_BASE_URL = 'http://localhost:4000/v1';

const PROVIDER_DEFAULT_BASE_URLS: Partial<Record<AIProvider, string>> = {
MINIMAX: DEFAULT_MINIMAX_BASE_URL,
LITELLM: DEFAULT_LITELLM_BASE_URL,
};

export const resolveProviderBaseUrl = (provider: AIProvider, baseUrl?: string): string => {
if (provider === 'MINIMAX' && !baseUrl?.trim()) {
return DEFAULT_MINIMAX_BASE_URL;
const providerDefault = PROVIDER_DEFAULT_BASE_URLS[provider];
if (providerDefault && !baseUrl?.trim()) {
return providerDefault;
}
return baseUrl || '';
};

export const resolveBaseUrlOnProviderChange = (provider: AIProvider, baseUrl?: string): string => {
if (provider === 'MINIMAX') {
if (PROVIDER_DEFAULT_BASE_URLS[provider]) {
return resolveProviderBaseUrl(provider, baseUrl);
}
if (baseUrl?.trim() === DEFAULT_MINIMAX_BASE_URL) {
// Clear a previously auto-filled provider default when switching to a provider
// that has none, so a stale default URL is not carried over.
if (baseUrl && Object.values(PROVIDER_DEFAULT_BASE_URLS).includes(baseUrl.trim())) {
return '';
}
return baseUrl || '';
Expand Down
2 changes: 1 addition & 1 deletion chat2db-community-client/src/service/aiModelConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { clientRuntime } from '@client-runtime';
import aiStreamService, { IModelOptionItem } from './aiStream';
import createRequest from './base';

export type AIProvider = 'OPENAI' | 'CLAUDE' | 'GEMINI' | 'MINIMAX';
export type AIProvider = 'OPENAI' | 'CLAUDE' | 'GEMINI' | 'MINIMAX' | 'LITELLM';

export interface IAIModelConfigItem {
id: string;
Expand Down
4 changes: 2 additions & 2 deletions chat2db-community-client/src/service/aiStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import createRequest from './base';
import { IChatAttachment } from './aiAttachment';

export interface IModelCatalogItem {
provider: 'OPENAI' | 'CLAUDE' | 'GEMINI' | 'MINIMAX';
provider: 'OPENAI' | 'CLAUDE' | 'GEMINI' | 'MINIMAX' | 'LITELLM';
models: string[];
}

export interface IModelOptionItem {
value: string;
label: string;
provider: 'OPENAI' | 'CLAUDE' | 'GEMINI' | 'MINIMAX';
provider: 'OPENAI' | 'CLAUDE' | 'GEMINI' | 'MINIMAX' | 'LITELLM';
model: string;
modelConfigId?: string;
customOption?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ public enum AiProviderEnum {
OPENAI,
CLAUDE,
GEMINI,
MINIMAX;
MINIMAX,
LITELLM;

public static AiProviderEnum from(String value) {
if (value == null || value.trim().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public class AiModelConfigServiceImpl implements IAiModelConfigService {
private static final String DEFAULT_GEMINI_LOCATION = "us-central1";
private static final String DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
private static final String DEFAULT_MINIMAX_BASE_URL = "https://api.minimax.io/v1";
// LiteLLM is self-hosted; default to the proxy's conventional local address.
private static final String DEFAULT_LITELLM_BASE_URL = "http://localhost:4000/v1";
private static final int TEST_ERROR_BODY_MAX_LENGTH = 2000;
private static final String CONFIG_VALUE_PREFIX = "config:";
private static final String PRESET_VALUE_PREFIX = "preset:";
Expand Down Expand Up @@ -224,6 +226,9 @@ public ModelConfigTestResponse testModelConfig(AiModelConfigSaveRequest request)
}
return testOpenAiCompatibleConfig(request, DEFAULT_MINIMAX_BASE_URL);
}
if (provider == AiProviderEnum.LITELLM) {
return testOpenAiCompatibleConfig(request, DEFAULT_LITELLM_BASE_URL);
}
return ModelConfigTestResponse.failure(null, null,
"Connection test currently supports OpenAI-compatible models only.");
}
Expand Down Expand Up @@ -298,7 +303,8 @@ public synchronized AiRuntimeModel resolveRuntimeModel(AiChatRuntimeResolveReque
*/
private void normalizeRuntimeBaseUrl(AiRuntimeModel runtimeModel) {
AiProviderEnum provider = AiProviderEnum.from(runtimeModel.getProvider());
if (provider != AiProviderEnum.OPENAI && provider != AiProviderEnum.CLAUDE && provider != AiProviderEnum.MINIMAX) {
if (provider != AiProviderEnum.OPENAI && provider != AiProviderEnum.CLAUDE
&& provider != AiProviderEnum.MINIMAX && provider != AiProviderEnum.LITELLM) {
return;
}
runtimeModel.setBaseUrl(stripTrailingV1(runtimeModel.getBaseUrl()));
Expand Down Expand Up @@ -346,6 +352,12 @@ private void fillRuntimeFallback(AiRuntimeModel runtimeModel) {
runtimeModel.setBaseUrl(defaultValue(trimToNull(runtimeModel.getBaseUrl()), trimToNull(System.getenv("ANTHROPIC_BASE_URL"))));
return;
}
if (provider == AiProviderEnum.LITELLM) {
runtimeModel.setApiKey(defaultValue(trimToNull(runtimeModel.getApiKey()), trimToNull(System.getenv("LITELLM_API_KEY"))));
runtimeModel.setBaseUrl(defaultValue(trimToNull(runtimeModel.getBaseUrl()), trimToNull(System.getenv("LITELLM_BASE_URL"))));
runtimeModel.setBaseUrl(defaultValue(trimToNull(runtimeModel.getBaseUrl()), DEFAULT_LITELLM_BASE_URL));
return;
}
if (provider == AiProviderEnum.GEMINI) {
runtimeModel.setProjectId(defaultValue(trimToNull(runtimeModel.getProjectId()), trimToNull(System.getenv("GOOGLE_CLOUD_PROJECT"))));
runtimeModel.setLocation(defaultValue(trimToNull(runtimeModel.getLocation()), trimToNull(System.getenv("GOOGLE_CLOUD_LOCATION"))));
Expand All @@ -364,7 +376,8 @@ private void validateRuntimeModel(AiRuntimeModel runtimeModel) {
if (provider == null) {
throw new IllegalArgumentException("Unsupported provider: " + runtimeModel.getProvider());
}
if (provider == AiProviderEnum.OPENAI || provider == AiProviderEnum.CLAUDE) {
if (provider == AiProviderEnum.OPENAI || provider == AiProviderEnum.CLAUDE
|| provider == AiProviderEnum.LITELLM) {
return;
}
if (provider == AiProviderEnum.GEMINI) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ public AiChatClient create(AiRuntimeModel runtimeModel, RequestMode requestMode)
if (provider == AiProviderEnum.GEMINI) {
return geminiClient(runtimeModel, retryTemplate);
}
if (provider == AiProviderEnum.LITELLM) {
// LiteLLM exposes an OpenAI-compatible endpoint (the LiteLLM proxy), so it
// reuses the OpenAI client pointed at the user's configured base URL. One
// provider entry then reaches every model the proxy is configured to serve.
return openAiClient(runtimeModel, retryTemplate);
}
throw new IllegalArgumentException("Unsupported provider: " + runtimeModel.getProvider());
}

Expand Down
Loading