Add the Rakestrawhome catalog module

This commit is contained in:
2026-08-26 14:54:15 +00:00
parent 9bda87d097
commit 2edad4f8e0
8 changed files with 288 additions and 1 deletions

View File

@@ -1,2 +1,30 @@
# promptkit-backend-rakestrawhome
# Promptkit Rakestrawhome Catalog
This repository publishes immutable Rakestrawhome catalog assets for Promptkit.
It supplies catalog data only; it does not implement provider behavior, load
credentials, or make network requests.
Promptkit owns catalog parsing, validation, runtime behavior, credential
handling, and consumer documentation. This module owns the Rakestrawhome
backend manifest and its maintained profile data. Consumers should use versions
chosen by their Promptkit release rather than treating this module as a
standalone runtime integration.
The profile asset originated in Promptkit. Its copyright holder has
intentionally authorized its distribution in this repository under its LGPL-3.0
terms; this is a deliberate relicensing decision, not an automatic consequence
of extracting a dependency.
## Compatibility
Catalog IDs are stable. New profiles are additive. Corrections are released as
compatible catalog updates when they preserve the profile contract; deprecated
profiles are announced before removal in a compatible-major release. Manifest
schema changes follow semantic-version compatibility rules and retain support
for compatible published catalog versions.
## Validation and releases
Run the offline Go tests, race tests, vet, build, formatting, hygiene, and
module-resolution checks before release. The complete source-only release
procedure is in [docs/release.md](docs/release.md).

19
catalog.go Normal file
View File

@@ -0,0 +1,19 @@
// Package rakestrawhome supplies immutable Rakestrawhome catalog assets for
// Promptkit. It does not implement provider behavior or runtime configuration.
package rakestrawhome
import (
"embed"
"io/fs"
)
// Root is the filesystem root containing this module's catalog assets.
const Root = "catalog"
//go:embed catalog
var catalogFS embed.FS
// FS returns the immutable embedded filesystem containing the catalog assets.
func FS() fs.FS {
return catalogFS
}

9
catalog/backend.json Normal file
View File

@@ -0,0 +1,9 @@
{
"schema_version": 1,
"id": "rakestrawhome",
"endpoint": "https://inference.ai.rakestrawhome.com/v1",
"api_key_env": "RAKESTRAWHOME_INFERENCE_API_KEY",
"concurrency_limit": 4,
"queue_capacity": 1024,
"extra_params": null
}

View File

@@ -0,0 +1,3 @@
id: rakestrawhome-gemma-4-31b
backend: rakestrawhome
model: google/gemma-4-31b-it

204
catalog_test.go Normal file
View File

@@ -0,0 +1,204 @@
package rakestrawhome
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"strings"
"testing"
"gopkg.in/yaml.v3"
)
func TestCatalogLayoutAndContents(t *testing.T) {
fsys := FS()
if fsys == nil {
t.Fatal("FS returned nil")
}
manifestPath := Root + "/backend.json"
manifestData, err := fs.ReadFile(fsys, manifestPath)
if err != nil {
t.Fatalf("read manifest: %v", err)
}
assertRakestrawhomeManifest(t, manifestData)
profilesRoot := Root + "/profiles"
profileCount := 0
ids := map[string]string{}
err = fs.WalkDir(fsys, Root, func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if path == Root || path == profilesRoot || entry.IsDir() {
return nil
}
if path == manifestPath {
if !entry.Type().IsRegular() {
return fmt.Errorf("manifest is not a regular file")
}
return nil
}
if !strings.HasPrefix(path, profilesRoot+"/") {
return fmt.Errorf("unexpected catalog entry %q", path)
}
if !entry.Type().IsRegular() || !strings.HasSuffix(path, ".yml") {
return fmt.Errorf("profile asset %q is not a regular .yml file", path)
}
data, err := fs.ReadFile(fsys, path)
if err != nil {
return err
}
id := assertRakestrawhomeProfile(t, path, data)
if previous, found := ids[id]; found {
return fmt.Errorf("duplicate profile ID %q in %s and %s", id, previous, path)
}
ids[id] = path
profileCount++
return nil
})
if err != nil {
t.Fatalf("validate catalog layout: %v", err)
}
if profileCount != 1 {
t.Fatalf("catalog contains %d profiles, want 1", profileCount)
}
if _, found := ids["rakestrawhome-gemma-4-31b"]; !found {
t.Fatal("catalog does not contain the maintained Rakestrawhome profile")
}
}
func assertRakestrawhomeManifest(t *testing.T, data []byte) {
t.Helper()
type manifest struct {
SchemaVersion int `json:"schema_version"`
ID string `json:"id"`
Endpoint string `json:"endpoint"`
APIKeyEnv string `json:"api_key_env"`
ConcurrencyLimit int `json:"concurrency_limit"`
QueueCapacity int `json:"queue_capacity"`
ExtraParams json.RawMessage `json:"extra_params"`
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
var value manifest
if err := decoder.Decode(&value); err != nil {
t.Fatalf("decode manifest: %v", err)
}
if err := requireJSONEnd(decoder); err != nil {
t.Fatalf("decode manifest: %v", err)
}
if value.SchemaVersion != 1 ||
value.ID != "rakestrawhome" ||
value.Endpoint != "https://inference.ai.rakestrawhome.com/v1" ||
value.APIKeyEnv != "RAKESTRAWHOME_INFERENCE_API_KEY" ||
value.ConcurrencyLimit != 4 ||
value.QueueCapacity != 1024 ||
string(value.ExtraParams) != "null" {
t.Fatalf("unexpected manifest: %#v", value)
}
}
func requireJSONEnd(decoder *json.Decoder) error {
var trailing any
if err := decoder.Decode(&trailing); errors.Is(err, io.EOF) {
return nil
} else if err != nil {
return err
}
return errors.New("catalog manifest must contain exactly one JSON value")
}
func assertRakestrawhomeProfile(t *testing.T, path string, data []byte) string {
t.Helper()
var document yaml.Node
decoder := yaml.NewDecoder(bytes.NewReader(data))
if err := decoder.Decode(&document); err != nil {
t.Fatalf("decode profile %s: %v", path, err)
}
if err := requireYAMLEnd(decoder); err != nil {
t.Fatalf("decode profile %s: %v", path, err)
}
if document.Kind != yaml.DocumentNode || len(document.Content) != 1 || document.Content[0].Kind != yaml.MappingNode {
t.Fatalf("profile %s must contain one mapping document", path)
}
fields := yamlFields(document.Content[0])
idField := fields["id"]
if idField == nil {
t.Fatalf("profile %s has no ID", path)
}
id := strings.TrimSpace(idField.Value)
if id == "" {
t.Fatalf("profile %s has a blank ID", path)
}
if fields["backend"] == nil || fields["backend"].Value != "rakestrawhome" {
t.Fatalf("profile %s does not select rakestrawhome", path)
}
for _, field := range []string{"endpoint", "api_key_env", "api_key"} {
if fields[field] != nil {
t.Fatalf("profile %s contains prohibited %s field", path, field)
}
}
if extraParams := fields["extra_params"]; extraParams != nil {
assertNoSecretKeys(t, path, extraParams)
}
return id
}
func requireYAMLEnd(decoder *yaml.Decoder) error {
var trailing yaml.Node
if err := decoder.Decode(&trailing); errors.Is(err, io.EOF) {
return nil
} else if err != nil {
return err
}
return errors.New("profile must contain exactly one YAML document")
}
func yamlFields(mapping *yaml.Node) map[string]*yaml.Node {
fields := make(map[string]*yaml.Node, len(mapping.Content)/2)
for index := 0; index+1 < len(mapping.Content); index += 2 {
fields[mapping.Content[index].Value] = mapping.Content[index+1]
}
return fields
}
func assertNoSecretKeys(t *testing.T, path string, node *yaml.Node) {
t.Helper()
forbidden := map[string]bool{
"api_key": true,
"apikey": true,
"authorization": true,
"credential": true,
"credentials": true,
"password": true,
"secret": true,
"token": true,
"access_token": true,
}
var visit func(*yaml.Node)
visit = func(current *yaml.Node) {
if current.Kind == yaml.MappingNode {
for index := 0; index+1 < len(current.Content); index += 2 {
if forbidden[strings.ToLower(current.Content[index].Value)] {
t.Fatalf("profile %s contains a prohibited extra_params key", path)
}
visit(current.Content[index+1])
}
return
}
for _, child := range current.Content {
visit(child)
}
}
visit(node)
}

15
docs/release.md Normal file
View File

@@ -0,0 +1,15 @@
# Catalog Release Procedure
This repository releases source tags only. It has no generated artifacts.
Before creating a semantic version tag, start from a clean, synchronized
`main` branch. Confirm that no Go workspace, `replace` directive, or `vendor`
tree is active, then run `GOWORK=off go test ./...`, `GOWORK=off go test -race
./...`, `GOWORK=off go vet ./...`, `GOWORK=off go build ./...`, formatting,
repository-hygiene, credential, and ordinary module-resolution checks.
Create an annotated tag only after that validation succeeds. Do not create or
move an existing tag. Push `main`, push only the selected tag, and verify the
remote tag identifies the intended commit. Finally, from a temporary module
with `GOWORK=off` and no replacement, resolve the published module version and
verify the resolved version is the selected tag.

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome
go 1.25.5
require gopkg.in/yaml.v3 v3.0.1

4
go.sum Normal file
View File

@@ -0,0 +1,4 @@
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=