Skip to content

Commit 776b34a

Browse files
authored
fix: chat.send no reply when thinking metadata is set (#98855)
* fix(gateway): preserve chat thinking metadata * test: harden chat thinking metadata coverage * test: harden chat thinking metadata coverage
1 parent 3e2d373 commit 776b34a

11 files changed

Lines changed: 566 additions & 40 deletions

src/auto-reply/reply/get-reply-directives.target-session.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { buildTestCtx } from "./test-ctx.js";
88
const mocks = vi.hoisted(() => ({
99
createModelSelectionState: vi.fn(),
1010
applyInlineDirectiveOverrides: vi.fn(),
11+
listAgentEntries: vi.fn(),
1112
resolveFastModeState: vi.fn(),
1213
resolveReplyExecOverrides: vi.fn(),
1314
}));
@@ -145,6 +146,8 @@ async function resolveHelloWithModelDefaults(params: {
145146
body?: string;
146147
sessionEntry?: SessionEntry;
147148
agentCfg?: { reasoningDefault?: "off" | "on" | "stream" };
149+
agentEntries?: Array<{ id?: string; thinkingDefault?: "off" | "low" }>;
150+
hasConfiguredThinkingDefault?: boolean;
148151
commandAuthorized?: boolean;
149152
hasOneTurnModelOverride?: boolean;
150153
selectedProvider?: string;
@@ -156,13 +159,15 @@ async function resolveHelloWithModelDefaults(params: {
156159
}) {
157160
const resolveDefaultThinkingLevel = vi.fn(async () => params.defaultThinking);
158161
const resolveDefaultReasoningLevel = vi.fn(async () => params.defaultReasoning);
162+
mocks.listAgentEntries.mockReturnValue(params.agentEntries ?? []);
159163
mocks.createModelSelectionState.mockResolvedValueOnce({
160164
provider: params.selectedProvider ?? "openai",
161165
model: params.selectedModel ?? "gpt-4o-mini",
162166
allowedModelKeys: new Set<string>(),
163167
allowedModelCatalog: [],
164168
resetModelOverride: false,
165169
resolveDefaultThinkingLevel,
170+
hasConfiguredThinkingDefault: params.hasConfiguredThinkingDefault,
166171
resolveDefaultReasoningLevel,
167172
});
168173

@@ -210,7 +215,7 @@ async function resolveHelloWithModelDefaults(params: {
210215
}
211216

212217
vi.mock("../../agents/agent-scope.js", () => ({
213-
listAgentEntries: vi.fn(() => []),
218+
listAgentEntries: (...args: unknown[]) => mocks.listAgentEntries(...args),
214219
}));
215220

216221
vi.mock("../../agents/defaults.js", () => ({
@@ -296,9 +301,11 @@ describe("resolveReplyDirectives", () => {
296301
beforeEach(() => {
297302
mocks.createModelSelectionState.mockReset();
298303
mocks.applyInlineDirectiveOverrides.mockReset();
304+
mocks.listAgentEntries.mockReset();
299305
mocks.resolveFastModeState.mockReset();
300306
mocks.resolveReplyExecOverrides.mockReset();
301307

308+
mocks.listAgentEntries.mockReturnValue([]);
302309
mocks.createModelSelectionState.mockResolvedValue({
303310
provider: "openai",
304311
model: "gpt-4o-mini",
@@ -556,6 +563,48 @@ describe("resolveReplyDirectives", () => {
556563
expect(resolveDefaultReasoningLevel).not.toHaveBeenCalled();
557564
});
558565

566+
it("does not re-enable model reasoning when thinking override explicitly disables thinking", async () => {
567+
const { result, resolveDefaultReasoningLevel } = await resolveHelloWithModelDefaults({
568+
defaultThinking: "off",
569+
defaultReasoning: "on",
570+
opts: { thinkingLevelOverride: "off" },
571+
});
572+
573+
expectContinueResult(result, {
574+
resolvedThinkLevel: "off",
575+
resolvedReasoningLevel: "off",
576+
});
577+
expect(resolveDefaultReasoningLevel).not.toHaveBeenCalled();
578+
});
579+
580+
it("does not re-enable model reasoning when per-agent thinking default disables thinking", async () => {
581+
const { result, resolveDefaultReasoningLevel } = await resolveHelloWithModelDefaults({
582+
defaultThinking: "off",
583+
defaultReasoning: "on",
584+
agentEntries: [{ id: "main", thinkingDefault: "off" }],
585+
});
586+
587+
expectContinueResult(result, {
588+
resolvedThinkLevel: "off",
589+
resolvedReasoningLevel: "off",
590+
});
591+
expect(resolveDefaultReasoningLevel).not.toHaveBeenCalled();
592+
});
593+
594+
it("does not re-enable model reasoning when per-model thinking config disables thinking", async () => {
595+
const { result, resolveDefaultReasoningLevel } = await resolveHelloWithModelDefaults({
596+
defaultThinking: "off",
597+
defaultReasoning: "on",
598+
hasConfiguredThinkingDefault: true,
599+
});
600+
601+
expectContinueResult(result, {
602+
resolvedThinkLevel: "off",
603+
resolvedReasoningLevel: "off",
604+
});
605+
expect(resolveDefaultReasoningLevel).not.toHaveBeenCalled();
606+
});
607+
559608
it("skips the model reasoning default when thinking is active", async () => {
560609
const { result, resolveDefaultReasoningLevel } = await resolveHelloWithModelDefaults({
561610
defaultThinking: "low",

src/auto-reply/reply/get-reply-directives.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -442,8 +442,11 @@ export async function resolveReplyDirectives(params: {
442442
const sessionThinkLevel = directives.clearThinkLevel
443443
? undefined
444444
: (targetSessionEntry?.thinkingLevel as ThinkLevel | undefined);
445-
const resolvedThinkLevel =
446-
normalizeThinkLevel(opts?.thinkingLevelOverride) ?? directives.thinkLevel ?? sessionThinkLevel;
445+
const thinkingLevelOverride = normalizeThinkLevel(opts?.thinkingLevelOverride);
446+
const configuredThinkingDefault =
447+
normalizeThinkLevel(agentEntry?.thinkingDefault) ??
448+
normalizeThinkLevel(agentCfg?.thinkingDefault);
449+
const resolvedThinkLevel = thinkingLevelOverride ?? directives.thinkLevel ?? sessionThinkLevel;
447450

448451
const resolvedVerboseLevel =
449452
directives.verboseLevel ??
@@ -546,12 +549,14 @@ export async function resolveReplyDirectives(params: {
546549
const resolvedThinkLevelWithDefault =
547550
resolvedThinkLevel ??
548551
(await modelState.resolveDefaultThinkingLevel()) ??
549-
(agentCfg?.thinkingDefault as ThinkLevel | undefined);
552+
configuredThinkingDefault;
550553

551554
const thinkingExplicitlySet =
555+
thinkingLevelOverride !== undefined ||
552556
directives.thinkLevel !== undefined ||
553557
sessionThinkLevel !== undefined ||
554-
agentCfg?.thinkingDefault !== undefined;
558+
configuredThinkingDefault !== undefined ||
559+
modelState.hasConfiguredThinkingDefault === true;
555560

556561
// When neither directive nor session nor agent set reasoning, default to model capability
557562
// (e.g. OpenRouter with reasoning: true). Skip model default when thinking is active

src/auto-reply/reply/get-reply-run.media-only.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,39 @@ describe("runPreparedReply media-only handling", () => {
386386
expect(call.followupRun.run.thinkLevel).toBe("off");
387387
});
388388

389+
it("reports unsupported explicit one-turn thinking overrides", async () => {
390+
const result = await runPreparedReply(
391+
baseParams({
392+
provider: "openai",
393+
model: "chat-latest",
394+
resolvedThinkLevel: "xhigh",
395+
opts: { thinkingLevelOverride: "xhigh" },
396+
modelState: {
397+
resolveDefaultThinkingLevel: async () => "high",
398+
resolveThinkingCatalog: async () => [
399+
{
400+
provider: "openai",
401+
id: "chat-latest",
402+
reasoning: false,
403+
},
404+
],
405+
allowedModelCatalog: [
406+
{
407+
provider: "openai",
408+
id: "chat-latest",
409+
name: "Chat Latest",
410+
},
411+
],
412+
} as never,
413+
}),
414+
);
415+
416+
expect(Array.isArray(result) ? undefined : result?.text).toContain(
417+
'Thinking level "xhigh" is not supported',
418+
);
419+
expect(runReplyAgent).not.toHaveBeenCalled();
420+
});
421+
389422
it("does not persist turn-local thinking fallback over a stored session override", async () => {
390423
const sessionEntry: SessionEntry = {
391424
sessionId: "session-thinking",

src/auto-reply/reply/get-reply-run.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ export async function runPreparedReply(
541541
let { sessionEntry, resolvedThinkLevel } = params;
542542
const isHeartbeat = opts?.isHeartbeat === true;
543543
const heartbeatRunScope = resolveHeartbeatRunScope(opts);
544+
const explicitThinkingLevelOverride = normalizeThinkLevel(opts?.thinkingLevelOverride);
544545
const traceAttributes = {
545546
provider,
546547
hasSessionKey: Boolean(sessionKey),
@@ -947,7 +948,9 @@ export async function runPreparedReply(
947948
});
948949
}
949950
if (!thinkingLevelSupported) {
950-
const explicitThink = directives.hasThinkDirective && directives.thinkLevel !== undefined;
951+
const explicitThink =
952+
(directives.hasThinkDirective && directives.thinkLevel !== undefined) ||
953+
explicitThinkingLevelOverride !== undefined;
951954
if (explicitThink) {
952955
typing.cleanup();
953956
return {

0 commit comments

Comments
 (0)