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:
tinkle-community
2026-07-28 18:16:43 +09:00
parent c5277c0fb9
commit 2f8c6111b1
9 changed files with 157 additions and 292 deletions

1
.gitignore vendored
View File

@@ -49,6 +49,7 @@ web/node_modules/
node_modules/
web/dist/
web/.vite/
*.tsbuildinfo
# ESLint 临时报告文件(调试时生成,不纳入版本控制)
eslint-*.json

View File

@@ -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)

View File

@@ -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

View File

@@ -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() {

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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' }}
>

View File

@@ -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',