Skip to content

[FEATURE]: New Plugin Hook assistant.output.beforeParse #3367

Closed

Description

@webboty

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 with args.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).

Security/Perf Considerations

  • The hook runs synchronously in-process; plugin authors must keep work O(n) on text.
  • Provide a maxChars safeguard 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

  1. added
    discussionUsed for feature requests, proposals, ideas, etc. Open discussion
    on Oct 22, 2025
  2. rekram1-node commented on Oct 23, 2025

    @rekram1-node
    Collaborator

    Yeah we could prolly do something like onResponse, but i guess you would need to handle every chunk yourself

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    discussionUsed for feature requests, proposals, ideas, etc. Open discussion

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions