Skip to content
This repository was archived by the owner on Jun 9, 2026. It is now read-only.
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
24 changes: 9 additions & 15 deletions PAI-Install/cli/quick-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import { join } from "node:path";
import { homedir } from "node:os";
import type { InstallState } from "../engine/types";
import { createFreshState } from "../engine/state";
import { stepPrerequisites, stepBuildOpenCode, stepProviderConfig, ZEN_FREE_MODELS, stepIdentity, stepVoice, stepInstallPAI } from "../engine/steps-fresh";
import { stepPrerequisites, stepBuildOpenCode, stepProviderConfig, stepIdentity, stepVoice, stepInstallPAI } from "../engine/steps-fresh";
import { PROVIDER_MODELS } from "../engine/provider-models";
import type { ProviderName } from "../engine/provider-models";
import { stepDetectMigration, stepCreateBackup, stepMigrate, stepBinaryUpdate, stepMigrationDone } from "../engine/steps-migrate";
import { stepDetectUpdate, stepApplyUpdate, stepUpdateDone } from "../engine/steps-update";

Expand Down Expand Up @@ -153,25 +155,17 @@ async function runFreshInstall(): Promise<void> {

// Step 4: Provider Config
onProgress(75, "Configuring provider...");
const validProviders = Object.keys(PROVIDER_MODELS) as ProviderName[];
const preset = values.preset || "zen";

// Type guard for valid presets
const validPresets = ["zen", "quick", "standard", "advanced", "anthropic", "openrouter", "openai"];
const validatedPreset = validPresets.includes(preset) ? preset : "zen";

const models = validatedPreset === "zen" ? ZEN_FREE_MODELS : {
quick: "claude-haiku-3.5",
standard: "claude-sonnet-4.6",
advanced: "claude-opus-4.6",
};

const provider: ProviderName = validProviders.includes(preset as ProviderName)
? (preset as ProviderName)
: "zen";

await stepProviderConfig(
state,
{
provider: validatedPreset,
provider,
apiKey: values["api-key"] || "",
modelTier: "standard",
models,
},
onProgress
);
Expand Down
66 changes: 66 additions & 0 deletions PAI-Install/engine/provider-models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* PAI-OpenCode Installer — Provider Model Maps
*
* Defines quick/standard/advanced model strings for each supported provider.
* The installer substitutes these into the opencode.json template at install time.
*
* To add a new provider: add an entry below and handle it in steps-fresh.ts.
*/

export type ProviderName = "anthropic" | "zen" | "openrouter" | "openai";

export interface ModelTierMap {
quick: string;
standard: string;
advanced: string;
}

/**
* Model strings per provider, formatted as "provider/model-name" ready for
* insertion into opencode.json agent entries.
*/
export const PROVIDER_MODELS: Record<ProviderName, ModelTierMap> = {
anthropic: {
quick: "anthropic/claude-haiku-4-5",
standard: "anthropic/claude-sonnet-4-5",
advanced: "anthropic/claude-opus-4-6",
},
zen: {
// OpenCode Zen — cost-optimised tiers (IDs verified against opencode.ai/docs/zen/)
quick: "zen/minimax-m2.5-free", // FREE
standard: "zen/gpt-5.1-codex-mini", // $0.25/M in+out
advanced: "zen/claude-3-5-haiku", // $0.80/M — catalog ID for Claude Haiku 3.5
},
openrouter: {
quick: "openrouter/google/gemini-flash-1.5",
standard: "openrouter/anthropic/claude-4.5-sonnet",
advanced: "openrouter/anthropic/claude-opus-4-6",
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
openai: {
quick: "openai/gpt-4o-mini",
standard: "openai/gpt-4o",
advanced: "openai/gpt-5",
},
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Human-readable labels shown in the installer wizard.
*/
export const PROVIDER_LABELS: Record<ProviderName, { label: string; description: string }> = {
anthropic: {
label: "Anthropic (Claude)",
description: "Premium quality — requires Anthropic API key",
},
zen: {
label: "OpenCode Zen (recommended)",
description: "Free tier available — 60× cost optimisation vs direct Anthropic",
},
openrouter: {
label: "OpenRouter",
description: "Multi-provider flexibility — one API key for many models",
},
openai: {
label: "OpenAI",
description: "GPT-4o and GPT-5 — requires OpenAI API key",
},
};
186 changes: 99 additions & 87 deletions PAI-Install/engine/steps-fresh.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
#!/usr/bin/env bun
/**
* PAI-OpenCode Installer — Fresh Install Steps
*
*
* 7-step fresh installation flow with OpenCode-Zen as default provider.
*/

import type { InstallState } from "./types";
import { buildOpenCodeBinary } from "./build-opencode";
import type { BuildResult } from "./build-opencode";
import { existsSync, mkdirSync, writeFileSync, chmodSync, copyFileSync, symlinkSync, unlinkSync, lstatSync, realpathSync } from "node:fs";
import type { InstallState } from "./types.ts";
import { buildOpenCodeBinary } from "./build-opencode.ts";
import type { BuildResult } from "./build-opencode.ts";
import { PROVIDER_MODELS, PROVIDER_LABELS } from "./provider-models.ts";
import type { ProviderName } from "./provider-models.ts";
import { existsSync, mkdirSync, writeFileSync, chmodSync, symlinkSync, unlinkSync, lstatSync, realpathSync } from "node:fs";
import { join, resolve } from "node:path";
import { homedir } from "node:os";

Expand Down Expand Up @@ -110,54 +112,32 @@ export async function stepBuildOpenCode(
// ═══════════════════════════════════════════════════════════

export interface ProviderConfig {
provider: "zen" | "anthropic" | "openrouter" | "openai";
provider: ProviderName;
apiKey: string;
modelTier: "quick" | "standard" | "advanced";
models: {
quick: string;
standard: string;
advanced: string;
};
}

export const ZEN_FREE_MODELS = {
quick: "minimax-m2.5-free", // FREE
standard: "gpt-5.1-codex-mini", // $0.25/M
advanced: "claude-haiku-3.5", // $0.80/M
};

export const ANTHROPIC_MODELS = {
quick: "claude-haiku-3.5",
standard: "claude-sonnet-4.6",
advanced: "claude-opus-4.6",
};

export const OPENROUTER_MODELS = {
quick: "google/gemini-flash-1.5",
standard: "anthropic/claude-3.5-sonnet",
advanced: "anthropic/claude-3-opus",
};

export const OPENAI_MODELS = {
quick: "gpt-4o-mini",
standard: "gpt-4o",
advanced: "gpt-5",
};
// Re-export for consumers that imported these from this module
export { PROVIDER_MODELS, PROVIDER_LABELS } from "./provider-models.ts";

// Legacy aliases kept for CLI quick-install.ts compatibility.
// Spread into new objects so mutations by consumers cannot corrupt PROVIDER_MODELS.
export const ZEN_FREE_MODELS = { ...PROVIDER_MODELS.zen };
export const ANTHROPIC_MODELS = { ...PROVIDER_MODELS.anthropic };
export const OPENROUTER_MODELS = { ...PROVIDER_MODELS.openrouter };
export const OPENAI_MODELS = { ...PROVIDER_MODELS.openai };

export async function stepProviderConfig(
state: InstallState,
config: ProviderConfig,
onProgress: (percent: number, message: string) => void
): Promise<void> {
onProgress(75, "Configuring AI provider...");
// Save provider settings

// Save provider + key; model strings are resolved from PROVIDER_MODELS at write time
state.collected.provider = config.provider;
state.collected.apiKey = config.apiKey;
state.collected.modelTier = config.modelTier;
state.collected.models = config.models;

// API key will be saved to .env by config generation step

// API key will be saved to .env by the install step
}

// ═══════════════════════════════════════════════════════════
Expand Down Expand Up @@ -245,8 +225,7 @@ export async function stepInstallPAI(
default: state.collected.provider || "zen",
[state.collected.provider || "zen"]: {
// apiKey is stored in .env, not here
modelTier: state.collected.modelTier || "standard",
models: state.collected.models || [],
// model strings are written to opencode.json via PROVIDER_MODELS
},
},
};
Expand Down Expand Up @@ -280,35 +259,75 @@ ${providerEnvVar}=${state.collected.apiKey || ""}
chmodSync(envPath, 0o600);
onProgress(95, "Created .env with secure permissions...");

// Generate opencode.json
const modelProvider = state.collected.provider || "anthropic";
const modelTier = state.collected.modelTier || "standard";
const modelMap = state.collected.models;
const modelName = modelMap && typeof modelMap === 'object' ?
(modelMap[modelTier] || modelMap['standard']) :
"claude-sonnet-4.6";
const modelString = `${modelProvider}/${modelName}`;

// Generate opencode.json — full agent-tier structure matching the repo template
const provider = (state.collected.provider || "zen") as ProviderName;
const tiers = PROVIDER_MODELS[provider] ?? PROVIDER_MODELS.zen;

/**
* Build a standard agent entry with quick/standard/advanced tiers.
* The top-level `model` mirrors the standard tier so opencode has a
* sensible default when no tier is specified by a caller.
*/
function agentEntry(standard: string, quick: string, advanced: string) {
return {
model: standard,
model_tiers: {
quick: { model: quick },
standard: { model: standard },
advanced: { model: advanced },
},
};
}

const opencode = {
ai: {
name: state.collected.aiName || "PAI",
model: modelString,
},
voice: {
enabled: state.collected.voiceEnabled || false,
provider: state.collected.voiceProvider || "none",
voiceId: state.collected.voiceId || "default",
$schema: "https://opencode.ai/config.json",
theme: "dark",
model: tiers.standard,
snapshot: true,
username: state.collected.principalName || "User",
permission: {
"*": "allow",
websearch: "allow",
codesearch: "allow",
webfetch: "allow",
doom_loop: "ask",
external_directory: "ask",
},
memory: {
enabled: true,
mode: {
build: {
prompt: "You are a Personal AI assistant powered by PAI-OpenCode infrastructure.",
},
plan: {
prompt: "You are a Personal AI assistant powered by PAI-OpenCode infrastructure.",
},
},
skills: {
autoLoad: true,
agent: {
// Algorithm agent always uses the highest-quality model for orchestration
Algorithm: { model: tiers.advanced },
Architect: agentEntry(tiers.standard, tiers.quick, tiers.advanced),
Engineer: agentEntry(tiers.standard, tiers.quick, tiers.advanced),
general: agentEntry(tiers.standard, tiers.quick, tiers.advanced),
// explore is always the quick model — speed matters more than quality
explore: { model: tiers.quick },
Intern: agentEntry(tiers.quick, tiers.quick, tiers.standard),
Writer: agentEntry(tiers.standard, tiers.quick, tiers.advanced),
DeepResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced),
// Specialised researchers keep their primary model but fall back to provider tiers
GeminiResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced),
GrokResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced),
PerplexityResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced),
CodexResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced),
// QATester has no tier override — single model is intentional
QATester: { model: tiers.standard },
Pentester: agentEntry(tiers.standard, tiers.quick, tiers.advanced),
Designer: agentEntry(tiers.standard, tiers.quick, tiers.advanced),
Artist: agentEntry(tiers.standard, tiers.quick, tiers.advanced),
},
};

writeFileSync(
join(localOpencodeDir, "opencode.json"),
JSON.stringify(opencode, null, 2)
JSON.stringify(opencode, null, 2),
);
onProgress(97, "Generated opencode.json...");

Expand All @@ -319,20 +338,21 @@ ${providerEnvVar}=${state.collected.apiKey || ""}
// Check if ~/.opencode exists
if (existsSync(globalOpencodeLink)) {
const stats = lstatSync(globalOpencodeLink);

if (stats.isSymbolicLink()) {
// It's already a symlink - check if it points to our location
let currentTarget: string;
try {
currentTarget = realpathSync(globalOpencodeLink);
} catch (err) {
// Symlink target doesn't exist (broken symlink)
// Remove and recreate
} catch {
// Symlink target doesn't exist (broken symlink) — remove and recreate
unlinkSync(globalOpencodeLink);
symlinkSync(localOpencodeDir, globalOpencodeLink, "dir");
continue;
// Assign so the subsequent check sees a defined, correct value
// and doesn't attempt a redundant remove+recreate
currentTarget = localOpencodeDir;
}

if (currentTarget !== localOpencodeDir) {
// Remove old symlink and create new one
unlinkSync(globalOpencodeLink);
Expand Down Expand Up @@ -388,25 +408,17 @@ export async function runFreshInstall(
// Step 3: Provider Configuration (API Keys)
await emit({ event: "step_start", step: "api-keys" });
// Collect provider config via interactive callbacks
const providerChoices = [
{ label: "OpenCode Zen (FREE tier available)", value: "zen", description: "Recommended - 60x cost optimization" },
{ label: "Anthropic (Claude)", value: "anthropic", description: "Premium quality, higher cost" },
{ label: "OpenRouter", value: "openrouter", description: "Multi-provider flexibility" },
];
const provider = await requestChoice("provider", "Choose your AI provider:", providerChoices);
const providerChoices = Object.entries(PROVIDER_LABELS).map(([value, { label, description }]) => ({
label,
value,
description,
}));
const provider = (await requestChoice("provider", "Choose your AI provider:", providerChoices)) as ProviderName || "zen";
const apiKey = await requestInput("api-key", `Enter your ${provider} API key:`, "key", "sk-...");

// Select models based on provider
const models = provider === "zen" ? ZEN_FREE_MODELS :
provider === "anthropic" ? ANTHROPIC_MODELS :
provider === "openrouter" ? OPENROUTER_MODELS :
provider === "openai" ? OPENAI_MODELS : ZEN_FREE_MODELS;


await stepProviderConfig(state, {
provider: provider || "zen",
provider,
apiKey: apiKey || "",
modelTier: "standard",
models,
}, (percent, message) => {
emit({ event: "progress", step: "api-keys", percent, detail: message });
});
Expand Down
11 changes: 6 additions & 5 deletions docs/epic/OPTIMIZED-PR-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ tags: [architecture, migration, v3.0, PR-strategy, native-transformation]
| **WP-N5** | Plan Update | #54 | ✅ **Merged** | Sync all planning docs to reflect N1-N4 complete |
| **WP-N6** | System Self-Awareness | #55 | ✅ **Merged** | OpenCodeSystem skill, 4 architecture reference docs, ADR-017 |
| **WP-N7** | roborev + Biome CI | #56 | ✅ **Merged** | roborev plugin handler, CodeReview skill, GitHub Actions CI, ADR-018 |
| **WP-N8** | Obsidian Formatting Guidelines | — | 🔄 **In Progress** | Formatting guidelines, agent capability matrix (split from WP-N7) |
| **WP-N8** | Obsidian Formatting Guidelines | #57 | ✅ **Merged** | Formatting guidelines, agent capability matrix (split from WP-N7) |
| **WP-N9** | Installer opencode.json Fix | — | 🔄 **In Progress** | provider-models.ts, full agent-tier generation, principalName in username |

> [!NOTE]
> **2026-03-08 Live Audit:** WP-C scope significantly reduced after comparing repo against v4.0.3.
Expand Down Expand Up @@ -218,11 +219,11 @@ Current state (dev branch):
| Metric | 2026-03-08 | 2026-03-11 | **Current (2026-03-12)** |
|--------|------------|------------|--------------------------|
| Port WPs done | 8 ✅ | 9 ✅ (WP-E) | **9 ✅** |
| Native WPs done | 0 | 4 ✅ (N1–N4) | **7 ✅ (N1–N7), N8 in progress** |
| Open PRs | 2 (C, D) | 1 (#55) | **1 (WP-N8 — open, in progress)** |
| Remaining native work | Not planned | WP-N6 in progress | **WP-N8 in progress (Obsidian formatting)** |
| Native WPs done | 0 | 4 ✅ (N1–N4) | **8 ✅ (N1–N8), N9 in progress** |
| Open PRs | 2 (C, D) | 1 (#55) | **1 (WP-N9 — open, in progress)** |
| Remaining native work | Not planned | WP-N6 in progress | **WP-N9 in progress (installer opencode.json fix)** |

**Status:** Port complete. Native transformation: WP-N1 through WP-N7 merged (PR #50–#56). WP-N8 in progress (Obsidian formatting guidelines + agent capability matrix).
**Status:** Port complete. Native transformation: WP-N1 through WP-N8 merged (PR #50–#57). WP-N9 in progress (installer opencode.json full agent-tier generation).

**Native transformation plan:** `docs/epic/EPIC-v3.0-OpenCode-Native.md`
**Full gap analysis:** `docs/epic/GAP-ANALYSIS-v3.0.md`
Expand Down
Loading
Loading