feat(installer): auto-configure athena council members based on available providers
The installer now detects which providers the user has (Anthropic, OpenAI, Google, Copilot, OpenCode Zen) and generates council member config for Athena. Requires at least 2 distinct providers; skips council config otherwise. This implements the documented claim in configurations.md.
This commit is contained in:
parent
5a72f21fc8
commit
628c9a8958
@ -4,6 +4,9 @@ exports[`generateModelConfig no providers available returns ULTIMATE_FALLBACK fo
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
@ -65,6 +68,10 @@ exports[`generateModelConfig single native provider uses Claude models when only
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
@ -127,6 +134,10 @@ exports[`generateModelConfig single native provider uses Claude models with isMa
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
@ -190,6 +201,10 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"variant": "high",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "openai/gpt-5.2",
|
||||
},
|
||||
@ -257,6 +272,10 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"variant": "high",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "openai/gpt-5.2",
|
||||
},
|
||||
@ -324,6 +343,10 @@ exports[`generateModelConfig single native provider uses Gemini models when only
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "high",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
},
|
||||
@ -385,6 +408,10 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "high",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
},
|
||||
@ -446,6 +473,26 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"council": {
|
||||
"members": [
|
||||
{
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"name": "Claude",
|
||||
},
|
||||
{
|
||||
"model": "openai/gpt-5.2",
|
||||
"name": "GPT",
|
||||
},
|
||||
{
|
||||
"model": "google/gemini-3-flash",
|
||||
"name": "Gemini",
|
||||
},
|
||||
],
|
||||
},
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
@ -520,6 +567,26 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"council": {
|
||||
"members": [
|
||||
{
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"name": "Claude",
|
||||
},
|
||||
{
|
||||
"model": "openai/gpt-5.2",
|
||||
"name": "GPT",
|
||||
},
|
||||
{
|
||||
"model": "google/gemini-3-flash",
|
||||
"name": "Gemini",
|
||||
},
|
||||
],
|
||||
},
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
@ -595,6 +662,10 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "opencode/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
},
|
||||
@ -669,6 +740,10 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "opencode/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
},
|
||||
@ -744,6 +819,10 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
@ -818,6 +897,10 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
@ -893,6 +976,9 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian whe
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
@ -948,6 +1034,9 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian wit
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
@ -1003,6 +1092,22 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"council": {
|
||||
"members": [
|
||||
{
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"name": "Claude",
|
||||
},
|
||||
{
|
||||
"model": "opencode/claude-sonnet-4-5",
|
||||
"name": "OpenCode Claude",
|
||||
},
|
||||
],
|
||||
},
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
},
|
||||
@ -1077,6 +1182,22 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"council": {
|
||||
"members": [
|
||||
{
|
||||
"model": "openai/gpt-5.2",
|
||||
"name": "GPT",
|
||||
},
|
||||
{
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
"name": "Copilot GPT",
|
||||
},
|
||||
],
|
||||
},
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
@ -1151,6 +1272,10 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
@ -1212,6 +1337,22 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"council": {
|
||||
"members": [
|
||||
{
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"name": "Claude",
|
||||
},
|
||||
{
|
||||
"model": "google/gemini-3-flash",
|
||||
"name": "Gemini",
|
||||
},
|
||||
],
|
||||
},
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
@ -1278,6 +1419,22 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"council": {
|
||||
"members": [
|
||||
{
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
"name": "Copilot GPT",
|
||||
},
|
||||
{
|
||||
"model": "opencode/claude-sonnet-4-5",
|
||||
"name": "OpenCode Claude",
|
||||
},
|
||||
],
|
||||
},
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
},
|
||||
@ -1352,6 +1509,34 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"council": {
|
||||
"members": [
|
||||
{
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"name": "Claude",
|
||||
},
|
||||
{
|
||||
"model": "openai/gpt-5.2",
|
||||
"name": "GPT",
|
||||
},
|
||||
{
|
||||
"model": "google/gemini-3-flash",
|
||||
"name": "Gemini",
|
||||
},
|
||||
{
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
"name": "Copilot GPT",
|
||||
},
|
||||
{
|
||||
"model": "opencode/claude-sonnet-4-5",
|
||||
"name": "OpenCode Claude",
|
||||
},
|
||||
],
|
||||
},
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
},
|
||||
@ -1426,6 +1611,34 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"athena": {
|
||||
"council": {
|
||||
"members": [
|
||||
{
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"name": "Claude",
|
||||
},
|
||||
{
|
||||
"model": "openai/gpt-5.2",
|
||||
"name": "GPT",
|
||||
},
|
||||
{
|
||||
"model": "google/gemini-3-flash",
|
||||
"name": "Gemini",
|
||||
},
|
||||
{
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
"name": "Copilot GPT",
|
||||
},
|
||||
{
|
||||
"model": "opencode/claude-sonnet-4-5",
|
||||
"name": "OpenCode Claude",
|
||||
},
|
||||
],
|
||||
},
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"atlas": {
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
},
|
||||
|
||||
137
src/cli/council-members-generator.test.ts
Normal file
137
src/cli/council-members-generator.test.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { generateCouncilMembers } from "./council-members-generator"
|
||||
import type { ProviderAvailability } from "./model-fallback-types"
|
||||
|
||||
function makeAvail(overrides: {
|
||||
native?: Partial<ProviderAvailability["native"]>
|
||||
opencodeZen?: boolean
|
||||
copilot?: boolean
|
||||
zai?: boolean
|
||||
kimiForCoding?: boolean
|
||||
isMaxPlan?: boolean
|
||||
}): ProviderAvailability {
|
||||
return {
|
||||
native: {
|
||||
claude: false,
|
||||
openai: false,
|
||||
gemini: false,
|
||||
...(overrides.native ?? {}),
|
||||
},
|
||||
opencodeZen: overrides.opencodeZen ?? false,
|
||||
copilot: overrides.copilot ?? false,
|
||||
zai: overrides.zai ?? false,
|
||||
kimiForCoding: overrides.kimiForCoding ?? false,
|
||||
isMaxPlan: overrides.isMaxPlan ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
describe("generateCouncilMembers", () => {
|
||||
//#given all three native providers
|
||||
//#when generating council members
|
||||
//#then returns 3 members (one per provider)
|
||||
test("returns 3 members when claude + openai + gemini available", () => {
|
||||
const members = generateCouncilMembers(makeAvail({
|
||||
native: { claude: true, openai: true, gemini: true },
|
||||
}))
|
||||
|
||||
expect(members).toHaveLength(3)
|
||||
expect(members.some(m => m.model.startsWith("anthropic/"))).toBe(true)
|
||||
expect(members.some(m => m.model.startsWith("openai/"))).toBe(true)
|
||||
expect(members.some(m => m.model.startsWith("google/"))).toBe(true)
|
||||
expect(members.every(m => m.name)).toBe(true)
|
||||
})
|
||||
|
||||
//#given claude + openai only
|
||||
//#when generating council members
|
||||
//#then returns 2 members
|
||||
test("returns 2 members when claude + openai available", () => {
|
||||
const members = generateCouncilMembers(makeAvail({
|
||||
native: { claude: true, openai: true },
|
||||
}))
|
||||
|
||||
expect(members).toHaveLength(2)
|
||||
expect(members.some(m => m.model.startsWith("anthropic/"))).toBe(true)
|
||||
expect(members.some(m => m.model.startsWith("openai/"))).toBe(true)
|
||||
})
|
||||
|
||||
//#given claude + gemini only
|
||||
//#when generating council members
|
||||
//#then returns 2 members
|
||||
test("returns 2 members when claude + gemini available", () => {
|
||||
const members = generateCouncilMembers(makeAvail({
|
||||
native: { claude: true, gemini: true },
|
||||
}))
|
||||
|
||||
expect(members).toHaveLength(2)
|
||||
})
|
||||
|
||||
//#given openai + gemini only
|
||||
//#when generating council members
|
||||
//#then returns 2 members
|
||||
test("returns 2 members when openai + gemini available", () => {
|
||||
const members = generateCouncilMembers(makeAvail({
|
||||
native: { openai: true, gemini: true },
|
||||
}))
|
||||
|
||||
expect(members).toHaveLength(2)
|
||||
})
|
||||
|
||||
//#given only one native provider
|
||||
//#when copilot is also available
|
||||
//#then returns 2 members (native + copilot)
|
||||
test("uses copilot as second member when only one native provider", () => {
|
||||
const members = generateCouncilMembers(makeAvail({
|
||||
native: { claude: true },
|
||||
copilot: true,
|
||||
}))
|
||||
|
||||
expect(members.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
//#given only one native provider
|
||||
//#when opencode zen is also available
|
||||
//#then returns 2 members
|
||||
test("uses opencode zen as second member when only one native provider", () => {
|
||||
const members = generateCouncilMembers(makeAvail({
|
||||
native: { gemini: true },
|
||||
opencodeZen: true,
|
||||
}))
|
||||
|
||||
expect(members.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
//#given no providers at all
|
||||
//#when generating council members
|
||||
//#then returns empty array (can't meet minimum 2)
|
||||
test("returns empty when no providers available", () => {
|
||||
const members = generateCouncilMembers(makeAvail({}))
|
||||
|
||||
expect(members).toHaveLength(0)
|
||||
})
|
||||
|
||||
//#given only one provider, no fallbacks
|
||||
//#when generating council members
|
||||
//#then returns empty (need at least 2 distinct models)
|
||||
test("returns empty when only one provider and no fallbacks", () => {
|
||||
const members = generateCouncilMembers(makeAvail({
|
||||
native: { claude: true },
|
||||
}))
|
||||
|
||||
expect(members).toHaveLength(0)
|
||||
})
|
||||
|
||||
//#given all members have names
|
||||
//#when generating council
|
||||
//#then each member has a human-readable name
|
||||
test("all members have name field", () => {
|
||||
const members = generateCouncilMembers(makeAvail({
|
||||
native: { claude: true, openai: true, gemini: true },
|
||||
}))
|
||||
|
||||
for (const m of members) {
|
||||
expect(m.name).toBeDefined()
|
||||
expect(typeof m.name).toBe("string")
|
||||
expect(m.name!.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
54
src/cli/council-members-generator.ts
Normal file
54
src/cli/council-members-generator.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import type { ProviderAvailability } from "./model-fallback-types"
|
||||
|
||||
export interface CouncilMember {
|
||||
model: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const COUNCIL_CANDIDATES: Array<{
|
||||
provider: (avail: ProviderAvailability) => boolean
|
||||
model: string
|
||||
name: string
|
||||
}> = [
|
||||
{
|
||||
provider: (a) => a.native.claude,
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
name: "Claude",
|
||||
},
|
||||
{
|
||||
provider: (a) => a.native.openai,
|
||||
model: "openai/gpt-5.2",
|
||||
name: "GPT",
|
||||
},
|
||||
{
|
||||
provider: (a) => a.native.gemini,
|
||||
model: "google/gemini-3-flash",
|
||||
name: "Gemini",
|
||||
},
|
||||
{
|
||||
provider: (a) => a.copilot,
|
||||
model: "github-copilot/gpt-5.2",
|
||||
name: "Copilot GPT",
|
||||
},
|
||||
{
|
||||
provider: (a) => a.opencodeZen,
|
||||
model: "opencode/claude-sonnet-4-5",
|
||||
name: "OpenCode Claude",
|
||||
},
|
||||
]
|
||||
|
||||
export function generateCouncilMembers(avail: ProviderAvailability): CouncilMember[] {
|
||||
const members: CouncilMember[] = []
|
||||
|
||||
for (const candidate of COUNCIL_CANDIDATES) {
|
||||
if (candidate.provider(avail)) {
|
||||
members.push({ model: candidate.model, name: candidate.name })
|
||||
}
|
||||
}
|
||||
|
||||
if (members.length < 2) {
|
||||
return []
|
||||
}
|
||||
|
||||
return members
|
||||
}
|
||||
@ -13,6 +13,7 @@ import {
|
||||
isRequiredProviderAvailable,
|
||||
resolveModelFromChain,
|
||||
} from "./fallback-chain-resolution"
|
||||
import { generateCouncilMembers } from "./council-members-generator"
|
||||
|
||||
export type { GeneratedOmoConfig } from "./model-fallback-types"
|
||||
|
||||
@ -122,6 +123,12 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
||||
}
|
||||
}
|
||||
|
||||
const councilMembers = generateCouncilMembers(avail)
|
||||
if (councilMembers.length >= 2) {
|
||||
const athenaAgent = agents.athena ?? {}
|
||||
agents.athena = { ...athenaAgent, council: { members: councilMembers } } as AgentConfig
|
||||
}
|
||||
|
||||
return {
|
||||
$schema: SCHEMA_URL,
|
||||
agents,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user