Featured image of post How kimi-code Handles kimi-k2.6: A Comparison with OpenCode

How kimi-code Handles kimi-k2.6: A Comparison with OpenCode

Recently, kimi-code migrated from Python to TypeScript. Here's a quick analysis.

语速

Recently, kimi-code migrated from Python to TypeScript. Here’s a quick analysis.

Based on my review of the kimi-code source code (particularly packages/kosong/src/providers/kimi.ts, kimi-schema.ts, kimi-files.ts, etc.) and relevant OpenCode compatibility issues, here are the kimi-k2.6-specific optimizations in kimi-code and how they differ from OpenCode.

1. Native Kimi Provider (Not a Generic OpenAI-compatible Layer)

kimi-code does not treat Kimi as “just another OpenAI-compatible endpoint.” Instead, it implements a dedicated kimi provider type:

Featurekimi-codeOpenCode
Provider TypeDedicated 'kimi' type with independent adapterAccessed via generic OpenAI/Anthropic bridge
Proprietary FieldsNative handling of reasoning_content, thinking, generationKwargsreasoning_content often lost in the bridge layer
Auth HeadersSupports kimiRequestHeaders, X-Msh-Tool-Call-Id, and other Moonshot-specific headersGeneric header forwarding

2. Full Lifecycle Handling of reasoning_content

kimi-k2.6 has thinking enabled by default and requires reasoning_content to be preserved across multi-turn conversation history. Otherwise, tool calls will result in a 400 error.

How kimi-code handles it:

  • convertMessage: Extracts internal think content parts and serializes them into the reasoning_content field, ensuring thinking content is never lost in message history
  • Streaming Parser: Explicitly extracts delta.reasoning_content / message.reasoning_content in both _convertStreamResponse and _convertNonStreamResponse
  • TUI Rendering: A dedicated ThinkingComponent renders thinking content in real time, with expand/collapse support and a spinner animation

OpenCode’s Problem:

The OpenCode Go bridge drops reasoning_content on the second turn, causing the Moonshot API to return:

1
thinking is enabled but reasoning_content is missing in assistant tool call message

3. JSON Schema Normalization (kimi-schema.ts)

Moonshot’s tool parameter validator has strict and unique requirements for JSON Schema. This is one of the primary sources of incompatibility between OpenCode and kimi-k2.6.

What kimi-code’s normalizeKimiToolSchema does:

  • Dereferences $ref: Inlines definitions from $defs / definitions, eliminating external references
  • Fills in missing type: The Kimi validator rejects nested property schemas that omit type (e.g., MCP-generated enum-only schemas). kimi-code infers and backfills type: string/object/array, etc.
  • Circular reference detection: Preserves the original $ref when a circular reference is detected, avoiding infinite recursion

OpenCode’s Problem:

Generated schemas use #/definitions/ instead of the #/$defs/ format required by Moonshot, and lack schema type inference and backfilling for Kimi, causing complex tool calls to fail with 400.

4. Native Thinking Mode Configuration System

kimi-code has built-in support for Kimi’s thinking mode from the configuration layer all the way to the UI:

  • Config Parsing: ThinkingConfigSchema supports mode: auto/on/off and effort: low/medium/high/xhigh/max

  • Model Capability Tags: ModelAlias supports capabilities: ['thinking', 'always_thinking']

  • Model Selector UI: Press ←→ to toggle thinking on/off; always-on models cannot be turned off

  • Provider Method: withThinking(effort) correctly generates:

    1
    2
    3
    4
    
    {
      "reasoning_effort": "high",
      "extra_body": { "thinking": { "type": "enabled" } }
    }
    
  • Token Budget: Automatically normalizes legacy max_tokens to Kimi’s preferred max_completion_tokens

OpenCode’s Problem:

When using the Anthropic bridge, it hardcodes thinking content blocks, but the Kimi API only supports text/image_url/video_url/video, resulting in:

1
Invalid value: thinking. Supported values are: 'text','image_url','video_url' and 'video'.

5. Native Moonshot Service Integration

kimi-code includes Moonshot-exclusive services instead of relying on generic local implementations:

  • MoonshotFetchURLProvider: Prioritizes Moonshot’s coding-fetch service (with built-in page text extraction), falling back to local fetch only on failure
  • MoonshotWebSearchProvider: Calls the Moonshot search API directly, supporting enable_page_crawling
  • KimiFiles: Uploads videos to the Moonshot file service, returning video_url in the ms://<file-id> format

6. Tool Call Layer Details

  • Built-in Functions: Tool names starting with $ are recognized as Kimi builtin functions and serialized as type: 'builtin_function'
  • Usage Extraction: Supports Moonshot’s proprietary choices[0].usage placement, as well as cached_tokens and other fields
  • Finish Reason Mapping: Maps OpenAI-style stop/tool_calls/length values to an internal unified enum

7. CLI Core and LLM SDK Architectural Isolation

This is an easily overlooked but important architectural difference.

The core CLI of kimi-code (apps/kimi-code) does not directly depend on any OpenAI or Anthropic TypeScript SDK. Looking at its package.json, the core dependencies are only generic libraries like TUI rendering (pi-tui), CLI parsing (commander), and syntax highlighting (cli-highlight). All LLM provider interactions are isolated within the self-developed kosong package.

While packages/kosong internally uses openai and @anthropic-ai/sdk as implementation details (since the Kimi API is OpenAI-compatible), it exposes a unified LLM abstraction interface to the outside. The CLI core only depends on kosong and has no awareness of underlying vendor SDKs.

OpenCode is different. Its packages/opencode core package directly depends on a large number of vendor SDKs:

  • @ai-sdk/openai
  • @ai-sdk/anthropic
  • @ai-sdk/google
  • @ai-sdk/azure
  • @openrouter/ai-sdk-provider
  • … (more than a dozen provider-specific packages in total)

This means OpenCode’s core code is deeply coupled with each vendor’s SDK, while kimi-code’s core CLI stays clean, with all model interactions fully isolated through a self-developed abstraction layer.

8. What Commit History Reveals About Evolution Paths

The structural code differences above are just a static snapshot. What’s more interesting is comparing the commit histories of the two projects—their dynamic evolution directions are completely different.

kimi-code: Native Design, Continuously Reducing Configuration Burden

842e699 — “Kimi For Coding” (Initial Commit)

This was the starting point of the entire project. The initial code already included:

  • packages/kosong/src/providers/kimi.ts: Dedicated Kimi provider
  • packages/kosong/src/providers/kimi-schema.ts: Dedicated JSON Schema normalizer
  • packages/kosong/src/providers/kimi-files.ts: Dedicated file upload service

Conclusion: kimi-code treated the Kimi API as a first-class citizen from day one, not as a later patch.


d95b013 fix(catalog): preserve reasoning fields in custom model (#70)

This commit fixed a very subtle issue. models.dev uses the interleaved field to mark reasoning support, but early code treated interleaved=true as undefined, causing models selected via /connect to silently lose their reasoning capability.

Fixes:

  • interleaved=true is mapped to the default reasoning_content
  • interleaved is added to the update-catalog.mjs allowlist; otherwise the offline catalog in release builds would silently drop the field again

61f7d0e fix(kosong): make openai-compatible thinking work without reasoning_key (#78)

This is the core commit for reasoning handling, showcasing kimi-code’s deep thinking on compatibility. The diff reveals a three-layer design:

  1. Inbound Auto-Scan (response parsing)

    1
    2
    
    const KNOWN_REASONING_KEYS = ['reasoning_content', 'reasoning_details', 'reasoning'] as const;
    // Auto-scan three fields; first string value wins
    
  2. Outbound Default Write-Back (request serialization)

    1
    2
    
    const DEFAULT_OUTBOUND_REASONING_KEY = KNOWN_REASONING_KEYS[0]; // 'reasoning_content'
    // Defaults to writing back as reasoning_content, no user config needed
    
  3. Auto-Inject reasoning_effort (historical continuity)

    1
    2
    
    // When history contains ThinkPart but caller hasn't explicitly set reasoning_effort,
    // auto-inject 'medium' to prevent strict gateways like One API / DeepSeek from returning 400
    

Edge cases are handled meticulously: blank reasoning_key ("") is normalized to undefined; values explicitly set by the caller via withGenerationKwargs are not silently overwritten by auto-injection.

The verification goal explicitly states:

Manually verified end-to-end against the real DeepSeek API with a hand-written config.toml that does not set reasoning_key: thinking content renders, no 400, multi-turn conversations work.


OpenCode: Generic Layer Design, OpenAI-centric

eb84f46 fix(llm): split OpenAI reasoning summary blocks (#29000)

This commit demonstrates OpenCode’s completely different approach to reasoning—designed around the OpenAI Responses API:

  • Maintains a state machine for encrypted_content and item_reference
  • Folds multiple summary parts by item_id + summary_index
  • When store:false, filters out reasoning items lacking encrypted_content

This is completely different from Kimi’s reasoning_content mechanism. Kimi does not need encrypted_content or item_reference; it simply attaches a reasoning_content field to the message.


A Hard Fact

  • OpenCode Issue #26331 “Bug: OpenCode Go bridge layer incompatible with kimi-k2.6 tool calls” — Status: still open
  • OpenCode Issue #27054 “KIMI K2.6 showing error in Opencode GO” — Status: closed, but the resolution was to disable MCP (a workaround)

The last comment on #27054:

The workaround is to disable your MCP and then initiate the session

That’s not a fix. That’s avoiding the problem.


Commit History Comparison Summary

Dimensionkimi-codeOpenCode
Initial DesignInitial commit includes full Kimi provider + schema normalizer + file serviceGeneric multi-model architecture, adapted later via bridge
Reasoning MechanismDesigned around reasoning_content field, with auto-scan / write-back / effort injectionDesigned around OpenAI Responses’ encrypted_content + item_reference
Schema HandlingDedicated normalizeKimiToolSchema, dereferences $ref + backfills typeGeneric schema validation, focused on friendly error messages
Config PhilosophyMakes OpenAI-compatible gateways “zero-config” by auto-inferring all fieldsRelies on users manually adapting via bridge/config
Issue StatusContinuously shipping reasoning-related patches (#70, #78)kimi-k2.6 compatibility issue #26331 still open

Summary: Core Differences

Dimensionkimi-codeOpenCode
Architecture PositioningNative design for Kimi/Moonshot, dedicated providerGeneric multi-model agent, adapted via bridge
Thinking/ReasoningNative support, full lifecycle preservation of reasoning_contentEasily lost in bridge layer, causing 400 errors
JSON SchemaDedicated normalizeKimiToolSchema for dereferencing and type backfillingGeneric schema generation, does not meet Kimi validator requirements
API FormatDirectly generates Moonshot-native format (including thinking config, $defs normalization, etc.)Transformed through OpenAI/Anthropic protocol conversion, causing format mismatches
Service IntegrationBuilt-in Moonshot fetch/search/file servicesUses generic local tools
Core DependenciesCLI core does not directly depend on vendor SDKs; isolated via self-developed kosong packageCore package directly coupled with @ai-sdk/openai and more than a dozen other vendor SDKs

Looking at commit history, kimi-code’s evolution is directed at continuously eliminating user configuration burden (reasoning_key went from required → optional override → auto-inferred; interleaved went from filtered → correctly mapped), while OpenCode’s evolution is directed at deepening OpenAI ecosystem integration (Responses API, encrypted reasoning, item reference), leaving Kimi adaptation stuck at the generic bridge layer.

That’s the truth at the commit level: one is native evolution, the other is a bridge gap.