diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml
index c87fcfce..3e3ddc53 100644
--- a/.github/workflows/linux-release.yml
+++ b/.github/workflows/linux-release.yml
@@ -38,21 +38,21 @@ jobs:
- name: Build Backend (amd64)
run: |
go mod download
- go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o one-api
+ go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api
- name: Build Backend (arm64)
run: |
sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu
- CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o one-api-arm64
+ CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api-arm64
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
- one-api
- one-api-arm64
+ new-api
+ new-api-arm64
draft: true
generate_release_notes: true
env:
diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml
index 1bc786ac..8eaf2d67 100644
--- a/.github/workflows/macos-release.yml
+++ b/.github/workflows/macos-release.yml
@@ -39,12 +39,12 @@ jobs:
- name: Build Backend
run: |
go mod download
- go build -ldflags "-X 'one-api/common.Version=$(git describe --tags)'" -o one-api-macos
+ go build -ldflags "-X 'one-api/common.Version=$(git describe --tags)'" -o new-api-macos
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
- files: one-api-macos
+ files: new-api-macos
draft: true
generate_release_notes: true
env:
diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml
index de3d83d5..30e864f3 100644
--- a/.github/workflows/windows-release.yml
+++ b/.github/workflows/windows-release.yml
@@ -41,12 +41,12 @@ jobs:
- name: Build Backend
run: |
go mod download
- go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)'" -o one-api.exe
+ go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)'" -o new-api.exe
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
- files: one-api.exe
+ files: new-api.exe
draft: true
generate_release_notes: true
env:
diff --git a/common/sys_log.go b/common/sys_log.go
index 478015f0..b29adc3e 100644
--- a/common/sys_log.go
+++ b/common/sys_log.go
@@ -2,9 +2,10 @@ package common
import (
"fmt"
- "github.com/gin-gonic/gin"
"os"
"time"
+
+ "github.com/gin-gonic/gin"
)
func SysLog(s string) {
@@ -22,3 +23,33 @@ func FatalLog(v ...any) {
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
os.Exit(1)
}
+
+func LogStartupSuccess(startTime time.Time, port string) {
+
+ duration := time.Since(startTime)
+ durationMs := duration.Milliseconds()
+
+ // Get network IPs
+ networkIps := GetNetworkIps()
+
+ // Print blank line for spacing
+ fmt.Fprintf(gin.DefaultWriter, "\n")
+
+ // Print the main success message
+ fmt.Fprintf(gin.DefaultWriter, " \033[32m%s %s\033[0m ready in %d ms\n", SystemName, Version, durationMs)
+ fmt.Fprintf(gin.DefaultWriter, "\n")
+
+ // Skip fancy startup message in container environments
+ if !IsRunningInContainer() {
+ // Print local URL
+ fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mLocal:\033[0m http://localhost:%s/\n", port)
+ }
+
+ // Print network URLs
+ for _, ip := range networkIps {
+ fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mNetwork:\033[0m http://%s:%s/\n", ip, port)
+ }
+
+ // Print blank line for spacing
+ fmt.Fprintf(gin.DefaultWriter, "\n")
+}
diff --git a/common/utils.go b/common/utils.go
index 883abfd1..21f72ec6 100644
--- a/common/utils.go
+++ b/common/utils.go
@@ -68,6 +68,78 @@ func GetIp() (ip string) {
return
}
+func GetNetworkIps() []string {
+ var networkIps []string
+ ips, err := net.InterfaceAddrs()
+ if err != nil {
+ log.Println(err)
+ return networkIps
+ }
+
+ for _, a := range ips {
+ if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
+ if ipNet.IP.To4() != nil {
+ ip := ipNet.IP.String()
+ // Include common private network ranges
+ if strings.HasPrefix(ip, "10.") ||
+ strings.HasPrefix(ip, "172.") ||
+ strings.HasPrefix(ip, "192.168.") {
+ networkIps = append(networkIps, ip)
+ }
+ }
+ }
+ }
+ return networkIps
+}
+
+// IsRunningInContainer detects if the application is running inside a container
+func IsRunningInContainer() bool {
+ // Method 1: Check for .dockerenv file (Docker containers)
+ if _, err := os.Stat("/.dockerenv"); err == nil {
+ return true
+ }
+
+ // Method 2: Check cgroup for container indicators
+ if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
+ content := string(data)
+ if strings.Contains(content, "docker") ||
+ strings.Contains(content, "containerd") ||
+ strings.Contains(content, "kubepods") ||
+ strings.Contains(content, "/lxc/") {
+ return true
+ }
+ }
+
+ // Method 3: Check environment variables commonly set by container runtimes
+ containerEnvVars := []string{
+ "KUBERNETES_SERVICE_HOST",
+ "DOCKER_CONTAINER",
+ "container",
+ }
+
+ for _, envVar := range containerEnvVars {
+ if os.Getenv(envVar) != "" {
+ return true
+ }
+ }
+
+ // Method 4: Check if init process is not the traditional init
+ if data, err := os.ReadFile("/proc/1/comm"); err == nil {
+ comm := strings.TrimSpace(string(data))
+ // In containers, process 1 is often not "init" or "systemd"
+ if comm != "init" && comm != "systemd" {
+ // Additional check: if it's a common container entrypoint
+ if strings.Contains(comm, "docker") ||
+ strings.Contains(comm, "containerd") ||
+ strings.Contains(comm, "runc") {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
var sizeKB = 1024
var sizeMB = sizeKB * 1024
var sizeGB = sizeMB * 1024
diff --git a/controller/channel.go b/controller/channel.go
index 17154ab0..480d5b4f 100644
--- a/controller/channel.go
+++ b/controller/channel.go
@@ -188,6 +188,8 @@ func FetchUpstreamModels(c *gin.Context) {
url = fmt.Sprintf("%s/v1beta/openai/models", baseURL) // Remove key in url since we need to use AuthHeader
case constant.ChannelTypeAli:
url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL)
+ case constant.ChannelTypeZhipu_v4:
+ url = fmt.Sprintf("%s/api/paas/v4/models", baseURL)
default:
url = fmt.Sprintf("%s/v1/models", baseURL)
}
@@ -1101,8 +1103,8 @@ func CopyChannel(c *gin.Context) {
// MultiKeyManageRequest represents the request for multi-key management operations
type MultiKeyManageRequest struct {
ChannelId int `json:"channel_id"`
- Action string `json:"action"` // "disable_key", "enable_key", "delete_disabled_keys", "get_key_status"
- KeyIndex *int `json:"key_index,omitempty"` // for disable_key and enable_key actions
+ Action string `json:"action"` // "disable_key", "enable_key", "delete_key", "delete_disabled_keys", "get_key_status"
+ KeyIndex *int `json:"key_index,omitempty"` // for disable_key, enable_key, and delete_key actions
Page int `json:"page,omitempty"` // for get_key_status pagination
PageSize int `json:"page_size,omitempty"` // for get_key_status pagination
Status *int `json:"status,omitempty"` // for get_key_status filtering: 1=enabled, 2=manual_disabled, 3=auto_disabled, nil=all
@@ -1430,6 +1432,86 @@ func ManageMultiKeys(c *gin.Context) {
})
return
+ case "delete_key":
+ if request.KeyIndex == nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": "未指定要删除的密钥索引",
+ })
+ return
+ }
+
+ keyIndex := *request.KeyIndex
+ if keyIndex < 0 || keyIndex >= channel.ChannelInfo.MultiKeySize {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": "密钥索引超出范围",
+ })
+ return
+ }
+
+ keys := channel.GetKeys()
+ var remainingKeys []string
+ var newStatusList = make(map[int]int)
+ var newDisabledTime = make(map[int]int64)
+ var newDisabledReason = make(map[int]string)
+
+ newIndex := 0
+ for i, key := range keys {
+ // 跳过要删除的密钥
+ if i == keyIndex {
+ continue
+ }
+
+ remainingKeys = append(remainingKeys, key)
+
+ // 保留其他密钥的状态信息,重新索引
+ if channel.ChannelInfo.MultiKeyStatusList != nil {
+ if status, exists := channel.ChannelInfo.MultiKeyStatusList[i]; exists && status != 1 {
+ newStatusList[newIndex] = status
+ }
+ }
+ if channel.ChannelInfo.MultiKeyDisabledTime != nil {
+ if t, exists := channel.ChannelInfo.MultiKeyDisabledTime[i]; exists {
+ newDisabledTime[newIndex] = t
+ }
+ }
+ if channel.ChannelInfo.MultiKeyDisabledReason != nil {
+ if r, exists := channel.ChannelInfo.MultiKeyDisabledReason[i]; exists {
+ newDisabledReason[newIndex] = r
+ }
+ }
+ newIndex++
+ }
+
+ if len(remainingKeys) == 0 {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": "不能删除最后一个密钥",
+ })
+ return
+ }
+
+ // Update channel with remaining keys
+ channel.Key = strings.Join(remainingKeys, "\n")
+ channel.ChannelInfo.MultiKeySize = len(remainingKeys)
+ channel.ChannelInfo.MultiKeyStatusList = newStatusList
+ channel.ChannelInfo.MultiKeyDisabledTime = newDisabledTime
+ channel.ChannelInfo.MultiKeyDisabledReason = newDisabledReason
+
+ err = channel.Update()
+ if err != nil {
+ common.ApiError(c, err)
+ return
+ }
+
+ model.InitChannelCache()
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "密钥已删除",
+ })
+ return
+
case "delete_disabled_keys":
keys := channel.GetKeys()
var remainingKeys []string
diff --git a/dto/gemini.go b/dto/gemini.go
index 5df67ba0..bc05c6aa 100644
--- a/dto/gemini.go
+++ b/dto/gemini.go
@@ -14,7 +14,30 @@ type GeminiChatRequest struct {
SafetySettings []GeminiChatSafetySettings `json:"safetySettings,omitempty"`
GenerationConfig GeminiChatGenerationConfig `json:"generationConfig,omitempty"`
Tools json.RawMessage `json:"tools,omitempty"`
+ ToolConfig *ToolConfig `json:"toolConfig,omitempty"`
SystemInstructions *GeminiChatContent `json:"systemInstruction,omitempty"`
+ CachedContent string `json:"cachedContent,omitempty"`
+}
+
+type ToolConfig struct {
+ FunctionCallingConfig *FunctionCallingConfig `json:"functionCallingConfig,omitempty"`
+ RetrievalConfig *RetrievalConfig `json:"retrievalConfig,omitempty"`
+}
+
+type FunctionCallingConfig struct {
+ Mode FunctionCallingConfigMode `json:"mode,omitempty"`
+ AllowedFunctionNames []string `json:"allowedFunctionNames,omitempty"`
+}
+type FunctionCallingConfigMode string
+
+type RetrievalConfig struct {
+ LatLng *LatLng `json:"latLng,omitempty"`
+ LanguageCode string `json:"languageCode,omitempty"`
+}
+
+type LatLng struct {
+ Latitude *float64 `json:"latitude,omitempty"`
+ Longitude *float64 `json:"longitude,omitempty"`
}
func (r *GeminiChatRequest) GetTokenCountMeta() *types.TokenCountMeta {
@@ -239,12 +262,20 @@ type GeminiChatGenerationConfig struct {
StopSequences []string `json:"stopSequences,omitempty"`
ResponseMimeType string `json:"responseMimeType,omitempty"`
ResponseSchema any `json:"responseSchema,omitempty"`
+ ResponseJsonSchema json.RawMessage `json:"responseJsonSchema,omitempty"`
+ PresencePenalty *float32 `json:"presencePenalty,omitempty"`
+ FrequencyPenalty *float32 `json:"frequencyPenalty,omitempty"`
+ ResponseLogprobs bool `json:"responseLogprobs,omitempty"`
+ Logprobs *int32 `json:"logprobs,omitempty"`
+ MediaResolution MediaResolution `json:"mediaResolution,omitempty"`
Seed int64 `json:"seed,omitempty"`
ResponseModalities []string `json:"responseModalities,omitempty"`
ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"`
SpeechConfig json.RawMessage `json:"speechConfig,omitempty"` // RawMessage to allow flexible speech config
}
+type MediaResolution string
+
type GeminiChatCandidate struct {
Content GeminiChatContent `json:"content"`
FinishReason *string `json:"finishReason"`
diff --git a/main.go b/main.go
index 0caf5361..b1421f9e 100644
--- a/main.go
+++ b/main.go
@@ -16,6 +16,7 @@ import (
"one-api/setting/ratio_setting"
"os"
"strconv"
+ "time"
"github.com/bytedance/gopkg/util/gopool"
"github.com/gin-contrib/sessions"
@@ -33,6 +34,7 @@ var buildFS embed.FS
var indexPage []byte
func main() {
+ startTime := time.Now()
err := InitResources()
if err != nil {
@@ -150,6 +152,10 @@ func main() {
if port == "" {
port = strconv.Itoa(*common.Port)
}
+
+ // Log startup success message
+ common.LogStartupSuccess(startTime, port)
+
err = server.Run(":" + port)
if err != nil {
common.FatalLog("failed to start HTTP server: " + err.Error())
@@ -204,4 +210,4 @@ func InitResources() error {
return err
}
return nil
-}
\ No newline at end of file
+}
diff --git a/web/src/components/common/markdown/MarkdownRenderer.jsx b/web/src/components/common/markdown/MarkdownRenderer.jsx
index f1283a64..05419f8c 100644
--- a/web/src/components/common/markdown/MarkdownRenderer.jsx
+++ b/web/src/components/common/markdown/MarkdownRenderer.jsx
@@ -181,8 +181,8 @@ export function PreCode(props) {
e.preventDefault();
e.stopPropagation();
if (ref.current) {
- const code =
- ref.current.querySelector('code')?.innerText ?? '';
+ const codeElement = ref.current.querySelector('code');
+ const code = codeElement?.textContent ?? '';
copy(code).then((success) => {
if (success) {
Toast.success(t('代码已复制到剪贴板'));
diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx
index ecc6ced6..dd620fe0 100644
--- a/web/src/components/table/channels/modals/EditChannelModal.jsx
+++ b/web/src/components/table/channels/modals/EditChannelModal.jsx
@@ -85,6 +85,26 @@ const REGION_EXAMPLE = {
'claude-3-5-sonnet-20240620': 'europe-west1',
};
+// 支持并且已适配通过接口获取模型列表的渠道类型
+const MODEL_FETCHABLE_TYPES = new Set([
+ 1,
+ 4,
+ 14,
+ 34,
+ 17,
+ 26,
+ 24,
+ 47,
+ 25,
+ 20,
+ 23,
+ 31,
+ 35,
+ 40,
+ 42,
+ 48,
+]);
+
function type2secretPrompt(type) {
// inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
switch (type) {
@@ -1904,13 +1924,15 @@ const EditChannelModal = (props) => {
>
{t('填入所有模型')}
-
+ {MODEL_FETCHABLE_TYPES.has(inputs.type) && (
+
+ )}
)}
+ handleDeleteKey(record.index)}
+ okType={'danger'}
+ position={'topRight'}
+ >
+
+
),
},
diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js
index b7092fe7..bc389b2e 100644
--- a/web/src/helpers/api.js
+++ b/web/src/helpers/api.js
@@ -118,7 +118,6 @@ export const buildApiPayload = (
model: inputs.model,
group: inputs.group,
messages: processedMessages,
- group: inputs.group,
stream: inputs.stream,
};
@@ -132,13 +131,15 @@ export const buildApiPayload = (
seed: 'seed',
};
+
Object.entries(parameterMappings).forEach(([key, param]) => {
- if (
- parameterEnabled[key] &&
- inputs[param] !== undefined &&
- inputs[param] !== null
- ) {
- payload[param] = inputs[param];
+ const enabled = parameterEnabled[key];
+ const value = inputs[param];
+ const hasValue = value !== undefined && value !== null;
+
+
+ if (enabled && hasValue) {
+ payload[param] = value;
}
});
diff --git a/web/src/helpers/utils.jsx b/web/src/helpers/utils.jsx
index e446ea69..bcd13230 100644
--- a/web/src/helpers/utils.jsx
+++ b/web/src/helpers/utils.jsx
@@ -75,13 +75,17 @@ export async function copy(text) {
await navigator.clipboard.writeText(text);
} catch (e) {
try {
- // 构建input 执行 复制命令
- var _input = window.document.createElement('input');
- _input.value = text;
- window.document.body.appendChild(_input);
- _input.select();
- window.document.execCommand('Copy');
- window.document.body.removeChild(_input);
+ // 构建 textarea 执行复制命令,保留多行文本格式
+ const textarea = window.document.createElement('textarea');
+ textarea.value = text;
+ textarea.setAttribute('readonly', '');
+ textarea.style.position = 'fixed';
+ textarea.style.left = '-9999px';
+ textarea.style.top = '-9999px';
+ window.document.body.appendChild(textarea);
+ textarea.select();
+ window.document.execCommand('copy');
+ window.document.body.removeChild(textarea);
} catch (e) {
okay = false;
console.error(e);
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index a305b0a9..ceb0f2d3 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -1889,6 +1889,10 @@
"确定要删除所有已自动禁用的密钥吗?": "Are you sure you want to delete all automatically disabled keys?",
"此操作不可撤销,将永久删除已自动禁用的密钥": "This operation cannot be undone, and all automatically disabled keys will be permanently deleted.",
"删除自动禁用密钥": "Delete auto disabled keys",
+ "确定要删除此密钥吗?": "Are you sure you want to delete this key?",
+ "此操作不可撤销,将永久删除该密钥": "This operation cannot be undone, and the key will be permanently deleted.",
+ "密钥已删除": "Key has been deleted",
+ "删除密钥失败": "Failed to delete key",
"图标": "Icon",
"模型图标": "Model icon",
"请输入图标名称": "Please enter the icon name",