Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ differs is a named `D-*` row in the ledger at `PLAN.md §18`.
## Unreleased

### Fixed
- **A provider's `options.apiKey` was never used as the credential (issue #28).**
The endpoint builder read `auth.json`/`auth-overlay.json` only, so every
provider whose key lived in the documented `options.apiKey` slot sent
`Authorization: Bearer ` (empty) to the upstream, which answers
`missing_api_key` and reads like a client bug. opencode 2.x no longer writes
`auth.json` (keys live in the `credential` table), so this hit every
config-declared provider on a 2.x install. A literal key was dropped as
silently as a `{env:...}` reference. Config `options.apiKey` now follows
upstream precedence (config first, auth layer as the fallback) and is
interpolated: `{env:NAME}` and `{file:PATH}`, with `{file:}` resolved relative
to the declaring config. `options/baseURL` is interpolated too. An unset
`{env:NAME}` still expands to the empty string (upstream parity) but logs the
field path, and a provider whose configured key resolves empty is named at
boot without the secret. `OCSERVE_CONFIG_INTERPOLATE=0` restores the old
verbatim behaviour for operators sharing one config with another server.
- **Credential rename and delete were no-ops.** `PATCH /api/credential/{id}` and
`DELETE /api/credential/{id}` returned 204 without touching the store, so a
client renaming a credential saw success and no change. Both now edit the
Expand Down
1 change: 1 addition & 0 deletions TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
| K-WATCHDOG2 | Two-phase provider watchdog (gateway keep-alive hangs) | The 2026-10-12 report: session `ses_001a12c733...` (mimo-v2.6-flash / opencode-go) stopped continuing — 4 stacked user messages, every turn killed at exactly 120s (`provider stream stalled: no data for 120s`; 2026-10-11 20:17/20:19, 2026-10-12 07:51/09:31). Live probe with the exact composite UA + key: the gateway answers headers immediately then streams `: keep-alive` comment pings every 5s while the free tier queues the model; 6 MB request first-data 13-17s normal, one probe 67s+ then a completion, 0.25/0.5/1/3 MB always completed — keep-alives satisfied the old socket-liveness read, and the first-real-event wait used the 120s stream budget. Fix: keep-alive frames are NOT data; first real event budget = `provider_ttfb()` (OCSERVE_PROVIDER_TTFB_SECS → OCSERVE_PROVIDER_STALL_SECS → default 300s), everything after stays `provider_stall()` (120s) — genuinely silent sockets still trip at 120s; compaction stream loop got the same split | `stall_gateway.rs` keep-alive-only-forever: trips at the TTFB budget, lock+task state released; `stall_gateway_data.rs` first-data at 3s under stream=1s/TTFB=6s: turn completes (+ negative control: neuter the phase split → red, proven) | green (476 tests, guards 26/26, replay 26/0) |
| K-FREETIER-TRANSPORT | opencode.ai requests pass the free-tier gate: BoringSSL TLS stack + composite UA + tools array; other providers keep rustls | `zen_live` trio (nightly, live: positive / text-only / negative), end-to-end prompt through a real server (`ZEN_E2E`), 5-stack replay evidence in `bench/zen-probe` | green |
| K-STREAM-FINISH-CONTENT | A chunk carrying both content and `finish_reason` produces the content event before Done (no dropped reply) | `finish_with_content_tests` (text, tool-call, split-chunk order) | green |
| K-PROVIDER-KEY | Config-declared provider credentials follow upstream precedence (`options.apiKey`, `{env:…}`/`{file:…}` interpolated, then the auth layer) — issue #28. Before this the endpoint builder read `auth.json`/`auth-overlay.json` ONLY, so a provider whose key lived in the documented `options.apiKey` slot (the shape opencode 2.x emits, since 2.x stopped writing auth.json) reached the upstream with `Bearer ` — empty, after `unwrap_or("")` — while `/config/providers` displayed the key. The provider then answered `missing_api_key`, which reads as a client bug. A literal key was dropped as silently as `{env:…}`, so both arms are covered. Deliberate additions: an unset `{env:…}` expands to "" (upstream parity) but logs the field path, and a provider whose configured key resolves empty is named at boot (no secret in the message). `{file:}` resolves relative to the declaring config, not the process cwd. `options/baseURL` is interpolated too. | `runtime::provider_key_tests` ×9 (`literal_config_apikey_reaches_the_endpoint`, `env_reference_in_config_apikey_is_interpolated`, `unset_env_reference_warns_and_expands_empty`, `other_placeholder_forms_are_left_verbatim`, `file_reference_resolves_relative_to_the_config_dir`, `missing_file_reference_warns_with_the_resolved_path`, `plain_strings_are_returned_untouched`, `auth_layer_is_the_fallback_and_config_wins`, `interpolation_kill_switch_returns_the_raw_value`) + live wire repro: fake upstream capture sink logs `Authorization`, ocserve pre-fix `Bearer ` / post-fix `Bearer secret-from-env` for `{env:FAKE_API_KEY}` and `Bearer sk-literal` for a literal | green |
| K-MODEL-OFFER | The served model list matches freeze: `deprecated` models are never offered, `alpha` only under `OPENCODE_ENABLE_EXPERIMENTAL_MODELS`, and the effective default cannot select a model the picker no longer lists | `runtime::tests::served_models_exclude_deprecated_and_alpha`, `experimental_flag_admits_alpha_but_never_deprecated`, `default_model_skips_a_deprecated_state_entry`; live check on a throwaway port: opencode 46 -> 9 models, matching upstream 1.18.31 | green |
| K-FREETIER-UPSELL | A spent opencode free allowance is terminal and actionable: classified from the provider body and surfaced as upstream's upsell in ONE typed `APIError` (durable and live), never a silent raw 429 | `ocserve-llm` free_tier classifier tests (real body; plain 429/500 excluded), `prompt::free_tier_tests`, `free_tier_error` e2e (one durable typed event; red as `UnknownError` when the marker is dropped), contract-validated `EventSessionError`; live journal 2026-10-25T15:46/15:47 (three silent attempts) and 17:26:58 (typed live, UnknownError durable) | green |
| K-BOUNDED-PROMPT-LOAD | Prompt history is loaded as a provable suffix (O(8 MB)) instead of the whole session (85 MB on the deep session); equivalence is machine-checked, with a full-load fallback and a kill switch | `crates/ocserve-store/tests/prompt_window.rs` (9 window shapes, incl. the multi-compaction late-insert anchor regression), rule 57, `prompt.rs::bounded_prompt_load_tests` (10 equivalence cases incl. planted control), `bounded_prompt_load_matches_real_corpus` (interleaved, warm-up discarded; `bench/prompt-window/REPORT.md`; real sessions: 16,616-msg deep session 4.6-5.2 s → 1.45-1.59 s; compacted session 117 ms → 22 ms; a dense mid-size session 2.89 → 2.56 s where the window is most of the data; emitted messages byte-identical in every case), `OCSERVE_PROMPT_HISTORY_UNBOUNDED=1` | green |
Expand Down
Loading