kimi-code / packages /klient /examples /model-requester-boundary.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
4e23b01 verified
Raw History Blame Contribute Delete
32.6 kB
/**
* Probe the ModelRequester problem boundary, in two parts.
*
* Part 1 — real config: bootstraps the agent-core-v2 App scope on the REAL
* Kimi home (`KIMI_CODE_HOME` or `~/.kimi-code`), resolves `IModelCatalog`
* with the providers/auth from `config.toml`, lists every provider/model, and
* pings every configured model through its `ModelRequester` (one tiny live
* request per model, real credentials). This is the "does the assembled
* pipeline actually reach each provider" smoke.
*
* Part 2 — deterministic boundary probes: points hand-built Models at a local
* stub HTTP server (OpenAI Chat Completions wire) and drives failure modes
* through `ModelRequesterImpl`, recording for each one WHO owned it:
*
* - wrapped by ChatProvider — the wire adapter converted the SDK/transport
* failure into the typed `ChatProviderError` family (APIStatusError /
* APIConnectionError / APIEmptyResponseError / ...), so the requester could
* translate it into a coded `Error2` (`provider.*` / `context.overflow`).
* - owned by ModelRequester — behavior the ChatProvider layer CANNOT
* provide: per-request credential injection (credentials.resolve per
* attempt), plus the final `translateProviderError` safety net that
* turns even unwrapped raw errors into `Error2` (`internal`).
* - owned by the caller — the OAuth 401 → credentials.invalidate →
* single replay: the requester surfaces the 401, and the call site
* re-resolves and re-sends (mirroring IModelCatalog.generate / ping).
* - owned by neither — user cancellation: the standard AbortError
* DOMException passes through BOTH layers untranslated, by design.
*
* Tool-call probes cover the decode/encode boundary specifically: streamed
* `delta.tool_calls` assembly (single, parallel-interleaved, index-less),
* malformed arguments (deliberately NOT the wire layer's problem), the
* strict-provider tool-exchange 400 staying recognizable through the wrap,
* and request-side encoding of tool declarations and tool results.
*
* Run (the examples tsconfig enables the decorators the engine sources need):
* pnpm -C packages/klient smoke:boundary
*
* Env:
* KIMI_CODE_HOME — default `~/.kimi-code`
* KIMI_BOUNDARY_MODELS — comma-separated model ids to ping (default: all)
* KIMI_BOUNDARY_SKIP_LIVE — set to `1` to skip part 1 (no real API calls)
*/
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { EXAMPLE_CLIENT_IDENTITY } from './identity.js';
import type { AddressInfo } from 'node:net';
import { bootstrap, logSeed, resolveLoggingConfig } from '@moonshot-ai/agent-core-v2';
import { isError2 } from '@moonshot-ai/agent-core-v2/_base/errors/errors';
import { IConfigService } from '@moonshot-ai/agent-core-v2/app/config/config';
import { UNKNOWN_CAPABILITY } from '@moonshot-ai/agent-core-v2/llm-adapter/contract/capability';
import {
APIContextOverflowError,
APIStatusError,
ChatProviderError,
isAbortError,
isToolExchangeAdjacencyError,
} from '@moonshot-ai/agent-core-v2/llm-adapter/contract/errors';
import {
createOAuthCredentialProvider,
createStaticCredentialProvider,
} from '@moonshot-ai/agent-core-v2/human/credentials/credentials';
import type {
ToolCall,
ToolDescription as Tool,
} from '@moonshot-ai/agent-core-v2/human/llm/message';
import type { LlmCredentialProvider } from '@moonshot-ai/agent-core-v2/human/llm/requester/requester';
import type { Model } from '@moonshot-ai/agent-core-v2/llm-adapter/model/catalog';
import { IModelCatalog } from '@moonshot-ai/agent-core-v2/llm-adapter/model/catalog';
import type {
ModelRequestInput,
ModelRequester,
} from '@moonshot-ai/agent-core-v2/llm-adapter/model/model-requester';
import { ModelRequesterImpl } from '@moonshot-ai/agent-core-v2/llm-adapter/model/model-requester-impl';
import { ProtocolAdapterRegistry } from '@moonshot-ai/agent-core-v2/llm-adapter/protocol/protocolAdapterRegistry';
function assert(cond: boolean, message: string): asserts cond {
if (!cond) throw new Error(`assertion failed: ${message}`);
}
const tick = (ms: number): Promise<void> =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});
// ---------------------------------------------------------------------------
// Part 1 — real config.toml: catalog listing + per-model live ping.
// ---------------------------------------------------------------------------
async function probeRealConfig(): Promise<void> {
const homeDir = process.env['KIMI_CODE_HOME'] ?? join(homedir(), '.kimi-code');
console.log(`\n=== part 1: real config (${homeDir}/config.toml) ===`);
const { app } = bootstrap({ homeDir, clientIdentity: EXAMPLE_CLIENT_IDENTITY }, [
...logSeed(resolveLoggingConfig({ homeDir, env: process.env })),
]);
try {
// Config (and every section on top of it) loads asynchronously.
await app.accessor.get(IConfigService).ready;
const catalog = app.accessor.get(IModelCatalog);
const providers = await catalog.listProviders();
for (const p of providers) {
console.log(
`[provider] ${p.id} type=${p.type} status=${p.status} ` +
`has_api_key=${p.has_api_key} models=${(p.models ?? []).length}`,
);
}
const models = await catalog.listModels();
const filter = process.env['KIMI_BOUNDARY_MODELS']?.split(',').map((s) => s.trim());
const targets = models.filter((m) => filter === undefined || filter.includes(m.model));
assert(targets.length > 0, 'at least one configured model to ping');
for (const m of targets) {
const startedAt = Date.now();
const result = await Promise.race([
catalog.ping(m.model),
tick(45_000).then(() => ({ ok: false as const, durationMs: 45_000, error: 'ping timed out after 45s' })),
]);
if (result.ok) {
console.log(
`[ping ok] ${m.model} (${m.provider}) ${String(Date.now() - startedAt)}ms ` +
`text=${JSON.stringify(result.text ?? '')} finish=${String(result.finishReason)} ` +
`usage=${JSON.stringify(result.usage ?? null)}`,
);
} else {
const firstLine = (result.error ?? 'unknown error').split('\n')[0];
console.log(`[ping fail] ${m.model} (${m.provider}) ${firstLine}`);
}
}
} finally {
app.dispose();
}
}
// ---------------------------------------------------------------------------
// Part 2 — stub-driven boundary probes.
// ---------------------------------------------------------------------------
const PING_INPUT: ModelRequestInput = {
systemPrompt: 'You are a connectivity probe. Answer with the single word "pong".',
tools: [],
messages: [{ role: 'user', content: [{ type: 'text', text: 'ping' }], toolCalls: [] }],
};
const WEATHER_TOOL: Tool = {
name: 'get_weather',
description: 'Get the weather for a city.',
parameters: {
type: 'object',
properties: { location: { type: 'string' } },
required: ['location'],
},
};
const TOOL_INPUT: ModelRequestInput = { ...PING_INPUT, tools: [WEATHER_TOOL] };
/** A prior tool exchange: assistant tool call + its tool result. */
const TOOL_HISTORY_INPUT: ModelRequestInput = {
systemPrompt: PING_INPUT.systemPrompt,
tools: [WEATHER_TOOL],
messages: [
{ role: 'user', content: [{ type: 'text', text: 'weather?' }], toolCalls: [] },
{
role: 'assistant',
content: [],
toolCalls: [
{
type: 'function',
id: 'call_1',
name: 'get_weather',
arguments: '{"location":"Hangzhou"}',
},
],
},
{
role: 'tool',
toolCallId: 'call_1',
content: [{ type: 'text', text: 'sunny' }],
toolCalls: [],
},
],
};
interface Collected {
readonly events: readonly string[];
readonly text: string;
readonly toolCalls: readonly ToolCall[];
readonly usage?: unknown;
readonly finishReason?: string | undefined;
}
async function collect(
requester: ModelRequester,
signal?: AbortSignal,
input: ModelRequestInput = PING_INPUT,
): Promise<Collected> {
const events: string[] = [];
let text = '';
let toolCalls: readonly ToolCall[] = [];
let usage: unknown;
let finishReason: string | undefined;
for await (const event of requester.request(input, signal)) {
events.push(event.type === 'part' ? `part:${event.part.type}` : event.type);
if (event.type === 'part' && event.part.type === 'text') text += event.part.text;
if (event.type === 'usage') usage = event.usage;
if (event.type === 'finish') {
finishReason = event.providerFinishReason ?? event.rawFinishReason;
toolCalls = event.message.toolCalls;
}
}
return { events, text, toolCalls, usage, finishReason };
}
async function collectWithRecovery(requester: ModelRequester): Promise<Collected> {
const credentialProvider = requester.model.credentialProvider;
try {
return await collect(requester);
} catch (error) {
if (credentialProvider?.canRecover?.(error) !== true) throw error;
credentialProvider?.invalidate?.();
return collect(requester);
}
}
// --- stub server -----------------------------------------------------------
type StubHandler = (req: IncomingMessage, res: ServerResponse) => void;
const sseChunk = (delta: object, finishReason: string | null): string =>
JSON.stringify({
id: 'chatcmpl-probe',
object: 'chat.completion.chunk',
created: 0,
model: 'probe-model',
choices: [{ index: 0, delta, finish_reason: finishReason }],
});
const sseToolDelta = (toolCalls: readonly object[], finishReason: string | null = null): string =>
sseChunk({ tool_calls: toolCalls }, finishReason);
const SSE_USAGE = JSON.stringify({
id: 'chatcmpl-probe',
object: 'chat.completion.chunk',
created: 0,
model: 'probe-model',
choices: [],
usage: { prompt_tokens: 5, completion_tokens: 1, total_tokens: 6 },
});
function writeSse(res: ServerResponse, chunks: readonly string[]): void {
res.writeHead(200, { 'content-type': 'text/event-stream' });
for (const chunk of chunks) res.write(`data: ${chunk}\n\n`);
res.end('data: [DONE]\n\n');
}
function writePong(res: ServerResponse): void {
writeSse(res, [
sseChunk({ role: 'assistant' }, null),
sseChunk({ content: 'pong' }, null),
sseChunk({}, 'stop'),
SSE_USAGE,
]);
}
function writeJsonError(
res: ServerResponse,
status: number,
message: string,
headers: Record<string, string> = {},
): void {
res.writeHead(status, { 'content-type': 'application/json', ...headers });
res.end(JSON.stringify({ error: { message, type: 'stub_error' } }));
}
// --- probe bookkeeping ------------------------------------------------------
interface ProbeReport {
readonly probe: string;
readonly outcome: string;
readonly wrappedBy: string;
}
const reports: ProbeReport[] = [];
/** Walk the cause chain looking for a typed ChatProviderError. */
function chatProviderCause(error: unknown): ChatProviderError | undefined {
let current: unknown = error;
while (current !== undefined && current !== null) {
if (current instanceof ChatProviderError) return current;
current = (current as { cause?: unknown }).cause;
}
return undefined;
}
function describeCaught(error: unknown): { outcome: string; wrappedBy: string } {
if (isAbortError(error)) {
return {
outcome: `${error instanceof DOMException ? 'DOMException' : 'Error'} AbortError`,
wrappedBy: 'neither — abort passes both layers untranslated (by design)',
};
}
if (isError2(error)) {
const cause = chatProviderCause(error.cause);
return {
outcome: `Error2 ${error.code} (cause: ${cause?.name ?? (error.cause instanceof Error ? error.cause.name : 'none')})`,
wrappedBy:
cause !== undefined
? 'ChatProvider wrapped → ModelRequester translated'
: 'NOT wrapped by ChatProvider — raw error caught by the ModelRequester safety net',
};
}
if (error instanceof ChatProviderError) {
return { outcome: `raw ${error.name} (escaped translation!)`, wrappedBy: 'ChatProvider only' };
}
return {
outcome: `raw ${error instanceof Error ? error.name : typeof error} (escaped EVERYTHING)`,
wrappedBy: 'none',
};
}
function report(probe: string, outcome: string, wrappedBy: string): void {
reports.push({ probe, outcome, wrappedBy });
console.log(`[probe] ${probe.padEnd(30)} -> ${outcome}`);
}
// --- part 2 main ------------------------------------------------------------
async function probeBoundaries(): Promise<void> {
console.log('\n=== part 2: deterministic boundary probes (local stub) ===');
let handler: StubHandler = () => {
throw new Error('no handler set');
};
let requestCount = 0;
let lastAuth: string | null | undefined;
let lastRequestBody: unknown;
const server = createServer((req, res) => {
requestCount += 1;
lastAuth = req.headers.authorization;
// Drain (and capture) the request body before answering so the SDK never
// sees a reset — tool-call probes assert on the captured wire JSON.
const chunks: Buffer[] = [];
req.on('data', (chunk: Buffer) => chunks.push(chunk));
req.on('end', () => {
try {
lastRequestBody = JSON.parse(Buffer.concat(chunks).toString('utf8'));
} catch {
lastRequestBody = undefined;
}
handler(req, res);
});
});
// Read through functions so TS literal narrowing on the counters does not
// leak across probes (assert() guards narrow `number` to a literal).
const requests = (): number => requestCount;
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => resolve());
});
const port = (server.address() as AddressInfo).port;
const baseUrl = `http://127.0.0.1:${String(port)}`;
const registry = new ProtocolAdapterRegistry();
const makeRequester = (credentialProvider: LlmCredentialProvider, url = baseUrl): ModelRequester => {
const model: Model = {
id: 'probe',
name: 'probe-model',
aliases: [],
protocol: 'openai',
baseUrl: url,
headers: {},
capabilities: { ...UNKNOWN_CAPABILITY, max_context_tokens: 8192 },
maxContextSize: 8192,
alwaysThinking: false,
providerName: 'probe',
credentialProvider,
};
return new ModelRequesterImpl(model, registry);
};
const resetCounts = (): void => {
requestCount = 0;
lastAuth = undefined;
};
try {
// 1) happy path — the requester's event envelope on top of the raw stream.
resetCounts();
handler = (_req, res) => writePong(res);
const ok = await collect(makeRequester(createStaticCredentialProvider('sk-probe')));
assert(ok.text === 'pong', 'happy path assembles streamed text');
assert(ok.events.includes('usage'), 'happy path emits a usage event');
assert(ok.events.includes('finish'), 'happy path emits a finish event');
assert(ok.events.includes('timing'), 'happy path emits a timing event');
assert(lastAuth === 'Bearer sk-probe', 'requester injects per-request auth');
report('happy-path', `events=${ok.events.join('>')} text=${JSON.stringify(ok.text)}`, '—');
// 2) 401 with a static key: ChatProvider wraps to APIStatusError(401), the
// requester translates to provider.auth_error. No replay (no canRecover).
resetCounts();
handler = (_req, res) => writeJsonError(res, 401, 'invalid api key');
try {
await collect(makeRequester(createStaticCredentialProvider('sk-bad')));
throw new Error('expected a failure');
} catch (error) {
const { outcome, wrappedBy } = describeCaught(error);
assert(isError2(error) && error.code === 'provider.auth_error', 'static 401 -> provider.auth_error');
assert(chatProviderCause(error.cause) instanceof APIStatusError, 'static 401 wrapped as APIStatusError');
assert(requests() === 1, 'static 401 is NOT replayed');
report('auth-401-static-key', outcome, wrappedBy);
}
// 3) 401 with refreshable credentials: the call site invalidates and
// replays ONCE — recovery the requester deliberately does not own.
resetCounts();
handler = (req, res) => {
if (req.headers.authorization === 'Bearer sk-good') writePong(res);
else writeJsonError(res, 401, 'token expired');
};
let resolveCalls = 0;
const refreshable = createOAuthCredentialProvider((options) => {
resolveCalls += 1;
return Promise.resolve(options?.force === true ? 'sk-good' : 'sk-stale');
});
const replayed = await collectWithRecovery(makeRequester(refreshable));
assert(replayed.text === 'pong', 'refresh+replay succeeds');
assert(resolveCalls === 2, 'resolve called twice (normal + forced)');
assert(requests() === 2, 'exactly one replay after the 401');
report('auth-401-refresh-replay', `success after ${String(requestCount)} attempts`, 'caller-owned recovery (ModelRequester surfaces the 401)');
// 4) 401 that survives a forced refresh: the provider rejected the account
// — surfaced as provider.auth_error, not a re-login prompt.
resetCounts();
handler = (_req, res) => writeJsonError(res, 401, 'account disabled');
try {
await collectWithRecovery(makeRequester(refreshable));
throw new Error('expected a failure');
} catch (error) {
const { outcome, wrappedBy } = describeCaught(error);
assert(isError2(error) && error.code === 'provider.auth_error', 'post-refresh 401 -> provider.auth_error');
assert(requests() === 2, 'exactly one replay before surfacing');
report('auth-401-refresh-rejected', outcome, wrappedBy);
}
// 5) 429 with Retry-After: typed rate-limit error carrying the server backoff.
resetCounts();
handler = (_req, res) => writeJsonError(res, 429, 'too many requests', { 'retry-after': '2' });
try {
await collect(makeRequester(createStaticCredentialProvider('sk-probe')));
throw new Error('expected a failure');
} catch (error) {
const { outcome, wrappedBy } = describeCaught(error);
assert(isError2(error) && error.code === 'provider.rate_limit', '429 -> provider.rate_limit');
const cause = chatProviderCause(error.cause);
assert(cause instanceof APIStatusError && cause.retryAfterMs === 2000, 'retry-after parsed to ms');
report('rate-limit-429', outcome, wrappedBy);
}
// 6) 400 context overflow: routed to its own recovery-owned code.
resetCounts();
handler = (_req, res) =>
writeJsonError(res, 400, 'This model\'s maximum context length is 8192 tokens.');
try {
await collect(makeRequester(createStaticCredentialProvider('sk-probe')));
throw new Error('expected a failure');
} catch (error) {
const { outcome, wrappedBy } = describeCaught(error);
assert(isError2(error) && error.code === 'context.overflow', '400 overflow -> context.overflow');
assert(chatProviderCause(error.cause) instanceof APIContextOverflowError, 'overflow typed at the ChatProvider layer');
report('context-overflow-400', outcome, wrappedBy);
}
// 7) 500 with an HTML error page: status error with a sanitized message.
resetCounts();
handler = (_req, res) => {
res.writeHead(500, { 'content-type': 'text/html' });
res.end('<html><head><title>500 Internal Server Error</title></head><body>oops</body></html>');
};
try {
await collect(makeRequester(createStaticCredentialProvider('sk-probe')));
throw new Error('expected a failure');
} catch (error) {
const { outcome, wrappedBy } = describeCaught(error);
assert(isError2(error) && error.code === 'provider.api_error', '500 -> provider.api_error');
assert(!error.message.includes('<'), 'HTML body sanitized to its <title>');
report('server-500-html', outcome, wrappedBy);
}
// 8) connection refused: transport failure wrapped as APIConnectionError.
resetCounts();
const dead = createServer();
await new Promise<void>((resolve) => {
dead.listen(0, '127.0.0.1', () => resolve());
});
const deadPort = (dead.address() as AddressInfo).port;
await new Promise<void>((resolve) => {
dead.close(() => resolve());
});
handler = (_req, res) => writePong(res); // unused — nothing listens there
try {
await collect(makeRequester(createStaticCredentialProvider('sk-probe'), `http://127.0.0.1:${String(deadPort)}`));
throw new Error('expected a failure');
} catch (error) {
const { outcome, wrappedBy } = describeCaught(error);
assert(isError2(error) && error.code === 'provider.connection_error', 'refused -> provider.connection_error');
report('connection-refused', outcome, wrappedBy);
}
// 9) empty stream (immediate [DONE]): generate() throws APIEmptyResponseError.
resetCounts();
handler = (_req, res) => writeSse(res, []);
try {
await collect(makeRequester(createStaticCredentialProvider('sk-probe')));
throw new Error('expected a failure');
} catch (error) {
const { outcome, wrappedBy } = describeCaught(error);
assert(isError2(error) && error.code === 'provider.api_error', 'empty stream -> provider.api_error');
report('empty-stream-done', outcome, wrappedBy);
}
// 10) malformed SSE chunk: who catches a wire-format violation? Reported,
// not hard-coded — the CODE tells whether ChatProvider wrapped it.
resetCounts();
handler = (_req, res) => {
res.writeHead(200, { 'content-type': 'text/event-stream' });
res.end('data: {this is not json}\n\ndata: [DONE]\n\n');
};
try {
await collect(makeRequester(createStaticCredentialProvider('sk-probe')));
throw new Error('expected a failure');
} catch (error) {
const { outcome, wrappedBy } = describeCaught(error);
assert(isError2(error), 'malformed stream still surfaces as a coded Error2');
report('malformed-sse-chunk', outcome, wrappedBy);
}
// 11) stream cut mid-flight: one valid chunk, then the socket dies.
resetCounts();
handler = (_req, res) => {
res.writeHead(200, { 'content-type': 'text/event-stream' });
res.write(`data: ${sseChunk({ role: 'assistant' }, null)}\n\n`, () => {
res.socket?.destroy();
});
};
try {
await collect(makeRequester(createStaticCredentialProvider('sk-probe')));
throw new Error('expected a failure');
} catch (error) {
const { outcome, wrappedBy } = describeCaught(error);
assert(isError2(error), 'cut stream still surfaces as a coded Error2');
report('stream-cut-mid-flight', outcome, wrappedBy);
}
// --- tool-call boundary probes -------------------------------------------
// Response side: streamed `delta.tool_calls` fragments are buffered per
// index by the OpenAI base and routed into `message.toolCalls` by the
// contract's generate() driver — both BELOW the ModelRequester, which just
// forwards the parts. Request side: tool declarations and tool-result
// history are encoded by the ChatProvider.
// 12) tool call happy path: header chunk + fragmented arguments, and the
// outbound request carries the tool declaration.
resetCounts();
handler = (_req, res) =>
writeSse(res, [
sseToolDelta([
{
index: 0,
id: 'call_1',
type: 'function',
function: { name: 'get_weather', arguments: '' },
},
]),
sseToolDelta([{ index: 0, function: { arguments: '{"location":"' } }]),
sseToolDelta([{ index: 0, function: { arguments: 'Hangzhou"}' } }]),
sseToolDelta([], 'tool_calls'),
SSE_USAGE,
]);
const toolOk = await collect(makeRequester(createStaticCredentialProvider('sk-probe')), undefined, TOOL_INPUT);
const wireTools = (lastRequestBody as { tools?: { function?: { name?: string } }[] }).tools;
assert(
wireTools?.some((t) => t.function?.name === 'get_weather') === true,
'request encodes the tool declaration',
);
assert(toolOk.events.includes('part:function'), 'function header part streamed');
assert(toolOk.events.includes('part:tool_call_part'), 'argument fragments streamed');
assert(toolOk.toolCalls.length === 1, 'one assembled tool call');
assert(toolOk.toolCalls[0]?.name === 'get_weather', 'tool call name assembled');
assert(
toolOk.toolCalls[0]?.arguments === '{"location":"Hangzhou"}',
`fragmented arguments reassembled in order (got ${JSON.stringify(toolOk.toolCalls[0]?.arguments)})`,
);
assert(toolOk.finishReason === 'tool_calls', "finish reason 'tool_calls' normalized");
report(
'toolcall-happy',
`toolCalls=[${toolOk.toolCalls[0]?.name}(${toolOk.toolCalls[0]?.arguments ?? ''})] finish=${String(toolOk.finishReason)}`,
'ChatProvider (decode) + generate() (assembly), below the requester',
);
// 13) parallel tool calls with interleaved argument fragments: per-index
// buffering must keep the two calls apart.
resetCounts();
handler = (_req, res) =>
writeSse(res, [
sseToolDelta([
{ index: 0, id: 'call_a', type: 'function', function: { name: 'tool_a', arguments: '' } },
]),
sseToolDelta([
{ index: 1, id: 'call_b', type: 'function', function: { name: 'tool_b', arguments: '' } },
]),
sseToolDelta([{ index: 0, function: { arguments: '{"a":' } }]),
sseToolDelta([{ index: 1, function: { arguments: '{"b":' } }]),
sseToolDelta([{ index: 0, function: { arguments: '1}' } }]),
sseToolDelta([{ index: 1, function: { arguments: '2}' } }]),
sseToolDelta([], 'tool_calls'),
SSE_USAGE,
]);
const parallel = await collect(makeRequester(createStaticCredentialProvider('sk-probe')), undefined, TOOL_INPUT);
assert(parallel.toolCalls.length === 2, 'two parallel tool calls assembled');
assert(
parallel.toolCalls[0]?.name === 'tool_a' && parallel.toolCalls[0]?.arguments === '{"a":1}',
'index 0 arguments routed to tool_a',
);
assert(
parallel.toolCalls[1]?.name === 'tool_b' && parallel.toolCalls[1]?.arguments === '{"b":2}',
'index 1 arguments routed to tool_b',
);
report(
'toolcall-parallel-interleaved',
`toolCalls=[${parallel.toolCalls.map((t) => `${t.name}(${t.arguments ?? ''})`).join(', ')}]`,
'generate() index routing — interleaving stays separated',
);
// 14) malformed tool-call arguments: the wire layer NEVER parses the
// arguments string — invalid JSON sails through both layers and only fails
// later at tool dispatch. A boundary neither layer owns, by design.
resetCounts();
handler = (_req, res) =>
writeSse(res, [
sseToolDelta([
{
index: 0,
id: 'call_1',
type: 'function',
function: { name: 'get_weather', arguments: '' },
},
]),
sseToolDelta([{ index: 0, function: { arguments: '{not json' } }]),
sseToolDelta([], 'tool_calls'),
SSE_USAGE,
]);
const malformedArgs = await collect(makeRequester(createStaticCredentialProvider('sk-probe')), undefined, TOOL_INPUT);
assert(
malformedArgs.toolCalls[0]?.arguments === '{not json',
'malformed arguments pass through untouched',
);
report(
'toolcall-malformed-arguments',
`success, arguments=${JSON.stringify(malformedArgs.toolCalls[0]?.arguments)} preserved verbatim`,
'neither — arguments validity is deferred to tool dispatch (by design)',
);
// 15) index-less fragments: a single call without `index` still assembles
// (header + trailing merge in generate()). Caveat NOT probed: two
// interleaved index-less calls would silently cross-merge — the wire layer
// trusts the provider's indices and does not guard that.
resetCounts();
handler = (_req, res) =>
writeSse(res, [
sseToolDelta([
{ id: 'call_1', type: 'function', function: { name: 'get_weather', arguments: '' } },
]),
sseToolDelta([{ function: { arguments: '{"location":"HZ"}' } }]),
sseToolDelta([], 'tool_calls'),
SSE_USAGE,
]);
const indexless = await collect(makeRequester(createStaticCredentialProvider('sk-probe')), undefined, TOOL_INPUT);
assert(
indexless.toolCalls[0]?.arguments === '{"location":"HZ"}',
'index-less fragments merge into the pending call',
);
report(
'toolcall-indexless-fragments',
`success, arguments=${indexless.toolCalls[0]?.arguments ?? ''}`,
'generate() pending-part merge (indices trusted, not guarded)',
);
// 16) tool-exchange adjacency rejection: the strict-provider 400 must stay
// recognizable as `isToolExchangeAdjacencyError` THROUGH the ChatProvider
// wrap — the agent loop's strict-resend recovery keys on that predicate.
resetCounts();
handler = (_req, res) =>
writeJsonError(res, 400, 'tool_call_id "call_1" is not found');
try {
await collect(makeRequester(createStaticCredentialProvider('sk-probe')), undefined, TOOL_HISTORY_INPUT);
throw new Error('expected a failure');
} catch (error) {
const { outcome, wrappedBy } = describeCaught(error);
assert(isError2(error) && error.code === 'provider.api_error', 'adjacency 400 -> provider.api_error');
assert(
isToolExchangeAdjacencyError(chatProviderCause(error.cause)),
'adjacency classification survives the ChatProvider wrap',
);
report('toolcall-adjacency-400', outcome, wrappedBy);
}
// 17) request-side encoding of a tool exchange: assistant tool_calls and
// the tool result must hit the wire in the provider's shape.
resetCounts();
handler = (_req, res) => writePong(res);
await collect(makeRequester(createStaticCredentialProvider('sk-probe')), undefined, TOOL_HISTORY_INPUT);
const wireMessages = (lastRequestBody as { messages?: Record<string, unknown>[] }).messages;
assert(
wireMessages?.some(
(m) => m['role'] === 'assistant' && Array.isArray(m['tool_calls']),
) === true,
'assistant message carries wire tool_calls',
);
assert(
wireMessages?.some((m) => m['role'] === 'tool' && m['tool_call_id'] === 'call_1') === true,
'tool result encoded as role=tool with tool_call_id',
);
report(
'toolcall-request-encoding',
'assistant.tool_calls + role=tool/tool_call_id on the wire',
'ChatProvider (request-side encoding boundary)',
);
// 18) user cancellation: the one boundary BOTH layers pass through.
resetCounts();
handler = (_req, res) => {
res.writeHead(200, { 'content-type': 'text/event-stream' });
res.write(`data: ${sseChunk({ role: 'assistant' }, null)}\n\n`);
const timer = setInterval(() => {
res.write(`data: ${sseChunk({ content: '.' }, null)}\n\n`);
}, 25);
res.on('close', () => clearInterval(timer));
};
const ac = new AbortController();
try {
for await (const event of makeRequester(createStaticCredentialProvider('sk-probe')).request(PING_INPUT, ac.signal)) {
if (event.type === 'part') ac.abort();
}
throw new Error('expected an abort');
} catch (error) {
const { outcome, wrappedBy } = describeCaught(error);
assert(isAbortError(error), 'abort surfaces as the standard AbortError');
assert(!isError2(error), 'abort is NOT translated into an Error2');
assert(!(error instanceof ChatProviderError), 'abort is NOT a ChatProviderError');
report('abort-mid-stream', outcome, wrappedBy);
}
} finally {
server.closeAllConnections();
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
console.log('\n=== boundary matrix ===');
for (const r of reports) {
console.log(`${r.probe.padEnd(30)} ${r.outcome}`);
console.log(`${''.padEnd(30)} └ ${r.wrappedBy}`);
}
}
// ---------------------------------------------------------------------------
async function main(): Promise<void> {
if (process.env['KIMI_BOUNDARY_SKIP_LIVE'] !== '1') {
await probeRealConfig();
}
await probeBoundaries();
console.log('\nboundary: OK');
}
try {
await main();
} catch (error) {
console.error(error);
process.exit(1);
}