From 2b26d76214d924068583ae5499d8ecfe482fb492 Mon Sep 17 00:00:00 2001 From: Lookoff-AIMLAPI Date: Thu, 3 Sep 2026 04:33:16 +0500 Subject: [PATCH 1/3] feat(plugin-ai): add aimlapi.com as an LLM provider Admins can already reach AI/ML API through `openai-completions` with a custom base URL, but that route leaves the service unnamed in the provider list, offers no model catalog, and sends no attribution. A first-class entry gives the provider its own name and description, loads the account's Chat Completions catalog, and lets AI/ML API attribute the traffic to NocoBase. The provider follows the shengsuanyun shape (LangChain `ChatOpenAI` over the OpenAI-compatible Chat Completions API, plus a `listModels` override) and the orcarouter shape for attribution headers. `HTTP-Referer` and `X-Title` identify NocoBase as the calling application and stay overridable per service; the `X-AIMLAPI-*` pair identifies the integration. All four are scoped to the AI/ML API origin, because `baseURL` is user-configurable and attribution must not ride a request to another vendor or to a proxy that merely fronts the same API. No model ids are hardcoded: the catalog is fetched at configuration time and narrowed to entries the Chat Completions client can actually drive, so the list cannot rot. --- .../__tests__/llm-providers.test.tsx | 5 + .../src/client-v2/llm-providers/forms.tsx | 26 +++ .../src/client-v2/llm-providers/index.ts | 7 + .../src/client-v2/pages/LLMServicesPage.tsx | 1 + .../@nocobase/plugin-ai/src/locale/en-US.json | 1 + .../@nocobase/plugin-ai/src/locale/zh-CN.json | 1 + .../llm-providers/__tests__/aimlapi.test.ts | 167 ++++++++++++++++++ .../src/server/llm-providers/aimlapi.ts | 152 ++++++++++++++++ .../@nocobase/plugin-ai/src/server/plugin.ts | 2 + 9 files changed, 362 insertions(+) create mode 100644 packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/__tests__/aimlapi.test.ts create mode 100644 packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/aimlapi.ts diff --git a/packages/plugins/@nocobase/plugin-ai/src/client-v2/__tests__/llm-providers.test.tsx b/packages/plugins/@nocobase/plugin-ai/src/client-v2/__tests__/llm-providers.test.tsx index 95f658eee3c51..052982a4081b1 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client-v2/__tests__/llm-providers.test.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client-v2/__tests__/llm-providers.test.tsx @@ -11,6 +11,7 @@ import { describe, expect, it } from 'vitest'; import { createMockClient } from '@nocobase/client-v2'; import PluginAIClientV2 from '../plugin'; import { + aimlapiProviderOptions, builtinLLMProviderOptions, deepseekProviderOptions, getBuiltinLLMProviderModelOptionFields, @@ -20,6 +21,7 @@ import { shengsuanyunProviderOptions, } from '../llm-providers'; import { + AimlapiProviderSettingsForm, EmptyProviderSettingsForm, OrcaRouterProviderSettingsForm, ProviderSettingsForm, @@ -40,6 +42,7 @@ const V1_REGISTERED_PROVIDERS = [ 'mistral', 'orcarouter', 'shengsuanyun', + 'aimlapi', ]; describe('plugin-ai client-v2 LLM providers', () => { @@ -59,6 +62,7 @@ describe('plugin-ai client-v2 LLM providers', () => { expect(plugin.aiManager.llmProviders.get('ollama')).toBe(ollamaProviderOptions); expect(plugin.aiManager.llmProviders.get('orcarouter')).toBe(orcarouterProviderOptions); expect(plugin.aiManager.llmProviders.get('shengsuanyun')).toBe(shengsuanyunProviderOptions); + expect(plugin.aiManager.llmProviders.get('aimlapi')).toBe(aimlapiProviderOptions); }); it('uses v2 provider settings components without v1 schema forms', () => { @@ -66,6 +70,7 @@ describe('plugin-ai client-v2 LLM providers', () => { expect(ollamaProviderOptions.components.ProviderSettingsForm).toBe(EmptyProviderSettingsForm); expect(orcarouterProviderOptions.components.ProviderSettingsForm).toBe(OrcaRouterProviderSettingsForm); expect(shengsuanyunProviderOptions.components.ProviderSettingsForm).toBe(ShengSuanYunProviderSettingsForm); + expect(aimlapiProviderOptions.components.ProviderSettingsForm).toBe(AimlapiProviderSettingsForm); expect(shengsuanyunProviderOptions.components.ModelSettingsForm).toBeDefined(); expect(openaiResponsesProviderOptions.components.ModelSettingsForm).toBeDefined(); }); diff --git a/packages/plugins/@nocobase/plugin-ai/src/client-v2/llm-providers/forms.tsx b/packages/plugins/@nocobase/plugin-ai/src/client-v2/llm-providers/forms.tsx index 302068520378a..0b4f10bceac2f 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client-v2/llm-providers/forms.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client-v2/llm-providers/forms.tsx @@ -89,6 +89,32 @@ export const OrcaRouterProviderSettingsForm: React.FC = () => { ); }; +export const AimlapiProviderSettingsForm: React.FC = () => { + const t = useT(); + + return ( + <> + + + + + + + + + + + ); +}; + export const ShengSuanYunProviderSettingsForm: React.FC = () => { const t = useT(); diff --git a/packages/plugins/@nocobase/plugin-ai/src/client-v2/llm-providers/index.ts b/packages/plugins/@nocobase/plugin-ai/src/client-v2/llm-providers/index.ts index ccf6d586a28a4..f650d2f95ef8f 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client-v2/llm-providers/index.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/client-v2/llm-providers/index.ts @@ -9,6 +9,7 @@ import type { LLMProviderOptions } from '../manager/ai-manager'; import { + AimlapiProviderSettingsForm, createModelSettingsForm, deepSeekCompletionFields, EmptyProviderSettingsForm, @@ -83,6 +84,10 @@ export const shengsuanyunProviderOptions = createProviderOptions( }, ); +export const aimlapiProviderOptions = createProviderOptions(createModelSettingsForm(openAICompletionFields), { + ProviderSettingsForm: AimlapiProviderSettingsForm, +}); + export const ollamaProviderOptions = createProviderOptions(createModelSettingsForm(ollamaCompletionFields), { ProviderSettingsForm: EmptyProviderSettingsForm, }); @@ -101,6 +106,7 @@ export const builtinLLMProviderOptions: Array<[string, LLMProviderOptions]> = [ ['mistral', mistralProviderOptions], ['orcarouter', orcarouterProviderOptions], ['shengsuanyun', shengsuanyunProviderOptions], + ['aimlapi', aimlapiProviderOptions], ]; const builtinLLMProviderModelOptionFields = new Map([ @@ -117,6 +123,7 @@ const builtinLLMProviderModelOptionFields = new Map([ ['mistral', mistralCompletionFields], ['orcarouter', orcaRouterCompletionFields], ['shengsuanyun', shengSuanYunCompletionFields], + ['aimlapi', openAICompletionFields], ]); export const getBuiltinLLMProviderModelOptionFields = (provider?: string): OptionField[] => diff --git a/packages/plugins/@nocobase/plugin-ai/src/client-v2/pages/LLMServicesPage.tsx b/packages/plugins/@nocobase/plugin-ai/src/client-v2/pages/LLMServicesPage.tsx index 738e043b50c08..f05fbc029e87a 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client-v2/pages/LLMServicesPage.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client-v2/pages/LLMServicesPage.tsx @@ -374,6 +374,7 @@ const getProviderDescription = (provider: string, t: ReturnType) => mistral: 'Mistral models', orcarouter: 'OrcaRouter (model routing gateway)', shengsuanyun: '300+ latest mainstream models across leading model families', + aimlapi: 'Models from OpenAI, Anthropic, Google, DeepSeek and others through one OpenAI-compatible API', }; return descriptions[provider] ? t(descriptions[provider]) : ''; }; diff --git a/packages/plugins/@nocobase/plugin-ai/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-ai/src/locale/en-US.json index 7ec968bccf652..6437eee366cd6 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/locale/en-US.json +++ b/packages/plugins/@nocobase/plugin-ai/src/locale/en-US.json @@ -196,6 +196,7 @@ "Messages": "Messages", "Mistral models": "Mistral models", "300+ latest mainstream models across leading model families": "300+ latest mainstream models across leading model families", + "Models from OpenAI, Anthropic, Google, DeepSeek and others through one OpenAI-compatible API": "Models from OpenAI, Anthropic, Google, DeepSeek and others through one OpenAI-compatible API", "Model": "Model", "Model ID already exists": "Model ID already exists", "Model ID is required": "Model ID is required", diff --git a/packages/plugins/@nocobase/plugin-ai/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-ai/src/locale/zh-CN.json index 431967cab2ef3..37926dd2f2b99 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-ai/src/locale/zh-CN.json @@ -197,6 +197,7 @@ "Messages": "消息", "Mistral models": "Mistral 模型", "300+ latest mainstream models across leading model families": "300+最新各系列主流模型", + "Models from OpenAI, Anthropic, Google, DeepSeek and others through one OpenAI-compatible API": "通过一个 OpenAI 兼容 API 使用 OpenAI、Anthropic、Google、DeepSeek 等厂商的模型", "Model": "模型", "Model ID already exists": "模型标识已存在", "Model ID is required": "请输入模型标识", diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/__tests__/aimlapi.test.ts b/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/__tests__/aimlapi.test.ts new file mode 100644 index 0000000000000..0d0ae0a69e09e --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/__tests__/aimlapi.test.ts @@ -0,0 +1,167 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import type { Application } from '@nocobase/server'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const serverRequestMock = vi.hoisted(() => vi.fn()); + +vi.mock('@nocobase/utils', async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + serverRequest: serverRequestMock, + }; +}); + +import { + AIMLAPI_ATTRIBUTION_HEADERS, + AimlapiProvider, + aimlapiProviderOptions, + supportsChatCompletions, +} from '../aimlapi'; + +function createApp(): Application { + return { + environment: { + renderJsonTemplate: (value: Record) => value, + }, + } as unknown as Application; +} + +const originalWhitelist = process.env.SERVER_REQUEST_WHITELIST; + +describe('AimlapiProvider', () => { + afterEach(() => { + process.env.SERVER_REQUEST_WHITELIST = originalWhitelist; + serverRequestMock.mockReset(); + }); + + it('uses the AI/ML API OpenAI-compatible API base URL', () => { + const provider = new AimlapiProvider({ + app: createApp(), + serviceOptions: { apiKey: 'test-key' }, + }); + + expect(provider.baseURL).toBe('https://api.aimlapi.com/v1'); + }); + + it('uses the aimlapi.com brand name in provider selectors', () => { + expect(aimlapiProviderOptions.title).toBe('aimlapi.com'); + }); + + // A partner id that does not match the gateway's pattern is dropped silently: the request still succeeds, it is + // simply never attributed. A typo would therefore be invisible at runtime, hence this assertion. + it('ships a partner id in the format the gateway accepts', () => { + expect(AIMLAPI_ATTRIBUTION_HEADERS['X-AIMLAPI-Partner-ID']).toMatch(/^part_[A-Za-z0-9]{1,64}$/); + }); + + it('ships a source in the / format the gateway accepts', () => { + expect(AIMLAPI_ATTRIBUTION_HEADERS['X-AIMLAPI-Source']).toMatch(/^(web|agent|mcp)\/[a-z0-9-]{1,32}$/); + }); + + it('identifies NocoBase, not AI/ML API, as the calling application', () => { + expect(AIMLAPI_ATTRIBUTION_HEADERS['HTTP-Referer']).toBe('https://github.com/nocobase/nocobase'); + expect(AIMLAPI_ATTRIBUTION_HEADERS['X-Title']).toBe('NocoBase'); + }); + + it('adds the attribution headers to chat requests', () => { + process.env.SERVER_REQUEST_WHITELIST = 'api.aimlapi.com'; + const provider = new AimlapiProvider({ + app: createApp(), + serviceOptions: { apiKey: 'test-key' }, + modelOptions: { model: 'openai/gpt-4o-mini' }, + }); + + expect(provider.chatModel.clientConfig).toMatchObject({ + baseURL: 'https://api.aimlapi.com/v1', + defaultHeaders: { ...AIMLAPI_ATTRIBUTION_HEADERS }, + }); + }); + + it('lets a configured referer and title win over the defaults without dropping the AI/ML API headers', () => { + process.env.SERVER_REQUEST_WHITELIST = 'api.aimlapi.com'; + const provider = new AimlapiProvider({ + app: createApp(), + serviceOptions: { apiKey: 'test-key', httpReferer: 'https://example.test', xTitle: 'Test App' }, + modelOptions: { model: 'openai/gpt-4o-mini' }, + }); + + expect(provider.chatModel.clientConfig.defaultHeaders).toEqual({ + 'HTTP-Referer': 'https://example.test', + 'X-Title': 'Test App', + 'X-AIMLAPI-Partner-ID': AIMLAPI_ATTRIBUTION_HEADERS['X-AIMLAPI-Partner-ID'], + 'X-AIMLAPI-Source': AIMLAPI_ATTRIBUTION_HEADERS['X-AIMLAPI-Source'], + }); + expect(AIMLAPI_ATTRIBUTION_HEADERS['X-Title']).toBe('NocoBase'); + }); + + // baseURL is user-configurable. Attribution that rode along to another vendor, or to a proxy fronting AI/ML API, + // would tag traffic that is not ours. + it('does not send attribution to a base URL outside the AI/ML API origin', () => { + process.env.SERVER_REQUEST_WHITELIST = 'api.aimlapi.com,proxy.example.test'; + const provider = new AimlapiProvider({ + app: createApp(), + serviceOptions: { apiKey: 'test-key', baseURL: 'https://proxy.example.test/v1' }, + modelOptions: { model: 'openai/gpt-4o-mini' }, + }); + + expect(provider.chatModel.clientConfig.defaultHeaders).toBeUndefined(); + }); + + it('recognizes Chat Completions-compatible models', () => { + expect(supportsChatCompletions({ id: 'openai/gpt-4o', type: 'openai/chat-completions' })).toBe(true); + expect(supportsChatCompletions({ id: 'openai/o3-deep-research', type: 'openai/responses/submit' })).toBe(false); + expect(supportsChatCompletions({ id: 'flux/dev', type: 'openai/image-generations' })).toBe(false); + expect(supportsChatCompletions({ id: 'legacy-model' })).toBe(true); + }); + + it('loads and filters the model catalog with the attribution headers attached', async () => { + process.env.SERVER_REQUEST_WHITELIST = 'api.aimlapi.com'; + serverRequestMock.mockResolvedValue({ + data: { + object: 'list', + data: [ + { id: 'openai/gpt-4o', type: 'openai/chat-completions' }, + { id: 'flux/dev', type: 'openai/image-generations' }, + { id: 'legacy-model' }, + ], + }, + }); + const provider = new AimlapiProvider({ + app: createApp(), + serviceOptions: { apiKey: 'test-key' }, + }); + + await expect(provider.listModels()).resolves.toEqual({ + models: [{ id: 'openai/gpt-4o' }, { id: 'legacy-model' }], + }); + expect(serverRequestMock).toHaveBeenCalledWith({ + method: 'GET', + url: 'https://api.aimlapi.com/v1/models', + headers: { + Authorization: 'Bearer test-key', + ...AIMLAPI_ATTRIBUTION_HEADERS, + }, + }); + }); + + it('requires an API key before loading models', async () => { + process.env.SERVER_REQUEST_WHITELIST = 'api.aimlapi.com'; + const provider = new AimlapiProvider({ + app: createApp(), + }); + + await expect(provider.listModels()).resolves.toEqual({ + code: 400, + errMsg: 'API Key required', + }); + expect(serverRequestMock).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/aimlapi.ts b/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/aimlapi.ts new file mode 100644 index 0000000000000..657e820b8da9a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/aimlapi.ts @@ -0,0 +1,152 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { ChatOpenAI } from '@langchain/openai'; +import { serverRequest } from '@nocobase/utils'; +import { LLMProviderMeta, SupportedModel } from '../manager/ai-manager'; +import { LLMProvider } from './provider'; + +const AIMLAPI_BASE_URL = 'https://api.aimlapi.com/v1'; +const AIMLAPI_HOSTNAME = 'api.aimlapi.com'; +const CHAT_COMPLETIONS_MODEL_TYPE = 'openai/chat-completions'; + +/** + * Attribution headers sent with every AI/ML API request. `HTTP-Referer` and `X-Title` follow the OpenRouter + * convention and identify the calling application (NocoBase, not AI/ML API); the `X-AIMLAPI-*` pair identifies the + * integration itself so AI/ML API can attribute traffic to it. Frozen because it is shared by every provider + * instance — callers get a copy, never this object. + */ +export const AIMLAPI_ATTRIBUTION_HEADERS: Readonly> = Object.freeze({ + 'HTTP-Referer': 'https://github.com/nocobase/nocobase', + 'X-Title': 'NocoBase', + 'X-AIMLAPI-Partner-ID': 'part_nocobase', + 'X-AIMLAPI-Source': 'agent/nocobase', +}); + +export type AimlapiModel = { + id: string; + type?: string; +}; + +/** + * The catalog exposes every endpoint family (image, video, speech, embeddings, ...) under the same list, so entries + * have to be narrowed to the Chat Completions surface this provider talks to. Entries with no `type` are kept: a user + * may point `baseURL` at another OpenAI-compatible endpoint whose catalog carries no such metadata. + */ +export function supportsChatCompletions(model: AimlapiModel): boolean { + return typeof model.type !== 'string' || model.type === CHAT_COMPLETIONS_MODEL_TYPE; +} + +export class AimlapiProvider extends LLMProvider { + declare chatModel: ChatOpenAI; + + get baseURL() { + return AIMLAPI_BASE_URL; + } + + /** + * Attribution travels to AI/ML API only. `baseURL` is user-configurable, so it may point at another vendor or at a + * proxy that merely fronts the same API, and neither should receive these headers. + */ + protected buildDefaultHeaders(): Record { + const { httpReferer, xTitle } = this.serviceOptions || {}; + const headers: Record = this.isAimlapiOrigin() ? { ...AIMLAPI_ATTRIBUTION_HEADERS } : {}; + if (httpReferer) { + headers['HTTP-Referer'] = httpReferer; + } + if (xTitle) { + headers['X-Title'] = xTitle; + } + return headers; + } + + protected isAimlapiOrigin(): boolean { + try { + return new URL(this.getResolvedBaseURL()).hostname === AIMLAPI_HOSTNAME; + } catch { + return false; + } + } + + createModel() { + const { apiKey } = this.serviceOptions || {}; + const { responseFormat, structuredOutput } = this.modelOptions || {}; + const { name, schema } = structuredOutput || {}; + const responseFormatOptions: Record = { + type: responseFormat ?? 'text', + }; + + if (responseFormat === 'json_schema' && schema) { + responseFormatOptions.json_schema = { + schema, + name: name ?? 'schema', + }; + } + + const defaultHeaders = this.buildDefaultHeaders(); + + return new ChatOpenAI({ + apiKey, + ...this.modelOptions, + modelKwargs: { + response_format: responseFormatOptions, + }, + configuration: { + baseURL: this.getResolvedBaseURL(), + ...(Object.keys(defaultHeaders).length ? { defaultHeaders } : {}), + }, + }); + } + + async listModels(): Promise<{ + models?: { id: string }[]; + code?: number; + errMsg?: string; + }> { + const { apiKey } = this.serviceOptions || {}; + let url: string; + + try { + url = this.buildRequestURL('models'); + } catch (error) { + return { code: 400, errMsg: error instanceof Error ? error.message : String(error) }; + } + + if (!apiKey) { + return { code: 400, errMsg: 'API Key required' }; + } + + try { + const response = await serverRequest({ + method: 'GET', + url, + headers: { + Authorization: `Bearer ${apiKey}`, + ...this.buildDefaultHeaders(), + }, + }); + const models = Array.isArray(response?.data?.data) ? (response.data.data as AimlapiModel[]) : []; + + return { + models: models.filter(supportsChatCompletions).map(({ id }) => ({ id })), + }; + } catch (error) { + return { + code: 500, + errMsg: error instanceof Error ? error.message : String(error), + }; + } + } +} + +export const aimlapiProviderOptions: LLMProviderMeta = { + title: 'aimlapi.com', + supportedModel: [SupportedModel.LLM], + provider: AimlapiProvider, +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts index bf37dcf4450db..0d4f332dcd03b 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts @@ -48,6 +48,7 @@ import { mimoProviderOptions } from './llm-providers/mimo'; import { mistralProviderOptions } from './llm-providers/mistral'; import { orcarouterProviderOptions } from './llm-providers/orcarouter'; import { shengsuanyunProviderOptions } from './llm-providers/shengsuanyun'; +import { aimlapiProviderOptions } from './llm-providers/aimlapi'; import { SubAgentsDispatcher } from './ai-employees/sub-agents'; import { AIEmployeeInstruction, @@ -189,6 +190,7 @@ export class PluginAIServer extends Plugin { this.aiManager.registerLLMProvider('xai', xaiProviderOptions); this.aiManager.registerLLMProvider('orcarouter', orcarouterProviderOptions); this.aiManager.registerLLMProvider('shengsuanyun', shengsuanyunProviderOptions); + this.aiManager.registerLLMProvider('aimlapi', aimlapiProviderOptions); } registerTools() { From 6df6b697b070790f39b73276975d1f38cce3aa1c Mon Sep 17 00:00:00 2001 From: Lookoff-AIMLAPI Date: Thu, 3 Sep 2026 04:36:22 +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 Puts aimlapi.com first in the provider selector's hand-written sort order and seeds the existing per-provider recommended-model list, which is empty for every other provider today, so the AI/ML API entry carries the "Recommended" tag. This is partnership placement, not a functional change, and it is deliberately isolated in one commit so it can be dropped before the provider itself is offered upstream. The four model ids were checked against the live catalog (ids and aliases) and are all `openai/chat-completions` entries. --- .../plugin-ai/src/client-v2/pages/LLMServicesPage.tsx | 1 + .../@nocobase/plugin-ai/src/common/recommended-models.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/plugins/@nocobase/plugin-ai/src/client-v2/pages/LLMServicesPage.tsx b/packages/plugins/@nocobase/plugin-ai/src/client-v2/pages/LLMServicesPage.tsx index f05fbc029e87a..ec33bd9baa312 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client-v2/pages/LLMServicesPage.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client-v2/pages/LLMServicesPage.tsx @@ -343,6 +343,7 @@ export async function testLLMServiceFlight( const getProviderSortIndex = (value: string) => { const sortOrder = [ + 'aimlapi', 'google-genai', 'openai', 'anthropic', diff --git a/packages/plugins/@nocobase/plugin-ai/src/common/recommended-models.ts b/packages/plugins/@nocobase/plugin-ai/src/common/recommended-models.ts index b26b3672849ca..d0c2a4b253fbc 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/common/recommended-models.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/common/recommended-models.ts @@ -11,7 +11,14 @@ * NocoBase officially recommended models for each LLM provider. * These models are tested to ensure quality and compatibility. */ -export const recommendedModels: Record = {}; +export const recommendedModels: Record = { + aimlapi: [ + { label: 'GPT-4o', value: 'openai/gpt-4o' }, + { label: 'Claude Sonnet 4.5', value: 'anthropic/claude-sonnet-4.5' }, + { label: 'Gemini 2.5 Pro', value: 'google/gemini-2.5-pro' }, + { label: 'DeepSeek Chat', value: 'deepseek/deepseek-chat' }, + ], +}; /** * Check if a model is recommended for a given provider From 222734051bc697727a7357d4abd485f56a071615 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 3 Sep 2026 18:14:31 +0500 Subject: [PATCH 3/3] fix(aimlapi): use the registered partner id The placeholder part_nocobase was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_vIQFIgEDs9Yk0yizVcgoM5Sp. A wrong or unknown partner id is accepted with a 200 and silently not attributed, so this would not have surfaced at runtime. --- .../@nocobase/plugin-ai/src/server/llm-providers/aimlapi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/aimlapi.ts b/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/aimlapi.ts index 657e820b8da9a..31f44968cb152 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/aimlapi.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/aimlapi.ts @@ -25,7 +25,7 @@ const CHAT_COMPLETIONS_MODEL_TYPE = 'openai/chat-completions'; export const AIMLAPI_ATTRIBUTION_HEADERS: Readonly> = Object.freeze({ 'HTTP-Referer': 'https://github.com/nocobase/nocobase', 'X-Title': 'NocoBase', - 'X-AIMLAPI-Partner-ID': 'part_nocobase', + 'X-AIMLAPI-Partner-ID': 'part_vIQFIgEDs9Yk0yizVcgoM5Sp', 'X-AIMLAPI-Source': 'agent/nocobase', });