-
Notifications
You must be signed in to change notification settings - Fork 244
test(e2e): set_thinking_effort mid-task workflow (DTE addendum) #1361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
easonLiangWorldedtech
wants to merge
27
commits into
Zoo-Code-Org:main
Choose a base branch
from
easonLiangWorldedtech:feat/dte-3-e2e
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 1cf4f0d
test(experiments): cover explicit false and omitted dynamic thinking …
easonliang28 6ea45b3
feat(task): task-local thinking effort state, per-request override, a…
easonliang28 5db5cf4
Merge remote-tracking branch 'upstream/main' into feat/dte-1-experiment
easonliang28 9275aa1
Merge remote-tracking branch 'upstream/main' into feat/dte-2-task-state
easonliang28 14d1f35
fix(task): keep override restore value current across profile switches
easonliang28 90b47b0
docs(task): JSDoc for diff-touched functions flagged by CodeRabbit
easonliang28 d64a473
Merge remote-tracking branch 'upstream/main' into feat/dte-3-native-tool
easonliang28 2d53e91
Merge remote-tracking branch 'origin/feat/dte-1-experiment' into feat…
easonliang28 fcc3cf4
feat(task): set_thinking_effort native tool
easonliang28 0ab4a60
Merge remote-tracking branch 'upstream/main' into feat/dte-3-native-tool
easonliang28 19954d3
fix(task): harden set_thinking_effort per review feedback
easonliang28 e83af72
test(e2e): set_thinking_effort mid-task workflow (DTE addendum)
easonliang28 42b423d
test(e2e): temporary DTE-DEBUG capture of request shapes (revert afte…
easonliang28 396a9b1
fix(tool): reject capability arrays with no settable effort
easonliang28 e502417
test(webview): type thinking-effort test helpers
easonliang28 27dea9b
feat(i18n): translate dynamic thinking effort setting (11 locales)
easonliang28 bbec2f9
fix(e2e): match post-tool DTE request by model + tool result
easonliang28 2a1a597
fix(e2e): keep DTE fixture file valid JSON
easonliang28 cfa6a64
Merge branch 'feat/dte-3-native-tool' into feat/dte-3-e2e
easonliang28 1396736
feat(i18n): translate dynamic thinking effort setting (5 more locales)
easonliang28 f309d3a
Merge branch 'feat/dte-3-native-tool' into feat/dte-3-e2e
easonliang28 f7057f0
test(webview): include source in thinking-effort say-tool test type
easonliang28 1270e7a
Merge branch 'feat/dte-3-native-tool' into feat/dte-3-e2e
easonliang28 37c8040
test(e2e): set_thinking_effort switching workflow (DTE addendum)
easonliang28 2983975
test(e2e): document DTE capture proxy + switching helper functions
easonliang28 f756aab
test(e2e): settle expected display says before detaching listener (CI…
easonliang28 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| }, | ||
| "response": { | ||
| "toolCalls": [ | ||
| { | ||
| "name": "attempt_completion", | ||
| "arguments": "{\"result\": \"42\"}", | ||
| "id": "call_dte_e2e_002" | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| ] | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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()))) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.