diff --git a/src/core/transport.ts b/src/core/transport.ts index 1efbb85..0cab2e2 100644 --- a/src/core/transport.ts +++ b/src/core/transport.ts @@ -42,6 +42,12 @@ export interface StreamRequestOptions { * - "always" — strip for every model */ stripThinkTags?: "never" | "auto" | "always"; + /** + * Force think-tag stripping regardless of stripThinkTags mode. + * Set to true when the request is a subagent/tool-call invocation + * to prevent tags in content from rendering as blank code blocks. + */ + forceStripThinkTags?: boolean; } export interface TransportRequestSummary { diff --git a/src/provider/OpenCodeProvider.ts b/src/provider/OpenCodeProvider.ts index 96d80a0..237c1e8 100644 --- a/src/provider/OpenCodeProvider.ts +++ b/src/provider/OpenCodeProvider.ts @@ -901,6 +901,11 @@ export class OpenCodeProvider implements vscode.LanguageModelChatProvider tags + // in content from rendering as blank code blocks in the chat UI. + const isToolCallRequest = Array.isArray(options.tools) && options.tools.length > 0; + const forceStripThinkTags = isToolCallRequest || undefined; if (routing.endpointKind === "messages") { await runStreamAnthropicMessages({ @@ -921,6 +926,7 @@ export class OpenCodeProvider implements vscode.LanguageModelChatProvider { this.storeReasoningContent(toolCallIds, reasoningContent); }, @@ -971,6 +978,7 @@ export class OpenCodeProvider implements vscode.LanguageModelChatProvider { this.storeReasoningContent(toolCallIds, reasoningContent); }, @@ -997,6 +1005,7 @@ export class OpenCodeProvider implements vscode.LanguageModelChatProvider { this.storeReasoningContent(toolCallIds, reasoningContent); diff --git a/src/transports/anthropic.ts b/src/transports/anthropic.ts index 2318286..01e5b8e 100644 --- a/src/transports/anthropic.ts +++ b/src/transports/anthropic.ts @@ -6,7 +6,7 @@ import { extractAnthropicParts } from "./extract"; /** Anthropic Messages API transport (Claude-family / MiniMax m2.x / Qwen 3.x-plus). */ export async function streamAnthropicMessages(options: StreamRequestOptions): Promise { - const thinkFilter = createThinkTagFilter(options.stripThinkTags, options.modelId); + const thinkFilter = createThinkTagFilter(options.stripThinkTags, options.modelId, options.forceStripThinkTags); const extractor = new AnthropicResponseExtractor( options.onReasoningContent, createReasoningDebugger(options.output, options.debugReasoning), diff --git a/src/transports/chatCompletions.ts b/src/transports/chatCompletions.ts index 64afce1..c10fcab 100644 --- a/src/transports/chatCompletions.ts +++ b/src/transports/chatCompletions.ts @@ -8,7 +8,7 @@ import { reportProgressPart } from "./streamParts"; /** OpenAI-compatible chat-completions transport. */ export async function streamChatCompletions(options: StreamRequestOptions): Promise { - const thinkFilter = createThinkTagFilter(options.stripThinkTags, options.modelId); + const thinkFilter = createThinkTagFilter(options.stripThinkTags, options.modelId, options.forceStripThinkTags); // Display decision: whether `reasoning_content` should be surfaced as visible // text instead of a thinking part. Computed UPSTREAM by the thinking provider // strategy from the resolved thinking config — not inferred from the body diff --git a/src/transports/google.ts b/src/transports/google.ts index 14c2ab5..715c0ad 100644 --- a/src/transports/google.ts +++ b/src/transports/google.ts @@ -7,7 +7,7 @@ import { extractChatCompletionParts } from "./extract"; /** Google Generative Language API transport (Gemini via Zen). */ export async function streamGoogleGenerateContent(options: StreamRequestOptions): Promise { - const thinkFilter = createThinkTagFilter(options.stripThinkTags, options.modelId); + const thinkFilter = createThinkTagFilter(options.stripThinkTags, options.modelId, options.forceStripThinkTags); const extractor = new OpenAiResponseExtractor( options.onReasoningContent, createReasoningDebugger(options.output, options.debugReasoning), diff --git a/src/transports/responses.ts b/src/transports/responses.ts index 1d9204b..b20df01 100644 --- a/src/transports/responses.ts +++ b/src/transports/responses.ts @@ -7,7 +7,7 @@ import { extractChatCompletionParts } from "./extract"; /** OpenAI Responses API transport (GPT-family models). */ export async function streamResponsesApi(options: StreamRequestOptions): Promise { - const thinkFilter = createThinkTagFilter(options.stripThinkTags, options.modelId); + const thinkFilter = createThinkTagFilter(options.stripThinkTags, options.modelId, options.forceStripThinkTags); const extractor = new OpenAiResponseExtractor( options.onReasoningContent, createReasoningDebugger(options.output, options.debugReasoning), diff --git a/src/transports/thinkTags.ts b/src/transports/thinkTags.ts index 784bf1f..195f4b9 100644 --- a/src/transports/thinkTags.ts +++ b/src/transports/thinkTags.ts @@ -32,8 +32,13 @@ export function shouldStripThinkTags(mode: "never" | "auto" | "always" | undefin return /^minimax-m/i.test(modelId); } -export function createThinkTagFilter(mode: "never" | "auto" | "always" | undefined, modelId: string): ThinkTagFilter | undefined { - return shouldStripThinkTags(mode, modelId) ? new ThinkTagFilter() : undefined; +export function createThinkTagFilter( + mode: "never" | "auto" | "always" | undefined, + modelId: string, + forceOverride?: boolean, +): ThinkTagFilter | undefined { + const effective = forceOverride ? "always" : mode; + return shouldStripThinkTags(effective, modelId) ? new ThinkTagFilter() : undefined; } export class ThinkTagFilter {