* Add /api/usage/request-details/raw endpoint serving a single stored request detail verbatim (raw payloads), with /raw doc clarifying it stays gated by the dashboard auth layer. * Add RawDetailModal opened from a new 'Raw' button in RequestDetailsTab. Modal loads /raw, exposes per-section copy buttons and a 'Copy all (JSON)' that bundles every section. * Capture the raw provider SSE text inside the streaming transform (cap 64KB) and forward it through onStreamComplete.rawProviderText so handler stores it as the providerResponse. response.content stays the extracted user text. Tool-call-only turns remain so the marker. * Accumulate from translated client-facing chunks instead of raw provider shapes so Responses, Claude delta types, and Gemini/Antigravity parts all contribute. * Drop redaction from the list endpoint; raw access is now via the dedicated /raw endpoint. Tests cover the new behavior.
160 lines
4.2 KiB
JavaScript
160 lines
4.2 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { FORMATS } from "../../open-sse/translator/formats.js";
|
|
import { createSSETransformStreamWithLogger } from "../../open-sse/utils/stream.js";
|
|
|
|
async function runStream({ targetFormat, sourceFormat, lines }) {
|
|
const encoder = new TextEncoder();
|
|
const stream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(lines.join("\n")));
|
|
controller.close();
|
|
},
|
|
});
|
|
|
|
let completed = null;
|
|
const output = stream.pipeThrough(
|
|
createSSETransformStreamWithLogger(
|
|
targetFormat,
|
|
sourceFormat,
|
|
"codex",
|
|
null,
|
|
null,
|
|
"gpt-5.5",
|
|
null,
|
|
null,
|
|
(contentObj) => {
|
|
completed = contentObj;
|
|
},
|
|
),
|
|
);
|
|
|
|
const reader = output.getReader();
|
|
const decoder = new TextDecoder();
|
|
let text = "";
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
text += decoder.decode(value, { stream: true });
|
|
}
|
|
text += decoder.decode();
|
|
return { text, completed };
|
|
}
|
|
|
|
const sse = (event, data) => [
|
|
`event: ${event}`,
|
|
`data: ${JSON.stringify(data)}`,
|
|
"",
|
|
];
|
|
|
|
const completedEvent = (id = "resp_1") =>
|
|
sse("response.completed", {
|
|
type: "response.completed",
|
|
response: { id, status: "completed" },
|
|
});
|
|
|
|
describe("streaming content accumulation for request details", () => {
|
|
it("accumulates Responses text deltas in same-format passthrough", async () => {
|
|
const lines = [
|
|
...sse("response.output_text.delta", {
|
|
type: "response.output_text.delta",
|
|
output_index: 0,
|
|
content_index: 0,
|
|
delta: "Hello ",
|
|
}),
|
|
...sse("response.output_text.delta", {
|
|
type: "response.output_text.delta",
|
|
output_index: 0,
|
|
content_index: 0,
|
|
delta: "world",
|
|
}),
|
|
...completedEvent(),
|
|
"data: [DONE]",
|
|
"",
|
|
];
|
|
|
|
const { completed } = await runStream({
|
|
targetFormat: FORMATS.OPENAI_RESPONSES,
|
|
sourceFormat: FORMATS.OPENAI_RESPONSES,
|
|
lines,
|
|
});
|
|
|
|
expect(completed?.content).toBe("Hello world");
|
|
expect(typeof completed?.rawProviderText).toBe("string");
|
|
expect(completed.rawProviderText).toContain("Hello ");
|
|
});
|
|
|
|
it("accumulates translated OpenAI chunks for Responses -> OpenAI clients", async () => {
|
|
const lines = [
|
|
...sse("response.output_text.delta", {
|
|
type: "response.output_text.delta",
|
|
output_index: 0,
|
|
content_index: 0,
|
|
delta: "Hello ",
|
|
}),
|
|
...sse("response.output_text.delta", {
|
|
type: "response.output_text.delta",
|
|
output_index: 0,
|
|
content_index: 0,
|
|
delta: "world",
|
|
}),
|
|
...completedEvent(),
|
|
"data: [DONE]",
|
|
"",
|
|
];
|
|
|
|
const { text, completed } = await runStream({
|
|
targetFormat: FORMATS.OPENAI_RESPONSES,
|
|
sourceFormat: FORMATS.OPENAI,
|
|
lines,
|
|
});
|
|
|
|
expect(completed?.content).toBe("Hello world");
|
|
expect(text).toContain("Hello ");
|
|
});
|
|
|
|
it("accumulates Gemini/Antigravity text chunks through passthrough", async () => {
|
|
const lines = [
|
|
`data: ${JSON.stringify({ response: { candidates: [{ content: { parts: [{ text: "Hello Gemini" }] } }] } })}`,
|
|
"",
|
|
];
|
|
|
|
const { completed } = await runStream({
|
|
targetFormat: FORMATS.GEMINI,
|
|
sourceFormat: FORMATS.GEMINI,
|
|
lines,
|
|
});
|
|
|
|
expect(completed?.content).toBe("Hello Gemini");
|
|
});
|
|
|
|
it("accumulates reasoning deltas into thinking", async () => {
|
|
const lines = [
|
|
...sse("response.reasoning_summary_text.delta", {
|
|
type: "response.reasoning_summary_text.delta",
|
|
output_index: 0,
|
|
summary_index: 0,
|
|
delta: "thinking...",
|
|
}),
|
|
...sse("response.output_text.delta", {
|
|
type: "response.output_text.delta",
|
|
output_index: 1,
|
|
content_index: 0,
|
|
delta: "answer",
|
|
}),
|
|
...completedEvent(),
|
|
"data: [DONE]",
|
|
"",
|
|
];
|
|
|
|
const { completed } = await runStream({
|
|
targetFormat: FORMATS.OPENAI_RESPONSES,
|
|
sourceFormat: FORMATS.OPENAI_RESPONSES,
|
|
lines,
|
|
});
|
|
|
|
expect(completed?.content).toBe("answer");
|
|
expect(completed?.thinking).toBe("thinking...");
|
|
});
|
|
});
|