feat: add AIMLAPI (aimlapi.com) as a model provider (#17311)

### Summary

This PR adds **aimlapi.com** as a model provider, so a RAGFlow user can
enter one API key in the model settings and use AIMLAPI's models across
the app. AIMLAPI ([aimlapi.com](https://aimlapi.com)) is an
OpenAI-compatible aggregator that serves 700+ models (LLM, embedding,
vision, TTS, ASR) from many providers behind a single API.

The change mirrors the repo's existing "add provider" pattern (e.g.
FuturMix / OpenRouter): provider logic lives in the same files those
providers use, and shared / UI files get only registration entries.

**Backend**
- `conf/llm_factories.json` — the `aimlapi.com` factory entry.
- `rag/llm/__init__.py`, `rag/llm/{chat,embedding,cv}_model.py` —
LiteLLM adapters (chat, embedding, image2text) with a production base
URL, overridable via `AIMLAPI_API_URL`.
- `rag/llm/model_meta.py` — an `AIMLAPI` model-meta so the provider
lists its full `/v1/models` catalog dynamically (classified by the
endpoint `type`), the same way OpenRouter does.
- `api/apps/restful_apis/aimlapi_api.py` — an optional "Get API key"
flow using AIMLAPI's agent-authorization (OAuth 2.0 Device Authorization
Grant, RFC 8628). The device code is kept server-side (Redis); only the
issued key reaches the browser.

**Frontend (`web/`)**
- Provider registration (constant, icon allowlist, brand logo), the
model picker (`LIST_MODEL_PROVIDERS` + a `buildLocalConfig` entry), and
the "Get API key" button in the provider dialog. Locales added to `en`
and `zh`.

**Configuration** — production defaults are compiled in; endpoints and
the partner id are overridable through `AIMLAPI_*` environment
variables, so the same build works across environments.

**Testing** — the `web` build passes; chat, embedding and dynamic model
listing were smoke-tested against the live API.
This commit is contained in:
Eugene
2026-07-24 17:50:14 +03:00
committed by GitHub
parent f4d1c9f8b2
commit 6b11f62391
23 changed files with 793 additions and 16 deletions

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 2029 2029">
<defs>
<clipPath id="clip-AIMLAPI">
<rect width="2029" height="2029"/>
</clipPath>
</defs>
<g id="AIMLAPI" clip-path="url(#clip-AIMLAPI)">
<circle id="Ellipse_5" data-name="Ellipse 5" cx="1014.5" cy="1014.5" r="1014.5"/>
<path id="Subtraction_430" data-name="Subtraction 430" d="M-20415.064,4029h-655.867a156.035,156.035,0,0,1-40.705-5.4,156.112,156.112,0,0,1-37.3-15.5,156.067,156.067,0,0,1-32.07-24.55,155.938,155.938,0,0,1-25.027-32.548l-327.936-568a155.812,155.812,0,0,1-15.674-37.948,156.027,156.027,0,0,1-5.225-40.05,156.027,156.027,0,0,1,5.225-40.05,155.809,155.809,0,0,1,15.674-37.949l327.936-568a155.915,155.915,0,0,1,25.027-32.552,156.072,156.072,0,0,1,32.07-24.551,156.111,156.111,0,0,1,37.3-15.5,156.045,156.045,0,0,1,40.705-5.4h655.867a156,156,0,0,1,40.7,5.4,156.071,156.071,0,0,1,37.295,15.5,156.047,156.047,0,0,1,32.072,24.551,156.011,156.011,0,0,1,25.029,32.551l327.936,568a155.952,155.952,0,0,1,15.672,37.949,156.027,156.027,0,0,1,5.225,40.05,156.027,156.027,0,0,1-5.225,40.05,155.951,155.951,0,0,1-15.672,37.948l-327.936,568a156.03,156.03,0,0,1-25.029,32.548,156.1,156.1,0,0,1-32.072,24.55,156.052,156.052,0,0,1-37.295,15.5A155.994,155.994,0,0,1-20415.064,4029Zm-439.025-904.879a70.149,70.149,0,0,0-13.3,1.274,69.656,69.656,0,0,0-12.9,3.823,69.189,69.189,0,0,0-23.033,15.292l-262.551,262.557a69.216,69.216,0,0,0-15.295,23.031,69.4,69.4,0,0,0-3.822,12.9,70.024,70.024,0,0,0-1.275,13.3,70.02,70.02,0,0,0,1.275,13.3,69.394,69.394,0,0,0,3.822,12.9,69.166,69.166,0,0,0,15.295,23.03,69.165,69.165,0,0,0,23.029,15.292,69.7,69.7,0,0,0,12.9,3.823,70.093,70.093,0,0,0,13.295,1.274,70.149,70.149,0,0,0,13.3-1.274,69.7,69.7,0,0,0,12.9-3.823,69.182,69.182,0,0,0,23.031-15.292l213.324-213.324L-20680.7,3465.6a69.214,69.214,0,0,0,23.031,15.292,69.666,69.666,0,0,0,12.9,3.823,70.113,70.113,0,0,0,13.3,1.274,70.113,70.113,0,0,0,13.3-1.274,69.692,69.692,0,0,0,12.9-3.823,69.2,69.2,0,0,0,23.029-15.292l262.557-262.557a69.193,69.193,0,0,0,15.293-23.03,69.665,69.665,0,0,0,3.822-12.9,70.178,70.178,0,0,0,1.275-13.3,70.176,70.176,0,0,0-1.275-13.3,69.661,69.661,0,0,0-3.822-12.9,69.217,69.217,0,0,0-15.293-23.033,69.164,69.164,0,0,0-23.029-15.292,69.6,69.6,0,0,0-12.9-3.823,70.094,70.094,0,0,0-13.3-1.275,70.094,70.094,0,0,0-13.3,1.275,69.665,69.665,0,0,0-12.9,3.823,69.216,69.216,0,0,0-23.031,15.292l-213.324,213.33-173.4-173.4a69.155,69.155,0,0,0-23.029-15.292,69.656,69.656,0,0,0-12.9-3.823A70.113,70.113,0,0,0-20854.09,3124.121Z" transform="translate(21757.5 -2290.499)" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -108,6 +108,7 @@ const svgIcons = [
LLMFactory.Qiniu,
LLMFactory.TokenHub,
LLMFactory.FunASR,
LLMFactory.AIMLAPI,
];
export const LlmIcon = ({

View File

@@ -87,6 +87,7 @@ export enum LLMFactory {
TokenHub = 'TokenHub',
NewAPI = 'New API',
FunASR = 'FunASR',
AIMLAPI = 'aimlapi.com',
}
// Please lowercase the file name
@@ -173,6 +174,7 @@ export const IconMap = {
[LLMFactory.SoMark]: 'somark',
[LLMFactory.NewAPI]: 'new-api',
[LLMFactory.FunASR]: 'funasr',
[LLMFactory.AIMLAPI]: 'aimlapi',
};
export const ModelTypeToField: Record<string, string> = {
@@ -195,6 +197,7 @@ export const FieldToModelType: Record<string, string> = {
export const APIMapUrl = {
[LLMFactory.OpenAI]: 'https://platform.openai.com/api-keys',
[LLMFactory.AIMLAPI]: 'https://aimlapi.com/app/keys',
[LLMFactory.Anthropic]: 'https://console.anthropic.com/settings/keys',
[LLMFactory.Gemini]: 'https://aistudio.google.com/app/apikey',
[LLMFactory.DeepSeek]: 'https://platform.deepseek.com/api_keys',

View File

@@ -0,0 +1,186 @@
import llmService from '@/services/llm-service';
import { useCallback, useEffect, useRef, useState } from 'react';
/**
* Drives the AIMLAPI agent-authorization (OAuth 2.0 device grant) flow from the
* provider dialog: start → open the AIMLAPI consent page in a popup → poll the
* RAGFlow backend until the key is issued. RAGFlow never sees the user's AIMLAPI
* credentials; the popup handles sign-in/consent on aimlapi.com.
*
* Mirrors the data-source connector OAuth pattern
* (`data-source/component/google-drive-token-field.tsx`): a popup opened
* synchronously on click, a self-scheduling `setTimeout` poll loop, a manual
* "check status" fallback, and cleanup on unmount.
*/
export type AimlapiAuthorizeStatus =
| 'idle'
| 'starting'
| 'awaiting_consent'
| 'success'
| 'denied'
| 'expired'
| 'error';
const TERMINAL_DENIED = new Set(['denied', 'rejected']);
const TERMINAL_EXPIRED = new Set(['expired', 'cancelled', 'canceled']);
const POPUP_NAME = 'ragflow-aimlapi-oauth';
const POPUP_FEATURES = 'width=600,height=760';
interface UseAimlapiAuthorizeOptions {
onKey: (apiKey: string) => void;
}
export function useAimlapiAuthorize({ onKey }: UseAimlapiAuthorizeOptions) {
const [status, setStatus] = useState<AimlapiAuthorizeStatus>('idle');
const [error, setError] = useState<string | null>(null);
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const deadlineRef = useRef<number>(0);
const requestIdRef = useRef<string | null>(null);
const popupRef = useRef<Window | null>(null);
const stoppedRef = useRef<boolean>(false);
const intervalMsRef = useRef<number>(5000);
const clearTimer = useCallback(() => {
if (pollTimerRef.current) {
clearTimeout(pollTimerRef.current);
pollTimerRef.current = null;
}
}, []);
useEffect(() => {
return () => {
stoppedRef.current = true;
clearTimer();
};
}, [clearTimer]);
const poll = useCallback(
async (requestId: string, intervalMs: number) => {
if (stoppedRef.current) return;
if (Date.now() > deadlineRef.current) {
setStatus('expired');
return;
}
try {
const { data } = await llmService.aimlapiAuthorizePoll({
request_id: requestId,
});
if (stoppedRef.current) return;
if (data?.code !== 0) {
setStatus('error');
setError(data?.message ?? null);
return;
}
const result = data.data ?? {};
const state: string = result.status ?? 'pending';
if (state === 'ready' && result.api_key) {
setStatus('success');
try {
popupRef.current?.close();
} catch {
/* popup may be cross-origin/closed */
}
onKey(result.api_key);
return;
}
if (TERMINAL_DENIED.has(state)) {
setStatus('denied');
return;
}
if (TERMINAL_EXPIRED.has(state)) {
setStatus('expired');
return;
}
// still pending → schedule the next poll
pollTimerRef.current = setTimeout(
() => poll(requestId, intervalMs),
intervalMs,
);
} catch (e: any) {
if (stoppedRef.current) return;
setStatus('error');
setError(e?.message ?? null);
}
},
[onKey],
);
const start = useCallback(async () => {
stoppedRef.current = false;
setError(null);
setStatus('starting');
clearTimer();
// Open the popup synchronously within the click handler so it is not blocked;
// navigate it once the backend returns the consent URL.
const popup = window.open('', POPUP_NAME, POPUP_FEATURES);
popupRef.current = popup;
try {
const { data } = await llmService.aimlapiAuthorizeStart();
if (data?.code !== 0) {
setStatus('error');
setError(data?.message ?? null);
try {
popup?.close();
} catch {
/* noop */
}
return;
}
const {
request_id: requestId,
verification_uri: verificationUri,
interval,
expires_in: expiresIn,
} = data.data ?? {};
if (!requestId || !verificationUri) {
setStatus('error');
setError('AIMLAPI authorization response is missing required fields.');
try {
popup?.close();
} catch {
/* noop */
}
return;
}
requestIdRef.current = requestId;
deadlineRef.current = Date.now() + (expiresIn ?? 900) * 1000;
if (popup && verificationUri) {
popup.location.href = verificationUri;
} else if (verificationUri) {
window.open(verificationUri, POPUP_NAME, POPUP_FEATURES);
}
setStatus('awaiting_consent');
const intervalMs = Math.max(interval ?? 5, 1) * 1000;
intervalMsRef.current = intervalMs;
pollTimerRef.current = setTimeout(
() => poll(requestId, intervalMs),
intervalMs,
);
} catch (e: any) {
setStatus('error');
setError(e?.message ?? null);
try {
popup?.close();
} catch {
/* noop */
}
}
}, [clearTimer, poll]);
// Manual "check now" fallback (e.g. if the popup's postMessage/redirect is missed).
const checkNow = useCallback(() => {
const requestId = requestIdRef.current;
if (!requestId || stoppedRef.current) return;
clearTimer();
poll(requestId, intervalMsRef.current);
}, [clearTimer, poll]);
return { status, error, start, checkNow };
}

View File

@@ -1935,6 +1935,7 @@ Example: Virtual Hosted Style`,
save: 'Save',
search: 'Search',
availableModels: 'Available models',
recommended: 'Recommended',
profile: 'Profile',
avatar: 'Avatar',
avatarTip: 'This will be displayed on your profile.',
@@ -1990,6 +1991,13 @@ Example: Virtual Hosted Style`,
apiKeyMessage: 'Please enter the API Key',
apiKeyTip:
'The API Key can be obtained by registering the corresponding LLM supplier.',
aimlapiGetKey: 'Get API key',
aimlapiCheckStatus: 'Check status',
aimlapiAwaitingConsent: 'Waiting for approval in the AI/ML API window…',
aimlapiKeyAdded: 'Your AI/ML API key has been generated and added above.',
aimlapiAuthDenied: 'Authorization was denied. Please try again.',
aimlapiAuthExpired: 'The request expired. Please try again.',
aimlapiAuthFailed: 'Sign-in failed. Please try again.',
showMoreModels: 'View models',
hideModels: 'Hide models',
baseUrl: 'Base URL',

View File

@@ -1628,6 +1628,7 @@ NER使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
save: '保存',
search: '搜索',
availableModels: '可选模型',
recommended: '推荐',
profile: '概要',
avatar: '头像',
avatarTip: '这会在你的个人主页展示',
@@ -1679,6 +1680,13 @@ NER使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
apiKey: 'API Key',
apiKeyMessage: '请输入API Key',
apiKeyTip: 'API Key可以通过注册相应的LLM供应商来获取。',
aimlapiGetKey: '获取 API Key',
aimlapiCheckStatus: '检查状态',
aimlapiAwaitingConsent: '正在等待 AI/ML API 窗口中的授权…',
aimlapiKeyAdded: '您的 AI/ML API Key 已生成并已填入上方。',
aimlapiAuthDenied: '授权被拒绝,请重试。',
aimlapiAuthExpired: '请求已过期,请重试。',
aimlapiAuthFailed: '登录失败,请重试。',
showMoreModels: '展示更多模型',
hideModels: '隐藏模型',
baseUrl: 'Base URL',

View File

@@ -0,0 +1,73 @@
import { Button } from '@/components/ui/button';
import { useTranslation } from 'react-i18next';
import {
AimlapiAuthorizeStatus,
useAimlapiAuthorize,
} from '@/hooks/use-aimlapi-authorize';
import { cn } from '@/lib/utils';
import { KeyRound, Loader2 } from 'lucide-react';
import { FC } from 'react';
const AimlapiStatusTextKey: Partial<Record<AimlapiAuthorizeStatus, string>> = {
awaiting_consent: 'aimlapiAwaitingConsent',
success: 'aimlapiKeyAdded',
denied: 'aimlapiAuthDenied',
expired: 'aimlapiAuthExpired',
error: 'aimlapiAuthFailed',
};
/**
* "Get API key" control for the AIMLAPI provider dialog. Runs the AIMLAPI
* agent-authorization (OAuth device grant) flow and, on success, hands the
* issued key back to the modal via `onKey` so it can be filled into the form.
*/
export const AimlapiGetKeyButton: FC<{ onKey: (apiKey: string) => void }> = ({
onKey,
}) => {
const { t } = useTranslation('translation', { keyPrefix: 'setting' });
const { status, error, start, checkNow } = useAimlapiAuthorize({ onKey });
const busy = status === 'starting' || status === 'awaiting_consent';
const statusKey = AimlapiStatusTextKey[status];
return (
<div className="flex flex-col gap-1.5 mb-4">
<div className="flex items-center gap-2">
<Button
type="button"
variant="secondary"
size="sm"
onClick={start}
disabled={busy}
>
{busy ? (
<Loader2 className="animate-spin" size={14} />
) : (
<KeyRound size={14} />
)}
{t('aimlapiGetKey')}
</Button>
{status === 'awaiting_consent' && (
<Button type="button" variant="ghost" size="sm" onClick={checkNow}>
{t('aimlapiCheckStatus')}
</Button>
)}
</div>
{statusKey && (
<span
className={cn('text-xs', {
'text-state-success': status === 'success',
'text-text-secondary': status === 'awaiting_consent',
'text-state-error':
status === 'denied' || status === 'expired' || status === 'error',
})}
>
{t(statusKey)}
{status === 'error' && error ? `: ${error}` : ''}
</span>
)}
</div>
);
};
export default AimlapiGetKeyButton;

View File

@@ -18,9 +18,10 @@
import { LlmIcon } from '@/components/svg-icon';
import { Button } from '@/components/ui/button';
import { SearchInput } from '@/components/ui/input';
import { APIMapUrl } from '@/constants/llm';
import { APIMapUrl, LLMFactory } from '@/constants/llm';
import { useFetchAvailableProviders } from '@/hooks/use-llm-request';
import { ArrowUpRight, Plus } from 'lucide-react';
import { sortLLmFactoryListBySpecifiedOrder } from '@/utils/common-util';
import { ArrowUpRight, Plus, Star } from 'lucide-react';
import { FC, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -82,12 +83,16 @@ export const AvailableModels: FC<{
}, [factoryList, searchTerm]);
const filteredModels = useMemo(() => {
if (selectedTag === null) {
return searchedModels;
}
return searchedModels.filter((model) =>
model.model_types.some((type) => type === selectedTag),
);
const filtered =
selectedTag === null
? searchedModels
: searchedModels.filter((model) =>
model.model_types.some((type) => type === selectedTag),
);
// AIMLAPI first (then RAGFlow's curated order) in the available list
return sortLLmFactoryListBySpecifiedOrder(
filtered as any,
) as unknown as typeof filtered;
}, [searchedModels, selectedTag]);
// Number of providers matching each tag, respecting the current search term so
@@ -172,7 +177,7 @@ export const AvailableModels: FC<{
</header>
{/* Models List */}
<div className="p-4 pt-0 flex flex-col gap-4 overflow-auto h-full scrollbar-auto">
<div className="@container p-4 pt-0 flex flex-col gap-4 overflow-auto h-full scrollbar-auto">
{filteredModels.map((model) => (
<div
key={model.name}
@@ -181,9 +186,12 @@ export const AvailableModels: FC<{
className="group border border-border-button rounded-lg p-3 hover:bg-bg-input transition-colors"
onClick={() => handleAddModel(model.name)}
>
<div className="flex items-center space-x-3 mb-3">
<LlmIcon name={model.name} imgClass="h-8 w-8 text-text-primary" />
<div className="flex flex-1 gap-1.5 items-center">
<div className="flex items-center gap-3 mb-3">
<LlmIcon
name={model.name}
imgClass="h-8 w-8 shrink-0 text-text-primary"
/>
<div className="flex flex-1 min-w-0 gap-1.5 items-center">
<div className="font-normal text-base truncate">
{model.name}
</div>
@@ -192,7 +200,7 @@ export const AvailableModels: FC<{
asLink
variant="ghost"
size="icon-xs"
className="size-4"
className="size-4 shrink-0"
to={APIMapUrl[model.name as keyof typeof APIMapUrl]}
target="_blank"
rel="noopener noreferrer"
@@ -203,11 +211,27 @@ export const AvailableModels: FC<{
<ArrowUpRight size={16} />
</Button>
)}
{model.name === LLMFactory.AIMLAPI && (
// Recommended badge: the star always shows; the label appears
// only when the card (container) is wide enough to fit it
// alongside the Add button — otherwise it collapses to just
// the star.
<span
className="shrink-0 inline-flex items-center gap-1 text-state-success"
title={t('setting.recommended')}
aria-label={t('setting.recommended')}
>
<Star size={14} className="shrink-0 fill-current" />
<span className="hidden @[20rem]:inline text-xs font-medium whitespace-nowrap">
{t('setting.recommended')}
</span>
</span>
)}
</div>
<Button
size="xs"
className="px-2 opacity-0 transition-all group-hover:opacity-100 group-focus-within:opacity-100"
className="shrink-0 px-2 opacity-0 transition-all group-hover:opacity-100 group-focus-within:opacity-100"
>
<Plus size={12} />
{t('setting.addTheModel')}

View File

@@ -15,7 +15,9 @@
*/
import { DynamicForm, DynamicFormRef } from '@/components/dynamic-form';
import { RefObject } from 'react';
import { LLMFactory } from '@/constants/llm';
import { useCallback, type RefObject } from 'react';
import { AimlapiGetKeyButton } from '../aimlapi-get-key-button';
import { DRAFT_INSTANCE_SENTINEL, DraftModeCardProps } from '../interface';
import { ModelsSection } from '../models-section';
import VerifyButton from '../verify-button';
@@ -45,6 +47,16 @@ export function DraftModeCard({
draftName,
setDraftName,
}: DraftModeCardProps) {
// On success, fold the OAuth-issued key into the current form values so it
// lands in the (editable) api_key field without clobbering other inputs.
const handleAimlapiKey = useCallback(
(key: string) => {
const current = formRef.current?.getValues() ?? {};
formRef.current?.reset({ ...current, api_key: key });
},
[formRef],
);
return (
<div className="px-2 py-3 flex flex-col gap-4">
<InstanceNameSection
@@ -62,6 +74,10 @@ export function DraftModeCard({
labelClassName="font-normal"
/>
{providerName === LLMFactory.AIMLAPI && (
<AimlapiGetKeyButton onKey={handleAimlapiKey} />
)}
<div className="pt-3">
<VerifyButton
onVerify={handleVerify}

View File

@@ -15,7 +15,10 @@
*/
import { DynamicFormRef } from '@/components/dynamic-form';
import message from '@/components/ui/message';
import { IModelInfo } from '@/interfaces/request/llm';
import { useTranslation } from 'react-i18next';
import { LIST_MODEL_PROVIDERS } from '../provider-schema/constants';
import {
forwardRef,
useEffect,
@@ -88,6 +91,7 @@ const GenericProviderInstanceCard = forwardRef<
const formRef = useRef<DynamicFormRef>(null);
// Mirror of the per-instance model list - written by ModelsSection
// via `setModelInfo`, read by the payload builder.
const { t } = useTranslation();
const modelInfoRef = useRef<IModelInfo[]>([]);
// Provider-specific config: carries `verifyTransform` / `submitTransform`
@@ -189,13 +193,21 @@ const GenericProviderInstanceCard = forwardRef<
// catch it). For both drafts and saved cards, run the form's
// own validation so errors surface in the UI.
if (isDraft && !draftName.trim()) return false;
// List-model providers (list picker) require at least one selected model.
if (
LIST_MODEL_PROVIDERS.has(providerName) &&
modelInfoRef.current.length === 0
) {
message.error(t('setting.selectModelBeforeVerify'));
return false;
}
const isValid = await formRef.current?.trigger();
return !!isValid;
},
getSavePayload,
markSaved,
}),
[isDraft, draftName, getSavePayload, markSaved],
[isDraft, draftName, getSavePayload, markSaved, providerName, t],
);
return (

View File

@@ -30,6 +30,10 @@ import { LLMFactory } from '@/constants/llm';
* 4 model_* fields directly.
*/
export const LIST_MODEL_PROVIDERS = new Set<string>([
// aimlapi.com serves its full /v1/models catalog (700+) via the AIMLAPI
// ModelMeta on the backend, so the dialog uses the list picker (mirror of
// that registry) instead of a single manual model_name field.
LLMFactory.AIMLAPI,
LLMFactory.Ollama,
LLMFactory.OpenRouter,
LLMFactory.VLLM,

View File

@@ -24,6 +24,28 @@ import { buildModelInfoFromValues } from './utils';
* Used for scenarios after OllamaModal merge
*/
export const LocalLlmConfigs: Record<string, ProviderConfig> = {
// aimlapi.com is a cloud OpenAI-compatible aggregator (like OpenRouter): it
// uses the list picker, so it needs a config whose submitTransform forwards
// `model_info` (the picker selection). Falling back to GenericApiKeyConfig
// dropped that field, so selected models were never persisted.
[LLMFactory.AIMLAPI]: buildLocalConfig(
LLMFactory.AIMLAPI,
'aimlapi.com',
['chat', 'embedding', 'image2text', 'tts', 'speech2text'],
undefined,
false,
[
{
name: 'base_url',
label: 'addLlmBaseUrl',
type: 'inputSelect',
required: false,
placeholder: 'baseUrlNameMessage',
shouldRender: 'hideWhenInstanceExists',
},
],
'https://docs.aimlapi.com/quickstart/simple-model',
),
[LLMFactory.Ollama]: buildLocalConfig(
LLMFactory.Ollama,
'Ollama',

View File

@@ -19,6 +19,8 @@ const {
patchInstanceModel,
deleteInstanceModels,
updateProviderInstance,
aimlapiAuthorizeStart,
aimlapiAuthorizePoll,
} = api;
const methods = {
@@ -94,6 +96,14 @@ const methods = {
url: updateProviderInstance,
method: 'put',
},
aimlapiAuthorizeStart: {
url: aimlapiAuthorizeStart,
method: 'post',
},
aimlapiAuthorizePoll: {
url: aimlapiAuthorizePoll,
method: 'post',
},
} as const;
const llmService = registerNextServer<keyof typeof methods>(methods);

View File

@@ -26,6 +26,9 @@ export default {
// llm model
listAllAddedModels: `${restAPIv1}/models`,
defaultModel: `${restAPIv1}/models/default`,
// AIMLAPI agent-authorization (OAuth device grant) — obtain a key from the provider dialog
aimlapiAuthorizeStart: `${restAPIv1}/llm/aimlapi/authorize/start`,
aimlapiAuthorizePoll: `${restAPIv1}/llm/aimlapi/authorize/poll`,
listProviders: `${restAPIv1}/providers`,
addProvider: `${restAPIv1}/providers`,
addProviderInstance: ({ llm_factory }: { llm_factory: string }) =>

View File

@@ -43,6 +43,7 @@ export const formatNumberWithThousandsSeparator = (numberStr: string) => {
};
const orderFactoryList = [
LLMFactory.AIMLAPI,
LLMFactory.OpenAI,
LLMFactory.Moonshot,
LLMFactory.PPIO,