Files
promptkit/docs/roadmap/implementation.md

301 lines
18 KiB
Markdown

# Bundled OpenRouter Implementation Plan
Implement [bundled-openrouter.md](bundled-openrouter.md) in the sequential stages
below. Each stage is one Luna-sized assignment. The feature roadmap owns scope
and consumer policy; this document owns the implementation sequence and choices.
## Baseline And Working Rules
The baseline is `9e05c3a`: Go 1.27.0, ConfigKit v1.1.0, recorded field
normalization, and no installed backend/profile catalog. The facade in
`engine.go` applies options; `internal/engine.New` passes caller configuration
to `internal/configuration.Load` before assembling execution collaborators.
Read `docs/development.md` and applicable agent, architecture, testing,
documentation, OSS, and release policies. Follow
[ADR 0001](../adr/0001-configuration-boundary.md). Reuse current source-loading,
generation, and smoke-diagnostic contracts. Preserve unrelated changes.
This is one end-to-end sprint: intermediate stages need not compile or form a
deployable state. Do not introduce compatibility scaffolding or intermediate
human-review/publication gates. Record incomplete checks honestly and reuse
successful checks for unchanged content. Do not publish a release or make paid
provider requests. No dependency upgrade, configuration-version change, or
provider-specific catalog module is required.
## Implementation Choices
- Add `internal/configuration/bundled/openrouter.yaml` and embed it from a small
`bundled.go` in that package. Provide an internal exported
`BundledOpenRouterUnderlay() configkit.Underlay` for engine assembly. It returns
one input created through `configkit.NewFilesystem` and its `File` method,
with a meaningful label such as `Promptkit bundled OpenRouter`.
- Use the existing source loader once. Do not independently parse, validate,
normalize, or cache a separate bundle registry before final composition.
Tests inspect the assembled effective configuration, not raw-source validity.
- Prepend the bundle as its own underlay in `internal/engine.New`. Allocate a
new outer slice and copy caller groups so spare slice capacity cannot let
assembly mutate caller-owned input. Preserve every caller group's contents,
order, main input, imports, and source context.
- Add `WithBundledOpenRouter(enabled bool) Option` in the facade. Add
`disableBundledOpenRouter bool` to internal engine options and a setter
`SetBundledOpenRouter(bool)` following existing setter style. The zero internal
options value enables the bundle; the setter stores `!enabled`. Apply options
in the existing order, so the last supplied bundle option wins. The facade
only delegates; embedding and composition stay internal.
- Disabled means no bundle source or definitions are installed. It does not
reserve names, remove caller definitions, or disable the OpenAI-compatible
client. Scoped views share the engine's captured configuration; they do not
reload the bundle.
- Keep the root backend and profiles described below. No built-in collection,
prompts, implicit execution profile, tuning defaults, catalog download, or
constructor-time provider/credential access is added.
## Exact Data To Install
The root backend is `openrouter`, with endpoint `https://openrouter.ai/api/v1`,
`api_key_env: OPENROUTER_API_KEY`, and:
```yaml
smoke_tests:
default_profile: gpt-luna-latest
authentication:
kind: openrouter_key
```
Leave concurrency, queue, provider parameters, and diagnostic endpoint overrides
unset. Existing reachability and authentication endpoint fallback applies.
Use these exact root profile IDs and model values. Each non-cost-tier profile
sets `backend: openrouter`. The ten family mappings were checked against the
linked provider pages on 2026-11-03; this is authoring evidence, not a guarantee
of future availability or a recurring release check.
| Profile ID | Model | Provider reference |
| --- | --- | --- |
| `gpt-astra-latest` | `~openai/gpt-astra-latest` | [Alias](https://openrouter.ai/~openai/gpt-astra-latest) |
| `gpt-sol-latest` | `~openai/gpt-sol-latest` | [Alias](https://openrouter.ai/~openai/gpt-sol-latest) |
| `gpt-terra-latest` | `~openai/gpt-terra-latest` | [Alias](https://openrouter.ai/~openai/gpt-terra-latest) |
| `gpt-luna-latest` | `~openai/gpt-luna-latest` | [Alias](https://openrouter.ai/~openai/gpt-luna-latest) |
| `claude-fable-latest` | `~anthropic/claude-fable-latest` | [Alias](https://openrouter.ai/~anthropic/claude-fable-latest) |
| `claude-opus-latest` | `~anthropic/claude-opus-latest` | [Alias](https://openrouter.ai/~anthropic/claude-opus-latest) |
| `claude-sonnet-latest` | `~anthropic/claude-sonnet-latest` | [Alias](https://openrouter.ai/~anthropic/claude-sonnet-latest) |
| `claude-haiku-latest` | `~anthropic/claude-haiku-latest` | [Alias](https://openrouter.ai/~anthropic/claude-haiku-latest) |
| `gemini-pro-latest` | `~google/gemini-pro-latest` | [Alias](https://openrouter.ai/~google/gemini-pro-latest) |
| `gemini-flash-latest` | `~google/gemini-flash-latest` | [Alias](https://openrouter.ai/~google/gemini-flash-latest) |
| `auto` | `openrouter/auto` | [Router](https://openrouter.ai/docs/guides/routing/routers/auto-router) |
| `free` | `openrouter/free` | [Router](https://openrouter.ai/docs/guides/routing/routers/free-router) |
The five remaining profiles are `auto-low`, `auto-medium`, `auto-high`,
`auto-xhigh`, and `auto-max`. Each uses `extends: {id: auto}` and only adds
`extra_params.plugins: [{id: auto-router, cost_tier: TIER}]`, where TIER is its
suffix. Do not duplicate backend/model in these children: they inherit the
effective parent, including an application replacement. Base `auto` sets no
cost tier. OpenRouter documents these five values and plugin spelling in its
[cost-tier contract](https://openrouter.ai/docs/guides/routing/routers/auto-router#cost-tier).
The [current-key endpoint](https://openrouter.ai/docs/api/api-reference/api-keys/get-current-api-key)
documents the existing authentication adapter's destination.
Preserve `~` and `/` in model values; neither makes the profile namespaced.
Do not add sampling, reasoning, timeout, token, or capability settings. If
contradictory provider evidence appears, report it rather than silently selecting
a fixed model or dropping a requested profile. A credentialed generation probe
is not needed to implement this data-backed plan.
## Stage 1: Add The Embedded Configuration Source
Scope: new YAML and small helper in `internal/configuration`.
1. Define one root backend and seventeen root profiles exactly as above. Use
ordinary identified lists and ConfigKit sibling inheritance for the five
cost-tier children. No collection wrapper or prompt definitions are needed.
2. Embed the file with `embed.FS`; expose only the internal underlay helper.
Use a fresh public ConfigKit source/context for loading, without mutable
package-level decoded data or filesystem discovery.
3. Add a focused loader-level test composing that underlay through `Load`.
Check the effective backend, the selected profile/model mapping, cost-tier
inheritance, smoke metadata, and meaningful source evidence. These assertions
protect the chosen catalog contents, not upstream model availability.
4. Do not create raw-YAML validation or live-provider tests. No public catalog
export, custom parser, catalog dependency, or parallel registry is needed.
Validation: `go test ./internal/configuration` after the helper is wired.
Completion: the embedded OpenRouter underlay and loader-level catalog test are
in place; the focused configuration suite passed with Go 1.27.0.
## Stage 2: Enable The Bundle Through Engine Construction
Scope: facade/internal options, assembly, and default/disabled contract tests.
1. Implement the option and default-on internal flag as specified. Prepend the
underlay only when enabled, using a new outer slice. Keep schema/client/
artifact options and main-source handling unchanged.
2. Add external-package facade tests showing `NewEngine(Config{})` exposes the
root backend and all seventeen profiles, but creates no prompts or synthetic
`openrouter` collection. Exercise representative `InspectProfile` values and
`InspectConfiguration` source explanations.
3. Test false disables installation, true retains it, and option ordering follows
ordinary last-option behavior. With false, caller-owned `openrouter` records
remain available. Construction succeeds with the API-key variable unset and
an HTTP transport that rejects unexpected calls.
4. Verify assembly leaves caller underlay slice/backing-array contents unchanged.
5. Adapt preexisting tests whose intended fixture is an application-only or empty
catalog by explicitly disabling the bundle in their local helpers. Update
default-inventory expectations where that is the behavior under test. Do not
globally hide default-on behavior or weaken not-found/validation assertions.
Internal direct configuration-loader tests do not need to install the bundle.
Validation: focused construction/listing/inspection tests, then
`go test ./internal/engine ./...` when intermediate wiring is coherent.
Completion: engine construction installs the bundle by default, with an
ordered opt-out option and a copied underlay slice. Facade contracts and the
full Go suite pass with Go 1.27.0.
## Stage 3: Protect Composition And Application Control
Scope: focused real-loader integration tests through the facade.
1. Verify a caller underlay and then the main input can replace a bundled ID in
order, and caller strict groups remain strict. Demonstrate complete record
replacement: a model-only replacement does not recover the old backend.
Preserve partial-profile support rather than demanding construction readiness.
2. Verify a distinct profile extending a bundled profile retains inherited
fields and applies selected overrides. Add an application-provided OpenRouter
collection with qualified references; root and collection profiles coexist.
3. Replace root backend `openrouter` with a complete local endpoint definition.
Confirm surviving bundled profiles use the replacement backend's settings.
No reserved-ID checks or special merge semantics are added.
4. Replace root `auto` with a profile selecting a different caller backend and
model. All five cost-tier children must inherit that target and retain their
plugin entries. Do not reject this customization or strip plugin settings.
5. Exercise explicit sequence modes using ConfigKit's existing controls:
`config.sequences: [{destination: [profiles], mode: append}]` with a duplicate
bundled ID fails normally; `mode: replace` with an application profile list
replaces all lower profiles. Include a nonconflicting append case. Keep
backend/profile references valid where testing successful replacement.
6. Show that clearing/replacing backend definitions without maintaining surviving
profile references follows existing validation, not a hidden fallback.
Disabling the bundle remains the simple application-only configuration path.
Validation: targeted new composition tests plus existing source, inheritance,
namespace, and profile-completion tests. Avoid reproducing ConfigKit's generic
sequence implementation suite.
Completion: facade integration tests cover caller precedence, whole-record
replacement, inheritance, scoped resources, sequence modes, strict conflicts,
and missing backend rejection. Targeted tests pass on Go 1.27.0.
## Stage 4: Verify Preparation And HTTP Request Construction
Scope: bundled-profile execution integration with existing runner/client paths.
1. With a minimal caller prompt and an injected deterministic client, prepare
and run using an explicit bundled profile. Check selected profile/backend,
model slug, and ordinary defaults/overrides. A prompt with no default and
no request profile must still produce the existing profile-required error.
2. For every cost tier, use the built-in HTTP client and an offline recording
RoundTripper or local server to inspect the outbound JSON. Assert the model
is `openrouter/auto` and `plugins` is a top-level array with the matching
`auto-router`/`cost_tier` entry, not nested under an `extra_params` member.
3. Verify base `auto` sends no explicit cost-tier plugin and `free` preserves
`openrouter/free`. Check at least one `~`-prefixed model reaches the request
unchanged. Return a minimal valid synthetic chat response.
4. Reuse existing runtime credential tests and override tests. Add only missing
bundle integration assertions, such as selecting `OPENROUTER_API_KEY` from
the installed backend. No real credentials, calls, provider capability
assumptions, hardcoded prices, or changes to optional-key behavior are needed.
Validation: new request cases and existing generation/profile/override tests.
Offline HTTP assertions prove payload construction, not provider acceptance.
Completion: offline preparation, injected-client execution, and HTTP payload
tests verify profile selection, overrides, credentials, router plugins, and model
slugs. Focused execution tests pass on Go 1.27.0.
## Stage 5: Verify Bundled Smoke-Test Defaults
Scope: facade-level smoke integration with a synthetic HTTP boundary.
1. On a default engine, explicitly request reachability with
`BackendID: openrouter`. Verify the existing backend-endpoint fallback.
Authentication should select the `openrouter_key` adapter and
`https://openrouter.ai/api/v1/key`, using an isolated synthetic key and a
recording transport. Return the existing adapter's minimal success fixture.
2. For text and structured inference, specify the backend without prompt/profile.
Assert fallback to `gpt-luna-latest` and its exact model slug. Return suitable
private-case responses using the existing smoke-test fixtures. Preserve
factual reports and nil-error concluded-failure semantics.
3. Show explicit profile selection wins over prompt default and backend metadata,
and a prompt default wins over backend metadata when no profile is supplied.
Smoke tests must still use their private cases, not execute caller prompt
content or output contracts.
4. Replace `gpt-luna-latest` through application configuration and verify fallback
uses the effective replacement. Replace backend smoke metadata to select a
different caller profile and confirm the ordinary selection path follows it.
5. Verify listing/inspection are not modified by diagnostics. Keep the existing
rule that diagnostics run only on explicit invocation; no new constructor,
ordinary-run, CI, or release probe is introduced.
Validation: targeted bundled smoke tests and existing smoke contract/integration
tests. Reuse existing endpoint/auth fixtures instead of adding an adapter or
duplicating the full diagnostic suite.
Completion: offline smoke tests verify reachability, authentication, both
inference cases, fallback and override selection, replacement behavior, and
unchanged inspection. Targeted smoke tests pass on Go 1.27.0.
## Stage 6: Update Documentation And Verify The Sprint
Scope: public GoDoc, current guidance/examples, and end-to-end verification.
1. Update `ConfigurationSources`, `Config`/`NewEngine`, and new-option GoDoc to
describe the dedicated lower underlay, default-on behavior, disabling, and
changed inventories. Keep exact option semantics in GoDoc.
2. Update `docs/formats.md` and `docs/consumers/pkg-promptkit.md` with the bundle,
root layout, explicit selection, optional credentials, smoke fallback, and
disabling/whole-record replacement/inherited-variant examples. Make one
canonical bundle inventory in the format reference; link rather than repeat
it throughout guidance. Link provider docs for alias/router behavior.
3. Remove stale claims of no installed catalogs from current migration guidance
and internal source/assembly documentation. Explain that an external
collection is still supported, and replacing `auto` changes inherited child
targets while retaining plugins. Do not add a rule that external catalogs
must be namespaced or upstream aliases must have fixed capabilities.
4. Keep the three maintained examples offline. Where their caller-only inventories
are intentional, pass `WithBundledOpenRouter(false)` and explain that choice;
retain useful fixture assertions rather than expanding them with unrelated
default inventory. Add a short consumer-guide bundled-profile example instead
of another live example program. Recheck README quickstart claims.
5. Add the feature and compatibility effects to the currently untagged
`docs/releases/v0.18.0.md`, including how to retain application-only behavior.
Explain that previously unresolved references matching bundled IDs can now
resolve, alongside the changed default inventories and inspection sources.
If that release has been published by implementation time, use the next
unreleased minor note instead; preserve published notes/tags. Follow the
release procedure for the note/footer. No release operation is part of this
sprint.
6. Format changes, prepare dependency caches, and run with the compatible local
Go toolchain:
```sh
go test -race -vet=off ./...
go vet ./...
go run ./examples/go-library/prepare
go run ./examples/go-library/run
go run ./examples/go-library/observe
git diff --check
```
7. Review the complete diff for unexpected API changes, caller mutation, new
source/history validation, implicit selection, network access, credential
values, and stale links. Confirm only intended documentation and inventories
changed. Follow the focused commit policy and record completion in the
feature roadmap; retire completed plans during release preparation.
Completion requires default availability, full disabling, ordinary caller
composition and provenance, correct cost-tier payloads and explicit diagnostic
fallbacks, and current documentation. There are no unresolved product choices,
external publication prerequisites, or new recurring release gates.