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
8 changes: 5 additions & 3 deletions packages/provider-codex/src/interceptors/responses/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

import { injectDefaultInstructions } from './inject-default-instructions.ts';
import { injectSessionId } from './inject-session-id.ts';
import { sanitizeMessageIds } from './sanitize-message-ids.ts';
import { stripUnsupportedFields } from './strip-unsupported-fields.ts';
import type { ResponsesBoundaryCtx } from './types.ts';
import type { Interceptor } from '@floway-dev/interceptor';

// Order rationale: none of the three interceptors below read or write a field
// the others touch, so order is positional only. inject-session-id last is
// Order rationale: none of the interceptors below read or write a field the
// others touch, so order is positional only. inject-session-id last is
// conventional but not load-bearing — it hashes only `instructions + first
// user-message text`, neither of which is mutated by the other two.
// user-message text`, neither of which is mutated by the earlier mutators.
//
// Each interceptor is generic over the terminal result type: the streaming
// `/responses` chain runs to ProviderStreamResult, the compaction chain runs
Expand All @@ -21,5 +22,6 @@ import type { Interceptor } from '@floway-dev/interceptor';
export const codexResponsesChain = <TResult>(): readonly Interceptor<ResponsesBoundaryCtx, object, TResult>[] => [
injectDefaultInstructions,
stripUnsupportedFields,
sanitizeMessageIds,
injectSessionId,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ResponsesBoundaryCtx } from './types.ts';
import type { ResponsesInputItem } from '@floway-dev/protocols/responses';

export const sanitizeMessageIds = async <TResult>(
ctx: ResponsesBoundaryCtx,
_request: object,
run: () => Promise<TResult>,
): Promise<TResult> => {
if (!Array.isArray(ctx.payload.input)) return await run();

const input = sanitizeInputItems(ctx.payload.input);
if (input !== ctx.payload.input) ctx.payload = { ...ctx.payload, input };
return await run();
};

const sanitizeInputItems = (items: ResponsesInputItem[]): ResponsesInputItem[] => {
let changed = false;
const sanitized = items.map(item => {
if (!isMessageInputItem(item)) return item;
const id = (item as { id?: unknown }).id;
if (id === undefined || (typeof id === 'string' && id.startsWith('msg'))) return item;

const next: Record<string, unknown> = { ...item };
delete next.id;
changed = true;
return next as unknown as ResponsesInputItem;
});
return changed ? sanitized : items;
};

const isMessageInputItem = (item: ResponsesInputItem): boolean => {
const obj = item as { type?: unknown; role?: unknown };
return obj.type === undefined ? isMessageRole(obj.role) : obj.type === 'message';
};

const isMessageRole = (role: unknown): boolean =>
role === 'user' || role === 'assistant' || role === 'system' || role === 'developer';
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { test } from 'vitest';

import { sanitizeMessageIds } from './sanitize-message-ids.ts';
import type { ResponsesBoundaryCtx } from './types.ts';
import type { ResponsesPayload, ResponsesStreamEvent } from '@floway-dev/protocols/responses';
import type { ProviderStreamResult } from '@floway-dev/provider';
import { assertEquals, stubUpstreamModel } from '@floway-dev/test-utils';

const stubRequest = {};

const okEvents = (): Promise<ProviderStreamResult<ResponsesStreamEvent>> =>
Promise.resolve({ ok: true, events: (async function* () {})(), modelKey: 'test', headers: new Headers() });

const invocation = (payload: ResponsesPayload): ResponsesBoundaryCtx => ({
payload,
headers: new Headers(),
model: stubUpstreamModel({ endpoints: { responses: {} } }),
});

test('drops Codex review rollout message ids while preserving review content', async () => {
const ctx = invocation({
model: 'gpt-test',
input: [
{
type: 'message',
id: 'review_rollout_user',
role: 'user',
content: [{ type: 'input_text', text: 'User initiated a review task.' }],
},
{
type: 'message',
id: 'review_rollout_assistant',
role: 'assistant',
content: [{ type: 'output_text', text: 'review assistant output' }],
},
],
});

await sanitizeMessageIds(ctx, stubRequest, okEvents);

assertEquals(ctx.payload.input, [
{
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'User initiated a review task.' }],
},
{
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: 'review assistant output' }],
},
]);
});

test('drops invalid ids from typeless easy input messages', async () => {
const ctx = invocation({
model: 'gpt-test',
input: [
{
id: 'review_rollout_user',
role: 'user',
content: 'User initiated a review task.',
},
],
} as unknown as ResponsesPayload);

await sanitizeMessageIds(ctx, stubRequest, okEvents);

assertEquals(ctx.payload.input, [
{
role: 'user',
content: 'User initiated a review task.',
},
]);
});

test('keeps Codex/OpenAI message ids intact', async () => {
const ctx = invocation({
model: 'gpt-test',
input: [
{ type: 'message', id: 'msg_valid', role: 'user', content: 'hello' },
],
});

await sanitizeMessageIds(ctx, stubRequest, okEvents);

assertEquals(ctx.payload.input, [
{ type: 'message', id: 'msg_valid', role: 'user', content: 'hello' },
]);
});

test('keeps non-message item ids intact', async () => {
const ctx = invocation({
model: 'gpt-test',
input: [
{
type: 'function_call',
id: 'call_local',
call_id: 'call_1',
name: 'tool',
arguments: '{}',
status: 'completed',
},
],
});

await sanitizeMessageIds(ctx, stubRequest, okEvents);

assertEquals(ctx.payload.input, [
{
type: 'function_call',
id: 'call_local',
call_id: 'call_1',
name: 'tool',
arguments: '{}',
status: 'completed',
},
]);
});

test('leaves string input untouched', async () => {
const ctx = invocation({ model: 'gpt-test', input: 'hello' });

await sanitizeMessageIds(ctx, stubRequest, okEvents);

assertEquals(ctx.payload, { model: 'gpt-test', input: 'hello' });
});