Skip to content

fix(translate): bail to non-strict for bare-{} optional tool args#830

Open
Symbiomancer wants to merge 1 commit into
mainfrom
fix/strictify-typeless-object-additionalprops
Open

fix(translate): bail to non-strict for bare-{} optional tool args#830
Symbiomancer wants to merge 1 commit into
mainfrom
fix/strictify-typeless-object-additionalprops

Conversation

@Symbiomancer

Copy link
Copy Markdown
Contributor

Problem

Claude Code sessions routed to an OpenAI Responses-path model (gpt-5.x) were dying on the first turn that carried the Workflow tool:

API Error: 400 Invalid schema for function 'Workflow':
In context=('properties','args','anyOf','0','type','3'),
'additionalProperties' is required to be supplied and to be false.

Workflow.args is a bare {} "any JSON value" optional parameter. #829 addressed an earlier form of this 400 by having makeNullable synthesize a six-type union branch (["string","number","boolean","object","array","null"]) so the tool could stay strict:true. But that union admits "object", and OpenAI strict mode then requires the node to also carry additionalProperties:false — which the synthetic branch lacks. So the request still 400s, just one level deeper (note the error path lands on anyOf.0.type.3, i.e. index 3 = "object" of the synthesized union).

Why not just add additionalProperties:false to the synthetic branch?

Because a bare {} has no valid strict representation:

  • Closing it with additionalProperties:false and no properties constrains args to an empty object only — breaking a parameter whose entire purpose is passing arbitrary JSON.
  • Narrowing the type union to exclude object/array silently drops valid structured args.

An open value is exactly the case strictify is meant to bail on and fall back to non-strict emission (the file's documented fail-open contract). Non-strict emission is already the path for oneOf, unresolved $ref, tuple items, etc., and toolcheck still validates tool calls against the original schema downstream.

Change

  • strictifyNode: when an optional property has no strict-expressible type (!schemaHasStrictType), return ok=false so the whole tool emits non-strict, instead of letting makeNullable synthesize an unrepresentable branch.
  • makeNullable: revert the fix(translate): bail strict-mode strictification for typeless optional properties #829 typeless fallback back to wrapping the node in anyOf + null. That path is now unreachable behind the new guard; the comment says so.
  • Replace TestStrictify_TypelessOptionalGetsExplicitValueType (asserted the now-wrong "stays strict" contract) with TestStrictify_TypelessOptionalBails.

Validation

  • go vet ./internal/translate/ — clean
  • go test ./internal/translate/... — pass (translate + toolcheck)
  • wv mr tc — clean
  • wv mr t (full router suite) — pass

🤖 Generated with Claude Code

#829 fixed the Workflow.args 400 by having makeNullable synthesize a
six-type union branch for a typeless optional, keeping the tool strict.
But that branch admits "object", and OpenAI strict mode then requires the
node to carry additionalProperties:false — which the synthetic branch
lacks — so the request still 400'd, now at
context=('properties','args','anyOf','0','type','3').

A bare {} "any JSON value" has no strict representation: closing it with
additionalProperties:false would forbid the arbitrary keys the parameter
exists to accept, and narrowing the type union would silently drop valid
object/array args. So strictifyNode now bails when an optional property
has no strict-expressible type, and the whole tool falls back to
non-strict emission per this file's documented fail-open contract.
Reverts makeNullable's typeless fallback to wrapping the node, now
unreachable behind the schemaHasStrictType guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @Symbiomancer's task —— View job


Advisory comment-length review posted on the PR diff.

@workweave-bot workweave-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Advisory only — comment-length nits. Won't block merge.

Comment on lines +171 to +178
// An optional property with no strict-expressible type (a bare {}
// "any JSON value", e.g. Workflow.args) cannot be made nullable
// without either admitting "object" — which strict mode then
// requires to carry additionalProperties:false, forbidding the
// arbitrary keys the schema exists to accept — or narrowing the
// value space and silently dropping valid object/array args.
// There is no strict representation of an open value, so bail to
// non-strict emission per this file's fail-open contract.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// An optional property with no strict-expressible type (a bare {}
// "any JSON value", e.g. Workflow.args) cannot be made nullable
// without either admitting "object" — which strict mode then
// requires to carry additionalProperties:false, forbidding the
// arbitrary keys the schema exists to accept — or narrowing the
// value space and silently dropping valid object/array args.
// There is no strict representation of an open value, so bail to
// non-strict emission per this file's fail-open contract.
// A bare {} optional (open value) has no valid strict form: admitting
// "object" requires additionalProperties:false (forbidding arbitrary keys),
// and narrowing to non-object types drops valid structured args. Bail.

8 lines explaining a 2-step constraint; the WHY fits in 3.

Comment on lines +220 to +223
// No type and no anyOf (e.g. bare enum): wrap the node itself. Callers must
// not pass a fully typeless node here — strictifyNode bails on typeless
// optionals before reaching this point, since an open value has no strict
// representation (see the schemaHasStrictType guard in strictifyNode).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// No type and no anyOf (e.g. bare enum): wrap the node itself. Callers must
// not pass a fully typeless node here — strictifyNode bails on typeless
// optionals before reaching this point, since an open value has no strict
// representation (see the schemaHasStrictType guard in strictifyNode).
// No type and no anyOf (e.g. bare enum): wrap node. Typeless optionals
// are rejected by strictifyNode before reaching here.

4 lines; the caller-contract note and cross-reference both fit in 2.

Comment on lines +239 to +243
// Workflow.args is a bare {} "any JSON value" optional. #829 kept it strict by
// synthesizing a 6-type union branch, but that branch admits "object" without
// additionalProperties:false, so OpenAI strict mode still 400'd
// (context=('properties','args','anyOf','0','type','3')). An open value has no
// strict representation, so the whole tool must bail to non-strict emission.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// Workflow.args is a bare {} "any JSON value" optional. #829 kept it strict by
// synthesizing a 6-type union branch, but that branch admits "object" without
// additionalProperties:false, so OpenAI strict mode still 400'd
// (context=('properties','args','anyOf','0','type','3')). An open value has no
// strict representation, so the whole tool must bail to non-strict emission.
// Workflow.args is a bare {} optional; an open value has no strict form
// (#829's 6-type union still 400'd because "object" needs additionalProperties:false).

5 lines; the key WHY fits in 2.

@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown

T-Rex T-Rex Logs

What T-Rex did

  • Ran go vet on internal/translate to validate static analysis, and it completed successfully within the 180-second timeout (exit code 0).
  • Executed go test for internal/translate; both translate packages passed, with exit code 0 under the 180-second timeout.
  • Ran the strictify-focused test for the translate package; TestStrictify completed with exit code 0.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "fix(translate): bail to non-strict for b..." | Re-trigger Greptile

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants