Skip to content

Commit 81ecd6c

Browse files
feat: Enhance Gemini integration with smart API detection and model consistency
This release significantly improves the Gemini integration with automatic API detection, OAuth token refresh, project onboarding, and consistent model defaults across the codebase. Major Features: - Smart base URL detection: Auto-selects Cloud Code API for OAuth users and Standard Gemini API for API key users - Automatic OAuth token refresh with 5-minute buffer before expiry - Gemini project onboarding flow with polling and persistence - Racing provider support for concurrent model requests with fallback - Request/response format adaptation based on authentication type - Comprehensive metrics tracking and monitoring - File backup system for write operations Gemini Improvements: - Added getBaseURL() method for dynamic API selection - Added getEndpoint() method for correct endpoint formatting - Conditional request/response handling (Cloud Code wrapper vs direct format) - Exported ProjectIDRequiredError with IsProjectIDRequired() helper - Enhanced logging to show which API is being used - Project ID persistence to config file to avoid re-onboarding - Added Content.Role field for proper API compatibility Model Consistency: - Standardized default model to gemini-2.0-flash-exp across all locations - Updated wizard prompts and defaults - Updated mergeWithExistingConfig() fallback - Updated generateYAML() fallback - Added gemini-2.0-flash-exp to provider model list - Updated GetDefaultModel() return value Configuration: - Changed base_url default from hardcoded to empty string for auto-detection - Enhanced wizard with Gemini OAuth onboarding integration - Improved config merging for existing configurations - Added proper OAuth credential handling API Enhancements: - Added CloudCodeRequestWrapper and CloudCodeResponseWrapper types - Implemented LoadCodeAssistRequest/Response types - Added OnboardUserRequest and LongRunningOperationResponse types - Enhanced error handling with proper error type exports - Racing provider with grace period and concurrent model support - Model selector for intelligent model routing Infrastructure: - Added metrics server with Prometheus-style endpoints - Request/response tracking and statistics - File backup system for safe write operations - Enhanced router with racing and fallback support - Improved logging throughout the codebase Cleanup: - Removed test-gemini development command - Cleaned up temporary test artifacts - Removed legacy_router.go (replaced by enhanced router) Breaking Changes: None - all changes maintain backward compatibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 50f1618 commit 81ecd6c

26 files changed

+3402
-441
lines changed

cmd/server.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/cecil-the-coder/mcp-code-api/internal/config"
1111
"github.com/cecil-the-coder/mcp-code-api/internal/logger"
1212
"github.com/cecil-the-coder/mcp-code-api/internal/mcp"
13+
"github.com/cecil-the-coder/mcp-code-api/internal/metrics"
1314
"github.com/spf13/cobra"
1415
"github.com/spf13/viper"
1516
)
@@ -49,6 +50,16 @@ The server will:
4950
// Load configuration
5051
cfg := config.Load()
5152

53+
// Apply logging configuration from config file
54+
logger.SetDebug(cfg.Logging.Debug)
55+
logger.SetVerbose(cfg.Logging.Verbose)
56+
logger.Debugf("Debug logging enabled: %v", cfg.Logging.Debug)
57+
logger.Debugf("Verbose logging enabled: %v", cfg.Logging.Verbose)
58+
59+
// Log config details now that debug/verbose are enabled
60+
logger.Debugf("Preferred provider order: %v", cfg.Providers.Order)
61+
logger.Debugf("Enabled providers: %v", cfg.Providers.Enabled)
62+
5263
// Check API keys availability (log to file only, not stderr)
5364
if cfg.Providers.Cerebras == nil || cfg.Providers.Cerebras.APIKey == "" {
5465
logger.Info("No Cerebras API key found")
@@ -64,7 +75,8 @@ The server will:
6475

6576
cerebrasAvail := cfg.Providers.Cerebras != nil && cfg.Providers.Cerebras.APIKey != ""
6677
openrouterAvail := cfg.Providers.OpenRouter != nil && cfg.Providers.OpenRouter.APIKey != ""
67-
if !cerebrasAvail && !openrouterAvail {
78+
geminiAvail := cfg.Providers.Gemini != nil && (cfg.Providers.Gemini.APIKey != "" || cfg.Providers.Gemini.AccessToken != "")
79+
if !cerebrasAvail && !openrouterAvail && !geminiAvail {
6880
logger.Error("No API keys available")
6981
return fmt.Errorf("no API keys configured")
7082
}
@@ -88,6 +100,39 @@ The server will:
88100
// Start the MCP server
89101
server := mcp.NewServer(cfg)
90102
logger.Info("MCP Server starting...")
103+
104+
// Create shared metrics store
105+
metricsStore, err := metrics.NewSharedMetricsStore()
106+
if err != nil {
107+
logger.Warnf("Failed to create shared metrics store: %v", err)
108+
} else {
109+
// Start periodic metrics updates
110+
metricsStore.Start(server.GetRouter())
111+
defer metricsStore.Stop()
112+
}
113+
114+
// Start metrics server if enabled
115+
var metricsServer *metrics.MetricsServer
116+
if cfg.Metrics.Enabled && metricsStore != nil {
117+
port := cfg.Metrics.Port
118+
if viper.IsSet("metrics_port") && viper.GetInt("metrics_port") != 0 {
119+
port = viper.GetInt("metrics_port")
120+
}
121+
122+
metricsServer = metrics.NewMetricsServer(metricsStore, cfg.Metrics.Host, port)
123+
if err := metricsServer.Start(); err != nil {
124+
logger.Warnf("Failed to start metrics server: %v", err)
125+
} else {
126+
logger.Infof("Metrics server started on http://%s:%d", cfg.Metrics.Host, port)
127+
defer func() {
128+
logger.Info("Shutting down metrics server...")
129+
if err := metricsServer.Stop(); err != nil {
130+
logger.Warnf("Error stopping metrics server: %v", err)
131+
}
132+
}()
133+
}
134+
}
135+
91136
if err := server.Start(ctx); err != nil {
92137
return fmt.Errorf("failed to start MCP server: %w", err)
93138
}
@@ -104,6 +149,9 @@ func init() {
104149
serverCmd.Flags().String("log-file", "", "path to log file")
105150
_ = viper.BindPFlag("log_file", serverCmd.Flags().Lookup("log-file"))
106151

152+
serverCmd.Flags().Int("metrics-port", 0, "port for metrics HTTP server (0 = use config default)")
153+
_ = viper.BindPFlag("metrics_port", serverCmd.Flags().Lookup("metrics-port"))
154+
107155
// Add usage examples
108156
serverCmd.SetUsageTemplate(serverCmd.UsageTemplate() + `
109157
Examples:
@@ -116,8 +164,11 @@ Examples:
116164
# Start server with custom log file
117165
mcp-code-api server --log-file /tmp/mcp.log
118166
167+
# Start server with custom metrics port
168+
mcp-code-api server --metrics-port 9090
169+
119170
# Set API keys via environment variables
120171
CEREBRAS_API_KEY=your_key mcp-code-api server
121172
OPENROUTER_API_KEY=your_key mcp-code-api server
122173
`)
123-
}
174+
}

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
module github.com/cecil-the-coder/mcp-code-api
22

3-
go 1.21
3+
go 1.24.0
44

55
require (
66
github.com/fatih/color v1.18.0
7+
github.com/mitchellh/mapstructure v1.5.0
78
github.com/spf13/cobra v1.8.0
89
github.com/spf13/viper v1.17.0
910
gopkg.in/yaml.v2 v2.4.0
11+
gopkg.in/yaml.v3 v3.0.1
1012
)
1113

1214
require (
@@ -16,7 +18,6 @@ require (
1618
github.com/magiconair/properties v1.8.7 // indirect
1719
github.com/mattn/go-colorable v0.1.13 // indirect
1820
github.com/mattn/go-isatty v0.0.20 // indirect
19-
github.com/mitchellh/mapstructure v1.5.0 // indirect
2021
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
2122
github.com/sagikazarmark/locafero v0.4.0 // indirect
2223
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
@@ -28,8 +29,8 @@ require (
2829
go.uber.org/atomic v1.9.0 // indirect
2930
go.uber.org/multierr v1.9.0 // indirect
3031
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
32+
golang.org/x/oauth2 v0.33.0 // indirect
3133
golang.org/x/sys v0.25.0 // indirect
3234
golang.org/x/text v0.14.0 // indirect
3335
gopkg.in/ini.v1 v1.67.0 // indirect
34-
gopkg.in/yaml.v3 v3.0.1 // indirect
3536
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
6868
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
6969
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
7070
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
71+
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
72+
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
7173
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7274
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7375
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=

internal/api/anthropic.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212
"time"
1313

14+
"github.com/cecil-the-coder/mcp-code-api/internal/api/types"
1415
"github.com/cecil-the-coder/mcp-code-api/internal/config"
1516
"github.com/cecil-the-coder/mcp-code-api/internal/logger"
1617
"github.com/cecil-the-coder/mcp-code-api/internal/utils"
@@ -21,6 +22,7 @@ type AnthropicClient struct {
2122
config config.AnthropicConfig
2223
client *http.Client
2324
keyManager *APIKeyManager
25+
lastUsage *types.Usage // Store last token usage
2426
}
2527

2628
// NewAnthropicClient creates a new Anthropic client
@@ -44,9 +46,9 @@ func NewAnthropicClient(cfg config.AnthropicConfig) *AnthropicClient {
4446
}
4547

4648
// GenerateCode generates code using the Anthropic API with automatic failover
47-
func (c *AnthropicClient) GenerateCode(ctx context.Context, prompt, contextStr, outputFile string, language *string, contextFiles []string) (string, error) {
49+
func (c *AnthropicClient) GenerateCode(ctx context.Context, prompt, contextStr, outputFile string, language *string, contextFiles []string) (*types.CodeGenerationResult, error) {
4850
if c.keyManager == nil {
49-
return "", fmt.Errorf("no Anthropic API key configured")
51+
return nil, fmt.Errorf("no Anthropic API key configured")
5052
}
5153

5254
// Determine language from file extension or explicit parameter
@@ -59,13 +61,22 @@ func (c *AnthropicClient) GenerateCode(ctx context.Context, prompt, contextStr,
5961
requestData := c.prepareRequest(fullPrompt, detectedLanguage)
6062

6163
// Use failover to try multiple API keys if needed
62-
return c.keyManager.ExecuteWithFailover(func(apiKey string) (string, error) {
64+
code, err := c.keyManager.ExecuteWithFailover(func(apiKey string) (string, error) {
6365
// Make the API call with this specific key
6466
response, err := c.makeAPICallWithKey(ctx, requestData, apiKey)
6567
if err != nil {
6668
return "", err
6769
}
6870

71+
// Store usage information
72+
c.lastUsage = &types.Usage{
73+
PromptTokens: response.Usage.InputTokens,
74+
CompletionTokens: response.Usage.OutputTokens,
75+
TotalTokens: response.Usage.InputTokens + response.Usage.OutputTokens,
76+
}
77+
logger.Debugf("Anthropic: Extracted token usage - Prompt: %d, Completion: %d, Total: %d",
78+
c.lastUsage.PromptTokens, c.lastUsage.CompletionTokens, c.lastUsage.TotalTokens)
79+
6980
// Extract and clean the content
7081
if len(response.Content) == 0 {
7182
return "", fmt.Errorf("no content in API response")
@@ -75,6 +86,22 @@ func (c *AnthropicClient) GenerateCode(ctx context.Context, prompt, contextStr,
7586

7687
return cleanedContent, nil
7788
})
89+
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
// Return result with usage information
95+
result := &types.CodeGenerationResult{
96+
Code: code,
97+
Usage: c.lastUsage,
98+
}
99+
if result.Usage != nil {
100+
logger.Debugf("Anthropic: Returning result with usage - Total tokens: %d", result.Usage.TotalTokens)
101+
} else {
102+
logger.Warnf("Anthropic: Returning result with nil usage")
103+
}
104+
return result, nil
78105
}
79106

80107
// buildFullPrompt builds the complete prompt including context and existing content

internal/api/apikey_manager.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,28 @@ func NewAPIKeyManager(providerName string, keys []string) *APIKeyManager {
5252
return manager
5353
}
5454

55+
// GetCurrentKey returns the first available API key without advancing the round-robin counter
56+
// This is useful for queries that don't need load balancing (e.g., rate limit checks)
57+
func (m *APIKeyManager) GetCurrentKey() string {
58+
if len(m.keys) == 0 {
59+
return ""
60+
}
61+
62+
// Try to find the first available key
63+
for _, key := range m.keys {
64+
m.mu.RLock()
65+
health := m.keyHealth[key]
66+
m.mu.RUnlock()
67+
68+
if m.isKeyAvailable(key, health) {
69+
return key
70+
}
71+
}
72+
73+
// If all keys are in backoff, return the first key anyway
74+
return m.keys[0]
75+
}
76+
5577
// GetNextKey returns the next available API key using round-robin load balancing
5678
// It skips keys that are currently in backoff due to failures
5779
func (m *APIKeyManager) GetNextKey() (string, error) {

internal/api/auth/gemini.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ package auth
33
import (
44
"context"
55
"fmt"
6+
7+
"github.com/cecil-the-coder/mcp-code-api/internal/oauth"
8+
)
9+
10+
// Official Google OAuth credentials from llxprt-code project
11+
// Source: https://github.com/google/llxprt-code
12+
var (
13+
GeminiOAuthClientID = oauth.GeminiOAuth.ClientID
14+
GeminiOAuthClientSecret = oauth.GeminiOAuth.ClientSecret
615
)
716

817
// GeminiOAuthConfig returns the OAuth configuration for Google Gemini

0 commit comments

Comments
 (0)