From f793672a9d9febe1717a1b44c0a732a6bf154909 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 07:50:41 +0500 Subject: [PATCH 1/3] feat(provider): add aimlapi.com model provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MaxKB 目前只能通过 OpenAI 供应商填写自定义 api_base 的方式访问 AI/ML API,模型需要手工输入,也没有独立的图标和渠道标识。 这里按照 regolo 的目录结构新增一个独立的供应商,支持 LLM、视觉、 向量和文生图四种模型类型。 几个和 AI/ML API 相关的实现细节: - 请求参数按“未设置就不下发”的方式构造。AI/ML API 上不同模型对 null 的容忍度不同:google/gemini-2.5-flash 可以接受,而 openai/gpt-4o-mini、 deepseek/deepseek-chat 在 temperature、top_p、seed、tools 为 null 时 直接返回 400。tools 尤其危险:Agent 第一轮成功、第二轮清空工具时失败。 已补充回归测试。 - 归因请求头按请求地址生效,只有 API URL 指向 api.aimlapi.com 时才携带, 用户改成其他服务商或中转代理时不会被带过去;合并而不是覆盖调用方的请求头。 - 向量模型直接用 openai SDK 下发字符串,而不是 langchain 的 OpenAIEmbeddings: 后者会先把文本转成 token id 数组,AI/ML API 的 /v1/embeddings 只接受字符串。 - 文生图的 size/quality 默认值为 auto,即不下发该参数。不同图片模型接受的 枚举值不同(openai/gpt-image-1 的 quality 只接受 low/medium/high, 下发 standard 会 400)。 - 文生图的凭据校验没有使用 GET /v1/models:该接口是公开的,任意 Key 都 返回 200,无法用来校验 Key。 - 输出最大 Token 数的提示文案补充说明:推理模型的思考过程可能超出该限制, 它并不能限制单次请求的费用。 --- apps/locales/en_US/LC_MESSAGES/django.po | 15 +++ apps/locales/zh_CN/LC_MESSAGES/django.po | 15 +++ apps/locales/zh_Hant/LC_MESSAGES/django.po | 15 +++ .../constants/model_provider_constants.py | 2 + .../impl/aimlapi_model_provider/__init__.py | 8 ++ .../aimlapi_model_provider.py | 112 ++++++++++++++++++ .../impl/aimlapi_model_provider/const.py | 63 ++++++++++ .../credential/embedding.py | 54 +++++++++ .../credential/image.py | 78 ++++++++++++ .../aimlapi_model_provider/credential/llm.py | 80 +++++++++++++ .../aimlapi_model_provider/credential/tti.py | 103 ++++++++++++++++ .../icon/aimlapi_icon_svg | 1 + .../aimlapi_model_provider/model/embedding.py | 48 ++++++++ .../aimlapi_model_provider/model/image.py | 35 ++++++ .../impl/aimlapi_model_provider/model/llm.py | 34 ++++++ .../impl/aimlapi_model_provider/model/tti.py | 85 +++++++++++++ apps/models_provider/tests.py | 63 ++++++++++ .../items/model/provider-data.ts | 5 + 18 files changed, 816 insertions(+) create mode 100644 apps/models_provider/impl/aimlapi_model_provider/__init__.py create mode 100644 apps/models_provider/impl/aimlapi_model_provider/aimlapi_model_provider.py create mode 100644 apps/models_provider/impl/aimlapi_model_provider/const.py create mode 100644 apps/models_provider/impl/aimlapi_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/aimlapi_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/aimlapi_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/aimlapi_model_provider/credential/tti.py create mode 100644 apps/models_provider/impl/aimlapi_model_provider/icon/aimlapi_icon_svg create mode 100644 apps/models_provider/impl/aimlapi_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/aimlapi_model_provider/model/image.py create mode 100644 apps/models_provider/impl/aimlapi_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/aimlapi_model_provider/model/tti.py 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..95bc0caf162 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 @@ -48,3 +49,4 @@ class ModelProvideConstants(Enum): model_siliconCloud_provider = SiliconCloudModelProvider() model_regolo_provider = RegoloModelProvider() model_minimax_provider = MiniMaxModelProvider() + model_aimlapi_provider = AIMLAPIModelProvider() 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..01fcf49259d --- /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_maxkb', + '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..a8a00353377 100644 --- a/ui/src/components/dynamics-form/items/model/provider-data.ts +++ b/ui/src/components/dynamics-form/items/model/provider-data.ts @@ -108,5 +108,10 @@ export const providerList = [ "provider": "model_minimax_provider", "name": "MiniMax", "icon": "" + }, + { + "provider": "model_aimlapi_provider", + "name": "aimlapi.com", + "icon": "" } ] From 5a55282978771eba7ec4128cd13a227459aca074 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 07:52:08 +0500 Subject: [PATCH 2/3] =?UTF-8?q?chore(aimlapi):=20fork-only=20placement=20?= =?UTF-8?q?=E2=80=94=20do=20not=20send=20upstream?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 供应商列表(后端 ModelProvideConstants 枚举、前端 providerList)都是 手工排序的,这里把 aimlapi.com 放到第一位。MaxKB 的供应商没有 “推荐”标记之类的机制,因此没有新增任何标记,只调整了顺序。 这个提交只在我们自己的 fork 中保留,向上游提交时应当去掉。 --- .../constants/model_provider_constants.py | 2 +- .../dynamics-form/items/model/provider-data.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/models_provider/constants/model_provider_constants.py b/apps/models_provider/constants/model_provider_constants.py index 95bc0caf162..f937c46d4b0 100644 --- a/apps/models_provider/constants/model_provider_constants.py +++ b/apps/models_provider/constants/model_provider_constants.py @@ -28,6 +28,7 @@ class ModelProvideConstants(Enum): + model_aimlapi_provider = AIMLAPIModelProvider() model_azure_provider = AzureModelProvider() model_wenxin_provider = WenxinModelProvider() model_ollama_provider = OllamaModelProvider() @@ -49,4 +50,3 @@ class ModelProvideConstants(Enum): model_siliconCloud_provider = SiliconCloudModelProvider() model_regolo_provider = RegoloModelProvider() model_minimax_provider = MiniMaxModelProvider() - model_aimlapi_provider = AIMLAPIModelProvider() 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 a8a00353377..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", @@ -108,10 +113,5 @@ export const providerList = [ "provider": "model_minimax_provider", "name": "MiniMax", "icon": "" - }, - { - "provider": "model_aimlapi_provider", - "name": "aimlapi.com", - "icon": "" } ] From 4c3d6b7f8e93ab7f1c83974cbe8e248d7f8074a9 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 18:13:37 +0500 Subject: [PATCH 3/3] fix(aimlapi): use the registered partner id The placeholder part_maxkb was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_BOQuEVgOgdpgCUqh5u0YkgzL. A wrong or unknown partner id is accepted with a 200 and silently not attributed, so this would not have surfaced at runtime. --- apps/models_provider/impl/aimlapi_model_provider/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/models_provider/impl/aimlapi_model_provider/const.py b/apps/models_provider/impl/aimlapi_model_provider/const.py index 01fcf49259d..78a0c2527ff 100644 --- a/apps/models_provider/impl/aimlapi_model_provider/const.py +++ b/apps/models_provider/impl/aimlapi_model_provider/const.py @@ -20,7 +20,7 @@ ATTRIBUTION_HEADERS = { 'HTTP-Referer': 'https://github.com/1Panel-dev/MaxKB', 'X-Title': 'MaxKB', - 'X-AIMLAPI-Partner-ID': 'part_maxkb', + 'X-AIMLAPI-Partner-ID': 'part_BOQuEVgOgdpgCUqh5u0YkgzL', 'X-AIMLAPI-Source': 'agent/maxkb', }