diff --git a/.env.example b/.env.example index 696a5b9e3..c568250c2 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,11 @@ LICENSE_KEY= ASK_USER= ## LLM Providers +## aimlapi.com LLM provider (aggregator, OpenAI-compatible) +AIMLAPI_API_KEY= +AIMLAPI_SERVER_URL=https://api.aimlapi.com/v1 +AIMLAPI_PROVIDER= + OPEN_AI_KEY= OPEN_AI_SERVER_URL=https://api.openai.com/v1 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..30a189543 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**: aimlapi.com, OpenAI, Anthropic, Gemini, AWS Bedrock, Ollama, DeepSeek, GLM, Kimi, Qwen, MiniMax, 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..65f3d921c 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ - [How to Use PentAGI After Login](#how-to-use-pentagi-after-login) - [API Access](#api-access) - [LLM Provider Configuration](#custom-llm-provider-configuration) + - [aimlapi.com](#aimlapicom-provider-configuration) - [Ollama](#ollama-provider-configuration) - [OpenAI](#openai-provider-configuration) - [Anthropic](#anthropic-provider-configuration) @@ -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 ([aimlapi.com](https://aimlapi.com/), [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). - 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 @@ -1475,6 +1479,50 @@ How to investigate: 3. Confirm the backend's tool-call parser and chat template are the ones recommended for the model you are serving, and that the model itself supports tool calling. 4. Update PentAGI to the latest build. Recent versions sanitize malformed function-call arguments returned by the model so a single bad response no longer stalls the whole flow; older builds forwarded the corrupted arguments and could get stuck. +### 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. + ### Ollama Provider Configuration PentAGI supports Ollama for both local LLM inference (zero-cost, enhanced privacy) and Ollama Cloud (managed service with free tier). @@ -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..737c51ff9 100644 --- a/backend/cmd/installer/wizard/models/llm_providers.go +++ b/backend/cmd/installer/wizard/models/llm_providers.go @@ -31,6 +31,7 @@ func NewLLMProvidersHandler(c controller.Controller, s styles.Styles, w window.W func (h *LLMProvidersHandler) LoadItems() []ListItem { items := []ListItem{ + {ID: LLMProviderAIMLAPIScreen}, {ID: LLMProviderOpenAIScreen}, {ID: LLMProviderAnthropicScreen}, {ID: LLMProviderGeminiScreen}, 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..b52704f88 100644 --- a/backend/docs/config.md +++ b/backend/docs/config.md @@ -30,6 +30,7 @@ This document serves as a comprehensive guide to the configuration system in Pen - [Web Scraper Settings](#web-scraper-settings) - [Usage Details](#usage-details-5) - [LLM Provider Settings](#llm-provider-settings) + - [aimlapi.com LLM Provider](#aimlapicom-llm-provider) - [OpenAI](#openai) - [Anthropic](#anthropic) - [Ollama LLM Provider](#ollama-llm-provider) @@ -712,6 +713,25 @@ The scraper settings enable critical functionality: These settings control the integration with various Large Language Model (LLM) providers, including OpenAI, Anthropic, and custom providers. +### 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. + ### OpenAI | 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..93c6a8334 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,11 +61,13 @@ const ( ProviderKimi ProviderType = "kimi" ProviderQwen ProviderType = "qwen" ProviderMiniMax ProviderType = "minimax" + ProviderAIMLAPI ProviderType = "aimlapi" ) // AllProviderTypes enumerates every supported provider type; keep it in sync with // the consts above. The API-layer type whitelist validates against it. var AllProviderTypes = ProvidersListTypes{ + ProviderAIMLAPI, ProviderOpenAI, ProviderAnthropic, ProviderGemini, @@ -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..e9729f9c7 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" @@ -44,6 +45,14 @@ func fromData( } var providerRegistry = []registryEntry{ + { + 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), + }, { Type: provider.ProviderOpenAI, Name: provider.DefaultProviderNameOpenAI, 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',