Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This file provides guidance to Claude and other AI models and agents when working with the code in this repository.

## Project Overview

GOModel is a high-performance AI gateway written in Go that routes requests to multiple LLM providers (OpenAI, Anthropic, Google Gemini). It's designed as a drop-in replacement for LiteLLM with superior concurrency and strict type safety.
GOModel is a high-performance AI gateway written in Go that routes requests to multiple LLM providers (OpenAI, Anthropic, Google Gemini). It is designed as a drop-in replacement for LiteLLM, offering superior quality, speed, concurrency, and strict type safety.

**Module name:** `gomodel`
**Go version:** 1.24.0
**GitHub link:** https://github.com/ENTERPILOT/GOModel
Comment thread
SantiagoDePolonia marked this conversation as resolved.
**Stage:** Development — not used in a real project, so backward compatibility is not a concern.

### Introductory Information

Please note that some of the information below may be slightly outdated. It is always best to verify the current project structure. These are guidelines only.

Feel free to suggest improvements to this file as you work with it.

## Common Development Commands

Expand Down Expand Up @@ -124,6 +132,22 @@ type Provider interface {
- Automatically creates provider configs from environment variables
- At least one provider API key is required

**Provider naming in config.yaml:**

Provider names (map keys) are arbitrary identifiers. Only the `type` field determines which provider implementation is used. You can use any name and have multiple providers of the same type:

```yaml
providers:
my-openai: # Any name works
type: "openai"
api_key: "${OPENAI_API_KEY}"

backup-openai: # Multiple providers of same type supported
type: "openai"
api_key: "${BACKUP_OPENAI_KEY}"
base_url: "https://custom.endpoint.com"
```

### Adding a New Provider

1. Create package in `internal/providers/{name}/`
Expand Down
2 changes: 1 addition & 1 deletion METRICS_CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ metrics:
endpoint: "/internal/prometheus" # Custom path

providers:
openai-primary:
openai:
type: "openai"
api_key: "${OPENAI_API_KEY}"
```
Expand Down
104 changes: 30 additions & 74 deletions cmd/gomodel/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,23 @@ package main

import (
"context"
"errors"
"flag"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"gomodel/config"
"gomodel/internal/auditlog"
"gomodel/internal/app"
"gomodel/internal/observability"
"gomodel/internal/providers"

// Import provider packages to trigger their init() registration
_ "gomodel/internal/providers/anthropic"
_ "gomodel/internal/providers/gemini"
_ "gomodel/internal/providers/groq"
_ "gomodel/internal/providers/openai"
_ "gomodel/internal/providers/xai"
"gomodel/internal/server"
"gomodel/internal/providers/anthropic"
"gomodel/internal/providers/gemini"
"gomodel/internal/providers/groq"
"gomodel/internal/providers/openai"
"gomodel/internal/providers/xai"
"gomodel/internal/version"
)

Expand Down Expand Up @@ -62,88 +57,49 @@ func main() {
os.Exit(1)
}

// Setup observability hooks for metrics collection (if enabled)
// This must be done BEFORE creating providers so they can use the hooks
// Create provider factory and register all providers explicitly
factory := providers.NewProviderFactory()

// Set observability hooks before registering providers
if cfg.Metrics.Enabled {
metricsHooks := observability.NewPrometheusHooks()
providers.SetGlobalHooks(metricsHooks)
slog.Info("prometheus metrics enabled", "endpoint", cfg.Metrics.Endpoint)
} else {
slog.Info("prometheus metrics disabled")
factory.SetHooks(observability.NewPrometheusHooks())
}

// Initialize provider infrastructure (cache, registry, router)
providerResult, err := providers.Init(context.Background(), cfg)
// Register all providers with the factory
factory.Register(openai.Registration)
factory.Register(anthropic.Registration)
factory.Register(gemini.Registration)
factory.Register(groq.Registration)
factory.Register(xai.Registration)

// Create the application
application, err := app.New(context.Background(), app.Config{
AppConfig: cfg,
Factory: factory,
})
if err != nil {
slog.Error("failed to initialize providers", "error", err)
slog.Error("failed to initialize application", "error", err)
os.Exit(1)
}
Comment on lines +76 to 83

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing defer for application.Shutdown. If app.New succeeds but app.Start fails or exits early (before the signal handler goroutine can catch a signal), the application resources (providers, cache, audit logger) will not be properly cleaned up. Add a defer statement after successful application creation to ensure cleanup happens in all code paths.

Copilot uses AI. Check for mistakes.
defer providerResult.Close()

// Security check: warn if no master key is configured
if cfg.Server.MasterKey == "" {
slog.Warn("SECURITY WARNING: GOMODEL_MASTER_KEY not set - server running in UNSAFE MODE",
"security_risk", "unauthenticated access allowed",
"recommendation", "set GOMODEL_MASTER_KEY environment variable to secure this gateway")
} else {
slog.Info("authentication enabled", "mode", "master_key")
}

// Initialize audit logging
auditResult, err := auditlog.New(context.Background(), cfg)
if err != nil {
slog.Error("failed to initialize audit logging", "error", err)
os.Exit(1)
}
defer auditResult.Close()

if cfg.Logging.Enabled {
slog.Info("audit logging enabled",
"storage_type", cfg.Logging.StorageType,
"log_bodies", cfg.Logging.LogBodies,
"log_headers", cfg.Logging.LogHeaders,
"retention_days", cfg.Logging.RetentionDays,
)
} else {
slog.Info("audit logging disabled")
}

// Create and start server
serverCfg := &server.Config{
MasterKey: cfg.Server.MasterKey,
MetricsEnabled: cfg.Metrics.Enabled,
MetricsEndpoint: cfg.Metrics.Endpoint,
BodySizeLimit: cfg.Server.BodySizeLimit,
AuditLogger: auditResult.Logger,
LogOnlyModelInteractions: cfg.Logging.OnlyModelInteractions,
}
srv := server.New(providerResult.Router, serverCfg)

// Handle graceful shutdown
go func() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

slog.Info("shutting down server...")

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := srv.Shutdown(ctx); err != nil {
slog.Error("server shutdown error", "error", err)
if err := application.Shutdown(ctx); err != nil {
slog.Error("application shutdown error", "error", err)
}
}()

// Start the server (blocking)
addr := ":" + cfg.Server.Port
slog.Info("starting server", "address", addr)

if err := srv.Start(addr); err != nil {
if errors.Is(err, http.ErrServerClosed) {
slog.Info("server stopped gracefully")
} else {
slog.Error("server failed to start", "error", err)
os.Exit(1)
}
if err := application.Start(addr); err != nil {
slog.Error("application failed", "error", err)
os.Exit(1)
}
}
Loading