Skip to content
Closed
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
go-version: '1.24'

- name: golangci-lint
uses: golangci/golangci-lint-action@v4
uses: golangci/golangci-lint-action@v8
with:
version: latest
version: v2.5.0
args: --timeout=5m
45 changes: 6 additions & 39 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,48 +1,15 @@
# golangci-lint v2 configuration
version: "2"

run:
timeout: 5m
tests: true
tests: false # Don't analyze test files

linters:
default: none # Start with no linters, only enable specific ones
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports

linters-settings:
errcheck:
check-blank: false
exclude-functions:
- fmt.Fprintf
- fmt.Fprintln
- (io.Closer).Close
- (*net/http.ResponseWriter).Write
- (*bufio.Writer).Flush
ignore: "fmt:.*,io:EOF"
gofmt:
simplify: true

issues:
exclude-use-default: true
exclude-rules:
# Exclude error checks in test files
- path: _test\.go
linters:
- errcheck
# Exclude common patterns that are safe to ignore
- text: "Error return value of.*os\\.(Setenv|Unsetenv).*is not checked"
linters:
- errcheck
- text: "Error return value of.*json\\.Marshal.*is not checked"
linters:
- errcheck
- text: "Error return value of.*w\\.Flush.*is not checked"
linters:
- errcheck
- text: "Error return value of.*resp\\.Body\\.Close.*is not checked"
linters:
- errcheck
- unused
2 changes: 1 addition & 1 deletion cmd/claude-code-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Flags:

Configuration:
Config file locations (checked in order):
1. ./.env
1. ./.env
2. ~/.claude/proxy.env
3. ~/.claude-code-proxy

Expand Down
5 changes: 5 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// Package config handles configuration loading from environment variables and .env files.
//
// It supports multiple config file locations (./.env, ~/.claude/proxy.env, ~/.claude-code-proxy)
// and detects the provider type (OpenRouter, OpenAI, Ollama) based on the OPENAI_BASE_URL.
// The package also handles model overrides for routing Claude model names to alternative providers.
package config

import (
Expand Down
41 changes: 30 additions & 11 deletions internal/converter/converter.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// Package converter handles bidirectional conversion between Claude and OpenAI API formats.
//
// It provides functions to convert Claude API requests to OpenAI-compatible format and
// OpenAI responses back to Claude format. This includes mapping models, converting message
// structures, handling tool calls, and extracting thinking blocks from reasoning responses.
package converter

import (
Expand All @@ -20,7 +25,9 @@ const (
DefaultHaikuModel = "gpt-5-mini"
)

// extractSystemText extracts system text from either string or array format
// extractSystemText extracts system text from Claude's flexible system parameter.
// Claude supports both string format ("system": "text") and array format with content blocks.
// This function normalizes both formats to a single string for OpenAI compatibility.
func extractSystemText(system interface{}) string {
if system == nil {
return ""
Expand Down Expand Up @@ -100,7 +107,9 @@ func ConvertRequest(claudeReq models.ClaudeRequest, cfg *config.Config) (*models

switch provider {
case config.ProviderOpenRouter:
// OpenRouter-specific format
// OpenRouter needs reasoning blocks and usage tracking enabled
// - reasoning.enabled: Enables thinking blocks in response
// - usage.include: Tracks token usage even in streaming mode
openaiReq.StreamOptions = map[string]interface{}{
"include_usage": true,
}
Expand All @@ -112,19 +121,17 @@ func ConvertRequest(claudeReq models.ClaudeRequest, cfg *config.Config) (*models
}

case config.ProviderOpenAI:
// OpenAI supports stream_options and reasoning (GPT-5 models)
// OpenAI GPT-5 models support reasoning_effort parameter
// This controls how much time the model spends thinking before responding
openaiReq.StreamOptions = map[string]interface{}{
"include_usage": true,
}
// GPT-5 models: Use Chat Completions reasoning_effort parameter
openaiReq.ReasoningEffort = "medium" // minimal | low | medium | high

case config.ProviderOllama:
// Force Ollama to use tools when they're provided
// Check claudeReq.Tools since openaiReq.Tools hasn't been set yet
// Ollama needs explicit tool_choice when tools are present
// Without this, Ollama models may not naturally choose to use tools
if len(claudeReq.Tools) > 0 {
// Set tool_choice to "required" to force tool usage
// This helps with models that don't naturally choose to use tools
openaiReq.ToolChoice = "required"
}
}
Expand Down Expand Up @@ -153,7 +160,10 @@ func ConvertRequest(claudeReq models.ClaudeRequest, cfg *config.Config) (*models
return openaiReq, nil
}

// mapModel implements pattern-based model routing
// mapModel maps Claude model names to provider-specific models using pattern matching.
// It routes haiku/sonnet/opus tiers to appropriate models (gpt-5-mini, gpt-5, etc.)
// and allows environment variable overrides for routing to alternative providers like
// Grok, Gemini, or DeepSeek. Non-Claude model names are passed through unchanged.
func mapModel(claudeModel string, cfg *config.Config) string {
modelLower := strings.ToLower(claudeModel)

Expand Down Expand Up @@ -185,7 +195,15 @@ func mapModel(claudeModel string, cfg *config.Config) string {
return claudeModel
}

// convertMessages converts Claude messages to OpenAI format
// convertMessages converts Claude messages to OpenAI format.
//
// Handles three content types:
// - String content: Simple text messages
// - Array content with blocks: text, tool_use (mapped to tool_calls), and tool_result (mapped to role=tool)
// - Tool results: Special handling to create OpenAI tool response messages
//
// The function maintains the conversation flow while translating Claude's content block
// structure to OpenAI's message format, ensuring tool call IDs are preserved for correlation.
func convertMessages(claudeMessages []models.ClaudeMessage, system string) []models.OpenAIMessage {
openaiMessages := []models.OpenAIMessage{}

Expand Down Expand Up @@ -314,7 +332,8 @@ func convertMessages(claudeMessages []models.ClaudeMessage, system string) []mod
return openaiMessages
}

// convertTools converts Claude tools to OpenAI format
// convertTools converts Claude tool definitions to OpenAI function calling format.
// Maps tool name, description, and input_schema to OpenAI's function structure.
func convertTools(claudeTools []models.Tool) []models.OpenAITool {
openaiTools := make([]models.OpenAITool, len(claudeTools))

Expand Down
9 changes: 7 additions & 2 deletions internal/daemon/daemon.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// Package daemon handles background process management for the proxy server.
//
// It manages PID file creation/deletion, process health checks, and provides functions
// to start, stop, and check the status of the proxy daemon. The daemon runs in the
// background and can be controlled via the CLI (start, stop, status commands).
package daemon

import (
Expand All @@ -18,7 +23,7 @@ func IsRunning() bool {
// Try health check first
resp, err := http.Get(healthURL)
if err == nil {
resp.Body.Close()
_ = resp.Body.Close()
return resp.StatusCode == 200
}

Expand Down Expand Up @@ -100,7 +105,7 @@ func readPID() (int, error) {
}

func cleanupPID() {
os.Remove(pidFile)
_ = os.Remove(pidFile) // Ignore error - cleanup is best-effort
}

func isProcessRunning() bool {
Expand Down
Loading
Loading