Skip to content
Merged
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
74 changes: 44 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,16 @@ To delegate from another provider instead, just ask: *"Ask Claude to review this

## Context window controls

Claude Code serves different context windows depending on the exact model id it receives (e.g. bare `claude-fable-5` serves 200K, while `claude-fable-5[1m]` serves 1M). `omp-claude-bridge` turns that into a single switch.
Claude Code serves different context windows depending on the exact model id it receives (e.g. bare `claude-fable-5` serves 200K, while `claude-fable-5[1m]` serves 1M). `omp-claude-bridge` exposes both as **separate entries in the `/model` picker**, so you choose the window on demand:

Set it once in `~/.omp/agent/claude-bridge.json`:
- `claude-bridge/claude-opus-4-8` → **Opus 4.8 (1M)**
- `claude-bridge/claude-opus-4-8-200k` → **Opus 4.8 (200K)**

Switching window is just picking the other entry — no config edit, no reload. Every model appears once per window it supports, the `(1M)` / `(200K)` label is always shown, and each entry reports its true window so OMP's status bar and auto-compaction threshold stay accurate.

### Default window

The **unsuffixed** id (e.g. `claude-opus-4-8`) maps to a default window; the other window gets a `-1m` / `-200k` suffixed id. `provider.contextWindow` in `~/.omp/agent/claude-bridge.json` picks that default — it no longer hides models, it only decides which window is unsuffixed:

```json
{
Expand All @@ -97,49 +104,56 @@ Set it once in `~/.omp/agent/claude-bridge.json`:
}
```

| Mode | Behavior |
| Mode | Default (unsuffixed) window |
| ---- | -------- |
| `"auto"` *(default)* | Per-model policy based on measured SDK behavior. Respects `plan` and `longContextExtraUsage`. |
| `"1m"` | Force the 1M window. Only 1M-capable models are registered; models with no 1M runtime are hidden. |
| `"200k"` | Force the 200K window. Only 200K-capable models are registered; models with no 200K runtime are hidden. |
| `"auto"` *(default)* | Per-model measured default. Respects `plan` and `longContextExtraUsage`. |
| `"1m"` | 1M where the model has a 1M runtime, else its only window. |
| `"200k"` | 200K where the model has a 200K runtime, else its only window. |

The registered window always matches what the bridge actually requests, so OMP's status bar and auto-compaction threshold stay accurate.
Both windows stay in the picker regardless of this setting (wherever a runtime exists); it only changes which one is the plain, unsuffixed id. So `modelRoles` / `enabledModels` that reference `claude-bridge/claude-opus-4-8` keep working and follow the default.

### Per-model context window
### Windows offered per model

| Model | `auto` | `1m` | `200k` |
| ----- | :----: | :--: | :----: |
| `claude-opus-4-8` | 1M | 1M | 200K |
| `claude-opus-4-7` | 1M | 1M | — hidden |
| `claude-opus-4-6` | 200K¹ | 1M | 200K |
| `claude-fable-5` | 200K | 1M | 200K |
| `claude-sonnet-5` | 1M | 1M | 200K |
| `claude-sonnet-4-6` | 200K² | 1M | 200K |
| `claude-haiku-4-5` | 200K | — hidden | 200K |
| Model | 200K entry | 1M entry | `auto` default |
| ----- | :--------: | :------: | :------------: |
| `claude-opus-4-8` | | | 1M |
| `claude-opus-4-7` | | | 1M |
| `claude-opus-4-6` | | | 200K¹ |
| `claude-fable-5` | | | 200K |
| `claude-sonnet-5` | | | 1M |
| `claude-sonnet-4-6` | | | 200K² |
| `claude-haiku-4-5` | | — | 200K |

¹ Opus 4.6 serves 1M in `auto` when `plan: "max"` or `longContextExtraUsage: true`.
² Sonnet 4.6 serves 1M in `auto` when `longContextExtraUsage: true`.
¹ Opus 4.6's `auto` default is 1M when `plan: "max"` or `longContextExtraUsage: true`.
² Sonnet 4.6's `auto` default is 1M when `longContextExtraUsage: true`.

The suffixed alternate exists only for the window that isn't the default — e.g. under `auto` you get `claude-opus-4-8` (1M) + `claude-opus-4-8-200k`, and under `"200k"` you get `claude-opus-4-8` (200K) + `claude-opus-4-8-1m`.

> Forcing 1M is a *request*: some models may still be **served** 200K by your subscription entitlement. Set `CLAUDE_BRIDGE_DEBUG=1` to log the served window (see [Debugging](#debugging)).

> An invalid `contextWindow` value logs a warning and falls back to `"auto"`, so a typo never breaks startup.

## Models

Select any of these from `/model`:
Pick any of these from `/model` — each entry shows a `(1M)` or `(200K)` label. The exact ids below assume the default `contextWindow: "auto"`; which id is unsuffixed vs `-1m` / `-200k` follows your configured [default window](#default-window).

| Picker id | Notes |
| --------- | ----- |
| `claude-bridge/claude-fable-5` | Fast, 200K by default |
| `claude-bridge/claude-opus-4-8` | Flagship, 1M by default |
| `claude-bridge/claude-opus-4-7` | 1M by default |
| Picker id (auto) | Window |
| --------- | ------ |
| `claude-bridge/claude-fable-5` | 200K |
| `claude-bridge/claude-fable-5-1m` | 1M |
| `claude-bridge/claude-opus-4-8` | 1M |
| `claude-bridge/claude-opus-4-8-200k` | 200K |
| `claude-bridge/claude-opus-4-7` | 1M |
| `claude-bridge/claude-opus-4-6` | 200K (1M on Max / Extra Usage) |
| `claude-bridge/claude-sonnet-5` | 1M by default, supports `xhigh` |
| `claude-bridge/claude-sonnet-4-6` | 200K (1M on Extra Usage), supports `xhigh` |
| `claude-bridge/claude-haiku-4-5` | 200K, cheapest |
| `claude-bridge/claude-opus-4-6-1m` | 1M |
| `claude-bridge/claude-sonnet-5` | 1M (supports `xhigh`) |
| `claude-bridge/claude-sonnet-5-200k` | 200K |
| `claude-bridge/claude-sonnet-4-6` | 200K (supports `xhigh`) |
| `claude-bridge/claude-sonnet-4-6-1m` | 1M |
| `claude-bridge/claude-haiku-4-5` | 200K (cheapest) |

Bash commands issued by Claude Code get a 120-second default timeout (matching Claude Code's default), since OMP's bash has no timeout by default.

**1M context notes:** Opus 4.7 and Opus 4.8 get 1M in `auto` by default. Opus 4.6 needs `plan: "max"` or `longContextExtraUsage: true`; Sonnet 4.6 needs `longContextExtraUsage: true`. Or just set `contextWindow: "1m"` to force it everywhere.

## AskClaude tool

Available whenever the active provider is **not** claude-bridge. Your current model can hand work to Claude Code and wait for the result:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Task: On-demand context-window model variants (Option A)
**Date Started**: 2026-07-06
**Status**: Complete
**Agent Lead**: 🏗️/⚙️

## Plan

### Objective
Let the user choose a Claude model's context window **on demand** from OMP's native `/model` picker, by registering each model at every window it supports (1M and/or 200K) as a distinct, clearly-labeled entry. Switching window = pick the other entry. Keep `provider.contextWindow` from config as the **default** (it decides which window the plain, unsuffixed model id maps to). No new UI, no runtime command.

### Current behavior (why there's nothing to select today)
- Context window is a single **global** choice in `~/.omp/agent/claude-bridge.json` (`provider.contextWindow: auto|1m|200k`), read **once at load**; changing it needs a `/reload`. Not per-model, not on-demand.
- OMP models carry a fixed `contextWindow`; there is no native per-model window knob (unlike thinking level's `setThinkingLevel`). Varying the window therefore requires registering variant models.
- `src/models.ts` today: `applyLongContext` filters to **one** variant per model from the global mode; `claudeCodeModelId(model, settings)` maps a pi model id → Claude Code cli id using the global settings; `resolveForcedOneMRuntimeModel` / `resolveForcedTwoHundredKRuntimeModel` already hold each model's 1M/200K cli id (or `null` when unavailable).

### Approach — variant scheme (per model, in MODEL_IDS_IN_ORDER)
- **Available windows** = union of { 1M if `resolveForcedOneMRuntimeModel` ≠ null, 200K if `resolveForcedTwoHundredKRuntimeModel` ≠ null }. Independent of config, so both are pickable wherever a runtime exists.
- **Default window** = the window `resolveClaudeCodeRuntimeModel(id, settings)` returns for the current config (auto respects `plan`/`longContextExtraUsage`; `1m`/`200k` force), falling back to the model's only available window when the preferred one has no runtime.
- **Unsuffixed id** (e.g. `claude-opus-4-8`) = the default-window variant → preserves existing ids, roles, and the user's `config.yml`.
- **Each other available window** gets a suffixed id: `${baseId}-1m` / `${baseId}-200k`.
- **Display name** = `${baseName} (1M)` / `${baseName} (200K)` on every entry, for unambiguous labeling.
- **`contextWindow`** on each entry = its true window (keeps status bar + auto-compaction accurate).

Resulting picker (config = `auto`, Pro plan):

| Label | Picker id | CC cli id | Window |
| --- | --- | --- | --- |
| Opus 4.8 (1M) | `claude-opus-4-8` | `claude-opus-4-8[1m]` | 1M (default) |
| Opus 4.8 (200K) | `claude-opus-4-8-200k` | `claude-opus-4-8` | 200K |
| Opus 4.7 (1M) | `claude-opus-4-7` | `claude-opus-4-7` | 1M (only) |
| Opus 4.6 (200K) | `claude-opus-4-6` | `claude-opus-4-6` | 200K (default) |
| Opus 4.6 (1M) | `claude-opus-4-6-1m` | `claude-opus-4-6[1m]` | 1M |
| Fable 5 (200K) | `claude-fable-5` | `claude-fable-5` | 200K (default) |
| Fable 5 (1M) | `claude-fable-5-1m` | `claude-fable-5[1m]` | 1M |
| Sonnet 5 (1M) | `claude-sonnet-5` | `claude-sonnet-5[1m]` | 1M (default) |
| Sonnet 5 (200K) | `claude-sonnet-5-200k` | `claude-sonnet-5` | 200K |
| Sonnet 4.6 (200K) | `claude-sonnet-4-6` | `claude-sonnet-4-6` | 200K (default) |
| Sonnet 4.6 (1M) | `claude-sonnet-4-6-1m` | `claude-sonnet-4-6[1m]` | 1M |
| Haiku 4.5 (200K) | `claude-haiku-4-5` | `claude-haiku-4-5` | 200K (only) |

Unsuffixed ids all keep their current default-per-config behavior, so `modelRoles.default: claude-bridge/claude-opus-4-8` still resolves and still gets 1M.

### Code changes
1. **`src/models.ts`**
- Add `parseVariantId(id)` → `{ baseId, forced?: "1m" | "200k" }` (suffix-aware; base ids never collide with `-1m`/`-200k`).
- Update `claudeCodeModelId(model, settings)`: if the id is suffixed, return the forced runtime's cli id (ignore global mode); else keep the existing default-per-config path.
- Add `buildVariantModels(models, settings)` that expands each model into its window variants (default-first ordering, labels, `contextWindow`). Replace `applyLongContext`; remove it (no back-compat shim).
2. **`src/index.ts`**
- Line ~1634: register `buildVariantModels(MODELS, longContextSettings)` instead of `applyLongContext(...)`.
- The three `claudeCodeModelId` call sites (≈366 compact, ≈1251 streaming, ≈1463 AskClaude) stay unchanged — variant parsing is internal.
3. **`tests/unit-*.mjs`** — expansion (ids/windows/cli ids/labels), default selection per config (auto/1m/200k × plan/extraUsage), fallbacks (opus-4-7 under `200k`, haiku under `1m`), no duplicate windows, `claudeCodeModelId` round-trips for suffixed + unsuffixed ids.
4. **`README.md`** — update "Context window controls" + "Models": variants in the picker, config sets the default, keep the per-model table; note the entitlement caveat.

### Tasks
- [ ] models.ts: `parseVariantId` + variant-aware `claudeCodeModelId`
- [ ] models.ts: `buildVariantModels` (default-first, labels, contextWindow); remove `applyLongContext`
- [ ] index.ts: swap registration to `buildVariantModels`
- [ ] Unit tests: expansion + id mapping + config defaults + fallbacks
- [ ] README updates
- [ ] `bun run typecheck` + `bun run test` green; manual `/model` check in OMP after `/reload`

### Risks / notes
- The default window has no suffixed alias (it's the unsuffixed id); a role hardcoded to that suffix breaks only when config flips that window to default. Common case (`auto`) is stable. Documented.
- Entitlement: forced-1M variants may be **served** at 200K by the subscription (issue #18); `logServedContextWindow` already surfaces the gap. The picker offers the request; runtime may differ.
- Picker grows 7 → ~12 entries; grouped default-first, acceptable.
- `resolveModel` partial-match (AskClaude short names) unchanged: `"opus"` → first opus = default variant. Bonus: AskClaude can target `-200k` / `-1m` explicitly.
- Single `registerProvider` retained → subagent re-registration guard unaffected.
- User `config.yml` (`modelRoles.default`, `enabledModels: claude-bridge/*`) keeps working: unsuffixed id preserved, wildcard enables all variants.

### Out of scope (MVP)
- `/ctx` runtime command (Option B).
- Removing the config knob (kept as default, per decision).
- Per-window cost/thinking differences (none exist today).

## Implementation Log

### 2026-07-06 — Implemented (Option A), all checks green
- **`src/models.ts`**: added exported `parseVariantId(id)` splitting `-1m`/`-200k` suffixes; made `claudeCodeModelId` variant-aware (suffixed id → forced runtime, ignores config; unsuffixed → config default). Replaced `applyLongContext` with `buildVariantModels` + a `variantName` helper — labels each entry `(1M)`/`(200K)`, sets the true `contextWindow`, orders default-first, offers both windows wherever a runtime exists, and makes the unsuffixed id the config default (falling back to the sole available window).
- **`src/index.ts`**: import + registration now call `buildVariantModels(MODELS, longContextSettings)`. The three `claudeCodeModelId` call sites (compact ~366, streaming ~1251, AskClaude ~1463) unchanged — parsing is internal. `applyLongContext` fully removed (grep-confirmed no remaining refs).
- **`tests/unit-context-window.mjs`**: rewritten for variants — 10 tests covering auto/Pro expansion (12 entries; ids/windows/labels), default-first ordering, auto/Max default flip (Opus 4.6), `200k` & `1m` config defaults + fallbacks (Opus 4.7, Haiku), the no-duplicate-window invariant, `parseVariantId`, and `claudeCodeModelId` round-trips (unsuffixed follows config, suffixed forces window, throws for impossible combos).
- **`README.md`**: "Context window controls" + "Models" rewritten — variants in `/model`, `provider.contextWindow` now sets the *default* (no longer hides models), added "Windows offered per model" table + entitlement caveat, updated the picker-id table.
- **Verification**: `bun run typecheck` clean; `bun run test` → 10/10 pass.
- **Deployment**: the plugin is symlinked into OMP (`~/.omp/plugins/node_modules/omp-claude-bridge` → this repo), so the change goes live after a `/reload` in the OMP TUI.

### Notes / follow-ups
- Not done (out of scope, no explicit ask): version bump + CHANGELOG entry (still `0.7.0`). Adding variants changes the registered model set, so consider `0.8.0` before publishing.
- Manual `/model` visual check must be done in the running OMP after `/reload` (can't drive the TUI from here).
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { appendFileSync, mkdirSync, realpathSync, statSync } from "fs";
import { homedir } from "os";
import { dirname, join } from "path";
import { PROVIDER_ID, messageContentToText, convertPiMessages } from "./convert.js";
import { applyLongContext, buildModels, claudeCodeModelId, type ContextWindowMode, type LongContextSettings, resolveModel as _resolveModel } from "./models.js";
import { buildVariantModels, buildModels, claudeCodeModelId, type ContextWindowMode, type LongContextSettings, resolveModel as _resolveModel } from "./models.js";
import { MCP_SERVER_NAME, MCP_TOOL_PREFIX, extractSkillsBlock } from "./skills.js";
import { verifyWrittenSession as _verifyWrittenSession } from "./session-verify.js";
import { extractAllToolResults as _extractAllToolResults, type McpResult } from "./extract-tool-results.js";
Expand Down Expand Up @@ -1631,7 +1631,7 @@ export default function (pi: ExtensionAPI) {
longContextExtraUsage: providerSettings.longContextExtraUsage ?? false,
contextWindow,
};
const registeredModels = applyLongContext(MODELS, longContextSettings);
const registeredModels = buildVariantModels(MODELS, longContextSettings);

// Reset shared session on pi session lifecycle events
const clearSession = (event: string) => {
Expand Down
Loading
Loading