diff --git a/README.md b/README.md index ce64e66..f3ac533 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,12 @@ Verify at `http://localhost:3456/plugins` — you should see `opencode-scrub` li | OhMyOpenCode/Sisyphus prompt | OMO identity line removed, `` block removed, "You are powered by..." line removed; persona rules preserved | | OpenCode prompt + user CLAUDE.md additions | identity stripped, all user content preserved | | OpenCode environment preamble + `` | duplicate preamble and redundant fields removed; bare `Working directory` retained for Meridian's cwd extraction | +| Request for a non-Claude model (e.g. `gpt-5.3-codex`) | unchanged | The plugin runs for the `opencode` adapter and for `passthrough` requests that still carry an OpenCode-specific identity or runtime marker. This covers OpenCode routed through LiteLLM when its client headers are removed. Other passthrough prompts, including genuine Claude Code prompts with an `` block, remain byte-for-byte unchanged. +Only requests that target a Claude model are scrubbed: Meridian's resolved aliases (`opus`, `sonnet`, `haiku`, `fable`, optionally with `[1m]`) and any model id containing `claude`. When Meridian serves another provider, such as OpenAI GPT/Codex models, the OpenCode prompt reaches that provider exactly as the client sent it. + ## Rules The scrub applies 8 independent regex replacements, each idempotent and each a no-op when its pattern is absent: diff --git a/src/__tests__/plugin.test.ts b/src/__tests__/plugin.test.ts index d170697..eb4eedf 100644 --- a/src/__tests__/plugin.test.ts +++ b/src/__tests__/plugin.test.ts @@ -47,3 +47,24 @@ describe("opencode-scrub adapter boundary", () => { expect(result?.systemContext).toContain("Working directory: /client/project") }) }) + +describe("opencode-scrub model boundary", () => { + const scrubbedWithoutModel = plugin.onRequest?.({ adapter: "opencode", systemContext: opencodePrompt, metadata: {} }) + + for (const model of ["opus", "opus[1m]", "sonnet", "sonnet[1m]", "haiku", "fable", "fable[1m]", "claude-opus-5-5", "anthropic/claude-sonnet-5"]) { + it(`scrubs requests for Claude model ${model}`, () => { + const ctx: RequestContext = { adapter: "opencode", model, systemContext: opencodePrompt, metadata: {} } + expect(plugin.onRequest?.(ctx)?.systemContext).toBe(scrubbedWithoutModel?.systemContext) + }) + } + + for (const adapter of ["opencode", "passthrough"]) { + for (const model of ["gpt-6-luna", "gpt-5.3-codex", "openai/gpt-5.5"]) { + it(`leaves ${adapter} requests for ${model} untouched`, () => { + const ctx: RequestContext = { adapter, model, systemContext: opencodePrompt, metadata: {} } + expect(plugin.onRequest?.(ctx)).toBe(ctx) + expect(ctx.systemContext).toBe(opencodePrompt) + }) + } + } +}) diff --git a/src/index.ts b/src/index.ts index b4d5623..ffde65f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,11 @@ export type { Transform, RequestContext } from "./types.js" const PASSTHROUGH_OPENCODE_MARKER = /You are OpenCode, the best coding agent on the planet\.|You are powered by the model named |||You are (?:"|\*\*)Sisyphus(?:"|\*\*)[^\n]*OhMyOpenCode/ +// Meridian resolves Claude requests to an SDK alias (`opus`, `sonnet[1m]`, ...) +// before plugins run; full `claude-*` ids cover other callers. Requests for +// other providers (e.g. OpenAI GPT/Codex models) keep their prompt as sent. +const CLAUDE_MODEL = /claude|^(?:opus|sonnet|haiku|fable)(?:\[1m\])?$/i + const plugin: Transform = { name: "opencode-scrub", version: packageMetadata.version, @@ -26,6 +31,7 @@ const plugin: Transform = { onRequest(ctx: RequestContext): RequestContext { if (!ctx.systemContext) return ctx + if (ctx.model !== undefined && !CLAUDE_MODEL.test(ctx.model)) return ctx if (ctx.adapter === "passthrough" && !PASSTHROUGH_OPENCODE_MARKER.test(ctx.systemContext)) return ctx const scrubbed = scrubOpencodeFingerprints(ctx.systemContext) if (scrubbed === ctx.systemContext) return ctx diff --git a/src/types.ts b/src/types.ts index 78e7f1c..6f15dc8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,6 +15,8 @@ export interface RequestContext { /** Agent adapter name (readonly from meridian's perspective) */ readonly adapter: string + /** Model the request targets, as resolved by meridian */ + model?: string /** Client-provided system prompt / context string, if any */ systemContext?: string /** Plugin-to-plugin state bag */