Skip to content
Merged
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
29 changes: 27 additions & 2 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,28 @@ function buildExecutorClientHeaders(
return Object.keys(normalized).length > 0 ? normalized : null;
}

function isCopilotClient(
headers: Headers | Record<string, unknown> | null | undefined,
userAgent?: string | null
) {
const isMatch = (value: unknown) =>
typeof value === "string" && value.toLowerCase().includes("copilot");

if (isMatch(userAgent)) return true;

if (headers instanceof Headers) {
for (const [key, value] of headers) {
if (isMatch(key) || isMatch(value)) return true;
}
} else if (headers && typeof headers === "object") {
for (const [key, value] of Object.entries(headers)) {
if (isMatch(key) || isMatch(value)) return true;
}
}

return false;
}
Comment on lines +925 to +945

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The isCopilotClient function eagerly builds an array of all header keys and values before performing the check. This results in unnecessary memory allocations and iterations, especially for requests with many headers. A more efficient approach would be to iterate and return early as soon as a match is found.

Suggested change
function isCopilotClient(
headers: Headers | Record<string, unknown> | null | undefined,
userAgent?: string | null
) {
const values: string[] = [];
if (typeof userAgent === "string") values.push(userAgent);
if (headers instanceof Headers) {
headers.forEach((value, key) => {
values.push(key, value);
});
} else if (headers && typeof headers === "object") {
for (const [key, value] of Object.entries(headers)) {
values.push(key);
if (typeof value === "string") values.push(value);
}
}
return values.some((value) => value.toLowerCase().includes("copilot"));
}
function isCopilotClient(
headers: Headers | Record<string, unknown> | null | undefined,
userAgent?: string | null
) {
const isMatch = (v: unknown) => typeof v === "string" && v.toLowerCase().includes("copilot");
if (isMatch(userAgent)) return true;
if (headers instanceof Headers) {
for (const [key, value] of headers) {
if (isMatch(key) || isMatch(value)) return true;
}
} else if (headers && typeof headers === "object") {
for (const [key, value] of Object.entries(headers)) {
if (isMatch(key) || isMatch(value)) return true;
}
}
return false;
}


export async function handleChatCore({
body,
modelInfo,
Expand Down Expand Up @@ -1099,6 +1121,7 @@ export async function handleChatCore({
});
const isDroidCLI =
userAgent?.toLowerCase().includes("droid") || userAgent?.toLowerCase().includes("codex-cli");
const copilotCompatibleReasoning = isCopilotClient(clientRawRequest?.headers, userAgent);
const clientResponseFormat =
sourceFormat === FORMATS.OPENAI_RESPONSES && !isResponsesEndpoint && !isDroidCLI
? FORMATS.OPENAI
Expand Down Expand Up @@ -3777,7 +3800,8 @@ export async function handleChatCore({
streamStateBody,
onStreamComplete,
apiKeyInfo,
handleStreamFailure
handleStreamFailure,
copilotCompatibleReasoning
);
} else if (needsTranslation(targetFormat, clientResponseFormat)) {
// Standard translation for other providers
Expand All @@ -3793,7 +3817,8 @@ export async function handleChatCore({
streamStateBody,
onStreamComplete,
apiKeyInfo,
handleStreamFailure
handleStreamFailure,
copilotCompatibleReasoning
);
} else {
log?.debug?.("STREAM", `Standard passthrough mode`);
Expand Down
3 changes: 3 additions & 0 deletions open-sse/handlers/responseSanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,9 @@ export function sanitizeStreamingChunk(parsed: unknown): unknown {
}
if (deltaRecord.reasoning_content !== undefined) {
delta.reasoning_content = deltaRecord.reasoning_content;
}
if (deltaRecord.reasoning_text !== undefined) {
delta.reasoning_text = deltaRecord.reasoning_text;
} else if (typeof deltaRecord.reasoning === "string" && deltaRecord.reasoning) {
// Alias: some providers use 'reasoning' instead of 'reasoning_content'
delta.reasoning_content = deltaRecord.reasoning;
Expand Down
5 changes: 4 additions & 1 deletion open-sse/translator/response/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,9 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
if (eventType === "response.reasoning_summary_text.delta") {
const reasoningDelta = data.delta || "";
if (!reasoningDelta) return null;
const reasoningDeltaShape = state.copilotCompatibleReasoning
? { reasoning_text: reasoningDelta }
: { reasoning: { summary: reasoningDelta } };
return {
id: state.chatId,
object: "chat.completion.chunk",
Expand All @@ -835,7 +838,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
choices: [
{
index: 0,
delta: { reasoning: { summary: reasoningDelta } },
delta: reasoningDeltaShape,
finish_reason: null,
},
],
Expand Down
8 changes: 7 additions & 1 deletion open-sse/utils/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ type StreamOptions = {
targetFormat?: string;
sourceFormat?: string;
clientResponseFormat?: string | null;
copilotCompatibleReasoning?: boolean;
provider?: string | null;
reqLogger?: StreamLogger | null;
toolNameMap?: unknown;
Expand All @@ -150,6 +151,7 @@ type TranslateState = ReturnType<typeof initState> & {
toolNameMap?: unknown;
usage?: unknown;
finishReason?: unknown;
copilotCompatibleReasoning?: boolean;
/** Accumulated message content for call log response body */
accumulatedContent?: string;
upstreamError?: {
Expand Down Expand Up @@ -531,6 +533,7 @@ export function createSSEStream(options: StreamOptions = {}) {
targetFormat,
sourceFormat,
clientResponseFormat = null,
copilotCompatibleReasoning = false,
provider = null,
reqLogger = null,
toolNameMap = null,
Expand Down Expand Up @@ -563,6 +566,7 @@ export function createSSEStream(options: StreamOptions = {}) {
...(initState(sourceFormat) as TranslateState),
provider,
toolNameMap,
copilotCompatibleReasoning,
accumulatedContent: "",
}
: null;
Expand Down Expand Up @@ -1904,7 +1908,8 @@ export function createSSETransformStreamWithLogger(
body: unknown = null,
onComplete: ((payload: StreamCompletePayload) => void) | null = null,
apiKeyInfo: unknown = null,
onFailure: ((payload: StreamFailurePayload) => void | Promise<void>) | null = null
onFailure: ((payload: StreamFailurePayload) => void | Promise<void>) | null = null,
copilotCompatibleReasoning = false
) {
return createSSEStream({
mode: STREAM_MODE.TRANSLATE,
Expand All @@ -1919,6 +1924,7 @@ export function createSSETransformStreamWithLogger(
body,
onComplete,
onFailure,
copilotCompatibleReasoning,
});
}

Expand Down
1 change: 1 addition & 0 deletions open-sse/utils/streamHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function hasValuableContent(chunk, format) {
if (typeof delta.content === "string" && delta.content.length > 0) return true;
if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0)
return true;
if (typeof delta.reasoning_text === "string" && delta.reasoning_text.length > 0) return true;
if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) return true;
if (chunk.choices[0].finish_reason) return true;
if (typeof delta.role === "string" && delta.role.length > 0) return true;
Expand Down
1 change: 0 additions & 1 deletion tests/integration/chat-pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const { BaseExecutor } = await import("../../open-sse/executors/base.ts");
const { getCircuitBreaker, resetAllCircuitBreakers } =
await import("../../src/shared/utils/circuitBreaker.ts");
const { clearProviderFailure } = await import("../../open-sse/services/accountFallback.ts");
const { setCliCompatProviders } = await import("../../open-sse/config/cliFingerprints.ts");

const originalFetch = globalThis.fetch;
const originalRetryDelayMs = BaseExecutor.RETRY_CONFIG.delayMs;
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/response-sanitizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,20 @@ test("sanitizeStreamingChunk converts reasoning_details arrays in deltas", () =>
assert.equal((sanitized as any).choices[0].delta.reasoning_content, "alphabeta");
});

test("sanitizeStreamingChunk preserves Copilot reasoning_text deltas", () => {
const sanitized = sanitizeStreamingChunk({
choices: [
{
delta: {
reasoning_text: "copilot reasoning",
},
},
],
});

assert.equal((sanitized as any).choices[0].delta.reasoning_text, "copilot reasoning");
});

test("sanitize functions return non-object inputs unchanged", () => {
assert.equal(sanitizeOpenAIResponse(null), null);
assert.equal(sanitizeStreamingChunk("raw text"), "raw text");
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/responses-translation-fixes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,29 @@ test("Responses→Chat streaming: reasoning delta emits reasoning_content in Cha
assert.equal(result.choices[0].delta.reasoning.summary, "thinking step...");
});

test("Responses→Chat streaming: Copilot mode emits reasoning_text for summary deltas", () => {
const state = {
started: false,
chatId: null,
created: null,
toolCallIndex: 0,
finishReasonSent: false,
copilotCompatibleReasoning: true,
};

const chunk = {
type: "response.reasoning_summary_text.delta",
delta: "thinking step...",
item_id: "rs_1",
output_index: 0,
summary_index: 0,
};
const result = openaiResponsesToOpenAIResponse(chunk, state);
assert.ok(result, "should return a chunk");
assert.equal(result.choices[0].delta.reasoning_text, "thinking step...");
assert.equal(result.choices[0].delta.reasoning, undefined);
});

test("Chat→Responses streaming: multiple <think> tags in one chunk handled", () => {
const state = initState(FORMATS.OPENAI_RESPONSES);

Expand Down
5 changes: 5 additions & 0 deletions tests/unit/streamHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ describe("hasValuableContent", () => {
assert.strictEqual(hasValuableContent(chunk, FORMATS.OPENAI), true);
});

it("returns true for Copilot reasoning_text", () => {
const chunk = { choices: [{ delta: { reasoning_text: "thinking" } }] };
assert.strictEqual(hasValuableContent(chunk, FORMATS.OPENAI), true);
});

it("returns true for finish_reason", () => {
const chunk = { choices: [{ delta: {}, finish_reason: "stop" }] };
assert.strictEqual(hasValuableContent(chunk, FORMATS.OPENAI), true);
Expand Down
Loading