From a248433d0f36a9cecdfca5e98c21db0cabde52e9 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 1 Aug 2026 02:41:44 +0000 Subject: [PATCH] Omit unset optional request parameters --- docs/formats.md | 20 +- docs/integrations/openai-compatible-chat.md | 5 +- docs/roadmap/fallback-profiles.md | 152 ++++++++ docs/roadmap/implementation.md | 378 ++++++++++++++++++++ docs/roadmap/optional-request-parameters.md | 83 +++++ engine_test.go | 4 +- internal/defaults/defaults.go | 6 - internal/usecase/runner_test.go | 30 -- types.go | 77 ++-- 9 files changed, 678 insertions(+), 77 deletions(-) create mode 100644 docs/roadmap/fallback-profiles.md create mode 100644 docs/roadmap/implementation.md create mode 100644 docs/roadmap/optional-request-parameters.md diff --git a/docs/formats.md b/docs/formats.md index 8994fdf..4e1b376 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -188,25 +188,27 @@ they also cannot collide with the standard fields listed in the Execution settings resolve in this order: -1. framework defaults; +1. the framework timeout baseline; 2. the selected backend, when the profile names one; 3. the selected profile; and 4. request `ExecutionTargetOverride` values. -The framework defaults are: +The framework baseline is: | Setting | Default | | --- | --- | -| `temperature` | `0` | -| `max_tokens` | `0` | -| `top_p` | `1` | +| `temperature` | 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 backend, profile, or runtime override selects it. | +| `top_p` | Unspecified and omitted from compatible provider requests unless a backend, profile, or runtime override selects it. | | `timeout_seconds` | `600` | Numeric zero in a file or in-memory profile means that the profile does not -replace the framework default. Numeric request overrides use pointers, so an -explicit zero is preserved. In particular, an explicit request -`timeout_seconds` of zero disables the per-generation deadline while leaving -the caller context and transport timeout intact. +replace a lower-precedence value. With no lower value, `temperature`, +`max_tokens`, and `top_p` remain unspecified. Numeric request overrides use +pointers, so an explicit zero is retained and sent to compatible providers. +In particular, an explicit request `timeout_seconds` of zero disables the +per-generation deadline while leaving the caller context and transport timeout +intact. Non-empty profile strings replace backend defaults, and non-empty request strings replace both. Request reasoning is the exception: a nil diff --git a/docs/integrations/openai-compatible-chat.md b/docs/integrations/openai-compatible-chat.md index bd16fdb..184cf74 100644 --- a/docs/integrations/openai-compatible-chat.md +++ b/docs/integrations/openai-compatible-chat.md @@ -53,8 +53,9 @@ never also sent as a session header. The client conditionally includes: -- `temperature`, `max_tokens`, and `top_p` when non-zero or explicitly - present; +- `temperature`, `max_tokens`, and `top_p` only when selected by a backend, + profile, or runtime override, including an explicit runtime zero; they are + absent when unspecified; - non-empty `service_tier` and effective `reasoning_effort`; an explicitly disabled reasoning setting is empty and therefore omitted; and - `response_format` for JSON Schema structured output, including its name, diff --git a/docs/roadmap/fallback-profiles.md b/docs/roadmap/fallback-profiles.md new file mode 100644 index 0000000..9942a69 --- /dev/null +++ b/docs/roadmap/fallback-profiles.md @@ -0,0 +1,152 @@ +# Application Fallback Profiles + +Status: Selected for implementation. + +## Purpose + +Promptkit will allow a consuming application to supply an embedded fallback +profile source below operator-configured profiles and above Promptkit's +built-in profile catalog. + +This gives consumers stable, application-owned profile IDs with useful +packaged defaults while preserving the existing ability for an operator to +replace those definitions. Promptkit will own the reusable source layer and +lookup semantics without owning downstream profile names, model assignments, +or application configuration policy. + +## Consumer Outcome + +A consumer such as Weatherreporter can embed profiles named for application +workloads or execution tiers, such as `weather-light`, `weather-balanced`, and +`weather-deep`. Prompts can select those logical IDs without coupling the +application to a particular provider or model. + +An operator can define the same profile ID in the application's ordinary +configured profile source to replace the packaged default. If no operator +definition exists, Promptkit resolves the application's embedded definition. +If neither source contains the ID, Promptkit retains access to its own built-in +profile catalog. + +## Scope + +Promptkit will add one optional `fs.FS`-backed application fallback profile +source to engine construction. The intended public surface is: + +```go +promptkit.WithFallbackProfileFS(profileFS, ".") +``` + +The source will use the existing profile YAML format, discovery behavior, +strict decoding, validation rules, and credential restrictions. The option +will accept a non-nil filesystem and nonblank root. Repeating the option will +follow Promptkit's same-category convention: the last valid value replaces the +earlier fallback source, while an invalid option still fails construction when +it is applied. + +No programmatic fallback-profile option is included. Consumers that need the +new precedence relationship can embed YAML assets, while `WithProfiles` +continues to serve the distinct highest-precedence in-memory use case. + +## Profile Selection And Source Precedence + +Profile ID selection remains separate from profile definition lookup. An +explicit request profile ID continues to take precedence over a prompt's +`default_profile`. After an ID has been selected, matching definitions resolve +in this order: + +1. in-memory profiles supplied through `WithProfiles`; +2. the ordinary configured profile source selected through `WithProfileFile`, + `WithProfileFS`, or `Config.ProfileDir`; +3. the application fallback profile source; and +4. Promptkit's embedded built-in profiles. + +A higher-precedence source falls through only when the requested profile ID is +absent. An unreadable, malformed, duplicate, ambiguous, or otherwise invalid +matching definition is an error and does not permit lookup in a lower layer. +Profiles are selected as complete values; sources do not merge fields or +inherit from one another. + +The fallback source remains lazily read and validated when a requested ID +reaches that layer. This feature does not introduce engine-wide eager source +validation, and an unrelated malformed asset does not acquire stronger +validation guarantees than it has in an ordinary profile source. + +## Consistent Engine Behavior + +The engine will assemble one profile repository with the complete precedence +chain. Exact profile inspection, ordinary preparation, prepared execution, and +ordinary execution will all use that same repository and therefore observe the +same definition for a given profile ID. + +Exact profile inspection remains side-effect free and does not contact a model +provider. Existing public error identities remain applicable to cancellation, +profile-not-found and profile-load failures, invalid fallback definitions, +credential resolution, and unknown backends. The feature does not add a new +fallback-specific public error category. + +Internally, the root facade will own composition of all profile-source layers. +The application fallback is another use of the profile repository's existing +error-preserving overlay semantics; it is not a separate profile-loading or +validation implementation. + +## Compatibility + +The feature is additive. An engine that does not configure an application +fallback source retains its current behavior and profile precedence. Existing +uses of `Config.ProfileDir`, `WithProfileFile`, `WithProfileFS`, and +`WithProfiles` keep their meanings. + +The application fallback is deliberately below every existing +consumer-configured profile source. An ordinary configured profile therefore +continues to override any packaged definition with the same ID. A missing or +invalid configured source also retains its current behavior; the new layer +does not turn configuration failures into silent fallthrough. + +## Ownership Boundaries + +Promptkit owns: + +- the additional source layer and its construction option; +- deterministic lookup and fallthrough semantics; +- use of the existing profile format, validation, and public error mapping; +- consistent repository use across inspection, preparation, and execution; + and +- canonical public, format, consumer, and contributor documentation for the + implemented capability. + +The consuming application owns: + +- whether to supply a fallback source; +- application-specific profile IDs and their domain meaning; +- embedded profile contents, backend selections, and model choices; +- application configuration discovery and operator override policy; +- assignment of profiles to reports or other workloads; and +- credential policy and operator-facing error presentation beyond Promptkit's + public contract. + +## Non-Goals + +This scope does not add: + +- downstream-specific profiles to Promptkit's built-in catalog; +- profile inheritance, aliases, or field-level merging; +- provider failover after a selected profile or generation attempt fails; +- automatic endpoint discovery, probing, benchmarking, or tier selection; +- application configuration discovery; +- eager validation of every profile in every source; +- source-provenance fields in profile inspection or preparation results; or +- an in-memory companion to `WithFallbackProfileFS`. + +## Target End State + +The feature is complete when a consumer can embed an application fallback +profile source through the root facade and rely on the documented four-layer +precedence everywhere profiles are resolved. Operator definitions override +application defaults, application defaults override Promptkit built-ins, +invalid matching definitions never silently fall through, and engines that do +not use the new option remain behaviorally compatible. + +The implemented public option and precedence contract will be owned by GoDoc +and the framework format reference. Consumer guidance will show the embedded +application-default workflow without making this temporary roadmap a second +current-state reference. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md new file mode 100644 index 0000000..4412a59 --- /dev/null +++ b/docs/roadmap/implementation.md @@ -0,0 +1,378 @@ +# Optional Request-Parameter Omission Implementation Plan + +**Status:** Ready for implementation. + +## Purpose + +This document is the decision-complete implementation plan for +[omitting unset optional request parameters](optional-request-parameters.md). +It is written for a `gpt-5.6-terra` coding agent that will implement each stage +in order. + +The feature roadmap owns the motivation, policy choices, compatibility +boundary, non-goals, and target end state. This document owns the concrete +design, file-level work, test ownership, documentation updates, validation, +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 + +- Complete the stages in order. Stage 1 must leave code, tests, GoDoc, and + current-state documentation mutually accurate; Stage 2 performs the final + audit and repository-wide acceptance. +- Preserve unrelated working-tree changes. In particular, do not edit, + implement, retire, or reclassify `fallback-profiles.md`. +- Follow every policy under `docs/policy/`, the task-specific reading guide in + `docs/development.md`, and the accepted behavior in + `optional-request-parameters.md`. +- Keep the existing package boundaries. Framework defaults remain in + `internal/defaults`, resolution remains in `internal/usecase`, and outbound + OpenAI-compatible serialization remains in `internal/llm`. +- Do not add an exported type, field, option, method, error, or public package. + This feature changes default and wire semantics within existing contracts. +- Do not replace numeric profile fields with pointers or add profile presence + tracking. File and in-memory profile zero values retain their existing + inheritance semantics; runtime pointer overrides remain the only supported + way to select an explicit numeric zero. +- Preserve required request fields, session IDs, structured output, + credentials, extra-parameter validation, reasoning clearing, deadlines, + capacity management, and all existing precedence rules. +- Do not query a provider for defaults or capabilities and do not add + backend- or model-specific serialization branches. +- Keep tests lean and behavioral. Use the existing root precedence test to own + resolved public/injected-client metadata and the existing model-client tests + to own wire inclusion and omission. Do not duplicate those matrices in a + new end-to-end fixture. +- Update exact exported semantics in GoDoc, profile/default semantics in + `docs/formats.md`, and provider request-body semantics in + `docs/integrations/openai-compatible-chat.md` in the same stage as the code. +- Do not add release notes, change a module version, commit, tag, push, or + publish a release as part of this plan. + +## Fixed Design + +### Framework Defaults + +In `internal/defaults/defaults.go`, remove these constants: + +```go +ExecutionDefaultTemperature +ExecutionDefaultMaxTokens +ExecutionDefaultTopP +``` + +They currently encode zero for `temperature` and `max_tokens` and one for +`top_p`. Optional provider controls are no longer framework defaults, so +retaining zero-valued constants under default-oriented names would obscure the +new contract. + +Keep `ExecutionDefaultTimeoutSeconds` at its current positive value. Timeout is +a Promptkit-owned generation deadline and is not an OpenAI-compatible request +body field. + +Keep `ExecutionTargetDefault` as the common resolution baseline, but have it +initialize only `TimeoutSeconds`. The zero Go values for `Temperature`, +`MaxTokens`, and `TopP` then represent unspecified provider controls. Do not +rename this internal function or add a second defaults constructor. + +### Resolution And Public Metadata + +Do not change the merge functions or precedence in +`internal/usecase/runner.go`: + +1. the baseline target supplies only the Promptkit timeout; +2. nonzero profile numeric fields replace the baseline; +3. non-nil runtime numeric overrides replace profile values; and +4. `ExecutionTargetPresence` records runtime overrides, including explicit + zero values. + +Consequently, when a profile omits the optional provider controls, resolved +`ExecutionTarget` values contain zero for `Temperature`, `MaxTokens`, and +`TopP`. That zero is stable metadata for “unspecified” unless the accompanying +`GenerateRequest.TargetPresence` bit reports an explicit runtime zero. + +Do not expose target presence in `PreparedRun`, `RunResult`, or +`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 +provider control an effective provider value: + +- `ExecutionTarget.Temperature`, `MaxTokens`, and `TopP` must each state that + zero leaves the field unspecified to compatible providers unless the + 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 +formatting, or copying behavior. + +### Outbound Request Semantics + +The current built-in client already has the required mechanism: +`openAIChatRequestFromGenerateRequest` includes `temperature`, `max_tokens`, or +`top_p` when the resolved value is nonzero or the corresponding target-presence +bit is true, and `openAIChatRequestPayload` omits nil fields. Preserve that +logic. + +No production change should be needed in +`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: + +- an omitted profile `top_p` resolves to zero and is absent from the body; +- a nonzero profile or runtime `top_p` is included; +- 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 + +Do not change YAML or public `Profile` field shapes. Numeric zero in a file or +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 +those files are explicit profile policy and remain effective. Existing +nonzero-profile tests are sufficient to protect explicit inclusion; do not add +one test per built-in asset or parameter. + +### Test Ownership + +Use these existing boundaries: + +- In `engine_test.go`, update the “framework defaults” row of + `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 +integration test. Add a new test only if the implementation exposes a distinct +contract risk not covered by the tests above, and record that reason in the +test name or nearby test structure rather than in a new planning document. + +### Canonical Documentation + +Update current-state documentation in Stage 1: + +- In `docs/formats.md`, replace the optional provider-control entries in the + framework-default table with clear unspecified/omitted semantics, while + retaining the positive `timeout_seconds` framework default. Explain that + profile numeric zero inherits a lower layer and otherwise remains + unspecified; an explicit runtime pointer zero is retained. +- In `docs/integrations/openai-compatible-chat.md`, state that + `temperature`, `max_tokens`, and `top_p` are included only when selected by a + profile or runtime override, including explicit runtime zero, and are absent + when unspecified. Keep the existing ownership of required fields, + `session_id`, structured output, extra parameters, and timeout behavior. +- In `types.go`, apply the GoDoc changes described above; these declarations + own the exact public value semantics. + +Do not add a README or release-document note. The consumer guide already +routes exact field behavior to GoDoc and profile/default behavior to the format +reference, so do not duplicate the new contract there. The internal LLM +document describes flow rather than exact field omission and does not require +a change unless its current text is found to contradict the implementation. + +## Stage 1: Implement Omission Semantics And Canonical Contracts + +### Objective + +Remove optional provider controls from the framework baseline, preserve +explicit profile and runtime values, update the canonical contracts, and prove +the behavior at the existing resolution and wire boundaries. + +### Implementation Prompt + +Implement only Stage 1 of `docs/roadmap/implementation.md`. Read the complete +feature roadmap, implementation rules, and fixed design above before editing. + +1. In `internal/defaults/defaults.go`, remove the three provider-control + constants and make `ExecutionTargetDefault` initialize only + `TimeoutSeconds`. +2. In `engine_test.go`, update and rename the default-precedence table row + exactly as described under Test Ownership. +3. Remove the redundant literal-default test from + `internal/usecase/runner_test.go`; do not weaken other precedence, + profile-value, or runtime-zero tests. +4. Update the affected exported GoDoc in `types.go` without changing any + declaration, JSON tag, or serialization shape. +5. Update `docs/formats.md` and + `docs/integrations/openai-compatible-chat.md` according to Canonical + Documentation. +6. Run the focused validation below. Repair regressions in scope, but do not + broaden the feature or change the established serializer merely to make a + mistaken expectation pass. + +Do not edit built-in profile assets, fallback-profile work, backend +registration, profile parsing, target merge logic, public value shapes, +prepared-execution lifecycle, capacity management, or release material. + +### Focused Validation + +Run from the repository root: + +```sh +gofmt -w internal/defaults/defaults.go types.go engine_test.go \ + internal/usecase/runner_test.go +go test . -run 'TestEngineExecutionSettingPrecedence' +go test ./internal/llm -run \ + 'TestOpenAICompatibleClient(GenerateSuccess|OmitsImplicitZeroNumericFields|SerializesExplicitZeroNumericOverrides)' +go test ./internal/usecase -run \ + 'Test(ResolveExecutionTarget|RunnerPrepareRequestNumericOverridePresence|RunnerPrepareSelectedProfileBeatsBuiltInDefault|RunnerRunSelectedProfileBeatsBuiltInDefault)' +go test . ./internal/defaults ./internal/usecase ./internal/llm +go vet . ./internal/defaults ./internal/usecase ./internal/llm +git diff --check +``` + +If a focused regular expression does not match an existing test name, inspect +the current names and run the narrowest equivalent set; do not silently skip +the intended resolution, explicit-profile, explicit-zero, and wire-omission +coverage. + +### Completion Gate + +Stage 1 is complete only when: + +- the resolution baseline contains no provider tuning value and retains the + Promptkit timeout; +- an omitted `top_p` resolves to zero and the built-in client omits it; +- nonzero profile and runtime values remain effective and serialized; +- explicit runtime zero values remain distinguishable and serialized through + `ExecutionTargetPresence`; +- no public type or stable JSON shape changed; +- required fields, structured output, session IDs, extra parameters, + reasoning, credentials, and deadlines retain their existing behavior; +- GoDoc, format documentation, and the integration contract describe the + implemented behavior without conflicting ownership; and +- all focused tests, vet, formatting, and whitespace checks pass. + +## Stage 2: Audit Compatibility And Validate The Repository + +### Objective + +Confirm that the narrow semantic change is complete across all public, +injected-client, built-in-profile, documentation, and repository surfaces, +then mark the temporary planning documents complete. + +### Implementation Prompt + +Implement only Stage 2 of `docs/roadmap/implementation.md` after Stage 1 +satisfies its completion gate. + +1. Search tracked Go and Markdown files for the removed constant names, + framework `top_p` defaults, claims that all effective provider controls have + concrete framework values, and request-body inclusion rules. Correct only + stale statements or tests owned by this feature. +2. Confirm that `internal/profile/builtin/assets/` has no feature-related diff + and that its explicit nonzero optional controls still pass ordinary profile + validation and resolution tests. +3. Confirm that `internal/llm/openai_compatible_client.go` either has no diff or + contains only a change required by a focused failing contract test. The + default policy must remain outside the transport. +4. Follow every changed Markdown link and confirm its target exists. Verify + that current-state documents describe implemented behavior and that exact + contracts remain with GoDoc, the format reference, and the integration + contract. +5. Run the complete validation sequence below and repair only in-scope + failures. +6. After all checks pass, change the status of + `optional-request-parameters.md` and this document to `Complete`. Do not + 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 +maintainer action. Do not add release notes, change versions, or create a +commit, tag, push, or release. + +### Full Validation + +Run from the repository root: + +```sh +gofmt -w internal/defaults/defaults.go types.go engine_test.go \ + internal/usecase/runner_test.go +gofmt -l $(git ls-files '*.go') +go test ./... +go test -race ./... +go vet ./... +go build ./... +go run ./examples/go-library/prepare +git diff --check +git status --short +``` + +The `gofmt -l` command must print no paths. The maintained example must remain +offline and require no real credential or provider. + +Inspect the final diff and confirm: + +- only this feature's files and pre-existing user changes are present; +- no built-in profile asset, public declaration shape, stable JSON tag, + credential rule, workspace file, local module replacement, generated binary, + or unrelated formatting changed; +- the removed provider-default constants have no remaining references; +- the provider omission policy is implemented by resolution plus the existing + generic serializer, not by a `top_p` transport special case; +- the optional-parameter roadmap and this plan are complete while the fallback + roadmap remains selected; and +- no commit, tag, push, or release was created. + +### Completion Gate + +The implementation is complete only when: + +- every Stage 1 gate remains satisfied; +- the ordinary and race-enabled suites pass; +- vet, build, formatting, the maintained offline example, Markdown links, and + whitespace checks pass; +- public metadata, injected-client presence, profile inheritance, and outbound + omission semantics are mutually consistent; +- explicit built-in and consumer profile values retain their behavior; +- both feature-specific roadmap statuses are `Complete`; +- `fallback-profiles.md` remains unchanged and selected for later work; and +- the repository is ready for maintainer review without a commit or release + having been created by this plan. + +## Open Questions + +None. The feature roadmap and fixed design above fully specify the behavior, +compatibility boundary, implementation, documentation ownership, and test +strategy. diff --git a/docs/roadmap/optional-request-parameters.md b/docs/roadmap/optional-request-parameters.md new file mode 100644 index 0000000..f59a731 --- /dev/null +++ b/docs/roadmap/optional-request-parameters.md @@ -0,0 +1,83 @@ +# Omit Unset Optional Request Parameters + +Status: Selected for implementation. + +## Purpose + +Promptkit will stop turning an unset optional provider control into an explicit +outbound value. Required protocol fields and settings needed to enforce +Promptkit's own contracts will retain defined behavior, while provider tuning +choices will be sent only when a supported configuration layer selects them. + +This keeps profiles intentional, avoids overriding model- or backend-specific +defaults, and improves compatibility across OpenAI-compatible endpoints with +different supported parameter sets. + +## Scope + +The framework-level `top_p` default of `1` will become unspecified. When no +profile or runtime override supplies `top_p`, the built-in OpenAI-compatible +client will omit it from the request body rather than serialize `1`. + +This establishes the general outbound policy for currently supported optional +controls: + +- `temperature`, `max_tokens`, and `top_p` are omitted when unspecified and + included when selected by a profile or runtime override; +- an explicit numeric zero supplied through a runtime override remains present + on the wire through the existing numeric-presence contract; +- `service_tier` and `reasoning_effort` remain omitted when their resolved + values are empty; +- backend, profile, or runtime `extra_params` remain explicit configuration and + are sent when present; and +- `session_id` remains conditional on a supplied nonempty value, while + `response_format` remains conditional on the effective output contract. + +The request body will continue to require `model` and `messages`. Endpoint and +credential resolution remain transport concerns rather than body defaults. +The positive framework `timeout_seconds` default also remains in place because +it enforces a Promptkit-owned generation deadline and is not serialized as a +provider request field. + +## Effective Settings And Compatibility + +An unspecified numeric provider control continues to use its zero Go value in +resolved public metadata. For a `GenerateRequest` delivered to an injected +client, the existing `ExecutionTargetPresence` value distinguishes an explicit +runtime zero from an inherited unspecified zero. Prepared and inspection +metadata will continue to report the resolved numeric value without adding +source-provenance fields. + +The behavior change is intentional and belongs in a minor release. Consumers +that require stable sampling behavior should declare the desired values in +their profiles or runtime overrides instead of relying on Promptkit to repeat a +provider's conventional default. + +Built-in profile parameters remain explicit profile policy and will continue +to be sent. An explicit built-in setting will not be removed merely because +the framework default becomes unspecified. + +## Non-Goals + +This change does not add: + +- pointer-valued numeric fields to file or in-memory profiles; +- a general provenance model for effective settings; +- automatic discovery of provider defaults or supported parameters; +- backend- or model-specific request-shape negotiation; +- changes to required request fields, structured-output behavior, credentials, + or timeout enforcement; or +- removal of deliberately configured settings from consumer or built-in + profiles. + +## Target End State + +An otherwise unset optional provider tuning parameter is absent from the +OpenAI-compatible request body. Explicit profile values and runtime overrides, +including explicit runtime zero values, retain their current precedence and +wire effect. Promptkit continues to supply only the required protocol fields +and the settings necessary to honor its own execution and output contracts. + +GoDoc, the framework format reference, and the OpenAI-compatible integration +contract will own the implemented omission and metadata semantics once the +feature lands. diff --git a/engine_test.go b/engine_test.go index 7245662..173948f 100644 --- a/engine_test.go +++ b/engine_test.go @@ -360,14 +360,14 @@ func TestEngineExecutionSettingPrecedence(t *testing.T) { wantPresence promptkit.ExecutionTargetPresence }{ { - name: "framework defaults fill zero-valued profile settings", + name: "unspecified provider controls retain framework timeout", profile: defaultsProfile, want: promptkit.ExecutionTarget{ Endpoint: defaultsProfile.endpoint, Model: defaultsProfile.model, Temperature: 0, MaxTokens: 0, - TopP: 1, + TopP: 0, TimeoutSeconds: 600, ServiceTier: defaultsProfile.serviceTier, ReasoningEffort: defaultsProfile.reasoningEffort, diff --git a/internal/defaults/defaults.go b/internal/defaults/defaults.go index a0cb744..9274f65 100644 --- a/internal/defaults/defaults.go +++ b/internal/defaults/defaults.go @@ -14,9 +14,6 @@ const ( ContentTypeApplicationJSON = "application/json" OpenAIChatCompletionsPath = "/chat/completions" - ExecutionDefaultTemperature = 0.0 - ExecutionDefaultMaxTokens = 0 - ExecutionDefaultTopP = 1.0 ExecutionDefaultTimeoutSeconds = 600 ) @@ -26,9 +23,6 @@ var ( func ExecutionTargetDefault() domain.ExecutionTarget { return domain.ExecutionTarget{ - Temperature: ExecutionDefaultTemperature, - MaxTokens: ExecutionDefaultMaxTokens, - TopP: ExecutionDefaultTopP, TimeoutSeconds: ExecutionDefaultTimeoutSeconds, } } diff --git a/internal/usecase/runner_test.go b/internal/usecase/runner_test.go index 7a62a58..c0bf3d3 100644 --- a/internal/usecase/runner_test.go +++ b/internal/usecase/runner_test.go @@ -1690,36 +1690,6 @@ func TestRunnerRunSelectedProfileBeatsBuiltInDefault(t *testing.T) { } } -func TestRunnerRunBuiltInDefaultsUsedWhenProfileOmitsOptionalFields(t *testing.T) { - promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)} - execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{ - "exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model"}, - }} - llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}} - runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), llmClient, nil, nil) - - res, err := runner.Run(context.Background(), domain.RunRequest{ - PromptID: "p", - ProfileID: "exec", - Inputs: singleInputRef(), - }) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - if res.EffectiveModelParams.Temperature != defaults.ExecutionDefaultTemperature { - t.Fatalf("expected default temperature %v, got %v", defaults.ExecutionDefaultTemperature, res.EffectiveModelParams.Temperature) - } - if res.EffectiveModelParams.TopP != defaults.ExecutionDefaultTopP { - t.Fatalf("expected default top_p %v, got %v", defaults.ExecutionDefaultTopP, res.EffectiveModelParams.TopP) - } - if res.EffectiveModelParams.MaxTokens != defaults.ExecutionDefaultMaxTokens { - t.Fatalf("expected default max_tokens %d, got %d", defaults.ExecutionDefaultMaxTokens, res.EffectiveModelParams.MaxTokens) - } - if res.EffectiveModelParams.TimeoutSeconds != defaults.ExecutionDefaultTimeoutSeconds { - t.Fatalf("expected default timeout_seconds %d, got %d", defaults.ExecutionDefaultTimeoutSeconds, res.EffectiveModelParams.TimeoutSeconds) - } -} - func TestRunnerRunAPIKeyEnvResolvesFromEnvironment(t *testing.T) { t.Setenv("PROMPTKIT_TEST_API_KEY", "secret") promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)} diff --git a/types.go b/types.go index d74f61a..1049187 100644 --- a/types.go +++ b/types.go @@ -116,7 +116,8 @@ type RunRequest struct { // empty maps are equivalent. Vars map[string]string // Execution optionally overrides individual execution settings. Nil uses - // the selected profile over its backend, when any, and framework defaults. + // the selected profile over its backend, when any, and the framework + // baseline. Execution *ExecutionTargetOverride // Validation optionally replaces the prompt's complete output contract. It // does not merge individual fields. Nil uses the prompt contract. @@ -144,9 +145,10 @@ type PreparedRun struct { // SelectedBackendID equals EffectiveModelParams.BackendID. It is empty for // an endpoint-only profile. SelectedBackendID string `json:"selected_backend_id,omitempty"` - // EffectiveModelParams contains framework defaults overlaid by the selected - // backend, profile, and then request overrides. It excludes resolved API-key - // values. + // EffectiveModelParams contains settings resolved from the framework timeout + // baseline, selected backend, profile, and then request overrides. Unset + // optional provider controls remain zero rather than reporting a provider + // default. It excludes resolved API-key values. EffectiveModelParams ExecutionTarget `json:"effective_model_params"` // OutputContract is the complete effective output contract. OutputContract OutputContract `json:"output_contract"` @@ -298,13 +300,17 @@ type ExecutionTarget struct { Endpoint string `json:"endpoint"` // Model is the provider model identifier. Model string `json:"model"` - // Temperature is the effective sampling temperature from 0 through 2. + // Temperature is the resolved sampling temperature from 0 through 2. Zero + // leaves the field unspecified to compatible providers unless the + // corresponding ExecutionTargetPresence bit is true. Temperature float64 `json:"temperature"` - // MaxTokens is the non-negative effective output-token limit. Zero leaves - // the limit unspecified to compatible providers unless it was an explicit - // request override. + // MaxTokens is the non-negative resolved output-token limit. Zero leaves + // the limit unspecified to compatible providers unless the corresponding + // ExecutionTargetPresence bit is true. MaxTokens int `json:"max_tokens"` - // TopP is the effective nucleus-sampling value from 0 through 1. + // TopP is the resolved nucleus-sampling value from 0 through 1. Zero leaves + // the field unspecified to compatible providers unless the corresponding + // ExecutionTargetPresence bit is true. TopP float64 `json:"top_p"` // TimeoutSeconds is the non-negative per-generation deadline. Zero disables // this deadline without disabling caller cancellation or the transport cap. @@ -329,9 +335,11 @@ type ExecutionTarget struct { type ProfileInspection struct { // ProfileID is the trimmed, exact profile ID inspected by the engine. ProfileID string - // EffectiveModelParams contains framework defaults overlaid by the selected - // backend and then the profile, without a request override. APIKeyEnv is an - // environment-variable name, never its credential value. + // EffectiveModelParams contains settings resolved from the framework timeout + // baseline, selected backend, and then profile, without a request override. + // Unset optional provider controls remain zero rather than reporting a + // provider default. APIKeyEnv is an environment-variable name, never its + // credential value. EffectiveModelParams ExecutionTarget // APIKeyRequired reports that a later execution must supply a direct API // key or an explicit request environment override. It is mutually exclusive @@ -382,19 +390,26 @@ type PromptInspection struct { // fields replace profile values and preserve explicit zero or empty values. A // non-empty ExtraParams map replaces the complete profile or backend map // rather than merging keys. Empty string fields, nil pointers, and a nil or -// empty ExtraParams map inherit the selected profile over its backend, when -// any, and framework defaults. +// empty ExtraParams map inherit lower-precedence values. An optional provider +// control that remains zero is unspecified; TimeoutSeconds retains its +// framework deadline when no higher-precedence value is present. type ExecutionTargetOverride struct { // Endpoint replaces the profile or backend endpoint when non-empty without // changing the effective BackendID. Endpoint string // Model replaces the profile model when non-empty. Model string - // Temperature, when non-nil, must point to a value from 0 through 2. + // Temperature, when non-nil, must point to a value from 0 through 2. A + // pointed-to zero is explicitly present; nil inherits a lower-precedence + // value and otherwise leaves the provider control unspecified. Temperature *float64 - // MaxTokens, when non-nil, must point to a non-negative value. + // MaxTokens, when non-nil, must point to a non-negative value. A pointed-to + // zero is explicitly present; nil inherits a lower-precedence value and + // otherwise leaves the provider control unspecified. MaxTokens *int - // TopP, when non-nil, must point to a value from 0 through 1. + // TopP, when non-nil, must point to a value from 0 through 1. A pointed-to + // zero is explicitly present; nil inherits a lower-precedence value and + // otherwise leaves the provider control unspecified. TopP *float64 // TimeoutSeconds, when non-nil, must point to a non-negative value. A // pointed-to zero disables the per-generation deadline. @@ -425,9 +440,11 @@ type ExecutionTargetOverride struct { // 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. Numeric -// zero, blank strings, and an empty ExtraParams map inherit framework defaults; -// use ExecutionTargetOverride pointer fields to request explicit numeric zero. +// WithProfiles validates and copies Profile values during NewEngine. Optional +// numeric zero, blank strings, and an empty ExtraParams map inherit +// lower-precedence values. An optional provider control that remains zero is +// unspecified; 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 @@ -441,19 +458,23 @@ type Profile struct { Endpoint string // Model is the required non-blank provider model identifier. Model string - // Temperature is from 0 through 2. Zero inherits the framework default. + // Temperature is from 0 through 2. Zero inherits a lower-precedence value + // and otherwise leaves the provider control unspecified. Temperature float64 - // MaxTokens is non-negative. Zero inherits the framework default. + // MaxTokens is non-negative. Zero inherits a lower-precedence value and + // otherwise leaves the provider control unspecified. MaxTokens int - // TopP is from 0 through 1. Zero inherits the framework default rather than - // selecting an explicit zero. + // TopP is from 0 through 1. Zero inherits a lower-precedence value and + // otherwise leaves the provider control unspecified rather than selecting an + // explicit zero. TopP float64 - // TimeoutSeconds is non-negative. Zero inherits the framework default. + // TimeoutSeconds is non-negative. Zero inherits a lower-precedence value and + // otherwise the framework deadline. TimeoutSeconds int - // ServiceTier is optional; a blank value inherits the framework default. + // ServiceTier is optional; a blank value inherits a lower-precedence value. ServiceTier string - // ReasoningEffort is optional; a blank value inherits the framework - // default. + // ReasoningEffort is optional; a blank value inherits a lower-precedence + // value. ReasoningEffort string // APIKeyRequired clears a backend's inherited API-key environment name and // requires a non-blank RunRequest.APIKey unless the request explicitly