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
1 change: 1 addition & 0 deletions client/src/components/cos/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export function pinnedPrCompletion(metadata) {
// Copy only — the ROSTER is `REVIEWER_VALUES` in `client/src/lib/reviewerPins.js`,
// which the server suite pins against the server's own enum.
const REVIEWER_COPY = {
pi: { label: 'Pi', description: 'Pi Coding Agent CLI reviews the supplied diff without tools' },
copilot: { label: 'Copilot', description: 'GitHub Copilot (GitHub-only)' },
claude: { label: 'Claude', description: 'Claude CLI reviews the PR diff (optional model on Models → Code Reviewers; supports an Ollama-backed Claude for local-only setups)' },
antigravity: { label: 'Antigravity', description: 'Antigravity CLI (agy) reviews the PR diff' },
Expand Down
6 changes: 5 additions & 1 deletion client/src/hooks/useReviewerModelOptions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import * as api from '../services/api';
import { filterSelectableModels, selectableModelsForProvider, isAntigravityProvider, isCursorProvider, isGrokBuildCli, isKimiProvider, antigravityModelEffortLevels } from '../utils/providers';
import { filterSelectableModels, selectableModelsForProvider, commandBasename, isAntigravityProvider, isCursorProvider, isGrokBuildCli, isKimiProvider, antigravityModelEffortLevels } from '../utils/providers';
import { MODEL_SELECTABLE_REVIEWERS } from '../components/cos/constants';
import { reviewerEffortLevels, normalizeReviewerSlug } from '../lib/reviewerPins';
import { LOCAL_LLM_BACKENDS } from '../lib/localLlmBackends';
Expand Down Expand Up @@ -53,6 +53,7 @@ const REVIEWER_PROVIDER_MATCHERS = Object.freeze({
// default — the broad predicate follows it for an install that only kept the TUI.
grok: [(p) => p.id === 'grok-cli', isGrokBuildCli],
cursor: [(p) => p.id === 'cursor-cli', isCursorProvider],
pi: [(p) => p.id === 'pi-cli', (p) => ['cli', 'tui'].includes(p.type) && commandBasename(p.command) === 'pi'],
kimi: [(p) => p.id === 'kimi-cli', isKimiProvider],
opencode: [(p) => p.id === 'opencode-zen-cli', (p) => p.id === 'opencode-zen-tui'],
mtplx: [(p) => p.id === 'mtplx'],
Expand Down Expand Up @@ -207,6 +208,7 @@ export default function useReviewerModelOptions() {
// regardless because grok, like every CLI reviewer, is free-text.
grok: providerTiers('grok'),
cursor: providerTiers('cursor'),
pi: providerTiers('pi'),
// Legitimately empty, for grok's documented reason: the shipped kimi
// provider carries only the configured-default sentinel, which
// `filterSelectableModels` strips. Free-text keeps the cell usable.
Expand All @@ -228,6 +230,8 @@ export default function useReviewerModelOptions() {
antigravity: providerDefault('antigravity'),
grok: providerDefault('grok'),
cursor: providerDefault('cursor'),
pi: null, // A bare reviewer uses Pi's own configured default.

kimi: providerDefault('kimi'),
// Deliberately null even though the Zen records carry one: the reviewer
// spawns a BARE `opencode`, which falls back to whatever the user's own
Expand Down
2 changes: 2 additions & 0 deletions client/src/hooks/useReviewerModelOptions.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const providers = [
// shown DEFAULT, since the reviewer is spawned non-interactively.
{ id: 'grok-tui', type: 'tui', command: 'grok', models: ['tui-only-id'] },
{ id: 'grok-cli', type: 'cli', command: 'grok', models: ['grok-configured-default', 'grok-code-fast-1'] },
{ id: 'pi-cli', type: 'cli', command: 'pi', models: ['example/model-a'], defaultModel: 'example/model-a' },
{ id: 'cursor-cli', type: 'cli', command: 'cursor-agent', models: ['auto', 'gpt-5'] },
{ id: 'mtplx', type: 'api', models: ['mtplx-qwen38-27b-optimized-speed'], defaultModel: 'mtplx-qwen38-27b-optimized-speed' },
// The seeded OpenCode Zen wrappers, whose namespaced ids the Harnesses page
Expand All @@ -51,6 +52,7 @@ describe('useReviewerModelOptions', () => {
it('offers options for every model-selectable reviewer', async () => {
const { result } = renderHook(() => useReviewerModelOptions());
await waitFor(() => expect(result.current.loaded).toBe(true));
expect(result.current.optionsByReviewer.pi).toEqual(['example/model-a']);
for (const reviewer of MODEL_SELECTABLE_REVIEWERS) {
expect(Array.isArray(result.current.optionsByReviewer[reviewer])).toBe(true);
}
Expand Down
5 changes: 3 additions & 2 deletions client/src/lib/reviewerPins.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
// EFFORT_SELECTABLE_REVIEWERS below: `grok`/`opencode`/`kimi` take a model but no
// pickable effort, and Cursor takes both while carrying its effort INSIDE the
// model id rather than as a separate flag.
export const MODEL_CAPABLE_CLI_REVIEWERS = ['codex', 'claude', 'antigravity', 'grok', 'cursor', 'opencode', 'kimi'];
export const MODEL_CAPABLE_CLI_REVIEWERS = ['codex', 'claude', 'antigravity', 'grok', 'cursor', 'pi', 'opencode', 'kimi'];

// The local-LLM backends, which take both a model and an effort.
export const LOCAL_LLM_REVIEWERS = ['lmstudio', 'ollama', 'mtplx'];
Expand Down Expand Up @@ -71,6 +71,7 @@ export const REVIEWER_EFFORT_LEVELS = Object.freeze({
codex: CODEX_EFFORT_LEVELS,
antigravity: ANTIGRAVITY_EFFORT_LEVELS,
cursor: CURSOR_EFFORT_LEVELS,
pi: ['low', 'medium', 'high', 'xhigh', 'max'],
grok: GROK_EFFORT_LEVELS,
lmstudio: LOCAL_LLM_EFFORT_LEVELS,
ollama: LOCAL_LLM_EFFORT_LEVELS,
Expand Down Expand Up @@ -132,7 +133,7 @@ export const sanitizeReviewerModelInput = (raw) =>
// never offer a slug the server's enum would reject. Mirror of REVIEWER_VALUES —
// a reviewer listed here but unknown to the server leaves the user configuring a
// review-loop reviewer that never runs; the reverse hides one their install has.
export const REVIEWER_VALUES = ['copilot', 'claude', 'antigravity', 'codex', 'grok', 'cursor', 'opencode', 'kimi', 'lmstudio', 'ollama', 'mtplx'];
export const REVIEWER_VALUES = ['copilot', 'claude', 'antigravity', 'codex', 'grok', 'cursor', 'pi', 'opencode', 'kimi', 'lmstudio', 'ollama', 'mtplx'];

// The reviewer a task falls back to when none is configured. Mirror of
// DEFAULT_REVIEWER / DEFAULT_REVIEWERS.
Expand Down
1 change: 1 addition & 0 deletions client/src/utils/providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,7 @@ export const effortLevelsForProvider = (provider, model = null) => {
return perModel.length ? perModel : null;
}
if (isCursorProvider(provider)) return CURSOR_EFFORT_LEVELS;
if (commandBasename(provider.command) === 'pi') return ['low', 'medium', 'high', 'xhigh', 'max'];
if (isGrokProvider(provider)) return GROK_EFFORT_LEVELS;
const id = String(provider.id || '').toLowerCase();
if (id.startsWith('claude-code') || commandBasename(provider.command) === 'claude') return CLAUDE_EFFORT_LEVELS;
Expand Down
2 changes: 1 addition & 1 deletion client/src/utils/providers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1226,7 +1226,7 @@ describe('supportsModelRefresh', () => {
// provider — nothing here can enumerate that, and Models → Harnesses
// ("Refresh models") is where their catalog comes from instead.
'opencode-zen',
'openrouter', 'orcarouter', 'slotstream',
'openrouter', 'orcarouter', 'pi-cli', 'pi-tui', 'slotstream',
]);
});
});
Expand Down
31 changes: 31 additions & 0 deletions data.reference/providers.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
{
"activeProvider": "claude-code-tui",
"providers": {
"pi-cli": {
"id": "pi-cli",
"name": "Pi Coding Agent CLI",
"type": "cli",
"command": "pi",
"args": [
"--print",
"--approve"
],
"models": [],
"defaultModel": null,
"timeout": 600000,
"enabled": false,
"envVars": {},
"secretEnvVars": []
},
"pi-tui": {
"id": "pi-tui",
"name": "Pi Coding Agent TUI",
"type": "tui",
"command": "pi",
"args": [
"--approve"
],
"models": [],
"defaultModel": null,
"timeout": 600000,
"enabled": false,
"envVars": {},
"secretEnvVars": []
},
"claude-code": {
"id": "claude-code",
"name": "Claude Code CLI",
Expand Down
39 changes: 39 additions & 0 deletions scripts/migrations/354-pi-provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** Add disabled Pi presets without changing configured providers or starting work. */
import { makeProviderSeedMigration } from './_lib.js';

export default makeProviderSeedMigration({
label: 'Pi Coding Agent',
defs: [
{
"id": "pi-cli",
"name": "Pi Coding Agent CLI",
"type": "cli",
"command": "pi",
"args": [
"--print",
"--approve"
],
"models": [],
"defaultModel": null,
"timeout": 600000,
"enabled": false,
"envVars": {},
"secretEnvVars": []
},
{
"id": "pi-tui",
"name": "Pi Coding Agent TUI",
"type": "tui",
"command": "pi",
"args": [
"--approve"
],
"models": [],
"defaultModel": null,
"timeout": 600000,
"enabled": false,
"envVars": {},
"secretEnvVars": []
}
],
});
22 changes: 22 additions & 0 deletions scripts/migrations/354-pi-provider.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { it, expect } from 'vitest';
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import migration from './354-pi-provider.js';

it('adds disabled Pi presets idempotently without replacing local configuration', async () => {
const rootDir = await mkdtemp(join(tmpdir(), 'pi-seed-'));
await mkdir(join(rootDir, 'data'));
const path = join(rootDir, 'data/providers.json');
const custom = { id: 'pi-cli', command: '/opt/bin/pi', enabled: true };
await writeFile(path, JSON.stringify({ activeProvider: 'pi-cli', providers: { 'pi-cli': custom } }));
await migration.up({ rootDir });
const once = await readFile(path, 'utf8');
await migration.up({ rootDir });
expect(await readFile(path, 'utf8')).toBe(once);
const state = JSON.parse(once);
expect(state.activeProvider).toBe('pi-cli');
expect(state.providers['pi-cli']).toEqual(custom);
expect(state.providers['pi-tui']).toMatchObject({ enabled: false, models: [], defaultModel: null, command: 'pi' });
await rm(rootDir, { recursive: true });
});
1 change: 1 addition & 0 deletions server/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ The barrel `server/lib/index.js` is a machine-checkable enumeration of every pub
| `videoDurationProfiles.js` | Pure pinned duration/frame contracts shared by model-registry upgrades and migrations. LTX-2.5 A2V follows the full uploaded audio, rounds up to its 8n+1 temporal grid, and tops out at 1017 frames under the API's single-pass boundary. |
| `videoReferenceModes.js` | The i2v reference-mode contract (#4874) — what a supplied conditioning image PROMISES. `I2V_REFERENCE_MODES` (`anchor` \| `inspire`) + `I2V_REFERENCE_MODE_OPTIONS` (the label + the promise sentence the UI prints), `I2V_REFERENCE_MODE_RUNTIMES` (only `ltx25` can honor `inspire` — it needs per-image conditioning strength), `INSPIRE_DEFAULT_IMAGE_STRENGTH`, plus `normalizeI2vReferenceMode` / `isDefaultI2vReferenceMode` / `isKnownI2vReferenceMode` / `runtimeSupportsI2vReferenceMode` / `i2vReferenceModeLabel` / `resolveI2vReferenceStrength` and the one rule `i2vReferenceModeViolation({ model, mode, referenceMode, hasFirstImage })` → `{ code, message }` or null. Pure (no `ServerError`) because it is MIRRORED to `client/src/lib/videoReferenceModes.js`; `videoGen/modeContract.js#videoReferenceModeError` wraps it for the route + render boundaries. |
| `videoTextEncoders.js` | Swappable prompt conditioners for local video runtimes. MiniMax H3 reads the *unnormalized* hidden state after Qwen3-VL language layer 49 (layers 50-63, the final norm and `lm_head` are never evaluated), so any checkpoint carrying the same embedding + layers 0-49 + vision tower is a drop-in conditioner — swapping it changes how the model reads a prompt without touching the diffusion weights. `TEXT_ENCODERS_BY_RUNTIME` declares the shipped options per runtime (pinned repo/revision plus an explicit `files` LIST — one repackaged safetensors, or just the shards of an upstream checkpoint that carry parameters the loader actually builds; in code rather than the media-models registry so a stale `data/media-models.json` can't name a file the runner can't map); `videoTextEncoderOptions(model)` returns the TRUE list stock-first (it deliberately does NOT collapse a one-entry runtime to `[]` — that is a presentation rule, and folding it in here would change what the server believes a model supports and empty the "offers …" list in the error; `TextEncoderPicker` owns the hide-when-there-is-no-real-choice check), `isStockTextEncoder(id)` makes absence and the `stock` sentinel the same request, `resolveVideoTextEncoder(model, id)` returns `null` for the stock choice or throws `VIDEO_TEXT_ENCODER_UNSUPPORTED` (with the non-throwing `supportsVideoTextEncoder` + `videoTextEncoderUnsupportedError` split out so the request path can reject before staging uploads), and `downloadableVideoTextEncoders()` (deduped by id — the table is keyed by RUNTIME, so one conditioner can be offered by two) / `downloadableVideoTextEncoder(id)` feed the `/api/video-gen/text-encoders/:id/(download\|repair)` lane. Two loader-mechanics fields exist because a ComfyUI-packaged conditioner is namespaced differently from the HF checkpoint the MLX port matches: `keyPrefixMap` (`model.` → `model.language_model.`, `visual.` → `model.visual.`) is applied to every checkpoint key by `scripts/generate_minimax_h3.py` BEFORE the pinned loader sees it — no fork of the pinned runtime — and `finalNormKey` names where the runner synthesizes a ones-filled `norm.weight` for a checkpoint published without one (correct upstream, since H3 reads the state *before* the norm, but the pinned loader refuses to load with any parameter missing). Both are absent for an UPSTREAM Qwen3-VL-32B checkpoint, which already uses the loader namespace and ships its own norm. A candidate must BE Qwen3-VL-32B (the shim reuses upstream's config/tokenizer/processor) — a different Qwen generation is not a substitute however close its conditioning width looks; see docs/features/video-text-encoders.md. `publicTextEncoderOption(entry)` is the client projection and deliberately drops both, so the UI can't reimplement the remap. The `ltx25` table (#4320) uses a third mechanic, `configOverrides`, because an LTX-2.5 pack's OWN Gemma 4 tower wins over `--gemma` inside the pinned fork: the substitution is a standalone shim directory whose generated `config.json` is the substitute's own with these keys merged over it (only ever the `model_type` label a unified checkpoint gets wrong — never `text_config`/`quantization`), and a candidate must BE Gemma 4 12B at 48 layers / hidden 3840 / vocab 262144 / `k_eq_v`. `verified` gates a substitute out of BOTH lanes (picker AND download) until it has been A/B-rendered against its runtime's stock conditioner — required on every non-built-in entry and fail-closed on absence, so a new entry is unreachable until someone states a verdict; both ltx25 substitutes are `verified: false` today. `declaredVideoTextEncoders()` is the UNFILTERED table for shape/invariant checks only — never the render or download path, and `videoTextEncoderRuntimes()` enumerates the table's runtime keys so parity/shape tests cover every runtime rather than the one that happened to exist when they were written. |
| `pi.js` | Pi command identity, headless/TUI arguments, and positional prompt delivery. |
| `providerModels.js` | Provider model resolution sentinels + helpers (`CODEX_CONFIGURED_DEFAULT` / `ANTIGRAVITY_CONFIGURED_DEFAULT` / `GROK_CONFIGURED_DEFAULT` / `KIMI_CONFIGURED_DEFAULT`, `resolveCliModel`, `filterSelectableModels`, Bedrock/OpenCode model mappers, `localRuntimeNamespace(provider)` — the OpenCode namespace only when it names a LOCAL daemon, i.e. the composed "namespace and not a hosted gateway" test that `cliChildEnv.js`, `localProviderRuntime.js` and `providerVendors.js` all key on, `CODEX_OSS_LOCAL_PROVIDERS` / `CODEX_OSS_MIN_VERSION` / `codexOssLocalProvider` / `codexUnsupportedLocalRuntime` — the codex half of that same axis: which local runtimes Codex's `--local-provider` can serve, and which marked runtime it cannot, kept here beside the namespace they wrap so `providerPrerequisites.js` classifies a codex record without importing `codex.js` (the argv emitter, `buildCodexOssArgs`, stays there), `OPENCODE_PUBLIC_REVIEW_AGENT` — the read-only OpenCode agent a no-tool public-review stage runs as, kept in this leaf because `providerVendors.js` must not import `opencodeConfig.js`, `parseOpencodeConfigContent` — the shared "is this stored OPENCODE_CONFIG_CONTENT usable?" read — plus `opencodeConfigIsLocalOnly` / `opencodeProviderIsLocalOnly`, the ONE locality rule `providerVendors.js` (gate eligibility) and `cliChildEnv.js` (public-review env allowlist) must not disagree about: if eligibility says yes where the allowlist strips the config, the stage spawns against the user's own ~/.config/opencode with tools intact while still reporting an enforced tool-free gate, `normalizeClaudeModelId` / `resolveClaudeCliModel` — the Claude-argv chokepoint that rewrites a dotted first-party version (`claude-fable-5.1`) to the dashed id Claude Code actually serves before the Bedrock mapping runs, model-flag scan helpers incl. `stripBrokenModelFlags`, `isCodexProvider`, `isKimiProvider`, `isAntigravityProvider`, `isCursorProvider`) plus reasoning-effort helpers for the claude/codex/agy/cursor CLIs (`CLAUDE_EFFORT_LEVELS` / `CODEX_EFFORT_LEVELS` / `ANTIGRAVITY_EFFORT_LEVELS` / `CURSOR_EFFORT_LEVELS` / `EFFORT_LEVELS`, `effortLevelsForProvider`, `resolveCliEffort` — clamps an out-of-range effort to the nearest level the target CLI accepts rather than dropping it, so a value saved against a wider ladder survives a provider switch — `hasEffortFlag`, `buildEffortArgs` — the one emitter of `--effort <level>` / `-c model_reasoning_effort=<level>`, and deliberately silent for cursor — and `foldCursorEffortIntoModel`, which carries a cursor level inside `--model` as Cursor’s own variant syntax (`gpt-5[effort=max]`) because `cursor-agent` has no `--effort` flag) plus codex startup-arg helpers (`CODEX_EFFORT_KEY`, `CODEX_UPDATE_CHECK_KEY`, `hasCodexUpdateCheckConfig`, `buildCodexStartupArgs` — the one emitter of `-c check_for_update_on_startup=false`, spread by every codex spawn builder to disable the blocking startup update modal) plus `PORTOS_CLI_CONFIG_KEYS` / `isPortosSuppliedConfigKey` — the exhaustive list of `-c <key>=<value>` config keys PortOS injects, read by the `cli-config-invalid` error analyzer to tell a rejected PortOS override apart from a bad line in the user's own CLI config file. |
| `providerVendors.js` | `PROVIDER_VENDORS` — one row per coding-agent CLI/TUI vendor (claude/codex/antigravity/opencode/grok/kimi/cursor, plus a deliberately-incomplete legacy `gemini-cli` row), consumed by every dispatch site that used to hand-roll its own vendor if-chain across ~8 branches in 5 files (#3618): `applyCommandDefaults`/`prepareCliPrompt` (re-exported from `tuiHandshake.js`/`cliProviderArgs.js`), `buildVendorCliArgs`/`buildVendorSpawnConfig` (consumed by `cliProviderArgs.js#buildCliArgs` / `agentCliSpawning.js#buildCliSpawnConfig`), `inferTuiCommand` (re-exported from `tuiHandshake.js`), and `injectTuiModelAndEffort` — the shared antigravity-validates-the-pair-vs-everyone-else `--model`/`--effort` injection used by both `tuiHandshake.js#buildTuiInvocation` and `agentTuiSpawning.js#buildTuiSpawnConfig`, replacing a second copy of that split that had already drifted once before this file existed. Doesn't rewrite any vendor's argv-building logic — that stays in `antigravity.js`/`grok.js`/`kimi.js`/`cursor.js`/`codex.js`. Dependency-light on purpose, mirroring those files. |
| `modelCapabilityTests.js` | Catalog + scoring for the CAPABILITY tests on `/models/performance` (run by `services/modelCapabilityTests.js`): `CAPABILITY_TESTS` (sandbox repair / image analysis / story outline / fiction scene / rhetoric evaluator, each gated on the capability badges the install catalog already shows), `applicabilityFor` + `applicableTests` (`applicable` / `not-applicable` / `unknown` — an UNCLAIMED capability is never a failure, and `null` capabilities mean the runtime reported none, which is distinct from `[]`), `scoreKeywords` + `VISION_FIXTURE_KEYWORDS` (required vs bonus terms, word-boundary matched with a negation guard so "no dog" doesn't score a dog), `scoreStoryBeats` + `HEROS_JOURNEY_BEATS` (coverage AND ordering, judged only over the beats present), `scoreSandboxRepair` (verdict from observed disk facts — editing the test instead of the module fails outright), `formatAgentEvent` (one agent stream frame → a transcript line), `rollUpVerdict`, and the verbatim `CAPABILITY_TEST_PROMPTS` / `SANDBOX_TASK_PROMPT` the consent gate shows. Pure, so any stored transcript can be re-scored with no provider call. |
Expand Down
Loading