Z ZPI 文档 v0.3.8
介绍页

源码

packages/ai/test/responses-websocket.test.ts

typescript861 行查看原文
import { createServer, type Server } from "node:http";
import { type AddressInfo, connect } from "node:net";
import { setImmediate } from "node:timers/promises";
import {
  createAssistantMessageEventStream,
  emptyAssistant,
  normalizeContext,
  streamSimple,
} from "@zpi-dev/ai";
import { ResponsesSession } from "@zpi-dev/ai/api/responses-session";
import { AgentSession, ModelRuntime, SessionManager } from "@zpi-dev/coding-agent";
import type { ResponsesWS } from "openai/resources/responses/ws";
import { afterEach, expect, it, vi } from "vitest";
import { WebSocket, WebSocketServer } from "ws";
import { deferred, fakeModel } from "../../../tests/fake-server.ts";
import type { ResponsesSessionDiagnostic } from "../src/api/responses-session.ts";
import type { Message, Model, SimpleStreamOptions } from "../src/types.ts";

type Request = Record<string, unknown> & { input: unknown[] };
type Received = { body: Request; socket: WebSocket; authorization?: string };
const cleanups: (() => Promise<void> | void)[] = [];
afterEach(async () => {
  for (const cleanup of cleanups.splice(0).reverse()) await cleanup();
});
const user = (content: string): Message => ({ role: "user", content, timestamp: 1 });
const text = (value: string) => ({
  type: "message",
  id: "msg",
  role: "assistant",
  status: "completed",
  content: [{ type: "output_text", text: value, annotations: [] }],
});
function completed(id: string, output: unknown[] = [text("hello")]) {
  return {
    type: "response.completed",
    sequence_number: 1,
    response: {
      id,
      model: "fake",
      status: "completed",
      output,
      usage: {
        input_tokens: 10,
        output_tokens: output.length ? 2 : 0,
        total_tokens: 12,
        input_tokens_details: { cached_tokens: 0 },
      },
    },
  };
}
async function fixture(
  handle?: (request: Received, count: number) => void,
  rejectUpgrade = false,
  upgradeDelayMs = 0,
) {
  const requests: Received[] = [];
  let httpCalls = 0;
  const server: Server = createServer((_request, response) => {
    httpCalls++;
    response.writeHead(200, { "content-type": "text/event-stream" });
    response.end(`data: ${JSON.stringify(completed("http"))}\n\n`);
  });
  const sockets = new WebSocketServer({ noServer: true });
  server.on("upgrade", (request, socket, head) => {
    if (rejectUpgrade) {
      socket.end("HTTP/1.1 426 Upgrade Required\r\nContent-Length: 0\r\n\r\n");
      return;
    }
    const upgrade = () =>
      sockets.handleUpgrade(request, socket, head, (ws) => {
        ws.on("message", (raw) => {
          const entry = {
            body: JSON.parse(raw.toString()) as Request,
            socket: ws,
            authorization: request.headers.authorization,
          };
          requests.push(entry);
          if (handle) handle(entry, requests.length);
          else
            ws.send(
              JSON.stringify(
                completed(`resp_${requests.length}`, entry.body.generate === false ? [] : undefined),
              ),
            );
        });
      });
    if (upgradeDelayMs) {
      const timer = setTimeout(upgrade, upgradeDelayMs);
      socket.on("close", () => clearTimeout(timer));
    } else upgrade();
  });
  await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
  cleanups.push(async () => {
    for (const socket of sockets.clients) socket.terminate();
    await new Promise<void>((resolve) => sockets.close(() => resolve()));
    await new Promise<void>((resolve) => server.close(() => resolve()));
  });
  const baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}/v1`;
  const model: Model = { ...fakeModel(baseUrl), api: "openai-responses" };
  const session = new ResponsesSession();
  cleanups.push(() => session.dispose());
  const run = (messages: Message[], options: SimpleStreamOptions = {}, target = model) =>
    streamSimple(target, normalizeContext({ messages }), {
      apiKey: "local-test-key",
      sessionId: "conversation",
      responsesSession: session,
      maxRetries: 0,
      ...options,
    }).result();
  return { run, requests, session, model, httpCalls: () => httpCalls };
}

it.each([false, true])(
  "warms up and continues the same authenticated socket (subscription=%s)",
  async (subscription) => {
    const f = await fixture();
    const model = { ...f.model, ...(subscription ? { auth: "chatgpt" as const } : {}) };
    const base = [user("earlier")];
    expect((await f.run(base, { responsesWarmup: true }, model)).stopReason).toBe("stop");
    const response = await f.run([...base, user("next")], {}, model);
    expect(response.content).toMatchObject([{ type: "text", text: "hello" }]);
    expect(f.requests[0].body).toMatchObject({ type: "response.create", generate: false, store: false });
    expect(f.requests[0].body.stream).toBeUndefined();
    expect(f.requests[1].body).toMatchObject({
      previous_response_id: "resp_1",
      input: [{ role: "user", content: "next" }],
    });
    expect(f.requests[1].socket).toBe(f.requests[0].socket);
    expect(f.requests[0].authorization).toBe("Bearer local-test-key");
    expect(f.httpCalls()).toBe(0);
  },
);

it("continues tool calls with just their results while retaining encrypted reasoning", async () => {
  const reason = { type: "reasoning", id: "rs", summary: [], encrypted_content: "local-encrypted" };
  const call = {
    type: "function_call",
    id: "fc",
    call_id: "call_1",
    name: "read",
    arguments: '{ "path": "README.md" }',
    status: "completed",
  };
  const f = await fixture(({ socket }, count) =>
    socket.send(JSON.stringify(completed(`resp_${count}`, count === 1 ? [reason, call] : undefined))),
  );
  const messages = [user("read it")];
  const first = await f.run(messages);
  expect(first.stopReason).toBe("toolUse");
  await f.run([
    ...messages,
    first,
    {
      role: "toolResult",
      toolCallId: "call_1|fc",
      toolName: "read",
      content: [{ type: "text", text: "file contents" }],
      isError: false,
      timestamp: 2,
    },
  ]);
  expect(f.requests[1].body).toMatchObject({
    previous_response_id: "resp_1",
    input: [{ type: "function_call_output", call_id: "call_1", output: "file contents" }],
  });
});

it("keeps assistant text and phases out of incremental user turns", async () => {
  const f = await fixture(({ socket }, count) =>
    socket.send(JSON.stringify(completed(`resp_${count}`, [{ ...text("hello"), phase: "final_answer" }]))),
  );
  const first = await f.run([user("first")]);
  await f.run([user("first"), first, user("second")]);
  expect(f.requests[1].body.input).toEqual([{ role: "user", content: "second" }]);
  expect(f.requests[1].body.previous_response_id).toBe("resp_1");
});

it.each(["compaction", "instructions", "reasoning"])("replays full input when %s changes", async (change) => {
  const f = await fixture();
  const first = await f.run([user("first")]);
  const messages =
    change === "compaction"
      ? [user("compacted summary"), user("second")]
      : [user("first"), first, user("second")];
  const result = await f.run(messages, {
    onPayload: (payload) => ({
      ...(payload as object),
      ...(change === "instructions" ? { instructions: "new instructions" } : {}),
      ...(change === "reasoning" ? { reasoning: { effort: "high" } } : {}),
    }),
  });
  expect(result.stopReason).toBe("stop");
  expect(f.requests[1].body.previous_response_id).toBeUndefined();
  expect(f.requests[1].body.input.length).toBe(messages.length);
});

it("replays all intervening domestic-model messages after switching back to GPT", async () => {
  const f = await fixture();
  const first = await f.run([user("first")]);
  const domestic = {
    ...emptyAssistant({ ...f.model, provider: "domestic", id: "glm" }),
    content: [{ type: "text" as const, text: "domestic answer" }],
  };
  const history = [user("first"), first, user("domestic question"), domestic, user("back to GPT")];
  const changedModel = { ...f.model, id: "another-gpt" };
  await f.run(history, {}, changedModel);
  await f.run(history);
  expect(f.requests[2].body.previous_response_id).toBeUndefined();
  expect(f.requests[2].body.input).toContainEqual({ role: "assistant", content: "domestic answer" });
  expect(f.requests[2].body.input.at(-1)).toEqual({ role: "user", content: "back to GPT" });
  expect(f.requests[2].socket).not.toBe(f.requests[0].socket);
});

it("opens a fresh socket and chain after credentials change", async () => {
  const f = await fixture();
  const first = await f.run([user("first")]);
  await f.run([user("first"), first, user("next")], { apiKey: "replacement-key" });
  expect(f.requests[1].authorization).toBe("Bearer replacement-key");
  expect(f.requests[1].body.previous_response_id).toBeUndefined();
  expect(f.requests[1].socket).not.toBe(f.requests[0].socket);
});

it.each([false, true])("only accepts a no-content, no-usage completion during warmup=%s", async (warmup) => {
  const f = await fixture(({ socket }, count) => {
    const event = completed(`resp_${count}`, count === 1 ? [] : undefined);
    socket.send(JSON.stringify({ ...event, response: { ...event.response, usage: null } }));
  });
  const result = await f.run([user("hello")], { responsesWarmup: warmup });
  expect(result.stopReason).toBe(warmup ? "stop" : "error");
  if (!warmup) expect(result.errorDetails?.code).toBe("EMPTY_MODEL_RESPONSE");
  expect((await f.run([user("hello")])).stopReason).toBe("stop");
  expect(f.requests[1].body.previous_response_id).toBe(warmup ? "resp_1" : undefined);
  expect(f.requests[1].body.input).toHaveLength(warmup ? 0 : 1);
});

it("refreshes a pre-output WebSocket 401 once without an HTTP fallback", async () => {
  const f = await fixture(({ socket }, count) =>
    socket.send(
      JSON.stringify(
        count === 1
          ? {
              type: "error",
              status: 401,
              error: { type: "authentication_error", code: "unauthorized", message: "Unauthorized" },
            }
          : completed("recovered"),
      ),
    ),
  );
  const getApiKey = vi.fn(async (rejected?: string) => (rejected ? "new-key" : "old-key"));
  const result = await streamSimple(f.model, normalizeContext({ messages: [user("hello")] }), {
    responsesSession: f.session,
    getApiKey,
  }).result();
  expect(result.stopReason).toBe("stop");
  expect(f.requests.map((request) => request.authorization)).toEqual(["Bearer old-key", "Bearer new-key"]);
  expect(f.requests[1].socket).not.toBe(f.requests[0].socket);
  expect(f.httpCalls()).toBe(0);
  expect(getApiKey.mock.calls).toEqual([[], ["old-key"]]);
});

it.each([false, true])(
  "reconnects an evicted previous response with full input, server closes=%s",
  async (closes) => {
    const f = await fixture(({ body, socket }, count) => {
      socket.send(
        JSON.stringify(
          body.previous_response_id
            ? {
                type: "error",
                status: 400,
                error: {
                  code: "previous_response_not_found",
                  message: "evicted",
                  type: "invalid_request_error",
                },
              }
            : completed(`resp_${count}`),
        ),
      );
      if (body.previous_response_id && closes) socket.close();
    });
    const first = await f.run([user("first")]);
    expect((await f.run([user("first"), first, user("next")])).stopReason).toBe("stop");
    expect(f.requests[1].body.previous_response_id).toBe("resp_1");
    expect(f.requests[2].body.previous_response_id).toBeUndefined();
    expect(f.requests[2].body.input).toHaveLength(3);
    expect(f.requests[2].socket).not.toBe(f.requests[1].socket);
    expect(f.session.diagnostics.reconnects).toBe(1);
    expect(f.httpCalls()).toBe(0);
  },
);

it("reconnects with full history after the idle socket closes", async () => {
  const f = await fixture();
  const first = await f.run([user("first")]);
  f.requests[0].socket.close();
  await vi.waitFor(() => expect(f.requests[0].socket.readyState).toBe(WebSocket.CLOSED));
  await f.run([user("first"), first, user("next")]);
  expect(f.requests[1].body.previous_response_id).toBeUndefined();
  expect(f.requests[1].body.input).toHaveLength(3);
});

it("reconnects before a slow close handshake finishes instead of reusing a closing socket", async () => {
  const f = await fixture();
  const first = await f.run([user("first")]);
  const old = f.requests[0].socket;
  old.pause(); // Delay the server's close acknowledgement, as on a high-latency network.
  const client = (f.session as unknown as { socket: ResponsesWS }).socket;
  client.close();
  expect(client.socket.readyState).toBe(WebSocket.CLOSING);
  try {
    const second = await f.run([user("first"), first, user("next")]);
    expect(second.responseId).toBe("resp_2");
    expect(f.requests[1].socket).not.toBe(old);
    expect(f.requests[1].body.previous_response_id).toBeUndefined();
    expect(f.requests[1].body.input).toHaveLength(3);
    expect(f.session.diagnostics).toMatchObject({ connectionsCreated: 2, reconnects: 1, httpFallbacks: 0 });
    expect(f.httpCalls()).toBe(0);
  } finally {
    old.resume();
  }
});

it("falls back to HTTP and avoids retrying a rejected upgrade every tool round", async () => {
  const f = await fixture(undefined, true);
  expect((await f.run([user("first")])).responseId).toBe("http");
  expect((await f.run([user("second")])).responseId).toBe("http");
  expect(f.httpCalls()).toBe(2);
  expect(f.requests).toHaveLength(0);
});

it("never falls back to HTTP during warmup", async () => {
  const f = await fixture(undefined, true);
  await f.run([], { responsesWarmup: true });
  expect(f.httpCalls()).toBe(0);
});

it("does not replay a failed HTTP request while in transport cooldown", async () => {
  const f = await fixture(undefined, true);
  let attempts = 0;
  const fetch: typeof globalThis.fetch = async () => {
    attempts++;
    return new Response(JSON.stringify({ error: { message: "upstream unavailable" } }), { status: 503 });
  };
  expect((await f.run([user("first")], { fetch })).stopReason).toBe("error");
  expect((await f.run([user("next")], { fetch })).stopReason).toBe("error");
  expect(attempts).toBe(2);
  expect(f.session.diagnostics).toMatchObject({ httpFallbacks: 1, httpRequests: 2 });
});

it("does not silently replay a response after receiving partial output", async () => {
  const f = await fixture(({ socket }) => {
    socket.send(
      JSON.stringify({
        type: "response.output_text.delta",
        item_id: "msg",
        content_index: 0,
        delta: "partial",
        sequence_number: 0,
      }),
    );
    socket.close();
  });
  const response = await f.run([user("first")]);
  expect(response.stopReason).toBe("error");
  expect(response.content).toMatchObject([{ type: "text", text: "partial" }]);
  expect(f.httpCalls()).toBe(0);
});

it("surfaces request and usage errors without a second HTTP request", async () => {
  const f = await fixture(({ socket }) =>
    socket.send(
      JSON.stringify({
        type: "error",
        status: 429,
        error: { code: "rate_limit_exceeded", message: "quota exhausted", type: "rate_limit_error" },
      }),
    ),
  );
  const response = await f.run([user("first")]);
  expect(response.stopReason).toBe("error");
  expect(response.errorMessage).toContain("quota exhausted");
  expect(f.httpCalls()).toBe(0);
});

it("aborts stalled generation and closes the connection without HTTP replay", async () => {
  const f = await fixture(() => {});
  const controller = new AbortController();
  const pending = f.run([user("first")], { signal: controller.signal });
  await vi.waitFor(() => expect(f.requests).toHaveLength(1));
  controller.abort();
  expect((await pending).stopReason).toBe("aborted");
  expect(f.httpCalls()).toBe(0);
});

it("isolates two conversations even when they use the same credentials", async () => {
  const f = await fixture();
  const other = new ResponsesSession();
  cleanups.push(() => other.dispose());
  await f.run([user("first")]);
  await f.run([user("other")], { responsesSession: other });
  expect(f.requests[1].socket).not.toBe(f.requests[0].socket);
  expect(f.requests[1].body.previous_response_id).toBeUndefined();
});

it("supports GPT → domestic model → GPT in a real agent session, then resumes incrementally", async () => {
  const f = await fixture();
  const reports: { diagnostic: ResponsesSessionDiagnostic; sessionId?: string; modelId: string }[] = [];
  const runtime = new ModelRuntime(undefined, {
    onResponsesDiagnostic: (diagnostic, context) => reports.push({ diagnostic, ...context }),
  });
  const { provider, baseUrl, ...definition } = f.model;
  runtime.registerProvider(provider, { baseUrl, apiKey: "local-key", models: [definition] });
  runtime.registerProvider("domestic", {
    models: [{ ...definition, id: "glm", api: "openai-completions" }],
    streamSimple(model) {
      const stream = createAssistantMessageEventStream();
      const result = emptyAssistant(model);
      result.content = [{ type: "text", text: "domestic answer" }];
      // A custom provider avoids any external inference dependency in this integration test.
      stream.end(result);
      return stream;
    },
  });
  vi.spyOn(runtime, "canUseResponsesSession").mockImplementation((model) => model.provider === provider);
  const manager = SessionManager.inMemory();
  const session = new AgentSession(f.model, runtime, manager, [], [], "", "off");
  cleanups.push(() => session.dispose());
  await session.prompt("first GPT question");
  const originalSocket = f.requests[0].socket;
  const domestic = runtime.getModel("domestic", "glm");
  if (!domestic) throw new Error("Missing local domestic model");
  await session.setModel(domestic);
  await session.prompt("domestic question one");
  await session.prompt("domestic question two");
  await session.setModel(f.model);
  await vi.waitFor(() => expect(f.requests.some((request) => request.body.generate === false)).toBe(true));
  await session.prompt("back to GPT");
  const warmup = f.requests.findLast((request) => request.body.generate === false);
  expect(warmup?.socket).not.toBe(originalSocket);
  expect(warmup?.body.previous_response_id).toBeUndefined();
  expect(
    warmup?.body.input.filter((item) => (item as { content?: string }).content === "domestic answer"),
  ).toHaveLength(2);
  expect(warmup?.body.input).toContainEqual({ role: "user", content: "domestic question two" });
  await session.prompt("another GPT question");
  expect(f.requests.at(-1)?.body.input).toEqual([{ role: "user", content: "another GPT question" }]);
  expect(f.requests.at(-1)?.body.previous_response_id).toBeDefined();
  expect(
    manager.buildSessionContext().messages.filter((message) => message.role === "assistant"),
  ).toHaveLength(5);
  expect(f.httpCalls()).toBe(0);
  expect(reports.some(({ diagnostic }) => diagnostic.warmup)).toBe(true);
  expect(reports.at(-1)).toMatchObject({
    sessionId: session.sessionId,
    modelId: f.model.id,
    diagnostic: { event: "complete", transport: "websocket", stats: { deltaRequests: 2 } },
  });
});

it("accepts a successful upgrade that takes longer than the old 1.5s limit", async () => {
  const f = await fixture(undefined, false, 1700);
  expect((await f.run([user("slow handshake")])).stopReason).toBe("stop");
  expect(f.requests).toHaveLength(1);
  expect(f.httpCalls()).toBe(0);
});

it.each([false, true])("recovers an uncoded SIWC parent error, with metadata=%s", async (metadata) => {
  const f = await fixture(({ socket }, count) => {
    if (count === 2) {
      if (metadata) socket.send(JSON.stringify({ type: "codex.response.metadata" }));
      socket.send(
        JSON.stringify({
          type: "error",
          status: 400,
          error: { type: "invalid_request_error", message: "Invalid `previous_response_id`." },
        }),
      );
      socket.close();
    } else socket.send(JSON.stringify(completed(`resp_${count}`)));
  });
  const first = await f.run([user("first")]);
  const second = await f.run([user("first"), first, user("next")]);
  expect(second.stopReason).toBe("stop");
  expect(second.responseId).toBe("resp_3");
  expect(f.requests[2].socket).not.toBe(f.requests[1].socket);
  expect(f.requests[2].body.input).toHaveLength(3);
  expect(f.requests[2].body.previous_response_id).toBeUndefined();
  await f.run([user("first"), first, user("next"), second, user("after recovery")]);
  expect(f.requests[3].body.previous_response_id).toBe("resp_3");
  expect(f.requests[3].body.input).toEqual([{ role: "user", content: "after recovery" }]);
  expect(f.httpCalls()).toBe(0);
});

it("reconnects once on the connection lifetime limit and then resumes incrementally", async () => {
  const f = await fixture(({ socket }, count) => {
    if (count === 2) {
      socket.send(
        JSON.stringify({
          type: "error",
          status: 400,
          error: { code: "websocket_connection_limit_reached", message: "expired" },
        }),
      );
      socket.close();
    } else socket.send(JSON.stringify(completed(`resp_${count}`)));
  });
  const first = await f.run([user("first")]);
  const history = [user("first"), first, user("next")];
  const second = await f.run(history);
  expect(second.responseId).toBe("resp_3");
  expect(f.requests[2].socket).not.toBe(f.requests[0].socket);
  expect(f.requests[2].body.previous_response_id).toBeUndefined();
  expect(f.requests[2].body.input).toHaveLength(3);
  await f.run([...history, second, user("continued")]);
  expect(f.requests[3].body.previous_response_id).toBe("resp_3");
  expect(f.session.diagnostics).toMatchObject({ connectionsCreated: 2, reconnects: 1, websocketFailures: 1 });
  expect(f.httpCalls()).toBe(0);
});

it("falls back after one unsuccessful reconnect and keeps the HTTP cooldown", async () => {
  const f = await fixture(({ socket }) => {
    socket.send(
      JSON.stringify({
        type: "error",
        status: 400,
        error: { code: "websocket_connection_limit_reached", message: "expired" },
      }),
    );
    socket.close();
  });
  expect((await f.run([user("first")])).responseId).toBe("http");
  expect((await f.run([user("next")])).responseId).toBe("http");
  expect(f.requests).toHaveLength(2);
  expect(f.session.diagnostics).toMatchObject({
    connectionsCreated: 2,
    reconnects: 1,
    websocketFailures: 2,
    httpRequests: 2,
  });
  expect(f.httpCalls()).toBe(2);
});

it("never reconnects or replays a connection limit error after model output", async () => {
  const f = await fixture(({ socket }) => {
    socket.send(
      JSON.stringify({
        type: "response.output_text.delta",
        item_id: "msg",
        content_index: 0,
        delta: "partial",
        sequence_number: 0,
      }),
    );
    socket.send(
      JSON.stringify({
        type: "error",
        status: 400,
        error: { code: "websocket_connection_limit_reached", message: "expired" },
      }),
    );
  });
  expect((await f.run([user("first")])).stopReason).toBe("error");
  expect(f.requests).toHaveLength(1);
  expect(f.session.diagnostics.reconnects).toBe(0);
  expect(f.httpCalls()).toBe(0);
});

it("reports isolated diagnostic snapshots for warmup and incremental requests without content", async () => {
  const f = await fixture();
  const reports: ResponsesSessionDiagnostic[] = [];
  const session = new ResponsesSession({ onDiagnostic: (event) => reports.push(event) });
  cleanups.push(() => session.dispose());
  await f.run([], { responsesSession: session, responsesWarmup: true });
  const first = await f.run([user("PRIVATE_PROMPT")], { responsesSession: session });
  await f.run([user("PRIVATE_PROMPT"), first, user("next")], { responsesSession: session });
  expect(session.diagnostics).toMatchObject({
    connectionsCreated: 1,
    connectionsReused: 2,
    warmupRequests: 1,
    websocketRequests: 3,
    fullContextRequests: 1,
    deltaRequests: 2,
    lastInputItems: 1,
    lastFullInputItems: 3,
    httpRequests: 0,
  });
  expect(reports[0]).toMatchObject({ event: "request", warmup: true, stats: { websocketRequests: 1 } });
  expect(reports.at(-1)).toMatchObject({ event: "complete", transport: "websocket", warmup: false });
  expect(JSON.stringify(reports)).not.toContain("PRIVATE_PROMPT");
  expect(JSON.stringify(reports)).not.toContain("local-test-key");
  session.diagnostics.websocketRequests = 99;
  expect(session.diagnostics.websocketRequests).toBe(3);
});

it("keeps diagnostic observers from breaking successful generation", async () => {
  const f = await fixture();
  const session = new ResponsesSession({
    onDiagnostic: () => {
      throw new Error("logging failed");
    },
  });
  cleanups.push(() => session.dispose());
  expect((await f.run([user("first")], { responsesSession: session })).stopReason).toBe("stop");
  expect(f.requests).toHaveLength(1);
  expect(f.httpCalls()).toBe(0);
});

it("does not recover a parent error after any model output", async () => {
  const f = await fixture(({ socket }, count) => {
    if (count === 2) {
      socket.send(
        JSON.stringify({
          type: "response.output_text.delta",
          item_id: "msg",
          content_index: 0,
          delta: "partial",
          sequence_number: 0,
        }),
      );
      socket.send(
        JSON.stringify({
          type: "error",
          status: 400,
          error: { type: "invalid_request_error", message: "Invalid `previous_response_id`." },
        }),
      );
    } else socket.send(JSON.stringify(completed(`resp_${count}`)));
  });
  const first = await f.run([user("first")]);
  const second = await f.run([user("first"), first, user("next")]);
  expect(second.stopReason).toBe("error");
  expect(second.content).toMatchObject([{ type: "text", text: "partial" }]);
  expect(f.httpCalls()).toBe(0);
});

function agentSession(f: Awaited<ReturnType<typeof fixture>>) {
  const runtime = new ModelRuntime();
  const model = { ...f.model, compat: { supportsReasoningEffort: true } };
  const { provider, baseUrl, ...definition } = model;
  runtime.registerProvider(provider, { baseUrl, apiKey: "local-key", models: [definition] });
  vi.spyOn(runtime, "canUseResponsesSession").mockReturnValue(true);
  const session = new AgentSession(model, runtime, SessionManager.inMemory(), [], [], "", "low");
  cleanups.push(() => session.dispose());
  return session;
}

it.each(["commentary", "final_answer"] as const)(
  "queues mail without interrupting a response ending in %s and reuses its socket",
  async (phase) => {
    const reason = { type: "reasoning", id: "rs", summary: [], encrypted_content: "local-encrypted" };
    let generations = 0;
    const f = await fixture(({ socket, body }) => {
      if (body.generate === false) {
        socket.send(JSON.stringify(completed("warmup", [])));
      } else if (++generations === 1) {
        socket.send(JSON.stringify({ type: "response.created", response: { id: "first", model: "fake" } }));
        socket.send(JSON.stringify({ type: "response.output_item.added", output_index: 0, item: reason }));
        socket.send(JSON.stringify({ type: "response.output_item.done", output_index: 0, item: reason }));
      } else socket.send(JSON.stringify(completed("follow-up")));
    });
    const session = agentSession(f);
    const queued = deferred();
    let delivered = false;
    session.subscribe((event) => {
      if (
        !delivered &&
        event.type === "message_update" &&
        event.assistantMessageEvent.type === "thinking_end"
      ) {
        delivered = true;
        expect(session.steer("Worker progress", { queueOnly: true })).toBe(true);
        expect(session.steer("Worker result", { queueOnly: true })).toBe(true);
        queued.resolve();
      }
    });
    const running = session.prompt("Research");
    await queued.promise;
    await setImmediate();
    const requests = () => f.requests.filter((request) => request.body.generate !== false);
    expect(requests()).toHaveLength(1);
    const first = requests()[0];
    expect(first.socket.readyState).toBe(WebSocket.OPEN);
    first.socket.send(JSON.stringify(completed("first", [reason, { ...text("Original answer"), phase }])));
    await running;
    expect(session.messages.find((message) => message.role === "assistant")?.content).toContainEqual(
      expect.objectContaining({ type: "text", text: "Original answer", phase }),
    );
    if (phase === "final_answer") {
      expect(requests()).toHaveLength(1);
      expect(session.modelPerformance.steps).toBe(1);
      await session.prompt("Continue");
    }
    expect(requests()).toHaveLength(2);
    expect(requests()[1].socket).toBe(first.socket);
    expect(requests()[1].body).toMatchObject({
      previous_response_id: "first",
      input:
        phase === "final_answer"
          ? [{ role: "user", content: "Continue" }]
          : [
              { role: "user", content: "Worker progress" },
              { role: "user", content: "Worker result" },
            ],
    });
    expect(session.messages.filter((message) => message.role === "assistant")).toHaveLength(2);
    expect(session.modelPerformance.steps).toBe(2);
    expect(f.httpCalls()).toBe(0);
  },
);

it("captures final reasoning when a caller sets it immediately after changing models", async () => {
  const f = await fixture();
  const session = agentSession(f);
  await session.setModel({ ...session.model, id: "new-model" });
  session.setThinkingLevel("medium");
  await vi.waitFor(() => expect(f.requests).toHaveLength(1));
  expect(f.requests[0].body).toMatchObject({ generate: false, reasoning: { effort: "medium" } });
  await session.prompt("ready");
  expect(f.requests[1].body).toMatchObject({
    previous_response_id: "resp_1",
    reasoning: { effort: "medium" },
  });
  expect(f.httpCalls()).toBe(0);
});

it("generates directly when input arrives before a scheduled warmup", async () => {
  const f = await fixture();
  const session = agentSession(f);
  await session.prompt("ready immediately");
  expect(f.requests).toHaveLength(1);
  expect(f.requests[0].body.generate).toBeUndefined();
});

it("cancels obsolete in-flight warmup when reasoning changes", async () => {
  const f = await fixture(({ socket, body }, count) => {
    if (count !== 1)
      socket.send(JSON.stringify(completed(`resp_${count}`, body.generate === false ? [] : undefined)));
  });
  const session = agentSession(f);
  await vi.waitFor(() => expect(f.requests).toHaveLength(1));
  session.setThinkingLevel("medium");
  await vi.waitFor(() => expect(f.requests).toHaveLength(2));
  expect(f.requests[1].socket).not.toBe(f.requests[0].socket);
  expect(f.requests[1].body).toMatchObject({ generate: false, reasoning: { effort: "medium" } });
  await session.prompt("after change");
  expect(f.requests[2].body.previous_response_id).toBe("resp_2");
  expect(f.httpCalls()).toBe(0);
});

it("routes WebSocket upgrades through the host's resolved HTTP CONNECT proxy", async () => {
  const f = await fixture();
  let connections = 0;
  const proxy = createServer();
  proxy.on("connect", (request, client, head) => {
    connections++;
    const target = new URL(`http://${request.url}`);
    const upstream = connect(Number(target.port), target.hostname, () => {
      client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
      upstream.write(head);
      client.pipe(upstream).pipe(client);
    });
    upstream.on("error", () => client.destroy());
    client.on("close", () => upstream.destroy());
  });
  await new Promise<void>((resolve) => proxy.listen(0, "127.0.0.1", resolve));
  cleanups.push(() => new Promise<void>((resolve) => proxy.close(() => resolve())));
  const proxied = new ResponsesSession({
    resolveProxy: async () => `http://127.0.0.1:${(proxy.address() as AddressInfo).port}`,
  });
  cleanups.push(() => proxied.dispose());
  const result = await f.run([user("proxied")], { responsesSession: proxied });
  expect(result.stopReason).toBe("stop");
  expect(connections).toBe(1);
  expect(f.httpCalls()).toBe(0);
});

it("does not retry generation when an onResponse observer fails", async () => {
  const f = await fixture();
  const result = await f.run([user("first")], {
    onResponse: () => {
      throw new Error("observer failed");
    },
  });
  expect(result.stopReason).toBe("error");
  expect(result.errorMessage).toContain("observer failed");
  expect(f.httpCalls()).toBe(0);
});

it("keeps a timed-out warmup from delaying the next request with another WebSocket attempt", async () => {
  const f = await fixture(() => {});
  const controller = new AbortController();
  const warming = f.run([], { responsesWarmup: true, signal: controller.signal });
  await vi.waitFor(() => expect(f.requests).toHaveLength(1));
  controller.abort();
  await warming;
  expect((await f.run([user("continue over HTTP")])).responseId).toBe("http");
  expect(f.requests).toHaveLength(1);
  expect(f.httpCalls()).toBe(1);
});

it("releases a stuck proxy warmup on cancellation so generation can fall back to HTTP", async () => {
  const f = await fixture();
  const proxy = vi.fn(() => new Promise<string | undefined>(() => {}));
  const session = new ResponsesSession({ resolveProxy: proxy });
  cleanups.push(() => session.dispose());
  const warmup = await f.run([user("hello")], {
    responsesSession: session,
    responsesWarmup: true,
    streamFirstResponseTimeoutMs: 20,
  });
  expect(warmup.stopReason).toBe("error");
  const result = await f.run([user("hello")], {
    responsesSession: session,
    streamFirstResponseTimeoutMs: 2000,
  });
  expect(result.stopReason).toBe("stop");
  expect(f.httpCalls()).toBe(1);
  expect(proxy).toHaveBeenCalledTimes(1);
});

it("cancels queued requests without disconnecting or overtaking the active request", async () => {
  const f = await fixture(({ socket }, count) => {
    if (count > 1) socket.send(JSON.stringify(completed(`resp_${count}`)));
  });
  const first = f.run([user("first")]);
  await vi.waitFor(() => expect(f.requests).toHaveLength(1));
  const controller = new AbortController();
  const waiting = f.run([user("cancel me")], { signal: controller.signal });
  controller.abort();
  expect((await waiting).stopReason).toBe("aborted");
  const third = f.run([user("third")]);
  await new Promise((resolve) => setTimeout(resolve, 20));
  expect(f.requests).toHaveLength(1);
  f.requests[0].socket.send(JSON.stringify(completed("resp_1")));
  expect((await first).stopReason).toBe("stop");
  expect((await third).stopReason).toBe("stop");
  expect(f.requests).toHaveLength(2);
});

it("disposes both an unresolved proxy lookup and its queued requests", async () => {
  const f = await fixture();
  const entered = Promise.withResolvers<void>();
  const session = new ResponsesSession({
    resolveProxy: () => {
      entered.resolve();
      return new Promise<string | undefined>(() => {});
    },
  });
  const first = f.run([user("first")], { responsesSession: session });
  await entered.promise;
  const second = f.run([user("second")], { responsesSession: session });
  session.dispose();
  for (const result of await Promise.all([first, second])) {
    expect(result.stopReason).toBe("error");
    expect(result.errorMessage).toContain("disposed");
  }
  expect(f.httpCalls()).toBe(0);
});