-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig-reader.ts
More file actions
100 lines (85 loc) · 2.9 KB
/
config-reader.ts
File metadata and controls
100 lines (85 loc) · 2.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* Config reader for the standalone fallback plugin.
*
* Reads `fallback_models` from OpenCode's agent config section
* (passed via the plugin config hook), NOT from oh-my-opencode.jsonc.
*/
type AgentRecord = Record<string, unknown>
const SESSION_ID_NOISE_WORDS = new Set(["ses", "work", "task", "session"])
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
export function normalizeFallbackModelsField(
value: unknown
): string[] {
if (!value) return []
if (typeof value === "string") return [value]
if (Array.isArray(value)) {
return value.filter((item): item is string => typeof item === "string")
}
return []
}
export function readFallbackModels(
agentName: string,
agents: AgentRecord | undefined
): string[] {
if (!agents) return []
const agentConfig = agents[agentName]
if (!isRecord(agentConfig)) return []
return normalizeFallbackModelsField(agentConfig.fallback_models)
}
export function resolveAgentForSession(
sessionID: string,
eventAgent?: string
): string | undefined {
if (eventAgent && eventAgent.trim().length > 0) {
return eventAgent.trim().toLowerCase()
}
const segments = sessionID.split(/[\s_\-/]+/).filter(Boolean)
for (const segment of segments) {
const candidate = segment.toLowerCase()
const isAlphaOnly = /^[a-z][a-z-]*$/.test(candidate)
if (candidate.length > 2 && isAlphaOnly && !SESSION_ID_NOISE_WORDS.has(candidate)) {
return candidate
}
}
return undefined
}
export function getFallbackModelsForSession(
sessionID: string,
eventAgent: string | undefined,
agents: AgentRecord | undefined,
globalFallbackModels?: string[]
): string[] {
const resolvedAgent = resolveAgentForSession(sessionID, eventAgent)
// Tier 1: Per-agent fallback_models
if (resolvedAgent && agents) {
const models = readFallbackModels(resolvedAgent, agents)
// Implicitly include the agent's configured primary model as a
// last-resort fallback candidate — but only when fallback_models
// was explicitly configured with entries. If the user didn't set
// fallback_models at all (or set it to []), we don't inject the
// primary — they didn't opt into fallback for this agent.
//
// This handles the case where the user manually switches to a
// fallback model and it later fails: the configured primary
// becomes available as a recovery target instead of the chain
// appearing exhausted.
if (models.length > 0) {
const agentConfig = agents[resolvedAgent]
if (isRecord(agentConfig) && typeof agentConfig.model === "string") {
const primaryModel = agentConfig.model
if (!models.includes(primaryModel)) {
models.unshift(primaryModel)
}
}
return models
}
}
// Tier 2: Global fallback_models from plugin config
if (globalFallbackModels && globalFallbackModels.length > 0) {
return globalFallbackModels
}
// Tier 3: No fallback
return []
}