Skip to content

Commit c1aacb3

Browse files
author
privapps
committed
Add model filtering, error handling, and config enhancements
Introduces support for filtering allowed models in API endpoints using the `allowed_models` configuration. Improves error handling by replacing string matching with structured error types. Updates logging to include model information when available. Refactors configuration validation for better modularity and adds tests for `allowed_models` behavior and proxy rejection of disallowed models. Enhances middleware to log request details and integrates support for `/v1/completions` endpoint. Removes unused dependencies from `go.mod` and adjusts Dockerfile to use a consistent command for starting the service.
1 parent acb3a25 commit c1aacb3

File tree

14 files changed

+464
-149
lines changed

14 files changed

+464
-149
lines changed

AGENTS.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
5+
Source code location:
6+
- `cmd/` — Application entry points
7+
- `internal/` — Core service modules (auth, config, API, middleware)
8+
- `pkg/` — Shared utilities/packages
9+
- `test/` — Integration and helper tests
10+
- `config.example.json`, `Dockerfile`, `docker-compose.yml` — Example/config files
11+
12+
## Build, Test, and Development Commands
13+
14+
Key commands (via Makefile):
15+
- `make build` — Build service binary
16+
- `make run` — Start proxy server locally
17+
- `make dev` — Hot reload (requires air)
18+
- `make test` — Unit tests
19+
- `make test-all` — All tests (unit + integration)
20+
- `make test-coverage` — Coverage report
21+
- `make lint` — Lint code (golangci-lint)
22+
- `make fmt` — Format code
23+
24+
Requires Go 1.23.0+
25+
26+
## Coding Style & Naming Conventions
27+
28+
- Indentation: tabs (Go standard)
29+
- Use camelCase or snake_case for names
30+
- Exported Go identifiers: PascalCase
31+
- Format code before PRs (`make fmt`), lint (`make lint`)
32+
33+
## Testing Guidelines
34+
35+
- Use Go `testing` package; name test files `_test.go`, test functions `TestXxx`
36+
- Unit tests: `internal/` and `pkg/`
37+
- Integration tests: `test/integration/`
38+
- Run: `make test-all`, `make test-coverage` (aim for >=45% coverage in core logic)
39+
40+
## Commit & Pull Request Guidelines
41+
42+
- Commit messages: short, present-tense (e.g., "Refactor code structure")
43+
- PRs: describe changes/reasoning, link issues, add screenshots for UI
44+
- Ensure all tests pass & code is formatted
45+
- Do not commit secrets or sensitive configs
46+
47+
## Security & Configuration Tips
48+
49+
- Store secrets in user-level config with permissions 0700
50+
- Never log sensitive data
51+
- Only use HTTPS for credentials/tokens
52+
- Do not push sensitive files; check `.gitignore`
53+
54+
---
55+
56+
For help, open an issue or see the README troubleshooting section.

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
4747
CMD wget --no-verbose --tries=1 --spider http://localhost:8081/health || exit 1
4848

4949
# Run the binary
50-
CMD ["./github-copilot-svcs", "run"]
50+
CMD ["./github-copilot-svcs", "start"]

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,19 @@ make security # Run security analysis
105105
make docker-build # Build Docker image
106106
make docker-run # Run Docker container
107107
```
108+
## Filtering Allowed Models
109+
110+
You can control which models are available by specifying `allowed_models` in your config file (`config.json`).
111+
112+
Example:
113+
```json
114+
{
115+
"allowed_models": ["gpt-4o", "claude-3.7-sonnet"]
116+
}
117+
118+
- If set, both CLI and REST /v1/models lists are filtered and show a note.
119+
- Proxy requests to /v1/chat/completions will only allow those models, rejecting others with HTTP 400.
120+
- If omitted or set to null, all models are permitted (default behavior).
108121

109122
## Building for Different OS/Architectures
110123

@@ -211,6 +224,20 @@ Content-Type: application/json
211224
}
212225
```
213226

227+
### Completions
228+
This endpoint is OpenAI-compatible and proxies requests to the upstream Copilot API `/completions` endpoint.
229+
230+
```bash
231+
POST http://localhost:8081/v1/completions
232+
Content-Type: application/json
233+
234+
{
235+
"model": "gpt-4",
236+
"prompt": "Write a hello world in Python",
237+
"max_tokens": 100
238+
}
239+
```
240+
214241
### Available Models
215242
```bash
216243
GET http://localhost:8081/v1/models

config.example.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"port": 8081,
3+
"allowed_models": null,
34
"headers": {
45
"user_agent": "GitHubCopilotChat/0.29.1",
56
"editor_version": "vscode/1.102.3",

go.mod

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,3 @@ module github.com/privapps/github-copilot-svcs
33
go 1.23.0
44

55
toolchain go1.23.5
6-
7-
require (
8-
github.com/beorn7/perks v1.0.1 // indirect
9-
github.com/cespare/xxhash/v2 v2.3.0 // indirect
10-
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
11-
golang.org/x/sys v0.33.0 // indirect
12-
google.golang.org/protobuf v1.36.6 // indirect
13-
)

go.sum

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +0,0 @@
1-
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
2-
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
3-
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
4-
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
5-
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
6-
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
7-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
8-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
9-
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
10-
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=

internal/cli.go

Lines changed: 89 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package internal
22

33
import (
4-
"encoding/json"
5-
"flag"
6-
"fmt"
7-
"os"
8-
"time"
9-
"strings"
4+
"encoding/json"
5+
"errors"
6+
"flag"
7+
"fmt"
8+
"os"
9+
"time"
10+
"github.com/privapps/github-copilot-svcs/pkg/transform"
1011
)
1112

1213
// Command constants to avoid goconst errors
@@ -112,14 +113,14 @@ func handleAuth() error {
112113
}
113114

114115
func handleStatusWithFormat(jsonOutput bool) error {
115-
cfg, err := LoadConfig()
116-
if err != nil {
117-
if strings.Contains(err.Error(), "either github_token or copilot_token must be provided") {
118-
fmt.Println("Not authenticated. Run 'auth' to authenticate.")
119-
return nil
120-
}
121-
return fmt.Errorf("failed to load config: %v", err)
122-
}
116+
cfg, err := LoadConfig()
117+
if err != nil {
118+
if errors.Is(err, ErrMissingTokens) {
119+
fmt.Println("Not authenticated. Run 'auth' to authenticate.")
120+
return nil
121+
}
122+
return fmt.Errorf("failed to load config: %v", err)
123+
}
123124

124125
if jsonOutput {
125126
return printStatusJSON(cfg)
@@ -213,14 +214,14 @@ func printStatusText(cfg *Config) error {
213214
}
214215

215216
func handleConfig() error {
216-
cfg, err := LoadConfig()
217-
if err != nil {
218-
if strings.Contains(err.Error(), "either github_token or copilot_token must be provided") {
219-
fmt.Println("Not authenticated. Run 'auth' to authenticate.")
220-
return nil
221-
}
222-
return fmt.Errorf("failed to load config: %v", err)
223-
}
217+
cfg, err := LoadConfig()
218+
if err != nil {
219+
if errors.Is(err, ErrMissingTokens) {
220+
fmt.Println("Not authenticated. Run 'auth' to authenticate.")
221+
return nil
222+
}
223+
return fmt.Errorf("failed to load config: %v", err)
224+
}
224225

225226
path, _ := GetConfigPath()
226227
fmt.Printf("Configuration file: %s\n", path)
@@ -248,20 +249,20 @@ func getCurrentTime() int64 {
248249
}
249250

250251
func handleRun() error {
251-
cfg, err := LoadConfig()
252-
if err != nil {
253-
if strings.Contains(err.Error(), "either github_token or copilot_token must be provided") {
254-
if authErr := handleAuth(); authErr != nil {
255-
return fmt.Errorf("authentication failed: %v", authErr)
256-
}
257-
cfg, err = LoadConfig()
258-
if err != nil {
259-
return fmt.Errorf("failed to load config after authentication: %v", err)
260-
}
261-
} else {
262-
return fmt.Errorf("failed to load config: %v", err)
263-
}
264-
}
252+
cfg, err := LoadConfig()
253+
if err != nil {
254+
if errors.Is(err, ErrMissingTokens) {
255+
if authErr := handleAuth(); authErr != nil {
256+
return fmt.Errorf("authentication failed: %v", authErr)
257+
}
258+
cfg, err = LoadConfig()
259+
if err != nil {
260+
return fmt.Errorf("failed to load config after authentication: %v", err)
261+
}
262+
} else {
263+
return fmt.Errorf("failed to load config: %v", err)
264+
}
265+
}
265266

266267
// Create HTTP client and auth service
267268
httpClient := CreateHTTPClient(cfg)
@@ -278,14 +279,14 @@ func handleRun() error {
278279
}
279280

280281
func handleModels() error {
281-
cfg, err := LoadConfig()
282-
if err != nil {
283-
if strings.Contains(err.Error(), "either github_token or copilot_token must be provided") {
284-
fmt.Println("Not authenticated. Run 'auth' to authenticate.")
285-
return nil
286-
}
287-
return fmt.Errorf("failed to load config: %v", err)
288-
}
282+
cfg, err := LoadConfig()
283+
if err != nil {
284+
if errors.Is(err, ErrMissingTokens) {
285+
fmt.Println("Not authenticated. Run 'auth' to authenticate.")
286+
return nil
287+
}
288+
return fmt.Errorf("failed to load config: %v", err)
289+
}
289290

290291
// Create HTTP client and auth service
291292
httpClient := CreateHTTPClient(cfg)
@@ -308,23 +309,52 @@ func handleModels() error {
308309
return nil
309310
}
310311

311-
fmt.Printf("Available models (%d total):\n", len(modelList.Data))
312-
for _, model := range modelList.Data {
313-
fmt.Printf(" - %s (%s)\n", model.ID, model.OwnedBy)
314-
}
315-
316-
return nil
317-
}
312+
filtered := modelList.Data
313+
var unknown []string
314+
filteredMsg := ""
315+
if len(cfg.AllowedModels) > 0 {
316+
allowedSet := make(map[string]struct{}, len(cfg.AllowedModels))
317+
for _, name := range cfg.AllowedModels {
318+
allowedSet[name] = struct{}{}
319+
}
320+
var tmp []transform.Model
321+
foundSet := make(map[string]struct{})
322+
for _, model := range filtered {
323+
if _, ok := allowedSet[model.ID]; ok {
324+
tmp = append(tmp, model)
325+
foundSet[model.ID] = struct{}{}
326+
}
327+
}
328+
for k := range allowedSet {
329+
if _, ok := foundSet[k]; !ok {
330+
unknown = append(unknown, k)
331+
}
332+
}
333+
filtered = tmp
334+
filteredMsg = "NOTE: The model list is filtered by allowed_models in config."
335+
if len(unknown) > 0 {
336+
fmt.Printf("WARNING: The following allowed_models were not found and are ignored: %v\n", unknown)
337+
}
338+
}
339+
fmt.Printf("Available models (%d shown):\n", len(filtered))
340+
for _, model := range filtered {
341+
fmt.Printf(" - %s (%s)\n", model.ID, model.OwnedBy)
342+
}
343+
if filteredMsg != "" {
344+
fmt.Println(filteredMsg)
345+
}
346+
return nil
347+
}
318348

319349
func handleRefresh() error {
320-
cfg, err := LoadConfig()
321-
if err != nil {
322-
if strings.Contains(err.Error(), "either github_token or copilot_token must be provided") {
323-
fmt.Println("Not authenticated. Run 'auth' to authenticate.")
324-
return nil
325-
}
326-
return fmt.Errorf("failed to load config: %v", err)
327-
}
350+
cfg, err := LoadConfig()
351+
if err != nil {
352+
if errors.Is(err, ErrMissingTokens) {
353+
fmt.Println("Not authenticated. Run 'auth' to authenticate.")
354+
return nil
355+
}
356+
return fmt.Errorf("failed to load config: %v", err)
357+
}
328358

329359
if cfg.CopilotToken == "" {
330360
return fmt.Errorf("no token to refresh - run 'auth' command first")

0 commit comments

Comments
 (0)