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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { OpenAIResponsesInterceptor } from './types.ts';
import { telemetryModelIdentity } from '../../../shared/telemetry/attribution.ts';
import { syntheticEventsFromResult } from '../items/output.ts';
import type { OpenAIResponsesResult } from '@floway-dev/protocols/openai-responses';
import { eventResult, providerModelOf } from '@floway-dev/provider';

// Codex opens every session with a WebSocket prewarm: a `response.create` that
// carries the session's instructions and tools with `generate: false`. It is
// connection setup rather than inference, so Codex waits only for the terminal
// `response.completed` and then continues from that response's id, sending
// just the items the prewarm did not already carry.
// https://github.com/openai/codex/blob/6989c6548b3737f108e2bb5ae1171b1d2032e30c/codex-rs/core/src/client.rs#L17-L18
// https://github.com/openai/codex/blob/6989c6548b3737f108e2bb5ae1171b1d2032e30c/codex-rs/core/src/client.rs#L2025
// https://github.com/openai/codex/blob/6989c6548b3737f108e2bb5ae1171b1d2032e30c/codex-rs/core/src/client.rs#L2181-L2184
//
// No upstream call can stand in for it. The Codex HTTP backend rejects the
// field with `{"detail":"Unsupported parameter: generate"}`, and a translated
// target drops it and runs a full, billed generation. The gateway answers the
// prewarm itself: by now serve preparation has resolved the model, expanded
// any `previous_response_id`, and staged this request's input, so the empty
// completed response commits a snapshot of exactly that input and the next
// turn's continuation replays it. An id no upstream serves still fails before
// this runs.
//
// No `performance` context on the result: a turn that never dialed the
// upstream has no latency to report. The usage row still lands, at zero, so the
// request stays visible in the dashboard.
export const answerWebSocketWarmup: OpenAIResponsesInterceptor = async (ctx, _gatewayCtx, run) => {
if (ctx.payload.generate !== false) return await run();
const result: OpenAIResponsesResult = {
// Replaced by the client-output boundary's own response id.
id: '',
object: 'response',
model: ctx.payload.model,
status: 'completed',
output: [],
error: null,
incomplete_details: null,
usage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
};
return eventResult(syntheticEventsFromResult(result), telemetryModelIdentity(ctx.candidate, providerModelOf(ctx.candidate).id));
};
Original file line number Diff line number Diff line change
Expand Up @@ -251,15 +251,10 @@ const resultMetadata = async (
...(result.performance !== undefined ? { performance: result.performance } : {}),
});

// The spec makes the item lifecycle the authority and requires nothing of the
// terminal's `output`; a Codex upstream states an `output` that omits the
// assistant message it just closed. A turn that closed nothing falls back to
// the terminal, as the client-facing egress does.
// The reassembler takes `output` from the closed items, since a Codex upstream
// states a terminal `output` that omits the assistant message it just closed.
// https://github.com/openresponses/openresponses/blob/92c12d96d7b61d6d15e2214daa5e9c6000ab6e1c/src/specifications/2026-04-24.mdx#L237
const summaryTextFrom = (closed: Map<number, OpenAIResponsesOutputItem>, stated: readonly OpenAIResponsesOutputItem[]): string => {
const items = closed.size === 0
? stated
: [...closed].sort(([left], [right]) => left - right).map(([, item]) => item);
const summaryTextFrom = (items: readonly OpenAIResponsesOutputItem[]): string => {
const parts: string[] = [];
for (const item of items) {
if (item.type !== 'message') continue;
Expand All @@ -273,17 +268,8 @@ const summaryTextFrom = (closed: Map<number, OpenAIResponsesOutputItem>, stated:
const collectSummaryTurn = async (
result: Extract<ExecuteResult<ProtocolFrame<OpenAIResponsesStreamEvent>>, { type: 'events' }>,
): Promise<{ response: OpenAIResponsesResult; text: string }> => {
const closedItems = new Map<number, OpenAIResponsesOutputItem>();
const observed = (async function* (): AsyncIterable<ProtocolFrame<OpenAIResponsesStreamEvent>> {
for await (const frame of result.events) {
if (frame.type === 'event' && frame.event.type === 'response.output_item.done') {
closedItems.set(frame.event.output_index, frame.event.item);
}
yield frame;
}
})();
const response = await collectOpenAIResponsesProtocolEventsToResult(observed);
return { response, text: summaryTextFrom(closedItems, response.output) };
const response = await collectOpenAIResponsesProtocolEventsToResult(result.events);
return { response, text: summaryTextFrom(response.output) };
};

const buildCompactionEnvelope = (cmpId: string, summaryText: string, upstream: OpenAIResponsesResult): OpenAIResponsesResult => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { answerWebSocketWarmup } from './answer-websocket-warmup.ts';
import { withRoleCompatibilityApplied } from './apply-role-compatibility.ts';
import { withOpenAIResponsesCollaborationShim } from './collaboration-shim.ts';
import { withOpenAIResponsesCompactShim } from './compact-shim.ts';
Expand All @@ -21,6 +22,8 @@ import { withVendorQwenOpenAIResponsesNormalize } from './vendor-qwen-normalize.
// after pairwise translation has finished.
//
// Order matters: earlier entries wrap later ones.
// - answerWebSocketWarmup: runs outermost so a `generate: false` prewarm is
// answered before any shim or upstream call can turn it into a generation.
// - withOpenAIResponsesCompactShim: runs outermost so the action pivot
// ('compact' → 'generate' for the inner summarization turn) is visible
// to every downstream interceptor + the provider terminal. Also
Expand Down Expand Up @@ -53,6 +56,7 @@ import { withVendorQwenOpenAIResponsesNormalize } from './vendor-qwen-normalize.
// the role-compatibility entry so each gets the final say on the outbound wire
// body.
export const openaiResponsesInterceptors: readonly OpenAIResponsesInterceptor[] = [
answerWebSocketWarmup,
withOpenAIResponsesCompactShim,
withOpenAIResponsesCollaborationShim,
withOpenAIResponsesServerToolShim([
Expand Down
Loading
Loading