Migrate Rakestrawhome catalog assets to keyed configuration

This commit is contained in:
2026-09-30 02:39:53 +00:00
parent 48918a3ad3
commit 993443d88c
7 changed files with 104 additions and 188 deletions

View File

@@ -6,7 +6,7 @@ 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
backend definition and its maintained profile data. Consumers should use versions
chosen by their Promptkit release rather than treating this module as a
standalone runtime integration.
@@ -16,17 +16,25 @@ and documentation.
## Compatibility
This compatibility policy applies to the `/v2` module. Catalog profiles track
This compatibility policy applies to the `/v3` module. Catalog profiles track
upstream availability and may be added, removed, renamed, or modified without
deprecation or advance notice. Consumers that need a particular profile set
should use the catalog version selected by their Promptkit release.
Backend definitions, the exported Go API, and the catalog manifest schema do
Backend definitions, the exported Go API, and the catalog asset format do
not share that profile-data exception. Their compatibility follows the normal
semantic-versioning rules. Published v1 releases retain their historical
contract; this README does not retroactively alter it.
semantic-versioning rules. Published v1 and `/v2` releases retain their historical
contracts; this README does not retroactively alter them.
Catalog releases use dated versions in the form `v2.YYYYMMDD.N`. The date
The `/v3` module replaces the `/v2` manifest and flat profile document with
ordinary keyed ConfigKit inputs: `catalog/backend.yml` defines
`backends.rakestrawhome`, and the file under `catalog/profiles/` defines its
literal ID beneath `profiles`. Profile inheritance uses `extends` instead of
`base_profile`. This asset-format change is incompatible with `/v2` consumers;
they should remain on a published `/v2` version. The exported `FS` and `Root`
remain the asset access surface.
Catalog releases use dated versions in the form `v3.YYYYMMDD.N`. The date
identifies when the catalog snapshot was published in UTC; it does not certify
that a provider currently offers a model or endpoint named by the data.

View File

@@ -1,9 +0,0 @@
{
"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
}

6
catalog/backend.yml Normal file
View File

@@ -0,0 +1,6 @@
backends:
rakestrawhome:
endpoint: https://inference.ai.rakestrawhome.com/v1
api_key_env: RAKESTRAWHOME_INFERENCE_API_KEY
concurrency_limit: 4
queue_capacity: 1024

View File

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

View File

@@ -2,203 +2,113 @@ 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 {
assets := FS()
if assets == 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)
backendPath := Root + "/backend.yml"
profilePath := Root + "/profiles/google/rakestrawhome-gemma-4-31b.yml"
for _, assetPath := range []string{backendPath, profilePath} {
entry, err := fs.Stat(assets, assetPath)
if err != nil || !entry.Mode().IsRegular() {
t.Fatalf("asset %s is not a regular file: %v", assetPath, 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 {
count := 0
err := fs.WalkDir(assets, Root, func(assetPath string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if path == Root || path == profilesRoot || entry.IsDir() {
if entry.IsDir() {
return nil
}
if path == manifestPath {
if !entry.Type().IsRegular() {
return fmt.Errorf("manifest is not a regular file")
}
return nil
if assetPath != backendPath && assetPath != profilePath {
return fmt.Errorf("unexpected catalog asset %q", assetPath)
}
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++
count++
return nil
})
if err != nil || count != 2 {
t.Fatalf("unexpected catalog layout: %d assets, %v", count, err)
}
backendData, err := fs.ReadFile(assets, backendPath)
if err != nil {
t.Fatalf("validate catalog layout: %v", err)
t.Fatal(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)
backend := singleMapping(t, "backend", singleMapping(t, "backend", decodeMapping(t, "backend", backendData), "backends"), "rakestrawhome")
for field, want := range map[string]string{
"endpoint": "https://inference.ai.rakestrawhome.com/v1",
"api_key_env": "RAKESTRAWHOME_INFERENCE_API_KEY",
"concurrency_limit": "4", "queue_capacity": "1024",
} {
value := backend[field]
if value == nil || value.Value != want {
t.Fatalf("backend %s: got %v, want %q", field, value, want)
}
}
if extraParams := fields["extra_params"]; extraParams != nil {
assertNoSecretKeys(t, path, extraParams)
if len(backend) != 4 {
t.Fatalf("unexpected backend fields: %v", backend)
}
profileData, err := fs.ReadFile(assets, profilePath)
if err != nil {
t.Fatal(err)
}
profile := singleMapping(t, "profile", singleMapping(t, "profile", decodeMapping(t, "profile", profileData), "profiles"), "rakestrawhome-gemma-4-31b")
if len(profile) != 2 || profile["backend"] == nil || profile["backend"].Value != "rakestrawhome" ||
profile["model"] == nil || profile["model"].Value != "google/gemma-4-31b-it" {
t.Fatalf("profile values changed: %v", profile)
}
return id
}
func requireYAMLEnd(decoder *yaml.Decoder) error {
func decodeMapping(t *testing.T, name string, data []byte) map[string]*yaml.Node {
t.Helper()
decoder := yaml.NewDecoder(bytes.NewReader(data))
var document yaml.Node
if err := decoder.Decode(&document); err != nil {
t.Fatalf("decode %s: %v", name, err)
}
var trailing yaml.Node
if err := decoder.Decode(&trailing); errors.Is(err, io.EOF) {
return nil
} else if err != nil {
return err
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
t.Fatalf("%s must contain exactly one YAML document: %v", name, err)
}
return errors.New("profile must contain exactly one YAML document")
if document.Kind != yaml.DocumentNode || len(document.Content) != 1 {
t.Fatalf("%s must contain one mapping document", name)
}
return mappingFields(t, name, document.Content[0])
}
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]
func singleMapping(t *testing.T, name string, fields map[string]*yaml.Node, key string) map[string]*yaml.Node {
t.Helper()
if len(fields) != 1 || fields[key] == nil {
t.Fatalf("%s must contain only %s", name, key)
}
return mappingFields(t, name, fields[key])
}
func mappingFields(t *testing.T, name string, node *yaml.Node) map[string]*yaml.Node {
t.Helper()
if node.Kind != yaml.MappingNode {
t.Fatalf("%s must contain a mapping", name)
}
fields := make(map[string]*yaml.Node, len(node.Content)/2)
for index := 0; index+1 < len(node.Content); index += 2 {
key := node.Content[index].Value
if _, exists := fields[key]; exists {
t.Fatalf("%s repeats key %q", name, key)
}
fields[key] = node.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)
}

View File

@@ -1,11 +1,11 @@
# Catalog Release Procedure
This repository releases source tags only. It has no generated artifacts. The
module path is `gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/v2`.
module path is `gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/v3`.
## Version selection
Release versions use `v2.YYYYMMDD.N`. `YYYYMMDD` is the actual UTC publication
Release versions use `v3.YYYYMMDD.N`. `YYYYMMDD` is the actual UTC publication
date. Begin at revision `0` for a date and increment `N` for each additional
release on that date. Versions are immutable: correct a published catalog with
a new dated version rather than moving or recreating a tag. This repository
@@ -20,7 +20,7 @@ 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.
Confirm the module reports the `/v2` identity before publication. Create an
Confirm the module reports the `/v3` identity before publication. Create an
annotated tag only after validation succeeds. Do not create or move an existing
tag. Publish the validated source commit through `main`, then push only the
selected annotated tag and verify that the remote tag and its peeled commit
@@ -28,5 +28,5 @@ identify that source commit.
Finally, from a temporary module with `GOWORK=off`, no workspace, no
replacement, and no vendor tree, resolve the exact published
`gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/v2@version`.
`gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/v3@version`.
Verify both the resolved module identity and version match the selected tag.

2
go.mod
View File

@@ -1,4 +1,4 @@
module gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/v2
module gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/v3
go 1.25.5