fix(cron): remove OpenAPI 3.0 incompatible JSON Schema keywords from cron tool#61221
Conversation
…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
Greptile SummaryThis PR fixes HTTP 400 errors from Gemini-backed GitHub Copilot API (e.g.
Confidence Score: 4/5Safe 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'.
|
|
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, I considered two approaches:
I went with approach 2. The comment at the top of One thing worth calling out: 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 |
vincentkoc
left a comment
There was a problem hiding this comment.
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.
…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
…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
…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
…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
…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
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 ofcron-tool.ts),type: ["array", "null"](lines 68-69), andtype: ["object", "boolean"]combined withnot: { 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(), andCronFailureAlertSchemahelpers insrc/agents/tools/cron-tool.tsemit raw JSON Schema draft 2020-12 syntax (typearrays,not/constcomposition) that is not valid under the OpenAPI 3.0 subset. The existingnormalizeToolParameterSchema()pipeline only invokescleanSchemaForGemini()whenmodelProvidercontains "google" or "gemini" — butgithub-copilotdoes not match either, so the schema is sent unmodified. Furthermore, even ifcleanSchemaForGemini()were invoked, it lacks handling for thenotkeyword, which would still pass through and trigger the 400.Fix: Rewrite the three schema helpers to emit provider-universal JSON Schema that avoids
typearrays andnot/constentirely.nullableStringSchema()now returnsType.Optional(Type.String(...)),nullableStringArraySchema()returnsType.Optional(Type.Array(Type.String(), ...)), andCronFailureAlertSchemausestype: "object"(scalar) with thefalsesemantics conveyed via the description field. Null/false runtime semantics are unchanged —normalizeCronJob*andcron/service/timer.tsalready handle these values regardless of schema declarations. Additionally,"not"is added toGEMINI_UNSUPPORTED_SCHEMA_KEYWORDSinclean-for-gemini.tsas a defense-in-depth measure so any future schema that introducesnotwill be automatically stripped for Gemini-backed providers.What changed:
src/agents/tools/cron-tool.ts: SimplifiednullableStringSchema()(removedType.Unsafewithtypearray), simplifiednullableStringArraySchema()(removedType.Unsafewithtypearray), rewroteCronFailureAlertSchema(removedtypearray andnot: { const: true }, changed to scalartype: "object", updated description)src/agents/schema/clean-for-gemini.ts: Added"not"toGEMINI_UNSUPPORTED_SCHEMA_KEYWORDSsrc/agents/tools/cron-tool.schema.test.ts: Updated assertions for the new schema shapes; added regression guard that serialized schema contains notypearrays ornotkeywordssrc/agents/schema/clean-for-gemini.test.ts: Added tests fornotkeyword stripping andtypearray collapsingWhat did NOT change (scope boundary):
cron/service/timer.ts,cron/normalize.ts)normalizeToolParameterSchema()provider-detection logic inpi-tools.schema.tscleanSchemaForGeminiWithDefs()core cleaning loop (only the keyword set was extended)Type.Unsafe<string | null>andType.Unsafe<Record<string, unknown> | false>are preserved for runtime type safetyReproduction
github-copilotprovider pointing to a Gemini model (e.g.gemini-2.5-flash-preview)Risk / Mitigation
type: ["string", "null"]to understand thatnullis a valid value foragentId/sessionKeymay stop sendingnull. However, the field descriptions already state "or null to keep it unset" / "or null to clear it", and the runtime (normalizeCronJob*) handlesnullregardless of schema declarations.failureAlertfromtype: ["object", "boolean"]totype: "object"means the schema no longer formally declares thatfalseis accepted. However, the updated description explicitly states "or the boolean value false to disable alerts", and the runtime (timer.ts:308-310) handlesfailureAlert === falseunconditionally.cron-tool.schema.test.tsare 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 inclean-for-gemini.test.tsverify thatnotstripping andtypearray collapsing work correctly.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Fixes #61206