Skip to content
Open
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: 4 additions & 0 deletions .github/workflows/update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ jobs:
env:
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}

- name: Umans
run: go run ./cmd/umans/main.go
continue-on-error: true

- name: Create Pull Request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
Expand Down
148 changes: 148 additions & 0 deletions cmd/umans/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Package main provides a command-line tool to fetch models from Umans
// and generate a configuration file for the provider.
package main

import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"slices"
"strings"
"time"

"charm.land/catwalk/pkg/catwalk"
)

// UmansModel represents a model from the Umans models API.
type UmansModel struct {
ID string `json:"id"`
OwnedBy string `json:"owned_by"`
ContextLength int64 `json:"context_length"`
Pricing UmansPricing `json:"pricing,omitempty"`
}

// UmansPricing contains per-million-token pricing for a model.
type UmansPricing struct {
Input float64 `json:"input,omitempty"`
Output float64 `json:"output,omitempty"`
}

// ModelsResponse is the response structure for the Umans models API.
type ModelsResponse struct {
Data []UmansModel `json:"data"`
}

func fetchUmansModels(apiEndpoint string) (*ModelsResponse, error) {
client := &http.Client{Timeout: 30 * time.Second}
req, _ := http.NewRequestWithContext(context.Background(), "GET", apiEndpoint+"/models", nil)
req.Header.Set("User-Agent", "Crush-Client/1.0")

resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("fetching models: %w", err)
}
defer func() { _ = resp.Body.Close() }()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading models response: %w", err)
}

if resp.StatusCode != 200 {
return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body)
}

_ = os.MkdirAll("tmp", 0o700)
_ = os.WriteFile("tmp/umans-response.json", body, 0o600)

var mr ModelsResponse
if err := json.Unmarshal(body, &mr); err != nil {
return nil, fmt.Errorf("decoding models response: %w", err)
}

return &mr, nil
}

func modelDisplayName(id string) string {
name := strings.TrimPrefix(id, "umans-")
name = strings.ReplaceAll(name, "-", " ")
words := strings.Fields(name)
for i, w := range words {
upper := strings.ToUpper(w)
if upper == "GLM" || upper == "A3B" || upper == "35B" {
words[i] = upper
} else {
words[i] = strings.ToUpper(w[:1]) + w[1:]
}
}
return "Umans " + strings.Join(words, " ")
}

// This is used to generate the umans.json config file.
func main() {
umansProvider := catwalk.Provider{
Name: "Umans",
ID: "umans",
APIKey: "$UMANS_API_KEY",
APIEndpoint: "https://api.code.umans.ai/v1",
Type: catwalk.TypeOpenAICompat,
DefaultLargeModelID: "umans-coder",
DefaultSmallModelID: "umans-flash",
}

modelsResp, err := fetchUmansModels(umansProvider.APIEndpoint)
if err != nil {
log.Fatal("Error fetching Umans models:", err)
}

for _, model := range modelsResp.Data {
// Skip quantized variants
if strings.Contains(model.ID, "nvfp4") {
continue
}

var defaultMaxTokens int64
if model.ID == "umans-flash" {
defaultMaxTokens = 8192
} else {
defaultMaxTokens = 32768
}

canReason := !strings.Contains(model.ID, "flash")

m := catwalk.Model{
ID: model.ID,
Name: modelDisplayName(model.ID),
CostPer1MIn: model.Pricing.Input,
CostPer1MOut: model.Pricing.Output,
CostPer1MInCached: 0,
CostPer1MOutCached: 0,
ContextWindow: model.ContextLength,
DefaultMaxTokens: defaultMaxTokens,
CanReason: canReason,
SupportsImages: true,
}

umansProvider.Models = append(umansProvider.Models, m)
}

slices.SortFunc(umansProvider.Models, func(a catwalk.Model, b catwalk.Model) int {
return strings.Compare(a.ID, b.ID)
})

data, err := json.MarshalIndent(umansProvider, "", " ")
if err != nil {
log.Fatal("Error marshaling Umans provider:", err)
}
data = append(data, '\n')

if err := os.WriteFile("internal/providers/configs/umans.json", data, 0o600); err != nil {
log.Fatal("Error writing Umans provider config:", err)
}

fmt.Printf("Generated umans.json with %d models\n", len(umansProvider.Models))
}
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand All @@ -9,8 +11,12 @@ github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYy
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand All @@ -19,8 +25,11 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
Expand All @@ -35,12 +44,18 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
71 changes: 71 additions & 0 deletions internal/providers/configs/umans.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"name": "Umans",
"id": "umans",
"api_key": "$UMANS_API_KEY",
"api_endpoint": "https://api.code.umans.ai/v1",
"type": "openai-compat",
"default_large_model_id": "umans-coder",
"default_small_model_id": "umans-flash",
"models": [
{
"id": "umans-coder",
"name": "Umans Coder",
"cost_per_1m_in": 0.95,
"cost_per_1m_out": 4,
"cost_per_1m_in_cached": 0,
"cost_per_1m_out_cached": 0,
"context_window": 262144,
"default_max_tokens": 32768,
"can_reason": true,
"supports_attachments": true
},
{
"id": "umans-flash",
"name": "Umans Flash",
"cost_per_1m_in": 0.15,
"cost_per_1m_out": 1,
"cost_per_1m_in_cached": 0,
"cost_per_1m_out_cached": 0,
"context_window": 262144,
"default_max_tokens": 8192,
"can_reason": false,
"supports_attachments": true
},
{
"id": "umans-glm-5.2",
"name": "Umans GLM 5.2",
"cost_per_1m_in": 1.4,
"cost_per_1m_out": 4.4,
"cost_per_1m_in_cached": 0,
"cost_per_1m_out_cached": 0,
"context_window": 405504,
"default_max_tokens": 32768,
"can_reason": true,
"supports_attachments": true
},
{
"id": "umans-kimi-k2.7",
"name": "Umans Kimi K2.7",
"cost_per_1m_in": 0.95,
"cost_per_1m_out": 4,
"cost_per_1m_in_cached": 0,
"cost_per_1m_out_cached": 0,
"context_window": 262144,
"default_max_tokens": 32768,
"can_reason": true,
"supports_attachments": true
},
{
"id": "umans-qwen3.6-35b-a3b",
"name": "Umans Qwen3.6 35B A3B",
"cost_per_1m_in": 0.15,
"cost_per_1m_out": 1,
"cost_per_1m_in_cached": 0,
"cost_per_1m_out_cached": 0,
"context_window": 262144,
"default_max_tokens": 32768,
"can_reason": true,
"supports_attachments": true
}
]
}
8 changes: 8 additions & 0 deletions internal/providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ var zhipuConfig []byte
//go:embed configs/zhipu-coding.json
var zhipuCodingConfig []byte

//go:embed configs/umans.json
var umansConfig []byte

// ProviderFunc is a function that returns a Provider.
type ProviderFunc func() catwalk.Provider

Expand Down Expand Up @@ -172,6 +175,7 @@ var providerRegistry = []ProviderFunc{
vertexAIProvider,
zhipuProvider,
zhipuCodingProvider,
umansProvider,
}

// GetAll returns all registered providers.
Expand Down Expand Up @@ -347,3 +351,7 @@ func zhipuProvider() catwalk.Provider {
func zhipuCodingProvider() catwalk.Provider {
return loadProviderFromConfig(zhipuCodingConfig)
}

func umansProvider() catwalk.Provider {
return loadProviderFromConfig(umansConfig)
}
2 changes: 2 additions & 0 deletions pkg/catwalk/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const (
InferenceProviderFireworks InferenceProvider = "fireworks"
InferenceProviderBaseten InferenceProvider = "baseten"
InferenceProviderMoonshot InferenceProvider = "moonshot"
InferenceProviderUmans InferenceProvider = "umans"
)

// Provider represents an AI provider configuration.
Expand Down Expand Up @@ -138,6 +139,7 @@ func KnownProviders() []InferenceProvider {
InferenceProviderFireworks,
InferenceProviderBaseten,
InferenceProviderMoonshot,
InferenceProviderUmans,
}
}

Expand Down