-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathindex.ts
More file actions
228 lines (210 loc) · 8.65 KB
/
Copy pathindex.ts
File metadata and controls
228 lines (210 loc) · 8.65 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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
/** ACP version, injected at build time by tsup define */
declare const ACP_VERSION: string | undefined
import type { Plugin } from "@opencode-ai/plugin"
import { getConfig } from "./lib/config"
import {
createAcpStatusTool,
createAcpContextRecapTool,
createCompressRangeTool,
createDecompressTool,
createSearchContextTool,
} from "./lib/compress"
import {
compressDisabledByOpencode,
hasExplicitToolPermission,
type HostPermissionSnapshot,
} from "./lib/host-permissions"
import { Logger } from "./lib/logger"
import { SessionStateRegistry } from "./lib/state"
import { PromptStore } from "./lib/prompts/store"
import {
createChatMessageTransformHandler,
createCommandExecuteHandler,
createEventHandler,
createSystemPromptHandler,
createTextCompleteHandler,
} from "./lib/hooks"
import { configureClientAuth, isSecureMode } from "./lib/auth"
import { findBiliProxyProviders } from "./lib/bili-proxy"
import { startAutoUpdate } from "./lib/update"
const server: Plugin = (async (ctx) => {
const config = getConfig(ctx)
if (!config.enabled) {
return {}
}
if (process.env.BILLION_CONTEXT_PROXY) {
console.log(
"[opencode-acp] disabled: BILLION_CONTEXT_PROXY detected — proxy handles compression",
)
return {}
}
const logger = new Logger(config.debug, config.debug ? "debug" : config.logLevel)
logger.info("ACP plugin initialized", {
version: typeof ACP_VERSION !== "undefined" ? ACP_VERSION : "dev",
workspace: ctx.directory,
logLevel: logger.level,
debug: config.debug,
autoUpdate: config.autoUpdate,
secureMode: isSecureMode(),
})
const registry = new SessionStateRegistry(logger)
const prompts = new PromptStore(logger, ctx.directory, config.experimental.customPrompts)
const hostPermissions: HostPermissionSnapshot = {
global: undefined,
agents: {},
}
if (isSecureMode()) {
configureClientAuth(ctx.client)
// logger.info("Secure mode detected, configured client authentication")
}
// [FIX #312] Seed the model-limit catalog so the FIRST request after a
// model switch resolves the new model's context window (the per-request
// system.transform refresh only fills entries for models already used in
// this instance). Fire-and-forget — never blocks init; outcome is logged
// so a silent degrade (empty catalog / failed fetch) is debuggable. On
// failure the fallback is per-request refresh, the pre-fix behavior.
registry.hydrateModelLimitsFromClient(ctx.client).then(
(recorded) => {
if (recorded > 0) {
logger.info("Model limit catalog seeded from provider config", {
models: recorded,
})
} else {
logger.warn(
"Model limit catalog seeding recorded no entries — " +
"falling back to per-request refresh (system.transform)",
)
}
},
(error) => {
logger.warn(
"Model limit catalog seeding failed — " +
"falling back to per-request refresh (system.transform)",
{ error: error instanceof Error ? error.message : String(error) },
)
},
)
logger.info("DCP initialized")
startAutoUpdate(ctx, config.autoUpdate, logger)
const compressToolContext = {
client: ctx.client,
registry,
logger,
config,
prompts,
}
// [FIX #337] Manual proxy mode: the bili proxy may be detected in a
// provider baseURL by the config hook (the BILLION_CONTEXT_PROXY env var
// is only set by the `bili <client>` launcher, not by manual proxy mode).
// When detected, every ACP hook becomes a no-op so the proxy handles
// compression alone. Assigned (not latched) so a config reload that
// removes the proxy restores ACP behavior.
let disabledByBiliProxy = false
const guard =
<TArgs extends unknown[]>(fn: (...args: TArgs) => Promise<void>) =>
(...args: TArgs): Promise<void> =>
disabledByBiliProxy ? Promise.resolve() : fn(...args)
return {
"experimental.chat.system.transform": guard(
createSystemPromptHandler(registry, logger, config, prompts),
),
"experimental.chat.messages.transform": guard(
createChatMessageTransformHandler(
ctx.client,
registry,
logger,
config,
prompts,
hostPermissions,
),
) as any,
"experimental.text.complete": guard(createTextCompleteHandler()),
"command.execute.before": guard(
createCommandExecuteHandler(
ctx.client,
registry,
logger,
config,
ctx.directory,
hostPermissions,
),
),
event: guard(createEventHandler(registry, logger)),
tool: {
...(config.compress.permission !== "deny" && {
compress: createCompressRangeTool(compressToolContext),
decompress: createDecompressTool(compressToolContext),
search_context: createSearchContextTool(compressToolContext),
acp_status: createAcpStatusTool(compressToolContext),
acp_context_recap: createAcpContextRecapTool(compressToolContext),
}),
},
config: async (opencodeConfig) => {
// [FIX #337] Manual proxy mode: a provider baseURL routed through
// the bili proxy (`/bili/` prefix) means the proxy handles context
// compression — ACP must stay fully off, mirroring the
// BILLION_CONTEXT_PROXY env-var guard. Denying the ACP tools
// removes them from the LLM tool list (verified against a live
// opencode instance), and the guard flag no-ops every hook.
const biliMatches = findBiliProxyProviders(opencodeConfig.provider)
disabledByBiliProxy = biliMatches.length > 0
if (biliMatches.length > 0) {
console.log(
"[opencode-acp] disabled: /bili/ proxy detected in provider baseURL (" +
biliMatches.map((m) => m.provider).join(", ") +
") — proxy handles compression",
)
const permission = opencodeConfig.permission ?? {}
opencodeConfig.permission = {
...permission,
compress: "deny",
decompress: "deny",
search_context: "deny",
acp_status: "deny",
acp_context_recap: "deny",
} as typeof permission
return
}
if (
config.compress.permission !== "deny" &&
compressDisabledByOpencode(opencodeConfig.permission)
) {
config.compress.permission = "deny"
}
if (config.commands.enabled && config.compress.permission !== "deny") {
opencodeConfig.command ??= {}
opencodeConfig.command["acp"] = {
template: "",
description: "Show available ACP commands",
}
}
const toolsToAdd: string[] = []
if (config.compress.permission !== "deny" && !config.allowSubAgents) {
toolsToAdd.push("compress", "decompress", "search_context", "acp_status")
}
if (toolsToAdd.length > 0) {
const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? []
opencodeConfig.experimental = {
...opencodeConfig.experimental,
primary_tools: [...existingPrimaryTools, ...toolsToAdd],
}
}
if (!hasExplicitToolPermission(opencodeConfig.permission, "compress")) {
const permission = opencodeConfig.permission ?? {}
opencodeConfig.permission = {
...permission,
compress: config.compress.permission,
acp_status: "allow",
} as typeof permission
}
hostPermissions.global = opencodeConfig.permission
hostPermissions.agents = Object.fromEntries(
Object.entries(opencodeConfig.agent ?? {}).map(([name, agent]) => [
name,
agent?.permission,
]),
)
},
}
}) satisfies Plugin
export default server