Files
scriptorium/docs/roadmap/implementation.md

10 KiB

Runtime Parameter Implementation Plan

This plan implements the target state in docs/roadmap/params.md.

Audience: LLM coding agents implementing the feature in order. Follow docs/policy/architecture.md, docs/policy/development.md, and docs/policy/documentation.md before changing code.

Constraints

  • Keep adapters thin. CLI and HTTP should capture caller intent and map it into domain request types; merge decisions belong in internal/usecase.
  • Keep external decoding strict. Unknown YAML/JSON fields must continue to fail.
  • Do not accept or emit raw API key values.
  • Do not add dependencies unless there is a clear need. This feature should use the standard library plus existing dependencies.
  • Do not expand the HTTP API surface beyond POST /v1/runs.
  • Do not add provider-specific adapter packages.
  • Keep each stage passing go test ./... before moving to the next stage.

Stage 1: Presence-Aware Request Overrides

Goal: make per-request numeric execution overrides presence-aware while keeping resolved execution settings concrete.

Domain Changes

  1. In internal/domain/domain.go, add a request-only type:
type ExecutionTargetOverride struct {
	Endpoint        string         `json:"endpoint,omitempty"`
	Model           string         `json:"model,omitempty"`
	Temperature     *float64       `json:"temperature,omitempty"`
	MaxTokens       *int           `json:"max_tokens,omitempty"`
	TopP            *float64       `json:"top_p,omitempty"`
	TimeoutSeconds  *int           `json:"timeout_seconds,omitempty"`
	ServiceTier     string         `json:"service_tier,omitempty"`
	ReasoningEffort string         `json:"reasoning_effort,omitempty"`
	APIKeyEnv       string         `json:"api_key_env,omitempty"`
	ExtraParams     map[string]any `json:"extra_params,omitempty"`
}
  1. Change domain.RunRequest.Execution from *ExecutionTarget to *ExecutionTargetOverride.
  2. Change ExecutionProfile.ExtraParams and ExecutionTarget.ExtraParams from map[string]string to map[string]any.
  3. Keep ExecutionTarget concrete. It represents the resolved effective runtime target after defaults, profile, and request overrides are merged.

Runner Changes

  1. Update internal/usecase/runner.go so profile values still merge over built-in defaults and request overrides merge over that result.
  2. Keep the existing concrete profile merge semantics for profile numeric fields.
  3. Add a separate request override merge path that uses pointer presence:
    • nil numeric pointer means omitted; preserve the current value.
    • non-nil numeric pointer means explicit override, even when the value is 0.
  4. Validate request override numeric values before or during merge:
    • temperature: 0 <= value <= 2
    • max_tokens: value >= 0
    • top_p: 0 <= value <= 1
    • timeout_seconds: value >= 0
  5. Preserve existing validation after merge:
    • effective endpoint required
    • effective model required
    • api_key_env, when set, must name a non-empty environment variable
  6. Preserve secret handling. The resolved API key value must never be stored in PreparedRun, RunResult, logs, or HTTP responses.

CLI Changes

  1. Update internal/adapter/cli/run.go request construction to build domain.ExecutionTargetOverride.
  2. Use the existing flagWasSet booleans to populate numeric pointers only when the user provided the flag.
  3. Required behavior:
    • omitted --temperature preserves profile/default temperature;
    • --temperature 0 explicitly sets temperature to zero;
    • omitted --top-p preserves profile/default top-p;
    • --top-p 0 explicitly sets top-p to zero;
    • omitted --max-tokens preserves profile/default max tokens;
    • --max-tokens 0 explicitly sets max tokens to zero;
    • omitted --timeout preserves profile/default timeout;
    • --timeout 0s explicitly sets timeout seconds to zero.
  4. Do not add new CLI flags in this stage.

HTTP Changes

  1. Update internal/adapter/http/dto.go so numeric model override fields are pointers:
    • Temperature *float64
    • MaxTokens *int
    • TopP *float64
    • TimeoutSeconds *int
  2. Update DTO mapping in internal/adapter/http/handler.go to build domain.ExecutionTargetOverride.
  3. Preserve strict JSON decoding and existing error mapping.
  4. Required behavior:
    • omitted numeric JSON fields preserve profile/default values;
    • explicit numeric zero JSON fields override profile/default values.

Tests

Add or update tests in:

  • internal/usecase/runner_test.go
  • internal/adapter/cli/run_test.go
  • internal/adapter/http/handler_test.go

Required test coverage:

  • Runner preserves profile value when request numeric override is omitted.
  • Runner applies explicit zero request override for temperature.
  • Runner applies explicit zero request override for top_p.
  • Runner applies explicit zero request override for max_tokens.
  • Runner applies explicit zero request override for timeout_seconds.
  • Invalid request override ranges fail as invalid request errors.
  • CLI --temperature 0 reaches effective settings as zero.
  • HTTP "temperature": 0 reaches effective settings as zero.
  • HTTP omitted temperature preserves profile/default value.

Verification

Run:

go test ./...

Stage 2: JSON-Compatible extra_params

Goal: allow provider-specific parameters to carry JSON-compatible values throughout profile, HTTP, prepared output, metadata, and LLM request construction.

Domain And Loader Changes

  1. Complete all compile fixes from changing ExtraParams to map[string]any.
  2. Ensure internal/profile/filesystem_repository.go continues to decode profiles strictly while allowing nested JSON-compatible values under extra_params.
  3. Add profile repository tests for extra_params containing:
    • string
    • number
    • boolean
    • nested object or array
  4. Ensure formatter output remains deterministic:
    • keep sorting extra_params keys in internal/format/prepared_run.go;
    • render non-string values with stable JSON encoding in text output.
  5. Preserve JSON formatter behavior through normal encoding/json output.

HTTP Changes

  1. Change HTTP model override ExtraParams to map[string]any.
  2. Add handler tests proving HTTP accepts JSON-compatible extra_params values.
  3. Preserve strict rejection of unknown fields and raw API-key payload fields.

Verification

Run:

go test ./...

Stage 3: Outbound Serialization

Goal: serialize reasoning_effort and extra_params to the OpenAI-compatible chat-completions request.

LLM Adapter Changes

  1. In internal/llm/openai_compatible_client.go, add first-class outbound support for reasoning_effort.
  2. Add extra_params support by flattening domain.ExecutionTarget.ExtraParams into additional top-level JSON request fields.
  3. Implement reserved-field collision checks before the HTTP request is made.
  4. Reserved keys must include:
    • model
    • session_id
    • messages
    • temperature
    • max_tokens
    • top_p
    • service_tier
    • reasoning_effort
    • response_format
  5. Reject empty extra_params keys.
  6. Ensure each extra_params value can be marshaled as JSON. If marshaling fails, return ErrInvalidRequest with context.
  7. Keep existing request behavior unchanged when reasoning_effort and extra_params are unset.

Use a custom marshal path for the outbound chat request rather than string manipulation.

One acceptable shape:

  • Add ReasoningEffort string and ExtraParams map[string]any to the internal openAIChatRequest.
  • Add a helper that converts openAIChatRequest into map[string]any, inserts first-class fields when set, then inserts ExtraParams after collision validation.
  • Marshal that map with encoding/json.

Do not construct outbound JSON with manual string concatenation.

Tests

Update internal/llm/openai_compatible_client_test.go.

Required test coverage:

  • outbound JSON includes reasoning_effort when set;
  • outbound JSON omits reasoning_effort when unset;
  • outbound JSON includes string, number, boolean, object, and array extra_params;
  • reserved extra_params keys fail before provider call;
  • empty extra_params keys fail before provider call;
  • existing message, cache-control, service-tier, response-format, and usage parsing tests continue to pass.

Verification

Run:

go test ./...

Stage 4: Documentation And Examples

Goal: move implemented behavior from roadmap to canonical docs after code is complete.

Update only after Stages 1 through 3 are implemented.

Required Docs

Update:

  • docs/config.md
  • docs/cli.md
  • docs/integrations/http-api.md
  • docs/integrations/openai-compatible-chat.md
  • docs/internal/runner.md
  • docs/internal/adapters.md

Required documentation content:

  • reasoning_effort is serialized outbound when set.
  • extra_params serializes as provider-specific top-level outbound JSON fields.
  • extra_params supports JSON-compatible values.
  • reserved extra_params fields are rejected.
  • per-request numeric overrides distinguish omitted values from explicit zero values.
  • CLI explicit zero behavior for existing numeric flags.
  • HTTP explicit zero behavior for model override numeric fields.
  • no raw API-key values are accepted or emitted.

Examples

Update examples only if needed to keep them accurate and runnable.

If adding an extra_params example, keep it secret-free and simple. Prefer a harmless provider-routing example over a vendor-specific feature that requires special credentials.

Verification

Run:

go test ./...
go run ./cmd/scriptorium render \
  --config ./examples/config.yml \
  --prompt generic.markdown_summary \
  --input transcript=./examples/fixtures/transcript.md \
  --input glossary=./examples/fixtures/glossary.yml \
  --format json

Final Checks

Before considering the feature complete:

  1. Confirm git diff contains only intended code, test, doc, and example changes.
  2. Confirm all non-roadmap docs describe implemented behavior only.
  3. Confirm no output path exposes raw API key values.
  4. Confirm go test ./... passes.
  5. Confirm the render smoke command passes.