Move profile composition to the engine facade

This commit is contained in:
2026-08-01 12:31:50 +00:00
parent ae2179d103
commit 01ca5430bd
9 changed files with 442 additions and 375 deletions

View File

@@ -197,18 +197,18 @@ The framework baseline is:
| Setting | Default | | Setting | Default |
| --- | --- | | --- | --- |
| `temperature` | Unspecified and omitted from compatible provider requests unless a backend, profile, or runtime override selects it. | | `temperature` | Unspecified and omitted from compatible provider requests unless a profile or runtime override selects it. |
| `max_tokens` | Unspecified and omitted from compatible provider requests unless a backend, profile, or runtime override selects it. | | `max_tokens` | Unspecified and omitted from compatible provider requests unless a profile or runtime override selects it. |
| `top_p` | Unspecified and omitted from compatible provider requests unless a backend, profile, or runtime override selects it. | | `top_p` | Unspecified and omitted from compatible provider requests unless a profile or runtime override selects it. |
| `timeout_seconds` | `600` | | `timeout_seconds` | `600` |
Numeric zero in a file or in-memory profile means that the profile does not Numeric zero in a file or in-memory profile does not select a numeric value.
replace a lower-precedence value. With no lower value, `temperature`, For `temperature`, `max_tokens`, and `top_p`, it leaves the provider control
`max_tokens`, and `top_p` remain unspecified. Numeric request overrides use unspecified. For `timeout_seconds`, it retains the framework deadline. Numeric
pointers, so an explicit zero is retained and sent to compatible providers. request overrides use pointers, so an explicit zero is retained and sent to
In particular, an explicit request `timeout_seconds` of zero disables the compatible providers. In particular, an explicit request `timeout_seconds` of
per-generation deadline while leaving the caller context and transport timeout zero disables the per-generation deadline while leaving the caller context and
intact. transport timeout intact.
Non-empty profile strings replace backend defaults, and non-empty request Non-empty profile strings replace backend defaults, and non-empty request
strings replace both. Request reasoning is the exception: a nil strings replace both. Request reasoning is the exception: a nil

View File

@@ -53,9 +53,9 @@ never also sent as a session header.
The client conditionally includes: The client conditionally includes:
- `temperature`, `max_tokens`, and `top_p` only when selected by a backend, - `temperature`, `max_tokens`, and `top_p` only when selected by a profile or
profile, or runtime override, including an explicit runtime zero; they are runtime override, including an explicit runtime zero; they are absent when
absent when unspecified; unspecified;
- non-empty `service_tier` and effective `reasoning_effort`; an explicitly - non-empty `service_tier` and effective `reasoning_effort`; an explicitly
disabled reasoning setting is empty and therefore omitted; and disabled reasoning setting is empty and therefore omitted; and
- `response_format` for JSON Schema structured output, including its name, - `response_format` for JSON Schema structured output, including its name,

View File

@@ -11,7 +11,7 @@ contributor workflow and validation.
| Component | Implemented responsibility | References | | Component | Implemented responsibility | References |
| --- | --- | --- | | --- | --- | --- |
| 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 errors, public error mapping, and engine-local 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 errors, public error mapping, and engine-local profile-source 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 built-in OpenRouter definition 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 the built-in OpenRouter definition 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) |
@@ -22,7 +22,7 @@ contributor workflow and validation.
| `internal/jsonvalue` | Validates and deeply copies JSON-compatible extra-parameter and prepared-schema trees while preserving supported concrete value types. | [JSON values](../../internal/jsonvalue/jsonvalue.go) | | `internal/jsonvalue` | Validates and deeply copies JSON-compatible extra-parameter and prepared-schema trees while preserving supported concrete value types. | [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, 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, 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/builtin` | Embeds the built-in profile catalog, whose entries select OpenRouter, and combines it with an optional primary repository. | [Built-in catalog](../formats.md#built-in-profile-catalog), [repository](../../internal/profile/builtin/repository.go) | | `internal/profile/builtin` | Embeds the built-in profile catalog, whose entries select OpenRouter. | [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 frozen validation plans for prepared execution. | [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 frozen validation plans for prepared execution. | [Framework formats](../formats.md#schemas), [internal sources and validation](sources.md) |

View File

@@ -28,26 +28,30 @@ duplicate detection, and source containment:
## Profiles And Built-Ins ## Profiles And Built-Ins
`internal/profile` loads and validates execution profiles from an `internal/profile` loads and validates execution profiles from an
operating-system filesystem or an `fs.FS`. It supports a primary repository operating-system filesystem or an `fs.FS`. Its overlay repository consults the
with fallback only when the primary reports that a profile is absent. Strict next repository only when the higher-precedence repository reports that a
YAML decoding recognizes the optional `backend` field, trims its value, and profile is absent. Strict YAML decoding recognizes the optional `backend`
requires a model plus at least one non-blank backend or endpoint. Loading does field, trims its value, and requires a model plus at least one non-blank
not check registry membership because the available registry belongs to the backend or endpoint. Loading does not check registry membership because the
assembled engine; the runner checks membership during preparation and exact available registry belongs to the assembled engine; the runner checks
profile inspection. membership during preparation and exact profile inspection.
The root engine assembles profile repositories in precedence order: in-memory
profiles, one ordinary configured source, then the embedded built-in catalog.
An explicit file or `fs.FS` profile source replaces `Config.ProfileDir` within
the ordinary configured-source category.
Exact profile inspection performs one point-in-time lookup through those Exact profile inspection performs one point-in-time lookup through those
profile sources and checks the resolved target without reading prompt, input, profile sources and checks the resolved target without reading prompt, input,
or schema sources. It does not retain that lookup for a later execution. or schema sources. It does not retain that lookup for a later execution.
`internal/profile/builtin` embeds the maintained built-in profile catalog and `internal/profile/builtin` embeds the maintained built-in profile catalog.
can place a caller-selected repository ahead of that catalog. Every embedded Every embedded profile selects `openrouter` and inherits its endpoint and
profile selects `openrouter` and inherits its endpoint and credential credential environment-variable name from the built-in backend registry rather
environment-variable name from the built-in backend registry rather than than repeating those values. Profile loading and overlay behavior are owned by
repeating those values. Profile behavior is owned by the the [profile repository tests](../../internal/profile/repository_test.go),
[profile repository tests](../../internal/profile/repository_test.go), while while catalog completeness, the backend-selection invariant, and duplicate IDs
catalog completeness, the backend-selection invariant, duplicate IDs, and are owned by the
overlay behavior are owned by the
[built-in repository tests](../../internal/profile/builtin/repository_test.go). [built-in repository tests](../../internal/profile/builtin/repository_test.go).
## Ordinary Artifacts ## Ordinary Artifacts

View File

@@ -1,334 +1,458 @@
# Optional Request-Parameter Omission Implementation Plan # Application Fallback Profiles Implementation Plan
**Status:** Complete. **Status:** Ready for implementation.
## Purpose ## Purpose
This document is the decision-complete implementation plan for This document is the decision-complete implementation plan for
[omitting unset optional request parameters](optional-request-parameters.md). [application fallback profiles](fallback-profiles.md). It is written for a
It is written for a `gpt-5.6-terra` coding agent that will implement each stage `gpt-5.6-terra` coding agent that will implement each stage in order.
in order.
The feature roadmap owns the motivation, policy choices, compatibility The feature roadmap owns the motivation, policy choices, compatibility
boundary, non-goals, and target end state. This document owns the concrete boundary, non-goals, and target end state. This document owns the concrete
design, file-level work, test ownership, documentation updates, validation, design, file-level work, test ownership, documentation updates, validation,
and completion gates. and completion gates.
The separate [application fallback profiles](fallback-profiles.md) roadmap is
not part of this implementation plan. Preserve it unchanged for its own later
planning and implementation cycle.
## Implementation Rules ## Implementation Rules
- Complete the stages in order. Stage 1 must leave code, tests, GoDoc, and - Complete the stages in order. Every stage must leave the repository
current-state documentation mutually accurate; Stage 2 performs the final buildable, tested for the behavior changed in that stage, and accurately
audit and repository-wide acceptance. documented for its implemented state.
- Preserve unrelated working-tree changes. In particular, do not edit, - Preserve unrelated working-tree changes. Inspect `git status --short` and
implement, retire, or reclassify `fallback-profiles.md`. the relevant diffs before editing, and do not overwrite or reformat
pre-existing user work.
- Follow every policy under `docs/policy/`, the task-specific reading guide in - Follow every policy under `docs/policy/`, the task-specific reading guide in
`docs/development.md`, and the accepted behavior in `docs/development.md`, and the accepted behavior in
`optional-request-parameters.md`. `fallback-profiles.md`.
- Keep the existing package boundaries. Framework defaults remain in - Keep the module root as the public facade. The root package owns profile
`internal/defaults`, resolution remains in `internal/usecase`, and outbound source selection and composition; `internal/profile` owns repository,
OpenAI-compatible serialization remains in `internal/llm`. parsing, validation, and error-preserving overlay behavior; and
- Do not add an exported type, field, option, method, error, or public package. `internal/profile/builtin` owns only the embedded built-in catalog.
This feature changes default and wire semantics within existing contracts. - Add only the public `WithFallbackProfileFS` option. Do not add an exported
- Do not replace numeric profile fields with pointers or add profile presence repository type, source-provenance value, fallback-specific error, config
tracking. File and in-memory profile zero values retain their existing field, programmatic fallback-profile option, or public package.
inheritance semantics; runtime pointer overrides remain the only supported - Reuse `profile.NewFSRepository` and `profile.NewOverlayRepository`. Do not
way to select an explicit numeric zero. add another parser, validator, repository implementation, or merge model.
- Preserve required request fields, session IDs, structured output, - Preserve lazy loading. Engine construction validates the option arguments,
credentials, extra-parameter validation, reasoning clearing, deadlines, not every file in the supplied filesystem. A profile source is read only
capacity management, and all existing precedence rules. when resolution reaches it.
- Do not query a provider for defaults or capabilities and do not add - Preserve existing error identities and mappings. Only
backend- or model-specific serialization branches. `profile.ErrProfileNotFound` permits an overlay to consult its next layer;
- Keep tests lean and behavioral. Use the existing root precedence test to own every other error from a higher layer must be returned and mapped through
resolved public/injected-client metadata and the existing model-client tests the existing public profile-load path.
to own wire inclusion and omission. Do not duplicate those matrices in a - Keep tests classical and behavior-focused. Public source precedence and
new end-to-end fixture. exported option semantics belong in external-package root tests; generic
- Update exact exported semantics in GoDoc, profile/default semantics in overlay behavior remains owned by `internal/profile` tests.
`docs/formats.md`, and provider request-body semantics in - Update GoDoc and current-state documentation in the same stage that exposes
`docs/integrations/openai-compatible-chat.md` in the same stage as the code. the public option. Do not describe the feature as implemented before that
stage is complete.
- Do not add release notes, change a module version, commit, tag, push, or - Do not add release notes, change a module version, commit, tag, push, or
publish a release as part of this plan. publish a release as part of this plan.
## Fixed Design ## Fixed Design
### Framework Defaults ### Public API
In `internal/defaults/defaults.go`, remove these constants: Add this function to `engine.go` beside the existing profile-source options:
```go ```go
ExecutionDefaultTemperature func WithFallbackProfileFS(fsys fs.FS, root string) Option
ExecutionDefaultMaxTokens
ExecutionDefaultTopP
``` ```
They currently encode zero for `temperature` and `max_tokens` and one for The option accepts the same filesystem and root forms as `WithProfileFS` and
`top_p`. Optional provider controls are no longer framework defaults, so constructs its repository with `profile.NewFSRepository(fsys, root)`. It must
retaining zero-valued constants under default-oriented names would obscure the return `ErrInvalidConfig` from option application when `fsys` is nil or when
new contract. `strings.TrimSpace(root)` is empty. Do not normalize or replace a valid root
before passing it to the repository.
Keep `ExecutionDefaultTimeoutSeconds` at its current positive value. Timeout is The fallback source is its own last-value-wins option category. Add these two
a Promptkit-owned generation deadline and is not an OpenAI-compatible request private fields to `engineOptions`:
body field.
Keep `ExecutionTargetDefault` as the common resolution baseline, but have it ```go
initialize only `TimeoutSeconds`. The zero Go values for `Temperature`, fallbackProfiles profile.Repository
`MaxTokens`, and `TopP` then represent unspecified provider controls. Do not fallbackProfileSource bool
rename this internal function or add a second defaults constructor. ```
### Resolution And Public Metadata Use the repository field for the selected source and the boolean only to
distinguish an unapplied option from an applied option. A later valid
`WithFallbackProfileFS` replaces both values. Option application remains
sequential, so an invalid option fails `NewEngine` immediately even if a later
option could otherwise replace it.
Do not change the merge functions or precedence in The exact GoDoc for `WithFallbackProfileFS` must state:
`internal/usecase/runner.go`:
1. the baseline target supplies only the Promptkit timeout; - that it supplies application-owned fallback profile definitions;
2. nonzero profile numeric fields replace the baseline; - the full four-layer lookup order;
3. non-nil runtime numeric overrides replace profile values; and - that definitions are whole profiles and are not field-merged;
4. `ExecutionTargetPresence` records runtime overrides, including explicit - that only a missing ID falls through, while a matching read, parse,
zero values. duplicate, validation, or credential-format failure stops resolution;
- that loading and validation are lazy;
- that files use the ordinary strict profile YAML and `api_key_env` rules;
- that nil filesystems and blank roots cause `NewEngine` to match
`ErrInvalidConfig`;
- that repeated calls use the last valid fallback source; and
- that this is definition lookup, not provider or generation failover.
Consequently, when a profile omits the optional provider controls, resolved Update the `Option`, `Config.ProfileDir`, `WithProfileFS`, `WithProfileFile`,
`ExecutionTarget` values contain zero for `Temperature`, `MaxTokens`, and and `WithProfiles` GoDoc in `engine.go` where necessary so their relative
`TopP`. That zero is stable metadata for “unspecified” unless the accompanying precedence is unambiguous. Exact declarations and behavior remain owned by
`GenerateRequest.TargetPresence` bit reports an explicit runtime zero. GoDoc; consumer documentation should summarize the workflow and link readers
back to the API rather than reproduce every error clause.
Do not expose target presence in `PreparedRun`, `RunResult`, or ### Repository Composition
`ProfileInspection`, and do not change their stable JSON shapes. As already
true for `max_tokens`, those metadata values report the resolved numeric value
rather than provenance. A prepared result containing `top_p: 0` therefore does
not distinguish an unspecified value from an explicit runtime zero; injected
clients receive the separate presence value when the distinction affects
execution.
Update root GoDoc in `types.go` so it no longer calls an unspecified optional Move all profile-source composition into one private root helper in
provider control an effective provider value: `engine.go`:
- `ExecutionTarget.Temperature`, `MaxTokens`, and `TopP` must each state that ```go
zero leaves the field unspecified to compatible providers unless the func newProfileRepository(profileDir string, options engineOptions) profile.Repository
corresponding `ExecutionTargetPresence` bit is true; ```
- `ExecutionTarget.TimeoutSeconds` retains its existing deadline semantics;
- `Profile` and `ExecutionTargetOverride` documentation must describe zero or
nil as inheriting a lower-precedence value and otherwise leaving the provider
control unspecified, rather than implying that every field receives a
concrete framework value; and
- `PreparedRun`, `ProfileInspection`, and other effective-target summaries may
continue to describe precedence, but must not imply that Promptkit knows a
provider's omitted default.
Do not change field types, field order, JSON tags, conversion functions, string `NewEngine` must call this helper once and pass its returned repository to the
formatting, or copying behavior. runner. The helper must build from lowest to highest precedence:
### Outbound Request Semantics 1. begin with `builtin.NewRepository()`;
2. if `options.fallbackProfileSource` is true, overlay
`options.fallbackProfiles` over the built-in repository;
3. select exactly one ordinary configured source: use `options.profiles` when
`options.profileSource` is true; otherwise, when `profileDir` is nonblank,
use `profile.NewFilesystemRepository(profileDir)`; overlay that selected
source over the current repository;
4. if `options.memorySource` is true, overlay `options.memoryProfiles` over
the current repository; and
5. return the resulting chain.
The current built-in client already has the required mechanism: This preserves the existing rule that `WithProfileFS` or `WithProfileFile`
`openAIChatRequestFromGenerateRequest` includes `temperature`, `max_tokens`, or replaces `Config.ProfileDir`; those sources are alternatives in one ordinary
`top_p` when the resolved value is nonzero or the corresponding target-presence configured-source category, not two independent layers. `WithProfiles` remains
bit is true, and `openAIChatRequestPayload` omits nil fields. Preserve that a distinct highest-precedence category.
logic.
No production change should be needed in The final lookup order is therefore:
`internal/llm/openai_compatible_client.go`. Change it only if a focused failing
test demonstrates that the existing implementation does not meet this plan;
do not special-case `top_p`, inspect profile provenance, or move framework
default policy into the transport.
The resulting behavior is: ```text
WithProfiles
-> WithProfileFile / WithProfileFS / Config.ProfileDir
-> WithFallbackProfileFS
-> Promptkit built-ins
```
- an omitted profile `top_p` resolves to zero and is absent from the body; Every arrow is a whole-profile, not-found-only fallback. Do not inspect or
- a nonzero profile or runtime `top_p` is included; copy profile fields in the composition helper.
- an explicit runtime `top_p` of zero is included because presence is true;
- the same rules continue to apply to `temperature` and `max_tokens`;
- empty `service_tier` and effective `reasoning_effort` remain absent;
- configured `extra_params` remain present after validation; and
- `model`, `messages`, conditional `session_id`, and conditional
`response_format` remain unchanged.
### Profile Formats And Built-In Profiles ### Built-In Package Boundary
Do not change YAML or public `Profile` field shapes. Numeric zero in a file or Reduce `internal/profile/builtin/repository.go` to the embedded catalog leaf:
in-memory profile continues to mean “do not replace the lower layer.” With no
lower provider value, zero therefore resolves to unspecified. An explicit
profile-level numeric zero remains unsupported; consumers use a runtime
pointer override when zero itself must be sent.
Do not edit files under `internal/profile/builtin/assets/`. Values declared in ```go
those files are explicit profile policy and remain effective. Existing func NewRepository() profile.Repository
nonzero-profile tests are sufficient to protect explicit inclusion; do not add ```
one test per built-in asset or parameter.
Remove `NewRepositoryWithPrimary` and `NewRepositoryWithDirectory`. Remove
their now-obsolete tests from `internal/profile/builtin/repository_test.go` and
remove imports used only by those helpers or tests. Do not move their tests to
another private helper: existing root public-behavior tests own assembled
precedence, and `internal/profile.TestOverlayRepository` owns not-found-only
overlay semantics.
Do not change built-in YAML assets, built-in validation, the `profile.Repository`
interface, `profile.NewOverlayRepository`, or filesystem repository behavior.
### Resolution And Error Semantics
The runner receives one assembled `profile.Repository`; do not add fallback
logic to `InspectProfile`, `Prepare`, `PrepareExecution`, `Run`, or
`RunPrepared`. Those paths must continue to resolve through the runner's one
repository dependency.
The existing overlay contract is authoritative:
- a successful lookup returns the complete higher-layer profile;
- `profile.ErrProfileNotFound` consults the next layer;
- cancellation, filesystem read failures, malformed YAML, duplicate matches,
raw `api_key`, invalid profiles, and all other errors stop lookup; and
- the root facade maps failures through the existing public identities such as
`ErrProfileNotFound` and `ErrProfileLoad`.
Do not add eager filesystem walking in the option or `NewEngine`. A malformed
asset unrelated to the requested ID retains the existing ordinary
`FSRepository` behavior; this plan does not strengthen that package's global
validation guarantees.
### Test Ownership ### Test Ownership
Use these existing boundaries: Use the following test boundaries and avoid duplicating the profile parser's
existing case matrix.
- In `engine_test.go`, update the “framework defaults” row of In `public_contract_test.go`:
`TestEngineExecutionSettingPrecedence` so the zero-valued profile expects
`TopP: 0` while retaining `Temperature: 0`, `MaxTokens: 0`, and the positive
timeout. Rename that row to describe unspecified provider controls plus the
framework timeout. Keep the rows proving nonzero profile precedence and
explicit runtime-zero presence unchanged.
- Remove
`TestRunnerRunBuiltInDefaultsUsedWhenProfileOmitsOptionalFields` from
`internal/usecase/runner_test.go`. Its literal-default assertions duplicate
the stronger assembled root precedence test and depend on the internal
constants being removed. Do not replace it with another internal
default-value test.
- Keep
`TestOpenAICompatibleClientOmitsImplicitZeroNumericFields` and
`TestOpenAICompatibleClientSerializesExplicitZeroNumericOverrides` in
`internal/llm/openai_compatible_client_test.go`. Together they own the wire
distinction and should pass without weakening their assertions.
- Keep the existing nonzero request serialization and profile-precedence tests
passing. They prove that explicitly configured values continue to be sent
and selected.
Do not add snapshots, golden files, provider calls, or a broad duplicate - Add `TestFallbackProfileSourcePrecedence`. Use minimal synthetic
integration test. Add a new test only if the implementation exposes a distinct `fstest.MapFS` profiles and, where `Config.ProfileDir` is under test, a
contract risk not covered by the tests above, and record that reason in the `t.TempDir`. Cover these distinct relationships: an in-memory profile beats
test name or nearby test structure rather than in a new planning document. both ordinary and fallback definitions; an ordinary `WithProfileFS` source
beats a fallback definition; `Config.ProfileDir` beats a fallback
definition when no ordinary source option replaces it; a fallback
definition beats a built-in definition with the same ID; and an ID absent
from the fallback source still resolves from the built-in catalog. Assert
the selected model or another stable complete-profile field rather than
internal repository structure.
- Extend `TestRepeatedOptionsUseLastValueInEachCategory` with a
`fallback profile source` subtest proving that the later valid fallback
filesystem is selected.
- Add `TestFallbackProfileSourcePreservesLazyLoadingAndErrors`. Prove that
engine construction succeeds without reading malformed fallback YAML, that
an unrelated malformed file does not prevent a valid requested fallback
definition from resolving under existing FS-repository semantics, that a
malformed fallback file whose stem matches a built-in profile ID yields
`ErrProfileLoad` instead of silently reaching the built-in, and that a
malformed ordinary configured definition yields `ErrProfileLoad` instead of
reaching a valid application fallback definition. Use `errors.Is`; do not
assert complete error strings.
- Add one representative workflow test that supplies a fallback-only profile
and verifies the same effective model through `InspectProfile`, `Prepare`,
a `PrepareExecution` followed by `RunPrepared`, and direct `Run`. Use the
existing deterministic injected-client style, no live provider, and no real
credential. This test owns the cross-workflow repository wiring; do not
repeat the full precedence matrix through every method.
In `engine_test.go`, extend `TestSourceOptionsRejectInvalidInputs` with nil
filesystem and blank-root cases for `WithFallbackProfileFS`. Both must make
`NewEngine` match `ErrInvalidConfig`.
Retain `internal/profile.TestOverlayRepository` unchanged unless a genuine
existing defect is found. It already owns success, not-found fallback, and
non-not-found error preservation. Do not add package-private tests for the new
root helper, snapshots, golden files, provider calls, or one test per profile
format error already covered by `internal/profile`.
### Canonical Documentation ### Canonical Documentation
Update current-state documentation in Stage 1: Update current-state documentation when the option is implemented:
- In `docs/formats.md`, replace the optional provider-control entries in the - In `docs/formats.md`, make the source-precedence section the canonical
framework-default table with clear unspecified/omitted semantics, while four-layer definition lookup order. State that ordinary configured sources
retaining the positive `timeout_seconds` framework default. Explain that override application fallbacks, application fallbacks override built-ins,
profile numeric zero inherits a lower layer and otherwise remains profiles are whole values, and only a missing ID falls through. Retain this
unspecified; an explicit runtime pointer zero is retained. document's ownership of strict YAML, credentials, validation, and source
- In `docs/integrations/openai-compatible-chat.md`, state that discovery details.
`temperature`, `max_tokens`, and `top_p` are included only when selected by a - In `docs/consumers/pkg-promptkit.md`, add a short task-oriented section that
profile or runtime override, including explicit runtime zero, and are absent shows an illustrative `embed.FS` declaration and
when unspecified. Keep the existing ownership of required fields, `WithFallbackProfileFS`. Explain that application defaults belong in the
`session_id`, structured output, extra parameters, and timeout behavior. embedded fallback and operator overrides belong in the ordinary configured
- In `types.go`, apply the GoDoc changes described above; these declarations source. Link to `docs/formats.md` for exact format and precedence rules, and
own the exact public value semantics. do not turn the snippet into a second complete maintained application.
- In `docs/internal/sources.md`, describe the root-owned four-layer
composition and the existing not-found-only overlay mechanism. Remove any
claim that the built-in package composes caller-selected repositories.
- In `docs/internal/overview.md`, keep the root facade responsible for source
assembly, describe `internal/profile/builtin` only as the embedded catalog,
and reflect the implemented fallback layer without duplicating the exact
public API contract.
- In `engine.go`, apply the GoDoc changes under Public API. GoDoc owns the
exact option signature, validation, category, and public semantics.
Do not add a README or release-document note. The consumer guide already The architecture policy already assigns assembly to the root facade and the
routes exact field behavior to GoDoc and profile/default behavior to the format built-in catalog to `internal/profile/builtin`; do not edit it unless the
reference, so do not duplicate the new contract there. The internal LLM implementation reveals an actual contradiction. No integration protocol,
document describes flow rather than exact field omission and does not require outbound request body, profile YAML shape, README orientation, or maintained
a change unless its current text is found to contradict the implementation. example changes as part of this feature.
## Stage 1: Implement Omission Semantics And Canonical Contracts ## Stage 1: Move Existing Profile Composition To The Root Facade
### Objective ### Objective
Remove optional provider controls from the framework baseline, preserve Establish the intended ownership boundary and a single root composition point
explicit profile and runtime values, update the canonical contracts, and prove without changing public behavior or adding the fallback option.
the behavior at the existing resolution and wire boundaries.
### Implementation Prompt ### Implementation Prompt
Implement only Stage 1 of `docs/roadmap/implementation.md`. Read the complete Implement only Stage 1 of `docs/roadmap/implementation.md`. Read the complete
feature roadmap, implementation rules, and fixed design above before editing. feature roadmap, implementation rules, and fixed design above before editing.
1. In `internal/defaults/defaults.go`, remove the three provider-control 1. In `engine.go`, add `newProfileRepository(profileDir string, options
constants and make `ExecutionTargetDefault` initialize only engineOptions) profile.Repository` and move the existing three-layer
`TimeoutSeconds`. assembly into it: built-ins, then the selected ordinary configured source,
2. In `engine_test.go`, update and rename the default-precedence table row then `WithProfiles`. Do not add fallback fields or the public option yet.
exactly as described under Test Ownership. 2. Replace the inline profile assembly in `NewEngine` with one call to the
3. Remove the redundant literal-default test from helper. Preserve the existing replacement relationship between
`internal/usecase/runner_test.go`; do not weaken other precedence, `Config.ProfileDir` and `WithProfileFS`/`WithProfileFile`.
profile-value, or runtime-zero tests. 3. In `internal/profile/builtin/repository.go`, remove
4. Update the affected exported GoDoc in `types.go` without changing any `NewRepositoryWithPrimary` and `NewRepositoryWithDirectory`, leaving
declaration, JSON tag, or serialization shape. `NewRepository` as the only constructor.
5. Update `docs/formats.md` and 4. Remove the three tests dedicated to the deleted built-in composition
`docs/integrations/openai-compatible-chat.md` according to Canonical helpers from `internal/profile/builtin/repository_test.go`. Preserve tests
Documentation. that validate the embedded catalog itself.
6. Run the focused validation below. Repair regressions in scope, but do not 5. Update `docs/internal/sources.md` and `docs/internal/overview.md` so they
broaden the feature or change the established serializer merely to make a describe the implemented Stage 1 ownership accurately. At this boundary
mistaken expectation pass. the source order is still in-memory, ordinary configured source, built-ins;
do not document the application fallback as implemented yet.
6. Run the focused validation below. Fix in-scope regressions without adding
fallback behavior early.
Do not edit built-in profile assets, fallback-profile work, backend Do not add or mention an implemented `WithFallbackProfileFS` in Stage 1. Do
registration, profile parsing, target merge logic, public value shapes, not change exported declarations, profile parsing, profile assets, error
prepared-execution lifecycle, capacity management, or release material. mapping, runner behavior, or consumer and format documentation.
### Focused Validation ### Focused Validation
Run from the repository root: Run from the repository root:
```sh ```sh
gofmt -w internal/defaults/defaults.go types.go engine_test.go \ gofmt -w engine.go internal/profile/builtin/repository.go \
internal/usecase/runner_test.go internal/profile/builtin/repository_test.go
go test . -run 'TestEngineExecutionSettingPrecedence' go test . -run \
go test ./internal/llm -run \ 'Test(PrepareUsesBuiltInProfileWithoutProfileDir|CustomProfileOverridesBuiltInProfile|InMemoryProfilesOverrideBuiltInsAndProfileSources)$'
'TestOpenAICompatibleClient(GenerateSuccess|OmitsImplicitZeroNumericFields|SerializesExplicitZeroNumericOverrides)' go test ./internal/profile/...
go test ./internal/usecase -run \ go test . ./internal/profile/...
'Test(ResolveExecutionTarget|RunnerPrepareRequestNumericOverridePresence|RunnerPrepareSelectedProfileBeatsBuiltInDefault|RunnerRunSelectedProfileBeatsBuiltInDefault)' go vet . ./internal/profile/...
go test . ./internal/defaults ./internal/usecase ./internal/llm
go vet . ./internal/defaults ./internal/usecase ./internal/llm
git diff --check git diff --check
``` ```
If a focused regular expression does not match an existing test name, inspect If a focused expression does not match an existing test name, inspect the
the current names and run the narrowest equivalent set; do not silently skip current suite and run the narrowest equivalent public precedence coverage; do
the intended resolution, explicit-profile, explicit-zero, and wire-omission not silently skip the intended relationship.
coverage.
### Completion Gate ### Completion Gate
Stage 1 is complete only when: Stage 1 is complete only when:
- the resolution baseline contains no provider tuning value and retains the - the root facade assembles the unchanged three-layer profile chain in one
Promptkit timeout; private helper;
- an omitted `top_p` resolves to zero and the built-in client omits it; - ordinary source options still replace `Config.ProfileDir` and in-memory
- nonzero profile and runtime values remain effective and serialized; profiles still have highest precedence;
- explicit runtime zero values remain distinguishable and serialized through - built-in profiles remain available and remain lower than consumer sources;
`ExecutionTargetPresence`; - only `internal/profile` owns generic overlay behavior and the built-in
- no public type or stable JSON shape changed; package owns only its embedded catalog;
- required fields, structured output, session IDs, extra parameters, - no public API or behavior changed;
reasoning, credentials, and deadlines retain their existing behavior; - internal current-state documentation matches that boundary; and
- GoDoc, format documentation, and the integration contract describe the
implemented behavior without conflicting ownership; and
- all focused tests, vet, formatting, and whitespace checks pass. - all focused tests, vet, formatting, and whitespace checks pass.
## Stage 2: Audit Compatibility And Validate The Repository ## Stage 2: Add Application Fallback Profiles And Public Contracts
### Objective ### Objective
Confirm that the narrow semantic change is complete across all public, Add the public option, insert the application fallback into the root-owned
injected-client, built-in-profile, documentation, and repository surfaces, repository chain, prove its precedence and failure behavior through public
then mark the temporary planning documents complete. workflows, and publish the canonical current-state documentation.
### Implementation Prompt ### Implementation Prompt
Implement only Stage 2 of `docs/roadmap/implementation.md` after Stage 1 Implement only Stage 2 of `docs/roadmap/implementation.md` after Stage 1
satisfies its completion gate. satisfies its completion gate.
1. Search tracked Go and Markdown files for the removed constant names, 1. Add `fallbackProfiles` and `fallbackProfileSource` to `engineOptions`, then
framework `top_p` defaults, claims that all effective provider controls have implement `WithFallbackProfileFS` exactly as specified under Public API.
concrete framework values, and request-body inclusion rules. Correct only 2. Extend `newProfileRepository` so it constructs the fixed four-layer chain
stale statements or tests owned by this feature. in the prescribed low-to-high order. Do not alter generic overlay logic or
2. Confirm that `internal/profile/builtin/assets/` has no feature-related diff add fallback branches to runner methods.
and that its explicit nonzero optional controls still pass ordinary profile 3. Update all affected `engine.go` GoDoc, including the option category list
validation and resolution tests. and the relative precedence descriptions for existing profile sources.
3. Confirm that `internal/llm/openai_compatible_client.go` either has no diff or 4. Add and extend the external-package root tests exactly as specified under
contains only a change required by a focused failing contract test. The Test Ownership. Reuse small existing fakes and fixture helpers where they
default policy must remain outside the transport. remain clear; add only minimal synthetic YAML helpers needed by these tests.
4. Follow every changed Markdown link and confirm its target exists. Verify 5. Extend `TestSourceOptionsRejectInvalidInputs` with the two fallback option
that current-state documents describe implemented behavior and that exact validation cases.
contracts remain with GoDoc, the format reference, and the integration 6. Update `docs/formats.md`, `docs/consumers/pkg-promptkit.md`,
contract. `docs/internal/sources.md`, and `docs/internal/overview.md` according to
5. Run the complete validation sequence below and repair only in-scope Canonical Documentation.
failures. 7. Run the focused validation below. Repair in-scope failures without
6. After all checks pass, change the status of weakening existing parser, error-identity, prepared-execution, or profile
`optional-request-parameters.md` and this document to `Complete`. Do not precedence guarantees.
change the status or contents of `fallback-profiles.md`.
7. Re-run `git diff --check` and inspect the final working tree and diff.
Do not delete temporary roadmaps in this stage; retirement is a separate Do not add a config field, in-memory fallback API, source provenance, profile
maintainer action. Do not add release notes, change versions, or create a inheritance, provider failover, eager validation, application-specific assets,
commit, tag, push, or release. or backend/model policy. Do not edit `internal/profile/builtin/assets/`.
### Focused Validation
Run from the repository root:
```sh
gofmt -w engine.go engine_test.go public_contract_test.go
go test . -run \
'Test(FallbackProfileSource|RepeatedOptionsUseLastValueInEachCategory|SourceOptionsRejectInvalidInputs)'
go test ./internal/profile/...
go test . ./internal/profile/... ./internal/usecase
go vet . ./internal/profile/... ./internal/usecase
git diff --check
```
The focused root expression must execute the precedence, lazy/error,
cross-workflow, repeated-option, and invalid-input coverage described above.
If the implemented names differ slightly, run explicit equivalent expressions
and record no skipped contract category.
### Completion Gate
Stage 2 is complete only when:
- `WithFallbackProfileFS` is the sole new public declaration and has complete,
accurate GoDoc;
- nil filesystem and blank root inputs fail construction with
`ErrInvalidConfig`, and the last valid repeated fallback option wins;
- the assembled order is in-memory, ordinary configured, application
fallback, built-ins;
- existing ordinary source options still replace `Config.ProfileDir`;
- lookup falls through only on a missing ID and never after a matching
higher-layer failure;
- profiles remain whole values and loading remains lazy;
- inspection, preparation, prepared execution, and ordinary execution resolve
the same fallback definition through one runner repository;
- no built-in asset, profile format, public error identity, provider request,
or existing consumer behavior changed unintentionally;
- GoDoc and all affected canonical documents describe implemented behavior
without duplicating ownership; and
- all focused tests, vet, formatting, links, and whitespace checks pass.
## Stage 3: Audit Compatibility And Validate The Repository
### Objective
Confirm that the implementation is complete, minimal, and consistent across
the public facade, internal boundaries, tests, and documentation, then mark
the temporary planning documents complete.
### Implementation Prompt
Implement only Stage 3 of `docs/roadmap/implementation.md` after Stage 2
satisfies its completion gate.
1. Search tracked Go and Markdown files for
`NewRepositoryWithPrimary`, `NewRepositoryWithDirectory`, profile source
precedence lists, and descriptions of built-in repository composition.
Remove stale references and correct only feature-owned contradictions.
2. Review `newProfileRepository` directly and confirm it has exactly four
possible layers in the required order, selects only one ordinary configured
source, and contains no profile field merging or eager I/O.
3. Review the public tests as a suite. Confirm that each distinct risk in Test
Ownership is protected once, generic parser and overlay cases remain with
`internal/profile`, and no test depends on private helper shape.
4. Confirm that `internal/profile/builtin/assets/`, external wire behavior,
backend configuration, credential resolution, public result shapes, and
stable JSON tags have no feature-related changes.
5. Follow every added or changed Markdown link and confirm that its target and
relevant heading exist. Verify that exact API details live in GoDoc, exact
profile format and precedence details live in `docs/formats.md`, consumer
guidance remains task-oriented, and internal documents describe only
implementation responsibility.
6. Run the complete validation sequence below and repair only in-scope
failures.
7. After every check passes, change the status of
`fallback-profiles.md` and this document to `Complete`. Do not delete or
retire either roadmap; retirement is a separate maintainer action.
8. Re-run `git diff --check`, inspect `git status --short`, and review the full
diff while distinguishing pre-existing user changes from this feature.
Do not add release notes, change versions, or create a commit, tag, push, or
release during this stage.
### Full Validation ### Full Validation
Run from the repository root: Run from the repository root:
```sh ```sh
gofmt -w internal/defaults/defaults.go types.go engine_test.go \ gofmt -w engine.go engine_test.go public_contract_test.go \
internal/usecase/runner_test.go internal/profile/builtin/repository.go \
internal/profile/builtin/repository_test.go
gofmt -l $(git ls-files '*.go') gofmt -l $(git ls-files '*.go')
go test ./... go test ./...
go test -race ./... go test -race ./...
@@ -345,34 +469,34 @@ offline and require no real credential or provider.
Inspect the final diff and confirm: Inspect the final diff and confirm:
- only this feature's files and pre-existing user changes are present; - only this feature's files and pre-existing user changes are present;
- no built-in profile asset, public declaration shape, stable JSON tag, - no built-in asset, public value shape, stable JSON tag, provider payload,
credential rule, workspace file, local module replacement, generated binary, workspace file, local module replacement, generated binary, or unrelated
or unrelated formatting changed; formatting changed;
- the removed provider-default constants have no remaining references; - deleted built-in composition helpers have no remaining references;
- the provider omission policy is implemented by resolution plus the existing - the fallback option reuses the ordinary FS repository and generic overlay;
generic serializer, not by a `top_p` transport special case; - the root constructs one repository used by every resolution workflow;
- the optional-parameter roadmap and this plan are complete while the fallback - the feature roadmap and this plan are both complete; and
roadmap remains selected; and
- no commit, tag, push, or release was created. - no commit, tag, push, or release was created.
### Completion Gate ### Completion Gate
The implementation is complete only when: The implementation is complete only when:
- every Stage 1 gate remains satisfied; - every Stage 1 and Stage 2 gate remains satisfied;
- the ordinary and race-enabled suites pass; - the ordinary and race-enabled suites pass;
- vet, build, formatting, the maintained offline example, Markdown links, and - vet, build, formatting, the maintained offline example, Markdown links, and
whitespace checks pass; whitespace checks pass;
- public metadata, injected-client presence, profile inheritance, and outbound - the four-layer precedence and not-found-only fallthrough are consistent in
omission semantics are mutually consistent; code, GoDoc, public tests, format reference, consumer guidance, and internal
- explicit built-in and consumer profile values retain their behavior; documentation;
- both feature-specific roadmap statuses are `Complete`; - engines without `WithFallbackProfileFS` retain their previous behavior;
- `fallback-profiles.md` remains unchanged and selected for later work; and - the public surface contains no speculative companion API or provenance;
- both temporary roadmap statuses are `Complete`; and
- the repository is ready for maintainer review without a commit or release - the repository is ready for maintainer review without a commit or release
having been created by this plan. having been created by this plan.
## Open Questions ## Open Questions
None. The feature roadmap and fixed design above fully specify the behavior, None. The feature roadmap and fixed design above fully specify the public API,
compatibility boundary, implementation, documentation ownership, and test repository composition, error and validation behavior, compatibility boundary,
strategy. documentation ownership, test strategy, and staged implementation sequence.

View File

@@ -350,13 +350,7 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
promptDefs = promptdef.NewFilesystemRepository(cfg.PromptDir) promptDefs = promptdef.NewFilesystemRepository(cfg.PromptDir)
} }
profiles := builtin.NewRepositoryWithDirectory(cfg.ProfileDir) profiles := newProfileRepository(cfg.ProfileDir, options)
if options.profileSource {
profiles = builtin.NewRepositoryWithPrimary(options.profiles)
}
if options.memorySource {
profiles = profile.NewOverlayRepository(options.memoryProfiles, profiles)
}
backendRegistry, err := backend.NewRegistry(options.backends) backendRegistry, err := backend.NewRegistry(options.backends)
if err != nil { if err != nil {
@@ -409,6 +403,22 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
}, nil }, nil
} }
func newProfileRepository(profileDir string, options engineOptions) profile.Repository {
repository := builtin.NewRepository()
if options.profileSource {
repository = profile.NewOverlayRepository(options.profiles, repository)
} else if strings.TrimSpace(profileDir) != "" {
repository = profile.NewOverlayRepository(profile.NewFilesystemRepository(profileDir), repository)
}
if options.memorySource {
repository = profile.NewOverlayRepository(options.memoryProfiles, repository)
}
return repository
}
func fileSource(name string) (fs.FS, string, error) { func fileSource(name string) (fs.FS, string, error) {
cleanName := strings.TrimSpace(name) cleanName := strings.TrimSpace(name)
if cleanName == "" { if cleanName == "" {
@@ -482,7 +492,7 @@ func (e *Engine) InspectPrompt(
// InspectProfile trims surrounding whitespace from profileID and looks up the // InspectProfile trims surrounding whitespace from profileID and looks up the
// resulting nonblank ID exactly and case-sensitively through the engine's // resulting nonblank ID exactly and case-sensitively through the engine's
// ordinary in-memory, configured-source, and built-in profile precedence. It // ordinary in-memory, configured-source, and built-in profile precedence. It
// applies framework defaults, the selected backend, and then the selected // applies the framework timeout baseline, selected backend, and then selected
// profile to EffectiveModelParams without a request override. BackendID is // profile to EffectiveModelParams without a request override. BackendID is
// empty for an endpoint-only profile. // empty for an endpoint-only profile.
// //

View File

@@ -2,7 +2,6 @@ package builtin
import ( import (
"embed" "embed"
"strings"
"gitea.maximumdirect.net/eric/promptkit/internal/profile" "gitea.maximumdirect.net/eric/promptkit/internal/profile"
) )
@@ -15,17 +14,3 @@ var assets embed.FS
func NewRepository() profile.Repository { func NewRepository() profile.Repository {
return profile.NewFSRepository(assets, assetRoot) return profile.NewFSRepository(assets, assetRoot)
} }
func NewRepositoryWithPrimary(primary profile.Repository) profile.Repository {
if primary == nil {
return NewRepository()
}
return profile.NewOverlayRepository(primary, NewRepository())
}
func NewRepositoryWithDirectory(dir string) profile.Repository {
if strings.TrimSpace(dir) == "" {
return NewRepository()
}
return NewRepositoryWithPrimary(profile.NewFilesystemRepository(dir))
}

View File

@@ -2,14 +2,11 @@ package builtin
import ( import (
"context" "context"
"errors"
"io/fs" "io/fs"
"strings" "strings"
"testing" "testing"
"gitea.maximumdirect.net/eric/promptkit/internal/backend" "gitea.maximumdirect.net/eric/promptkit/internal/backend"
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
"gitea.maximumdirect.net/eric/promptkit/internal/profile"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -91,53 +88,3 @@ func loadBuiltInProfileIDs(t *testing.T) map[string]string {
} }
return ids return ids
} }
func TestRepositoryWithPrimaryUsesPrimaryBeforeBuiltIns(t *testing.T) {
repo := NewRepositoryWithPrimary(staticProfileRepo{
profiles: map[string]string{"mistral-small-3": "custom-model"},
})
p, err := repo.GetProfile(context.Background(), "mistral-small-3")
if err != nil {
t.Fatalf("expected profile to load, got %v", err)
}
if p.Model != "custom-model" {
t.Fatalf("expected primary profile to override built-in, got %+v", p)
}
}
func TestRepositoryWithPrimaryFallsBackToBuiltIns(t *testing.T) {
repo := NewRepositoryWithPrimary(staticProfileRepo{})
p, err := repo.GetProfile(context.Background(), "mistral-small-3")
if err != nil {
t.Fatalf("expected built-in profile to load, got %v", err)
}
if p.ID != "mistral-small-3" {
t.Fatalf("unexpected profile: %+v", p)
}
}
func TestRepositoryWithPrimaryDoesNotFallBackAfterPrimaryError(t *testing.T) {
repo := NewRepositoryWithPrimary(staticProfileRepo{err: profile.ErrInvalidProfile})
_, err := repo.GetProfile(context.Background(), "mistral-small-3")
if !errors.Is(err, profile.ErrInvalidProfile) {
t.Fatalf("expected primary error, got %v", err)
}
}
type staticProfileRepo struct {
profiles map[string]string
err error
}
func (r staticProfileRepo) GetProfile(_ context.Context, id string) (*domain.ExecutionProfile, error) {
if r.err != nil {
return nil, r.err
}
if model, ok := r.profiles[id]; ok {
return &domain.ExecutionProfile{ID: id, Endpoint: "http://primary/v1", Model: model}, nil
}
return nil, profile.ErrProfileNotFound
}

View File

@@ -440,11 +440,12 @@ type ExecutionTargetOverride struct {
// use profile YAML api_key_env with file and FS profile sources. Profile has no // use profile YAML api_key_env with file and FS profile sources. Profile has no
// stable JSON representation. // stable JSON representation.
// //
// WithProfiles validates and copies Profile values during NewEngine. Optional // WithProfiles validates and copies Profile values during NewEngine. Zero
// numeric zero, blank strings, and an empty ExtraParams map inherit // Temperature, MaxTokens, and TopP values and blank ServiceTier and
// lower-precedence values. An optional provider control that remains zero is // ReasoningEffort values leave those provider controls unspecified. A zero
// unspecified; use ExecutionTargetOverride pointer fields to request an // TimeoutSeconds retains the framework deadline, while an empty ExtraParams map
// explicit numeric zero. // inherits backend request defaults. Use ExecutionTargetOverride pointer fields
// to request an explicit numeric zero.
type Profile struct { type Profile struct {
// ID is the required non-blank profile identifier. WithProfiles trims it. // ID is the required non-blank profile identifier. WithProfiles trims it.
ID string ID string
@@ -458,23 +459,19 @@ type Profile struct {
Endpoint string Endpoint string
// Model is the required non-blank provider model identifier. // Model is the required non-blank provider model identifier.
Model string Model string
// Temperature is from 0 through 2. Zero inherits a lower-precedence value // Temperature is from 0 through 2. Zero leaves the provider control
// and otherwise leaves the provider control unspecified. // unspecified.
Temperature float64 Temperature float64
// MaxTokens is non-negative. Zero inherits a lower-precedence value and // MaxTokens is non-negative. Zero leaves the provider control unspecified.
// otherwise leaves the provider control unspecified.
MaxTokens int MaxTokens int
// TopP is from 0 through 1. Zero inherits a lower-precedence value and // TopP is from 0 through 1. Zero leaves the provider control unspecified
// otherwise leaves the provider control unspecified rather than selecting an // rather than selecting an explicit zero.
// explicit zero.
TopP float64 TopP float64
// TimeoutSeconds is non-negative. Zero inherits a lower-precedence value and // TimeoutSeconds is non-negative. Zero retains the framework deadline.
// otherwise the framework deadline.
TimeoutSeconds int TimeoutSeconds int
// ServiceTier is optional; a blank value inherits a lower-precedence value. // ServiceTier is optional; a blank value leaves it unspecified.
ServiceTier string ServiceTier string
// ReasoningEffort is optional; a blank value inherits a lower-precedence // ReasoningEffort is optional; a blank value leaves it unspecified.
// value.
ReasoningEffort string ReasoningEffort string
// APIKeyRequired clears a backend's inherited API-key environment name and // APIKeyRequired clears a backend's inherited API-key environment name and
// requires a non-blank RunRequest.APIKey unless the request explicitly // requires a non-blank RunRequest.APIKey unless the request explicitly