diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po
index 78c598020ba..d78aef170e4 100644
--- a/apps/locales/en_US/LC_MESSAGES/django.po
+++ b/apps/locales/en_US/LC_MESSAGES/django.po
@@ -3698,6 +3698,21 @@ msgstr ""
msgid "Output the maximum Tokens"
msgstr ""
+#: apps/models_provider/impl/aimlapi_model_provider/credential/image.py:33
+#: apps/models_provider/impl/aimlapi_model_provider/credential/llm.py:34
+msgid ""
+"Specify the maximum number of tokens that the model can generate. Reasoning "
+"models may spend more tokens than this limit on their reasoning process, so "
+"it does not cap the cost of a request"
+msgstr ""
+
+#: apps/models_provider/impl/aimlapi_model_provider/credential/tti.py:24
+#: apps/models_provider/impl/aimlapi_model_provider/credential/tti.py:41
+msgid ""
+"Different image models accept different values, keep auto to let the model "
+"decide"
+msgstr ""
+
#: apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/llm.py:31
msgid "Specify the maximum number of tokens that the model can generate."
msgstr ""
diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po
index 8131866a881..d151e4340a5 100644
--- a/apps/locales/zh_CN/LC_MESSAGES/django.po
+++ b/apps/locales/zh_CN/LC_MESSAGES/django.po
@@ -3715,6 +3715,21 @@ msgstr "较高的数值会使输出更加随机,而较低的数值会使其更
msgid "Output the maximum Tokens"
msgstr "输出最大Token数"
+#: apps/models_provider/impl/aimlapi_model_provider/credential/image.py:33
+#: apps/models_provider/impl/aimlapi_model_provider/credential/llm.py:34
+msgid ""
+"Specify the maximum number of tokens that the model can generate. Reasoning "
+"models may spend more tokens than this limit on their reasoning process, so "
+"it does not cap the cost of a request"
+msgstr "指定模型可以生成的最大 tokens 数。推理模型的思考过程可能会超出该限制,因此它并不能限制单次请求的费用"
+
+#: apps/models_provider/impl/aimlapi_model_provider/credential/tti.py:24
+#: apps/models_provider/impl/aimlapi_model_provider/credential/tti.py:41
+msgid ""
+"Different image models accept different values, keep auto to let the model "
+"decide"
+msgstr "不同的图片模型支持的取值不同,保持 auto 则由模型自行决定"
+
#: apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/llm.py:31
msgid "Specify the maximum number of tokens that the model can generate."
msgstr "指定模型可以生成的最大 tokens 数"
diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po
index 54ca6ac533a..8e0ce622f04 100644
--- a/apps/locales/zh_Hant/LC_MESSAGES/django.po
+++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po
@@ -3715,6 +3715,21 @@ msgstr "較高的數值會使輸出更加隨機,而較低的數值會使其更
msgid "Output the maximum Tokens"
msgstr "輸出最大Token數"
+#: apps/models_provider/impl/aimlapi_model_provider/credential/image.py:33
+#: apps/models_provider/impl/aimlapi_model_provider/credential/llm.py:34
+msgid ""
+"Specify the maximum number of tokens that the model can generate. Reasoning "
+"models may spend more tokens than this limit on their reasoning process, so "
+"it does not cap the cost of a request"
+msgstr "指定模型可以生成的最大 tokens 數。推理模型的思考過程可能會超出該限制,因此它並不能限制單次請求的費用"
+
+#: apps/models_provider/impl/aimlapi_model_provider/credential/tti.py:24
+#: apps/models_provider/impl/aimlapi_model_provider/credential/tti.py:41
+msgid ""
+"Different image models accept different values, keep auto to let the model "
+"decide"
+msgstr "不同的圖片模型支援的取值不同,保持 auto 則由模型自行決定"
+
#: apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/llm.py:31
msgid "Specify the maximum number of tokens that the model can generate."
msgstr "指定模型可以生成的最大 tokens 數"
diff --git a/apps/models_provider/constants/model_provider_constants.py b/apps/models_provider/constants/model_provider_constants.py
index ae749795cb7..f937c46d4b0 100644
--- a/apps/models_provider/constants/model_provider_constants.py
+++ b/apps/models_provider/constants/model_provider_constants.py
@@ -1,6 +1,7 @@
# coding=utf-8
from enum import Enum
+from models_provider.impl.aimlapi_model_provider.aimlapi_model_provider import AIMLAPIModelProvider
from models_provider.impl.aliyun_bai_lian_model_provider.aliyun_bai_lian_model_provider import \
AliyunBaiLianModelProvider
from models_provider.impl.anthropic_model_provider.anthropic_model_provider import AnthropicModelProvider
@@ -27,6 +28,7 @@
class ModelProvideConstants(Enum):
+ model_aimlapi_provider = AIMLAPIModelProvider()
model_azure_provider = AzureModelProvider()
model_wenxin_provider = WenxinModelProvider()
model_ollama_provider = OllamaModelProvider()
diff --git a/apps/models_provider/impl/aimlapi_model_provider/__init__.py b/apps/models_provider/impl/aimlapi_model_provider/__init__.py
new file mode 100644
index 00000000000..ccf28d3c809
--- /dev/null
+++ b/apps/models_provider/impl/aimlapi_model_provider/__init__.py
@@ -0,0 +1,8 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:aimlapi.com
+ @file: __init__.py
+ @date:2026/09/03 10:00
+ @desc:
+"""
diff --git a/apps/models_provider/impl/aimlapi_model_provider/aimlapi_model_provider.py b/apps/models_provider/impl/aimlapi_model_provider/aimlapi_model_provider.py
new file mode 100644
index 00000000000..8dd6beb793e
--- /dev/null
+++ b/apps/models_provider/impl/aimlapi_model_provider/aimlapi_model_provider.py
@@ -0,0 +1,112 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:aimlapi.com
+ @file: aimlapi_model_provider.py
+ @date:2026/09/03 10:00
+ @desc:
+"""
+import os
+
+from common.utils.common import get_file_content
+from maxkb.conf import PROJECT_DIR
+from models_provider.base_model_provider import ModelInfo, ModelTypeConst, ModelInfoManage, IModelProvider, \
+ ModelProvideInfo
+from models_provider.impl.aimlapi_model_provider.credential.embedding import AIMLAPIEmbeddingCredential
+from models_provider.impl.aimlapi_model_provider.credential.image import AIMLAPIImageModelCredential
+from models_provider.impl.aimlapi_model_provider.credential.llm import AIMLAPILLMModelCredential
+from models_provider.impl.aimlapi_model_provider.credential.tti import AIMLAPITextToImageModelCredential
+from models_provider.impl.aimlapi_model_provider.model.embedding import AIMLAPIEmbeddingModel
+from models_provider.impl.aimlapi_model_provider.model.image import AIMLAPIImage
+from models_provider.impl.aimlapi_model_provider.model.llm import AIMLAPIChatModel
+from models_provider.impl.aimlapi_model_provider.model.tti import AIMLAPITextToImage
+
+aimlapi_llm_model_credential = AIMLAPILLMModelCredential()
+aimlapi_image_model_credential = AIMLAPIImageModelCredential()
+aimlapi_embedding_model_credential = AIMLAPIEmbeddingCredential()
+aimlapi_tti_model_credential = AIMLAPITextToImageModelCredential()
+
+# AI/ML API 提供 350 多个对话模型,这里只列出常用的一部分,
+# 其余模型可以在添加模型时直接填写模型名称。
+# 模型 ID 以 https://api.aimlapi.com/v1/models 为准,注意点号写法(如 claude-sonnet-4.5)。
+model_info_list = [
+ ModelInfo('openai/gpt-5-5', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('openai/gpt-5-mini', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('openai/gpt-4.1', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('openai/gpt-4.1-mini', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('openai/gpt-4o', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('openai/gpt-4o-mini', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('anthropic/claude-opus-4.5', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('anthropic/claude-sonnet-4.5', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('anthropic/claude-haiku-4.5', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('google/gemini-2.5-pro', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('google/gemini-2.5-flash', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('deepseek/deepseek-v4-flash', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('alibaba/qwen3-max', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('zhipu/glm-4.6', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('moonshotai/kimi-k2', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('meta-llama/Llama-3.3-70B-Instruct-Turbo', '', ModelTypeConst.LLM, aimlapi_llm_model_credential,
+ AIMLAPIChatModel),
+ ModelInfo('mistralai/mistral-medium-3.1', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+ ModelInfo('x-ai/grok-4-6', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel),
+]
+
+model_info_image_list = [
+ ModelInfo('openai/gpt-4o', '', ModelTypeConst.IMAGE, aimlapi_image_model_credential, AIMLAPIImage),
+ ModelInfo('openai/gpt-4o-mini', '', ModelTypeConst.IMAGE, aimlapi_image_model_credential, AIMLAPIImage),
+ ModelInfo('openai/gpt-4.1', '', ModelTypeConst.IMAGE, aimlapi_image_model_credential, AIMLAPIImage),
+ ModelInfo('google/gemini-2.5-pro', '', ModelTypeConst.IMAGE, aimlapi_image_model_credential, AIMLAPIImage),
+ ModelInfo('google/gemini-2.5-flash', '', ModelTypeConst.IMAGE, aimlapi_image_model_credential, AIMLAPIImage),
+ ModelInfo('anthropic/claude-sonnet-4.5', '', ModelTypeConst.IMAGE, aimlapi_image_model_credential, AIMLAPIImage),
+ ModelInfo('alibaba/qwen3-vl-plus', '', ModelTypeConst.IMAGE, aimlapi_image_model_credential, AIMLAPIImage),
+]
+
+model_info_embedding_list = [
+ ModelInfo('openai/text-embedding-3-small', '', ModelTypeConst.EMBEDDING, aimlapi_embedding_model_credential,
+ AIMLAPIEmbeddingModel),
+ ModelInfo('openai/text-embedding-3-large', '', ModelTypeConst.EMBEDDING, aimlapi_embedding_model_credential,
+ AIMLAPIEmbeddingModel),
+ ModelInfo('openai/text-embedding-ada-002', '', ModelTypeConst.EMBEDDING, aimlapi_embedding_model_credential,
+ AIMLAPIEmbeddingModel),
+ ModelInfo('alibaba/text-embedding-v4', '', ModelTypeConst.EMBEDDING, aimlapi_embedding_model_credential,
+ AIMLAPIEmbeddingModel),
+ ModelInfo('google/text-multilingual-embedding-002', '', ModelTypeConst.EMBEDDING,
+ aimlapi_embedding_model_credential, AIMLAPIEmbeddingModel),
+ ModelInfo('anthropic/voyage-multilingual-2', '', ModelTypeConst.EMBEDDING, aimlapi_embedding_model_credential,
+ AIMLAPIEmbeddingModel),
+]
+
+model_info_tti_list = [
+ ModelInfo('flux/schnell', '', ModelTypeConst.TTI, aimlapi_tti_model_credential, AIMLAPITextToImage),
+ ModelInfo('google/gemini-2.5-flash-image', '', ModelTypeConst.TTI, aimlapi_tti_model_credential,
+ AIMLAPITextToImage),
+ ModelInfo('openai/gpt-image-1', '', ModelTypeConst.TTI, aimlapi_tti_model_credential, AIMLAPITextToImage),
+ ModelInfo('alibaba/qwen-image', '', ModelTypeConst.TTI, aimlapi_tti_model_credential, AIMLAPITextToImage),
+ ModelInfo('bytedance/seedream-v4-text-to-image', '', ModelTypeConst.TTI, aimlapi_tti_model_credential,
+ AIMLAPITextToImage),
+]
+
+model_info_manage = (
+ ModelInfoManage.builder()
+ .append_model_info_list(model_info_list)
+ .append_default_model_info(
+ ModelInfo('openai/gpt-4o-mini', '', ModelTypeConst.LLM, aimlapi_llm_model_credential, AIMLAPIChatModel))
+ .append_model_info_list(model_info_image_list)
+ .append_default_model_info(model_info_image_list[0])
+ .append_model_info_list(model_info_embedding_list)
+ .append_default_model_info(model_info_embedding_list[0])
+ .append_model_info_list(model_info_tti_list)
+ .append_default_model_info(model_info_tti_list[0])
+ .build()
+)
+
+
+class AIMLAPIModelProvider(IModelProvider):
+
+ def get_model_info_manage(self):
+ return model_info_manage
+
+ def get_model_provide_info(self):
+ return ModelProvideInfo(provider='model_aimlapi_provider', name='aimlapi.com', icon=get_file_content(
+ os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'aimlapi_model_provider', 'icon',
+ 'aimlapi_icon_svg')))
diff --git a/apps/models_provider/impl/aimlapi_model_provider/const.py b/apps/models_provider/impl/aimlapi_model_provider/const.py
new file mode 100644
index 00000000000..78a0c2527ff
--- /dev/null
+++ b/apps/models_provider/impl/aimlapi_model_provider/const.py
@@ -0,0 +1,63 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:aimlapi.com
+ @file: const.py
+ @date:2026/09/03 10:00
+ @desc: AI/ML API 供应商的公共常量与请求参数处理
+"""
+from typing import Dict, Optional
+from urllib.parse import urlparse
+
+from models_provider.base_model_provider import MaxKBBaseModel
+
+# AI/ML API 的默认接口地址,兼容 OpenAI 协议
+API_BASE = 'https://api.aimlapi.com/v1'
+
+# 渠道归因请求头:HTTP-Referer / X-Title 标识调用方(即 MaxKB 本身),
+# X-AIMLAPI-* 是 AI/ML API 用于渠道统计的请求头。
+# 该常量是共享的,任何时候都不要就地修改,请使用 get_default_headers 生成新字典。
+ATTRIBUTION_HEADERS = {
+ 'HTTP-Referer': 'https://github.com/1Panel-dev/MaxKB',
+ 'X-Title': 'MaxKB',
+ 'X-AIMLAPI-Partner-ID': 'part_BOQuEVgOgdpgCUqh5u0YkgzL',
+ 'X-AIMLAPI-Source': 'agent/maxkb',
+}
+
+# 只有请求发往 AI/ML API 自己的域名时才携带归因请求头。
+# 用户可以把 API URL 改成其他服务商或中转代理,此时不应把这些请求头带过去。
+ATTRIBUTION_HOSTS = {'api.aimlapi.com'}
+
+
+def is_aimlapi_endpoint(api_base: Optional[str]) -> bool:
+ """
+ 判断接口地址是否指向 AI/ML API
+ @param api_base: 用户填写的 API URL,为空时使用默认地址
+ """
+ if not api_base:
+ return True
+ hostname = urlparse(api_base if '//' in api_base else f'//{api_base}').hostname
+ return (hostname or '').lower() in ATTRIBUTION_HOSTS
+
+
+def get_default_headers(api_base: Optional[str], default_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
+ """
+ 生成请求头,调用方自定义的请求头优先级更高(合并而不是覆盖)
+ @param api_base: API URL
+ @param default_headers: 调用方自定义的请求头
+ @return: 新的请求头字典
+ """
+ if not is_aimlapi_endpoint(api_base):
+ return {**(default_headers or {})}
+ return {**ATTRIBUTION_HEADERS, **(default_headers or {})}
+
+
+def filter_optional_params(model_kwargs: Dict[str, object]) -> Dict[str, object]:
+ """
+ 在 MaxKB 默认过滤逻辑之上再丢弃取值为 None 的参数。
+ AI/ML API 上不同模型对 null 的容忍度不一样:google/gemini-2.5-flash 可以接受,
+ 而 openai/gpt-4o-mini、deepseek/deepseek-chat 等模型在 temperature、top_p、seed、
+ tools 为 null 时直接返回 400。因此未设置的参数必须省略,而不是以 None 发送。
+ """
+ optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs)
+ return {key: value for key, value in optional_params.items() if value is not None}
diff --git a/apps/models_provider/impl/aimlapi_model_provider/credential/embedding.py b/apps/models_provider/impl/aimlapi_model_provider/credential/embedding.py
new file mode 100644
index 00000000000..6e43b48cf86
--- /dev/null
+++ b/apps/models_provider/impl/aimlapi_model_provider/credential/embedding.py
@@ -0,0 +1,54 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:aimlapi.com
+ @file: embedding.py
+ @date:2026/09/03 10:00
+ @desc:
+"""
+from typing import Dict
+
+from django.utils.translation import gettext as _
+
+from common import forms
+from common.exception.app_exception import AppApiException
+from common.forms import BaseForm
+from common.utils.logger import maxkb_logger
+from models_provider.base_model_provider import BaseModelCredential, ValidCode
+from models_provider.impl.aimlapi_model_provider.const import API_BASE
+
+
+class AIMLAPIEmbeddingCredential(BaseForm, BaseModelCredential):
+ def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider,
+ raise_exception=True):
+ model_type_list = provider.get_model_type_list()
+ if not any(list(filter(lambda mt: mt.get('value') == model_type, model_type_list))):
+ raise AppApiException(ValidCode.valid_error.value,
+ _('{model_type} Model type is not supported').format(model_type=model_type))
+
+ for key in ['api_base', 'api_key']:
+ if key not in model_credential:
+ if raise_exception:
+ raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key))
+ else:
+ return False
+ try:
+ model = provider.get_model(model_type, model_name, model_credential)
+ model.embed_query(_('Hello'))
+ except Exception as e:
+ maxkb_logger.error(f'Exception: {e}', exc_info=True)
+ if isinstance(e, AppApiException):
+ raise e
+ if raise_exception:
+ raise AppApiException(ValidCode.valid_error.value,
+ _('Verification failed, please check whether the parameters are correct: {error}').format(
+ error=str(e)))
+ else:
+ return False
+ return True
+
+ def encryption_dict(self, model: Dict[str, object]):
+ return {**model, 'api_key': super().encryption(model.get('api_key', ''))}
+
+ api_base = forms.TextInputField('API URL', required=True, default_value=API_BASE)
+ api_key = forms.PasswordInputField('API Key', required=True)
diff --git a/apps/models_provider/impl/aimlapi_model_provider/credential/image.py b/apps/models_provider/impl/aimlapi_model_provider/credential/image.py
new file mode 100644
index 00000000000..ca9b4afdc24
--- /dev/null
+++ b/apps/models_provider/impl/aimlapi_model_provider/credential/image.py
@@ -0,0 +1,78 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:aimlapi.com
+ @file: image.py
+ @date:2026/09/03 10:00
+ @desc:
+"""
+from typing import Dict
+
+from django.utils.translation import gettext_lazy as _, gettext
+from langchain_core.messages import HumanMessage
+
+from common import forms
+from common.exception.app_exception import AppApiException
+from common.forms import BaseForm, TooltipLabel
+from common.utils.logger import maxkb_logger
+from models_provider.base_model_provider import BaseModelCredential, ValidCode
+from models_provider.impl.aimlapi_model_provider.const import API_BASE
+
+
+class AIMLAPIImageModelParams(BaseForm):
+ temperature = forms.SliderField(TooltipLabel(_('Temperature'),
+ _('Higher values make the output more random, while lower values make it more focused and deterministic')),
+ required=True, default_value=0.7,
+ _min=0.1,
+ _max=1.0,
+ _step=0.01,
+ precision=2)
+
+ max_tokens = forms.SliderField(
+ TooltipLabel(_('Output the maximum Tokens'),
+ _('Specify the maximum number of tokens that the model can generate. Reasoning models may spend more tokens than this limit on their reasoning process, so it does not cap the cost of a request')),
+ required=True, default_value=8192,
+ _min=1,
+ _max=100000,
+ _step=1,
+ precision=0)
+
+
+class AIMLAPIImageModelCredential(BaseForm, BaseModelCredential):
+ api_base = forms.TextInputField('API URL', required=True, default_value=API_BASE)
+ api_key = forms.PasswordInputField('API Key', required=True)
+
+ def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider,
+ raise_exception=False):
+ model_type_list = provider.get_model_type_list()
+ if not any(list(filter(lambda mt: mt.get('value') == model_type, model_type_list))):
+ raise AppApiException(ValidCode.valid_error.value,
+ gettext('{model_type} Model type is not supported').format(model_type=model_type))
+
+ for key in ['api_base', 'api_key']:
+ if key not in model_credential:
+ if raise_exception:
+ raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key))
+ else:
+ return False
+ try:
+ model = provider.get_model(model_type, model_name, model_credential, **model_params)
+ model.stream([HumanMessage(content=[{"type": "text", "text": gettext('Hello')}])])
+ except Exception as e:
+ maxkb_logger.error(f'Exception: {e}', exc_info=True)
+ if isinstance(e, AppApiException):
+ raise e
+ if raise_exception:
+ raise AppApiException(ValidCode.valid_error.value,
+ gettext(
+ 'Verification failed, please check whether the parameters are correct: {error}').format(
+ error=str(e)))
+ else:
+ return False
+ return True
+
+ def encryption_dict(self, model: Dict[str, object]):
+ return {**model, 'api_key': super().encryption(model.get('api_key', ''))}
+
+ def get_model_params_setting_form(self, model_name):
+ return AIMLAPIImageModelParams()
diff --git a/apps/models_provider/impl/aimlapi_model_provider/credential/llm.py b/apps/models_provider/impl/aimlapi_model_provider/credential/llm.py
new file mode 100644
index 00000000000..0c935dd1062
--- /dev/null
+++ b/apps/models_provider/impl/aimlapi_model_provider/credential/llm.py
@@ -0,0 +1,80 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:aimlapi.com
+ @file: llm.py
+ @date:2026/09/03 10:00
+ @desc:
+"""
+from typing import Dict
+
+from django.utils.translation import gettext_lazy as _, gettext
+from langchain_core.messages import HumanMessage
+from openai import BadRequestError
+
+from common import forms
+from common.exception.app_exception import AppApiException
+from common.forms import BaseForm, TooltipLabel
+from common.utils.logger import maxkb_logger
+from models_provider.base_model_provider import BaseModelCredential, ValidCode
+from models_provider.impl.aimlapi_model_provider.const import API_BASE
+
+
+class AIMLAPILLMModelParams(BaseForm):
+ temperature = forms.SliderField(TooltipLabel(_('Temperature'),
+ _('Higher values make the output more random, while lower values make it more focused and deterministic')),
+ required=True, default_value=0.7,
+ _min=0.1,
+ _max=1.0,
+ _step=0.01,
+ precision=2)
+
+ max_tokens = forms.SliderField(
+ TooltipLabel(_('Output the maximum Tokens'),
+ _('Specify the maximum number of tokens that the model can generate. Reasoning models may spend more tokens than this limit on their reasoning process, so it does not cap the cost of a request')),
+ required=True, default_value=8192,
+ _min=1,
+ _max=100000,
+ _step=1,
+ precision=0)
+
+
+class AIMLAPILLMModelCredential(BaseForm, BaseModelCredential):
+
+ def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider,
+ raise_exception=False):
+ model_type_list = provider.get_model_type_list()
+ if not any(list(filter(lambda mt: mt.get('value') == model_type, model_type_list))):
+ raise AppApiException(ValidCode.valid_error.value,
+ gettext('{model_type} Model type is not supported').format(model_type=model_type))
+
+ for key in ['api_base', 'api_key']:
+ if key not in model_credential:
+ if raise_exception:
+ raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key))
+ else:
+ return False
+ try:
+ model = provider.get_model(model_type, model_name, model_credential, **model_params)
+ model.invoke([HumanMessage(content=gettext('Hello'))])
+ except Exception as e:
+ maxkb_logger.error(f'Exception: {e}', exc_info=True)
+ if isinstance(e, AppApiException) or isinstance(e, BadRequestError):
+ raise e
+ if raise_exception:
+ raise AppApiException(ValidCode.valid_error.value,
+ gettext(
+ 'Verification failed, please check whether the parameters are correct: {error}').format(
+ error=str(e)))
+ else:
+ return False
+ return True
+
+ def encryption_dict(self, model: Dict[str, object]):
+ return {**model, 'api_key': super().encryption(model.get('api_key', ''))}
+
+ api_base = forms.TextInputField('API URL', required=True, default_value=API_BASE)
+ api_key = forms.PasswordInputField('API Key', required=True)
+
+ def get_model_params_setting_form(self, model_name):
+ return AIMLAPILLMModelParams()
diff --git a/apps/models_provider/impl/aimlapi_model_provider/credential/tti.py b/apps/models_provider/impl/aimlapi_model_provider/credential/tti.py
new file mode 100644
index 00000000000..c345ef8e655
--- /dev/null
+++ b/apps/models_provider/impl/aimlapi_model_provider/credential/tti.py
@@ -0,0 +1,103 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:aimlapi.com
+ @file: tti.py
+ @date:2026/09/03 10:00
+ @desc:
+"""
+from typing import Dict
+
+from django.utils.translation import gettext_lazy as _, gettext
+
+from common import forms
+from common.exception.app_exception import AppApiException
+from common.forms import BaseForm, TooltipLabel
+from common.utils.logger import maxkb_logger
+from models_provider.base_model_provider import BaseModelCredential, ValidCode
+from models_provider.impl.aimlapi_model_provider.const import API_BASE
+
+
+class AIMLAPITTIModelParams(BaseForm):
+ size = forms.SingleSelect(
+ TooltipLabel(_('Image size'),
+ _('Different image models accept different values, keep auto to let the model decide')),
+ required=True,
+ default_value='auto',
+ option_list=[
+ {'value': 'auto', 'label': 'auto'},
+ {'value': '1024x1024', 'label': '1024x1024'},
+ {'value': '1024x1536', 'label': '1024x1536'},
+ {'value': '1536x1024', 'label': '1536x1024'},
+ {'value': '1024x1792', 'label': '1024x1792'},
+ {'value': '1792x1024', 'label': '1792x1024'},
+ ],
+ text_field='label',
+ value_field='value'
+ )
+
+ quality = forms.SingleSelect(
+ TooltipLabel(_('Picture quality'),
+ _('Different image models accept different values, keep auto to let the model decide')),
+ required=True,
+ default_value='auto',
+ option_list=[
+ {'value': 'auto', 'label': 'auto'},
+ {'value': 'standard', 'label': 'standard'},
+ {'value': 'hd', 'label': 'hd'},
+ {'value': 'low', 'label': 'low'},
+ {'value': 'medium', 'label': 'medium'},
+ {'value': 'high', 'label': 'high'},
+ ],
+ text_field='label',
+ value_field='value'
+ )
+
+ n = forms.SliderField(
+ TooltipLabel(_('Number of pictures'),
+ _('1 as default')),
+ required=True, default_value=1,
+ _min=1,
+ _max=10,
+ _step=1,
+ precision=0)
+
+
+class AIMLAPITextToImageModelCredential(BaseForm, BaseModelCredential):
+ api_base = forms.TextInputField('API URL', required=True, default_value=API_BASE)
+ api_key = forms.PasswordInputField('API Key', required=True)
+
+ def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider,
+ raise_exception=False):
+ model_type_list = provider.get_model_type_list()
+ if not any(list(filter(lambda mt: mt.get('value') == model_type, model_type_list))):
+ raise AppApiException(ValidCode.valid_error.value,
+ gettext('{model_type} Model type is not supported').format(model_type=model_type))
+
+ for key in ['api_base', 'api_key']:
+ if key not in model_credential:
+ if raise_exception:
+ raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key))
+ else:
+ return False
+ try:
+ model = provider.get_model(model_type, model_name, model_credential, **model_params)
+ model.check_auth()
+ except Exception as e:
+ maxkb_logger.error(f'Exception: {e}', exc_info=True)
+ if isinstance(e, AppApiException):
+ raise e
+ if raise_exception:
+ raise AppApiException(ValidCode.valid_error.value,
+ gettext(
+ 'Verification failed, please check whether the parameters are correct: {error}').format(
+ error=str(e)))
+ else:
+ return False
+ return True
+
+ def encryption_dict(self, model: Dict[str, object]):
+ return {**model, 'api_key': super().encryption(model.get('api_key', ''))}
+
+ def get_model_params_setting_form(self, model_name):
+ return AIMLAPITTIModelParams()
diff --git a/apps/models_provider/impl/aimlapi_model_provider/icon/aimlapi_icon_svg b/apps/models_provider/impl/aimlapi_model_provider/icon/aimlapi_icon_svg
new file mode 100644
index 00000000000..723f89e7403
--- /dev/null
+++ b/apps/models_provider/impl/aimlapi_model_provider/icon/aimlapi_icon_svg
@@ -0,0 +1 @@
+
diff --git a/apps/models_provider/impl/aimlapi_model_provider/model/embedding.py b/apps/models_provider/impl/aimlapi_model_provider/model/embedding.py
new file mode 100644
index 00000000000..4ddfde75551
--- /dev/null
+++ b/apps/models_provider/impl/aimlapi_model_provider/model/embedding.py
@@ -0,0 +1,48 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:aimlapi.com
+ @file: embedding.py
+ @date:2026/09/03 10:00
+ @desc:
+"""
+from typing import Dict, List
+
+import openai
+
+from models_provider.base_model_provider import MaxKBBaseModel
+from models_provider.impl.aimlapi_model_provider.const import API_BASE, filter_optional_params, get_default_headers
+
+
+class AIMLAPIEmbeddingModel(MaxKBBaseModel):
+ model_name: str
+ optional_params: dict
+
+ def __init__(self, api_key, base_url, model_name: str, optional_params: dict):
+ # 这里直接使用 openai SDK 下发字符串,而不是 langchain 的 OpenAIEmbeddings:
+ # 后者会先把文本切成 token id 数组再发送,AI/ML API 的 /v1/embeddings 只接受字符串,
+ # 收到 token id 数组会返回 400(details[].path = input)。
+ self.client = openai.OpenAI(api_key=api_key, base_url=base_url,
+ default_headers=get_default_headers(base_url)).embeddings
+ self.model_name = model_name
+ self.optional_params = optional_params
+
+ def is_cache_model(self):
+ return False
+
+ @staticmethod
+ def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs):
+ return AIMLAPIEmbeddingModel(
+ api_key=model_credential.get('api_key'),
+ model_name=model_name,
+ base_url=model_credential.get('api_base') or API_BASE,
+ optional_params=filter_optional_params(model_kwargs)
+ )
+
+ def embed_query(self, text: str):
+ return self.embed_documents([text])[0]
+
+ def embed_documents(self, texts: List[str], chunk_size: int | None = None) -> List[List[float]]:
+ res = self.client.create(input=texts, model=self.model_name, encoding_format="float",
+ **self.optional_params)
+ return [e.embedding for e in res.data]
diff --git a/apps/models_provider/impl/aimlapi_model_provider/model/image.py b/apps/models_provider/impl/aimlapi_model_provider/model/image.py
new file mode 100644
index 00000000000..8eb90629daf
--- /dev/null
+++ b/apps/models_provider/impl/aimlapi_model_provider/model/image.py
@@ -0,0 +1,35 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:aimlapi.com
+ @file: image.py
+ @date:2026/09/03 10:00
+ @desc:
+"""
+from typing import Dict
+
+from models_provider.base_model_provider import MaxKBBaseModel
+from models_provider.impl.aimlapi_model_provider.const import API_BASE, filter_optional_params, get_default_headers
+from models_provider.impl.base_chat_open_ai import BaseChatOpenAI
+
+
+class AIMLAPIImage(MaxKBBaseModel, BaseChatOpenAI):
+
+ @staticmethod
+ def is_cache_model():
+ return False
+
+ @staticmethod
+ def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs):
+ api_base = model_credential.get('api_base') or API_BASE
+ optional_params = filter_optional_params(model_kwargs)
+ default_headers = optional_params.pop('default_headers', None)
+ return AIMLAPIImage(
+ model=model_name,
+ openai_api_base=api_base,
+ openai_api_key=model_credential.get('api_key'),
+ default_headers=get_default_headers(api_base, default_headers),
+ streaming=True,
+ stream_usage=True,
+ **optional_params,
+ )
diff --git a/apps/models_provider/impl/aimlapi_model_provider/model/llm.py b/apps/models_provider/impl/aimlapi_model_provider/model/llm.py
new file mode 100644
index 00000000000..8c36417f0b2
--- /dev/null
+++ b/apps/models_provider/impl/aimlapi_model_provider/model/llm.py
@@ -0,0 +1,34 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:aimlapi.com
+ @file: llm.py
+ @date:2026/09/03 10:00
+ @desc:
+"""
+from typing import Dict
+
+from models_provider.base_model_provider import MaxKBBaseModel
+from models_provider.impl.aimlapi_model_provider.const import API_BASE, filter_optional_params, get_default_headers
+from models_provider.impl.base_chat_open_ai import BaseChatOpenAI
+
+
+class AIMLAPIChatModel(MaxKBBaseModel, BaseChatOpenAI):
+
+ @staticmethod
+ def is_cache_model():
+ return False
+
+ @staticmethod
+ def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs):
+ api_base = model_credential.get('api_base') or API_BASE
+ optional_params = filter_optional_params(model_kwargs)
+ default_headers = optional_params.pop('default_headers', None)
+ return AIMLAPIChatModel(
+ model=model_name,
+ openai_api_base=api_base,
+ openai_api_key=model_credential.get('api_key'),
+ default_headers=get_default_headers(api_base, default_headers),
+ streaming=model_kwargs.get('streaming', True),
+ **optional_params,
+ )
diff --git a/apps/models_provider/impl/aimlapi_model_provider/model/tti.py b/apps/models_provider/impl/aimlapi_model_provider/model/tti.py
new file mode 100644
index 00000000000..19223c649e7
--- /dev/null
+++ b/apps/models_provider/impl/aimlapi_model_provider/model/tti.py
@@ -0,0 +1,85 @@
+# coding=utf-8
+"""
+ @project: MaxKB
+ @Author:aimlapi.com
+ @file: tti.py
+ @date:2026/09/03 10:00
+ @desc:
+"""
+from typing import Dict
+
+from openai import NotFoundError, OpenAI
+
+from models_provider.base_model_provider import MaxKBBaseModel
+from models_provider.impl.aimlapi_model_provider.const import API_BASE, get_default_headers
+from models_provider.impl.base_tti import BaseTextToImage
+
+# 表单中选择“自动”表示该参数不下发,由模型自己决定
+AUTO_VALUE = 'auto'
+# 仅用于校验 API Key 的占位模型名,不会真正生成图片
+CREDENTIAL_CHECK_MODEL = 'maxkb-credential-check'
+
+
+class AIMLAPITextToImage(MaxKBBaseModel, BaseTextToImage):
+ api_base: str
+ api_key: str
+ model: str
+ params: dict
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.api_key = kwargs.get('api_key')
+ self.api_base = kwargs.get('api_base')
+ self.model = kwargs.get('model')
+ self.params = kwargs.get('params')
+
+ @staticmethod
+ def is_cache_model():
+ return False
+
+ @staticmethod
+ def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs):
+ # 未设置的参数一律省略,不下发 None。AI/ML API 上不同图片模型接受的取值不同,
+ # 例如 openai/gpt-image-1 的 quality 只接受 low/medium/high,
+ # 下发 standard 或 null 都会返回 400。
+ params = {}
+ for key, value in model_kwargs.items():
+ if key in ['model_id', 'use_local', 'streaming']:
+ continue
+ if value is None or value == AUTO_VALUE or value == '':
+ continue
+ params[key] = value
+ params.setdefault('n', 1)
+ return AIMLAPITextToImage(
+ model=model_name,
+ api_base=model_credential.get('api_base') or API_BASE,
+ api_key=model_credential.get('api_key'),
+ params=params,
+ )
+
+ def get_client(self):
+ return OpenAI(api_key=self.api_key, base_url=self.api_base,
+ default_headers=get_default_headers(self.api_base))
+
+ def check_auth(self):
+ # AI/ML API 的 GET /v1/models 是公开接口,任意 Key 都返回 200,因此不能用它校验 Key。
+ # 这里向图片接口发送一个模型名不存在的请求:Key 无效返回 401,Key 有效返回 404,
+ # 既能真正校验 Key,又不会产生任何生成费用。
+ try:
+ self.get_client().images.generate(model=CREDENTIAL_CHECK_MODEL, prompt='ping', n=1)
+ except NotFoundError:
+ return True
+ return True
+
+ def generate_image(self, prompt: str, negative_prompt: str = None):
+ res = self.get_client().images.generate(model=self.model, prompt=prompt, **self.params)
+ file_urls = []
+ try:
+ for content in res.data:
+ if content.url:
+ file_urls.append(content.url)
+ elif content.b64_json:
+ file_urls.append(content.b64_json)
+ return file_urls
+ except Exception as e:
+ raise RuntimeError(f"AIMLAPITextToImage generate_image error: {e}") from e
diff --git a/apps/models_provider/tests.py b/apps/models_provider/tests.py
index 44e7258f159..3bc8e10f48b 100644
--- a/apps/models_provider/tests.py
+++ b/apps/models_provider/tests.py
@@ -3,6 +3,9 @@
from django.test import SimpleTestCase
+from models_provider.impl.aimlapi_model_provider.const import ATTRIBUTION_HEADERS, get_default_headers
+from models_provider.impl.aimlapi_model_provider.model.llm import AIMLAPIChatModel
+from models_provider.impl.aimlapi_model_provider.model.tti import AIMLAPITextToImage
from models_provider.impl.vllm_model_provider.model.whisper_sst import VllmWhisperSpeechToText
@@ -24,3 +27,63 @@ def test_normalizes_trailing_slash_in_v1_base_url(self, openai_mock):
base_url='https://vllm.example/v1',
)
self.assertEqual(result, 'transcript')
+
+
+class AIMLAPIAttributionTest(SimpleTestCase):
+ """AI/ML API 渠道归因请求头:格式错误的 partner id 不会报错,只会静默丢失归因,所以必须有测试"""
+
+ credential = {'api_key': 'test-key', 'api_base': 'https://api.aimlapi.com/v1'}
+
+ def test_partner_id_matches_gateway_pattern(self):
+ self.assertRegex(ATTRIBUTION_HEADERS['X-AIMLAPI-Partner-ID'], r'^part_[A-Za-z0-9]{1,64}$')
+ self.assertRegex(ATTRIBUTION_HEADERS['X-AIMLAPI-Source'], r'^(web|agent|mcp)/[a-z0-9-]{1,32}$')
+
+ def test_headers_are_only_sent_to_aimlapi(self):
+ headers = get_default_headers('https://api.aimlapi.com/v1')
+ self.assertEqual(sorted(headers), ['HTTP-Referer', 'X-AIMLAPI-Partner-ID', 'X-AIMLAPI-Source', 'X-Title'])
+ # API URL 指向其他服务商或中转代理时不带归因请求头
+ for api_base in ['https://api.openai.com/v1', 'https://aimlapi.com.example.com/v1', 'http://127.0.0.1:8000/v1']:
+ self.assertEqual(get_default_headers(api_base), {}, api_base)
+
+ def test_caller_headers_win_and_constant_is_not_mutated(self):
+ headers = get_default_headers('https://api.aimlapi.com/v1', {'X-Title': 'Custom', 'X-Extra': '1'})
+ self.assertEqual(headers['X-Title'], 'Custom')
+ self.assertEqual(headers['X-Extra'], '1')
+ self.assertEqual(headers['X-AIMLAPI-Partner-ID'], ATTRIBUTION_HEADERS['X-AIMLAPI-Partner-ID'])
+ self.assertEqual(ATTRIBUTION_HEADERS['X-Title'], 'MaxKB')
+ self.assertNotIn('X-Extra', ATTRIBUTION_HEADERS)
+
+ def test_chat_model_sends_attribution_headers(self):
+ model = AIMLAPIChatModel.new_instance('LLM', 'openai/gpt-4o-mini', self.credential)
+ self.assertEqual(model.default_headers['X-AIMLAPI-Partner-ID'],
+ ATTRIBUTION_HEADERS['X-AIMLAPI-Partner-ID'])
+
+ def test_chat_model_omits_unset_params_instead_of_sending_null(self):
+ # AI/ML API 上部分模型(openai/gpt-4o-mini、deepseek/deepseek-chat 等)
+ # 在 temperature/top_p/seed/tools 为 null 时返回 400,未设置的参数必须省略
+ model = AIMLAPIChatModel.new_instance(
+ 'LLM', 'openai/gpt-4o-mini', self.credential,
+ model_id='1', streaming=True,
+ temperature=None, max_tokens=None, top_p=None, seed=None, tools=None,
+ )
+ self.assertEqual(model.model_kwargs, {})
+ params = model._default_params
+ for key in ['temperature', 'max_tokens', 'max_completion_tokens', 'top_p', 'seed', 'tools']:
+ self.assertNotIn(key, params)
+ self.assertEqual([key for key, value in params.items() if value is None], [])
+
+ def test_chat_model_keeps_params_that_are_set(self):
+ model = AIMLAPIChatModel.new_instance(
+ 'LLM', 'openai/gpt-4o-mini', self.credential, model_id='1', temperature=0.7, max_tokens=8192)
+ params = model._default_params
+ self.assertEqual(params['temperature'], 0.7)
+ # langchain-openai 会把 max_tokens 转换成 max_completion_tokens 下发
+ self.assertEqual(params['max_completion_tokens'], 8192)
+
+ def test_text_to_image_omits_auto_params(self):
+ model = AIMLAPITextToImage.new_instance(
+ 'TTI', 'openai/gpt-image-1', self.credential, model_id='1', size='auto', quality='auto', n=1)
+ self.assertEqual(model.params, {'n': 1})
+ model = AIMLAPITextToImage.new_instance(
+ 'TTI', 'flux/schnell', self.credential, model_id='1', size='1024x1024', quality=None, n=2)
+ self.assertEqual(model.params, {'size': '1024x1024', 'n': 2})
diff --git a/ui/src/components/dynamics-form/items/model/provider-data.ts b/ui/src/components/dynamics-form/items/model/provider-data.ts
index 5e7b39b82c6..82169b2b2ae 100644
--- a/ui/src/components/dynamics-form/items/model/provider-data.ts
+++ b/ui/src/components/dynamics-form/items/model/provider-data.ts
@@ -1,4 +1,9 @@
export const providerList = [
+ {
+ "provider": "model_aimlapi_provider",
+ "name": "aimlapi.com",
+ "icon": ""
+ },
{
"provider": "model_azure_provider",
"name": "Azure OpenAI",