Просмотр исходного кода

feat: support load channel model from config file (#418)

* feat: support load channel model from config file

* feat: auto create admin key

* fix: ci lint
zijiren 4 месяцев назад
Родитель
Сommit
1e465dcbb8

+ 1 - 3
.github/workflows/release.yml

@@ -86,9 +86,7 @@ jobs:
 
       - name: Generate Swagger
         working-directory: core
-        run: |
-          go install github.com/swaggo/swag/cmd/swag@latest
-          bash scripts/swag.sh
+        run: bash scripts/swag.sh
 
       - name: Build
         working-directory: core

+ 0 - 2
Dockerfile

@@ -16,8 +16,6 @@ COPY ./ /aiproxy
 
 COPY --from=frontend-builder /aiproxy/web/dist/ /aiproxy/core/public/dist/
 
-RUN go install github.com/swaggo/swag/cmd/swag@latest
-
 RUN sh scripts/swag.sh
 
 RUN go build -trimpath -ldflags "-s -w" -o aiproxy

+ 186 - 0
config.example.yaml

@@ -0,0 +1,186 @@
+# AIProxy Configuration File
+# Priority: Environment Variables > Config File > Database
+# This file allows you to configure channels, model configs, and options without using the database
+
+# Channels Configuration
+# Note: Channels from YAML are assigned negative IDs automatically and are not persisted to database
+# They are merged with database channels in memory
+channels:
+  - name: "openai-channel-1"
+    type_name: "openai"  # Can use type_name instead of numeric type
+    key: "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+    base_url: "https://api.openai.com"
+    models:
+      - "gpt-4"
+      - "gpt-3.5-turbo"
+    model_mapping:
+      "gpt-4": "gpt-4-0613"
+    status: 1  # 1=Enabled, 2=Disabled
+    priority: 0
+    balance: 100.0
+    balance_threshold: 10.0
+    enabled_auto_balance_check: true
+    sets:
+      - "default"
+    config:
+      spec:
+        organization: "org-xxxxx"
+
+  - name: "azure-channel-1"
+    type_name: "azure"  # Type name is case-insensitive
+    key: "your-azure-api-key"
+    base_url: "https://your-resource.openai.azure.com"
+    models:
+      - "gpt-4"
+    status: 1
+    priority: 1
+    sets:
+      - "default"
+
+  - name: "claude-channel-1"
+    type_name: "claude"  # "claude" is an alias for "anthropic"
+    key: "sk-ant-xxxxx"
+    base_url: "https://api.anthropic.com"
+    models:
+      - "claude-3-opus-20240229"
+      - "claude-3-sonnet-20240229"
+    status: 1
+    priority: 0
+    sets:
+      - "default"
+      - "premium"
+
+  # You can also use numeric type if preferred
+  - name: "gemini-channel-1"
+    type: 24  # Google Gemini
+    key: "your-gemini-api-key"
+    models:
+      - "gemini-pro"
+    status: 1
+
+# Model Configurations
+modelconfigs:
+  - model: "gpt-4"
+    owner: "openai"
+    type_name: "chat"  # Can use type_name instead of numeric type
+    # OR use numeric type:
+    # type: 1  # ChatCompletions
+    rpm: 3500
+    tpm: 80000
+    retry_times: 3
+    timeout_config:
+      request_timeout: 300
+      stream_request_timeout: 600
+    warn_error_rate: 0.5
+    max_error_rate: 0.8
+    price:
+      input: 0.03
+      output: 0.06
+    config:
+      max_context_tokens: 8192
+      max_output_tokens: 4096
+      vision: false
+      tool_choice: true
+
+  - model: "gpt-3.5-turbo"
+    owner: "openai"
+    type_name: "chat"
+    rpm: 3500
+    tpm: 90000
+    price:
+      input: 0.0005
+      output: 0.0015
+    config:
+      max_context_tokens: 16384
+      max_output_tokens: 4096
+
+  - model: "claude-3-opus-20240229"
+    owner: "anthropic"
+    type_name: "chat"
+    rpm: 4000
+    tpm: 400000
+    price:
+      input: 0.015
+      output: 0.075
+    config:
+      max_context_tokens: 200000
+      max_output_tokens: 4096
+      vision: true
+
+  - model: "claude-3-sonnet-20240229"
+    owner: "anthropic"
+    type_name: "chat"
+    rpm: 4000
+    tpm: 400000
+    price:
+      input: 0.003
+      output: 0.015
+    config:
+      max_context_tokens: 200000
+      max_output_tokens: 4096
+      vision: true
+
+  - model: "text-embedding-3-small"
+    owner: "openai"
+    type_name: "embedding"  # Embedding model
+    rpm: 3000
+    tpm: 1000000
+    price:
+      input: 0.00002
+      output: 0
+    config:
+      max_context_tokens: 8191
+
+  - model: "dall-e-3"
+    owner: "openai"
+    type_name: "image"  # Image generation
+    rpm: 50
+    image_quality_prices:
+      "1024x1024":
+        "standard": 0.040
+        "hd": 0.080
+      "1024x1792":
+        "standard": 0.080
+        "hd": 0.120
+      "1792x1024":
+        "standard": 0.080
+        "hd": 0.120
+
+# System Options Configuration
+options:
+  # Log retention settings (in hours)
+  LogStorageHours: "168"  # 7 days
+  RetryLogStorageHours: "72"  # 3 days
+  LogDetailStorageHours: "24"  # 1 day
+
+  # Clean log batch size
+  CleanLogBatchSize: "1000"
+
+  # IP rate limiting
+  IPGroupsThreshold: "100"  # Requests per minute
+  IPGroupsBanThreshold: "200"  # Ban threshold
+
+  # Log detail settings
+  SaveAllLogDetail: "false"
+  LogDetailRequestBodyMaxSize: "10000"
+  LogDetailResponseBodyMaxSize: "10000"
+
+  # Retry settings
+  RetryTimes: "3"
+
+  # Group settings
+  GroupMaxTokenNum: "0"  # 0 means unlimited
+  GroupConsumeLevelRatio: '{"1":1,"2":0.9,"3":0.8}'
+
+  # Error rate alerts
+  DefaultWarnNotifyErrorRate: "0.5"
+
+  # Usage alerts
+  UsageAlertThreshold: "100"
+  UsageAlertMinAvgThreshold: "10"
+
+  # Fuzzy token threshold
+  FuzzyTokenThreshold: "240000"
+
+  # Disable serve (for maintenance)
+  DisableServe: "false"

+ 267 - 0
config.md

@@ -0,0 +1,267 @@
+# YAML Configuration Guide
+
+AIProxy now supports YAML configuration files for managing channels, model configurations, and system options.
+
+## Configuration Priority
+
+The configuration system follows this priority order (highest to lowest):
+
+1. **Environment Variables** (highest priority)
+2. **YAML Configuration File** (medium priority)
+3. **Database** (lowest priority)
+
+This means:
+- Values set via environment variables will always take precedence
+- YAML configuration will override database values
+- Database values are used as defaults when no other configuration is provided
+
+## Configuration File Location
+
+By default, AIProxy looks for `config.yaml` in the current working directory.
+
+You can specify a custom location using the `CONFIG_FILE_PATH` environment variable:
+
+```bash
+export CONFIG_FILE_PATH=/path/to/your/config.yaml
+```
+
+## Configuration File Structure
+
+The YAML configuration file has three main sections. The channel and modelconfig structures directly correspond to the database model types, making it easy to understand and maintain.
+
+### 1. Channels Configuration
+
+Define your API provider channels:
+
+```yaml
+channels:
+  - name: "openai-primary"
+    type_name: "openai"  # Human-readable type name (recommended)
+    # OR use numeric type:
+    # type: 1  # OpenAI channel type
+    key: "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+    base_url: "https://api.openai.com"
+    models:
+      - "gpt-4"
+      - "gpt-3.5-turbo"
+    model_mapping:
+      "gpt-4": "gpt-4-0613"
+    status: 1  # 1=Enabled, 2=Disabled
+    priority: 0
+    balance: 100.0
+    balance_threshold: 10.0
+    enabled_auto_balance_check: true
+    sets:
+      - "default"
+```
+
+#### Channel Type Names
+
+You can use either `type_name` (human-readable string) or `type` (numeric code). Using `type_name` is recommended for better readability:
+
+**Supported Type Names:**
+- `openai`: OpenAI API
+- `azure` / `azure2`: Azure OpenAI
+- `anthropic` / `claude`: Anthropic Claude
+- `gemini` / `google gemini`: Google Gemini
+- `gemini-openai` / `google gemini (openai)`: Google Gemini via OpenAI API
+- `zhipu`: Zhipu AI
+- `ali` / `aliyun`: Alibaba Cloud
+- `baidu`: Baidu Wenxin
+- `baiduv2` / `baidu v2`: Baidu Wenxin v2
+- `xunfei`: iFlytek Spark
+- `tencent`: Tencent Hunyuan
+- `moonshot`: Moonshot AI
+- `deepseek`: DeepSeek
+- `aws`: AWS Bedrock
+- `vertexai` / `vertex`: Google Vertex AI
+- `xai`: xAI Grok
+- `groq`: Groq
+- `mistral`: Mistral AI
+- `cohere`: Cohere
+- `openrouter`: OpenRouter
+- And many more... (see `core/model/yaml_integration.go` for the complete list)
+
+**Numeric Channel Types:**
+- `1`: OpenAI
+- `3`: Azure
+- `14`: Anthropic/Claude
+- `24`: Google Gemini
+- See `core/model/chtype.go` for complete list
+
+### 2. Model Configurations
+
+Define model-specific settings:
+
+```yaml
+modelconfigs:
+  - model: "gpt-4"
+    owner: "openai"
+    type_name: "chat"  # Human-readable type name (recommended)
+    # OR use numeric type:
+    # type: 1  # ChatCompletions
+    rpm: 3500  # Requests per minute
+    tpm: 80000  # Tokens per minute
+    retry_times: 3
+    timeout_config:
+      request_timeout: 300
+      stream_request_timeout: 600
+    warn_error_rate: 0.5
+    max_error_rate: 0.8
+    price:
+      input: 0.03  # Price per 1000 input tokens
+      output: 0.06  # Price per 1000 output tokens
+    config:
+      max_context_tokens: 8192
+      max_output_tokens: 4096
+      vision: false
+      tool_choice: true
+
+  - model: "text-embedding-3-small"
+    owner: "openai"
+    type_name: "embedding"  # Embedding model
+    rpm: 3000
+    tpm: 1000000
+    price:
+      input: 0.00002
+      output: 0
+```
+
+#### Model Type Names
+
+You can use either `type_name` (human-readable string) or `type` (numeric code). Using `type_name` is recommended for better readability:
+
+**Supported Type Names:**
+- `chat` / `chatcompletions`: Chat completion models
+- `completion` / `completions`: Text completion models
+- `embedding` / `embeddings`: Embedding models
+- `moderation` / `moderations`: Moderation models
+- `image` / `imagegenerations`: Image generation models
+- `imageedit` / `imageedits`: Image editing models
+- `audio` / `speech` / `audiospeech`: Text-to-speech models
+- `transcription` / `audiotranscription`: Audio transcription models
+- `translation` / `audiotranslation`: Audio translation models
+- `rerank`: Reranking models
+- `pdf` / `parsepdf`: PDF parsing models
+- `anthropic`: Anthropic-specific models
+- And more... (see `core/model/yaml_integration.go` for the complete list)
+
+**Numeric Model Types:**
+- `1`: ChatCompletions
+- `2`: Completions
+- `3`: Embeddings
+- `4`: Moderations
+- `5`: ImagesGenerations
+- See `core/relay/mode/define.go` for complete list
+
+#### Model Config Keys
+
+Common configuration keys:
+- `max_context_tokens`: Maximum context window size
+- `max_output_tokens`: Maximum output tokens
+- `vision`: Whether the model supports vision/image inputs
+- `tool_choice`: Whether the model supports function calling
+
+### 3. System Options
+
+Configure system-wide options:
+
+```yaml
+options:
+  # Log retention (in hours)
+  LogStorageHours: "168"  # 7 days
+  RetryLogStorageHours: "72"  # 3 days
+  LogDetailStorageHours: "24"  # 1 day
+
+  # Log settings
+  SaveAllLogDetail: "false"
+  LogDetailRequestBodyMaxSize: "10000"
+  LogDetailResponseBodyMaxSize: "10000"
+
+  # Rate limiting
+  IPGroupsThreshold: "100"  # Requests per minute
+  IPGroupsBanThreshold: "200"
+
+  # Retry settings
+  RetryTimes: "3"
+
+  # Error rate alerts
+  DefaultWarnNotifyErrorRate: "0.5"
+
+  # Usage alerts
+  UsageAlertThreshold: "100"
+```
+
+#### Available Options
+
+- `LogStorageHours`: How long to keep logs (hours)
+- `RetryLogStorageHours`: How long to keep retry logs (hours)
+- `LogDetailStorageHours`: How long to keep detailed logs (hours)
+- `CleanLogBatchSize`: Batch size for log cleanup operations
+- `IPGroupsThreshold`: Request rate limit per IP
+- `IPGroupsBanThreshold`: Ban threshold for IP
+- `SaveAllLogDetail`: Whether to save all request/response details
+- `LogDetailRequestBodyMaxSize`: Max size of request body to log
+- `LogDetailResponseBodyMaxSize`: Max size of response body to log
+- `DisableServe`: Disable API serving (for maintenance)
+- `RetryTimes`: Number of retry attempts
+- `DefaultChannelModels`: Default models for new channels (JSON array)
+- `GroupMaxTokenNum`: Max tokens per group
+- `DefaultWarnNotifyErrorRate`: Default error rate warning threshold
+- `UsageAlertThreshold`: Usage alert threshold
+- `FuzzyTokenThreshold`: Fuzzy token matching threshold
+
+## Example: Complete Configuration
+
+See `config.example.yaml` for a complete example configuration file.
+
+## Usage
+
+1. Create a `config.yaml` file in your project root or specify a custom location via `CONFIG_FILE_PATH`
+
+2. Start AIProxy as usual:
+   ```bash
+   ./aiproxy
+   ```
+
+3. The configuration will be loaded in this order:
+   - Database values (if any)
+   - YAML configuration (overrides database)
+   - Environment variables (overrides everything)
+
+## Updating Configuration at Runtime
+
+Changes to the YAML configuration file require restarting the application to take effect.
+
+However, you can still use the web UI or API to modify configurations at runtime, which will be stored in the database.
+
+## Migration from Database-only Configuration
+
+You can extract your current database configuration and convert it to YAML format:
+
+1. Export your channels via the web UI
+2. Export your model configs via the web UI
+3. Convert to YAML format following the structure in `config.example.yaml`
+
+## Notes
+
+- All values in the `options` section must be strings (they will be parsed according to their type)
+- Channel and model config IDs are optional in YAML - if omitted, the system will auto-generate them
+- When both YAML and database contain the same configuration, YAML takes precedence
+- New configurations from YAML that don't exist in the database will be automatically added
+
+## Troubleshooting
+
+### Configuration not loading
+
+- Check the log output on startup - it will show how many channels/models were loaded from YAML
+- Verify the YAML syntax is correct (use a YAML validator)
+- Ensure the file path is correct (check `CONFIG_FILE_PATH` environment variable)
+
+### Environment variables not overriding YAML
+
+Environment variables override YAML values through the `config.SetXxx()` functions which check for environment variables on every call. Make sure you're using the correct environment variable names (see `core/common/config/env.go` for the list).
+
+### Values reverting to database values
+
+If you modify configuration through the web UI or API, those changes will be written to the database. On next restart, YAML will override those database values again. Use YAML for persistent configuration and the web UI for temporary changes.

+ 2 - 0
core/common/config/env.go

@@ -18,6 +18,7 @@ var (
 	DisableModelConfig   bool
 	Redis                string
 	RedisKeyPrefix       string
+	ConfigFilePath       string
 )
 
 func ReloadEnv() {
@@ -32,6 +33,7 @@ func ReloadEnv() {
 	DisableModelConfig = env.Bool("DISABLE_MODEL_CONFIG", false)
 	Redis = env.String("REDIS", os.Getenv("REDIS_CONN_STRING"))
 	RedisKeyPrefix = os.Getenv("REDIS_KEY_PREFIX")
+	ConfigFilePath = env.String("CONFIG_FILE_PATH", "./config.yaml")
 }
 
 func init() {

+ 19 - 0
core/common/config/yaml_config.go

@@ -0,0 +1,19 @@
+package config
+
+import (
+	"os"
+)
+
+func LoadYAMLConfigData() ([]byte, error) {
+	// Check if file exists
+	info, err := os.Stat(ConfigFilePath)
+	if os.IsNotExist(err) {
+		return nil, err
+	}
+
+	if info.IsDir() {
+		return nil, nil
+	}
+
+	return os.ReadFile(ConfigFilePath)
+}

+ 6 - 36
core/controller/channel.go

@@ -1,7 +1,6 @@
 package controller
 
 import (
-	"errors"
 	"fmt"
 	"maps"
 	"net/http"
@@ -9,13 +8,11 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/bytedance/sonic/ast"
 	"github.com/gin-gonic/gin"
 	"github.com/labring/aiproxy/core/controller/utils"
 	"github.com/labring/aiproxy/core/middleware"
 	"github.com/labring/aiproxy/core/model"
 	"github.com/labring/aiproxy/core/monitor"
-	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/adaptors"
 	log "github.com/sirupsen/logrus"
 )
@@ -218,7 +215,7 @@ func GetChannel(c *gin.Context) {
 // AddChannelRequest represents the request body for adding a channel
 type AddChannelRequest struct {
 	ModelMapping map[string]string    `json:"model_mapping"`
-	Config       *model.ChannelConfig `json:"config"`
+	Configs      model.ChannelConfigs `json:"configs"`
 	Name         string               `json:"name"`
 	Key          string               `json:"key"`
 	BaseURL      string               `json:"base_url"`
@@ -261,37 +258,10 @@ func (r *AddChannelRequest) ToChannel() (*model.Channel, error) {
 		}
 	}
 
-	if r.Config != nil {
-		for key, template := range metadata.Config {
-			v, err := r.Config.Get(key)
-			if err != nil {
-				if errors.Is(err, ast.ErrNotExist) {
-					if template.Required {
-						return nil, fmt.Errorf("config %s is required: %w", key, err)
-					}
-					continue
-				}
-
-				return nil, fmt.Errorf("config %s is invalid: %w", key, err)
-			}
-
-			if !v.Exists() {
-				if template.Required {
-					return nil, fmt.Errorf("config %s is required: %w", key, err)
-				}
-				continue
-			}
-
-			if template.Validator != nil {
-				i, err := v.Interface()
-				if err != nil {
-					return nil, fmt.Errorf("config %s is invalid: %w", key, err)
-				}
-
-				err = adaptor.ValidateConfigTemplateValue(template, i)
-				if err != nil {
-					return nil, fmt.Errorf("config %s is invalid: %w", key, err)
-				}
+	if r.Configs != nil {
+		if metadata.ConfigTemplates.Validator != nil {
+			if err := metadata.ConfigTemplates.Validator(r.Configs); err != nil {
+				return nil, fmt.Errorf("config validate faild: %w", err)
 			}
 		}
 	}
@@ -305,7 +275,7 @@ func (r *AddChannelRequest) ToChannel() (*model.Channel, error) {
 		ModelMapping: maps.Clone(r.ModelMapping),
 		Priority:     r.Priority,
 		Status:       r.Status,
-		Config:       r.Config,
+		Configs:      r.Configs,
 		Sets:         slices.Clone(r.Sets),
 	}, nil
 }

+ 5 - 6
core/controller/dashboard.go

@@ -1,7 +1,6 @@
 package controller
 
 import (
-	"encoding/json"
 	"errors"
 	"fmt"
 	"net/http"
@@ -9,7 +8,6 @@ import (
 	"strconv"
 	"time"
 
-	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
 	"github.com/labring/aiproxy/core/common/reqlimit"
 	"github.com/labring/aiproxy/core/controller/utils"
@@ -317,15 +315,16 @@ type GroupModel struct {
 	EnabledPlugins []string           `json:"enabled_plugins,omitempty"`
 }
 
-func getEnabledPlugins(plugin map[string]json.RawMessage) []string {
+func getEnabledPlugins(plugin map[string]map[string]any) []string {
 	enabledPlugins := make([]string, 0, len(plugin))
 	for pluginName, pluginConfig := range plugin {
-		pluginConfigNode, err := sonic.Get(pluginConfig)
-		if err != nil {
+		enable, ok := pluginConfig["enable"]
+		if !ok {
 			continue
 		}
 
-		if enable, err := pluginConfigNode.Get("enable").Bool(); err == nil && enable {
+		e, _ := enable.(bool)
+		if e {
 			enabledPlugins = append(enabledPlugins, pluginName)
 		}
 	}

+ 68 - 55
core/docs/docs.go

@@ -4830,7 +4830,9 @@ const docTemplate = `{
                             47,
                             48,
                             49,
-                            50
+                            50,
+                            51,
+                            52
                         ],
                         "type": "integer",
                         "description": "Channel type",
@@ -8393,44 +8395,25 @@ const docTemplate = `{
                 "description": {
                     "type": "string"
                 },
-                "example": {},
+                "example": {
+                    "type": "string"
+                },
                 "name": {
                     "type": "string"
                 },
                 "required": {
                     "type": "boolean"
-                },
-                "type": {
-                    "$ref": "#/definitions/adaptor.ConfigType"
                 }
             }
         },
-        "adaptor.ConfigTemplates": {
-            "type": "object",
-            "additionalProperties": {
-                "$ref": "#/definitions/adaptor.ConfigTemplate"
-            }
-        },
-        "adaptor.ConfigType": {
-            "type": "string",
-            "enum": [
-                "string",
-                "number",
-                "bool",
-                "object"
-            ],
-            "x-enum-varnames": [
-                "ConfigTypeString",
-                "ConfigTypeNumber",
-                "ConfigTypeBool",
-                "ConfigTypeObject"
-            ]
-        },
         "adaptors.AdaptorMeta": {
             "type": "object",
             "properties": {
-                "config": {
-                    "$ref": "#/definitions/adaptor.ConfigTemplates"
+                "configs": {
+                    "type": "object",
+                    "additionalProperties": {
+                        "$ref": "#/definitions/adaptor.ConfigTemplate"
+                    }
                 },
                 "defaultBaseUrl": {
                     "type": "string"
@@ -8466,8 +8449,8 @@ const docTemplate = `{
                 "base_url": {
                     "type": "string"
                 },
-                "config": {
-                    "$ref": "#/definitions/model.ChannelConfig"
+                "configs": {
+                    "$ref": "#/definitions/model.ChannelConfigs"
                 },
                 "key": {
                     "type": "string"
@@ -8516,6 +8499,15 @@ const docTemplate = `{
                 "name": {
                     "type": "string"
                 },
+                "period_last_update_time": {
+                    "type": "integer"
+                },
+                "period_quota": {
+                    "type": "number"
+                },
+                "period_type": {
+                    "type": "string"
+                },
                 "quota": {
                     "type": "number"
                 },
@@ -8574,10 +8566,8 @@ const docTemplate = `{
                 "plugin": {
                     "type": "object",
                     "additionalProperties": {
-                        "type": "array",
-                        "items": {
-                            "type": "integer"
-                        }
+                        "type": "object",
+                        "additionalProperties": {}
                     }
                 },
                 "price": {
@@ -9314,10 +9304,8 @@ const docTemplate = `{
                 "plugin": {
                     "type": "object",
                     "additionalProperties": {
-                        "type": "array",
-                        "items": {
-                            "type": "integer"
-                        }
+                        "type": "object",
+                        "additionalProperties": {}
                     }
                 },
                 "price": {
@@ -9555,6 +9543,14 @@ const docTemplate = `{
                 "name": {
                     "description": "The name of the tool.",
                     "type": "string"
+                },
+                "outputSchema": {
+                    "description": "A JSON Schema object defining the expected output returned by the tool .",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/mcp.ToolOutputSchema"
+                        }
+                    ]
                 }
             }
         },
@@ -9605,6 +9601,28 @@ const docTemplate = `{
                 }
             }
         },
+        "mcp.ToolOutputSchema": {
+            "type": "object",
+            "properties": {
+                "$defs": {
+                    "type": "object",
+                    "additionalProperties": {}
+                },
+                "properties": {
+                    "type": "object",
+                    "additionalProperties": {}
+                },
+                "required": {
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    }
+                },
+                "type": {
+                    "type": "string"
+                }
+            }
+        },
         "middleware.APIResponse": {
             "type": "object",
             "properties": {
@@ -9701,8 +9719,8 @@ const docTemplate = `{
                         "$ref": "#/definitions/model.ChannelTest"
                     }
                 },
-                "config": {
-                    "$ref": "#/definitions/model.ChannelConfig"
+                "configs": {
+                    "$ref": "#/definitions/model.ChannelConfigs"
                 },
                 "created_at": {
                     "type": "string"
@@ -9760,16 +9778,9 @@ const docTemplate = `{
                 }
             }
         },
-        "model.ChannelConfig": {
+        "model.ChannelConfigs": {
             "type": "object",
-            "properties": {
-                "spec": {
-                    "type": "array",
-                    "items": {
-                        "type": "integer"
-                    }
-                }
-            }
+            "additionalProperties": {}
         },
         "model.ChannelTest": {
             "type": "object",
@@ -9849,7 +9860,9 @@ const docTemplate = `{
                 47,
                 48,
                 49,
-                50
+                50,
+                51,
+                52
             ],
             "x-enum-varnames": [
                 "ChannelTypeOpenAI",
@@ -9889,7 +9902,9 @@ const docTemplate = `{
                 "ChannelTypeJina",
                 "ChannelTypeTextEmbeddingsInference",
                 "ChannelTypeQianfan",
-                "ChannelTypeSangforAICP"
+                "ChannelTypeSangforAICP",
+                "ChannelTypeStreamlake",
+                "ChannelTypeZhipuCoding"
             ]
         },
         "model.ChartData": {
@@ -11111,10 +11126,8 @@ const docTemplate = `{
                 "plugin": {
                     "type": "object",
                     "additionalProperties": {
-                        "type": "array",
-                        "items": {
-                            "type": "integer"
-                        }
+                        "type": "object",
+                        "additionalProperties": {}
                     }
                 },
                 "price": {

+ 68 - 55
core/docs/swagger.json

@@ -4821,7 +4821,9 @@
                             47,
                             48,
                             49,
-                            50
+                            50,
+                            51,
+                            52
                         ],
                         "type": "integer",
                         "description": "Channel type",
@@ -8384,44 +8386,25 @@
                 "description": {
                     "type": "string"
                 },
-                "example": {},
+                "example": {
+                    "type": "string"
+                },
                 "name": {
                     "type": "string"
                 },
                 "required": {
                     "type": "boolean"
-                },
-                "type": {
-                    "$ref": "#/definitions/adaptor.ConfigType"
                 }
             }
         },
-        "adaptor.ConfigTemplates": {
-            "type": "object",
-            "additionalProperties": {
-                "$ref": "#/definitions/adaptor.ConfigTemplate"
-            }
-        },
-        "adaptor.ConfigType": {
-            "type": "string",
-            "enum": [
-                "string",
-                "number",
-                "bool",
-                "object"
-            ],
-            "x-enum-varnames": [
-                "ConfigTypeString",
-                "ConfigTypeNumber",
-                "ConfigTypeBool",
-                "ConfigTypeObject"
-            ]
-        },
         "adaptors.AdaptorMeta": {
             "type": "object",
             "properties": {
-                "config": {
-                    "$ref": "#/definitions/adaptor.ConfigTemplates"
+                "configs": {
+                    "type": "object",
+                    "additionalProperties": {
+                        "$ref": "#/definitions/adaptor.ConfigTemplate"
+                    }
                 },
                 "defaultBaseUrl": {
                     "type": "string"
@@ -8457,8 +8440,8 @@
                 "base_url": {
                     "type": "string"
                 },
-                "config": {
-                    "$ref": "#/definitions/model.ChannelConfig"
+                "configs": {
+                    "$ref": "#/definitions/model.ChannelConfigs"
                 },
                 "key": {
                     "type": "string"
@@ -8507,6 +8490,15 @@
                 "name": {
                     "type": "string"
                 },
+                "period_last_update_time": {
+                    "type": "integer"
+                },
+                "period_quota": {
+                    "type": "number"
+                },
+                "period_type": {
+                    "type": "string"
+                },
                 "quota": {
                     "type": "number"
                 },
@@ -8565,10 +8557,8 @@
                 "plugin": {
                     "type": "object",
                     "additionalProperties": {
-                        "type": "array",
-                        "items": {
-                            "type": "integer"
-                        }
+                        "type": "object",
+                        "additionalProperties": {}
                     }
                 },
                 "price": {
@@ -9305,10 +9295,8 @@
                 "plugin": {
                     "type": "object",
                     "additionalProperties": {
-                        "type": "array",
-                        "items": {
-                            "type": "integer"
-                        }
+                        "type": "object",
+                        "additionalProperties": {}
                     }
                 },
                 "price": {
@@ -9546,6 +9534,14 @@
                 "name": {
                     "description": "The name of the tool.",
                     "type": "string"
+                },
+                "outputSchema": {
+                    "description": "A JSON Schema object defining the expected output returned by the tool .",
+                    "allOf": [
+                        {
+                            "$ref": "#/definitions/mcp.ToolOutputSchema"
+                        }
+                    ]
                 }
             }
         },
@@ -9596,6 +9592,28 @@
                 }
             }
         },
+        "mcp.ToolOutputSchema": {
+            "type": "object",
+            "properties": {
+                "$defs": {
+                    "type": "object",
+                    "additionalProperties": {}
+                },
+                "properties": {
+                    "type": "object",
+                    "additionalProperties": {}
+                },
+                "required": {
+                    "type": "array",
+                    "items": {
+                        "type": "string"
+                    }
+                },
+                "type": {
+                    "type": "string"
+                }
+            }
+        },
         "middleware.APIResponse": {
             "type": "object",
             "properties": {
@@ -9692,8 +9710,8 @@
                         "$ref": "#/definitions/model.ChannelTest"
                     }
                 },
-                "config": {
-                    "$ref": "#/definitions/model.ChannelConfig"
+                "configs": {
+                    "$ref": "#/definitions/model.ChannelConfigs"
                 },
                 "created_at": {
                     "type": "string"
@@ -9751,16 +9769,9 @@
                 }
             }
         },
-        "model.ChannelConfig": {
+        "model.ChannelConfigs": {
             "type": "object",
-            "properties": {
-                "spec": {
-                    "type": "array",
-                    "items": {
-                        "type": "integer"
-                    }
-                }
-            }
+            "additionalProperties": {}
         },
         "model.ChannelTest": {
             "type": "object",
@@ -9840,7 +9851,9 @@
                 47,
                 48,
                 49,
-                50
+                50,
+                51,
+                52
             ],
             "x-enum-varnames": [
                 "ChannelTypeOpenAI",
@@ -9880,7 +9893,9 @@
                 "ChannelTypeJina",
                 "ChannelTypeTextEmbeddingsInference",
                 "ChannelTypeQianfan",
-                "ChannelTypeSangforAICP"
+                "ChannelTypeSangforAICP",
+                "ChannelTypeStreamlake",
+                "ChannelTypeZhipuCoding"
             ]
         },
         "model.ChartData": {
@@ -11102,10 +11117,8 @@
                 "plugin": {
                     "type": "object",
                     "additionalProperties": {
-                        "type": "array",
-                        "items": {
-                            "type": "integer"
-                        }
+                        "type": "object",
+                        "additionalProperties": {}
                     }
                 },
                 "price": {

+ 58 - 48
core/docs/swagger.yaml

@@ -3,34 +3,19 @@ definitions:
     properties:
       description:
         type: string
-      example: {}
+      example:
+        type: string
       name:
         type: string
       required:
         type: boolean
-      type:
-        $ref: '#/definitions/adaptor.ConfigType'
-    type: object
-  adaptor.ConfigTemplates:
-    additionalProperties:
-      $ref: '#/definitions/adaptor.ConfigTemplate'
     type: object
-  adaptor.ConfigType:
-    enum:
-    - string
-    - number
-    - bool
-    - object
-    type: string
-    x-enum-varnames:
-    - ConfigTypeString
-    - ConfigTypeNumber
-    - ConfigTypeBool
-    - ConfigTypeObject
   adaptors.AdaptorMeta:
     properties:
-      config:
-        $ref: '#/definitions/adaptor.ConfigTemplates'
+      configs:
+        additionalProperties:
+          $ref: '#/definitions/adaptor.ConfigTemplate'
+        type: object
       defaultBaseUrl:
         type: string
       fetures:
@@ -53,8 +38,8 @@ definitions:
     properties:
       base_url:
         type: string
-      config:
-        $ref: '#/definitions/model.ChannelConfig'
+      configs:
+        $ref: '#/definitions/model.ChannelConfigs'
       key:
         type: string
       model_mapping:
@@ -86,6 +71,12 @@ definitions:
         type: array
       name:
         type: string
+      period_last_update_time:
+        type: integer
+      period_quota:
+        type: number
+      period_type:
+        type: string
       quota:
         type: number
       subnets:
@@ -126,9 +117,8 @@ definitions:
         $ref: '#/definitions/model.ModelOwner'
       plugin:
         additionalProperties:
-          items:
-            type: integer
-          type: array
+          additionalProperties: {}
+          type: object
         type: object
       price:
         $ref: '#/definitions/model.Price'
@@ -613,9 +603,8 @@ definitions:
         $ref: '#/definitions/model.ModelOwner'
       plugin:
         additionalProperties:
-          items:
-            type: integer
-          type: array
+          additionalProperties: {}
+          type: object
         type: object
       price:
         $ref: '#/definitions/model.Price'
@@ -778,6 +767,11 @@ definitions:
       name:
         description: The name of the tool.
         type: string
+      outputSchema:
+        allOf:
+        - $ref: '#/definitions/mcp.ToolOutputSchema'
+        description: A JSON Schema object defining the expected output returned by
+          the tool .
     type: object
   mcp.ToolAnnotation:
     properties:
@@ -812,6 +806,21 @@ definitions:
       type:
         type: string
     type: object
+  mcp.ToolOutputSchema:
+    properties:
+      $defs:
+        additionalProperties: {}
+        type: object
+      properties:
+        additionalProperties: {}
+        type: object
+      required:
+        items:
+          type: string
+        type: array
+      type:
+        type: string
+    type: object
   middleware.APIResponse:
     properties:
       data: {}
@@ -889,8 +898,8 @@ definitions:
         items:
           $ref: '#/definitions/model.ChannelTest'
         type: array
-      config:
-        $ref: '#/definitions/model.ChannelConfig'
+      configs:
+        $ref: '#/definitions/model.ChannelConfigs'
       created_at:
         type: string
       enabled_auto_balance_check:
@@ -928,12 +937,8 @@ definitions:
       used_amount:
         type: number
     type: object
-  model.ChannelConfig:
-    properties:
-      spec:
-        items:
-          type: integer
-        type: array
+  model.ChannelConfigs:
+    additionalProperties: {}
     type: object
   model.ChannelTest:
     properties:
@@ -1000,6 +1005,8 @@ definitions:
     - 48
     - 49
     - 50
+    - 51
+    - 52
     type: integer
     x-enum-varnames:
     - ChannelTypeOpenAI
@@ -1040,6 +1047,8 @@ definitions:
     - ChannelTypeTextEmbeddingsInference
     - ChannelTypeQianfan
     - ChannelTypeSangforAICP
+    - ChannelTypeStreamlake
+    - ChannelTypeZhipuCoding
   model.ChartData:
     properties:
       audio_input_tokens:
@@ -1062,16 +1071,16 @@ definitions:
         type: integer
       retry_count:
         type: integer
-      status_400_count:
-        type: integer
-      status_429_count:
-        type: integer
       status_4xx_count:
         type: integer
       status_500_count:
         type: integer
       status_5xx_count:
         type: integer
+      status_400_count:
+        type: integer
+      status_429_count:
+        type: integer
       timestamp:
         type: integer
       total_time_milliseconds:
@@ -1473,8 +1482,6 @@ definitions:
         type: integer
       rpm:
         type: integer
-      status_5xx_count:
-        type: integer
       status_400_count:
         type: integer
       status_429_count:
@@ -1483,6 +1490,8 @@ definitions:
         type: integer
       status_500_count:
         type: integer
+      status_5xx_count:
+        type: integer
       token_names:
         items:
           type: string
@@ -1856,9 +1865,8 @@ definitions:
         $ref: '#/definitions/model.ModelOwner'
       plugin:
         additionalProperties:
-          items:
-            type: integer
-          type: array
+          additionalProperties: {}
+          type: object
         type: object
       price:
         $ref: '#/definitions/model.Price'
@@ -2419,6 +2427,8 @@ definitions:
         type: integer
       retry_count:
         type: integer
+      status_5xx_count:
+        type: integer
       status_400_count:
         type: integer
       status_429_count:
@@ -2427,8 +2437,6 @@ definitions:
         type: integer
       status_500_count:
         type: integer
-      status_5xx_count:
-        type: integer
       timestamp:
         type: integer
       token_name:
@@ -5618,6 +5626,8 @@ paths:
         - 48
         - 49
         - 50
+        - 51
+        - 52
         in: path
         name: type
         required: true

+ 2 - 1
core/go.mod

@@ -13,6 +13,7 @@ require (
 	github.com/gin-contrib/gzip v1.2.5
 	github.com/gin-gonic/gin v1.11.0
 	github.com/glebarez/sqlite v1.11.0
+	github.com/go-viper/mapstructure/v2 v2.4.0
 	github.com/golang-jwt/jwt/v5 v5.3.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/websocket v1.5.3
@@ -39,6 +40,7 @@ require (
 	golang.org/x/image v0.33.0
 	golang.org/x/sync v0.18.0
 	google.golang.org/api v0.256.0
+	gopkg.in/yaml.v3 v3.0.1
 	gorm.io/driver/mysql v1.6.0
 	gorm.io/driver/postgres v1.6.0
 	gorm.io/gorm v1.31.1
@@ -150,7 +152,6 @@ require (
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
 	google.golang.org/grpc v1.76.0 // indirect
 	google.golang.org/protobuf v1.36.10 // indirect
-	gopkg.in/yaml.v3 v3.0.1 // indirect
 	modernc.org/libc v1.67.0 // indirect
 	modernc.org/mathutil v1.7.1 // indirect
 	modernc.org/memory v1.11.0 // indirect

+ 2 - 0
core/go.sum

@@ -134,6 +134,8 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1
 github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
 github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
 github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
+github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
+github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=

+ 4 - 0
core/main.go

@@ -42,6 +42,10 @@ func main() {
 
 	config.ReloadEnv()
 
+	if err := ensureAdminKey(); err != nil {
+		log.Warn("failed to ensure AdminKey: " + err.Error())
+	}
+
 	common.InitLog(log.StandardLogger(), config.DebugEnabled)
 
 	printLoadedEnvFiles()

+ 38 - 1
core/model/cache.go

@@ -20,6 +20,7 @@ import (
 	"github.com/maruel/natural"
 	"github.com/redis/go-redis/v9"
 	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
 )
 
 const (
@@ -888,6 +889,9 @@ func InitModelConfigAndChannelCache() error {
 		return err
 	}
 
+	// Apply YAML config overrides to model configs
+	modelConfig = applyYAMLConfigToModelConfigCache(modelConfig)
+
 	// Load enabled channels from database
 	enabledChannels, err := LoadEnabledChannels()
 	if err != nil {
@@ -938,6 +942,12 @@ func LoadEnabledChannels() ([]*Channel, error) {
 		return nil, err
 	}
 
+	configChannels := NewConfigChannels(LoadYAMLConfig(), ChannelStatusEnabled)
+	if len(configChannels) != 0 {
+		log.Infof("added %d channels from config", len(configChannels))
+		channels = append(channels, configChannels...)
+	}
+
 	for _, channel := range channels {
 		initializeChannelModels(channel)
 		initializeChannelModelMapping(channel)
@@ -954,6 +964,12 @@ func LoadDisabledChannels() ([]*Channel, error) {
 		return nil, err
 	}
 
+	configChannels := NewConfigChannels(LoadYAMLConfig(), ChannelStatusDisabled)
+	if len(configChannels) != 0 {
+		log.Infof("added %d channels from config", len(configChannels))
+		channels = append(channels, configChannels...)
+	}
+
 	for _, channel := range channels {
 		initializeChannelModels(channel)
 		initializeChannelModelMapping(channel)
@@ -970,6 +986,12 @@ func LoadChannels() ([]*Channel, error) {
 		return nil, err
 	}
 
+	configChannels := NewConfigChannels(LoadYAMLConfig(), 0)
+	if len(configChannels) != 0 {
+		log.Infof("added %d channels from config", len(configChannels))
+		channels = append(channels, configChannels...)
+	}
+
 	for _, channel := range channels {
 		initializeChannelModels(channel)
 		initializeChannelModelMapping(channel)
@@ -983,7 +1005,22 @@ func LoadChannelByID(id int) (*Channel, error) {
 
 	err := DB.First(&channel, id).Error
 	if err != nil {
-		return nil, err
+		if !errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil, err
+		}
+
+		chs, err := LoadChannels()
+		if err != nil {
+			return nil, err
+		}
+
+		for _, c := range chs {
+			if c.ID == id {
+				return c, nil
+			}
+		}
+
+		return nil, gorm.ErrRecordNotFound
 	}
 
 	initializeChannelModels(&channel)

+ 37 - 61
core/model/channel.go

@@ -2,14 +2,12 @@ package model
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"slices"
 	"strings"
 	"time"
 
 	"github.com/bytedance/sonic"
-	"github.com/bytedance/sonic/ast"
 	"github.com/labring/aiproxy/core/common"
 	"github.com/labring/aiproxy/core/common/config"
 	"github.com/labring/aiproxy/core/monitor"
@@ -32,66 +30,29 @@ const (
 	ChannelDefaultSet = "default"
 )
 
-type ChannelConfig struct {
-	Spec json.RawMessage `json:"spec"`
-}
-
-// validate spec json is map[string]any
-func (c *ChannelConfig) UnmarshalJSON(data []byte) error {
-	type Alias ChannelConfig
-
-	alias := (*Alias)(c)
-	if err := sonic.Unmarshal(data, alias); err != nil {
-		return err
-	}
-
-	if len(alias.Spec) > 0 {
-		var spec map[string]any
-		if err := sonic.Unmarshal(alias.Spec, &spec); err != nil {
-			return fmt.Errorf("invalid spec json: %w", err)
-		}
-	}
-
-	return nil
-}
-
-func (c *ChannelConfig) SpecConfig(obj any) error {
-	if c == nil || len(c.Spec) == 0 {
-		return nil
-	}
-	return sonic.Unmarshal(c.Spec, obj)
-}
-
-func (c *ChannelConfig) Get(key ...any) (ast.Node, error) {
-	if c == nil || len(c.Spec) == 0 {
-		return ast.Node{}, ast.ErrNotExist
-	}
-	return sonic.Get(c.Spec, key...)
-}
-
 type Channel struct {
-	DeletedAt               gorm.DeletedAt    `gorm:"index"                              json:"-"`
-	CreatedAt               time.Time         `gorm:"index"                              json:"created_at"`
-	LastTestErrorAt         time.Time         `                                          json:"last_test_error_at"`
-	ChannelTests            []*ChannelTest    `gorm:"foreignKey:ChannelID;references:ID" json:"channel_tests,omitempty"`
-	BalanceUpdatedAt        time.Time         `                                          json:"balance_updated_at"`
-	ModelMapping            map[string]string `gorm:"serializer:fastjson;type:text"      json:"model_mapping"`
-	Key                     string            `gorm:"type:text;index:,length:191"        json:"key"`
-	Name                    string            `gorm:"size:64;index"                      json:"name"`
-	BaseURL                 string            `gorm:"size:128;index"                     json:"base_url"`
-	Models                  []string          `gorm:"serializer:fastjson;type:text"      json:"models"`
-	Balance                 float64           `                                          json:"balance"`
-	ID                      int               `gorm:"primaryKey"                         json:"id"`
-	UsedAmount              float64           `gorm:"index"                              json:"used_amount"`
-	RequestCount            int               `gorm:"index"                              json:"request_count"`
-	RetryCount              int               `gorm:"index"                              json:"retry_count"`
-	Status                  int               `gorm:"default:1;index"                    json:"status"`
-	Type                    ChannelType       `gorm:"default:0;index"                    json:"type"`
-	Priority                int32             `                                          json:"priority"`
-	EnabledAutoBalanceCheck bool              `                                          json:"enabled_auto_balance_check"`
-	BalanceThreshold        float64           `                                          json:"balance_threshold"`
-	Config                  *ChannelConfig    `gorm:"serializer:fastjson;type:text"      json:"config,omitempty"`
-	Sets                    []string          `gorm:"serializer:fastjson;type:text"      json:"sets,omitempty"`
+	DeletedAt               gorm.DeletedAt    `gorm:"index"                              json:"-"                          yaml:"-"`
+	CreatedAt               time.Time         `gorm:"index"                              json:"created_at"                 yaml:"-"`
+	LastTestErrorAt         time.Time         `                                          json:"last_test_error_at"         yaml:"-"`
+	ChannelTests            []*ChannelTest    `gorm:"foreignKey:ChannelID;references:ID" json:"channel_tests,omitempty"    yaml:"-"`
+	BalanceUpdatedAt        time.Time         `                                          json:"balance_updated_at"         yaml:"-"`
+	ModelMapping            map[string]string `gorm:"serializer:fastjson;type:text"      json:"model_mapping"              yaml:"model_mapping,omitempty"`
+	Key                     string            `gorm:"type:text;index:,length:191"        json:"key"                        yaml:"key,omitempty"`
+	Name                    string            `gorm:"size:64;index"                      json:"name"                       yaml:"name,omitempty"`
+	BaseURL                 string            `gorm:"size:128;index"                     json:"base_url"                   yaml:"base_url,omitempty"`
+	Models                  []string          `gorm:"serializer:fastjson;type:text"      json:"models"                     yaml:"models,omitempty"`
+	Balance                 float64           `                                          json:"balance"                    yaml:"balance,omitempty"`
+	ID                      int               `gorm:"primaryKey"                         json:"id"                         yaml:"id,omitempty"`
+	UsedAmount              float64           `gorm:"index"                              json:"used_amount"                yaml:"-"`
+	RequestCount            int               `gorm:"index"                              json:"request_count"              yaml:"-"`
+	RetryCount              int               `gorm:"index"                              json:"retry_count"                yaml:"-"`
+	Status                  int               `gorm:"default:1;index"                    json:"status"                     yaml:"status,omitempty"`
+	Type                    ChannelType       `gorm:"default:0;index"                    json:"type"                       yaml:"type,omitempty"`
+	Priority                int32             `                                          json:"priority"                   yaml:"priority,omitempty"`
+	EnabledAutoBalanceCheck bool              `                                          json:"enabled_auto_balance_check" yaml:"enabled_auto_balance_check,omitempty"`
+	BalanceThreshold        float64           `                                          json:"balance_threshold"          yaml:"balance_threshold,omitempty"`
+	Configs                 ChannelConfigs    `gorm:"serializer:fastjson;type:text"      json:"configs,omitempty"          yaml:"configs,omitempty"`
+	Sets                    []string          `gorm:"serializer:fastjson;type:text"      json:"sets,omitempty"             yaml:"sets,omitempty"`
 }
 
 func (c *Channel) GetSets() []string {
@@ -123,6 +84,21 @@ func (c *Channel) GetPriority() int32 {
 	return c.Priority
 }
 
+type ChannelConfigs map[string]any
+
+func (c ChannelConfigs) LoadConfig(config any) error {
+	if len(c) == 0 {
+		return nil
+	}
+
+	v, err := sonic.Marshal(c)
+	if err != nil {
+		return err
+	}
+
+	return sonic.Unmarshal(v, config)
+}
+
 func GetModelConfigWithModels(models []string) ([]string, []string, error) {
 	if len(models) == 0 || config.DisableModelConfig {
 		return models, nil, nil

+ 22 - 22
core/model/modelconfig.go

@@ -1,13 +1,13 @@
 package model
 
 import (
-	"encoding/json"
 	"errors"
 	"fmt"
 	"strings"
 	"time"
 
 	"github.com/bytedance/sonic"
+	"github.com/go-viper/mapstructure/v2"
 	"github.com/labring/aiproxy/core/common"
 	"github.com/labring/aiproxy/core/relay/mode"
 	"gorm.io/gorm"
@@ -19,31 +19,31 @@ const (
 )
 
 type TimeoutConfig struct {
-	RequestTimeout       int64 `json:"request_timeout,omitempty"`
-	StreamRequestTimeout int64 `json:"stream_request_timeout,omitempty"`
+	RequestTimeout       int64 `json:"request_timeout,omitempty"        yaml:"request_timeout,omitempty"`
+	StreamRequestTimeout int64 `json:"stream_request_timeout,omitempty" yaml:"stream_request_timeout,omitempty"`
 }
 
 type ModelConfig struct {
-	CreatedAt        time.Time                  `gorm:"index;autoCreateTime"          json:"created_at"`
-	UpdatedAt        time.Time                  `gorm:"index;autoUpdateTime"          json:"updated_at"`
-	Config           map[ModelConfigKey]any     `gorm:"serializer:fastjson;type:text" json:"config,omitempty"`
-	Plugin           map[string]json.RawMessage `gorm:"serializer:fastjson;type:text" json:"plugin,omitempty"`
-	Model            string                     `gorm:"size:64;primaryKey"            json:"model"`
-	Owner            ModelOwner                 `gorm:"type:varchar(32);index"        json:"owner"`
-	Type             mode.Mode                  `                                     json:"type"`
-	ExcludeFromTests bool                       `                                     json:"exclude_from_tests,omitempty"`
-	RPM              int64                      `                                     json:"rpm,omitempty"`
-	TPM              int64                      `                                     json:"tpm,omitempty"`
+	CreatedAt        time.Time                 `gorm:"index;autoCreateTime"          json:"created_at"                     yaml:"-"`
+	UpdatedAt        time.Time                 `gorm:"index;autoUpdateTime"          json:"updated_at"                     yaml:"-"`
+	Config           map[ModelConfigKey]any    `gorm:"serializer:fastjson;type:text" json:"config,omitempty"               yaml:"config,omitempty"`
+	Plugin           map[string]map[string]any `gorm:"serializer:fastjson;type:text" json:"plugin,omitempty"               yaml:"plugin,omitempty"`
+	Model            string                    `gorm:"size:64;primaryKey"            json:"model"                          yaml:"model,omitempty"`
+	Owner            ModelOwner                `gorm:"type:varchar(32);index"        json:"owner"                          yaml:"owner,omitempty"`
+	Type             mode.Mode                 `                                     json:"type"                           yaml:"type,omitempty"`
+	ExcludeFromTests bool                      `                                     json:"exclude_from_tests,omitempty"   yaml:"exclude_from_tests,omitempty"`
+	RPM              int64                     `                                     json:"rpm,omitempty"                  yaml:"rpm,omitempty"`
+	TPM              int64                     `                                     json:"tpm,omitempty"                  yaml:"tpm,omitempty"`
 	// map[size]map[quality]price_per_image
-	ImageQualityPrices map[string]map[string]float64 `gorm:"serializer:fastjson;type:text" json:"image_quality_prices,omitempty"`
+	ImageQualityPrices map[string]map[string]float64 `gorm:"serializer:fastjson;type:text" json:"image_quality_prices,omitempty" yaml:"image_quality_prices,omitempty"`
 	// map[size]price_per_image
-	ImagePrices     map[string]float64 `gorm:"serializer:fastjson;type:text" json:"image_prices,omitempty"`
-	Price           Price              `gorm:"embedded"                      json:"price,omitempty"`
-	RetryTimes      int64              `                                     json:"retry_times,omitempty"`
-	TimeoutConfig   TimeoutConfig      `gorm:"embedded"                      json:"timeout_config,omitempty"`
-	WarnErrorRate   float64            `                                     json:"warn_error_rate,omitempty"`
-	MaxErrorRate    float64            `                                     json:"max_error_rate,omitempty"`
-	ForceSaveDetail bool               `                                     json:"force_save_detail,omitempty"`
+	ImagePrices     map[string]float64 `gorm:"serializer:fastjson;type:text" json:"image_prices,omitempty"         yaml:"image_prices,omitempty"`
+	Price           Price              `gorm:"embedded"                      json:"price,omitempty"                yaml:"price,omitempty"`
+	RetryTimes      int64              `                                     json:"retry_times,omitempty"          yaml:"retry_times,omitempty"`
+	TimeoutConfig   TimeoutConfig      `gorm:"embedded"                      json:"timeout_config,omitempty"       yaml:"timeout_config,omitempty"`
+	WarnErrorRate   float64            `                                     json:"warn_error_rate,omitempty"      yaml:"warn_error_rate,omitempty"`
+	MaxErrorRate    float64            `                                     json:"max_error_rate,omitempty"       yaml:"max_error_rate,omitempty"`
+	ForceSaveDetail bool               `                                     json:"force_save_detail,omitempty"    yaml:"force_save_detail,omitempty"`
 }
 
 func (c *ModelConfig) BeforeSave(_ *gorm.DB) (err error) {
@@ -89,7 +89,7 @@ func (c *ModelConfig) LoadPluginConfig(pluginName string, config any) error {
 		return nil
 	}
 
-	return sonic.Unmarshal(pluginConfig, config)
+	return mapstructure.Decode(pluginConfig, config)
 }
 
 func (c *ModelConfig) LoadFromGroupModelConfig(groupModelConfig GroupModelConfig) ModelConfig {

+ 15 - 0
core/model/option.go

@@ -145,12 +145,27 @@ func storeOptionMap() error {
 }
 
 func loadOptionsFromDatabase(isInit bool) error {
+	// First, load options from YAML config if available
+	yamlOptions := make(map[string]string)
+
+	yamlConfig := LoadYAMLConfig()
+	if yamlConfig != nil && len(yamlConfig.Options) > 0 {
+		yamlOptions = yamlConfig.Options
+	}
+
+	// Then load options from database
+	// Skip options that are already set from YAML config
 	options, err := GetAllOption()
 	if err != nil {
 		return err
 	}
 
 	for _, option := range options {
+		// Skip if already loaded from YAML
+		if v, ok := yamlOptions[option.Key]; ok {
+			option.Value = v
+		}
+
 		err := updateOption(option.Key, option.Value, isInit)
 		if err != nil {
 			if !errors.Is(err, ErrUnknownOptionKey) {

+ 333 - 0
core/model/yaml_integration.go

@@ -0,0 +1,333 @@
+package model
+
+import (
+	"os"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/relay/mode"
+	log "github.com/sirupsen/logrus"
+	"gopkg.in/yaml.v3"
+)
+
+// ChannelItem wraps Channel for YAML configuration
+// Adds TypeName field for human-readable channel type specification
+type ChannelItem struct {
+	Channel  `       yaml:",inline"` // Embed Channel to inherit all fields
+	TypeName string                  `yaml:"type_name,omitempty"` // Alternative to Type (e.g., "openai", "claude")
+}
+
+// GetChannelType returns the channel type, converting from TypeName if Type is not set
+func (c *ChannelItem) GetChannelType() ChannelType {
+	if c.Type != 0 {
+		return c.Type
+	}
+	// Convert TypeName to Type
+	return ChannelType(ChannelTypeNameToType(c.TypeName))
+}
+
+// ModelConfigItem wraps ModelConfig for YAML configuration
+// Adds TypeName field for human-readable model type specification
+type ModelConfigItem struct {
+	ModelConfig `       yaml:",inline"` // Embed ModelConfig to inherit all fields
+	TypeName    string                  `yaml:"type_name,omitempty"` // Alternative to Type (e.g., "chat", "embedding")
+}
+
+// GetModelType returns the model type, converting from TypeName if Type is not set
+func (m *ModelConfigItem) GetModelType() mode.Mode {
+	if m.Type != 0 {
+		return m.Type
+	}
+	// Convert TypeName to Type
+	return ModelTypeNameToType(m.TypeName)
+}
+
+// ChannelTypeNameToType converts a channel type name to its numeric type
+func ChannelTypeNameToType(typeName string) int {
+	typeName = strings.ToLower(strings.TrimSpace(typeName))
+
+	typeMap := map[string]int{
+		"openai":                                1,
+		"azure":                                 3,
+		"azure2":                                4,
+		"google gemini (openai)":                12,
+		"gemini-openai":                         12,
+		"baidu v2":                              13,
+		"baiduv2":                               13,
+		"anthropic":                             14,
+		"claude":                                14,
+		"baidu":                                 15,
+		"zhipu":                                 16,
+		"ali":                                   17,
+		"aliyun":                                17,
+		"xunfei":                                18,
+		"ai360":                                 19,
+		"360":                                   19,
+		"openrouter":                            20,
+		"tencent":                               23,
+		"google gemini":                         24,
+		"gemini":                                24,
+		"moonshot":                              25,
+		"baichuan":                              26,
+		"minimax":                               27,
+		"mistral":                               28,
+		"groq":                                  29,
+		"ollama":                                30,
+		"lingyiwanwu":                           31,
+		"stepfun":                               32,
+		"aws":                                   33,
+		"coze":                                  34,
+		"cohere":                                35,
+		"deepseek":                              36,
+		"cloudflare":                            37,
+		"doubao":                                40,
+		"novita":                                41,
+		"vertexai":                              42,
+		"vertex":                                42,
+		"siliconflow":                           43,
+		"doubao audio":                          44,
+		"doubaoaudio":                           44,
+		"xai":                                   45,
+		"doc2x":                                 46,
+		"jina":                                  47,
+		"huggingface text-embeddings-inference": 48,
+		"text-embeddings-inference":             48,
+		"tei":                                   48,
+		"qianfan":                               49,
+		"sangfor aicp":                          50,
+		"aicp":                                  50,
+		"streamlake":                            51,
+		"zhipu coding":                          52,
+		"zhipucoding":                           52,
+	}
+
+	if typ, ok := typeMap[typeName]; ok {
+		return typ
+	}
+
+	return 0
+}
+
+// ModelTypeNameToType converts a model type name to its numeric type
+func ModelTypeNameToType(typeName string) mode.Mode {
+	typeName = strings.ToLower(strings.TrimSpace(typeName))
+
+	typeMap := map[string]mode.Mode{
+		"chat":                    mode.ChatCompletions,
+		"chatcompletion":          mode.ChatCompletions,
+		"chatcompletions":         mode.ChatCompletions,
+		"completion":              mode.Completions,
+		"completions":             mode.Completions,
+		"embedding":               mode.Embeddings,
+		"embeddings":              mode.Embeddings,
+		"moderation":              mode.Moderations,
+		"moderations":             mode.Moderations,
+		"image":                   mode.ImagesGenerations,
+		"imagegeneration":         mode.ImagesGenerations,
+		"imagegenerations":        mode.ImagesGenerations,
+		"imageedit":               mode.ImagesEdits,
+		"imageedits":              mode.ImagesEdits,
+		"audio":                   mode.AudioSpeech,
+		"audiospeech":             mode.AudioSpeech,
+		"speech":                  mode.AudioSpeech,
+		"audiotranscription":      mode.AudioTranscription,
+		"transcription":           mode.AudioTranscription,
+		"audiotranslation":        mode.AudioTranslation,
+		"translation":             mode.AudioTranslation,
+		"rerank":                  mode.Rerank,
+		"parsepdf":                mode.ParsePdf,
+		"pdf":                     mode.ParsePdf,
+		"anthropic":               mode.Anthropic,
+		"videogeneration":         mode.VideoGenerationsJobs,
+		"videogenerationsjobs":    mode.VideoGenerationsJobs,
+		"videogenerationsgetjobs": mode.VideoGenerationsGetJobs,
+		"videogenerationscontent": mode.VideoGenerationsContent,
+		"responses":               mode.Responses,
+		"responsesget":            mode.ResponsesGet,
+		"responsesdelete":         mode.ResponsesDelete,
+		"responsescancel":         mode.ResponsesCancel,
+		"responsesinputitems":     mode.ResponsesInputItems,
+	}
+
+	if typ, ok := typeMap[typeName]; ok {
+		return typ
+	}
+
+	return mode.Unknown
+}
+
+// YAMLConfig represents the complete configuration with proper types
+type YAMLConfig struct {
+	Channels     []ChannelItem     `yaml:"channels,omitempty"`
+	ModelConfigs []ModelConfigItem `yaml:"modelconfigs,omitempty"`
+	Options      map[string]string `yaml:"options,omitempty"`
+}
+
+var (
+	yamlConfigCache      *YAMLConfig
+	yamlConfigCacheTime  time.Time
+	yamlConfigCacheMutex sync.RWMutex
+	yamlConfigCacheTTL   = 60 * time.Second
+)
+
+// LoadYAMLConfig loads and parses YAML configuration with proper types
+// Uses a 60-second cache with double-check locking for performance
+func LoadYAMLConfig() *YAMLConfig {
+	yamlConfigCacheMutex.RLock()
+
+	if yamlConfigCache != nil && time.Since(yamlConfigCacheTime) < yamlConfigCacheTTL {
+		cache := yamlConfigCache
+
+		yamlConfigCacheMutex.RUnlock()
+		return cache
+	}
+
+	yamlConfigCacheMutex.RUnlock()
+
+	// Acquire write lock to update cache
+	yamlConfigCacheMutex.Lock()
+	defer yamlConfigCacheMutex.Unlock()
+
+	// Double check: another goroutine might have updated the cache
+	if yamlConfigCache != nil && time.Since(yamlConfigCacheTime) < yamlConfigCacheTTL {
+		return yamlConfigCache
+	}
+
+	// Load raw YAML data from file
+	data, err := config.LoadYAMLConfigData()
+	if err != nil {
+		if os.IsNotExist(err) {
+			yamlConfigCache = nil
+			yamlConfigCacheTime = time.Now()
+			return nil
+		}
+
+		log.Errorf("load config: %v", err)
+
+		yamlConfigCache = nil
+		yamlConfigCacheTime = time.Now()
+
+		return nil
+	}
+
+	// Parse YAML directly into our types
+	var yamlConfig YAMLConfig
+	//nolint:musttag
+	if err := yaml.Unmarshal(data, &yamlConfig); err != nil {
+		log.Errorf("unmarshal config: %v", err)
+
+		yamlConfigCache = nil
+		yamlConfigCacheTime = time.Now()
+
+		return nil
+	}
+
+	// Update cache
+	yamlConfigCache = &yamlConfig
+	yamlConfigCacheTime = time.Now()
+
+	return yamlConfigCache
+}
+
+// applyYAMLConfigToModelConfigCache applies YAML model configs to the model config cache
+// Creates a wrapper cache that checks YAML first, then falls back to database cache
+func applyYAMLConfigToModelConfigCache(
+	cache ModelConfigCache,
+) ModelConfigCache {
+	yamlConfig := LoadYAMLConfig()
+	if yamlConfig == nil || len(yamlConfig.ModelConfigs) == 0 {
+		// No YAML model configs, use existing cache from database
+		return cache
+	}
+
+	// Build YAML model config map
+	yamlModelConfigMap := make(map[string]ModelConfig)
+	for i := range yamlConfig.ModelConfigs {
+		modelConfigItem := &yamlConfig.ModelConfigs[i]
+
+		// Convert ModelConfigItem to ModelConfig
+		modelConfig := modelConfigItem.ModelConfig
+
+		// Convert TypeName to Type if Type is not set
+		if modelConfigItem.TypeName != "" && modelConfig.Type == 0 {
+			modelConfig.Type = modelConfigItem.GetModelType()
+		}
+
+		if modelConfig.Model != "" {
+			yamlModelConfigMap[modelConfig.Model] = modelConfig
+		}
+	}
+
+	log.Infof("loaded %d model configs from config", len(yamlModelConfigMap))
+
+	// Create wrapper cache: YAML configs override database configs
+	wrappedCache := &yamlModelConfigCache{
+		yamlConfigs: yamlModelConfigMap,
+		dbCache:     cache,
+	}
+
+	return wrappedCache
+}
+
+// yamlModelConfigCache wraps database cache with YAML overrides
+// When looking up a model config:
+// 1. First check YAML configs (high priority)
+// 2. If not found, fall back to database cache (low priority)
+var _ ModelConfigCache = (*yamlModelConfigCache)(nil)
+
+type yamlModelConfigCache struct {
+	yamlConfigs map[string]ModelConfig
+	dbCache     ModelConfigCache
+}
+
+func (y *yamlModelConfigCache) GetModelConfig(model string) (ModelConfig, bool) {
+	// First check YAML configs
+	if config, ok := y.yamlConfigs[model]; ok {
+		return config, true
+	}
+
+	// Fall back to database cache
+	return y.dbCache.GetModelConfig(model)
+}
+
+// NewConfigChannels merges YAML channels with database channels
+// YAML channels are assigned negative IDs to distinguish them from database channels
+// Note: YAML channels are NOT persisted to the database
+func NewConfigChannels(yamlConfig *YAMLConfig, status int) []*Channel {
+	if yamlConfig == nil || len(yamlConfig.Channels) == 0 {
+		return nil
+	}
+
+	newChannels := make([]*Channel, 0, len(yamlConfig.Channels))
+
+	// Start negative IDs from -1000 to avoid conflicts
+	nextNegativeID := -1
+
+	// Add all YAML channels with negative IDs (they don't override database channels)
+	for _, yamlChannelItem := range yamlConfig.Channels {
+		// Convert ChannelItem to Channel
+		channel := &yamlChannelItem.Channel
+
+		if status != 0 && channel.Status != status {
+			continue
+		}
+
+		// Convert TypeName to Type if Type is not set
+		if yamlChannelItem.TypeName != "" && channel.Type == 0 {
+			channel.Type = yamlChannelItem.GetChannelType()
+		}
+
+		// Assign negative ID to distinguish from database channels
+		channel.ID = nextNegativeID
+		nextNegativeID--
+
+		initializeChannelModels(channel)
+		initializeChannelModelMapping(channel)
+
+		newChannels = append(newChannels, channel)
+	}
+
+	return newChannels
+}

+ 0 - 7
core/relay/adaptor/ali/config.go

@@ -1,7 +0,0 @@
-package ali
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-func (a *Adaptor) ConfigTemplates() adaptor.ConfigTemplates {
-	return nil
-}

+ 0 - 7
core/relay/adaptor/anthropic/config.go

@@ -1,7 +0,0 @@
-package anthropic
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-func (a *Adaptor) ConfigTemplates() adaptor.ConfigTemplates {
-	return nil
-}

+ 0 - 7
core/relay/adaptor/doc2x/config.go

@@ -1,7 +0,0 @@
-package doc2x
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-func (a *Adaptor) ConfigTemplates() adaptor.ConfigTemplates {
-	return nil
-}

+ 0 - 1
core/relay/adaptor/gemini/adaptor.go

@@ -137,6 +137,5 @@ func (a *Adaptor) Metadata() adaptor.Metadata {
 			"Chat、Embeddings、Image generation Support",
 		},
 		Models: ModelList,
-		Config: ConfigTemplates,
 	}
 }

+ 1 - 1
core/relay/adaptor/gemini/claude.go

@@ -22,7 +22,7 @@ import (
 func ConvertClaudeRequest(meta *meta.Meta, req *http.Request) (adaptor.ConvertResult, error) {
 	adaptorConfig := Config{}
 
-	err := meta.ChannelConfig.SpecConfig(&adaptorConfig)
+	err := meta.ChannelConfigs.LoadConfig(&adaptorConfig)
 	if err != nil {
 		return adaptor.ConvertResult{}, err
 	}

+ 0 - 38
core/relay/adaptor/gemini/config.go

@@ -1,43 +1,5 @@
 package gemini
 
-import (
-	"fmt"
-
-	"github.com/labring/aiproxy/core/relay/adaptor"
-)
-
 type Config struct {
 	Safety string `json:"safety"`
 }
-
-var ConfigTemplates = adaptor.ConfigTemplates{
-	"safety": {
-		Name:        "Safety",
-		Description: "Safety settings: https://ai.google.dev/gemini-api/docs/safety-settings, default is BLOCK_NONE",
-		Example:     "BLOCK_NONE",
-		Type:        adaptor.ConfigTypeString,
-		Validator: func(a any) error {
-			s, ok := a.(string)
-			if !ok {
-				return fmt.Errorf("invalid safety settings type: %v, must be a string", a)
-			}
-			switch s {
-			case "BLOCK_NONE",
-				"BLOCK_ONLY_HIGH",
-				"BLOCK_MEDIUM_AND_ABOVE",
-				"BLOCK_LOW_AND_ABOVE",
-				"HARM_BLOCK_THRESHOLD_UNSPECIFIED":
-				return nil
-			default:
-				return fmt.Errorf(
-					"invalid safety settings: %s, must be one of: BLOCK_NONE, BLOCK_ONLY_HIGH, BLOCK_MEDIUM_AND_ABOVE, BLOCK_LOW_AND_ABOVE, HARM_BLOCK_THRESHOLD_UNSPECIFIED",
-					s,
-				)
-			}
-		},
-	},
-}
-
-func (a *Adaptor) ConfigTemplates() adaptor.ConfigTemplates {
-	return ConfigTemplates
-}

+ 1 - 1
core/relay/adaptor/gemini/main.go

@@ -413,7 +413,7 @@ func processImageTasks(ctx context.Context, imageTasks []*Part) error {
 func ConvertRequest(meta *meta.Meta, req *http.Request) (adaptor.ConvertResult, error) {
 	adaptorConfig := Config{}
 
-	err := meta.ChannelConfig.SpecConfig(&adaptorConfig)
+	err := meta.ChannelConfigs.LoadConfig(&adaptorConfig)
 	if err != nil {
 		return adaptor.ConvertResult{}, err
 	}

+ 12 - 20
core/relay/adaptor/interface.go

@@ -28,10 +28,10 @@ type Store interface {
 }
 
 type Metadata struct {
-	Config   ConfigTemplates
-	KeyHelp  string
-	Features []string
-	Models   []model.ModelConfig
+	ConfigTemplates ConfigTemplates
+	KeyHelp         string
+	Features        []string
+	Models          []model.ModelConfig
 }
 
 type RequestURL struct {
@@ -101,22 +101,14 @@ type KeyValidator interface {
 	ValidateKey(key string) error
 }
 
-type ConfigType string
-
-const (
-	ConfigTypeString ConfigType = "string"
-	ConfigTypeNumber ConfigType = "number"
-	ConfigTypeBool   ConfigType = "bool"
-	ConfigTypeObject ConfigType = "object"
-)
-
 type ConfigTemplate struct {
-	Name        string          `json:"name"`
-	Description string          `json:"description"`
-	Example     any             `json:"example,omitempty"`
-	Validator   func(any) error `json:"-"`
-	Required    bool            `json:"required"`
-	Type        ConfigType      `json:"type"`
+	Name        string `json:"name"`
+	Description string `json:"description"`
+	Example     string `json:"example,omitempty"`
+	Required    bool   `json:"required"`
 }
 
-type ConfigTemplates = map[string]ConfigTemplate
+type ConfigTemplates struct {
+	Configs   map[string]ConfigTemplate
+	Validator func(model.ChannelConfigs) error `json:"-"`
+}

+ 0 - 7
core/relay/adaptor/openai/config.go

@@ -1,7 +0,0 @@
-package openai
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-func (a *Adaptor) ConfigTemplates() adaptor.ConfigTemplates {
-	return nil
-}

+ 0 - 62
core/relay/adaptor/utils.go

@@ -1,9 +1,7 @@
 package adaptor
 
 import (
-	"errors"
 	"fmt"
-	"reflect"
 
 	"github.com/bytedance/sonic"
 )
@@ -31,63 +29,3 @@ func NewError[T any](statusCode int, err T) Error {
 		statusCode: statusCode,
 	}
 }
-
-func ValidateConfigTemplate(template ConfigTemplate) error {
-	if template.Name == "" {
-		return errors.New("config template is invalid: name is empty")
-	}
-
-	if template.Type == "" {
-		return fmt.Errorf("config template %s is invalid: type is empty", template.Name)
-	}
-
-	if template.Example != nil {
-		if err := ValidateConfigTemplateValue(template, template.Example); err != nil {
-			return fmt.Errorf("config template %s is invalid: %w", template.Name, err)
-		}
-	}
-
-	return nil
-}
-
-func ValidateConfigTemplateValue(template ConfigTemplate, value any) error {
-	if template.Validator == nil {
-		return nil
-	}
-
-	switch template.Type {
-	case ConfigTypeString:
-		_, ok := value.(string)
-		if !ok {
-			return fmt.Errorf("config template %s is invalid: value is not a string", template.Name)
-		}
-	case ConfigTypeNumber:
-		switch value.(type) {
-		case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
-			return nil
-		default:
-			return fmt.Errorf("config template %s is invalid: value is not a number", template.Name)
-		}
-	case ConfigTypeBool:
-		_, ok := value.(bool)
-		if !ok {
-			return fmt.Errorf("config template %s is invalid: value is not a bool", template.Name)
-		}
-	case ConfigTypeObject:
-		if reflect.TypeOf(value).Kind() != reflect.Map &&
-			reflect.TypeOf(value).Kind() != reflect.Struct {
-			return fmt.Errorf("config template %s is invalid: value is not a object", template.Name)
-		}
-	}
-
-	if err := template.Validator(value); err != nil {
-		return fmt.Errorf(
-			"config template %s(%s) is invalid: %w",
-			template.Name,
-			template.Name,
-			err,
-		)
-	}
-
-	return nil
-}

+ 0 - 10
core/relay/adaptor/vertexai/config.go

@@ -1,10 +0,0 @@
-package vertexai
-
-import (
-	"github.com/labring/aiproxy/core/relay/adaptor"
-	"github.com/labring/aiproxy/core/relay/adaptor/gemini"
-)
-
-func (a *Adaptor) ConfigTemplates() adaptor.ConfigTemplates {
-	return gemini.ConfigTemplates
-}

+ 11 - 15
core/relay/adaptors/register.go

@@ -95,11 +95,11 @@ func GetAdaptor(channelType model.ChannelType) (adaptor.Adaptor, bool) {
 }
 
 type AdaptorMeta struct {
-	Name           string                  `json:"name"`
-	KeyHelp        string                  `json:"keyHelp"`
-	DefaultBaseURL string                  `json:"defaultBaseUrl"`
-	Fetures        []string                `json:"fetures,omitempty"`
-	Config         adaptor.ConfigTemplates `json:"config,omitempty"`
+	Name            string                            `json:"name"`
+	KeyHelp         string                            `json:"keyHelp"`
+	DefaultBaseURL  string                            `json:"defaultBaseUrl"`
+	Fetures         []string                          `json:"fetures,omitempty"`
+	ConfigTemplates map[string]adaptor.ConfigTemplate `json:"configs,omitempty"`
 }
 
 var ChannelMetas = map[model.ChannelType]AdaptorMeta{}
@@ -109,20 +109,16 @@ func init() {
 		adaptorMeta := a.Metadata()
 
 		meta := AdaptorMeta{
-			Name:           i.String(),
-			KeyHelp:        adaptorMeta.KeyHelp,
-			DefaultBaseURL: a.DefaultBaseURL(),
-			Fetures:        adaptorMeta.Features,
-			Config:         adaptorMeta.Config,
+			Name:            i.String(),
+			KeyHelp:         adaptorMeta.KeyHelp,
+			DefaultBaseURL:  a.DefaultBaseURL(),
+			Fetures:         adaptorMeta.Features,
+			ConfigTemplates: adaptorMeta.ConfigTemplates.Configs,
 		}
-		for key, template := range meta.Config {
+		for key, template := range adaptorMeta.ConfigTemplates.Configs {
 			if template.Name == "" {
 				log.Fatalf("config template %s is invalid: name is empty", key)
 			}
-
-			if err := adaptor.ValidateConfigTemplate(template); err != nil {
-				log.Fatalf("config template %s(%s) is invalid: %v", key, template.Name, err)
-			}
 		}
 
 		ChannelMetas[i] = meta

+ 8 - 10
core/relay/meta/meta.go

@@ -18,12 +18,12 @@ type ChannelMeta struct {
 }
 
 type Meta struct {
-	values        map[string]any
-	Channel       ChannelMeta
-	ChannelConfig model.ChannelConfig
-	Group         model.GroupCache
-	Token         model.TokenCache
-	ModelConfig   model.ModelConfig
+	values         map[string]any
+	Channel        ChannelMeta
+	ChannelConfigs model.ChannelConfigs
+	Group          model.GroupCache
+	Token          model.TokenCache
+	ModelConfig    model.ModelConfig
 
 	Endpoint    string
 	RequestAt   time.Time
@@ -142,16 +142,14 @@ func (m *Meta) SetChannel(channel *model.Channel) {
 	m.Channel.Type = channel.Type
 
 	m.Channel.ModelMapping = channel.ModelMapping
-	if channel.Config != nil {
-		m.ChannelConfig = *channel.Config
-	}
+	m.ChannelConfigs = channel.Configs
 
 	m.ActualModel, _ = GetMappedModelName(m.OriginModel, channel.ModelMapping)
 }
 
 func (m *Meta) CopyChannelFromMeta(meta *Meta) {
 	m.Channel = meta.Channel
-	m.ChannelConfig = meta.ChannelConfig
+	m.ChannelConfigs = meta.ChannelConfigs
 	m.ActualModel, _ = GetMappedModelName(meta.OriginModel, meta.Channel.ModelMapping)
 }
 

+ 1 - 0
core/scripts/swag.sh

@@ -1,4 +1,5 @@
 #!/bin/bash
 
+go install github.com/swaggo/swag/cmd/swag@latest
 swag init --parseDependency --parseInternal
 swag fmt

+ 72 - 0
core/startup.go

@@ -4,9 +4,11 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"math/rand/v2"
 	"net/http"
 	"os"
 	"path/filepath"
+	"strings"
 	"sync"
 	"time"
 
@@ -15,6 +17,7 @@ import (
 	"github.com/labring/aiproxy/core/common"
 	"github.com/labring/aiproxy/core/common/balance"
 	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/common/conv"
 	"github.com/labring/aiproxy/core/common/notify"
 	"github.com/labring/aiproxy/core/common/pprof"
 	"github.com/labring/aiproxy/core/middleware"
@@ -121,6 +124,7 @@ func loadEnv() {
 	envfiles := []string{
 		".env",
 		".env.local",
+		".env.aiproxy.local",
 	}
 	for _, envfile := range envfiles {
 		absPath, err := filepath.Abs(envfile)
@@ -163,3 +167,71 @@ func listenAndServe(srv *http.Server) {
 		log.Fatal("failed to start HTTP server: " + err.Error())
 	}
 }
+
+const (
+	keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+)
+
+func generateAdminKey() string {
+	key := make([]byte, 48)
+	for i := range key {
+		key[i] = keyChars[rand.IntN(len(keyChars))]
+	}
+
+	return conv.BytesToString(key)
+}
+
+func writeToEnvFile(envFile, key, value string) error {
+	var lines []string
+	if content, err := os.ReadFile(envFile); err == nil {
+		lines = strings.Split(string(content), "\n")
+	}
+
+	keyPrefix := key + "="
+
+	found := false
+	for i, line := range lines {
+		if strings.HasPrefix(line, keyPrefix) {
+			lines[i] = key + "=" + value
+			found = true
+			break
+		}
+	}
+
+	if !found {
+		lines = append(lines, key+"="+value)
+	}
+
+	content := strings.Join(lines, "\n")
+	if !strings.HasSuffix(content, "\n") && content != "" {
+		content += "\n"
+	}
+
+	return os.WriteFile(envFile, []byte(content), 0o644)
+}
+
+func ensureAdminKey() error {
+	if config.AdminKey != "" {
+		log.Info("AdminKey is already set")
+		return nil
+	}
+
+	log.Info("AdminKey is not set, generating new AdminKey...")
+
+	config.AdminKey = generateAdminKey()
+
+	envFile := ".env.aiproxy.local"
+
+	absEnvFile, err := filepath.Abs(envFile)
+	if err == nil {
+		envFile = absEnvFile
+	}
+
+	if err := writeToEnvFile(envFile, "ADMIN_KEY", config.AdminKey); err != nil {
+		return fmt.Errorf("failed to write AdminKey to %s: %w", envFile, err)
+	}
+
+	log.Info("Generated new AdminKey and saved to " + envFile)
+
+	return nil
+}

+ 1 - 0
mcp-servers/go.mod

@@ -52,6 +52,7 @@ require (
 	github.com/go-playground/validator/v10 v10.28.0 // indirect
 	github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c // indirect
 	github.com/go-sql-driver/mysql v1.9.3 // indirect
+	github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
 	github.com/goccy/go-yaml v1.18.0 // indirect
 	github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect

+ 1 - 0
mcp-servers/go.sum

@@ -92,6 +92,7 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1
 github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
 github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
 github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
+github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=