Use external catalogs for maintained backends
This commit is contained in:
@@ -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
|
2. the ordinary configured source selected by a profile file, `fs.FS`, or
|
||||||
configured profile directory;
|
configured profile directory;
|
||||||
3. application fallback profiles supplied with `WithFallbackProfileFS`; and
|
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
|
A profile source supplies a complete definition; definitions and their fields
|
||||||
are not merged across sources. A higher-precedence source falls back only when
|
are not merged across sources. A higher-precedence source falls back only when
|
||||||
|
|||||||
@@ -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) |
|
| 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/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) |
|
| `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/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/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) |
|
| `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/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/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` | 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/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/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) |
|
| `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) |
|
||||||
|
|||||||
@@ -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
|
preserves safe source metadata including explicitly present YAML fields, and
|
||||||
publishes independently copied values from memory. It does not resolve profile
|
publishes independently copied values from memory. It does not resolve profile
|
||||||
inheritance. Configured consumer sources continue to use the lazy point lookup
|
inheritance. Configured consumer sources continue to use the lazy point lookup
|
||||||
repositories described above; engine assembly still uses the embedded built-in
|
repositories described above.
|
||||||
catalog at this point.
|
|
||||||
|
|
||||||
`internal/catalog` validates the imported immutable OpenRouter and
|
`internal/catalog` validates the imported immutable OpenRouter and
|
||||||
Rakestrawhome asset modules as one private adapter boundary. It enforces their
|
Rakestrawhome asset modules as one private adapter boundary. It enforces their
|
||||||
manifest, layout, profile ownership, inheritance, and secret-safety rules
|
manifest, layout, profile ownership, inheritance, and secret-safety rules
|
||||||
before returning raw catalog sources. The root engine continues to use the
|
before returning raw catalog sources. Root assembly uses those validated
|
||||||
embedded built-ins until runtime cutover.
|
catalogs as the maintained lowest-precedence profile source.
|
||||||
|
|
||||||
The overlay repository consults the next repository only when the
|
The overlay repository consults the next repository only when the
|
||||||
higher-precedence repository reports that a profile is absent. A reliably
|
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:
|
The root engine assembles one raw composite catalog in precedence order:
|
||||||
in-memory profiles, one ordinary configured source, an application fallback
|
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
|
source replaces `Config.ProfileDir` within the ordinary configured-source
|
||||||
category. One outer resolving repository wraps that complete raw catalog, so
|
category. One outer resolving repository wraps that complete raw catalog, so
|
||||||
each base lookup observes the same precedence and shadowing rules.
|
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
|
Prepared execution instead freezes the fully resolved target; a later ordinary
|
||||||
operation performs a fresh traversal.
|
operation performs a fresh traversal.
|
||||||
|
|
||||||
`internal/profile/builtin` embeds the maintained built-in profile catalog.
|
The maintained external catalog provides every built-in profile and its
|
||||||
Every embedded profile selects a maintained built-in backend and inherits that
|
matching backend definition. Profile loading and overlay behavior are owned by the
|
||||||
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
|
|
||||||
[profile repository tests](../../internal/profile/repository_test.go), while
|
[profile repository tests](../../internal/profile/repository_test.go), while
|
||||||
catalog completeness, the backend-selection invariant, and duplicate IDs are
|
catalog completeness, backend selection, and duplicate IDs are owned by the
|
||||||
owned by the
|
[catalog adapter tests](../../internal/catalog/catalog_test.go).
|
||||||
[built-in repository tests](../../internal/profile/builtin/repository_test.go).
|
|
||||||
|
|
||||||
## Ordinary Artifacts
|
## Ordinary Artifacts
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ The implemented internal components consist of:
|
|||||||
- `internal/domain`, which owns framework data values and source-neutral
|
- `internal/domain`, which owns framework data values and source-neutral
|
||||||
invariants shared by later internal components;
|
invariants shared by later internal components;
|
||||||
- `internal/backend`, which owns validated immutable OpenAI-compatible backend
|
- `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
|
- `internal/capacity`, which owns engine-local bounded run admission and
|
||||||
model-generation scheduling for limited backends;
|
model-generation scheduling for limited backends;
|
||||||
- `internal/defaults`, which owns application-neutral framework defaults and
|
- `internal/defaults`, which owns application-neutral framework defaults and
|
||||||
@@ -37,8 +37,6 @@ The implemented internal components consist of:
|
|||||||
filesystem and `fs.FS` sources;
|
filesystem and `fs.FS` sources;
|
||||||
- `internal/profile`, which loads, validates, and overlays execution profiles
|
- `internal/profile`, which loads, validates, and overlays execution profiles
|
||||||
from filesystem and `fs.FS` sources;
|
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/prompt`, which renders prompt messages from Go templates;
|
||||||
- `internal/artifact`, which resolves ordinary inline and unrestricted
|
- `internal/artifact`, which resolves ordinary inline and unrestricted
|
||||||
caller-selected file references;
|
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 root facade assembles one immutable backend registry, one capacity manager,
|
||||||
the internal repositories, renderer, validator, outbound client, and use-case
|
the internal repositories, renderer, validator, outbound client, and use-case
|
||||||
runner while translating public values and errors at the library boundary. The
|
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
|
The facade constructs the capacity manager from the registry's immutable
|
||||||
policy snapshot, wraps the selected built-in or injected model client, and
|
policy snapshot, wraps the selected built-in or injected model client, and
|
||||||
supplies bounded admission to the runner. The defaults and renderer depend on
|
supplies bounded admission to the runner. The defaults and renderer depend on
|
||||||
the domain model. Prompt-definition and profile repositories use the domain
|
the domain model. Prompt-definition and profile repositories use the domain
|
||||||
model, file catalog, and YAML decoder. The built-in profile repository supplies
|
model, file catalog, and YAML decoder. The catalog adapter validates imported
|
||||||
an embedded `fs.FS` to the profile package. Artifact reading uses the domain
|
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
|
model and application-neutral defaults. Validation uses the domain model, file
|
||||||
catalog, and JSON Schema implementation. The model client uses the domain
|
catalog, and JSON Schema implementation. The model client uses the domain
|
||||||
model, application-neutral defaults, and an injected or standard-library HTTP
|
model, application-neutral defaults, and an injected or standard-library HTTP
|
||||||
|
|||||||
@@ -503,7 +503,7 @@ source as the active implementation.
|
|||||||
|
|
||||||
## Stage 6: Cut Promptkit Over To The External Catalogs
|
## Stage 6: Cut Promptkit Over To The External Catalogs
|
||||||
|
|
||||||
**Status:** Pending
|
**Status:** Complete
|
||||||
|
|
||||||
### Repository
|
### Repository
|
||||||
|
|
||||||
|
|||||||
20
engine.go
20
engine.go
@@ -11,14 +11,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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"
|
artifactadapter "gitea.maximumdirect.net/eric/promptkit/internal/artifact"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/backend"
|
"gitea.maximumdirect.net/eric/promptkit/internal/backend"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/capacity"
|
"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/defaults"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/llm"
|
"gitea.maximumdirect.net/eric/promptkit/internal/llm"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/profile"
|
"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/prompt"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/promptdef"
|
"gitea.maximumdirect.net/eric/promptkit/internal/promptdef"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/usecase"
|
"gitea.maximumdirect.net/eric/promptkit/internal/usecase"
|
||||||
@@ -390,9 +392,17 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
|
|||||||
promptDefs = promptdef.NewFilesystemRepository(cfg.PromptDir)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: failed to construct backend registry: %v", ErrInvalidConfig, err)
|
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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProfileRepository(profileDir string, options engineOptions) profile.Repository {
|
func newProfileRepository(profileDir string, options engineOptions, maintained profile.Repository) profile.Repository {
|
||||||
repository := builtin.NewRepository()
|
repository := maintained
|
||||||
|
|
||||||
if options.fallbackProfileSource {
|
if options.fallbackProfileSource {
|
||||||
repository = profile.NewOverlayRepository(options.fallbackProfiles, repository)
|
repository = profile.NewOverlayRepository(options.fallbackProfiles, repository)
|
||||||
|
|||||||
@@ -1545,31 +1545,21 @@ func TestPrepareUsesBuiltInProfileWithoutProfileDir(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
profileID string
|
profileID string
|
||||||
backendID string
|
backendID string
|
||||||
endpoint string
|
|
||||||
apiKeyEnv string
|
|
||||||
model string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "OpenRouter",
|
name: "OpenRouter",
|
||||||
profileID: "mistral-small-3",
|
profileID: "mistral-small-3",
|
||||||
backendID: promptkit.BackendOpenRouter,
|
backendID: promptkit.BackendOpenRouter,
|
||||||
endpoint: "https://openrouter.ai/api/v1",
|
|
||||||
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
||||||
model: "mistralai/mistral-small-3.2-24b-instruct",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Rakestrawhome",
|
name: "Rakestrawhome",
|
||||||
profileID: "rakestrawhome-gemma-4-31b",
|
profileID: "rakestrawhome-gemma-4-31b",
|
||||||
backendID: promptkit.BackendRakestrawHome,
|
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 {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Setenv(tc.apiKeyEnv, "test-key")
|
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{
|
engine, err := promptkit.NewEngine(promptkit.Config{
|
||||||
PromptDir: frameworkPromptDir,
|
PromptDir: frameworkPromptDir,
|
||||||
SchemaDir: frameworkSchemaDir,
|
SchemaDir: frameworkSchemaDir,
|
||||||
@@ -1581,6 +1571,7 @@ func TestPrepareUsesBuiltInProfileWithoutProfileDir(t *testing.T) {
|
|||||||
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
|
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
|
||||||
PromptID: frameworkMarkdownSummaryPromptID,
|
PromptID: frameworkMarkdownSummaryPromptID,
|
||||||
ProfileID: tc.profileID,
|
ProfileID: tc.profileID,
|
||||||
|
APIKey: "test-key",
|
||||||
Inputs: map[string]promptkit.ArtifactRef{
|
Inputs: map[string]promptkit.ArtifactRef{
|
||||||
"transcript": promptkit.Inline("Rin opens the gate."),
|
"transcript": promptkit.Inline("Rin opens the gate."),
|
||||||
"glossary": promptkit.Inline("gate: A guarded passage."),
|
"glossary": promptkit.Inline("gate: A guarded passage."),
|
||||||
@@ -1591,10 +1582,7 @@ func TestPrepareUsesBuiltInProfileWithoutProfileDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if prepared.SelectedProfileID != tc.profileID ||
|
if prepared.SelectedProfileID != tc.profileID ||
|
||||||
prepared.SelectedBackendID != tc.backendID ||
|
prepared.SelectedBackendID != tc.backendID ||
|
||||||
prepared.EffectiveModelParams.BackendID != tc.backendID ||
|
prepared.EffectiveModelParams.BackendID != tc.backendID {
|
||||||
prepared.EffectiveModelParams.Endpoint != tc.endpoint ||
|
|
||||||
prepared.EffectiveModelParams.APIKeyEnv != tc.apiKeyEnv ||
|
|
||||||
prepared.EffectiveModelParams.Model != tc.model {
|
|
||||||
t.Fatalf("unexpected built-in preparation: %#v", prepared)
|
t.Fatalf("unexpected built-in preparation: %#v", prepared)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,15 +22,6 @@ const (
|
|||||||
// backend.
|
// backend.
|
||||||
RakestrawHomeID = "rakestrawhome"
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,16 +35,15 @@ type Registry struct {
|
|||||||
backends map[string]domain.Backend
|
backends map[string]domain.Backend
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRegistry constructs a registry containing the built-in definitions
|
// NewRegistry constructs a registry containing maintained definitions followed
|
||||||
// followed by the supplied additions. Every ID must be unique.
|
// by consumer additions. Every ID must be unique across both groups.
|
||||||
func NewRegistry(additions []domain.Backend) (*Registry, error) {
|
func NewRegistry(maintained, additions []domain.Backend) (*Registry, error) {
|
||||||
builtIns := builtInBackends()
|
|
||||||
registry := &Registry{
|
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 := make([]domain.Backend, 0, len(maintained)+len(additions))
|
||||||
definitions = append(definitions, builtIns...)
|
definitions = append(definitions, maintained...)
|
||||||
definitions = append(definitions, additions...)
|
definitions = append(definitions, additions...)
|
||||||
|
|
||||||
for _, definition := range definitions {
|
for _, definition := range definitions {
|
||||||
@@ -75,23 +65,6 @@ func NewRegistry(additions []domain.Backend) (*Registry, error) {
|
|||||||
return registry, nil
|
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.
|
// GetBackend returns a defensive copy of the backend registered with id.
|
||||||
func (r *Registry) GetBackend(id string) (domain.Backend, error) {
|
func (r *Registry) GetBackend(id string) (domain.Backend, error) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package backend_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -11,48 +12,24 @@ import (
|
|||||||
|
|
||||||
const validEndpoint = "https://backend.example/v1"
|
const validEndpoint = "https://backend.example/v1"
|
||||||
|
|
||||||
func TestRegistryIncludesExactBuiltInDefinitions(t *testing.T) {
|
func TestRegistryIncludesMaintainedDefinitions(t *testing.T) {
|
||||||
registry, err := backend.NewRegistry(nil)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("construct registry: %v", err)
|
t.Fatalf("construct registry: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
for _, expected := range maintained {
|
||||||
name string
|
t.Run(expected.ID, func(t *testing.T) {
|
||||||
id string
|
definition, err := registry.GetBackend(expected.ID)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("look up built-in: %v", err)
|
t.Fatalf("look up maintained definition: %v", err)
|
||||||
}
|
}
|
||||||
if definition.ID != tc.id ||
|
if !reflect.DeepEqual(definition, expected) {
|
||||||
definition.Endpoint != tc.endpoint ||
|
t.Fatalf("unexpected maintained definition: %#v", definition)
|
||||||
definition.APIKeyEnv != tc.apiKeyEnv ||
|
|
||||||
definition.ConcurrencyLimit != tc.concurrent ||
|
|
||||||
definition.QueueCapacity != 1024 ||
|
|
||||||
!definition.QueueCapacitySet ||
|
|
||||||
definition.ExtraParams != nil {
|
|
||||||
t.Fatalf("unexpected built-in definition: %#v", definition)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -60,12 +37,12 @@ func TestRegistryIncludesExactBuiltInDefinitions(t *testing.T) {
|
|||||||
policies := registry.CapacityPolicies()
|
policies := registry.CapacityPolicies()
|
||||||
if len(policies) != 2 ||
|
if len(policies) != 2 ||
|
||||||
policies[backend.OpenRouterID] != (domain.BackendCapacityPolicy{
|
policies[backend.OpenRouterID] != (domain.BackendCapacityPolicy{
|
||||||
ConcurrencyLimit: 16,
|
ConcurrencyLimit: 2,
|
||||||
QueueCapacity: 1024,
|
QueueCapacity: 3,
|
||||||
}) ||
|
}) ||
|
||||||
policies[backend.RakestrawHomeID] != (domain.BackendCapacityPolicy{
|
policies[backend.RakestrawHomeID] != (domain.BackendCapacityPolicy{
|
||||||
ConcurrencyLimit: 4,
|
ConcurrencyLimit: 4,
|
||||||
QueueCapacity: 1024,
|
QueueCapacity: 5,
|
||||||
}) {
|
}) {
|
||||||
t.Fatalf("unexpected built-in capacity policies: %#v", policies)
|
t.Fatalf("unexpected built-in capacity policies: %#v", policies)
|
||||||
}
|
}
|
||||||
@@ -77,7 +54,7 @@ func TestRegistryNormalizesUniqueAdditionsAndIsolatesMutations(t *testing.T) {
|
|||||||
"count": int64(7),
|
"count": int64(7),
|
||||||
"nested": nested,
|
"nested": nested,
|
||||||
}
|
}
|
||||||
registry, err := backend.NewRegistry([]domain.Backend{
|
registry, err := backend.NewRegistry(nil, []domain.Backend{
|
||||||
{
|
{
|
||||||
ID: " custom ",
|
ID: " custom ",
|
||||||
Endpoint: " https://custom.example/openai/v1 ",
|
Endpoint: " https://custom.example/openai/v1 ",
|
||||||
@@ -140,12 +117,11 @@ func TestRegistryNormalizesUniqueAdditionsAndIsolatesMutations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
policies := registry.CapacityPolicies()
|
policies := registry.CapacityPolicies()
|
||||||
if len(policies) != 3 {
|
if len(policies) != 1 {
|
||||||
t.Fatalf("unexpected capacity policy count: %#v", policies)
|
t.Fatalf("unexpected capacity policy count: %#v", policies)
|
||||||
}
|
}
|
||||||
policies["custom"] = domain.BackendCapacityPolicy{}
|
policies["custom"] = domain.BackendCapacityPolicy{}
|
||||||
delete(policies, backend.OpenRouterID)
|
delete(policies, "custom")
|
||||||
delete(policies, backend.RakestrawHomeID)
|
|
||||||
againPolicies := registry.CapacityPolicies()
|
againPolicies := registry.CapacityPolicies()
|
||||||
if againPolicies["custom"] != (domain.BackendCapacityPolicy{
|
if againPolicies["custom"] != (domain.BackendCapacityPolicy{
|
||||||
ConcurrencyLimit: 3,
|
ConcurrencyLimit: 3,
|
||||||
@@ -153,12 +129,6 @@ func TestRegistryNormalizesUniqueAdditionsAndIsolatesMutations(t *testing.T) {
|
|||||||
}) {
|
}) {
|
||||||
t.Fatalf("capacity policy map mutated registry state: %#v", againPolicies)
|
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) {
|
func TestNewRegistryNormalizesCapacityPolicy(t *testing.T) {
|
||||||
@@ -234,7 +204,7 @@ func TestNewRegistryNormalizesCapacityPolicy(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
tc.definition.ID = "custom"
|
tc.definition.ID = "custom"
|
||||||
tc.definition.Endpoint = validEndpoint
|
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 tc.wantError {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected invalid capacity policy error")
|
t.Fatal("expected invalid capacity policy error")
|
||||||
@@ -298,7 +268,7 @@ func TestNewRegistryRejectsDuplicateIDs(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
_, err := backend.NewRegistry(tc.additions)
|
_, err := backend.NewRegistry(nil, tc.additions)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected duplicate ID error")
|
t.Fatal("expected duplicate ID error")
|
||||||
}
|
}
|
||||||
@@ -312,7 +282,7 @@ func TestNewRegistryRejectsDuplicateIDs(t *testing.T) {
|
|||||||
func TestNewRegistryValidatesIDs(t *testing.T) {
|
func TestNewRegistryValidatesIDs(t *testing.T) {
|
||||||
for _, id := range []string{"", " \t\n "} {
|
for _, id := range []string{"", " \t\n "} {
|
||||||
t.Run(id, func(t *testing.T) {
|
t.Run(id, func(t *testing.T) {
|
||||||
_, err := backend.NewRegistry([]domain.Backend{{
|
_, err := backend.NewRegistry(nil, []domain.Backend{{
|
||||||
ID: id,
|
ID: id,
|
||||||
Endpoint: validEndpoint,
|
Endpoint: validEndpoint,
|
||||||
}})
|
}})
|
||||||
@@ -341,7 +311,7 @@ func TestNewRegistryValidatesEndpoints(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
_, err := backend.NewRegistry([]domain.Backend{{
|
_, err := backend.NewRegistry(nil, []domain.Backend{{
|
||||||
ID: "custom",
|
ID: "custom",
|
||||||
Endpoint: tc.endpoint,
|
Endpoint: tc.endpoint,
|
||||||
}})
|
}})
|
||||||
@@ -355,7 +325,7 @@ func TestNewRegistryValidatesEndpoints(t *testing.T) {
|
|||||||
func TestNewRegistryValidatesEnvironmentVariableNames(t *testing.T) {
|
func TestNewRegistryValidatesEnvironmentVariableNames(t *testing.T) {
|
||||||
for _, name := range []string{"1API_KEY", "API-KEY", "API KEY", "ÅPI_KEY"} {
|
for _, name := range []string{"1API_KEY", "API-KEY", "API KEY", "ÅPI_KEY"} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
_, err := backend.NewRegistry([]domain.Backend{{
|
_, err := backend.NewRegistry(nil, []domain.Backend{{
|
||||||
ID: "custom",
|
ID: "custom",
|
||||||
Endpoint: validEndpoint,
|
Endpoint: validEndpoint,
|
||||||
APIKeyEnv: name,
|
APIKeyEnv: name,
|
||||||
@@ -378,7 +348,7 @@ func TestNewRegistryRejectsInvalidAndReservedExtraParameters(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
_, err := backend.NewRegistry([]domain.Backend{{
|
_, err := backend.NewRegistry(nil, []domain.Backend{{
|
||||||
ID: "custom",
|
ID: "custom",
|
||||||
Endpoint: validEndpoint,
|
Endpoint: validEndpoint,
|
||||||
ExtraParams: tc.extraParams,
|
ExtraParams: tc.extraParams,
|
||||||
@@ -391,7 +361,7 @@ func TestNewRegistryRejectsInvalidAndReservedExtraParameters(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRegistryLookupReportsNotFound(t *testing.T) {
|
func TestRegistryLookupReportsNotFound(t *testing.T) {
|
||||||
registry, err := backend.NewRegistry(nil)
|
registry, err := backend.NewRegistry(nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("construct registry: %v", err)
|
t.Fatalf("construct registry: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
id: claude-fable-latest
|
|
||||||
backend: openrouter
|
|
||||||
model: "~anthropic/claude-fable-latest"
|
|
||||||
reasoning_effort: high
|
|
||||||
timeout_seconds: 600
|
|
||||||
service_tier: flex
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
id: claude-haiku-latest
|
|
||||||
backend: openrouter
|
|
||||||
model: "~anthropic/claude-haiku-latest"
|
|
||||||
reasoning_effort: medium
|
|
||||||
timeout_seconds: 240
|
|
||||||
service_tier: flex
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
id: claude-opus-latest
|
|
||||||
backend: openrouter
|
|
||||||
model: "~anthropic/claude-opus-latest"
|
|
||||||
reasoning_effort: high
|
|
||||||
timeout_seconds: 240
|
|
||||||
service_tier: flex
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
id: claude-sonnet-latest
|
|
||||||
backend: openrouter
|
|
||||||
model: "~anthropic/claude-sonnet-latest"
|
|
||||||
reasoning_effort: high
|
|
||||||
timeout_seconds: 240
|
|
||||||
service_tier: flex
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
id: deepseek-3-2
|
|
||||||
backend: openrouter
|
|
||||||
model: deepseek/deepseek-v3.2
|
|
||||||
reasoning_effort: high
|
|
||||||
timeout_seconds: 180
|
|
||||||
service_tier: flex
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
id: deepseek-4-flash
|
|
||||||
backend: openrouter
|
|
||||||
model: deepseek/deepseek-v4-flash
|
|
||||||
#reasoning_effort: medium
|
|
||||||
timeout_seconds: 180
|
|
||||||
service_tier: flex
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
id: deepseek-4-pro
|
|
||||||
backend: openrouter
|
|
||||||
model: deepseek/deepseek-v4-pro
|
|
||||||
reasoning_effort: high
|
|
||||||
timeout_seconds: 180
|
|
||||||
service_tier: flex
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
id: rakestrawhome-gemma-4-31b
|
|
||||||
backend: rakestrawhome
|
|
||||||
model: google/gemma-4-31b-it
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user