Compare commits

...

10 Commits

Author SHA1 Message Date
Luis Pater
e24af6e545 Merge branch 'router-for-me:main' into main 2025-12-04 18:03:42 +08:00
Luis Pater
bceecfb2e3 Fixed: #414
**refactor(gemini): comment out unused CLI preview entry**
2025-12-04 17:55:13 +08:00
Luis Pater
48dd987867 Merge branch 'router-for-me:main' into main 2025-12-04 16:14:52 +08:00
Luis Pater
6a2906e3e5 **feat(antigravity): add support for Claude-Opus-4-5-Thinking model** 2025-12-04 16:13:13 +08:00
Luis Pater
d72886c801 Merge pull request #405 from thurstonsand/fix/amp-missing-proxy-routes
fix(amp): add missing /auth/* and /api/tab/* proxy routes for AMP CLI
2025-12-03 22:23:41 +08:00
Luis Pater
6efba3d829 Merge pull request #406 from router-for-me/api
refactor(api): remove legacy generative-language-api-key endpoints and duplicate GetConfigYAML
2025-12-03 22:21:15 +08:00
Thurston Sandberg
373ea8d7e4 fix(logging): handle nil caller in LogFormatter to prevent panic 2025-12-03 05:54:38 -05:00
hkfires
b5de004c01 refactor(api): remove legacy generative-language-api-key endpoints and duplicate GetConfigYAML 2025-12-03 18:35:08 +08:00
Thurston Sandberg
94ec772521 test(amp): add tests for /auth/* and /api/tab/* routes 2025-12-03 05:03:25 -05:00
Thurston Sandberg
e216d26731 fix(amp): add missing /auth/* and /api/tab/* proxy routes 2025-12-03 04:40:32 -05:00
8 changed files with 28 additions and 93 deletions

View File

@@ -20,25 +20,6 @@ func (h *Handler) GetConfig(c *gin.Context) {
c.JSON(200, &cfgCopy)
}
func (h *Handler) GetConfigYAML(c *gin.Context) {
data, err := os.ReadFile(h.configFilePath)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "read_failed", "message": err.Error()})
return
}
var node yaml.Node
if err = yaml.Unmarshal(data, &node); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "parse_failed", "message": err.Error()})
return
}
c.Header("Content-Type", "application/yaml; charset=utf-8")
c.Header("Vary", "format, Accept")
enc := yaml.NewEncoder(c.Writer)
enc.SetIndent(2)
_ = enc.Encode(&node)
_ = enc.Close()
}
func WriteConfig(path string, data []byte) error {
data = config.NormalizeCommentIndentation(data)
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
@@ -110,9 +91,9 @@ func (h *Handler) PutConfigYAML(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true, "changed": []string{"config"}})
}
// GetConfigFile returns the raw config.yaml file bytes without re-encoding.
// GetConfigYAML returns the raw config.yaml file bytes without re-encoding.
// It preserves comments and original formatting/styles.
func (h *Handler) GetConfigFile(c *gin.Context) {
func (h *Handler) GetConfigYAML(c *gin.Context) {
data, err := os.ReadFile(h.configFilePath)
if err != nil {
if os.IsNotExist(err) {

View File

@@ -104,52 +104,6 @@ func (h *Handler) deleteFromStringList(c *gin.Context, target *[]string, after f
c.JSON(400, gin.H{"error": "missing index or value"})
}
func sanitizeStringSlice(in []string) []string {
out := make([]string, 0, len(in))
for i := range in {
if trimmed := strings.TrimSpace(in[i]); trimmed != "" {
out = append(out, trimmed)
}
}
return out
}
func geminiKeyStringsFromConfig(cfg *config.Config) []string {
if cfg == nil || len(cfg.GeminiKey) == 0 {
return nil
}
out := make([]string, 0, len(cfg.GeminiKey))
for i := range cfg.GeminiKey {
if key := strings.TrimSpace(cfg.GeminiKey[i].APIKey); key != "" {
out = append(out, key)
}
}
return out
}
func (h *Handler) applyLegacyKeys(keys []string) {
if h == nil || h.cfg == nil {
return
}
sanitized := sanitizeStringSlice(keys)
existing := make(map[string]config.GeminiKey, len(h.cfg.GeminiKey))
for _, entry := range h.cfg.GeminiKey {
if key := strings.TrimSpace(entry.APIKey); key != "" {
existing[key] = entry
}
}
newList := make([]config.GeminiKey, 0, len(sanitized))
for _, key := range sanitized {
if entry, ok := existing[key]; ok {
newList = append(newList, entry)
} else {
newList = append(newList, config.GeminiKey{APIKey: key})
}
}
h.cfg.GeminiKey = newList
h.cfg.SanitizeGeminiKeys()
}
// api-keys
func (h *Handler) GetAPIKeys(c *gin.Context) { c.JSON(200, gin.H{"api-keys": h.cfg.APIKeys}) }
func (h *Handler) PutAPIKeys(c *gin.Context) {
@@ -165,24 +119,6 @@ func (h *Handler) DeleteAPIKeys(c *gin.Context) {
h.deleteFromStringList(c, &h.cfg.APIKeys, func() { h.cfg.Access.Providers = nil })
}
// generative-language-api-key
func (h *Handler) GetGlKeys(c *gin.Context) {
c.JSON(200, gin.H{"generative-language-api-key": geminiKeyStringsFromConfig(h.cfg)})
}
func (h *Handler) PutGlKeys(c *gin.Context) {
h.putStringList(c, func(v []string) {
h.applyLegacyKeys(v)
}, nil)
}
func (h *Handler) PatchGlKeys(c *gin.Context) {
target := append([]string(nil), geminiKeyStringsFromConfig(h.cfg)...)
h.patchStringList(c, &target, func() { h.applyLegacyKeys(target) })
}
func (h *Handler) DeleteGlKeys(c *gin.Context) {
target := append([]string(nil), geminiKeyStringsFromConfig(h.cfg)...)
h.deleteFromStringList(c, &target, func() { h.applyLegacyKeys(target) })
}
// gemini-api-key: []GeminiKey
func (h *Handler) GetGeminiKeys(c *gin.Context) {
c.JSON(200, gin.H{"gemini-api-key": h.cfg.GeminiKey})

View File

@@ -110,6 +110,8 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha
ampAPI.Any("/threads/*path", proxyHandler)
ampAPI.Any("/otel", proxyHandler)
ampAPI.Any("/otel/*path", proxyHandler)
ampAPI.Any("/tab", proxyHandler)
ampAPI.Any("/tab/*path", proxyHandler)
// Root-level routes that AMP CLI expects without /api prefix
// These need the same security middleware as the /api/* routes
@@ -119,6 +121,12 @@ func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *ha
}
engine.GET("/threads.rss", append(rootMiddleware, proxyHandler)...)
// Root-level auth routes for CLI login flow
// Amp uses multiple auth routes: /auth/cli-login, /auth/callback, /auth/sign-in, /auth/logout
// We proxy all /auth/* to support the complete OAuth flow
engine.Any("/auth", append(rootMiddleware, proxyHandler)...)
engine.Any("/auth/*path", append(rootMiddleware, proxyHandler)...)
// Google v1beta1 passthrough with OAuth fallback
// AMP CLI uses non-standard paths like /publishers/google/models/...
// We bridge these to our standard Gemini handler to enable local OAuth.

View File

@@ -39,6 +39,11 @@ func TestRegisterManagementRoutes(t *testing.T) {
{"/api/threads", http.MethodGet},
{"/threads.rss", http.MethodGet}, // Root-level route (no /api prefix)
{"/api/otel", http.MethodGet},
{"/api/tab", http.MethodGet},
{"/api/tab/some/path", http.MethodGet},
{"/auth", http.MethodGet}, // Root-level auth route
{"/auth/cli-login", http.MethodGet}, // CLI login flow
{"/auth/callback", http.MethodGet}, // OAuth callback
// Google v1beta1 bridge should still proxy non-model requests (GET) and allow POST
{"/api/provider/google/v1beta1/models", http.MethodGet},
{"/api/provider/google/v1beta1/models", http.MethodPost},

View File

@@ -470,8 +470,8 @@ func (s *Server) registerManagementRoutes() {
{
mgmt.GET("/usage", s.mgmt.GetUsageStatistics)
mgmt.GET("/config", s.mgmt.GetConfig)
mgmt.GET("/config.yaml", s.mgmt.GetConfigYAML)
mgmt.PUT("/config.yaml", s.mgmt.PutConfigYAML)
mgmt.GET("/config.yaml", s.mgmt.GetConfigFile)
mgmt.GET("/debug", s.mgmt.GetDebug)
mgmt.PUT("/debug", s.mgmt.PutDebug)
@@ -503,11 +503,6 @@ func (s *Server) registerManagementRoutes() {
mgmt.PATCH("/api-keys", s.mgmt.PatchAPIKeys)
mgmt.DELETE("/api-keys", s.mgmt.DeleteAPIKeys)
mgmt.GET("/generative-language-api-key", s.mgmt.GetGlKeys)
mgmt.PUT("/generative-language-api-key", s.mgmt.PutGlKeys)
mgmt.PATCH("/generative-language-api-key", s.mgmt.PatchGlKeys)
mgmt.DELETE("/generative-language-api-key", s.mgmt.DeleteGlKeys)
mgmt.GET("/gemini-api-key", s.mgmt.GetGeminiKeys)
mgmt.PUT("/gemini-api-key", s.mgmt.PutGeminiKeys)
mgmt.PATCH("/gemini-api-key", s.mgmt.PatchGeminiKey)

View File

@@ -38,7 +38,13 @@ func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
timestamp := entry.Time.Format("2006-01-02 15:04:05")
message := strings.TrimRight(entry.Message, "\r\n")
formatted := fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, filepath.Base(entry.Caller.File), entry.Caller.Line, message)
var formatted string
if entry.Caller != nil {
formatted = fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, filepath.Base(entry.Caller.File), entry.Caller.Line, message)
} else {
formatted = fmt.Sprintf("[%s] [%s] %s\n", timestamp, entry.Level, message)
}
buffer.WriteString(formatted)
return buffer.Bytes(), nil

View File

@@ -734,6 +734,8 @@ func modelName2Alias(modelName string) string {
return "gemini-claude-sonnet-4-5"
case "claude-sonnet-4-5-thinking":
return "gemini-claude-sonnet-4-5-thinking"
case "claude-opus-4-5-thinking":
return "gemini-claude-opus-4-5-thinking"
case "chat_20706", "chat_23310", "gemini-2.5-flash-thinking", "gemini-3-pro-low", "gemini-2.5-pro":
return ""
default:
@@ -753,6 +755,8 @@ func alias2ModelName(modelName string) string {
return "claude-sonnet-4-5"
case "gemini-claude-sonnet-4-5-thinking":
return "claude-sonnet-4-5-thinking"
case "gemini-claude-opus-4-5-thinking":
return "claude-opus-4-5-thinking"
default:
return modelName
}

View File

@@ -667,7 +667,7 @@ func cliPreviewFallbackOrder(model string) []string {
case "gemini-2.5-pro":
return []string{
// "gemini-2.5-pro-preview-05-06",
"gemini-2.5-pro-preview-06-05",
// "gemini-2.5-pro-preview-06-05",
}
case "gemini-2.5-flash":
return []string{