forked from boos/BoosAPI
Fix responses compact fallback handling (#426)
* fix route drag overlay alignment * fix(web): address route drag review comments * fix route card review follow-up * Fix responses compact fallback handling * Fix compact endpoint candidate typing * Tighten compact fallback matcher
This commit is contained in:
@@ -131,6 +131,7 @@ export function buildConfig(env: NodeJS.ProcessEnv) {
|
|||||||
proxySessionChannelLeaseTtlMs: Math.max(5_000, Math.trunc(parseNumber(env.PROXY_SESSION_CHANNEL_LEASE_TTL_MS, 90_000))),
|
proxySessionChannelLeaseTtlMs: Math.max(5_000, Math.trunc(parseNumber(env.PROXY_SESSION_CHANNEL_LEASE_TTL_MS, 90_000))),
|
||||||
proxySessionChannelLeaseKeepaliveMs: Math.max(1_000, Math.trunc(parseNumber(env.PROXY_SESSION_CHANNEL_LEASE_KEEPALIVE_MS, 15_000))),
|
proxySessionChannelLeaseKeepaliveMs: Math.max(1_000, Math.trunc(parseNumber(env.PROXY_SESSION_CHANNEL_LEASE_KEEPALIVE_MS, 15_000))),
|
||||||
codexUpstreamWebsocketEnabled: parseBoolean(env.CODEX_UPSTREAM_WEBSOCKET_ENABLED, false),
|
codexUpstreamWebsocketEnabled: parseBoolean(env.CODEX_UPSTREAM_WEBSOCKET_ENABLED, false),
|
||||||
|
responsesCompactFallbackToResponsesEnabled: parseBoolean(env.RESPONSES_COMPACT_FALLBACK_TO_RESPONSES_ENABLED, false),
|
||||||
disableCrossProtocolFallback: parseBoolean(env.DISABLE_CROSS_PROTOCOL_FALLBACK, false),
|
disableCrossProtocolFallback: parseBoolean(env.DISABLE_CROSS_PROTOCOL_FALLBACK, false),
|
||||||
proxyDebugTraceEnabled: parseBoolean(env.PROXY_DEBUG_TRACE_ENABLED, false),
|
proxyDebugTraceEnabled: parseBoolean(env.PROXY_DEBUG_TRACE_ENABLED, false),
|
||||||
proxyDebugCaptureHeaders: parseBoolean(env.PROXY_DEBUG_CAPTURE_HEADERS, true),
|
proxyDebugCaptureHeaders: parseBoolean(env.PROXY_DEBUG_CAPTURE_HEADERS, true),
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
function asTrimmedString(value: unknown): string {
|
||||||
|
return typeof value === 'string' ? value.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeCompactResponsesRequestBody(
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const next = { ...body };
|
||||||
|
delete next.stream;
|
||||||
|
delete next.stream_options;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldFallbackCompactResponsesToResponses(input: {
|
||||||
|
status?: number;
|
||||||
|
rawErrText?: string;
|
||||||
|
}): boolean {
|
||||||
|
const status = Number.isFinite(Number(input.status)) ? Number(input.status) : 0;
|
||||||
|
const compact = asTrimmedString(input.rawErrText).toLowerCase();
|
||||||
|
const hasCompactHint = (
|
||||||
|
compact.includes('/responses/compact')
|
||||||
|
|| compact.includes('responses/compact')
|
||||||
|
|| compact.includes('compact endpoint')
|
||||||
|
|| /(^|[^a-z])compact([^a-z]|$)/.test(compact)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status === 404 || status === 405 || status === 501) return true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
compact.includes("unknown parameter: 'stream'")
|
||||||
|
|| compact.includes('invalid url')
|
||||||
|
|| (
|
||||||
|
hasCompactHint
|
||||||
|
&& (
|
||||||
|
compact.includes('not supported')
|
||||||
|
|| compact.includes('unsupported')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
buildUpstreamEndpointRequest,
|
buildUpstreamEndpointRequest,
|
||||||
resolveUpstreamEndpointCandidates,
|
resolveUpstreamEndpointCandidates,
|
||||||
|
type UpstreamEndpoint,
|
||||||
} from '../../services/upstreamEndpointRuntime.js';
|
} from '../../services/upstreamEndpointRuntime.js';
|
||||||
import {
|
import {
|
||||||
getUpstreamEndpointRuntimeStateSnapshot,
|
getUpstreamEndpointRuntimeStateSnapshot,
|
||||||
@@ -57,6 +58,10 @@ import {
|
|||||||
summarizeConversationFileInputsInOpenAiBody,
|
summarizeConversationFileInputsInOpenAiBody,
|
||||||
summarizeConversationFileInputsInResponsesBody,
|
summarizeConversationFileInputsInResponsesBody,
|
||||||
} from '../capabilities/conversationFileCapabilities.js';
|
} from '../capabilities/conversationFileCapabilities.js';
|
||||||
|
import {
|
||||||
|
sanitizeCompactResponsesRequestBody,
|
||||||
|
shouldFallbackCompactResponsesToResponses,
|
||||||
|
} from '../capabilities/responsesCompact.js';
|
||||||
import { detectDownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
|
import { detectDownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
|
||||||
import { getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
|
import { getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
|
||||||
import { shouldAbortSameSiteEndpointFallback } from '../../services/proxyRetryPolicy.js';
|
import { shouldAbortSameSiteEndpointFallback } from '../../services/proxyRetryPolicy.js';
|
||||||
@@ -411,20 +416,22 @@ export async function handleOpenAiResponsesSurfaceRequest(
|
|||||||
const responsesConversationFileSummary = summarizeConversationFileInputsInResponsesBody(normalizedResponsesBody);
|
const responsesConversationFileSummary = summarizeConversationFileInputsInResponsesBody(normalizedResponsesBody);
|
||||||
const requiresNativeResponsesFileUrl = responsesConversationFileSummary.hasRemoteDocumentUrl
|
const requiresNativeResponsesFileUrl = responsesConversationFileSummary.hasRemoteDocumentUrl
|
||||||
|| carriesResponsesFileUrlInput(normalizedResponsesBody.input);
|
|| carriesResponsesFileUrlInput(normalizedResponsesBody.input);
|
||||||
const endpointCandidates = await resolveUpstreamEndpointCandidates(
|
const endpointCandidates: UpstreamEndpoint[] = isCompactRequest
|
||||||
{
|
? ['responses']
|
||||||
site: selected.site,
|
: await resolveUpstreamEndpointCandidates(
|
||||||
account: selected.account,
|
{
|
||||||
},
|
site: selected.site,
|
||||||
modelName,
|
account: selected.account,
|
||||||
'responses',
|
},
|
||||||
requestedModel,
|
modelName,
|
||||||
{
|
'responses',
|
||||||
hasNonImageFileInput,
|
requestedModel,
|
||||||
conversationFileSummary,
|
{
|
||||||
wantsNativeResponsesReasoning: prefersNativeResponsesReasoning,
|
hasNonImageFileInput,
|
||||||
},
|
conversationFileSummary,
|
||||||
);
|
wantsNativeResponsesReasoning: prefersNativeResponsesReasoning,
|
||||||
|
},
|
||||||
|
);
|
||||||
if (endpointCandidates.length === 0) {
|
if (endpointCandidates.length === 0) {
|
||||||
endpointCandidates.push('responses', 'chat', 'messages');
|
endpointCandidates.push('responses', 'chat', 'messages');
|
||||||
}
|
}
|
||||||
@@ -460,8 +467,9 @@ export async function handleOpenAiResponsesSurfaceRequest(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
const executeEndpointResultForSiteApiBaseUrl = async (siteApiBaseUrl: string) => {
|
const executeEndpointResultForSiteApiBaseUrl = async (siteApiBaseUrl: string) => {
|
||||||
|
const forceCodexUpstreamStream = isCodexSite && !isCompactRequest;
|
||||||
const buildEndpointRequest = (endpoint: 'chat' | 'messages' | 'responses') => {
|
const buildEndpointRequest = (endpoint: 'chat' | 'messages' | 'responses') => {
|
||||||
const upstreamStream = isStream || (isCodexSite && endpoint === 'responses');
|
const upstreamStream = isStream || (forceCodexUpstreamStream && endpoint === 'responses');
|
||||||
const responsesOriginalBody = (
|
const responsesOriginalBody = (
|
||||||
endpoint === 'responses'
|
endpoint === 'responses'
|
||||||
&& isCodexSite
|
&& isCodexSite
|
||||||
@@ -496,11 +504,16 @@ export async function handleOpenAiResponsesSurfaceRequest(
|
|||||||
? `${endpointRequest.path}/compact`
|
? `${endpointRequest.path}/compact`
|
||||||
: endpointRequest.path
|
: endpointRequest.path
|
||||||
);
|
);
|
||||||
|
const requestBody = (
|
||||||
|
isCompactRequest && endpoint === 'responses'
|
||||||
|
? sanitizeCompactResponsesRequestBody(endpointRequest.body as Record<string, unknown>)
|
||||||
|
: endpointRequest.body as Record<string, unknown>
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
endpoint,
|
endpoint,
|
||||||
path: upstreamPath,
|
path: upstreamPath,
|
||||||
headers: endpointRequest.headers,
|
headers: endpointRequest.headers,
|
||||||
body: endpointRequest.body as Record<string, unknown>,
|
body: requestBody,
|
||||||
runtime: endpointRequest.runtime,
|
runtime: endpointRequest.runtime,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -523,7 +536,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
const endpointStrategy = openAiResponsesTransformer.compatibility.createEndpointStrategy({
|
const endpointStrategy = openAiResponsesTransformer.compatibility.createEndpointStrategy({
|
||||||
isStream: isStream || isCodexSite,
|
isStream: isStream || forceCodexUpstreamStream,
|
||||||
requiresNativeResponsesFileUrl,
|
requiresNativeResponsesFileUrl,
|
||||||
dispatchRequest,
|
dispatchRequest,
|
||||||
});
|
});
|
||||||
@@ -574,13 +587,39 @@ export async function handleOpenAiResponsesSurfaceRequest(
|
|||||||
ctx.rawErrText = await readRuntimeResponseText(recoveredResponse).catch(() => 'unknown error');
|
ctx.rawErrText = await readRuntimeResponseText(recoveredResponse).catch(() => 'unknown error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
isCompactRequest
|
||||||
|
&& config.responsesCompactFallbackToResponsesEnabled
|
||||||
|
&& ctx.request.endpoint === 'responses'
|
||||||
|
&& ctx.request.path.endsWith('/responses/compact')
|
||||||
|
&& shouldFallbackCompactResponsesToResponses({
|
||||||
|
status: ctx.response.status,
|
||||||
|
rawErrText: ctx.rawErrText,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
const recoveredRequest = {
|
||||||
|
...ctx.request,
|
||||||
|
path: ctx.request.path.replace(/\/compact$/, ''),
|
||||||
|
};
|
||||||
|
const recoveredResponse = await dispatchRequest(recoveredRequest);
|
||||||
|
if (recoveredResponse.ok) {
|
||||||
|
return {
|
||||||
|
upstream: recoveredResponse,
|
||||||
|
upstreamPath: recoveredRequest.path,
|
||||||
|
request: recoveredRequest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ctx.request = recoveredRequest;
|
||||||
|
ctx.response = recoveredResponse;
|
||||||
|
ctx.rawErrText = await readRuntimeResponseText(recoveredResponse).catch(() => 'unknown error');
|
||||||
|
}
|
||||||
return endpointStrategy.tryRecover(ctx);
|
return endpointStrategy.tryRecover(ctx);
|
||||||
};
|
};
|
||||||
|
|
||||||
const debugAttemptBase = reserveSurfaceProxyDebugAttemptBase(debugTrace, endpointCandidates.length);
|
const debugAttemptBase = reserveSurfaceProxyDebugAttemptBase(debugTrace, endpointCandidates.length);
|
||||||
return executeEndpointFlow({
|
return executeEndpointFlow({
|
||||||
siteUrl: siteApiBaseUrl,
|
siteUrl: siteApiBaseUrl,
|
||||||
disableCrossProtocolFallback: config.disableCrossProtocolFallback,
|
disableCrossProtocolFallback: isCompactRequest || config.disableCrossProtocolFallback,
|
||||||
firstByteTimeoutMs: Math.max(0, Math.trunc((config.proxyFirstByteTimeoutSec || 0) * 1000)),
|
firstByteTimeoutMs: Math.max(0, Math.trunc((config.proxyFirstByteTimeoutSec || 0) * 1000)),
|
||||||
endpointCandidates,
|
endpointCandidates,
|
||||||
buildRequest: (endpoint) => buildEndpointRequest(endpoint),
|
buildRequest: (endpoint) => buildEndpointRequest(endpoint),
|
||||||
@@ -591,12 +630,14 @@ export async function handleOpenAiResponsesSurfaceRequest(
|
|||||||
ctx.rawErrText || ctx.errText,
|
ctx.rawErrText || ctx.errText,
|
||||||
),
|
),
|
||||||
onAttemptFailure: async (ctx) => {
|
onAttemptFailure: async (ctx) => {
|
||||||
const memoryWrite = recordUpstreamEndpointFailure({
|
const memoryWrite = isCompactRequest
|
||||||
...endpointRuntimeContext,
|
? null
|
||||||
endpoint: ctx.request.endpoint,
|
: recordUpstreamEndpointFailure({
|
||||||
status: ctx.response.status,
|
...endpointRuntimeContext,
|
||||||
errorText: ctx.rawErrText,
|
endpoint: ctx.request.endpoint,
|
||||||
});
|
status: ctx.response.status,
|
||||||
|
errorText: ctx.rawErrText,
|
||||||
|
});
|
||||||
await safeInsertSurfaceProxyDebugAttempt(debugTrace, {
|
await safeInsertSurfaceProxyDebugAttempt(debugTrace, {
|
||||||
attemptIndex: debugAttemptBase + ctx.endpointIndex,
|
attemptIndex: debugAttemptBase + ctx.endpointIndex,
|
||||||
endpoint: ctx.request.endpoint,
|
endpoint: ctx.request.endpoint,
|
||||||
@@ -616,10 +657,12 @@ export async function handleOpenAiResponsesSurfaceRequest(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onAttemptSuccess: async (ctx) => {
|
onAttemptSuccess: async (ctx) => {
|
||||||
const memoryWrite = recordUpstreamEndpointSuccess({
|
const memoryWrite = isCompactRequest
|
||||||
...endpointRuntimeContext,
|
? null
|
||||||
endpoint: ctx.request.endpoint,
|
: recordUpstreamEndpointSuccess({
|
||||||
});
|
...endpointRuntimeContext,
|
||||||
|
endpoint: ctx.request.endpoint,
|
||||||
|
});
|
||||||
const responseBody = await captureSurfaceProxyDebugSuccessResponseBody(debugTrace, ctx);
|
const responseBody = await captureSurfaceProxyDebugSuccessResponseBody(debugTrace, ctx);
|
||||||
await safeInsertSurfaceProxyDebugAttempt(debugTrace, {
|
await safeInsertSurfaceProxyDebugAttempt(debugTrace, {
|
||||||
attemptIndex: debugAttemptBase + ctx.endpointIndex,
|
attemptIndex: debugAttemptBase + ctx.endpointIndex,
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ interface RuntimeSettingsBody {
|
|||||||
systemProxyUrl?: string;
|
systemProxyUrl?: string;
|
||||||
modelAvailabilityProbeEnabled?: boolean;
|
modelAvailabilityProbeEnabled?: boolean;
|
||||||
codexUpstreamWebsocketEnabled?: boolean;
|
codexUpstreamWebsocketEnabled?: boolean;
|
||||||
|
responsesCompactFallbackToResponsesEnabled?: boolean;
|
||||||
disableCrossProtocolFallback?: boolean;
|
disableCrossProtocolFallback?: boolean;
|
||||||
proxySessionChannelConcurrencyLimit?: number;
|
proxySessionChannelConcurrencyLimit?: number;
|
||||||
proxySessionChannelQueueWaitMs?: number;
|
proxySessionChannelQueueWaitMs?: number;
|
||||||
@@ -425,6 +426,11 @@ function applyImportedSettingToRuntime(key: string, value: unknown) {
|
|||||||
config.codexUpstreamWebsocketEnabled = value;
|
config.codexUpstreamWebsocketEnabled = value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case 'responses_compact_fallback_to_responses_enabled': {
|
||||||
|
if (typeof value !== 'boolean') return;
|
||||||
|
config.responsesCompactFallbackToResponsesEnabled = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
case 'disable_cross_protocol_fallback': {
|
case 'disable_cross_protocol_fallback': {
|
||||||
if (typeof value !== 'boolean') return;
|
if (typeof value !== 'boolean') return;
|
||||||
config.disableCrossProtocolFallback = value;
|
config.disableCrossProtocolFallback = value;
|
||||||
@@ -711,6 +717,7 @@ function getRuntimeSettingsResponse(currentAdminIp = '') {
|
|||||||
logCleanupRetentionDays: config.logCleanupRetentionDays,
|
logCleanupRetentionDays: config.logCleanupRetentionDays,
|
||||||
modelAvailabilityProbeEnabled: config.modelAvailabilityProbeEnabled,
|
modelAvailabilityProbeEnabled: config.modelAvailabilityProbeEnabled,
|
||||||
codexUpstreamWebsocketEnabled: config.codexUpstreamWebsocketEnabled,
|
codexUpstreamWebsocketEnabled: config.codexUpstreamWebsocketEnabled,
|
||||||
|
responsesCompactFallbackToResponsesEnabled: config.responsesCompactFallbackToResponsesEnabled,
|
||||||
disableCrossProtocolFallback: config.disableCrossProtocolFallback,
|
disableCrossProtocolFallback: config.disableCrossProtocolFallback,
|
||||||
proxySessionChannelConcurrencyLimit: config.proxySessionChannelConcurrencyLimit,
|
proxySessionChannelConcurrencyLimit: config.proxySessionChannelConcurrencyLimit,
|
||||||
proxySessionChannelQueueWaitMs: config.proxySessionChannelQueueWaitMs,
|
proxySessionChannelQueueWaitMs: config.proxySessionChannelQueueWaitMs,
|
||||||
@@ -1166,6 +1173,24 @@ export async function settingsRoutes(app: FastifyInstance) {
|
|||||||
upsertSetting('codex_upstream_websocket_enabled', config.codexUpstreamWebsocketEnabled);
|
upsertSetting('codex_upstream_websocket_enabled', config.codexUpstreamWebsocketEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (body.responsesCompactFallbackToResponsesEnabled !== undefined) {
|
||||||
|
let nextValue = false;
|
||||||
|
try {
|
||||||
|
nextValue = parseBooleanFlag(body.responsesCompactFallbackToResponsesEnabled, 'Compact 回退到 Responses 开关');
|
||||||
|
} catch (err: any) {
|
||||||
|
return reply.code(400).send({
|
||||||
|
success: false,
|
||||||
|
message: err?.message || 'Compact 回退到 Responses 开关格式无效',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextValue !== config.responsesCompactFallbackToResponsesEnabled) {
|
||||||
|
changedLabels.push('Compact 不支持时回退到普通 Responses');
|
||||||
|
}
|
||||||
|
config.responsesCompactFallbackToResponsesEnabled = nextValue;
|
||||||
|
upsertSetting('responses_compact_fallback_to_responses_enabled', config.responsesCompactFallbackToResponsesEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
if (body.disableCrossProtocolFallback !== undefined) {
|
if (body.disableCrossProtocolFallback !== undefined) {
|
||||||
let nextValue = false;
|
let nextValue = false;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import Fastify, { type FastifyInstance } from 'fastify';
|
import Fastify, { type FastifyInstance } from 'fastify';
|
||||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { config } from '../../config.js';
|
||||||
|
import { resetUpstreamEndpointRuntimeState } from '../../services/upstreamEndpointRuntimeMemory.js';
|
||||||
|
|
||||||
const fetchMock = vi.fn();
|
const fetchMock = vi.fn();
|
||||||
const selectChannelMock = vi.fn();
|
const selectChannelMock = vi.fn();
|
||||||
@@ -105,6 +107,7 @@ vi.mock('../../db/index.js', () => ({
|
|||||||
|
|
||||||
describe('responses proxy compact upstream routing', () => {
|
describe('responses proxy compact upstream routing', () => {
|
||||||
let app: FastifyInstance;
|
let app: FastifyInstance;
|
||||||
|
const originalResponsesCompactFallbackToResponsesEnabled = config.responsesCompactFallbackToResponsesEnabled;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const { responsesProxyRoute } = await import('./responses.js');
|
const { responsesProxyRoute } = await import('./responses.js');
|
||||||
@@ -113,6 +116,8 @@ describe('responses proxy compact upstream routing', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
resetUpstreamEndpointRuntimeState();
|
||||||
|
config.responsesCompactFallbackToResponsesEnabled = false;
|
||||||
fetchMock.mockReset();
|
fetchMock.mockReset();
|
||||||
selectChannelMock.mockReset();
|
selectChannelMock.mockReset();
|
||||||
selectNextChannelMock.mockReset();
|
selectNextChannelMock.mockReset();
|
||||||
@@ -129,7 +134,7 @@ describe('responses proxy compact upstream routing', () => {
|
|||||||
|
|
||||||
selectChannelMock.mockReturnValue({
|
selectChannelMock.mockReturnValue({
|
||||||
channel: { id: 11, routeId: 22 },
|
channel: { id: 11, routeId: 22 },
|
||||||
site: { id: 44, name: 'demo-site', url: 'https://upstream.example.com', platform: 'openai' },
|
site: { id: 44, name: 'demo-site', url: 'https://api.openai.com', platform: 'openai' },
|
||||||
account: { id: 33, username: 'demo-user' },
|
account: { id: 33, username: 'demo-user' },
|
||||||
tokenName: 'default',
|
tokenName: 'default',
|
||||||
tokenValue: 'sk-demo',
|
tokenValue: 'sk-demo',
|
||||||
@@ -140,6 +145,7 @@ describe('responses proxy compact upstream routing', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
config.responsesCompactFallbackToResponsesEnabled = originalResponsesCompactFallbackToResponsesEnabled;
|
||||||
if (app) {
|
if (app) {
|
||||||
await app.close();
|
await app.close();
|
||||||
}
|
}
|
||||||
@@ -171,6 +177,193 @@ describe('responses proxy compact upstream routing', () => {
|
|||||||
expect(targetUrl).toContain('/v1/responses/compact');
|
expect(targetUrl).toContain('/v1/responses/compact');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps native compact as the default even for generic openai-compatible upstreams', async () => {
|
||||||
|
selectChannelMock.mockReturnValue({
|
||||||
|
channel: { id: 11, routeId: 22 },
|
||||||
|
site: { id: 44, name: 'generic-openai-site', url: 'https://upstream.example.com', platform: 'openai' },
|
||||||
|
account: { id: 33, username: 'demo-user' },
|
||||||
|
tokenName: 'default',
|
||||||
|
tokenValue: 'sk-demo',
|
||||||
|
actualModel: 'upstream-gpt',
|
||||||
|
});
|
||||||
|
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
||||||
|
error: {
|
||||||
|
message: 'Invalid URL (POST /v1/responses/compact)',
|
||||||
|
type: 'invalid_request_error',
|
||||||
|
},
|
||||||
|
}), {
|
||||||
|
status: 404,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/v1/responses/compact',
|
||||||
|
payload: {
|
||||||
|
model: 'gpt-5.2',
|
||||||
|
input: 'hello',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(404);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
const [targetUrl] = fetchMock.mock.calls[0] as [string, any];
|
||||||
|
expect(targetUrl).toContain('/v1/responses/compact');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('optionally falls back from compact to ordinary responses when compact is explicitly unsupported', async () => {
|
||||||
|
config.responsesCompactFallbackToResponsesEnabled = true;
|
||||||
|
selectChannelMock.mockReturnValue({
|
||||||
|
channel: { id: 11, routeId: 22 },
|
||||||
|
site: { id: 44, name: 'generic-openai-site', url: 'https://upstream.example.com', platform: 'openai' },
|
||||||
|
account: { id: 33, username: 'demo-user' },
|
||||||
|
tokenName: 'default',
|
||||||
|
tokenValue: 'sk-demo',
|
||||||
|
actualModel: 'upstream-gpt',
|
||||||
|
});
|
||||||
|
fetchMock
|
||||||
|
.mockResolvedValueOnce(new Response(JSON.stringify({
|
||||||
|
error: {
|
||||||
|
message: 'Invalid URL (POST /v1/responses/compact)',
|
||||||
|
type: 'invalid_request_error',
|
||||||
|
},
|
||||||
|
}), {
|
||||||
|
status: 404,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
}))
|
||||||
|
.mockResolvedValueOnce(new Response(JSON.stringify({
|
||||||
|
id: 'resp_123',
|
||||||
|
object: 'response',
|
||||||
|
output_text: 'hello from fallback',
|
||||||
|
usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 },
|
||||||
|
}), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/v1/responses/compact',
|
||||||
|
payload: {
|
||||||
|
model: 'gpt-5.2',
|
||||||
|
input: 'hello',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(String(fetchMock.mock.calls[0]?.[0] || '')).toContain('/v1/responses/compact');
|
||||||
|
expect(String(fetchMock.mock.calls[1]?.[0] || '')).toContain('/v1/responses');
|
||||||
|
expect(String(fetchMock.mock.calls[1]?.[0] || '')).not.toContain('/compact');
|
||||||
|
expect(response.json()).toMatchObject({
|
||||||
|
object: 'response.compaction',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not fall back for unrelated unsupported errors that do not mention compact', async () => {
|
||||||
|
config.responsesCompactFallbackToResponsesEnabled = true;
|
||||||
|
selectChannelMock.mockReturnValue({
|
||||||
|
channel: { id: 11, routeId: 22 },
|
||||||
|
site: { id: 44, name: 'generic-openai-site', url: 'https://upstream.example.com', platform: 'openai' },
|
||||||
|
account: { id: 33, username: 'demo-user' },
|
||||||
|
tokenName: 'default',
|
||||||
|
tokenValue: 'sk-demo',
|
||||||
|
actualModel: 'upstream-gpt',
|
||||||
|
});
|
||||||
|
fetchMock
|
||||||
|
.mockResolvedValueOnce(new Response(JSON.stringify({
|
||||||
|
error: {
|
||||||
|
message: 'Model not supported for this account',
|
||||||
|
type: 'invalid_request_error',
|
||||||
|
},
|
||||||
|
}), {
|
||||||
|
status: 422,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
}))
|
||||||
|
.mockResolvedValueOnce(new Response(JSON.stringify({
|
||||||
|
id: 'resp_ignored',
|
||||||
|
object: 'response',
|
||||||
|
output_text: 'should not fallback',
|
||||||
|
usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 },
|
||||||
|
}), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/v1/responses/compact',
|
||||||
|
payload: {
|
||||||
|
model: 'gpt-5.2',
|
||||||
|
input: 'hello',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(422);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(String(fetchMock.mock.calls[0]?.[0] || '')).toContain('/v1/responses/compact');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips stream fields when forwarding compact requests to codex upstreams', async () => {
|
||||||
|
selectChannelMock.mockReturnValue({
|
||||||
|
channel: { id: 11, routeId: 22 },
|
||||||
|
site: { id: 44, name: 'codex-site', url: 'https://chatgpt.com/backend-api/codex', platform: 'codex' },
|
||||||
|
account: {
|
||||||
|
id: 33,
|
||||||
|
username: 'codex-user@example.com',
|
||||||
|
extraConfig: JSON.stringify({
|
||||||
|
credentialMode: 'session',
|
||||||
|
oauth: {
|
||||||
|
provider: 'codex',
|
||||||
|
accountId: 'chatgpt-account-123',
|
||||||
|
email: 'codex-user@example.com',
|
||||||
|
planType: 'plus',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
tokenName: 'default',
|
||||||
|
tokenValue: 'sk-demo',
|
||||||
|
actualModel: 'gpt-5.4',
|
||||||
|
});
|
||||||
|
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
||||||
|
id: 'cmp_123',
|
||||||
|
object: 'response.compaction',
|
||||||
|
input_tokens: 4,
|
||||||
|
output_tokens: 2,
|
||||||
|
total_tokens: 6,
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
id: 'rs_123',
|
||||||
|
type: 'compaction',
|
||||||
|
encrypted_content: 'enc-compact-payload',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/v1/responses/compact',
|
||||||
|
payload: {
|
||||||
|
model: 'gpt-5.4',
|
||||||
|
input: 'hello',
|
||||||
|
stream_options: { include_obfuscation: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
const [targetUrl, options] = fetchMock.mock.calls[0] as [string, any];
|
||||||
|
expect(targetUrl).toContain('/responses/compact');
|
||||||
|
const forwardedBody = JSON.parse(String(options.body));
|
||||||
|
expect(forwardedBody.stream).toBeUndefined();
|
||||||
|
expect(forwardedBody.stream_options).toBeUndefined();
|
||||||
|
expect(forwardedBody.instructions).toBe('');
|
||||||
|
expect(forwardedBody.store).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('preserves native response.compaction payloads instead of coercing them into object=response', async () => {
|
it('preserves native response.compaction payloads instead of coercing them into object=response', async () => {
|
||||||
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
||||||
id: 'cmp_123',
|
id: 'cmp_123',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ afterEach(() => {
|
|||||||
describe('applyRuntimeSettings', () => {
|
describe('applyRuntimeSettings', () => {
|
||||||
it('hydrates persisted runtime settings that should survive restarts', () => {
|
it('hydrates persisted runtime settings that should survive restarts', () => {
|
||||||
config.disableCrossProtocolFallback = false;
|
config.disableCrossProtocolFallback = false;
|
||||||
|
config.responsesCompactFallbackToResponsesEnabled = false;
|
||||||
config.webhookEnabled = true;
|
config.webhookEnabled = true;
|
||||||
config.barkEnabled = true;
|
config.barkEnabled = true;
|
||||||
config.serverChanEnabled = true;
|
config.serverChanEnabled = true;
|
||||||
@@ -19,6 +20,7 @@ describe('applyRuntimeSettings', () => {
|
|||||||
|
|
||||||
applyRuntimeSettings(new Map([
|
applyRuntimeSettings(new Map([
|
||||||
['disable_cross_protocol_fallback', JSON.stringify(true)],
|
['disable_cross_protocol_fallback', JSON.stringify(true)],
|
||||||
|
['responses_compact_fallback_to_responses_enabled', JSON.stringify(true)],
|
||||||
['webhook_enabled', JSON.stringify(false)],
|
['webhook_enabled', JSON.stringify(false)],
|
||||||
['bark_enabled', JSON.stringify(false)],
|
['bark_enabled', JSON.stringify(false)],
|
||||||
['serverchan_enabled', JSON.stringify(false)],
|
['serverchan_enabled', JSON.stringify(false)],
|
||||||
@@ -26,6 +28,7 @@ describe('applyRuntimeSettings', () => {
|
|||||||
]));
|
]));
|
||||||
|
|
||||||
expect(config.disableCrossProtocolFallback).toBe(true);
|
expect(config.disableCrossProtocolFallback).toBe(true);
|
||||||
|
expect(config.responsesCompactFallbackToResponsesEnabled).toBe(true);
|
||||||
expect(config.webhookEnabled).toBe(false);
|
expect(config.webhookEnabled).toBe(false);
|
||||||
expect(config.barkEnabled).toBe(false);
|
expect(config.barkEnabled).toBe(false);
|
||||||
expect(config.serverChanEnabled).toBe(false);
|
expect(config.serverChanEnabled).toBe(false);
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ export function applyRuntimeSettings(settingsMap: Map<string, string>) {
|
|||||||
config.codexUpstreamWebsocketEnabled = codexUpstreamWebsocketEnabled;
|
config.codexUpstreamWebsocketEnabled = codexUpstreamWebsocketEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const responsesCompactFallbackToResponsesEnabled = parseSettingFromMap<boolean>(settingsMap, 'responses_compact_fallback_to_responses_enabled');
|
||||||
|
if (typeof responsesCompactFallbackToResponsesEnabled === 'boolean') {
|
||||||
|
config.responsesCompactFallbackToResponsesEnabled = responsesCompactFallbackToResponsesEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
const disableCrossProtocolFallback = parseSettingFromMap<boolean>(settingsMap, 'disable_cross_protocol_fallback');
|
const disableCrossProtocolFallback = parseSettingFromMap<boolean>(settingsMap, 'disable_cross_protocol_fallback');
|
||||||
if (typeof disableCrossProtocolFallback === 'boolean') {
|
if (typeof disableCrossProtocolFallback === 'boolean') {
|
||||||
config.disableCrossProtocolFallback = disableCrossProtocolFallback;
|
config.disableCrossProtocolFallback = disableCrossProtocolFallback;
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ export type RuntimeSettingsPayload = {
|
|||||||
systemProxyUrl?: string;
|
systemProxyUrl?: string;
|
||||||
modelAvailabilityProbeEnabled?: boolean;
|
modelAvailabilityProbeEnabled?: boolean;
|
||||||
codexUpstreamWebsocketEnabled?: boolean;
|
codexUpstreamWebsocketEnabled?: boolean;
|
||||||
|
responsesCompactFallbackToResponsesEnabled?: boolean;
|
||||||
disableCrossProtocolFallback?: boolean;
|
disableCrossProtocolFallback?: boolean;
|
||||||
proxySessionChannelConcurrencyLimit?: number;
|
proxySessionChannelConcurrencyLimit?: number;
|
||||||
proxySessionChannelQueueWaitMs?: number;
|
proxySessionChannelQueueWaitMs?: number;
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ type RuntimeSettings = {
|
|||||||
logCleanupRetentionDays: number;
|
logCleanupRetentionDays: number;
|
||||||
modelAvailabilityProbeEnabled: boolean;
|
modelAvailabilityProbeEnabled: boolean;
|
||||||
codexUpstreamWebsocketEnabled: boolean;
|
codexUpstreamWebsocketEnabled: boolean;
|
||||||
|
responsesCompactFallbackToResponsesEnabled: boolean;
|
||||||
disableCrossProtocolFallback: boolean;
|
disableCrossProtocolFallback: boolean;
|
||||||
proxySessionChannelConcurrencyLimit: number;
|
proxySessionChannelConcurrencyLimit: number;
|
||||||
proxySessionChannelQueueWaitMs: number;
|
proxySessionChannelQueueWaitMs: number;
|
||||||
@@ -241,6 +242,7 @@ export default function Settings() {
|
|||||||
logCleanupRetentionDays: 30,
|
logCleanupRetentionDays: 30,
|
||||||
modelAvailabilityProbeEnabled: false,
|
modelAvailabilityProbeEnabled: false,
|
||||||
codexUpstreamWebsocketEnabled: false,
|
codexUpstreamWebsocketEnabled: false,
|
||||||
|
responsesCompactFallbackToResponsesEnabled: false,
|
||||||
disableCrossProtocolFallback: false,
|
disableCrossProtocolFallback: false,
|
||||||
proxySessionChannelConcurrencyLimit: 2,
|
proxySessionChannelConcurrencyLimit: 2,
|
||||||
proxySessionChannelQueueWaitMs: 1500,
|
proxySessionChannelQueueWaitMs: 1500,
|
||||||
@@ -488,6 +490,7 @@ export default function Settings() {
|
|||||||
: 30,
|
: 30,
|
||||||
modelAvailabilityProbeEnabled: !!runtimeInfo.modelAvailabilityProbeEnabled,
|
modelAvailabilityProbeEnabled: !!runtimeInfo.modelAvailabilityProbeEnabled,
|
||||||
codexUpstreamWebsocketEnabled: !!runtimeInfo.codexUpstreamWebsocketEnabled,
|
codexUpstreamWebsocketEnabled: !!runtimeInfo.codexUpstreamWebsocketEnabled,
|
||||||
|
responsesCompactFallbackToResponsesEnabled: !!runtimeInfo.responsesCompactFallbackToResponsesEnabled,
|
||||||
disableCrossProtocolFallback: !!runtimeInfo.disableCrossProtocolFallback,
|
disableCrossProtocolFallback: !!runtimeInfo.disableCrossProtocolFallback,
|
||||||
proxySessionChannelConcurrencyLimit: Number(runtimeInfo.proxySessionChannelConcurrencyLimit) >= 0
|
proxySessionChannelConcurrencyLimit: Number(runtimeInfo.proxySessionChannelConcurrencyLimit) >= 0
|
||||||
? Math.trunc(Number(runtimeInfo.proxySessionChannelConcurrencyLimit))
|
? Math.trunc(Number(runtimeInfo.proxySessionChannelConcurrencyLimit))
|
||||||
@@ -709,6 +712,7 @@ export default function Settings() {
|
|||||||
try {
|
try {
|
||||||
const res = await api.updateRuntimeSettings({
|
const res = await api.updateRuntimeSettings({
|
||||||
codexUpstreamWebsocketEnabled: runtime.codexUpstreamWebsocketEnabled,
|
codexUpstreamWebsocketEnabled: runtime.codexUpstreamWebsocketEnabled,
|
||||||
|
responsesCompactFallbackToResponsesEnabled: runtime.responsesCompactFallbackToResponsesEnabled,
|
||||||
proxySessionChannelConcurrencyLimit: runtime.proxySessionChannelConcurrencyLimit,
|
proxySessionChannelConcurrencyLimit: runtime.proxySessionChannelConcurrencyLimit,
|
||||||
proxySessionChannelQueueWaitMs: runtime.proxySessionChannelQueueWaitMs,
|
proxySessionChannelQueueWaitMs: runtime.proxySessionChannelQueueWaitMs,
|
||||||
});
|
});
|
||||||
@@ -717,6 +721,9 @@ export default function Settings() {
|
|||||||
codexUpstreamWebsocketEnabled: typeof res?.codexUpstreamWebsocketEnabled === 'boolean'
|
codexUpstreamWebsocketEnabled: typeof res?.codexUpstreamWebsocketEnabled === 'boolean'
|
||||||
? res.codexUpstreamWebsocketEnabled
|
? res.codexUpstreamWebsocketEnabled
|
||||||
: prev.codexUpstreamWebsocketEnabled,
|
: prev.codexUpstreamWebsocketEnabled,
|
||||||
|
responsesCompactFallbackToResponsesEnabled: typeof res?.responsesCompactFallbackToResponsesEnabled === 'boolean'
|
||||||
|
? res.responsesCompactFallbackToResponsesEnabled
|
||||||
|
: prev.responsesCompactFallbackToResponsesEnabled,
|
||||||
proxySessionChannelConcurrencyLimit: Number(res?.proxySessionChannelConcurrencyLimit) >= 0
|
proxySessionChannelConcurrencyLimit: Number(res?.proxySessionChannelConcurrencyLimit) >= 0
|
||||||
? Math.trunc(Number(res.proxySessionChannelConcurrencyLimit))
|
? Math.trunc(Number(res.proxySessionChannelConcurrencyLimit))
|
||||||
: prev.proxySessionChannelConcurrencyLimit,
|
: prev.proxySessionChannelConcurrencyLimit,
|
||||||
@@ -1439,6 +1446,14 @@ export default function Settings() {
|
|||||||
/>
|
/>
|
||||||
允许 metapi 到 Codex 上游使用 WebSocket
|
允许 metapi 到 Codex 上游使用 WebSocket
|
||||||
</label>
|
</label>
|
||||||
|
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--color-text-secondary)', marginBottom: 14 }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={runtime.responsesCompactFallbackToResponsesEnabled}
|
||||||
|
onChange={(e) => setRuntime((prev) => ({ ...prev, responsesCompactFallbackToResponsesEnabled: e.target.checked }))}
|
||||||
|
/>
|
||||||
|
Compact 明确不支持时回退到普通 Responses
|
||||||
|
</label>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 10 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 10 }}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 6 }}>会话通道并发上限</div>
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 6 }}>会话通道并发上限</div>
|
||||||
@@ -1481,6 +1496,9 @@ export default function Settings() {
|
|||||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.7 }}>
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.7 }}>
|
||||||
这组 lease 只作用于能识别稳定 `session_id` 的会话型请求;没有稳定会话标识的普通请求不会进入这个池。
|
这组 lease 只作用于能识别稳定 `session_id` 的会话型请求;没有稳定会话标识的普通请求不会进入这个池。
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.7 }}>
|
||||||
|
默认只走原生 `/responses/compact`。开启回退后,只有当上游明确返回 compact 不支持或路径不存在时,才会退回普通 `/responses`。
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button onClick={saveProxyTransportSettings} disabled={savingProxyTransport} className="btn btn-primary">
|
<button onClick={saveProxyTransportSettings} disabled={savingProxyTransport} className="btn btn-primary">
|
||||||
{savingProxyTransport ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 保存中...</> : '保存传输与并发'}
|
{savingProxyTransport ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 保存中...</> : '保存传输与并发'}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ describe('Settings proxy transport', () => {
|
|||||||
logCleanupProgramLogsEnabled: true,
|
logCleanupProgramLogsEnabled: true,
|
||||||
logCleanupRetentionDays: 14,
|
logCleanupRetentionDays: 14,
|
||||||
codexUpstreamWebsocketEnabled: false,
|
codexUpstreamWebsocketEnabled: false,
|
||||||
|
responsesCompactFallbackToResponsesEnabled: false,
|
||||||
proxySessionChannelConcurrencyLimit: 4,
|
proxySessionChannelConcurrencyLimit: 4,
|
||||||
proxySessionChannelQueueWaitMs: 3200,
|
proxySessionChannelQueueWaitMs: 3200,
|
||||||
routingFallbackUnitCost: 1,
|
routingFallbackUnitCost: 1,
|
||||||
@@ -74,6 +75,7 @@ describe('Settings proxy transport', () => {
|
|||||||
apiMock.updateRuntimeSettings.mockResolvedValue({
|
apiMock.updateRuntimeSettings.mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
codexUpstreamWebsocketEnabled: true,
|
codexUpstreamWebsocketEnabled: true,
|
||||||
|
responsesCompactFallbackToResponsesEnabled: true,
|
||||||
proxySessionChannelConcurrencyLimit: 6,
|
proxySessionChannelConcurrencyLimit: 6,
|
||||||
proxySessionChannelQueueWaitMs: 4200,
|
proxySessionChannelQueueWaitMs: 4200,
|
||||||
});
|
});
|
||||||
@@ -104,6 +106,12 @@ describe('Settings proxy transport', () => {
|
|||||||
));
|
));
|
||||||
const websocketToggle = websocketToggleLabel.findByType('input');
|
const websocketToggle = websocketToggleLabel.findByType('input');
|
||||||
expect(websocketToggle.props.checked).toBe(false);
|
expect(websocketToggle.props.checked).toBe(false);
|
||||||
|
const compactFallbackToggleLabel = root.root.find((node) => (
|
||||||
|
node.type === 'label'
|
||||||
|
&& collectText(node).includes('Compact 明确不支持时回退到普通 Responses')
|
||||||
|
));
|
||||||
|
const compactFallbackToggle = compactFallbackToggleLabel.findByType('input');
|
||||||
|
expect(compactFallbackToggle.props.checked).toBe(false);
|
||||||
|
|
||||||
const concurrencyInput = root.root.find((node) => (
|
const concurrencyInput = root.root.find((node) => (
|
||||||
node.type === 'input'
|
node.type === 'input'
|
||||||
@@ -118,6 +126,7 @@ describe('Settings proxy transport', () => {
|
|||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
websocketToggle.props.onChange({ target: { checked: true } });
|
websocketToggle.props.onChange({ target: { checked: true } });
|
||||||
|
compactFallbackToggle.props.onChange({ target: { checked: true } });
|
||||||
concurrencyInput.props.onChange({ target: { value: '6' } });
|
concurrencyInput.props.onChange({ target: { value: '6' } });
|
||||||
queueWaitInput.props.onChange({ target: { value: '4200' } });
|
queueWaitInput.props.onChange({ target: { value: '4200' } });
|
||||||
});
|
});
|
||||||
@@ -134,6 +143,7 @@ describe('Settings proxy transport', () => {
|
|||||||
|
|
||||||
expect(apiMock.updateRuntimeSettings).toHaveBeenCalledWith({
|
expect(apiMock.updateRuntimeSettings).toHaveBeenCalledWith({
|
||||||
codexUpstreamWebsocketEnabled: true,
|
codexUpstreamWebsocketEnabled: true,
|
||||||
|
responsesCompactFallbackToResponsesEnabled: true,
|
||||||
proxySessionChannelConcurrencyLimit: 6,
|
proxySessionChannelConcurrencyLimit: 6,
|
||||||
proxySessionChannelQueueWaitMs: 4200,
|
proxySessionChannelQueueWaitMs: 4200,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user