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
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,13 @@ MINIMAX_API_KEY=
MINIMAX_MODEL=MiniMax-M2.7
MINIMAX_BASE_URL=https://api.minimax.io/v1

# 88API (set LLM_PROVIDER=api88)
# OpenAI-compatible multi-model gateway: one key for GPT, Claude, Gemini,
# DeepSeek, Grok, MiniMax and more. Get a key at https://88api.ai
API88_API_KEY=
API88_MODEL=claude-sonnet-5-5
API88_BASE_URL=https://88api.ai/v1

# =========================
# Common background jobs
# =========================
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ not include credentials, account data, or exploitable details in public issues.
| Strategies | Strategy API V2 intents, sizing, risk, backtests, and live runtime. |
| Crypto | Binance, OKX, Bitget, Bybit, Gate, HTX, and adapter extensions. |
| Traditional brokers | IBKR and Alpaca workflows. |
| AI providers | OpenRouter, OpenAI-compatible APIs, Google, DeepSeek, Grok, MiniMax, and custom endpoints. |
| AI providers | OpenRouter, OpenAI-compatible APIs, Google, DeepSeek, Grok, MiniMax, 88API, and custom endpoints. |
| Automation | Human API, Agent Gateway, MCP server, Celery jobs, schedules, and notifications. |

Start with the [Indicator guide](docs/trading/INDICATOR_DEV_GUIDE.md),
Expand Down
2 changes: 1 addition & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ docker compose \
| 策略 | Strategy API V2 交易意图、仓位计算、风险控制、回测和实盘运行时。 |
| 加密货币 | Binance、OKX、Bitget、Bybit、Gate、HTX 及可扩展适配器。 |
| 传统券商 | IBKR 和 Alpaca 工作流。 |
| AI 提供商 | OpenRouter、OpenAI 兼容 API、Google、DeepSeek、Grok、MiniMax 和自定义端点。 |
| AI 提供商 | OpenRouter、OpenAI 兼容 API、Google、DeepSeek、Grok、MiniMax、88API 和自定义端点。 |
| 自动化 | Human API、Agent Gateway、MCP Server、Celery 任务、调度和通知。 |

建议从[指标开发指南](docs/trading/INDICATOR_DEV_GUIDE.md)、
Expand Down
10 changes: 10 additions & 0 deletions backend_api_python/app/config/api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@ def LITELLM_API_KEY(cls):
from app.utils.config_loader import load_addon_config
val = load_addon_config().get('litellm', {}).get('api_key')
return val if val else ''

@property
def API88_API_KEY(cls):
"""88API API key"""
env_val = os.getenv('API88_API_KEY', '').strip()
if env_val:
return env_val
from app.utils.config_loader import load_addon_config
val = load_addon_config().get('api88', {}).get('api_key')
return val if val else ''

@property
def TAVILY_API_KEYS(cls):
Expand Down
32 changes: 31 additions & 1 deletion backend_api_python/app/routes/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
'AI_CODE_GEN_MODEL', 'LLM_PROXY_URL', 'LLM_USE_SYSTEM_PROXY',
'JEV_BASE_URL', 'JEV_MODEL', 'JEV_TIMEOUT_SECONDS', 'JEV_MIN_CONFIDENCE', 'AI_DECISION_TIMEOUT_SECONDS',
'SEC_EDGAR_USER_AGENT',
'OPENAI_BASE_URL', 'DEEPSEEK_BASE_URL', 'GROK_BASE_URL', 'ATLASCLOUD_BASE_URL', 'MINIMAX_BASE_URL',
'OPENAI_BASE_URL', 'DEEPSEEK_BASE_URL', 'GROK_BASE_URL', 'ATLASCLOUD_BASE_URL', 'MINIMAX_BASE_URL', 'API88_BASE_URL',
# Trading internals
'ORDER_MODE', 'MAKER_WAIT_SEC',
'FINNHUB_API_KEY', 'FINNHUB_FREE_ONLY',
Expand Down Expand Up @@ -468,6 +468,7 @@
{'value': 'custom', 'label': 'Custom API (OpenAI-compatible)'},
{'value': 'minimax', 'label': 'MiniMax'},
{'value': 'litellm', 'label': 'LiteLLM (100+ providers)'},
{'value': 'api88', 'label': '88API (Multi-model gateway)'},
],
'description': 'Select your preferred LLM provider'
},
Expand Down Expand Up @@ -720,6 +721,35 @@
'description': 'Optional. Override provider base URL (e.g. Azure endpoint)',
'group': 'litellm'
},
# 88API
{
'key': 'API88_API_KEY',
'label': '88API API Key',
'type': 'password',
'required': False,
'link': 'https://88api.ai/console/token',
'link_text': 'settings.link.getApiKey',
'description': '88API API key. One key calls GPT, Claude, Gemini, DeepSeek, Grok, MiniMax and more through an OpenAI-compatible endpoint.',
'group': 'api88'
},
{
'key': 'API88_MODEL',
'label': '88API Model',
'type': 'text',
'default': 'claude-sonnet-5-5',
'link': 'https://88api.ai/pricing',
'link_text': 'settings.link.viewModels',
'description': '88API model ID without provider prefix, e.g. claude-sonnet-5-5, gpt-5.5, gemini-2.5-pro, deepseek-v4-pro. The API key group must include the model.',
'group': 'api88'
},
{
'key': 'API88_BASE_URL',
'label': '88API Base URL',
'type': 'text',
'default': 'https://88api.ai/v1',
'description': '88API OpenAI-compatible API endpoint. Must include /v1.',
'group': 'api88'
},
# Common settings
{
'key': 'OPENROUTER_TEMPERATURE',
Expand Down
36 changes: 28 additions & 8 deletions backend_api_python/app/services/llm.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
LLM service.
Supports multiple providers: OpenRouter, OpenAI, Google Gemini, DeepSeek, Grok,
AtlasCloud, Custom (OpenAI-compatible), MiniMax.
AtlasCloud, Custom (OpenAI-compatible), MiniMax, 88API.
Kept separate from AnalysisService to avoid circular imports.
"""
import json
Expand Down Expand Up @@ -115,6 +115,7 @@ class LLMProvider(Enum):
CUSTOM = "custom"
MINIMAX = "minimax"
LITELLM = "litellm"
API88 = "api88" # 88API; enum and env names cannot start with a digit


# Provider configurations
Expand Down Expand Up @@ -164,6 +165,11 @@ class LLMProvider(Enum):
"default_model": "openai/gpt-5.4",
"fallback_model": "gpt-4o-mini",
},
LLMProvider.API88: {
"base_url": "https://88api.ai/v1",
"default_model": "claude-sonnet-5-5",
"fallback_model": "claude-haiku-5-5",
},
}


Expand All @@ -175,7 +181,7 @@ def __init__(self, provider: str = None):
Initialize LLM service.

Args:
provider: Override the default provider (openrouter, openai, google, deepseek, grok, atlascloud, custom, minimax)
provider: Override the default provider (openrouter, openai, google, deepseek, grok, atlascloud, custom, minimax, api88)
"""
self._provider_override = provider
# Populated by call_llm_api() so callers can report what actually answered;
Expand Down Expand Up @@ -207,7 +213,7 @@ def provider(self) -> LLMProvider:
pass

# Auto-detect: find any provider with a configured API key
# Priority: DeepSeek > AtlasCloud > Grok > MiniMax > OpenAI > Google > OpenRouter
# Priority: DeepSeek > AtlasCloud > Grok > MiniMax > OpenAI > Google > 88API > OpenRouter
# (LiteLLM excluded from auto-detect; must be set explicitly via LLM_PROVIDER=litellm)
priority_order = [
LLMProvider.DEEPSEEK,
Expand All @@ -216,6 +222,7 @@ def provider(self) -> LLMProvider:
LLMProvider.MINIMAX,
LLMProvider.OPENAI,
LLMProvider.GOOGLE,
LLMProvider.API88,
LLMProvider.OPENROUTER,
]

Expand All @@ -241,6 +248,7 @@ def get_api_key(self, provider: LLMProvider = None) -> str:
LLMProvider.CUSTOM: APIKeys.CUSTOM_API_KEY,
LLMProvider.MINIMAX: APIKeys.MINIMAX_API_KEY,
LLMProvider.LITELLM: APIKeys.LITELLM_API_KEY,
LLMProvider.API88: APIKeys.API88_API_KEY,
}
return key_map.get(p, "") or ""

Expand Down Expand Up @@ -447,7 +455,7 @@ def _llm_post(self, url: str, *, headers: dict, json_payload: dict, timeout: int
def _call_openai_compatible(self, messages: list, model: str, temperature: float,
api_key: str, base_url: str, timeout: int,
use_json_mode: bool = True) -> str:
"""Call OpenAI-compatible API (OpenAI, DeepSeek, Grok, AtlasCloud, OpenRouter)."""
"""Call OpenAI-compatible API (OpenAI, DeepSeek, Grok, AtlasCloud, OpenRouter, 88API)."""
url = f"{base_url}/chat/completions"

headers = {"Content-Type": "application/json"}
Expand All @@ -470,18 +478,25 @@ def _call_openai_compatible(self, messages: list, model: str, temperature: float
# its public parameter table currently lists model/messages/temperature/
# max_tokens/stream/top_p and not response_format. Keep prompts JSON-
# oriented while avoiding a provider-side 400 from an unsupported knob.
if use_json_mode and "atlascloud" not in (base_url or "").lower():
# 88API routes claude-* models to Anthropic, which rejects
# {"type": "json_object"} (only text or json_schema), so skip it there too.
normalized_base_url = (base_url or "").lower()
skip_json_mode = "atlascloud" in normalized_base_url or (
"88api.ai" in normalized_base_url and (model or "").lower().startswith("claude")
)
if use_json_mode and not skip_json_mode:
data["response_format"] = {"type": "json_object"}

response = self._llm_post(url, headers=headers, json_payload=data, timeout=timeout)

# Handle non-2xx with provider/model-aware details
if response.status_code >= 400:
normalized_base_url = (base_url or "").lower()
if "atlascloud" in normalized_base_url:
provider_name = "AtlasCloud"
elif "openrouter" in normalized_base_url:
provider_name = "OpenRouter"
elif "88api.ai" in normalized_base_url:
provider_name = "88API"
else:
provider_name = "LLM"
err_text = self._extract_provider_error(response)
Expand Down Expand Up @@ -1137,6 +1152,8 @@ def _normalize_model_for_provider(self, model: str, provider: LLMProvider) -> st
'atlascloud': LLMProvider.ATLASCLOUD,
'atlas': LLMProvider.ATLASCLOUD,
'minimax': LLMProvider.MINIMAX,
'api88': LLMProvider.API88,
'88api': LLMProvider.API88,
}

# If the model prefix matches the current provider, use the extracted model name
Expand Down Expand Up @@ -1171,6 +1188,8 @@ def _detect_provider_from_model(self, model: str) -> Optional[LLMProvider]:
'atlascloud': LLMProvider.ATLASCLOUD,
'atlas': LLMProvider.ATLASCLOUD,
'minimax': LLMProvider.MINIMAX,
'api88': LLMProvider.API88,
'88api': LLMProvider.API88,
'anthropic': LLMProvider.OPENROUTER, # Anthropic only via OpenRouter
'meta': LLMProvider.OPENROUTER, # Meta/Llama only via OpenRouter
'mistral': LLMProvider.OPENROUTER, # Mistral only via OpenRouter
Expand Down Expand Up @@ -1247,7 +1266,7 @@ def call_llm_api(self, messages: list, model: str = None, temperature: float = 0
)
# If no API key for current provider, try to find any available provider
if try_alternative_providers:
for alt_provider in [LLMProvider.DEEPSEEK, LLMProvider.ATLASCLOUD, LLMProvider.GROK, LLMProvider.MINIMAX, LLMProvider.OPENAI, LLMProvider.GOOGLE, LLMProvider.OPENROUTER]:
for alt_provider in [LLMProvider.DEEPSEEK, LLMProvider.ATLASCLOUD, LLMProvider.GROK, LLMProvider.MINIMAX, LLMProvider.OPENAI, LLMProvider.GOOGLE, LLMProvider.API88, LLMProvider.OPENROUTER]:
if alt_provider != p and self.get_api_key(alt_provider):
logger.warning(f"No API key for {p.value}, switching to {alt_provider.value}")
p = alt_provider
Expand Down Expand Up @@ -1460,7 +1479,7 @@ def _try_alternative_providers(self, messages: list, model: str, temperature: fl
"""
Try alternative providers when current provider fails.

Priority: DeepSeek > AtlasCloud > Grok > MiniMax > OpenAI > Google > OpenRouter
Priority: DeepSeek > AtlasCloud > Grok > MiniMax > OpenAI > Google > 88API > OpenRouter
"""
priority_order = [
LLMProvider.DEEPSEEK,
Expand All @@ -1469,6 +1488,7 @@ def _try_alternative_providers(self, messages: list, model: str, temperature: fl
LLMProvider.MINIMAX,
LLMProvider.OPENAI,
LLMProvider.GOOGLE,
LLMProvider.API88,
LLMProvider.OPENROUTER,
]

Expand Down
5 changes: 5 additions & 0 deletions backend_api_python/app/utils/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ def env_get(name: str) -> Optional[str]:
('LITELLM_BASE_URL', 'litellm.base_url', 'string'),
('LITELLM_MODEL', 'litellm.model', 'string'),

# 88API
('API88_API_KEY', 'api88.api_key', 'string'),
('API88_BASE_URL', 'api88.base_url', 'string'),
('API88_MODEL', 'api88.model', 'string'),

# LLM Provider Selection
('LLM_PROVIDER', 'llm.provider', 'string'),
('LLM_PROXY_URL', 'llm.proxy_url', 'string'),
Expand Down
7 changes: 7 additions & 0 deletions backend_api_python/env.example
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,13 @@ MINIMAX_API_KEY=
MINIMAX_MODEL=MiniMax-M2.7
MINIMAX_BASE_URL=https://api.minimax.io/v1

# 88API (set LLM_PROVIDER=api88)
# OpenAI-compatible multi-model gateway: one key for GPT, Claude, Gemini,
# DeepSeek, Grok, MiniMax and more. Get a key at https://88api.ai
API88_API_KEY=
API88_MODEL=claude-sonnet-5-5
API88_BASE_URL=https://88api.ai/v1

# =========================
# Common background jobs
# =========================
Expand Down
Loading
Loading