Skip to content
Merged
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
37 changes: 37 additions & 0 deletions src/__tests__/stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,43 @@ describe("streamQoder", () => {
expect(body.model_config.key).toBe("lite");
});

it("sends full disable signals when thinking is off", async () => {
globalThis.fetch = mockFetch(SUCCESS_SSE);
const offOptions = { apiKey: "fake", reasoning: "off" } as unknown as Parameters<typeof streamQoder>[2];
await consume(streamQoder(makeModel("qoder", "Ultimate"), makeContext(), offOptions));

const body = decodeQoderBody(vi.mocked(globalThis.fetch).mock.calls[0][1]);
const parameters = body.parameters as Record<string, unknown>;
expect(parameters.enable_thinking).toBe(false);
expect(parameters.reasoning_effort).toBe("none");
expect(body.model_config).toMatchObject({ is_reasoning: false });
expect(body.chat_context).toMatchObject({ extra: { modelConfig: { is_reasoning: false } } });
});

it("honors the host's disableReasoning flag as off", async () => {
globalThis.fetch = mockFetch(SUCCESS_SSE);
const offOptions = { apiKey: "fake", disableReasoning: true } as unknown as Parameters<typeof streamQoder>[2];
await consume(streamQoder(makeModel("qoder", "Ultimate"), makeContext(), offOptions));

const body = decodeQoderBody(vi.mocked(globalThis.fetch).mock.calls[0][1]);
const parameters = body.parameters as Record<string, unknown>;
expect(parameters.enable_thinking).toBe(false);
expect(parameters.reasoning_effort).toBe("none");
expect(body.model_config).toMatchObject({ is_reasoning: false });
});

it("disables thinking when reasoning is omitted (pi's off signal", async () => {
globalThis.fetch = mockFetch(SUCCESS_SSE);
await consume(streamQoder(makeModel("qoder", "Ultimate"), makeContext(), { apiKey: "fake" }));

const body = decodeQoderBody(vi.mocked(globalThis.fetch).mock.calls[0][1]);
const parameters = body.parameters as Record<string, unknown>;
expect(parameters.enable_thinking).toBe(false);
expect(parameters.reasoning_effort).toBe("none");
expect(body.model_config).toMatchObject({ is_reasoning: false });
expect(body.chat_context).toMatchObject({ extra: { modelConfig: { is_reasoning: false } } });
});

it("binds tools delivered via the transcript system message (pi-ai >=0.86)", async () => {
globalThis.fetch = mockFetch(SUCCESS_SSE);
await consume(streamQoder(makeModel(), makeTranscriptContext(), { apiKey: "fake" }));
Expand Down
30 changes: 25 additions & 5 deletions src/protocol/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,24 @@ export function streamQoder(
// map to the upstream effort name. clampThinkingLevel returns "off" when
// the level is unsupported or the user disabled thinking.
const requestedLevel = options?.reasoning;
const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : undefined;

// Normalize every host's "thinking off" signal into one flag. pi and OMP
// both resolve level "off" to `reasoning: undefined`; OMP's fork also sets
// `disableReasoning` / `forceReasoningOff`; `false` / `"off"` cover older
// hosts. When off, skip clamping so the request never snaps back up to a
// concrete level.
const extOptions = options as
| (SimpleStreamOptions & { disableReasoning?: boolean; forceReasoningOff?: boolean })
| undefined;
const explicitlyOff =
extOptions?.disableReasoning === true ||
extOptions?.forceReasoningOff === true ||
(requestedLevel as unknown) === false ||
(requestedLevel as unknown) === "off";

// `undefined` here (explicit off, omitted level, or clamped to off) means
// "disable thinking"; the else branch below turns it into the wire signals.
const clamped = !explicitlyOff && requestedLevel ? clampThinkingLevel(model, requestedLevel) : undefined;
const reasoningLevel = clamped === "off" ? undefined : clamped;
const parameters: Record<string, unknown> = { max_tokens: maxTokens };
if (reasoningLevel) {
Expand All @@ -446,9 +463,12 @@ export function streamQoder(
parameters.reasoning_effort = effort;
}
} else {
// No reasoning level selected (or clamped to off): explicitly disable
// thinking so the model does not reason by default.
// Explicitly disable thinking. `enable_thinking: false` alone is not
// enough: qodercli also sends `reasoning_effort: "none"` (and flips
// `model_config.is_reasoning` below), otherwise the gateway still
// routes the reasoning model as reasoning-enabled.
parameters.enable_thinking = false;
parameters.reasoning_effort = "none";
}

const reqBody: Record<string, unknown> = {
Expand Down Expand Up @@ -483,14 +503,14 @@ export function streamQoder(
context: [],
modelConfig: {
key: qoderModel,
is_reasoning: isReasoning,
is_reasoning: reasoningLevel ? isReasoning : false,
},
originalContent: lastUserText,
},
features: [],
text: lastUserText,
},
model_config: modelConfig,
model_config: reasoningLevel ? modelConfig : { ...modelConfig, is_reasoning: false },
business: {
product: "cli",
version: "1.0.0",
Expand Down