Skip to content
Open
13 changes: 11 additions & 2 deletions docs/providers/kimicode.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@ keywords: ["Kimi Code", "Moonshot", "quota", "provider setup"]

Kimi Code is an OpenAI-compatible coding assistant served at `https://api.kimi.com/coding/v1`.
GoModel routes chat, model listing, embeddings, and passthrough requests through the shared
OpenAI adapter. The `/v1/responses` endpoint is translated through chat completions, while
files and batches are not supported by the upstream endpoint.
OpenAI adapter. The `/v1/responses` endpoint is forwarded natively to the upstream Responses
API instead of being translated through chat completions, while files and batches are not
supported by the upstream endpoint.

Kimi Code retains no responses. Requests with `store: true` are rewritten to `store: false`
(the upstream rejects `store: true` with a 400). Chaining works only through GoModel: with a
response store configured, the gateway expands a `previous_response_id` chain by replaying the
stored history into the request before dispatch, and a `conversation` reference resolves
through the conversation store the same way. Without those stores, a request carrying
`previous_response_id` or `conversation` is rejected with an invalid-request error, because
the upstream can never resolve the referenced state.

## Configure

Expand Down
87 changes: 82 additions & 5 deletions internal/providers/kimicode/kimicode.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
// Package kimicode provides Kimi Code API integration for the LLM gateway.
//
// The "kimicode" provider routes to Kimi Code's OpenAI-compatible chat
// completions endpoint, so all transport goes through the shared chat-centric
// adapter and model IDs are forwarded unchanged.
// The "kimicode" provider routes to Kimi Code's OpenAI-compatible API: chat
// completions, model listing, embeddings, and passthrough go through the
// shared chat-centric adapter, while the Responses API is served natively by
// the upstream /responses endpoint. Kimi Code retains no responses, so
// store=true is pinned to false.
package kimicode

import (
"context"
"io"
"strings"

"github.com/enterpilot/gomodel/internal/core"
"github.com/enterpilot/gomodel/internal/providers"
"github.com/enterpilot/gomodel/internal/providers/openai"
Expand All @@ -23,9 +29,11 @@ var Registration = providers.Registration{
}

// Provider implements the core.Provider interface for Kimi Code. Kimi Code is
// OpenAI-compatible, so all transport goes through the shared chat-centric
// OpenAI-compatible, so most transport goes through the shared chat-centric
// adapter: chat completions, model listing, embeddings, and passthrough are
// exposed via the embedded *openai.ChatCompatible.
// exposed via the embedded *openai.ChatCompatible. The Responses API is
// forwarded natively to the upstream /responses endpoint through the same
// adapter instance.
type Provider struct {
*openai.ChatCompatible
}
Expand All @@ -39,3 +47,72 @@ func New(cfg providers.ProviderConfig, opts providers.ProviderOptions) core.Prov
BaseURL: providers.ResolveBaseURL(cfg.BaseURL, defaultBaseURL),
})}
}

// Responses serves the Responses API natively through the upstream /responses
// endpoint. Kimi Code retains no responses, so a non-empty
// previous_response_id is rejected before any upstream call (see
// rejectPreviousResponseID); store=true is pinned to false by
// adaptResponsesRequest.
func (p *Provider) Responses(ctx context.Context, req *core.ResponsesRequest) (*core.ResponsesResponse, error) {
if err := rejectPreviousResponseID(req); err != nil {
return nil, err
}
return p.Compatible().Responses(ctx, adaptResponsesRequest(req))
}

// StreamResponses forwards the request to the upstream /responses endpoint
// with stream enabled, returning its Responses SSE stream. Like Responses, it
// rejects a non-empty previous_response_id before any upstream call.
func (p *Provider) StreamResponses(ctx context.Context, req *core.ResponsesRequest) (io.ReadCloser, error) {
if err := rejectPreviousResponseID(req); err != nil {
return nil, err
}
return p.Compatible().StreamResponses(ctx, adaptResponsesRequest(req))
}

// rejectPreviousResponseID fails requests chaining from earlier state:
// Kimi Code cannot resolve a previous response ID or a gateway-local
// conversation upstream, and answering statelessly would silently drop the
// conversation context the caller expects. The rejection only fires when the
// gateway has no store to expand the chain with; requests whose state the
// gateway already replayed into input (both fields cleared) pass through.
// The ID check mirrors the gateway and the chat-translation validator, both
// of which treat a whitespace-only ID as empty.
func rejectPreviousResponseID(req *core.ResponsesRequest) error {
if req == nil {
return nil
}
if req.Conversation != nil {
return core.NewInvalidRequestError(
"kimicode does not retain responses: conversation is not supported", nil)
}
if strings.TrimSpace(req.PreviousResponseID) == "" {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
return nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return core.NewInvalidRequestError(
"kimicode does not retain responses: previous_response_id is not supported", nil)
}

// adaptResponsesRequest pins store to false: the service retains no
// responses, so store=true fails upstream with a 400 (Postel's law — adapt
// instead of failing). A whitespace-only previous_response_id is treated as
// empty by rejectPreviousResponseID and cleared here, because omitempty does
// not omit a non-empty whitespace string and the upstream cannot resolve it.
func adaptResponsesRequest(req *core.ResponsesRequest) *core.ResponsesRequest {
if req == nil {
return nil
}
whitespaceID := req.PreviousResponseID != "" && strings.TrimSpace(req.PreviousResponseID) == ""
if (req.Store == nil || !*req.Store) && !whitespaceID {
return req
}
cp := *req
if req.Store != nil && *req.Store {
disabled := false
cp.Store = &disabled
}
if whitespaceID {
cp.PreviousResponseID = ""
}
return &cp
}
Loading
Loading