Repository navigation
Description
Feature hasn't been suggested before.
- I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request
Feature Request: assistant.output.beforeParse Plugin Hook
Goal
Allow plugins to normalize/transform the assistant’s raw text before OpenCode parses <tool_call>…</tool_call> blocks. This enables lightweight model-specific fixes (e.g., inserting a newline after a function name) without forking OpenCode or adding external proxies.
Why
Some models (e.g., GLM-4.6) occasionally emit tool calls like:
<tool_call>write <arg_key>filePath</arg_key> ...
Parsers typically expect:
<tool_call>write
<arg_key>filePath</arg_key>
...
Today, plugins can’t intercept the raw text pre-parse. Existing hooks (event, tool.execute.before/after) happen after parsing—too late to fix formatting.
Proposed Hook
-
Name:
assistant.output.beforeParse -
Timing: After the model finishes a turn (final text available), before OpenCode parses tool calls.
-
Signature (TypeScript):
type AssistantOutputBeforeParseArgs = { modelId: string; // provider’s model id (e.g., "mlx-community/glm-4.6") text: string; // raw assistant text, exactly as generated metadata?: Record<string, any>; // optional provider/runtime metadata replaceText: (next: string) => void; // call to replace the raw text context: { conversationId: string; messageId: string; project?: string; }; }; // Plugin surface: assistant: { output: { beforeParse?: (args: AssistantOutputBeforeParseArgs) => Promise<void> | void; } }
-
Behavior:
- If
replaceText()is not called, OpenCode proceeds withargs.text. - If called, OpenCode uses the replacement for downstream parsing.
- Multiple plugins: run in registration order; each sees the latest text.
- Errors in a plugin should be caught and logged without aborting the run (fail-open).
- If
Security/Perf Considerations
- The hook runs synchronously in-process; plugin authors must keep work O(n) on
text. - Provide a
maxCharssafeguard or document a soft limit (e.g., 256k) equal to message cap. - Log the plugin name and time spent for observability.
Minimal Example Behavior
Normalize same-line function names in <tool_call> blocks by inserting a newline before the first <arg_key> when needed.
Example Plugin (will work once the hook exists)
Place at .opencode/plugin/glm-normalizer.ts (or global: ~/.config/opencode/plugin/glm-normalizer.ts).
// .opencode/plugin/glm-normalizer.ts
export const GLMNormalizer = async () => {
// Regexes:
// 1) Find each <tool_call>...</tool_call> block
const TOOL = /<tool_call>([\s\S]*?)<\/tool_call>/g;
// 2) Find the FIRST <arg_key> inside a block
const FIRST_ARG = /<arg_key>/;
function normalizeBlock(blockInner: string): string {
// Ensure function name is on its own line:
// If the first "<arg_key>" appears on the same line as the function name,
// insert a newline before it.
const idx = blockInner.indexOf('<arg_key>');
if (idx <= 0) return blockInner;
// Everything up to the first arg_key
const head = blockInner.slice(0, idx);
// If head already contains a newline, we’re fine
if (/\r?\n\s*$/.test(head)) return blockInner;
// Otherwise, insert a newline before the first arg_key
return head + '\n' + blockInner.slice(idx);
}
function normalizeAll(text: string): string {
return text.replace(TOOL, (_match, inner) => {
const fixed = normalizeBlock(inner);
return `<tool_call>${fixed}</tool_call>`;
});
}
return {
assistant: {
output: {
// NEW HOOK — requires platform support
beforeParse: async ({ text, replaceText }) => {
// NOTE: You can scope by modelId if desired,
// e.g., if (modelId.includes("glm-4.6")) { ... }
const fixed = normalizeAll(text);
if (fixed !== text) replaceText(fixed);
},
},
},
};
};What it does
- Scans each
<tool_call>…</tool_call>block. - If
<arg_key>comes immediately after the function name on the same line, it inserts a newline. - Leaves everything else untouched.
- Zero network calls; O(n) on the string.
Activity
rekram1-node commentedon Oct 23, 2025
Yeah we could prolly do something like onResponse, but i guess you would need to handle every chunk yourself