mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-28 22:42:52 +08:00
feat: restrict AI models to claw402 gateway, default GPT-5.6 Sol
- Offer only claw402 (x402 USDC) models: remove direct-API providers (deepseek/qwen/openai/claude/gemini/grok/kimi/minimax) and BlockRun from supported models, defaults, and the config UI - Sync model catalog with the claw402 catalog (GET /api/v1/catalog): GPT-5.6 Sol/Terra/Luna, Claude Fable 5 / Opus 4.8, DeepSeek-V4 Flash/Pro, GLM-5 — official naming, brand icons, verified prices - Default model gpt-5.6 (Sol) across backend and frontend - fix: omit temperature for GPT-5/o-series reasoning models; they reject non-default values and fail the whole request - fix: hot-reload running traders when a model config is saved under its provider key (previously required a restart to take effect) - fix: allow saving model config edits without re-entering the stored key; backend preserves the existing key when api_key is empty - fix: record AI charges with the effective model name so per-call cost tracking matches the invoked model and price
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -49,6 +49,7 @@ web/node_modules/
|
||||
node_modules/
|
||||
web/dist/
|
||||
web/.vite/
|
||||
*.tsbuildinfo
|
||||
|
||||
# ESLint 临时报告文件(调试时生成,不纳入版本控制)
|
||||
eslint-*.json
|
||||
|
||||
@@ -68,14 +68,7 @@ func (s *Server) handleGetModelConfigs(c *gin.Context) {
|
||||
if len(models) == 0 {
|
||||
logger.Infof("⚠️ No AI models in database, returning defaults")
|
||||
defaultModels := []SafeModelConfig{
|
||||
{ID: "deepseek", Name: "DeepSeek AI", Provider: "deepseek", Enabled: false, HasAPIKey: false},
|
||||
{ID: "qwen", Name: "Qwen AI", Provider: "qwen", Enabled: false, HasAPIKey: false},
|
||||
{ID: "openai", Name: "OpenAI", Provider: "openai", Enabled: false, HasAPIKey: false},
|
||||
{ID: "claude", Name: "Claude AI", Provider: "claude", Enabled: false, HasAPIKey: false},
|
||||
{ID: "gemini", Name: "Gemini AI", Provider: "gemini", Enabled: false, HasAPIKey: false},
|
||||
{ID: "grok", Name: "Grok AI", Provider: "grok", Enabled: false, HasAPIKey: false},
|
||||
{ID: "kimi", Name: "Kimi AI", Provider: "kimi", Enabled: false, HasAPIKey: false},
|
||||
{ID: "minimax", Name: "MiniMax AI", Provider: "minimax", Enabled: false, HasAPIKey: false},
|
||||
{ID: "claw402", Name: "Claw402 (Base USDC)", Provider: "claw402", Enabled: false, HasAPIKey: false},
|
||||
}
|
||||
c.JSON(http.StatusOK, defaultModels)
|
||||
return
|
||||
@@ -116,14 +109,7 @@ func (s *Server) handleGetModelConfigs(c *gin.Context) {
|
||||
if len(safeModels) == 0 {
|
||||
logger.Infof("⚠️ No visible AI models in database, returning defaults")
|
||||
defaultModels := []SafeModelConfig{
|
||||
{ID: "deepseek", Name: "DeepSeek AI", Provider: "deepseek", Enabled: false, HasAPIKey: false},
|
||||
{ID: "qwen", Name: "Qwen AI", Provider: "qwen", Enabled: false, HasAPIKey: false},
|
||||
{ID: "openai", Name: "OpenAI", Provider: "openai", Enabled: false, HasAPIKey: false},
|
||||
{ID: "claude", Name: "Claude AI", Provider: "claude", Enabled: false, HasAPIKey: false},
|
||||
{ID: "gemini", Name: "Gemini AI", Provider: "gemini", Enabled: false, HasAPIKey: false},
|
||||
{ID: "grok", Name: "Grok AI", Provider: "grok", Enabled: false, HasAPIKey: false},
|
||||
{ID: "kimi", Name: "Kimi AI", Provider: "kimi", Enabled: false, HasAPIKey: false},
|
||||
{ID: "minimax", Name: "MiniMax AI", Provider: "minimax", Enabled: false, HasAPIKey: false},
|
||||
{ID: "claw402", Name: "Claw402 (Base USDC)", Provider: "claw402", Enabled: false, HasAPIKey: false},
|
||||
}
|
||||
c.JSON(http.StatusOK, defaultModels)
|
||||
return
|
||||
@@ -192,7 +178,24 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
|
||||
logger.Infof("🔓 Decrypted model config data (UserID: %s)", userID)
|
||||
}
|
||||
|
||||
// Update each model's configuration and track traders that need reload
|
||||
// Update each model's configuration and track traders that need reload.
|
||||
// The request key may be either the model row id or the provider name
|
||||
// (legacy clients send the provider, e.g. "claw402", while trader rows
|
||||
// reference the full model id) — resolve both, mirroring the matching in
|
||||
// AIModelStore.Update, otherwise running traders keep the old model.
|
||||
modelIDCandidates := func(modelID string) map[string]bool {
|
||||
candidates := map[string]bool{modelID: true}
|
||||
if models, listErr := s.store.AIModel().List(userID); listErr == nil {
|
||||
for _, m := range models {
|
||||
if m.ID == modelID || m.Provider == modelID {
|
||||
candidates[m.ID] = true
|
||||
candidates[m.Provider] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
tradersToReload := make(map[string]bool)
|
||||
for modelID, modelData := range req.Models {
|
||||
// SSRF protection: validate custom_api_url before storing
|
||||
@@ -206,9 +209,11 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Find traders using this AI model BEFORE updating
|
||||
traders, _ := s.store.Trader().ListByAIModelID(userID, modelID)
|
||||
for _, t := range traders {
|
||||
tradersToReload[t.ID] = true
|
||||
for candidateID := range modelIDCandidates(modelID) {
|
||||
traders, _ := s.store.Trader().ListByAIModelID(userID, candidateID)
|
||||
for _, t := range traders {
|
||||
tradersToReload[t.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
err := s.store.AIModel().Update(userID, modelID, modelData.Enabled, modelData.APIKey, modelData.CustomAPIURL, modelData.CustomModelName)
|
||||
@@ -239,17 +244,7 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) {
|
||||
func (s *Server) handleGetSupportedModels(c *gin.Context) {
|
||||
// Return static list of supported AI models with default versions
|
||||
supportedModels := []map[string]interface{}{
|
||||
{"id": "deepseek", "name": "DeepSeek", "provider": "deepseek", "defaultModel": "deepseek-chat"},
|
||||
{"id": "qwen", "name": "Qwen", "provider": "qwen", "defaultModel": "qwen3-max"},
|
||||
{"id": "openai", "name": "OpenAI", "provider": "openai", "defaultModel": "gpt-5.1"},
|
||||
{"id": "claude", "name": "Claude", "provider": "claude", "defaultModel": "claude-opus-4-6"},
|
||||
{"id": "gemini", "name": "Google Gemini", "provider": "gemini", "defaultModel": "gemini-3-pro-preview"},
|
||||
{"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"},
|
||||
{"id": "kimi", "name": "Kimi (Moonshot)", "provider": "kimi", "defaultModel": "moonshot-v1-auto"},
|
||||
{"id": "minimax", "name": "MiniMax", "provider": "minimax", "defaultModel": "MiniMax-M2.7"},
|
||||
{"id": "blockrun-base", "name": "BlockRun (Base Wallet)", "provider": "blockrun-base", "defaultModel": "auto"},
|
||||
{"id": "blockrun-sol", "name": "BlockRun (Solana Wallet)", "provider": "blockrun-sol", "defaultModel": "auto"},
|
||||
{"id": "claw402", "name": "Claw402 (Base USDC)", "provider": "claw402", "defaultModel": "deepseek-v4-flash"},
|
||||
{"id": "claw402", "name": "Claw402 (Base USDC)", "provider": "claw402", "defaultModel": "gpt-5.6"},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, supportedModels)
|
||||
|
||||
@@ -213,6 +213,18 @@ func (client *Client) SetAuthHeader(reqHeader http.Header) {
|
||||
reqHeader.Set("Authorization", fmt.Sprintf("Bearer %s", client.APIKey))
|
||||
}
|
||||
|
||||
// modelSupportsCustomTemperature reports whether the target model accepts a
|
||||
// non-default temperature. OpenAI reasoning models (gpt-5 family, o-series)
|
||||
// reject any value other than the default and fail the whole request with
|
||||
// "unsupported_value", so temperature must be omitted for them.
|
||||
func modelSupportsCustomTemperature(model string) bool {
|
||||
m := strings.ToLower(model)
|
||||
return !strings.HasPrefix(m, "gpt-5") &&
|
||||
!strings.HasPrefix(m, "o1") &&
|
||||
!strings.HasPrefix(m, "o3") &&
|
||||
!strings.HasPrefix(m, "o4")
|
||||
}
|
||||
|
||||
func (client *Client) BuildMCPRequestBody(systemPrompt, userPrompt string) map[string]any {
|
||||
// Build messages array
|
||||
messages := []map[string]string{}
|
||||
@@ -242,9 +254,11 @@ func (client *Client) BuildMCPRequestBody(systemPrompt, userPrompt string) map[s
|
||||
|
||||
// Build request body
|
||||
requestBody := map[string]interface{}{
|
||||
"model": client.Model,
|
||||
"messages": messages,
|
||||
"temperature": client.Cfg.Temperature, // Use configured temperature
|
||||
"model": client.Model,
|
||||
"messages": messages,
|
||||
}
|
||||
if modelSupportsCustomTemperature(client.Model) {
|
||||
requestBody["temperature"] = client.Cfg.Temperature
|
||||
}
|
||||
// OpenAI newer models use max_completion_tokens instead of max_tokens
|
||||
if client.Provider == ProviderOpenAI {
|
||||
@@ -655,11 +669,13 @@ func (client *Client) BuildRequestBodyFromRequest(req *Request) map[string]any {
|
||||
}
|
||||
|
||||
// Add optional parameters (only add non-nil parameters)
|
||||
if req.Temperature != nil {
|
||||
requestBody["temperature"] = *req.Temperature
|
||||
} else {
|
||||
// If not set in Request, use Client's configuration
|
||||
requestBody["temperature"] = client.Cfg.Temperature
|
||||
if modelSupportsCustomTemperature(req.Model) {
|
||||
if req.Temperature != nil {
|
||||
requestBody["temperature"] = *req.Temperature
|
||||
} else {
|
||||
// If not set in Request, use Client's configuration
|
||||
requestBody["temperature"] = client.Cfg.Temperature
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI newer models use max_completion_tokens instead of max_tokens
|
||||
|
||||
@@ -50,37 +50,26 @@ func shortAddr(addr string) string {
|
||||
|
||||
const (
|
||||
DefaultClaw402URL = "https://claw402.ai"
|
||||
DefaultClaw402Model = "deepseek-v4-flash"
|
||||
DefaultClaw402Model = "gpt-5.6"
|
||||
)
|
||||
|
||||
// claw402ModelEndpoints maps user-friendly model names to claw402 API paths.
|
||||
// Must stay in sync with the claw402 catalog (GET /api/v1/catalog).
|
||||
var claw402ModelEndpoints = map[string]string{
|
||||
// OpenAI
|
||||
"gpt-5.4": "/api/v1/ai/openai/chat/5.4",
|
||||
"gpt-5.4-pro": "/api/v1/ai/openai/chat/5.4-pro",
|
||||
"gpt-5.3": "/api/v1/ai/openai/chat/5.3",
|
||||
"gpt-5-mini": "/api/v1/ai/openai/chat/5-mini",
|
||||
"gpt-5.6": "/api/v1/ai/openai/chat/5.6",
|
||||
"gpt-5.6-terra": "/api/v1/ai/openai/chat/5.6-terra",
|
||||
"gpt-5.6-luna": "/api/v1/ai/openai/chat/5.6-luna",
|
||||
// Anthropic
|
||||
"claude-opus": "/api/v1/ai/anthropic/messages/opus",
|
||||
"claude-fable": "/api/v1/ai/anthropic/messages/fable",
|
||||
"claude-opus": "/api/v1/ai/anthropic/messages/opus",
|
||||
// DeepSeek
|
||||
"deepseek": "/api/v1/ai/deepseek/chat",
|
||||
"deepseek-reasoner": "/api/v1/ai/deepseek/chat/reasoner",
|
||||
"deepseek-v4-flash": "/api/v1/ai/deepseek/v4-flash",
|
||||
"deepseek-v4-pro": "/api/v1/ai/deepseek/v4-pro",
|
||||
// Qwen
|
||||
"qwen-max": "/api/v1/ai/qwen/chat/max",
|
||||
"qwen-plus": "/api/v1/ai/qwen/chat/plus",
|
||||
"qwen-turbo": "/api/v1/ai/qwen/chat/turbo",
|
||||
"qwen-flash": "/api/v1/ai/qwen/chat/flash",
|
||||
// Grok
|
||||
"grok-4.1": "/api/v1/ai/grok/chat/4.1",
|
||||
// Gemini
|
||||
"gemini-3.1-pro": "/api/v1/ai/gemini/chat/3.1-pro",
|
||||
// Kimi
|
||||
"kimi-k2.5": "/api/v1/ai/kimi/chat/k2.5",
|
||||
// Z.AI (Zhipu)
|
||||
"glm-5": "/api/v1/ai/zhipu/chat",
|
||||
"glm-5-turbo": "/api/v1/ai/zhipu/chat/turbo",
|
||||
"glm-5": "/api/v1/ai/zhipu/chat",
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -24,18 +24,12 @@ var modelPrices = map[string]float64{
|
||||
"deepseek-reasoner": 0.005,
|
||||
"deepseek-v4-flash": 0.003,
|
||||
"deepseek-v4-pro": 0.01,
|
||||
"gpt-5.4": 0.05,
|
||||
"gpt-5.4-pro": 0.50,
|
||||
"gpt-5.3": 0.01,
|
||||
"gpt-5-mini": 0.005,
|
||||
"gpt-5.6": 0.06,
|
||||
"gpt-5.6-terra": 0.03,
|
||||
"gpt-5.6-luna": 0.012,
|
||||
"claude-fable": 0.24,
|
||||
"claude-opus": 0.12,
|
||||
"qwen-max": 0.01,
|
||||
"qwen-plus": 0.005,
|
||||
"qwen-turbo": 0.002,
|
||||
"qwen-flash": 0.002,
|
||||
"grok-4.1": 0.06,
|
||||
"gemini-3.1-pro": 0.03,
|
||||
"kimi-k2.5": 0.008,
|
||||
"glm-5": 0.003,
|
||||
}
|
||||
|
||||
// GetModelPrice returns the price per call for a given model
|
||||
|
||||
@@ -131,9 +131,16 @@ func (at *AutoTrader) runCycle() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Record AI charge (track cost regardless of decision outcome)
|
||||
// Record AI charge (track cost regardless of decision outcome).
|
||||
// Use the effective model name (custom model, e.g. "gpt-5.6") so the
|
||||
// per-call price lookup matches what was actually invoked — at.aiModel is
|
||||
// the provider id (e.g. "claw402") and would fall back to the default price.
|
||||
if aiDecision != nil && at.store != nil {
|
||||
if chargeErr := at.store.AICharge().Record(at.id, at.aiModel, at.config.AIModel); chargeErr != nil {
|
||||
chargeModel := at.config.CustomModelName
|
||||
if chargeModel == "" {
|
||||
chargeModel = at.aiModel
|
||||
}
|
||||
if chargeErr := at.store.AICharge().Record(at.id, chargeModel, at.config.AIModel); chargeErr != nil {
|
||||
at.logWarnf("⚠️ Failed to record AI charge: %v", chargeErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ const MODEL_COLORS: Record<string, string> = {
|
||||
openai: '#10A37F',
|
||||
minimax: '#E45735',
|
||||
claw402: '#7C3AED',
|
||||
zhipu: '#3859F3',
|
||||
}
|
||||
|
||||
// Returns the icon for an AI model
|
||||
|
||||
@@ -4,12 +4,12 @@ import { Trash2, Brain, ExternalLink } from 'lucide-react'
|
||||
import type { AIModel } from '../../types'
|
||||
import type { Language } from '../../i18n/translations'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { getModelIcon } from '../common/ModelIcons'
|
||||
import { getModelIcon, getModelColor } from '../common/ModelIcons'
|
||||
import { ModelStepIndicator } from './ModelStepIndicator'
|
||||
import { ModelCard } from './ModelCard'
|
||||
import {
|
||||
BLOCKRUN_MODELS,
|
||||
CLAW402_MODELS,
|
||||
DEFAULT_CLAW402_MODEL,
|
||||
AI_PROVIDER_CONFIG,
|
||||
getShortName,
|
||||
} from './model-constants'
|
||||
@@ -50,11 +50,18 @@ export function ModelConfigModal({
|
||||
const [baseUrl, setBaseUrl] = useState('')
|
||||
const [modelName, setModelName] = useState('')
|
||||
|
||||
// Always prefer allModels (supportedModels) for provider/id lookup;
|
||||
// fall back to configuredModels for edit mode details (apiKey etc.)
|
||||
const selectedModel =
|
||||
allModels?.find((m) => m.id === selectedModelId) ||
|
||||
configuredModels?.find((m) => m.id === selectedModelId)
|
||||
// The configured entry carries the saved details (wallet address, custom
|
||||
// model name, has_api_key); the template from supportedModels only describes
|
||||
// the provider. When editing, the configured entry must win — both can share
|
||||
// the same id (e.g. "claw402").
|
||||
const configuredModel = configuredModels?.find((m) => m.id === selectedModelId)
|
||||
const templateModel = allModels?.find((m) => m.id === selectedModelId)
|
||||
const selectedModel = editingModelId
|
||||
? configuredModel || templateModel
|
||||
: templateModel || configuredModel
|
||||
const hasExistingKey = Boolean(
|
||||
configuredModel?.has_api_key || configuredModel?.apiKey
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (editingModelId && selectedModel) {
|
||||
@@ -80,7 +87,10 @@ export function ModelConfigModal({
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!selectedModelId || !apiKey.trim()) return
|
||||
if (!selectedModelId) return
|
||||
// Editing with a stored key: an empty key means "keep the existing one"
|
||||
// (the backend preserves the stored key when api_key is empty).
|
||||
if (!apiKey.trim() && !(editingModelId && hasExistingKey)) return
|
||||
onSave(
|
||||
selectedModelId,
|
||||
apiKey.trim(),
|
||||
@@ -189,6 +199,7 @@ export function ModelConfigModal({
|
||||
apiKey={apiKey}
|
||||
modelName={modelName}
|
||||
editingModelId={editingModelId}
|
||||
hasExistingKey={hasExistingKey}
|
||||
initialWalletAddress={selectedModel.walletAddress}
|
||||
initialBalanceUsdc={selectedModel.balanceUsdc}
|
||||
onApiKeyChange={setApiKey}
|
||||
@@ -323,7 +334,7 @@ function ModelSelectionStep({
|
||||
border: '1px solid rgba(46, 139, 87, 0.2)',
|
||||
}}
|
||||
>
|
||||
GPT · Claude · DeepSeek · Gemini · Grok · Qwen · Kimi
|
||||
GPT · Claude · DeepSeek · GLM
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
@@ -345,39 +356,6 @@ function ModelSelectionStep({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{availableModels.some((m) => m.provider?.startsWith('blockrun')) && (
|
||||
<>
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<div
|
||||
className="flex-1 h-px"
|
||||
style={{ background: 'rgba(26,24,19,0.14)' }}
|
||||
/>
|
||||
<span
|
||||
className="text-xs font-medium px-2"
|
||||
style={{ color: '#8A8478' }}
|
||||
>
|
||||
{t('modelConfig.viaBlockrunWallet', language)}
|
||||
</span>
|
||||
<div
|
||||
className="flex-1 h-px"
|
||||
style={{ background: 'rgba(26,24,19,0.14)' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{availableModels
|
||||
.filter((m) => m.provider?.startsWith('blockrun'))
|
||||
.map((model) => (
|
||||
<ModelCard
|
||||
key={model.id}
|
||||
model={model}
|
||||
selected={selectedModelId === model.id}
|
||||
onClick={() => onSelectModel(model.id)}
|
||||
configured={configuredIds.has(model.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="text-xs text-center pt-2" style={{ color: '#8A8478' }}>
|
||||
{t('modelConfig.modelsConfigured', language)}
|
||||
</div>
|
||||
@@ -389,6 +367,7 @@ function Claw402ConfigForm({
|
||||
apiKey,
|
||||
modelName,
|
||||
editingModelId,
|
||||
hasExistingKey,
|
||||
initialWalletAddress,
|
||||
initialBalanceUsdc,
|
||||
onApiKeyChange,
|
||||
@@ -400,6 +379,7 @@ function Claw402ConfigForm({
|
||||
apiKey: string
|
||||
modelName: string
|
||||
editingModelId: string | null
|
||||
hasExistingKey?: boolean
|
||||
initialWalletAddress?: string
|
||||
initialBalanceUsdc?: string
|
||||
onApiKeyChange: (value: string) => void
|
||||
@@ -442,6 +422,11 @@ function Claw402ConfigForm({
|
||||
apiKey.startsWith('0x') &&
|
||||
/^0x[0-9a-fA-F]{64}$/.test(apiKey)
|
||||
|
||||
// Editing with a stored key: allow saving (e.g. switching model) without
|
||||
// re-entering the private key, as long as the field is left blank.
|
||||
const canSubmit =
|
||||
isKeyValid || (Boolean(editingModelId) && Boolean(hasExistingKey) && !apiKey)
|
||||
|
||||
// Truncate address for display
|
||||
|
||||
// Debounced validation when apiKey changes
|
||||
@@ -561,7 +546,7 @@ function Claw402ConfigForm({
|
||||
{t('modelConfig.allModelsClaw', language)}
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-3 mt-3 flex-wrap">
|
||||
{['GPT', 'Claude', 'DeepSeek', 'Gemini', 'Grok', 'Qwen', 'Kimi'].map(
|
||||
{['GPT', 'Claude', 'DeepSeek', 'GLM'].map(
|
||||
(name) => (
|
||||
<span
|
||||
key={name}
|
||||
@@ -592,7 +577,7 @@ function Claw402ConfigForm({
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{CLAW402_MODELS.map((m) => {
|
||||
const isSelected = (modelName || 'deepseek') === m.id
|
||||
const isSelected = (modelName || DEFAULT_CLAW402_MODEL) === m.id
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
@@ -608,7 +593,22 @@ function Claw402ConfigForm({
|
||||
: '1px solid rgba(26,24,19,0.14)',
|
||||
}}
|
||||
>
|
||||
<span className="text-base mt-0.5">{m.icon}</span>
|
||||
<div
|
||||
className="w-7 h-7 mt-0.5 rounded-lg flex items-center justify-center shrink-0"
|
||||
style={{
|
||||
background: '#fff',
|
||||
border: '1px solid rgba(26,24,19,0.10)',
|
||||
}}
|
||||
>
|
||||
{getModelIcon(m.brand, { width: 18, height: 18 }) || (
|
||||
<span
|
||||
className="text-xs font-bold"
|
||||
style={{ color: getModelColor(m.brand) }}
|
||||
>
|
||||
{m.provider[0]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<div
|
||||
@@ -636,8 +636,11 @@ function Claw402ConfigForm({
|
||||
>
|
||||
{m.provider} · {m.desc}
|
||||
</div>
|
||||
<div className="text-[10px]" style={{ color: '#2E8B57' }}>
|
||||
~${m.price}/call
|
||||
<div
|
||||
className="text-[10px] font-medium"
|
||||
style={{ color: '#2E8B57' }}
|
||||
>
|
||||
${m.price} / call
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
@@ -1134,11 +1137,11 @@ function Claw402ConfigForm({
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isKeyValid}
|
||||
disabled={!canSubmit}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: isKeyValid ? '#E0483B' : '#E8E2D5',
|
||||
color: isKeyValid ? '#fff' : '#8A8478',
|
||||
background: canSubmit ? '#E0483B' : '#E8E2D5',
|
||||
color: canSubmit ? '#fff' : '#8A8478',
|
||||
}}
|
||||
>
|
||||
{'🚀 ' + t('modelConfig.startTrading', language)}
|
||||
@@ -1216,9 +1219,7 @@ function StandardProviderConfigForm({
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" style={{ color: '#E0483B' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#E0483B' }}>
|
||||
{selectedModel.provider?.startsWith('blockrun')
|
||||
? t('modelConfig.getStarted', language)
|
||||
: t('modelConfig.getApiKey', language)}
|
||||
{t('modelConfig.getApiKey', language)}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
@@ -1276,9 +1277,7 @@ function StandardProviderConfigForm({
|
||||
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
|
||||
/>
|
||||
</svg>
|
||||
{selectedModel.provider?.startsWith('blockrun')
|
||||
? t('modelConfig.walletPrivateKeyLabel', language)
|
||||
: 'API Key *'}
|
||||
{'API Key *'}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
@@ -1287,11 +1286,7 @@ function StandardProviderConfigForm({
|
||||
placeholder={
|
||||
editingModelId && selectedModel.has_api_key
|
||||
? 'Saved. Re-enter to replace.'
|
||||
: selectedModel.provider === 'blockrun-base'
|
||||
? '0x... (EVM private key)'
|
||||
: selectedModel.provider === 'blockrun-sol'
|
||||
? 'bs58 encoded key (Solana)'
|
||||
: t('enterAPIKey', language)
|
||||
: t('enterAPIKey', language)
|
||||
}
|
||||
className="w-full px-4 py-3 rounded-xl"
|
||||
style={{
|
||||
@@ -1299,13 +1294,12 @@ function StandardProviderConfigForm({
|
||||
border: '1px solid rgba(26,24,19,0.14)',
|
||||
color: '#1A1813',
|
||||
}}
|
||||
required
|
||||
required={!(editingModelId && selectedModel.has_api_key)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Custom Base URL (hidden for BlockRun) */}
|
||||
{!selectedModel.provider?.startsWith('blockrun') && (
|
||||
<div className="space-y-2">
|
||||
{/* Custom Base URL */}
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
className="flex items-center gap-2 text-sm font-semibold"
|
||||
style={{ color: '#1A1813' }}
|
||||
@@ -1338,15 +1332,13 @@ function StandardProviderConfigForm({
|
||||
color: '#1A1813',
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs" style={{ color: '#8A8478' }}>
|
||||
{t('leaveBlankForDefault', language)}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#8A8478' }}>
|
||||
{t('leaveBlankForDefault', language)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Custom Model Name (hidden for BlockRun) */}
|
||||
{!selectedModel.provider?.startsWith('blockrun') && (
|
||||
<div className="space-y-2">
|
||||
{/* Custom Model Name */}
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
className="flex items-center gap-2 text-sm font-semibold"
|
||||
style={{ color: '#1A1813' }}
|
||||
@@ -1379,68 +1371,10 @@ function StandardProviderConfigForm({
|
||||
color: '#1A1813',
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs" style={{ color: '#8A8478' }}>
|
||||
{t('leaveBlankForDefaultModel', language)}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#8A8478' }}>
|
||||
{t('leaveBlankForDefaultModel', language)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* BlockRun Model Selector */}
|
||||
{selectedModel.provider?.startsWith('blockrun') && (
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
className="flex items-center gap-2 text-sm font-semibold"
|
||||
style={{ color: '#1A1813' }}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
style={{ color: '#E0483B' }}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
{t('modelConfig.selectModelLabel', language)}
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{BLOCKRUN_MODELS.map((m) => {
|
||||
const isSelected = (modelName || BLOCKRUN_MODELS[0].id) === m.id
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
onClick={() => onModelNameChange(m.id)}
|
||||
className="flex flex-col items-start px-3 py-2 rounded-xl text-left transition-all"
|
||||
style={{
|
||||
background: isSelected
|
||||
? 'rgba(224, 72, 59, 0.12)'
|
||||
: '#F1ECE2',
|
||||
border: isSelected
|
||||
? '1px solid #E0483B'
|
||||
: '1px solid rgba(26,24,19,0.14)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: isSelected ? '#E0483B' : '#1A1813' }}
|
||||
>
|
||||
{m.name}
|
||||
</span>
|
||||
<span className="text-[10px]" style={{ color: '#8A8478' }}>
|
||||
{m.desc}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div
|
||||
@@ -1478,7 +1412,11 @@ function StandardProviderConfigForm({
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!selectedModel || !apiKey.trim()}
|
||||
disabled={
|
||||
!selectedModel ||
|
||||
(!apiKey.trim() &&
|
||||
!(editingModelId && selectedModel.has_api_key))
|
||||
}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{ background: '#E0483B', color: '#fff' }}
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface Claw402Model {
|
||||
name: string
|
||||
provider: string
|
||||
desc: string
|
||||
icon: string
|
||||
brand: string // key for getModelIcon / getModelColor
|
||||
price: number // USD per call
|
||||
isNew?: boolean
|
||||
}
|
||||
@@ -16,12 +16,6 @@ export interface AIProviderConfig {
|
||||
apiName: string
|
||||
}
|
||||
|
||||
export interface BlockrunModel {
|
||||
id: string
|
||||
name: string
|
||||
desc: string
|
||||
}
|
||||
|
||||
// Get friendly AI model display name
|
||||
export function getModelDisplayName(modelId: string): string {
|
||||
switch (modelId.toLowerCase()) {
|
||||
@@ -42,93 +36,23 @@ export function getShortName(fullName: string): string {
|
||||
return parts.length > 1 ? parts[parts.length - 1] : fullName
|
||||
}
|
||||
|
||||
export const DEFAULT_CLAW402_MODEL = 'deepseek-v4-flash'
|
||||
export const DEFAULT_CLAW402_MODEL = 'gpt-5.6'
|
||||
|
||||
// Models available through Claw402 (x402 USDC payment protocol)
|
||||
// Must stay in sync with the claw402 catalog (GET /api/v1/catalog)
|
||||
export const CLAW402_MODELS: Claw402Model[] = [
|
||||
{ id: 'deepseek-v4-flash', name: 'DeepSeek V4 Flash', provider: 'DeepSeek', desc: '$0.003/call', icon: '⚡', price: 0.003, isNew: true },
|
||||
{ id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro', provider: 'DeepSeek', desc: '$0.01/call', icon: '🧠', price: 0.01, isNew: true },
|
||||
{ id: 'deepseek', name: 'DeepSeek V3', provider: 'DeepSeek', desc: '$0.003/call', icon: '🔥', price: 0.003 },
|
||||
{ id: 'deepseek-reasoner', name: 'DeepSeek R1', provider: 'DeepSeek', desc: '$0.005/call', icon: '🤔', price: 0.005 },
|
||||
{ id: 'gpt-5-mini', name: 'GPT-5 Mini', provider: 'OpenAI', desc: '$0.005/call', icon: '🚀', price: 0.005 },
|
||||
{ id: 'qwen-turbo', name: 'Qwen Turbo', provider: 'Alibaba', desc: '$0.002/call', icon: '⚡', price: 0.002 },
|
||||
{ id: 'qwen-flash', name: 'Qwen Flash', provider: 'Alibaba', desc: '$0.002/call', icon: '⚡', price: 0.002 },
|
||||
{ id: 'qwen-plus', name: 'Qwen Plus', provider: 'Alibaba', desc: '$0.005/call', icon: '✨', price: 0.005 },
|
||||
{ id: 'kimi-k2.5', name: 'Kimi K2.5', provider: 'Moonshot', desc: '$0.008/call', icon: '🌙', price: 0.008 },
|
||||
{ id: 'gpt-5.3', name: 'GPT-5.3', provider: 'OpenAI', desc: '$0.01/call', icon: '💡', price: 0.01 },
|
||||
{ id: 'qwen-max', name: 'Qwen Max', provider: 'Alibaba', desc: '$0.01/call', icon: '🌟', price: 0.01 },
|
||||
{ id: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro', provider: 'Google', desc: '$0.03/call', icon: '💎', price: 0.03 },
|
||||
{ id: 'gpt-5.4', name: 'GPT-5.4', provider: 'OpenAI', desc: '$0.05/call', icon: '⚡', price: 0.05 },
|
||||
{ id: 'grok-4.1', name: 'Grok 4.1', provider: 'xAI', desc: '$0.06/call', icon: '⚡', price: 0.06 },
|
||||
{ id: 'claude-opus', name: 'Claude Opus', provider: 'Anthropic', desc: '$0.12/call', icon: '🎯', price: 0.12 },
|
||||
{ id: 'gpt-5.4-pro', name: 'GPT-5.4 Pro', provider: 'OpenAI', desc: '$0.50/call', icon: '🧠', price: 0.50 },
|
||||
]
|
||||
|
||||
export const BLOCKRUN_MODELS: BlockrunModel[] = [
|
||||
{
|
||||
id: 'gpt-5.2',
|
||||
name: 'GPT-5.2',
|
||||
desc: 'Base wallet payment',
|
||||
},
|
||||
{
|
||||
id: 'claude-opus-4-6',
|
||||
name: 'Claude Opus 4.6',
|
||||
desc: 'Base wallet payment',
|
||||
},
|
||||
{
|
||||
id: 'gemini-3.1-pro',
|
||||
name: 'Gemini 3.1 Pro',
|
||||
desc: 'Base wallet payment',
|
||||
},
|
||||
{
|
||||
id: 'qwen3-max',
|
||||
name: 'Qwen 3 Max',
|
||||
desc: 'Base wallet payment',
|
||||
},
|
||||
{ id: 'gpt-5.6', name: 'GPT-5.6 Sol', provider: 'OpenAI', desc: 'Flagship', brand: 'openai', price: 0.06 },
|
||||
{ id: 'gpt-5.6-terra', name: 'GPT-5.6 Terra', provider: 'OpenAI', desc: 'Balanced', brand: 'openai', price: 0.03 },
|
||||
{ id: 'gpt-5.6-luna', name: 'GPT-5.6 Luna', provider: 'OpenAI', desc: 'Cost-efficient', brand: 'openai', price: 0.012 },
|
||||
{ id: 'claude-fable', name: 'Claude Fable 5', provider: 'Anthropic', desc: 'Most capable', brand: 'claude', price: 0.24 },
|
||||
{ id: 'claude-opus', name: 'Claude Opus 4.8', provider: 'Anthropic', desc: 'Coding & agents flagship', brand: 'claude', price: 0.12 },
|
||||
{ id: 'deepseek-v4-flash', name: 'DeepSeek-V4 Flash', provider: 'DeepSeek', desc: 'Fast general model', brand: 'deepseek', price: 0.003 },
|
||||
{ id: 'deepseek-v4-pro', name: 'DeepSeek-V4 Pro', provider: 'DeepSeek', desc: 'Advanced reasoning', brand: 'deepseek', price: 0.01 },
|
||||
{ id: 'glm-5', name: 'GLM-5', provider: 'Z.ai', desc: 'Deep reasoning flagship', brand: 'zhipu', price: 0.003 },
|
||||
]
|
||||
|
||||
// AI Provider configuration - default models and API links
|
||||
export const AI_PROVIDER_CONFIG: Record<string, AIProviderConfig> = {
|
||||
deepseek: {
|
||||
defaultModel: 'deepseek-chat',
|
||||
apiUrl: 'https://platform.deepseek.com/api_keys',
|
||||
apiName: 'DeepSeek',
|
||||
},
|
||||
qwen: {
|
||||
defaultModel: 'qwen3-max',
|
||||
apiUrl: 'https://dashscope.console.aliyun.com/apiKey',
|
||||
apiName: 'Alibaba Cloud',
|
||||
},
|
||||
openai: {
|
||||
defaultModel: 'gpt-5.2',
|
||||
apiUrl: 'https://platform.openai.com/api-keys',
|
||||
apiName: 'OpenAI',
|
||||
},
|
||||
claude: {
|
||||
defaultModel: 'claude-opus-4-6',
|
||||
apiUrl: 'https://console.anthropic.com/settings/keys',
|
||||
apiName: 'Anthropic',
|
||||
},
|
||||
gemini: {
|
||||
defaultModel: 'gemini-3-pro-preview',
|
||||
apiUrl: 'https://aistudio.google.com/app/apikey',
|
||||
apiName: 'Google AI Studio',
|
||||
},
|
||||
grok: {
|
||||
defaultModel: 'grok-3-latest',
|
||||
apiUrl: 'https://console.x.ai/',
|
||||
apiName: 'xAI',
|
||||
},
|
||||
kimi: {
|
||||
defaultModel: 'moonshot-v1-auto',
|
||||
apiUrl: 'https://platform.moonshot.ai/console/api-keys',
|
||||
apiName: 'Moonshot',
|
||||
},
|
||||
minimax: {
|
||||
defaultModel: 'MiniMax-M2.7',
|
||||
apiUrl: 'https://platform.minimax.io',
|
||||
apiName: 'MiniMax',
|
||||
},
|
||||
claw402: {
|
||||
defaultModel: DEFAULT_CLAW402_MODEL,
|
||||
apiUrl: 'https://claw402.ai',
|
||||
|
||||
Reference in New Issue
Block a user