Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@ For Meta Responses at the exact `api.meta.ai` host, OpenCodex omits the unsuppor
`output.encrypted_content` include requested by Codex Desktop. Meta otherwise rejects the
request before inference. `reasoning.encrypted_content` and other include entries remain
intact, so stateless reasoning replay is preserved. The same exact-entry normalization
applies to xAI; unrelated Responses destinations keep their original include list.
applies to xAI and to Kimi Code at its exact HTTPS Responses endpoint
(`https://api.kimi.com/coding/v1/responses`); unrelated Responses destinations keep their original include list.

These request-shape fixes do not change account entitlements. Meta limits Muse Code
subscription credentials to the Muse Code client; other clients require a separately
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -1334,7 +1334,7 @@
"ollama-native-v4.test.ts": "providers/ollama", "ollama-native-scaling.test.ts": "providers/ollama", "ollama-native.test.ts": "providers/ollama", "ollama-show-enrichment-v7.test.ts": "providers/ollama", "ollama-show-enrichment.test.ts": "providers/ollama",
"ollama-show-ignore-abort.test.ts": "providers/ollama",
"opaque-blob-wrapped-rejection.test.ts": "responses", "openai-api-virtual-models.test.ts": "adapters/openai", "openai-chat-bounded-tool-names.test.ts": "adapters/openai", "openai-chat-dangling-toolcalls.test.ts": "adapters/openai",
"openai-chat-developer-position.test.ts": "adapters/openai", "openai-chat-eof.test.ts": "adapters/openai", "openai-chat-hardening.test.ts": "adapters/openai", "openai-empty-assistant-history.test.ts": "adapters/openai", "openai-chat-image-normalization.test.ts": "adapters/openai",
"openai-chat-developer-position.test.ts": "adapters/openai", "openai-chat-eof.test.ts": "adapters/openai", "openai-chat-hardening.test.ts": "adapters/openai", "openai-empty-assistant-history.test.ts": "adapters/openai", "kimi-coding-output-include.test.ts": "providers", "openai-chat-image-normalization.test.ts": "adapters/openai",
"openai-chat-inline-think-tags.test.ts": "adapters/openai", "openai-chat-invalid-tool-call-diagnostics.test.ts": "adapters/openai", "openai-chat-mistral-content.test.ts": "adapters/openai", "openai-chat-model-suffix.test.ts": "adapters/openai", "openai-chat-native-policy.test.ts": "adapters/openai",
"openai-chat-parallel-stream.test.ts": "adapters/openai", "openai-chat-path-override.test.ts": "adapters/openai", "openai-chat-qwen38-leading-system.test.ts": "adapters/openai", "openai-chat-reasoning-wire-policy.test.ts": "adapters/openai",
"openai-chat-sanitization-review-regressions.test.ts": "adapters/openai", "openai-chat-serialized-tool-call-content.test.ts": "adapters/openai", "openai-chat-serialized-tool-call-hold-bound.test.ts": "adapters/openai", "openai-chat-serialized-tool-call-think.test.ts": "adapters/openai",
Expand Down
18 changes: 15 additions & 3 deletions src/adapters/openai-responses/empty-assistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,23 @@ function isEmptyAssistantMessage(item: unknown): boolean {
return item.content === "" || (Array.isArray(item.content) && item.content.every(isEmptyTextPart));
}

/** Exactly Kimi Code's native Responses endpoint; lookalike hosts and other Kimi paths do not match. */
export function isKimiCodingResponsesUrl(responseUrl: string): boolean {
let url: URL;
try { url = new URL(responseUrl); } catch { return false; }
return url.hostname === "api.kimi.com" && /^\/coding\/v1\/responses\/?$/.test(url.pathname);
}

/** The same endpoint over HTTPS on the default port only, for request rewrites scoped to Kimi Code itself. */
export function isKimiCodingResponsesHttpsUrl(responseUrl: string): boolean {
if (!isKimiCodingResponsesUrl(responseUrl)) return false;
const url = new URL(responseUrl);
return url.protocol === "https:" && url.port === "";
}

/** Kimi Code rejects content-empty assistant messages even on its native Responses endpoint. */
export function stripKimiEmptyAssistantMessages(body: unknown, responseUrl: string): unknown {
let url: URL;
try { url = new URL(responseUrl); } catch { return body; }
if (url.hostname !== "api.kimi.com" || !/^\/coding\/v1\/responses\/?$/.test(url.pathname)) return body;
if (!isKimiCodingResponsesUrl(responseUrl)) return body;
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
const referencedIds = new Set<string>();
for (const item of body.input) {
Expand Down
9 changes: 5 additions & 4 deletions src/adapters/openai-responses/passthrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { observeOutbound } from "../../usage/cache-diagnostic";
import { normalizeMuseToolCatalog } from "./muse-tool-catalog";
import { normalizeMuseToolChoice } from "./muse-tool-choice";
import { declaresHostedWebSearch, projectHostedSearchHistory, renderCompactionSearchHistoryItems } from "./compaction-search-history";
import { stripKimiEmptyAssistantMessages } from "./empty-assistant";
import { isKimiCodingResponsesHttpsUrl, stripKimiEmptyAssistantMessages } from "./empty-assistant";
import { normalizeOpenGatewayResponsesInput } from "./opengateway-input";

/**
Expand Down Expand Up @@ -358,12 +358,13 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
// OpenCode Go speaks the same dialect, which the final model id and URL identify.
outBody = normalizeXaiResponsesWebSearch(outBody, provider, { modelId: parsed.modelId, responseUrl: url });
outBody = injectXaiResponsesXSearch(outBody, provider, parsed._replayPrefixLen);
// xAI, Meta and Azure answer 400 for `output.encrypted_content` instead of ignoring it;
// Codex Desktop asks for it on every turn (#7878).
// xAI, Meta, Azure and Kimi Code (#8075) answer 400 for `output.encrypted_content`
// instead of ignoring it; Codex Desktop asks for it on every turn (#7878).
// Azure also rejects this exact include entry, including on plain Responses routes.
const isAzureResponses = provider.adapter === "azure" || provider.adapter === "azure-openai"
|| new URL(url).hostname.toLowerCase().endsWith(".openai.azure.com");
if (isXaiResponsesDestination(provider) || isMetaAiResponsesDestination(url) || isAzureResponses) {
if (isXaiResponsesDestination(provider) || isMetaAiResponsesDestination(url) || isAzureResponses
|| isKimiCodingResponsesHttpsUrl(url)) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
outBody = stripUnsupportedOutputInclude(outBody);
}
// xAI and explicitly classified compatible gateways reject these OpenAI web_search
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/openai-responses/request-strips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { activateDeferredTool } from "./tool-schema";
import { stripOpenAiOnlyWebSearchFields } from "./web-search";

/**
* Codex requests output.encrypted_content, which xAI, Meta and Azure reject with HTTP 400.
* Codex requests output.encrypted_content, which xAI, Meta, Azure and Kimi Code reject with HTTP 400.
* The caller gates this on the destination. Preserve reasoning.encrypted_content
* for stateless replay and leave unknown or malformed values for upstream validation.
*/
Expand Down
2 changes: 1 addition & 1 deletion structure/transports/responses-wire-shapes.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Google metadata follows the [destination-scoped replay projection](../providers/

xAI keeps `openai-chat` provider-wide; Grok 4.5/4.6/4.7 subscription Responses default to native `openai-responses`, retaining namespace, hosted-search and reasoning-replay normalization. The reserved `xai` OAuth transport is name-pinned
to the Grok CLI gateway even if its saved base URL differs; custom provider IDs do not inherit this
default. Exact xAI Responses destinations, including the US regional API, omit only `output.encrypted_content` from `include`; other entries remain unchanged. Meta at the exact `api.meta.ai` host shares this normalization, as do Azure adapters and resource hosts under .openai.azure.com, including when configured with openai-responses; unrelated destinations retain their include list ([request compatibility](../providers/xai-grok.md#responses-request-compatibility)). API-key requests, translated Chat/Anthropic defaults and other Grok models retain their
default. Exact xAI Responses destinations, including the US regional API, omit only `output.encrypted_content` from `include`; other entries remain unchanged. Meta at the exact `api.meta.ai` host shares this normalization, as do Azure adapters and resource hosts under .openai.azure.com, including when configured with openai-responses. Kimi Code shares it only at its exact HTTPS Responses endpoint (`https://api.kimi.com/coding/v1/responses`, default port; #8075); unrelated destinations retain their include list ([request compatibility](../providers/xai-grok.md#responses-request-compatibility)). API-key requests, translated Chat/Anthropic defaults and other Grok models retain their
existing wire and tier policy. The OAuth lane is service-tier classified per model
(`modelSupportsServiceTier` on the registry entry, live-probed 2026-09-13 and 2026-09-23;
`devlog/_fin/260923_grok47_parity/010_probe-evidence.md` records 4.7): grok-4.7, grok-4.6, grok-4.5,
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/test-layout-expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -1104,7 +1104,7 @@
"openai-chat-bounded-tool-names.test.ts": "adapters/openai",
"openai-chat-dangling-toolcalls.test.ts": "adapters/openai",
"openai-chat-developer-position.test.ts": "adapters/openai", "openai-chat-eof.test.ts": "adapters/openai",
"openai-chat-hardening.test.ts": "adapters/openai", "openai-empty-assistant-history.test.ts": "adapters/openai", "openai-chat-image-normalization.test.ts": "adapters/openai",
"openai-chat-hardening.test.ts": "adapters/openai", "openai-empty-assistant-history.test.ts": "adapters/openai", "kimi-coding-output-include.test.ts": "providers", "openai-chat-image-normalization.test.ts": "adapters/openai",
"openai-chat-inline-think-tags.test.ts": "adapters/openai",
"openai-chat-invalid-tool-call-diagnostics.test.ts": "adapters/openai",
"openai-chat-mistral-content.test.ts": "adapters/openai",
Expand Down
61 changes: 61 additions & 0 deletions tests/providers/kimi-coding-output-include.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, expect, test } from "bun:test";
import { createResponsesPassthroughAdapter } from "../../src/adapters/openai-responses";
import { parseRequest } from "../../src/responses/parser";
import { createTranslatorBudget } from "../../src/lib/translator-budget";
import type { OcxProviderConfig } from "../../src/types";

// #8075: Kimi Code's native Responses endpoint answers 400 for the output.encrypted_content
// include entry that Codex Desktop sends on every turn.
const provider: OcxProviderConfig = {
adapter: "openai-responses", baseUrl: "https://api.kimi.com/coding/v1",
authMode: "key", apiKey: "test-credential", modelSuffixBracketStrip: true,
};
const input = [
{ type: "message", role: "user", content: "read the file" },
{ type: "function_call", call_id: "call_1", name: "read_file", arguments: "{}" },
{ type: "function_call_output", call_id: "call_1", output: "first line" },
];

function wire(include: unknown, target: OcxProviderConfig = provider): Record<string, unknown> {
const raw = { model: "k3.1-pro[256k]", input, stream: true, ...(include === undefined ? {} : { include }) };
const snapshot = JSON.stringify(raw);
const budget = createTranslatorBudget();
const request = createResponsesPassthroughAdapter(target)
.buildRequest(parseRequest(raw), { headers: new Headers(), translatorBudget: budget });
request.releaseBodyObservation?.();
budget.dispose();
expect(JSON.stringify(raw)).toBe(snapshot);
return JSON.parse(request.body) as Record<string, unknown>;
}

describe("Kimi Code Responses include normalization (#8075)", () => {
test("removes only output.encrypted_content and keeps the other entries in order", () => {
const body = wire(["reasoning.encrypted_content", "output.encrypted_content", "message.output_text.logprobs"]);
expect(body.include).toEqual(["reasoning.encrypted_content", "message.output_text.logprobs"]);
expect(body.input).toEqual(input);
});

test("omits include when the rejected entry was the only one", () => {
expect(wire(["output.encrypted_content"])).not.toHaveProperty("include");
});

test("removes every duplicate output.encrypted_content", () => {
expect(wire(["output.encrypted_content", "reasoning.encrypted_content", "output.encrypted_content"]).include)
.toEqual(["reasoning.encrypted_content"]);
});

test("an absent or already-supported include list is unchanged", () => {
expect(wire(undefined)).not.toHaveProperty("include");
expect(wire(["reasoning.encrypted_content"]).include).toEqual(["reasoning.encrypted_content"]);
});

test.each([
"https://api.kimi.com/v1", "https://api.kimi.com/coding/v2",
"https://api.kimi.com.evil.test/coding/v1", "https://kimi.example.test/coding/v1",
"https://api.moonshot.ai/v1", "http://api.kimi.com/coding/v1", "https://api.kimi.com:444/coding/v1",
])("leaves other destinations untouched: %s", baseUrl => {
const include = ["reasoning.encrypted_content", "output.encrypted_content"];
expect(wire(include, { ...provider, baseUrl }).include).toEqual(include);
});
});

Loading