Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ LEON_ZAI_API_KEY=
# MiniMax API key
LEON_MINIMAX_API_KEY=

# AI/ML API (aimlapi.com) API key
LEON_AIMLAPI_API_KEY=

# OpenAI API key
LEON_OPENAI_API_KEY=

Expand Down
6 changes: 6 additions & 0 deletions config.sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ llm:
api_key:
# The value is read from LEON_MINIMAX_API_KEY in your profile .env file.
env: LEON_MINIMAX_API_KEY
aimlapi:
# aimlapi.com OpenAI-compatible Chat Completions endpoint.
base_url: https://api.aimlapi.com/v1
api_key:
# The value is read from LEON_AIMLAPI_API_KEY in your profile .env file.
env: LEON_AIMLAPI_API_KEY
openai:
api_key:
# The value is read from LEON_OPENAI_API_KEY in your profile .env file.
Expand Down
2 changes: 1 addition & 1 deletion scripts/commit-msg.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { LogHelper } from '@/helpers/log-helper'
'utf8'
)
const regex =
'(build|BREAKING|chore|ci|docs|feat|fix|perf|refactor|style|test)(\\((web app|scripts|server|agent mode|controlled mode|aurora|messaging app|built-in command|hotword|python tcp server|bridge\\/(python|nodejs)|tool\\/([\\w-]+)|skill\\/([\\w-]+)|provider\\/(llamacpp|sglang|openrouter|zai|minimax|openai|anthropic|moonshotai|huggingface|cerebras|groq|celeris)))?\\)?: .{1,50}'
'(build|BREAKING|chore|ci|docs|feat|fix|perf|refactor|style|test)(\\((web app|scripts|server|agent mode|controlled mode|aurora|messaging app|built-in command|hotword|python tcp server|bridge\\/(python|nodejs)|tool\\/([\\w-]+)|skill\\/([\\w-]+)|provider\\/(llamacpp|sglang|openrouter|zai|minimax|aimlapi|openai|anthropic|moonshotai|huggingface|cerebras|groq|celeris)))?\\)?: .{1,50}'

if (commitMessage.match(regex) !== null) {
LogHelper.success('Commit message validated')
Expand Down
6 changes: 6 additions & 0 deletions server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ const DEFAULT_CONFIG: LeonConfig = {
env: 'LEON_MINIMAX_API_KEY'
}
},
aimlapi: {
base_url: 'https://api.aimlapi.com/v1',
api_key: {
env: 'LEON_AIMLAPI_API_KEY'
}
},
openai: {
api_key: {
env: 'LEON_OPENAI_API_KEY'
Expand Down
23 changes: 23 additions & 0 deletions server/src/core/llm-manager/llm-model-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,29 @@ const ROUTABLE_SPEED = [
* setup and command autocomplete remain deterministic and work offline.
*/
export const LLM_MODEL_CATALOG: readonly LLMModelCatalogEntry[] = [
/**
* aimlapi.com is an aggregator, so the model id is the label, as for
* OpenRouter. Every id below was checked against the live catalog
* (`GET /v1/models?include=all`, type `openai/chat-completions`) and
* answered a real completion. The dotted spelling is deliberate: the dashed
* `anthropic/claude-*` variants publish only `streaming` capabilities.
* Reasoning is left on `auto` because the gateway rejects the
* vendor-specific reasoning fields Leon would otherwise attach.
*
* @see https://docs.aimlapi.com/api-references/model-database
*/
{ provider: LLMProviders.AIMLAPI, model: 'openai/gpt-5.6-sol', label: 'openai/gpt-5.6-sol', recommended: true, reasoning: AUTO_REASONING, speed: AUTO_SPEED },
/** @see https://docs.aimlapi.com/api-references/model-database */
{ provider: LLMProviders.AIMLAPI, model: 'anthropic/claude-opus-4.8', label: 'anthropic/claude-opus-4.8', reasoning: AUTO_REASONING, speed: AUTO_SPEED },
/** @see https://docs.aimlapi.com/api-references/model-database */
{ provider: LLMProviders.AIMLAPI, model: 'anthropic/claude-sonnet-4.6', label: 'anthropic/claude-sonnet-4.6', reasoning: AUTO_REASONING, speed: AUTO_SPEED },
/** @see https://docs.aimlapi.com/api-references/model-database */
{ provider: LLMProviders.AIMLAPI, model: 'google/gemini-3.8-flash', label: 'google/gemini-3.8-flash', reasoning: AUTO_REASONING, speed: AUTO_SPEED },
/** @see https://docs.aimlapi.com/api-references/model-database */
{ provider: LLMProviders.AIMLAPI, model: 'deepseek/deepseek-v4-pro', label: 'deepseek/deepseek-v4-pro', reasoning: AUTO_REASONING, speed: AUTO_SPEED },
/** @see https://docs.aimlapi.com/api-references/model-database */
{ provider: LLMProviders.AIMLAPI, model: 'moonshot/kimi-k3', label: 'moonshot/kimi-k3', reasoning: AUTO_REASONING, speed: AUTO_SPEED },

/**
* @see https://docs.celeris.ai/models Fast diffusion model for short agentic calls.
*/
Expand Down
6 changes: 6 additions & 0 deletions server/src/core/llm-manager/llm-provider-account-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export interface LLMProviderAccountConfig {
*/
export const LLM_PROVIDER_ACCOUNT_CONFIGS: ReadonlyArray<LLMProviderAccountConfig> =
Object.freeze([
{
label: 'aimlapi.com',
value: LLMProviders.AIMLAPI,
apiKeyEnv: 'LEON_AIMLAPI_API_KEY',
apiKeyURL: 'https://aimlapi.com/app/keys'
},
{
label: 'OpenRouter',
value: LLMProviders.OpenRouter,
Expand Down
2 changes: 2 additions & 0 deletions server/src/core/llm-manager/llm-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const LLM_PROVIDERS_MAP = {
[LLMProviders.OpenRouter]: 'openrouter-llm-provider',
[LLMProviders.ZAI]: 'z-ai-llm-provider',
[LLMProviders.MiniMax]: 'minimax-llm-provider',
[LLMProviders.AIMLAPI]: 'aimlapi-llm-provider',
[LLMProviders.OpenAI]: 'openai-llm-provider',
[LLMProviders.Anthropic]: 'anthropic-llm-provider',
[LLMProviders.MoonshotAI]: 'moonshotai-llm-provider',
Expand Down Expand Up @@ -2674,6 +2675,7 @@ export default class LLMProvider {
LLMProviders.SGLang,
LLMProviders.ZAI,
LLMProviders.MiniMax,
LLMProviders.AIMLAPI,
LLMProviders.Anthropic,
LLMProviders.MoonshotAI,
LLMProviders.Cerebras,
Expand Down
80 changes: 80 additions & 0 deletions server/src/core/llm-manager/llm-providers/aimlapi-llm-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import AISDKRemoteLLMProvider from '@/core/llm-manager/llm-providers/ai-sdk-remote-llm-provider'
import type { ResolvedLLMTarget } from '@/core/llm-manager/llm-routing'
import { CONFIG_MANAGER } from '@/config'

const DEFAULT_BASE_URL = 'https://api.aimlapi.com/v1'
const OFFICIAL_API_HOSTNAME = 'api.aimlapi.com'

/**
* Attribution sent with AI/ML API requests. `HTTP-Referer` and `X-Title`
* follow the OpenRouter convention and identify Leon, the calling project,
* never the gateway.
*/
const AIMLAPI_ATTRIBUTION_HEADERS: Readonly<Record<string, string>> =
Object.freeze({
'HTTP-Referer': 'https://github.com/leon-ai/leon',
'X-Title': 'Leon',
'X-AIMLAPI-Source': 'agent/leon',
'X-AIMLAPI-Partner-ID': 'part_lcAMsJBHJpF6eW4JFtT3pJfW'
})

function resolveBaseURL(): string {
return CONFIG_MANAGER.getProviderBaseURL('aimlapi') || DEFAULT_BASE_URL
}

/**
* The Base URL is user-configurable, so attribution is scoped to the official
* host. A self-hosted gateway or a third-party proxy fronting the same schema
* must not receive Leon's AI/ML API attribution, and a new object is returned
* on every call so the shared constant can never be mutated.
*/
export function buildAIMLAPIHeaders(baseURL: string): Record<string, string> {
let hostname: string

try {
hostname = new URL(baseURL).hostname
} catch {
return {}
}

if (hostname !== OFFICIAL_API_HOSTNAME) {
return {}
}

return { ...AIMLAPI_ATTRIBUTION_HEADERS }
}

/**
* AI/ML API is an aggregator exposing many upstream vendors behind a single
* OpenAI-compatible Chat Completions schema.
*
* Its validator rejects a `null` value for optional request fields
* (`temperature`, `top_p`, `seed`, `reasoning_effort`, `tools`, `tool_choice`,
* `response_format`, `max_tokens`, … all answer HTTP 400), where the upstream
* OpenAI API accepts them. Unset parameters must therefore be omitted from the
* request body rather than sent as `null`, which is what the base class does
* and what `test/agent/unit/aimlapi-llm-provider.spec.ts` guards.
*
* Vendor-specific reasoning extension fields are not forwarded either: the
* field that controls reasoning differs per upstream vendor, so the empty
* provider-options builder stops Leon's generic compatible adapter from
* attaching one that this gateway would reject.
*
* @see https://docs.aimlapi.com/api-references/text-models-llm
*/
export default class AIMLAPILLMProvider extends AISDKRemoteLLMProvider {
constructor(target: ResolvedLLMTarget) {
const baseURL = resolveBaseURL()

super({
name: 'AI/ML API LLM Provider',
providerName: 'aimlapi',
apiKeyEnv: 'LEON_AIMLAPI_API_KEY',
model: target.model,
baseURL,
flavor: 'openai-compatible',
headers: () => buildAIMLAPIHeaders(baseURL),
buildProviderOptions: () => ({})
})
}
}
1 change: 1 addition & 0 deletions server/src/core/llm-manager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum LLMProviders {
OpenRouter = 'openrouter',
ZAI = 'zai',
MiniMax = 'minimax',
AIMLAPI = 'aimlapi',
OpenAI = 'openai',
Anthropic = 'anthropic',
MoonshotAI = 'moonshotai',
Expand Down
1 change: 1 addition & 0 deletions server/src/core/session-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const TITLE_REASONING_MODE_OFF_PROVIDERS = [
LLMProviders.OpenRouter,
LLMProviders.ZAI,
LLMProviders.MiniMax,
LLMProviders.AIMLAPI,
LLMProviders.Anthropic,
LLMProviders.MoonshotAI,
LLMProviders.HuggingFace,
Expand Down
1 change: 1 addition & 0 deletions server/src/schemas/core-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const configSchemaObject = strictObject({
openrouter: llmProvider,
zai: llmProvider,
minimax: llmProviderWithBaseURL,
aimlapi: llmProviderWithBaseURL,
openai: llmProvider,
anthropic: llmProvider,
moonshotai: llmProvider,
Expand Down
8 changes: 8 additions & 0 deletions test/agent/e2e/provider-matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ export const PROVIDER_MATRIX = [
requiredEnv: 'LEON_MINIMAX_API_KEY',
llmTarget: 'minimax/MiniMax-M3',
reasoning: 'none'
},
{
provider: 'aimlapi',
requiredEnv: 'LEON_AIMLAPI_API_KEY',
llmTarget: 'aimlapi/openai/gpt-5.6-sol',
// The gateway rejects the vendor reasoning fields Leon would attach, so
// the curated catalog exposes auto only.
reasoning: null
}
] as const

Expand Down
Loading