1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::claude::count_tokens::types as ct;
4use crate::openai::count_tokens::types as ot;
5use crate::transform::claude::utils::claude_model_supports_enabled_thinking;
6
7const CLAUDE_TOOL_USE_ID_PREFIX: &str = "toolu_";
8const CLAUDE_SERVER_TOOL_USE_ID_PREFIX: &str = "srvtoolu_";
9
10fn claude_tool_use_id_matches(id: &str, prefix: &str) -> bool {
11 id.strip_prefix(prefix).is_some_and(|suffix| {
12 !suffix.is_empty()
13 && suffix
14 .chars()
15 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
16 })
17}
18
19fn sanitize_claude_tool_use_suffix(id: &str) -> String {
20 let mut suffix = String::new();
21 let mut previous_was_underscore = false;
22
23 for ch in id.chars() {
24 let mapped = if ch.is_ascii_alphanumeric() || ch == '_' {
25 ch
26 } else {
27 '_'
28 };
29
30 if mapped == '_' {
31 if suffix.is_empty() || previous_was_underscore {
32 continue;
33 }
34 previous_was_underscore = true;
35 } else {
36 previous_was_underscore = false;
37 }
38
39 suffix.push(mapped);
40 }
41
42 while suffix.ends_with('_') {
43 suffix.pop();
44 }
45
46 if suffix.is_empty() {
47 "generated".to_string()
48 } else {
49 suffix
50 }
51}
52
53fn normalize_claude_tool_use_id(
54 mappings: &mut BTreeMap<String, String>,
55 used_ids: &mut BTreeSet<String>,
56 original: String,
57 prefix: &str,
58) -> String {
59 if let Some(existing) = mappings.get(&original) {
60 return existing.clone();
61 }
62
63 let base = if claude_tool_use_id_matches(&original, prefix) {
64 original.clone()
65 } else {
66 let raw_suffix = original.strip_prefix(prefix).unwrap_or(&original);
67 format!("{prefix}{}", sanitize_claude_tool_use_suffix(raw_suffix))
68 };
69
70 let mut candidate = base.clone();
71 let mut suffix = 1usize;
72 while used_ids.contains(&candidate) {
73 candidate = format!("{base}_{suffix}");
74 suffix += 1;
75 }
76
77 mappings.insert(original, candidate.clone());
78 used_ids.insert(candidate.clone());
79 candidate
80}
81
82#[derive(Debug, Default)]
83pub struct ClaudeToolUseIdMapper {
84 tool_use_ids: BTreeMap<String, String>,
85 used_tool_use_ids: BTreeSet<String>,
86 server_tool_use_ids: BTreeMap<String, String>,
87 used_server_tool_use_ids: BTreeSet<String>,
88}
89
90impl ClaudeToolUseIdMapper {
91 pub fn tool_use_id(&mut self, original: impl Into<String>) -> String {
92 normalize_claude_tool_use_id(
93 &mut self.tool_use_ids,
94 &mut self.used_tool_use_ids,
95 original.into(),
96 CLAUDE_TOOL_USE_ID_PREFIX,
97 )
98 }
99
100 pub fn server_tool_use_id(&mut self, original: impl Into<String>) -> String {
101 normalize_claude_tool_use_id(
102 &mut self.server_tool_use_ids,
103 &mut self.used_server_tool_use_ids,
104 original.into(),
105 CLAUDE_SERVER_TOOL_USE_ID_PREFIX,
106 )
107 }
108}
109
110pub use crate::transform::utils::push_message_block;
116
117fn text_block(text: String) -> ct::BetaContentBlockParam {
118 ct::BetaContentBlockParam::Text(ct::BetaTextBlockParam {
119 text,
120 type_: ct::BetaTextBlockType::Text,
121 cache_control: None,
122 citations: None,
123 })
124}
125
126fn parse_data_url_to_image_source(url: &str) -> Option<ct::BetaImageSource> {
127 if !url.starts_with("data:") {
128 return None;
129 }
130
131 let data_index = url.find(";base64,")?;
132 let mime = &url[5..data_index];
133 let data = &url[(data_index + ";base64,".len())..];
134
135 let media_type = match mime {
136 "image/jpeg" => ct::BetaImageMediaType::ImageJpeg,
137 "image/png" => ct::BetaImageMediaType::ImagePng,
138 "image/gif" => ct::BetaImageMediaType::ImageGif,
139 "image/webp" => ct::BetaImageMediaType::ImageWebp,
140 _ => return None,
141 };
142
143 Some(ct::BetaImageSource::Base64(ct::BetaBase64ImageSource {
144 data: data.to_string(),
145 media_type,
146 type_: ct::BetaBase64SourceType::Base64,
147 }))
148}
149
150fn openai_content_to_claude_block(
151 content: ot::ResponseInputContent,
152) -> Option<ct::BetaContentBlockParam> {
153 match content {
154 ot::ResponseInputContent::Text(part) => Some(text_block(part.text)),
155 ot::ResponseInputContent::Image(part) => {
156 if let Some(file_id) = part.file_id {
157 return Some(ct::BetaContentBlockParam::Image(ct::BetaImageBlockParam {
158 source: ct::BetaImageSource::File(ct::BetaFileImageSource {
159 file_id,
160 type_: ct::BetaFileSourceType::File,
161 }),
162 type_: ct::BetaImageBlockType::Image,
163 cache_control: None,
164 }));
165 }
166 if let Some(image_url) = part.image_url {
167 if let Some(source) = parse_data_url_to_image_source(&image_url) {
168 return Some(ct::BetaContentBlockParam::Image(ct::BetaImageBlockParam {
169 source,
170 type_: ct::BetaImageBlockType::Image,
171 cache_control: None,
172 }));
173 }
174 if !image_url.is_empty() {
175 return Some(ct::BetaContentBlockParam::Image(ct::BetaImageBlockParam {
176 source: ct::BetaImageSource::Url(ct::BetaUrlImageSource {
177 type_: ct::BetaUrlSourceType::Url,
178 url: image_url,
179 }),
180 type_: ct::BetaImageBlockType::Image,
181 cache_control: None,
182 }));
183 }
184 }
185 None
186 }
187 ot::ResponseInputContent::File(part) => {
188 if let Some(file_url) = part.file_url {
189 return Some(text_block(file_url));
190 }
191 if let Some(file_id) = part.file_id {
192 return Some(text_block(format!("file_id:{file_id}")));
193 }
194 if let Some(filename) = part.filename {
195 return Some(text_block(filename));
196 }
197 part.file_data.map(text_block)
198 }
199 }
200}
201
202pub fn openai_message_content_to_claude(
203 content: ot::ResponseInputMessageContent,
204) -> ct::BetaMessageContent {
205 match content {
206 ot::ResponseInputMessageContent::Text(text) => ct::BetaMessageContent::Text(text),
207 ot::ResponseInputMessageContent::List(parts) => {
208 let blocks = parts
209 .into_iter()
210 .filter_map(openai_content_to_claude_block)
211 .collect::<Vec<_>>();
212
213 if blocks.is_empty() {
214 ct::BetaMessageContent::Text(String::new())
215 } else {
216 ct::BetaMessageContent::Blocks(blocks)
217 }
218 }
219 }
220}
221
222pub fn response_input_content_to_claude_block(
223 content: ot::ResponseInputContent,
224) -> Option<ct::BetaContentBlockParam> {
225 openai_content_to_claude_block(content)
226}
227
228pub fn response_input_contents_to_tool_result_content(
229 parts: Vec<ot::ResponseInputContent>,
230) -> Option<ct::BetaToolResultBlockParamContent> {
231 let mut text_parts = Vec::new();
232 let mut content_blocks = Vec::new();
233
234 for part in parts {
235 match openai_content_to_claude_block(part)? {
236 ct::BetaContentBlockParam::Text(block) => text_parts.push(block.text),
237 ct::BetaContentBlockParam::Image(block) => {
238 content_blocks.push(ct::BetaToolResultContentBlockParam::Image(block))
239 }
240 ct::BetaContentBlockParam::SearchResult(block) => {
241 content_blocks.push(ct::BetaToolResultContentBlockParam::SearchResult(block))
242 }
243 ct::BetaContentBlockParam::RequestDocument(block) => {
244 content_blocks.push(ct::BetaToolResultContentBlockParam::Document(block))
245 }
246 _ => return None,
247 }
248 }
249
250 if !content_blocks.is_empty() {
251 if !text_parts.is_empty() {
252 content_blocks.insert(
253 0,
254 ct::BetaToolResultContentBlockParam::Text(ct::BetaTextBlockParam {
255 text: text_parts.join("\n"),
256 type_: ct::BetaTextBlockType::Text,
257 cache_control: None,
258 citations: None,
259 }),
260 );
261 }
262 Some(ct::BetaToolResultBlockParamContent::Blocks(content_blocks))
263 } else if text_parts.is_empty() {
264 None
265 } else {
266 Some(ct::BetaToolResultBlockParamContent::Text(
267 text_parts.join("\n"),
268 ))
269 }
270}
271
272pub fn openai_role_to_claude(role: ot::ResponseInputMessageRole) -> ct::BetaMessageRole {
273 match role {
274 ot::ResponseInputMessageRole::Assistant => ct::BetaMessageRole::Assistant,
275 ot::ResponseInputMessageRole::User
276 | ot::ResponseInputMessageRole::System
277 | ot::ResponseInputMessageRole::Developer => ct::BetaMessageRole::User,
278 }
279}
280
281pub fn openai_reasoning_to_claude(
282 reasoning: Option<ot::ResponseReasoning>,
283 max_tokens: Option<u64>,
284 model: Option<&ct::Model>,
285) -> Option<ct::BetaThinkingConfigParam> {
286 const MIN_BUDGET_TOKENS: u64 = 1_024;
287
288 fn effort_ratio(effort: &ot::ResponseReasoningEffort) -> (u64, u64) {
289 match effort {
290 ot::ResponseReasoningEffort::Minimal => (1, 8),
291 ot::ResponseReasoningEffort::Low => (1, 4),
292 ot::ResponseReasoningEffort::Medium => (1, 2),
293 ot::ResponseReasoningEffort::High => (3, 4),
294 ot::ResponseReasoningEffort::XHigh => (19, 20),
295 ot::ResponseReasoningEffort::None => (0, 1),
296 }
297 }
298
299 fn budget_for_effort(effort: &ot::ResponseReasoningEffort, max_tokens: u64) -> Option<u64> {
300 if max_tokens < MIN_BUDGET_TOKENS {
301 return None;
302 }
303 let (num, den) = effort_ratio(effort);
304 let target = max_tokens.saturating_mul(num) / den;
305 let upper = max_tokens.saturating_sub(1);
306 if upper < MIN_BUDGET_TOKENS {
307 return None;
308 }
309 Some(target.clamp(MIN_BUDGET_TOKENS, upper))
310 }
311
312 let effort = reasoning.and_then(|config| config.effort)?;
313 if !claude_model_supports_enabled_thinking(model) {
314 return Some(match effort {
315 ot::ResponseReasoningEffort::None => {
316 ct::BetaThinkingConfigParam::Disabled(ct::BetaThinkingConfigDisabled {
317 type_: ct::BetaThinkingConfigDisabledType::Disabled,
318 })
319 }
320 ot::ResponseReasoningEffort::Minimal
321 | ot::ResponseReasoningEffort::Low
322 | ot::ResponseReasoningEffort::Medium
323 | ot::ResponseReasoningEffort::High
324 | ot::ResponseReasoningEffort::XHigh => {
325 ct::BetaThinkingConfigParam::Adaptive(ct::BetaThinkingConfigAdaptive {
326 type_: ct::BetaThinkingConfigAdaptiveType::Adaptive,
327 display: None,
328 })
329 }
330 });
331 }
332 if !matches!(effort, ot::ResponseReasoningEffort::None)
333 && max_tokens.is_some_and(|tokens| tokens < MIN_BUDGET_TOKENS)
334 {
335 return Some(ct::BetaThinkingConfigParam::Disabled(
336 ct::BetaThinkingConfigDisabled {
337 type_: ct::BetaThinkingConfigDisabledType::Disabled,
338 },
339 ));
340 }
341 Some(match effort {
342 ot::ResponseReasoningEffort::None => {
343 ct::BetaThinkingConfigParam::Disabled(ct::BetaThinkingConfigDisabled {
344 type_: ct::BetaThinkingConfigDisabledType::Disabled,
345 })
346 }
347 ot::ResponseReasoningEffort::Minimal
348 | ot::ResponseReasoningEffort::Low
349 | ot::ResponseReasoningEffort::Medium
350 | ot::ResponseReasoningEffort::High
351 | ot::ResponseReasoningEffort::XHigh => {
352 if let Some(max_tokens) = max_tokens {
353 match budget_for_effort(&effort, max_tokens) {
354 Some(budget_tokens) => {
355 ct::BetaThinkingConfigParam::Enabled(ct::BetaThinkingConfigEnabled {
356 budget_tokens,
357 type_: ct::BetaThinkingConfigEnabledType::Enabled,
358 display: None,
359 })
360 }
361 None => ct::BetaThinkingConfigParam::Disabled(ct::BetaThinkingConfigDisabled {
362 type_: ct::BetaThinkingConfigDisabledType::Disabled,
363 }),
364 }
365 } else {
366 ct::BetaThinkingConfigParam::Adaptive(ct::BetaThinkingConfigAdaptive {
367 type_: ct::BetaThinkingConfigAdaptiveType::Adaptive,
368 display: None,
369 })
370 }
371 }
372 })
373}
374
375pub fn parallel_disable(parallel_tool_calls: Option<bool>) -> Option<bool> {
376 parallel_tool_calls.map(|enabled| !enabled)
377}
378
379pub fn openai_tool_choice_to_claude(
380 tool_choice: Option<ot::ResponseToolChoice>,
381 disable_parallel_tool_use: Option<bool>,
382) -> Option<ct::BetaToolChoice> {
383 match tool_choice {
384 Some(ot::ResponseToolChoice::Options(ot::ResponseToolChoiceOptions::Auto)) => {
385 Some(ct::BetaToolChoice::Auto(ct::BetaToolChoiceAuto {
386 type_: ct::BetaToolChoiceAutoType::Auto,
387 disable_parallel_tool_use,
388 }))
389 }
390 Some(ot::ResponseToolChoice::Options(ot::ResponseToolChoiceOptions::Required)) => {
391 Some(ct::BetaToolChoice::Any(ct::BetaToolChoiceAny {
392 type_: ct::BetaToolChoiceAnyType::Any,
393 disable_parallel_tool_use,
394 }))
395 }
396 Some(ot::ResponseToolChoice::Options(ot::ResponseToolChoiceOptions::None)) => {
397 Some(ct::BetaToolChoice::None(ct::BetaToolChoiceNone {
398 type_: ct::BetaToolChoiceNoneType::None,
399 }))
400 }
401 Some(ot::ResponseToolChoice::Function(tool)) => {
402 Some(ct::BetaToolChoice::Tool(ct::BetaToolChoiceTool {
403 name: tool.name,
404 type_: ct::BetaToolChoiceToolType::Tool,
405 disable_parallel_tool_use,
406 }))
407 }
408 Some(ot::ResponseToolChoice::Custom(tool)) => {
409 Some(ct::BetaToolChoice::Tool(ct::BetaToolChoiceTool {
410 name: tool.name,
411 type_: ct::BetaToolChoiceToolType::Tool,
412 disable_parallel_tool_use,
413 }))
414 }
415 Some(ot::ResponseToolChoice::Mcp(tool)) => {
416 if let Some(name) = tool.name {
417 Some(ct::BetaToolChoice::Tool(ct::BetaToolChoiceTool {
418 name,
419 type_: ct::BetaToolChoiceToolType::Tool,
420 disable_parallel_tool_use,
421 }))
422 } else {
423 Some(ct::BetaToolChoice::Any(ct::BetaToolChoiceAny {
424 type_: ct::BetaToolChoiceAnyType::Any,
425 disable_parallel_tool_use,
426 }))
427 }
428 }
429 Some(ot::ResponseToolChoice::Allowed(choice)) => match choice.mode {
430 ot::ResponseToolChoiceAllowedMode::Auto => {
431 Some(ct::BetaToolChoice::Auto(ct::BetaToolChoiceAuto {
432 type_: ct::BetaToolChoiceAutoType::Auto,
433 disable_parallel_tool_use,
434 }))
435 }
436 ot::ResponseToolChoiceAllowedMode::Required => {
437 Some(ct::BetaToolChoice::Any(ct::BetaToolChoiceAny {
438 type_: ct::BetaToolChoiceAnyType::Any,
439 disable_parallel_tool_use,
440 }))
441 }
442 },
443 Some(ot::ResponseToolChoice::Types(tool)) => {
444 let name = match tool.type_ {
445 ot::ResponseToolChoiceBuiltinType::FileSearch => "tool_search_tool_bm25",
446 ot::ResponseToolChoiceBuiltinType::Computer
447 | ot::ResponseToolChoiceBuiltinType::ComputerUsePreview
448 | ot::ResponseToolChoiceBuiltinType::ComputerUse => "computer",
449 ot::ResponseToolChoiceBuiltinType::WebSearchPreview
450 | ot::ResponseToolChoiceBuiltinType::WebSearchPreview20250311 => "web_search",
451 ot::ResponseToolChoiceBuiltinType::CodeInterpreter => "code_execution",
452 ot::ResponseToolChoiceBuiltinType::ImageGeneration => {
453 return Some(ct::BetaToolChoice::Any(ct::BetaToolChoiceAny {
454 type_: ct::BetaToolChoiceAnyType::Any,
455 disable_parallel_tool_use,
456 }));
457 }
458 };
459 Some(ct::BetaToolChoice::Tool(ct::BetaToolChoiceTool {
460 name: name.to_string(),
461 type_: ct::BetaToolChoiceToolType::Tool,
462 disable_parallel_tool_use,
463 }))
464 }
465 Some(ot::ResponseToolChoice::ApplyPatch(_)) => {
466 Some(ct::BetaToolChoice::Tool(ct::BetaToolChoiceTool {
467 name: "str_replace_based_edit_tool".to_string(),
468 type_: ct::BetaToolChoiceToolType::Tool,
469 disable_parallel_tool_use,
470 }))
471 }
472 Some(ot::ResponseToolChoice::Shell(_)) => {
473 Some(ct::BetaToolChoice::Tool(ct::BetaToolChoiceTool {
474 name: "bash".to_string(),
475 type_: ct::BetaToolChoiceToolType::Tool,
476 disable_parallel_tool_use,
477 }))
478 }
479 None => None,
480 }
481}
482
483pub fn mcp_allowed_tools_to_configs(
484 allowed_tools: Option<&ot::ResponseMcpAllowedTools>,
485) -> Option<BTreeMap<String, ct::BetaMcpToolConfig>> {
486 let names = match allowed_tools {
487 Some(ot::ResponseMcpAllowedTools::ToolNames(names)) => names.clone(),
488 Some(ot::ResponseMcpAllowedTools::Filter(filter)) => {
489 filter.tool_names.clone().unwrap_or_default()
490 }
491 None => Vec::new(),
492 };
493
494 let mut configs = BTreeMap::new();
495 for name in names {
496 configs.insert(
497 name,
498 ct::BetaMcpToolConfig {
499 defer_loading: None,
500 enabled: Some(true),
501 },
502 );
503 }
504
505 if configs.is_empty() {
506 None
507 } else {
508 Some(configs)
509 }
510}
511
512pub fn openai_mcp_tool_to_server(
513 tool: &ot::ResponseMcpTool,
514) -> Option<ct::BetaRequestMcpServerUrlDefinition> {
515 let url = tool.server_url.clone()?;
516 let allowed_tools = match &tool.allowed_tools {
517 Some(ot::ResponseMcpAllowedTools::ToolNames(names)) => Some(names.clone()),
518 Some(ot::ResponseMcpAllowedTools::Filter(filter)) => filter.tool_names.clone(),
519 None => None,
520 };
521
522 Some(ct::BetaRequestMcpServerUrlDefinition {
523 name: tool.server_label.clone(),
524 type_: ct::BetaRequestMcpServerType::Url,
525 url,
526 authorization_token: tool.authorization.clone(),
527 tool_configuration: Some(ct::BetaRequestMcpServerToolConfiguration {
528 allowed_tools,
529 enabled: Some(true),
530 }),
531 })
532}
533
534pub fn tool_from_function(tool: ot::ResponseFunctionTool) -> ct::BetaToolUnion {
535 let input_schema = function_parameters_to_tool_input_schema(tool.parameters);
536 ct::BetaToolUnion::Custom(ct::BetaTool {
537 input_schema,
538 name: tool.name,
539 common: ct::BetaToolCommonFields {
540 strict: tool.strict,
541 ..ct::BetaToolCommonFields::default()
542 },
543 description: tool.description,
544 eager_input_streaming: None,
545 type_: None,
546 })
547}
548
549fn function_parameters_to_tool_input_schema(
550 mut parameters: ot::JsonObject,
551) -> ct::BetaToolInputSchema {
552 let required = parameters.remove("required").and_then(|value| match value {
553 serde_json::Value::Array(items) => Some(
554 items
555 .iter()
556 .filter_map(|item| item.as_str().map(ToOwned::to_owned))
557 .collect::<Vec<_>>(),
558 )
559 .filter(|items| !items.is_empty()),
560 _ => None,
561 });
562
563 let properties = parameters
564 .remove("properties")
565 .as_ref()
566 .and_then(json_object_to_btree);
567
568 let _ = parameters.remove("type");
570
571 let mut extra_fields = parameters;
573
574 let properties = properties.or_else(|| {
575 let fallback_keys = extra_fields
576 .iter()
577 .filter(|(key, _)| !is_json_schema_keyword(key))
578 .map(|(key, _)| key.clone())
579 .collect::<Vec<_>>();
580
581 if fallback_keys.is_empty() {
582 return None;
583 }
584
585 let fallback = fallback_keys
586 .iter()
587 .filter_map(|key| extra_fields.remove(key).map(|value| (key.clone(), value)))
588 .collect::<ct::JsonObject>();
589
590 if fallback.is_empty() {
591 None
592 } else {
593 Some(fallback)
594 }
595 });
596
597 ct::BetaToolInputSchema {
598 type_: ct::BetaToolInputSchemaType::Object,
599 properties,
600 required,
601 extra_fields,
602 }
603}
604
605fn is_json_schema_keyword(key: &str) -> bool {
606 matches!(
607 key,
608 "$schema"
609 | "$id"
610 | "$defs"
611 | "definitions"
612 | "$ref"
613 | "type"
614 | "properties"
615 | "required"
616 | "additionalProperties"
617 | "patternProperties"
618 | "propertyNames"
619 | "unevaluatedProperties"
620 | "items"
621 | "prefixItems"
622 | "contains"
623 | "minContains"
624 | "maxContains"
625 | "allOf"
626 | "anyOf"
627 | "oneOf"
628 | "not"
629 | "if"
630 | "then"
631 | "else"
632 | "dependentSchemas"
633 | "dependentRequired"
634 | "const"
635 | "enum"
636 | "format"
637 | "default"
638 | "title"
639 | "description"
640 | "examples"
641 | "readOnly"
642 | "writeOnly"
643 | "deprecated"
644 | "nullable"
645 | "minimum"
646 | "maximum"
647 | "exclusiveMinimum"
648 | "exclusiveMaximum"
649 | "multipleOf"
650 | "minLength"
651 | "maxLength"
652 | "pattern"
653 | "minItems"
654 | "maxItems"
655 | "uniqueItems"
656 | "minProperties"
657 | "maxProperties"
658 | "contentEncoding"
659 | "contentMediaType"
660 | "contentSchema"
661 )
662}
663
664fn json_object_to_btree(value: &serde_json::Value) -> Option<ct::JsonObject> {
665 let serde_json::Value::Object(map) = value else {
666 return None;
667 };
668 Some(
669 map.iter()
670 .map(|(key, value)| (key.clone(), value.clone()))
671 .collect::<ct::JsonObject>(),
672 )
673}
674
675#[cfg(test)]
676mod tests {
677 use super::openai_reasoning_to_claude;
678 use crate::claude::count_tokens::types as ct;
679 use crate::openai::count_tokens::types as ot;
680
681 fn reasoning(effort: ot::ResponseReasoningEffort) -> ot::ResponseReasoning {
682 ot::ResponseReasoning {
683 effort: Some(effort),
684 generate_summary: None,
685 summary: None,
686 }
687 }
688
689 #[test]
690 fn opus_47_reasoning_uses_adaptive_thinking_instead_of_enabled_budget() {
691 let thinking = openai_reasoning_to_claude(
692 Some(reasoning(ot::ResponseReasoningEffort::High)),
693 Some(8_192),
694 Some(&ct::Model::Known(ct::ModelKnown::ClaudeOpus47)),
695 )
696 .expect("thinking config");
697
698 assert!(matches!(thinking, ct::BetaThinkingConfigParam::Adaptive(_)));
699 }
700
701 #[test]
702 fn older_models_keep_enabled_thinking_budget_mapping() {
703 let thinking = openai_reasoning_to_claude(
704 Some(reasoning(ot::ResponseReasoningEffort::High)),
705 Some(8_192),
706 Some(&ct::Model::Known(ct::ModelKnown::ClaudeOpus46)),
707 )
708 .expect("thinking config");
709
710 assert!(matches!(thinking, ct::BetaThinkingConfigParam::Enabled(_)));
711 }
712}