Skip to content

fix(cron): remove OpenAPI 3.0 incompatible JSON Schema keywords from cron tool#61221

Merged
vincentkoc merged 1 commit into
openclaw:mainfrom
openperf:fix/cron-tool-schema-openapi-compat
Apr 5, 2026
Merged

fix(cron): remove OpenAPI 3.0 incompatible JSON Schema keywords from cron tool#61221
vincentkoc merged 1 commit into
openclaw:mainfrom
openperf:fix/cron-tool-schema-openapi-compat

Conversation

@openperf

@openperf openperf commented Apr 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Problem: Sending any message to a Gemini model via github-copilot/gemini-* returns HTTP 400 when the cron tool is enabled (v2026.4.2, commit d74a122). The GitHub Copilot API enforces an OpenAPI 3.0 JSON Schema subset when routing to Gemini backends, and the cron tool schema contains three incompatible constructs: type: ["string", "null"] (lines 59-60 of cron-tool.ts), type: ["array", "null"] (lines 68-69), and type: ["object", "boolean"] combined with not: { const: true } (lines 144-146). The reporter confirmed via mitmproxy binary search that removing the cron tool from the request immediately yields 200 OK.

  • Root Cause: The nullableStringSchema(), nullableStringArraySchema(), and CronFailureAlertSchema helpers in src/agents/tools/cron-tool.ts emit raw JSON Schema draft 2020-12 syntax (type arrays, not/const composition) that is not valid under the OpenAPI 3.0 subset. The existing normalizeToolParameterSchema() pipeline only invokes cleanSchemaForGemini() when modelProvider contains "google" or "gemini" — but github-copilot does not match either, so the schema is sent unmodified. Furthermore, even if cleanSchemaForGemini() were invoked, it lacks handling for the not keyword, which would still pass through and trigger the 400.

  • Fix: Rewrite the three schema helpers to emit provider-universal JSON Schema that avoids type arrays and not/const entirely. nullableStringSchema() now returns Type.Optional(Type.String(...)), nullableStringArraySchema() returns Type.Optional(Type.Array(Type.String(), ...)), and CronFailureAlertSchema uses type: "object" (scalar) with the false semantics conveyed via the description field. Null/false runtime semantics are unchanged — normalizeCronJob* and cron/service/timer.ts already handle these values regardless of schema declarations. Additionally, "not" is added to GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS in clean-for-gemini.ts as a defense-in-depth measure so any future schema that introduces not will be automatically stripped for Gemini-backed providers.

  • What changed:

    • src/agents/tools/cron-tool.ts: Simplified nullableStringSchema() (removed Type.Unsafe with type array), simplified nullableStringArraySchema() (removed Type.Unsafe with type array), rewrote CronFailureAlertSchema (removed type array and not: { const: true }, changed to scalar type: "object", updated description)
    • src/agents/schema/clean-for-gemini.ts: Added "not" to GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS
    • src/agents/tools/cron-tool.schema.test.ts: Updated assertions for the new schema shapes; added regression guard that serialized schema contains no type arrays or not keywords
    • src/agents/schema/clean-for-gemini.test.ts: Added tests for not keyword stripping and type array collapsing
  • What did NOT change (scope boundary):

    • No changes to runtime cron job execution logic (cron/service/timer.ts, cron/normalize.ts)
    • No changes to the normalizeToolParameterSchema() provider-detection logic in pi-tools.schema.ts
    • No changes to cleanSchemaForGeminiWithDefs() core cleaning loop (only the keyword set was extended)
    • No changes to any other tool schemas — only the cron tool is affected
    • No changes to TypeScript types — Type.Unsafe<string | null> and Type.Unsafe<Record<string, unknown> | false> are preserved for runtime type safety

Reproduction

  1. Configure openclaw v2026.4.2 with a github-copilot provider pointing to a Gemini model (e.g. gemini-2.5-flash-preview)
  2. Ensure the cron tool is enabled (default)
  3. Send any message to the model
  4. Observe HTTP 400 response from the Copilot API
  5. After applying this patch, the same request returns 200 OK

Risk / Mitigation

  • Risk: LLMs that previously relied on type: ["string", "null"] to understand that null is a valid value for agentId/sessionKey may stop sending null. However, the field descriptions already state "or null to keep it unset" / "or null to clear it", and the runtime (normalizeCronJob*) handles null regardless of schema declarations.
  • Risk: Changing failureAlert from type: ["object", "boolean"] to type: "object" means the schema no longer formally declares that false is accepted. However, the updated description explicitly states "or the boolean value false to disable alerts", and the runtime (timer.ts:308-310) handles failureAlert === false unconditionally.
  • Mitigation: Existing tests in cron-tool.schema.test.ts are updated to assert the new shapes. A new regression guard test (serialized schema contains no type-array or not/const keywords) prevents future reintroduction of incompatible keywords. Three new tests in clean-for-gemini.test.ts verify that not stripping and type array collapsing work correctly.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Gateway
  • Cron
  • Tool Schema
  • Provider Compatibility

Linked Issue/PR

Fixes #61206

…cron tool

The cron tool schema used type arrays (['string','null']), the 'not'
keyword, and 'const' — all unsupported by the OpenAPI 3.0 subset that
Gemini-backed providers (e.g. GitHub Copilot) enforce. This caused
HTTP 400 for every request when cron was enabled.

Replace type arrays with scalar types, remove not/const from
CronFailureAlertSchema, and add 'not' to the Gemini unsupported
keywords list as defense-in-depth.

Fixes openclaw#61206
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Apr 5, 2026
@greptile-apps

greptile-apps Bot commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes HTTP 400 errors from Gemini-backed GitHub Copilot API (e.g. github-copilot/gemini-2.5-flash-preview) caused by three OpenAPI 3.0-incompatible JSON Schema constructs in the cron tool schema.

  • Root cause fixed: nullableStringSchema() and nullableStringArraySchema() emitted type arrays (["string", "null"], ["array", "null"]), and CronFailureAlertSchema used both a type array (["object", "boolean"]) and not: { const: true } — none of which are valid under the OpenAPI 3.0 subset enforced by the GitHub Copilot API when routing to Gemini backends. Because github-copilot does not match the existing "google"/"gemini" provider detection in normalizeToolParameterSchema(), the schema cleaning pipeline was never invoked.
  • Fix: The three helpers are rewritten to emit scalar type values and standard TypeBox constructs. CronFailureAlertSchema now uses type: "object" (scalar) with the false semantics conveyed via the description field. Runtime null/false handling in cron/service/timer.ts and cron/normalize.ts is unchanged.
  • Defense-in-depth: "not" is added to GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS so future schemas introducing not will be automatically stripped for Gemini-backed providers.
  • Test coverage: A regression guard in cron-tool.schema.test.ts prevents future reintroduction of type arrays or not keywords. Three new tests in clean-for-gemini.test.ts verify not stripping and type-array collapsing behavior.

Confidence Score: 4/5

Safe to merge — targeted schema fix with no runtime logic changes and solid regression test coverage.

The fix correctly removes all three incompatible schema constructs. No Static type extractions exist in the codebase, so the narrowing of helper return types from string | null to string has no practical impact on compiled TypeScript. The only minor finding is a misleading test name that mentions 'const' in a guard that only checks for 'not'.

src/agents/tools/cron-tool.schema.test.ts — one test description mentions 'const' in a guard that only checks for 'not'.

Comments Outside Diff (1)

  1. src/agents/tools/cron-tool.schema.test.ts, line 181-188 (link)

    P2 Misleading test name references 'const'

    The test description says "no type-array or not/const keywords" but the regex only checks for "not". The const keyword is legitimately present in the serialized schema (TypeBox's Type.Literal emits { const: "value" }), and it is handled separately in the Gemini cleaner by converting to enum — it is not stripped. Including "const" in the test label implies a guard that does not exist.

    Consider renaming the test to drop "const":

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/agents/tools/cron-tool.schema.test.ts
    Line: 181-188
    
    Comment:
    **Misleading test name references 'const'**
    
    The test description says "no type-array or not/const keywords" but the regex only checks for `"not"`. The `const` keyword is legitimately present in the serialized schema (TypeBox's `Type.Literal` emits `{ const: "value" }`), and it is handled separately in the Gemini cleaner by converting to `enum` — it is not stripped. Including "const" in the test label implies a guard that does not exist.
    
    Consider renaming the test to drop "const":
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/tools/cron-tool.schema.test.ts
Line: 181-188

Comment:
**Misleading test name references 'const'**

The test description says "no type-array or not/const keywords" but the regex only checks for `"not"`. The `const` keyword is legitimately present in the serialized schema (TypeBox's `Type.Literal` emits `{ const: "value" }`), and it is handled separately in the Gemini cleaner by converting to `enum` — it is not stripped. Including "const" in the test label implies a guard that does not exist.

Consider renaming the test to drop "const":

```suggestion
  it("serialized schema contains no type-array or not keywords", () => {
    const json = JSON.stringify(CronToolSchema);
    // type arrays like ["string","null"] are not valid in OpenAPI 3.0
    expect(json).not.toMatch(/"type"\s*:\s*\[/);
    // "not" composition keyword is not supported by OpenAPI 3.0
    expect(json).not.toMatch(/"not"\s*:\s*\{/);
  });
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(cron): remove OpenAPI 3.0 incompatib..." | Re-trigger Greptile

@openperf

openperf commented Apr 5, 2026

Copy link
Copy Markdown
Member Author

Hey, adding some context on the approach taken here.

The core issue is that the cron tool schema was written in valid JSON Schema draft 2020-12 — type arrays, not/const — but a growing number of providers only accept the OpenAPI 3.0 subset. GitHub Copilot's Gemini routing is the one that caught it this time, but honestly any proxy or gateway that validates against OpenAPI 3.0 would choke on the same constructs.

I considered two approaches:

  1. Patch the provider detection — teach normalizeToolParameterSchema() to also match github-copilot and route it through cleanSchemaForGemini(). This felt like whack-a-mole. The next proxy provider that shows up would hit the exact same wall, and we'd be back here adding another string match.

  2. Fix the schema at the source — make the cron tool emit universally safe schema in the first place. This is what the PR does. The nullable semantics were never enforced at the schema level anyway; normalizeCronJob* and timer.ts handle null/false at runtime regardless of what the schema says. So dropping the type arrays and not keyword doesn't change any actual behavior — it just stops confusing providers that can't parse them.

I went with approach 2. The comment at the top of cron-tool.ts already says "Nested unions are avoided; runtime validation happens in normalizeCronJob*" — so the type arrays and not/const were actually drifting from the original design intent. Cleaning them up brings the schema back in line with that principle.

One thing worth calling out: cleanSchemaForGemini was also missing "not" in its unsupported keywords list. Even if we'd gone with approach 1, the not: { const: true } in CronFailureAlertSchema would have sailed right through the cleaning and still triggered a 400. So that's patched too, as defense-in-depth.

The test updates are intentionally minimal — existing tests are updated to match the new shapes, plus one regression guard that scans the serialized schema for type arrays and not keywords so this class of issue can't sneak back in.

@vincentkoc vincentkoc self-assigned this Apr 5, 2026

@vincentkoc vincentkoc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no findings.

bug-fix evidence bar looks met: issue #61206 has concrete 400 symptom evidence, the root cause is in the cron tool schema shape, and this patch fixes the implicated path plus adds regression coverage.

minor note only: the new cleaner tests mostly lock pre-existing type-array collapse behavior, while the real new defense is stripping not. that is fine.

@vincentkoc
vincentkoc merged commit 8c1ca1f into openclaw:main Apr 5, 2026
30 of 35 checks passed
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…cron tool (openclaw#61221)

The cron tool schema used type arrays (['string','null']), the 'not'
keyword, and 'const' — all unsupported by the OpenAPI 3.0 subset that
Gemini-backed providers (e.g. GitHub Copilot) enforce. This caused
HTTP 400 for every request when cron was enabled.

Replace type arrays with scalar types, remove not/const from
CronFailureAlertSchema, and add 'not' to the Gemini unsupported
keywords list as defense-in-depth.

Fixes openclaw#61206
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…cron tool (openclaw#61221)

The cron tool schema used type arrays (['string','null']), the 'not'
keyword, and 'const' — all unsupported by the OpenAPI 3.0 subset that
Gemini-backed providers (e.g. GitHub Copilot) enforce. This caused
HTTP 400 for every request when cron was enabled.

Replace type arrays with scalar types, remove not/const from
CronFailureAlertSchema, and add 'not' to the Gemini unsupported
keywords list as defense-in-depth.

Fixes openclaw#61206
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…cron tool (openclaw#61221)

The cron tool schema used type arrays (['string','null']), the 'not'
keyword, and 'const' — all unsupported by the OpenAPI 3.0 subset that
Gemini-backed providers (e.g. GitHub Copilot) enforce. This caused
HTTP 400 for every request when cron was enabled.

Replace type arrays with scalar types, remove not/const from
CronFailureAlertSchema, and add 'not' to the Gemini unsupported
keywords list as defense-in-depth.

Fixes openclaw#61206
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…cron tool (openclaw#61221)

The cron tool schema used type arrays (['string','null']), the 'not'
keyword, and 'const' — all unsupported by the OpenAPI 3.0 subset that
Gemini-backed providers (e.g. GitHub Copilot) enforce. This caused
HTTP 400 for every request when cron was enabled.

Replace type arrays with scalar types, remove not/const from
CronFailureAlertSchema, and add 'not' to the Gemini unsupported
keywords list as defense-in-depth.

Fixes openclaw#61206
Nachx639 pushed a commit to Nachx639/clawdbot that referenced this pull request Jun 17, 2026
…cron tool (openclaw#61221)

The cron tool schema used type arrays (['string','null']), the 'not'
keyword, and 'const' — all unsupported by the OpenAPI 3.0 subset that
Gemini-backed providers (e.g. GitHub Copilot) enforce. This caused
HTTP 400 for every request when cron was enabled.

Replace type arrays with scalar types, remove not/const from
CronFailureAlertSchema, and add 'not' to the Gemini unsupported
keywords list as defense-in-depth.

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

Labels

agents Agent runtime and tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: HTTP 400 on github-copilot/gemini-3-flash-preview due to cron tool JSON Schema

2 participants