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
11 changes: 11 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
run:
timeout: 5m

linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build run clean tidy test
.PHONY: build run clean tidy test lint lint-fix

# Build the application
build:
Expand All @@ -20,3 +20,11 @@ tidy:
test:
go test ./... -v

# Run linter
lint:
golangci-lint run ./...

# Run linter with auto-fix
lint-fix:
golangci-lint run --fix ./...

44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,50 @@ A high-performance LLM gateway written in Go.
-d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Hello!"}]}'
```

## Development

### Linting

This project uses [golangci-lint](https://golangci-lint.run/) for code quality checks.

#### Installation

**macOS:**

```bash
brew install golangci-lint
```

**Linux:**

```bash
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
```

**Windows:**

```bash
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
```

For other installation methods, see the [official documentation](https://golangci-lint.run/welcome/install/).

#### Usage

Run the linter:

```bash
make lint
```

Run the linter with auto-fix:

```bash
make lint-fix
```

The linter configuration is defined in `.golangci.yml` and includes essential checks for code quality and correctness.

## Running with Docker

You can use the official `golang:1.21-alpine` image to run the project in a container:
Expand Down
1 change: 1 addition & 0 deletions cmd/gomodel/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package main is the entry point for the LLM gateway server.
package main

import (
Expand Down
3 changes: 2 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package config provides configuration management for the application.
package config

import (
Expand Down Expand Up @@ -33,7 +34,7 @@ func Load() (*Config, error) {
viper.SetDefault("server.port", "8088")

// Read config file (optional, won't fail if not found)
_ = viper.ReadInConfig()
_ = viper.ReadInConfig() //nolint:errcheck

// Environment variables override config file
viper.AutomaticEnv()
Expand Down
2 changes: 2 additions & 0 deletions config/failover.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#failover:
# "gpt-5": [gemini-1.5-pro, gemini2.5-pro]
1 change: 1 addition & 0 deletions internal/core/interfaces.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package core defines the core interfaces and types for the LLM gateway.
package core

import (
Expand Down
10 changes: 5 additions & 5 deletions internal/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package core

// ChatRequest represents the incoming chat completion request
type ChatRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Temperature *float64 `json:"temperature,omitempty"`
MaxTokens *int `json:"max_tokens,omitempty"`
Model string `json:"model"`
Messages []Message `json:"messages"`
Stream bool `json:"stream,omitempty"`
}

Expand All @@ -19,17 +19,17 @@ type Message struct {
type ChatResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []Choice `json:"choices"`
Usage Usage `json:"usage"`
Created int64 `json:"created"`
}

// Choice represents a single completion choice
type Choice struct {
Index int `json:"index"`
Message Message `json:"message"`
FinishReason string `json:"finish_reason"`
Index int `json:"index"`
}

// Usage represents token usage information
Expand All @@ -43,8 +43,8 @@ type Usage struct {
type Model struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
OwnedBy string `json:"owned_by"`
Created int64 `json:"created"`
}

// ModelsResponse represents the response from the /v1/models endpoint
Expand Down
18 changes: 13 additions & 5 deletions internal/providers/openai/openai.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package openai provides OpenAI API integration for the LLM gateway.
package openai

import (
Expand All @@ -18,9 +19,9 @@ const (

// Provider implements the core.Provider interface for OpenAI
type Provider struct {
httpClient *http.Client
apiKey string
baseURL string
httpClient *http.Client
}

// New creates a new OpenAI provider
Expand Down Expand Up @@ -61,7 +62,9 @@ func (p *Provider) ChatCompletion(ctx context.Context, req *core.ChatRequest) (*
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close() //nolint:errcheck
}()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
Expand Down Expand Up @@ -103,8 +106,11 @@ func (p *Provider) StreamChatCompletion(ctx context.Context, req *core.ChatReque
}

if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
respBody = []byte("failed to read error response")
}
_ = resp.Body.Close() //nolint:errcheck
return nil, fmt.Errorf("OpenAI API error (status %d): %s", resp.StatusCode, string(respBody))
}

Expand All @@ -124,7 +130,9 @@ func (p *Provider) ListModels(ctx context.Context) (*core.ModelsResponse, error)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
defer func() {
_ = resp.Body.Close() //nolint:errcheck
}()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
Expand Down
10 changes: 8 additions & 2 deletions internal/server/handlers.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package server provides HTTP handlers and server setup for the LLM gateway.
package server

import (
Expand Down Expand Up @@ -42,14 +43,19 @@ func (h *Handler) ChatCompletion(c echo.Context) error {
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
defer stream.Close()
defer func() {
_ = stream.Close() //nolint:errcheck
}()

c.Response().Header().Set("Content-Type", "text/event-stream")
c.Response().Header().Set("Cache-Control", "no-cache")
c.Response().Header().Set("Connection", "keep-alive")
c.Response().WriteHeader(http.StatusOK)

io.Copy(c.Response().Writer, stream)
if _, err := io.Copy(c.Response().Writer, stream); err != nil {
// Can't return error after headers are sent, log it

Copilot AI Dec 6, 2025

Copy link

Choose a reason for hiding this comment

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

The comment indicates that the error should be logged, but the code doesn't actually log it. Consider adding actual logging:

if _, err := io.Copy(c.Response().Writer, stream); err != nil {
    // Can't return error after headers are sent, log it
    // TODO: Add proper logging (e.g., log.Error(err))
    return nil
}

Or if logging isn't implemented yet, the comment should reflect the current behavior.

Suggested change
// Can't return error after headers are sent, log it
// Can't return error after headers are sent, so log it
c.Logger().Error(err)

Copilot uses AI. Check for mistakes.
return nil
}
return nil
}

Expand Down
4 changes: 2 additions & 2 deletions internal/server/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import (

// mockProvider implements core.Provider for testing
type mockProvider struct {
supportedModels []string
err error
response *core.ChatResponse
modelsResponse *core.ModelsResponse
streamData string
err error
supportedModels []string
}

func (m *mockProvider) Supports(model string) bool {
Expand Down
1 change: 0 additions & 1 deletion internal/server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,3 @@ func New(provider core.Provider) *Server {
func (s *Server) Start(addr string) error {
return s.echo.Start(addr)
}