Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,19 @@ reviews:
instructions: >-
Act as an adversarial second-opinion reviewer. Verify PR claims against implementation and
contracts. Trace changed inputs through normal, boundary, error, cancellation, retry, and
default paths and their consumers. Seek plausible counterexamples and regressions from removed
safeguards. Identify assumptions in changed code that depend on facts outside the diff. First
verify repository conventions, tests, and related implementations. When a potential finding
depends on external behavior, use web search and prefer official documentation, specifications,
or upstream repositories. Report only concrete, actionable conflicts or failure modes, citing
the relevant repository location or external source. Prioritize correctness, security, data loss,
lifecycle, and test gaps. Do not report generic best practices, unsupported concerns, speculative
style comments, or unrelated refactors. When changed code introduces a local implementation of a
cross-cutting concern, check whether it bypasses or duplicates an established repository abstraction
or nearby convention. Report only a concrete inconsistency with behavioral or maintenance impact,
and allow intentional deviations.
default paths and their direct test counterparts. Do not flag defects in files not changed by
this PR unless the defect is directly triggered by changed code and cannot be detected in the
changed file alone. Seek plausible counterexamples and regressions from removed safeguards.
Identify assumptions in changed code that depend on facts outside the diff. First verify
repository conventions, tests, and related implementations. When a potential finding depends on
external behavior, use web search and prefer official documentation, specifications, or upstream
repositories. Report only concrete, actionable conflicts or failure modes, citing the relevant
repository location or external source. Prioritize correctness, security, data loss, lifecycle,
and test gaps. Do not report generic best practices, unsupported concerns, speculative style
comments, or unrelated refactors. When changed code introduces a local implementation of a
cross-cutting concern, check whether it bypasses or duplicates an established repository
abstraction or nearby convention. Report only a concrete inconsistency with behavioral or
maintenance impact, and allow intentional deviations.

- path: "**/*.{ts,tsx,js,jsx,mts,mjs,cts,cjs}"
instructions: >-
Expand Down
22 changes: 13 additions & 9 deletions .github/workflows/code-qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ jobs:
node src/scripts/verify-lcov.mjs src/coverage/services/lcov.info
node src/scripts/verify-lcov.mjs src/coverage/misc/lcov.info
node src/scripts/verify-lcov.mjs src/coverage/tree-sitter/lcov.info
- name: Merge extension coverage reports
run: |
mkdir -p src/coverage/merged
pnpm --dir src run merge:coverage
node src/scripts/verify-lcov.mjs src/coverage/merged/lcov.info
# Validate cache boundaries before publishing any new Turbo entries.
- name: Verify coverage cache inputs
run: pnpm --dir src run verify:coverage-cache-inputs
- name: Save Turbo cache
if: steps.turbo-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
Expand All @@ -184,21 +192,16 @@ jobs:
# there mostly adds Codecov overhead without changing pass/fail
# behavior.
# Coverage is uploaded in separate steps so each LCOV gets the
# correct flag set. Codecov double-counts overlapping lines when a
# single upload carries multiple flags whose paths overlap, so the
# core lanes and webview lane must be uploaded individually with
# their own flag.
# correct flag set. Extension lanes instrument the same sources, so
# union them before upload; a line is covered when any lane executes
# it. Core and webview reports retain their independent flags.
# See https://docs.codecov.com/docs/flags
- name: Upload non-core coverage to Codecov
if: matrix.upload-coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: >-
src/coverage/api/lcov.info,
src/coverage/core/lcov.info,
src/coverage/services/lcov.info,
src/coverage/misc/lcov.info,
src/coverage/tree-sitter/lcov.info,
src/coverage/merged/lcov.info,
packages/cloud/coverage/lcov.info,
packages/telemetry/coverage/lcov.info,
apps/cli/coverage/lcov.info
Expand Down Expand Up @@ -240,6 +243,7 @@ jobs:
src/coverage/services/lcov.info
src/coverage/misc/lcov.info
src/coverage/tree-sitter/lcov.info
src/coverage/merged/lcov.info
webview-ui/coverage/lcov.info
packages/cloud/coverage/lcov.info
packages/telemetry/coverage/lcov.info
Expand Down
12 changes: 3 additions & 9 deletions .github/workflows/mutation-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,6 @@ jobs:
fetch-depth: 0
persist-credentials: false

- name: Fetch pull request base
if: github.event_name == 'pull_request'
env:
BASE_REPOSITORY_URL: ${{ github.server_url }}/${{ github.repository }}.git
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: git fetch --no-tags "$BASE_REPOSITORY_URL" "$BASE_SHA"

- name: Setup Node.js and pnpm
if: github.event_name == 'pull_request'
uses: ./.github/actions/setup-node-pnpm
Expand All @@ -56,9 +49,10 @@ jobs:
- name: Enforce executable-line scope and run advisory mutation testing
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.sha }}
run: node scripts/stryker-diff.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA"
run: |
BASE_SHA="$(git rev-parse "$HEAD_SHA^1")"
node scripts/stryker-diff.mjs ci --base "$BASE_SHA" --head "$HEAD_SHA"

- name: Upload mutation reports
id: mutation_report
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"check-types": "turbo check-types --log-order grouped --output-logs new-only",
"test": "turbo test --log-order grouped --output-logs new-only",
"test:mutation-ci": "node --test scripts/stryker-diff.test.mjs",
"lifecycle:model-check": "tsx scripts/check-task-lifecycle.ts && tsx scripts/check-task-store-concurrency.ts && tsx scripts/check-provider-handoff-scheduler.ts && pnpm cleanup-protocol:model-check && pnpm parser-scope:model-check && tsx scripts/check-completion-persistence.ts",
"lifecycle:model-check": "tsx scripts/check-task-lifecycle.ts && tsx scripts/check-task-store-concurrency.ts && tsx scripts/check-provider-handoff-scheduler.ts && pnpm cleanup-protocol:model-check && pnpm parser-scope:model-check && tsx scripts/check-completion-persistence.ts && tsx scripts/check-delegated-mode-readers.ts",
"cleanup-protocol:model-check": "tsx scripts/check-task-cleanup-protocol.ts",
"parser-scope:model-check": "node scripts/run-native-tool-call-parser-scoping.mjs",
"mcp:integration-check": "tsx scripts/check-mcp-oauth-integration.ts",
Expand Down
20 changes: 15 additions & 5 deletions packages/types/src/__tests__/deepseek-v4-pro.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ describe("DeepSeek V4 Pro 0813 provider catalogs", () => {
expect(model?.contextWindow).toBeGreaterThanOrEqual(1_000_000)
})

it("uses peak first-party pricing and unchanged OpenCode Go pricing", () => {
it("uses current peak first-party pricing and unchanged OpenCode Go pricing", () => {
expect(deepSeekModels["deepseek-flash"]).toMatchObject({
supportsImages: true,
outputPrice: 1.2,
cacheWritesPrice: 0.3,
cacheReadsPrice: 0.006,
})
expect(deepSeekModels["deepseek-v4-flash"]).toMatchObject({
supportsImages: false,
outputPrice: 1.32,
cacheWritesPrice: 0.44,
cacheReadsPrice: 0.014,
supportsImages: true,
outputPrice: 1.2,
cacheWritesPrice: 0.3,
cacheReadsPrice: 0.006,
})
expect(deepSeekModels["deepseek-v4-pro"].supportsImages).toBe(false)
expect(deepSeekModels["deepseek-v4-pro"]).toMatchObject({
Expand All @@ -42,6 +48,10 @@ describe("DeepSeek V4 Pro 0813 provider catalogs", () => {
expect(model.supportsPromptCache).toBe(true)
expect(model.contextWindow).toBeGreaterThanOrEqual(1_000_000)
expect(model.supportsReasoningEffort).toEqual(["disable", "low", "high", "max"])
expect(model).toMatchObject({ outputPrice: 1.2, cacheWritesPrice: 0.3, cacheReadsPrice: 0.006 })
expect(model.description).toContain("Legacy model name")
expect(model).not.toHaveProperty("supportsTemperature")
expect(model).not.toHaveProperty("defaultTemperature")
})

// Self-hosted providers retain separate IDs for the preview weights and 0813 checkpoint.
Expand Down
41 changes: 27 additions & 14 deletions packages/types/src/providers/deepseek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,38 @@ import type { ModelInfo } from "../model.js"
// continuation within the same turn. See: https://api-docs.deepseek.com/guides/thinking_mode
export type DeepSeekModelId = keyof typeof deepSeekModels

export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-v4-flash"
export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-flash"

export const deepSeekModels = {
"deepseek-flash": {
maxTokens: 384_000,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["disable", "low", "high", "max"], // Updated 2026-09-10
preserveReasoning: true,
reasoningEffort: "high",
inputPrice: 0, // the inputs are priced as cache read/write, so `inputPrice` should be 0
// Static estimates use peak rates; off-peak rates are 50% lower. Effective 2026-09-10.
outputPrice: 1.2,
cacheWritesPrice: 0.3,
cacheReadsPrice: 0.006,
description: `DeepSeek-V4.1-Flash is DeepSeek's fast multimodal model with image understanding. It supports thinking and non-thinking modes, JSON output, tool calls, chat prefix completion (beta), and FIM completion (beta) in non-thinking mode.`,
},
"deepseek-v4-flash": {
maxTokens: 384_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: ["disable", "low", "high", "max"], // Updated 2026-08-13
preserveReasoning: true,
reasoningEffort: "high",
inputPrice: 0, // the inputs are priced as cache read/write, so `inputPrice` should be 0
// Static estimates use peak rates; off-peak rates are 50% lower. Effective 2026-08-16.
outputPrice: 1.32,
cacheWritesPrice: 0.44,
cacheReadsPrice: 0.014,
description: `DeepSeek-V4-Flash is DeepSeek's fast, cost-efficient V4 model. It supports thinking and non-thinking modes, JSON output, tool calls, chat prefix completion (beta), and FIM completion (beta) in non-thinking mode.`,
// This retired ID is billed as the current Flash model.
outputPrice: 1.2,
cacheWritesPrice: 0.3,
cacheReadsPrice: 0.006,
description: `Legacy model name routed to the latest DeepSeek Flash model, which supports image input. Use deepseek-flash for new configurations.`,
},
"deepseek-v4-pro": {
displayName: "DeepSeek V4 Pro 0813",
Expand All @@ -49,14 +64,12 @@ export const deepSeekModels = {
supportsReasoningEffort: ["disable", "low", "high", "max"], // Updated 2026-08-13
preserveReasoning: true,
reasoningEffort: "high",
supportsTemperature: true,
defaultTemperature: 1.0,
inputPrice: 0, // the inputs are priced as cache read/write, so `inputPrice` should be 0
// Static estimates use peak rates; off-peak rates are 50% lower.
outputPrice: 1.32,
cacheWritesPrice: 0.44,
cacheReadsPrice: 0.014,
description: `DeepSeek-V4-Flash-Vision-Exp is DeepSeek's experimental multimodal V4 Flash model with image understanding. It supports thinking and non-thinking modes, JSON output, tool calls, chat prefix completion (beta), and image input through Chat Completions, Responses, and Anthropic-compatible APIs.`,
// This retired ID is billed as the current Flash model.
outputPrice: 1.2,
cacheWritesPrice: 0.3,
cacheReadsPrice: 0.006,
description: `Legacy model name routed to the latest DeepSeek Flash model, which supports image input. Use deepseek-flash for new configurations.`,
},
} as const satisfies Record<string, ModelInfo>

Expand Down
170 changes: 170 additions & 0 deletions scripts/check-delegated-mode-readers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// check-delegated-mode-readers.ts
//
// Refinement check for the delegated-child mode-reader invariant (issue #1623).
//
// check-provider-handoff-scheduler.ts verifies the write side: that
// selectHandoffExecutionContext stores the task-local mode correctly.
// This script verifies the read side: that the mode observable by
// tool-validation readers is the task-local mode, not the shared provider mode.
//
// The VS Code-dependent readers (getEnvironmentDetails,
// presentAssistantMessage) are covered by their vitest regression tests.
// This script covers the pure-TS parts of the invariant chain and proves
// that the two sources of mode are observably different, so any reader
// that uses the wrong source silently produces wrong behavior.
//
// Invariant: for any delegated child task C with taskMode = M,
// toolAllowedForMode(tool, M) ≠ toolAllowedForMode(tool, providerMode)
// whenever M ≠ providerMode and the two modes differ on the tool's group.

import assert from "node:assert/strict"

import { DEFAULT_MODES } from "../packages/types/src/mode"

import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, TOOL_ALIASES } from "../src/shared/tools"
import { selectHandoffExecutionContext, type TaskExecutionContext } from "../src/core/task/providerHandoff"

// ---------------------------------------------------------------------------
// Minimal inline mode-allows-tool check.
// Avoids importing src/shared/modes.ts, which pulls in VS Code.
// Only covers built-in modes (no custom modes, no file-regex options).
// That is enough to prove the behavioral divergence this check needs.
// ---------------------------------------------------------------------------

type ModeConfig = (typeof DEFAULT_MODES)[number]
type GroupEntry = ModeConfig["groups"][number]

function groupName(entry: GroupEntry): string {
return Array.isArray(entry) ? entry[0] : (entry as string)
}

function toolAllowedForMode(tool: string, modeSlug: string): boolean {
const resolvedTool = (TOOL_ALIASES as Record<string, string>)[tool] ?? tool
if ((ALWAYS_AVAILABLE_TOOLS as readonly string[]).includes(resolvedTool)) return true
const mode = DEFAULT_MODES.find((m) => m.slug === modeSlug)
if (!mode) return false
for (const entry of mode.groups) {
const groupTools = (TOOL_GROUPS as Record<string, { tools: readonly string[] }>)[groupName(entry)]?.tools ?? []
if (groupTools.includes(resolvedTool)) return true
}
return false
}

// ---------------------------------------------------------------------------
// Scenario: parent in "orchestrator" mode delegates child to "code".
// Regression behavior: both readers used providerMode ("orchestrator").
// Correct behavior: readers use taskMode ("code").
//
// orchestrator groups: [] → apply_diff blocked
// code groups: [...edit] → apply_diff allowed
// ---------------------------------------------------------------------------

const parentCtx: TaskExecutionContext = {
mode: "orchestrator",
apiConfigName: undefined,
apiConfiguration: { apiProvider: "anthropic", consecutiveMistakeLimit: 3 },
}

// 1. Handoff stores the task-local mode, not the parent mode.
const childCtx = selectHandoffExecutionContext(parentCtx, "code", parentCtx.mode, false, undefined)
assert.equal(childCtx.mode, "code", "handoff must store the requested task-local mode")
assert.notEqual(childCtx.mode, parentCtx.mode, "test scenario requires divergent provider and task modes")

// 2. The two modes produce observably different tool-validation outcomes.
assert.equal(toolAllowedForMode("apply_diff", "orchestrator"), false, "orchestrator has no edit group")
assert.equal(toolAllowedForMode("apply_diff", "code"), true, "code has the edit group")

// 3. Regression claim: a reader that consumes providerMode rejects apply_diff;
// a reader that consumes taskMode correctly allows it.
const viaProviderMode = toolAllowedForMode("apply_diff", parentCtx.mode) // "orchestrator" — wrong source
const viaTaskMode = toolAllowedForMode("apply_diff", childCtx.mode) // "code" — correct source
assert.equal(viaProviderMode, false, "stale provider mode rejects apply_diff (regression behavior)")
assert.equal(viaTaskMode, true, "task-local mode allows apply_diff (correct behavior)")

// 4. Additional mode pairs that show the same divergence.
const DIVERGENT_PAIRS: Array<{
label: string
providerMode: string
taskMode: string
probe: string
blockedInProvider: boolean
allowedInTask: boolean
}> = [
// orchestrator → code: edit tools blocked at provider level, allowed at task level
{
label: "orchestrator→code apply_diff",
providerMode: "orchestrator",
taskMode: "code",
probe: "apply_diff",
blockedInProvider: true,
allowedInTask: true,
},
// orchestrator → code: command tools blocked at provider level, allowed at task level
{
label: "orchestrator→code execute_command",
providerMode: "orchestrator",
taskMode: "code",
probe: "execute_command",
blockedInProvider: true,
allowedInTask: true,
},
// code → ask: edit tools allowed at provider level, blocked at task level
{
label: "code→ask apply_diff",
providerMode: "code",
taskMode: "ask",
probe: "apply_diff",
blockedInProvider: false,
allowedInTask: false,
},
// ask → code: edit tools blocked at provider level, allowed at task level
{
label: "ask→code write_to_file",
providerMode: "ask",
taskMode: "code",
probe: "write_to_file",
blockedInProvider: true,
allowedInTask: true,
},
]

for (const pair of DIVERGENT_PAIRS) {
const ctx = selectHandoffExecutionContext(
{ ...parentCtx, mode: pair.providerMode },
pair.taskMode,
pair.providerMode,
false,
undefined,
)
assert.equal(ctx.mode, pair.taskMode, `${pair.label}: handoff must store task-local mode`)
assert.equal(
toolAllowedForMode(pair.probe, pair.providerMode),
!pair.blockedInProvider,
`${pair.label}: wrong provider-mode result`,
)
assert.equal(
toolAllowedForMode(pair.probe, pair.taskMode),
pair.allowedInTask,
`${pair.label}: wrong task-mode result`,
)
// The two sources disagree, so using the wrong one is always observable.
assert.notEqual(
toolAllowedForMode(pair.probe, pair.providerMode),
toolAllowedForMode(pair.probe, pair.taskMode),
`${pair.label}: provider and task mode must differ on this probe tool`,
)
}

// 5. For every built-in mode as a delegation target: selectHandoffExecutionContext
// always stores the requested mode, regardless of parent mode.
for (const mode of DEFAULT_MODES) {
const ctx = selectHandoffExecutionContext(parentCtx, mode.slug, parentCtx.mode, false, undefined)
assert.equal(ctx.mode, mode.slug, `handoff must store ${mode.slug}, not parent mode ${parentCtx.mode}`)
}

console.log(
`Delegated mode reader check passed: ` +
`regression scenario verified, ` +
`${DIVERGENT_PAIRS.length} divergent-mode pairs checked, ` +
`${DEFAULT_MODES.length}/${DEFAULT_MODES.length} built-in modes verified`,
)
Loading