llm-switcher 1.6.8 → 1.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@ import {
12
12
  createChatStream, buildChatMessage, chatFinish, anthropicStopReason,
13
13
  smartText, smartReasoning, sanitizeJsonSchema, normalizeUpstream, buildVertexMessage,
14
14
  createCodeAssistStream,
15
- budgetToEffort, effortToBudget, normalizeEffort, capEffort, mapEquivalentEffort, EFFORT_LEVELS
15
+ budgetToEffort, effortToBudget, normalizeEffort, capEffort, mapEquivalentEffort, EFFORT_LEVELS, parseArgs, buildAnthropicMessage
16
16
  } from '../formats.mjs';
17
17
  import { assertValidAnthropicEvents } from './helpers.mjs';
18
18
 
@@ -420,7 +420,7 @@ test('toGeminiSchema: bare string shorthands become Schema objects (Vertex 400 f
420
420
  assert.deepEqual(toGeminiSchema('string'), { type: 'string' });
421
421
  assert.deepEqual(
422
422
  toGeminiSchema({ type: 'object', properties: { tags: { type: 'array', items: 'object' }, n: 'string' } }),
423
- { type: 'object', properties: { tags: { type: 'array', items: { type: 'object', properties: {} } }, n: { type: 'string' } } }
423
+ { type: 'object', properties: { tags: { type: 'array', items: { type: 'object', properties: { _placeholder: { type: 'boolean', description: 'Unused. Leave unset.' } } } }, n: { type: 'string' } } }
424
424
  );
425
425
  });
426
426
 
@@ -1053,3 +1053,293 @@ test('thinkingEffort profile option overrides client effort across all upstreams
1053
1053
  const autoChat = emitUpstreamBody('openai-chat', clientIR, 'gpt-5', { thinkingEffort: 'auto' });
1054
1054
  assert.equal(autoChat.reasoning_effort, 'low');
1055
1055
  });
1056
+
1057
+ test('compaction_trigger with tool history keeps tools and forces tool_choice none', () => {
1058
+ const ir = responsesToIR({
1059
+ model: 'm', tools: [fn('Read')], text: { format: { type: 'json_object' } }, input: [
1060
+ { role: 'user', content: 'read it' },
1061
+ { type: 'function_call', call_id: 'c1', name: 'Read', arguments: '{"p":"a"}' },
1062
+ { type: 'function_call_output', call_id: 'c1', output: 'data' },
1063
+ { type: 'compaction_trigger' }
1064
+ ]
1065
+ });
1066
+ assert.equal(ir.toolChoice, 'none');
1067
+ assert.equal(ir.responseFormat, null);
1068
+ const chat = irToChatBody(ir, 'gpt-x');
1069
+ assert.equal(chat.tools?.length, 1);
1070
+ assert.equal(chat.tool_choice, 'none');
1071
+ assert.equal(chat.response_format, undefined);
1072
+ const anthropic = irToAnthropicBody(ir, 'claude-x');
1073
+ assert.equal(anthropic.tools?.length, 1);
1074
+ assert.deepEqual(anthropic.tool_choice, { type: 'none' });
1075
+ assert.ok(anthropic.messages.some(m => m.content.some(b => b.type === 'tool_use')));
1076
+ const vertex = irToVertexBody(ir, 'gemini-x');
1077
+ assert.equal(vertex.tools?.[0].functionDeclarations.length, 1);
1078
+ assert.equal(vertex.toolConfig.functionCallingConfig.mode, 'NONE');
1079
+ // With no tools at all there is nothing to switch off: tool_choice without tools is refused.
1080
+ const bare = responsesToIR({ model: 'm', input: [{ role: 'user', content: 'hi' }, { type: 'compaction_trigger' }] });
1081
+ assert.equal(irToChatBody(bare, 'gpt-x').tool_choice, undefined);
1082
+ assert.equal(irToAnthropicBody(bare, 'claude-x').tool_choice, undefined);
1083
+ });
1084
+
1085
+ test('anthropic emitters sanitize foreign tool ids consistently', () => {
1086
+ const foreign = 'functions.Read:0';
1087
+ const valid = /^[a-zA-Z0-9_-]+$/;
1088
+ const history = [
1089
+ { role: 'user', content: 'go' },
1090
+ { role: 'assistant', content: [{ type: 'tool_use', id: foreign, name: 'Read', input: {} }] },
1091
+ { role: 'user', content: [{ type: 'tool_result', tool_use_id: foreign, content: 'ok' }] }
1092
+ ];
1093
+ const ir = anthropicToIR({ model: 'x', tools: [{ name: 'Read', input_schema: { type: 'object', properties: {} } }], messages: history });
1094
+ const body = irToAnthropicBody(ir, 'claude-x');
1095
+ const use = body.messages[1].content.find(b => b.type === 'tool_use');
1096
+ assert.match(use.id, valid);
1097
+ assert.equal(body.messages[2].content[0].tool_use_id, use.id);
1098
+ assert.equal(irToAnthropicBody(ir, 'claude-x').messages[1].content[0].id, use.id, 'a replayed history maps identically');
1099
+
1100
+ const message = buildAnthropicMessage({ model: 'x', tools: [{ id: foreign, name: 'Read', args: '{}' }], finish: 'tool_calls' });
1101
+ assert.equal(message.content[0].id, use.id);
1102
+ const events = [];
1103
+ const stream = createAnthropicStream((e, d) => events.push(d), 'x');
1104
+ stream.start();
1105
+ stream.tool({ index: 0, id: foreign, name: 'Read', args: '{}' });
1106
+ stream.finish('tool_calls');
1107
+ assert.equal(events.find(e => e.type === 'content_block_start').content_block.id, use.id);
1108
+
1109
+ const healed = healAnthropicPayload({ model: 'claude', messages: history });
1110
+ assert.equal(healed.changed, true);
1111
+ assert.equal(healed.payload.messages[1].content[0].id, use.id);
1112
+ assert.equal(healed.payload.messages[2].content[0].tool_use_id, use.id);
1113
+
1114
+ // Two ids that differ only in an invalid character must not collide, and a valid id is kept.
1115
+ const ids = ['a.b', 'a:b', 'a_b'].map(id => buildAnthropicMessage({ model: 'x', tools: [{ id, name: 'f', args: '{}' }] }).content[0].id);
1116
+ assert.equal(new Set(ids).size, 3);
1117
+ assert.equal(ids[2], 'a_b');
1118
+ assert.match(buildAnthropicMessage({ model: 'x', tools: [{ id: '', name: 'f', args: '{}' }] }).content[0].id, valid);
1119
+ });
1120
+
1121
+ test('healAnthropicPayload drops empty system and empty text blocks', () => {
1122
+ const nonEmpty = msgs => msgs.every(m => typeof m.content === 'string' ? m.content.trim()
1123
+ : m.content.length && m.content.every(b => b.type !== 'text' || b.text.trim()));
1124
+ const alternates = msgs => msgs[0].role === 'user' && msgs.every((m, i) => !i || m.role !== msgs[i - 1].role);
1125
+ const r = healAnthropicPayload({
1126
+ model: 'claude', system: [{ type: 'text', text: '' }], messages: [
1127
+ { role: 'user', content: [{ type: 'text', text: '' }, { type: 'text', text: 'hi' }] },
1128
+ { role: 'assistant', content: [{ type: 'text', text: ' ' }, { type: 'tool_use', id: 't1', name: 'f', input: {} }] },
1129
+ { role: 'user', content: [{ type: 'tool_result', tool_use_id: 't1', content: 'ok' }] },
1130
+ { role: 'assistant', content: [{ type: 'text', text: '' }] },
1131
+ { role: 'user', content: [{ type: 'text', text: 'next' }] }
1132
+ ]
1133
+ });
1134
+ assert.equal(r.changed, true);
1135
+ assert.equal('system' in r.payload, false);
1136
+ assert.ok(nonEmpty(r.payload.messages), JSON.stringify(r.payload.messages));
1137
+ assert.ok(alternates(r.payload.messages), JSON.stringify(r.payload.messages));
1138
+ assert.deepEqual(r.payload.messages.at(-1).content.map(b => b.type), ['tool_result', 'text']);
1139
+
1140
+ // A user turn that empties out between two assistant turns, or at the end, keeps its place.
1141
+ const r2 = healAnthropicPayload({
1142
+ model: 'claude', system: [{ type: 'text', text: 'sys' }, { type: 'text', text: '' }], messages: [
1143
+ { role: 'user', content: 'go' },
1144
+ { role: 'assistant', content: 'a' },
1145
+ { role: 'user', content: [{ type: 'text', text: '' }] },
1146
+ { role: 'assistant', content: 'b' },
1147
+ { role: 'user', content: '' }
1148
+ ]
1149
+ });
1150
+ assert.deepEqual(r2.payload.system, [{ type: 'text', text: 'sys' }]);
1151
+ assert.ok(nonEmpty(r2.payload.messages), JSON.stringify(r2.payload.messages));
1152
+ assert.ok(alternates(r2.payload.messages), JSON.stringify(r2.payload.messages));
1153
+ assert.equal(r2.payload.messages.length, 5);
1154
+ assert.equal(r2.payload.messages.at(-1).role, 'user');
1155
+ });
1156
+
1157
+ test("parseArgs('') returns {}", () => {
1158
+ assert.deepEqual(parseArgs(''), {});
1159
+ assert.deepEqual(parseArgs(' \n'), {});
1160
+ assert.deepEqual(parseArgs('{"a":1}'), { a: 1 });
1161
+ });
1162
+
1163
+ test('Gemini MALFORMED_FUNCTION_CALL finish is an error, not stop', () => {
1164
+ const ev = normalizeUpstream({ candidates: [{ content: { parts: [] }, finishReason: 'MALFORMED_FUNCTION_CALL' }] }, 'vertex');
1165
+ assert.equal(ev.finish, null);
1166
+ assert.match(String(ev.error), /MALFORMED_FUNCTION_CALL/);
1167
+ const chat = normalizeUpstream({ choices: [{ delta: {}, finish_reason: 'malformed_function_call' }] }, 'openai-chat');
1168
+ assert.match(String(chat.error), /malformed_function_call/i);
1169
+ });
1170
+
1171
+ // DeepSeek/Kimi thinking mode refuse a tool step whose earlier tool-call turn lost its reasoning_content.
1172
+ test('irToChatBody replays thinking as reasoning_content on tool-call turns', () => {
1173
+ const ir = anthropicToIR({ model: 'x', max_tokens: 8000, thinking: { type: 'enabled', budget_tokens: 4000 }, messages: [
1174
+ { role: 'user', content: 'list files' },
1175
+ { role: 'assistant', content: [{ type: 'thinking', thinking: 'I should run ls', signature: 'sig' }, { type: 'tool_use', id: 'toolu_1', name: 'Bash', input: { command: 'ls' } }] },
1176
+ { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'a.txt' }] },
1177
+ { role: 'assistant', content: [{ type: 'tool_use', id: 'toolu_2', name: 'Bash', input: { command: 'pwd' } }] },
1178
+ { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'toolu_2', content: '/' }] }
1179
+ ] });
1180
+ for (const opts of [{ thinkingMode: 'auto' }, { thinkingMode: 'native', replayReasoning: true }]) {
1181
+ const calls = irToChatBody(ir, 'deepseek-reasoner', opts).messages.filter(m => m.tool_calls);
1182
+ assert.equal(calls[0].reasoning_content, 'I should run ls', opts.thinkingMode);
1183
+ assert.equal(calls[1].reasoning_content, '', opts.thinkingMode);
1184
+ }
1185
+ const off = emitUpstreamBody('openai-chat', ir, 'deepseek-reasoner', { thinkingMode: 'off' });
1186
+ assert.ok(off.messages.every(m => !('reasoning_content' in m)));
1187
+
1188
+ const rir = responsesToIR({ model: 'x', reasoning: { effort: 'high' }, input: [
1189
+ { type: 'message', role: 'user', content: 'go' },
1190
+ { type: 'reasoning', summary: [{ type: 'summary_text', text: 'Plan: run ls' }], encrypted_content: 'opaque' },
1191
+ { type: 'function_call', call_id: 'c1', name: 'shell', arguments: '{}' },
1192
+ { type: 'function_call_output', call_id: 'c1', output: 'ok' }
1193
+ ] });
1194
+ const rb = irToChatBody(rir, 'kimi-k2-thinking', { thinkingMode: 'native', replayReasoning: true });
1195
+ assert.equal(rb.messages.find(m => m.tool_calls).reasoning_content, 'Plan: run ls');
1196
+ });
1197
+
1198
+ // Strict OpenAI-compatible servers may refuse the extra key, so native mode sends it only on the profile opt-in.
1199
+ test('irToChatBody native mode sends no reasoning_content without the profile opt-in', () => {
1200
+ const ir = anthropicToIR({ model: 'x', max_tokens: 8000, thinking: { type: 'enabled', budget_tokens: 4000 }, messages: [
1201
+ { role: 'user', content: 'list files' },
1202
+ { role: 'assistant', content: [{ type: 'thinking', thinking: 'I should run ls', signature: 'sig' }, { type: 'tool_use', id: 'toolu_1', name: 'Bash', input: { command: 'ls' } }] },
1203
+ { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'a.txt' }] },
1204
+ { role: 'assistant', content: [{ type: 'tool_use', id: 'toolu_2', name: 'Bash', input: { command: 'pwd' } }] },
1205
+ { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'toolu_2', content: '/' }] }
1206
+ ] });
1207
+ const cases = [
1208
+ ['gpt-5', ir, { thinkingMode: 'native' }],
1209
+ ['deepseek-reasoner', ir, { thinkingMode: 'native' }],
1210
+ ['gpt-5', ir, { thinkingMode: 'native', replayReasoning: 'yes' }],
1211
+ ['gpt-5', { ...ir, replayReasoning: true }, { thinkingMode: 'native' }]
1212
+ ];
1213
+ for (const [model, input, opts] of cases) {
1214
+ const body = irToChatBody(input, model, opts);
1215
+ assert.ok(body.messages.some(m => m.tool_calls), 'fixture has tool-call turns');
1216
+ assert.ok(body.messages.every(m => !('reasoning_content' in m)), `${model} ${JSON.stringify(opts)}`);
1217
+ }
1218
+ });
1219
+
1220
+ test('irToAnthropicBody drops thinking for an unsigned tool loop', () => {
1221
+ const ir = responsesToIR({ model: 'x', reasoning: { effort: 'high' }, input: [
1222
+ { type: 'message', role: 'user', content: 'run ls' },
1223
+ { type: 'function_call', call_id: 'c1', name: 'shell', arguments: '{"command":["ls"]}' },
1224
+ { type: 'function_call_output', call_id: 'c1', output: 'a.txt' }
1225
+ ] });
1226
+ const body = irToAnthropicBody(ir, 'claude-opus-4-6');
1227
+ assert.equal(body.thinking, undefined);
1228
+ assert.equal(body.messages.at(-1).content[0].type, 'tool_result');
1229
+ const forced = irToAnthropicBody(ir, 'claude-opus-4-6', { thinkingEffort: 'high' });
1230
+ assert.equal(forced.thinking, undefined);
1231
+ const fresh = irToAnthropicBody(responsesToIR({ model: 'x', reasoning: { effort: 'high' }, input: 'hi' }), 'claude-opus-4-6');
1232
+ assert.equal(fresh.thinking?.type, 'enabled');
1233
+ });
1234
+
1235
+ test('toGeminiSchema never emits an empty nested OBJECT', () => {
1236
+ const out = toGeminiSchema({ type: 'object', properties: {
1237
+ env: { type: 'object', additionalProperties: { type: 'string' } },
1238
+ list: { type: 'array', items: { type: 'object' } },
1239
+ either: { anyOf: [{ type: 'object' }, { type: 'string' }] },
1240
+ short: 'object'
1241
+ } });
1242
+ const walk = (s, nested) => {
1243
+ if (!s || typeof s !== 'object') return;
1244
+ if (nested && s.type === 'object') assert.ok(Object.keys(s.properties || {}).length, JSON.stringify(s));
1245
+ for (const p of Object.values(s.properties || {})) walk(p, true);
1246
+ if (s.items) walk(s.items, true);
1247
+ for (const b of s.anyOf || []) walk(b, true);
1248
+ };
1249
+ walk(out, false);
1250
+ assert.deepEqual(toGeminiSchema({ type: 'object' }), { type: 'object', properties: {} });
1251
+ });
1252
+
1253
+ test('irToVertexBody wraps tool output with $ keys', () => {
1254
+ const output = JSON.stringify({ $ref: '#/definitions/Pet', definitions: { Pet: { $id: 'p' } } });
1255
+ const nested = JSON.stringify({ a: { b: [{ $schema: 'x' }] } });
1256
+ const ir = { ...anthropicToIR({ model: 'x', messages: [
1257
+ { role: 'user', content: 'go' },
1258
+ { role: 'assistant', content: [{ type: 'tool_use', id: 't1', name: 'Bash', input: {} }, { type: 'tool_use', id: 't2', name: 'Bash', input: {} }, { type: 'tool_use', id: 't3', name: 'Bash', input: {} }] },
1259
+ { role: 'user', content: [
1260
+ { type: 'tool_result', tool_use_id: 't1', content: output },
1261
+ { type: 'tool_result', tool_use_id: 't2', content: nested },
1262
+ { type: 'tool_result', tool_use_id: 't3', content: '{"ok":true}' }
1263
+ ] }
1264
+ ] }) };
1265
+ const responses = irToVertexBody(ir, 'gemini-2.5-pro').contents.flatMap(c => c.parts).filter(p => p.functionResponse).map(p => p.functionResponse.response);
1266
+ assert.deepEqual(responses[0], { result: output });
1267
+ assert.deepEqual(responses[1], { result: nested });
1268
+ assert.deepEqual(responses[2], { ok: true });
1269
+ });
1270
+
1271
+ test('toGeminiSchema stringifies an untyped const', () => {
1272
+ const out = toGeminiSchema({ type: 'object', properties: { flag: { const: true }, n: { const: 3 }, s: { const: 'x' } } });
1273
+ assert.deepEqual(out.properties.flag, { enum: ['true'], type: 'string' });
1274
+ assert.deepEqual(out.properties.n, { enum: ['3'], type: 'string' });
1275
+ assert.deepEqual(out.properties.s, { enum: ['x'], type: 'string' });
1276
+ });
1277
+
1278
+ test('responsesToIR keeps long colliding tool names distinct', () => {
1279
+ const ns = `mcp__${'server_with_a_long_name_'.repeat(3)}`;
1280
+ const ir = responsesToIR({ model: 'x', input: [
1281
+ { type: 'message', role: 'user', content: 'go' },
1282
+ { type: 'function_call', call_id: 'c1', namespace: ns, name: 'list_items_with_filter_b', arguments: '{}' },
1283
+ { type: 'function_call_output', call_id: 'c1', output: 'ok' }
1284
+ ], tools: [{ type: 'namespace', name: ns, tools: [
1285
+ { type: 'function', name: 'list_items_with_filter_a', parameters: { type: 'object', properties: {} } },
1286
+ { type: 'function', name: 'list_items_with_filter_b', parameters: { type: 'object', properties: {} } }
1287
+ ] }] });
1288
+ const names = ir.tools.map(t => t.name);
1289
+ assert.equal(names.length, 2);
1290
+ assert.notEqual(names[0], names[1]);
1291
+ assert.ok(names.every(n => n.length <= 64 && /^[A-Za-z0-9_-]+$/.test(n)), names.join());
1292
+ assert.equal(ir.toolMeta[names[1]].name, 'list_items_with_filter_b');
1293
+ assert.equal(ir.messages[1].toolCalls[0].name, names[1]);
1294
+ assert.equal(responsesToIR({ model: 'x', input: 'hi', tools: [{ type: 'function', name: 'read_file', parameters: {} }] }).tools[0].name, 'read_file');
1295
+ });
1296
+
1297
+ test('sanitizeJsonSchema drops null keywords and lookaround patterns', () => {
1298
+ const out = sanitizeJsonSchema({ type: 'object', required: null, description: null, properties: {
1299
+ slug: { type: 'string', pattern: '^(?!-)[a-z-]+$' },
1300
+ tag: { type: 'string', pattern: '(?<=#)\\w+' },
1301
+ plain: { type: 'string', pattern: '^[a-z]+$', default: null },
1302
+ named: { type: 'string', pattern: '^(?<year>\\d{4})$' },
1303
+ pattern: { type: 'string', enum: ['a', null], const: null }
1304
+ } });
1305
+ assert.equal('required' in out, false);
1306
+ assert.equal('description' in out, false);
1307
+ assert.equal('pattern' in out.properties.slug, false);
1308
+ assert.equal('pattern' in out.properties.tag, false);
1309
+ assert.equal(out.properties.plain.pattern, '^[a-z]+$');
1310
+ assert.equal(out.properties.plain.default, null);
1311
+ assert.equal(out.properties.named.pattern, '^(?<year>\\d{4})$');
1312
+ assert.deepEqual(out.properties.pattern, { type: 'string', enum: ['a', null], const: null });
1313
+ assert.deepEqual(sanitizeJsonSchema({ type: 'object', properties: {}, default: { a: null, b: {} } }).default, { a: null, b: {} });
1314
+ });
1315
+
1316
+ test('irToVertexBody never ends on a model turn', () => {
1317
+ const ir = anthropicToIR({ model: 'x', messages: [
1318
+ { role: 'user', content: 'Write a haiku' },
1319
+ { role: 'assistant', content: 'Autumn moon' }
1320
+ ] });
1321
+ const body = irToVertexBody(ir, 'gemini-2.5-pro');
1322
+ assert.equal(body.contents.at(-1).role, 'user');
1323
+ assert.ok(body.contents.some(c => c.role === 'model' && c.parts.some(p => p.text === 'Autumn moon')), 'the prefill text is kept');
1324
+ });
1325
+
1326
+ test('healAnthropicPayload dedupes repeated tool_use ids', () => {
1327
+ const use = (id, cmd) => ({ type: 'tool_use', id, name: 'Bash', input: { command: cmd } });
1328
+ const result = (id, text) => ({ type: 'tool_result', tool_use_id: id, content: text });
1329
+ const r = healAnthropicPayload({ model: 'claude', messages: [
1330
+ { role: 'user', content: 'go' },
1331
+ { role: 'assistant', content: [use('functions_Bash_0', 'ls')] },
1332
+ { role: 'user', content: [result('functions_Bash_0', 'a.txt')] },
1333
+ { role: 'assistant', content: [use('functions_Bash_0', 'pwd'), use('functions_Bash_1', 'id')] },
1334
+ { role: 'user', content: [result('functions_Bash_0', '/'), result('functions_Bash_1', 'uid')] }
1335
+ ] });
1336
+ assert.equal(r.changed, true);
1337
+ const msgs = r.payload.messages;
1338
+ const ids = msgs.filter(m => m.role === 'assistant').flatMap(m => m.content.filter(b => b.type === 'tool_use').map(b => b.id));
1339
+ assert.equal(new Set(ids).size, ids.length, ids.join());
1340
+ assert.ok(ids.every(id => /^[a-zA-Z0-9_-]+$/.test(id)));
1341
+ assert.equal(ids[0], 'functions_Bash_0');
1342
+ // Each result still answers its own call.
1343
+ assert.deepEqual(msgs[2].content.map(b => [b.tool_use_id, b.content]), [[ids[0], 'a.txt']]);
1344
+ assert.deepEqual(msgs[4].content.map(b => [b.tool_use_id, b.content]), [[ids[1], '/'], [ids[2], 'uid']]);
1345
+ });
@@ -112,6 +112,10 @@ function startUpstream() {
112
112
  res.writeHead(429, { 'Content-Type': 'application/json', 'retry-after': '7' });
113
113
  return res.end(JSON.stringify({ error: { message: 'slow down' } }));
114
114
  }
115
+ if (lastUser.includes('CONTEXT_OVERFLOW')) {
116
+ res.writeHead(400, { 'Content-Type': 'application/json' });
117
+ return res.end(JSON.stringify({ error: { message: "This model's maximum context length is 131072 tokens. However, you requested 140000 tokens (132000 in the messages, 8000 in the completion). Please reduce the length of the messages or completion.", type: 'invalid_request_error', code: 'invalid_request_error' } }));
118
+ }
115
119
  if (lastUser.includes('APPLY_PATCH')) {
116
120
  return sse(res, [
117
121
  { choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: 'call_patch', type: 'function', function: { name: 'apply_patch', arguments: '{"input":"*** Begin Patch\\n' } }] } }] },
@@ -407,7 +411,7 @@ test('Codex via ag/* target: Gemini-hostile tool schemas are rewritten before up
407
411
  const params = up.body.tools[0].function.parameters;
408
412
  assert.deepEqual(params.properties.ids, { type: 'array', items: { type: 'string' }, nullable: true });
409
413
  assert.deepEqual(params.properties.part, { type: 'object', properties: { t: { type: 'string' } } });
410
- assert.deepEqual(params.properties.raw, { type: 'object', properties: {} });
414
+ assert.deepEqual(params.properties.raw, { type: 'object', properties: { _placeholder: { type: 'boolean', description: 'Unused. Leave unset.' } } });
411
415
  assert.deepEqual(params.required, ['ids']);
412
416
  assert.ok(!JSON.stringify(params).includes('$ref'), 'no $ref survives');
413
417
  assert.equal(params.$defs, undefined);
@@ -594,6 +598,20 @@ test('Upstream 429 is returned in Anthropic error shape with retry-after', async
594
598
  assert.deepEqual(json, { type: 'error', error: { type: 'rate_limit_error', message: 'slow down' } });
595
599
  });
596
600
 
601
+ // Claude Code compacts only on "prompt is too long", and Codex only on context_length_exceeded.
602
+ test('sendClientError maps context overflow per client', async () => {
603
+ const cc = await post('/v1/messages', { model: 'claude-opus-4-6', max_tokens: 10, messages: [{ role: 'user', content: 'CONTEXT_OVERFLOW' }] });
604
+ assert.equal(cc.status, 400);
605
+ const ccJson = await cc.json();
606
+ assert.equal(ccJson.error.type, 'invalid_request_error');
607
+ assert.match(ccJson.error.message, /^prompt is too long: 140000 tokens > 131072 maximum/);
608
+ const codex = await post('/v1/responses', { model: 'gpt-5-codex', input: 'CONTEXT_OVERFLOW' });
609
+ assert.equal(codex.status, 400);
610
+ const codexJson = await codex.json();
611
+ assert.equal(codexJson.error.code, 'context_length_exceeded');
612
+ assert.match(codexJson.error.message, /maximum context length is 131072/);
613
+ });
614
+
597
615
  test('Mid-stream upstream error surfaces as an error event instead of a fake end_turn', async () => {
598
616
  const res = await post('/v1/messages', { model: 'claude-opus-4-6', max_tokens: 10, stream: true, messages: [{ role: 'user', content: 'MID_STREAM_ERROR' }] });
599
617
  const events = parseSSE(await res.text());
package/tests/ui.test.mjs CHANGED
@@ -124,10 +124,11 @@ test('documentation tab is properly integrated with sidebar navigation, sections
124
124
  // Table of Contents navigation exists
125
125
  assert.match(html, /<nav class="docs-toc" id="docsToc"/);
126
126
 
127
- // All 10 core sections exist
127
+ // All 11 core sections exist
128
128
  const expectedSections = [
129
129
  'doc-overview',
130
130
  'doc-install',
131
+ 'doc-intact',
131
132
  'doc-cli',
132
133
  'doc-blindfold',
133
134
  'doc-agy',
@@ -150,3 +151,8 @@ test('documentation tab is properly integrated with sidebar navigation, sections
150
151
  assert.match(html, /function copyDocCode\(/);
151
152
  assert.match(html, /function initDocsScrollSpy\(/);
152
153
  });
154
+
155
+ test('sidebar menu is arranged logically by frequency of use', () => {
156
+ const navMatches = [...html.matchAll(/data-nav="([^"]+)"/g)].map(m => m[1]);
157
+ assert.deepEqual(navMatches, ['routes', 'logs', 'models', 'doctor', 'goal-check', 'compact', 'docs']);
158
+ });