diff --git a/.trellis/tasks/04-17-04-17-fix-sync-panic/task.json b/.trellis/tasks/04-17-04-17-fix-sync-panic/task.json new file mode 100644 index 0000000..21bfc40 --- /dev/null +++ b/.trellis/tasks/04-17-04-17-fix-sync-panic/task.json @@ -0,0 +1,53 @@ +{ + "id": "04-17-fix-sync-panic", + "name": "04-17-fix-sync-panic", + "title": "Fix sync command panic", + "description": "", + "status": "planning", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P2", + "creator": "Apale", + "assignee": "Apale", + "createdAt": "2026-04-17", + "completedAt": null, + "branch": null, + "base_branch": "master", + "worktree_path": null, + "current_phase": 0, + "next_action": [ + { + "phase": 1, + "action": "brainstorm" + }, + { + "phase": 2, + "action": "research" + }, + { + "phase": 3, + "action": "implement" + }, + { + "phase": 4, + "action": "check" + }, + { + "phase": 5, + "action": "update-spec" + }, + { + "phase": 6, + "action": "record-session" + } + ], + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/.trellis/tasks/04-17-forwarding-log-viewer-gui/prd.md b/.trellis/tasks/04-17-forwarding-log-viewer-gui/prd.md new file mode 100644 index 0000000..0b35cc0 --- /dev/null +++ b/.trellis/tasks/04-17-forwarding-log-viewer-gui/prd.md @@ -0,0 +1,469 @@ +# OLPX Forwarding Log Viewer Design + +## Summary + +`olpx` currently exposes enough per-request routing information for header-level debugging, but it does not keep a durable request log and it does not provide any local UI. + +This task defines a minimal, implementation-ready design for a local web log viewer that shows each forwarded request with: + +- the selected provider +- the selected upstream model +- input, output, and total token counts when available +- whether failover was triggered +- the attempt chain that led to the final result + +The design is intentionally scoped to the current codebase: + +- Go backend only +- no existing frontend build pipeline +- only OpenAI Responses proxying today +- failover is only valid before first downstream byte + +## Current Project State + +### What already exists + +`olpx` already has the core proxy path in `internal/proxy/server.go`: + +- `POST /v1/responses` +- deterministic ordered failover +- retry on transport errors, `429`, and `5xx` +- no mid-stream failover after downstream response starts +- debug response headers: + - `X-OLPX-Alias` + - `X-OLPX-Provider` + - `X-OLPX-Remote-Model` + - `X-OLPX-Attempt` + - `X-OLPX-Failover-Count` + +### What is missing + +- no durable per-request log +- no token accounting capture +- no HTTP endpoint for log history +- no local HTML page +- no request attempt history persisted anywhere + +## Product Goal + +Give the user a simple local page that makes forwarding behavior visible without reading raw terminal logs. + +For every completed forwarding operation, the page should make it obvious: + +1. which alias was requested +2. which provider and upstream model finally served it +3. whether failover happened +4. how many attempts were made +5. how many input, output, and total tokens were reported + +## Non-Goals + +1. No hosted dashboard. +2. No Electron or native desktop GUI. +3. No cost accounting or billing estimation. +4. No prompt/response body browsing. +5. No full-text search over request payloads. +6. No live stream reconstruction in the browser. +7. No multi-user auth model beyond the current local-only default posture. + +## Recommended MVP Shape + +Choose a local web UI, not a separate GUI app. + +Reason: + +- it fits the current Go-only codebase +- it avoids adding a frontend toolchain +- it can run off the same `olpx serve` process and listener +- it is the smallest path to a usable visual log surface + +## High-Level Design + +When `olpx serve` is running, the same HTTP server should expose three surfaces: + +1. proxy API + - `POST /v1/responses` + - `GET /v1/models` + +2. log viewer UI + - `GET /logs` + +3. log viewer data API + - `GET /api/logs` + - `GET /api/logs/{id}` + +The UI should be a single embedded HTML page with minimal CSS and vanilla JS. + +No Node, bundler, or SPA framework should be introduced for this feature. + +## Data Flow + +```text +incoming /v1/responses request + -> create in-memory request log draft + -> record each upstream attempt result + -> on final completion, attach usage if available + -> append one finalized JSON record to local log file + -> push finalized record into in-memory recent ring buffer + -> /api/logs serves recent records to /logs page +``` + +The key design choice is: persist one finalized record per request, not one row per streaming chunk and not one row per partial attempt. + +This keeps storage simple and makes the UI directly match the user's mental model: one forwarding operation equals one log entry. + +## Storage Strategy + +### Recommended MVP storage + +Use an append-only JSONL file stored next to the existing `olpx` config: + +- config path today: `~/.config/olpx/config.json` by default +- proposed log path: `~/.config/olpx/request-log.jsonl` + +Reason: + +- no database dependency required +- minimal code and operational complexity +- easy to inspect manually +- easy to rotate or delete +- aligned with current non-SQLite implementation + +### In-memory cache + +Maintain a small in-memory ring buffer of the most recent finalized records, for example the latest `500` entries. + +On process start: + +- load the latest records from `request-log.jsonl` into memory + +During runtime: + +- append finalized record to disk +- append finalized record to ring buffer + +This gives fast UI reads while still preserving history across restarts. + +## Log Record Schema + +Each finalized request log entry should contain at least the following fields: + +```json +{ + "id": "req_20260417_000123", + "started_at": "2026-04-17T12:34:56.123Z", + "completed_at": "2026-04-17T12:34:58.004Z", + "duration_ms": 1881, + "protocol": "openai-responses", + "alias": "gpt-5.4", + "raw_model": "olpx/gpt-5.4", + "stream": true, + "final_status": 200, + "final_provider": "p2", + "final_remote_model": "GPT-5.4", + "attempt_count": 2, + "failover_count": 1, + "failover_triggered": true, + "usage": { + "input_tokens": 812, + "output_tokens": 143, + "total_tokens": 955, + "available": true, + "source": "response_usage" + }, + "attempts": [ + { + "attempt": 1, + "provider": "p1", + "remote_model": "up-1", + "outcome": "retryable_status", + "upstream_status": 429, + "error_summary": "upstream 429: rate limit", + "duration_ms": 220 + }, + { + "attempt": 2, + "provider": "p2", + "remote_model": "GPT-5.4", + "outcome": "success", + "upstream_status": 200, + "duration_ms": 1661 + } + ] +} +``` + +### Required field semantics + +- `alias`: normalized local alias actually routed by `olpx` +- `raw_model`: original request payload `model` value before normalization +- `final_provider`: provider that produced the downstream response visible to the client +- `final_remote_model`: upstream model name actually sent to that provider +- `failover_triggered`: `true` when the request had to move beyond the initial target +- `attempts`: ordered attempt chain, including retryable failures before success + +### Fields that must stay nullable + +`usage.input_tokens`, `usage.output_tokens`, and `usage.total_tokens` must be nullable in storage and UI. + +Reason: + +- some failures end before usage exists +- some providers may omit usage +- some stream terminations may prevent complete usage extraction + +## Token Collection Design + +### Requirement boundary + +The user asked for input/output token counts, so MVP must collect usage from upstream responses when that information is present. + +### Non-streaming path + +For non-streaming OpenAI Responses responses: + +- capture the full JSON body already headed to the client +- parse `usage` from the final response object + +### Streaming path + +For streaming responses: + +- do not buffer the whole response before writing downstream +- keep current streaming pass-through behavior +- tee streamed bytes into a lightweight SSE parser +- extract usage from the terminal lifecycle event that contains final response usage when present + +In practice this will usually mean parsing the final response completion event and reading its usage payload. + +### Missing usage behavior + +If usage cannot be determined: + +- keep the request log entry +- set usage fields to `null` +- set `usage.available = false` +- record a short reason such as `missing_from_upstream` or `stream_incomplete` + +The request itself must not fail just because logging could not extract usage. + +## Failover Visibility Design + +The UI must make failover impossible to miss. + +### Request list presentation + +Each row should show a failover badge with one of two states: + +- `Primary` when `failover_triggered = false` +- `Failover` when `failover_triggered = true` + +### Request detail presentation + +Expanding a row should show the attempt chain in order, for example: + +```text +Attempt 1 p1 / up-1 429 retryable +Attempt 2 p2 / GPT-5.4 success +``` + +This detail view is necessary because `failover_count` alone does not explain why failover happened. + +## UI Design + +### Route + +`GET /logs` + +### Layout + +Use one compact page with two sections: + +1. top summary strip +2. request table with expandable detail rows + +### Summary strip + +Show a small set of metrics derived from loaded records: + +- recent request count +- recent failover count +- recent failover rate +- last request time + +This should stay informational only. No charts are required for MVP. + +### Request table columns + +The main table should show: + +- Time +- Alias +- Final Provider +- Final Model +- Status +- Tokens In +- Tokens Out +- Tokens Total +- Failover +- Duration + +### Filters + +Keep filters intentionally small: + +- text search for alias/provider/model +- failover only toggle +- status filter +- page size selector + +### Detail row + +Expanding a row should show: + +- request id +- raw model +- stream true/false +- attempt timeline +- short error summary for failed attempts + +Do not show prompt text, response text, or authorization headers. + +## HTTP API Design + +### `GET /api/logs` + +Returns recent finalized records in reverse chronological order. + +Suggested query params: + +- `limit` +- `cursor` +- `failover_only` +- `status` +- `q` + +### `GET /api/logs/{id}` + +Returns one full finalized record including all attempts. + +### Pagination + +Cursor pagination is preferred over offset pagination. + +Reason: + +- append-only log shape +- stable ordering by completion time +- easier incremental page refresh + +## Security and Privacy + +### Must not log + +- upstream API keys +- incoming Authorization headers +- full request body +- full model input text +- full model output text + +### Why + +The viewer is for routing observability, not transcript storage. + +This keeps the feature useful without turning it into a sensitive local prompt archive. + +### Local exposure model + +For MVP, the log viewer can follow the same trust model as the current local proxy: + +- same local process +- same loopback listener by default +- intended for single-user localhost usage + +If `olpx` is later allowed to bind non-loopback addresses, the log UI and log API should be explicitly gated before reuse in that mode. + +## Implementation Plan + +### Phase 1: request log model and storage + +- add a request log record type +- add append-only JSONL writer +- add startup loader for recent records +- add in-memory ring buffer + +### Phase 2: proxy instrumentation + +- create one draft log object per incoming request +- capture normalized alias and raw model +- record every attempt outcome +- record final provider, remote model, status, and duration +- extract usage from success responses when available + +### Phase 3: local web surface + +- add `GET /logs` +- add `GET /api/logs` +- add `GET /api/logs/{id}` +- embed a single static HTML/CSS/JS page + +### Phase 4: docs and tests + +- document the viewer route in `README.md` +- add tests for failover logging +- add tests for non-failover logging +- add tests for usage extraction fallback behavior + +## Suggested Package Additions + +Keep the changes minimal. + +Suggested new package: + +```text +internal/requestlog/ + model.go + store.go + api.go +``` + +Suggested existing files to extend: + +- `internal/proxy/server.go` +- `internal/proxy/server_test.go` +- `internal/cli/serve.go` +- `README.md` + +## Acceptance Criteria + +This design is considered implemented successfully when: + +1. running `olpx serve` exposes a local page at `/logs` +2. the page shows one row per completed forwarding request +3. each row clearly shows final provider and final upstream model +4. each row clearly shows whether failover happened +5. token counts are displayed when upstream usage is available +6. expanding a row shows the attempt chain that led to the final result +7. no request/response text or secrets are persisted to the log + +## Out of Scope for This Task + +1. historical charts and trends +2. exporting CSV +3. provider health scoring +4. live websocket or SSE push to the browser +5. per-provider cost calculation +6. editing provider or alias config from the UI + +## Final Recommendation + +Implement this as a same-process local web viewer backed by append-only JSONL request summaries. + +This is the smallest design that satisfies the user's stated need: + +- concise GUI/web page +- explicit provider/model visibility +- token visibility +- unambiguous failover visibility + +It also fits the actual repository state today without introducing a frontend stack, database migration, or broader dashboard scope. diff --git a/.trellis/tasks/04-17-forwarding-log-viewer-gui/task.json b/.trellis/tasks/04-17-forwarding-log-viewer-gui/task.json new file mode 100644 index 0000000..3275e0c --- /dev/null +++ b/.trellis/tasks/04-17-forwarding-log-viewer-gui/task.json @@ -0,0 +1,73 @@ +{ + "id": "forwarding-log-viewer-gui", + "name": "forwarding-log-viewer-gui", + "title": "Design forwarding log viewer GUI for olpx", + "description": "Define a minimal local web UI and request logging design for olpx forwarding operations, including provider/model visibility, token usage, and failover display.", + "status": "completed", + "dev_type": "docs", + "scope": "observability", + "package": null, + "priority": "P2", + "creator": "Apale", + "assignee": "Apale", + "createdAt": "2026-04-17", + "completedAt": "2026-04-17", + "branch": null, + "base_branch": "master", + "worktree_path": null, + "current_phase": 0, + "next_action": [ + { + "phase": 1, + "action": "brainstorm" + }, + { + "phase": 2, + "action": "research" + }, + { + "phase": 3, + "action": "implement" + }, + { + "phase": 4, + "action": "check" + }, + { + "phase": 5, + "action": "update-spec" + }, + { + "phase": 6, + "action": "record-session" + } + ], + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [ + ".trellis/tasks/04-17-forwarding-log-viewer-gui/prd.md", + "internal/proxy/server.go", + "internal/proxy/server_test.go", + "internal/cli/serve.go", + "README.md" + ], + "notes": "Design-only task completed. The PRD recommends a same-process local web viewer with append-only JSONL request logs and no new frontend build system.", + "meta": { + "recommendedSurface": "local-web-ui", + "protocolScope": [ + "openai-responses" + ], + "storageRecommendation": "jsonl-next-to-config", + "mustShow": [ + "provider", + "remote-model", + "input-tokens", + "output-tokens", + "total-tokens", + "failover" + ] + } +} diff --git a/.trellis/tasks/04-17-preserve-opencode-model-metadata-sync/task.json b/.trellis/tasks/04-17-preserve-opencode-model-metadata-sync/task.json new file mode 100644 index 0000000..64e7a4a --- /dev/null +++ b/.trellis/tasks/04-17-preserve-opencode-model-metadata-sync/task.json @@ -0,0 +1,53 @@ +{ + "id": "preserve-opencode-model-metadata-sync", + "name": "preserve-opencode-model-metadata-sync", + "title": "Preserve OpenCode model metadata during olpx sync", + "description": "", + "status": "completed", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P2", + "creator": "Apale", + "assignee": "Apale", + "createdAt": "2026-04-17", + "completedAt": "2026-04-17", + "branch": null, + "base_branch": "master", + "worktree_path": null, + "current_phase": 0, + "next_action": [ + { + "phase": 1, + "action": "brainstorm" + }, + { + "phase": 2, + "action": "research" + }, + { + "phase": 3, + "action": "implement" + }, + { + "phase": 4, + "action": "check" + }, + { + "phase": 5, + "action": "update-spec" + }, + { + "phase": 6, + "action": "record-session" + } + ], + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} diff --git a/.trellis/workspace/Apale/index.md b/.trellis/workspace/Apale/index.md index 04b2dff..96f0b0b 100644 --- a/.trellis/workspace/Apale/index.md +++ b/.trellis/workspace/Apale/index.md @@ -8,7 +8,7 @@ - **Active File**: `journal-1.md` -- **Total Sessions**: 4 +- **Total Sessions**: 5 - **Last Active**: 2026-04-17 @@ -19,7 +19,7 @@ | File | Lines | Status | |------|-------|--------| -| `journal-1.md` | ~179 | Active | +| `journal-1.md` | ~216 | Active | --- @@ -29,6 +29,7 @@ | # | Date | Title | Commits | Branch | |---|------|-------|---------|--------| +| 5 | 2026-04-17 | Fix sync command panic | `887eb14` | `master` | | 4 | 2026-04-17 | Review MVP completion status | - | `master` | | 3 | 2026-04-17 | Support disabling providers in olpx | HEAD | `master` | | 2 | 2026-04-17 | Build OPS MVP failover proxy | `2b04d91` | `master` | diff --git a/.trellis/workspace/Apale/journal-1.md b/.trellis/workspace/Apale/journal-1.md index 0b316ff..3bac283 100644 --- a/.trellis/workspace/Apale/journal-1.md +++ b/.trellis/workspace/Apale/journal-1.md @@ -177,3 +177,40 @@ Reviewed current implementation against the archived MVP PRD and classified impl ### Next Steps - None - task complete + + +## Session 5: Fix sync command panic + +**Date**: 2026-04-17 +**Task**: Fix sync command panic +**Branch**: `master` + +### Summary + +Fixed a panic in opencode sync when preserved model metadata contains slices, and added package plus CLI regression coverage. + +### Main Changes + +- Root cause: `internal/opencode/opencode.go` compared `interface{}` values directly inside `mapsEqualShallow`, which panicked when preserved `provider.olpx.models.` metadata included slices. +- Fix: replaced the unsafe hand-rolled comparison with `reflect.DeepEqual` so existing model metadata with nested maps/slices can be compared safely during `sync`. +- Added regression tests in `internal/opencode/opencode_test.go` and `internal/cli/cli_test.go` covering preserved slice metadata and a real `opencode sync --target` no-op path. +- Verification: ran `rtk go test ./internal/opencode`, `rtk go test ./internal/cli ./internal/opencode`, and `rtk go test ./...` successfully. + + +### Git Commits + +| Hash | Message | +|------|---------| +| `887eb14` | (see git log) | + +### Testing + +- [OK] (Add test results) + +### Status + +[OK] **Completed** + +### Next Steps + +- None - task complete