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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This project uses selective package publishing. Each release entry lists the pub

### Fixed

- OpenAI Responses sellers now preserve explicit reasoning effort across Chat Completions and Responses request adaptation, so `reasoning_effort: "none"` reaches the upstream as `reasoning: { effort: "none" }` instead of being silently removed and allowing the upstream to use a different default. Non-stream responses also preserve completed streamed output items when an upstream's final event contains an empty `output`, preventing valid CatGPT answers from becoming blank responses.
- Desktop telemetry launches now appear as sessions in PostHog. Events carried the launch id only as a custom `session_id` property and as a v4 UUID, so PostHog's Sessions explorer — which keys on `$session_id` and derives the session start from a UUIDv7 timestamp — showed nothing. The launch id is now a UUIDv7 and is sent as both `session_id` and `$session_id`, enabling per-launch analysis (session duration, events per launch, crash rate) without changing anything built on `session_id`.
- Phones browsing the website in "Desktop site" mode no longer download desktop installers they can't run. Mobile detection for download CTAs previously relied on viewport width alone, so a phone requesting the desktop site (which widens the layout viewport and, in Samsung Internet, spoofs an `X11; Linux` user agent) was handed the Linux AppImage. The reroute to the `/get-started` flow now also checks touch-only hardware (`pointer: coarse` + `hover: none`) and the UA-CH mobile signal — neither of which desktop-site mode changes — and platform detection treats such devices as unknown, so an installer is never resolved for them. Analytics counts these taps as `get_started` funnel entries instead of download conversions, matching the behavior. Touchscreen laptops keep a fine, hover-capable primary pointer and still get the direct download.
- Seller and transport failures returned through the buyer protocol now clearly explain that the selected peer failed, suggest choosing another peer or Auto routing, preserve the seller's original response and status for diagnostics, and identify pinned-peer failures so clients can surface them immediately without retrying the same peer. Buyer-side failures, payment-required responses, and actionable request errors remain unchanged.
Expand Down
12 changes: 12 additions & 0 deletions packages/api-adapter/src/canonical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface CanonicalLlmRequest {
instructions?: string;
input: CanonicalInputItem[];
maxOutputTokens?: number;
reasoningEffort?: string;
temperature?: number;
topP?: number;
stop?: string | string[];
Expand Down Expand Up @@ -178,6 +179,7 @@ export function renderCanonicalRequestToOpenAIChatBody(
...(request.stream ? { stream_options: { include_usage: true } } : {}),
};
if (typeof request.maxOutputTokens === 'number') body.max_tokens = request.maxOutputTokens;
if (request.reasoningEffort !== undefined) body.reasoning_effort = request.reasoningEffort;
if (typeof request.temperature === 'number') body.temperature = request.temperature;
if (typeof request.topP === 'number') body.top_p = request.topP;
if (request.stop !== undefined) body.stop = request.stop;
Expand Down Expand Up @@ -234,6 +236,7 @@ export function renderCanonicalRequestToOpenAIResponsesBody(
stream: request.stream,
};
if (typeof request.maxOutputTokens === 'number') body.max_output_tokens = request.maxOutputTokens;
if (request.reasoningEffort !== undefined) body.reasoning = { effort: request.reasoningEffort };
if (typeof request.temperature === 'number') body.temperature = request.temperature;
if (typeof request.topP === 'number') body.top_p = request.topP;
if (request.stop !== undefined) body.stop = request.stop;
Expand Down Expand Up @@ -471,6 +474,9 @@ export function normalizeOpenAIChatRequestBody(body: Record<string, unknown>): C
}

if (typeof body.max_tokens === 'number') request.maxOutputTokens = body.max_tokens;
if (typeof body.reasoning_effort === 'string' && body.reasoning_effort.length > 0) {
request.reasoningEffort = body.reasoning_effort;
}
if (typeof body.temperature === 'number') request.temperature = body.temperature;
if (typeof body.top_p === 'number') request.topP = body.top_p;
if (typeof body.stop === 'string' || Array.isArray(body.stop)) request.stop = body.stop as string | string[];
Expand Down Expand Up @@ -542,6 +548,12 @@ export function normalizeOpenAIResponsesRequestBody(body: Record<string, unknown
}

if (typeof body.max_output_tokens === 'number') request.maxOutputTokens = body.max_output_tokens;
if (body.reasoning && typeof body.reasoning === 'object' && !Array.isArray(body.reasoning)) {
const effort = (body.reasoning as Record<string, unknown>).effort;
if (typeof effort === 'string' && effort.length > 0) {
request.reasoningEffort = effort;
}
}
if (typeof body.temperature === 'number') request.temperature = body.temperature;
if (typeof body.top_p === 'number') request.topP = body.top_p;
if (typeof body.stop === 'string' || Array.isArray(body.stop)) request.stop = body.stop as string | string[];
Expand Down
32 changes: 32 additions & 0 deletions packages/api-adapter/tests/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,20 @@ describe('transformRequest responses to chat', () => {
expect(messages[1]).toEqual({ role: 'user', content: 'What is the capital of France?' });
});

it('preserves reasoning effort when converting responses to chat completions', () => {
const request = makeResponsesRequest({
body: new TextEncoder().encode(JSON.stringify({
model: 'gpt-5.6-sol',
input: 'Return only 42',
reasoning: { effort: 'none' },
})),
});
const result = transformRequest(request, { from: 'openai-responses', to: 'openai-chat-completions' });

const body = JSON.parse(new TextDecoder().decode(result!.request.body)) as Record<string, unknown>;
expect(body.reasoning_effort).toBe('none');
});

it('converts array input to messages', () => {
const request = makeResponsesRequest({
body: new TextEncoder().encode(JSON.stringify({
Expand Down Expand Up @@ -1848,6 +1862,24 @@ describe('transformRequest chat to responses', () => {
]);
});

it('preserves reasoning effort when converting chat completions to responses', () => {
const request: SerializedHttpRequest = {
requestId: 'req-chat-reasoning-effort',
method: 'POST',
path: '/v1/chat/completions',
headers: { 'content-type': 'application/json' },
body: new TextEncoder().encode(JSON.stringify({
model: 'gpt-5.6-sol',
messages: [{ role: 'user', content: 'Return only 42' }],
reasoning_effort: 'none',
})),
};
const result = transformRequest(request, { from: 'openai-chat-completions', to: 'openai-responses' });

const body = JSON.parse(new TextDecoder().decode(result!.request.body)) as Record<string, unknown>;
expect(body.reasoning).toEqual({ effort: 'none' });
});

it('carries an explicit prompt_cache_key through to the responses body', () => {
const request: SerializedHttpRequest = {
requestId: 'req-chat-cache-key',
Expand Down
68 changes: 65 additions & 3 deletions plugins/provider-openai-responses/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,47 @@ describe('provider-openai-responses plugin', () => {
rmSync(dirname(authFile), { recursive: true, force: true });
});

it('preserves an explicit reasoning profile when relaying to the Codex backend', async () => {
const authFile = writeAuthFile({
tokens: {
access_token: makeJwt({}),
account_id: 'acct-file',
},
});
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ id: 'resp_1' }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;

const provider = plugin.createProvider({
OPENAI_RESPONSES_AUTH_FILE: authFile,
ANTSEED_ALLOWED_SERVICES: 'gpt-5.6-sol',
});

await provider.handleRequest({
requestId: 'req-reasoning-profile',
method: 'POST',
path: '/v1/responses',
headers: { 'content-type': 'application/json' },
body: new TextEncoder().encode(JSON.stringify({
model: 'gpt-5.6-sol',
input: 'Return only 42',
reasoning: { effort: 'none' },
stream: false,
})),
});

const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
const upstreamBody = JSON.parse(
new TextDecoder().decode((init.body as Uint8Array) ?? new Uint8Array(0)),
) as Record<string, unknown>;
expect(upstreamBody.reasoning).toEqual({ effort: 'none' });
rmSync(dirname(authFile), { recursive: true, force: true });
});

it('rewrites announced service names via alias map', async () => {
const authFile = writeAuthFile({
tokens: {
Expand Down Expand Up @@ -337,9 +378,23 @@ describe('provider-openai-responses plugin', () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(
'event: response.created\n'
+ 'data: {"type":"response.created","response":{"id":"resp_1","model":"gpt-5.5","status":"in_progress","output":[]}}\n\n'
+ 'data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_1","model":"gpt-5.5","status":"in_progress","output":[]}}\n\n'
+ 'event: response.output_item.added\n'
+ 'data: {"type":"response.output_item.added","sequence_number":1,"output_index":0,"item":{"type":"message","id":"msg_1","role":"assistant","status":"in_progress","content":[]}}\n\n'
+ 'event: response.content_part.added\n'
+ 'data: {"type":"response.content_part.added","sequence_number":2,"item_id":"msg_1","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}}\n\n'
+ 'event: response.output_text.delta\n'
+ 'data: {"type":"response.output_text.delta","sequence_number":3,"item_id":"msg_1","output_index":0,"content_index":0,"delta":"h","logprobs":[]}\n\n'
+ 'event: response.output_text.delta\n'
+ 'data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_1","output_index":0,"content_index":0,"delta":"i","logprobs":[]}\n\n'
+ 'event: response.output_text.done\n'
+ 'data: {"type":"response.output_text.done","sequence_number":5,"item_id":"msg_1","output_index":0,"content_index":0,"text":"hi","logprobs":[]}\n\n'
+ 'event: response.content_part.done\n'
+ 'data: {"type":"response.content_part.done","sequence_number":6,"item_id":"msg_1","output_index":0,"content_index":0,"part":{"type":"output_text","text":"hi","annotations":[]}}\n\n'
+ 'event: response.output_item.done\n'
+ 'data: {"type":"response.output_item.done","sequence_number":7,"output_index":0,"item":{"type":"message","id":"msg_1","role":"assistant","status":"completed","content":[{"type":"output_text","text":"hi","annotations":[]}]}}\n\n'
+ 'event: response.completed\n'
+ 'data: {"type":"response.completed","response":{"id":"resp_1","object":"response","model":"gpt-5.5","status":"completed","output":[{"type":"message","id":"msg_1","role":"assistant","status":"completed","content":[{"type":"output_text","text":"hi","annotations":[]}]}],"output_text":"hi","usage":{"input_tokens":3,"output_tokens":1,"total_tokens":4}}}\n\n'
+ 'data: {"type":"response.completed","sequence_number":8,"response":{"id":"resp_1","object":"response","model":"gpt-5.5","status":"completed","output":[],"usage":{"input_tokens":3,"output_tokens":1,"total_tokens":4}}}\n\n'
+ 'data: [DONE]\n\n',
{
status: 200,
Expand Down Expand Up @@ -374,7 +429,14 @@ describe('provider-openai-responses plugin', () => {
expect(response.headers['content-type']).toBe('application/json');
const body = JSON.parse(new TextDecoder().decode(response.body)) as Record<string, unknown>;
expect(body.id).toBe('resp_1');
expect(body.output_text).toBe('hi');
expect(body.output).toEqual([{
type: 'message',
id: 'msg_1',
role: 'assistant',
status: 'completed',
content: [{ type: 'output_text', text: 'hi', annotations: [] }],
}]);
expect(body.output_text).toBeUndefined();
rmSync(dirname(authFile), { recursive: true, force: true });
});

Expand Down
19 changes: 19 additions & 0 deletions plugins/provider-openai-responses/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ function collapseResponsesSseResponse(response: SerializedHttpResponse): Seriali
}

function parseResponsesSse(text: string): { body: Record<string, unknown>; statusCode?: number } | null {
const outputItems: Array<Record<string, unknown> | undefined> = [];

for (const block of text.replace(/\r\n/g, '\n').split('\n\n')) {
const lines = block.split('\n');
const event = lines
Expand All @@ -399,6 +401,7 @@ function parseResponsesSse(text: string): { body: Record<string, unknown>; statu
event !== 'response.completed'
&& event !== 'response.failed'
&& event !== 'error'
&& event !== 'response.output_item.done'
) {
continue;
}
Expand All @@ -412,6 +415,16 @@ function parseResponsesSse(text: string): { body: Record<string, unknown>; statu

try {
const parsed = JSON.parse(data) as Record<string, unknown>;
if (event === 'response.output_item.done') {
const item = parsed.item;
const outputIndex = parsed.output_index;
if (typeof outputIndex === 'number' && Number.isInteger(outputIndex) && outputIndex >= 0
&& item && typeof item === 'object' && !Array.isArray(item)) {
outputItems[outputIndex] = item as Record<string, unknown>;
}
continue;
}

if (event === 'error') {
return { body: normalizeUpstreamStreamError(parsed), statusCode: 502 };
}
Expand All @@ -422,6 +435,12 @@ function parseResponsesSse(text: string): { body: Record<string, unknown>; statu
if (event === 'response.failed') {
return { body: normalizeUpstreamStreamError(body), statusCode: 502 };
}
const completedOutput = outputItems.filter(
(item): item is Record<string, unknown> => item !== undefined,
);
if (completedOutput.length > 0 && (!Array.isArray(body.output) || body.output.length === 0)) {
return { body: { ...body, output: completedOutput } };
}
return { body };
}

Expand Down
Loading