|
1 | 1 | package claude |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "encoding/base64" |
| 5 | + "strings" |
4 | 6 | "testing" |
5 | 7 |
|
6 | 8 | "github.com/tidwall/gjson" |
@@ -134,74 +136,143 @@ func TestConvertClaudeRequestToCodex_ParallelToolCalls(t *testing.T) { |
134 | 136 | } |
135 | 137 | } |
136 | 138 |
|
137 | | -func TestConvertClaudeRequestToCodex_ThinkingSignatureToEncryptedContent(t *testing.T) { |
138 | | - result := ConvertClaudeRequestToCodex("test-model", []byte(`{ |
| 139 | +func TestConvertClaudeRequestToCodex_AssistantThinkingSignatureToReasoningItem(t *testing.T) { |
| 140 | + signature := validCodexReasoningSignature() |
| 141 | + inputJSON := `{ |
139 | 142 | "model": "claude-3-opus", |
140 | | - "messages": [{ |
141 | | - "role": "assistant", |
142 | | - "content": [ |
143 | | - {"type": "thinking", "thinking": "Internal reasoning.", "signature": "sig_123"}, |
144 | | - {"type": "text", "text": "Visible answer."} |
145 | | - ] |
146 | | - }] |
147 | | - }`), false) |
| 143 | + "messages": [ |
| 144 | + { |
| 145 | + "role": "assistant", |
| 146 | + "content": [ |
| 147 | + { |
| 148 | + "type": "thinking", |
| 149 | + "thinking": "visible summary must not be replayed", |
| 150 | + "signature": "` + signature + `" |
| 151 | + }, |
| 152 | + { |
| 153 | + "type": "text", |
| 154 | + "text": "visible answer" |
| 155 | + } |
| 156 | + ] |
| 157 | + }, |
| 158 | + { |
| 159 | + "role": "user", |
| 160 | + "content": "continue" |
| 161 | + } |
| 162 | + ] |
| 163 | + }` |
| 164 | + |
| 165 | + result := ConvertClaudeRequestToCodex("test-model", []byte(inputJSON), false) |
148 | 166 | resultJSON := gjson.ParseBytes(result) |
149 | 167 | inputs := resultJSON.Get("input").Array() |
150 | | - |
151 | | - if len(inputs) != 2 { |
152 | | - t.Fatalf("got %d input items, want 2. Output: %s", len(inputs), string(result)) |
| 168 | + if len(inputs) != 3 { |
| 169 | + t.Fatalf("got %d input items, want 3. Output: %s", len(inputs), string(result)) |
153 | 170 | } |
154 | 171 |
|
155 | 172 | reasoning := inputs[0] |
156 | 173 | if got := reasoning.Get("type").String(); got != "reasoning" { |
157 | | - t.Fatalf("input[0].type = %q, want %q. Output: %s", got, "reasoning", string(result)) |
| 174 | + t.Fatalf("first input type = %q, want reasoning. Output: %s", got, string(result)) |
158 | 175 | } |
159 | | - if got := reasoning.Get("encrypted_content").String(); got != "sig_123" { |
160 | | - t.Fatalf("encrypted_content = %q, want %q. Output: %s", got, "sig_123", string(result)) |
| 176 | + if got := reasoning.Get("encrypted_content").String(); got != signature { |
| 177 | + t.Fatalf("encrypted_content = %q, want %q", got, signature) |
161 | 178 | } |
162 | | - if got := reasoning.Get("summary.0.type").String(); got != "summary_text" { |
163 | | - t.Fatalf("summary.0.type = %q, want %q. Output: %s", got, "summary_text", string(result)) |
| 179 | + if got := reasoning.Get("summary").Raw; got != "[]" { |
| 180 | + t.Fatalf("summary = %s, want []", got) |
164 | 181 | } |
165 | | - if got := reasoning.Get("summary.0.text").String(); got != "Internal reasoning." { |
166 | | - t.Fatalf("summary.0.text = %q, want %q. Output: %s", got, "Internal reasoning.", string(result)) |
| 182 | + if got := reasoning.Get("content").Raw; got != "null" { |
| 183 | + t.Fatalf("content = %s, want null", got) |
167 | 184 | } |
168 | 185 |
|
169 | | - message := inputs[1] |
170 | | - if got := message.Get("type").String(); got != "message" { |
171 | | - t.Fatalf("input[1].type = %q, want %q. Output: %s", got, "message", string(result)) |
| 186 | + assistantMessage := inputs[1] |
| 187 | + if got := assistantMessage.Get("role").String(); got != "assistant" { |
| 188 | + t.Fatalf("second input role = %q, want assistant. Output: %s", got, string(result)) |
172 | 189 | } |
173 | | - if got := message.Get("role").String(); got != "assistant" { |
174 | | - t.Fatalf("input[1].role = %q, want %q. Output: %s", got, "assistant", string(result)) |
| 190 | + if got := assistantMessage.Get("content.0.type").String(); got != "output_text" { |
| 191 | + t.Fatalf("assistant content type = %q, want output_text", got) |
175 | 192 | } |
176 | | - if got := message.Get("content.0.type").String(); got != "output_text" { |
177 | | - t.Fatalf("content.0.type = %q, want %q. Output: %s", got, "output_text", string(result)) |
| 193 | + if got := assistantMessage.Get("content.0.text").String(); got != "visible answer" { |
| 194 | + t.Fatalf("assistant text = %q, want visible answer", got) |
178 | 195 | } |
179 | | - if got := message.Get("content.0.text").String(); got != "Visible answer." { |
180 | | - t.Fatalf("content.0.text = %q, want %q. Output: %s", got, "Visible answer.", string(result)) |
| 196 | + if strings.Contains(string(result), "visible summary must not be replayed") { |
| 197 | + t.Fatalf("thinking text should not be replayed into Codex input. Output: %s", string(result)) |
181 | 198 | } |
182 | 199 | } |
183 | 200 |
|
184 | | -func TestConvertClaudeRequestToCodex_ThinkingSignatureWithoutText(t *testing.T) { |
185 | | - result := ConvertClaudeRequestToCodex("test-model", []byte(`{ |
186 | | - "model": "claude-3-opus", |
187 | | - "messages": [{ |
188 | | - "role": "assistant", |
189 | | - "content": [{"type": "thinking", "thinking": "", "signature": "sig_empty_text"}] |
190 | | - }] |
191 | | - }`), false) |
192 | | - resultJSON := gjson.ParseBytes(result) |
193 | | - inputs := resultJSON.Get("input").Array() |
194 | | - |
195 | | - if len(inputs) != 1 { |
196 | | - t.Fatalf("got %d input items, want 1. Output: %s", len(inputs), string(result)) |
197 | | - } |
198 | | - if got := inputs[0].Get("type").String(); got != "reasoning" { |
199 | | - t.Fatalf("input[0].type = %q, want %q. Output: %s", got, "reasoning", string(result)) |
200 | | - } |
201 | | - if got := inputs[0].Get("encrypted_content").String(); got != "sig_empty_text" { |
202 | | - t.Fatalf("encrypted_content = %q, want %q. Output: %s", got, "sig_empty_text", string(result)) |
| 201 | +func TestConvertClaudeRequestToCodex_IgnoresNonCodexThinkingSignatures(t *testing.T) { |
| 202 | + tests := []struct { |
| 203 | + name string |
| 204 | + inputJSON string |
| 205 | + }{ |
| 206 | + { |
| 207 | + name: "Ignore user thinking even with Codex-shaped signature", |
| 208 | + inputJSON: `{ |
| 209 | + "model": "claude-3-opus", |
| 210 | + "messages": [ |
| 211 | + { |
| 212 | + "role": "user", |
| 213 | + "content": [ |
| 214 | + { |
| 215 | + "type": "thinking", |
| 216 | + "thinking": "user supplied thinking", |
| 217 | + "signature": "` + validCodexReasoningSignature() + `" |
| 218 | + }, |
| 219 | + { |
| 220 | + "type": "text", |
| 221 | + "text": "hello" |
| 222 | + } |
| 223 | + ] |
| 224 | + } |
| 225 | + ] |
| 226 | + }`, |
| 227 | + }, |
| 228 | + { |
| 229 | + name: "Ignore Anthropic native signature", |
| 230 | + inputJSON: `{ |
| 231 | + "model": "claude-3-opus", |
| 232 | + "messages": [ |
| 233 | + { |
| 234 | + "role": "assistant", |
| 235 | + "content": [ |
| 236 | + { |
| 237 | + "type": "thinking", |
| 238 | + "thinking": "anthropic thinking", |
| 239 | + "signature": "Eo8Canthropic-state" |
| 240 | + }, |
| 241 | + { |
| 242 | + "type": "text", |
| 243 | + "text": "visible answer" |
| 244 | + } |
| 245 | + ] |
| 246 | + } |
| 247 | + ] |
| 248 | + }`, |
| 249 | + }, |
203 | 250 | } |
204 | | - if got := len(inputs[0].Get("summary").Array()); got != 0 { |
205 | | - t.Fatalf("summary length = %d, want 0. Output: %s", got, string(result)) |
| 251 | + |
| 252 | + for _, tt := range tests { |
| 253 | + t.Run(tt.name, func(t *testing.T) { |
| 254 | + result := ConvertClaudeRequestToCodex("test-model", []byte(tt.inputJSON), false) |
| 255 | + if got := countRequestInputItemsByType(result, "reasoning"); got != 0 { |
| 256 | + t.Fatalf("got %d reasoning items, want 0. Output: %s", got, string(result)) |
| 257 | + } |
| 258 | + }) |
206 | 259 | } |
207 | 260 | } |
| 261 | + |
| 262 | +func countRequestInputItemsByType(result []byte, itemType string) int { |
| 263 | + count := 0 |
| 264 | + gjson.GetBytes(result, "input").ForEach(func(_, item gjson.Result) bool { |
| 265 | + if item.Get("type").String() == itemType { |
| 266 | + count++ |
| 267 | + } |
| 268 | + return true |
| 269 | + }) |
| 270 | + return count |
| 271 | +} |
| 272 | + |
| 273 | +func validCodexReasoningSignature() string { |
| 274 | + raw := make([]byte, 1+8+16+16+32) |
| 275 | + raw[0] = 0x80 |
| 276 | + raw[8] = 1 |
| 277 | + return base64.URLEncoding.EncodeToString(raw) |
| 278 | +} |
0 commit comments