diff --git a/AGENTS.md b/AGENTS.md index 57c2cd407..7b6aa814b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -150,6 +150,12 @@ - For shared route surfaces and large feature UIs, prefer putting the decisive dark-theme overrides in the global theme stylesheet (`src/style.css`) instead of relying only on component-scoped `:global(:root.dark)` blocks. - Scoped dark overrides are fine for truly local elements, but if a full route still looks like light theme in dark mode, add or strengthen the global selectors for that surface. +## UI Interaction Component Rule + +- Never add a normal HTML `` or `prompt()` usage, preserve the existing behavior and state wiring while moving the interaction into the app component system. + ## NPX Testing Rule - For any `npx` package behavior test, **publish first**, then test the published `@latest` package. diff --git a/llm-wiki/raw/fixes/opencode-zen-reasoning-summary-replay.md b/llm-wiki/raw/fixes/opencode-zen-reasoning-summary-replay.md new file mode 100644 index 000000000..9e1351525 --- /dev/null +++ b/llm-wiki/raw/fixes/opencode-zen-reasoning-summary-replay.md @@ -0,0 +1,36 @@ +# OpenCode Zen Reasoning Summary Replay Fix + +## Date +2026-05-10 + +## Problem +After an OpenCode Zen `big-pickle` turn produced reasoning metadata, a later Responses-backed turn could fail with: + +```json +{"type":"invalid_request_error","message":"[ArrayParam] [input[4].content] [array_above_max_length] Invalid 'input[4].content': array too long. Expected an array with maximum length 0, but got an array with length 1 instead."} +``` + +The local proxy had been translating upstream Chat Completions `reasoning_content` into a Responses `reasoning` output item with `content: [{ "type": "reasoning_text", "text": "..." }]`. When that item was replayed as conversation history into a later Responses request, the provider rejected non-empty `reasoning.content`. + +## Fix +The unified Responses proxy now emits translated reasoning output as: + +```json +{ + "type": "reasoning", + "summary": [{ "type": "summary_text", "text": "..." }], + "content": [] +} +``` + +`responsesInputToMessages()` reads both `content` and `summary`, so later Chat Completions proxy turns still recover `reasoning_content` from the summary while Responses providers receive schema-valid empty reasoning content. + +## Files +- `src/server/unifiedResponsesProxy.ts` +- `src/server/unifiedResponsesProxy.test.ts` +- `tests.md` + +## Verification +- `pnpm vitest run src/server/unifiedResponsesProxy.test.ts` +- `pnpm run test:unit` +- `pnpm run build` diff --git a/llm-wiki/raw/fixes/provider-scoped-model-selection-zen.md b/llm-wiki/raw/fixes/provider-scoped-model-selection-zen.md new file mode 100644 index 000000000..cb35ad747 --- /dev/null +++ b/llm-wiki/raw/fixes/provider-scoped-model-selection-zen.md @@ -0,0 +1,92 @@ +# Provider-Scoped Model Selection and Zen Receive Verification + +Date: 2026-05-10 + +## Problem + +Codex Web Local could show a provider selection such as OpenRouter or OpenCode Zen while the composer model was stale, blank, or inherited from another provider/thread. In one observed flow: + +- The sidebar Provider dropdown showed OpenRouter. +- The composer still showed the Zen model `big-pickle`. +- A later Codex-thread route showed the model placeholder `Model` instead of a usable model. +- Send-path testing initially verified only the outgoing `turn/start` request and did not wait for an assistant reply. + +This made provider/model state hard to trust when changing threads and switching between OpenRouter, OpenCode Zen, and Codex-started threads. + +## Root Causes + +Provider/model state had several different authorities: + +- Backend `thread/resume` can report a model for the resumed thread. +- Free-mode status reports the active free-mode provider and current provider model. +- Frontend local state stores provider-scoped model choices by thread and by provider default. +- The visible composer selection is the model actually sent in the current turn. + +The frontend needed the visible provider-scoped composer model to win for sends, but it also needed free-mode status to hydrate provider-scoped state after provider switches and startup. Without that hydration, switching from OpenRouter to OpenCode Zen could leave the composer on the `Model` placeholder until another refresh path corrected it. + +Another review finding was accepted: provider switching should not overwrite an existing per-thread/per-provider model selection with the provider's new-thread default. Existing thread/provider choices must be preserved. + +`getCurrentModelConfig()` also called `/codex-api/free-mode/status`; that request needed a timeout so a stalled status endpoint could not hang model refresh or startup. + +## Fix + +Relevant commits: + +- `dc7871a8 Send visible provider model from composer` +- `28f76372 Hydrate provider model from free-mode status` +- `6ecfed96 Preserve thread model during provider status sync` + +Implementation details: + +- `ThreadComposer.vue` includes the visible `selectedModel` in the submit payload. +- `App.vue` passes that model into `sendMessageToSelectedThread()`. +- `useDesktopState.ts` uses the selected model override for pending details and `turn/start`, so a resumed backend model cannot silently replace the visible provider model at send time. +- `loadFreeModeStatus()` now derives the active provider, previews the provider model selection, and stores `status.currentModel` into the provider-scoped new-thread context. +- If a selected thread has no model for the current provider, `loadFreeModeStatus()` seeds that thread/provider model from `status.currentModel`. +- `onProviderChange()` calls `loadFreeModeStatus()` immediately after provider write, before the broader refresh. +- `onProviderChange()` only seeds the active existing thread from the provider default when the thread lacks a provider-scoped model already. +- `getFreeModeStatus()` uses `AbortSignal.timeout(8000)` and throws on non-OK status. + +## Verification + +Unit/build: + +```bash +pnpm vitest run src/composables/useDesktopState.test.ts src/api/codexGateway.test.ts +pnpm run build:frontend +``` + +Browser fallback testing used Playwright against: + +```text +http://127.0.0.1:5174 +``` + +Browser Use was attempted first, but the in-app browser could not open localhost/127.0.0.1 in that run because navigation failed with `net::ERR_BLOCKED_BY_CLIENT`. + +Successful send-and-receive checks: + +| Case | Thread | Provider | Sent model | Received reply? | +|------|--------|----------|------------|-----------------| +| OpenRouter existing thread | `019e0aef-f2ca-7d61-8345-efd4aac9ea7b` | OpenRouter | `openrouter/free` | Yes | +| Zen provider test thread | `019e0d1c-41e0-7670-a55a-664fe46f80a8` | OpenCode Zen | `big-pickle` | Yes | +| Zen older thread | `019dc7fa-5291-7670-8b9b-d06ae0548d01` | OpenCode Zen | `big-pickle` | Yes | + +Each browser test filled the composer, clicked the send button, captured the outgoing `/codex-api/rpc` `turn/start` model, and waited for an assistant message row containing the exact marker. + +Screenshot artifacts: + +- `output/playwright/openrouter-receive-received.png` +- `output/playwright/zen-receive-primary-received.png` +- `output/playwright/zen-receive-older-received.png` + +Dark theme check: + +- Thread `019dc7fa-5291-7670-a55a-664fe46f80a8` +- Provider: `OpenCode Zen` +- Composer model: `big-pickle` +- Screenshot: `output/playwright/provider-zen-dark-model.png` + +## Operational Rule + +Provider/model browser tests must wait for a received assistant reply, not only a submitted `turn/start` request. A send-only check proves payload wiring, but it does not prove the selected provider/model can complete a user-visible turn. diff --git a/llm-wiki/wiki/concepts/opencode-zen-big-pickle.md b/llm-wiki/wiki/concepts/opencode-zen-big-pickle.md index 9ef4e7e93..fed8638bc 100644 --- a/llm-wiki/wiki/concepts/opencode-zen-big-pickle.md +++ b/llm-wiki/wiki/concepts/opencode-zen-big-pickle.md @@ -51,14 +51,39 @@ model_provider = "opencode-zen" Codex Web Local can expose OpenCode Zen through its local Responses-compatible proxy. The proxy translates between Codex-style Responses input and Zen's Chat Completions-only API. For thinking-mode models behind `big-pickle`, the proxy must preserve assistant reasoning in both directions: -- Upstream Chat `message.reasoning_content` becomes a Responses `reasoning` output item. +- Upstream Chat `message.reasoning_content` becomes a Responses `reasoning` output item with `summary_text` and `content: []`, so later Responses-backed turns do not replay non-empty reasoning content and trigger `array_above_max_length`. - Later Responses `reasoning` input becomes assistant Chat `reasoning_content`. - Reasoning that precedes function calls is attached to the assistant tool-call message. - Streaming Chat `reasoning_content` deltas are emitted as synthetic Responses reasoning output. This behavior was fixed in commit `47d52c8c` after a Docker repro using an empty `CODEX_HOME`, no login, and no Zen API key. +## Provider-Scoped Composer Model Behavior + +When Codex Web Local is running in free-mode provider workflows, the visible composer model is the model that must be sent for the next turn. Backend `thread/resume`, free-mode status, provider defaults, and per-thread provider choices can all report model state, but the send path must use the currently visible provider-scoped composer model. + +The provider/thread scoping fix landed across commits `dc7871a8`, `28f76372`, and `6ecfed96`: + +- The composer submit payload includes the visible selected model. +- The selected model override is passed through `App.vue` into `sendMessageToSelectedThread()`. +- `turn/start` uses that selected model override instead of allowing a resumed backend model from another provider to replace it. +- Free-mode status hydrates the provider-scoped model after startup and provider switches. +- Provider switching preserves existing per-thread/per-provider model selections and only seeds a thread from the provider default when no thread/provider model exists. +- Free-mode status fetches are bounded with an 8 second timeout. + +The validated provider-switch flow was: + +| Thread | Provider | Composer model | Sent model | Received reply | +|--------|----------|----------------|------------|----------------| +| `019e0aef-f2ca-7d61-8345-efd4aac9ea7b` | OpenRouter | `openrouter/free` | `openrouter/free` | Yes | +| `019e0d1c-41e0-7670-a55a-664fe46f80a8` | OpenCode Zen | `big-pickle` | `big-pickle` | Yes | +| `019dc7fa-5291-7670-8b9b-d06ae0548d01` | OpenCode Zen | `big-pickle` | `big-pickle` | Yes | + +The browser verification clicked send and waited for an assistant message row containing the exact marker, not only for the outgoing `/codex-api/rpc` `turn/start` payload. + ## Related - Source: [opencode-zen-big-pickle-codex-cli.md](../../raw/fixes/opencode-zen-big-pickle-codex-cli.md) - Source: [opencode-zen-reasoning-content-proxy.md](../../raw/fixes/opencode-zen-reasoning-content-proxy.md) +- Source: [opencode-zen-reasoning-summary-replay.md](../../raw/fixes/opencode-zen-reasoning-summary-replay.md) +- Source: [provider-scoped-model-selection-zen.md](../../raw/fixes/provider-scoped-model-selection-zen.md) - [merge-to-main-workflow.md](./merge-to-main-workflow.md) diff --git a/llm-wiki/wiki/index.md b/llm-wiki/wiki/index.md index 94b762bd3..decd17455 100644 --- a/llm-wiki/wiki/index.md +++ b/llm-wiki/wiki/index.md @@ -24,3 +24,5 @@ - [../raw/projects/codex-web-local.md](../raw/projects/codex-web-local.md): immutable source snapshot for project facts. - [../raw/fixes/opencode-zen-big-pickle-codex-cli.md](../raw/fixes/opencode-zen-big-pickle-codex-cli.md): Big Pickle + Codex CLI fix details. - [../raw/fixes/opencode-zen-reasoning-content-proxy.md](../raw/fixes/opencode-zen-reasoning-content-proxy.md): Codex Web Local Zen proxy reasoning_content round-trip fix and Docker verification. +- [../raw/fixes/opencode-zen-reasoning-summary-replay.md](../raw/fixes/opencode-zen-reasoning-summary-replay.md): Zen reasoning output summary format that keeps later Responses replay valid. +- [../raw/fixes/provider-scoped-model-selection-zen.md](../raw/fixes/provider-scoped-model-selection-zen.md): provider-scoped model selection, OpenRouter/Zen thread switching, and send-plus-receive browser verification. diff --git a/llm-wiki/wiki/log.md b/llm-wiki/wiki/log.md index 6f1bc3d43..a6f9f34c7 100644 --- a/llm-wiki/wiki/log.md +++ b/llm-wiki/wiki/log.md @@ -46,3 +46,15 @@ - Updated wiki page: `concepts/opencode-zen-big-pickle.md`. - Documents: DeepSeek thinking-mode `reasoning_content` round-trip requirement, Chat-shaped Zen proxy endpoint selection, streaming reasoning preservation, Docker validation, and the `/tmp/app.tar` restart gotcha. - Updated `index.md`. + +## [2026-05-10] ingest | provider-scoped model selection and Zen receive verification +- Added source: `raw/fixes/provider-scoped-model-selection-zen.md`. +- Updated wiki page: `concepts/opencode-zen-big-pickle.md`. +- Documents: visible composer model authority, free-mode status hydration, per-thread/per-provider preservation, bounded free-mode status fetch, and OpenRouter/Zen send-plus-receive browser verification. +- Updated `index.md`. + +## [2026-05-10] ingest | Zen reasoning summary replay fix +- Added source: `raw/fixes/opencode-zen-reasoning-summary-replay.md`. +- Updated wiki page: `concepts/opencode-zen-big-pickle.md`. +- Documents: non-empty Responses reasoning `content` caused `array_above_max_length`, and translated Zen reasoning now uses `summary_text` with `content: []`. +- Updated `index.md`. diff --git a/src/App.vue b/src/App.vue index 8106e4068..2fbd27700 100644 --- a/src/App.vue +++ b/src/App.vue @@ -207,13 +207,13 @@