diff --git a/docs/consumers/pkg-promptkit.md b/docs/consumers/pkg-promptkit.md index 2cf8bd5..4dca6ad 100644 --- a/docs/consumers/pkg-promptkit.md +++ b/docs/consumers/pkg-promptkit.md @@ -189,6 +189,26 @@ For programmatic profiles, [`OpenAICompatibleProfile`](../../profiles.go) converts ordinary OpenAI-compatible settings into a value accepted by `WithProfiles`. +### Alias A Built-In Profile + +Give an application-owned profile ID a built-in base when prompts should select +the application ID while inheriting the built-in target. The child can override +only the setting it owns: + +```go +promptkit.WithProfiles(promptkit.Profile{ + ID: "weather-light", + BaseProfileID: "deepseek-4-flash", + ReasoningEffort: "high", +}) +``` + +Select `weather-light` in a prompt or `RunRequest.ProfileID`; it remains the +reported selected profile. See the [profile inheritance format +reference](../formats.md#profile-inheritance) and the +[`Profile` GoDoc](../../types.go) for exact lookup, merging, and validation +behavior. + ### Use The Rakestrawhome Built-In Profile Set `RAKESTRAWHOME_INFERENCE_API_KEY` in the application environment, then diff --git a/docs/formats.md b/docs/formats.md index fa5d1a4..570efae 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -173,9 +173,19 @@ extra_params: provider_option: enabled ``` +A derived profile can use a named base and override only the settings it owns: + +```yaml +id: local-summary-fast +base_profile: local-summary +timeout_seconds: 30 +reasoning_effort: low +``` + | Field | Required | Meaning | | --- | --- | --- | | `id` | yes | Profile identifier, trimmed before selection and publication. It must be non-empty after trimming and unique within one source after normalization. | +| `base_profile` | no | One optional parent profile ID. A derived profile may inherit target fields from it. | | `backend` | unless `endpoint` is present | Backend registry ID. It is trimmed and registry membership is checked when the profile is prepared or inspected. | | `endpoint` | unless `backend` is present | OpenAI-compatible base URL, including an API version path when required. A nonempty value is trimmed and must be absolute HTTP or HTTPS with a host and without user information, a query, or a fragment. When both connection fields are present, this overrides the backend endpoint without changing backend identity. | | `model` | yes | Non-empty provider model name. | @@ -191,6 +201,11 @@ extra_params: Raw `api_key` is prohibited in profile YAML. Store only an environment variable name in `api_key_env`. +A standalone profile must provide a model and at least one of `backend` or +`endpoint`. A derived profile may omit those target fields because its selected +base chain can provide them. Local parsing still validates a derived profile's +own ID, supplied endpoint, execution-setting bounds, and `extra_params`. + Promptkit does not infer a backend from a model or endpoint. Endpoint-only profiles remain supported and have no effective backend ID. The engine always provides the built-in `openrouter` and `rakestrawhome` IDs. @@ -266,6 +281,32 @@ profiles. They use `APIKeyRequired` for request-scoped credentials instead of `api_key_env`. Preparation and exact profile inspection use this same source precedence. +When a selected definition names `base_profile`, every profile ID in that +chain is looked up through this same precedence order. A higher-precedence +definition therefore shadows a lower-precedence definition of the same base +ID, including a built-in. References are not source-qualified. + +### Profile Inheritance + +Promptkit resolves one linear base chain of at most 32 profiles, including the +selected profile. It merges settings from the root base to the selected leaf. +The leaf's `id` remains the selected profile identity. Nonblank string fields +(`backend`, `endpoint`, `model`, `service_tier`, `reasoning_effort`, and +`api_key_env`) and nonzero numeric fields replace inherited values. A nonempty +`extra_params` map replaces the complete inherited map rather than merging +keys, and `APIKeyRequired: true` remains true through the chain. Backend and +endpoint are independent: replacing one does not clear the other. + +There is no profile-level clearing syntax. Blank strings, zero numbers, false, +and empty maps remain unspecified and inherit from a base. Use existing +presence-aware request overrides where an execution needs an explicit zero or +empty reasoning setting. + +An absent directly selected profile reports the ordinary not-found error. Once +the selected profile exists, a missing base, cycle, overlong chain, or +incomplete resolved target is a profile-load failure. Ordinary operations +resolve chains afresh; prepared execution retains the fully resolved target. + ## Built-In Profile Catalog Every built-in profile selects one maintained built-in backend and inherits diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 3171984..d844bdd 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -21,7 +21,7 @@ contributor workflow and validation. | `internal/filecatalog` | Provides deterministic YAML discovery and path helpers for operating-system filesystems and `fs.FS` sources. | [File catalog](../../internal/filecatalog/catalog.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/profile` | Loads strictly decoded, validated execution profiles, including backend selection, from filesystem and `fs.FS` sources and composes repositories with error-preserving fallback. | [Framework formats](../formats.md), [profile repositories](../../internal/profile/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) | diff --git a/docs/internal/sources.md b/docs/internal/sources.md index 60952da..7f6e280 100644 --- a/docs/internal/sources.md +++ b/docs/internal/sources.md @@ -39,17 +39,17 @@ duplicate detection, and source containment: ## Profiles And Built-Ins -`internal/profile` loads and validates execution profiles from an -operating-system filesystem or an `fs.FS`. A file contains exactly one YAML -document and its trimmed YAML `id` is its only selection identity; filenames do -not confer authority. Each point lookup reads discovered files once for their -metadata and reuses the selected file's bytes for strict decoding; unrelated -profiles are not fully decoded. Strict selected decoding recognizes the -optional `backend` field, trims its value, and requires a model plus at least -one non-blank backend or endpoint. File-backed `extra_params` values are -validated and defensively copied through the shared bounded JSON-value owner -before a profile is published. OpenAI-compatible reserved-field policy remains -with the model-client and backend-registry owners. +`internal/profile` loads, locally validates, overlays, and resolves execution +profiles from an operating-system filesystem or an `fs.FS`. A file contains +exactly one YAML document and its trimmed YAML `id` is its only selection +identity; filenames do not confer authority. Each point lookup reads discovered +files once for their metadata and reuses the selected file's bytes for strict +decoding; unrelated profiles are not fully decoded. Strict selected decoding +recognizes `base_profile` and the optional `backend` field, trims their values, +and permits inherited target fields only when a base is named. File-backed +`extra_params` values are validated and defensively copied through the shared +bounded JSON-value owner before a profile is published. OpenAI-compatible +reserved-field policy remains with the model-client and backend-registry owners. The overlay repository consults the next repository only when the higher-precedence repository reports that a profile is absent. A reliably @@ -59,14 +59,25 @@ backend registry membership because the available registry belongs to the assembled engine; the runner checks membership during preparation and exact profile inspection. -The root engine assembles profile repositories 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 -replaces `Config.ProfileDir` within the ordinary configured-source category. +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 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. -Exact profile inspection performs one point-in-time lookup through those -profile sources and checks the resolved target without reading prompt, input, +The resolving repository traverses every selected chain afresh, retains no +cache, detects cycles, limits a chain to 32 profiles, merges root-to-leaf into a +new caller-owned value, and validates the final target before publishing it. It +does not check backend registry membership. Exact `base_profile` syntax, merge +rules, and consumer-visible failure behavior belong to the [framework format +reference](../formats.md#profile-inheritance). + +Exact profile inspection performs one point-in-time resolved lookup through +those profile sources and checks the final target without reading prompt, input, 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 diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 77b7db9..08b05d2 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -467,6 +467,8 @@ Stage 4 is complete when all focused and repository-wide checks pass and the implementation, GoDoc, format reference, internal documentation, and consumer guidance agree. +**Status:** Complete. + ## Open Questions None. Naming, supported sources, lookup precedence, merge behavior, connection diff --git a/engine.go b/engine.go index 44a9638..4673e37 100644 --- a/engine.go +++ b/engine.go @@ -304,10 +304,12 @@ func WithFallbackProfileFS(fsys fs.FS, root string) Option { // WithProfiles configures in-memory profiles that take precedence over // ordinary configured, application fallback, and built-in profiles. // -// NewEngine validates and copies every profile. IDs must be unique within one -// call. An invalid profile, duplicate ID, or unsupported ExtraParams value -// makes construction fail with ErrInvalidConfig. Repeating WithProfiles -// replaces the complete earlier in-memory set rather than merging it. +// NewEngine locally validates and copies every profile. IDs must be unique +// within one call. An invalid local definition, duplicate ID, or unsupported +// ExtraParams value makes construction fail with ErrInvalidConfig. A derived +// profile's base reference and resolved target completeness are checked when it +// is selected or inspected. Repeating WithProfiles replaces the complete +// earlier in-memory set rather than merging it. func WithProfiles(profiles ...Profile) Option { return optionFunc(func(options *engineOptions) error { repo, err := newMemoryProfileRepository(profiles) diff --git a/profiles.go b/profiles.go index 36dd823..7ed7218 100644 --- a/profiles.go +++ b/profiles.go @@ -9,8 +9,9 @@ import ( "gitea.maximumdirect.net/eric/promptkit/internal/profile" ) -// OpenAICompatibleProfile returns an ordinary in-memory Profile for an -// OpenAI-compatible chat-completions endpoint. +// OpenAICompatibleProfile returns an in-memory Profile for an OpenAI-compatible +// chat-completions endpoint. A non-blank BaseProfileID permits its target +// fields to be inherited when the profile is selected or inspected. // // It does not register global state, maintain a model catalog, or resolve // credentials. If APIKeyRequired is true, callers satisfy it with diff --git a/types.go b/types.go index c73ca82..4c7b74c 100644 --- a/types.go +++ b/types.go @@ -443,18 +443,22 @@ type ExecutionTargetOverride struct { // Profile is an in-memory execution profile for library consumers. // -// It is equivalent to a loaded profile file after validation. Raw API keys do -// not belong in profiles; use APIKeyRequired to require callers to provide a -// RunRequest.APIKey or explicit request ExecutionTargetOverride.APIKeyEnv, or -// use profile YAML api_key_env with file and FS profile sources. Profile has no -// stable JSON representation. +// A standalone Profile is equivalent to a loaded profile file after local +// validation. A derived profile names BaseProfileID and can inherit target +// fields when selected or inspected. Raw API keys do not belong in profiles; +// use APIKeyRequired to require callers to provide a RunRequest.APIKey or +// explicit request ExecutionTargetOverride.APIKeyEnv, or use profile YAML +// api_key_env with file and FS profile sources. Profile has no stable JSON +// representation. // -// WithProfiles validates and copies Profile values during NewEngine. Zero -// Temperature, MaxTokens, and TopP values and blank ServiceTier and -// ReasoningEffort values leave those provider controls unspecified. A zero -// TimeoutSeconds retains the framework deadline, while an empty ExtraParams map -// inherits backend request defaults. Use ExecutionTargetOverride pointer fields -// to request an explicit numeric zero. +// WithProfiles locally validates and copies Profile values during NewEngine. +// It checks base-reference existence and resolved target completeness when a +// derived profile is selected or inspected. Zero Temperature, MaxTokens, and +// TopP values and blank ServiceTier and ReasoningEffort values leave those +// provider controls unspecified. A zero TimeoutSeconds retains the framework +// deadline, while an empty ExtraParams map inherits backend request defaults. +// Use ExecutionTargetOverride pointer fields to request an explicit numeric +// zero. type Profile struct { // ID is the required non-blank profile identifier. WithProfiles trims it. ID string @@ -468,12 +472,14 @@ type Profile struct { // Backend membership is checked when a request selects the profile; an // unknown ID makes preparation fail with ErrProfileLoad. BackendID string - // Endpoint is the model-provider base URL. It is required only when - // BackendID is blank and otherwise overrides the backend endpoint when + // Endpoint is the model-provider base URL. A standalone Profile requires an + // endpoint when BackendID is blank; a derived Profile may inherit either + // field. A non-blank endpoint overrides the backend endpoint when // non-blank. WithProfiles trims it and requires an absolute HTTP or HTTPS URL // with a host and no user information, query, or fragment. Endpoint string - // Model is the required non-blank provider model identifier. + // Model is the provider model identifier. It is required for a standalone + // Profile and may be inherited by a derived Profile. Model string // Temperature is from 0 through 2. Zero leaves the provider control // unspecified. @@ -505,14 +511,15 @@ type Profile struct { // profile. // // It contains ordinary profile fields for OpenAI-compatible chat-completions -// endpoints. APIKeyRequired follows Profile.APIKeyRequired. Raw API keys do not -// belong in this config. OpenAICompatibleProfileConfig has no stable JSON +// endpoints. BaseProfileID and APIKeyRequired follow Profile. Raw API keys do +// not belong in this config. OpenAICompatibleProfileConfig has no stable JSON // representation and is not validated until its resulting Profile is supplied // through WithProfiles to NewEngine. type OpenAICompatibleProfileConfig struct { // ID becomes Profile.ID. ID string - // BaseProfileID becomes Profile.BaseProfileID. + // BaseProfileID becomes Profile.BaseProfileID. A non-blank value permits the + // resulting Profile to inherit target fields when it is selected or inspected. BaseProfileID string // BackendID becomes Profile.BackendID. BackendID string