Skip to content

Anthropic 400: tools.N.custom.input_schema.type Field required (tool schema missing top-level type) #75

Closed

Description

@seotk0319

요약 / Summary

Anthropic(Claude) 백엔드로 사용할 때, Codex가 서브에이전트/브라우저/코덱스 툴 등 추가 툴을 붙이는 순간 아래 400이 발생합니다.

Provider error 400: {"type":"error","error":{"type":"invalid_request_error","message":"tools.26.custom.input_schema.type: Field required"},"request_id":"req_011CcqiYNzcinns1gsUkV9Cg"}

When using an Anthropic (Claude) backend, requests fail as soon as extra tools are attached (sub-agent, browser use, Codex tools). Anthropic rejects a tool whose input_schema lacks a top-level type.

환경 / Environment

  • Package: @bitkyc08/opencodex@2.6.31-preview.20260707 (same code path exists upstream)
  • OS: Windows
  • Backend: Anthropic (Claude Opus)
  • Trigger: only when the tool list grows (sub-agent / browser / Codex tools). Basic chat without those tools works fine.

원인 / Root cause

src/adapters/anthropic.ts 의 toolsToAnthropicFormat 가 input_schema 로 t.parameters 를 그대로 전달합니다. 일부 Codex/MCP/서브에이전트/브라우저 툴의 파라미터 스키마에는 최상위 type: "object" 가 없습니다. OpenAI 와이어는 이를 허용하지만 Anthropic Messages API 는 input_schema.type 을 필수로 검증하므로 400 을 반환합니다.

toolsToAnthropicFormat passes t.parameters straight through as input_schema. Some Codex/MCP/sub-agent/browser tools ship a parameter schema without a top-level type: "object". The OpenAI wire tolerates this, but the Anthropic Messages API requires input_schema.type, so it 400s. The failing index (e.g. tools.26) is one of the extra tools, which is why plain chat works but adding tools breaks.

const converted = tools.map(t => ({
  name: toolNames.toWire(namespacedToolName(t.namespace, t.name)),
  description: t.description,
  input_schema: t.parameters, // <- schema without top-level `type` is forwarded verbatim
}));

제안 수정 / Proposed fix

input_schema 를 Anthropic 요구사항에 맞게 정규화합니다 (최상위 type 누락 시 object 보정 + properties 보장).

// Anthropic requires every tool's `input_schema` to be a JSON Schema object with an
// explicit top-level `type` (it validates `tools.N.custom.input_schema.type`). Codex/
// OpenAI Responses tool params frequently omit it, which the OpenAI wire tolerates but
// Anthropic 400s on. Normalize so a missing/invalid top-level `type` becomes an object
// schema with `properties`.
function normalizeAnthropicInputSchema(schema: unknown): Record<string, unknown> {
  if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
    return { type: "object", properties: {} };
  }
  const s: Record<string, unknown> = { ...(schema as Record<string, unknown>) };
  if (typeof s.type !== "string") s.type = "object";
  if (s.type === "object" && (s.properties === undefined || s.properties === null)) {
    s.properties = {};
  }
  return s;
}

// ...in toolsToAnthropicFormat:
input_schema: normalizeAnthropicInputSchema(t.parameters),

재현 / Reproduction

  1. Configure an Anthropic (Claude) backend.
  2. In Codex, trigger a sub-agent, browser use, or Codex tools so the tool list grows.
  3. Request 400s with tools.N.custom.input_schema.type: Field required.

로컬에서 위 패치 적용 후 서브에이전트/브라우저/코덱스 툴 호출이 정상 동작하는 것을 확인했습니다. Verified locally: after applying the patch, sub-agent/browser/Codex tool calls work. Happy to open a PR if the approach looks good.

Activity

  1. seotk0319 commented on Jul 9, 2026

    @seotk0319
    Author

    정정 / Correction — the type-only fix is not enough

    원래 제안한 "최상위 type 보정"만으로는 해결되지 않았습니다. Anthropic Messages API는 input_schema 루트에 oneOf/anyOf/allOf 콤비네이터가 있으면 그것도 거부합니다. Codex/OpenAI Responses 쪽 일부 툴(그리고 MCP 툴)이 루트에 union 스키마를 그대로 내보내는 경우가 있어서, type만 채워도 같은 400이 계속 납니다.

    Adding a top-level type alone does not fix it. The Anthropic Messages API also rejects a top-level oneOf/anyOf/allOf on input_schema. Some Codex/OpenAI Responses (and MCP) tools emit a root-level union schema, so filling in type still 400s.

    완전한 원인 / Full root cause

    • input_schema 루트에 type 누락 → 거부
    • input_schema 루트에 oneOf/anyOf/allOf 존재 → 거부 (프로퍼티 내부에 중첩된 anyOf 등은 허용)

    완전한 수정 / Complete fix (src/adapters/anthropic.ts)

    루트 콤비네이터를 평탄화합니다: 각 branch의 properties를 루트 object 스키마로 병합, required는 allOf에서만 병합(oneOf/anyOf는 과도 강제 방지), 중첩된 anyOf는 그대로 보존, 그 외 루트 키(title, additionalProperties 등)는 유지.

    // Anthropic requires every tool's `input_schema` to be a JSON Schema object with an
    // explicit top-level `type` (it validates `tools.N.custom.input_schema.type`). It also
    // rejects top-level oneOf/allOf/anyOf, even though Codex/OpenAI Responses tools may emit
    // them. Normalize only the root schema so missing/invalid `type` becomes an object and
    // root compositions are flattened while nested property schemas stay intact.
    function normalizeAnthropicInputSchema(schema: unknown): Record<string, unknown> {
      if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
        return { type: "object", properties: {} };
      }
      const obj = schema as Record<string, unknown>;
      const compositionKeys = ["oneOf", "anyOf", "allOf"] as const;
      const hasRootComposition = compositionKeys.some(k => Array.isArray(obj[k]));
    
      if (!hasRootComposition) {
        const s: Record<string, unknown> = { ...obj };
        if (typeof s.type !== "string") s.type = "object";
        if (s.type === "object" && (s.properties === undefined || s.properties === null)) {
          s.properties = {};
        }
        return s;
      }
    
      const properties: Record<string, unknown> = {};
      const required = new Set<string>();
    
      if (obj.properties && typeof obj.properties === "object" && !Array.isArray(obj.properties)) {
        Object.assign(properties, obj.properties as Record<string, unknown>);
      }
      if (Array.isArray(obj.required)) {
        for (const item of obj.required) if (typeof item === "string") required.add(item);
      }
    
      for (const key of compositionKeys) {
        const variants = obj[key];
        if (!Array.isArray(variants)) continue;
        const mergeRequired = key === "allOf";
        for (const variant of variants) {
          if (!variant || typeof variant !== "object" || Array.isArray(variant)) continue;
          const v = variant as Record<string, unknown>;
          if (v.properties && typeof v.properties === "object" && !Array.isArray(v.properties)) {
            Object.assign(properties, v.properties as Record<string, unknown>);
          }
          if (mergeRequired && Array.isArray(v.required)) {
            for (const item of v.required) if (typeof item === "string") required.add(item);
          }
        }
      }
    
      const s: Record<string, unknown> = {};
      for (const [key, value] of Object.entries(obj)) {
        if (key === "oneOf" || key === "anyOf" || key === "allOf") continue;
        if (key === "type" || key === "properties" || key === "required") continue;
        s[key] = value;
      }
      s.type = "object";
      s.properties = properties;
      if (required.size > 0) s.required = [...required];
      return s;
    }

    toolsToAnthropicFormat 내부는 동일하게 input_schema: normalizeAnthropicInputSchema(t.parameters) 로 연결합니다.

    검증 / Verification

    파일에 적용한 실제 함수 본문을 그대로 실행하여 다음 케이스를 확인했습니다:

    • type 누락 → {type:"object",properties:{}}
    • root anyOf / oneOf → 각 branch properties 병합, required 강제 안 함
    • root allOf → properties + required 병합 (["p","r"])
    • nested anyOf (프로퍼티 내부) → 원형 그대로 보존
    • root union + 여분 키(title, additionalProperties) → 여분 키 유지, 콤비네이터만 제거

    이 상태에서 서브에이전트/브라우저/Codex 툴 호출이 모두 정상 동작합니다. Original title/body should be read together with this correction. Happy to open a PR with this complete version.

  2. Ingwannu commented on Jul 9, 2026

    @Ingwannu
    Owner

    Confirmed valid and opened a fix PR: #76

    What I verified:

    • Current dev was forwarding Anthropic input_schema: t.parameters verbatim.
    • A schema with properties/required but no root type stayed missing type.
    • A root anyOf schema also stayed at the root, matching the corrected report here.

    Fix in #76:

    • Normalize Anthropic tool input_schema to a root type: "object" schema.
    • Ensure properties exists.
    • Flatten root oneOf / anyOf / allOf while preserving nested property schemas.
    • Merge required only for allOf, not oneOf / anyOf.

    Validation:

    • bun test tests/anthropic-tool-schema.test.ts tests/anthropic-empty-content.test.ts tests/kiro-adapter.test.ts -> 38 pass
    • bun run typecheck -> pass
    • bun test ./tests/ -> 1677 pass, 0 fail

    The PR body includes Fixes #75, so this issue should close automatically when the PR is merged.

  3. lidge-jun commented on Jul 9, 2026

    @lidge-jun
    Owner

    Fixed and verified. The Anthropic adapter now normalizes tool input_schema roots so schemas without a top-level type are sent as object schemas, and the regression coverage is in tests/anthropic-tool-schema.test.ts.\n\nVerified locally with:\n- bun test tests/anthropic-tool-schema.test.ts tests/anthropic-empty-content.test.ts tests/kiro-adapter.test.ts\n- bun test tests/multi-agent-compat.test.ts tests/native-model-toggle.test.ts\n- npm run build (gui)\n- bun run typecheck\n\nAlso verified on dev Cross-platform CI: https://github.com/lidge-jun/opencodex/actions/runs/29034128441

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions