diff --git a/docs/formats.md b/docs/formats.md index 05f201a..23c7919 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -283,7 +283,7 @@ Profile sources resolve matching IDs in this order: 2. the ordinary configured source selected by a profile file, `fs.FS`, or configured profile directory; 3. application fallback profiles supplied with `WithFallbackProfileFS`; and -4. embedded built-in profiles. +4. maintained external catalog profiles. A profile source supplies a complete definition; definitions and their fields are not merged across sources. A higher-precedence source falls back only when diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 843c112..c87edc7 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -14,7 +14,7 @@ contributor workflow and validation. | Root `promptkit` package | Provides the supported engine facade, source, backend-registration, and injection options, public request, result, prompt-inspection, and profile-inspection values, opaque prepared-execution handles, profile construction, extension interfaces, value conversion, redacted formatting, typed capacity and generation error mapping, engine-local profile-source assembly including application fallbacks, and bounded output-repair assembly. | [Package GoDoc](../../doc.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine assembly](../../engine.go) | | `examples/go-library/prepare` | Demonstrates an offline downstream consumer using a prompt file, in-memory profile, inline input, and `Prepare`. It is not a public library package. | [Example program](../../examples/go-library/prepare/main.go) | | `examples/go-library/run` | Demonstrates an offline downstream consumer using a prompt file, in-memory profile, inline input, an injected deterministic model client, and `Run`. It is not a public library package. | [Example program](../../examples/go-library/run/main.go) | -| `internal/backend` | Constructs each engine's immutable registry from the maintained built-in definitions and consumer additions, validates and defensively copies definitions through the shared JSON-value package, and consumes the LLM-owned OpenAI-compatible reserved request-field rule. | [Backend registry](../../internal/backend/registry.go) | +| `internal/backend` | Constructs each engine's immutable registry from maintained definitions and consumer additions, validates and defensively copies definitions through the shared JSON-value package, and consumes the LLM-owned OpenAI-compatible reserved request-field rule. | [Backend registry](../../internal/backend/registry.go) | | `internal/catalog` | Strictly validates imported immutable maintained backend and profile catalog assets before runtime cutover. | [Catalog adapter](../../internal/catalog/catalog.go), [internal sources](sources.md#profiles-and-built-ins) | | `internal/capacity` | Owns engine-local bounded execution admission and FIFO model-generation permits for limited backend IDs, including cancellation-safe waiter removal and client wrapping. | [Internal capacity management](capacity.md) | | `internal/domain` | Defines internal framework values for requests, artifacts, prompt definitions, profiles, execution targets, rendering, generation, and validation, and owns source-neutral invariants for shared execution settings, OpenAI-compatible base endpoints, session identifiers, and output contracts. Source parsing, required fields, other source-specific normalization, defaulting, and boundary-specific error classification remain with their callers. | [Domain declarations](../../internal/domain/domain.go), [endpoint invariant](../../internal/domain/endpoint.go) | @@ -23,7 +23,6 @@ contributor workflow and validation. | `internal/jsonvalue` | Validates and deeply copies bounded JSON-compatible extra-parameter and prepared-schema trees while preserving supported concrete value types and rejecting cycles or excessive depth and work. | [JSON values](../../internal/jsonvalue/jsonvalue.go) | | `internal/promptdef` | Loads strictly decoded, validated prompt definitions from filesystem and `fs.FS` sources, including version selection and contained file-backed message content. | [Framework formats](../formats.md), [prompt-definition repository](../../internal/promptdef/filesystem_repository.go) | | `internal/profile` | Loads strictly decoded, locally validated execution profiles from filesystem and `fs.FS` sources, overlays raw sources with error-preserving fallback, and resolves inherited profiles. | [Framework formats](../formats.md), [profile repositories](../../internal/profile/filesystem_repository.go), [internal sources](sources.md#profiles-and-built-ins) | -| `internal/profile/builtin` | Embeds the built-in profile catalog, whose entries select maintained built-in backends. | [Built-in catalog](../formats.md#built-in-profile-catalog), [repository](../../internal/profile/builtin/repository.go) | | `internal/prompt` | Renders prompt messages from Go templates with artifact, variable, session, and cache-control data. | [Go-template renderer](../../internal/prompt/go_renderer.go) | | `internal/artifact` | Resolves ordinary inline and unrestricted caller-selected file references into copied artifacts with metadata and hashes. | [Internal sources and validation](sources.md) | | `internal/validate` | Validates basic, JSON, and JSON Schema output using operating-system filesystem or `fs.FS` schema sources and creates operation-local validation plans with canonical contained schema resources. | [Framework formats](../formats.md#schemas), [internal sources and validation](sources.md) | diff --git a/docs/internal/sources.md b/docs/internal/sources.md index 0e4d2f3..ff09970 100644 --- a/docs/internal/sources.md +++ b/docs/internal/sources.md @@ -62,14 +62,13 @@ catalog consumers. It discovers and strictly validates every raw profile once, preserves safe source metadata including explicitly present YAML fields, and publishes independently copied values from memory. It does not resolve profile inheritance. Configured consumer sources continue to use the lazy point lookup -repositories described above; engine assembly still uses the embedded built-in -catalog at this point. +repositories described above. `internal/catalog` validates the imported immutable OpenRouter and Rakestrawhome asset modules as one private adapter boundary. It enforces their manifest, layout, profile ownership, inheritance, and secret-safety rules -before returning raw catalog sources. The root engine continues to use the -embedded built-ins until runtime cutover. +before returning raw catalog sources. Root assembly uses those validated +catalogs as the maintained lowest-precedence profile source. The overlay repository consults the next repository only when the higher-precedence repository reports that a profile is absent. A reliably @@ -81,7 +80,7 @@ profile inspection. The root engine assembles one raw composite catalog in precedence order: in-memory profiles, one ordinary configured source, an application fallback -source, then the embedded built-in catalog. An explicit file or `fs.FS` profile +source, then the maintained external catalog. An explicit file or `fs.FS` profile source replaces `Config.ProfileDir` within the ordinary configured-source category. One outer resolving repository wraps that complete raw catalog, so each base lookup observes the same precedence and shadowing rules. @@ -99,15 +98,11 @@ or schema sources. It does not retain that lookup for a later execution. Prepared execution instead freezes the fully resolved target; a later ordinary operation performs a fresh traversal. -`internal/profile/builtin` embeds the maintained built-in profile catalog. -Every embedded profile selects a maintained built-in backend and inherits that -backend's endpoint and credential environment-variable name from the built-in -backend registry rather than repeating those values. Profile loading and -overlay behavior are owned by the +The maintained external catalog provides every built-in profile and its +matching backend definition. Profile loading and overlay behavior are owned by the [profile repository tests](../../internal/profile/repository_test.go), while -catalog completeness, the backend-selection invariant, and duplicate IDs are -owned by the -[built-in repository tests](../../internal/profile/builtin/repository_test.go). +catalog completeness, backend selection, and duplicate IDs are owned by the +[catalog adapter tests](../../internal/catalog/catalog_test.go). ## Ordinary Artifacts diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index f098205..9e44327 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -22,7 +22,7 @@ The implemented internal components consist of: - `internal/domain`, which owns framework data values and source-neutral invariants shared by later internal components; - `internal/backend`, which owns validated immutable OpenAI-compatible backend - definitions and the maintained built-in definitions; + definitions; - `internal/capacity`, which owns engine-local bounded run admission and model-generation scheduling for limited backends; - `internal/defaults`, which owns application-neutral framework defaults and @@ -37,8 +37,6 @@ The implemented internal components consist of: filesystem and `fs.FS` sources; - `internal/profile`, which loads, validates, and overlays execution profiles from filesystem and `fs.FS` sources; -- `internal/profile/builtin`, which embeds the built-in execution profile - catalog; - `internal/prompt`, which renders prompt messages from Go templates; - `internal/artifact`, which resolves ordinary inline and unrestricted caller-selected file references; @@ -56,13 +54,15 @@ packages or participate in internal assembly. The root facade assembles one immutable backend registry, one capacity manager, the internal repositories, renderer, validator, outbound client, and use-case runner while translating public values and errors at the library boundary. The -registry contains built-ins plus validated engine-scoped consumer additions. +registry contains validated maintained definitions plus engine-scoped consumer +additions. The facade constructs the capacity manager from the registry's immutable policy snapshot, wraps the selected built-in or injected model client, and supplies bounded admission to the runner. The defaults and renderer depend on the domain model. Prompt-definition and profile repositories use the domain -model, file catalog, and YAML decoder. The built-in profile repository supplies -an embedded `fs.FS` to the profile package. Artifact reading uses the domain +model, file catalog, and YAML decoder. The catalog adapter validates imported +immutable maintained data before root assembly supplies it to the profile +package. Artifact reading uses the domain model and application-neutral defaults. Validation uses the domain model, file catalog, and JSON Schema implementation. The model client uses the domain model, application-neutral defaults, and an injected or standard-library HTTP diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index bd9ff18..9cd703e 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -503,7 +503,7 @@ source as the active implementation. ## Stage 6: Cut Promptkit Over To The External Catalogs -**Status:** Pending +**Status:** Complete ### Repository diff --git a/engine.go b/engine.go index cbea102..2529e97 100644 --- a/engine.go +++ b/engine.go @@ -11,14 +11,16 @@ import ( "strings" "time" + openrouter "gitea.maximumdirect.net/eric/promptkit-backend-openrouter" + rakestrawhome "gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome" artifactadapter "gitea.maximumdirect.net/eric/promptkit/internal/artifact" "gitea.maximumdirect.net/eric/promptkit/internal/backend" "gitea.maximumdirect.net/eric/promptkit/internal/capacity" + "gitea.maximumdirect.net/eric/promptkit/internal/catalog" "gitea.maximumdirect.net/eric/promptkit/internal/defaults" "gitea.maximumdirect.net/eric/promptkit/internal/domain" "gitea.maximumdirect.net/eric/promptkit/internal/llm" "gitea.maximumdirect.net/eric/promptkit/internal/profile" - "gitea.maximumdirect.net/eric/promptkit/internal/profile/builtin" "gitea.maximumdirect.net/eric/promptkit/internal/prompt" "gitea.maximumdirect.net/eric/promptkit/internal/promptdef" "gitea.maximumdirect.net/eric/promptkit/internal/usecase" @@ -390,9 +392,17 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) { promptDefs = promptdef.NewFilesystemRepository(cfg.PromptDir) } - profiles := newProfileRepository(cfg.ProfileDir, options) + maintainedCatalogs, err := catalog.Load( + catalog.Source{Name: "OpenRouter", ExpectedBackendID: backend.OpenRouterID, FS: openrouter.FS(), Root: openrouter.Root}, + catalog.Source{Name: "Rakestrawhome", ExpectedBackendID: backend.RakestrawHomeID, FS: rakestrawhome.FS(), Root: rakestrawhome.Root}, + ) + if err != nil { + return nil, fmt.Errorf("%w: failed to load maintained catalogs: %v", ErrInvalidConfig, err) + } - backendRegistry, err := backend.NewRegistry(options.backends) + profiles := newProfileRepository(cfg.ProfileDir, options, maintainedCatalogs.Profiles) + + backendRegistry, err := backend.NewRegistry(maintainedCatalogs.Backends, options.backends) if err != nil { return nil, fmt.Errorf("%w: failed to construct backend registry: %v", ErrInvalidConfig, err) } @@ -444,8 +454,8 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) { }, nil } -func newProfileRepository(profileDir string, options engineOptions) profile.Repository { - repository := builtin.NewRepository() +func newProfileRepository(profileDir string, options engineOptions, maintained profile.Repository) profile.Repository { + repository := maintained if options.fallbackProfileSource { repository = profile.NewOverlayRepository(options.fallbackProfiles, repository) diff --git a/engine_test.go b/engine_test.go index edd109c..0e9b6f8 100644 --- a/engine_test.go +++ b/engine_test.go @@ -1545,31 +1545,21 @@ func TestPrepareUsesBuiltInProfileWithoutProfileDir(t *testing.T) { name string profileID string backendID string - endpoint string - apiKeyEnv string - model string }{ { name: "OpenRouter", profileID: "mistral-small-3", backendID: promptkit.BackendOpenRouter, - endpoint: "https://openrouter.ai/api/v1", - apiKeyEnv: "OPENROUTER_API_KEY", - model: "mistralai/mistral-small-3.2-24b-instruct", }, { name: "Rakestrawhome", profileID: "rakestrawhome-gemma-4-31b", backendID: promptkit.BackendRakestrawHome, - endpoint: "https://inference.ai.rakestrawhome.com/v1", - apiKeyEnv: "RAKESTRAWHOME_INFERENCE_API_KEY", - model: "google/gemma-4-31b-it", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - t.Setenv(tc.apiKeyEnv, "test-key") engine, err := promptkit.NewEngine(promptkit.Config{ PromptDir: frameworkPromptDir, SchemaDir: frameworkSchemaDir, @@ -1581,6 +1571,7 @@ func TestPrepareUsesBuiltInProfileWithoutProfileDir(t *testing.T) { prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{ PromptID: frameworkMarkdownSummaryPromptID, ProfileID: tc.profileID, + APIKey: "test-key", Inputs: map[string]promptkit.ArtifactRef{ "transcript": promptkit.Inline("Rin opens the gate."), "glossary": promptkit.Inline("gate: A guarded passage."), @@ -1591,10 +1582,7 @@ func TestPrepareUsesBuiltInProfileWithoutProfileDir(t *testing.T) { } if prepared.SelectedProfileID != tc.profileID || prepared.SelectedBackendID != tc.backendID || - prepared.EffectiveModelParams.BackendID != tc.backendID || - prepared.EffectiveModelParams.Endpoint != tc.endpoint || - prepared.EffectiveModelParams.APIKeyEnv != tc.apiKeyEnv || - prepared.EffectiveModelParams.Model != tc.model { + prepared.EffectiveModelParams.BackendID != tc.backendID { t.Fatalf("unexpected built-in preparation: %#v", prepared) } }) diff --git a/internal/backend/registry.go b/internal/backend/registry.go index 59a0363..40a4a09 100644 --- a/internal/backend/registry.go +++ b/internal/backend/registry.go @@ -22,16 +22,7 @@ const ( // backend. RakestrawHomeID = "rakestrawhome" - openRouterEndpoint = "https://openrouter.ai/api/v1" - openRouterAPIKeyEnv = "OPENROUTER_API_KEY" - - openRouterConcurrencyLimit = 16 - - rakestrawHomeEndpoint = "https://inference.ai.rakestrawhome.com/v1" - rakestrawHomeAPIKeyEnv = "RAKESTRAWHOME_INFERENCE_API_KEY" - - rakestrawHomeConcurrencyLimit = 4 - defaultQueueCapacity = 1024 + defaultQueueCapacity = 1024 ) // ErrBackendNotFound identifies a registry lookup for an unknown backend ID. @@ -44,16 +35,15 @@ type Registry struct { backends map[string]domain.Backend } -// NewRegistry constructs a registry containing the built-in definitions -// followed by the supplied additions. Every ID must be unique. -func NewRegistry(additions []domain.Backend) (*Registry, error) { - builtIns := builtInBackends() +// NewRegistry constructs a registry containing maintained definitions followed +// by consumer additions. Every ID must be unique across both groups. +func NewRegistry(maintained, additions []domain.Backend) (*Registry, error) { registry := &Registry{ - backends: make(map[string]domain.Backend, len(builtIns)+len(additions)), + backends: make(map[string]domain.Backend, len(maintained)+len(additions)), } - definitions := make([]domain.Backend, 0, len(builtIns)+len(additions)) - definitions = append(definitions, builtIns...) + definitions := make([]domain.Backend, 0, len(maintained)+len(additions)) + definitions = append(definitions, maintained...) definitions = append(definitions, additions...) for _, definition := range definitions { @@ -75,23 +65,6 @@ func NewRegistry(additions []domain.Backend) (*Registry, error) { return registry, nil } -func builtInBackends() []domain.Backend { - return []domain.Backend{ - { - ID: OpenRouterID, - Endpoint: openRouterEndpoint, - APIKeyEnv: openRouterAPIKeyEnv, - ConcurrencyLimit: openRouterConcurrencyLimit, - }, - { - ID: RakestrawHomeID, - Endpoint: rakestrawHomeEndpoint, - APIKeyEnv: rakestrawHomeAPIKeyEnv, - ConcurrencyLimit: rakestrawHomeConcurrencyLimit, - }, - } -} - // GetBackend returns a defensive copy of the backend registered with id. func (r *Registry) GetBackend(id string) (domain.Backend, error) { if r == nil { diff --git a/internal/backend/registry_test.go b/internal/backend/registry_test.go index 753454a..52f0d23 100644 --- a/internal/backend/registry_test.go +++ b/internal/backend/registry_test.go @@ -2,6 +2,7 @@ package backend_test import ( "errors" + "reflect" "strings" "testing" @@ -11,48 +12,24 @@ import ( const validEndpoint = "https://backend.example/v1" -func TestRegistryIncludesExactBuiltInDefinitions(t *testing.T) { - registry, err := backend.NewRegistry(nil) +func TestRegistryIncludesMaintainedDefinitions(t *testing.T) { + maintained := []domain.Backend{ + {ID: backend.OpenRouterID, Endpoint: validEndpoint, ConcurrencyLimit: 2, QueueCapacity: 3, QueueCapacitySet: true}, + {ID: backend.RakestrawHomeID, Endpoint: "https://second.example/v1", ConcurrencyLimit: 4, QueueCapacity: 5, QueueCapacitySet: true}, + } + registry, err := backend.NewRegistry(maintained, nil) if err != nil { t.Fatalf("construct registry: %v", err) } - tests := []struct { - name string - id string - endpoint string - apiKeyEnv string - concurrent int - }{ - { - name: "OpenRouter", - id: backend.OpenRouterID, - endpoint: "https://openrouter.ai/api/v1", - apiKeyEnv: "OPENROUTER_API_KEY", - concurrent: 16, - }, - { - name: "Rakestrawhome", - id: backend.RakestrawHomeID, - endpoint: "https://inference.ai.rakestrawhome.com/v1", - apiKeyEnv: "RAKESTRAWHOME_INFERENCE_API_KEY", - concurrent: 4, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - definition, err := registry.GetBackend(tc.id) + for _, expected := range maintained { + t.Run(expected.ID, func(t *testing.T) { + definition, err := registry.GetBackend(expected.ID) if err != nil { - t.Fatalf("look up built-in: %v", err) + t.Fatalf("look up maintained definition: %v", err) } - if definition.ID != tc.id || - definition.Endpoint != tc.endpoint || - definition.APIKeyEnv != tc.apiKeyEnv || - definition.ConcurrencyLimit != tc.concurrent || - definition.QueueCapacity != 1024 || - !definition.QueueCapacitySet || - definition.ExtraParams != nil { - t.Fatalf("unexpected built-in definition: %#v", definition) + if !reflect.DeepEqual(definition, expected) { + t.Fatalf("unexpected maintained definition: %#v", definition) } }) } @@ -60,12 +37,12 @@ func TestRegistryIncludesExactBuiltInDefinitions(t *testing.T) { policies := registry.CapacityPolicies() if len(policies) != 2 || policies[backend.OpenRouterID] != (domain.BackendCapacityPolicy{ - ConcurrencyLimit: 16, - QueueCapacity: 1024, + ConcurrencyLimit: 2, + QueueCapacity: 3, }) || policies[backend.RakestrawHomeID] != (domain.BackendCapacityPolicy{ ConcurrencyLimit: 4, - QueueCapacity: 1024, + QueueCapacity: 5, }) { t.Fatalf("unexpected built-in capacity policies: %#v", policies) } @@ -77,7 +54,7 @@ func TestRegistryNormalizesUniqueAdditionsAndIsolatesMutations(t *testing.T) { "count": int64(7), "nested": nested, } - registry, err := backend.NewRegistry([]domain.Backend{ + registry, err := backend.NewRegistry(nil, []domain.Backend{ { ID: " custom ", Endpoint: " https://custom.example/openai/v1 ", @@ -140,12 +117,11 @@ func TestRegistryNormalizesUniqueAdditionsAndIsolatesMutations(t *testing.T) { } policies := registry.CapacityPolicies() - if len(policies) != 3 { + if len(policies) != 1 { t.Fatalf("unexpected capacity policy count: %#v", policies) } policies["custom"] = domain.BackendCapacityPolicy{} - delete(policies, backend.OpenRouterID) - delete(policies, backend.RakestrawHomeID) + delete(policies, "custom") againPolicies := registry.CapacityPolicies() if againPolicies["custom"] != (domain.BackendCapacityPolicy{ ConcurrencyLimit: 3, @@ -153,12 +129,6 @@ func TestRegistryNormalizesUniqueAdditionsAndIsolatesMutations(t *testing.T) { }) { t.Fatalf("capacity policy map mutated registry state: %#v", againPolicies) } - if _, ok := againPolicies[backend.OpenRouterID]; !ok { - t.Fatalf("OpenRouter capacity policy deletion mutated registry state: %#v", againPolicies) - } - if _, ok := againPolicies[backend.RakestrawHomeID]; !ok { - t.Fatalf("Rakestrawhome capacity policy deletion mutated registry state: %#v", againPolicies) - } } func TestNewRegistryNormalizesCapacityPolicy(t *testing.T) { @@ -234,7 +204,7 @@ func TestNewRegistryNormalizesCapacityPolicy(t *testing.T) { t.Run(tc.name, func(t *testing.T) { tc.definition.ID = "custom" tc.definition.Endpoint = validEndpoint - registry, err := backend.NewRegistry([]domain.Backend{tc.definition}) + registry, err := backend.NewRegistry(nil, []domain.Backend{tc.definition}) if tc.wantError { if err == nil { t.Fatal("expected invalid capacity policy error") @@ -298,7 +268,7 @@ func TestNewRegistryRejectsDuplicateIDs(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - _, err := backend.NewRegistry(tc.additions) + _, err := backend.NewRegistry(nil, tc.additions) if err == nil { t.Fatal("expected duplicate ID error") } @@ -312,7 +282,7 @@ func TestNewRegistryRejectsDuplicateIDs(t *testing.T) { func TestNewRegistryValidatesIDs(t *testing.T) { for _, id := range []string{"", " \t\n "} { t.Run(id, func(t *testing.T) { - _, err := backend.NewRegistry([]domain.Backend{{ + _, err := backend.NewRegistry(nil, []domain.Backend{{ ID: id, Endpoint: validEndpoint, }}) @@ -341,7 +311,7 @@ func TestNewRegistryValidatesEndpoints(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - _, err := backend.NewRegistry([]domain.Backend{{ + _, err := backend.NewRegistry(nil, []domain.Backend{{ ID: "custom", Endpoint: tc.endpoint, }}) @@ -355,7 +325,7 @@ func TestNewRegistryValidatesEndpoints(t *testing.T) { func TestNewRegistryValidatesEnvironmentVariableNames(t *testing.T) { for _, name := range []string{"1API_KEY", "API-KEY", "API KEY", "ÅPI_KEY"} { t.Run(name, func(t *testing.T) { - _, err := backend.NewRegistry([]domain.Backend{{ + _, err := backend.NewRegistry(nil, []domain.Backend{{ ID: "custom", Endpoint: validEndpoint, APIKeyEnv: name, @@ -378,7 +348,7 @@ func TestNewRegistryRejectsInvalidAndReservedExtraParameters(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - _, err := backend.NewRegistry([]domain.Backend{{ + _, err := backend.NewRegistry(nil, []domain.Backend{{ ID: "custom", Endpoint: validEndpoint, ExtraParams: tc.extraParams, @@ -391,7 +361,7 @@ func TestNewRegistryRejectsInvalidAndReservedExtraParameters(t *testing.T) { } func TestRegistryLookupReportsNotFound(t *testing.T) { - registry, err := backend.NewRegistry(nil) + registry, err := backend.NewRegistry(nil, nil) if err != nil { t.Fatalf("construct registry: %v", err) } diff --git a/internal/profile/builtin/assets/aion-labs/aion-2.yml b/internal/profile/builtin/assets/aion-labs/aion-2.yml deleted file mode 100644 index a22cd8f..0000000 --- a/internal/profile/builtin/assets/aion-labs/aion-2.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/internal/profile/builtin/assets/anthropic/claude-fable-latest.yml b/internal/profile/builtin/assets/anthropic/claude-fable-latest.yml deleted file mode 100644 index 0affde6..0000000 --- a/internal/profile/builtin/assets/anthropic/claude-fable-latest.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: claude-fable-latest -backend: openrouter -model: "~anthropic/claude-fable-latest" -reasoning_effort: high -timeout_seconds: 600 -service_tier: flex diff --git a/internal/profile/builtin/assets/anthropic/claude-haiku-latest.yml b/internal/profile/builtin/assets/anthropic/claude-haiku-latest.yml deleted file mode 100644 index e5ada35..0000000 --- a/internal/profile/builtin/assets/anthropic/claude-haiku-latest.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: claude-haiku-latest -backend: openrouter -model: "~anthropic/claude-haiku-latest" -reasoning_effort: medium -timeout_seconds: 240 -service_tier: flex diff --git a/internal/profile/builtin/assets/anthropic/claude-opus-latest.yml b/internal/profile/builtin/assets/anthropic/claude-opus-latest.yml deleted file mode 100644 index 6d628ac..0000000 --- a/internal/profile/builtin/assets/anthropic/claude-opus-latest.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: claude-opus-latest -backend: openrouter -model: "~anthropic/claude-opus-latest" -reasoning_effort: high -timeout_seconds: 240 -service_tier: flex diff --git a/internal/profile/builtin/assets/anthropic/claude-sonnet-latest.yml b/internal/profile/builtin/assets/anthropic/claude-sonnet-latest.yml deleted file mode 100644 index adce8d6..0000000 --- a/internal/profile/builtin/assets/anthropic/claude-sonnet-latest.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: claude-sonnet-latest -backend: openrouter -model: "~anthropic/claude-sonnet-latest" -reasoning_effort: high -timeout_seconds: 240 -service_tier: flex diff --git a/internal/profile/builtin/assets/deepseek/deepseek-3-2.yml b/internal/profile/builtin/assets/deepseek/deepseek-3-2.yml deleted file mode 100644 index b857d0a..0000000 --- a/internal/profile/builtin/assets/deepseek/deepseek-3-2.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: deepseek-3-2 -backend: openrouter -model: deepseek/deepseek-v3.2 -reasoning_effort: high -timeout_seconds: 180 -service_tier: flex diff --git a/internal/profile/builtin/assets/deepseek/deepseek-4-flash.yml b/internal/profile/builtin/assets/deepseek/deepseek-4-flash.yml deleted file mode 100644 index bffa903..0000000 --- a/internal/profile/builtin/assets/deepseek/deepseek-4-flash.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: deepseek-4-flash -backend: openrouter -model: deepseek/deepseek-v4-flash -#reasoning_effort: medium -timeout_seconds: 180 -service_tier: flex diff --git a/internal/profile/builtin/assets/deepseek/deepseek-4-pro.yml b/internal/profile/builtin/assets/deepseek/deepseek-4-pro.yml deleted file mode 100644 index ec79389..0000000 --- a/internal/profile/builtin/assets/deepseek/deepseek-4-pro.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: deepseek-4-pro -backend: openrouter -model: deepseek/deepseek-v4-pro -reasoning_effort: high -timeout_seconds: 180 -service_tier: flex diff --git a/internal/profile/builtin/assets/google/gemini-2-flash-lite.yml b/internal/profile/builtin/assets/google/gemini-2-flash-lite.yml deleted file mode 100644 index 970f29e..0000000 --- a/internal/profile/builtin/assets/google/gemini-2-flash-lite.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/internal/profile/builtin/assets/google/gemini-2-flash.yml b/internal/profile/builtin/assets/google/gemini-2-flash.yml deleted file mode 100644 index 01bd8d7..0000000 --- a/internal/profile/builtin/assets/google/gemini-2-flash.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/internal/profile/builtin/assets/google/gemini-2-pro.yml b/internal/profile/builtin/assets/google/gemini-2-pro.yml deleted file mode 100644 index fed78cd..0000000 --- a/internal/profile/builtin/assets/google/gemini-2-pro.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/internal/profile/builtin/assets/google/gemini-3-flash-lite.yml b/internal/profile/builtin/assets/google/gemini-3-flash-lite.yml deleted file mode 100644 index 4ff439b..0000000 --- a/internal/profile/builtin/assets/google/gemini-3-flash-lite.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/internal/profile/builtin/assets/google/gemini-flash-latest.yml b/internal/profile/builtin/assets/google/gemini-flash-latest.yml deleted file mode 100644 index 89acd90..0000000 --- a/internal/profile/builtin/assets/google/gemini-flash-latest.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/internal/profile/builtin/assets/google/gemini-pro-latest.yml b/internal/profile/builtin/assets/google/gemini-pro-latest.yml deleted file mode 100644 index 2ce5303..0000000 --- a/internal/profile/builtin/assets/google/gemini-pro-latest.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/internal/profile/builtin/assets/google/gemma-4-31b.yml b/internal/profile/builtin/assets/google/gemma-4-31b.yml deleted file mode 100644 index 39c3529..0000000 --- a/internal/profile/builtin/assets/google/gemma-4-31b.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/internal/profile/builtin/assets/google/rakestrawhome-gemma-4-31b.yml b/internal/profile/builtin/assets/google/rakestrawhome-gemma-4-31b.yml deleted file mode 100644 index d81fe5e..0000000 --- a/internal/profile/builtin/assets/google/rakestrawhome-gemma-4-31b.yml +++ /dev/null @@ -1,3 +0,0 @@ -id: rakestrawhome-gemma-4-31b -backend: rakestrawhome -model: google/gemma-4-31b-it diff --git a/internal/profile/builtin/assets/minimax/minimax-m2.yml b/internal/profile/builtin/assets/minimax/minimax-m2.yml deleted file mode 100644 index 385a181..0000000 --- a/internal/profile/builtin/assets/minimax/minimax-m2.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/internal/profile/builtin/assets/minimax/minimax-m3.yml b/internal/profile/builtin/assets/minimax/minimax-m3.yml deleted file mode 100644 index c0c1bd0..0000000 --- a/internal/profile/builtin/assets/minimax/minimax-m3.yml +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/internal/profile/builtin/assets/mistral/mistral-large-2512.yml b/internal/profile/builtin/assets/mistral/mistral-large-2512.yml deleted file mode 100644 index c77c9d8..0000000 --- a/internal/profile/builtin/assets/mistral/mistral-large-2512.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: mistral-large-2512 -backend: openrouter -model: mistralai/mistral-large-2512 -temperature: 0.15 -top_p: 0.98 -timeout_seconds: 180 diff --git a/internal/profile/builtin/assets/mistral/mistral-medium-3-5.yml b/internal/profile/builtin/assets/mistral/mistral-medium-3-5.yml deleted file mode 100644 index b5201ca..0000000 --- a/internal/profile/builtin/assets/mistral/mistral-medium-3-5.yml +++ /dev/null @@ -1,7 +0,0 @@ -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 diff --git a/internal/profile/builtin/assets/mistral/mistral-small-3.yml b/internal/profile/builtin/assets/mistral/mistral-small-3.yml deleted file mode 100644 index 7e01e55..0000000 --- a/internal/profile/builtin/assets/mistral/mistral-small-3.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: mistral-small-3 -backend: openrouter -model: mistralai/mistral-small-3.2-24b-instruct -temperature: 0.05 -top_p: 1.0 -timeout_seconds: 180 diff --git a/internal/profile/builtin/assets/mistral/mistral-small-4.yml b/internal/profile/builtin/assets/mistral/mistral-small-4.yml deleted file mode 100644 index 2d9b44c..0000000 --- a/internal/profile/builtin/assets/mistral/mistral-small-4.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: mistral-small-4 -backend: openrouter -model: mistralai/mistral-small-2603 -temperature: 0.1 -reasoning_effort: high -top_p: 0.98 -timeout_seconds: 180 diff --git a/internal/profile/builtin/assets/nvidia/nemotron-3-ultra.yml b/internal/profile/builtin/assets/nvidia/nemotron-3-ultra.yml deleted file mode 100644 index 90f84f3..0000000 --- a/internal/profile/builtin/assets/nvidia/nemotron-3-ultra.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: nemotron-3-ultra -backend: openrouter -model: nvidia/nemotron-3-ultra-550b-a55b -reasoning_effort: high -timeout_seconds: 180 -service_tier: flex diff --git a/internal/profile/builtin/assets/openai/gpt-5-mini.yml b/internal/profile/builtin/assets/openai/gpt-5-mini.yml deleted file mode 100644 index c9b4bb2..0000000 --- a/internal/profile/builtin/assets/openai/gpt-5-mini.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: gpt-5-mini -backend: openrouter -model: "openai/gpt-5.4-mini" -reasoning_effort: high -timeout_seconds: 240 -service_tier: flex diff --git a/internal/profile/builtin/assets/openai/gpt-5-nano.yml b/internal/profile/builtin/assets/openai/gpt-5-nano.yml deleted file mode 100644 index 0688ddc..0000000 --- a/internal/profile/builtin/assets/openai/gpt-5-nano.yml +++ /dev/null @@ -1,6 +0,0 @@ -id: gpt-5-nano -backend: openrouter -model: "openai/gpt-5.4-nano" -reasoning_effort: high -timeout_seconds: 240 -service_tier: flex diff --git a/internal/profile/builtin/repository.go b/internal/profile/builtin/repository.go deleted file mode 100644 index aaffb97..0000000 --- a/internal/profile/builtin/repository.go +++ /dev/null @@ -1,16 +0,0 @@ -package builtin - -import ( - "embed" - - "gitea.maximumdirect.net/eric/promptkit/internal/profile" -) - -const assetRoot = "assets" - -//go:embed assets/**/*.yml -var assets embed.FS - -func NewRepository() profile.Repository { - return profile.NewFSRepository(assets, assetRoot) -} diff --git a/internal/profile/builtin/repository_test.go b/internal/profile/builtin/repository_test.go deleted file mode 100644 index c9eb029..0000000 --- a/internal/profile/builtin/repository_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package builtin - -import ( - "context" - "encoding/json" - "errors" - "io" - "io/fs" - "os" - "path/filepath" - "reflect" - "strings" - "testing" - - "gitea.maximumdirect.net/eric/promptkit/internal/backend" - "gitea.maximumdirect.net/eric/promptkit/internal/domain" - "gopkg.in/yaml.v3" -) - -type catalogFixture struct { - Backends []backendFixture `json:"backends"` - Profiles []profileFixture `json:"profiles"` -} - -type backendFixture struct { - ID string `json:"id"` - Endpoint string `json:"endpoint"` - APIKeyEnv string `json:"api_key_env"` - ExtraParams map[string]any `json:"extra_params"` - ConcurrencyLimit int `json:"concurrency_limit"` - QueueCapacity int `json:"queue_capacity"` - QueueCapacitySet bool `json:"queue_capacity_set"` -} - -type profileFixture struct { - ID string `json:"id"` - BaseProfileID string `json:"base_profile"` - BackendID string `json:"backend"` - Endpoint string `json:"endpoint"` - Model string `json:"model"` - Temperature float64 `json:"temperature"` - MaxTokens int `json:"max_tokens"` - TopP float64 `json:"top_p"` - TimeoutSeconds int `json:"timeout_seconds"` - ServiceTier string `json:"service_tier"` - ReasoningEffort string `json:"reasoning_effort"` - APIKeyEnv string `json:"api_key_env"` - APIKeyRequired bool `json:"api_key_required"` - ExtraParams map[string]any `json:"extra_params"` -} - -func TestBuiltInCatalogMatchesCompatibilityFixture(t *testing.T) { - fixture := loadCatalogFixture(t) - assertCatalogFixtureOrdering(t, fixture) - - registry, err := backend.NewRegistry(nil) - if err != nil { - t.Fatalf("construct built-in backend registry: %v", err) - } - actualBackends := make(map[string]backendFixture) - for id := range registry.CapacityPolicies() { - definition, err := registry.GetBackend(id) - if err != nil { - t.Fatalf("load built-in backend %q: %v", id, err) - } - actualBackends[id] = backendFixtureFromDefinition(definition) - } - expectedBackends := make(map[string]backendFixture, len(fixture.Backends)) - for _, expected := range fixture.Backends { - expectedBackends[expected.ID] = expected - } - if !mapsEqual(actualBackends, expectedBackends) { - t.Fatalf("built-in backends differ from compatibility fixture: got %#v, want %#v", actualBackends, expectedBackends) - } - - profilePaths := loadBuiltInProfileIDs(t) - actualProfiles := make(map[string]profileFixture, len(profilePaths)) - for id := range profilePaths { - definition, err := NewRepository().GetProfile(context.Background(), id) - if err != nil { - t.Fatalf("load built-in profile %q: %v", id, err) - } - actualProfiles[id] = profileFixtureFromDefinition(*definition) - } - expectedProfiles := make(map[string]profileFixture, len(fixture.Profiles)) - for _, expected := range fixture.Profiles { - expectedProfiles[expected.ID] = expected - } - if !mapsEqual(actualProfiles, expectedProfiles) { - t.Fatalf("built-in profiles differ from compatibility fixture: got %#v, want %#v", actualProfiles, expectedProfiles) - } -} - -func loadCatalogFixture(t *testing.T) catalogFixture { - t.Helper() - - path := filepath.Join("..", "..", "..", "testdata", "builtin-catalog-v1.json") - data, err := os.ReadFile(path) - if err != nil { - t.Fatalf("read compatibility fixture: %v", err) - } - - decoder := json.NewDecoder(strings.NewReader(string(data))) - decoder.DisallowUnknownFields() - var fixture catalogFixture - if err := decoder.Decode(&fixture); err != nil { - t.Fatalf("decode compatibility fixture: %v", err) - } - if err := ensureJSONEnd(decoder); err != nil { - t.Fatalf("decode compatibility fixture: %v", err) - } - return fixture -} - -func ensureJSONEnd(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("fixture must contain exactly one JSON value") -} - -func assertCatalogFixtureOrdering(t *testing.T, fixture catalogFixture) { - t.Helper() - for index := 1; index < len(fixture.Backends); index++ { - if fixture.Backends[index-1].ID >= fixture.Backends[index].ID { - t.Fatalf("compatibility fixture backends are not sorted by ID") - } - } - for index := 1; index < len(fixture.Profiles); index++ { - if fixture.Profiles[index-1].ID >= fixture.Profiles[index].ID { - t.Fatalf("compatibility fixture profiles are not sorted by ID") - } - } -} - -func backendFixtureFromDefinition(definition domain.Backend) backendFixture { - return backendFixture{ - ID: definition.ID, - Endpoint: definition.Endpoint, - APIKeyEnv: definition.APIKeyEnv, - ExtraParams: definition.ExtraParams, - ConcurrencyLimit: definition.ConcurrencyLimit, - QueueCapacity: definition.QueueCapacity, - QueueCapacitySet: definition.QueueCapacitySet, - } -} - -func profileFixtureFromDefinition(definition domain.ExecutionProfile) profileFixture { - return profileFixture{ - ID: definition.ID, - BaseProfileID: definition.BaseProfileID, - BackendID: definition.BackendID, - Endpoint: definition.Endpoint, - Model: definition.Model, - Temperature: definition.Temperature, - MaxTokens: definition.MaxTokens, - TopP: definition.TopP, - TimeoutSeconds: definition.TimeoutSeconds, - ServiceTier: definition.ServiceTier, - ReasoningEffort: definition.ReasoningEffort, - APIKeyEnv: definition.APIKeyEnv, - APIKeyRequired: definition.APIKeyRequired, - ExtraParams: definition.ExtraParams, - } -} - -func mapsEqual[K comparable, V any](actual, expected map[K]V) bool { - return reflect.DeepEqual(actual, expected) -} - -func TestBuiltInProfilesValidateThroughRepository(t *testing.T) { - repo := NewRepository() - ids := loadBuiltInProfileIDs(t) - if len(ids) == 0 { - t.Fatal("expected built-in profiles") - } - - for id := range ids { - t.Run(id, func(t *testing.T) { - p, err := repo.GetProfile(context.Background(), id) - if err != nil { - t.Fatalf("expected built-in profile %q to load, got %v", id, err) - } - if p.ID != id { - t.Fatalf("expected profile id %q, got %q", id, p.ID) - } - if !builtInBackendIDs[p.BackendID] { - t.Fatalf("expected profile %q to select a maintained built-in, got %q", id, p.BackendID) - } - if p.Endpoint != "" || p.APIKeyEnv != "" { - t.Fatalf("expected profile %q to inherit backend connection settings, got endpoint=%q api_key_env=%q", id, p.Endpoint, p.APIKeyEnv) - } - }) - } -} - -func TestBuiltInProfilesDoNotContainDuplicateIDsOrRawAPIKeys(t *testing.T) { - loadBuiltInProfileIDs(t) -} - -func TestRakestrawhomeGemmaProfileUsesNativeDefaults(t *testing.T) { - p, err := NewRepository().GetProfile(context.Background(), "rakestrawhome-gemma-4-31b") - if err != nil { - t.Fatalf("load Rakestrawhome Gemma profile: %v", err) - } - if p.ID != "rakestrawhome-gemma-4-31b" || - p.BackendID != backend.RakestrawHomeID || - p.Model != "google/gemma-4-31b-it" || - p.Endpoint != "" || - p.Temperature != 0 || - p.MaxTokens != 0 || - p.TopP != 0 || - p.TimeoutSeconds != 0 || - p.ServiceTier != "" || - p.ReasoningEffort != "" || - p.APIKeyEnv != "" || - p.APIKeyRequired || - p.ExtraParams != nil { - t.Fatalf("unexpected Rakestrawhome Gemma profile: %#v", p) - } -} - -var builtInBackendIDs = map[string]bool{ - backend.OpenRouterID: true, - backend.RakestrawHomeID: true, -} - -func loadBuiltInProfileIDs(t *testing.T) map[string]string { - t.Helper() - - ids := map[string]string{} - err := fs.WalkDir(assets, assetRoot, func(name string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() || !strings.HasSuffix(name, ".yml") { - return nil - } - - data, err := assets.ReadFile(name) - if err != nil { - t.Fatalf("failed to read built-in profile %s: %v", name, err) - } - - var raw map[string]any - if err := yaml.Unmarshal(data, &raw); err != nil { - t.Fatalf("failed to decode built-in profile %s: %v", name, err) - } - if _, ok := raw["api_key"]; ok { - t.Fatalf("built-in profile %s contains raw api_key", name) - } - backendID, ok := raw["backend"].(string) - if !ok || !builtInBackendIDs[backendID] { - t.Fatalf("built-in profile %s does not select a maintained built-in: %#v", name, raw["backend"]) - } - if _, ok := raw["endpoint"]; ok { - t.Fatalf("built-in profile %s repeats endpoint", name) - } - if _, ok := raw["api_key_env"]; ok { - t.Fatalf("built-in profile %s repeats api_key_env", name) - } - id, ok := raw["id"].(string) - if !ok || strings.TrimSpace(id) == "" { - t.Fatalf("built-in profile %s has missing id", name) - } - if previous, ok := ids[id]; ok { - t.Fatalf("duplicate built-in profile id %q in %s and %s", id, previous, name) - } - ids[id] = name - return nil - }) - if err != nil { - t.Fatalf("failed to walk built-in profiles: %v", err) - } - return ids -}