From e1e5351c5d866ad44cf770684f4ee51d46c34f29 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 4 Jul 2026 17:24:02 -0500 Subject: [PATCH] Clean up completed roadmap docs --- docs/roadmap/builtins.md | 142 ------ docs/roadmap/implementation.md | 451 ------------------ docs/roadmap/library.md | 176 ------- docs/roadmap/profiles/aion-labs/aion-2.yml | 9 - .../anthropic/claude-fable-latest.yml | 7 - .../anthropic/claude-haiku-latest.yml | 7 - .../profiles/anthropic/claude-opus-latest.yml | 7 - .../anthropic/claude-sonnet-latest.yml | 7 - .../profiles/deepseek/deepseek-3-2.yml | 7 - .../profiles/deepseek/deepseek-4-pro.yml | 7 - .../profiles/google/gemini-2-flash-lite.yml | 9 - .../profiles/google/gemini-2-flash.yml | 9 - docs/roadmap/profiles/google/gemini-2-pro.yml | 9 - .../profiles/google/gemini-3-flash-lite.yml | 9 - .../profiles/google/gemini-flash-latest.yml | 9 - .../profiles/google/gemini-pro-latest.yml | 9 - docs/roadmap/profiles/google/gemma-4-31b.yml | 9 - docs/roadmap/profiles/minimax/minimax-m2.yml | 9 - docs/roadmap/profiles/minimax/minimax-m3.yml | 9 - .../profiles/mistral/mistral-large-2512.yml | 7 - .../profiles/mistral/mistral-medium-3-5.yml | 8 - .../profiles/mistral/mistral-small-3.yml | 7 - .../profiles/mistral/mistral-small-4.yml | 8 - .../profiles/nvidia/nemotron-3-ultra.yml | 7 - docs/roadmap/profiles/openai/gpt-5-mini.yml | 7 - docs/roadmap/profiles/openai/gpt-5-nano.yml | 7 - 26 files changed, 952 deletions(-) delete mode 100644 docs/roadmap/builtins.md delete mode 100644 docs/roadmap/implementation.md delete mode 100644 docs/roadmap/library.md delete mode 100644 docs/roadmap/profiles/aion-labs/aion-2.yml delete mode 100644 docs/roadmap/profiles/anthropic/claude-fable-latest.yml delete mode 100644 docs/roadmap/profiles/anthropic/claude-haiku-latest.yml delete mode 100644 docs/roadmap/profiles/anthropic/claude-opus-latest.yml delete mode 100644 docs/roadmap/profiles/anthropic/claude-sonnet-latest.yml delete mode 100644 docs/roadmap/profiles/deepseek/deepseek-3-2.yml delete mode 100644 docs/roadmap/profiles/deepseek/deepseek-4-pro.yml delete mode 100644 docs/roadmap/profiles/google/gemini-2-flash-lite.yml delete mode 100644 docs/roadmap/profiles/google/gemini-2-flash.yml delete mode 100644 docs/roadmap/profiles/google/gemini-2-pro.yml delete mode 100644 docs/roadmap/profiles/google/gemini-3-flash-lite.yml delete mode 100644 docs/roadmap/profiles/google/gemini-flash-latest.yml delete mode 100644 docs/roadmap/profiles/google/gemini-pro-latest.yml delete mode 100644 docs/roadmap/profiles/google/gemma-4-31b.yml delete mode 100644 docs/roadmap/profiles/minimax/minimax-m2.yml delete mode 100644 docs/roadmap/profiles/minimax/minimax-m3.yml delete mode 100644 docs/roadmap/profiles/mistral/mistral-large-2512.yml delete mode 100644 docs/roadmap/profiles/mistral/mistral-medium-3-5.yml delete mode 100644 docs/roadmap/profiles/mistral/mistral-small-3.yml delete mode 100644 docs/roadmap/profiles/mistral/mistral-small-4.yml delete mode 100644 docs/roadmap/profiles/nvidia/nemotron-3-ultra.yml delete mode 100644 docs/roadmap/profiles/openai/gpt-5-mini.yml delete mode 100644 docs/roadmap/profiles/openai/gpt-5-nano.yml diff --git a/docs/roadmap/builtins.md b/docs/roadmap/builtins.md deleted file mode 100644 index 6bc8e93..0000000 --- a/docs/roadmap/builtins.md +++ /dev/null @@ -1,142 +0,0 @@ -# Built-In Profiles Roadmap - -This roadmap defines the target behavior for adding built-in execution profiles to Scriptorium. - -Built-in profiles are useful for CLI, HTTP, subprocess, and library consumers, and they provide a clean foundation for public-package ergonomics. - -## Motivation - -Scriptorium currently requires a profile source for every run path. That is appropriate for fully custom deployments, but it creates unnecessary setup for common model targets where stable profile definitions can be shipped with the application. - -Built-in profiles should let callers select standard profile IDs without creating local profile files. Users and downstream applications should still be able to override any built-in profile by providing a custom profile with the same ID. - -## Target Behavior - -Scriptorium should include a built-in set of execution profiles compiled into the binary/package. - -Profile lookup should use this precedence: - -1. user-provided or downstream-provided profiles; -2. built-in profiles; -3. profile-not-found error. - -If a user profile and a built-in profile share the same ID, the user profile wins. This is intentional override behavior and should not be treated as a duplicate-profile error. - -Duplicate profile IDs within the user profile source should remain invalid. Duplicate profile IDs within the built-in profile set should be prevented by tests. Duplicate IDs across the user source and built-in source are valid because they express override intent. - -Once built-ins exist, `profile_dir` should no longer be required for CLI, HTTP, or public library engine construction. When no custom profile source is configured, Scriptorium should use the built-in profile repository alone. When a custom profile source is configured, Scriptorium should overlay it on top of the built-in repository. - -## Architecture - -Built-in profiles should be modeled as another implementation of the existing `profile.Repository` boundary. - -Recommended repository structure: - -- filesystem or custom profile repository for user-provided profiles; -- built-in profile repository backed by embedded profile YAML; -- overlay repository that checks the primary repository first and falls back to built-ins only when the primary returns `profile.ErrProfileNotFound`. - -The runner should continue to depend only on `profile.Repository`. It should not know whether a selected profile came from a file, a built-in definition, or a future public-package source. - -### Built-In Repository - -Built-in definitions should be stored as normal profile YAML and embedded into the binary with Go `embed`. - -Recommended package shape: - -- `internal/profile/builtin` owns embedded built-in profile assets and exposes a repository constructor. -- built-in profile files live under that package in a stable asset directory. -- the built-in repository reuses the same strict decoding and validation rules as normal profiles. - -Using YAML for built-ins keeps the built-in profile format aligned with the documented profile format and lets maintainers add stable definitions without duplicating profile construction logic in Go. - -### FS Repository - -The implementation should introduce or reuse an `fs.FS`-based profile repository rather than making the built-in loader special-purpose. - -That repository supports: - -- embedded built-in profile assets; -- embedded or virtual profile sources in public library work; -- fixture-based tests without temporary directory setup where useful. - -The existing filesystem repository can remain as a thin path-based adapter, or it can delegate internally to the `fs.FS` repository where that is clean and maintainable. - -### Overlay Repository - -An overlay repository should compose two repositories: - -- primary: user-provided, custom, or downstream profile source; -- fallback: built-in profile source. - -Lookup behavior: - -- return the primary result if primary lookup succeeds; -- if primary returns `profile.ErrProfileNotFound`, try fallback; -- if primary returns any other error, return that error and do not try fallback; -- return fallback result or fallback error. - -This preserves strict validation of user profile sources. A malformed selected user profile should not silently fall through to a built-in with the same ID. - -## CLI And HTTP Behavior - -The CLI and HTTP server should no longer require `profile_dir` once built-in profiles are available. - -Expected behavior: - -- `profile_dir` omitted: built-ins are available. -- `profile_dir` provided: profiles from that directory override built-ins with the same ID. -- selected profile ID present only in built-ins: run succeeds. -- selected profile ID present in both custom profiles and built-ins: custom profile is used. -- selected profile ID missing from both sources: existing profile-not-found behavior is preserved. -- selected profile ID matches a malformed custom profile: profile-load failure is returned, not fallback to built-in. - -Configuration and CLI documentation should describe `profile_dir` as optional once built-in profiles are available. - -## Public Library Interaction - -This feature should support the current public package behavior and the production library roadmap. - -For the current public engine, `ProfileDir` should become optional once built-ins exist. A caller that does not configure a custom profile directory should still be able to use built-in profile IDs. - -The production library roadmap may add `fs.FS`, single-file, and in-memory profile sources. Those sources should become overlay primaries above the same built-in repository. - -Credential behavior for built-ins should follow the active execution path: - -- current CLI/HTTP behavior may continue to use `api_key_env` in profile definitions; -- the public library API may supply direct API-key values without changing built-in profile IDs; -- built-in profile files must never contain raw API keys. - -## Scope - -In scope: - -- built-in execution profile assets; -- strict validation of all built-in profiles; -- `fs.FS` profile repository support where needed for embedded assets; -- overlay profile repository with user-over-built-in precedence; -- optional `profile_dir` for CLI, HTTP, and public engine construction; -- tests for lookup precedence, override behavior, duplicate handling, and error behavior; -- documentation of CLI/config/profile behavior. - -Out of scope: - -- changing the profile YAML format; -- accepting raw API keys in profile YAML; -- adding a mutable runtime profile registry; -- adding a provider/model catalog that must track rapidly changing model availability; -- changing prompt `default_profile` semantics beyond allowing built-in IDs; -- implementing the broader `fs.FS` prompt/schema/library source work from `docs/roadmap/library.md`. - -## Acceptance Criteria - -- Scriptorium can run or render using a built-in profile ID with no configured `profile_dir`. -- CLI `run`, CLI `render`, and HTTP `serve` no longer fail solely because `profile_dir` is omitted. -- A profile in `profile_dir` overrides a built-in profile with the same ID. -- Duplicate profile IDs inside `profile_dir` remain invalid. -- Duplicate profile IDs inside the built-in profile set are caught by tests. -- A malformed selected custom profile does not fall back to a built-in profile with the same ID. -- Profile-not-found behavior remains clear when an ID exists in neither custom profiles nor built-ins. -- Built-in profiles are loaded through the same validation rules as file-based profiles. -- Existing runtime override behavior continues to apply to built-in profiles. -- Existing CLI, HTTP, and public error mapping remains consistent with current profile-load and profile-not-found semantics. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md deleted file mode 100644 index 26b7e28..0000000 --- a/docs/roadmap/implementation.md +++ /dev/null @@ -1,451 +0,0 @@ -# Built-In Profiles And Library API Implementation Plan - -This plan implements the target states in: - -- `docs/roadmap/builtins.md` -- `docs/roadmap/library.md` - -Audience: LLM coding agents implementing the work in order. Review and follow `docs/policy/architecture.md`, `docs/policy/development.md`, and `docs/policy/documentation.md` before changing code. - -## Global Constraints - -- Implement built-in profiles before the public library production upgrades. -- Keep orchestration in `internal/usecase`; adapters and the public package should translate inputs and wire components. -- Keep `internal/*` packages internal. Public package types must remain facade types. -- Do not add a mutable global profile registry. -- Do not add a credential resolver or secret-manager abstraction. -- Do not accept raw API keys in YAML/JSON config, profile files, prompt files, CLI flags, or HTTP request bodies. -- Do not emit raw API keys in prepared output, run results, logs, or examples. -- Prefer standard library APIs. Do not add dependencies unless a later implementation prompt explicitly approves one. -- Keep each stage passing `go test ./...` before moving to the next stage. - -## Stage 1: Profile Repository Foundations - -Goal: add reusable profile repository primitives that support built-ins without changing runner behavior. - -### Implementation Steps - -1. Add an `fs.FS`-backed profile repository in `internal/profile`. - - Constructor shape should be similar to `NewFSRepository(fsys fs.FS, root string) Repository`. - - It must scan YAML files recursively below `root`. - - It must use the same strict YAML decoding, profile validation, raw `api_key` rejection, and duplicate-ID behavior as the existing filesystem repository. - - It must return the existing profile package sentinel errors where applicable. - -2. Refactor shared profile-loading behavior. - - Avoid duplicating validation and metadata logic between filesystem and `fs.FS` repositories. - - The existing `NewFilesystemRepository(dir)` API should remain available. - - It may either delegate to the `fs.FS` repository through `os.DirFS` or share unexported loader helpers. - -3. Add an overlay profile repository in `internal/profile`. - - Constructor shape should be similar to `NewOverlayRepository(primary, fallback Repository) Repository`. - - Lookup must return the primary result when primary succeeds. - - Lookup must fall back only when `errors.Is(err, profile.ErrProfileNotFound)` for the primary. - - Lookup must return primary load/validation errors directly and must not fall back after those errors. - - Nil repository inputs should be handled deliberately. Prefer treating nil primary as "no primary" and requiring a non-nil fallback for built-in-only operation. - -### Tests - -Add focused tests under `internal/profile`. - -Required coverage: - -- `NewFSRepository` loads valid profiles from nested directories. -- `NewFSRepository` rejects unknown YAML fields. -- `NewFSRepository` rejects raw `api_key` in the selected profile. -- `NewFSRepository` ignores raw `api_key` in non-selected profiles, matching current filesystem behavior. -- `NewFSRepository` rejects duplicate IDs within one source. -- `NewFilesystemRepository` still satisfies all existing repository tests. -- `NewOverlayRepository` returns primary matches before fallback matches. -- `NewOverlayRepository` falls back on primary not found. -- `NewOverlayRepository` does not fall back after a primary invalid YAML/profile/raw-key error. -- `NewOverlayRepository` returns not found when both sources miss. - -### Verification - -Run: - -```bash -go test ./internal/profile -go test ./... -``` - -## Stage 2: Built-In Profile Assets And Wiring - -Goal: compile built-in profiles into Scriptorium and make profile lookup use user-over-built-in precedence. - -### Implementation Steps - -1. Add an `internal/profile/builtin` package. - - Store built-in profile YAML files in a stable asset directory under that package. - - Use Go `embed` to compile those files into the binary/package. - - Expose a constructor such as `builtin.NewRepository() profile.Repository`. - - The repository should use the `fs.FS` profile repository from Stage 1. - -2. Add built-in profile validation tests. - - Tests should load every built-in profile through the real profile loader. - - Tests should fail if the built-in profile set contains duplicate IDs. - - Tests should fail if any built-in profile contains raw `api_key`. - -3. Add built-in profiles. - - Use `docs/roadmap/profiles/` as the source catalog for the initial built-in profile set. - - Copy those YAML files into the built-in profile asset directory, preserving provider subdirectories unless the implementation has a clear reason to flatten them. - - Do not invent a broad provider/model catalog. - - Do not add raw API keys. - - Built-in profiles may use `api_key_env` for CLI/HTTP compatibility when the provider requires authentication. - -4. Wire repositories through a small helper. - - Add an internal helper near adapter wiring, or in `internal/profile`, that returns: - - built-in repository only when no custom profile source is configured; - - overlay repository when a custom profile source is configured. - - The runner should still receive only a `profile.Repository`. - -5. Make `profile_dir` optional. - - CLI `run`, CLI `render`, and HTTP `serve` argument/config validation should require `prompt_dir` but no longer require `profile_dir`. - - Public `NewEngine` should no longer reject an empty `Config.ProfileDir`. - - When `profile_dir` is empty, wire only built-ins. - - When `profile_dir` is non-empty, wire filesystem profiles over built-ins. - -### Tests - -Add or update tests under `internal/adapter/cli`, `internal/adapter/http`, root public package tests, and profile/builtin tests. - -Required coverage: - -- CLI parse/config tests accept missing `profile_dir` when `prompt_dir` is present. -- HTTP serve parse/config tests accept missing `profile_dir` when `prompt_dir` is present. -- Public `NewEngine` accepts missing `ProfileDir`. -- A built-in profile ID can be selected with no custom `profile_dir`. -- A prompt `default_profile` can refer to a built-in profile ID. -- A custom profile in `profile_dir` overrides a built-in with the same ID. -- A malformed selected custom profile does not fall back to a built-in with the same ID. -- A missing profile ID still maps to the existing profile-not-found behavior. -- Existing duplicate-ID tests for filesystem profiles continue to fail within the custom source. - -### Documentation - -After code behavior exists, update non-roadmap docs: - -- `docs/config.md`: document `profile_dir` as optional and describe built-in fallback/override behavior. -- `docs/cli.md`: remove claims that `--profile-dir` is required. -- `docs/internal/adapters.md`: document built-in profile repository composition. -- Any affected examples or README snippets that imply `profile_dir` is mandatory. - -Do not document built-in profile IDs outside implemented assets. - -### Verification - -Run: - -```bash -go test ./internal/profile ./internal/adapter/cli ./internal/adapter/http . -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 -``` - -Also run a new smoke command that uses a built-in profile without `--profile-dir` once a concrete built-in profile ID is available. - -## Stage 3: Public API Credential Value - -Goal: support the production library credential model: a direct API-key Go value, without adding a resolver or accepting raw keys in serialized config. - -### Public API Decision - -Use a single public credential-supply method: - -```go -type RunRequest struct { - // existing fields... - APIKey string `json:"-"` -} -``` - -Do not add `Config.APIKey`, `WithAPIKey`, or a credential resolver in this stage. A request-level value avoids storing secrets on long-lived engines and supports per-tenant callers. - -### Implementation Steps - -1. Add direct API-key plumbing through internal request/target types. - - Add an internal direct API-key field where needed, with `json:"-"` and `yaml:"-"` tags. - - Convert public `RunRequest.APIKey` into the internal request. - - Carry the value to the effective execution target used for LLM generation. - - Do not include the value in prepared/run public results, formatted output, logs, hashes, or docs examples. - -2. Update credential validation. - - If a selected/effective profile requires authentication and a direct API key is provided, do not require the environment variable to be set for the public path. - - Preserve existing CLI/HTTP behavior that uses `api_key_env`. - - Preserve existing errors for missing environment variables in CLI/HTTP paths. - -3. Update the OpenAI-compatible LLM client. - - Prefer the direct API-key value when present. - - Fall back to existing `api_key_env` behavior for CLI/HTTP compatibility. - - Never serialize or log the direct API-key value. - -4. Update public LLM injection conversion. - - Do not expose the raw API key to injected public `LLMClient` implementations unless that is strictly necessary for custom LLM execution. - - If custom LLM clients need the key, expose it only on the public `GenerateRequest` with `json:"-"` and document that fake/test clients should avoid logging it. - -### Tests - -Required coverage: - -- Public `Run` can call the default OpenAI-compatible client path with a direct API key without requiring the configured `api_key_env` environment variable. -- CLI/HTTP behavior using `api_key_env` still works. -- Missing credentials still fail clearly when a selected profile requires authentication and neither direct key nor usable env value is available. -- Public `PreparedRun` and `RunResult` JSON do not include the direct API key. -- Formatted prepared output does not include the direct API key. -- Direct API-key values are not included in hashes. -- Injected fake LLM tests either receive no key or receive it only through a `json:"-"` field, depending on the implementation choice above. - -### Verification - -Run: - -```bash -go test ./internal/llm ./internal/usecase ./internal/adapter/cli ./internal/adapter/http . -go test ./... -``` - -## Stage 4: Public Asset Source Options - -Goal: let library consumers load standard Scriptorium prompt/profile/schema assets from directories, single files, and `fs.FS` sources. - -### Public API - -Keep existing `Config` directory fields working. Add options: - -```go -func WithPromptFS(fsys fs.FS, root string) Option -func WithPromptFile(path string) Option -func WithProfileFS(fsys fs.FS, root string) Option -func WithProfileFile(path string) Option -func WithSchemaFS(fsys fs.FS, root string) Option -func WithSchemaFile(path string) Option -``` - -Rules: - -- Directory `Config` fields remain the compatibility path. -- Explicit options override the corresponding `Config` directory field. -- Prompt source is required. -- Profile source is optional because built-in profiles exist. -- Schema source is optional and should default to the current schema default behavior when not configured. -- Nil `fs.FS` values, empty required roots, and invalid option combinations return `ErrInvalidConfig`. - -### Implementation Steps - -1. Add `fs.FS` prompt-definition support. - - Implement an `fs.FS` prompt repository that preserves existing strict prompt YAML behavior. - - Preserve prompt lookup by YAML `id`, not path. - - Preserve duplicate prompt ID errors within one source. - - Resolve `content_file` relative to the prompt file's directory inside the same source. - - Keep the existing filesystem repository API; it may delegate to the `fs.FS` implementation. - -2. Add public prompt source wiring. - - `Config.PromptDir` wires the filesystem repository. - - `WithPromptFS` wires the `fs.FS` repository. - - `WithPromptFile(path)` wires a single-file source and must still select by prompt YAML `id`. - -3. Add public profile source wiring. - - Reuse the Stage 1 profile `fs.FS` repository. - - `WithProfileFS` and `WithProfileFile` become overlay primaries above built-ins. - - Empty profile source still means built-ins only. - -4. Add schema `fs.FS` support. - - Extend or wrap the standard validator so schema files can be loaded from an `fs.FS` source. - - `WithSchemaFS` should preserve existing `schema_path` semantics. - - `WithSchemaFile(path)` should expose the file by its base name; prompts using it should set `schema_path` to that base name. - -5. Keep adapter scope narrow. - - This stage is for the public package and shared repositories/validators. - - Do not change CLI/HTTP request shapes for prompt or schema `fs.FS` sources. - -### Tests - -Required coverage: - -- Public `Prepare` works with prompt definitions from `embed.FS`. -- `content_file` references resolve relative to the prompt file in `embed.FS`. -- Public `Prepare` works with `WithPromptFile`. -- Public `Run` or `Prepare` works with `WithProfileFS` over built-ins. -- Public `Run` or `Prepare` works with `WithProfileFile` over built-ins. -- Public structured-output schema validation works with `WithSchemaFS`. -- `WithSchemaFile` works when the prompt's `schema_path` is the schema file base name. -- Explicit source options override `Config` directory fields. -- Invalid/nil source options return `ErrInvalidConfig`. -- Existing filesystem prompt/profile/schema behavior remains unchanged. - -### Verification - -Run: - -```bash -go test ./internal/promptdef ./internal/profile ./internal/validate . -go test ./... -``` - -## Stage 5: Public In-Memory Profiles And Profile Templates - -Goal: allow library callers to provide typed profile values and construct stable OpenAI-compatible profile templates without generating YAML. - -### Public API - -Add a public profile facade aligned with the profile YAML contract: - -```go -type Profile struct { - ID string - Endpoint string - Model string - Temperature float64 - MaxTokens int - TopP float64 - TimeoutSeconds int - ServiceTier string - ReasoningEffort string - APIKeyRequired bool - ExtraParams map[string]any -} - -func WithProfiles(profiles ...Profile) Option -``` - -Add an OpenAI-compatible template constructor: - -```go -type OpenAICompatibleProfileConfig struct { - ID string - Endpoint string - Model string - APIKeyRequired bool - Temperature float64 - MaxTokens int - TopP float64 - TimeoutSeconds int - ServiceTier string - ReasoningEffort string - ExtraParams map[string]any -} - -func OpenAICompatibleProfile(cfg OpenAICompatibleProfileConfig) Profile -``` - -Rules: - -- `WithProfiles` profiles override built-ins with the same ID. -- If `WithProfiles` and a file/FS profile source are both configured, in-memory profiles have highest precedence, then file/FS profiles, then built-ins. -- Public profile values must not include raw API-key fields. -- Use `APIKeyRequired` to indicate whether `RunRequest.APIKey` is required for the public path. Internal conversion may map this to the existing auth-required/profile credential model without exposing raw keys. - -### Implementation Steps - -1. Add a profile repository for public in-memory `Profile` values. - - Validate with the same effective rules as YAML profiles. - - Reject duplicate IDs within the provided values. - - Deep-copy `ExtraParams` across public/internal boundaries. - -2. Compose profile sources in public `NewEngine`. - - Highest: in-memory public profiles. - - Next: configured profile file/FS/directory source. - - Fallback: built-in profiles. - -3. Add template constructor conversion. - - `OpenAICompatibleProfile` should be a convenience constructor only. - - It should not register global state. - - It should not maintain a broad model catalog. - -4. Ensure direct API-key behavior works with in-memory/template profiles. - - Profiles marked `APIKeyRequired` should require `RunRequest.APIKey` for public default LLM execution. - - Profiles not marked `APIKeyRequired` should not require an API key. - -### Tests - -Required coverage: - -- Public `Prepare`/`Run` uses `WithProfiles` without any profile files. -- In-memory profiles override built-ins. -- In-memory profiles override file/FS profile sources when IDs collide. -- Duplicate in-memory profile IDs return `ErrInvalidConfig`. -- Template-created profiles execute through the same path as normal profiles. -- Template-created profiles requiring an API key work with `RunRequest.APIKey`. -- Template-created profiles that do not require an API key work without one. -- `ExtraParams` in public profiles are deep-copied and isolated from caller mutation. - -### Verification - -Run: - -```bash -go test . -go test ./... -``` - -## Stage 6: Public Documentation And Examples - -Goal: document implemented behavior in canonical locations after code exists. - -### Documentation - -Update: - -- `README.md`: add a short pointer to library usage and built-in profiles without turning the README into a manual. -- `docs/config.md`: document optional `profile_dir`, built-in override behavior, and implemented built-in profile IDs. -- `docs/cli.md`: document optional `--profile-dir` and built-in profile selection. -- `docs/internal/adapters.md`: document repository composition and public library adapter surface. -- `docs/consumers/api.md`: describe public consumer surfaces at a high level. -- `docs/consumers/pkg-scriptorium.md`: document root package usage, source options, direct API-key value, errors, and examples. -- `docs/integrations/openai-compatible-chat.md`: update only if direct API-key plumbing changes provider-call semantics. - -Do not document unimplemented roadmap behavior outside `docs/roadmap/`. - -### Examples - -Add or update copyable examples that require no real provider credentials: - -- prepare with built-in profile and no `profile_dir`; -- public library prepare from `embed.FS`; -- public library run with injected fake LLM; -- public library run with typed/template profile and direct API key, using a fake/local provider path so no real secret is required. - -Examples must be secret-free. - -### Tests And Smoke Commands - -Required verification: - -```bash -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 -``` - -Also run smoke commands for any new examples, such as: - -```bash -go run ./examples/go-library/prepare -``` - -If an example relies on a specific built-in profile ID, use an ID from `docs/roadmap/profiles/` and include that smoke command in the implementing change. - -## Final Completion Checklist - -Before marking the combined feature complete: - -1. `go test ./...` passes. -2. Existing CLI render smoke command passes. -3. At least one smoke command proves built-in profile lookup works without `profile_dir`. -4. Public package examples compile/run without real provider credentials. -5. `profile_dir` is optional in CLI, HTTP, and public engine construction. -6. Custom profiles override built-ins with the same ID. -7. Malformed selected custom profiles do not fall back to built-ins. -8. Built-in profiles use the same validation rules as file profiles. -9. Public `RunRequest.APIKey` is the documented library credential method. -10. Raw API keys do not appear in YAML/JSON config, prepared output, run results, logs, hashes, or examples. -11. Non-roadmap docs describe only implemented behavior. diff --git a/docs/roadmap/library.md b/docs/roadmap/library.md deleted file mode 100644 index 8bd9886..0000000 --- a/docs/roadmap/library.md +++ /dev/null @@ -1,176 +0,0 @@ -# Library API Production Roadmap - -This roadmap defines the target state for making Scriptorium's public Go package production-ready for downstream applications while preserving the existing CLI and HTTP behavior. - -The library remains an additional adapter surface. It should not replace the subprocess, CLI, or HTTP contracts that already exist. - -## Motivation - -Many Go applications can use Scriptorium more cleanly as an imported package than as a subprocess. A downstream developer should be able to keep prompt assets in standard Scriptorium format, pass application data through a small adapter, and receive a typed response without reimplementing prompt rendering, profile resolution, validation, or OpenAI-compatible request construction. - -The core consumer story is: - -- the downstream app owns one or more `prompt.yml` files in standard Scriptorium format; -- those prompts may use inline content, `content_file` references, variables, cache-control markers, and structured-output schemas; -- the app may provide its own `profile.yml`, or select a standard built-in/profile-template configuration; -- the app supplies an API key as a normal Go value when the selected profile requires one; -- the app calls the public Go package to prepare or run the request and receives typed results. - -## Current State - -The public package already provides the first library facade: - -- root package import; -- typed engine construction; -- typed prepare/run requests and results; -- file and inline artifact references; -- execution overrides; -- custom LLM injection for testing or alternate execution; -- public error categories that map internal failures to stable caller-facing errors. - -The remaining production-readiness gaps are mostly about consumer ergonomics and asset sourcing: - -- callers are still oriented around filesystem prompt/profile/schema directories; -- embedded prompt/profile/schema assets are not a first-class public use case; -- standard or built-in profile selection is not yet available; -- library credential supply is still tightly coupled to environment-variable lookup rather than direct API-key values; -- public documentation and examples need to show the intended downstream app adapter pattern. - -## Target State - -The public package should let a downstream Go application use standard Scriptorium assets without temporary directories, subprocess invocation, or internal package imports. - -### Prompt Assets - -The library should support prompt definitions from: - -- existing prompt directories; -- a single prompt file; -- `fs.FS`, including `embed.FS`. - -Prompt syntax should remain the standard Scriptorium prompt YAML format. `content_file` references should continue to be supported and should resolve relative to the prompt definition's source location within the same asset source. - -The public API should not introduce a separate in-code prompt DSL as the primary path. YAML remains the canonical authoring format so prompts can be shared between CLI, HTTP, subprocess, and library usage. - -### Schema Assets - -Structured-output schemas should be loadable from the same kinds of sources as prompt definitions: - -- existing schema directories; -- a single schema file where appropriate; -- `fs.FS`, including `embed.FS`. - -Schema references should retain the existing prompt-format semantics. A schema referenced by a prompt should resolve through the configured schema source, not through ad hoc caller code. - -### Profile Assets - -The library should support both custom and standard profile configuration: - -- existing profile directories; -- a single profile file; -- `fs.FS`, including `embed.FS`; -- direct public profile values for applications that already have profile configuration in memory; -- built-in/profile-template helpers for common OpenAI-compatible targets. - -Custom profiles and built-in/template profiles should flow through the same internal profile resolution and request-construction path. The built-in path should not become a separate execution mode. - -### Built-In Profile Templates - -Built-in support should favor stable profile templates over a large registry of fixed model IDs. - -For example, the public package should make it easy to construct or select an OpenAI-compatible profile by supplying the durable parts of the profile: - -- profile ID or name; -- base URL; -- model; -- whether the profile requires an API key; -- default numeric parameters where desired; -- structured-output and extra-parameter behavior consistent with normal profiles. - -The package may include a small set of named helpers for common OpenAI-compatible services, but those helpers should avoid hard-coding a broad and fast-changing list of model names. - -### Credentials - -The public package must keep raw API keys out of prompt/profile YAML, prepared-run output, run results, logs, and examples. - -For the public library API, the single supported credential-supply method should be a direct API-key value passed by the consuming Go application. The consuming application is responsible for loading and managing its own secrets before calling Scriptorium. - -This may be exposed as a field such as `Config.APIKey`, an option such as `WithAPIKey`, or an equivalent request/engine-level value that is easy to pass through an application adapter. The exact API should avoid accidental serialization in prepared output, run results, logs, and examples. - -The public package should not encourage raw API-key storage in prompt/profile YAML. Existing CLI behavior may continue to use environment-variable references for compatibility, but the production library path should not introduce a separate credential resolver or secret-manager abstraction. - -### Public API Shape - -The public API should remain narrow, idiomatic, and stable. Recommended additions include: - -- engine options for prompt/profile/schema directories; -- engine options for prompt/profile/schema `fs.FS` sources; -- engine options for single prompt/profile/schema files where useful; -- public profile/template constructors that map to internal profile definitions; -- a direct API-key value for profiles that require authentication; -- examples showing `embed.FS`, custom profile files, template profile selection, and fake LLM testing. - -The public package should continue to expose facade types rather than exporting internal package types. Internal package layout should remain free to evolve. - -## Scope - -In scope: - -- first-class `fs.FS` support for public library prompt, profile, and schema sources; -- ergonomic single-file asset options where they reduce caller boilerplate; -- built-in/profile-template helpers for common OpenAI-compatible usage; -- in-memory public profile values where appropriate; -- direct API-key value support for the public library path; -- consumer-facing examples under `examples/`; -- consumer package documentation under `docs/consumers/` once behavior is implemented; -- tests proving library behavior matches existing CLI/use-case behavior. - -Out of scope: - -- changing standard prompt, profile, or schema file formats; -- exposing internal packages as public API; -- replacing or removing CLI, HTTP, or subprocess support; -- adding a multi-step workflow engine; -- adding non-Go bindings; -- maintaining a comprehensive provider/model catalog; -- accepting raw API keys in serialized YAML/JSON configuration; -- adding a credential resolver or secret-manager abstraction. - -## Acceptance Criteria - -- A Go caller can import the root package and run a standard Scriptorium prompt without invoking a subprocess. -- A Go caller can use prompt definitions from `embed.FS`, including prompts with `content_file` references. -- A Go caller can use structured-output schemas from `embed.FS` or filesystem sources. -- A Go caller can provide a custom profile from filesystem, `fs.FS`, or public in-memory profile values. -- A Go caller can select a standard OpenAI-compatible profile template without writing a full profile file. -- A Go caller can supply an API key as a normal Go value without raw secrets appearing in serialized config, prepared output, or results. -- Public library behavior remains consistent with CLI/HTTP semantics for rendering, validation, profile resolution, runtime overrides, structured output, cache control, and LLM invocation. -- Existing CLI and HTTP behavior remains unchanged. -- Library tests use fake or local LLM boundaries and do not require real provider credentials. -- Public docs outside `docs/roadmap/` describe only implemented behavior after the feature is built. - -## Design Decisions - -### Asset Source API - -Add first-class `fs.FS` options for prompt, profile, and schema sources while keeping existing directory-based configuration. Also add single-file convenience options where they remove meaningful caller boilerplate. Resolve `content_file` references relative to the prompt file's location inside the same source. - -Reasoning: - -This is the most idiomatic path for production Go libraries because it supports `embed.FS`, `os.DirFS`, tests, and in-memory fixture files through the same abstraction. It also avoids requiring downstream applications to unpack embedded assets into temporary directories. - -### Built-In Profile Strategy - -Provide stable profile-template helpers for OpenAI-compatible endpoints rather than a broad registry of fixed provider/model profiles. Let callers choose the model and endpoint where those values are service-specific or fast-changing. - -Reasoning: - -Endpoint shape and credential mechanics are relatively stable; model catalogs change frequently. Templates give consumers a short, correct path without making Scriptorium responsible for tracking every provider's model list. - -### In-Memory Profile Values - -Expose a small public profile facade type for in-memory profile configuration and map it to internal profile definitions. Keep it intentionally aligned with the existing profile YAML contract. - -Reasoning: - -Many applications already hold configuration in typed structs and should not need to generate YAML files just to call Scriptorium. A public facade keeps internal types private while making the library practical for production use. diff --git a/docs/roadmap/profiles/aion-labs/aion-2.yml b/docs/roadmap/profiles/aion-labs/aion-2.yml deleted file mode 100644 index 785a6fd..0000000 --- a/docs/roadmap/profiles/aion-labs/aion-2.yml +++ /dev/null @@ -1,9 +0,0 @@ -id: aion-2 -endpoint: https://openrouter.ai/api/v1 -model: aion-labs/aion-2.0 -temperature: 0.72 -reasoning_effort: high -top_p: 0.95 -timeout_seconds: 180 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/anthropic/claude-fable-latest.yml b/docs/roadmap/profiles/anthropic/claude-fable-latest.yml deleted file mode 100644 index 1a1e7aa..0000000 --- a/docs/roadmap/profiles/anthropic/claude-fable-latest.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: claude-fable-latest -endpoint: https://openrouter.ai/api/v1 -model: "~anthropic/claude-fable-latest" -reasoning_effort: high -timeout_seconds: 600 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/anthropic/claude-haiku-latest.yml b/docs/roadmap/profiles/anthropic/claude-haiku-latest.yml deleted file mode 100644 index f22bf15..0000000 --- a/docs/roadmap/profiles/anthropic/claude-haiku-latest.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: claude-haiku-latest -endpoint: https://openrouter.ai/api/v1 -model: "~anthropic/claude-haiku-latest" -reasoning_effort: medium -timeout_seconds: 240 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/anthropic/claude-opus-latest.yml b/docs/roadmap/profiles/anthropic/claude-opus-latest.yml deleted file mode 100644 index c192c3a..0000000 --- a/docs/roadmap/profiles/anthropic/claude-opus-latest.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: claude-opus-latest -endpoint: https://openrouter.ai/api/v1 -model: "~anthropic/claude-opus-latest" -reasoning_effort: high -timeout_seconds: 240 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/anthropic/claude-sonnet-latest.yml b/docs/roadmap/profiles/anthropic/claude-sonnet-latest.yml deleted file mode 100644 index c7be449..0000000 --- a/docs/roadmap/profiles/anthropic/claude-sonnet-latest.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: claude-sonnet-latest -endpoint: https://openrouter.ai/api/v1 -model: "~anthropic/claude-sonnet-latest" -reasoning_effort: high -timeout_seconds: 240 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/deepseek/deepseek-3-2.yml b/docs/roadmap/profiles/deepseek/deepseek-3-2.yml deleted file mode 100644 index 1f27fbc..0000000 --- a/docs/roadmap/profiles/deepseek/deepseek-3-2.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: deepseek-3-2 -endpoint: https://openrouter.ai/api/v1 -model: deepseek/deepseek-v3.2 -reasoning_effort: high -timeout_seconds: 180 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/deepseek/deepseek-4-pro.yml b/docs/roadmap/profiles/deepseek/deepseek-4-pro.yml deleted file mode 100644 index c7af1ff..0000000 --- a/docs/roadmap/profiles/deepseek/deepseek-4-pro.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: deepseek-4-pro -endpoint: https://openrouter.ai/api/v1 -model: deepseek/deepseek-v4-pro -reasoning_effort: high -timeout_seconds: 180 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/google/gemini-2-flash-lite.yml b/docs/roadmap/profiles/google/gemini-2-flash-lite.yml deleted file mode 100644 index e648e70..0000000 --- a/docs/roadmap/profiles/google/gemini-2-flash-lite.yml +++ /dev/null @@ -1,9 +0,0 @@ -id: gemini-2-flash-lite -endpoint: https://openrouter.ai/api/v1 -model: "google/gemini-2.5-flash-lite" -#temperature: 0.15 -reasoning_effort: high -#top_p: 0.98 -timeout_seconds: 240 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/google/gemini-2-flash.yml b/docs/roadmap/profiles/google/gemini-2-flash.yml deleted file mode 100644 index 3b1267e..0000000 --- a/docs/roadmap/profiles/google/gemini-2-flash.yml +++ /dev/null @@ -1,9 +0,0 @@ -id: gemini-2-flash -endpoint: https://openrouter.ai/api/v1 -model: "google/gemini-2.5-flash" -#temperature: 0.15 -reasoning_effort: high -#top_p: 0.98 -timeout_seconds: 240 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/google/gemini-2-pro.yml b/docs/roadmap/profiles/google/gemini-2-pro.yml deleted file mode 100644 index b779db6..0000000 --- a/docs/roadmap/profiles/google/gemini-2-pro.yml +++ /dev/null @@ -1,9 +0,0 @@ -id: gemini-2-pro -endpoint: https://openrouter.ai/api/v1 -model: "google/gemini-2.5-pro" -#temperature: 0.15 -reasoning_effort: high -#top_p: 0.98 -timeout_seconds: 240 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/google/gemini-3-flash-lite.yml b/docs/roadmap/profiles/google/gemini-3-flash-lite.yml deleted file mode 100644 index ae44f66..0000000 --- a/docs/roadmap/profiles/google/gemini-3-flash-lite.yml +++ /dev/null @@ -1,9 +0,0 @@ -id: gemini-3-flash-lite -endpoint: https://openrouter.ai/api/v1 -model: "google/gemini-3.1-flash-lite" -#temperature: 0.15 -reasoning_effort: high -#top_p: 0.98 -timeout_seconds: 240 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/google/gemini-flash-latest.yml b/docs/roadmap/profiles/google/gemini-flash-latest.yml deleted file mode 100644 index 2bcda6e..0000000 --- a/docs/roadmap/profiles/google/gemini-flash-latest.yml +++ /dev/null @@ -1,9 +0,0 @@ -id: gemini-flash-latest -endpoint: https://openrouter.ai/api/v1 -model: "~google/gemini-flash-latest" -#temperature: 0.15 -reasoning_effort: high -#top_p: 0.98 -timeout_seconds: 240 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/google/gemini-pro-latest.yml b/docs/roadmap/profiles/google/gemini-pro-latest.yml deleted file mode 100644 index 2e77cbc..0000000 --- a/docs/roadmap/profiles/google/gemini-pro-latest.yml +++ /dev/null @@ -1,9 +0,0 @@ -id: gemini-pro-latest -endpoint: https://openrouter.ai/api/v1 -model: "~google/gemini-pro-latest" -#temperature: 0.15 -reasoning_effort: high -#top_p: 0.98 -timeout_seconds: 240 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/google/gemma-4-31b.yml b/docs/roadmap/profiles/google/gemma-4-31b.yml deleted file mode 100644 index f8ff113..0000000 --- a/docs/roadmap/profiles/google/gemma-4-31b.yml +++ /dev/null @@ -1,9 +0,0 @@ -id: gemma-4-31b -endpoint: https://openrouter.ai/api/v1 -model: google/gemma-4-31b-it:exacto -temperature: 0.15 -reasoning_effort: high -top_p: 0.98 -timeout_seconds: 240 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/minimax/minimax-m2.yml b/docs/roadmap/profiles/minimax/minimax-m2.yml deleted file mode 100644 index 6ce7fdf..0000000 --- a/docs/roadmap/profiles/minimax/minimax-m2.yml +++ /dev/null @@ -1,9 +0,0 @@ -id: minimax-m2 -endpoint: https://openrouter.ai/api/v1 -model: minimax/minimax-m2.5 -temperature: 0.5 -reasoning_effort: high -top_p: 0.95 -timeout_seconds: 180 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/minimax/minimax-m3.yml b/docs/roadmap/profiles/minimax/minimax-m3.yml deleted file mode 100644 index da9a3af..0000000 --- a/docs/roadmap/profiles/minimax/minimax-m3.yml +++ /dev/null @@ -1,9 +0,0 @@ -id: minimax-m3 -endpoint: https://openrouter.ai/api/v1 -model: minimax/minimax-m3 -#temperature: 0.5 -reasoning_effort: high -#top_p: 0.95 -timeout_seconds: 180 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/mistral/mistral-large-2512.yml b/docs/roadmap/profiles/mistral/mistral-large-2512.yml deleted file mode 100644 index 106ea2e..0000000 --- a/docs/roadmap/profiles/mistral/mistral-large-2512.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: mistral-large-2512 -endpoint: https://openrouter.ai/api/v1 -model: mistralai/mistral-large-2512 -temperature: 0.15 -top_p: 0.98 -timeout_seconds: 180 -api_key_env: OPENROUTER_API_KEY diff --git a/docs/roadmap/profiles/mistral/mistral-medium-3-5.yml b/docs/roadmap/profiles/mistral/mistral-medium-3-5.yml deleted file mode 100644 index 762149f..0000000 --- a/docs/roadmap/profiles/mistral/mistral-medium-3-5.yml +++ /dev/null @@ -1,8 +0,0 @@ -id: mistral-medium-3-5 -endpoint: https://openrouter.ai/api/v1 -model: mistralai/mistral-medium-3-5 -temperature: 0.15 -reasoning_effort: high -top_p: 0.98 -timeout_seconds: 180 -api_key_env: OPENROUTER_API_KEY diff --git a/docs/roadmap/profiles/mistral/mistral-small-3.yml b/docs/roadmap/profiles/mistral/mistral-small-3.yml deleted file mode 100644 index d077918..0000000 --- a/docs/roadmap/profiles/mistral/mistral-small-3.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: mistral-small-3 -endpoint: https://openrouter.ai/api/v1 -model: mistralai/mistral-small-3.2-24b-instruct -temperature: 0.05 -top_p: 1.0 -timeout_seconds: 180 -api_key_env: OPENROUTER_API_KEY diff --git a/docs/roadmap/profiles/mistral/mistral-small-4.yml b/docs/roadmap/profiles/mistral/mistral-small-4.yml deleted file mode 100644 index fd0fce6..0000000 --- a/docs/roadmap/profiles/mistral/mistral-small-4.yml +++ /dev/null @@ -1,8 +0,0 @@ -id: mistral-small-4 -endpoint: https://openrouter.ai/api/v1 -model: mistralai/mistral-small-2603 -temperature: 0.1 -reasoning_effort: high -top_p: 0.98 -timeout_seconds: 180 -api_key_env: OPENROUTER_API_KEY diff --git a/docs/roadmap/profiles/nvidia/nemotron-3-ultra.yml b/docs/roadmap/profiles/nvidia/nemotron-3-ultra.yml deleted file mode 100644 index bb55536..0000000 --- a/docs/roadmap/profiles/nvidia/nemotron-3-ultra.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: nemotron-3-ultra -endpoint: https://openrouter.ai/api/v1 -model: nvidia/nemotron-3-ultra-550b-a55b -reasoning_effort: high -timeout_seconds: 180 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/openai/gpt-5-mini.yml b/docs/roadmap/profiles/openai/gpt-5-mini.yml deleted file mode 100644 index 72ea4b7..0000000 --- a/docs/roadmap/profiles/openai/gpt-5-mini.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: gpt-5-mini -endpoint: https://openrouter.ai/api/v1 -model: "openai/gpt-5.4-mini" -reasoning_effort: high -timeout_seconds: 240 -api_key_env: OPENROUTER_API_KEY -service_tier: flex diff --git a/docs/roadmap/profiles/openai/gpt-5-nano.yml b/docs/roadmap/profiles/openai/gpt-5-nano.yml deleted file mode 100644 index 069a3d5..0000000 --- a/docs/roadmap/profiles/openai/gpt-5-nano.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: gpt-5-nano -endpoint: https://openrouter.ai/api/v1 -model: "openai/gpt-5.4-nano" -reasoning_effort: high -timeout_seconds: 240 -api_key_env: OPENROUTER_API_KEY -service_tier: flex