P1.5.2 Kimi K2 Adapter — Execution Packet
Round: R92, Phase 1.5, Wave 3 (parallel slice 1/3)
Base SHA: 89adef66
Branch: feature/p1-5-2-kimi-adapter
Worktree: E:\AMS\.worktrees\claude\p1-5-2-kimi-adapter
1. Scope summary
Ship src/domains/router/adapters/kimi.ts — a Kimi K2 HTTP wrapper with the same CompletionFn-compatible surface as the Phase 0 Claude adapter, plus 5–10 parity tests at src/__tests__/domains/router/adapters/kimi.test.ts.
src/domains/router/index.ts is NOT modified in this slice (parallel-T3 race override).
2. File-by-file plan
2.1 src/domains/router/adapters/kimi.ts — new (≈ 380 lines)
Module structure (mirrors src/domains/integrations/claude.ts):
| Section | Lines (est.) | Content |
|---|---|---|
| Header doc-comment | 1–40 | Phase 1.5 invariants, ADR-005 anchor, sibling-race scope override. |
| Imports | 42–48 | CompletionResult, AnthropicTool type-imports from ../../integrations/claude.js. |
| Constants | 50–60 | DEFAULT_BASE_URL, DEFAULT_MODEL, DEFAULT_MAX_TOKENS, MAX_RETRIES, BASE_DELAY_MS. |
| Error classes | 65–115 | KimiConfigError, KimiApiError. |
| Types | 120–155 | KimiCompletionOptions. Re-export AnthropicTool and CompletionResult as type. |
| Internal helpers | 160–280 | isRetryable, sleep, buildRequestBody, mapToolsToKimi, mapKimiToolCallsToAnthropic, mapFinishReason, parseResult. |
| Retry loop | 285–340 | attemptWithRetry. |
| Public API | 345–385 | createKimiCompletion, createKimiCompletionWithTools. |
Key differences from claude.ts:
- Endpoint path:
/chat/completions(not/messages). - Headers:
Authorization: Bearer <key>(notx-api-key); noanthropic-version. - Body shape:
system→ firstmessages[0]withrole: 'system'.tools→ OpenAIfunctionshape permapToolsToKimi.
- Response parse:
choices[0].message.contenttext →result.content(string).choices[0].message.tool_calls[]→ Anthropic-shapetool_useblocks →JSON.stringify(blocks)becomesresult.content.choices[0].finish_reason→mapFinishReason→result.stopReason.usage.prompt_tokens→result.promptTokens.usage.completion_tokens→result.completionTokens.
2.2 src/__tests__/domains/router/adapters/kimi.test.ts — new (≈ 450 lines)
7 parity tests (well within the 5–10 range):
| # | Test name | Asserts |
|---|---|---|
| 1 | happy path: returns CompletionResult with correct fields |
result.content === 'Hello!', fields present, latencyMs ≥ 0. |
| 2 | token accounting: prompt + completion tokens |
result.promptTokens === 10, result.completionTokens === 5. |
| 3 | error: 401 from Kimi → KimiApiError with status 401 |
Throws KimiApiError, err.status === 401. |
| 4 | config: missing COLIBRI_KIMI_API_KEY → KimiConfigError |
Throws KimiConfigError. |
| 5 | tool-use response → Anthropic-shape JSON-stringified content |
JSON.parse(result.content) is [{type:'tool_use', id, name, input}]. |
| 6 | injection seam: fetchFn override is invoked |
Mock fetchFn called with POST + Kimi URL. |
| 7 | latency measurement: 50ms delay → latencyMs >= 50 |
result.latencyMs >= 50. |
Plus 4 additional parity tests for full coverage:
| # | Test name | Asserts |
|---|---|---|
| 8 | sends POST to Kimi base URL chat/completions endpoint |
URL is https://api.moonshot.ai/v1/chat/completions. |
| 9 | Authorization: Bearer header includes API key |
Header set; no x-api-key. |
| 10 | tools array maps AnthropicTool to Kimi function shape |
body.tools[0].type === 'function', .function.parameters === input_schema. |
| 11 | 429 retryable → exponential backoff up to MAX_RETRIES |
delayFn called with [100, 200, 400]; final throw is KimiApiError code === 'KIMI_RETRIES_EXHAUSTED'. |
| 12 | system prompt prepended as messages[0] with role=system |
body.messages[0] is {role:'system', content:<prompt>}. |
| 13 | empty tools array does not send tools key |
'tools' in body === false. |
| 14 | tool args JSON.parse failure → _parse_error wrapper |
input._parse_error set, no throw. |
| 15 | finish_reason 'tool_calls' maps to 'tool_use' |
result.stopReason === 'tool_use'. |
Total: 15 tests (we’ll keep the count comfortably ≥ 5).
Test fixture helpers re-used from the Claude test file pattern (NOT imported — defined locally):
makeMockFetch(responses)makeSilentLogger()makeInstantDelay()baseOptions(overrides)
3. Implementation order
- Skeleton: write the imports, constants, error classes, and
KimiCompletionOptionsinterface. - Tool mapping helpers:
mapToolsToKimi,mapKimiToolCallsToAnthropic,mapFinishReason. Pure functions. Easy to unit-test. parseResult: assemblesCompletionResultfrom a Kimi JSON response. Handles both text-content and tool_calls.buildRequestBody: assembles the JSON body for the POST.attemptWithRetry: the retry loop. Mirrorsclaude.ts:attemptWithRetrystructurally.createKimiCompletion+createKimiCompletionWithTools: public entry points. Validate key, callattemptWithRetry.- Tests: write the 15 parity tests, run the build/lint/test gate iteratively until green.
4. Pure-function spec for tool mappers
mapToolsToKimi(tools: AnthropicTool[]): KimiTool[]
return tools.map((t) => ({
type: 'function' as const,
function: {
name: t.name,
description: t.description,
parameters: t.input_schema,
},
}));
mapKimiToolCallsToAnthropic(toolCalls: KimiToolCall[]): AnthropicToolUseBlock[]
return toolCalls.map((c) => {
let input: Record<string, unknown>;
try {
input = JSON.parse(c.function.arguments) as Record<string, unknown>;
} catch (err) {
input = {
_parse_error: err instanceof Error ? err.message : String(err),
_raw: c.function.arguments,
};
}
return {
type: 'tool_use' as const,
id: c.id,
name: c.function.name,
input,
};
});
mapFinishReason(kimi: string | undefined): string
switch (kimi) {
case 'stop': return 'end_turn';
case 'tool_calls': return 'tool_use';
case 'length': return 'max_tokens';
case 'content_filter': return 'refusal';
case undefined: return 'unknown';
default: return kimi;
}
5. Boilerplate gates
npm run build— TypeScript strict compile must pass.npm run lint— ESLint passes.npm test— All tests pass. Expected: baseline 3153 (or current main count) + 15 new tests = ~3168. Zero regression.
6. Risk register
| Risk | Mitigation |
|---|---|
| Test fixtures drift from Claude adapter’s pattern. | Mirror function-by-function; same helper names. |
| Tool-args parse-fail crashes tests. | Wrap in try/catch + _parse_error envelope per I12. |
| Module-load throw on missing env. | Use process.env[...] at call-time only. Module top-level has zero process.env reads. |
src/domains/router/index.ts accidentally modified. |
Explicit override: do NOT touch. Pre-commit visually verify. |
| Logger writes to stdout. | Default to console.error; tests assert via injected logger. |
fetch global not available. |
Node 20+ baseline (CLAUDE.md §1 stack guardrail). |
| Retry exhaustion uses wrong literal. | Use 'KIMI_RETRIES_EXHAUSTED' per contract §4. |
| Tool descriptor mapping field-rename mistake. | parameters: t.input_schema is the only field rename. |
Authorization header malformed. |
Always 'Bearer ' + key. |
7. Out of scope
- MCP tool registration (P1.5.7).
- Fallback chain logic update (P1.5.5).
- Scoring algorithm changes (P1.5.1 — already shipped).
src/domains/router/index.tsbarrel re-export (fold-in commit between W3 and W4).src/config.tsaddingCOLIBRI_KIMI_*schemas (Phase 1.5 follow-up).- Live API calls (tests always use
fetchFnmocks). - Codex / OpenAI adapters (sibling parallel slices).
8. Exit criteria
- Implementation order specified (§3).
- Pure-function mapper specs drafted (§4).
- Gates listed (§5).
- Risks registered (§6).
- Out-of-scope explicit (§7).
Next: Step 4 (implementation).