Skip to content

[Provider]: Kimi/Moonshot 400 — $ref with sibling keywords in tool schema $defs is not normalized (openai-chat adapter) #2673

Closed

Description

@lidge-jun

Client or integration

Codex App

Provider or upstream service

kimi (Moonshot / api.kimi.com coding endpoint)

OpenCodex version

2.32.1-preview.20260825

Endpoint or capability

/v1/chat/completions (tool calls)

Current behaviour

Requests routed to kimi/k3 / kimi/k3[1m] fail with HTTP 400 as soon as the Codex tool catalog contains a schema in which a $defs node carries $ref together with sibling validation keywords:

Provider error 400: tools.function.parameters is not a valid moonshot flavored json schema,
details: <At path '$defs.__schema20': when using $ref, type should be defined in the referenced
schema instead of the parent schema>

The offending node is generated by Codex itself. Captured verbatim from a local Codex Desktop session rollout (codex-cli 0.145.0, automation_update tool):

"__schema20": {
  "type": "string",
  "minLength": 1,
  "format": "uuid",
  "description": "Target thread UUID for heartbeat automations. ...",
  "$ref": "#/$defs/__schema2"
}

In that one tool schema, 8 of the $defs nodes have sibling keys next to $ref. Seven of them only carry description, which Moonshot tolerates; __schema20 also carries type, minLength, and format, and that is the node the API names in the error.

This is valid JSON Schema 2020-12 — $ref no longer replaces siblings there, it is an in-place applicator, so type/minLength/format are meant to be combined with the referenced schema. Moonshot enforces the older draft-07 reading where $ref must stand alone.

The proxy forwards the schema unchanged in this respect. In src/adapters/openai-chat.ts, toolsToChatFormat() only applies ensureRootObjectType() for non-xAI targets:

const parameters = stripResponsesOnlyEncryptedMarker(xaiTarget
  ? normalizeXaiToolParameters(t.parameters)
  : ensureRootObjectType(t.parameters));

ensureRootObjectType() (added for #228) only stamps type: "object" on the root. It never walks $defs/properties, so a sibling-$ref node deeper in the schema reaches Moonshot untouched. There is already a per-provider deep normalizer for xAI (resolveXaiSchemaRefs) and for Google (src/adapters/google-tool-schema.ts inlines local $defs refs), but nothing on the Kimi path.

Expected behaviour

For the Kimi/Moonshot target, function parameter schemas should be normalized so no node emits $ref alongside sibling keywords. Requests carrying the Codex automation_update catalog (or any zod-to-json-schema output with deduplicated $defs) should return HTTP 200 and be able to call tools.

Minimal redacted request or reproduction

# Provider: kimi (OAuth), baseUrl https://api.kimi.com/coding/v1
curl http://127.0.0.1:10100/v1/responses \
  -H 'content-type: application/json' \
  --data-binary '{
    "model": "kimi/k3",
    "input": "Reply exactly OK. Do not call tools.",
    "tool_choice": "auto",
    "stream": false,
    "tools": [{
      "type": "function",
      "name": "schema_probe",
      "description": "Schema compatibility probe.",
      "parameters": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": { "threadId": { "$ref": "#/$defs/__schema20" } },
        "required": ["threadId"],
        "additionalProperties": false,
        "$defs": {
          "__schema2": { "type": "string", "minLength": 1 },
          "__schema20": {
            "type": "string",
            "minLength": 1,
            "format": "uuid",
            "description": "Target thread UUID.",
            "$ref": "#/$defs/__schema2"
          }
        }
      }
    }]
  }'

Removing the "$ref" line from __schema20 (or removing type/minLength/format and keeping only $ref) makes the same request succeed.

Note on scope: the identical payload sent through a Moonshot-compatible third-party endpoint (openai-chat adapter, non-api.kimi.com host serving Kimi K3) returns HTTP 200, so the rejection comes from Moonshot's own schema validator on the first-party endpoint, not from the model or the adapter's transport.

Actual response or error

HTTP 400
Provider error 400: tools.function.parameters is not a valid moonshot flavored json schema,
details: <At path '$defs.__schema20': when using $ref, type should be defined in the referenced
schema instead of the parent schema>

Upstream documentation

Suggested mapping or implementation notes

Two candidate shapes, in the order I would try them:

  1. Inline the referenced schema (preferred). Add a Kimi-scoped normalizer next to ensureRootObjectType() in src/adapters/openai-chat.ts, gated on the Kimi host in the same style as isXaiSchemaTarget(). Walk the schema; whenever a node has $ref plus other keys, resolve the local #/$defs/... / #/definitions/... pointer and merge the target under the node's own keywords (node keywords win, matching 2020-12 semantics), dropping $ref. src/adapters/google-tool-schema.ts already does exactly this shape of local-ref inlining and resolveXaiSchemaRefs() already has the cycle guard, so the logic is proven twice in-tree — a shared helper would be better than a third private copy. Cyclic or unresolvable refs need a bail-out; dropping the sibling keywords instead of failing the whole tool is the safer fallback there.

  2. Strip the siblings. Simpler, but lossy: format: "uuid" and minLength: 1 disappear from the wire, so the model loses constraints it would otherwise honor. Only worth it if inlining proves to blow up schema size on catalogs with many shared $defs.

Whichever is chosen, the fix should sit behind the same host check as the other provider-scoped schema paths so no other openai-chat provider changes shape, and it should be covered by a focused test alongside tests/xai-tool-schema.test.ts / tests/google-tool-schema.test.ts — a fixture carrying a sibling-$ref $defs node, asserting no $ref-with-siblings survives in the emitted tools[].function.parameters.

Worth noting this is the second Moonshot schema-strictness 400 in the same code path after #228 (root type requirement). A single Kimi schema-conformance pass, rather than another point patch, would probably be the durable answer.

Additional context and attachments

  • Related: [Bug]: Kimi returns 400 when a Codex tool schema has no root object type #228 (tools.function.parameters.type is required and must be "object" — same adapter, same provider, different strictness rule; fixed by ensureRootObjectType(), which does not walk nested nodes).
  • The trigger schema comes from Codex's own deferred automation_update tool, so any Kimi user in Codex Desktop hits this as soon as that tool loads. It is not caused by user configuration.
  • Environment: macOS 26.5.2 arm64, Codex Desktop, codex-cli 0.145.0, opencodex 2.32.1-preview.20260825.

Checks

  • I searched existing provider and compatibility issues.
  • The request and response were redacted.
  • The expected behaviour is based on an upstream specification or a concrete client requirement.

Activity

  1. added
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    on Aug 26, 2026
  2. Ingwannu commented on Aug 26, 2026

    @Ingwannu
    Owner

    Confirmed against current dev@779b6090c. The first-party Kimi/Moonshot chat path only applies ensureRootObjectType(); it does not traverse $defs or remove sibling validation keywords from a $ref node, so the reported schema reaches Moonshot unchanged.

    I will implement this as a provider-scoped schema compatibility layer:

    • apply it only to the first-party api.kimi.com, api.moonshot.ai, and api.moonshot.cn chat destinations; third-party openai-chat gateways remain byte-shape compatible;
    • move the deep rewrite into a dedicated Moonshot schema module rather than adding another large block to openai-chat.ts;
    • recursively normalize local #/$defs/... and #/definitions/... references when they have siblings, merging the referenced schema first and the node's own keywords last, then removing $ref;
    • preserve pure $ref nodes; cyclic or unresolvable sibling refs degrade to a pure $ref instead of inventing constraints;
    • bound depth, node count, and active-reference expansion; an over-limit tool is omitted rather than partially weakened;
    • add focused regressions for the reported automation_update shape, pure refs, cycles/unresolvable refs, the first-party host gate, and a third-party preservation control.

    I will link the implementation PR here and keep the issue open until exact-head CI is green and the fix reaches dev.

  3. Ingwannu commented on Aug 26, 2026

    @Ingwannu
    Owner

    Implemented in draft PR #2675 at exact head c7916e72822909da269da6328e46ad426b079f9b.

    The fix is limited to first-party Kimi/Moonshot chat destinations. It expands resolvable local sibling refs, preserves pure refs, degrades cyclic/unresolvable sibling refs to a pure ref, bounds depth/node/ref expansion, and leaves third-party OpenAI-chat gateways unchanged. The reported nested $defs.__schema20 shape is covered directly.

    Focused validation is green (65/65 for the new normalizer plus existing OpenAI-chat hardening; 17/17 for xAI boundary, Moonshot cases, and dangling tool-call compatibility), with typecheck, privacy scan, and diff check passing. This issue remains open until exact-head CI is green and the PR reaches dev.

  4. lidge-jun commented on Aug 26, 2026

    @lidge-jun
    OwnerAuthor

    리뷰 · 우선순위 73 / 80

    설명

    이 이슈는 Codex 앱이 kimi/k3 또는 kimi/k3[1m] 으로 도구를 실을 때, Moonshot 이 HTTP 400 tools.function.parameters is not a valid moonshot flavored json schema 를 내는 문제입니다. 원인은 $defs 노드가 $ref 와 함께 type, minLength, format 같은 형제 키를 가진 형태입니다. 작성자가 Codex Desktop (codex-cli 0.145.0) 의 automation_update 도구에서 그대로 잡은 __schema20 이 그 모양입니다. JSON Schema 2020-12 에서는 $ref 가 형제를 허용하는 제자리 적용자이지만, Moonshot 은 draft-07 처럼 $ref 가 혼자 있어야 한다고 봅니다.

    지금 current dev HEAD 779b6090c 의 src/providers/registry.ts 1243-1246줄 kimi 제공자는 adapter: "openai-chat", baseUrl: "https://api.kimi.com/coding/v1" 입니다. 채팅 도구 변환은 src/adapters/openai-chat.ts 1226줄 toolsToChatFormat 입니다. 1231-1234줄은 isXaiSchemaTarget 가 참일 때만 normalizeXaiToolParameters 를 쓰고, 나머지는 ensureRootObjectType 만 호출합니다. ensureRootObjectType 은 954-961줄에서 루트에 type: "object" 만 찍습니다. $defs 나 properties 안을 걷지 않으므로, 형제 $ref 노드는 Moonshot 까지 그대로 갑니다.

    이미 제공자별 깊은 정규화는 두 곳이 있습니다. 같은 파일 1002줄 resolveXaiSchemaRefs 는 로컬 #/ 포인터를 풀고, 형제가 있으면 대상과 나머지를 합칩니다. src/adapters/google-tool-schema.ts 168-176줄 sanitizeSchema 는 $ref 를 로컬 $defs/definitions 로 풀어 mergeRefTarget 로 합친 뒤 다시 걷습니다. 925-929줄 isXaiSchemaTarget 은 호스트 cli-chat-proxy.grok.com 에만 묶입니다. HEAD 에는 normalizeMoonshot, isMoonshotSchemaTarget, src/adapters/moonshot-tool-schema.ts 가 없습니다. 이슈가 적은 세 번째 사본을 피하고 공유 헬퍼를 쓰라는 제안은, 아직 코드에 반영되지 않았습니다.

    재현 curl 은 /v1/responses 로 들어가 채팅 어댑터가 도구 스키마를 그대로 내보내는 경로입니다. 같은 몸을 1차 호스트가 아닌 openai-chat 게이트웨이로 보내면 HTTP 200 이 나온다고 적혀 있으므로, 거절은 모델이 아니라 Moonshot 검증기입니다. 형제를 지우기만 하면 format: "uuid" 와 minLength: 1 이 사라져 모델 안내가 줄어듭니다. 로컬 참조를 인라인하는 쪽이 이슈가 선호한 길입니다. 순환이거나 못 푸는 참조는 도구 전체를 실패시키기보다 형제를 버리고 순수 $ref 로 남기는 편이 안전합니다.

    초안 PR #2675 가 이미 이 이슈를 닫겠다고 연결되어 있습니다. 그 PR 은 1차 api.kimi.com / api.moonshot.ai / api.moonshot.cn 에만 묶인 전용 모듈을 추가합니다. 이슈는 exact-head CI 가 초록이고 고침이 dev 에 들어가기 전까지 열린 채로 두겠다고 본문에 적혀 있습니다. 이 댓글은 그 구현 PR 이 아니라, HEAD 에 구멍이 남아 있다는 이슈 자체에 대한 리뷰입니다.

    src/adapters/openai-chat.ts:1234 - 1차 Kimi 경로는 ensureRootObjectType 만 써서 $defs 안의 형제 $ref 가 Moonshot 까지 갑니다
    src/adapters/openai-chat.ts:954 - ensureRootObjectType 은 루트 type: "object" 만 찍고 중첩 노드를 걷지 않습니다
    src/providers/registry.ts:1246 - 기본 Kimi 코딩 끝점은 https://api.kimi.com/coding/v1 이라, 호스트 게이트가 이 이름을 봐야 합니다
    src/adapters/google-tool-schema.ts:168 - Google 은 이미 로컬 $ref 를 인라인하므로, Kimi 만 그 층이 없습니다
    경로/심볼 - HEAD 에 Moonshot 전용 스키마 정규화가 없어 Codex automation_update 카탈로그가 400을 냅니다

    메인테이너의 판단이 필요한 지점

    • 고침을 1차 Kimi/Moonshot 호스트에만 둘지, 모든 openai-chat 목적지에 둘지 정해야 합니다
    • 형제 $ref 를 인라인할지, 형제를 지워 순수 $ref 만 남길지 정해야 합니다
    • xAI/Google 과 공유 헬퍼를 만들지, Moonshot 전용 모듈을 둘지 정해야 합니다
    • 순환·한도 초과 때 도구를 통째로 뺄지, 순수 $ref 로 낮출지 정해야 합니다

    너의 추천

    이 이슈는 열어 두고 구현을 진행하는 것을 추천합니다. 고침은 src/adapters/openai-chat.ts 의 toolsToChatFormat 옆에, isXaiSchemaTarget 과 같은 호스트 게이트로 좁게 넣으세요. 로컬 #/$defs 와 #/definitions 형제 $ref 만 인라인하고, 순수 $ref 와 3자 게이트웨이는 그대로 두세요. 초안 #2675 가 그 방향입니다. exact-head CI 가 초록이고 #2675 가 dev 에 들어가기 전에는 이슈를 닫지 마세요. 미리보기 배포는 계획에 없습니다.

    이 댓글은 grok-bot이 작성했습니다

  5. Ingwannu commented on Aug 26, 2026

    @Ingwannu
    Owner

    Correction to draft PR #2675: review of the duplicate #2677 exposed that object-spread inlining could overwrite referenced assertions when the same keyword appeared in both schemas. I fixed the same weakness in #2675 at exact head 0c211d7c2384cd4886ca04a23c37b3da6c187c0e.

    Equal duplicate assertions and non-conflicting keys still inline for the reported Codex schema. Conflicting required, properties, enum, minimum, or other duplicate assertions now degrade to the pure ref instead of weakening the referenced contract. Focused tests are 78/78 green; typecheck and privacy scan pass. The issue remains open pending exact-head CI and another maintainer review.

  6. lidge-jun commented on Aug 26, 2026

    @lidge-jun
    OwnerAuthor

    Landed via #2677 at 8412fe1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, planslanded-via-maintainerOriginal PR closed after landing via a maintainer merge trainproviderProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reports

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions