Thank you for your interest in contributing to CLASP! This document provides guidelines and instructions for contributing.
Please be respectful and constructive in all interactions. We welcome contributions from everyone.
- Go 1.22 or later
- Node.js 20+ (for npm package)
- Docker (optional, for container builds)
# Clone the repository
git clone https://github.com/jedarden/CLASP.git
cd CLASP
# Install Go dependencies
go mod download
# Build
go build ./cmd/clasp
# Run tests
go test -v ./...
# Run linter
golangci-lint runfeat/description- New featuresfix/description- Bug fixesdocs/description- Documentation updatesrefactor/description- Code refactoringtest/description- Test additions/updates
Follow Conventional Commits:
type(scope): description
[optional body]
[optional footer]
Types:
feat- New feature (triggers minor version bump)fix- Bug fix (triggers patch version bump)docs- Documentation onlystyle- Code style (formatting, etc.)refactor- Code refactoringtest- Adding/updating testschore- Maintenance tasks
Examples:
feat(proxy): add support for Gemini provider
fix(stream): resolve race condition in XML buffer
docs(readme): update installation instructions
CLASP is designed to make adding new providers straightforward. Follow these steps:
Create a new file in internal/provider/:
// internal/provider/newprovider.go
package provider
type NewProvider struct {
baseURL string
apiKey string
}
func NewNewProvider(baseURL string) *NewProvider {
return &NewProvider{baseURL: baseURL}
}
func NewNewProviderWithKey(baseURL, apiKey string) *NewProvider {
return &NewProvider{baseURL: baseURL, apiKey: apiKey}
}
func (p *NewProvider) Name() string {
return "newprovider"
}
func (p *NewProvider) GetEndpointURL() string {
return p.baseURL + "/v1/chat/completions"
}
func (p *NewProvider) GetHeaders(apiKey string) http.Header {
key := apiKey
if p.apiKey != "" {
key = p.apiKey
}
return http.Header{
"Content-Type": []string{"application/json"},
"Authorization": []string{"Bearer " + key},
}
}
func (p *NewProvider) RequiresTransformation() bool {
return true // Set to false if API is Anthropic-compatible
}
func (p *NewProvider) TransformModelID(model string) string {
return model // Transform if needed
}Add to internal/config/config.go:
const (
// ... existing providers
ProviderNewProvider ProviderType = "newprovider"
)Update internal/proxy/handler.go:
case config.ProviderNewProvider:
return provider.NewNewProvider(cfg.NewProviderBaseURL), nilUpdate internal/config/config.go with any provider-specific config:
type Config struct {
// ... existing fields
NewProviderBaseURL string
NewProviderAPIKey string
}if url := os.Getenv("NEWPROVIDER_BASE_URL"); url != "" {
cfg.NewProviderBaseURL = url
}Create internal/provider/newprovider_test.go:
func TestNewProvider_Name(t *testing.T) {
p := NewNewProvider("https://api.example.com")
if p.Name() != "newprovider" {
t.Errorf("expected 'newprovider', got %s", p.Name())
}
}- Add provider to README.md
- Document any special configuration requirements
When adding support for a new API format:
- Study the target API's request format
- Implement translation in
internal/translator/request.go - Handle all content block types (text, images, tool use)
- Preserve tool definitions and tool choice settings
- Handle both streaming and non-streaming responses
- Map finish/stop reasons correctly
- Preserve usage/token information
- Handle error responses gracefully
CLASP translates OpenAI-style SSE to Anthropic-style SSE:
OpenAI: data: {"choices":[{"delta":{"content":"Hi"}}]}
↓
Anthropic: event: content_block_delta
data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Hi"}}
# All tests
go test -v ./...
# With race detection
go test -race ./...
# With coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Specific package
go test -v ./internal/translator/...- Use table-driven tests where appropriate
- Include edge cases and error scenarios
- Mock external dependencies
func TestFunction(t *testing.T) {
tests := []struct {
name string
input string
expected string
wantErr bool
}{
{"valid input", "test", "TEST", false},
{"empty input", "", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Function(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("unexpected error: %v", err)
}
if result != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, result)
}
})
}
}- Fork and Branch: Create a feature branch from
main - Implement: Make your changes following the guidelines
- Test: Ensure all tests pass and add new tests
- Lint: Run
golangci-lint runand fix any issues - Document: Update documentation if needed
- PR: Open a pull request with a clear description
- Tests pass locally (
go test ./...) - Linter passes (
golangci-lint run) - Code follows project style
- Documentation updated (if applicable)
- Commit messages follow conventional commits
- No secrets or sensitive data committed
Releases are automated via GitHub Actions:
- Push to
maintriggers version bump based on commit messages - Binaries are built for all platforms
- GitHub Release is created
- npm package is published
- Docker image is pushed to GHCR
- Open an issue for bugs or feature requests
- Use discussions for questions
- Check existing issues before creating new ones
By contributing, you agree that your contributions will be licensed under the MIT License.