Add the OpenRouter catalog module
This commit is contained in:
30
README.md
30
README.md
@@ -1,2 +1,30 @@
|
|||||||
# promptkit-backend-openrouter
|
# Promptkit OpenRouter Catalog
|
||||||
|
|
||||||
|
This repository publishes immutable OpenRouter 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 OpenRouter 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 assets originated in Promptkit. Their copyright holder has
|
||||||
|
intentionally authorized their 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
19
catalog.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Package openrouter supplies immutable OpenRouter catalog assets for
|
||||||
|
// Promptkit. It does not implement provider behavior or runtime configuration.
|
||||||
|
package openrouter
|
||||||
|
|
||||||
|
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
9
catalog/backend.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
8
catalog/profiles/aion-labs/aion-2.yml
Normal file
8
catalog/profiles/aion-labs/aion-2.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
6
catalog/profiles/anthropic/claude-fable-latest.yml
Normal file
6
catalog/profiles/anthropic/claude-fable-latest.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id: claude-fable-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: "~anthropic/claude-fable-latest"
|
||||||
|
reasoning_effort: high
|
||||||
|
timeout_seconds: 600
|
||||||
|
service_tier: flex
|
||||||
6
catalog/profiles/anthropic/claude-haiku-latest.yml
Normal file
6
catalog/profiles/anthropic/claude-haiku-latest.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id: claude-haiku-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: "~anthropic/claude-haiku-latest"
|
||||||
|
reasoning_effort: medium
|
||||||
|
timeout_seconds: 240
|
||||||
|
service_tier: flex
|
||||||
6
catalog/profiles/anthropic/claude-opus-latest.yml
Normal file
6
catalog/profiles/anthropic/claude-opus-latest.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id: claude-opus-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: "~anthropic/claude-opus-latest"
|
||||||
|
reasoning_effort: high
|
||||||
|
timeout_seconds: 240
|
||||||
|
service_tier: flex
|
||||||
6
catalog/profiles/anthropic/claude-sonnet-latest.yml
Normal file
6
catalog/profiles/anthropic/claude-sonnet-latest.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id: claude-sonnet-latest
|
||||||
|
backend: openrouter
|
||||||
|
model: "~anthropic/claude-sonnet-latest"
|
||||||
|
reasoning_effort: high
|
||||||
|
timeout_seconds: 240
|
||||||
|
service_tier: flex
|
||||||
6
catalog/profiles/deepseek/deepseek-3-2.yml
Normal file
6
catalog/profiles/deepseek/deepseek-3-2.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id: deepseek-3-2
|
||||||
|
backend: openrouter
|
||||||
|
model: deepseek/deepseek-v3.2
|
||||||
|
reasoning_effort: high
|
||||||
|
timeout_seconds: 180
|
||||||
|
service_tier: flex
|
||||||
6
catalog/profiles/deepseek/deepseek-4-flash.yml
Normal file
6
catalog/profiles/deepseek/deepseek-4-flash.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id: deepseek-4-flash
|
||||||
|
backend: openrouter
|
||||||
|
model: deepseek/deepseek-v4-flash
|
||||||
|
#reasoning_effort: medium
|
||||||
|
timeout_seconds: 180
|
||||||
|
service_tier: flex
|
||||||
6
catalog/profiles/deepseek/deepseek-4-pro.yml
Normal file
6
catalog/profiles/deepseek/deepseek-4-pro.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id: deepseek-4-pro
|
||||||
|
backend: openrouter
|
||||||
|
model: deepseek/deepseek-v4-pro
|
||||||
|
reasoning_effort: high
|
||||||
|
timeout_seconds: 180
|
||||||
|
service_tier: flex
|
||||||
8
catalog/profiles/google/gemini-2-flash-lite.yml
Normal file
8
catalog/profiles/google/gemini-2-flash-lite.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
id: gemini-2-flash-lite
|
||||||
|
backend: openrouter
|
||||||
|
model: "google/gemini-2.5-flash-lite"
|
||||||
|
#temperature: 0.15
|
||||||
|
reasoning_effort: high
|
||||||
|
#top_p: 0.98
|
||||||
|
timeout_seconds: 240
|
||||||
|
service_tier: flex
|
||||||
8
catalog/profiles/google/gemini-2-flash.yml
Normal file
8
catalog/profiles/google/gemini-2-flash.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
id: gemini-2-flash
|
||||||
|
backend: openrouter
|
||||||
|
model: "google/gemini-2.5-flash"
|
||||||
|
#temperature: 0.15
|
||||||
|
reasoning_effort: high
|
||||||
|
#top_p: 0.98
|
||||||
|
timeout_seconds: 240
|
||||||
|
service_tier: flex
|
||||||
8
catalog/profiles/google/gemini-2-pro.yml
Normal file
8
catalog/profiles/google/gemini-2-pro.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
id: gemini-2-pro
|
||||||
|
backend: openrouter
|
||||||
|
model: "google/gemini-2.5-pro"
|
||||||
|
#temperature: 0.15
|
||||||
|
reasoning_effort: high
|
||||||
|
#top_p: 0.98
|
||||||
|
timeout_seconds: 240
|
||||||
|
service_tier: flex
|
||||||
8
catalog/profiles/google/gemini-3-flash-lite.yml
Normal file
8
catalog/profiles/google/gemini-3-flash-lite.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
8
catalog/profiles/google/gemini-flash-latest.yml
Normal file
8
catalog/profiles/google/gemini-flash-latest.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
8
catalog/profiles/google/gemini-pro-latest.yml
Normal file
8
catalog/profiles/google/gemini-pro-latest.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
8
catalog/profiles/google/gemma-4-31b.yml
Normal file
8
catalog/profiles/google/gemma-4-31b.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
8
catalog/profiles/minimax/minimax-m2.yml
Normal file
8
catalog/profiles/minimax/minimax-m2.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
8
catalog/profiles/minimax/minimax-m3.yml
Normal file
8
catalog/profiles/minimax/minimax-m3.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
6
catalog/profiles/mistral/mistral-large-2512.yml
Normal file
6
catalog/profiles/mistral/mistral-large-2512.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id: mistral-large-2512
|
||||||
|
backend: openrouter
|
||||||
|
model: mistralai/mistral-large-2512
|
||||||
|
temperature: 0.15
|
||||||
|
top_p: 0.98
|
||||||
|
timeout_seconds: 180
|
||||||
7
catalog/profiles/mistral/mistral-medium-3-5.yml
Normal file
7
catalog/profiles/mistral/mistral-medium-3-5.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
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
|
||||||
6
catalog/profiles/mistral/mistral-small-3.yml
Normal file
6
catalog/profiles/mistral/mistral-small-3.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id: mistral-small-3
|
||||||
|
backend: openrouter
|
||||||
|
model: mistralai/mistral-small-3.2-24b-instruct
|
||||||
|
temperature: 0.05
|
||||||
|
top_p: 1.0
|
||||||
|
timeout_seconds: 180
|
||||||
7
catalog/profiles/mistral/mistral-small-4.yml
Normal file
7
catalog/profiles/mistral/mistral-small-4.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
id: mistral-small-4
|
||||||
|
backend: openrouter
|
||||||
|
model: mistralai/mistral-small-2603
|
||||||
|
temperature: 0.1
|
||||||
|
reasoning_effort: high
|
||||||
|
top_p: 0.98
|
||||||
|
timeout_seconds: 180
|
||||||
6
catalog/profiles/nvidia/nemotron-3-ultra.yml
Normal file
6
catalog/profiles/nvidia/nemotron-3-ultra.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id: nemotron-3-ultra
|
||||||
|
backend: openrouter
|
||||||
|
model: nvidia/nemotron-3-ultra-550b-a55b
|
||||||
|
reasoning_effort: high
|
||||||
|
timeout_seconds: 180
|
||||||
|
service_tier: flex
|
||||||
6
catalog/profiles/openai/gpt-5-mini.yml
Normal file
6
catalog/profiles/openai/gpt-5-mini.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id: gpt-5-mini
|
||||||
|
backend: openrouter
|
||||||
|
model: "openai/gpt-5.4-mini"
|
||||||
|
reasoning_effort: high
|
||||||
|
timeout_seconds: 240
|
||||||
|
service_tier: flex
|
||||||
6
catalog/profiles/openai/gpt-5-nano.yml
Normal file
6
catalog/profiles/openai/gpt-5-nano.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
id: gpt-5-nano
|
||||||
|
backend: openrouter
|
||||||
|
model: "openai/gpt-5.4-nano"
|
||||||
|
reasoning_effort: high
|
||||||
|
timeout_seconds: 240
|
||||||
|
service_tier: flex
|
||||||
201
catalog_test.go
Normal file
201
catalog_test.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package openrouter
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
assertOpenRouterManifest(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 := assertOpenRouterProfile(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 == 0 {
|
||||||
|
t.Fatal("catalog contains no profiles")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertOpenRouterManifest(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 != "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 {
|
||||||
|
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
15
docs/release.md
Normal 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
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module gitea.maximumdirect.net/eric/promptkit-backend-openrouter
|
||||||
|
|
||||||
|
go 1.25.5
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
4
go.sum
Normal file
4
go.sum
Normal 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=
|
||||||
Reference in New Issue
Block a user