diff --git a/.goreleaser.yml b/.goreleaser.yml
index f291b7fb..1e944d1b 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -8,6 +8,7 @@ builds:
- linux
- windows
- darwin
+ - freebsd
goarch:
- amd64
- arm64
diff --git a/README_JA.md b/README_JA.md
index 4dbd36bb..8d5cb2bc 100644
--- a/README_JA.md
+++ b/README_JA.md
@@ -30,6 +30,10 @@ GLM CODING PLANを10%割引で取得:https://z.ai/subscribe?ic=8JVLJQFSKB
 |
AICodeMirrorのスポンサーシップに感謝します!AICodeMirrorはClaude Code / Codex / Gemini CLI向けの公式高安定性リレーサービスを提供しており、エンタープライズグレードの同時接続、迅速な請求書発行、24時間365日の専任技術サポートを備えています。Claude Code / Codex / Geminiの公式チャネルが元の価格の38% / 2% / 9%で利用でき、チャージ時にはさらに割引があります!CLIProxyAPIユーザー向けの特別特典:こちらのリンクから登録すると、初回チャージが20%割引になり、エンタープライズのお客様は最大25%割引を受けられます! |
+
+ |
+本プロジェクトにご支援いただいた BmoPlus に感謝いたします!BmoPlusは、AIサブスクリプションのヘビーユーザー向けに特化した信頼性の高いAIアカウントサービスプロバイダーであり、安定した ChatGPT Plus / ChatGPT Pro (完全保証) / Claude Pro / Super Grok / Gemini Pro の公式代行チャージおよび即納アカウントを提供しています。こちらのBmoPlus AIアカウント専門店/代行チャージ経由でご登録・ご注文いただいたユーザー様は、GPTを 公式サイト価格の約1割(90% OFF) という驚異的な価格でご利用いただけます! |
+
diff --git a/assets/bmoplus.png b/assets/bmoplus.png
new file mode 100644
index 00000000..27b8df41
Binary files /dev/null and b/assets/bmoplus.png differ
diff --git a/config.example.yaml b/config.example.yaml
index 0ea51e5b..6d87ee18 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -25,6 +25,10 @@ remote-management:
# Disable the bundled management control panel asset download and HTTP route when true.
disable-control-panel: false
+ # Disable automatic periodic background updates of the management panel from GitHub (default: false).
+ # When enabled, the panel is only downloaded on first access if missing, and never auto-updated afterward.
+ # disable-auto-update-panel: false
+
# GitHub repository for the management control panel. Accepts a repository URL or releases API URL.
panel-github-repository: 'https://github.com/router-for-me/Cli-Proxy-API-Management-Center'
diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go
index a0d9c159..5f067e49 100644
--- a/internal/api/handlers/management/auth_files.go
+++ b/internal/api/handlers/management/auth_files.go
@@ -550,10 +550,23 @@ func isRuntimeOnlyAuth(auth *coreauth.Auth) bool {
return strings.EqualFold(strings.TrimSpace(auth.Attributes["runtime_only"]), "true")
}
+func isUnsafeAuthFileName(name string) bool {
+ if strings.TrimSpace(name) == "" {
+ return true
+ }
+ if strings.ContainsAny(name, "/\\") {
+ return true
+ }
+ if filepath.VolumeName(name) != "" {
+ return true
+ }
+ return false
+}
+
// Download single auth file by name
func (h *Handler) DownloadAuthFile(c *gin.Context) {
- name := c.Query("name")
- if name == "" || strings.Contains(name, string(os.PathSeparator)) {
+ name := strings.TrimSpace(c.Query("name"))
+ if isUnsafeAuthFileName(name) {
c.JSON(400, gin.H{"error": "invalid name"})
return
}
@@ -635,8 +648,8 @@ func (h *Handler) UploadAuthFile(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "no files uploaded"})
return
}
- name := c.Query("name")
- if name == "" || strings.Contains(name, string(os.PathSeparator)) {
+ name := strings.TrimSpace(c.Query("name"))
+ if isUnsafeAuthFileName(name) {
c.JSON(400, gin.H{"error": "invalid name"})
return
}
@@ -869,7 +882,7 @@ func uniqueAuthFileNames(names []string) []string {
func (h *Handler) deleteAuthFileByName(ctx context.Context, name string) (string, int, error) {
name = strings.TrimSpace(name)
- if name == "" || strings.Contains(name, string(os.PathSeparator)) {
+ if isUnsafeAuthFileName(name) {
return "", http.StatusBadRequest, fmt.Errorf("invalid name")
}
diff --git a/internal/api/handlers/management/auth_files_download_test.go b/internal/api/handlers/management/auth_files_download_test.go
new file mode 100644
index 00000000..a2a20d30
--- /dev/null
+++ b/internal/api/handlers/management/auth_files_download_test.go
@@ -0,0 +1,62 @@
+package management
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
+)
+
+func TestDownloadAuthFile_ReturnsFile(t *testing.T) {
+ t.Setenv("MANAGEMENT_PASSWORD", "")
+ gin.SetMode(gin.TestMode)
+
+ authDir := t.TempDir()
+ fileName := "download-user.json"
+ expected := []byte(`{"type":"codex"}`)
+ if err := os.WriteFile(filepath.Join(authDir, fileName), expected, 0o600); err != nil {
+ t.Fatalf("failed to write auth file: %v", err)
+ }
+
+ h := NewHandlerWithoutConfigFilePath(&config.Config{AuthDir: authDir}, nil)
+
+ rec := httptest.NewRecorder()
+ ctx, _ := gin.CreateTestContext(rec)
+ ctx.Request = httptest.NewRequest(http.MethodGet, "/v0/management/auth-files/download?name="+url.QueryEscape(fileName), nil)
+ h.DownloadAuthFile(ctx)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected download status %d, got %d with body %s", http.StatusOK, rec.Code, rec.Body.String())
+ }
+ if got := rec.Body.Bytes(); string(got) != string(expected) {
+ t.Fatalf("unexpected download content: %q", string(got))
+ }
+}
+
+func TestDownloadAuthFile_RejectsPathSeparators(t *testing.T) {
+ t.Setenv("MANAGEMENT_PASSWORD", "")
+ gin.SetMode(gin.TestMode)
+
+ h := NewHandlerWithoutConfigFilePath(&config.Config{AuthDir: t.TempDir()}, nil)
+
+ for _, name := range []string{
+ "../external/secret.json",
+ `..\\external\\secret.json`,
+ "nested/secret.json",
+ `nested\\secret.json`,
+ } {
+ rec := httptest.NewRecorder()
+ ctx, _ := gin.CreateTestContext(rec)
+ ctx.Request = httptest.NewRequest(http.MethodGet, "/v0/management/auth-files/download?name="+url.QueryEscape(name), nil)
+ h.DownloadAuthFile(ctx)
+
+ if rec.Code != http.StatusBadRequest {
+ t.Fatalf("expected %d for name %q, got %d with body %s", http.StatusBadRequest, name, rec.Code, rec.Body.String())
+ }
+ }
+}
diff --git a/internal/api/handlers/management/auth_files_download_windows_test.go b/internal/api/handlers/management/auth_files_download_windows_test.go
new file mode 100644
index 00000000..8c174ccf
--- /dev/null
+++ b/internal/api/handlers/management/auth_files_download_windows_test.go
@@ -0,0 +1,51 @@
+//go:build windows
+
+package management
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/gin-gonic/gin"
+ "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
+)
+
+func TestDownloadAuthFile_PreventsWindowsSlashTraversal(t *testing.T) {
+ t.Setenv("MANAGEMENT_PASSWORD", "")
+ gin.SetMode(gin.TestMode)
+
+ tempDir := t.TempDir()
+ authDir := filepath.Join(tempDir, "auth")
+ externalDir := filepath.Join(tempDir, "external")
+ if err := os.MkdirAll(authDir, 0o700); err != nil {
+ t.Fatalf("failed to create auth dir: %v", err)
+ }
+ if err := os.MkdirAll(externalDir, 0o700); err != nil {
+ t.Fatalf("failed to create external dir: %v", err)
+ }
+
+ secretName := "secret.json"
+ secretPath := filepath.Join(externalDir, secretName)
+ if err := os.WriteFile(secretPath, []byte(`{"secret":true}`), 0o600); err != nil {
+ t.Fatalf("failed to write external file: %v", err)
+ }
+
+ h := NewHandlerWithoutConfigFilePath(&config.Config{AuthDir: authDir}, nil)
+
+ rec := httptest.NewRecorder()
+ ctx, _ := gin.CreateTestContext(rec)
+ ctx.Request = httptest.NewRequest(
+ http.MethodGet,
+ "/v0/management/auth-files/download?name="+url.QueryEscape("../external/"+secretName),
+ nil,
+ )
+ h.DownloadAuthFile(ctx)
+
+ if rec.Code != http.StatusBadRequest {
+ t.Fatalf("expected status %d, got %d with body %s", http.StatusBadRequest, rec.Code, rec.Body.String())
+ }
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index 487f873d..8c6f2f7b 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -195,6 +195,9 @@ type RemoteManagement struct {
SecretKey string `yaml:"secret-key"`
// DisableControlPanel skips serving and syncing the bundled management UI when true.
DisableControlPanel bool `yaml:"disable-control-panel"`
+ // DisableAutoUpdatePanel disables automatic periodic background updates of the management panel asset from GitHub.
+ // When false (the default), the background updater remains enabled; when true, the panel is only downloaded on first access if missing.
+ DisableAutoUpdatePanel bool `yaml:"disable-auto-update-panel"`
// PanelGitHubRepository overrides the GitHub repository used to fetch the management panel asset.
// Accepts either a repository URL (https://github.com/org/repo) or an API releases endpoint.
PanelGitHubRepository string `yaml:"panel-github-repository"`
diff --git a/internal/managementasset/updater.go b/internal/managementasset/updater.go
index 7284b729..ae2bc819 100644
--- a/internal/managementasset/updater.go
+++ b/internal/managementasset/updater.go
@@ -31,6 +31,7 @@ const (
httpUserAgent = "CLIProxyAPI-management-updater"
managementSyncMinInterval = 30 * time.Second
updateCheckInterval = 3 * time.Hour
+ maxAssetDownloadSize = 50 << 20 // 10 MB safety limit for management asset downloads
)
// ManagementFileName exposes the control panel asset filename.
@@ -88,6 +89,10 @@ func runAutoUpdater(ctx context.Context) {
log.Debug("management asset auto-updater skipped: control panel disabled")
return
}
+ if cfg.RemoteManagement.DisableAutoUpdatePanel {
+ log.Debug("management asset auto-updater skipped: disable-auto-update-panel is enabled")
+ return
+ }
configPath, _ := schedulerConfigPath.Load().(string)
staticDir := StaticDir(configPath)
@@ -259,7 +264,8 @@ func EnsureLatestManagementHTML(ctx context.Context, staticDir string, proxyURL
}
if remoteHash != "" && !strings.EqualFold(remoteHash, downloadedHash) {
- log.Warnf("remote digest mismatch for management asset: expected %s got %s", remoteHash, downloadedHash)
+ log.Errorf("management asset digest mismatch: expected %s got %s — aborting update for safety", remoteHash, downloadedHash)
+ return nil, nil
}
if err = atomicWriteFile(localPath, data); err != nil {
@@ -282,6 +288,9 @@ func ensureFallbackManagementHTML(ctx context.Context, client *http.Client, loca
return false
}
+ log.Warnf("management asset downloaded from fallback URL without digest verification (hash=%s) — "+
+ "enable verified GitHub updates by keeping disable-auto-update-panel set to false", downloadedHash)
+
if err = atomicWriteFile(localPath, data); err != nil {
log.WithError(err).Warn("failed to persist fallback management control panel page")
return false
@@ -392,10 +401,13 @@ func downloadAsset(ctx context.Context, client *http.Client, downloadURL string)
return nil, "", fmt.Errorf("unexpected download status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
- data, err := io.ReadAll(resp.Body)
+ data, err := io.ReadAll(io.LimitReader(resp.Body, maxAssetDownloadSize+1))
if err != nil {
return nil, "", fmt.Errorf("read download body: %w", err)
}
+ if int64(len(data)) > maxAssetDownloadSize {
+ return nil, "", fmt.Errorf("download exceeds maximum allowed size of %d bytes", maxAssetDownloadSize)
+ }
sum := sha256.Sum256(data)
return data, hex.EncodeToString(sum[:]), nil
diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_request.go b/internal/translator/claude/openai/chat-completions/claude_openai_request.go
index 112e286d..e9d8d35b 100644
--- a/internal/translator/claude/openai/chat-completions/claude_openai_request.go
+++ b/internal/translator/claude/openai/chat-completions/claude_openai_request.go
@@ -165,29 +165,22 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
// Process messages and transform them to Claude Code format
if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
messageIndex := 0
- systemMessageIndex := -1
messages.ForEach(func(_, message gjson.Result) bool {
role := message.Get("role").String()
contentResult := message.Get("content")
switch role {
case "system":
- if systemMessageIndex == -1 {
- systemMsg := []byte(`{"role":"user","content":[]}`)
- out, _ = sjson.SetRawBytes(out, "messages.-1", systemMsg)
- systemMessageIndex = messageIndex
- messageIndex++
- }
if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
textPart := []byte(`{"type":"text","text":""}`)
textPart, _ = sjson.SetBytes(textPart, "text", contentResult.String())
- out, _ = sjson.SetRawBytes(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart)
+ out, _ = sjson.SetRawBytes(out, "system.-1", textPart)
} else if contentResult.Exists() && contentResult.IsArray() {
contentResult.ForEach(func(_, part gjson.Result) bool {
if part.Get("type").String() == "text" {
textPart := []byte(`{"type":"text","text":""}`)
textPart, _ = sjson.SetBytes(textPart, "text", part.Get("text").String())
- out, _ = sjson.SetRawBytes(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart)
+ out, _ = sjson.SetRawBytes(out, "system.-1", textPart)
}
return true
})
@@ -269,6 +262,16 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
}
return true
})
+
+ // Preserve a minimal conversational turn for system-only inputs.
+ // Claude payloads with top-level system instructions but no messages are risky for downstream validation.
+ if messageIndex == 0 {
+ system := gjson.GetBytes(out, "system")
+ if system.Exists() && system.IsArray() && len(system.Array()) > 0 {
+ fallbackMsg := []byte(`{"role":"user","content":[{"type":"text","text":""}]}`)
+ out, _ = sjson.SetRawBytes(out, "messages.-1", fallbackMsg)
+ }
+ }
}
// Tools mapping: OpenAI tools -> Claude Code tools
diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_request_test.go b/internal/translator/claude/openai/chat-completions/claude_openai_request_test.go
index ed84661d..ead08d72 100644
--- a/internal/translator/claude/openai/chat-completions/claude_openai_request_test.go
+++ b/internal/translator/claude/openai/chat-completions/claude_openai_request_test.go
@@ -135,3 +135,111 @@ func TestConvertOpenAIRequestToClaude_ToolResultURLImageOnly(t *testing.T) {
t.Fatalf("Unexpected image URL: %q", got)
}
}
+
+func TestConvertOpenAIRequestToClaude_SystemRoleBecomesTopLevelSystem(t *testing.T) {
+ inputJSON := `{
+ "model": "gpt-4.1",
+ "messages": [
+ {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "user", "content": "Hello"}
+ ]
+ }`
+
+ result := ConvertOpenAIRequestToClaude("claude-sonnet-4-5", []byte(inputJSON), false)
+ resultJSON := gjson.ParseBytes(result)
+
+ system := resultJSON.Get("system")
+ if !system.IsArray() {
+ t.Fatalf("Expected top-level system array, got %s", system.Raw)
+ }
+ if len(system.Array()) != 1 {
+ t.Fatalf("Expected 1 system block, got %d. System: %s", len(system.Array()), system.Raw)
+ }
+ if got := system.Get("0.type").String(); got != "text" {
+ t.Fatalf("Expected system block type %q, got %q", "text", got)
+ }
+ if got := system.Get("0.text").String(); got != "You are a helpful assistant." {
+ t.Fatalf("Expected system text %q, got %q", "You are a helpful assistant.", got)
+ }
+
+ messages := resultJSON.Get("messages").Array()
+ if len(messages) != 1 {
+ t.Fatalf("Expected 1 non-system message, got %d. Messages: %s", len(messages), resultJSON.Get("messages").Raw)
+ }
+ if got := messages[0].Get("role").String(); got != "user" {
+ t.Fatalf("Expected remaining message role %q, got %q", "user", got)
+ }
+ if got := messages[0].Get("content.0.text").String(); got != "Hello" {
+ t.Fatalf("Expected user text %q, got %q", "Hello", got)
+ }
+}
+
+func TestConvertOpenAIRequestToClaude_MultipleSystemMessagesMergedIntoTopLevelSystem(t *testing.T) {
+ inputJSON := `{
+ "model": "gpt-4.1",
+ "messages": [
+ {"role": "system", "content": "Rule 1"},
+ {"role": "system", "content": [{"type": "text", "text": "Rule 2"}]},
+ {"role": "user", "content": "Hello"}
+ ]
+ }`
+
+ result := ConvertOpenAIRequestToClaude("claude-sonnet-4-5", []byte(inputJSON), false)
+ resultJSON := gjson.ParseBytes(result)
+
+ system := resultJSON.Get("system").Array()
+ if len(system) != 2 {
+ t.Fatalf("Expected 2 system blocks, got %d. System: %s", len(system), resultJSON.Get("system").Raw)
+ }
+ if got := system[0].Get("text").String(); got != "Rule 1" {
+ t.Fatalf("Expected first system text %q, got %q", "Rule 1", got)
+ }
+ if got := system[1].Get("text").String(); got != "Rule 2" {
+ t.Fatalf("Expected second system text %q, got %q", "Rule 2", got)
+ }
+
+ messages := resultJSON.Get("messages").Array()
+ if len(messages) != 1 {
+ t.Fatalf("Expected 1 non-system message, got %d. Messages: %s", len(messages), resultJSON.Get("messages").Raw)
+ }
+ if got := messages[0].Get("role").String(); got != "user" {
+ t.Fatalf("Expected remaining message role %q, got %q", "user", got)
+ }
+ if got := messages[0].Get("content.0.text").String(); got != "Hello" {
+ t.Fatalf("Expected user text %q, got %q", "Hello", got)
+ }
+}
+
+func TestConvertOpenAIRequestToClaude_SystemOnlyInputKeepsFallbackUserMessage(t *testing.T) {
+ inputJSON := `{
+ "model": "gpt-4.1",
+ "messages": [
+ {"role": "system", "content": "You are a helpful assistant."}
+ ]
+ }`
+
+ result := ConvertOpenAIRequestToClaude("claude-sonnet-4-5", []byte(inputJSON), false)
+ resultJSON := gjson.ParseBytes(result)
+
+ system := resultJSON.Get("system").Array()
+ if len(system) != 1 {
+ t.Fatalf("Expected 1 system block, got %d. System: %s", len(system), resultJSON.Get("system").Raw)
+ }
+ if got := system[0].Get("text").String(); got != "You are a helpful assistant." {
+ t.Fatalf("Expected system text %q, got %q", "You are a helpful assistant.", got)
+ }
+
+ messages := resultJSON.Get("messages").Array()
+ if len(messages) != 1 {
+ t.Fatalf("Expected 1 fallback message, got %d. Messages: %s", len(messages), resultJSON.Get("messages").Raw)
+ }
+ if got := messages[0].Get("role").String(); got != "user" {
+ t.Fatalf("Expected fallback message role %q, got %q", "user", got)
+ }
+ if got := messages[0].Get("content.0.type").String(); got != "text" {
+ t.Fatalf("Expected fallback content type %q, got %q", "text", got)
+ }
+ if got := messages[0].Get("content.0.text").String(); got != "" {
+ t.Fatalf("Expected fallback text %q, got %q", "", got)
+ }
+}
diff --git a/internal/watcher/diff/config_diff.go b/internal/watcher/diff/config_diff.go
index 7997f04e..fccdaf8d 100644
--- a/internal/watcher/diff/config_diff.go
+++ b/internal/watcher/diff/config_diff.go
@@ -256,6 +256,9 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
if oldCfg.RemoteManagement.DisableControlPanel != newCfg.RemoteManagement.DisableControlPanel {
changes = append(changes, fmt.Sprintf("remote-management.disable-control-panel: %t -> %t", oldCfg.RemoteManagement.DisableControlPanel, newCfg.RemoteManagement.DisableControlPanel))
}
+ if oldCfg.RemoteManagement.DisableAutoUpdatePanel != newCfg.RemoteManagement.DisableAutoUpdatePanel {
+ changes = append(changes, fmt.Sprintf("remote-management.disable-auto-update-panel: %t -> %t", oldCfg.RemoteManagement.DisableAutoUpdatePanel, newCfg.RemoteManagement.DisableAutoUpdatePanel))
+ }
oldPanelRepo := strings.TrimSpace(oldCfg.RemoteManagement.PanelGitHubRepository)
newPanelRepo := strings.TrimSpace(newCfg.RemoteManagement.PanelGitHubRepository)
if oldPanelRepo != newPanelRepo {
diff --git a/internal/watcher/diff/config_diff_test.go b/internal/watcher/diff/config_diff_test.go
index f35ceeea..c7b73f11 100644
--- a/internal/watcher/diff/config_diff_test.go
+++ b/internal/watcher/diff/config_diff_test.go
@@ -20,10 +20,11 @@ func TestBuildConfigChangeDetails(t *testing.T) {
RestrictManagementToLocalhost: false,
},
RemoteManagement: config.RemoteManagement{
- AllowRemote: false,
- SecretKey: "old",
- DisableControlPanel: false,
- PanelGitHubRepository: "repo-old",
+ AllowRemote: false,
+ SecretKey: "old",
+ DisableControlPanel: false,
+ DisableAutoUpdatePanel: false,
+ PanelGitHubRepository: "repo-old",
},
OAuthExcludedModels: map[string][]string{
"providerA": {"m1"},
@@ -54,10 +55,11 @@ func TestBuildConfigChangeDetails(t *testing.T) {
},
},
RemoteManagement: config.RemoteManagement{
- AllowRemote: true,
- SecretKey: "new",
- DisableControlPanel: true,
- PanelGitHubRepository: "repo-new",
+ AllowRemote: true,
+ SecretKey: "new",
+ DisableControlPanel: true,
+ DisableAutoUpdatePanel: true,
+ PanelGitHubRepository: "repo-new",
},
OAuthExcludedModels: map[string][]string{
"providerA": {"m1", "m2"},
@@ -88,6 +90,7 @@ func TestBuildConfigChangeDetails(t *testing.T) {
expectContains(t, details, "ampcode.upstream-url: http://old-upstream -> http://new-upstream")
expectContains(t, details, "ampcode.model-mappings: updated (1 -> 2 entries)")
expectContains(t, details, "remote-management.allow-remote: false -> true")
+ expectContains(t, details, "remote-management.disable-auto-update-panel: false -> true")
expectContains(t, details, "remote-management.secret-key: updated")
expectContains(t, details, "oauth-excluded-models[providera]: updated (1 -> 2 entries)")
expectContains(t, details, "oauth-excluded-models[providerb]: added (1 entries)")
@@ -265,9 +268,10 @@ func TestBuildConfigChangeDetails_FlagsAndKeys(t *testing.T) {
ModelMappings: []config.AmpModelMapping{{From: "a", To: "b"}},
},
RemoteManagement: config.RemoteManagement{
- DisableControlPanel: true,
- PanelGitHubRepository: "new/repo",
- SecretKey: "",
+ DisableControlPanel: true,
+ DisableAutoUpdatePanel: true,
+ PanelGitHubRepository: "new/repo",
+ SecretKey: "",
},
SDKConfig: sdkconfig.SDKConfig{
RequestLog: true,
@@ -299,6 +303,7 @@ func TestBuildConfigChangeDetails_FlagsAndKeys(t *testing.T) {
expectContains(t, details, "ampcode.restrict-management-to-localhost: false -> true")
expectContains(t, details, "ampcode.upstream-api-key: removed")
expectContains(t, details, "remote-management.disable-control-panel: false -> true")
+ expectContains(t, details, "remote-management.disable-auto-update-panel: false -> true")
expectContains(t, details, "remote-management.panel-github-repository: old/repo -> new/repo")
expectContains(t, details, "remote-management.secret-key: deleted")
}
@@ -336,10 +341,11 @@ func TestBuildConfigChangeDetails_AllBranches(t *testing.T) {
ForceModelMappings: false,
},
RemoteManagement: config.RemoteManagement{
- AllowRemote: false,
- DisableControlPanel: false,
- PanelGitHubRepository: "old/repo",
- SecretKey: "old",
+ AllowRemote: false,
+ DisableControlPanel: false,
+ DisableAutoUpdatePanel: false,
+ PanelGitHubRepository: "old/repo",
+ SecretKey: "old",
},
SDKConfig: sdkconfig.SDKConfig{
RequestLog: false,
@@ -389,10 +395,11 @@ func TestBuildConfigChangeDetails_AllBranches(t *testing.T) {
ForceModelMappings: true,
},
RemoteManagement: config.RemoteManagement{
- AllowRemote: true,
- DisableControlPanel: true,
- PanelGitHubRepository: "new/repo",
- SecretKey: "",
+ AllowRemote: true,
+ DisableControlPanel: true,
+ DisableAutoUpdatePanel: true,
+ PanelGitHubRepository: "new/repo",
+ SecretKey: "",
},
SDKConfig: sdkconfig.SDKConfig{
RequestLog: true,
@@ -460,6 +467,7 @@ func TestBuildConfigChangeDetails_AllBranches(t *testing.T) {
expectContains(t, changes, "oauth-excluded-models[p2]: added (1 entries)")
expectContains(t, changes, "remote-management.allow-remote: false -> true")
expectContains(t, changes, "remote-management.disable-control-panel: false -> true")
+ expectContains(t, changes, "remote-management.disable-auto-update-panel: false -> true")
expectContains(t, changes, "remote-management.panel-github-repository: old/repo -> new/repo")
expectContains(t, changes, "remote-management.secret-key: deleted")
expectContains(t, changes, "openai-compatibility:")