fix(webhook): preserve the initial model selection

This commit is contained in:
Tianyi Cui
2026-08-23 01:48:35 +08:00
parent d06f544d8c
commit ea3d0ffcee
15 changed files with 121 additions and 24 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-22-fire-and-forget-webhook-sessions.md
2026-08-22-fire-and-forget-webhook-sessions.md: 8d61e623343cc765ebed34e22a76c955b48c34e6
2026-08-22-fire-and-forget-webhook-sessions.zh.md: f4b6e3e5d7192acfe59bbb559e6cb5b99571b6cb
2026-08-22-fire-and-forget-webhook-sessions.md: f72956a282e988ff017e647e7ad97ed6138a6319
2026-08-22-fire-and-forget-webhook-sessions.zh.md: fa64298d0ea245a56c9206b74b0f0f87a9f88f7f
@@ -26,7 +26,7 @@ Patch loading anchors relative plugin names in inserted rows to the patch file.
## Session creation
A rule result names a local Workspace path, title, text prompt, agent preset, permission preset, and optional complete model selection. The runtime validates presets before mutation, resolves or creates the canonical Workspace, creates the Agent with that path as Session cwd, mounts the preset before publication, and attaches the Session before admitting the prompt.
A rule result names a local Workspace path, title, text prompt, agent preset, permission preset, and optional explicit provider/model route with an output cap. Without that route, the runtime snapshots the complete live default, including reasoning effort, until the first request records its durable header. It validates presets before mutation, resolves or creates the canonical Workspace, creates the Agent with that path as Session cwd, mounts the preset before publication, and attaches the Session before admitting the prompt.
The initial follow-up is an ordinary durable user-role message with webhook provider, source, delivery, and rule provenance. Its inbox insertion is the webhook operation's last boundary. Ordinary Session persistence and Agent lifecycle own later work; the runtime neither flushes specially nor waits for a turn.
@@ -26,7 +26,7 @@ Patch 加载会把插入行中的相对插件名锚定到 patch 文件。因而
## Session creation
规则结果会指定本地 Workspace 路径、标题、文本提示词、agent preset、permission preset 与可选完整模型选择。runtime 会在变更状态前验证 preset,解析或创建规范 Workspace,以该路径作为 Session cwd 创建 Agent,在发布前挂载 preset,并在接纳提示词前附加 Session。
规则结果会指定本地 Workspace 路径、标题、文本提示词、agent preset、permission preset,以及可选的明确提供方/模型路由与输出上限。没有明确路由时,runtime 会快照包含推理强度的完整实时默认选择,直到首个请求记录其持久 header。runtime 会在变更状态前验证 preset,解析或创建规范 Workspace,以该路径作为 Session cwd 创建 Agent,在发布前挂载 preset,并在接纳提示词前附加 Session。
初始 follow-up 是普通持久 user-role 消息,并携带 webhook 提供方、来源、交付和规则来源信息。它的 inbox 插入是 webhook 操作的最后边界。之后的工作由普通 Session persistence 与 Agent 生命周期拥有;runtime 既不执行特殊 flush,也不等待轮次。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
event-producer-consumer.md: cddf5668f75311827d1872684e89f6bb2f3647c5
event-producer-consumer.zh.md: ee758ff76ce5453b97b95d577b95cd701790a594
event-producer-consumer.md: a9cfc1201c7d851405c99fce2e8177297f0cfe5f
event-producer-consumer.zh.md: ecccc50fc9b3cd2fd730e30ec2636c773334b00d
+1 -1
View File
@@ -16,7 +16,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, `apiproxy`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server` |
+1 -1
View File
@@ -18,7 +18,7 @@
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, `apiproxy`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server` |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/webhook.md
webhook.md: 154f4adcc03bd5ce372322a06f8bd03bc27df2dd
webhook.zh.md: 9ce4618b78fcd17d8d18e449fec28d81b89af62b
webhook.md: a6257de3e0b81bf7d34d6ce40848303983bbf8b7
webhook.zh.md: bdace07eb9ada471174548321e53d41943abb98e
+1 -1
View File
@@ -14,7 +14,7 @@ The Webhook subsystem turns authenticated external deliveries into optional ordi
`WebhookRule<K>` contains a unique id, provider kind, and `run(delivery, signal)`. The callback may execute arbitrary trusted code. It returns `null` or one `WebhookSessionRequest`, and it must observe the signal for asynchronous work that should stop when the registration unloads.
`WebhookSessionRequest` requires an absolute `workspacePath`, title, text prompt, agent preset, and permission preset. Optional `model` names a complete provider/model pair plus optional output-token cap; omission reads the current deployment default.
`WebhookSessionRequest` requires an absolute `workspacePath`, title, text prompt, agent preset, and permission preset. Optional `model` names an explicit provider/model route plus optional output-token cap and uses that adapter's reasoning default. Omission snapshots the complete current deployment selection, including reasoning effort, until the first request records its durable header.
## Fire-and-forget dispatch
+1 -1
View File
@@ -14,7 +14,7 @@ Webhook 子系统会把已通过身份验证的外部交付转换为可选的普
`WebhookRule<K>` 包含唯一 id、提供方种类与 `run(delivery, signal)`。回调可以执行任意受信任代码。它返回 `null` 或一个 `WebhookSessionRequest`,并且异步工作若应在注册卸载时停止,就必须观察 signal。
`WebhookSessionRequest` 要求绝对 `workspacePath`、标题、文本提示词、agent preset 与 permission preset。可选 `model` 会指定完整提供方/模型组合与可选输出 token 上限;省略时读取当前部署默认值
`WebhookSessionRequest` 要求绝对 `workspacePath`、标题、文本提示词、agent preset 与 permission preset。可选 `model` 会指定明确的提供方/模型路由与可选输出 token 上限,并使用该适配器的默认推理强度。省略时会快照包含推理强度的完整当前部署选择,直到首个请求记录持久 header
## Fire-and-forget 分发
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/webhook/webhook/README.md
README.md: 940d03c4088b1dd9ec52e4f8b8c2d03df1fa9d41
README.zh.md: b5ae9649a1ee0275ac7cf39cd2b194cf32695018
README.md: 79337c7edb708f6862e0dc9b8ce98ebb491daf34
README.zh.md: 90555a71eea8485766e2d96baccff07303daacf0
+1 -1
View File
@@ -14,7 +14,7 @@ Registration is an effect. Its awaitable disposer first hides the rule, then abo
## Session request
`WebhookSessionRequest` requires `workspacePath`, `title`, `prompt`, `agentPreset`, and `permissionPreset`; an optional complete model selection names provider, model, and output-token cap together. Omission reads the current deployment default.
`WebhookSessionRequest` requires `workspacePath`, `title`, `prompt`, `agentPreset`, and `permissionPreset`; optional `model` names an explicit provider/model route plus an output-token cap. An explicit route uses its adapter's reasoning default. Omission snapshots the complete current deployment selection, including reasoning effort, until the first request records its durable header; later Web model changes retain the ordinary session behavior.
The runtime validates presets before mutation, resolves or creates the canonical Workspace, creates an Agent with that Workspace path as `SessionHeader.cwd`, mounts the agent preset before publication, and attaches the Session before applying permissions, title, and prompt. Failed attachment disposes the unpublished action. A later pre-prompt failure detaches the Workspace and disposes the Agent on a best-effort rollback.
+1 -1
View File
@@ -14,7 +14,7 @@
## Session 请求
`WebhookSessionRequest` 要求 `workspacePath``title``prompt``agentPreset``permissionPreset`;可选的完整模型选择会同时指定提供方模型与输出 token 上限。省略时读取当前部署默认值
`WebhookSessionRequest` 要求 `workspacePath``title``prompt``agentPreset``permissionPreset`;可选 `model` 会指定明确的提供方模型路由与输出 token 上限。明确路由使用其适配器的默认推理强度。省略时会快照包含推理强度的完整当前部署选择,直到首个请求记录持久 header;之后的 Web 模型变更保留普通 Session 行为
runtime 会在变更状态前验证 preset,解析或创建规范 Workspace,以该 Workspace 路径作为 `SessionHeader.cwd` 创建 Agent,在发布前挂载 agent preset,并在应用权限、标题与提示词前附加 Session。附加失败会释放尚未提交动作的 Agent。之后若在提示词前失败,则以尽力而为方式脱离 Workspace 并释放 Agent。
+26 -3
View File
@@ -3,10 +3,10 @@
import type { Context } from '@deepseek-ai/cordis'
import { randomUUID } from 'node:crypto'
import { isAbsolute } from 'node:path'
import type {} from '@deepseek-ai/dsh-agent'
import type { ModelSelection } from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-agent-default-model'
import type {} from '@deepseek-ai/dsh-agent-presets'
import { boundContextSummary, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm'
import { boundContextSummary, createUserMessage, errorChain, type LlmCallConfig } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-permission-presets'
import { SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-session-title'
@@ -21,6 +21,7 @@ interface ResolvedWebhookSessionRequest {
readonly prompt: string
readonly agentPreset: string
readonly permissionPreset: string
readonly modelSelection: ModelSelection
readonly agentOptions: {
readonly provider: string
readonly model: string
@@ -57,9 +58,11 @@ function resolveRequest(ctx: Context, input: WebhookSessionRequest): ResolvedWeb
throw new TypeError('webhook Session request model must be an object')
}
let agentOptions: ResolvedWebhookSessionRequest['agentOptions']
let modelSelection: ModelSelection
if (model === undefined) {
const selected = ctx.agentDefaultModel.currentSelection()
agentOptions = { provider: selected.provider, model: selected.model }
modelSelection = { ...selected }
} else {
const modelRecord = model as Record<string, unknown>
const provider = requiredString(modelRecord, 'provider')
@@ -74,8 +77,9 @@ function resolveRequest(ctx: Context, input: WebhookSessionRequest): ResolvedWeb
model: modelId,
...(maxTokens === undefined ? {} : { maxTokens }),
}
modelSelection = { provider, model: modelId }
}
return { workspacePath, title, prompt, agentPreset, permissionPreset, agentOptions }
return { workspacePath, title, prompt, agentPreset, permissionPreset, modelSelection, agentOptions }
}
/** Log a rollback failure without replacing the operation's original failure. */
@@ -83,6 +87,24 @@ function reportRollbackFailure(ctx: Context, subject: string, error: unknown): v
ctx.logger.warn(`webhook: ${subject} rollback failed: ${errorChain(error)}`)
}
/** Apply the creation-time selection until its first durable request header exists. */
function installInitialModelSelection(agentCtx: Context, selection: ModelSelection): void {
agentCtx.on('agent/request', async (_payload, next): Promise<LlmCallConfig> => {
const resolved = await next()
const agent = agentCtx.agent
/* v8 ignore next -- AgentRegistry setup always provides the unpublished scoped Agent. */
if (agent === undefined) throw new Error('webhook Session setup has no scoped Agent')
if (agent.session.requestHeader() !== undefined
|| resolved.provider !== selection.provider
|| resolved.model !== selection.model) return resolved
const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved
return {
...withoutInheritedEffort,
...selection.reasoningEffort === undefined ? {} : { reasoningEffort: selection.reasoningEffort },
}
})
}
/**
* Create, attach, title, configure, and prompt one ordinary root Session.
* Successful prompt admission ends webhook ownership of the operation; the
@@ -117,6 +139,7 @@ export async function createWebhookSession(
agentOptions: resolved.agentOptions,
setup: async (agentCtx) => {
await ctx.agentPresets.mount(agentCtx, preset.id)
installInitialModelSelection(agentCtx, resolved.modelSelection)
},
})
+2 -2
View File
@@ -24,7 +24,7 @@ export interface VerifiedWebhookDelivery<K extends string = string> {
readonly receivedAt: number
}
/** Optional complete model selection for a webhook-created Agent. */
/** Optional explicit model route and output cap for a webhook-created Agent. */
export interface WebhookModelSelection {
/** Registered provider route. */
readonly provider: string
@@ -46,7 +46,7 @@ export interface WebhookSessionRequest {
readonly agentPreset: string
/** Sandbox and approval preset applied before prompt admission. */
readonly permissionPreset: string
/** Optional explicit model; omission uses the current deployment default. */
/** Optional explicit route; omission uses the complete current default, including reasoning effort. */
readonly model?: WebhookModelSelection
}
+77 -3
View File
@@ -1,4 +1,5 @@
import type { Context } from '@deepseek-ai/cordis'
import { ReasoningEffortId, type LlmCallConfig } from '@deepseek-ai/dsh-llm'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
WebhookDeliveryId,
@@ -20,6 +21,8 @@ interface SessionHarness {
readonly ctx: Context
readonly calls: string[]
readonly messages: unknown[]
readonly modelListeners: Map<string, unknown>
markRequestHeader(): void
readonly controller: AbortController
readonly request: WebhookSessionRequest
}
@@ -34,8 +37,14 @@ afterEach(() => {
function harness(options: HarnessOptions = {}): SessionHarness {
const calls: string[] = []
const messages: unknown[] = []
const modelListeners = new Map<string, unknown>()
const controller = new AbortController()
const session = { id: 'webhook-session', header: { cwd: '/workspace' } }
let requestHeader: object | undefined
const session = {
id: 'webhook-session',
header: { cwd: '/workspace' },
requestHeader: () => requestHeader,
}
const agent = {
id: 'webhook-session',
session,
@@ -79,7 +88,7 @@ function harness(options: HarnessOptions = {}): SessionHarness {
agentDefaultModel: {
currentSelection() {
calls.push('default-model')
return { provider: 'default-provider', model: 'default-model', reasoningEffort: 'ignored' }
return { provider: 'default-provider', model: 'default-model', reasoningEffort: 'high' }
},
},
agentPresets: {
@@ -110,7 +119,13 @@ function harness(options: HarnessOptions = {}): SessionHarness {
async create(createOptions: { setup?: (ctx: unknown) => Promise<void> }) {
calls.push('agent-create')
if (options.failAt === 'agent') throw new Error('agent failed')
await createOptions.setup?.({})
await createOptions.setup?.({
agent,
on(event: string, listener: unknown) {
modelListeners.set(event, listener)
return () => {}
},
})
if (options.abortAt === 'agent') controller.abort(new Error('abort after agent'))
return handle
},
@@ -127,6 +142,8 @@ function harness(options: HarnessOptions = {}): SessionHarness {
ctx: fake as unknown as Context,
calls,
messages,
modelListeners,
markRequestHeader() { requestHeader = {} },
controller,
request: {
workspacePath: '/workspace',
@@ -158,6 +175,21 @@ async function create(test: SessionHarness, request = test.request): Promise<voi
)
}
/** Read the initial model-selection listener installed during Agent setup. */
function modelRequestListener(test: SessionHarness): (
payload: unknown,
next: () => Promise<LlmCallConfig>,
) => Promise<LlmCallConfig> {
const listener = test.modelListeners.get('agent/request')
if (typeof listener !== 'function') {
throw new Error('webhook Session did not install its initial model selection')
}
return listener as (
payload: unknown,
next: () => Promise<LlmCallConfig>,
) => Promise<LlmCallConfig>
}
describe('webhook Session creation', () => {
it('preflights, mounts, attaches, configures, titles, and prompts in order', async () => {
const test = harness()
@@ -192,6 +224,48 @@ describe('webhook Session creation', () => {
const withoutCap = harness()
await create(withoutCap, { ...withoutCap.request, model: { provider: 'p', model: 'm' } })
expect(withoutCap.calls).not.toContain('default-model')
await expect(modelRequestListener(withoutCap)(undefined, async () => ({
provider: 'p', model: 'm', reasoningEffort: ReasoningEffortId('inherited'),
}))).resolves.toEqual({ provider: 'p', model: 'm' })
})
it('preserves default reasoning until the first request header is durable', async () => {
const test = harness()
await create(test)
const request = modelRequestListener(test)
await expect(request(undefined, async () => ({
provider: 'other-provider',
model: 'default-model',
reasoningEffort: ReasoningEffortId('other-provider-effort'),
}))).resolves.toMatchObject({ reasoningEffort: 'other-provider-effort' })
await expect(request(undefined, async () => ({
provider: 'default-provider',
model: 'other-model',
reasoningEffort: ReasoningEffortId('other-model-effort'),
}))).resolves.toMatchObject({ reasoningEffort: 'other-model-effort' })
const routed = await request(undefined, async () => ({
provider: 'default-provider',
model: 'default-model',
reasoningEffort: ReasoningEffortId('inherited'),
})) as unknown
expect(routed).toEqual({
provider: 'default-provider',
model: 'default-model',
reasoningEffort: 'high',
})
test.markRequestHeader()
await expect(request(undefined, async () => ({
provider: 'later-provider',
model: 'later-model',
reasoningEffort: ReasoningEffortId('later'),
}))).resolves.toEqual({
provider: 'later-provider',
model: 'later-model',
reasoningEffort: 'later',
})
})
it.each([