1 Commits

Author SHA1 Message Date
f17d208cb7 Migrate OpenRouter catalog assets to keyed configuration 2026-09-30 02:36:53 +00:00
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,4 +1,5 @@
id: aion-2 profiles:
aion-2:
backend: openrouter backend: openrouter
model: aion-labs/aion-2.0 model: aion-labs/aion-2.0
temperature: 0.72 temperature: 0.72

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
id: claude-fable-latest profiles:
claude-fable-latest:
backend: openrouter backend: openrouter
model: "~anthropic/claude-fable-latest" model: "~anthropic/claude-fable-latest"
reasoning_effort: high reasoning_effort: high

View File

@@ -1,4 +1,5 @@
id: claude-haiku-latest profiles:
claude-haiku-latest:
backend: openrouter backend: openrouter
model: "~anthropic/claude-haiku-latest" model: "~anthropic/claude-haiku-latest"
reasoning_effort: medium reasoning_effort: medium

View File

@@ -1,4 +1,5 @@
id: claude-opus-latest profiles:
claude-opus-latest:
backend: openrouter backend: openrouter
model: "~anthropic/claude-opus-latest" model: "~anthropic/claude-opus-latest"
reasoning_effort: high reasoning_effort: high

View File

@@ -1,4 +1,5 @@
id: claude-sonnet-latest profiles:
claude-sonnet-latest:
backend: openrouter backend: openrouter
model: "~anthropic/claude-sonnet-latest" model: "~anthropic/claude-sonnet-latest"
reasoning_effort: high reasoning_effort: high

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
id: gemini-3-flash-lite profiles:
gemini-3-flash-lite:
backend: openrouter backend: openrouter
model: "google/gemini-3.1-flash-lite" model: "google/gemini-3.1-flash-lite"
#temperature: 0.15 #temperature: 0.15

View File

@@ -1,4 +1,5 @@
id: gemini-flash-latest profiles:
gemini-flash-latest:
backend: openrouter backend: openrouter
model: "~google/gemini-flash-latest" model: "~google/gemini-flash-latest"
#temperature: 0.15 #temperature: 0.15

View File

@@ -1,4 +1,5 @@
id: gemini-pro-latest profiles:
gemini-pro-latest:
backend: openrouter backend: openrouter
model: "~google/gemini-pro-latest" model: "~google/gemini-pro-latest"
#temperature: 0.15 #temperature: 0.15

View File

@@ -1,4 +1,5 @@
id: gemma-4-31b profiles:
gemma-4-31b:
backend: openrouter backend: openrouter
model: google/gemma-4-31b-it:exacto model: google/gemma-4-31b-it:exacto
temperature: 0.15 temperature: 0.15

View File

@@ -1,4 +1,5 @@
id: minimax-m2 profiles:
minimax-m2:
backend: openrouter backend: openrouter
model: minimax/minimax-m2.5 model: minimax/minimax-m2.5
temperature: 0.5 temperature: 0.5

View File

@@ -1,4 +1,5 @@
id: minimax-m3 profiles:
minimax-m3:
backend: openrouter backend: openrouter
model: minimax/minimax-m3 model: minimax/minimax-m3
#temperature: 0.5 #temperature: 0.5

View File

@@ -1,4 +1,5 @@
id: mistral-large-2512 profiles:
mistral-large-2512:
backend: openrouter backend: openrouter
model: mistralai/mistral-large-2512 model: mistralai/mistral-large-2512
temperature: 0.15 temperature: 0.15

View File

@@ -1,4 +1,5 @@
id: mistral-medium-3-5 profiles:
mistral-medium-3-5:
backend: openrouter backend: openrouter
model: mistralai/mistral-medium-3-5 model: mistralai/mistral-medium-3-5
temperature: 0.15 temperature: 0.15

View File

@@ -1,4 +1,5 @@
id: mistral-small-3 profiles:
mistral-small-3:
backend: openrouter backend: openrouter
model: mistralai/mistral-small-3.2-24b-instruct model: mistralai/mistral-small-3.2-24b-instruct
temperature: 0.05 temperature: 0.05

View File

@@ -1,4 +1,5 @@
id: mistral-small-4 profiles:
mistral-small-4:
backend: openrouter backend: openrouter
model: mistralai/mistral-small-2603 model: mistralai/mistral-small-2603
temperature: 0.1 temperature: 0.1

View File

@@ -1,4 +1,5 @@
id: nemotron-3-ultra profiles:
nemotron-3-ultra:
backend: openrouter backend: openrouter
model: nvidia/nemotron-3-ultra-550b-a55b model: nvidia/nemotron-3-ultra-550b-a55b
reasoning_effort: high reasoning_effort: high

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
id: glm-latest profiles:
glm-latest:
backend: openrouter backend: openrouter
model: "~z-ai/glm-latest" model: "~z-ai/glm-latest"
service_tier: flex 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 {
t.Fatalf("backend %s: got %v, want %q", field, value, want)
} }
if err := requireJSONEnd(decoder); err != nil {
t.Fatalf("decode manifest: %v", err)
} }
if value.SchemaVersion != 1 || if len(definition) != 4 {
value.ID != "openrouter" || t.Fatalf("unexpected backend fields: %v", definition)
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 { func assertProfile(t *testing.T, assetPath string, data []byte) string {
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() t.Helper()
root := decodeMapping(t, assetPath, data)
var document yaml.Node profiles := requireSingleMapping(t, assetPath, root, "profiles")
decoder := yaml.NewDecoder(bytes.NewReader(data)) if len(profiles) != 1 {
if err := decoder.Decode(&document); err != nil { t.Fatalf("profile asset %s must define exactly one ID", assetPath)
t.Fatalf("decode profile %s: %v", path, err)
} }
if err := requireYAMLEnd(decoder); err != nil { for id, entry := range profiles {
t.Fatalf("decode profile %s: %v", path, err) if id == "" || id != strings.TrimSuffix(path.Base(assetPath), ".yml") {
} t.Fatalf("profile asset %s has unexpected ID %q", assetPath, id)
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)
} }
fields := mappingFields(t, assetPath, entry)
if fields["backend"] == nil || fields["backend"].Value != "openrouter" { if fields["backend"] == nil || fields["backend"].Value != "openrouter" {
t.Fatalf("profile %s does not select openrouter", path) t.Fatalf("profile %s does not select openrouter", assetPath)
} }
for _, field := range []string{"endpoint", "api_key_env", "api_key"} { if fields["model"] == nil || strings.TrimSpace(fields["model"].Value) == "" {
if fields[field] != nil { t.Fatalf("profile %s has no model", assetPath)
t.Fatalf("profile %s contains prohibited %s field", path, field)
} }
for _, obsolete := range []string{"id", "base_profile"} {
if fields[obsolete] != nil {
t.Fatalf("profile %s contains obsolete %s field", assetPath, obsolete)
} }
if extraParams := fields["extra_params"]; extraParams != nil {
assertNoSecretKeys(t, path, extraParams)
} }
return id return id
} }
panic("unreachable")
}
func requireYAMLEnd(decoder *yaml.Decoder) error { 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