diff --git a/.env.example b/.env.example index 696a5b9e3..7a30462d9 100644 --- a/.env.example +++ b/.env.example @@ -71,6 +71,11 @@ MINIMAX_API_KEY= MINIMAX_SERVER_URL=https://api.minimax.io/v1 MINIMAX_PROVIDER= +## aimlapi.com LLM provider (aggregator, OpenAI-compatible) +AIMLAPI_API_KEY= +AIMLAPI_SERVER_URL=https://api.aimlapi.com/v1 +AIMLAPI_PROVIDER= + ## Custom LLM provider LLM_SERVER_URL= LLM_SERVER_KEY= diff --git a/.vscode/launch.json b/.vscode/launch.json index becb5cd7d..60bc316ad 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -66,6 +66,7 @@ // "-type", "kimi", // "-type", "qwen", // "-type", "minimax", + // "-type", "aimlapi", "-config", "${workspaceFolder}/examples/configs/moonshot.provider.yml", // "-config", "${workspaceFolder}/examples/configs/deepseek.provider.yml", // "-config", "${workspaceFolder}/examples/configs/ollama-cloud.provider.yml", @@ -103,6 +104,7 @@ // "-report", "${workspaceFolder}/examples/tests/kimi-report.md", // "-report", "${workspaceFolder}/examples/tests/qwen-report.md", // "-report", "${workspaceFolder}/examples/tests/minimax-report.md", + // "-report", "${workspaceFolder}/examples/tests/aimlapi-report.md", // "-report", "${workspaceFolder}/examples/tests/hcnsec-report.md", // "-report", "${workspaceFolder}/examples/tests/custom-openai-report.md", // "-report", "${workspaceFolder}/examples/tests/opencode-report.md", diff --git a/CLAUDE.md b/CLAUDE.md index 3a6da8ad7..2bc23857c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,7 +118,7 @@ State is managed primarily through Apollo Client (GraphQL) with real-time update ### Key Integrations -- **LLM Providers**: OpenAI, Anthropic, Gemini, AWS Bedrock, Ollama, DeepSeek, GLM, Kimi, Qwen, and custom HTTP endpoints — configured via environment variables or the Settings UI +- **LLM Providers**: OpenAI, Anthropic, Gemini, AWS Bedrock, Ollama, DeepSeek, GLM, Kimi, Qwen, MiniMax, aimlapi.com, and custom HTTP endpoints — configured via environment variables or the Settings UI - **Search**: DuckDuckGo, Google, Tavily, Firecrawl, Traversaal, Perplexity, Searxng - **Databases**: PostgreSQL + pgvector (required), Neo4j (optional, for knowledge graph) - **Observability**: OpenTelemetry → VictoriaMetrics + Loki + Jaeger → Grafana; Langfuse for LLM analytics diff --git a/README.md b/README.md index 74ccf132d..1b7bb16d3 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ - [Kimi](#kimi-provider-configuration) - [Qwen](#qwen-provider-configuration) - [MiniMax](#minimax-provider-configuration) + - [aimlapi.com](#aimlapicom-provider-configuration) - [Advanced Setup](#advanced-setup) - [Langfuse Integration](#langfuse-integration) - [Monitoring and Observability](#monitoring-and-observability) @@ -75,7 +76,7 @@ You can watch the video **PentAGI overview**: - Persistent Storage. All commands and outputs are stored in PostgreSQL with [pgvector](https://hub.docker.com/r/vxcontrol/pgvector) extension. - Scalable Architecture. Microservices-based design supporting horizontal scaling. - Self-Hosted Solution. Complete control over your deployment and data. -- Flexible Authentication. Support for 10+ LLM providers ([OpenAI](https://platform.openai.com/), [Anthropic](https://www.anthropic.com/), [Google AI/Gemini](https://ai.google.dev/), [AWS Bedrock](https://aws.amazon.com/bedrock/), [Ollama](https://ollama.com/), [DeepSeek](https://www.deepseek.com/en/), [GLM](https://z.ai/), [Kimi](https://platform.moonshot.ai/), [Qwen](https://www.alibabacloud.com/en/), [MiniMax](https://www.minimax.io/), Custom) plus aggregators ([OpenRouter](https://openrouter.ai/), [DeepInfra](https://deepinfra.com/), [Atlas Cloud](https://www.atlascloud.ai/), [OpenCode Go plan](https://opencode.ai/en/go)). For production local deployments, see our [vLLM + Qwen3.5-27B-FP8 guide](examples/guides/vllm-qwen35-27b-fp8.md). +- Flexible Authentication. Support for 10+ LLM providers ([OpenAI](https://platform.openai.com/), [Anthropic](https://www.anthropic.com/), [Google AI/Gemini](https://ai.google.dev/), [AWS Bedrock](https://aws.amazon.com/bedrock/), [Ollama](https://ollama.com/), [DeepSeek](https://www.deepseek.com/en/), [GLM](https://z.ai/), [Kimi](https://platform.moonshot.ai/), [Qwen](https://www.alibabacloud.com/en/), [MiniMax](https://www.minimax.io/), Custom) plus aggregators ([OpenRouter](https://openrouter.ai/), [DeepInfra](https://deepinfra.com/), [Atlas Cloud](https://www.atlascloud.ai/), [OpenCode Go plan](https://opencode.ai/en/go), [aimlapi.com](https://aimlapi.com/)). For production local deployments, see our [vLLM + Qwen3.5-27B-FP8 guide](examples/guides/vllm-qwen35-27b-fp8.md). - API Token Authentication. Secure Bearer token system for programmatic access to REST and GraphQL APIs. - Quick Deployment. Easy setup through [Docker Compose](https://docs.docker.com/compose/) with comprehensive environment configuration. @@ -766,6 +767,9 @@ BEDROCK_DEFAULT_AUTH=true # Option 1: Use AWS SDK default # QWEN_API_KEY=your_qwen_key # Qwen (Alibaba Cloud, multimodal) # MINIMAX_API_KEY=your_minimax_key # MiniMax +# Optional: aggregators (one key, many vendors) +# AIMLAPI_API_KEY=your_aimlapi_key # aimlapi.com (350+ chat models) + # Optional: Local LLM provider (zero-cost inference) OLLAMA_SERVER_URL=http://localhost:11434 OLLAMA_SERVER_MODEL=your_model_name @@ -2576,6 +2580,50 @@ PentAGI ships 3 MiniMax models with tool calling, JSON output, and streaming. `M **LiteLLM Integration**: Set `MINIMAX_PROVIDER=minimax` to enable model name prefixing when using default PentAGI configurations with LiteLLM proxy. Leave empty for direct API usage. +### aimlapi.com Provider Configuration + +[aimlapi.com](https://aimlapi.com/) is an aggregator: one OpenAI-compatible endpoint at `https://api.aimlapi.com/v1` and one key in front of 350+ chat models from many vendors, with tool calling, structured output, streaming and reasoning support. + +Requests PentAGI sends to `api.aimlapi.com` carry `HTTP-Referer`, `X-Title`, `X-AIMLAPI-Source` and `X-AIMLAPI-Partner-ID` identifying PentAGI as the calling application. The headers are scoped to that host, so pointing `AIMLAPI_SERVER_URL` at a proxy or a self-hosted gateway disables them rather than tagging someone else's traffic. + +#### Configuration Variables + +| Variable | Default Value | Description | +| -------------------- | ---------------------------- | -------------------------------------------------- | +| `AIMLAPI_API_KEY` | | aimlapi.com API key for authentication | +| `AIMLAPI_SERVER_URL` | `https://api.aimlapi.com/v1` | aimlapi.com API endpoint URL | +| `AIMLAPI_PROVIDER` | | Provider prefix for LiteLLM integration (optional) | + +#### Configuration Examples + +```bash +# Direct API usage +AIMLAPI_API_KEY=your_aimlapi_api_key +AIMLAPI_SERVER_URL=https://api.aimlapi.com/v1 + +# With LiteLLM proxy +AIMLAPI_API_KEY=your_litellm_key +AIMLAPI_SERVER_URL=http://litellm-proxy:4000 +AIMLAPI_PROVIDER=aimlapi # Adds prefix to model names for LiteLLM +``` + +#### Supported Models + +PentAGI ships 6 aimlapi.com models across the 13 agent roles. Model ids are vendor-namespaced and are sent verbatim; the gateway also accepts short aliases, but an alias can resolve to a different model than its name suggests, so only canonical ids are used. Prices are USD per 1M tokens as published by `GET https://api.aimlapi.com/v1/models?include=all` on 2026-09-03. + +| Model ID | Context | Price (Input/Output) | Use Case | +| ----------------------------- | ------- | -------------------- | ----------------------------------------------------------------- | +| `deepseek/deepseek-v4-flash`* | 1M | $0.182/$0.364 | Utility agents, enricher and pentester; provider-wide fallback (default) | +| `deepseek/deepseek-v4-pro` | 1M | $0.5655/$1.131 | Reserved for heavy multi-step reasoning | +| `z-ai/glm-5-turbo` | 262K | $1.56/$5.20 | Orchestrator and assistant, tuned for tool invocation | +| `zhipu/glm-5.2` | 1M | $1.82/$5.72 | Plan generation and refinement at max reasoning effort | +| `minimax/minimax-m3` | 524K | $0.39/$1.56 | Adviser, adaptive thinking via `extra_body` | +| `moonshot/kimi-k2-7-code` | 262K | $1.235/$5.20 | Coder and installer | + +The default roster deliberately excludes the OpenAI/Anthropic/Google models the gateway also carries: this product runs offensive-security workloads and those vendors' guardrails false-positive on legitimate exploit-development content. Any other catalog id can be selected per agent role in the Settings UI or in a provider config file. + +**LiteLLM Integration**: Set `AIMLAPI_PROVIDER=aimlapi` to enable model name prefixing when using default PentAGI configurations with LiteLLM proxy. Leave empty for direct API usage. + ## Advanced Setup ### Langfuse Integration @@ -3997,7 +4045,7 @@ To access detailed logs: The main utility accepts several options: - `-env ` - Path to environment file (optional, default: `.env`) -- `-provider ` - Provider type to use (default: `custom`, options: `openai`, `anthropic`, `gemini`, `bedrock`, `ollama`, `deepseek`, `glm`, `kimi`, `qwen`, `minimax`, `custom`) +- `-provider ` - Provider type to use (default: `custom`, options: `openai`, `anthropic`, `gemini`, `bedrock`, `ollama`, `deepseek`, `glm`, `kimi`, `qwen`, `minimax`, `aimlapi`, `custom`) - `-flow ` - Flow ID for testing functions that require it (0 means using mocks, default: `0`) - `-user ` - User ID for testing functions that require it (default: `0`; `1` is the default admin user) - `-task ` - Task ID for agent context (optional) diff --git a/backend/cmd/ctester/main.go b/backend/cmd/ctester/main.go index 99b16081b..ec410f3ca 100644 --- a/backend/cmd/ctester/main.go +++ b/backend/cmd/ctester/main.go @@ -10,6 +10,7 @@ import ( "time" "pentagi/pkg/config" + "pentagi/pkg/providers/aimlapi" "pentagi/pkg/providers/anthropic" "pentagi/pkg/providers/bedrock" "pentagi/pkg/providers/custom" @@ -33,7 +34,7 @@ import ( func main() { envFile := flag.String("env", ".env", "Path to environment file") - providerType := flag.String("type", "custom", "Provider type [custom, openai, anthropic, gemini, bedrock, ollama, deepseek, glm, kimi, qwen, minimax]") + providerType := flag.String("type", "custom", "Provider type [custom, openai, anthropic, gemini, bedrock, ollama, deepseek, glm, kimi, qwen, minimax, aimlapi]") providerName := flag.String("name", "", "Provider name using as PROVDER_NAME/MODEL_NAME while building provider config") configPath := flag.String("config", "", "Path to provider config file") testsPath := flag.String("tests", "", "Path to custom tests YAML file") @@ -235,6 +236,16 @@ func createProvider(providerType string, cfg *config.Config) (provider.Provider, } return minimax.New(cfg, provider.DefaultProviderNameMiniMax, providerConfig) + case "aimlapi": + if cfg.AIMLAPIKey == "" { + return nil, fmt.Errorf("AI/ML API key is not set") + } + providerConfig, err := aimlapi.DefaultProviderConfig() + if err != nil { + return nil, fmt.Errorf("error creating aimlapi provider config: %w", err) + } + return aimlapi.New(cfg, provider.DefaultProviderNameAIMLAPI, providerConfig) + default: return nil, fmt.Errorf("unsupported provider type: %s", providerType) } diff --git a/backend/cmd/ftester/main.go b/backend/cmd/ftester/main.go index 76d56fca2..a6d25633e 100644 --- a/backend/cmd/ftester/main.go +++ b/backend/cmd/ftester/main.go @@ -29,7 +29,7 @@ import ( func main() { envFile := flag.String("env", ".env", "Path to environment file") - providerName := flag.String("provider", "custom", "Provider name (openai, anthropic, gemini, bedrock, ollama, deepseek, glm, kimi, qwen, minimax, custom)") + providerName := flag.String("provider", "custom", "Provider name (openai, anthropic, gemini, bedrock, ollama, deepseek, glm, kimi, qwen, minimax, aimlapi, custom)") flowID := flag.Int64("flow", 0, "Flow ID for testing functions that require it (0 means using mocks)") userID := flag.Int64("user", 0, "User ID for testing functions that require it (1 is default admin user)") taskID := flag.Int64("task", 0, "Task ID for testing functions with default unset") diff --git a/backend/cmd/installer/wizard/controller/controller.go b/backend/cmd/installer/wizard/controller/controller.go index d7ba51cb3..e55f32e66 100644 --- a/backend/cmd/installer/wizard/controller/controller.go +++ b/backend/cmd/installer/wizard/controller/controller.go @@ -148,8 +148,8 @@ type LLMProviderConfig struct { // direct form field mappings using loader.EnvVar // these fields directly correspond to environment variables and form inputs (not computed) - BaseURL loader.EnvVar // OPEN_AI_SERVER_URL | ANTHROPIC_SERVER_URL | GEMINI_SERVER_URL | BEDROCK_SERVER_URL | OLLAMA_SERVER_URL | DEEPSEEK_SERVER_URL | GLM_SERVER_URL | KIMI_SERVER_URL | QWEN_SERVER_URL | MINIMAX_SERVER_URL | LLM_SERVER_URL - APIKey loader.EnvVar // OPEN_AI_KEY | ANTHROPIC_API_KEY | GEMINI_API_KEY | LLM_SERVER_KEY | DEEPSEEK_API_KEY | GLM_API_KEY | KIMI_API_KEY | QWEN_API_KEY | MINIMAX_API_KEY | OLLAMA_SERVER_API_KEY + BaseURL loader.EnvVar // OPEN_AI_SERVER_URL | ANTHROPIC_SERVER_URL | GEMINI_SERVER_URL | BEDROCK_SERVER_URL | OLLAMA_SERVER_URL | DEEPSEEK_SERVER_URL | GLM_SERVER_URL | KIMI_SERVER_URL | QWEN_SERVER_URL | MINIMAX_SERVER_URL | AIMLAPI_SERVER_URL | LLM_SERVER_URL + APIKey loader.EnvVar // OPEN_AI_KEY | ANTHROPIC_API_KEY | GEMINI_API_KEY | LLM_SERVER_KEY | DEEPSEEK_API_KEY | GLM_API_KEY | KIMI_API_KEY | QWEN_API_KEY | MINIMAX_API_KEY | AIMLAPI_API_KEY | OLLAMA_SERVER_API_KEY Model loader.EnvVar // LLM_SERVER_MODEL // AWS Bedrock specific fields DefaultAuth loader.EnvVar // BEDROCK_DEFAULT_AUTH @@ -203,6 +203,7 @@ func (c *controller) GetLLMProviders() map[string]*LLMProviderConfig { "kimi": c.GetLLMProviderConfig("kimi"), "qwen": c.GetLLMProviderConfig("qwen"), "minimax": c.GetLLMProviderConfig("minimax"), + "aimlapi": c.GetLLMProviderConfig("aimlapi"), "custom": c.GetLLMProviderConfig("custom"), } } @@ -298,6 +299,13 @@ func (c *controller) GetLLMProviderConfig(providerID string) *LLMProviderConfig providerConfig.ProviderName, _ = c.GetVar("MINIMAX_PROVIDER") providerConfig.Configured = providerConfig.APIKey.Value != "" + case "aimlapi": + providerConfig.Name = "aimlapi.com" + providerConfig.APIKey, _ = c.GetVar("AIMLAPI_API_KEY") + providerConfig.BaseURL, _ = c.GetVar("AIMLAPI_SERVER_URL") + providerConfig.ProviderName, _ = c.GetVar("AIMLAPI_PROVIDER") + providerConfig.Configured = providerConfig.APIKey.Value != "" + case "custom": providerConfig.Name = "Custom" providerConfig.BaseURL, _ = c.GetVar("LLM_SERVER_URL") @@ -450,7 +458,7 @@ func (c *controller) UpdateLLMProviderConfig(providerID string, config *LLMProvi return fmt.Errorf("failed to set %s: %w", config.ProviderName.Name, err) } - case "minimax": + case "minimax", "aimlapi": if err := c.SetVar(config.APIKey.Name, config.APIKey.Value); err != nil { return fmt.Errorf("failed to set %s: %w", config.APIKey.Name, err) } @@ -540,6 +548,8 @@ func (c *controller) ResetLLMProviderConfig(providerID string) map[string]*LLMPr vars = []string{"QWEN_API_KEY", "QWEN_SERVER_URL", "QWEN_PROVIDER"} case "minimax": vars = []string{"MINIMAX_API_KEY", "MINIMAX_SERVER_URL", "MINIMAX_PROVIDER"} + case "aimlapi": + vars = []string{"AIMLAPI_API_KEY", "AIMLAPI_SERVER_URL", "AIMLAPI_PROVIDER"} case "custom": vars = []string{ "LLM_SERVER_URL", "LLM_SERVER_KEY", "LLM_SERVER_MODEL", @@ -2347,6 +2357,9 @@ func (c *controller) getVariableDescription(varName string) string { "MINIMAX_API_KEY": locale.EnvDesc_MINIMAX_API_KEY, "MINIMAX_SERVER_URL": locale.EnvDesc_MINIMAX_SERVER_URL, "MINIMAX_PROVIDER": locale.EnvDesc_MINIMAX_PROVIDER, + "AIMLAPI_API_KEY": locale.EnvDesc_AIMLAPI_API_KEY, + "AIMLAPI_SERVER_URL": locale.EnvDesc_AIMLAPI_SERVER_URL, + "AIMLAPI_PROVIDER": locale.EnvDesc_AIMLAPI_PROVIDER, "LLM_SERVER_URL": locale.EnvDesc_LLM_SERVER_URL, "LLM_SERVER_KEY": locale.EnvDesc_LLM_SERVER_KEY, "LLM_SERVER_MODEL": locale.EnvDesc_LLM_SERVER_MODEL, @@ -2542,6 +2555,7 @@ var maskedVariables = map[string]bool{ "KIMI_API_KEY": true, "QWEN_API_KEY": true, "MINIMAX_API_KEY": true, + "AIMLAPI_API_KEY": true, "LLM_SERVER_KEY": true, "LANGFUSE_PUBLIC_KEY": true, "LANGFUSE_SECRET_KEY": true, @@ -2633,6 +2647,9 @@ var criticalVariables = map[string]bool{ "MINIMAX_API_KEY": true, "MINIMAX_SERVER_URL": true, "MINIMAX_PROVIDER": true, + "AIMLAPI_API_KEY": true, + "AIMLAPI_SERVER_URL": true, + "AIMLAPI_PROVIDER": true, "LLM_SERVER_URL": true, "LLM_SERVER_KEY": true, "LLM_SERVER_MODEL": true, diff --git a/backend/cmd/installer/wizard/locale/locale.go b/backend/cmd/installer/wizard/locale/locale.go index 248739836..a6b152cff 100644 --- a/backend/cmd/installer/wizard/locale/locale.go +++ b/backend/cmd/installer/wizard/locale/locale.go @@ -367,6 +367,7 @@ const ( LLMProviderKimi = "Kimi Moonshot AI" LLMProviderQwen = "Qwen Alibaba Cloud" LLMProviderMiniMax = "MiniMax" + LLMProviderAIMLAPI = "aimlapi.com" LLMProviderCustom = "Custom" LLMProviderOpenAIDesc = "Industry-leading GPT models with excellent general performance" LLMProviderAnthropicDesc = "Claude models with superior reasoning and safety features" @@ -378,6 +379,7 @@ const ( LLMProviderKimiDesc = "Moonshot AI's long-context models for document analysis" LLMProviderQwenDesc = "Alibaba Cloud's Qwen models for multilingual tasks" LLMProviderMiniMaxDesc = "MiniMax's M-series models for agentic reasoning and long-context tasks" + LLMProviderAIMLAPIDesc = "Single OpenAI-compatible endpoint fronting 350+ chat models from many vendors" LLMProviderCustomDesc = "Custom OpenAI-compatible endpoint for maximum flexibility" ) @@ -627,6 +629,36 @@ Cost: Competitive per-token pricing across the M-series Setup: Get your API key from https://platform.minimax.io/` + LLMFormAIMLAPIHelp = `aimlapi.com is an aggregator: one OpenAI-compatible endpoint and one API key in front of 350+ chat models from many vendors. + +Default PentAGI Models: +• deepseek/deepseek-v4-flash: utility agents, enricher and pentester (1M context, hybrid thinking) +• deepseek/deepseek-v4-pro: reserved for heavy multi-step reasoning +• z-ai/glm-5-turbo: orchestrator and assistant, tuned for tool invocation +• zhipu/glm-5.2: plan generation and refinement at max reasoning effort +• minimax/minimax-m3: adviser, adaptive thinking via extra_body +• moonshot/kimi-k2-7-code: coder and installer + +Key Advantages: +• One key and one endpoint across DeepSeek, GLM, MiniMax, Kimi, Qwen and others +• Model ids are vendor-namespaced, so swapping a role's model needs no new credential +• Full tool calling, structured output, streaming and reasoning support + +API Endpoint: +• https://api.aimlapi.com/v1 (default) + +LiteLLM Integration: +• Set Provider Name to 'aimlapi' when using a LiteLLM proxy +• Enables model prefix (e.g., aimlapi/deepseek/deepseek-v4-flash) without modifying config.yml +• Optional for direct aimlapi.com usage + +Note: the default model roster deliberately excludes OpenAI/Anthropic/Google models that the gateway also carries, because their guardrails false-positive on legitimate exploit-development work. + +Best for: Trying several vendors' models across agent roles without managing one account per vendor +Cost: Per-token, published per model at https://api.aimlapi.com/v1/models?include=all + +Setup: Get your API key from https://aimlapi.com/app/keys` + LLMFormCustomHelp = `Configure any OpenAI-compatible API endpoint for maximum flexibility and integration with existing infrastructure. Ready-to-use Configurations: @@ -2352,6 +2384,9 @@ const ( EnvDesc_MINIMAX_API_KEY = "MiniMax API Key" EnvDesc_MINIMAX_SERVER_URL = "MiniMax Server URL" EnvDesc_MINIMAX_PROVIDER = "MiniMax Provider Name Prefix (for LiteLLM, e.g., 'minimax')" + EnvDesc_AIMLAPI_API_KEY = "aimlapi.com API Key" + EnvDesc_AIMLAPI_SERVER_URL = "aimlapi.com Server URL" + EnvDesc_AIMLAPI_PROVIDER = "aimlapi.com Provider Name Prefix (for LiteLLM, e.g., 'aimlapi')" EnvDesc_LLM_SERVER_URL = "Custom LLM Server URL" EnvDesc_LLM_SERVER_KEY = "Custom LLM API Key" EnvDesc_LLM_SERVER_MODEL = "Custom LLM Model" diff --git a/backend/cmd/installer/wizard/models/llm_provider_form.go b/backend/cmd/installer/wizard/models/llm_provider_form.go index c589cd30b..92ad41b96 100644 --- a/backend/cmd/installer/wizard/models/llm_provider_form.go +++ b/backend/cmd/installer/wizard/models/llm_provider_form.go @@ -69,7 +69,8 @@ func (m *LLMProviderFormModel) BuildForm() tea.Cmd { fields = append(fields, m.createPullEnabledField(config)) fields = append(fields, m.createLoadModelsEnabledField(config)) - case LLMProviderDeepSeek, LLMProviderGLM, LLMProviderKimi, LLMProviderQwen, LLMProviderMiniMax: + case LLMProviderDeepSeek, LLMProviderGLM, LLMProviderKimi, LLMProviderQwen, LLMProviderMiniMax, + LLMProviderAIMLAPI: fields = append(fields, m.createBaseURLField(config)) fields = append(fields, m.createAPIKeyField(config)) fields = append(fields, m.createProviderNameField(config)) @@ -367,6 +368,8 @@ func (m *LLMProviderFormModel) GetFormDescription() string { return locale.LLMProviderQwenDesc case LLMProviderMiniMax: return locale.LLMProviderMiniMaxDesc + case LLMProviderAIMLAPI: + return locale.LLMProviderAIMLAPIDesc case LLMProviderCustom: return locale.LLMProviderCustomDesc default: @@ -396,6 +399,8 @@ func (m *LLMProviderFormModel) GetFormName() string { return locale.LLMProviderQwen case LLMProviderMiniMax: return locale.LLMProviderMiniMax + case LLMProviderAIMLAPI: + return locale.LLMProviderAIMLAPI case LLMProviderCustom: return locale.LLMProviderCustom default: @@ -514,7 +519,8 @@ func (m *LLMProviderFormModel) GetCurrentConfiguration() string { locale.LLMFormFieldLoadModelsEnabled, m.GetStyles().Info.Render(config.LoadModelsEnabled.Value))) } - case LLMProviderDeepSeek, LLMProviderGLM, LLMProviderKimi, LLMProviderQwen, LLMProviderMiniMax: + case LLMProviderDeepSeek, LLMProviderGLM, LLMProviderKimi, LLMProviderQwen, LLMProviderMiniMax, + LLMProviderAIMLAPI: if config.BaseURL.Value != "" { sections = append(sections, fmt.Sprintf("• %s: %s", locale.LLMFormFieldBaseURL, m.GetStyles().Info.Render(locale.StatusConfigured))) @@ -593,6 +599,8 @@ func (m *LLMProviderFormModel) GetHelpContent() string { sections = append(sections, locale.LLMFormQwenHelp) case LLMProviderMiniMax: sections = append(sections, locale.LLMFormMiniMaxHelp) + case LLMProviderAIMLAPI: + sections = append(sections, locale.LLMFormAIMLAPIHelp) case LLMProviderCustom: sections = append(sections, locale.LLMFormCustomHelp) } @@ -792,6 +800,8 @@ func (m *LLMProviderFormModel) getDefaultBaseURL() string { return "https://dashscope-us.aliyuncs.com/compatible-mode/v1" case LLMProviderMiniMax: return "https://api.minimax.io/v1" + case LLMProviderAIMLAPI: + return "https://api.aimlapi.com/v1" case LLMProviderCustom: return "http://llm-server:8000" default: diff --git a/backend/cmd/installer/wizard/models/llm_providers.go b/backend/cmd/installer/wizard/models/llm_providers.go index a4c504c3c..eaa454801 100644 --- a/backend/cmd/installer/wizard/models/llm_providers.go +++ b/backend/cmd/installer/wizard/models/llm_providers.go @@ -41,6 +41,7 @@ func (h *LLMProvidersHandler) LoadItems() []ListItem { {ID: LLMProviderKimiScreen}, {ID: LLMProviderQwenScreen}, {ID: LLMProviderMiniMaxScreen}, + {ID: LLMProviderAIMLAPIScreen}, {ID: LLMProviderCustomScreen}, } diff --git a/backend/cmd/installer/wizard/models/types.go b/backend/cmd/installer/wizard/models/types.go index 568f091f8..e088270f3 100644 --- a/backend/cmd/installer/wizard/models/types.go +++ b/backend/cmd/installer/wizard/models/types.go @@ -93,6 +93,7 @@ const ( LLMProviderKimiScreen ScreenID = "llm_provider_form§kimi" LLMProviderQwenScreen ScreenID = "llm_provider_form§qwen" LLMProviderMiniMaxScreen ScreenID = "llm_provider_form§minimax" + LLMProviderAIMLAPIScreen ScreenID = "llm_provider_form§aimlapi" // Summarizer screens SummarizerScreen ScreenID = "summarizer" @@ -145,6 +146,7 @@ const ( LLMProviderKimi LLMProviderID = "kimi" LLMProviderQwen LLMProviderID = "qwen" LLMProviderMiniMax LLMProviderID = "minimax" + LLMProviderAIMLAPI LLMProviderID = "aimlapi" ) // NavigationMsg represents screen navigation requests diff --git a/backend/cmd/installer/wizard/registry/registry.go b/backend/cmd/installer/wizard/registry/registry.go index bcfee91ae..8dc51febb 100644 --- a/backend/cmd/installer/wizard/registry/registry.go +++ b/backend/cmd/installer/wizard/registry/registry.go @@ -60,6 +60,7 @@ func (r *registry) initScreens() { r.screens[models.LLMProviderKimiScreen] = models.NewLLMProviderFormModel(r.controller, r.styles, r.window, models.LLMProviderKimi) r.screens[models.LLMProviderQwenScreen] = models.NewLLMProviderFormModel(r.controller, r.styles, r.window, models.LLMProviderQwen) r.screens[models.LLMProviderMiniMaxScreen] = models.NewLLMProviderFormModel(r.controller, r.styles, r.window, models.LLMProviderMiniMax) + r.screens[models.LLMProviderAIMLAPIScreen] = models.NewLLMProviderFormModel(r.controller, r.styles, r.window, models.LLMProviderAIMLAPI) r.screens[models.LLMProviderOllamaScreen] = models.NewLLMProviderFormModel(r.controller, r.styles, r.window, models.LLMProviderOllama) r.screens[models.LLMProviderCustomScreen] = models.NewLLMProviderFormModel(r.controller, r.styles, r.window, models.LLMProviderCustom) diff --git a/backend/docs/config.md b/backend/docs/config.md index 923a51d86..eb9c4931a 100644 --- a/backend/docs/config.md +++ b/backend/docs/config.md @@ -40,6 +40,7 @@ This document serves as a comprehensive guide to the configuration system in Pen - [Kimi LLM Provider](#kimi-llm-provider) - [Qwen LLM Provider](#qwen-llm-provider) - [MiniMax LLM Provider](#minimax-llm-provider) + - [aimlapi.com LLM Provider](#aimlapicom-llm-provider) - [Custom LLM Provider](#custom-llm-provider) - [Usage Details](#usage-details-6) - [Embedding Settings](#embedding-settings) @@ -837,6 +838,25 @@ There is no `VERTEX_API_KEY` or `GOOGLE_APPLICATION_CREDENTIALS` variable wired **LiteLLM Integration**: Set `MINIMAX_PROVIDER=minimax` to enable model prefixing (e.g., `minimax/MiniMax-M3`) when using LiteLLM proxy with default PentAGI configs. +### aimlapi.com LLM Provider + +| Option | Environment Variable | Default Value | Description | +| ---------------- | -------------------- | ---------------------------- | ------------------------------------------------------- | +| AIMLAPIKey | `AIMLAPI_API_KEY` | *(none)* | aimlapi.com API key for authentication | +| AIMLAPIServerURL | `AIMLAPI_SERVER_URL` | `https://api.aimlapi.com/v1` | aimlapi.com API endpoint URL | +| AIMLAPIProvider | `AIMLAPI_PROVIDER` | *(none)* | Provider name prefix for LiteLLM integration (optional) | + +aimlapi.com is an aggregator: one OpenAI-compatible endpoint fronting 350+ chat +models from many vendors. Model ids are vendor-namespaced (`deepseek/deepseek-v4-flash`) +and are sent verbatim. + +Requests to `api.aimlapi.com` carry `HTTP-Referer`, `X-Title`, `X-AIMLAPI-Source` +and `X-AIMLAPI-Partner-ID` identifying PentAGI as the calling application. Those +headers are scoped to that host: pointing `AIMLAPI_SERVER_URL` at a proxy or a +self-hosted gateway disables them instead of tagging another operator's traffic. + +**LiteLLM Integration**: Set `AIMLAPI_PROVIDER=aimlapi` to enable model prefixing when using LiteLLM proxy with default PentAGI configs. + ### Custom LLM Provider | Option | Environment Variable | Default Value | Description | diff --git a/backend/docs/database.md b/backend/docs/database.md index 5455dbbac..3d706a497 100644 --- a/backend/docs/database.md +++ b/backend/docs/database.md @@ -198,7 +198,7 @@ PostgreSQL enums are migrated explicitly and generated as Go string types in `mo | `MSGLOG_RESULT_FORMAT` | `plain`, `markdown`, `terminal` | | `TERMLOG_TYPE` | `stdin`, `stdout`, `stderr` | | `VECSTORE_ACTION_TYPE` | `retrieve`, `store` | -| `PROVIDER_TYPE` | `openai`, `anthropic`, `gemini`, `bedrock`, `ollama`, `custom`, `deepseek`, `glm`, `kimi`, `qwen`, `minimax` | +| `PROVIDER_TYPE` | `openai`, `anthropic`, `gemini`, `bedrock`, `ollama`, `custom`, `deepseek`, `glm`, `kimi`, `qwen`, `minimax`, `aimlapi` | | `SEARCHENGINE_TYPE` | `google`, `tavily`, `firecrawl`, `traversaal`, `browser`, `duckduckgo`, `perplexity`, `searxng`, `sploitus` | | `PROMPT_TYPE` | Agent/system prompt keys from `primary_agent` through `task_assignment_wrapper` (full list in `models.go`) | diff --git a/backend/migrations/sql/20260903_120000_add_aimlapi_provider.sql b/backend/migrations/sql/20260903_120000_add_aimlapi_provider.sql new file mode 100644 index 000000000..e74d31f6d --- /dev/null +++ b/backend/migrations/sql/20260903_120000_add_aimlapi_provider.sql @@ -0,0 +1,89 @@ +-- +goose Up +-- +goose StatementBegin +-- Add the aimlapi.com provider to the provider_type enum +CREATE TYPE PROVIDER_TYPE_NEW AS ENUM ( + 'openai', + 'anthropic', + 'gemini', + 'bedrock', + 'ollama', + 'custom', + 'deepseek', + 'glm', + 'kimi', + 'qwen', + 'minimax', + 'aimlapi' +); + +-- Update columns to use the new enum type +ALTER TABLE providers + ALTER COLUMN type TYPE PROVIDER_TYPE_NEW USING type::text::PROVIDER_TYPE_NEW; + +ALTER TABLE flows + ALTER COLUMN model_provider_type TYPE PROVIDER_TYPE_NEW USING model_provider_type::text::PROVIDER_TYPE_NEW; + +ALTER TABLE assistants + ALTER COLUMN model_provider_type TYPE PROVIDER_TYPE_NEW USING model_provider_type::text::PROVIDER_TYPE_NEW; + +-- Drop the old type and rename the new one +DROP TYPE PROVIDER_TYPE; +ALTER TYPE PROVIDER_TYPE_NEW RENAME TO PROVIDER_TYPE; + +-- Ensure NOT NULL constraints are preserved +ALTER TABLE providers + ALTER COLUMN type SET NOT NULL; + +ALTER TABLE flows + ALTER COLUMN model_provider_type SET NOT NULL; + +ALTER TABLE assistants + ALTER COLUMN model_provider_type SET NOT NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- Delete providers using the aimlapi.com type before reverting the enum +DELETE FROM providers WHERE type IN ('aimlapi'); +DELETE FROM flows WHERE model_provider_type IN ('aimlapi'); +DELETE FROM assistants WHERE model_provider_type IN ('aimlapi'); + +-- Create new enum type without the aimlapi.com provider +CREATE TYPE PROVIDER_TYPE_NEW AS ENUM ( + 'openai', + 'anthropic', + 'gemini', + 'bedrock', + 'ollama', + 'custom', + 'deepseek', + 'glm', + 'kimi', + 'qwen', + 'minimax' +); + +-- Update columns to use the new enum type +ALTER TABLE providers + ALTER COLUMN type TYPE PROVIDER_TYPE_NEW USING type::text::PROVIDER_TYPE_NEW; + +ALTER TABLE flows + ALTER COLUMN model_provider_type TYPE PROVIDER_TYPE_NEW USING model_provider_type::text::PROVIDER_TYPE_NEW; + +ALTER TABLE assistants + ALTER COLUMN model_provider_type TYPE PROVIDER_TYPE_NEW USING model_provider_type::text::PROVIDER_TYPE_NEW; + +-- Drop the old type and rename the new one +DROP TYPE PROVIDER_TYPE; +ALTER TYPE PROVIDER_TYPE_NEW RENAME TO PROVIDER_TYPE; + +-- Ensure NOT NULL constraints are preserved +ALTER TABLE providers + ALTER COLUMN type SET NOT NULL; + +ALTER TABLE flows + ALTER COLUMN model_provider_type SET NOT NULL; + +ALTER TABLE assistants + ALTER COLUMN model_provider_type SET NOT NULL; +-- +goose StatementEnd diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index 4e7acb3f4..12561fe01 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -160,6 +160,11 @@ type Config struct { MiniMaxServerURL string `env:"MINIMAX_SERVER_URL" envDefault:"https://api.minimax.io/v1"` MiniMaxProvider string `env:"MINIMAX_PROVIDER"` + // === LLM Provider: aimlapi.com === + AIMLAPIKey string `env:"AIMLAPI_API_KEY"` + AIMLAPIServerURL string `env:"AIMLAPI_SERVER_URL" envDefault:"https://api.aimlapi.com/v1"` + AIMLAPIProvider string `env:"AIMLAPI_PROVIDER"` + // === Search Engine: DuckDuckGo === DuckDuckGoEnabled bool `env:"DUCKDUCKGO_ENABLED" envDefault:"true"` DuckDuckGoRegion string `env:"DUCKDUCKGO_REGION"` @@ -456,6 +461,7 @@ func (c *Config) GetSecretPatterns() []patterns.Pattern { {c.KimiAPIKey, "Kimi Key"}, {c.QwenAPIKey, "Qwen Key"}, {c.MiniMaxAPIKey, "MiniMax Key"}, + {c.AIMLAPIKey, "AI/ML API Key"}, {c.GoogleAPIKey, "Google API Key"}, {c.GoogleCXKey, "Google CX Key"}, {c.OAuthGoogleClientID, "Google Client ID"}, diff --git a/backend/pkg/config/config_test.go b/backend/pkg/config/config_test.go index bf8057535..311fcc5d7 100644 --- a/backend/pkg/config/config_test.go +++ b/backend/pkg/config/config_test.go @@ -226,6 +226,7 @@ func TestGetSecretPatterns_AllFields(t *testing.T) { KimiAPIKey: "kimi-123", QwenAPIKey: "qwen-123", MiniMaxAPIKey: "minimax-123", + AIMLAPIKey: "aimlapi-123", GoogleAPIKey: "AIza123", GoogleCXKey: "cx-123", OAuthGoogleClientID: "google-client-id", @@ -242,7 +243,7 @@ func TestGetSecretPatterns_AllFields(t *testing.T) { patterns := cfg.GetSecretPatterns() - expectedCount := 30 + expectedCount := 31 if len(patterns) != expectedCount { t.Errorf("expected %d patterns, got %d", expectedCount, len(patterns)) } @@ -298,6 +299,7 @@ func clearConfigEnv(t *testing.T) { "KIMI_API_KEY", "KIMI_SERVER_URL", "KIMI_PROVIDER", "QWEN_API_KEY", "QWEN_SERVER_URL", "QWEN_PROVIDER", "MINIMAX_API_KEY", "MINIMAX_SERVER_URL", "MINIMAX_PROVIDER", + "AIMLAPI_API_KEY", "AIMLAPI_SERVER_URL", "AIMLAPI_PROVIDER", "DUCKDUCKGO_ENABLED", "DUCKDUCKGO_REGION", "DUCKDUCKGO_SAFESEARCH", "DUCKDUCKGO_TIME_RANGE", "SPLOITUS_ENABLED", "GOOGLE_API_KEY", "GOOGLE_CX_KEY", "GOOGLE_LR_KEY", diff --git a/backend/pkg/database/models.go b/backend/pkg/database/models.go index ba21027a9..3e7bf03b0 100644 --- a/backend/pkg/database/models.go +++ b/backend/pkg/database/models.go @@ -433,6 +433,7 @@ const ( ProviderTypeKimi ProviderType = "kimi" ProviderTypeQwen ProviderType = "qwen" ProviderTypeMinimax ProviderType = "minimax" + ProviderTypeAimlapi ProviderType = "aimlapi" ) func (e *ProviderType) Scan(src interface{}) error { diff --git a/backend/pkg/graph/generated.go b/backend/pkg/graph/generated.go index aac0cce29..11371f764 100644 --- a/backend/pkg/graph/generated.go +++ b/backend/pkg/graph/generated.go @@ -210,6 +210,7 @@ type ComplexityRoot struct { } DefaultProvidersConfig struct { + Aimlapi func(childComplexity int) int Anthropic func(childComplexity int) int Bedrock func(childComplexity int) int Custom func(childComplexity int) int @@ -450,6 +451,7 @@ type ComplexityRoot struct { } ProvidersModelsList struct { + Aimlapi func(childComplexity int) int Anthropic func(childComplexity int) int Bedrock func(childComplexity int) int Custom func(childComplexity int) int @@ -464,6 +466,7 @@ type ComplexityRoot struct { } ProvidersReadinessStatus struct { + Aimlapi func(childComplexity int) int Anthropic func(childComplexity int) int Bedrock func(childComplexity int) int Custom func(childComplexity int) int @@ -1648,6 +1651,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.DefaultPrompts.Tools(childComplexity), true + case "DefaultProvidersConfig.aimlapi": + if e.complexity.DefaultProvidersConfig.Aimlapi == nil { + break + } + + return e.complexity.DefaultProvidersConfig.Aimlapi(childComplexity), true + case "DefaultProvidersConfig.anthropic": if e.complexity.DefaultProvidersConfig.Anthropic == nil { break @@ -2963,6 +2973,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ProvidersConfig.UserDefined(childComplexity), true + case "ProvidersModelsList.aimlapi": + if e.complexity.ProvidersModelsList.Aimlapi == nil { + break + } + + return e.complexity.ProvidersModelsList.Aimlapi(childComplexity), true + case "ProvidersModelsList.anthropic": if e.complexity.ProvidersModelsList.Anthropic == nil { break @@ -3040,6 +3057,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ProvidersModelsList.Qwen(childComplexity), true + case "ProvidersReadinessStatus.aimlapi": + if e.complexity.ProvidersReadinessStatus.Aimlapi == nil { + break + } + + return e.complexity.ProvidersReadinessStatus.Aimlapi(childComplexity), true + case "ProvidersReadinessStatus.anthropic": if e.complexity.ProvidersReadinessStatus.Anthropic == nil { break @@ -14583,6 +14607,61 @@ func (ec *executionContext) fieldContext_DefaultProvidersConfig_minimax(_ contex return fc, nil } +func (ec *executionContext) _DefaultProvidersConfig_aimlapi(ctx context.Context, field graphql.CollectedField, obj *model.DefaultProvidersConfig) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DefaultProvidersConfig_aimlapi(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Aimlapi, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.ProviderConfig) + fc.Result = res + return ec.marshalOProviderConfig2ᚖpentagiᚋpkgᚋgraphᚋmodelᚐProviderConfig(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DefaultProvidersConfig_aimlapi(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DefaultProvidersConfig", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_ProviderConfig_id(ctx, field) + case "name": + return ec.fieldContext_ProviderConfig_name(ctx, field) + case "type": + return ec.fieldContext_ProviderConfig_type(ctx, field) + case "agents": + return ec.fieldContext_ProviderConfig_agents(ctx, field) + case "createdAt": + return ec.fieldContext_ProviderConfig_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_ProviderConfig_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ProviderConfig", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Flow_id(ctx context.Context, field graphql.CollectedField, obj *model.Flow) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Flow_id(ctx, field) if err != nil { @@ -22080,6 +22159,8 @@ func (ec *executionContext) fieldContext_ProvidersConfig_enabled(_ context.Conte return ec.fieldContext_ProvidersReadinessStatus_qwen(ctx, field) case "minimax": return ec.fieldContext_ProvidersReadinessStatus_minimax(ctx, field) + case "aimlapi": + return ec.fieldContext_ProvidersReadinessStatus_aimlapi(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ProvidersReadinessStatus", field.Name) }, @@ -22148,6 +22229,8 @@ func (ec *executionContext) fieldContext_ProvidersConfig_default(_ context.Conte return ec.fieldContext_DefaultProvidersConfig_qwen(ctx, field) case "minimax": return ec.fieldContext_DefaultProvidersConfig_minimax(ctx, field) + case "aimlapi": + return ec.fieldContext_DefaultProvidersConfig_aimlapi(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type DefaultProvidersConfig", field.Name) }, @@ -22271,6 +22354,8 @@ func (ec *executionContext) fieldContext_ProvidersConfig_models(_ context.Contex return ec.fieldContext_ProvidersModelsList_qwen(ctx, field) case "minimax": return ec.fieldContext_ProvidersModelsList_minimax(ctx, field) + case "aimlapi": + return ec.fieldContext_ProvidersModelsList_aimlapi(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ProvidersModelsList", field.Name) }, @@ -22892,6 +22977,61 @@ func (ec *executionContext) fieldContext_ProvidersModelsList_minimax(_ context.C return fc, nil } +func (ec *executionContext) _ProvidersModelsList_aimlapi(ctx context.Context, field graphql.CollectedField, obj *model.ProvidersModelsList) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ProvidersModelsList_aimlapi(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Aimlapi, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*model.ModelConfig) + fc.Result = res + return ec.marshalOModelConfig2ᚕᚖpentagiᚋpkgᚋgraphᚋmodelᚐModelConfigᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ProvidersModelsList_aimlapi(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ProvidersModelsList", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_ModelConfig_name(ctx, field) + case "description": + return ec.fieldContext_ModelConfig_description(ctx, field) + case "releaseDate": + return ec.fieldContext_ModelConfig_releaseDate(ctx, field) + case "thinking": + return ec.fieldContext_ModelConfig_thinking(ctx, field) + case "reasoning": + return ec.fieldContext_ModelConfig_reasoning(ctx, field) + case "price": + return ec.fieldContext_ModelConfig_price(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ModelConfig", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _ProvidersReadinessStatus_openai(ctx context.Context, field graphql.CollectedField, obj *model.ProvidersReadinessStatus) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ProvidersReadinessStatus_openai(ctx, field) if err != nil { @@ -23376,6 +23516,50 @@ func (ec *executionContext) fieldContext_ProvidersReadinessStatus_minimax(_ cont return fc, nil } +func (ec *executionContext) _ProvidersReadinessStatus_aimlapi(ctx context.Context, field graphql.CollectedField, obj *model.ProvidersReadinessStatus) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ProvidersReadinessStatus_aimlapi(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Aimlapi, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ProvidersReadinessStatus_aimlapi(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ProvidersReadinessStatus", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Query_providers(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_providers(ctx, field) if err != nil { @@ -38795,6 +38979,8 @@ func (ec *executionContext) _DefaultProvidersConfig(ctx context.Context, sel ast out.Values[i] = ec._DefaultProvidersConfig_qwen(ctx, field, obj) case "minimax": out.Values[i] = ec._DefaultProvidersConfig_minimax(ctx, field, obj) + case "aimlapi": + out.Values[i] = ec._DefaultProvidersConfig_aimlapi(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -40437,6 +40623,8 @@ func (ec *executionContext) _ProvidersModelsList(ctx context.Context, sel ast.Se out.Values[i] = ec._ProvidersModelsList_qwen(ctx, field, obj) case "minimax": out.Values[i] = ec._ProvidersModelsList_minimax(ctx, field, obj) + case "aimlapi": + out.Values[i] = ec._ProvidersModelsList_aimlapi(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -40526,6 +40714,11 @@ func (ec *executionContext) _ProvidersReadinessStatus(ctx context.Context, sel a if out.Values[i] == graphql.Null { out.Invalids++ } + case "aimlapi": + out.Values[i] = ec._ProvidersReadinessStatus_aimlapi(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/backend/pkg/graph/model/models_gen.go b/backend/pkg/graph/model/models_gen.go index 37872d2b1..95278535a 100644 --- a/backend/pkg/graph/model/models_gen.go +++ b/backend/pkg/graph/model/models_gen.go @@ -200,6 +200,7 @@ type DefaultProvidersConfig struct { Kimi *ProviderConfig `json:"kimi,omitempty"` Qwen *ProviderConfig `json:"qwen,omitempty"` Minimax *ProviderConfig `json:"minimax,omitempty"` + Aimlapi *ProviderConfig `json:"aimlapi,omitempty"` } type Flow struct { @@ -417,6 +418,7 @@ type ProvidersModelsList struct { Kimi []*ModelConfig `json:"kimi,omitempty"` Qwen []*ModelConfig `json:"qwen,omitempty"` Minimax []*ModelConfig `json:"minimax,omitempty"` + Aimlapi []*ModelConfig `json:"aimlapi,omitempty"` } type ProvidersReadinessStatus struct { @@ -431,6 +433,7 @@ type ProvidersReadinessStatus struct { Kimi bool `json:"kimi"` Qwen bool `json:"qwen"` Minimax bool `json:"minimax"` + Aimlapi bool `json:"aimlapi"` } type Query struct { @@ -1196,6 +1199,7 @@ const ( ProviderTypeKimi ProviderType = "kimi" ProviderTypeQwen ProviderType = "qwen" ProviderTypeMinimax ProviderType = "minimax" + ProviderTypeAimlapi ProviderType = "aimlapi" ) var AllProviderType = []ProviderType{ @@ -1210,11 +1214,12 @@ var AllProviderType = []ProviderType{ ProviderTypeKimi, ProviderTypeQwen, ProviderTypeMinimax, + ProviderTypeAimlapi, } func (e ProviderType) IsValid() bool { switch e { - case ProviderTypeOpenai, ProviderTypeAnthropic, ProviderTypeGemini, ProviderTypeBedrock, ProviderTypeOllama, ProviderTypeCustom, ProviderTypeDeepseek, ProviderTypeGlm, ProviderTypeKimi, ProviderTypeQwen, ProviderTypeMinimax: + case ProviderTypeOpenai, ProviderTypeAnthropic, ProviderTypeGemini, ProviderTypeBedrock, ProviderTypeOllama, ProviderTypeCustom, ProviderTypeDeepseek, ProviderTypeGlm, ProviderTypeKimi, ProviderTypeQwen, ProviderTypeMinimax, ProviderTypeAimlapi: return true } return false diff --git a/backend/pkg/graph/schema.graphqls b/backend/pkg/graph/schema.graphqls index 863d70115..894a8f5fe 100644 --- a/backend/pkg/graph/schema.graphqls +++ b/backend/pkg/graph/schema.graphqls @@ -23,6 +23,7 @@ enum ProviderType { kimi qwen minimax + aimlapi } # Reasoning effort levels for advanced AI models @@ -738,6 +739,7 @@ type ProvidersModelsList { kimi: [ModelConfig!] qwen: [ModelConfig!] minimax: [ModelConfig!] + aimlapi: [ModelConfig!] } # Provider availability status @@ -753,6 +755,7 @@ type ProvidersReadinessStatus { kimi: Boolean! qwen: Boolean! minimax: Boolean! + aimlapi: Boolean! } # Default provider configurations @@ -768,6 +771,7 @@ type DefaultProvidersConfig { kimi: ProviderConfig qwen: ProviderConfig minimax: ProviderConfig + aimlapi: ProviderConfig } # Complete providers configuration diff --git a/backend/pkg/graph/schema.resolvers.go b/backend/pkg/graph/schema.resolvers.go index 90a84c95d..6f10f0858 100644 --- a/backend/pkg/graph/schema.resolvers.go +++ b/backend/pkg/graph/schema.resolvers.go @@ -15,6 +15,7 @@ import ( "pentagi/pkg/database/converter" "pentagi/pkg/flowfiles" "pentagi/pkg/graph/model" + "pentagi/pkg/providers/aimlapi" "pentagi/pkg/providers/anthropic" "pentagi/pkg/providers/bedrock" "pentagi/pkg/providers/deepseek" @@ -2165,6 +2166,11 @@ func (r *queryResolver) SettingsProviders(ctx context.Context) (*model.Providers if models, err := minimax.DefaultModels(); err == nil { config.Models.Minimax = converter.ConvertModels(models, prvtype.ReasoningProvider()) } + case provider.ProviderAIMLAPI: + config.Default.Aimlapi = mpcfg + if models, err := aimlapi.DefaultModels(); err == nil { + config.Models.Aimlapi = converter.ConvertModels(models, prvtype.ReasoningProvider()) + } } } @@ -2202,6 +2208,8 @@ func (r *queryResolver) SettingsProviders(ctx context.Context) (*model.Providers config.Enabled.Qwen = true case provider.ProviderMiniMax: config.Enabled.Minimax = true + case provider.ProviderAIMLAPI: + config.Enabled.Aimlapi = true } } diff --git a/backend/pkg/providers/aimlapi/aimlapi.go b/backend/pkg/providers/aimlapi/aimlapi.go new file mode 100644 index 000000000..2f80ef3d8 --- /dev/null +++ b/backend/pkg/providers/aimlapi/aimlapi.go @@ -0,0 +1,88 @@ +package aimlapi + +import ( + "embed" + + "pentagi/pkg/config" + "pentagi/pkg/providers/openaicompat" + "pentagi/pkg/providers/pconfig" + "pentagi/pkg/providers/provider" + "pentagi/pkg/system" + + "github.com/vxcontrol/langchaingo/llms" +) + +//go:embed config.yml models.yml +var configFS embed.FS + +// AIMLAPIAgentModel is the fallback model used when an agent role resolves to no +// model of its own. Model ids are namespaced by upstream vendor and must be sent +// verbatim; the gateway also accepts short aliases, but an alias can resolve to a +// different model than its name suggests, so the canonical id is used everywhere. +const AIMLAPIAgentModel = "deepseek/deepseek-v4-flash" + +// AIMLAPIToolCallIDTemplate is deliberately empty. AI/ML API multiplexes many +// upstream vendors behind one endpoint, so the tool-call id format follows the +// model actually serving the request rather than the gateway. Leaving it empty +// makes the shared detector derive the template per model instead of asserting a +// single format for the whole provider. +const AIMLAPIToolCallIDTemplate = "" + +func BuildProviderConfig(configData []byte) (*pconfig.ProviderConfig, error) { + defaultOptions := []llms.CallOption{ + llms.WithModel(AIMLAPIAgentModel), + llms.WithN(1), + llms.WithMaxTokens(4000), + } + + providerConfig, err := pconfig.LoadConfigData(configData, defaultOptions) + if err != nil { + return nil, err + } + + return providerConfig, nil +} + +func DefaultProviderConfig() (*pconfig.ProviderConfig, error) { + configData, err := configFS.ReadFile("config.yml") + if err != nil { + return nil, err + } + + return BuildProviderConfig(configData) +} + +func DefaultModels() (pconfig.ModelsConfig, error) { + configData, err := configFS.ReadFile("models.yml") + if err != nil { + return nil, err + } + + return pconfig.LoadModelsConfigData(configData) +} + +func New( + cfg *config.Config, + providerName provider.ProviderName, + providerConfig *pconfig.ProviderConfig, +) (provider.Provider, error) { + httpClient, err := system.GetHTTPClient(cfg) + if err != nil { + return nil, err + } + + models, err := DefaultModels() + if err != nil { + return nil, err + } + + return openaicompat.New(openaicompat.Spec{ + Type: provider.ProviderAIMLAPI, + Model: AIMLAPIAgentModel, + ToolCallIDTemplate: AIMLAPIToolCallIDTemplate, + APIKey: cfg.AIMLAPIKey, + ServerURL: cfg.AIMLAPIServerURL, + Prefix: cfg.AIMLAPIProvider, + PreserveReasoning: true, + }, withAttribution(httpClient, cfg.AIMLAPIServerURL), models, providerName, providerConfig) +} diff --git a/backend/pkg/providers/aimlapi/aimlapi_test.go b/backend/pkg/providers/aimlapi/aimlapi_test.go new file mode 100644 index 000000000..583ce825d --- /dev/null +++ b/backend/pkg/providers/aimlapi/aimlapi_test.go @@ -0,0 +1,370 @@ +package aimlapi + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "regexp" + "testing" + + "pentagi/pkg/config" + "pentagi/pkg/providers/pconfig" + "pentagi/pkg/providers/provider" + + "github.com/vxcontrol/langchaingo/llms" +) + +func testConfig(serverURL string) *config.Config { + return &config.Config{ + AIMLAPIKey: "test-key", + AIMLAPIServerURL: serverURL, + } +} + +func newTestProvider(t *testing.T, cfg *config.Config) provider.Provider { + t.Helper() + + providerConfig, err := DefaultProviderConfig() + if err != nil { + t.Fatalf("Failed to create provider config: %v", err) + } + + prov, err := New(cfg, provider.DefaultProviderNameAIMLAPI, providerConfig) + if err != nil { + t.Fatalf("Failed to create provider: %v", err) + } + + return prov +} + +func TestConfigLoading(t *testing.T) { + prov := newTestProvider(t, testConfig("https://api.aimlapi.com/v1")) + + if len(prov.GetRawConfig()) == 0 { + t.Fatal("Raw config should not be empty") + } + + if prov.GetProviderConfig() == nil { + t.Fatal("Provider config should not be nil") + } + + for _, agentType := range pconfig.AllAgentTypes { + if model := prov.Model(agentType); model == "" { + t.Errorf("Agent type %v should have a model assigned", agentType) + } + } + + for _, agentType := range pconfig.AllAgentTypes { + priceInfo := prov.GetPriceInfo(agentType) + if priceInfo == nil { + t.Errorf("Agent type %v should have price information", agentType) + continue + } + if priceInfo.Input <= 0 || priceInfo.Output <= 0 { + t.Errorf("Agent type %v should have positive input (%f) and output (%f) prices", + agentType, priceInfo.Input, priceInfo.Output) + } + } +} + +func TestProviderType(t *testing.T) { + prov := newTestProvider(t, testConfig("https://api.aimlapi.com/v1")) + + if prov.Type() != provider.ProviderAIMLAPI { + t.Errorf("Expected provider type %q, got %q", provider.ProviderAIMLAPI, prov.Type()) + } + if prov.Name() != provider.DefaultProviderNameAIMLAPI { + t.Errorf("Expected provider name %q, got %q", provider.DefaultProviderNameAIMLAPI, prov.Name()) + } +} + +func TestMissingAPIKey(t *testing.T) { + providerConfig, err := DefaultProviderConfig() + if err != nil { + t.Fatalf("Failed to create provider config: %v", err) + } + + cfg := &config.Config{AIMLAPIServerURL: "https://api.aimlapi.com/v1"} + if _, err := New(cfg, provider.DefaultProviderNameAIMLAPI, providerConfig); err == nil { + t.Fatal("Expected error when API key is missing") + } +} + +// TestModelsAreCatalogIds guards the models the config assigns to agent roles +// against the models.yml catalog. A model id that is not in the catalog is either +// a typo or an id retired upstream; either way the agent using it 404s at runtime +// with nothing in the test suite to catch it. +func TestModelsAreCatalogIds(t *testing.T) { + models, err := DefaultModels() + if err != nil { + t.Fatalf("Failed to load models: %v", err) + } + + known := make(map[string]struct{}, len(models)) + for _, model := range models { + known[model.Name] = struct{}{} + } + + prov := newTestProvider(t, testConfig("https://api.aimlapi.com/v1")) + for _, agentType := range pconfig.AllAgentTypes { + model := prov.Model(agentType) + if _, ok := known[model]; !ok { + t.Errorf("Agent type %v uses model %q, which is not in models.yml", agentType, model) + } + } +} + +// TestPartnerIDShape asserts the gateway's partner-id contract, /^part_[A-Za-z0-9]{1,64}$/. +// A malformed id is not rejected by the API — it is silently treated as untagged +// traffic — so a typo here is invisible at runtime and only a test can catch it. +func TestPartnerIDShape(t *testing.T) { + pattern := regexp.MustCompile(`^part_[A-Za-z0-9]{1,64}$`) + if !pattern.MatchString(attributionPartnerID) { + t.Errorf("Partner ID %q does not match %s", attributionPartnerID, pattern) + } + + if got := attributionHeaders()["X-AIMLAPI-Partner-ID"]; got != attributionPartnerID { + t.Errorf("Header carries partner ID %q, want %q", got, attributionPartnerID) + } +} + +// TestAttributionHeadersAreNotShared catches the classic mistake of handing every +// provider instance the same map and letting one mutation leak into all of them. +func TestAttributionHeadersAreNotShared(t *testing.T) { + first := attributionHeaders() + first["X-AIMLAPI-Partner-ID"] = "part_mutated" + + if got := attributionHeaders()["X-AIMLAPI-Partner-ID"]; got != attributionPartnerID { + t.Errorf("Mutating one header map changed the next one: got %q", got) + } +} + +func TestAttributionHost(t *testing.T) { + tests := []struct { + baseURL string + want string + }{ + {"https://api.aimlapi.com/v1", "api.aimlapi.com"}, + {"https://API.AIMLAPI.COM/v1", "api.aimlapi.com"}, + {"https://aimlapi.com/v1", "aimlapi.com"}, + // A proxy or self-hosted gateway that fronts us must not be tagged: its + // traffic is not this integration's to claim. + {"https://litellm.internal:4000/v1", ""}, + {"http://llm-server:8000/v1", ""}, + {"https://notaimlapi.com/v1", ""}, + {"https://aimlapi.com.evil.example/v1", ""}, + {"", ""}, + } + + for _, tt := range tests { + if got := attributionHost(tt.baseURL); got != tt.want { + t.Errorf("attributionHost(%q) = %q, want %q", tt.baseURL, got, tt.want) + } + } +} + +func TestAttributionTransportTagsOurOriginOnly(t *testing.T) { + var seen http.Header + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + seen = r.Header.Clone() + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + transport := &attributionTransport{ + host: "127.0.0.1", + headers: attributionHeaders(), + } + client := &http.Client{Transport: transport} + + resp, err := client.Get(server.URL) + if err != nil { + t.Fatalf("request failed: %v", err) + } + resp.Body.Close() + + for key, want := range attributionHeaders() { + if got := seen.Get(key); got != want { + t.Errorf("Header %s = %q, want %q", key, got, want) + } + } + + // Same client, a host that is not ours: nothing may be attached. + transport.host = "api.aimlapi.com" + resp, err = client.Get(server.URL) + if err != nil { + t.Fatalf("request failed: %v", err) + } + resp.Body.Close() + + for key := range attributionHeaders() { + if got := seen.Get(key); got != "" { + t.Errorf("Header %s leaked to a foreign host with value %q", key, got) + } + } +} + +func TestAttributionDoesNotOverrideCallerHeaders(t *testing.T) { + var seen http.Header + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + seen = r.Header.Clone() + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + client := &http.Client{Transport: &attributionTransport{host: "127.0.0.1", headers: attributionHeaders()}} + + req, err := http.NewRequest(http.MethodGet, server.URL, nil) + if err != nil { + t.Fatalf("failed to build request: %v", err) + } + req.Header.Set("X-Title", "operator-supplied") + + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + resp.Body.Close() + + if got := seen.Get("X-Title"); got != "operator-supplied" { + t.Errorf("Attribution overwrote a caller header: X-Title = %q", got) + } + if got := seen.Get("X-AIMLAPI-Partner-ID"); got != attributionPartnerID { + t.Errorf("Attribution dropped the partner id when a caller header was present: %q", got) + } + if req.Header.Get("X-AIMLAPI-Partner-ID") != "" { + t.Error("RoundTrip mutated the request it was given instead of tagging a clone") + } +} + +func TestWithAttributionDoesNotMutateSharedClient(t *testing.T) { + base := &http.Client{} + + tagged := withAttribution(base, "https://api.aimlapi.com/v1") + if tagged == base { + t.Fatal("withAttribution returned the client it was given instead of a copy") + } + if base.Transport != nil { + t.Error("withAttribution mutated the shared HTTP client's transport") + } + + // A non-aimlapi base URL leaves the client untouched, attribution disabled. + if got := withAttribution(base, "http://llm-server:8000/v1"); got != base { + t.Error("withAttribution wrapped a client pointed at a foreign endpoint") + } +} + +// TestRequestOmitsUnsetSamplingParams is a regression test for a live failure +// mode of the gateway: it rejects `temperature: null`, `top_p: null` and +// `seed: null` with HTTP 400, while accepting them as absent. Clients that +// serialise unset optionals as explicit nulls therefore fail on every real call +// while a mocked test suite stays green. This drives the actual provider and +// inspects the bytes on the wire. +func TestRequestOmitsUnsetSamplingParams(t *testing.T) { + var body map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + raw, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("failed to read request body: %v", err) + } + if err := json.Unmarshal(raw, &body); err != nil { + t.Errorf("failed to decode request body: %v", err) + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"1","object":"chat.completion","choices":[{"index":0,` + + `"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],` + + `"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`)) + })) + defer server.Close() + + prov := newTestProvider(t, testConfig(server.URL)) + + // The searcher role sets temperature but no top_p and no seed, so those two + // must be absent from the payload rather than present and null. + if _, err := prov.Call(context.Background(), pconfig.OptionsTypeSearcher, "ping"); err != nil { + t.Fatalf("Call failed: %v", err) + } + + for key, value := range body { + if value == nil { + t.Errorf("Request serialised %q as null; the gateway rejects null sampling params", key) + } + } + for _, key := range []string{"top_p", "seed"} { + if _, present := body[key]; present { + t.Errorf("Request carries %q although the agent config leaves it unset", key) + } + } + if _, present := body["temperature"]; !present { + t.Error("Request dropped temperature although the agent config sets it") + } +} + +// TestToolsAreOmittedNotNulledOnFollowUpTurn is the agent-loop half of the same +// gateway behaviour: `"tools": null` is a 400. A turn that carries tools followed +// by a turn that clears them is the ordinary shape of an agent loop, so a client +// that nulls the cleared field succeeds on turn one and fails on every turn two. +func TestToolsAreOmittedNotNulledOnFollowUpTurn(t *testing.T) { + var bodies []map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + raw, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("failed to read request body: %v", err) + } + + var body map[string]any + if err := json.Unmarshal(raw, &body); err != nil { + t.Errorf("failed to decode request body: %v", err) + } + bodies = append(bodies, body) + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"1","object":"chat.completion","choices":[{"index":0,` + + `"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],` + + `"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`)) + })) + defer server.Close() + + prov := newTestProvider(t, testConfig(server.URL)) + chain := []llms.MessageContent{llms.TextParts(llms.ChatMessageTypeHuman, "scan 10.0.0.7")} + tools := []llms.Tool{{ + Type: "function", + Function: &llms.FunctionDefinition{ + Name: "get_open_ports", + Description: "Return the list of open TCP ports on a host.", + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{"host": map[string]any{"type": "string"}}, + }, + }, + }} + + ctx := context.Background() + if _, err := prov.CallWithTools(ctx, pconfig.OptionsTypePentester, chain, tools, nil); err != nil { + t.Fatalf("first turn failed: %v", err) + } + if _, err := prov.CallWithTools(ctx, pconfig.OptionsTypePentester, chain, nil, nil); err != nil { + t.Fatalf("second turn failed: %v", err) + } + + if len(bodies) != 2 { + t.Fatalf("expected 2 requests, got %d", len(bodies)) + } + if _, present := bodies[0]["tools"]; !present { + t.Error("First turn dropped the tools it was given") + } + if _, present := bodies[1]["tools"]; present { + t.Errorf("Second turn serialised cleared tools as %v; the gateway rejects a null tools field", + bodies[1]["tools"]) + } + for i, body := range bodies { + for key, value := range body { + if value == nil { + t.Errorf("Turn %d serialised %q as null; the gateway rejects null for most fields", i+1, key) + } + } + } +} diff --git a/backend/pkg/providers/aimlapi/attribution.go b/backend/pkg/providers/aimlapi/attribution.go new file mode 100644 index 000000000..077c92bcb --- /dev/null +++ b/backend/pkg/providers/aimlapi/attribution.go @@ -0,0 +1,106 @@ +package aimlapi + +import ( + "net/http" + "net/url" + "strings" +) + +// aimlapiDomain is the only domain that may receive the attribution headers below. +const aimlapiDomain = "aimlapi.com" + +// Attribution headers for AI/ML API's partner programme. +// +// HTTP-Referer / X-Title are the OpenRouter convention and name the *calling* +// application — PentAGI — not the gateway. X-AIMLAPI-Source and +// X-AIMLAPI-Partner-ID are AI/ML API's own channel attribution; a malformed +// partner id is silently treated as untagged traffic rather than rejected, +// which is why its shape is asserted in a test instead of at runtime. +const ( + attributionReferer = "https://github.com/vxcontrol/pentagi" + attributionTitle = "PentAGI" + attributionSource = "agent/pentagi" + attributionPartnerID = "part_6bffrRIYBS8OtYbQhsEPi0SS" +) + +// attributionHeaders builds a fresh map per call so no caller can mutate a +// package-level map shared by every provider instance. +func attributionHeaders() map[string]string { + return map[string]string{ + "HTTP-Referer": attributionReferer, + "X-Title": attributionTitle, + "X-AIMLAPI-Source": attributionSource, + "X-AIMLAPI-Partner-ID": attributionPartnerID, + } +} + +// attributionHost returns the host allowed to receive attribution headers for +// baseURL, or "" to disable attribution entirely. +// +// Only aimlapi.com and its subdomains qualify. AIMLAPI_SERVER_URL can legally +// point at a LiteLLM proxy or a self-hosted gateway that fronts AI/ML API, and +// tagging those would attribute another operator's traffic to this integration, +// so anything else disables the headers rather than forwarding them. +func attributionHost(baseURL string) string { + parsed, err := url.Parse(baseURL) + if err != nil { + return "" + } + + host := strings.ToLower(parsed.Hostname()) + if host == aimlapiDomain || strings.HasSuffix(host, "."+aimlapiDomain) { + return host + } + + return "" +} + +// attributionTransport tags requests bound for host with the attribution headers. +type attributionTransport struct { + base http.RoundTripper + host string + headers map[string]string +} + +func (t *attributionTransport) RoundTrip(req *http.Request) (*http.Response, error) { + base := t.base + if base == nil { + base = http.DefaultTransport + } + + // Re-checked per request rather than once at construction: a redirect can + // move a request off our origin, and the headers must not follow it. + if t.host == "" || !strings.EqualFold(req.URL.Hostname(), t.host) { + return base.RoundTrip(req) + } + + // RoundTrip must not modify the request it was handed, so tag a clone. Values + // already present win, so an operator-supplied header is never overwritten. + tagged := req.Clone(req.Context()) + for key, value := range t.headers { + if tagged.Header.Get(key) == "" { + tagged.Header.Set(key, value) + } + } + + return base.RoundTrip(tagged) +} + +// withAttribution returns a client that tags AI/ML API traffic. The client it is +// given is copied, never mutated: it is the process-wide client built from proxy +// and TLS settings, and every other provider is handed the same shape. +func withAttribution(client *http.Client, baseURL string) *http.Client { + host := attributionHost(baseURL) + if client == nil || host == "" { + return client + } + + tagged := *client + tagged.Transport = &attributionTransport{ + base: client.Transport, + host: host, + headers: attributionHeaders(), + } + + return &tagged +} diff --git a/backend/pkg/providers/aimlapi/config.yml b/backend/pkg/providers/aimlapi/config.yml new file mode 100644 index 000000000..0407af2da --- /dev/null +++ b/backend/pkg/providers/aimlapi/config.yml @@ -0,0 +1,200 @@ +# AI/ML API agent configuration. +# +# Strategy mirrors examples/configs/openrouter.provider.yml, because the two are +# the same kind of backend: one OpenAI-compatible endpoint fronting many vendors. +# Models are picked per agent role for price/intelligence fit, drawn from four +# families (DeepSeek, GLM, MiniMax, Kimi) and deliberately excluding the +# OpenAI/Anthropic/Google models the gateway also carries — this product runs +# offensive-security workloads and those vendors' guardrails false-positive on +# legitimate exploit-development content. +# +# - deepseek/deepseek-v4-flash : utility tier (simple/simple_json/reflector/ +# searcher/enricher) and pentester. The pentester keeps thinking on for tool-call +# quality but stays on the flash tier — it is the most frequently invoked +# agent in a flow. +# - z-ai/glm-5-turbo : primary_agent/assistant — tool-invocation-optimized, +# which is what the orchestration loop needs. +# - zhipu/glm-5.2 : generator/refiner — flagship GLM, the only model in +# this roster whose catalog entry documents graduated reasoning effort, used +# at max effort for the most consequential decisions in a flow. +# - minimax/minimax-m3 : adviser — adaptive thinking, toggled through +# extra_body.thinking.type (MiniMax exposes no reasoning_effort). NOT used for +# the enricher, unlike the OpenRouter config: through this gateway m3 ignores +# both reasoning_effort:"none" and extra_body.thinking.type:"disabled" and +# still thinks, returning the chain inline in `content` wrapped in +# tags rather than in reasoning_content. The enricher is the one role that +# explicitly wants thinking off, so it stays on deepseek-v4-flash, which does +# honour it (verified by ctester's reasoning_off capability test). +# - moonshot/kimi-k2-7-code : coder/installer — coding-specialized, also covers +# the installer's build/environment scripting, keeping the roster small. +# +# Prices are USD per 1M tokens from the gateway catalog on 2026-09-03 and match +# models.yml. Ids are canonical (never aliases) and each was checked against both +# the `id` and `aliases` sets of `GET /v1/models?include=all`. +# +# Reasoning notes: +# - `reasoning: {mode: off}` maps to langchaingo's disable wire for the true +# fast/deterministic utility agents. +# - `reasoning: {effort: ...}` is set only where the model documents graduated +# effort support (glm-5.2). glm-5-turbo and kimi-k2-7-code do not, so their +# own hybrid-thinking defaults are left alone. +# - minimax-m3's depth is set via extra_body.thinking.type, which the gateway +# forwards verbatim to the MiniMax backend. + +simple: + model: "deepseek/deepseek-v4-flash" + temperature: 1.0 + n: 1 + max_tokens: 8192 + reasoning: + mode: off + price: + input: 0.182 + output: 0.364 + cache_read: 0.00364 + +simple_json: + model: "deepseek/deepseek-v4-flash" + temperature: 1.0 + n: 1 + max_tokens: 4096 + json: true + reasoning: + mode: off + price: + input: 0.182 + output: 0.364 + cache_read: 0.00364 + +primary_agent: + model: "z-ai/glm-5-turbo" + temperature: 1.0 + top_p: 0.95 + n: 1 + max_tokens: 16384 + price: + input: 1.56 + output: 5.2 + cache_read: 0.312 + +assistant: + model: "z-ai/glm-5-turbo" + temperature: 1.0 + top_p: 0.95 + n: 1 + max_tokens: 16384 + price: + input: 1.56 + output: 5.2 + cache_read: 0.312 + +generator: + model: "zhipu/glm-5.2" + temperature: 1.0 + top_p: 0.95 + n: 1 + max_tokens: 32768 + reasoning: + effort: max + price: + input: 1.82 + output: 5.72 + cache_read: 0.338 + +refiner: + model: "zhipu/glm-5.2" + temperature: 1.0 + top_p: 0.95 + n: 1 + max_tokens: 32768 + reasoning: + effort: max + price: + input: 1.82 + output: 5.72 + cache_read: 0.338 + +adviser: + model: "minimax/minimax-m3" + temperature: 1.0 + top_p: 0.95 + top_k: 40 + n: 1 + max_tokens: 16384 + extra_body: + thinking: + type: adaptive + price: + input: 0.39 + output: 1.56 + cache_read: 0.078 + +reflector: + model: "deepseek/deepseek-v4-flash" + temperature: 1.0 + n: 1 + max_tokens: 4096 + reasoning: + mode: off + price: + input: 0.182 + output: 0.364 + cache_read: 0.00364 + +searcher: + model: "deepseek/deepseek-v4-flash" + temperature: 1.0 + n: 1 + max_tokens: 4096 + reasoning: + mode: off + price: + input: 0.182 + output: 0.364 + cache_read: 0.00364 + +enricher: + model: "deepseek/deepseek-v4-flash" + temperature: 1.0 + n: 1 + max_tokens: 8192 + reasoning: + mode: off + price: + input: 0.182 + output: 0.364 + cache_read: 0.00364 + +coder: + model: "moonshot/kimi-k2-7-code" + temperature: 1.0 + top_p: 0.95 + n: 1 + max_tokens: 20480 + price: + input: 1.235 + output: 5.2 + cache_read: 0.247 + +installer: + model: "moonshot/kimi-k2-7-code" + temperature: 1.0 + top_p: 0.95 + n: 1 + max_tokens: 16384 + price: + input: 1.235 + output: 5.2 + cache_read: 0.247 + +pentester: + model: "deepseek/deepseek-v4-flash" + temperature: 1.0 + n: 1 + max_tokens: 16384 + reasoning: + effort: high + price: + input: 0.182 + output: 0.364 + cache_read: 0.00364 diff --git a/backend/pkg/providers/aimlapi/models.yml b/backend/pkg/providers/aimlapi/models.yml new file mode 100644 index 000000000..55699d45f --- /dev/null +++ b/backend/pkg/providers/aimlapi/models.yml @@ -0,0 +1,71 @@ +# AI/ML API (https://aimlapi.com) model catalog. +# +# Ids are the gateway's canonical, vendor-namespaced ids and are sent verbatim. +# The gateway also publishes short aliases (`deepseek-v4-flash`), but an alias can +# resolve to a different model than its name implies — `deepseek/deepseek-chat`, +# for instance, is both its own listed model and an alias of deepseek-v4-flash — +# so only canonical ids appear here. +# +# Prices are USD per 1M tokens as published by +# `GET https://api.aimlapi.com/v1/models?include=all` on 2026-09-03. That endpoint +# omits pricing, capabilities and modalities unless `include=all` is passed. Every +# id below was checked against both the `id` and `aliases` sets of that response. +# +# The roster deliberately avoids OpenAI/Anthropic/Google models even though the +# gateway carries them: PentAGI runs offensive-security workloads, and those +# vendors' guardrails false-positive on legitimate exploit development. This is +# the same reasoning already applied in examples/configs/openrouter.provider.yml. + +# --- DeepSeek: utility tier and the provider-wide fallback model --- +- name: deepseek/deepseek-v4-flash + description: DeepSeek V4 Flash via AI/ML API - cost-efficient workhorse with hybrid thinking, tool calling, parallel tool calls and structured output. 1M context, 384K max output. Default model for the utility agents. + thinking: true + price: + input: 0.182 + output: 0.364 + cache_read: 0.00364 + +- name: deepseek/deepseek-v4-pro + description: DeepSeek V4 Pro via AI/ML API - higher-tier reasoning model for complex logic, exploit analysis and long multi-step planning. 1M context, 384K max output. + thinking: true + price: + input: 0.5655 + output: 1.131 + cache_read: 0.0047125 + +# --- Z.AI / GLM: orchestration and plan generation --- +- name: z-ai/glm-5-turbo + description: GLM 5 Turbo via AI/ML API - tool-invocation-optimized model for long-chain execution; used for the orchestrator and assistant roles. 262K context, 131K max output. + thinking: true + price: + input: 1.56 + output: 5.2 + cache_read: 0.312 + +- name: zhipu/glm-5.2 + description: GLM 5.2 via AI/ML API - flagship GLM with graduated reasoning effort (high/max), used for plan generation and refinement. 1M context, 131K max output. + thinking: true + reasoning: + efforts: [high, max] + price: + input: 1.82 + output: 5.72 + cache_read: 0.338 + +# --- MiniMax: advisory --- +- name: minimax/minimax-m3 + description: MiniMax M3 via AI/ML API - long-context agentic model with adaptive thinking toggled through extra_body.thinking.type rather than reasoning_effort. 524K context. + thinking: true + price: + input: 0.39 + output: 1.56 + cache_read: 0.078 + +# --- Moonshot / Kimi: code generation and environment setup --- +- name: moonshot/kimi-k2-7-code + description: Kimi K2.7 Code via AI/ML API - coding-specialized model with strong long-context programming performance; covers the coder and installer roles. 262K context, 260K max output. + thinking: false + price: + input: 1.235 + output: 5.2 + cache_read: 0.247 diff --git a/backend/pkg/providers/provider/provider.go b/backend/pkg/providers/provider/provider.go index 20d7e464d..04fd91cde 100644 --- a/backend/pkg/providers/provider/provider.go +++ b/backend/pkg/providers/provider/provider.go @@ -41,7 +41,8 @@ func (p ProviderType) ReasoningProvider() reasoning.Provider { return reasoning.ProviderBedrock case ProviderGemini: return reasoning.ProviderGoogleAI - case ProviderOpenAI, ProviderDeepSeek, ProviderGLM, ProviderKimi, ProviderQwen, ProviderMiniMax, ProviderCustom: + case ProviderOpenAI, ProviderDeepSeek, ProviderGLM, ProviderKimi, ProviderQwen, ProviderMiniMax, + ProviderAIMLAPI, ProviderCustom: return reasoning.ProviderOpenAI default: // ProviderOllama and anything unrecognized return reasoning.ProviderUnknown @@ -60,6 +61,7 @@ const ( ProviderKimi ProviderType = "kimi" ProviderQwen ProviderType = "qwen" ProviderMiniMax ProviderType = "minimax" + ProviderAIMLAPI ProviderType = "aimlapi" ) // AllProviderTypes enumerates every supported provider type; keep it in sync with @@ -76,6 +78,7 @@ var AllProviderTypes = ProvidersListTypes{ ProviderKimi, ProviderQwen, ProviderMiniMax, + ProviderAIMLAPI, } type ProviderName string @@ -96,6 +99,7 @@ const ( DefaultProviderNameKimi ProviderName = ProviderName(ProviderKimi) DefaultProviderNameQwen ProviderName = ProviderName(ProviderQwen) DefaultProviderNameMiniMax ProviderName = ProviderName(ProviderMiniMax) + DefaultProviderNameAIMLAPI ProviderName = ProviderName(ProviderAIMLAPI) ) type Provider interface { diff --git a/backend/pkg/providers/providers_test.go b/backend/pkg/providers/providers_test.go index 524916772..828841bac 100644 --- a/backend/pkg/providers/providers_test.go +++ b/backend/pkg/providers/providers_test.go @@ -10,6 +10,7 @@ import ( "pentagi/pkg/config" "pentagi/pkg/database" + "pentagi/pkg/providers/aimlapi" "pentagi/pkg/providers/anthropic" "pentagi/pkg/providers/bedrock" "pentagi/pkg/providers/deepseek" @@ -144,6 +145,7 @@ func TestOpenAICompatProvidersDoNotUseAdaptiveThinking(t *testing.T) { {"deepseek", deepseek.DefaultProviderConfig, deepseek.DefaultModels}, {"kimi", kimi.DefaultProviderConfig, kimi.DefaultModels}, {"minimax", minimax.DefaultProviderConfig, minimax.DefaultModels}, + {"aimlapi", aimlapi.DefaultProviderConfig, aimlapi.DefaultModels}, } for _, p := range providers { @@ -193,6 +195,7 @@ func TestAgentConfigPricesMatchCatalog(t *testing.T) { {"glm", glm.DefaultProviderConfig, glm.DefaultModels}, {"kimi", kimi.DefaultProviderConfig, kimi.DefaultModels}, {"minimax", minimax.DefaultProviderConfig, minimax.DefaultModels}, + {"aimlapi", aimlapi.DefaultProviderConfig, aimlapi.DefaultModels}, {"openai", openai.DefaultProviderConfig, openai.DefaultModels}, {"qwen", qwen.DefaultProviderConfig, qwen.DefaultModels}, } diff --git a/backend/pkg/providers/registry.go b/backend/pkg/providers/registry.go index 828f35ef7..a9dede832 100644 --- a/backend/pkg/providers/registry.go +++ b/backend/pkg/providers/registry.go @@ -2,6 +2,7 @@ package providers import ( "pentagi/pkg/config" + "pentagi/pkg/providers/aimlapi" "pentagi/pkg/providers/anthropic" "pentagi/pkg/providers/bedrock" "pentagi/pkg/providers/custom" @@ -138,6 +139,14 @@ var providerRegistry = []registryEntry{ New: minimax.New, BuildConfig: fromData(minimax.BuildProviderConfig), }, + { + Type: provider.ProviderAIMLAPI, + Name: provider.DefaultProviderNameAIMLAPI, + Enabled: func(c *config.Config) bool { return c.AIMLAPIKey != "" }, + NewConfig: ignoreConfig(aimlapi.DefaultProviderConfig), + New: aimlapi.New, + BuildConfig: fromData(aimlapi.BuildProviderConfig), + }, } func entryForType(t provider.ProviderType) (registryEntry, bool) { diff --git a/backend/pkg/server/models/providers_test.go b/backend/pkg/server/models/providers_test.go index efb1e2718..8ca080f61 100644 --- a/backend/pkg/server/models/providers_test.go +++ b/backend/pkg/server/models/providers_test.go @@ -25,6 +25,7 @@ func TestProviderTypeValid(t *testing.T) { {"kimi", ProviderType("kimi")}, {"qwen", ProviderType("qwen")}, {"minimax", ProviderType("minimax")}, + {"aimlapi", ProviderType("aimlapi")}, } for _, tt := range validTypes { diff --git a/docker-compose.yml b/docker-compose.yml index 97c5a7ae6..39b624c9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,6 +75,9 @@ services: - MINIMAX_API_KEY=${MINIMAX_API_KEY:-} - MINIMAX_SERVER_URL=${MINIMAX_SERVER_URL:-} - MINIMAX_PROVIDER=${MINIMAX_PROVIDER:-} + - AIMLAPI_API_KEY=${AIMLAPI_API_KEY:-} + - AIMLAPI_SERVER_URL=${AIMLAPI_SERVER_URL:-} + - AIMLAPI_PROVIDER=${AIMLAPI_PROVIDER:-} - LLM_SERVER_URL=${LLM_SERVER_URL:-} - LLM_SERVER_KEY=${LLM_SERVER_KEY:-} - LLM_SERVER_MODEL=${LLM_SERVER_MODEL:-} diff --git a/examples/tests/aimlapi-report.md b/examples/tests/aimlapi-report.md new file mode 100644 index 000000000..df95c572c --- /dev/null +++ b/examples/tests/aimlapi-report.md @@ -0,0 +1,580 @@ +# LLM Agent Testing Report + +Generated: Thu, 03 Sep 2026 01:10:30 UTC + +## Overall Results + +| Agent | Model | Reasoning | Success Rate | Average Latency | +|-------|-------|-----------|--------------|-----------------| +| simple | deepseek/deepseek-v4-flash | true | 24/25 (96.00%) | 1.717s | +| simple_json | deepseek/deepseek-v4-flash | false | 7/7 (100.00%) | 1.160s | +| primary_agent | z-ai/glm-5-turbo | true | 23/24 (95.83%) | 3.726s | +| assistant | z-ai/glm-5-turbo | true | 22/24 (91.67%) | 8.316s | +| generator | zhipu/glm-5.2 | true | 24/24 (100.00%) | 3.893s | +| refiner | zhipu/glm-5.2 | true | 24/24 (100.00%) | 3.708s | +| adviser | minimax/minimax-m3 | true | 23/24 (95.83%) | 3.469s | +| reflector | deepseek/deepseek-v4-flash | true | 25/25 (100.00%) | 1.351s | +| searcher | deepseek/deepseek-v4-flash | true | 24/25 (96.00%) | 1.398s | +| enricher | deepseek/deepseek-v4-flash | true | 24/25 (96.00%) | 1.220s | +| coder | moonshot/kimi-k2-7-code | true | 24/24 (100.00%) | 4.091s | +| installer | moonshot/kimi-k2-7-code | true | 24/24 (100.00%) | 3.929s | +| pentester | deepseek/deepseek-v4-flash | true | 24/24 (100.00%) | 1.347s | + +**Total**: 292/299 (97.66%) successful tests +**Overall average latency**: 3.110s + +## Detailed Results + +### simple (deepseek/deepseek-v4-flash) + +#### Basic Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Simple Math | ✅ Pass | 1.080s | | +| Text Transform Uppercase | ✅ Pass | 1.034s | | +| Count from 1 to 5 | ✅ Pass | 1.056s | | +| Math Calculation | ✅ Pass | 0.799s | | +| Basic Echo Function | ✅ Pass | 1.071s | | +| Streaming Simple Math Streaming | ✅ Pass | 0.932s | | +| Streaming Count from 1 to 3 Streaming | ✅ Pass | 0.972s | | +| Streaming Basic Echo Function Streaming | ✅ Pass | 0.930s | | + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| JSON Response Function | ✅ Pass | 1.391s | | +| Search Query Function | ✅ Pass | 1.581s | | +| Ask Advice Function | ✅ Pass | 1.206s | | +| Streaming Search Query Function Streaming | ✅ Pass | 1.373s | | +| Basic Context Memory Test | ✅ Pass | 1.406s | | +| Function Argument Memory Test | ✅ Pass | 0.978s | | +| Function Response Memory Test | ✅ Pass | 0.974s | | +| Penetration Testing Memory with Tool Call | ❌ Fail | 13.172s | expected function 'generate\_report' not found in tool calls: expected function generate\_report not found in tool calls | +| Cybersecurity Workflow Memory Test | ✅ Pass | 0.898s | | +| Read a file, then edit it via unified diff | ✅ Pass | 2.915s | | +| Penetration Testing Methodology | ✅ Pass | 0.852s | | +| Vulnerability Assessment Tools | ✅ Pass | 3.055s | | +| SQL Injection Attack Type | ✅ Pass | 1.252s | | +| Penetration Testing Framework | ✅ Pass | 1.217s | | +| Web Application Security Scanner | ✅ Pass | 0.834s | | +| Penetration Testing Tool Selection | ✅ Pass | 1.122s | | + +#### Capability Tests + +| Test | Capability | Result | Latency | Note | +|------|------------|--------|---------|------| +| Explicit Reasoning Off Suppresses Reasoning | reasoning_off | ✅ Pass | 0.810s | | + +**Summary**: 24/25 (96.00%) successful tests + +**Average latency**: 1.717s + +--- + +### simple_json (deepseek/deepseek-v4-flash) + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Vulnerability Report Memory Test | ✅ Pass | 1.217s | | +| Person Information JSON | ✅ Pass | 1.149s | | +| Project Information JSON | ✅ Pass | 1.003s | | +| User Profile JSON | ✅ Pass | 0.980s | | +| Streaming Person Information JSON Streaming | ✅ Pass | 0.913s | | +| JSON Array Response Without Schema | ✅ Pass | 1.005s | | + +#### Capability Tests + +| Test | Capability | Result | Latency | Note | +|------|------------|--------|---------|------| +| Structured Output With JSON Schema | structured_output | ✅ Pass | 1.847s | | + +**Summary**: 7/7 (100.00%) successful tests + +**Average latency**: 1.160s + +--- + +### primary_agent (z-ai/glm-5-turbo) + +#### Basic Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Simple Math | ✅ Pass | 2.747s | | +| Text Transform Uppercase | ✅ Pass | 2.658s | | +| Count from 1 to 5 | ✅ Pass | 3.042s | | +| Math Calculation | ✅ Pass | 2.275s | | +| Basic Echo Function | ✅ Pass | 1.930s | | +| Streaming Simple Math Streaming | ✅ Pass | 2.348s | | +| Streaming Count from 1 to 3 Streaming | ✅ Pass | 2.883s | | +| Streaming Basic Echo Function Streaming | ✅ Pass | 1.856s | | + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| JSON Response Function | ✅ Pass | 1.991s | | +| Search Query Function | ✅ Pass | 1.721s | | +| Ask Advice Function | ✅ Pass | 1.847s | | +| Streaming Search Query Function Streaming | ✅ Pass | 1.724s | | +| Basic Context Memory Test | ✅ Pass | 5.499s | | +| Function Argument Memory Test | ✅ Pass | 1.803s | | +| Function Response Memory Test | ✅ Pass | 1.461s | | +| Penetration Testing Memory with Tool Call | ✅ Pass | 14.872s | | +| Cybersecurity Workflow Memory Test | ✅ Pass | 4.181s | | +| Read a file, then edit it via unified diff | ❌ Fail | 6.980s | edit\_file's diff did not apply: diff is empty | +| Penetration Testing Methodology | ✅ Pass | 6.521s | | +| Vulnerability Assessment Tools | ✅ Pass | 5.813s | | +| SQL Injection Attack Type | ✅ Pass | 3.569s | | +| Penetration Testing Framework | ✅ Pass | 5.559s | | +| Web Application Security Scanner | ✅ Pass | 4.165s | | +| Penetration Testing Tool Selection | ✅ Pass | 1.978s | | + +**Summary**: 23/24 (95.83%) successful tests + +**Average latency**: 3.726s + +--- + +### assistant (z-ai/glm-5-turbo) + +#### Basic Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Simple Math | ✅ Pass | 2.750s | | +| Text Transform Uppercase | ✅ Pass | 2.863s | | +| Math Calculation | ✅ Pass | 2.090s | | +| Basic Echo Function | ✅ Pass | 1.657s | | +| Streaming Simple Math Streaming | ✅ Pass | 2.082s | | +| Streaming Count from 1 to 3 Streaming | ✅ Pass | 3.310s | | +| Streaming Basic Echo Function Streaming | ✅ Pass | 1.635s | | +| Count from 1 to 5 | ❌ Fail | 90.316s | API returned unexpected status code: 504 | + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| JSON Response Function | ✅ Pass | 1.860s | | +| Search Query Function | ✅ Pass | 1.658s | | +| Ask Advice Function | ✅ Pass | 1.933s | | +| Streaming Search Query Function Streaming | ✅ Pass | 1.720s | | +| Basic Context Memory Test | ✅ Pass | 5.297s | | +| Function Argument Memory Test | ✅ Pass | 1.912s | | +| Function Response Memory Test | ✅ Pass | 3.586s | | +| Penetration Testing Memory with Tool Call | ✅ Pass | 3.825s | | +| Cybersecurity Workflow Memory Test | ✅ Pass | 4.177s | | +| Read a file, then edit it via unified diff | ❌ Fail | 5.112s | edit\_file's diff did not apply: diff is empty | +| Vulnerability Assessment Tools | ✅ Pass | 9.865s | | +| Penetration Testing Methodology | ✅ Pass | 30.869s | | +| SQL Injection Attack Type | ✅ Pass | 4.061s | | +| Penetration Testing Framework | ✅ Pass | 8.532s | | +| Web Application Security Scanner | ✅ Pass | 6.452s | | +| Penetration Testing Tool Selection | ✅ Pass | 2.007s | | + +**Summary**: 22/24 (91.67%) successful tests + +**Average latency**: 8.316s + +--- + +### generator (zhipu/glm-5.2) + +#### Basic Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Simple Math | ✅ Pass | 3.772s | | +| Text Transform Uppercase | ✅ Pass | 3.236s | | +| Count from 1 to 5 | ✅ Pass | 3.685s | | +| Math Calculation | ✅ Pass | 2.445s | | +| Basic Echo Function | ✅ Pass | 2.101s | | +| Streaming Simple Math Streaming | ✅ Pass | 2.488s | | +| Streaming Count from 1 to 3 Streaming | ✅ Pass | 3.318s | | +| Streaming Basic Echo Function Streaming | ✅ Pass | 1.748s | | + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| JSON Response Function | ✅ Pass | 1.970s | | +| Search Query Function | ✅ Pass | 2.426s | | +| Ask Advice Function | ✅ Pass | 2.152s | | +| Streaming Search Query Function Streaming | ✅ Pass | 1.945s | | +| Basic Context Memory Test | ✅ Pass | 4.177s | | +| Function Argument Memory Test | ✅ Pass | 3.111s | | +| Function Response Memory Test | ✅ Pass | 2.670s | | +| Penetration Testing Memory with Tool Call | ✅ Pass | 4.096s | | +| Cybersecurity Workflow Memory Test | ✅ Pass | 2.594s | | +| Read a file, then edit it via unified diff | ✅ Pass | 5.752s | | +| Penetration Testing Methodology | ✅ Pass | 8.444s | | +| Vulnerability Assessment Tools | ✅ Pass | 11.271s | | +| SQL Injection Attack Type | ✅ Pass | 4.194s | | +| Penetration Testing Framework | ✅ Pass | 6.774s | | +| Web Application Security Scanner | ✅ Pass | 6.445s | | +| Penetration Testing Tool Selection | ✅ Pass | 2.613s | | + +**Summary**: 24/24 (100.00%) successful tests + +**Average latency**: 3.893s + +--- + +### refiner (zhipu/glm-5.2) + +#### Basic Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Simple Math | ✅ Pass | 3.000s | | +| Text Transform Uppercase | ✅ Pass | 3.298s | | +| Count from 1 to 5 | ✅ Pass | 3.682s | | +| Math Calculation | ✅ Pass | 1.660s | | +| Basic Echo Function | ✅ Pass | 1.754s | | +| Streaming Simple Math Streaming | ✅ Pass | 2.763s | | +| Streaming Count from 1 to 3 Streaming | ✅ Pass | 3.255s | | +| Streaming Basic Echo Function Streaming | ✅ Pass | 3.706s | | + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| JSON Response Function | ✅ Pass | 2.149s | | +| Search Query Function | ✅ Pass | 1.898s | | +| Ask Advice Function | ✅ Pass | 2.065s | | +| Streaming Search Query Function Streaming | ✅ Pass | 2.090s | | +| Basic Context Memory Test | ✅ Pass | 4.054s | | +| Function Argument Memory Test | ✅ Pass | 2.238s | | +| Function Response Memory Test | ✅ Pass | 1.888s | | +| Penetration Testing Memory with Tool Call | ✅ Pass | 3.422s | | +| Cybersecurity Workflow Memory Test | ✅ Pass | 2.458s | | +| Read a file, then edit it via unified diff | ✅ Pass | 5.605s | | +| Penetration Testing Methodology | ✅ Pass | 5.516s | | +| Vulnerability Assessment Tools | ✅ Pass | 12.984s | | +| SQL Injection Attack Type | ✅ Pass | 4.802s | | +| Penetration Testing Framework | ✅ Pass | 3.778s | | +| Web Application Security Scanner | ✅ Pass | 5.188s | | +| Penetration Testing Tool Selection | ✅ Pass | 5.734s | | + +**Summary**: 24/24 (100.00%) successful tests + +**Average latency**: 3.708s + +--- + +### adviser (minimax/minimax-m3) + +#### Basic Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Simple Math | ✅ Pass | 6.825s | | +| Text Transform Uppercase | ✅ Pass | 2.270s | | +| Count from 1 to 5 | ✅ Pass | 1.708s | | +| Math Calculation | ✅ Pass | 1.027s | | +| Basic Echo Function | ✅ Pass | 1.902s | | +| Streaming Simple Math Streaming | ✅ Pass | 0.972s | | +| Streaming Count from 1 to 3 Streaming | ✅ Pass | 1.741s | | +| Streaming Basic Echo Function Streaming | ✅ Pass | 3.000s | | + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| JSON Response Function | ✅ Pass | 0.933s | | +| Search Query Function | ✅ Pass | 4.079s | | +| Ask Advice Function | ✅ Pass | 2.276s | | +| Streaming Search Query Function Streaming | ✅ Pass | 1.231s | | +| Basic Context Memory Test | ✅ Pass | 4.537s | | +| Function Argument Memory Test | ✅ Pass | 1.631s | | +| Function Response Memory Test | ✅ Pass | 4.413s | | +| Penetration Testing Memory with Tool Call | ✅ Pass | 3.746s | | +| Cybersecurity Workflow Memory Test | ✅ Pass | 0.908s | | +| Read a file, then edit it via unified diff | ❌ Fail | 4.607s | edit\_file's diff applied but did not produce "Priority: high" \(result: "Status: draft\nPriority: high\nPriority: low\n"\) | +| Penetration Testing Methodology | ✅ Pass | 1.613s | | +| Vulnerability Assessment Tools | ✅ Pass | 10.932s | | +| SQL Injection Attack Type | ✅ Pass | 1.867s | | +| Penetration Testing Framework | ✅ Pass | 12.397s | | +| Web Application Security Scanner | ✅ Pass | 2.311s | | +| Penetration Testing Tool Selection | ✅ Pass | 6.311s | | + +**Summary**: 23/24 (95.83%) successful tests + +**Average latency**: 3.469s + +--- + +### reflector (deepseek/deepseek-v4-flash) + +#### Basic Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Simple Math | ✅ Pass | 0.876s | | +| Text Transform Uppercase | ✅ Pass | 0.746s | | +| Count from 1 to 5 | ✅ Pass | 0.802s | | +| Math Calculation | ✅ Pass | 1.129s | | +| Basic Echo Function | ✅ Pass | 1.358s | | +| Streaming Simple Math Streaming | ✅ Pass | 0.868s | | +| Streaming Count from 1 to 3 Streaming | ✅ Pass | 1.239s | | +| Streaming Basic Echo Function Streaming | ✅ Pass | 0.977s | | + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| JSON Response Function | ✅ Pass | 1.204s | | +| Search Query Function | ✅ Pass | 1.214s | | +| Ask Advice Function | ✅ Pass | 1.052s | | +| Streaming Search Query Function Streaming | ✅ Pass | 1.113s | | +| Basic Context Memory Test | ✅ Pass | 1.139s | | +| Function Argument Memory Test | ✅ Pass | 0.866s | | +| Function Response Memory Test | ✅ Pass | 0.955s | | +| Penetration Testing Memory with Tool Call | ✅ Pass | 1.697s | | +| Cybersecurity Workflow Memory Test | ✅ Pass | 0.982s | | +| Read a file, then edit it via unified diff | ✅ Pass | 2.946s | | +| Penetration Testing Methodology | ✅ Pass | 2.477s | | +| Vulnerability Assessment Tools | ✅ Pass | 1.966s | | +| SQL Injection Attack Type | ✅ Pass | 0.912s | | +| Penetration Testing Framework | ✅ Pass | 0.992s | | +| Web Application Security Scanner | ✅ Pass | 1.108s | | +| Penetration Testing Tool Selection | ✅ Pass | 4.100s | | + +#### Capability Tests + +| Test | Capability | Result | Latency | Note | +|------|------------|--------|---------|------| +| Explicit Reasoning Off Suppresses Reasoning | reasoning_off | ✅ Pass | 1.053s | | + +**Summary**: 25/25 (100.00%) successful tests + +**Average latency**: 1.351s + +--- + +### searcher (deepseek/deepseek-v4-flash) + +#### Basic Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Simple Math | ✅ Pass | 1.001s | | +| Text Transform Uppercase | ✅ Pass | 1.046s | | +| Count from 1 to 5 | ✅ Pass | 0.884s | | +| Math Calculation | ✅ Pass | 0.844s | | +| Basic Echo Function | ✅ Pass | 1.176s | | +| Streaming Simple Math Streaming | ✅ Pass | 1.060s | | +| Streaming Count from 1 to 3 Streaming | ✅ Pass | 0.927s | | +| Streaming Basic Echo Function Streaming | ✅ Pass | 1.258s | | + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| JSON Response Function | ✅ Pass | 1.435s | | +| Search Query Function | ✅ Pass | 1.288s | | +| Ask Advice Function | ✅ Pass | 1.050s | | +| Streaming Search Query Function Streaming | ✅ Pass | 1.042s | | +| Basic Context Memory Test | ✅ Pass | 0.915s | | +| Function Argument Memory Test | ✅ Pass | 1.073s | | +| Function Response Memory Test | ✅ Pass | 0.818s | | +| Penetration Testing Memory with Tool Call | ❌ Fail | 1.151s | expected function 'generate\_report' not found in tool calls: expected function generate\_report not found in tool calls | +| Cybersecurity Workflow Memory Test | ✅ Pass | 0.864s | | +| Read a file, then edit it via unified diff | ✅ Pass | 3.122s | | +| Penetration Testing Methodology | ✅ Pass | 1.742s | | +| Vulnerability Assessment Tools | ✅ Pass | 3.914s | | +| SQL Injection Attack Type | ✅ Pass | 1.241s | | +| Penetration Testing Framework | ✅ Pass | 0.969s | | +| Web Application Security Scanner | ✅ Pass | 0.988s | | +| Penetration Testing Tool Selection | ✅ Pass | 4.241s | | + +#### Capability Tests + +| Test | Capability | Result | Latency | Note | +|------|------------|--------|---------|------| +| Explicit Reasoning Off Suppresses Reasoning | reasoning_off | ✅ Pass | 0.888s | | + +**Summary**: 24/25 (96.00%) successful tests + +**Average latency**: 1.398s + +--- + +### enricher (deepseek/deepseek-v4-flash) + +#### Basic Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Simple Math | ✅ Pass | 1.127s | | +| Text Transform Uppercase | ✅ Pass | 0.839s | | +| Count from 1 to 5 | ✅ Pass | 1.025s | | +| Math Calculation | ✅ Pass | 1.117s | | +| Basic Echo Function | ✅ Pass | 1.320s | | +| Streaming Simple Math Streaming | ✅ Pass | 0.879s | | +| Streaming Count from 1 to 3 Streaming | ✅ Pass | 1.074s | | +| Streaming Basic Echo Function Streaming | ✅ Pass | 1.039s | | + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| JSON Response Function | ✅ Pass | 1.256s | | +| Search Query Function | ✅ Pass | 1.070s | | +| Ask Advice Function | ✅ Pass | 1.243s | | +| Streaming Search Query Function Streaming | ✅ Pass | 1.326s | | +| Basic Context Memory Test | ✅ Pass | 1.156s | | +| Function Argument Memory Test | ✅ Pass | 0.843s | | +| Function Response Memory Test | ✅ Pass | 0.807s | | +| Penetration Testing Memory with Tool Call | ❌ Fail | 1.575s | expected function 'generate\_report' not found in tool calls: expected function generate\_report not found in tool calls | +| Cybersecurity Workflow Memory Test | ✅ Pass | 0.829s | | +| Read a file, then edit it via unified diff | ✅ Pass | 2.724s | | +| Penetration Testing Methodology | ✅ Pass | 1.357s | | +| Vulnerability Assessment Tools | ✅ Pass | 3.172s | | +| SQL Injection Attack Type | ✅ Pass | 0.839s | | +| Penetration Testing Framework | ✅ Pass | 0.791s | | +| Web Application Security Scanner | ✅ Pass | 1.197s | | +| Penetration Testing Tool Selection | ✅ Pass | 1.072s | | + +#### Capability Tests + +| Test | Capability | Result | Latency | Note | +|------|------------|--------|---------|------| +| Explicit Reasoning Off Suppresses Reasoning | reasoning_off | ✅ Pass | 0.820s | | + +**Summary**: 24/25 (96.00%) successful tests + +**Average latency**: 1.220s + +--- + +### coder (moonshot/kimi-k2-7-code) + +#### Basic Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Text Transform Uppercase | ✅ Pass | 2.136s | | +| Simple Math | ✅ Pass | 17.172s | | +| Count from 1 to 5 | ✅ Pass | 2.447s | | +| Math Calculation | ✅ Pass | 1.745s | | +| Basic Echo Function | ✅ Pass | 1.931s | | +| Streaming Simple Math Streaming | ✅ Pass | 1.881s | | +| Streaming Count from 1 to 3 Streaming | ✅ Pass | 1.969s | | +| Streaming Basic Echo Function Streaming | ✅ Pass | 1.999s | | + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| JSON Response Function | ✅ Pass | 2.371s | | +| Search Query Function | ✅ Pass | 16.675s | | +| Ask Advice Function | ✅ Pass | 2.522s | | +| Streaming Search Query Function Streaming | ✅ Pass | 2.379s | | +| Basic Context Memory Test | ✅ Pass | 2.435s | | +| Function Argument Memory Test | ✅ Pass | 1.576s | | +| Function Response Memory Test | ✅ Pass | 9.518s | | +| Penetration Testing Memory with Tool Call | ✅ Pass | 3.499s | | +| Cybersecurity Workflow Memory Test | ✅ Pass | 1.967s | | +| Read a file, then edit it via unified diff | ✅ Pass | 3.906s | | +| Penetration Testing Methodology | ✅ Pass | 6.603s | | +| Vulnerability Assessment Tools | ✅ Pass | 5.848s | | +| SQL Injection Attack Type | ✅ Pass | 1.854s | | +| Penetration Testing Framework | ✅ Pass | 2.378s | | +| Web Application Security Scanner | ✅ Pass | 1.674s | | +| Penetration Testing Tool Selection | ✅ Pass | 1.694s | | + +**Summary**: 24/24 (100.00%) successful tests + +**Average latency**: 4.091s + +--- + +### installer (moonshot/kimi-k2-7-code) + +#### Basic Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Simple Math | ✅ Pass | 13.196s | | +| Text Transform Uppercase | ✅ Pass | 4.970s | | +| Count from 1 to 5 | ✅ Pass | 2.546s | | +| Math Calculation | ✅ Pass | 1.972s | | +| Basic Echo Function | ✅ Pass | 2.660s | | +| Streaming Simple Math Streaming | ✅ Pass | 2.135s | | +| Streaming Count from 1 to 3 Streaming | ✅ Pass | 1.929s | | +| Streaming Basic Echo Function Streaming | ✅ Pass | 1.875s | | + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| JSON Response Function | ✅ Pass | 2.522s | | +| Ask Advice Function | ✅ Pass | 2.456s | | +| Search Query Function | ✅ Pass | 18.833s | | +| Streaming Search Query Function Streaming | ✅ Pass | 1.912s | | +| Basic Context Memory Test | ✅ Pass | 2.370s | | +| Function Argument Memory Test | ✅ Pass | 1.650s | | +| Function Response Memory Test | ✅ Pass | 6.795s | | +| Penetration Testing Memory with Tool Call | ✅ Pass | 2.986s | | +| Cybersecurity Workflow Memory Test | ✅ Pass | 1.710s | | +| Read a file, then edit it via unified diff | ✅ Pass | 3.538s | | +| Penetration Testing Methodology | ✅ Pass | 3.096s | | +| Vulnerability Assessment Tools | ✅ Pass | 4.913s | | +| SQL Injection Attack Type | ✅ Pass | 1.724s | | +| Penetration Testing Framework | ✅ Pass | 2.386s | | +| Web Application Security Scanner | ✅ Pass | 4.279s | | +| Penetration Testing Tool Selection | ✅ Pass | 1.834s | | + +**Summary**: 24/24 (100.00%) successful tests + +**Average latency**: 3.929s + +--- + +### pentester (deepseek/deepseek-v4-flash) + +#### Basic Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| Simple Math | ✅ Pass | 0.922s | | +| Text Transform Uppercase | ✅ Pass | 1.315s | | +| Count from 1 to 5 | ✅ Pass | 1.014s | | +| Math Calculation | ✅ Pass | 1.040s | | +| Basic Echo Function | ✅ Pass | 1.327s | | +| Streaming Simple Math Streaming | ✅ Pass | 1.441s | | +| Streaming Count from 1 to 3 Streaming | ✅ Pass | 1.024s | | +| Streaming Basic Echo Function Streaming | ✅ Pass | 1.515s | | + +#### Advanced Tests + +| Test | Result | Latency | Error | +|------|--------|---------|-------| +| JSON Response Function | ✅ Pass | 1.313s | | +| Search Query Function | ✅ Pass | 1.188s | | +| Ask Advice Function | ✅ Pass | 1.374s | | +| Streaming Search Query Function Streaming | ✅ Pass | 1.491s | | +| Basic Context Memory Test | ✅ Pass | 1.384s | | +| Function Argument Memory Test | ✅ Pass | 1.085s | | +| Function Response Memory Test | ✅ Pass | 0.984s | | +| Penetration Testing Memory with Tool Call | ✅ Pass | 1.954s | | +| Cybersecurity Workflow Memory Test | ✅ Pass | 1.472s | | +| Read a file, then edit it via unified diff | ✅ Pass | 3.047s | | +| Penetration Testing Methodology | ✅ Pass | 1.339s | | +| Vulnerability Assessment Tools | ✅ Pass | 1.462s | | +| SQL Injection Attack Type | ✅ Pass | 1.201s | | +| Penetration Testing Framework | ✅ Pass | 1.062s | | +| Web Application Security Scanner | ✅ Pass | 0.990s | | +| Penetration Testing Tool Selection | ✅ Pass | 1.375s | | + +**Summary**: 24/24 (100.00%) successful tests + +**Average latency**: 1.347s + +--- + diff --git a/frontend/e2e/mocks/cassettes/settings-providers.ts b/frontend/e2e/mocks/cassettes/settings-providers.ts index 0eb560499..c29a188e2 100644 --- a/frontend/e2e/mocks/cassettes/settings-providers.ts +++ b/frontend/e2e/mocks/cassettes/settings-providers.ts @@ -85,6 +85,7 @@ const modelConfig = (name: string): ModelConfigFragmentFragment => // create-form's model-dropdown seeding path structurally unreachable. const allDefaults = () => entity('DefaultProvidersConfig', { + aimlapi: defaultConfig(ProviderType.Aimlapi), anthropic: defaultConfig(ProviderType.Anthropic), bedrock: defaultConfig(ProviderType.Bedrock), custom: defaultConfig(ProviderType.Custom), @@ -102,6 +103,7 @@ const catalog = (type: ProviderType) => [modelConfig(`e2e-${type}-model`), model const allModels = () => entity('ProvidersModelsList', { + aimlapi: catalog(ProviderType.Aimlapi), anthropic: catalog(ProviderType.Anthropic), bedrock: catalog(ProviderType.Bedrock), custom: catalog(ProviderType.Custom), @@ -119,6 +121,7 @@ const noProviders: ResultOf = { settingsProviders: entity('ProvidersConfig', { default: allDefaults(), enabled: entity('ProvidersReadinessStatus', { + aimlapi: false, anthropic: false, bedrock: false, custom: false, @@ -170,6 +173,7 @@ export const providersList = (...userDefined: ProviderConfigFragmentFragment[]) settingsProviders: entity('ProvidersConfig', { default: allDefaults(), enabled: entity('ProvidersReadinessStatus', { + aimlapi: false, anthropic: true, bedrock: false, custom: false, diff --git a/frontend/graphql-schema.graphql b/frontend/graphql-schema.graphql index 0547b3d04..05160f01c 100644 --- a/frontend/graphql-schema.graphql +++ b/frontend/graphql-schema.graphql @@ -569,6 +569,7 @@ query settingsProviders { kimi qwen minimax + aimlapi } default { openai { @@ -604,6 +605,9 @@ query settingsProviders { minimax { ...providerConfigFragment } + aimlapi { + ...providerConfigFragment + } } userDefined { ...providerConfigFragment @@ -642,6 +646,9 @@ query settingsProviders { minimax { ...modelConfigFragment } + aimlapi { + ...modelConfigFragment + } } } } diff --git a/frontend/src/components/icons/aimlapi.tsx b/frontend/src/components/icons/aimlapi.tsx new file mode 100644 index 000000000..904b78327 --- /dev/null +++ b/frontend/src/components/icons/aimlapi.tsx @@ -0,0 +1,25 @@ +import { cn } from '@/lib/utils'; + +interface AimlapiProps extends React.SVGProps { + className?: string; +} + +// Aggregator mark: one hub routing to several upstream vendors. Drawn in +// currentColor like every other provider icon here so it inherits the theme. +function Aimlapi({ className, ...props }: AimlapiProps) { + return ( + + aimlapi.com + + + + ); +} + +export default Aimlapi; diff --git a/frontend/src/components/icons/provider-icon.tsx b/frontend/src/components/icons/provider-icon.tsx index 06a8b7670..ade985b4a 100644 --- a/frontend/src/components/icons/provider-icon.tsx +++ b/frontend/src/components/icons/provider-icon.tsx @@ -6,6 +6,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { ProviderType } from '@/graphql/types'; import { cn } from '@/lib/utils'; +import Aimlapi from './aimlapi'; import Anthropic from './anthropic'; import Bedrock from './bedrock'; import Custom from './custom'; @@ -30,6 +31,7 @@ interface ProviderIconProps { } export const providerIcons: Record = { + [ProviderType.Aimlapi]: { className: 'text-emerald-500', icon: Aimlapi }, [ProviderType.Anthropic]: { className: 'text-purple-500', icon: Anthropic }, [ProviderType.Bedrock]: { className: 'text-blue-500', icon: Bedrock }, [ProviderType.Custom]: { className: 'text-blue-500', icon: Custom }, diff --git a/frontend/src/graphql/types.ts b/frontend/src/graphql/types.ts index a1a2533e8..e2aebb11b 100644 --- a/frontend/src/graphql/types.ts +++ b/frontend/src/graphql/types.ts @@ -204,6 +204,7 @@ export enum PromptValidationErrorType { } export enum ProviderType { + Aimlapi = 'aimlapi', Anthropic = 'anthropic', Bedrock = 'bedrock', Custom = 'custom', @@ -735,6 +736,7 @@ export type SettingsProvidersQuery = { kimi: boolean; qwen: boolean; minimax: boolean; + aimlapi: boolean; }; default: { openai: ProviderConfigFragmentFragment; @@ -748,6 +750,7 @@ export type SettingsProvidersQuery = { kimi: ProviderConfigFragmentFragment | null; qwen: ProviderConfigFragmentFragment | null; minimax: ProviderConfigFragmentFragment | null; + aimlapi: ProviderConfigFragmentFragment | null; }; userDefined: Array | null; models: { @@ -762,6 +765,7 @@ export type SettingsProvidersQuery = { kimi: Array | null; qwen: Array | null; minimax: Array | null; + aimlapi: Array | null; }; }; }; @@ -3561,6 +3565,7 @@ export const SettingsProvidersDocument = { { kind: 'Field', name: { kind: 'Name', value: 'kimi' } }, { kind: 'Field', name: { kind: 'Name', value: 'qwen' } }, { kind: 'Field', name: { kind: 'Name', value: 'minimax' } }, + { kind: 'Field', name: { kind: 'Name', value: 'aimlapi' } }, ], }, }, @@ -3713,6 +3718,19 @@ export const SettingsProvidersDocument = { ], }, }, + { + kind: 'Field', + name: { kind: 'Name', value: 'aimlapi' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'FragmentSpread', + name: { kind: 'Name', value: 'providerConfigFragment' }, + }, + ], + }, + }, ], }, }, @@ -3878,6 +3896,19 @@ export const SettingsProvidersDocument = { ], }, }, + { + kind: 'Field', + name: { kind: 'Name', value: 'aimlapi' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'FragmentSpread', + name: { kind: 'Name', value: 'modelConfigFragment' }, + }, + ], + }, + }, ], }, }, diff --git a/frontend/src/pages/settings/settings-providers.tsx b/frontend/src/pages/settings/settings-providers.tsx index fe0c9181d..e437f5c52 100644 --- a/frontend/src/pages/settings/settings-providers.tsx +++ b/frontend/src/pages/settings/settings-providers.tsx @@ -35,6 +35,7 @@ type Provider = ProviderConfigFragmentFragment; // Exhaustive Record so a newly-added ProviderType is a compile error here, not a // provider silently missing from the create-provider menu. const providerLabels: Record = { + [ProviderType.Aimlapi]: 'aimlapi.com', [ProviderType.Anthropic]: 'Anthropic', [ProviderType.Bedrock]: 'Bedrock', [ProviderType.Custom]: 'Custom',