Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a05830c
feat(experiments): add dynamic thinking effort experimental setting
easonliang28 Aug 21, 2026
1cf4f0d
test(experiments): cover explicit false and omitted dynamic thinking …
easonliang28 Aug 21, 2026
6ea45b3
feat(task): task-local thinking effort state, per-request override, a…
easonliang28 Aug 21, 2026
5db5cf4
Merge remote-tracking branch 'upstream/main' into feat/dte-1-experiment
easonliang28 Aug 21, 2026
9275aa1
Merge remote-tracking branch 'upstream/main' into feat/dte-2-task-state
easonliang28 Aug 22, 2026
14d1f35
fix(task): keep override restore value current across profile switches
easonliang28 Aug 22, 2026
90b47b0
docs(task): JSDoc for diff-touched functions flagged by CodeRabbit
easonliang28 Aug 22, 2026
d64a473
Merge remote-tracking branch 'upstream/main' into feat/dte-3-native-tool
easonliang28 Aug 22, 2026
2d53e91
Merge remote-tracking branch 'origin/feat/dte-1-experiment' into feat…
easonliang28 Aug 22, 2026
fcc3cf4
feat(task): set_thinking_effort native tool
easonliang28 Aug 23, 2026
0ab4a60
Merge remote-tracking branch 'upstream/main' into feat/dte-3-native-tool
easonliang28 Aug 23, 2026
19954d3
fix(task): harden set_thinking_effort per review feedback
easonliang28 Aug 23, 2026
e83af72
test(e2e): set_thinking_effort mid-task workflow (DTE addendum)
easonliang28 Aug 24, 2026
42b423d
test(e2e): temporary DTE-DEBUG capture of request shapes (revert afte…
easonliang28 Aug 24, 2026
396a9b1
fix(tool): reject capability arrays with no settable effort
easonliang28 Aug 24, 2026
e502417
test(webview): type thinking-effort test helpers
easonliang28 Aug 24, 2026
27dea9b
feat(i18n): translate dynamic thinking effort setting (11 locales)
easonliang28 Aug 24, 2026
bbec2f9
fix(e2e): match post-tool DTE request by model + tool result
easonliang28 Aug 24, 2026
2a1a597
fix(e2e): keep DTE fixture file valid JSON
easonliang28 Aug 24, 2026
cfa6a64
Merge branch 'feat/dte-3-native-tool' into feat/dte-3-e2e
easonliang28 Aug 24, 2026
1396736
feat(i18n): translate dynamic thinking effort setting (5 more locales)
easonliang28 Aug 24, 2026
f309d3a
Merge branch 'feat/dte-3-native-tool' into feat/dte-3-e2e
easonliang28 Aug 24, 2026
f7057f0
test(webview): include source in thinking-effort say-tool test type
easonliang28 Aug 24, 2026
1270e7a
Merge branch 'feat/dte-3-native-tool' into feat/dte-3-e2e
easonliang28 Aug 24, 2026
37c8040
test(e2e): set_thinking_effort switching workflow (DTE addendum)
easonliang28 Aug 24, 2026
2983975
test(e2e): document DTE capture proxy + switching helper functions
easonliang28 Aug 24, 2026
f756aab
test(e2e): settle expected display says before detaching listener (CI…
easonliang28 Aug 24, 2026
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
84 changes: 84 additions & 0 deletions apps/vscode-e2e/fixtures/thinking-effort-switching.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"fixtures": [
{
"match": {
"model": "openai/gpt-5.1",
"hasToolResult": false,
"turnIndex": 0
},
"response": {
"toolCalls": [
{
"name": "set_thinking_effort",
"arguments": "{\"effort\": \"medium\", \"reason\": \"start at medium\"}",
"id": "call_dte_sw_001"
}
]
}
},
{
"match": {
"model": "openai/gpt-5.1",
"hasToolResult": true,
"turnIndex": 1
},
"response": {
"toolCalls": [
{
"name": "set_thinking_effort",
"arguments": "{\"effort\": \"medium\", \"reason\": \"confirm current level\"}",
"id": "call_dte_sw_002"
}
]
}
},
{
"match": {
"model": "openai/gpt-5.1",
"hasToolResult": true,
"turnIndex": 2
},
"response": {
"toolCalls": [
{
"name": "set_thinking_effort",
"arguments": "{\"effort\": \"high\", \"reason\": \"raise to high\"}",
"id": "call_dte_sw_003"
}
]
}
},
{
"match": {
"model": "openai/gpt-5.1",
"hasToolResult": true,
"turnIndex": 3
},
"response": {
"toolCalls": [
{
"name": "set_thinking_effort",
"arguments": "{\"effort\": \"medium\", \"reason\": \"try returning to medium\"}",
"id": "call_dte_sw_004"
}
]
}
},
{
"match": {
"model": "openai/gpt-5.1",
"hasToolResult": true,
"turnIndex": 4
},
"response": {
"toolCalls": [
{
"name": "attempt_completion",
"arguments": "{\"result\": \"DTE_E2E_SWITCH_DONE\"}",
"id": "call_dte_sw_005"
}
]
}
}
]
}
35 changes: 35 additions & 0 deletions apps/vscode-e2e/fixtures/thinking-effort-tool.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"fixtures": [
{
"match": {
"sequenceIndex": 0,
"userMessage": "DTE_E2E_EFFORT_APPLY: answer the math question"
},
"response": {
"toolCalls": [
{
"name": "set_thinking_effort",
"arguments": "{\"effort\": \"high\", \"reason\": \"multi-step math\"}",
"id": "call_dte_e2e_001"
}
]
}
},
{
"match": {
"model": "openai/gpt-5",
"hasToolResult": true,
"turnIndex": 1
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
"response": {
"toolCalls": [
{
"name": "attempt_completion",
"arguments": "{\"result\": \"42\"}",
"id": "call_dte_e2e_002"
}
]
}
}
]
}
212 changes: 212 additions & 0 deletions apps/vscode-e2e/src/suite/thinking-effort-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { createServer, type IncomingMessage, type ServerResponse } from "http"

/**
* Shared loopback capture proxy for the DTE e2e suites
* (thinking-effort-tool / thinking-effort-switching).
*
* Pattern from anthropic-opus-4-7.test.ts: it intercepts the
* OpenRouter-compatible chat/completions POST so request shapes can be
* asserted (model, reasoning envelope, message content), then forwards the
* request unchanged to the upstream — aimock in replay/record mode — which
* answers with the fixture-driven SSE.
*/

export type DteReasoningEnvelope = {
effort?: string
max_tokens?: number
exclude?: boolean
}

export type CapturedDteRequest = {
model?: string
reasoning: DteReasoningEnvelope | undefined
/** Raw JSON body, so assertions can inspect any part of the wire request (e.g. tool result text). */
bodyText: string
lastUserMessage: string
}

type OpenRouterChatCompletionBody = {
model?: string
reasoning?: DteReasoningEnvelope
messages?: Array<{ role?: string; content?: unknown }>
}

const ALLOWED_PROXY_HOSTS = new Set(["127.0.0.1", "localhost"])
const CHAT_COMPLETIONS_PATH = "/v1/chat/completions"
const HOP_BY_HOP = new Set([
"connection",
"keep-alive",
"transfer-encoding",
"te",
"trailer",
"upgrade",
"proxy-connection",
"proxy-authenticate",
"proxy-authorization",
"host",
"content-length",
])

/**
* Whether a raw URL targets the OpenRouter-compatible chat/completions endpoint.
*/
function isChatCompletionsUrl(rawUrl: string): boolean {
try {
return new URL(rawUrl).pathname.endsWith(CHAT_COMPLETIONS_PATH)
} catch {
return false
}
}

/**
* Collects the full request body as a UTF-8 string.
*/
function readRequestBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = []
req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)))
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")))
req.on("error", reject)
})
}

/**
* Mirrors the upstream response headers onto the proxy response, dropping the
* headers that would break fetch()-decoded streaming (content-encoding / length).
*/
function writeResponseHeaders(target: ServerResponse, source: Response) {
const headers: Record<string, string> = {}
source.headers.forEach((value, key) => {
const lower = key.toLowerCase()
// fetch() automatically decompresses the body, so strip content-encoding to
// prevent the SDK from attempting a second decompression. Also strip
// content-length since the decoded body length differs from the compressed one.
if (lower !== "content-length" && lower !== "content-encoding") {
headers[key] = value
}
})
target.writeHead(source.status, headers)
}

/**
* Streams the upstream (already-decoded) fetch body through to the proxy
* response, ending the response when the body completes.
*/
async function pipeFetchResponse(target: ServerResponse, source: Response) {
writeResponseHeaders(target, source)

if (!source.body) {
target.end()
return
}

const reader = source.body.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
target.write(value)
}

target.end()
}

/**
* Resolves the upstream chat/completions URL, rejecting any target that is not
* a loopback HTTP origin (the proxy must never forward to a real endpoint).
*/
function resolveAllowedUpstreamUrl(baseUrl: string): URL {
const upstreamBase = new URL(baseUrl)

if (!ALLOWED_PROXY_HOSTS.has(upstreamBase.hostname) || upstreamBase.protocol !== "http:") {
throw new Error("Unexpected OpenRouter proxy target: " + upstreamBase.origin)
}

return new URL(CHAT_COMPLETIONS_PATH, upstreamBase)
}

/**
* Serves a loopback capture proxy for the OpenRouter-compatible
* chat/completions endpoint: captures each request body for assertions and
* forwards it unchanged to the upstream (aimock in replay/record mode).
*/
export async function withOpenRouterCaptureProxy<T>(
upstreamUrl: string,
run: (args: { proxyUrl: string; requests: CapturedDteRequest[] }) => Promise<T>,
): Promise<T> {
const requests: CapturedDteRequest[] = []
const upstreamTarget = resolveAllowedUpstreamUrl(upstreamUrl)
let proxyError: Error | undefined

const server = createServer(async (req, res) => {
try {
const requestUrl = req.url ?? "/"

if (!isChatCompletionsUrl("http://127.0.0.1" + requestUrl)) {
res.writeHead(404)
res.end("Not found")
return
}

const bodyText = await readRequestBody(req)
const body = JSON.parse(bodyText) as OpenRouterChatCompletionBody
const lastUser = [...(body.messages ?? [])].reverse().find((message) => message.role === "user")
const lastUserMessage =
typeof lastUser?.content === "string" ? lastUser.content : JSON.stringify(lastUser?.content ?? "")

requests.push({
model: body.model,
reasoning: body.reasoning,
bodyText,
lastUserMessage,
})

const forwardHeaders: Record<string, string> = {}
for (const [key, value] of Object.entries(req.headers)) {
if (!HOP_BY_HOP.has(key.toLowerCase()) && value !== undefined) {
forwardHeaders[key] = Array.isArray(value) ? value.join(", ") : value
}
}

const upstream = await fetch(upstreamTarget, {
method: req.method,
headers: forwardHeaders,
body: bodyText,
})

await pipeFetchResponse(res, upstream)
} catch (error) {
proxyError = error instanceof Error ? error : new Error(String(error))
console.error("OpenRouter proxy request failed:", proxyError)
if (!res.headersSent) {
res.writeHead(502)
res.end("Capture proxy error")
} else if (!res.writableEnded) {
res.destroy()
}
}
})

await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", () => resolve())
})

const address = server.address()
if (address === null || typeof address === "string") {
server.close()
throw new Error("Capture proxy failed to bind a loopback port")
}

const proxyUrl = "http://127.0.0.1:" + address.port

try {
const result = await run({ proxyUrl, requests })
if (proxyError) {
throw proxyError
}
return result
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())))
}
}
Loading
Loading