fix(provider): preserve optional Responses tool arguments (#164)
- commit
- 01c3d318cbb7f456313cfc27a29c0b668580428a
- author
- bnema · · 2026-08-30 16:06 UTC
- committer
- GitHub · · 2026-08-30 16:06 UTC
- parent
- ddb26d4
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown - refs
- tag: v0.27.0
2 files · +32 -0
M packages/provider/openai_codex.go +3 -0
| @@ -174,6 +174,9 @@ type codexTool struct { | |||
| 174 | 174 | Name string `json:"name"` | |
| 175 | 175 | Description string `json:"description,omitempty"` | |
| 176 | 176 | Parameters json.RawMessage `json:"parameters"` | |
| 177 | + | // Responses otherwise attempts to normalize schemas into strict mode, | |
| 178 | + | // which makes non-nullable optional properties required model output. | |
| 179 | + | Strict bool `json:"strict"` | |
| 177 | 180 | } | |
| 178 | 181 | ||
| 179 | 182 | type codexReasoningConfig struct { | |
| @@ -174,6 +174,9 @@ type codexTool struct { | |||||
| 174 | Name string `json:"name"` | 174 | Name string `json:"name"` | ||
| 175 | Description string `json:"description,omitempty"` | 175 | Description string `json:"description,omitempty"` | ||
| 176 | Parameters json.RawMessage `json:"parameters"` | 176 | Parameters json.RawMessage `json:"parameters"` | ||
| 177 | + | // Responses otherwise attempts to normalize schemas into strict mode, | |||
| 178 | + | // which makes non-nullable optional properties required model output. | |||
| 179 | + | Strict bool `json:"strict"` | |||
| 177 | } | 180 | } | ||
| 178 | 181 | ||||
| 179 | type codexReasoningConfig struct { | 182 | type codexReasoningConfig struct { | ||
M packages/provider/openai_codex_test.go +29 -0
| @@ -146,6 +146,35 @@ func TestGPT56UsesNativeMaxReasoningEffort(t *testing.T) { | |||
| 146 | 146 | } | |
| 147 | 147 | } | |
| 148 | 148 | ||
| 149 | + | func TestResponsesToolsExplicitlyDisableStrictSchemaNormalization(t *testing.T) { | |
| 150 | + | c := newOpenAICodexClient("token", "acct", "") | |
| 151 | + | wire, err := c.buildRequest(Request{ | |
| 152 | + | Model: "gpt-5.6-sol", | |
| 153 | + | Tools: []Tool{{ | |
| 154 | + | Name: "subagent_spawn", | |
| 155 | + | Schema: json.RawMessage(`{ | |
| 156 | + | "type":"object", | |
| 157 | + | "properties":{ | |
| 158 | + | "task":{"type":"string"}, | |
| 159 | + | "budget_ratio":{"type":"number"}, | |
| 160 | + | "budget_tokens":{"type":"integer"} | |
| 161 | + | }, | |
| 162 | + | "required":["task"] | |
| 163 | + | }`), | |
| 164 | + | }}, | |
| 165 | + | }) | |
| 166 | + | if err != nil { | |
| 167 | + | t.Fatal(err) | |
| 168 | + | } | |
| 169 | + | encoded, err := json.Marshal(wire.Tools) | |
| 170 | + | if err != nil { | |
| 171 | + | t.Fatal(err) | |
| 172 | + | } | |
| 173 | + | if !bytes.Contains(encoded, []byte(`"strict":false`)) { | |
| 174 | + | t.Fatalf("responses tool omitted strict opt-out: %s", encoded) | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | ||
| 149 | 178 | func TestResponsesRequestUsesExplicitReasoningLevelMap(t *testing.T) { | |
| 150 | 179 | SetLiveModels([]Model{{ | |
| 151 | 180 | Provider: "custom-responses", | |
| @@ -146,6 +146,35 @@ func TestGPT56UsesNativeMaxReasoningEffort(t *testing.T) { | |||||
| 146 | } | 146 | } | ||
| 147 | } | 147 | } | ||
| 148 | 148 | ||||
| 149 | + | func TestResponsesToolsExplicitlyDisableStrictSchemaNormalization(t *testing.T) { | |||
| 150 | + | c := newOpenAICodexClient("token", "acct", "") | |||
| 151 | + | wire, err := c.buildRequest(Request{ | |||
| 152 | + | Model: "gpt-5.6-sol", | |||
| 153 | + | Tools: []Tool{{ | |||
| 154 | + | Name: "subagent_spawn", | |||
| 155 | + | Schema: json.RawMessage(`{ | |||
| 156 | + | "type":"object", | |||
| 157 | + | "properties":{ | |||
| 158 | + | "task":{"type":"string"}, | |||
| 159 | + | "budget_ratio":{"type":"number"}, | |||
| 160 | + | "budget_tokens":{"type":"integer"} | |||
| 161 | + | }, | |||
| 162 | + | "required":["task"] | |||
| 163 | + | }`), | |||
| 164 | + | }}, | |||
| 165 | + | }) | |||
| 166 | + | if err != nil { | |||
| 167 | + | t.Fatal(err) | |||
| 168 | + | } | |||
| 169 | + | encoded, err := json.Marshal(wire.Tools) | |||
| 170 | + | if err != nil { | |||
| 171 | + | t.Fatal(err) | |||
| 172 | + | } | |||
| 173 | + | if !bytes.Contains(encoded, []byte(`"strict":false`)) { | |||
| 174 | + | t.Fatalf("responses tool omitted strict opt-out: %s", encoded) | |||
| 175 | + | } | |||
| 176 | + | } | |||
| 177 | + | ||||
| 149 | func TestResponsesRequestUsesExplicitReasoningLevelMap(t *testing.T) { | 178 | func TestResponsesRequestUsesExplicitReasoningLevelMap(t *testing.T) { | ||
| 150 | SetLiveModels([]Model{{ | 179 | SetLiveModels([]Model{{ | ||
| 151 | Provider: "custom-responses", | 180 | Provider: "custom-responses", | ||