## Implementation Plan: Display Deep Research Costs for Admin Users ### Overview The goal is to pipe cost data from the OpenAI gateway response headers through the entire deep research pipeline, then render it in the session template only for admin viewers. Four internal layers need changes, following the established pattern in `correction.py` and `bot/llm.py`. No changes to the gateway are required; cost headers (`X-Gateway-Cost-USD`, etc.) are already returned. --- ### 1. `devplacepy/services/deepsearch/llm.py` (LLM client) **Change:** Extract cost from `response.headers` in `request_completion()` and include it in the returned `usage` dict. - **Import** `parse_usage_headers` from `devplacepy.services.openai_gateway.usage` (add line in existing imports). - **Line 38–44:** After `response = await client.post(…)` and before `data = response.json()`, parse headers: ```python headers = response.headers cost_info = parse_usage_headers(headers) ``` - Modify the return value to include cost. The current return is `(data, body_usage, elapsed_ms)`. Change to: ```python usage = { "tokens_in": body_usage.get("prompt_tokens", 0), "tokens_out": body_usage.get("completion_tokens", 0), "elapsed_ms": elapsed_ms, } if cost_info: usage["cost_usd"] = cost_info["cost_usd"] usage["prompt_tokens"] = cost_info["prompt_tokens"] usage["completion_tokens"] = cost_info["completion_tokens"] return data, usage, elapsed_ms ``` --- ### 2. `devplacepy/services/jobs/deepsearch/enhance.py` (Planner) **Change:** Apply the same header extraction in the planner’s OpenAI call. - **Lines 86–92:** After `response = await client.post(…)` and before `data = response.json()`, add: ```python headers = response.headers cost_info = parse_usage_headers(headers) ``` - **Return value** (function `enhance_query`): Currently returns `enhanced_query`. Change signature to return a dict that includes both query and cost, e.g., `{"query": enhanced_query, "cost_usd": cost_info["cost_usd"] if cost_info else None}`. The caller in `orchestrate.py` will need to unpack accordingly. --- ### 3. `devplacepy/services/jobs/deepsearch/orchestrate.py` (Orchestration) **Change:** Thread cost through `_complete()` and the `Orchestration` dataclass, and accumulate into the final outcome. - **`Orchestration` dataclass (lines 35–42):** Add a field `total_cost_usd: float = 0.0`. - **`_complete()` method (lines 153–169):** After calling `llm.request_completion()`, capture the `cost_usd` from the `usage` dict and add it to `self.total_cost_usd`: ```python usage = response[1] # the new usage dict if usage.get("cost_usd"): self.total_cost_usd += usage["cost_usd"] ``` - **`_agent_done()` method (lines 289–300):** Include `cost_usd` in the progress frame dict for future tracing. - **Final outcome building (around line 320–340):** When constructing the `Outcome` namedtuple or dict that the worker receives, include `total_cost_usd`. --- ### 4. `devplacepy/services/jobs/deepsearch/worker.py` (Worker report) **Change:** Add `total_cost_usd` key to the `report` dict. - **Lines 215–231:** Add after `"chunk_count": chunk_count,`: ```python "total_cost_usd": outcome.total_cost_usd if hasattr(outcome, "total_cost_usd") else 0.0, ``` --- ### 5. `devplacepy/routers/tools/deepsearch.py` (Router) **Change:** Plumb cost from `report` into the session context. - **`_session_context()` (lines 225–257):** After `viewer_is_admin = bool(current_user and …)`, add: ```python cost_usd = report.get("total_cost_usd", None) ``` - Return `cost_usd` in the context dict. Example addition: `"show_cost": viewer_is_admin and cost_usd is not None,` - Also consider the `_job_payload()` function (lines 55–78) – it currently returns metrics but no cost. This function is used for API payloads; if needed, add a `cost` field there for future use. --- ### 6. `devplacepy/templates/tools/deepsearch_session.html` (Template) **Change:** Render the cost metric conditionally for admin users. - **Lines 13–19:** After the `diversity` metric, add: ```html {% if viewer_is_admin and total_cost_usd is not None %} ${{ total_cost_usd }} cost {% endif %} ``` Note: The template variable name must match what the router passes. Use `total_cost_usd` or `cost_usd` consistently. --- ### 7. `tests/api/tools/deepsearch/session.py` (Tests) **Change:** Update test fixture and assertions. - **`_seed_done_session()` fixture (lines 30–42):** Add `total_cost_usd: 0.15` (or any plausible float) to the report dict. - **Assertions (lines 90–99):** Add check that the rendered HTML contains the cost metric when the viewer is an admin, e.g., ```python assert b"$0.15" in response.content ``` For the non-admin case, assert the cost is not shown. --- ### Definition of Done - [ ] All code changes above are implemented across the 7 files. - [ ] The project’s test command passes: `pip install -e '.[dev]' -q && make test-unit` - [ ] The project’s lint command passes: `pip install -q ruff && ruff check .` - [ ] In the running application, an admin user viewing a deep research session sees a “$X cost” metric in the metrics row (verified by inspecting the template rendering). - [ ] A non‑admin user does not see the cost metric. - [ ] The cost value displayed matches the total USD cost recorded by the OpenAI gateway for that deep research job. - [ ] All existing unit tests in `tests/unit/services/openai_gateway/usage.py` still pass. - [ ] The new test in `tests/api/tools/deepsearch/session.py` correctly asserts cost rendering for admin and absence for non‑admin.