Skip to content

Commit 9667f49

Browse files
committed
Integrate upstream Responses websocket and compaction updates
Merge czy-all into dev after preserving local Responses handling while accepting upstream websocket safety, rate-limit adaptation, session header normalization, and server-side compaction changes. Constraint: best-of-both-worlds workflow requires dev-side conflict resolution with user-approved hunks Rejected: resolving conflicts on czy-all | would pollute the upstream buffer branch Confidence: high Scope-risk: moderate Directive: keep future czy-all updates as pure caozhiyuan/dev synchronization only Tested: bun run lint:all --fix; bun run build; bun test; bun run typecheck Not-tested: live Copilot upstream behavior
2 parents ee240fa + e07dd7f commit 9667f49

10 files changed

Lines changed: 390 additions & 70 deletions

desktop/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "copilot-api-desktop",
3-
"version": "1.10.25",
3+
"version": "1.10.29",
44
"description": "Copilot API Desktop App",
55
"main": "out/main/index.js",
66
"scripts": {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://json.schemastore.org/package.json",
33
"name": "@jeffreycao/copilot-api",
4-
"version": "1.10.25",
4+
"version": "1.10.29",
55
"description": "OpenAI and Anthropic-compatible gateway for GitHub Copilot or Codex or third-party providers.",
66
"keywords": [
77
"ai-gateway",

src/routes/responses/handler.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -457,10 +457,9 @@ const handleWithCopilotResponses = async ({
457457
)
458458
}
459459

460-
applyResponsesApiContextManagement(
461-
payload,
462-
selectedModel.capabilities.limits.max_prompt_tokens,
463-
)
460+
// Smaller than the client compaction threshold, use server-side compaction to maintain cache hit rate
461+
const maxPromptTokens = selectedModel?.capabilities.limits.max_prompt_tokens
462+
applyResponsesApiContextManagement(payload, maxPromptTokens, 0.8)
464463

465464
debugJson(logger, "Translated Responses payload:", payload)
466465

src/services/codex/create-responses.ts

Lines changed: 142 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { events, type ServerSentEventMessage } from "fetch-event-stream"
44

55
import type {
66
CreateResponsesReturn,
7+
ResponseInputContent,
8+
ResponseInputItem,
9+
ResponseInputMessage,
710
ResponsesPayload,
811
ResponseErrorEvent,
912
ResponsesResult,
@@ -132,7 +135,7 @@ export function buildCodexResponsesHeaders(
132135
headers.set("originator", "opencode")
133136
const sessionId = requestContext.getStore()?.sessionAffinity
134137
if (sessionId) {
135-
headers.set("session_id", sessionId)
138+
headers.set("session-id", sessionId)
136139
}
137140
}
138141
return headers
@@ -161,7 +164,7 @@ export function buildCodexResponsesWebSocketPayload(
161164
payload: ResponsesPayload,
162165
): CodexResponsesWebSocketPayload {
163166
const websocketPayload: CodexResponsesWebSocketPayload = {
164-
...payload,
167+
...normalizeCodexResponsesPayload(payload),
165168
type: "response.create",
166169
}
167170

@@ -199,24 +202,13 @@ export async function forwardCodexResponses(
199202
transport?: ResponsesTransport
200203
} = {},
201204
): Promise<CreateResponsesReturn> {
202-
const normalizedPayload: ResponsesPayload = {
203-
...payload,
204-
store: false,
205-
temperature: undefined,
206-
top_p: undefined,
207-
max_output_tokens: undefined,
208-
metadata: undefined,
209-
}
210-
211205
const transport = resolveCodexResponsesTransport(options.transport)
212-
if (normalizedPayload.stream && transport === "websocket") {
213-
return forwardCodexResponsesOverWebSocket(
214-
normalizedPayload,
215-
requestHeaders,
216-
baseUrl,
217-
)
206+
if (payload.stream && transport === "websocket") {
207+
return forwardCodexResponsesOverWebSocket(payload, requestHeaders, baseUrl)
218208
}
219209

210+
const normalizedPayload = normalizeCodexResponsesPayload(payload)
211+
220212
const response = await fetch(resolveCodexResponsesUrl(baseUrl), {
221213
method: "POST",
222214
headers: buildCodexResponsesHeaders(requestHeaders, {
@@ -236,6 +228,136 @@ export async function forwardCodexResponses(
236228
return (await response.json()) as ResponsesResult
237229
}
238230

231+
const normalizeCodexResponsesPayload = (
232+
payload: ResponsesPayload,
233+
): ResponsesPayload => {
234+
const normalizedPayload: ResponsesPayload = {
235+
...payload,
236+
store: false,
237+
}
238+
239+
delete normalizedPayload.temperature
240+
delete normalizedPayload.top_p
241+
delete normalizedPayload.max_output_tokens
242+
delete normalizedPayload.metadata
243+
244+
if (
245+
(typeof normalizedPayload.instructions === "string"
246+
&& normalizedPayload.instructions.trim().length > 0)
247+
|| !Array.isArray(normalizedPayload.input)
248+
) {
249+
return normalizedPayload
250+
}
251+
252+
const instructions: Array<string> = []
253+
let messageCount = 0
254+
const remainingInput = normalizedPayload.input.filter((inputItem) => {
255+
const message = getResponseInputMessage(inputItem)
256+
if (!message) {
257+
return true
258+
}
259+
260+
messageCount += 1
261+
if (message.role !== "system" || messageCount > 3) {
262+
return true
263+
}
264+
265+
const systemPrompt = getTextContent(message.content)
266+
if (systemPrompt === undefined) {
267+
return true
268+
}
269+
if (systemPrompt.trim().length > 0) {
270+
instructions.push(systemPrompt)
271+
}
272+
273+
return false
274+
})
275+
276+
if (remainingInput.length === normalizedPayload.input.length) {
277+
return normalizedPayload
278+
}
279+
280+
if (instructions.length > 0) {
281+
// Codex expects system prompts in instructions instead of input messages.
282+
normalizedPayload.instructions = instructions.join("\n\n")
283+
}
284+
285+
if (remainingInput.length > 0) {
286+
normalizedPayload.input = remainingInput
287+
} else {
288+
delete normalizedPayload.input
289+
}
290+
291+
return normalizedPayload
292+
}
293+
294+
const getResponseInputMessage = (
295+
inputItem: ResponseInputItem,
296+
): ResponseInputMessage | undefined => {
297+
if (typeof inputItem !== "object" || inputItem === null) {
298+
return undefined
299+
}
300+
301+
const { role, type } = inputItem as {
302+
role?: unknown
303+
type?: unknown
304+
}
305+
if (typeof role !== "string" || (type !== undefined && type !== "message")) {
306+
return undefined
307+
}
308+
309+
return inputItem as ResponseInputMessage
310+
}
311+
312+
const getTextContent = (
313+
content: ResponseInputMessage["content"],
314+
): string | undefined => {
315+
if (typeof content === "string") {
316+
return content
317+
}
318+
319+
if (content === undefined) {
320+
return ""
321+
}
322+
323+
if (!Array.isArray(content)) {
324+
return undefined
325+
}
326+
327+
const textBlocks: Array<string> = []
328+
for (const contentBlock of content) {
329+
const text = getTextBlock(contentBlock)
330+
if (text === undefined) {
331+
return undefined
332+
}
333+
334+
if (text.length > 0) {
335+
textBlocks.push(text)
336+
}
337+
}
338+
339+
return textBlocks.join("\n\n")
340+
}
341+
342+
const getTextBlock = (
343+
contentBlock: ResponseInputContent,
344+
): string | undefined => {
345+
if (typeof contentBlock !== "object" || contentBlock === null) {
346+
return undefined
347+
}
348+
349+
const { text, type } = contentBlock as {
350+
text?: unknown
351+
type?: unknown
352+
}
353+
354+
if (type !== undefined && type !== "input_text" && type !== "output_text") {
355+
return undefined
356+
}
357+
358+
return typeof text === "string" ? text : undefined
359+
}
360+
239361
const buildCodexResponsesWebSocketPoolKey = (
240362
payload: ResponsesPayload,
241363
headers: Record<string, string>,
@@ -250,9 +372,9 @@ const buildCodexResponsesWebSocketPoolKey = (
250372
const headerFingerprint = createHash("sha256")
251373
.update(
252374
JSON.stringify(
253-
Object.entries(headers).sort(([left], [right]) =>
254-
left.localeCompare(right),
255-
),
375+
Object.entries(headers)
376+
.filter(([headerName]) => !headerName.toLowerCase().includes("trace"))
377+
.sort(([left], [right]) => left.localeCompare(right)),
256378
),
257379
)
258380
.digest("hex")

src/services/copilot/create-responses.ts

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ export interface ResponseErrorEvent {
372372
param: string | null
373373
sequence_number: number
374374
type: "error"
375+
error?: {
376+
code: string | null
377+
message: string
378+
}
375379
}
376380

377381
export interface ResponseFunctionCallArgumentsDeltaEvent {
@@ -451,6 +455,12 @@ export type ResponsesStream = ReturnType<typeof events>
451455
export type CreateResponsesReturn = ResponsesResult | ResponsesStream
452456
export type ResponsesTransport = "http" | "websocket"
453457

458+
type ResponsesStreamChunk = {
459+
data?: string
460+
event?: string
461+
id?: string | number
462+
}
463+
454464
interface ResponsesRequestOptions {
455465
vision: boolean
456466
initiator: "agent" | "user"
@@ -698,14 +708,26 @@ export const getResponsesWebSocketInitiator = (
698708
const createPooledResponsesWebSocketStream = (
699709
request: ResponsesWebSocketRequest,
700710
): ResponsesStream =>
701-
createPooledWebSocketStream(request, {
702-
createChunk: createResponsesWebSocketStreamChunk,
703-
isTerminalChunk: isTerminalResponsesStreamChunk,
704-
openErrorMessage: "Failed to create responses websocket",
705-
streamErrorMessage: "Responses websocket stream error",
706-
terminalChunkMissingMessage:
707-
"Responses websocket ended without a terminal response",
708-
}) as ResponsesStream
711+
createResponsesSafeStream(
712+
createPooledWebSocketStream(request, {
713+
createChunk: createResponsesWebSocketStreamChunk,
714+
isTerminalChunk: isTerminalResponsesStreamChunk,
715+
openErrorMessage: "Failed to create responses websocket",
716+
streamErrorMessage: "Responses websocket stream error",
717+
terminalChunkMissingMessage:
718+
"Responses websocket ended without a terminal response",
719+
}),
720+
)
721+
722+
const createResponsesSafeStream = async function* (
723+
source: AsyncIterable<ResponsesStreamChunk>,
724+
): AsyncGenerator<ResponsesStreamChunk, void, unknown> {
725+
try {
726+
yield* source
727+
} catch (error) {
728+
yield createResponsesErrorServerSentEventChunk(getErrorMessage(error))
729+
}
730+
}
709731

710732
export const buildResponsesWebSocketPayload = (
711733
payload: ResponsesPayload,
@@ -754,10 +776,20 @@ const createResponsesWebSocketStreamChunk = (
754776
copilot_quota_snapshots?: Record<string, CopilotQuotaSnapshot>
755777
id?: unknown
756778
type?: unknown
779+
error?: {
780+
code: string | null
781+
message: string
782+
}
783+
code?: string | null
784+
message?: string
757785
}
758786
if (parsed.type === "response.completed") {
759787
logCopilotQuotaSnapshots(parsed.copilot_quota_snapshots)
760788
}
789+
if (parsed.type === "error" && parsed.error) {
790+
parsed.code = parsed.error.code
791+
parsed.message = parsed.error.message
792+
}
761793
return {
762794
data: JSON.stringify(parsed),
763795
event: typeof parsed.type === "string" ? parsed.type : undefined,
@@ -785,3 +817,28 @@ const isTerminalResponsesStreamChunk = (chunk: { data?: string }): boolean => {
785817
return false
786818
}
787819
}
820+
821+
const createResponsesErrorServerSentEventChunk = (
822+
message: string,
823+
): ResponsesStreamChunk => {
824+
const errorEvent: ResponseErrorEvent = {
825+
code: null,
826+
message,
827+
param: null,
828+
sequence_number: 0,
829+
type: "error",
830+
}
831+
832+
return {
833+
data: JSON.stringify(errorEvent),
834+
event: errorEvent.type,
835+
}
836+
}
837+
838+
const getErrorMessage = (error: unknown): string => {
839+
if (error instanceof Error && error.message) {
840+
return error.message
841+
}
842+
843+
return String(error)
844+
}

0 commit comments

Comments
 (0)