diff --git a/assets/magic-context.schema.json b/assets/magic-context.schema.json index a68d74801..8a2ec79b3 100644 --- a/assets/magic-context.schema.json +++ b/assets/magic-context.schema.json @@ -46,6 +46,14 @@ "description": "Enable automatic npm self-update checks for the OpenCode plugin. Security: USER-only in config loader, so hostile project configs cannot suppress updates.", "type": "boolean" }, + "anthropic_transport_providers": { + "description": "ProviderIDs whose Claude-named models are served through OpenCode's \"@ai-sdk/anthropic\" adapter (e.g. [\"github-copilot\"] for Copilot Claude), i.e. the wire drops empty text/reasoning parts before the API exactly like the canonical Anthropic provider. Extends Magic Context's reasoning-strip machinery to those routes. Security: USER-only in the config loader; a project config cannot widen the gate. List a provider only if every Claude model under it uses the @ai-sdk/anthropic adapter.", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, "language": { "description": "Output language for Magic Context's generated content and guidance, as a 2-letter ISO 639-1 code (e.g. \"tr\", \"es\", \"de\", \"ja\", \"pt\"). When set, the historian, dreamer, sidekick, and the agent-guidance block instruct the model to write its PROSE in this language while keeping all structural tokens (XML tags, the five memory category names, code identifiers, file paths) in English. USER-LEVEL ONLY (ignored in project config for security). Unset = today's behavior (model mirrors the conversation; English scaffolding). Changing it triggers one cache re-materialization; existing compartments/memories keep their original language until naturally rewritten.", "type": "string" diff --git a/packages/docs/src/content/docs/reference/configuration.md b/packages/docs/src/content/docs/reference/configuration.md index f3cafc74d..36ea4315d 100644 --- a/packages/docs/src/content/docs/reference/configuration.md +++ b/packages/docs/src/content/docs/reference/configuration.md @@ -246,6 +246,7 @@ Behavior tuning most installs never need to touch. | Key | Type | Default | Description | |---|---|---|---| +| `anthropic_transport_providers` | string[] | — | ProviderIDs whose Claude-named models are served through OpenCode's "@ai-sdk/anthropic" adapter (e.g. ["github-copilot"] for Copilot Claude), i.e. the wire drops empty text/reasoning parts before the API exactly like the canonical Anthropic provider. Extends Magic Context's reasoning-strip machinery to those routes. Security: USER-only in the config loader; a project config cannot widen the gate. List a provider only if every Claude model under it uses the @ai-sdk/anthropic adapter. | | `smart_notes.retina_handoff` | boolean | `false` | When true, dreamer skips smart notes whose surface conditions compiled to retina provider configs at authoring time. Default false keeps both paths active until the retina consumer is deployed. | | `models.window_overlay_path` | string | — | | | `toast_duration_ms` | number (0–60000) | `5000` | TUI toast lifetime in milliseconds for Magic Context notifications. Set to 0 to disable Magic Context toasts entirely (min: 0, max: 60000, default: 5000) | diff --git a/packages/plugin/src/config/project-security.test.ts b/packages/plugin/src/config/project-security.test.ts index ba195e75c..eea1d488f 100644 --- a/packages/plugin/src/config/project-security.test.ts +++ b/packages/plugin/src/config/project-security.test.ts @@ -32,6 +32,15 @@ describe("stripUnsafeProjectConfigFields", () => { expect(warnings.some((w) => w.includes("auto_update"))).toBe(true); }); + it("strips anthropic_transport_providers from project config (transport gating is user-tier)", () => { + const raw: Record = { + anthropic_transport_providers: ["github-copilot"], + }; + const warnings = stripUnsafeProjectConfigFields(raw); + expect("anthropic_transport_providers" in raw).toBe(false); + expect(warnings.some((w) => w.includes("anthropic_transport_providers"))).toBe(true); + }); + it("strips fail_closed_blocking from project config (user-tier only)", () => { const raw: Record = { fail_closed_blocking: false, diff --git a/packages/plugin/src/config/project-security.ts b/packages/plugin/src/config/project-security.ts index 20a410151..276713b38 100644 --- a/packages/plugin/src/config/project-security.ts +++ b/packages/plugin/src/config/project-security.ts @@ -317,6 +317,15 @@ export function stripUnsafeProjectConfigFields(projectRaw: Record { expect(MagicContextConfigSchema.parse({ auto_update: true }).auto_update).toBe(true); }); + it("accepts an optional anthropic_transport_providers allow-list and normalizes it", () => { + const parsed = MagicContextConfigSchema.parse({ + anthropic_transport_providers: [" GitHub-Copilot "], + }); + expect(parsed.anthropic_transport_providers).toEqual(["github-copilot"]); + expect( + MagicContextConfigSchema.parse({}).anthropic_transport_providers, + ).toBeUndefined(); + }); + it("accepts an explicitly configured Pi subagent extension allowlist", () => { expect( MagicContextConfigSchema.parse({ diff --git a/packages/plugin/src/config/schema/magic-context.ts b/packages/plugin/src/config/schema/magic-context.ts index ef96f0fce..646a28f6b 100644 --- a/packages/plugin/src/config/schema/magic-context.ts +++ b/packages/plugin/src/config/schema/magic-context.ts @@ -751,6 +751,9 @@ export interface MagicContextConfig { /** Auto-update the cached OpenCode plugin wrapper when a newer npm version is available. * USER config only; project configs cannot disable it. Default: true. */ auto_update?: boolean; + /** ProviderIDs whose Claude models ride OpenCode's @ai-sdk/anthropic transport. + * USER config only; project configs cannot set it. See sentinel.modelAcceptsEmptyContent. */ + anthropic_transport_providers?: string[]; /** Output language for generated Magic Context prose. USER config only. */ language?: string; /** Active user-owned model profile after user/project resolution. */ @@ -957,6 +960,12 @@ export const MagicContextConfigSchema = z .describe( "Enable automatic npm self-update checks for the OpenCode plugin. Security: USER-only in config loader, so hostile project configs cannot suppress updates.", ), + anthropic_transport_providers: z + .array(z.string().trim().toLowerCase().min(1)) + .optional() + .describe( + 'ProviderIDs whose Claude-named models are served through OpenCode\'s "@ai-sdk/anthropic" adapter (e.g. ["github-copilot"] for Copilot Claude), i.e. the wire drops empty text/reasoning parts before the API exactly like the canonical Anthropic provider. Extends Magic Context\'s reasoning-strip machinery to those routes. Security: USER-only in the config loader; a project config cannot widen the gate. List a provider only if every Claude model under it uses the @ai-sdk/anthropic adapter.', + ), language: z .string() .trim() diff --git a/packages/plugin/src/hooks/magic-context/sentinel.test.ts b/packages/plugin/src/hooks/magic-context/sentinel.test.ts new file mode 100644 index 000000000..82b2fc19b --- /dev/null +++ b/packages/plugin/src/hooks/magic-context/sentinel.test.ts @@ -0,0 +1,55 @@ +/// + +import { afterEach, describe, expect, it } from "bun:test"; + +import { modelAcceptsEmptyContent, setAnthropicTransportProviders } from "./sentinel"; + +describe("modelAcceptsEmptyContent", () => { + afterEach(() => { + setAnthropicTransportProviders([]); + }); + + it("accepts the canonical anthropic provider regardless of model name", () => { + expect(modelAcceptsEmptyContent("anthropic")).toBe(true); + expect(modelAcceptsEmptyContent("anthropic", "claude-opus-4-7")).toBe(true); + expect(modelAcceptsEmptyContent("anthropic", "some-other-model")).toBe(true); + }); + + it("rejects unconfigured providers even for Claude-named models", () => { + expect(modelAcceptsEmptyContent("github-copilot")).toBe(false); + expect(modelAcceptsEmptyContent("github-copilot", "claude-sonnet-4-6")).toBe(false); + expect(modelAcceptsEmptyContent(undefined, "claude-sonnet-4-6")).toBe(false); + expect(modelAcceptsEmptyContent("openrouter")).toBe(false); + }); + + it("accepts Claude-named models under a configured Anthropic-transport provider", () => { + setAnthropicTransportProviders(["github-copilot"]); + expect(modelAcceptsEmptyContent("github-copilot", "claude-sonnet-4-6")).toBe(true); + expect(modelAcceptsEmptyContent("github-copilot", "CLAUDE-Opus-5")).toBe(true); + expect(modelAcceptsEmptyContent("GITHUB-COPILOT", "claude-sonnet-4-6")).toBe(true); + }); + + it("keeps non-Claude models under a configured provider fail-closed", () => { + // Mixed-adapter providers route non-Claude models through adapters that + // do NOT filter empty parts; the gate must stay closed for them. + setAnthropicTransportProviders(["github-copilot"]); + expect(modelAcceptsEmptyContent("github-copilot", "gpt-5.5")).toBe(false); + expect(modelAcceptsEmptyContent("github-copilot")).toBe(false); + expect(modelAcceptsEmptyContent("github-copilot", "")).toBe(false); + }); + + it("normalizes and validates configured provider entries", () => { + setAnthropicTransportProviders([" GitHub-Copilot ", "", " "]); + expect(modelAcceptsEmptyContent("github-copilot", "claude-x")).toBe(true); + expect(modelAcceptsEmptyContent("", "claude-x")).toBe(false); + }); + + it("resets to fail-closed when the allow-list is cleared", () => { + setAnthropicTransportProviders(["github-copilot"]); + setAnthropicTransportProviders([]); + expect(modelAcceptsEmptyContent("github-copilot", "claude-x")).toBe(false); + setAnthropicTransportProviders(["github-copilot"]); + setAnthropicTransportProviders(undefined); + expect(modelAcceptsEmptyContent("github-copilot", "claude-x")).toBe(false); + }); +}); diff --git a/packages/plugin/src/hooks/magic-context/sentinel.ts b/packages/plugin/src/hooks/magic-context/sentinel.ts index 63baede1f..d884805e0 100644 --- a/packages/plugin/src/hooks/magic-context/sentinel.ts +++ b/packages/plugin/src/hooks/magic-context/sentinel.ts @@ -31,9 +31,48 @@ export const WHOLE_MESSAGE_PLACEHOLDER_TEXT = "[dropped]"; * * Unknown or non-canonical providers therefore must keep native parts (or use * non-empty whole-message placeholders) rather than producing empty sentinels. + * + * One user-asserted exception exists: providerIDs listed in the user-tier + * `anthropic_transport_providers` setting serve Claude-named models through + * OpenCode's `@ai-sdk/anthropic` adapter as well, so the wire filter drops + * empty sentinels for them too (see isClaudeModelUnderConfiguredAnthropicTransport). + */ +export function modelAcceptsEmptyContent(providerID?: string, modelName?: string): boolean { + if (providerID === "anthropic") return true; + return isClaudeModelUnderConfiguredAnthropicTransport(providerID, modelName); +} + +let anthropicTransportProviders: ReadonlySet = new Set(); + +/** + * Populate the Anthropic-transport allow-list from the user-tier setting + * `anthropic_transport_providers` (see config entry point). Only providers + * whose Claude models ride OpenCode's `@ai-sdk/anthropic` adapter belong here: + * that adapter filters empty text/reasoning parts before the wire. A provider + * that mixes adapters (Claude models on @ai-sdk/anthropic, other models on an + * openai-compatible adapter) is safe to list because the gate additionally + * requires a Claude-named model. Fail-closed: unlisted providers, missing + * model names, and non-Claude model names keep native parts. */ -export function modelAcceptsEmptyContent(providerID?: string): boolean { - return providerID === "anthropic"; +export function setAnthropicTransportProviders(providerIDs?: Iterable): void { + const normalized = new Set(); + for (const id of providerIDs ?? []) { + if (typeof id === "string" && id.trim().length > 0) { + normalized.add(id.trim().toLowerCase()); + } + } + anthropicTransportProviders = normalized; +} + +function isClaudeModelUnderConfiguredAnthropicTransport( + providerID?: string, + modelName?: string, +): boolean { + if (!providerID || !modelName) return false; + return ( + anthropicTransportProviders.has(providerID.toLowerCase()) && + modelName.toLowerCase().includes("claude") + ); } /** diff --git a/packages/plugin/src/hooks/magic-context/strip-content.test.ts b/packages/plugin/src/hooks/magic-context/strip-content.test.ts index 823ff388a..ff8f34a26 100644 --- a/packages/plugin/src/hooks/magic-context/strip-content.test.ts +++ b/packages/plugin/src/hooks/magic-context/strip-content.test.ts @@ -1,6 +1,7 @@ /// import { beforeEach, describe, expect, it, mock } from "bun:test"; +import { setAnthropicTransportProviders } from "./sentinel"; import { clearOldReasoning, findLatestAssistantReasoningMutationExemptMessage, @@ -1184,7 +1185,7 @@ describe("strip-content", () => { expect(a2.parts[0]).toEqual({ type: "reasoning", text: "second reasoning" }); }); - it("#then is a no-op for github-copilot", () => { + it("#then is a no-op for github-copilot without a configured transport allow-list", () => { const u = message("m-u", "user", [{ type: "text", text: "hi" }]); const a1 = message("m-a1", "assistant", [{ type: "reasoning", text: "first" }]); const a2 = message("m-a2", "assistant", [{ type: "reasoning", text: "second" }]); @@ -1194,6 +1195,81 @@ describe("strip-content", () => { expect(stripped).toBe(0); }); + it("#then strips github-copilot Claude models when the provider is in anthropic_transport_providers", () => { + setAnthropicTransportProviders(["github-copilot"]); + try { + const u = message("m-u", "user", [{ type: "text", text: "hi" }]); + const a1 = message("m-a1", "assistant", [{ type: "reasoning", text: "first" }]); + const a2 = message("m-a2", "assistant", [ + { type: "reasoning", text: "second" }, + ]); + + const stripped = stripReasoningFromMergedAssistants( + [u, a1, a2], + "github-copilot", + { + modelName: "claude-sonnet-4-6", + }, + ); + + expect(stripped).toBe(1); + // The first assistant keeps its reasoning (index-0 rule); the + // second is replaced with an empty-text sentinel. + expect(a1.parts[0]).toEqual({ type: "reasoning", text: "first" }); + expect(a2.parts[0]).toEqual({ type: "text", text: "" }); + } finally { + setAnthropicTransportProviders([]); + } + }); + + it("#then keeps non-Claude models under a configured provider untouched", () => { + setAnthropicTransportProviders(["github-copilot"]); + try { + const u = message("m-u", "user", [{ type: "text", text: "hi" }]); + const a1 = message("m-a1", "assistant", [{ type: "reasoning", text: "first" }]); + const a2 = message("m-a2", "assistant", [ + { type: "reasoning", text: "second" }, + ]); + + const stripped = stripReasoningFromMergedAssistants( + [u, a1, a2], + "github-copilot", + { + modelName: "gpt-5.5", + }, + ); + + expect(stripped).toBe(0); + expect(a1.parts[0]).toEqual({ type: "reasoning", text: "first" }); + expect(a2.parts[0]).toEqual({ type: "reasoning", text: "second" }); + } finally { + setAnthropicTransportProviders([]); + } + }); + + it("#then findMergedReasoningStripCandidateIds follows the same gate", () => { + const u = message("m-u", "user", [{ type: "text", text: "hi" }]); + const a1 = message("m-a1", "assistant", [{ type: "reasoning", text: "first" }]); + const a2 = message("m-a2", "assistant", [{ type: "reasoning", text: "second" }]); + + expect( + findMergedReasoningStripCandidateIds([u, a1, a2], "github-copilot", { + modelName: "claude-sonnet-4-6", + }), + ).toEqual([]); + + setAnthropicTransportProviders(["github-copilot"]); + try { + expect( + findMergedReasoningStripCandidateIds([u, a1, a2], "github-copilot", { + modelName: "claude-sonnet-4-6", + }), + ).toEqual(["m-a2"]); + } finally { + setAnthropicTransportProviders([]); + } + }); + it("#then runs normally for providerID === 'anthropic'", () => { const u = message("m-u", "user", [{ type: "text", text: "hi" }]); const a1 = message("m-a1", "assistant", [ diff --git a/packages/plugin/src/hooks/magic-context/strip-content.ts b/packages/plugin/src/hooks/magic-context/strip-content.ts index 98e1f2529..f4abd94cc 100644 --- a/packages/plugin/src/hooks/magic-context/strip-content.ts +++ b/packages/plugin/src/hooks/magic-context/strip-content.ts @@ -1,5 +1,10 @@ import { isRecord } from "../../shared/record-type-guard"; -import { isSentinel, makeSentinel, makeWholeMessageSentinel } from "./sentinel"; +import { + isSentinel, + makeSentinel, + makeWholeMessageSentinel, + modelAcceptsEmptyContent, +} from "./sentinel"; import type { MessageLike, ThinkingLikePart } from "./tag-messages"; const DROPPED_PLACEHOLDER_PATTERN = /^\[dropped §\d+§\]$/; @@ -797,9 +802,9 @@ export function applyFrozenTrailingBlankDecisions( export function findMergedReasoningStripCandidateIds( messages: MessageLike[], providerID?: string, - options?: { mutationExemptMessage?: MessageLike }, + options?: { mutationExemptMessage?: MessageLike; modelName?: string }, ): string[] { - if (providerID !== "anthropic") return []; + if (!modelAcceptsEmptyContent(providerID, options?.modelName)) return []; const ids = new Set(); for (const entry of planMergedAssistantReasoningStrip( @@ -861,8 +866,10 @@ export function stripReasoningFromAssistantIds( messages: MessageLike[], providerID: string | undefined, messageIds: ReadonlySet, + modelName?: string, ): number { - if (providerID !== "anthropic" || messageIds.size === 0) return 0; + if (messageIds.size === 0) return 0; + if (!modelAcceptsEmptyContent(providerID, modelName)) return 0; let stripped = 0; for (const message of messages) { const id = message.info.id; @@ -883,15 +890,18 @@ export function stripReasoningFromMergedAssistants( options?: { mutationExemptMessage?: MessageLike; frozenMessageIds?: ReadonlySet; + modelName?: string; }, ): number { - // Anthropic-only workaround for @ai-sdk/anthropic's groupIntoBlocks - // index-0-thinking rule. openai-compatible providers like Kimi/ - // Moonshot enforce the opposite invariant (every tool-call assistant - // must have non-empty `reasoning_content`), so the strip would - // trigger 400 "reasoning_content is missing" there. See call site - // in transform.ts for the full rationale. - if (providerID !== "anthropic") return 0; + // Workaround for @ai-sdk/anthropic's groupIntoBlocks index-0-thinking + // rule. Besides the canonical provider, user-configured Anthropic-transport + // providers running Claude models (see sentinel.ts) hit the same signed- + // block contract. OpenAI-compatible non-Claude models like Kimi/Moonshot + // enforce the opposite invariant (every tool-call assistant must have + // non-empty `reasoning_content`), so they stay excluded — the strip would + // trigger 400 "reasoning_content is missing" there. See the call site in + // transform.ts for the full rationale. + if (!modelAcceptsEmptyContent(providerID, options?.modelName)) return 0; let stripped = 0; for (const entry of planMergedAssistantReasoningStrip( diff --git a/packages/plugin/src/hooks/magic-context/transform-postprocess-phase.ts b/packages/plugin/src/hooks/magic-context/transform-postprocess-phase.ts index 865624431..4bf03089b 100644 --- a/packages/plugin/src/hooks/magic-context/transform-postprocess-phase.ts +++ b/packages/plugin/src/hooks/magic-context/transform-postprocess-phase.ts @@ -679,6 +679,14 @@ interface RunPostTransformPhaseArgs { * cannot diverge from the main transform on cold DB-recovered passes. */ resolvedProviderID?: string; + /** + * Model identifier resolved once by the main transform for this pass. + * Extends the empty-sentinel gate to Claude models served under + * user-configured Anthropic-transport providerIDs (see + * sentinel.modelAcceptsEmptyContent); call sites without a model name + * stay canonical-only. + */ + resolvedModelName?: string; /** True only when the live request is canonical Anthropic Fable 5.1. */ thinkingBindingRecoveryEnabledForModel?: boolean; /** Raw harness observations captured before any Magic Context insertion or sentinelization. */ @@ -815,10 +823,11 @@ export function finalizeMessageRepresentation( trailingBlankDecisions?: ReadonlyMap; skipMergedReasoningStrip?: boolean; skipTrailingWhitespaceStrip?: boolean; + resolvedModelName?: string; }, ): { clearedParts: number; mergedReasoningParts: number } { let clearedParts = 0; - if (modelAcceptsEmptyContent(resolvedProviderID)) { + if (modelAcceptsEmptyContent(resolvedProviderID, options?.resolvedModelName)) { const prependedMessageCount = Math.min( messages.length, Math.max(0, options?.prependedMessageCount ?? 0), @@ -852,6 +861,7 @@ export function finalizeMessageRepresentation( messages, resolvedProviderID, options?.thinkingBindingRecoveryMessageIds ?? new Set(), + options?.resolvedModelName, ); const mergedReasoningParts = bindingRecoveryParts + @@ -860,8 +870,12 @@ export function finalizeMessageRepresentation( : stripReasoningFromMergedAssistants(messages, resolvedProviderID, { mutationExemptMessage: options?.reasoningMutationExemptMessage, frozenMessageIds: options?.mergedReasoningStrippedIds, + modelName: options?.resolvedModelName, })); - if (!options?.skipTrailingWhitespaceStrip && modelAcceptsEmptyContent(resolvedProviderID)) { + if ( + !options?.skipTrailingWhitespaceStrip && + modelAcceptsEmptyContent(resolvedProviderID, options?.resolvedModelName) + ) { applyFrozenTrailingBlankDecisions( messages, typeof newestAssistant?.info.id === "string" ? newestAssistant.info.id : undefined, @@ -1168,7 +1182,10 @@ export async function runPostTransformPhase( sessionLog(args.sessionId, "ctx_reduce permission read failed (ignored):", error); } } - const canUseEmptySentinels = modelAcceptsEmptyContent(args.resolvedProviderID); + const canUseEmptySentinels = modelAcceptsEmptyContent( + args.resolvedProviderID, + args.resolvedModelName, + ); if (shouldRunHeuristics) { const subagentRerun = !args.fullFeatureMode && @@ -2269,7 +2286,10 @@ export async function runPostTransformPhase( const candidates = findMergedReasoningStripCandidateIds( args.messages, args.resolvedProviderID, - { mutationExemptMessage: reasoningMutationExemptMessage }, + { + mutationExemptMessage: reasoningMutationExemptMessage, + modelName: args.resolvedModelName, + }, ); const newlyDetectedIds = candidates.filter( (id) => !mergedReasoningStrippedIds.has(id), @@ -2380,6 +2400,7 @@ export async function runPostTransformPhase( reasoningMutationExemptMessage, trailingBlankNewestAssistant, mergedReasoningStrippedIds, + resolvedModelName: args.resolvedModelName, thinkingBindingRecoveryMessageIds, trailingBlankDecisions, skipMergedReasoningStrip: compactionOff, diff --git a/packages/plugin/src/hooks/magic-context/transform.ts b/packages/plugin/src/hooks/magic-context/transform.ts index 261664539..743df110d 100644 --- a/packages/plugin/src/hooks/magic-context/transform.ts +++ b/packages/plugin/src/hooks/magic-context/transform.ts @@ -2250,10 +2250,13 @@ export function createTransform(deps: TransformDeps) { // the primary agent that spawned them. cavemanTextCompression: !reducedMode ? deps.cavemanTextCompression : undefined, smartDrops: deps.smartDrops === true, - // Pass the single resolved provider through to postprocess so every - // empty-sentinel gate and whole-message placeholder choice agrees for - // this transform pass, including cold DB-recovered passes. + // Pass the single resolved provider + model through to postprocess + // so every empty-sentinel gate and whole-message placeholder choice + // agrees for this transform pass, including cold DB-recovered passes. + // The model name extends the gate to Claude models served under + // user-configured Anthropic-transport providerIDs. resolvedProviderID, + resolvedModelName: modelForBudget?.modelID, thinkingBindingRecoveryEnabledForModel: isFable51ThinkingBindingModel( modelForBudget?.providerID, modelForBudget?.modelID, diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 8d2e45cde..0ff13218e 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -42,6 +42,7 @@ import { createLiveSessionState } from "./hooks/magic-context/live-session-state import { SubcModuleTransport } from "./hooks/magic-context/module-transport"; import { preloadTokenizer } from "./hooks/magic-context/read-session-formatting"; import type { RustModeModuleClient } from "./hooks/magic-context/rust-mode-transform"; +import { setAnthropicTransportProviders } from "./hooks/magic-context/sentinel"; import { beginBootQuietPeriod, scheduleAfterBootQuiet } from "./plugin/boot-quiet"; import { cleanupConflictWarnings, sendConflictWarning } from "./plugin/conflict-warning-hook"; import { startDreamScheduleTimer } from "./plugin/dream-timer"; @@ -109,6 +110,10 @@ const server: Plugin = async (ctx) => { // Debug data-collection toggle: when on, keep subagent child sessions // (historian/dreamer/sidekick/migration) instead of deleting on success. setKeepSubagents(pluginConfig.keep_subagents === true); + // Anthropic-transport allow-list (user-tier `anthropic_transport_providers`): + // extends the empty-sentinel gate to Claude models served through listed + // providerIDs whose wire is OpenCode's @ai-sdk/anthropic adapter. + setAnthropicTransportProviders(pluginConfig.anthropic_transport_providers); const autoUpdateAbort = new AbortController(); // Abort on process exit via the shared single-listener registry. Registering // a process.once("exit") here directly would add one listener PER plugin