From ee6e0566a2467019a33e885c7af081803dc1d848 Mon Sep 17 00:00:00 2001 From: Philipp Hug Date: Mon, 6 Jul 2026 08:51:36 +0200 Subject: [PATCH 1/2] feat: add Umans provider --- go.sum | 15 ++++++ internal/providers/configs/umans.json | 71 +++++++++++++++++++++++++++ internal/providers/providers.go | 8 +++ pkg/catwalk/provider.go | 2 + 4 files changed, 96 insertions(+) create mode 100644 internal/providers/configs/umans.json diff --git a/go.sum b/go.sum index 8f1d0e4c..e47aba5a 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= @@ -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= diff --git a/internal/providers/configs/umans.json b/internal/providers/configs/umans.json new file mode 100644 index 00000000..90bf7fb0 --- /dev/null +++ b/internal/providers/configs/umans.json @@ -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, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 256000, + "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, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 256000, + "default_max_tokens": 32768, + "can_reason": true, + "supports_attachments": true + }, + { + "id": "umans-glm-5.2", + "name": "Umans GLM 5.2", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 400000, + "default_max_tokens": 32768, + "can_reason": true, + "supports_attachments": true + }, + { + "id": "umans-glm-5.1", + "name": "Umans GLM 5.1", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 200000, + "default_max_tokens": 32768, + "can_reason": true, + "supports_attachments": true + }, + { + "id": "umans-flash", + "name": "Umans Flash", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 256000, + "default_max_tokens": 8192, + "can_reason": false, + "supports_attachments": true + } + ] +} diff --git a/internal/providers/providers.go b/internal/providers/providers.go index 6f11d669..379d5f1a 100644 --- a/internal/providers/providers.go +++ b/internal/providers/providers.go @@ -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 @@ -172,6 +175,7 @@ var providerRegistry = []ProviderFunc{ vertexAIProvider, zhipuProvider, zhipuCodingProvider, + umansProvider, } // GetAll returns all registered providers. @@ -347,3 +351,7 @@ func zhipuProvider() catwalk.Provider { func zhipuCodingProvider() catwalk.Provider { return loadProviderFromConfig(zhipuCodingConfig) } + +func umansProvider() catwalk.Provider { + return loadProviderFromConfig(umansConfig) +} diff --git a/pkg/catwalk/provider.go b/pkg/catwalk/provider.go index f3a31375..af1cc0e5 100644 --- a/pkg/catwalk/provider.go +++ b/pkg/catwalk/provider.go @@ -59,6 +59,7 @@ const ( InferenceProviderFireworks InferenceProvider = "fireworks" InferenceProviderBaseten InferenceProvider = "baseten" InferenceProviderMoonshot InferenceProvider = "moonshot" + InferenceProviderUmans InferenceProvider = "umans" ) // Provider represents an AI provider configuration. @@ -138,6 +139,7 @@ func KnownProviders() []InferenceProvider { InferenceProviderFireworks, InferenceProviderBaseten, InferenceProviderMoonshot, + InferenceProviderUmans, } } From c6b79f9a0af252f7320f9ab5a50a11080207af69 Mon Sep 17 00:00:00 2001 From: Philipp Hug Date: Wed, 8 Jul 2026 11:05:37 +0200 Subject: [PATCH 2/2] feat: add Umans provider updater command - Create cmd/umans/main.go to auto-fetch models from the Umans API - Update umans.json with correct context windows, pricing, and new models - Add Umans to .github/workflows/update.yml --- .github/workflows/update.yml | 4 + cmd/umans/main.go | 148 ++++++++++++++++++++++++++ internal/providers/configs/umans.json | 50 ++++----- 3 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 cmd/umans/main.go diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index a7d8ab5d..7a83a711 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -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: diff --git a/cmd/umans/main.go b/cmd/umans/main.go new file mode 100644 index 00000000..769747ee --- /dev/null +++ b/cmd/umans/main.go @@ -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)) +} diff --git a/internal/providers/configs/umans.json b/internal/providers/configs/umans.json index 90bf7fb0..d1461bef 100644 --- a/internal/providers/configs/umans.json +++ b/internal/providers/configs/umans.json @@ -10,61 +10,61 @@ { "id": "umans-coder", "name": "Umans Coder", - "cost_per_1m_in": 0, - "cost_per_1m_out": 0, + "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": 256000, + "context_window": 262144, "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, - "cost_per_1m_out": 0, + "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": 256000, - "default_max_tokens": 32768, - "can_reason": true, + "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": 0, - "cost_per_1m_out": 0, + "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": 400000, + "context_window": 405504, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true }, { - "id": "umans-glm-5.1", - "name": "Umans GLM 5.1", - "cost_per_1m_in": 0, - "cost_per_1m_out": 0, + "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": 200000, + "context_window": 262144, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true }, { - "id": "umans-flash", - "name": "Umans Flash", - "cost_per_1m_in": 0, - "cost_per_1m_out": 0, + "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": 256000, - "default_max_tokens": 8192, - "can_reason": false, + "context_window": 262144, + "default_max_tokens": 32768, + "can_reason": true, "supports_attachments": true } ]