P1.5.2 Kimi K2 Adapter — Behavioral Contract

Round: R92, Phase 1.5, Wave 3 (parallel slice 1/3) Base SHA: 89adef66

1. Public surface

Module: src/domains/router/adapters/kimi.ts.

export class KimiConfigError extends Error {
  readonly code: 'KIMI_CONFIG_ERROR';
  readonly name: 'KimiConfigError';
}

export class KimiApiError extends Error {
  readonly code: 'KIMI_API_ERROR' | 'KIMI_RETRIES_EXHAUSTED';
  readonly status: number | undefined;
  readonly name: 'KimiApiError';
}

export interface KimiCompletionOptions {
  readonly model?: string;
  readonly maxTokens?: number;
  readonly systemPrompt?: string;
  readonly fetchFn?: typeof fetch;
  readonly logger?: (...args: unknown[]) => void;
  readonly delayFn?: (ms: number) => Promise<void>;
  readonly apiKey?: string;
  readonly baseUrl?: string;
}

export function createKimiCompletion(
  prompt: string,
  options?: KimiCompletionOptions,
): Promise<CompletionResult>;

export function createKimiCompletionWithTools(
  prompt: string,
  tools: AnthropicTool[],
  options?: KimiCompletionOptions,
): Promise<CompletionResult>;

Both return shapes (CompletionResult, AnthropicTool) are imported from ../../integrations/claude.js via type-only imports to guarantee bit-identical interface parity.

2. Behavioral invariants

ID Invariant
I1 Surface parity. createKimiCompletion returns the same CompletionResult shape (content, model, promptTokens, completionTokens, latencyMs, stopReason) as createCompletion from claude.ts.
I2 Call-time env validation. Module load never throws on missing env. createKimiCompletion / createKimiCompletionWithTools throw KimiConfigError when no key is present.
I3 Key resolution order. options.apiKey ?? process.env['COLIBRI_KIMI_API_KEY'].
I4 Base URL resolution order. options.baseUrl ?? process.env['COLIBRI_KIMI_BASE_URL'] ?? 'https://api.moonshot.ai/v1'.
I5 Default model. When options.model is absent, default to 'kimi-k2-0905-preview'. Single string constant in the module.
I6 Default max_tokens. When options.maxTokens is absent, default to 1024 (parity with Claude adapter).
I7 Retry policy parity. MAX_RETRIES = 3, BASE_DELAY_MS = 100, doubling. 429 + 5xx retryable; everything else terminal.
I8 Network error wrap. A thrown fetchFn error becomes a KimiApiError with status: undefined, code: 'KIMI_API_ERROR', NOT retried.
I9 Logging shape. logger('[kimi] model=<m> prompt_tokens=<p> completion_tokens=<c> latency_ms=<l>') on success. Defaults to console.error so stdout (owned by StdioServerTransport) is never touched.
I10 Empty tools array → degrade. createKimiCompletionWithTools(prompt, [], options) MUST NOT send a tools key in the request body. (Parity with claude.ts:183.)
I11 Tool-use response → Anthropic-shape content. When Kimi returns tool_calls, the adapter must map to Anthropic-shape [{type:'tool_use', id, name, input}] and return JSON.stringify(blocks) as content. (Parity with claude.ts:213.)
I12 Tool args parse-fail tolerance. A tool_call with non-JSON argumentsinput: { _parse_error: <msg>, _raw: <string> }. NEVER throws.
I13 finish_reason normalization. Map: stop→end_turn, tool_calls→tool_use, length→max_tokens, content_filter→refusal; pass-through for unknowns.
I14 Injection seam parity. fetchFn, logger, delayFn are all pluggable via options. Tests inject mocks; production uses fetch / console.error / a setTimeout-based sleep.
I15 No barrel export in this slice. src/domains/router/index.ts is NOT touched. Re-export coordination lands in a fold-in commit between Wave 3 and Wave 4.
I16 No MCP tool registration. Library-only. Phase 1.5 router_call lands in P1.5.7.
I17 Authorization header. Authorization: Bearer <key> — NOT x-api-key.
I18 System prompt placement. Kimi/OpenAI shape: prepend {role:'system', content: systemPrompt} to messages, NOT a top-level system field.
I19 Tool descriptor mapping (request). AnthropicTool {name, description, input_schema}{type:'function', function:{name, description, parameters: input_schema}}.
I20 No mutation of inputs. tools[], options, prompt are read-only — adapter does not mutate caller-supplied state.

3. Tool-use mapping (binding contract)

Request side — tools translation

For each AnthropicTool input:

// Input  (AnthropicTool)
{ name: 'get_weather', description: 'Get weather', input_schema: {...} }

// Output (KimiTool)
{ type: 'function', function: { name: 'get_weather', description: 'Get weather', parameters: {...} } }

The input_schema JSON schema object is mapped directly to parameters — no transformation, no validation, no field renames. (Both Anthropic and OpenAI use vanilla JSON Schema for tool parameter declarations.)

Response side — tool_calls translation

For each choices[0].message.tool_calls[i] from Kimi:

// Input  (Kimi tool_call)
{ id: 'call_001', type: 'function', function: { name: 'get_weather', arguments: '{"location":"London"}' } }

// Output (Anthropic-shape tool_use block)
{ type: 'tool_use', id: 'call_001', name: 'get_weather', input: { location: 'London' } }

arguments is a JSON-string in Kimi; the adapter parses it. On JSON.parse failure:

input: { _parse_error: '<error.message>', _raw: '<original arguments string>' }

This keeps the response well-formed for downstream consumers while preserving the raw payload for diagnostics. Per I12, this never throws.

When tool_calls are present AND choices[0].message.content is non-empty, the text content is appended as a leading {type:'text', text} block before the tool_use blocks. Matches Claude’s hybrid responses (rare, but supported).

content field of returned CompletionResult

  • Pure text response (no tool_calls): result.content === <text string>.
  • Tool-use response (tool_calls present): result.content === JSON.stringify(toolUseBlocks), where toolUseBlocks is the Anthropic-shape array. (Parity with claude.ts:213.)

4. Error class hierarchy

Class code Trigger
KimiConfigError 'KIMI_CONFIG_ERROR' COLIBRI_KIMI_API_KEY absent and no options.apiKey.
KimiApiError 'KIMI_API_ERROR' Terminal HTTP error (non-retryable status), network failure.
KimiApiError 'KIMI_RETRIES_EXHAUSTED' All 3 retries exhausted on retryable status.

Each subclass:

  • Extends Error.
  • Sets this.name to the class name.
  • Sets this.code as a readonly literal.
  • For KimiApiError, sets this.status as number | undefined.

Shape parity with AnthropicConfigError / AnthropicApiError. Callers can write generic if (err instanceof Error && 'code' in err) checks.

5. Env-var contract

Var Type Default Validation
COLIBRI_KIMI_API_KEY string (none) Call-time. Absent → KimiConfigError.
COLIBRI_KIMI_BASE_URL string 'https://api.moonshot.ai/v1' Call-time. Empty string treated as unset (falls back to default).

The two vars are not declared in src/config.ts because:

  1. The Phase 0 boot-time schema is frozen for the 14-tool surface; new envs go through ADR-006 graduation.
  2. The Claude adapter precedent (ANTHROPIC_API_KEY) reads process.env directly at call-time.
  3. This slice is library-only — no MCP tool reads these vars yet.

Direct process.env[...] access at call-time is the documented Phase 1.5 pattern.

6. Retry policy

attempt 1 → 0ms before request
on 429 or 5xx → sleep(100ms), attempt 2
on 429 or 5xx → sleep(200ms), attempt 3
on 429 or 5xx → sleep(400ms), attempt 4
on 429 or 5xx after attempt 4 → KimiApiError(code='KIMI_RETRIES_EXHAUSTED')

MAX_RETRIES = 3 means up to 4 attempts total (initial + 3 retries) — same constant semantics as the Claude adapter (where attempt >= MAX_RETRIES triggers exhaustion AFTER the 4th attempt has failed).

Network errors are NOT retried — they signal a config / DNS / firewall problem, not a transient server load issue.

7. Logging contract

On success (after parseResult):

[kimi] model=<resolved_model> prompt_tokens=<p> completion_tokens=<c> latency_ms=<n>
  • resolved_model is the model field from the API response, or 'unknown' if absent.
  • latency_ms is Date.now() - startMs measured around the fetch call.
  • Default destination: console.error (stderr-only, never process.stdout).
  • Tests pass a silent logger that captures lines into a string array.

8. Determinism

The adapter is deterministic with respect to inputs UP TO the wall-clock-derived latencyMs field. With a stable fetchFn mock and a stable delayFn, the same prompt/options always produces the same CompletionResult (modulo latencyMs).

Test pattern (matches claude.ts test fixtures):

  • makeMockFetch(responses) — deterministic response sequence.
  • makeSilentLogger() — captures log lines.
  • makeInstantDelay() — records sleep durations without actually sleeping.

9. Acceptance criteria (echoed from task prompt)

  • createKimiCompletion(prompt, options) returns Promise<CompletionResult>.
  • createKimiCompletionWithTools(prompt, tools, options) returns Promise<CompletionResult>.
  • Reads COLIBRI_KIMI_API_KEY env. Missing → KimiConfigError.
  • Reads COLIBRI_KIMI_BASE_URL env. Default https://api.moonshot.ai/v1.
  • Tool-use response shape: Anthropic-SDK compatible ({type:'tool_use', id, name, input}).
  • Injection seams: fetchFn, logger, delayFn.
  • KimiApiError shape parity with AnthropicApiError.
  • 5–10 parity tests pass.
  • No MCP tool registration.
  • No src/domains/router/index.ts mutation (parallel-T3 race).
  • npm run build && npm run lint && npm test green.

10. Exit criteria

  • Public surface declared (§1).
  • Behavioral invariants enumerated (§2).
  • Tool-use mapping bound (§3).
  • Error hierarchy specified (§4).
  • Env vars contracted (§5).
  • Retry semantics specified (§6).
  • Logging contract specified (§7).
  • Determinism specified (§8).
  • Acceptance criteria listed (§9).

Next: Step 3 (packet — execution plan).


Back to top

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

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