mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-10-19 13:30:37 +00:00
Merged via /merge-batch (2026-10-18). Native Claude OAuth requests now honor the connection's summarizeThinking opt-in for the thinking display (complements #17040, which stops redacting thinking when summarized display is requested). Integration on top: the new claudeThinkingOptions.ts no longer imports @/lib/providers/requestDefaults — base.ts (which already imports it) resolves summarizeThinking and passes a boolean, so the open-sse → src boundary stays at 774/774. Evidence: 24 thinking/redact/context-management suites 428/428, open-sse typecheck 0, ESLint clean, cognitive complexity unchanged vs tip. Remaining CI reds are the inherited base-red #15306.
This commit is contained in:
parent
4e7c7f0ec8
commit
d3d86fc5e5
4 changed files with 143 additions and 13 deletions
1
changelog.d/fixes/16407-native-thinking-display.md
Normal file
1
changelog.d/fixes/16407-native-thinking-display.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Honor the configured thinking-summary display for native Claude OAuth requests while preserving explicit display settings and disabled thinking modes. (#16407)
|
||||
|
|
@ -96,6 +96,7 @@ import {
|
|||
stripProxyToolPrefix,
|
||||
} from "./claudeIdentity.ts";
|
||||
import { withForcedResponsesUpstream } from "./forceResponsesUpstream.ts";
|
||||
import { finalizeClaudeThinkingOptions } from "./claudeThinkingOptions.ts";
|
||||
import { detachClaudeCodeRewriteTargets } from "./claudeOauthBodyIsolation.ts";
|
||||
import {
|
||||
mergeUpstreamExtraHeaders,
|
||||
|
|
@ -228,11 +229,7 @@ export type CountTokensInput = {
|
|||
signal?: AbortSignal | null;
|
||||
};
|
||||
|
||||
import {
|
||||
hasActiveClaudeThinking,
|
||||
readNestedThinkingBudget,
|
||||
clampNestedThinkingBudget,
|
||||
} from "../utils/thinkingBudget.ts";
|
||||
import { readNestedThinkingBudget, clampNestedThinkingBudget } from "../utils/thinkingBudget.ts";
|
||||
|
||||
/**
|
||||
* Strip the OmniRoute provider prefix from tool model fields (e.g.
|
||||
|
|
@ -1151,14 +1148,12 @@ export class BaseExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
// Real CLI always pairs context_management with thinking. Mirror
|
||||
// that invariant so long sessions don't accumulate thinking blocks
|
||||
// toward the context cap.
|
||||
if (hasActiveClaudeThinking(tb) && !tb.context_management) {
|
||||
tb.context_management = {
|
||||
edits: [{ type: "clear_thinking_20251015", keep: "all" }],
|
||||
};
|
||||
}
|
||||
finalizeClaudeThinkingOptions(
|
||||
tb,
|
||||
this.provider,
|
||||
getClaudeCodeCompatibleRequestDefaults(requestCredentials?.providerSpecificData)
|
||||
.summarizeThinking === true
|
||||
);
|
||||
|
||||
const seed = activeCredentials?.accessToken || activeCredentials?.apiKey || "anon";
|
||||
const psd = activeCredentials?.providerSpecificData as
|
||||
|
|
|
|||
20
open-sse/executors/claudeThinkingOptions.ts
Normal file
20
open-sse/executors/claudeThinkingOptions.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { applyClaudeCodeCompatibleThinkingDisplay } from "../services/claudeCodeCompatibleThinkingDisplay.ts";
|
||||
import { hasActiveClaudeThinking } from "../utils/thinkingBudget.ts";
|
||||
|
||||
/** Pair active thinking with context management, then apply the native display opt-in. */
|
||||
export function finalizeClaudeThinkingOptions(
|
||||
body: Record<string, unknown>,
|
||||
provider: string,
|
||||
summarizeThinking: boolean
|
||||
): void {
|
||||
if (!hasActiveClaudeThinking(body)) return;
|
||||
// Preserve the existing native/CC pairing so long sessions retain thinking context.
|
||||
if (!body.context_management) {
|
||||
body.context_management = {
|
||||
edits: [{ type: "clear_thinking_20251015", keep: "all" }],
|
||||
};
|
||||
}
|
||||
if (provider !== "claude") return;
|
||||
const configuration = body.thinking as Record<string, unknown>;
|
||||
body.thinking = applyClaudeCodeCompatibleThinkingDisplay(configuration, { summarizeThinking });
|
||||
}
|
||||
114
tests/unit/claude-native-thinking-display-16407.test.ts
Normal file
114
tests/unit/claude-native-thinking-display-16407.test.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { BaseExecutor } from "../../open-sse/executors/base.ts";
|
||||
|
||||
class NativeClaudeExecutor extends BaseExecutor {
|
||||
constructor(provider = "claude") {
|
||||
super(provider, { baseUrls: ["https://api.anthropic.com/v1/messages"] });
|
||||
}
|
||||
needsRefresh() {
|
||||
return false;
|
||||
}
|
||||
async transformRequest(_model: string, body: Record<string, unknown>) {
|
||||
return structuredClone(body);
|
||||
}
|
||||
}
|
||||
|
||||
async function capture(
|
||||
thinking: Record<string, unknown> | undefined,
|
||||
summarizeThinking?: boolean,
|
||||
extra: Record<string, unknown> = {},
|
||||
clientHeaders: Record<string, string> = {},
|
||||
provider = "claude"
|
||||
) {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let sent: Record<string, unknown> | undefined;
|
||||
globalThis.fetch = async (_url, init) => {
|
||||
sent = JSON.parse(String(init?.body));
|
||||
return Response.json({ content: [{ type: "text", text: "ok" }] });
|
||||
};
|
||||
try {
|
||||
await new NativeClaudeExecutor(provider).execute({
|
||||
// Manual budgets remain supported on 4.6; Sonnet 5 only accepts adaptive thinking.
|
||||
model: thinking?.type === "enabled" ? "claude-sonnet-4-6" : "claude-sonnet-5",
|
||||
body: { messages: [{ role: "user", content: "hello" }], thinking, ...extra },
|
||||
stream: false,
|
||||
credentials: {
|
||||
accessToken: "sk-ant-oat-test-display-16407",
|
||||
providerSpecificData: { requestDefaults: { summarizeThinking } },
|
||||
},
|
||||
clientHeaders,
|
||||
});
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
assert.ok(sent, "the final upstream payload was captured");
|
||||
return sent;
|
||||
}
|
||||
|
||||
for (const thinking of [{ type: "adaptive" }, { type: "enabled", budget_tokens: 2048 }]) {
|
||||
test(`native Claude honors summary opt-in for ${thinking.type}`, async () => {
|
||||
const sent = await capture(thinking, true);
|
||||
assert.deepEqual(sent.thinking, { ...thinking, display: "summarized" });
|
||||
});
|
||||
}
|
||||
|
||||
for (const preference of [undefined, false]) {
|
||||
test(`native Claude preserves absent display when summary preference is ${preference}`, async () => {
|
||||
const sent = await capture({ type: "adaptive" }, preference);
|
||||
assert.deepEqual(sent.thinking, { type: "adaptive" });
|
||||
});
|
||||
}
|
||||
|
||||
for (const display of ["omitted", "updates"]) {
|
||||
test(`an explicit ${display} display wins over the connection default`, async () => {
|
||||
const thinking = { type: "adaptive", display };
|
||||
assert.deepEqual((await capture(thinking, true)).thinking, thinking);
|
||||
});
|
||||
}
|
||||
|
||||
for (const type of ["disabled", "between_tools"]) {
|
||||
test(`${type} never receives an unsupported display field`, async () => {
|
||||
const sent = await capture({ type }, true);
|
||||
assert.deepEqual(sent.thinking, { type });
|
||||
assert.equal(sent.context_management, undefined);
|
||||
});
|
||||
}
|
||||
|
||||
test("the opt-in does not enable thinking on a generic client", async () => {
|
||||
assert.equal((await capture(undefined, true)).thinking, undefined);
|
||||
});
|
||||
|
||||
test("an explicit off header still removes thinking", async () => {
|
||||
const sent = await capture({ type: "adaptive" }, true, {}, { "x-omniroute-thinking": "off" });
|
||||
assert.equal(sent.thinking, undefined);
|
||||
});
|
||||
|
||||
test("forced tool use still removes thinking", async () => {
|
||||
const sent = await capture({ type: "adaptive" }, true, {
|
||||
tools: [{ name: "lookup", description: "Lookup", input_schema: { type: "object" } }],
|
||||
tool_choice: { type: "tool", name: "lookup" },
|
||||
});
|
||||
assert.equal(sent.thinking, undefined);
|
||||
});
|
||||
|
||||
test("another provider does not acquire the native display default", async () => {
|
||||
const sent = await capture({ type: "adaptive" }, true, {}, {}, "openai");
|
||||
assert.deepEqual(sent.thinking, { type: "adaptive" });
|
||||
});
|
||||
|
||||
test("active thinking retains its existing context-management pairing without the opt-in", async () => {
|
||||
const sent = await capture({ type: "adaptive" }, false);
|
||||
assert.deepEqual(sent.context_management, {
|
||||
edits: [{ type: "clear_thinking_20251015", keep: "all" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("explicit context-management policy survives the summary opt-in", async () => {
|
||||
const contextManagement = { edits: [{ type: "clear_thinking_20251015", keep: 2 }] };
|
||||
const sent = await capture({ type: "adaptive" }, true, {
|
||||
context_management: contextManagement,
|
||||
});
|
||||
assert.deepEqual(sent.context_management, contextManagement);
|
||||
assert.deepEqual(sent.thinking, { type: "adaptive", display: "summarized" });
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue