fix(batch): enforce delivery boundaries and normalize thinking overrides

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
yiliang114 2026-09-25 15:45:31 +08:00
parent 16e0b67b7a
commit 8560fe95f2
4 changed files with 88 additions and 24 deletions

View file

@ -163,6 +163,20 @@ describe('assembleRequests', () => {
'enable_thinking',
);
});
it('enables tiered thinking when the frozen model settings disabled it', () => {
const request = freezeRequest({ reasoning: false }, 'qwen3.8-max');
const body = assembleRequests(
{ ...plan, enableThinking: true },
[item()],
1,
root,
'qwen3.8-max',
request,
)[0].line['body'];
expect(body).not.toHaveProperty('reasoning_effort');
expect(body).not.toHaveProperty('enable_thinking');
expect(request.params).toEqual({ reasoning_effort: 'none' });
});
});
describe('freezeRequest', () => {
@ -212,6 +226,18 @@ describe('freezeRequest', () => {
});
});
it('removes a disabling effort from thinking-mandatory models', () => {
const request = freezeRequest(
{
thinkingMandatory: true,
extra_body: { reasoning_effort: 'none' },
},
'qwen3.8-max',
);
expect(request.params).not.toHaveProperty('reasoning_effort');
expect(request.notes.join()).toMatch(/cannot be disabled/);
});
it('resolves the thinking switch with the realtime precedence', () => {
// extra_body is merged last on the realtime wire, so it wins.
expect(
@ -501,6 +527,26 @@ describe('deliverResult', () => {
}
});
it('holds a visible symlink into a hidden directory before creating children', () => {
fs.mkdirSync(path.join(root, '.qwen'));
fs.symlinkSync(
path.join(root, '.qwen'),
path.join(root, 'output'),
'junction',
);
const outcome = deliverResult(
item({ target: 'output/new/settings.json' }),
content,
root,
sourceHash,
);
expect(outcome).toMatchObject({
kind: 'held',
reason: expect.stringContaining('hidden path'),
});
expect(fs.existsSync(path.join(root, '.qwen', 'new'))).toBe(false);
});
it('holds without creating directories outside a symlinked parent', () => {
// `linked/new/` does not exist yet: containment must be proven at the
// nearest existing ancestor before mkdir creates anything outside.

View file

@ -18,7 +18,7 @@ import {
} from '@qwen-code/qwen-code-core/core/modalityDefaults.js';
import { isWithinRoot } from '../config/path-comparison.js';
import type { BatchPlan, TaskItem } from './batch-task.js';
import { customIdOf } from './batch-task.js';
import { customIdOf, targetProblem } from './batch-task.js';
export const sha256 = (text: string) =>
crypto.createHash('sha256').update(text).digest('hex');
@ -86,20 +86,26 @@ export const outputBudgetKey = (params: Record<string, unknown> = {}) =>
/**
* The Qwen-on-DashScope wire shape for "thinking off", as realtime emits it:
* the tiered family reads `reasoning_effort`, the rest `enable_thinking`.
* Other model families have their own knobs, so they get nothing here.
* Enabling a tiered model removes the disable override, preserving its default
* or an explicitly configured effort. Other model families get nothing here.
*/
function disableThinking(
export function setThinking(
params: Record<string, unknown>,
model: string | undefined,
enabled: boolean,
): boolean {
if (isTieredEffortWireModel(model)) {
delete params['enable_thinking'];
delete params['thinking_budget'];
params['reasoning_effort'] = 'none';
if (!enabled) params['reasoning_effort'] = 'none';
else if (params['reasoning_effort'] === 'none')
delete params['reasoning_effort'];
return true;
}
if (isQwenFamilyWireModel(model)) {
params['enable_thinking'] = false;
if (enabled && params['reasoning_effort'] === 'none')
delete params['reasoning_effort'];
params['enable_thinking'] = enabled;
return true;
}
return false;
@ -124,7 +130,7 @@ export function freezeRequest(
!config.thinkingMandatory &&
params['enable_thinking'] === undefined &&
params['reasoning_effort'] === undefined &&
!disableThinking(params, model)
!setThinking(params, model, false)
) {
notes.push(
`disabled reasoning is not reproduced for ${model ?? 'this model'} in Batch requests; the provider default thinking mode applies`,
@ -132,9 +138,13 @@ export function freezeRequest(
}
if (
config?.thinkingMandatory &&
(config.reasoning === false || params['enable_thinking'] === false)
(config.reasoning === false ||
params['enable_thinking'] === false ||
params['reasoning_effort'] === 'none')
) {
delete params['enable_thinking'];
if (params['enable_thinking'] === false) delete params['enable_thinking'];
if (params['reasoning_effort'] === 'none')
delete params['reasoning_effort'];
notes.push('thinking cannot be disabled for this model; left on');
}
if (
@ -228,10 +238,8 @@ export function assembleRequests(
if (plan.maxOutputTokens !== undefined) {
body[outputBudgetKey(request?.params)] = plan.maxOutputTokens;
}
if (plan.enableThinking === true) {
body['enable_thinking'] = true;
} else if (plan.enableThinking === false) {
disableThinking(body, model);
if (plan.enableThinking !== undefined) {
setThinking(body, model, plan.enableThinking);
}
const inputTokens = estimateTokens(JSON.stringify(messages));
requests.push({
@ -441,6 +449,15 @@ export function deliverResult(
reason: `target directory "${item.target}" resolves outside the project root`,
};
}
const ancestorProblem = targetProblem(
path.relative(realRoot, fs.realpathSync(ancestor)),
);
if (ancestorProblem) {
return {
kind: 'held',
reason: `target directory "${item.target}" ${ancestorProblem}`,
};
}
fs.mkdirSync(parent, { recursive: true });
// Revalidate the created parent: the chain must really live under the
// project at delivery time, not just before the mkdir.
@ -451,6 +468,13 @@ export function deliverResult(
reason: `target directory "${item.target}" resolves outside the project root`,
};
}
const parentProblem = targetProblem(path.relative(realRoot, realParent));
if (parentProblem) {
return {
kind: 'held',
reason: `target directory "${item.target}" ${parentProblem}`,
};
}
if (fs.existsSync(targetPath)) {
const existing = fs.readFileSync(targetPath, 'utf8');
if (existing === content) {

View file

@ -80,7 +80,7 @@ export type BatchPlan = z.infer<typeof batchPlanSchema>;
* hidden path — at any depth, since `.git/hooks`, `.github/workflows` and
* `.qwen/settings.json` configure tools or run code in nested projects too.
*/
function targetProblem(target: string): string | undefined {
export function targetProblem(target: string): string | undefined {
const normalized = path.normalize(target);
const segments = normalized.split(/[\\/]/);
if (path.isAbsolute(target) || segments[0] === '..') {

View file

@ -54,6 +54,7 @@ import {
deliverResult,
sha256,
freezeRequest,
setThinking,
describeThinking,
outputBudgetKey,
} from './batch-docs.js';
@ -527,17 +528,10 @@ export async function runPlan(
? `preview: ${task.items.length} item(s), window ${task.completionWindow} — nothing uploaded, nothing billed`
: `task ${task.id}: ${task.items.length} item(s), window ${task.completionWindow}`,
);
const effective = {
...request,
params: {
...request.params,
...(plan.enableThinking === undefined
? {}
: plan.enableThinking
? { enable_thinking: true }
: { enable_thinking: false, reasoning_effort: 'none' }),
},
};
const effective = { ...request, params: { ...request.params } };
if (plan.enableThinking !== undefined) {
setThinking(effective.params, task.model, plan.enableThinking);
}
const limit = outputLimitOf(task, attempt.maxOutputTokens);
deps.out(
`model ${task.model}, ${describeThinking(effective)}, ` +