refactor(cli): split doctor/model-resolution and run/events into focused modules
Doctor checks: - model-resolution-cache.ts, model-resolution-config.ts - model-resolution-details.ts, model-resolution-effective-model.ts - model-resolution-types.ts, model-resolution-variant.ts Run events: - event-formatting.ts, event-handlers.ts - event-state.ts, event-stream-processor.ts
This commit is contained in:
@@ -15,6 +15,12 @@ export * from "./opencode"
|
||||
export * from "./plugin"
|
||||
export * from "./config"
|
||||
export * from "./model-resolution"
|
||||
export * from "./model-resolution-types"
|
||||
export * from "./model-resolution-cache"
|
||||
export * from "./model-resolution-config"
|
||||
export * from "./model-resolution-effective-model"
|
||||
export * from "./model-resolution-variant"
|
||||
export * from "./model-resolution-details"
|
||||
export * from "./auth"
|
||||
export * from "./dependencies"
|
||||
export * from "./gh"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import type { AvailableModelsInfo } from "./model-resolution-types"
|
||||
|
||||
function getOpenCodeCacheDir(): string {
|
||||
const xdgCache = process.env.XDG_CACHE_HOME
|
||||
if (xdgCache) return join(xdgCache, "opencode")
|
||||
return join(homedir(), ".cache", "opencode")
|
||||
}
|
||||
|
||||
export function loadAvailableModelsFromCache(): AvailableModelsInfo {
|
||||
const cacheFile = join(getOpenCodeCacheDir(), "models.json")
|
||||
|
||||
if (!existsSync(cacheFile)) {
|
||||
return { providers: [], modelCount: 0, cacheExists: false }
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(cacheFile, "utf-8")
|
||||
const data = JSON.parse(content) as Record<string, { models?: Record<string, unknown> }>
|
||||
|
||||
const providers = Object.keys(data)
|
||||
let modelCount = 0
|
||||
for (const providerId of providers) {
|
||||
const models = data[providerId]?.models
|
||||
if (models && typeof models === "object") {
|
||||
modelCount += Object.keys(models).length
|
||||
}
|
||||
}
|
||||
|
||||
return { providers, modelCount, cacheExists: true }
|
||||
} catch {
|
||||
return { providers: [], modelCount: 0, cacheExists: false }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { detectConfigFile, parseJsonc } from "../../../shared"
|
||||
import type { OmoConfig } from "./model-resolution-types"
|
||||
|
||||
const PACKAGE_NAME = "oh-my-opencode"
|
||||
const USER_CONFIG_DIR = join(homedir(), ".config", "opencode")
|
||||
const USER_CONFIG_BASE = join(USER_CONFIG_DIR, PACKAGE_NAME)
|
||||
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
|
||||
|
||||
export function loadOmoConfig(): OmoConfig | null {
|
||||
const projectDetected = detectConfigFile(PROJECT_CONFIG_BASE)
|
||||
if (projectDetected.format !== "none") {
|
||||
try {
|
||||
const content = readFileSync(projectDetected.path, "utf-8")
|
||||
return parseJsonc<OmoConfig>(content)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const userDetected = detectConfigFile(USER_CONFIG_BASE)
|
||||
if (userDetected.format !== "none") {
|
||||
try {
|
||||
const content = readFileSync(userDetected.path, "utf-8")
|
||||
return parseJsonc<OmoConfig>(content)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { AvailableModelsInfo, ModelResolutionInfo, OmoConfig } from "./model-resolution-types"
|
||||
import { formatModelWithVariant, getCategoryEffectiveVariant, getEffectiveVariant } from "./model-resolution-variant"
|
||||
|
||||
export function buildModelResolutionDetails(options: {
|
||||
info: ModelResolutionInfo
|
||||
available: AvailableModelsInfo
|
||||
config: OmoConfig
|
||||
}): string[] {
|
||||
const details: string[] = []
|
||||
|
||||
details.push("═══ Available Models (from cache) ═══")
|
||||
details.push("")
|
||||
if (options.available.cacheExists) {
|
||||
details.push(` Providers in cache: ${options.available.providers.length}`)
|
||||
details.push(
|
||||
` Sample: ${options.available.providers.slice(0, 6).join(", ")}${options.available.providers.length > 6 ? "..." : ""}`
|
||||
)
|
||||
details.push(` Total models: ${options.available.modelCount}`)
|
||||
details.push(` Cache: ~/.cache/opencode/models.json`)
|
||||
details.push(` ℹ Runtime: only connected providers used`)
|
||||
details.push(` Refresh: opencode models --refresh`)
|
||||
} else {
|
||||
details.push(" ⚠ Cache not found. Run 'opencode' to populate.")
|
||||
}
|
||||
details.push("")
|
||||
|
||||
details.push("═══ Configured Models ═══")
|
||||
details.push("")
|
||||
details.push("Agents:")
|
||||
for (const agent of options.info.agents) {
|
||||
const marker = agent.userOverride ? "●" : "○"
|
||||
const display = formatModelWithVariant(
|
||||
agent.effectiveModel,
|
||||
getEffectiveVariant(agent.name, agent.requirement, options.config)
|
||||
)
|
||||
details.push(` ${marker} ${agent.name}: ${display}`)
|
||||
}
|
||||
details.push("")
|
||||
details.push("Categories:")
|
||||
for (const category of options.info.categories) {
|
||||
const marker = category.userOverride ? "●" : "○"
|
||||
const display = formatModelWithVariant(
|
||||
category.effectiveModel,
|
||||
getCategoryEffectiveVariant(category.name, category.requirement, options.config)
|
||||
)
|
||||
details.push(` ${marker} ${category.name}: ${display}`)
|
||||
}
|
||||
details.push("")
|
||||
details.push("● = user override, ○ = provider fallback")
|
||||
|
||||
return details
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ModelRequirement } from "../../../shared/model-requirements"
|
||||
|
||||
function formatProviderChain(providers: string[]): string {
|
||||
return providers.join(" → ")
|
||||
}
|
||||
|
||||
export function getEffectiveModel(requirement: ModelRequirement, userOverride?: string): string {
|
||||
if (userOverride) {
|
||||
return userOverride
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
if (!firstEntry) {
|
||||
return "unknown"
|
||||
}
|
||||
return `${firstEntry.providers[0]}/${firstEntry.model}`
|
||||
}
|
||||
|
||||
export function buildEffectiveResolution(requirement: ModelRequirement, userOverride?: string): string {
|
||||
if (userOverride) {
|
||||
return `User override: ${userOverride}`
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
if (!firstEntry) {
|
||||
return "No fallback chain defined"
|
||||
}
|
||||
return `Provider fallback: ${formatProviderChain(firstEntry.providers)} → ${firstEntry.model}`
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ModelRequirement } from "../../../shared/model-requirements"
|
||||
|
||||
export interface AgentResolutionInfo {
|
||||
name: string
|
||||
requirement: ModelRequirement
|
||||
userOverride?: string
|
||||
userVariant?: string
|
||||
effectiveModel: string
|
||||
effectiveResolution: string
|
||||
}
|
||||
|
||||
export interface CategoryResolutionInfo {
|
||||
name: string
|
||||
requirement: ModelRequirement
|
||||
userOverride?: string
|
||||
userVariant?: string
|
||||
effectiveModel: string
|
||||
effectiveResolution: string
|
||||
}
|
||||
|
||||
export interface ModelResolutionInfo {
|
||||
agents: AgentResolutionInfo[]
|
||||
categories: CategoryResolutionInfo[]
|
||||
}
|
||||
|
||||
export interface OmoConfig {
|
||||
agents?: Record<string, { model?: string; variant?: string; category?: string }>
|
||||
categories?: Record<string, { model?: string; variant?: string }>
|
||||
}
|
||||
|
||||
export interface AvailableModelsInfo {
|
||||
providers: string[]
|
||||
modelCount: number
|
||||
cacheExists: boolean
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { ModelRequirement } from "../../../shared/model-requirements"
|
||||
import type { OmoConfig } from "./model-resolution-types"
|
||||
|
||||
export function formatModelWithVariant(model: string, variant?: string): string {
|
||||
return variant ? `${model} (${variant})` : model
|
||||
}
|
||||
|
||||
function getAgentOverride(
|
||||
agentName: string,
|
||||
config: OmoConfig
|
||||
): { variant?: string; category?: string } | undefined {
|
||||
const agentOverrides = config.agents
|
||||
if (!agentOverrides) return undefined
|
||||
|
||||
return (
|
||||
agentOverrides[agentName] ??
|
||||
Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
|
||||
)
|
||||
}
|
||||
|
||||
export function getEffectiveVariant(
|
||||
agentName: string,
|
||||
requirement: ModelRequirement,
|
||||
config: OmoConfig
|
||||
): string | undefined {
|
||||
const agentOverride = getAgentOverride(agentName, config)
|
||||
|
||||
if (agentOverride?.variant) {
|
||||
return agentOverride.variant
|
||||
}
|
||||
|
||||
const categoryName = agentOverride?.category
|
||||
if (categoryName) {
|
||||
const categoryVariant = config.categories?.[categoryName]?.variant
|
||||
if (categoryVariant) {
|
||||
return categoryVariant
|
||||
}
|
||||
}
|
||||
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
return firstEntry?.variant ?? requirement.variant
|
||||
}
|
||||
|
||||
export function getCategoryEffectiveVariant(
|
||||
categoryName: string,
|
||||
requirement: ModelRequirement,
|
||||
config: OmoConfig
|
||||
): string | undefined {
|
||||
const categoryVariant = config.categories?.[categoryName]?.variant
|
||||
if (categoryVariant) {
|
||||
return categoryVariant
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
return firstEntry?.variant ?? requirement.variant
|
||||
}
|
||||
@@ -1,132 +1,14 @@
|
||||
import { readFileSync, existsSync } from "node:fs"
|
||||
import type { CheckResult, CheckDefinition } from "../types"
|
||||
import { CHECK_IDS, CHECK_NAMES } from "../constants"
|
||||
import { parseJsonc, detectConfigFile } from "../../../shared"
|
||||
import {
|
||||
AGENT_MODEL_REQUIREMENTS,
|
||||
CATEGORY_MODEL_REQUIREMENTS,
|
||||
type ModelRequirement,
|
||||
} from "../../../shared/model-requirements"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
function getOpenCodeCacheDir(): string {
|
||||
const xdgCache = process.env.XDG_CACHE_HOME
|
||||
if (xdgCache) return join(xdgCache, "opencode")
|
||||
return join(homedir(), ".cache", "opencode")
|
||||
}
|
||||
|
||||
function loadAvailableModels(): { providers: string[]; modelCount: number; cacheExists: boolean } {
|
||||
const cacheFile = join(getOpenCodeCacheDir(), "models.json")
|
||||
|
||||
if (!existsSync(cacheFile)) {
|
||||
return { providers: [], modelCount: 0, cacheExists: false }
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(cacheFile, "utf-8")
|
||||
const data = JSON.parse(content) as Record<string, { models?: Record<string, unknown> }>
|
||||
|
||||
const providers = Object.keys(data)
|
||||
let modelCount = 0
|
||||
for (const providerId of providers) {
|
||||
const models = data[providerId]?.models
|
||||
if (models && typeof models === "object") {
|
||||
modelCount += Object.keys(models).length
|
||||
}
|
||||
}
|
||||
|
||||
return { providers, modelCount, cacheExists: true }
|
||||
} catch {
|
||||
return { providers: [], modelCount: 0, cacheExists: false }
|
||||
}
|
||||
}
|
||||
|
||||
const PACKAGE_NAME = "oh-my-opencode"
|
||||
const USER_CONFIG_DIR = join(homedir(), ".config", "opencode")
|
||||
const USER_CONFIG_BASE = join(USER_CONFIG_DIR, PACKAGE_NAME)
|
||||
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
|
||||
|
||||
export interface AgentResolutionInfo {
|
||||
name: string
|
||||
requirement: ModelRequirement
|
||||
userOverride?: string
|
||||
userVariant?: string
|
||||
effectiveModel: string
|
||||
effectiveResolution: string
|
||||
}
|
||||
|
||||
export interface CategoryResolutionInfo {
|
||||
name: string
|
||||
requirement: ModelRequirement
|
||||
userOverride?: string
|
||||
userVariant?: string
|
||||
effectiveModel: string
|
||||
effectiveResolution: string
|
||||
}
|
||||
|
||||
export interface ModelResolutionInfo {
|
||||
agents: AgentResolutionInfo[]
|
||||
categories: CategoryResolutionInfo[]
|
||||
}
|
||||
|
||||
interface OmoConfig {
|
||||
agents?: Record<string, { model?: string; variant?: string; category?: string }>
|
||||
categories?: Record<string, { model?: string; variant?: string }>
|
||||
}
|
||||
|
||||
function loadConfig(): OmoConfig | null {
|
||||
const projectDetected = detectConfigFile(PROJECT_CONFIG_BASE)
|
||||
if (projectDetected.format !== "none") {
|
||||
try {
|
||||
const content = readFileSync(projectDetected.path, "utf-8")
|
||||
return parseJsonc<OmoConfig>(content)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const userDetected = detectConfigFile(USER_CONFIG_BASE)
|
||||
if (userDetected.format !== "none") {
|
||||
try {
|
||||
const content = readFileSync(userDetected.path, "utf-8")
|
||||
return parseJsonc<OmoConfig>(content)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function formatProviderChain(providers: string[]): string {
|
||||
return providers.join(" → ")
|
||||
}
|
||||
|
||||
function getEffectiveModel(requirement: ModelRequirement, userOverride?: string): string {
|
||||
if (userOverride) {
|
||||
return userOverride
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
if (!firstEntry) {
|
||||
return "unknown"
|
||||
}
|
||||
return `${firstEntry.providers[0]}/${firstEntry.model}`
|
||||
}
|
||||
|
||||
function buildEffectiveResolution(
|
||||
requirement: ModelRequirement,
|
||||
userOverride?: string,
|
||||
): string {
|
||||
if (userOverride) {
|
||||
return `User override: ${userOverride}`
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
if (!firstEntry) {
|
||||
return "No fallback chain defined"
|
||||
}
|
||||
return `Provider fallback: ${formatProviderChain(firstEntry.providers)} → ${firstEntry.model}`
|
||||
}
|
||||
import type { OmoConfig, ModelResolutionInfo, AgentResolutionInfo, CategoryResolutionInfo } from "./model-resolution-types"
|
||||
import { loadAvailableModelsFromCache } from "./model-resolution-cache"
|
||||
import { loadOmoConfig } from "./model-resolution-config"
|
||||
import { buildEffectiveResolution, getEffectiveModel } from "./model-resolution-effective-model"
|
||||
import { buildModelResolutionDetails } from "./model-resolution-details"
|
||||
|
||||
export function getModelResolutionInfo(): ModelResolutionInfo {
|
||||
const agents: AgentResolutionInfo[] = Object.entries(AGENT_MODEL_REQUIREMENTS).map(
|
||||
@@ -184,116 +66,10 @@ export function getModelResolutionInfoWithOverrides(config: OmoConfig): ModelRes
|
||||
return { agents, categories }
|
||||
}
|
||||
|
||||
function formatModelWithVariant(model: string, variant?: string): string {
|
||||
return variant ? `${model} (${variant})` : model
|
||||
}
|
||||
|
||||
function getAgentOverride(
|
||||
agentName: string,
|
||||
config: OmoConfig,
|
||||
): { variant?: string; category?: string } | undefined {
|
||||
const agentOverrides = config.agents
|
||||
if (!agentOverrides) return undefined
|
||||
|
||||
// Direct lookup first, then case-insensitive lookup (matches agent-variant.ts)
|
||||
return (
|
||||
agentOverrides[agentName] ??
|
||||
Object.entries(agentOverrides).find(
|
||||
([key]) => key.toLowerCase() === agentName.toLowerCase()
|
||||
)?.[1]
|
||||
)
|
||||
}
|
||||
|
||||
function getEffectiveVariant(
|
||||
name: string,
|
||||
requirement: ModelRequirement,
|
||||
config: OmoConfig,
|
||||
): string | undefined {
|
||||
const agentOverride = getAgentOverride(name, config)
|
||||
|
||||
// Priority 1: Agent's direct variant override
|
||||
if (agentOverride?.variant) {
|
||||
return agentOverride.variant
|
||||
}
|
||||
|
||||
// Priority 2: Agent's category -> category's variant (matches agent-variant.ts)
|
||||
const categoryName = agentOverride?.category
|
||||
if (categoryName) {
|
||||
const categoryVariant = config.categories?.[categoryName]?.variant
|
||||
if (categoryVariant) {
|
||||
return categoryVariant
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Fall back to requirement's fallback chain
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
return firstEntry?.variant ?? requirement.variant
|
||||
}
|
||||
|
||||
interface AvailableModelsInfo {
|
||||
providers: string[]
|
||||
modelCount: number
|
||||
cacheExists: boolean
|
||||
}
|
||||
|
||||
function getCategoryEffectiveVariant(
|
||||
categoryName: string,
|
||||
requirement: ModelRequirement,
|
||||
config: OmoConfig,
|
||||
): string | undefined {
|
||||
const categoryVariant = config.categories?.[categoryName]?.variant
|
||||
if (categoryVariant) {
|
||||
return categoryVariant
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
return firstEntry?.variant ?? requirement.variant
|
||||
}
|
||||
|
||||
function buildDetailsArray(info: ModelResolutionInfo, available: AvailableModelsInfo, config: OmoConfig): string[] {
|
||||
const details: string[] = []
|
||||
|
||||
details.push("═══ Available Models (from cache) ═══")
|
||||
details.push("")
|
||||
if (available.cacheExists) {
|
||||
details.push(` Providers in cache: ${available.providers.length}`)
|
||||
details.push(` Sample: ${available.providers.slice(0, 6).join(", ")}${available.providers.length > 6 ? "..." : ""}`)
|
||||
details.push(` Total models: ${available.modelCount}`)
|
||||
details.push(` Cache: ~/.cache/opencode/models.json`)
|
||||
details.push(` ℹ Runtime: only connected providers used`)
|
||||
details.push(` Refresh: opencode models --refresh`)
|
||||
} else {
|
||||
details.push(" ⚠ Cache not found. Run 'opencode' to populate.")
|
||||
}
|
||||
details.push("")
|
||||
|
||||
details.push("═══ Configured Models ═══")
|
||||
details.push("")
|
||||
details.push("Agents:")
|
||||
for (const agent of info.agents) {
|
||||
const marker = agent.userOverride ? "●" : "○"
|
||||
const display = formatModelWithVariant(agent.effectiveModel, getEffectiveVariant(agent.name, agent.requirement, config))
|
||||
details.push(` ${marker} ${agent.name}: ${display}`)
|
||||
}
|
||||
details.push("")
|
||||
details.push("Categories:")
|
||||
for (const category of info.categories) {
|
||||
const marker = category.userOverride ? "●" : "○"
|
||||
const display = formatModelWithVariant(
|
||||
category.effectiveModel,
|
||||
getCategoryEffectiveVariant(category.name, category.requirement, config)
|
||||
)
|
||||
details.push(` ${marker} ${category.name}: ${display}`)
|
||||
}
|
||||
details.push("")
|
||||
details.push("● = user override, ○ = provider fallback")
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
export async function checkModelResolution(): Promise<CheckResult> {
|
||||
const config = loadConfig() ?? {}
|
||||
const config = loadOmoConfig() ?? {}
|
||||
const info = getModelResolutionInfoWithOverrides(config)
|
||||
const available = loadAvailableModels()
|
||||
const available = loadAvailableModelsFromCache()
|
||||
|
||||
const agentCount = info.agents.length
|
||||
const categoryCount = info.categories.length
|
||||
@@ -308,7 +84,7 @@ export async function checkModelResolution(): Promise<CheckResult> {
|
||||
name: CHECK_NAMES[CHECK_IDS.MODEL_RESOLUTION],
|
||||
status: available.cacheExists ? "pass" : "warn",
|
||||
message: `${agentCount} agents, ${categoryCount} categories${overrideNote}${cacheNote}`,
|
||||
details: buildDetailsArray(info, available, config),
|
||||
details: buildModelResolutionDetails({ info, available, config }),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user