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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
- **Scale-to-Zero**: Workloads are serverless by default
- **Volume Storage**: Mount distributed storage volumes
- **GPU Support**: Run on our cloud (4090s, H100s, and more) or bring your own GPUs
- **Multi-Provider LLM Support**: Built-in support for OpenAI and [MiniMax](https://www.minimaxi.com) models via OpenAI-compatible API

## 📦 Installation

Expand Down
11 changes: 10 additions & 1 deletion pkg/abstractions/experimental/bot/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,17 @@ func NewBotInterface(opts botInterfaceOpts) (*BotInterface, error) {
}
}

var client *openai.Client
if opts.BotConfig.BaseUrl != "" {
config := openai.DefaultConfig(opts.BotConfig.ApiKey)
config.BaseURL = opts.BotConfig.BaseUrl
client = openai.NewClientWithConfig(config)
} else {
client = openai.NewClient(opts.BotConfig.ApiKey)
}

bi := &BotInterface{
client: openai.NewClient(opts.BotConfig.ApiKey),
client: client,
botConfig: opts.BotConfig,
model: opts.BotConfig.Model,
systemPrompt: systemPrompt,
Expand Down
129 changes: 129 additions & 0 deletions pkg/abstractions/experimental/bot/interface_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package bot

import (
"testing"
)

func TestBotConfigBaseUrl(t *testing.T) {
t.Run("default base url is empty", func(t *testing.T) {
config := BotConfig{
Model: "gpt-4o",
ApiKey: "test-key",
}
if config.BaseUrl != "" {
t.Errorf("expected empty BaseUrl, got %q", config.BaseUrl)
}
})

t.Run("base url can be set for MiniMax", func(t *testing.T) {
config := BotConfig{
Model: "MiniMax-M2.7",
ApiKey: "test-key",
BaseUrl: "https://api.minimax.io/v1",
}
if config.BaseUrl != "https://api.minimax.io/v1" {
t.Errorf("expected MiniMax base URL, got %q", config.BaseUrl)
}
})

t.Run("base url is optional for OpenAI models", func(t *testing.T) {
config := BotConfig{
Model: "gpt-4o",
ApiKey: "test-key",
}
if config.BaseUrl != "" {
t.Errorf("expected empty BaseUrl for OpenAI model, got %q", config.BaseUrl)
}
})
}

func TestBotConfigJSON(t *testing.T) {
t.Run("base url is omitted when empty", func(t *testing.T) {
config := BotConfig{
Model: "gpt-4o",
ApiKey: "test-key",
}
// BaseUrl should be zero-value and omitted in JSON
if config.BaseUrl != "" {
t.Errorf("expected empty BaseUrl, got %q", config.BaseUrl)
}
})

t.Run("base url is included when set", func(t *testing.T) {
config := BotConfig{
Model: "MiniMax-M2.5",
ApiKey: "test-key",
BaseUrl: "https://api.minimax.io/v1",
}
if config.BaseUrl == "" {
t.Error("expected non-empty BaseUrl")
}
})
}

func TestBotConfigMiniMaxModels(t *testing.T) {
miniMaxModels := []string{
"MiniMax-M2.7",
"MiniMax-M2.5",
"MiniMax-M2.5-highspeed",
}

for _, model := range miniMaxModels {
t.Run("config accepts "+model, func(t *testing.T) {
config := BotConfig{
Model: model,
ApiKey: "test-minimax-key",
BaseUrl: "https://api.minimax.io/v1",
}
if config.Model != model {
t.Errorf("expected model %q, got %q", model, config.Model)
}
if config.BaseUrl != "https://api.minimax.io/v1" {
t.Errorf("expected MiniMax base URL, got %q", config.BaseUrl)
}
})
}
}

func TestBotConfigFormatLocations(t *testing.T) {
config := BotConfig{
Locations: map[string]BotLocationConfig{},
}
result := config.FormatLocations()
if result != "There are no known locations." {
t.Errorf("expected no locations message, got %q", result)
}
}

func TestBotConfigFormatTransitions(t *testing.T) {
config := BotConfig{
Transitions: map[string]BotTransitionConfig{},
}
result := config.FormatTransitions()
if result != "There are no known transitions that can be performed." {
t.Errorf("expected no transitions message, got %q", result)
}
}

func TestParseContainerId(t *testing.T) {
t.Run("valid container id", func(t *testing.T) {
containerId := "bot-transition-ef3f780c-6fe1-4f38-a201-96d32e825bb3-5886f9-41f80f43"
container, err := parseContainerId(containerId)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if container.StubId != "ef3f780c-6fe1-4f38-a201-96d32e825bb3" {
t.Errorf("unexpected stub id: %s", container.StubId)
}
if container.SessionId != "5886f9" {
t.Errorf("unexpected session id: %s", container.SessionId)
}
})

t.Run("invalid container id", func(t *testing.T) {
_, err := parseContainerId("invalid-id")
if err == nil {
t.Error("expected error for invalid container id")
}
})
}
1 change: 1 addition & 0 deletions pkg/abstractions/experimental/bot/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ type BotConfig struct {
Locations map[string]BotLocationConfig `json:"locations" redis:"locations"`
Transitions map[string]BotTransitionConfig `json:"transitions" redis:"transitions"`
ApiKey string `json:"api_key" redis:"api_key"`
BaseUrl string `json:"base_url,omitempty" redis:"base_url,omitempty"`
Authorized bool `json:"authorized" redis:"authorized"`
WelcomeMessage string `json:"welcome_message" redis:"welcome_message"`
}
Expand Down
23 changes: 22 additions & 1 deletion sdk/src/beta9/abstractions/experimental/bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,14 @@ class Bot(RunnerAbstraction, DeployableMixin):
Parameters:
model (Optional[str]):
Which model to use for the bot. Default is "gpt-4o".
Supports OpenAI models (gpt-4o, gpt-4, etc.) and MiniMax models
(MiniMax-M2.7, MiniMax-M2.5, MiniMax-M2.5-highspeed).
api_key (str):
OpenAI API key to use for the bot. In the future this will support other LLM providers.
API key for the LLM provider. Works with OpenAI, MiniMax, or any
OpenAI-compatible API.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new “OpenAI-compatible API” support is blocked by the existing model allowlist: non-allowlisted models still raise ValueError before base_url is applied, so custom providers cannot be used despite the added documentation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk/src/beta9/abstractions/experimental/bot/bot.py, line 231:

<comment>The new “OpenAI-compatible API” support is blocked by the existing model allowlist: non-allowlisted models still raise ValueError before base_url is applied, so custom providers cannot be used despite the added documentation.</comment>

<file context>
@@ -224,8 +224,14 @@ class Bot(RunnerAbstraction, DeployableMixin):
         api_key (str):
-            OpenAI API key to use for the bot. In the future this will support other LLM providers.
+            API key for the LLM provider. Works with OpenAI, MiniMax, or any
+            OpenAI-compatible API.
+        base_url (Optional[str]):
+            Custom base URL for the LLM API. When using MiniMax models, this
</file context>
Fix with Cubic

base_url (Optional[str]):
Custom base URL for the LLM API. When using MiniMax models, this
is automatically set to https://api.minimax.io/v1 if not provided.
locations (Optional[List[BotLocation]]):
A list of locations where the bot can store markers. Default is [].
description (Optional[str]):
Expand All @@ -251,12 +257,21 @@ class Bot(RunnerAbstraction, DeployableMixin):
"gpt-3.5-turbo-16k",
"gpt-3.5-turbo-0613",
"gpt-4-0613",
"MiniMax-M2.7",
"MiniMax-M2.5",
"MiniMax-M2.5-highspeed",
]

# Provider base URLs for OpenAI-compatible providers
PROVIDER_BASE_URLS = {
"minimax": "https://api.minimax.io/v1",
}

def __init__(
self,
model: str = "gpt-4o",
api_key: str = "",
base_url: Optional[str] = None,
locations: List[BotLocation] = [],
description: Optional[str] = None,
volumes: Optional[List[Volume]] = None,
Expand All @@ -273,6 +288,10 @@ def __init__(
f"Invalid model name: {model}. We currently only support: {', '.join(self.VALID_MODELS)}"
)

# Auto-detect provider base URL from model name
if base_url is None and model.startswith("MiniMax-"):
base_url = self.PROVIDER_BASE_URLS["minimax"]

self.is_websocket = True
self._bot_stub: Optional[BotServiceStub] = None
self.syncer: FileSyncer = FileSyncer(self.gateway_stub)
Expand All @@ -286,6 +305,8 @@ def __init__(
self.extra["api_key"] = api_key
self.extra["authorized"] = authorized
self.extra["welcome_message"] = welcome_message
if base_url:
self.extra["base_url"] = base_url

for location in self.locations:
location_config = location.to_dict()
Expand Down
Loading