Preserve the active worktree snapshot on dev for follow-on integration

The root checkout already contained a large in-progress proxy, transformer,
and settings refactor. This commit captures that exact work on a dedicated
local dev branch without disturbing the original dirty worktree, so the
changes can be continued or split from dev instead of living only as
uncommitted state.

Constraint: The source checkout was already dirty on another branch, so the move had to preserve that worktree while excluding local .idea and .omx residue from the commit
Rejected: Switch the dirty root checkout directly to dev | risked disrupting the original branch and in-progress state
Confidence: medium
Scope-risk: broad
Reversibility: clean
Directive: Treat this as a transfer snapshot; fix the failing oauth/accountTokens/normalized/docs test regressions before merge or publication
Tested: git diff --check; npm run typecheck; npm run repo:drift-check
Not-tested: Clean test pass; npm test currently fails in oauth routes, accountTokens sync, shared normalized helpers, gemini route expectations, architecture-boundaries, and docs vitepress config
This commit is contained in:
cita-777
2026-04-17 19:41:14 +08:00
parent 6cca74b039
commit 88f40e91f7
133 changed files with 13780 additions and 5670 deletions
@@ -0,0 +1,101 @@
import { describe, expect, it } from 'vitest';
import {
ensureCompactResponsesJsonAcceptHeader,
sanitizeCompactResponsesRequestBody,
shouldForceResponsesUpstreamStream,
} from './responsesCompact.js';
describe('sanitizeCompactResponsesRequestBody', () => {
it('removes stream fields and store from codex compact requests', () => {
expect(sanitizeCompactResponsesRequestBody({
stream: true,
stream_options: { include_obfuscation: true },
store: false,
input: 'hello',
}, {
sitePlatform: 'codex',
})).toEqual({
input: 'hello',
});
});
it('removes store from sub2api compact requests', () => {
expect(sanitizeCompactResponsesRequestBody({
stream: true,
stream_options: { include_obfuscation: true },
store: false,
input: 'hello',
}, {
sitePlatform: 'sub2api',
})).toEqual({
input: 'hello',
});
});
it('keeps store for unrelated compact platforms while still removing stream fields', () => {
expect(sanitizeCompactResponsesRequestBody({
stream: true,
stream_options: { include_obfuscation: true },
store: false,
input: 'hello',
}, {
sitePlatform: 'openai',
})).toEqual({
store: false,
input: 'hello',
});
});
});
describe('shouldForceResponsesUpstreamStream', () => {
it('forces non-compact responses streaming for codex and sub2api', () => {
expect(shouldForceResponsesUpstreamStream({
sitePlatform: 'codex',
isCompactRequest: false,
})).toBe(true);
expect(shouldForceResponsesUpstreamStream({
sitePlatform: 'sub2api',
isCompactRequest: false,
})).toBe(true);
});
it('does not force compact or unrelated platforms', () => {
expect(shouldForceResponsesUpstreamStream({
sitePlatform: 'codex',
isCompactRequest: true,
})).toBe(false);
expect(shouldForceResponsesUpstreamStream({
sitePlatform: 'openai',
isCompactRequest: false,
})).toBe(false);
});
});
describe('ensureCompactResponsesJsonAcceptHeader', () => {
it('adds application/json accept for codex and sub2api compact requests when missing', () => {
expect(ensureCompactResponsesJsonAcceptHeader({}, {
sitePlatform: 'codex',
})).toEqual({
accept: 'application/json',
});
expect(ensureCompactResponsesJsonAcceptHeader({}, {
sitePlatform: 'sub2api',
})).toEqual({
accept: 'application/json',
});
});
it('preserves existing accept values and unrelated platforms', () => {
expect(ensureCompactResponsesJsonAcceptHeader({
Accept: 'application/json',
}, {
sitePlatform: 'codex',
})).toEqual({
Accept: 'application/json',
});
expect(ensureCompactResponsesJsonAcceptHeader({}, {
sitePlatform: 'openai',
})).toEqual({});
});
});
@@ -2,35 +2,81 @@ function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function shouldStripCompactResponsesStore(sitePlatform?: string): boolean {
const normalized = asTrimmedString(sitePlatform).toLowerCase();
return normalized === 'codex' || normalized === 'sub2api';
}
function shouldForceCompactResponsesJsonAccept(sitePlatform?: string): boolean {
return shouldStripCompactResponsesStore(sitePlatform);
}
export function shouldForceResponsesUpstreamStream(input: {
sitePlatform?: string;
isCompactRequest?: boolean;
}): boolean {
if (input.isCompactRequest) return false;
const sitePlatform = asTrimmedString(input.sitePlatform).toLowerCase();
return sitePlatform === 'codex' || sitePlatform === 'sub2api';
}
export function sanitizeCompactResponsesRequestBody(
body: Record<string, unknown>,
options?: {
sitePlatform?: string;
},
): Record<string, unknown> {
const next = { ...body };
delete next.stream;
delete next.stream_options;
if (shouldStripCompactResponsesStore(options?.sitePlatform)) {
delete next.store;
}
return next;
}
export function ensureCompactResponsesJsonAcceptHeader(
headers: Record<string, string>,
options?: {
sitePlatform?: string;
},
): Record<string, string> {
if (!shouldForceCompactResponsesJsonAccept(options?.sitePlatform)) return headers;
const existingAccept = asTrimmedString(headers.accept) || asTrimmedString(headers.Accept);
if (existingAccept) return headers;
return {
...headers,
accept: 'application/json',
};
}
export function shouldFallbackCompactResponsesToResponses(input: {
status?: number;
rawErrText?: string;
requestPath?: string;
}): boolean {
const status = Number.isFinite(Number(input.status)) ? Number(input.status) : 0;
const compact = asTrimmedString(input.rawErrText).toLowerCase();
const hasCompactHint = (
const requestPath = asTrimmedString(input.requestPath).toLowerCase();
const hasRawCompactHint = (
compact.includes('/responses/compact')
|| compact.includes('responses/compact')
|| compact.includes('compact endpoint')
|| /(^|[^a-z])compact([^a-z]|$)/.test(compact)
);
const hasCompactRequestPathHint = (
requestPath.endsWith('/responses/compact')
|| requestPath.endsWith('/v1/responses/compact')
);
const isInvalidCompactUrl = compact.includes('invalid url') && hasRawCompactHint;
if (status === 404 || status === 405 || status === 501) return true;
return (
compact.includes("unknown parameter: 'stream'")
|| compact.includes('invalid url')
(compact.includes("unknown parameter: 'stream'") && (hasRawCompactHint || hasCompactRequestPathHint))
|| isInvalidCompactUrl
|| (
hasCompactHint
hasRawCompactHint
&& (
compact.includes('not supported')
|| compact.includes('unsupported')
@@ -1,71 +1,14 @@
import type { CliProfileDefinition, DetectCliProfileInput } from './types.js';
import {
detectCodexOfficialClientApp as detectCodexOfficialClientAppFromHeaders,
isCodexOfficialClientHeaders,
} from '../../shared/codexClientFamily.js';
type CodexOfficialClientApp = {
clientAppId: string;
clientAppName: string;
};
const CODEX_OFFICIAL_CLIENT_USER_AGENT_PREFIXES = [
'codex_cli_rs/',
'codex_vscode/',
'codex_app/',
'codex_chatgpt_desktop/',
'codex_atlas/',
'codex_exec/',
'codex_sdk_ts/',
'codex ',
];
const CODEX_OFFICIAL_CLIENT_ORIGINATOR_PREFIXES = [
'codex_',
'codex ',
];
const CODEX_OFFICIAL_CLIENT_APP_RULES = [
{
id: 'codex_cli_rs',
name: 'Codex CLI',
userAgentPrefixes: ['codex_cli_rs/'],
originatorPrefixes: ['codex_cli_rs'],
},
{
id: 'codex_vscode',
name: 'Codex VSCode',
userAgentPrefixes: ['codex_vscode/'],
originatorPrefixes: ['codex_vscode'],
},
{
id: 'codex_app',
name: 'Codex App',
userAgentPrefixes: ['codex_app/'],
originatorPrefixes: ['codex_app'],
},
{
id: 'codex_chatgpt_desktop',
name: 'Codex Desktop',
userAgentPrefixes: ['codex_chatgpt_desktop/', 'codex desktop/'],
originatorPrefixes: ['codex_chatgpt_desktop', 'codex desktop'],
},
{
id: 'codex_atlas',
name: 'Codex Atlas',
userAgentPrefixes: ['codex_atlas/'],
originatorPrefixes: ['codex_atlas'],
},
{
id: 'codex_exec',
name: 'Codex Exec',
userAgentPrefixes: ['codex_exec/'],
originatorPrefixes: ['codex_exec'],
},
{
id: 'codex_sdk_ts',
name: 'Codex SDK TS',
userAgentPrefixes: ['codex_sdk_ts/'],
originatorPrefixes: ['codex_sdk_ts'],
},
] as const;
function headerValueToStrings(value: unknown): string[] {
if (typeof value === 'string') {
const trimmed = value.trim();
@@ -114,20 +57,6 @@ function hasHeaderPrefix(headers: Record<string, unknown> | undefined, prefix: s
});
}
function matchesHeaderPrefixes(value: string | string[] | null, prefixes: readonly string[]): boolean {
const values = Array.isArray(value)
? value.map((item) => item.trim().toLowerCase()).filter(Boolean)
: [value?.trim().toLowerCase() || ''].filter(Boolean);
if (values.length === 0) return false;
return values.some((normalizedValue) => prefixes.some((prefix) => {
const normalizedPrefix = prefix.trim().toLowerCase();
if (!normalizedPrefix) return false;
return normalizedValue.startsWith(normalizedPrefix)
|| normalizedValue.includes(normalizedPrefix);
}));
}
function isCodexPath(path: string): boolean {
const normalizedPath = path.trim().toLowerCase();
return normalizedPath === '/v1/responses'
@@ -138,16 +67,13 @@ function isCodexPath(path: string): boolean {
export function detectCodexOfficialClientApp(
headers?: Record<string, unknown>,
): CodexOfficialClientApp | null {
for (const rule of CODEX_OFFICIAL_CLIENT_APP_RULES) {
const matchesOriginator = matchesHeaderPrefixes(getHeaderValues(headers, 'originator'), rule.originatorPrefixes);
const matchesUserAgent = matchesHeaderPrefixes(getHeaderValues(headers, 'user-agent'), rule.userAgentPrefixes);
if (!matchesOriginator && !matchesUserAgent) continue;
return {
clientAppId: rule.id,
clientAppName: rule.name,
};
}
return null;
const detected = detectCodexOfficialClientAppFromHeaders(headers);
return detected
? {
clientAppId: detected.clientAppId,
clientAppName: detected.clientAppName,
}
: null;
}
export function isCodexResponsesSurface(headers?: Record<string, unknown>): boolean {
@@ -158,7 +84,10 @@ export function isCodexResponsesSurface(headers?: Record<string, unknown>): bool
}
export function getCodexSessionId(headers?: Record<string, unknown>): string | null {
return getHeaderValue(headers, 'session_id') || getHeaderValue(headers, 'session-id');
return getHeaderValue(headers, 'session_id')
|| getHeaderValue(headers, 'session-id')
|| getHeaderValue(headers, 'conversation_id')
|| getHeaderValue(headers, 'conversation-id');
}
export function isCodexRequest(input: DetectCliProfileInput): boolean {
@@ -166,9 +95,7 @@ export function isCodexRequest(input: DetectCliProfileInput): boolean {
const headers = input.headers;
if (!headers) return false;
const originator = getHeaderValues(headers, 'originator');
if (matchesHeaderPrefixes(originator, CODEX_OFFICIAL_CLIENT_ORIGINATOR_PREFIXES)) return true;
if (matchesHeaderPrefixes(getHeaderValues(headers, 'user-agent'), CODEX_OFFICIAL_CLIENT_USER_AGENT_PREFIXES)) return true;
if (isCodexOfficialClientHeaders(headers)) return true;
if (getHeaderValue(headers, 'openai-beta')) return true;
if (hasHeaderPrefix(headers, 'x-stainless-')) return true;
if (getCodexSessionId(headers)) return true;
@@ -42,6 +42,29 @@ describe('detectCliProfile', () => {
});
});
it('treats conversation_id as a Codex continuation marker when session_id is absent', () => {
expect(detectCliProfile({
downstreamPath: '/v1/responses',
headers: {
conversation_id: 'codex-conversation-123',
},
})).toEqual({
id: 'codex',
sessionId: 'codex-conversation-123',
traceHint: 'codex-conversation-123',
clientAppId: 'codex',
clientAppName: 'Codex',
clientConfidence: 'heuristic',
capabilities: {
supportsResponsesCompact: true,
supportsResponsesWebsocketIncremental: true,
preservesContinuation: true,
supportsCountTokens: false,
echoesTurnState: true,
},
});
});
it('detects broader Codex official-client headers from user-agent and originator prefixes', () => {
expect(detectCliProfile({
downstreamPath: '/v1/responses',
@@ -1,5 +1,10 @@
import type { PreparedProviderRequest, PrepareProviderRequestInput, ProviderProfile } from './types.js';
import { buildClaudeRuntimeHeaders } from './headerUtils.js';
import {
buildClaudeRuntimeHeaders,
CLAUDE_API_KEY_DEFAULT_BETA_HEADER,
CLAUDE_DEFAULT_BETA_HEADER,
CLAUDE_TOKEN_COUNTING_BETA,
} from './headerUtils.js';
export const claudeProviderProfile: ProviderProfile = {
id: 'claude',
@@ -22,6 +27,10 @@ export const claudeProviderProfile: ProviderProfile = {
stream: isCountTokens ? false : input.stream,
isClaudeOauthUpstream,
tokenValue: input.tokenValue,
defaultBetaHeader: isClaudeOauthUpstream
? CLAUDE_DEFAULT_BETA_HEADER
: CLAUDE_API_KEY_DEFAULT_BETA_HEADER,
...(isCountTokens ? { extraBetas: [CLAUDE_TOKEN_COUNTING_BETA] } : {}),
}),
body: input.body,
runtime: {
@@ -95,6 +95,7 @@ describe('provider header utils', () => {
expect(headers['anthropic-version']).toBe('2023-06-01');
expect(headers['anthropic-beta']).toContain('claude-code-20250219');
expect(headers['anthropic-beta']).toContain('fine-grained-tool-streaming-2025-05-14');
expect(headers['anthropic-beta']).toContain('custom-beta');
expect(headers.Authorization).toBe('Bearer oauth-token');
expect(headers.authorization).toBeUndefined();
@@ -1,9 +1,12 @@
import { pbkdf2Sync, randomUUID } from 'node:crypto';
import { inferCodexOfficialOriginator } from '../../shared/codexClientFamily.js';
const CODEX_CLIENT_VERSION = '0.101.0';
const CODEX_DEFAULT_USER_AGENT = 'codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464';
const CLAUDE_DEFAULT_USER_AGENT = 'claude-cli/2.1.63 (external, cli)';
const CLAUDE_DEFAULT_BETA_HEADER = 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05';
export const CLAUDE_TOKEN_COUNTING_BETA = 'token-counting-2024-11-01';
export const CLAUDE_DEFAULT_BETA_HEADER = 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05';
export const CLAUDE_API_KEY_DEFAULT_BETA_HEADER = 'claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05';
export function headerValueToString(value: unknown): string | null {
if (typeof value === 'string') {
@@ -132,7 +135,9 @@ export function buildCodexRuntimeHeaders(input: {
|| getInputHeader(input.baseHeaders, 'Authorization')
|| ''
);
const originator = getInputHeader(input.providerHeaders, 'originator')
const originator = inferCodexOfficialOriginator(input.providerHeaders)
|| inferCodexOfficialOriginator(input.baseHeaders)
|| getInputHeader(input.providerHeaders, 'originator')
|| input.originatorDefault
|| 'codex_cli_rs';
const accountId = getInputHeader(input.providerHeaders, 'chatgpt-account-id');
@@ -17,7 +17,7 @@ describe('resolveProviderProfile', () => {
Authorization: 'Bearer oauth-access-token',
},
providerHeaders: {
Originator: 'codex_cli_rs',
Originator: 'Codex Desktop',
'Chatgpt-Account-Id': 'chatgpt-account-123',
},
codexSessionCacheKey: 'gpt-5.2-codex:user-123',
@@ -30,7 +30,7 @@ describe('resolveProviderProfile', () => {
expect(result.path).toBe('/responses');
expect(result.headers.Authorization).toBe('Bearer oauth-access-token');
expect(result.headers.Originator).toBe('codex_cli_rs');
expect(result.headers.Originator).toBe('codex_chatgpt_desktop');
expect(result.headers['Chatgpt-Account-Id']).toBe('chatgpt-account-123');
expect(result.headers.Session_id).toMatch(/^[0-9a-f-]{36}$/i);
expect(result.headers.Conversation_id).toBe(result.headers.Session_id);
@@ -74,6 +74,7 @@ describe('resolveProviderProfile', () => {
expect(result.headers.Authorization).toBe('Bearer oauth-access-token');
expect(result.headers['x-api-key']).toBeUndefined();
expect(result.headers['anthropic-version']).toBe('2023-06-01');
expect(result.headers['anthropic-beta']).toContain('oauth-2025-04-20');
expect(result.headers['Accept-Encoding']).toBe('gzip, deflate, br, zstd');
expect(result.runtime).toMatchObject({
executor: 'claude',
@@ -83,6 +84,60 @@ describe('resolveProviderProfile', () => {
expect(result.body).toBe(protocolBody);
});
it('drops oauth-only claude betas for api-key upstreams', () => {
const profile = resolveProviderProfile('claude');
expect(profile?.id).toBe('claude');
const result = profile!.prepareRequest({
endpoint: 'messages',
modelName: 'claude-opus-4-6',
stream: false,
tokenValue: 'sk-claude-api-key',
sitePlatform: 'openai',
baseHeaders: {
'Content-Type': 'application/json',
},
claudeHeaders: {},
body: {
model: 'claude-opus-4-6',
max_tokens: 256,
messages: [{ role: 'user', content: 'hello' }],
},
});
expect(result.headers.Authorization).toBeUndefined();
expect(result.headers['x-api-key']).toBe('sk-claude-api-key');
expect(result.headers['anthropic-beta']).not.toContain('oauth-2025-04-20');
expect(result.headers['anthropic-beta']).toContain('fine-grained-tool-streaming-2025-05-14');
});
it('adds token-counting beta when building claude count_tokens requests', () => {
const profile = resolveProviderProfile('claude');
expect(profile?.id).toBe('claude');
const result = profile!.prepareRequest({
endpoint: 'messages',
modelName: 'claude-opus-4-6',
stream: false,
action: 'countTokens',
tokenValue: 'oauth-access-token',
oauthProvider: 'claude',
sitePlatform: 'claude',
baseHeaders: {
'Content-Type': 'application/json',
},
claudeHeaders: {},
body: {
model: 'claude-opus-4-6',
max_tokens: 256,
messages: [{ role: 'user', content: 'hello' }],
},
});
expect(result.path).toBe('/v1/messages/count_tokens?beta=true');
expect(result.headers['anthropic-beta']).toContain('token-counting-2024-11-01');
});
it('builds gemini-cli provider requests with wrapped runtime envelope and project validation', () => {
const profile = resolveProviderProfile('gemini-cli');
expect(profile?.id).toBe('gemini-cli');
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { config } from '../../config.js';
import {
buildCodexSessionResponseStoreKey,
clearCodexSessionResponseId,
@@ -7,6 +8,10 @@ import {
setCodexSessionResponseId,
} from './codexSessionResponseStore.js';
afterEach(() => {
vi.useRealTimers();
});
describe('codexSessionResponseStore', () => {
it('evicts the oldest session id when the store exceeds the cap', () => {
resetCodexSessionResponseStore();
@@ -154,4 +159,39 @@ describe('codexSessionResponseStore', () => {
resetCodexSessionResponseStore();
});
it('expires remembered continuation ids after the sticky-session TTL window', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-09T00:00:00Z'));
resetCodexSessionResponseStore();
setCodexSessionResponseId('session-expire', 'resp-expire');
expect(getCodexSessionResponseId('session-expire')).toBe('resp-expire');
vi.advanceTimersByTime(Math.max(5 * 60 * 1000, Math.trunc(config.proxyStickySessionTtlMs || 0)) + 1);
expect(getCodexSessionResponseId('session-expire')).toBeNull();
resetCodexSessionResponseStore();
});
it('refreshes the remembered continuation TTL when an active session reads it', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-04-09T00:00:00Z'));
resetCodexSessionResponseStore();
const ttlMs = Math.max(5 * 60 * 1000, Math.trunc(config.proxyStickySessionTtlMs || 0));
setCodexSessionResponseId('session-touch', 'resp-touch');
vi.advanceTimersByTime(ttlMs - 1_000);
expect(getCodexSessionResponseId('session-touch')).toBe('resp-touch');
vi.advanceTimersByTime(2_000);
expect(getCodexSessionResponseId('session-touch')).toBe('resp-touch');
vi.advanceTimersByTime(ttlMs + 1);
expect(getCodexSessionResponseId('session-touch')).toBeNull();
resetCodexSessionResponseStore();
});
});
@@ -1,6 +1,14 @@
const MAX_CODEX_SESSION_RESPONSE_IDS = 10_000;
import { config } from '../../config.js';
const codexSessionResponseIds = new Map<string, string>();
const MAX_CODEX_SESSION_RESPONSE_IDS = 10_000;
const MIN_CODEX_SESSION_RESPONSE_TTL_MS = 5 * 60 * 1000;
type SessionResponseEntry = {
responseId: string;
updatedAtMs: number;
};
const codexSessionResponseIds = new Map<string, SessionResponseEntry>();
const SCOPED_SESSION_SEGMENT_PREFIX = 'session:';
const SCOPED_STORE_KEY_SEGMENT_PATTERN = /^(site|account|channel):\d+$/;
@@ -71,6 +79,33 @@ function normalizeSessionId(sessionId: string): string {
return sessionId.trim();
}
function getCodexSessionResponseTtlMs(): number {
return Math.max(
MIN_CODEX_SESSION_RESPONSE_TTL_MS,
Math.trunc(config.proxyStickySessionTtlMs || 0),
);
}
function isExpiredSessionResponseEntry(entry: SessionResponseEntry, nowMs = Date.now()): boolean {
return (entry.updatedAtMs + getCodexSessionResponseTtlMs()) <= nowMs;
}
function sweepExpiredSessionResponseIds(nowMs = Date.now()): void {
for (const [key, entry] of codexSessionResponseIds.entries()) {
if (!isExpiredSessionResponseEntry(entry, nowMs)) continue;
codexSessionResponseIds.delete(key);
}
}
function touchSessionResponseEntry(sessionId: string, nowMs = Date.now()): void {
for (const key of getSessionStoreKeys(sessionId)) {
const entry = codexSessionResponseIds.get(key);
if (!entry) continue;
entry.updatedAtMs = nowMs;
codexSessionResponseIds.set(key, entry);
}
}
export function buildCodexSessionResponseStoreKey(input: {
sessionId: string;
siteId?: number | null;
@@ -89,17 +124,23 @@ export function buildCodexSessionResponseStoreKey(input: {
}
export function getCodexSessionResponseId(sessionId: string): string | null {
const nowMs = Date.now();
sweepExpiredSessionResponseIds(nowMs);
const normalized = normalizeSessionId(sessionId);
if (!normalized) return null;
const direct = codexSessionResponseIds.get(normalized);
if (direct) return direct;
if (direct) {
touchSessionResponseEntry(normalized, nowMs);
return direct.responseId;
}
for (const fallbackKey of getFallbackSessionStoreKeys(normalized)) {
const fallback = codexSessionResponseIds.get(fallbackKey);
if (fallback) {
reconcileScopedSessionFallback(fallbackKey);
return fallback;
touchSessionResponseEntry(fallbackKey, nowMs);
return fallback.responseId;
}
}
@@ -107,6 +148,8 @@ export function getCodexSessionResponseId(sessionId: string): string | null {
}
export function setCodexSessionResponseId(sessionId: string, responseId: string): void {
const nowMs = Date.now();
sweepExpiredSessionResponseIds(nowMs);
const normalizedSessionId = normalizeSessionId(sessionId);
const normalizedResponseId = responseId.trim();
if (!normalizedSessionId || !normalizedResponseId) return;
@@ -117,7 +160,10 @@ export function setCodexSessionResponseId(sessionId: string, responseId: string)
if (codexSessionResponseIds.has(key)) {
codexSessionResponseIds.delete(key);
}
codexSessionResponseIds.set(key, normalizedResponseId);
codexSessionResponseIds.set(key, {
responseId: normalizedResponseId,
updatedAtMs: nowMs,
});
}
while (codexSessionResponseIds.size > MAX_CODEX_SESSION_RESPONSE_IDS) {
@@ -141,6 +141,67 @@ describe('codexWebsocketRuntime', () => {
await runtime.closeSession('exec-session-close');
});
it('preserves remembered continuation ids across websocket session closes and reconnects', async () => {
const { createCodexWebsocketRuntime } = await import('./codexWebsocketRuntime.js');
const runtime = createCodexWebsocketRuntime();
await runtime.sendRequest({
sessionId: 'exec-session-continue-after-close',
requestUrl: upstreamWsUrl,
headers: {
Authorization: 'Bearer oauth-access-token',
'OpenAI-Beta': 'responses_websockets=2026-02-06',
},
body: {
model: 'gpt-5.4',
input: [],
},
});
await runtime.closeSession('exec-session-continue-after-close');
const recovered = await runtime.sendRequest({
sessionId: 'exec-session-continue-after-close',
requestUrl: upstreamWsUrl,
headers: {
Authorization: 'Bearer oauth-access-token',
'OpenAI-Beta': 'responses_websockets=2026-02-06',
},
body: {
model: 'gpt-5.4',
input: [
{
id: 'tool_out_runtime_1',
type: 'function_call_output',
call_id: 'call_runtime_1',
output: '{"ok":true}',
},
],
},
});
expect(recovered.events[0]).toMatchObject({
type: 'response.completed',
response: { id: 'resp-2' },
});
expect(upstreamConnectionCount).toBe(2);
expect(upstreamRequests).toHaveLength(2);
expect(upstreamRequests[1]).toMatchObject({
type: 'response.create',
previous_response_id: 'resp-1',
input: [
{
id: 'tool_out_runtime_1',
type: 'function_call_output',
call_id: 'call_runtime_1',
output: '{"ok":true}',
},
],
});
await runtime.closeSession('exec-session-continue-after-close');
});
it('returns response.incomplete as a terminal websocket event without rejecting the session turn', async () => {
upstreamMessageHandler = (socket, parsed) => {
socket.send(JSON.stringify({
@@ -413,7 +474,8 @@ describe('codexWebsocketRuntime', () => {
message: 'account mismatch',
status: 502,
});
expect((error as CodexWebsocketRuntimeError).events).toEqual([
const runtimeError = error as InstanceType<typeof CodexWebsocketRuntimeError>;
expect(runtimeError.events).toEqual([
expect.objectContaining({
type: 'error',
error: expect.objectContaining({
@@ -337,7 +337,7 @@ async function sendSessionRequestAttempt(
socket.once('error', onError);
socket.once('close', onClose);
socket.send(JSON.stringify(buildCodexWebsocketRequestBody(input.body)), (error) => {
socket.send(JSON.stringify(buildCodexWebsocketRequestBody(input.body)), (error?: Error) => {
if (!error) return;
clearSessionSocket(session, socket);
rejectWith(error.message || 'failed to send upstream websocket request');
@@ -409,7 +409,9 @@ export function createCodexWebsocketRuntime(input?: {
await closeSocket(session.socket);
session.socket = null;
session.socketUrl = null;
clearCodexSessionResponseId(sessionId);
// Intentionally preserve the remembered previous_response_id across
// websocket reconnects. Closing a transport session should not sever the
// logical downstream continuation chain for the same session key.
},
async closeAllSessions(): Promise<void> {
@@ -0,0 +1,270 @@
import { zstdCompressSync } from 'node:zlib';
import { describe, expect, it } from 'vitest';
import { Response } from 'undici';
import { collectResponsesFinalPayloadFromSse } from './responsesSseFinal.js';
describe('collectResponsesFinalPayloadFromSse', () => {
it('treats event:error payloads as upstream failures', async () => {
const upstream = {
async text() {
return [
'event: error',
'data: {"error":{"message":"quota exceeded"},"type":"error"}',
'',
'data: [DONE]',
'',
].join('\n');
},
};
await expect(collectResponsesFinalPayloadFromSse(upstream, 'gpt-5.4'))
.rejects
.toThrow('quota exceeded');
});
it('prefers aggregated stream content when response.completed only carries an empty output array', async () => {
const upstream = {
async text() {
return [
'event: response.created',
'data: {"type":"response.created","response":{"id":"resp_empty_completed","model":"gpt-5.4","created_at":1706000000,"status":"in_progress","output":[]}}',
'',
'event: response.output_item.added',
'data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_empty_completed","type":"message","role":"assistant","status":"in_progress","content":[]}}',
'',
'event: response.output_text.delta',
'data: {"type":"response.output_text.delta","output_index":0,"item_id":"msg_empty_completed","delta":"pong"}',
'',
'event: response.completed',
'data: {"type":"response.completed","response":{"id":"resp_empty_completed","model":"gpt-5.4","status":"completed","output":[],"usage":{"input_tokens":3,"output_tokens":1,"total_tokens":4}}}',
'',
'data: [DONE]',
'',
].join('\n');
},
};
await expect(collectResponsesFinalPayloadFromSse(upstream, 'gpt-5.4'))
.resolves
.toMatchObject({
payload: {
id: 'resp_empty_completed',
status: 'completed',
output: [
{
id: 'msg_empty_completed',
type: 'message',
role: 'assistant',
status: 'completed',
content: [
{
type: 'output_text',
text: 'pong',
},
],
},
],
output_text: 'pong',
usage: {
input_tokens: 3,
output_tokens: 1,
total_tokens: 4,
},
},
});
});
it('decodes zstd-compressed responses SSE before aggregating the final payload', async () => {
const upstream = new Response(zstdCompressSync(Buffer.from([
'event: response.created',
'data: {"type":"response.created","response":{"id":"resp_zstd_completed","model":"gpt-5.4","created_at":1706000000,"status":"in_progress","output":[]}}',
'',
'event: response.output_item.added',
'data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_zstd_completed","type":"message","role":"assistant","status":"in_progress","content":[]}}',
'',
'event: response.output_text.delta',
'data: {"type":"response.output_text.delta","output_index":0,"item_id":"msg_zstd_completed","delta":"你好,来自 zstd 聚合 SSE"}',
'',
'event: response.completed',
'data: {"type":"response.completed","response":{"id":"resp_zstd_completed","model":"gpt-5.4","status":"completed","output":[],"usage":{"input_tokens":3,"output_tokens":1,"total_tokens":4}}}',
'',
'data: [DONE]',
'',
].join('\n'))), {
status: 200,
headers: {
'content-encoding': 'zstd',
'content-type': 'text/event-stream; charset=utf-8',
},
});
await expect(collectResponsesFinalPayloadFromSse(upstream, 'gpt-5.4'))
.resolves
.toMatchObject({
payload: {
id: 'resp_zstd_completed',
status: 'completed',
output_text: '你好,来自 zstd 聚合 SSE',
},
rawText: expect.stringContaining('你好,来自 zstd 聚合 SSE'),
});
});
it('returns response.incomplete payloads instead of treating them as upstream failures', async () => {
const upstream = {
async text() {
return [
'event: response.created',
'data: {"type":"response.created","response":{"id":"resp_incomplete_terminal","model":"gpt-5.4","created_at":1706000000,"status":"in_progress","output":[]}}',
'',
'event: response.incomplete',
'data: {"type":"response.incomplete","response":{"id":"resp_incomplete_terminal","model":"gpt-5.4","status":"incomplete","output":[{"id":"msg_incomplete_1","type":"message","role":"assistant","status":"incomplete","content":[{"type":"output_text","text":"partial answer"}]}],"output_text":"partial answer","incomplete_details":{"reason":"max_output_tokens"},"usage":{"input_tokens":3,"output_tokens":1,"total_tokens":4}}}',
'',
'data: [DONE]',
'',
].join('\n');
},
};
await expect(collectResponsesFinalPayloadFromSse(upstream, 'gpt-5.4'))
.resolves
.toMatchObject({
payload: {
id: 'resp_incomplete_terminal',
status: 'incomplete',
output_text: 'partial answer',
incomplete_details: {
reason: 'max_output_tokens',
},
},
});
});
it('returns response.incomplete terminal payloads instead of throwing', async () => {
const upstream = {
async text() {
return [
'event: response.created',
'data: {"type":"response.created","response":{"id":"resp_incomplete_1","model":"gpt-5.4","created_at":1706000000,"status":"in_progress","output":[]}}',
'',
'event: response.output_text.delta',
'data: {"type":"response.output_text.delta","output_index":0,"item_id":"msg_incomplete_1","delta":"partial"}',
'',
'event: response.incomplete',
'data: {"type":"response.incomplete","response":{"id":"resp_incomplete_1","model":"gpt-5.4","status":"incomplete","output":[{"id":"msg_incomplete_1","type":"message","role":"assistant","status":"incomplete","content":[{"type":"output_text","text":"partial"}]}],"incomplete_details":{"reason":"max_output_tokens"},"usage":{"input_tokens":3,"output_tokens":1,"total_tokens":4}}}',
'',
'data: [DONE]',
'',
].join('\n');
},
};
await expect(collectResponsesFinalPayloadFromSse(upstream, 'gpt-5.4'))
.resolves
.toMatchObject({
payload: {
id: 'resp_incomplete_1',
status: 'incomplete',
incomplete_details: {
reason: 'max_output_tokens',
},
output_text: 'partial',
},
});
});
it('treats response.incomplete as terminal even when only response.output carries the visible text', async () => {
const upstream = {
async text() {
return [
'event: response.created',
'data: {"type":"response.created","response":{"id":"resp_incomplete_output_only","model":"gpt-5.4","created_at":1706000000,"status":"in_progress","output":[]}}',
'',
'event: response.incomplete',
'data: {"type":"response.incomplete","response":{"id":"resp_incomplete_output_only","model":"gpt-5.4","status":"incomplete","output":[{"id":"msg_incomplete_output_only","type":"message","role":"assistant","status":"incomplete","content":[{"type":"output_text","text":"partial from output"}]}],"incomplete_details":{"reason":"max_output_tokens"},"usage":{"input_tokens":3,"output_tokens":1,"total_tokens":4}}}',
'',
'data: [DONE]',
'',
].join('\n');
},
};
await expect(collectResponsesFinalPayloadFromSse(upstream, 'gpt-5.4'))
.resolves
.toMatchObject({
payload: {
id: 'resp_incomplete_output_only',
status: 'incomplete',
output: [
{
id: 'msg_incomplete_output_only',
type: 'message',
role: 'assistant',
status: 'incomplete',
content: [
{
type: 'output_text',
text: 'partial from output',
},
],
},
],
output_text: 'partial from output',
incomplete_details: {
reason: 'max_output_tokens',
},
},
});
});
it('preserves incomplete item status when response.incomplete needs aggregate output repair', async () => {
const upstream = {
async text() {
return [
'event: response.created',
'data: {"type":"response.created","response":{"id":"resp_incomplete_repair","model":"gpt-5.4","created_at":1706000000,"status":"in_progress","output":[]}}',
'',
'event: response.output_item.added',
'data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_incomplete_repair","type":"message","role":"assistant","status":"in_progress","content":[]}}',
'',
'event: response.output_text.delta',
'data: {"type":"response.output_text.delta","output_index":0,"item_id":"msg_incomplete_repair","delta":"partial repair"}',
'',
'event: response.incomplete',
'data: {"type":"response.incomplete","response":{"id":"resp_incomplete_repair","model":"gpt-5.4","status":"incomplete","output":[],"incomplete_details":{"reason":"max_output_tokens"},"usage":{"input_tokens":3,"output_tokens":1,"total_tokens":4}}}',
'',
'data: [DONE]',
'',
].join('\n');
},
};
await expect(collectResponsesFinalPayloadFromSse(upstream, 'gpt-5.4'))
.resolves
.toMatchObject({
payload: {
id: 'resp_incomplete_repair',
status: 'incomplete',
output: [
{
id: 'msg_incomplete_repair',
type: 'message',
role: 'assistant',
status: 'incomplete',
content: [
{
type: 'output_text',
text: 'partial repair',
},
],
},
],
output_text: 'partial repair',
incomplete_details: {
reason: 'max_output_tokens',
},
},
});
});
});
@@ -0,0 +1,431 @@
import { openAiResponsesTransformer } from '../../transformers/openai/responses/index.js';
import { mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js';
import { readRuntimeResponseText } from '../executors/types.js';
type ResponsesTerminalStatus = 'completed' | 'incomplete';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
}
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function parseResponsesSsePayload(data: string): Record<string, unknown> | null {
try {
const parsed = JSON.parse(data);
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
}
function getResponsesFailureMessage(payload: Record<string, unknown>): string {
if (isRecord(payload.error) && typeof payload.error.message === 'string' && payload.error.message.trim()) {
return payload.error.message.trim();
}
if (typeof payload.message === 'string' && payload.message.trim()) {
return payload.message.trim();
}
return 'upstream stream failed';
}
function hasMeaningfulMessageContent(content: unknown): boolean {
if (!Array.isArray(content)) return false;
return content.some((part) => {
if (!isRecord(part)) return false;
const partType = typeof part.type === 'string' ? part.type.trim().toLowerCase() : '';
if (partType === 'output_text' || partType === 'text') {
return typeof part.text === 'string' && part.text.length > 0;
}
return true;
});
}
function hasMeaningfulResponsesOutput(output: unknown): boolean {
if (!Array.isArray(output)) return false;
return output.some((item) => {
if (!isRecord(item)) return false;
const itemType = typeof item.type === 'string' ? item.type.trim().toLowerCase() : '';
if (itemType === 'message') {
return hasMeaningfulMessageContent(item.content);
}
if (itemType === 'reasoning') {
return (
(Array.isArray(item.summary) && item.summary.length > 0)
|| (typeof item.encrypted_content === 'string' && item.encrypted_content.trim().length > 0)
);
}
return itemType.length > 0;
});
}
function hasCompleteFinalResponsesPayload(payload: Record<string, unknown>): boolean {
return (
payload.object === 'response.compaction'
|| Array.isArray(payload.output)
|| Object.prototype.hasOwnProperty.call(payload, 'output_text')
);
}
function hasMeaningfulFinalResponsesPayload(payload: Record<string, unknown>): boolean {
if (payload.object === 'response.compaction') {
return Array.isArray(payload.output) && payload.output.length > 0;
}
if (typeof payload.output_text === 'string' && payload.output_text.length > 0) {
return true;
}
return hasMeaningfulResponsesOutput(payload.output);
}
function collectResponsesOutputText(payload: Record<string, unknown>): string {
const output = Array.isArray(payload.output) ? payload.output : [];
const parts: string[] = [];
for (const item of output) {
if (!isRecord(item)) continue;
if (asTrimmedString(item.type).toLowerCase() !== 'message') continue;
const content = Array.isArray(item.content) ? item.content : [];
for (const part of content) {
if (!isRecord(part)) continue;
const partType = asTrimmedString(part.type).toLowerCase();
const text = typeof part.text === 'string' ? part.text : '';
if ((partType === 'output_text' || partType === 'text') && text) {
parts.push(text);
}
}
}
return parts.join('');
}
function rememberStreamResponseEnvelope(
streamContext: ReturnType<typeof openAiResponsesTransformer.createStreamContext>,
payload: Record<string, unknown>,
): void {
const responsePayload = isRecord(payload.response) ? payload.response : payload;
if (typeof responsePayload.id === 'string' && responsePayload.id.trim().length > 0) {
streamContext.id = responsePayload.id;
}
if (typeof responsePayload.model === 'string' && responsePayload.model.trim().length > 0) {
streamContext.model = responsePayload.model;
}
const createdAt = (
typeof responsePayload.created_at === 'number' && Number.isFinite(responsePayload.created_at)
? responsePayload.created_at
: (typeof responsePayload.created === 'number' && Number.isFinite(responsePayload.created)
? responsePayload.created
: null)
);
if (createdAt !== null) {
streamContext.created = createdAt;
}
}
function ensureResponseId(rawId: string): string {
const trimmed = rawId.trim() || `resp_${Date.now()}`;
return trimmed.startsWith('resp_') ? trimmed : `resp_${trimmed}`;
}
function buildUsagePayload(usage: ReturnType<typeof parseProxyUsage>): Record<string, unknown> {
const payload: Record<string, unknown> = {
input_tokens: usage.promptTokens,
output_tokens: usage.completionTokens,
total_tokens: usage.totalTokens,
};
const inputDetails: Record<string, unknown> = {};
if ((usage.cacheReadTokens || 0) > 0) inputDetails.cached_tokens = usage.cacheReadTokens;
if ((usage.cacheCreationTokens || 0) > 0) inputDetails.cache_creation_tokens = usage.cacheCreationTokens;
if (Object.keys(inputDetails).length > 0) payload.input_tokens_details = inputDetails;
return payload;
}
function cloneAggregateOutputItem(
item: unknown,
terminalStatus: ResponsesTerminalStatus,
): Record<string, unknown> | null {
if (!isRecord(item)) return null;
const next = structuredClone(item);
const currentStatus = asTrimmedString(next.status).toLowerCase();
next.status = currentStatus && currentStatus !== 'in_progress'
? currentStatus
: terminalStatus;
return next;
}
function materializeTerminalPayloadFromAggregate(
aggregateState: ReturnType<typeof openAiResponsesTransformer.aggregator.createState>,
streamContext: ReturnType<typeof openAiResponsesTransformer.createStreamContext>,
usage: ReturnType<typeof parseProxyUsage>,
terminalStatus: ResponsesTerminalStatus,
): Record<string, unknown> {
const output = aggregateState.outputItems
.map((item) => cloneAggregateOutputItem(item, terminalStatus))
.filter((item): item is Record<string, unknown> => !!item);
const usagePayload = buildUsagePayload(usage);
const usageWithExtras = Object.keys(aggregateState.usageExtras).length > 0
? { ...usagePayload, ...structuredClone(aggregateState.usageExtras) }
: usagePayload;
return {
id: ensureResponseId(
asTrimmedString(aggregateState.responseId)
|| asTrimmedString(streamContext.id)
|| asTrimmedString(aggregateState.modelName),
),
object: 'response',
created_at: (
typeof aggregateState.createdAt === 'number' && Number.isFinite(aggregateState.createdAt)
? aggregateState.createdAt
: streamContext.created
) || Math.floor(Date.now() / 1000),
status: terminalStatus,
model: asTrimmedString(streamContext.model) || asTrimmedString(aggregateState.modelName),
output,
output_text: collectResponsesOutputText({ output }),
usage: usageWithExtras,
};
}
function enrichTerminalPayload(
payload: Record<string, unknown>,
aggregateState: ReturnType<typeof openAiResponsesTransformer.aggregator.createState>,
streamContext: ReturnType<typeof openAiResponsesTransformer.createStreamContext>,
usage: ReturnType<typeof parseProxyUsage>,
terminalStatus: ResponsesTerminalStatus,
): Record<string, unknown> {
const next = structuredClone(payload);
const materialized = materializeTerminalPayloadFromAggregate(
aggregateState,
streamContext,
usage,
terminalStatus,
);
if (!hasMeaningfulResponsesOutput(next.output) && materialized && hasMeaningfulResponsesOutput(materialized.output)) {
next.output = materialized.output;
}
const currentOutputText = typeof next.output_text === 'string' ? next.output_text : '';
if (!currentOutputText) {
const derivedOutputText = collectResponsesOutputText(next)
|| (materialized && typeof materialized.output_text === 'string' ? materialized.output_text : '');
if (derivedOutputText) {
next.output_text = derivedOutputText;
}
}
if (materialized && next.usage === undefined && materialized.usage !== undefined) {
next.usage = materialized.usage;
}
return next;
}
function mergeMissingResponsesTerminalFields(
payload: Record<string, unknown>,
fallbackPayload: Record<string, unknown> | null,
): Record<string, unknown> {
if (!fallbackPayload) return payload;
const merged = { ...payload };
if (merged.output === undefined && fallbackPayload.output !== undefined) {
merged.output = fallbackPayload.output;
}
if (
(typeof merged.output_text !== 'string' || merged.output_text.length === 0)
&& typeof fallbackPayload.output_text === 'string'
&& fallbackPayload.output_text.length > 0
) {
merged.output_text = fallbackPayload.output_text;
}
if (merged.object === undefined && fallbackPayload.object !== undefined) {
merged.object = fallbackPayload.object;
}
if (merged.created_at === undefined && fallbackPayload.created_at !== undefined) {
merged.created_at = fallbackPayload.created_at;
}
if (merged.usage === undefined && fallbackPayload.usage !== undefined) {
merged.usage = fallbackPayload.usage;
}
return merged;
}
export function looksLikeResponsesSseText(rawText: string): boolean {
const { events, rest } = openAiResponsesTransformer.pullSseEvents(rawText);
if (events.length === 0 || rest.trim().length > 0) return false;
return events.some((event) => {
if (event.data === '[DONE]') return true;
if (event.event === 'error' || event.event.startsWith('response.')) return true;
const payload = parseResponsesSsePayload(event.data);
const payloadType = typeof payload?.type === 'string' ? payload.type : '';
return payloadType === 'error' || payloadType.startsWith('response.');
});
}
export function createSingleChunkStreamReader(rawText: string): {
read(): Promise<{ done: boolean; value?: Uint8Array }>;
cancel(reason?: unknown): Promise<unknown>;
releaseLock(): void;
} {
const chunk = Buffer.from(rawText, 'utf8');
let done = false;
return {
async read() {
if (done) return { done: true };
done = true;
return { done: false, value: chunk };
},
async cancel() {
done = true;
return undefined;
},
releaseLock() {},
};
}
export function collectResponsesFinalPayloadFromSseText(
rawText: string,
modelName: string,
): { payload: Record<string, unknown>; rawText: string } {
const { events } = openAiResponsesTransformer.pullSseEvents(rawText);
const streamContext = openAiResponsesTransformer.createStreamContext(modelName);
const aggregateState = openAiResponsesTransformer.aggregator.createState(modelName);
let usage = {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
cacheReadTokens: 0,
cacheCreationTokens: 0,
promptTokensIncludeCache: null as boolean | null,
};
let completedPayload: Record<string, unknown> | null = null;
let terminalStatus: ResponsesTerminalStatus = 'completed';
const captureCompletedPayloadFromEvent = (
eventType: string,
payload: Record<string, unknown>,
) => {
if (completedPayload) return;
if (eventType === 'response.failed' || eventType === 'error') {
throw new Error(getResponsesFailureMessage(payload));
}
if (eventType !== 'response.completed' && eventType !== 'response.incomplete') {
return;
}
terminalStatus = eventType === 'response.incomplete' ? 'incomplete' : 'completed';
if (isRecord(payload.response) && hasCompleteFinalResponsesPayload(payload.response)) {
completedPayload = payload.response;
return;
}
if (hasCompleteFinalResponsesPayload(payload)) {
completedPayload = payload;
}
};
const captureCompletedPayloadFromLines = (lines: string[]) => {
if (completedPayload) return;
const parsed = openAiResponsesTransformer.pullSseEvents(lines.join(''));
for (const event of parsed.events) {
if (event.data === '[DONE]') continue;
const payload = parseResponsesSsePayload(event.data);
if (!payload) continue;
const payloadType = typeof payload.type === 'string' ? payload.type : '';
captureCompletedPayloadFromEvent(payloadType || event.event, payload);
if (completedPayload) {
return;
}
}
};
for (const event of events) {
if (event.data === '[DONE]') continue;
const payload = parseResponsesSsePayload(event.data);
if (!payload) continue;
const payloadType = typeof payload.type === 'string' ? payload.type : '';
const eventType = payloadType || event.event;
rememberStreamResponseEnvelope(streamContext, payload);
usage = mergeProxyUsage(usage, parseProxyUsage(payload));
captureCompletedPayloadFromEvent(eventType, payload);
if (completedPayload) {
continue;
}
const normalizedEvent = openAiResponsesTransformer.transformStreamEvent(
payload,
streamContext,
modelName,
);
captureCompletedPayloadFromLines(openAiResponsesTransformer.aggregator.serialize({
state: aggregateState,
streamContext,
event: normalizedEvent,
usage,
}));
}
if (
completedPayload
&& !hasMeaningfulFinalResponsesPayload(completedPayload)
&& hasMeaningfulResponsesOutput(aggregateState.outputItems)
) {
completedPayload = mergeMissingResponsesTerminalFields(
completedPayload,
materializeTerminalPayloadFromAggregate(
aggregateState,
streamContext,
usage,
terminalStatus,
),
);
}
if (completedPayload) {
completedPayload = mergeMissingResponsesTerminalFields(
completedPayload,
materializeTerminalPayloadFromAggregate(
aggregateState,
streamContext,
usage,
terminalStatus,
),
);
}
if (!completedPayload) {
const materialized = materializeTerminalPayloadFromAggregate(
aggregateState,
streamContext,
usage,
terminalStatus,
);
if (materialized) {
completedPayload = materialized;
}
}
if (completedPayload) {
return {
payload: enrichTerminalPayload(
completedPayload,
aggregateState,
streamContext,
usage,
terminalStatus,
),
rawText,
};
}
throw new Error('stream disconnected before terminal responses event');
}
export async function collectResponsesFinalPayloadFromSse(
upstream: { text(): Promise<string>; headers?: { get(name: string): string | null } },
modelName: string,
): Promise<{ payload: Record<string, unknown>; rawText: string }> {
const rawText = typeof upstream.headers?.get === 'function'
? await readRuntimeResponseText(upstream as Parameters<typeof readRuntimeResponseText>[0])
: await upstream.text();
return collectResponsesFinalPayloadFromSseText(rawText, modelName);
}
+23 -20
View File
@@ -5,6 +5,8 @@ import { tokenRouter } from '../../services/tokenRouter.js';
import { reportProxyAllFailed } from '../../services/alertService.js';
import { hasProxyUsagePayload, mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js';
import { type DownstreamFormat } from '../../transformers/shared/normalized.js';
import { promoteRequiredEndpointCandidateAfterProtocolError } from '../../transformers/shared/endpointCompatibility.js';
import { shouldForceResponsesUpstreamStream } from '../capabilities/responsesCompact.js';
import {
buildClaudeCountTokensUpstreamRequest,
buildUpstreamEndpointRequest,
@@ -21,7 +23,7 @@ import {
recordDownstreamCostUsage,
} from '../../routes/proxy/downstreamPolicy.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../orchestration/endpointFlow.js';
import { detectProxyFailure } from '../../routes/proxy/proxyFailureJudge.js';
import { detectProxyFailure } from '../../services/proxyFailureJudge.js';
import { openAiChatTransformer } from '../../transformers/openai/chat/index.js';
import { anthropicMessagesTransformer } from '../../transformers/anthropic/messages/index.js';
import { shouldPreferResponsesForAnthropicContinuation } from '../../transformers/anthropic/messages/compatibility.js';
@@ -39,11 +41,11 @@ import {
collectResponsesFinalPayloadFromSseText,
createSingleChunkStreamReader,
looksLikeResponsesSseText,
} from '../../routes/proxy/responsesSseFinal.js';
} from '../runtime/responsesSseFinal.js';
import {
createGeminiCliStreamReader,
unwrapGeminiCliPayload,
} from '../../routes/proxy/geminiCliCompat.js';
} from '../../transformers/gemini/generate-content/cliBridge.js';
import { summarizeConversationFileInputsInOpenAiBody } from '../capabilities/conversationFileCapabilities.js';
import { getObservedResponseMeta } from '../firstByteTimeout.js';
import { getRuntimeResponseReader, readRuntimeResponseText } from '../executors/types.js';
@@ -90,17 +92,6 @@ function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function prioritizeEndpointCandidate(
candidates: Array<'chat' | 'messages' | 'responses'>,
preferred: 'chat' | 'messages' | 'responses',
): Array<'chat' | 'messages' | 'responses'> {
if (!candidates.includes(preferred)) return candidates;
return [
preferred,
...candidates.filter((candidate) => candidate !== preferred),
];
}
function finalizeRetryAsUpstreamFailure(status: number, message: string) {
return {
action: 'respond' as const,
@@ -292,11 +283,11 @@ export async function handleChatSurfaceRequest(
conversationFileSummary,
wantsContinuationAwareResponses,
},
{
oauthProvider: oauth?.provider,
},
),
];
if (oauth?.provider === 'codex' && downstreamFormat === 'openai') {
endpointCandidates = prioritizeEndpointCandidate(endpointCandidates, 'responses');
}
const endpointRuntimeContext = {
siteId: selected.site.id,
modelName,
@@ -328,11 +319,15 @@ export async function handleChatSurfaceRequest(
})
);
const executeEndpointResultForSiteApiBaseUrl = async (siteApiBaseUrl: string) => {
const forceResponsesUpstreamStream = shouldForceResponsesUpstreamStream({
sitePlatform: selected.site.platform,
isCompactRequest: false,
});
const buildEndpointRequest = (
endpoint: 'chat' | 'messages' | 'responses',
options: { forceNormalizeClaudeBody?: boolean } = {},
) => {
const upstreamStream = isStream || (isCodexSite && endpoint === 'responses');
const upstreamStream = isStream || (forceResponsesUpstreamStream && endpoint === 'responses');
const endpointRequest = buildUpstreamEndpointRequest({
endpoint,
modelName,
@@ -369,7 +364,7 @@ export async function handleChatSurfaceRequest(
modelName,
requestedModelHint: requestedModel,
sitePlatform: selected.site.platform,
isStream: isStream || isCodexSite,
isStream: isStream || forceResponsesUpstreamStream,
buildRequest: ({ endpoint, forceNormalizeClaudeBody }) => buildEndpointRequest(
endpoint,
{ forceNormalizeClaudeBody },
@@ -455,6 +450,10 @@ export async function handleChatSurfaceRequest(
},
shouldDowngrade: endpointStrategy.shouldDowngrade,
onDowngrade: async (ctx) => {
promoteRequiredEndpointCandidateAfterProtocolError(endpointCandidates, {
currentEndpoint: ctx.request.endpoint,
upstreamErrorText: ctx.rawErrText,
});
await safeUpdateSurfaceProxyDebugAttempt(debugTrace, debugAttemptBase + ctx.endpointIndex, {
downgradeDecision: true,
downgradeReason: ctx.errText,
@@ -1183,6 +1182,10 @@ export async function handleClaudeCountTokensSurfaceRequest(
modelName,
'claude',
requestedModel,
undefined,
{
requestKind: 'claude-count-tokens',
},
);
await safeUpdateSurfaceProxyDebugCandidates(debugTrace, {
endpointCandidates,
@@ -1194,7 +1197,7 @@ export async function handleClaudeCountTokensSurfaceRequest(
countTokens: true,
},
});
if (!endpointCandidates.includes('messages')) {
if (endpointCandidates.length === 0) {
if (canRetryChannelSelection(retryCount, forcedChannelId)) {
retryCount += 1;
continue;
@@ -15,7 +15,7 @@ import { resolveChannelProxyUrl, withSiteRecordProxyRequestInit } from '../../se
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
import { getDownstreamRoutingPolicy } from '../../routes/proxy/downstreamPolicy.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../orchestration/endpointFlow.js';
import { composeProxyLogMessage } from '../../routes/proxy/logPathMeta.js';
import { composeProxyLogMessage } from '../../services/proxyLogMessage.js';
import {
buildUpstreamEndpointRequest,
resolveUpstreamEndpointCandidates,
@@ -35,7 +35,7 @@ import {
createGeminiCliStreamReader,
unwrapGeminiCliPayload,
wrapGeminiCliRequest,
} from '../../routes/proxy/geminiCliCompat.js';
} from '../../transformers/gemini/generate-content/cliBridge.js';
import { dispatchRuntimeRequest } from '../../services/runtimeDispatch.js';
import { detectDownstreamClientContext, type DownstreamClientContext } from '../downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
@@ -23,9 +23,10 @@ import {
} from '../../services/upstreamEndpointRuntimeMemory.js';
import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from '../../routes/proxy/downstreamPolicy.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../orchestration/endpointFlow.js';
import { detectProxyFailure } from '../../routes/proxy/proxyFailureJudge.js';
import { detectProxyFailure } from '../../services/proxyFailureJudge.js';
import { getProxyAuthContext, getProxyResourceOwner } from '../../middleware/auth.js';
import { normalizeInputFileBlock } from '../../transformers/shared/inputFile.js';
import { promoteRequiredEndpointCandidateAfterProtocolError } from '../../transformers/shared/endpointCompatibility.js';
import {
ProxyInputFileResolutionError,
resolveResponsesBodyInputFiles,
@@ -39,11 +40,11 @@ import {
collectResponsesFinalPayloadFromSseText,
createSingleChunkStreamReader,
looksLikeResponsesSseText,
} from '../../routes/proxy/responsesSseFinal.js';
} from '../runtime/responsesSseFinal.js';
import {
createGeminiCliStreamReader,
unwrapGeminiCliPayload,
} from '../../routes/proxy/geminiCliCompat.js';
} from '../../transformers/gemini/generate-content/cliBridge.js';
import { isCodexResponsesSurface } from '../cliProfiles/codexProfile.js';
import { getObservedResponseMeta } from '../firstByteTimeout.js';
import { getRuntimeResponseReader, readRuntimeResponseText } from '../executors/types.js';
@@ -59,7 +60,9 @@ import {
summarizeConversationFileInputsInResponsesBody,
} from '../capabilities/conversationFileCapabilities.js';
import {
ensureCompactResponsesJsonAcceptHeader,
sanitizeCompactResponsesRequestBody,
shouldForceResponsesUpstreamStream,
shouldFallbackCompactResponsesToResponses,
} from '../capabilities/responsesCompact.js';
import { detectDownstreamClientContext } from '../downstreamClientContext.js';
@@ -104,7 +107,12 @@ function isRecord(value: unknown): value is Record<string, unknown> {
function getCodexSessionHeaderValue(headers: Record<string, string>): string {
for (const [rawKey, rawValue] of Object.entries(headers)) {
const normalizedKey = rawKey.trim().toLowerCase();
if (normalizedKey === 'session_id' || normalizedKey === 'session-id') {
if (
normalizedKey === 'session_id'
|| normalizedKey === 'session-id'
|| normalizedKey === 'conversation_id'
|| normalizedKey === 'conversation-id'
) {
return String(rawValue || '').trim();
}
}
@@ -417,7 +425,24 @@ export async function handleOpenAiResponsesSurfaceRequest(
const requiresNativeResponsesFileUrl = responsesConversationFileSummary.hasRemoteDocumentUrl
|| carriesResponsesFileUrlInput(normalizedResponsesBody.input);
const endpointCandidates: UpstreamEndpoint[] = isCompactRequest
? ['responses']
? await resolveUpstreamEndpointCandidates(
{
site: selected.site,
account: selected.account,
},
modelName,
'responses',
requestedModel,
{
hasNonImageFileInput,
conversationFileSummary,
wantsNativeResponsesReasoning: prefersNativeResponsesReasoning,
},
{
requestKind: 'responses-compact',
requiresNativeResponsesFileUrl,
},
)
: await resolveUpstreamEndpointCandidates(
{
site: selected.site,
@@ -431,10 +456,10 @@ export async function handleOpenAiResponsesSurfaceRequest(
conversationFileSummary,
wantsNativeResponsesReasoning: prefersNativeResponsesReasoning,
},
{
requiresNativeResponsesFileUrl,
},
);
if (endpointCandidates.length === 0) {
endpointCandidates.push('responses', 'chat', 'messages');
}
const endpointRuntimeContext = {
siteId: selected.site.id,
modelName,
@@ -467,9 +492,12 @@ export async function handleOpenAiResponsesSurfaceRequest(
})
);
const executeEndpointResultForSiteApiBaseUrl = async (siteApiBaseUrl: string) => {
const forceCodexUpstreamStream = isCodexSite && !isCompactRequest;
const forceResponsesUpstreamStream = shouldForceResponsesUpstreamStream({
sitePlatform: selected.site.platform,
isCompactRequest,
});
const buildEndpointRequest = (endpoint: 'chat' | 'messages' | 'responses') => {
const upstreamStream = isStream || (forceCodexUpstreamStream && endpoint === 'responses');
const upstreamStream = isStream || (forceResponsesUpstreamStream && endpoint === 'responses');
const responsesOriginalBody = (
endpoint === 'responses'
&& isCodexSite
@@ -498,6 +526,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
responsesOriginalBody,
downstreamHeaders: request.headers as Record<string, unknown>,
providerHeaders: buildProviderHeaders(),
codexExplicitSessionId: codexSessionId || null,
});
const upstreamPath = (
isCompactRequest && endpoint === 'responses'
@@ -506,13 +535,22 @@ export async function handleOpenAiResponsesSurfaceRequest(
);
const requestBody = (
isCompactRequest && endpoint === 'responses'
? sanitizeCompactResponsesRequestBody(endpointRequest.body as Record<string, unknown>)
? sanitizeCompactResponsesRequestBody(endpointRequest.body as Record<string, unknown>, {
sitePlatform: selected.site.platform,
})
: endpointRequest.body as Record<string, unknown>
);
const requestHeaders = (
isCompactRequest && endpoint === 'responses'
? ensureCompactResponsesJsonAcceptHeader(endpointRequest.headers, {
sitePlatform: selected.site.platform,
})
: endpointRequest.headers
);
return {
endpoint,
path: upstreamPath,
headers: endpointRequest.headers,
headers: requestHeaders,
body: requestBody,
runtime: endpointRequest.runtime,
};
@@ -536,8 +574,9 @@ export async function handleOpenAiResponsesSurfaceRequest(
);
};
const endpointStrategy = openAiResponsesTransformer.compatibility.createEndpointStrategy({
isStream: isStream || forceCodexUpstreamStream,
isStream: isStream || forceResponsesUpstreamStream,
requiresNativeResponsesFileUrl,
sitePlatform: selected.site.platform,
dispatchRequest,
});
const tryRecover = async (ctx: Parameters<NonNullable<typeof endpointStrategy.tryRecover>>[0]) => {
@@ -587,19 +626,44 @@ export async function handleOpenAiResponsesSurfaceRequest(
ctx.rawErrText = await readRuntimeResponseText(recoveredResponse).catch(() => 'unknown error');
}
}
const compactFallbackEnabled = config.responsesCompactFallbackToResponsesEnabled || !isCodexSite;
if (
isCompactRequest
&& config.responsesCompactFallbackToResponsesEnabled
&& compactFallbackEnabled
&& ctx.request.endpoint === 'responses'
&& ctx.request.path.endsWith('/responses/compact')
&& shouldFallbackCompactResponsesToResponses({
status: ctx.response.status,
rawErrText: ctx.rawErrText,
requestPath: ctx.request.path,
})
) {
const normalizedSitePlatform = String(selected.site.platform || '').trim().toLowerCase();
const recoveredUpstreamStream = shouldForceResponsesUpstreamStream({
sitePlatform: selected.site.platform,
isCompactRequest: false,
});
const recoveredHeaders = { ...ctx.request.headers } as Record<string, string>;
delete (recoveredHeaders as Record<string, unknown>).Accept;
if (recoveredUpstreamStream) {
recoveredHeaders.accept = 'text/event-stream';
}
const recoveredBody = isRecord(ctx.request.body)
? { ...ctx.request.body }
: ctx.request.body;
if (isRecord(recoveredBody)) {
if (recoveredUpstreamStream) {
recoveredBody.stream = true;
}
if (normalizedSitePlatform === 'codex' || normalizedSitePlatform === 'sub2api') {
recoveredBody.store = false;
}
}
const recoveredRequest = {
...ctx.request,
path: ctx.request.path.replace(/\/compact$/, ''),
headers: recoveredHeaders,
body: recoveredBody,
};
const recoveredResponse = await dispatchRequest(recoveredRequest);
if (recoveredResponse.ok) {
@@ -684,6 +748,10 @@ export async function handleOpenAiResponsesSurfaceRequest(
},
shouldDowngrade: endpointStrategy.shouldDowngrade,
onDowngrade: async (ctx) => {
promoteRequiredEndpointCandidateAfterProtocolError(endpointCandidates, {
currentEndpoint: ctx.request.endpoint,
upstreamErrorText: ctx.rawErrText,
});
await safeUpdateSurfaceProxyDebugAttempt(debugTrace, debugAttemptBase + ctx.endpointIndex, {
downgradeDecision: true,
downgradeReason: ctx.errText,
@@ -50,11 +50,11 @@ vi.mock('../../services/proxyChannelCoordinator.js', () => ({
},
}));
vi.mock('../../services/modelService.js', () => ({
vi.mock('../../services/routeRefreshWorkflow.js', () => ({
refreshModelsAndRebuildRoutes: (...args: unknown[]) => refreshModelsAndRebuildRoutesMock(...args),
}));
vi.mock('../../routes/proxy/logPathMeta.js', () => ({
vi.mock('../../services/proxyLogMessage.js', () => ({
composeProxyLogMessage: (...args: unknown[]) => composeProxyLogMessageMock(...args),
}));
@@ -71,7 +71,7 @@ vi.mock('../../services/siteProxy.js', () => ({
withSiteRecordProxyRequestInit: (...args: unknown[]) => withSiteRecordProxyRequestInitMock(...args),
}));
vi.mock('../../routes/proxy/runtimeExecutor.js', () => ({
vi.mock('../../services/runtimeDispatch.js', () => ({
dispatchRuntimeRequest: (...args: unknown[]) => dispatchRuntimeRequestMock(...args),
}));
@@ -98,7 +98,7 @@ vi.mock('../../services/proxyUsageFallbackService.js', () => ({
resolveProxyUsageWithSelfLogFallback: (...args: unknown[]) => resolveProxyUsageWithSelfLogFallbackMock(...args),
}));
vi.mock('../../routes/proxy/proxyBilling.js', () => ({
vi.mock('../../services/proxyBilling.js', () => ({
resolveProxyLogBilling: (...args: unknown[]) => resolveProxyLogBillingMock(...args),
}));
@@ -7,8 +7,8 @@ import type { DownstreamRoutingPolicy } from '../../services/downstreamPolicyTyp
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
import { isTokenExpiredError } from '../../services/alertRules.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { composeProxyLogMessage } from '../../routes/proxy/logPathMeta.js';
import { resolveProxyLogBilling } from '../../routes/proxy/proxyBilling.js';
import { composeProxyLogMessage } from '../../services/proxyLogMessage.js';
import { resolveProxyLogBilling } from '../../services/proxyBilling.js';
import type { DownstreamClientContext } from '../downstreamClientContext.js';
import { insertProxyLog } from '../../services/proxyLogStore.js';
import { dispatchRuntimeRequest } from '../../services/runtimeDispatch.js';
@@ -37,11 +37,11 @@ vi.mock('../../services/proxyRetryPolicy.js', () => ({
shouldRetryProxyRequest: vi.fn(() => false),
}));
vi.mock('../../routes/proxy/logPathMeta.js', () => ({
vi.mock('../../services/proxyLogMessage.js', () => ({
composeProxyLogMessage: (...args: unknown[]) => composeProxyLogMessageMock(...args),
}));
vi.mock('../../routes/proxy/proxyBilling.js', () => ({
vi.mock('../../services/proxyBilling.js', () => ({
resolveProxyLogBilling: vi.fn(),
}));