Skip to content
Draft
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: 1 addition & 1 deletion docs-site/src/content/docs/guides/sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ See the [Configuration reference](/reference/configuration/server/#sidecars) for

Search responses with malformed optional fields retain valid answer text and citations. Invalid error-message values use a generic failure message.

The OpenAI search and vision sidecars accept CR, LF and CRLF line endings, including mixed endings and CRLF split across network chunks. They read a final completion or failure event even when the server closes the body without a trailing newline. An unfinished response or one stopped by a size limit still reports an error.
The OpenAI search and vision sidecars accept CR, LF and CRLF line endings, including mixed endings and CRLF split across network chunks. They read a final completion or failure event even when the server closes the body without a trailing newline. The first terminal event or `[DONE]` ends collection; later frames are ignored even when they share its network chunk. An unfinished response or one stopped by a size limit still reports an error.

When a search answer ends with a Sources list, an unmatched list item stays in the answer instead of being removed as a citation. A title followed by its URL is still consumed as one citation. Non-HTTP URI-like text inside inline code, such as `mode:fast`, stays in the answer; code-formatted HTTP(S) URLs are still citations.

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/ko/guides/sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ fingerprint 선례를 따르지만, 실제 계정과 작업량으로 충분히 s

검색 응답의 선택 필드 형식이 잘못되어도 유효한 답변과 출처는 유지합니다. 오류 메시지 값의 형식이 잘못되면 일반 실패 메시지를 표시합니다.

OpenAI 검색·비전 사이드카는 CR, LF, CRLF 줄바꿈과 이들이 섞인 응답을 처리하며, CRLF가 여러 네트워크 청크로 나뉘어도 이벤트를 읽습니다. 서버가 마지막 줄바꿈 없이 응답 본문을 닫아도 마지막 완료 또는 실패 이벤트를 읽습니다. 완료되지 않았거나 크기 제한으로 중단된 응답은 계속 오류로 처리합니다.
OpenAI 검색·비전 사이드카는 CR, LF, CRLF 줄바꿈과 이들이 섞인 응답을 처리하며, CRLF가 여러 네트워크 청크로 나뉘어도 이벤트를 읽습니다. 서버가 마지막 줄바꿈 없이 응답 본문을 닫아도 마지막 완료 또는 실패 이벤트를 읽습니다. 첫 종료 이벤트 또는 `[DONE]`에서 수집을 끝내며, 같은 네트워크 청크에 있어도 뒤따르는 이벤트는 무시합니다. 완료되지 않았거나 크기 제한으로 중단된 응답은 계속 오류로 처리합니다.

검색 답변의 Sources 목록에서 URL과 짝을 이루지 못한 글머리표는 출처로 삭제하지 않고 답변에 남깁니다. 제목 다음 줄에 해당 URL이 있으면 하나의 출처로 처리합니다. `mode:fast`처럼 인라인 코드 안의 비 HTTP URI 형태 텍스트는 답변에 남기며, 코드로 표시한 HTTP(S) URL은 출처로 처리합니다.

Expand Down
2 changes: 1 addition & 1 deletion src/web-search/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
};

const handle = (payload: string): void => {
if (!payload) return;
if (!payload || acc.terminalEvent) return;
if (payload === "[DONE]") {
acc.terminalEvent = true;
return;
Expand Down
2 changes: 2 additions & 0 deletions structure/data-planes/search.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Search Data Plane

The Responses sidecar parser in `src/web-search/parse.ts` closes collection at the first terminal event or `[DONE]`. Later frames in that network chunk cannot change text, sources, error state or decoded accounting. `tests/web-search/web-search-eof.test.ts` checks this with whole and fragmented bodies.

The opt-in key-auth Responses hosted-search bridge follows the
[continuation binding contract](../providers-and-adapters.md#hosted-search-continuation-binding).
`src/web-search/loop.ts` follows [prepaid initial sends](../transports/responses-spend.md#prepaid-initial-sends)
Expand Down
34 changes: 34 additions & 0 deletions tests/web-search/web-search-eof.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,40 @@ function eofResponse(wire: string, fragmented = false): Response {

const prefix = 'data: {"type":"response.output_text.delta","delta":"draft"}\n\n';

describe("Responses sidecar terminal boundary", () => {
for (const terminal of [
'data: {"type":"response.completed"}\n\n',
'data: {"type":"response.done"}\n\n',
"data: [DONE]\n\n",
]) {
for (const fragmented of [false, true]) {
test(`ignores later events after ${terminal.trim()} (${fragmented ? "fragmented" : "one chunk"})`, async () => {
const wire = prefix + terminal + 'data: {"type":"response.output_text.delta","delta":"unexpected"}\n\n' +
'data: {"type":"response.failed","error":{"message":"late failure"}}\n\n';
expect(await parseSidecarSSE(eofResponse(wire, fragmented))).toEqual({ text: "draft", sources: [] });
});
}
}

for (const fragmented of [false, true]) {
test(`a failure cannot acquire a later response's final output (${fragmented ? "fragmented" : "one chunk"})`, async () => {
const wire = prefix + 'data: {"type":"response.failed","error":{"message":"stopped"}}\n\n' +
`data: ${JSON.stringify({ type: "response.completed", response: { output: [
{ type: "message", content: [{ type: "output_text", text: "Later response" }] },
] } })}\n\n`;
expect(await parseSidecarSSE(eofResponse(wire, fragmented))).toEqual({
text: "draft", sources: [], error: "stopped",
});
});

test(`text after completion cannot consume the decoded budget (${fragmented ? "fragmented" : "one chunk"})`, async () => {
const wire = prefix + 'data: {"type":"response.completed"}\n\n' +
`data: ${JSON.stringify({ type: "response.output_text.delta", delta: "x".repeat(MAX_SIDECAR_DECODED_CHARS) })}\n\n`;
expect(await parseSidecarSSE(eofResponse(wire, fragmented))).toEqual({ text: "draft", sources: [] });
});
}
});

describe("Responses sidecar final data line at EOF", () => {
test.each(["response.completed", "response.done"])("accepts an unterminated %s with authoritative text and sources", async type => {
const final = { type, response: { output: [{ type: "message", content: [{
Expand Down
Loading