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
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
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 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
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
backend: openrouter
model: aion-labs/aion-2.0
temperature: 0.72
reasoning_effort: high
top_p: 0.95
timeout_seconds: 180
service_tier: flex
profiles:
aion-2:
backend: openrouter
model: aion-labs/aion-2.0
temperature: 0.72
reasoning_effort: high
top_p: 0.95
timeout_seconds: 180
service_tier: flex

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,11 @@ package openrouter
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"path"
"strings"
"testing"
@@ -14,188 +14,145 @@ import (
)
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)
backendPath := Root + "/backend.yml"
backendData, err := fs.ReadFile(assets, backendPath)
if err != nil {
t.Fatalf("read manifest: %v", err)
t.Fatalf("read backend: %v", err)
}
assertOpenRouterManifest(t, manifestData)
assertBackend(t, backendData)
profilesRoot := Root + "/profiles"
profileCount := 0
ids := map[string]string{}
err = fs.WalkDir(fsys, Root, func(path string, entry fs.DirEntry, walkErr error) error {
ids := make(map[string]string)
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 assetPath == Root || assetPath == profilesRoot || entry.IsDir() {
return nil
}
if path == manifestPath {
if assetPath == backendPath {
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
}
if !strings.HasPrefix(path, profilesRoot+"/") {
return fmt.Errorf("unexpected catalog entry %q", path)
if !strings.HasPrefix(assetPath, profilesRoot+"/") {
return fmt.Errorf("unexpected catalog entry %q", assetPath)
}
if !entry.Type().IsRegular() || !strings.HasSuffix(path, ".yml") {
return fmt.Errorf("profile asset %q is not a regular .yml file", path)
if !entry.Type().IsRegular() || !strings.HasSuffix(assetPath, ".yml") {
return fmt.Errorf("profile asset %q is not a regular .yml file", assetPath)
}
data, err := fs.ReadFile(fsys, path)
data, err := fs.ReadFile(assets, assetPath)
if err != nil {
return err
}
id := assertOpenRouterProfile(t, path, data)
id := assertProfile(t, assetPath, data)
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
profileCount++
ids[id] = assetPath
return nil
})
if err != nil {
t.Fatalf("validate catalog layout: %v", err)
}
if profileCount == 0 {
t.Fatal("catalog contains no profiles")
if len(ids) != 36 {
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()
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"`
root := decodeMapping(t, "backend", data)
backends := requireSingleMapping(t, "backend", root, "backends")
if len(backends) != 1 || backends["openrouter"] == nil {
t.Fatalf("backend asset must define only openrouter: %v", backends)
}
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 != "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)
definition := mappingFields(t, "backend", backends["openrouter"])
for field, want := range map[string]string{
"endpoint": "https://openrouter.ai/api/v1", "api_key_env": "OPENROUTER_API_KEY",
"concurrency_limit": "16", "queue_capacity": "1024",
} {
value := definition[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(definition) != 4 {
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
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 || 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 {
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 requireSingleMapping(t *testing.T, name string, parent *yaml.Node, key string) map[string]*yaml.Node {
t.Helper()
fields := mappingFields(t, name, parent)
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-openrouter/v2`.
module path is `gitea.maximumdirect.net/eric/promptkit-backend-openrouter/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-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.

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