Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/app/v1/_lib/proxy/forwarder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ function decodeRequestBodyAsJson(body: BodyInit | undefined): Record<string, unk
text = body;
} else if (Buffer.isBuffer(body)) {
text = body.toString("utf8");
} else if (body instanceof ArrayBuffer) {
text = Buffer.from(body).toString("utf8");
} else if (body instanceof Uint8Array) {
text = Buffer.from(body).toString("utf8");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const mocks = vi.hoisted(() => ({
getAgent: vi.fn(),
markOriginUnhealthy: vi.fn(),
})),
evaluateResponsesWsEligibility: vi.fn(async () => ({
isWebsocketClient: false,
eligible: false,
})),
tryResponsesWebsocketUpstream: vi.fn(),
}));

vi.mock("@/lib/config", async (importOriginal) => {
Expand All @@ -27,6 +32,24 @@ vi.mock("@/lib/proxy-agent", () => ({
getGlobalAgentPool: mocks.getGlobalAgentPool,
}));

vi.mock("@/app/v1/_lib/responses-ws/eligibility", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/app/v1/_lib/responses-ws/eligibility")>();
return {
...actual,
evaluateResponsesWsEligibility: mocks.evaluateResponsesWsEligibility,
getResponsesWsSessionId: vi.fn(() => "client-ws-session"),
};
});

vi.mock("@/app/v1/_lib/responses-ws/upstream-adapter", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/app/v1/_lib/responses-ws/upstream-adapter")>();
return {
...actual,
tryResponsesWebsocketUpstream: mocks.tryResponsesWebsocketUpstream,
};
});

import { resolveEndpointPolicy } from "@/app/v1/_lib/proxy/endpoint-policy";
import { ProxyForwarder } from "@/app/v1/_lib/proxy/forwarder";
import { rectifyResponseInput } from "@/app/v1/_lib/proxy/response-input-rectifier";
Expand Down Expand Up @@ -124,6 +147,11 @@ function readBodyText(body: BodyInit | undefined): string | null {
describe("ProxyForwarder raw passthrough regression", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.evaluateResponsesWsEligibility.mockResolvedValue({
isWebsocketClient: false,
eligible: false,
});
mocks.tryResponsesWebsocketUpstream.mockReset();
});

it("raw passthrough 应优先保留原始请求体字节,而不是重新 JSON.stringify", async () => {
Expand Down Expand Up @@ -189,6 +217,56 @@ describe("ProxyForwarder raw passthrough regression", () => {
expect(await response.text()).toBe(upstreamSse);
});

it("remote compaction v2 ArrayBuffer 请求体仍通过上游 Responses WebSocket", async () => {
const requestBody = {
model: "gpt-5.5",
stream: true,
previous_response_id: "resp_previous",
input: [{ type: "compaction_trigger" }],
};
const originalBody = JSON.stringify(requestBody);
const upstreamSse =
'event: response.completed\ndata: {"type":"response.completed","response":{"id":"resp_compact","status":"completed"}}\n\n';
const session = createRawPassthroughSession(originalBody, {
"x-codex-beta-features": "remote_compaction_v2",
});
session.requestUrl = new URL("https://proxy.example.com/v1/responses");
const provider = createProvider();

mocks.evaluateResponsesWsEligibility.mockResolvedValue({
isWebsocketClient: true,
eligible: true,
endpointId: null,
});
mocks.tryResponsesWebsocketUpstream.mockResolvedValue({
response: new Response(upstreamSse, {
status: 200,
headers: { "content-type": "text/event-stream" },
}),
connected: true,
reused: true,
});

const fetchWithoutAutoDecode = vi.spyOn(ProxyForwarder as any, "fetchWithoutAutoDecode");
fetchWithoutAutoDecode.mockImplementationOnce(
async () => new Response("unexpected HTTP fallback", { status: 500 })
);
const { doForward } = ProxyForwarder as unknown as {
doForward: (session: ProxySession, provider: Provider, baseUrl: string) => Promise<Response>;
};

const response = await doForward(session, provider, provider.url);

expect(mocks.tryResponsesWebsocketUpstream).toHaveBeenCalledWith(
expect.objectContaining({
body: requestBody,
sessionId: "client-ws-session",
})
);
expect(fetchWithoutAutoDecode).not.toHaveBeenCalled();
expect(await response.text()).toBe(upstreamSse);
});

it("remote compaction v2 将单对象 input 规范化后再透传", async () => {
const originalBody = '{"model":"gpt-5.5","stream":true,"input":{"type":"compaction_trigger"}}';
const session = createRawPassthroughSession(originalBody, {
Expand Down
Loading