Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9836ad0
Use ComposerDropdown for provider selector
friuns May 9, 2026
1e98502
Document app interaction component rules
friuns May 9, 2026
5d623d2
Use ComposerDropdown for remaining app selects
friuns May 9, 2026
3f7782e
Configure app-server with selected web port
friuns May 9, 2026
e81b072
Prefer configured model after provider switch
friuns May 9, 2026
b5e5f82
Reset new thread model on provider switch
friuns May 9, 2026
2493c2e
Scope model selections by provider
friuns May 9, 2026
5f109da
Fix Qodo review findings
friuns May 9, 2026
8be5068
Load provider models during background refresh
friuns May 9, 2026
67721b5
Preserve thread provider switching
friuns May 9, 2026
cdf4a08
Keep Zen model when selecting threads
friuns May 9, 2026
94db5ff
Fix provider model dropdown switching
friuns May 9, 2026
e33c850
Preview provider model during switch
friuns May 9, 2026
f939bcb
Limit Zen dropdown to free models
friuns May 9, 2026
5b14251
Add provider switching test checklist
friuns May 9, 2026
7633fbb
Refresh provider state on startup and switch
friuns May 9, 2026
1ea6ef9
Allow custom gpt-prefixed provider models
friuns May 9, 2026
cdeb442
Remove legacy model selection fallbacks
friuns May 9, 2026
31395bf
Keep provider model when switching chats
friuns May 9, 2026
54671ab
Normalize Codex provider model context
friuns May 9, 2026
7e0475a
Show provider model in composer
friuns May 9, 2026
77d3cc8
Respect valid resumed thread models
friuns May 9, 2026
0c8be16
Fix Qodo provider review issues
friuns May 9, 2026
dc7871a
Send visible provider model from composer
friuns May 9, 2026
28f7637
Hydrate provider model from free-mode status
friuns May 9, 2026
6ecfed9
Preserve thread model during provider status sync
friuns May 9, 2026
6491cb3
Update wiki for provider model verification
friuns May 10, 2026
5cbe96c
Keep Zen reasoning replay Responses-safe
friuns May 10, 2026
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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<select>` for app UI. Use `ComposerDropdown` by default, or another existing app dropdown component when it is already the established pattern for that surface.
- Never use browser-native `prompt()` for app UI. Use an existing modal, inline editor, drawer, or app-styled form flow instead.
- When replacing legacy `<select>` 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.
Expand Down
36 changes: 36 additions & 0 deletions llm-wiki/raw/fixes/opencode-zen-reasoning-summary-replay.md
Original file line number Diff line number Diff line change
@@ -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`
92 changes: 92 additions & 0 deletions llm-wiki/raw/fixes/provider-scoped-model-selection-zen.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 26 additions & 1 deletion llm-wiki/wiki/concepts/opencode-zen-big-pickle.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 2 additions & 0 deletions llm-wiki/wiki/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
12 changes: 12 additions & 0 deletions llm-wiki/wiki/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Loading