Skip to content

Commit ab19278

Browse files
author
Mehmet Aydin
committed
Initial commit for githubmodels.go
0 parents  commit ab19278

File tree

19 files changed

+821
-0
lines changed

19 files changed

+821
-0
lines changed

.github/workflows/go.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Go CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
build:
13+
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v3
19+
20+
- name: Set up Go
21+
uses: actions/setup-go@v4
22+
with:
23+
go-version: 1.21 # or your preferred Go version
24+
25+
- name: Install dependencies
26+
run: go mod tidy
27+
28+
- name: Build
29+
run: go build ./...
30+
31+
- name: Run tests
32+
run: go test -v ./...

.gitignore

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Binaries
2+
/bin/
3+
/*.exe
4+
/*.exe~
5+
/*.dll
6+
/*.so
7+
/*.dylib
8+
9+
# Build output
10+
/build/
11+
/out/
12+
13+
# Vendor directory (optional, if you use Go modules you may not need this)
14+
vendor/
15+
16+
# Editor files
17+
*.swp
18+
*.swo
19+
*.idea/
20+
*.vscode/
21+
*.DS_Store
22+
23+
# Test output
24+
*.test
25+
coverage.out
26+
27+
# Go dependency files (do NOT ignore go.mod or go.sum)
28+
# go.sum should be committed, go.mod too

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Tigillo
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# githubmodels-go
2+
3+
[![Go Reference](https://pkg.go.dev/badge/github.com/tigillo/githubmodels-go.svg)](https://pkg.go.dev/github.com/tigillo/githubmodels-go)
4+
5+
`githubmodels-go` is a Go client library for interacting with the [GitHub Models API](https://docs.github.com/en/rest/models), inspired by the OpenAI Go SDK (`openai-go`).
6+
It allows you to list models, perform chat/inference completions, and supports streaming responses using your `GITHUB_TOKEN` for authentication.
7+
8+
---
9+
10+
## Features
11+
12+
- List available models in the GitHub Models catalog
13+
- Create chat completions (like OpenAI’s `ChatCompletion`)
14+
- Optional streaming support for real-time responses
15+
- Supports organization-scoped endpoints
16+
- Easy-to-use Go client interface
17+
18+
---
19+
20+
## Installation
21+
22+
```bash
23+
go get github.com/tigillo/githubmodels-go
24+
```
25+
26+
## Usage
27+
### Initialize Client
28+
```
29+
package main
30+
31+
import (
32+
"context"
33+
"fmt"
34+
"os"
35+
36+
githubmodels "github.com/tigillo/githubmodels-go/client"
37+
)
38+
39+
func main() {
40+
token := os.Getenv("GITHUB_TOKEN")
41+
client := githubmodels.NewClient(token)
42+
43+
ctx := context.Background()
44+
45+
// Example: list models
46+
models, err := client.ListModels(ctx)
47+
if err != nil {
48+
panic(err)
49+
}
50+
51+
for _, m := range models {
52+
fmt.Println(m.ID, "-", m.Description)
53+
}
54+
}
55+
```
56+
57+
### Create Chat Completion
58+
```
59+
resp, err := client.ChatCompletion(ctx, githubmodels.ChatRequest{
60+
Model: "github/code-chat",
61+
Messages: []githubmodels.Message{
62+
{Role: "user", Content: "Write a Go function to reverse a string"},
63+
},
64+
})
65+
if err != nil {
66+
panic(err)
67+
}
68+
69+
fmt.Println(resp.Choices[0].Message.Content)
70+
```
71+
72+
## Environment Variables
73+
74+
The library uses the `GITHUB_TOKEN` environment variable by default for authentication.
75+
Ensure your token has the required scopes:
76+
77+
- `models:read` for catalog access
78+
- `models:execute` for inference/chat completions
79+
80+
## Contributing
81+
82+
Contributions are welcome! Feel free to:
83+
- Open issues for bugs or feature requests
84+
- Submit pull requests for enhancements or fixes
85+
- Add examples or tests
86+
87+

client/client.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package client
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"net/http"
9+
10+
"github.com/tigillo/githubmodels-go/models"
11+
)
12+
13+
// Client is the main GitHub Models API client
14+
type Client struct {
15+
token string
16+
Client *http.Client
17+
BaseURL string // exported so tests can override
18+
}
19+
20+
// NewClient creates a new GitHub Models API client
21+
func NewClient(token string) *Client {
22+
return &Client{
23+
token: token,
24+
Client: http.DefaultClient,
25+
BaseURL: "https://api.github.ai", // production default
26+
}
27+
}
28+
29+
// Model represents a GitHub Models API model
30+
type Model struct {
31+
ID string `json:"id"`
32+
Description string `json:"description"`
33+
}
34+
35+
// ListModels returns all available models from the catalog
36+
func (c *Client) ListModels(ctx context.Context) ([]Model, error) {
37+
url := fmt.Sprintf("%s/catalog/models", c.BaseURL)
38+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
39+
if err != nil {
40+
return nil, err
41+
}
42+
req.Header.Set("Authorization", "Bearer "+c.token)
43+
req.Header.Set("Accept", "application/vnd.github+json")
44+
45+
resp, err := c.Client.Do(req)
46+
if err != nil {
47+
return nil, err
48+
}
49+
defer resp.Body.Close()
50+
51+
if resp.StatusCode != http.StatusOK {
52+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
53+
}
54+
55+
var models []Model
56+
if err := json.NewDecoder(resp.Body).Decode(&models); err != nil {
57+
return nil, err
58+
}
59+
60+
return models, nil
61+
}
62+
63+
// ChatCompletion sends a chat completion request to GitHub Models API
64+
func (c *Client) ChatCompletion(ctx context.Context, reqData models.ChatRequest) (*models.ChatResponse, error) {
65+
url := fmt.Sprintf("%s/inference/chat/completions", c.BaseURL)
66+
67+
bodyBytes, err := json.Marshal(reqData)
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes))
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
req.Header.Set("Authorization", "Bearer "+c.token)
78+
req.Header.Set("Accept", "application/vnd.github+json")
79+
req.Header.Set("Content-Type", "application/json")
80+
81+
resp, err := c.Client.Do(req)
82+
if err != nil {
83+
return nil, err
84+
}
85+
defer resp.Body.Close()
86+
87+
if resp.StatusCode != http.StatusOK {
88+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
89+
}
90+
91+
var chatResp models.ChatResponse
92+
if err := json.NewDecoder(resp.Body).Decode(&chatResp); err != nil {
93+
return nil, err
94+
}
95+
96+
return &chatResp, nil
97+
}

client/client_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/tigillo/githubmodels-go/models"
11+
)
12+
13+
// Helper to create a test client pointing to a mock server
14+
func newTestClient(ts *httptest.Server) *Client {
15+
return &Client{
16+
token: "test-token",
17+
BaseURL: ts.URL,
18+
Client: ts.Client(),
19+
}
20+
}
21+
22+
func TestListModels(t *testing.T) {
23+
// Mock response
24+
mockModels := []models.Model{
25+
{ID: "github/code-chat", Name: "Code Chat", Description: "Chat with code model"},
26+
}
27+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28+
if r.URL.Path != "/catalog/models" {
29+
t.Errorf("unexpected path: %s", r.URL.Path)
30+
}
31+
if r.Header.Get("Authorization") != "Bearer test-token" {
32+
t.Errorf("missing or wrong Authorization header")
33+
}
34+
w.Header().Set("Content-Type", "application/json")
35+
json.NewEncoder(w).Encode(mockModels)
36+
}))
37+
defer ts.Close()
38+
39+
c := newTestClient(ts)
40+
ctx := context.Background()
41+
42+
modelsList, err := c.ListModels(ctx)
43+
if err != nil {
44+
t.Fatalf("ListModels failed: %v", err)
45+
}
46+
47+
if len(modelsList) != 1 {
48+
t.Fatalf("expected 1 model, got %d", len(modelsList))
49+
}
50+
if modelsList[0].ID != "github/code-chat" {
51+
t.Errorf("unexpected model ID: %s", modelsList[0].ID)
52+
}
53+
}
54+
55+
func TestChatCompletion(t *testing.T) {
56+
mockResponse := models.ChatResponse{
57+
ID: "chat-1",
58+
Choices: []models.Choice{
59+
{Message: models.Message{Role: "assistant", Content: "Hello!"}},
60+
},
61+
}
62+
63+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
64+
if r.URL.Path != "/inference/chat/completions" {
65+
t.Errorf("unexpected path: %s", r.URL.Path)
66+
}
67+
w.Header().Set("Content-Type", "application/json")
68+
json.NewEncoder(w).Encode(mockResponse)
69+
}))
70+
defer ts.Close()
71+
72+
c := newTestClient(ts)
73+
ctx := context.Background()
74+
75+
req := models.ChatRequest{
76+
Model: "github/code-chat",
77+
Messages: []models.Message{
78+
{Role: "user", Content: "Hello"},
79+
},
80+
}
81+
82+
resp, err := c.ChatCompletion(ctx, req)
83+
if err != nil {
84+
t.Fatalf("ChatCompletion failed: %v", err)
85+
}
86+
87+
if resp.ID != "chat-1" {
88+
t.Errorf("unexpected response ID: %s", resp.ID)
89+
}
90+
if len(resp.Choices) != 1 {
91+
t.Errorf("expected 1 choice, got %d", len(resp.Choices))
92+
}
93+
if resp.Choices[0].Message.Content != "Hello!" {
94+
t.Errorf("unexpected response content: %s", resp.Choices[0].Message.Content)
95+
}
96+
}

0 commit comments

Comments
 (0)