Migrate OpenRouter catalog assets to keyed configuration

This commit is contained in:
2026-09-30 02:36:53 +00:00
parent 4f731d217e
commit f17d208cb7
42 changed files with 352 additions and 354 deletions

View File

@@ -6,7 +6,7 @@ credentials, or make network requests.
Promptkit owns catalog parsing, validation, runtime behavior, credential Promptkit owns catalog parsing, validation, runtime behavior, credential
handling, and consumer documentation. This module owns the OpenRouter backend handling, and consumer documentation. This module owns the OpenRouter backend
manifest and its maintained profile data. Consumers should use versions chosen definition and its maintained profile data. Consumers should use versions chosen
by their Promptkit release rather than treating this module as a standalone by their Promptkit release rather than treating this module as a standalone
runtime integration. runtime integration.
@@ -16,17 +16,25 @@ and documentation.
## Compatibility ## 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 upstream availability and may be added, removed, renamed, or modified without
deprecation or advance notice. Consumers that need a particular profile set deprecation or advance notice. Consumers that need a particular profile set
should use the catalog version selected by their Promptkit release. 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 not share that profile-data exception. Their compatibility follows the normal
semantic-versioning rules. Published v1 releases retain their historical semantic-versioning rules. Published v1 and `/v2` releases retain their historical
contract; this README does not retroactively alter it. 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 documents with
ordinary keyed ConfigKit inputs: `catalog/backend.yml` defines
`backends.openrouter`, and each file under `catalog/profiles/` defines one
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 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. that a provider currently offers a model or endpoint named by the data.

View File

@@ -1,9 +0,0 @@
{
"schema_version": 1,
"id": "openrouter",
"endpoint": "https://openrouter.ai/api/v1",
"api_key_env": "OPENROUTER_API_KEY",
"concurrency_limit": 16,
"queue_capacity": 1024,
"extra_params": null
}

6
catalog/backend.yml Normal file
View File

@@ -0,0 +1,6 @@
backends:
openrouter:
endpoint: https://openrouter.ai/api/v1
api_key_env: OPENROUTER_API_KEY
concurrency_limit: 16
queue_capacity: 1024

View File

@@ -1,8 +1,9 @@
id: aion-2 profiles:
backend: openrouter aion-2:
model: aion-labs/aion-2.0 backend: openrouter
temperature: 0.72 model: aion-labs/aion-2.0
reasoning_effort: high temperature: 0.72
top_p: 0.95 reasoning_effort: high
timeout_seconds: 180 top_p: 0.95
service_tier: flex timeout_seconds: 180
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: aion-3-0-mini profiles:
backend: openrouter aion-3-0-mini:
model: "aion-labs/aion-3.0-mini" backend: openrouter
service_tier: flex model: "aion-labs/aion-3.0-mini"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: aion-3-0 profiles:
backend: openrouter aion-3-0:
model: "aion-labs/aion-3.0" backend: openrouter
service_tier: flex model: "aion-labs/aion-3.0"
service_tier: flex

View File

@@ -1,6 +1,7 @@
id: claude-fable-latest profiles:
backend: openrouter claude-fable-latest:
model: "~anthropic/claude-fable-latest" backend: openrouter
reasoning_effort: high model: "~anthropic/claude-fable-latest"
timeout_seconds: 600 reasoning_effort: high
service_tier: flex timeout_seconds: 600
service_tier: flex

View File

@@ -1,6 +1,7 @@
id: claude-haiku-latest profiles:
backend: openrouter claude-haiku-latest:
model: "~anthropic/claude-haiku-latest" backend: openrouter
reasoning_effort: medium model: "~anthropic/claude-haiku-latest"
timeout_seconds: 240 reasoning_effort: medium
service_tier: flex timeout_seconds: 240
service_tier: flex

View File

@@ -1,6 +1,7 @@
id: claude-opus-latest profiles:
backend: openrouter claude-opus-latest:
model: "~anthropic/claude-opus-latest" backend: openrouter
reasoning_effort: high model: "~anthropic/claude-opus-latest"
timeout_seconds: 240 reasoning_effort: high
service_tier: flex timeout_seconds: 240
service_tier: flex

View File

@@ -1,6 +1,7 @@
id: claude-sonnet-latest profiles:
backend: openrouter claude-sonnet-latest:
model: "~anthropic/claude-sonnet-latest" backend: openrouter
reasoning_effort: high model: "~anthropic/claude-sonnet-latest"
timeout_seconds: 240 reasoning_effort: high
service_tier: flex timeout_seconds: 240
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: deepseek-flash-latest profiles:
backend: openrouter deepseek-flash-latest:
model: "~deepseek/deepseek-flash-latest" backend: openrouter
service_tier: flex model: "~deepseek/deepseek-flash-latest"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: deepseek-pro-latest profiles:
backend: openrouter deepseek-pro-latest:
model: "~deepseek/deepseek-pro-latest" backend: openrouter
service_tier: flex model: "~deepseek/deepseek-pro-latest"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gemini-3-5-flash-lite profiles:
backend: openrouter gemini-3-5-flash-lite:
model: "google/gemini-3.5-flash-lite" backend: openrouter
service_tier: flex model: "google/gemini-3.5-flash-lite"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gemini-3-5-flash profiles:
backend: openrouter gemini-3-5-flash:
model: "google/gemini-3.5-flash" backend: openrouter
service_tier: flex model: "google/gemini-3.5-flash"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gemini-3-6-flash profiles:
backend: openrouter gemini-3-6-flash:
model: "google/gemini-3.6-flash" backend: openrouter
service_tier: flex model: "google/gemini-3.6-flash"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gemini-3-7-flash profiles:
backend: openrouter gemini-3-7-flash:
model: "google/gemini-3.7-flash" backend: openrouter
service_tier: flex model: "google/gemini-3.7-flash"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gemini-3-8-flash profiles:
backend: openrouter gemini-3-8-flash:
model: "google/gemini-3.8-flash" backend: openrouter
service_tier: flex model: "google/gemini-3.8-flash"
service_tier: flex

View File

@@ -1,8 +1,9 @@
id: gemini-3-flash-lite profiles:
backend: openrouter gemini-3-flash-lite:
model: "google/gemini-3.1-flash-lite" backend: openrouter
#temperature: 0.15 model: "google/gemini-3.1-flash-lite"
reasoning_effort: high #temperature: 0.15
#top_p: 0.98 reasoning_effort: high
timeout_seconds: 240 #top_p: 0.98
service_tier: flex timeout_seconds: 240
service_tier: flex

View File

@@ -1,8 +1,9 @@
id: gemini-flash-latest profiles:
backend: openrouter gemini-flash-latest:
model: "~google/gemini-flash-latest" backend: openrouter
#temperature: 0.15 model: "~google/gemini-flash-latest"
reasoning_effort: high #temperature: 0.15
#top_p: 0.98 reasoning_effort: high
timeout_seconds: 240 #top_p: 0.98
service_tier: flex timeout_seconds: 240
service_tier: flex

View File

@@ -1,8 +1,9 @@
id: gemini-pro-latest profiles:
backend: openrouter gemini-pro-latest:
model: "~google/gemini-pro-latest" backend: openrouter
#temperature: 0.15 model: "~google/gemini-pro-latest"
reasoning_effort: high #temperature: 0.15
#top_p: 0.98 reasoning_effort: high
timeout_seconds: 240 #top_p: 0.98
service_tier: flex timeout_seconds: 240
service_tier: flex

View File

@@ -1,8 +1,9 @@
id: gemma-4-31b profiles:
backend: openrouter gemma-4-31b:
model: google/gemma-4-31b-it:exacto backend: openrouter
temperature: 0.15 model: google/gemma-4-31b-it:exacto
reasoning_effort: high temperature: 0.15
top_p: 0.98 reasoning_effort: high
timeout_seconds: 240 top_p: 0.98
service_tier: flex timeout_seconds: 240
service_tier: flex

View File

@@ -1,8 +1,9 @@
id: minimax-m2 profiles:
backend: openrouter minimax-m2:
model: minimax/minimax-m2.5 backend: openrouter
temperature: 0.5 model: minimax/minimax-m2.5
reasoning_effort: high temperature: 0.5
top_p: 0.95 reasoning_effort: high
timeout_seconds: 180 top_p: 0.95
service_tier: flex timeout_seconds: 180
service_tier: flex

View File

@@ -1,8 +1,9 @@
id: minimax-m3 profiles:
backend: openrouter minimax-m3:
model: minimax/minimax-m3 backend: openrouter
#temperature: 0.5 model: minimax/minimax-m3
reasoning_effort: high #temperature: 0.5
#top_p: 0.95 reasoning_effort: high
timeout_seconds: 180 #top_p: 0.95
service_tier: flex timeout_seconds: 180
service_tier: flex

View File

@@ -1,6 +1,7 @@
id: mistral-large-2512 profiles:
backend: openrouter mistral-large-2512:
model: mistralai/mistral-large-2512 backend: openrouter
temperature: 0.15 model: mistralai/mistral-large-2512
top_p: 0.98 temperature: 0.15
timeout_seconds: 180 top_p: 0.98
timeout_seconds: 180

View File

@@ -1,7 +1,8 @@
id: mistral-medium-3-5 profiles:
backend: openrouter mistral-medium-3-5:
model: mistralai/mistral-medium-3-5 backend: openrouter
temperature: 0.15 model: mistralai/mistral-medium-3-5
reasoning_effort: high temperature: 0.15
top_p: 0.98 reasoning_effort: high
timeout_seconds: 180 top_p: 0.98
timeout_seconds: 180

View File

@@ -1,6 +1,7 @@
id: mistral-small-3 profiles:
backend: openrouter mistral-small-3:
model: mistralai/mistral-small-3.2-24b-instruct backend: openrouter
temperature: 0.05 model: mistralai/mistral-small-3.2-24b-instruct
top_p: 1.0 temperature: 0.05
timeout_seconds: 180 top_p: 1.0
timeout_seconds: 180

View File

@@ -1,7 +1,8 @@
id: mistral-small-4 profiles:
backend: openrouter mistral-small-4:
model: mistralai/mistral-small-2603 backend: openrouter
temperature: 0.1 model: mistralai/mistral-small-2603
reasoning_effort: high temperature: 0.1
top_p: 0.98 reasoning_effort: high
timeout_seconds: 180 top_p: 0.98
timeout_seconds: 180

View File

@@ -1,6 +1,7 @@
id: nemotron-3-ultra profiles:
backend: openrouter nemotron-3-ultra:
model: nvidia/nemotron-3-ultra-550b-a55b backend: openrouter
reasoning_effort: high model: nvidia/nemotron-3-ultra-550b-a55b
timeout_seconds: 180 reasoning_effort: high
service_tier: flex timeout_seconds: 180
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gpt-5-6-luna profiles:
backend: openrouter gpt-5-6-luna:
model: "openai/gpt-5.6-luna" backend: openrouter
service_tier: flex model: "openai/gpt-5.6-luna"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gpt-5-6-sol profiles:
backend: openrouter gpt-5-6-sol:
model: "openai/gpt-5.6-sol" backend: openrouter
service_tier: flex model: "openai/gpt-5.6-sol"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gpt-5-6-terra profiles:
backend: openrouter gpt-5-6-terra:
model: "openai/gpt-5.6-terra" backend: openrouter
service_tier: flex model: "openai/gpt-5.6-terra"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gpt-6-astra profiles:
backend: openrouter gpt-6-astra:
model: "openai/gpt-6-astra" backend: openrouter
service_tier: flex model: "openai/gpt-6-astra"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gpt-astra-latest profiles:
backend: openrouter gpt-astra-latest:
model: "~openai/gpt-astra-latest" backend: openrouter
service_tier: flex model: "~openai/gpt-astra-latest"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gpt-luna-latest profiles:
backend: openrouter gpt-luna-latest:
model: "~openai/gpt-luna-latest" backend: openrouter
service_tier: flex model: "~openai/gpt-luna-latest"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gpt-sol-latest profiles:
backend: openrouter gpt-sol-latest:
model: "~openai/gpt-sol-latest" backend: openrouter
service_tier: flex model: "~openai/gpt-sol-latest"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: gpt-terra-latest profiles:
backend: openrouter gpt-terra-latest:
model: "~openai/gpt-terra-latest" backend: openrouter
service_tier: flex model: "~openai/gpt-terra-latest"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: grok-latest profiles:
backend: openrouter grok-latest:
model: "~x-ai/grok-latest" backend: openrouter
service_tier: flex model: "~x-ai/grok-latest"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: glm-flash-latest profiles:
backend: openrouter glm-flash-latest:
model: "~z-ai/glm-flash-latest" backend: openrouter
service_tier: flex model: "~z-ai/glm-flash-latest"
service_tier: flex

View File

@@ -1,4 +1,5 @@
id: glm-latest profiles:
backend: openrouter glm-latest:
model: "~z-ai/glm-latest" backend: openrouter
service_tier: flex model: "~z-ai/glm-latest"
service_tier: flex

View File

@@ -2,11 +2,11 @@ package openrouter
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"path"
"strings" "strings"
"testing" "testing"
@@ -14,188 +14,145 @@ import (
) )
func TestCatalogLayoutAndContents(t *testing.T) { func TestCatalogLayoutAndContents(t *testing.T) {
fsys := FS() assets := FS()
if fsys == nil { if assets == nil {
t.Fatal("FS returned nil") t.Fatal("FS returned nil")
} }
backendPath := Root + "/backend.yml"
manifestPath := Root + "/backend.json" backendData, err := fs.ReadFile(assets, backendPath)
manifestData, err := fs.ReadFile(fsys, manifestPath)
if err != nil { if err != nil {
t.Fatalf("read manifest: %v", err) t.Fatalf("read backend: %v", err)
} }
assertOpenRouterManifest(t, manifestData) assertBackend(t, backendData)
profilesRoot := Root + "/profiles" profilesRoot := Root + "/profiles"
profileCount := 0 ids := make(map[string]string)
ids := map[string]string{} err = fs.WalkDir(assets, Root, func(assetPath string, entry fs.DirEntry, walkErr error) error {
err = fs.WalkDir(fsys, Root, func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil { if walkErr != nil {
return walkErr return walkErr
} }
if path == Root || path == profilesRoot || entry.IsDir() { if assetPath == Root || assetPath == profilesRoot || entry.IsDir() {
return nil return nil
} }
if path == manifestPath { if assetPath == backendPath {
if !entry.Type().IsRegular() { if !entry.Type().IsRegular() {
return fmt.Errorf("manifest is not a regular file") return fmt.Errorf("backend asset is not a regular file")
} }
return nil return nil
} }
if !strings.HasPrefix(path, profilesRoot+"/") { if !strings.HasPrefix(assetPath, profilesRoot+"/") {
return fmt.Errorf("unexpected catalog entry %q", path) return fmt.Errorf("unexpected catalog entry %q", assetPath)
} }
if !entry.Type().IsRegular() || !strings.HasSuffix(path, ".yml") { if !entry.Type().IsRegular() || !strings.HasSuffix(assetPath, ".yml") {
return fmt.Errorf("profile asset %q is not a regular .yml file", path) return fmt.Errorf("profile asset %q is not a regular .yml file", assetPath)
} }
data, err := fs.ReadFile(assets, assetPath)
data, err := fs.ReadFile(fsys, path)
if err != nil { if err != nil {
return err return err
} }
id := assertOpenRouterProfile(t, path, data) id := assertProfile(t, assetPath, data)
if previous, found := ids[id]; found { if previous, found := ids[id]; found {
return fmt.Errorf("duplicate profile ID %q in %s and %s", id, previous, path) return fmt.Errorf("duplicate profile ID %q in %s and %s", id, previous, assetPath)
} }
ids[id] = path ids[id] = assetPath
profileCount++
return nil return nil
}) })
if err != nil { if err != nil {
t.Fatalf("validate catalog layout: %v", err) t.Fatalf("validate catalog layout: %v", err)
} }
if profileCount == 0 { if len(ids) != 36 {
t.Fatal("catalog contains no profiles") t.Fatalf("profile inventory changed: got %d, want 36", len(ids))
} }
} }
func assertOpenRouterManifest(t *testing.T, data []byte) { func assertBackend(t *testing.T, data []byte) {
t.Helper() t.Helper()
root := decodeMapping(t, "backend", data)
type manifest struct { backends := requireSingleMapping(t, "backend", root, "backends")
SchemaVersion int `json:"schema_version"` if len(backends) != 1 || backends["openrouter"] == nil {
ID string `json:"id"` t.Fatalf("backend asset must define only openrouter: %v", backends)
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"`
} }
definition := mappingFields(t, "backend", backends["openrouter"])
decoder := json.NewDecoder(bytes.NewReader(data)) for field, want := range map[string]string{
decoder.DisallowUnknownFields() "endpoint": "https://openrouter.ai/api/v1", "api_key_env": "OPENROUTER_API_KEY",
var value manifest "concurrency_limit": "16", "queue_capacity": "1024",
if err := decoder.Decode(&value); err != nil { } {
t.Fatalf("decode manifest: %v", err) value := definition[field]
} if value == nil || value.Value != want {
if err := requireJSONEnd(decoder); err != nil { t.Fatalf("backend %s: got %v, want %q", field, value, want)
t.Fatalf("decode manifest: %v", err)
}
if value.SchemaVersion != 1 ||
value.ID != "openrouter" ||
value.Endpoint != "https://openrouter.ai/api/v1" ||
value.APIKeyEnv != "OPENROUTER_API_KEY" ||
value.ConcurrencyLimit != 16 ||
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 assertOpenRouterProfile(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 != "openrouter" {
t.Fatalf("profile %s does not select openrouter", 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 { if len(definition) != 4 {
assertNoSecretKeys(t, path, extraParams) t.Fatalf("unexpected backend fields: %v", definition)
} }
return id
} }
func requireYAMLEnd(decoder *yaml.Decoder) error { func assertProfile(t *testing.T, assetPath string, data []byte) string {
t.Helper()
root := decodeMapping(t, assetPath, data)
profiles := requireSingleMapping(t, assetPath, root, "profiles")
if len(profiles) != 1 {
t.Fatalf("profile asset %s must define exactly one ID", assetPath)
}
for id, entry := range profiles {
if id == "" || id != strings.TrimSuffix(path.Base(assetPath), ".yml") {
t.Fatalf("profile asset %s has unexpected ID %q", assetPath, id)
}
fields := mappingFields(t, assetPath, entry)
if fields["backend"] == nil || fields["backend"].Value != "openrouter" {
t.Fatalf("profile %s does not select openrouter", assetPath)
}
if fields["model"] == nil || strings.TrimSpace(fields["model"].Value) == "" {
t.Fatalf("profile %s has no model", assetPath)
}
for _, obsolete := range []string{"id", "base_profile"} {
if fields[obsolete] != nil {
t.Fatalf("profile %s contains obsolete %s field", assetPath, obsolete)
}
}
return id
}
panic("unreachable")
}
func decodeMapping(t *testing.T, name string, data []byte) *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 var trailing yaml.Node
if err := decoder.Decode(&trailing); errors.Is(err, io.EOF) { if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
return nil t.Fatalf("%s must contain exactly one YAML document: %v", name, err)
} else if err != nil {
return err
} }
return errors.New("profile must contain exactly one YAML document") if document.Kind != yaml.DocumentNode || len(document.Content) != 1 || document.Content[0].Kind != yaml.MappingNode {
t.Fatalf("%s must contain one mapping document", name)
}
return document.Content[0]
} }
func yamlFields(mapping *yaml.Node) map[string]*yaml.Node { func requireSingleMapping(t *testing.T, name string, parent *yaml.Node, key string) map[string]*yaml.Node {
fields := make(map[string]*yaml.Node, len(mapping.Content)/2) t.Helper()
for index := 0; index+1 < len(mapping.Content); index += 2 { fields := mappingFields(t, name, parent)
fields[mapping.Content[index].Value] = mapping.Content[index+1] 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 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 # Catalog Release Procedure
This repository releases source tags only. It has no generated artifacts. The This repository releases source tags only. It has no generated artifacts. The
module path is `gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v2`. module path is `gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v3`.
## Version selection ## 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 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 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 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, ./...`, `GOWORK=off go vet ./...`, `GOWORK=off go build ./...`, formatting,
repository-hygiene, credential, and ordinary module-resolution checks. 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 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 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 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 Finally, from a temporary module with `GOWORK=off`, no workspace, no
replacement, and no vendor tree, resolve the exact published replacement, and no vendor tree, resolve the exact published
`gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v2@version`. Verify `gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v3@version`. Verify
both the resolved module identity and version match the selected tag. 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-openrouter/v2 module gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v3
go 1.25.5 go 1.25.5