From d8d14f64e08b93bc026f39a8c4ba88e070f7b365 Mon Sep 17 00:00:00 2001 From: weselben Date: Wed, 23 Sep 2026 18:19:50 +0000 Subject: [PATCH 1/2] feat(providers/kimicode): add native Responses API support Forward Responses API requests natively to the upstream /responses endpoint. Kimi Code retains no responses: previous_response_id is rejected with an invalid-request error and store=true is pinned to false. Preserve completed_at, store, and input_tokens_details.cache_write_tokens in Responses payloads. --- docs/providers/kimicode.mdx | 8 +- internal/core/responses.go | 22 +- internal/core/responses_json_test.go | 49 ++++ internal/core/types.go | 9 +- internal/core/usage_json_test.go | 31 +++ internal/providers/kimicode/kimicode.go | 97 ++++++- internal/providers/kimicode/kimicode_test.go | 257 ++++++++++++++++++- 7 files changed, 449 insertions(+), 24 deletions(-) diff --git a/docs/providers/kimicode.mdx b/docs/providers/kimicode.mdx index 214ccff17..9c4dcbc13 100644 --- a/docs/providers/kimicode.mdx +++ b/docs/providers/kimicode.mdx @@ -7,8 +7,12 @@ 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, while files and batches are not supported by the upstream endpoint. + +The upstream Responses API keeps no state: requests with `store: true` are rewritten to +`store: false`, and requests with a `previous_response_id` are rejected with an +invalid-request error instead of being answered statelessly. ## Configure diff --git a/internal/core/responses.go b/internal/core/responses.go index 16b6826f5..fa159197a 100644 --- a/internal/core/responses.go +++ b/internal/core/responses.go @@ -184,15 +184,19 @@ type ResponsesInputElement struct { // ResponsesResponse represents the response from the Responses API. type ResponsesResponse struct { - ID string `json:"id"` - Object string `json:"object"` // "response" - CreatedAt int64 `json:"created_at"` - Model string `json:"model"` - Provider string `json:"provider"` - Status string `json:"status"` // "completed", "incomplete", "failed", "in_progress" - Output []ResponsesOutputItem `json:"output"` - Usage *ResponsesUsage `json:"usage,omitempty"` - Error *ResponsesError `json:"error,omitempty"` + ID string `json:"id"` + Object string `json:"object"` // "response" + CreatedAt int64 `json:"created_at"` + // CompletedAt is the upstream completion timestamp; zero while the + // response is still in progress. + CompletedAt int64 `json:"completed_at,omitempty"` + Store *bool `json:"store,omitempty"` + Model string `json:"model"` + Provider string `json:"provider"` + Status string `json:"status"` // "completed", "incomplete", "failed", "in_progress" + Output []ResponsesOutputItem `json:"output"` + Usage *ResponsesUsage `json:"usage,omitempty"` + Error *ResponsesError `json:"error,omitempty"` // IncompleteDetails explains a status of "incomplete": the model hit // max_output_tokens, was stopped by a content filter, or the upstream // stream was interrupted. diff --git a/internal/core/responses_json_test.go b/internal/core/responses_json_test.go index b7fb2f56b..7fc30646b 100644 --- a/internal/core/responses_json_test.go +++ b/internal/core/responses_json_test.go @@ -938,3 +938,52 @@ func TestResponsesBlocksFromContentPartsKeepsLargeIntegersExact(t *testing.T) { require.Contains(t, string(encoded), want) } } + +func TestResponsesResponseJSON_CompletedAtAndStoreRoundTrip(t *testing.T) { + cases := []struct { + name string + storeJSON string + wantStore *bool + wantStored bool + }{ + {name: "store false", storeJSON: `,"store":false`, wantStore: new(bool), wantStored: true}, + {name: "store absent", storeJSON: ``, wantStore: nil, wantStored: false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var resp ResponsesResponse + err := json.Unmarshal([]byte(`{ + "id":"resp_123", + "object":"response", + "created_at":1677652288, + "completed_at":1677652299, + "model":"gpt-4o-mini", + "provider":"openai", + "status":"completed", + "output":[]`+tc.storeJSON+` + }`), &resp) + require.NoError(t, err) + require.Equal(t, int64(1677652299), resp.CompletedAt) + if tc.wantStore == nil { + require.Nil(t, resp.Store) + } else { + require.NotNil(t, resp.Store) + require.Equal(t, *tc.wantStore, *resp.Store) + } + + body, err := json.Marshal(resp) + require.NoError(t, err) + + var decoded map[string]any + err = json.Unmarshal(body, &decoded) + require.NoError(t, err) + require.Equal(t, float64(1677652299), decoded["completed_at"]) + store, present := decoded["store"] + require.Equal(t, tc.wantStored, present, "store presence mismatch in marshaled payload: %s", string(body)) + if tc.wantStore != nil { + require.Equal(t, *tc.wantStore, store) + } + }) + } +} diff --git a/internal/core/types.go b/internal/core/types.go index a13f42671..b9a18a583 100644 --- a/internal/core/types.go +++ b/internal/core/types.go @@ -155,9 +155,12 @@ type ResponseMessage struct { // PromptTokensDetails holds extended input token breakdown (OpenAI/xAI). type PromptTokensDetails struct { CachedTokens int `json:"cached_tokens"` - AudioTokens int `json:"audio_tokens"` - TextTokens int `json:"text_tokens"` - ImageTokens int `json:"image_tokens"` + // CacheWriteTokens counts tokens written to the provider's prompt cache + // (Kimi Code / Anthropic-style cache creation). + CacheWriteTokens int `json:"cache_write_tokens,omitempty"` + AudioTokens int `json:"audio_tokens"` + TextTokens int `json:"text_tokens"` + ImageTokens int `json:"image_tokens"` } // CompletionTokensDetails holds extended output token breakdown (OpenAI/xAI). diff --git a/internal/core/usage_json_test.go b/internal/core/usage_json_test.go index 24044fed3..2a3282b23 100644 --- a/internal/core/usage_json_test.go +++ b/internal/core/usage_json_test.go @@ -118,3 +118,34 @@ func TestResponsesUsageMarshalJSON_UsesResponsesDetailFieldNames(t *testing.T) { _, exists = payload["raw_usage"] require.False(t, exists, "did not expect raw_usage in marshaled responses payload: %s", string(body)) } + +func TestResponsesUsageJSON_CacheWriteTokensRoundTrip(t *testing.T) { + var usage ResponsesUsage + err := json.Unmarshal([]byte(`{ + "input_tokens": 100, + "output_tokens": 20, + "total_tokens": 120, + "input_tokens_details": { + "cached_tokens": 88, + "cache_write_tokens": 12 + } + }`), &usage) + require.NoError(t, err) + require.NotNil(t, usage.PromptTokensDetails) + require.Equal(t, 88, usage.PromptTokensDetails.CachedTokens) + require.Equal(t, 12, usage.PromptTokensDetails.CacheWriteTokens) + + body, err := json.Marshal(usage) + require.NoError(t, err) + + var payload map[string]any + err = json.Unmarshal(body, &payload) + require.NoError(t, err) + + inputDetails, ok := payload["input_tokens_details"].(map[string]any) + require.True(t, ok, "decoded input_tokens_details = %#v, want object", payload["input_tokens_details"]) + require.Equal(t, float64(88), inputDetails["cached_tokens"]) + require.Equal(t, float64(12), inputDetails["cache_write_tokens"]) + _, exists := payload["raw_usage"] + require.False(t, exists, "did not expect cache_write_tokens to leak into raw_usage: %s", string(body)) +} diff --git a/internal/providers/kimicode/kimicode.go b/internal/providers/kimicode/kimicode.go index 2899e6acb..1d1167ea2 100644 --- a/internal/providers/kimicode/kimicode.go +++ b/internal/providers/kimicode/kimicode.go @@ -1,11 +1,18 @@ // 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 +// previous_response_id is rejected with an invalid-request error and +// store=true is pinned to false. package kimicode import ( + "context" + "io" + "net/http" + "github.com/enterpilot/gomodel/internal/core" "github.com/enterpilot/gomodel/internal/providers" "github.com/enterpilot/gomodel/internal/providers/openai" @@ -23,19 +30,93 @@ 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 (rejecting +// previous_response_id and pinning store to false — see Responses below). type Provider struct { *openai.ChatCompatible + responses *openai.CompatibleProvider } var _ core.Provider = (*Provider)(nil) // New creates a new Kimi Code provider. func New(cfg providers.ProviderConfig, opts providers.ProviderOptions) core.Provider { - return &Provider{openai.NewChatCompatible(cfg.APIKey, opts, openai.CompatibleProviderConfig{ + baseURL := providers.ResolveBaseURL(cfg.BaseURL, defaultBaseURL) + return &Provider{ + ChatCompatible: openai.NewChatCompatible(cfg.APIKey, opts, compatibleConfig(baseURL)), + responses: openai.NewCompatibleProvider(cfg.APIKey, opts, compatibleConfig(baseURL)), + } +} + +// compatibleConfig is the shared OpenAI-compatible configuration for both +// adapter instances. SetHeaders defaults to plain Bearer auth because +// NewCompatibleProvider (unlike NewChatCompatible) applies no default. +func compatibleConfig(baseURL string) openai.CompatibleProviderConfig { + return openai.CompatibleProviderConfig{ ProviderName: "kimicode", - BaseURL: providers.ResolveBaseURL(cfg.BaseURL, defaultBaseURL), - })} + BaseURL: baseURL, + SetHeaders: func(req *http.Request, apiKey string) { + providers.SetAuthHeaders(req, apiKey, providers.AuthHeaderConfig{AuthScheme: "Bearer "}) + }, + } +} + +// SetBaseURL overrides the upstream endpoint for both the chat-centric +// adapter and the native Responses adapter. +func (p *Provider) SetBaseURL(url string) { + p.ChatCompatible.SetBaseURL(url) + p.responses.SetBaseURL(url) +} + +// 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 with an invalid-request error before any +// upstream call; 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.responses.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.responses.StreamResponses(ctx, adaptResponsesRequest(req)) +} + +// rejectPreviousResponseID fails requests chaining from an earlier response: +// Kimi Code does not retain responses, so a previous response ID can never be +// resolved and answering statelessly would silently drop the conversation +// context the caller expects. +func rejectPreviousResponseID(req *core.ResponsesRequest) error { + if req == nil || req.PreviousResponseID == "" { + return nil + } + 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). previous_response_id is not adapted here; +// rejectPreviousResponseID rejects it instead. +func adaptResponsesRequest(req *core.ResponsesRequest) *core.ResponsesRequest { + if req == nil { + return nil + } + if req.Store == nil || !*req.Store { + return req + } + cp := *req + disabled := false + cp.Store = &disabled + return &cp } diff --git a/internal/providers/kimicode/kimicode_test.go b/internal/providers/kimicode/kimicode_test.go index 54eecf1ce..3252cd1b7 100644 --- a/internal/providers/kimicode/kimicode_test.go +++ b/internal/providers/kimicode/kimicode_test.go @@ -1,9 +1,17 @@ package kimicode import ( + "context" + "encoding/json" + "io" "net/http" + "net/http/httptest" + "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/enterpilot/gomodel/internal/core" "github.com/enterpilot/gomodel/internal/llmclient" "github.com/enterpilot/gomodel/internal/providers" @@ -12,7 +20,8 @@ import ( // Kimi Code is a thin wrapper over the shared chat-centric adapter and // forwards embeddings upstream unchanged, so the shared contract covers its -// surface. +// surface. The Responses API is forwarded natively to the upstream /responses +// endpoint. func TestChatCompatibleContract(t *testing.T) { providertest.AssertChatCompatible(t, providertest.ChatCompatible{ Registration: Registration, @@ -23,6 +32,250 @@ func TestChatCompatibleContract(t *testing.T) { opts.HTTPClient = client return New(providers.ProviderConfig{APIKey: apiKey, BaseURL: baseURL}, opts) }, - Embeddings: true, + Embeddings: true, + NativeResponses: true, + }) +} + +func boolPtr(b bool) *bool { return &b } + +// newTestProvider builds a provider wired to the test server through the +// injected HTTP client, matching how the shared contract constructs it. +func newTestProvider(server *httptest.Server) core.Provider { + opts := providertest.Options(llmclient.Hooks{}) + opts.HTTPClient = server.Client() + return New(providers.ProviderConfig{APIKey: "kimi-key", BaseURL: server.URL}, opts) +} + +func TestAdaptResponsesRequest(t *testing.T) { + t.Run("nil passes through", func(t *testing.T) { + assert.Nil(t, adaptResponsesRequest(nil)) + }) + + t.Run("clean request is returned unchanged", func(t *testing.T) { + req := &core.ResponsesRequest{Model: "kimi-for-coding", Input: "hi"} + assert.Same(t, req, adaptResponsesRequest(req)) + }) + + t.Run("store true is pinned to false", func(t *testing.T) { + req := &core.ResponsesRequest{Model: "kimi-for-coding", Input: "hi", Store: boolPtr(true)} + got := adaptResponsesRequest(req) + require.NotSame(t, req, got, "adapted request should be a copy") + require.NotNil(t, got.Store) + assert.False(t, *got.Store) + // The caller's request must not be mutated. + assert.True(t, *req.Store, "original request Store was mutated") + }) + + t.Run("previous_response_id is preserved", func(t *testing.T) { + // adaptResponsesRequest does not touch PreviousResponseID; the + // Responses/StreamResponses methods reject it instead (tested below). + req := &core.ResponsesRequest{ + Model: "kimi-for-coding", + Input: "hi", + PreviousResponseID: "resp_old", + Store: boolPtr(false), + } + got := adaptResponsesRequest(req) + require.Equal(t, "resp_old", got.PreviousResponseID) + require.NotNil(t, got.Store) + assert.False(t, *got.Store, "explicit store=false should stay false") + }) +} + +// responsesGoldenBody mirrors a real non-streaming /responses reply from the +// Kimi Code upstream (recorded 2026-09-08, trimmed to the members GoModel +// consumes). The upstream reply keeps extra members (prompt_cache_key, +// safety_identifier, service_tier); unknown members are ignored on decode. +const responsesGoldenBody = `{ + "id": "resp_golden", + "object": "response", + "created_at": 1788866012, + "completed_at": 1788866014, + "status": "completed", + "output": [ + { + "type": "reasoning", + "id": "rs_golden", + "status": "completed", + "summary": [{"type": "summary_text", "text": "Simple request."}] + }, + { + "type": "message", + "id": "msg_golden", + "status": "completed", + "role": "assistant", + "content": [{"type": "output_text", "text": "OK", "annotations": []}] + } + ], + "usage": { + "input_tokens": 88, + "input_tokens_details": {"cache_write_tokens": 12, "cached_tokens": 88}, + "output_tokens": 53, + "output_tokens_details": {"reasoning_tokens": 37}, + "total_tokens": 141 + }, + "store": false, + "model": "kimi-for-coding" +}` + +func TestResponses_NativeEndpoint(t *testing.T) { + var gotPath, gotAuth string + var gotBody map[string]any + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + gotAuth = r.Header.Get("Authorization") + _ = json.NewDecoder(r.Body).Decode(&gotBody) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(responsesGoldenBody)) + })) + defer server.Close() + + provider := newTestProvider(server) + + resp, err := provider.Responses(context.Background(), &core.ResponsesRequest{ + Model: "kimi-for-coding", + Input: "Say OK", + Store: boolPtr(true), + }) + require.NoError(t, err) + assert.Equal(t, "/responses", gotPath) + assert.Equal(t, "Bearer kimi-key", gotAuth) + // store=true is pinned to false before the request leaves. + assert.Equal(t, false, gotBody["store"], "wire store") + assert.NotContains(t, gotBody, "stream", "non-streaming request must not set stream on the wire") + + assert.Equal(t, "resp_golden", resp.ID) + assert.Equal(t, "kimi-for-coding", resp.Model) + assert.Equal(t, int64(1788866014), resp.CompletedAt) + require.NotNil(t, resp.Store) + assert.False(t, *resp.Store) + require.Len(t, resp.Output, 2) + require.NotNil(t, resp.Usage) + assert.Equal(t, 141, resp.Usage.TotalTokens) + require.NotNil(t, resp.Usage.PromptTokensDetails) + assert.Equal(t, 12, resp.Usage.PromptTokensDetails.CacheWriteTokens) + assert.Equal(t, 88, resp.Usage.PromptTokensDetails.CachedTokens) +} + +func TestStreamResponses_NativeEndpoint(t *testing.T) { + var gotPath string + var gotBody map[string]any + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + _ = json.NewDecoder(r.Body).Decode(&gotBody) + w.Header().Set("Content-Type", "text/event-stream") + // No trailing [DONE]: providers.EnsureResponsesDone must append it. + _, _ = io.WriteString(w, strings.Join([]string{ + `event: response.created`, + `data: {"type":"response.created","response":{"id":"resp_stream","object":"response","status":"in_progress","model":"kimi-for-coding"}}`, + ``, + `event: response.completed`, + `data: {"type":"response.completed","response":{"id":"resp_stream","object":"response","status":"completed","model":"kimi-for-coding"}}`, + ``, + }, "\n")) + })) + defer server.Close() + + provider := newTestProvider(server) + + stream, err := provider.StreamResponses(context.Background(), &core.ResponsesRequest{ + Model: "kimi-for-coding", + Input: "Say OK", + Store: boolPtr(true), + }) + require.NoError(t, err) + defer func() { _ = stream.Close() }() + + body, err := io.ReadAll(stream) + require.NoError(t, err) + assert.Equal(t, "/responses", gotPath) + assert.Equal(t, true, gotBody["stream"], "wire stream") + assert.Contains(t, string(body), "event: response.completed") + assert.True(t, strings.HasSuffix(strings.TrimSpace(string(body)), "data: [DONE]"), + "stream should end with data: [DONE], got %q", string(body)) +} + +// Kimi Code retains no responses, so a request chaining from an earlier +// response must be rejected before any upstream call instead of being +// answered statelessly. +func TestResponses_RejectsPreviousResponseID(t *testing.T) { + upstreamHits := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + upstreamHits++ + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + provider := newTestProvider(server) + + resp, err := provider.Responses(context.Background(), &core.ResponsesRequest{ + Model: "kimi-for-coding", + Input: "Say OK", + PreviousResponseID: "resp_old", + }) + require.Error(t, err) + assert.Nil(t, resp) + + var gatewayErr *core.GatewayError + require.ErrorAs(t, err, &gatewayErr, "error type = %T, want *core.GatewayError", err) + assert.Equal(t, core.ErrorTypeInvalidRequest, gatewayErr.Type) + assert.Contains(t, gatewayErr.Error(), "previous_response_id") + assert.Equal(t, 0, upstreamHits, "rejected request must not reach the upstream") +} + +func TestStreamResponses_RejectsPreviousResponseID(t *testing.T) { + upstreamHits := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + upstreamHits++ + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + provider := newTestProvider(server) + + stream, err := provider.StreamResponses(context.Background(), &core.ResponsesRequest{ + Model: "kimi-for-coding", + Input: "Say OK", + PreviousResponseID: "resp_old", + }) + require.Error(t, err) + assert.Nil(t, stream) + + var gatewayErr *core.GatewayError + require.ErrorAs(t, err, &gatewayErr, "error type = %T, want *core.GatewayError", err) + assert.Equal(t, core.ErrorTypeInvalidRequest, gatewayErr.Type) + assert.Contains(t, gatewayErr.Error(), "previous_response_id") + assert.Equal(t, 0, upstreamHits, "rejected request must not reach the upstream") +} + +// SetBaseURL must retarget both the chat-centric adapter and the native +// Responses adapter. +func TestSetBaseURL(t *testing.T) { + var gotPath string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(responsesGoldenBody)) + })) + defer server.Close() + + p := New(providers.ProviderConfig{APIKey: "kimi-key"}, providertest.Options(llmclient.Hooks{})) + kp, ok := p.(*Provider) + require.True(t, ok) + require.Equal(t, defaultBaseURL, kp.GetBaseURL()) + + kp.SetBaseURL(server.URL) + + assert.Equal(t, server.URL, kp.GetBaseURL()) + assert.Equal(t, server.URL, kp.responses.GetBaseURL()) + + _, err := p.Responses(context.Background(), &core.ResponsesRequest{ + Model: "kimi-for-coding", + Input: "Say OK", }) + require.NoError(t, err) + assert.Equal(t, "/responses", gotPath) } From 001ddeb74dfbf0a4066fd148591042810c9b8e0a Mon Sep 17 00:00:00 2001 From: weselben Date: Wed, 23 Sep 2026 18:50:14 +0000 Subject: [PATCH 2/2] docs(api): regenerate schemas for new Responses fields cache_write_tokens, completed_at, and store are now part of the public Responses schema (Greptile P2). --- cmd/gomodel/docs/docs.go | 11 +++++++++++ docs/openapi.json | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/cmd/gomodel/docs/docs.go b/cmd/gomodel/docs/docs.go index 24ffb6836..6464b7dad 100644 --- a/cmd/gomodel/docs/docs.go +++ b/cmd/gomodel/docs/docs.go @@ -10645,6 +10645,10 @@ const docTemplate = `{ "audio_tokens": { "type": "integer" }, + "cache_write_tokens": { + "description": "CacheWriteTokens counts tokens written to the provider's prompt cache\n(Kimi Code / Anthropic-style cache creation).", + "type": "integer" + }, "cached_tokens": { "type": "integer" }, @@ -11162,6 +11166,10 @@ const docTemplate = `{ "core.ResponsesResponse": { "type": "object", "properties": { + "completed_at": { + "description": "CompletedAt is the upstream completion timestamp; zero while the\nresponse is still in progress.", + "type": "integer" + }, "created_at": { "type": "integer" }, @@ -11203,6 +11211,9 @@ const docTemplate = `{ "description": "\"completed\", \"incomplete\", \"failed\", \"in_progress\"", "type": "string" }, + "store": { + "type": "boolean" + }, "usage": { "$ref": "#/definitions/core.ResponsesUsage" } diff --git a/docs/openapi.json b/docs/openapi.json index 4d8e196fa..d07798006 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -14462,6 +14462,10 @@ "audio_tokens": { "type": "integer" }, + "cache_write_tokens": { + "description": "CacheWriteTokens counts tokens written to the provider's prompt cache\n(Kimi Code / Anthropic-style cache creation).", + "type": "integer" + }, "cached_tokens": { "type": "integer" }, @@ -15012,6 +15016,10 @@ "core.ResponsesResponse": { "type": "object", "properties": { + "completed_at": { + "description": "CompletedAt is the upstream completion timestamp; zero while the\nresponse is still in progress.", + "type": "integer" + }, "created_at": { "type": "integer" }, @@ -15053,6 +15061,9 @@ "description": "\"completed\", \"incomplete\", \"failed\", \"in_progress\"", "type": "string" }, + "store": { + "type": "boolean" + }, "usage": { "$ref": "#/components/schemas/core.ResponsesUsage" }