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> (not x-api-key); no anthropic-version.
  • Body shape:
    • system → first messages[0] with role: 'system'.
    • tools → OpenAI function shape per mapToolsToKimi.
  • Response parse:
    • choices[0].message.content text → result.content (string).
    • choices[0].message.tool_calls[] → Anthropic-shape tool_use blocks → JSON.stringify(blocks) becomes result.content.
    • choices[0].finish_reasonmapFinishReasonresult.stopReason.
    • usage.prompt_tokensresult.promptTokens.
    • usage.completion_tokensresult.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

  1. Skeleton: write the imports, constants, error classes, and KimiCompletionOptions interface.
  2. Tool mapping helpers: mapToolsToKimi, mapKimiToolCallsToAnthropic, mapFinishReason. Pure functions. Easy to unit-test.
  3. parseResult: assembles CompletionResult from a Kimi JSON response. Handles both text-content and tool_calls.
  4. buildRequestBody: assembles the JSON body for the POST.
  5. attemptWithRetry: the retry loop. Mirrors claude.ts:attemptWithRetry structurally.
  6. createKimiCompletion + createKimiCompletionWithTools: public entry points. Validate key, call attemptWithRetry.
  7. 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.ts barrel re-export (fold-in commit between W3 and W4).
  • src/config.ts adding COLIBRI_KIMI_* schemas (Phase 1.5 follow-up).
  • Live API calls (tests always use fetchFn mocks).
  • 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).


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.