Skip to content

Commit 6d8b625

Browse files
鲁工鲁工
authored andcommitted
fix: stop policing per-message schemas the upstream owns
1.8.3's request validation whitelisted messages[].role to user/assistant, but Claude Code v2.1+ sends harness context as role:"system" entries on /v1/messages?beta=true - every real session died locally with "400 messages[1].role must be user or assistant" before reaching any provider. Root cause verified by capturing live Claude Code traffic through a logging proxy. The gateway now validates only what routing depends on (body shape, model, messages array, stream flag) and leaves message schemas to the upstream protocol, which evolves with the client.
1 parent 8d7ce49 commit 6d8b625

5 files changed

Lines changed: 40 additions & 18 deletions

File tree

dist/server.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/server.js

Lines changed: 4 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/server.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/server.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,10 @@ function requestValidationError(body: unknown): string | null {
9595
if (!Array.isArray(request.messages) || request.messages.length === 0) {
9696
return 'messages must be a non-empty array';
9797
}
98-
for (const [index, message] of request.messages.entries()) {
99-
if (!message || (message.role !== 'user' && message.role !== 'assistant')) {
100-
return `messages[${index}].role must be user or assistant`;
101-
}
102-
if (typeof message.content !== 'string' && !Array.isArray(message.content)) {
103-
return `messages[${index}].content must be a string or content-block array`;
104-
}
105-
}
98+
// Do NOT validate individual message shapes (role, content, ...): the
99+
// message schema belongs to the upstream protocol and evolves with the
100+
// client - Claude Code v2.1+ already sends role:"system" entries. The
101+
// gateway only checks what routing itself depends on.
106102
if (
107103
request.max_tokens !== undefined &&
108104
(!Number.isInteger(request.max_tokens) || request.max_tokens <= 0)

tests/server.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,36 @@ describe('POST /v1/messages forwarding', () => {
247247
expect(text).toContain('message_delta');
248248
});
249249

250+
// Claude Code v2.1+ puts harness context into the messages array as
251+
// role:"system" entries (POST /v1/messages?beta=true). The gateway must
252+
// not police message schemas the upstream owns - 1.8.3's role whitelist
253+
// rejected every such request with 400 before it ever left the machine.
254+
it('forwards messages with roles beyond user/assistant untouched', async () => {
255+
const configFile = writeTempConfig(testConfig(`http://127.0.0.1:${upstreamPort}`));
256+
const gateway = await startGateway(configFile);
257+
258+
const res = await fetch(`${gateway.url}/v1/messages`, {
259+
method: 'POST',
260+
headers: { 'Content-Type': 'application/json' },
261+
body: JSON.stringify({
262+
model: 'testmodel-v1',
263+
max_tokens: 100,
264+
messages: [
265+
{ role: 'user', content: 'hi' },
266+
{ role: 'system', content: 'Available agent types for the Agent tool: ...' },
267+
],
268+
}),
269+
});
270+
271+
expect(res.status).toBe(200);
272+
const forwarded = captured.body?.messages as Array<{ role: string; content: string }>;
273+
expect(forwarded).toHaveLength(2);
274+
expect(forwarded[1]).toEqual({
275+
role: 'system',
276+
content: 'Available agent types for the Agent tool: ...',
277+
});
278+
});
279+
250280
it('returns 400 invalid_model for unknown models', async () => {
251281
const configFile = writeTempConfig(testConfig(`http://127.0.0.1:${upstreamPort}`));
252282
const gateway = await startGateway(configFile);

0 commit comments

Comments
 (0)