# 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. ## 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. ## 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. ## 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. ## 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. ## 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.