Address high-priority review gaps before merging the dev snapshot

The release branch still had unresolved review comments around payload-rule
atomicity, brand blocking, compact fallback controls, and response
compatibility edge-cases. This patch pulls those behaviors back in line with
the intended contracts and adds focused regression coverage for the fragile
bridges.

Constraint: The fixes must stay narrow to the PR branch so review/merge can continue without reopening the larger dev snapshot conflict set
Rejected: Leave outdated-looking threads unresolved until after merge | reviewers still see concrete unfixed regressions in the current release branch
Rejected: Re-run another broad merge from dev/main | too risky while the PR branch is already green and mergeable
Confidence: medium
Scope-risk: moderate
Directive: If these release-branch fixes need to persist for future release cuts, backport them onto dev instead of re-fighting the same review threads next time
Tested: Focused vitest for settings events, brand matcher, compact responses, codex compatibility, anthropic stream bridge, openai chat response bridge; typecheck; repo drift check; git diff --check
Not-tested: Full npm test; live PR thread resolution state after push
This commit is contained in:
cita-777
2026-04-18 13:05:52 +08:00
parent 57db7fd107
commit 4b5d045b00
13 changed files with 338 additions and 72 deletions
+7 -2
View File
@@ -905,6 +905,7 @@ export async function settingsRoutes(app: FastifyInstance) {
const body = parsedBody.data as RuntimeSettingsBody;
const changedLabels: string[] = [];
const currentRequestIp = extractClientIp(request.ip, request.headers['x-forwarded-for']);
let pendingPayloadRules: typeof config.payloadRules | undefined;
const webhookTouched = body.webhookUrl !== undefined || body.webhookEnabled !== undefined;
const nextWebhookUrl = body.webhookUrl !== undefined
@@ -1149,8 +1150,7 @@ export async function settingsRoutes(app: FastifyInstance) {
if (previousRules !== nextRules) {
changedLabels.push('Payload 规则');
}
config.payloadRules = parsedPayloadRules.normalized;
await upsertSetting('payload_rules', config.payloadRules);
pendingPayloadRules = parsedPayloadRules.normalized;
}
if (body.modelAvailabilityProbeEnabled !== undefined) {
@@ -1718,6 +1718,11 @@ export async function settingsRoutes(app: FastifyInstance) {
upsertSetting('token_router_failure_cooldown_max_sec', normalized);
}
if (pendingPayloadRules !== undefined) {
config.payloadRules = pendingPayloadRules;
await upsertSetting('payload_rules', pendingPayloadRules);
}
if (changedLabels.length > 0) {
let eventType: 'checkin' | 'balance' | 'proxy' | 'status' | 'token' = 'status';
if (changedLabels.length === 1) {
@@ -179,7 +179,7 @@ describe('responses proxy compact upstream routing', () => {
expect(targetUrl).toContain('/v1/responses/compact');
});
it('automatically falls back from compact to ordinary responses for generic openai-compatible upstreams when compact is unsupported', async () => {
it('does not fall back from compact to ordinary responses for generic openai-compatible upstreams when the fallback toggle is off', async () => {
selectChannelMock.mockReturnValue({
channel: { id: 11, routeId: 22 },
site: { id: 44, name: 'generic-openai-site', url: 'https://upstream.example.com', platform: 'openai' },
@@ -199,12 +199,12 @@ describe('responses proxy compact upstream routing', () => {
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 },
error: {
message: 'Invalid URL (POST /v1/responses/compact)',
type: 'invalid_request_error',
},
}), {
status: 200,
status: 404,
headers: { 'content-type': 'application/json' },
}));
@@ -217,19 +217,9 @@ describe('responses proxy compact upstream routing', () => {
},
});
expect(response.statusCode).toBe(200);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(response.statusCode).toBe(404);
expect(fetchMock).toHaveBeenCalledTimes(1);
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',
usage: {
input_tokens: 10,
output_tokens: 5,
total_tokens: 15,
},
});
});
it('optionally falls back from compact to ordinary responses when compact is explicitly unsupported', async () => {
@@ -282,6 +272,7 @@ describe('responses proxy compact upstream routing', () => {
});
it('automatically falls back from compact when a generic upstream rejects the stream parameter on the compact endpoint', 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' },
@@ -331,6 +322,7 @@ describe('responses proxy compact upstream routing', () => {
});
it('rebuilds compact fallback as an SSE-capable non-compact /v1/responses request for sub2api', async () => {
config.responsesCompactFallbackToResponsesEnabled = true;
selectChannelMock.mockReturnValue({
channel: { id: 11, routeId: 22 },
site: { id: 44, name: 'sub2api-site', url: 'https://sub2api.example.com', platform: 'sub2api' },