Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type CacheControlEphemeral = {
* - `1h`: 1 hour
*
* Defaults to `5m`. See [prompt caching
* pricing](https://docs.claude.com/en/docs/build-with-claude/prompt-caching) for details.
* pricing](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) for
* details.
*/
ttl: Ttl | null, type: CacheControlEphemeralType, };
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
import type { Block } from "./Block";
import type { RequestWebSearchToolResultError } from "./RequestWebSearchToolResultError";

export type InputContentBlockContent = Array<Block> | RequestWebSearchToolResultError | string;
export type InputContentBlockContent = Array<Block> | string | RequestWebSearchToolResultError;
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
import type { RequestCodeExecutionOutputBlock } from "./RequestCodeExecutionOutputBlock";
import type { RequestDocumentBlock } from "./RequestDocumentBlock";

export type RequestWebSearchToolResultErrorContent = Array<RequestCodeExecutionOutputBlock> | RequestDocumentBlock | string;
export type RequestWebSearchToolResultErrorContent = string | Array<RequestCodeExecutionOutputBlock> | RequestDocumentBlock;
2 changes: 1 addition & 1 deletion bindings/typescript/src/generated/anthropic/Source.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SourceSource } from "./SourceSource";

export type Source = SourceSource | string;
export type Source = string | SourceSource;
3 changes: 2 additions & 1 deletion bindings/typescript/src/generated/anthropic/Ttl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* - `1h`: 1 hour
*
* Defaults to `5m`. See [prompt caching
* pricing](https://docs.claude.com/en/docs/build-with-claude/prompt-caching) for details.
* pricing](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) for
* details.
*/
export type Ttl = "1h" | "5m";
88 changes: 86 additions & 2 deletions crates/generate-types/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,88 @@ fn post_process_quicktype_output_for_anthropic(quicktype_output: &str) -> String
];
processed = add_ts_export_to_types(&processed, &entry_points, "anthropic/");

// === Stabilize quicktype's arbitrary type names ===
// Quicktype uses color-themed disambiguation (Purple, Fluffy, Tentacled, etc.) that
// shifts unpredictably when the input schema changes. Normalize to stable names that
// the consuming adapter/converter code expects.

// Phase 1: Park types whose names conflict with Phase 2 targets.
// ORDER MATTERS: replace TentacledType BEFORE introducing any temp name
// containing "TentacledType" as a substring (e.g. QTFIX_TentacledType),
// otherwise the second replace will match the substring inside the temp name.
// Current TentacledType (web_search_result) → old IndigoType
processed = processed.replace("TentacledType", "QTFIX_IndigoType");
// Current FluffyMediaType (pdf, text/plain) → old TentacledMediaType
processed = processed.replace("FluffyMediaType", "QTFIX_TentacledMediaType");
// Current FluffyType (code execution output types) → old TentacledType
processed = processed.replace("FluffyType", "QTFIX_TentacledType");
// Current StickyType (base64, text – response doc source) → old IndecentType
processed = processed.replace("StickyType", "QTFIX_IndecentType");

// Phase 2: Source hierarchy – inner struct first, then outer, then enum.
// Inner source struct: current SourceSource → old SourceSourceClass
processed = processed.replace("SourceSource", "SourceSourceClass");
// Outer source struct definition: current Source → old SourceSource
processed = processed.replace("pub struct Source {", "pub struct SourceSource {");
// Enum variant wrapping the outer source struct
processed = processed.replace(" Source(Source),", " SourceSource(SourceSource),");
// Source enum: current SourceUnion → old Source
processed = processed.replace("SourceUnion", "Source");

// Phase 3: Apply primary type renames.
processed = processed.replace("Base64ImageSourceMediaType", "FluffyMediaType");
processed = processed.replace("Base64ImageSourceType", "FluffyType");
processed = processed.replace("Base64ImageSourceContent", "SourceContent");
processed = processed.replace("RequestDocumentBlockSource", "PurpleSource");
processed = processed.replace("RequestDocumentBlockType", "QTFIX_StickyType");
processed = processed.replace("ResponseDocumentBlockSource", "FluffySource");

// Phase 4: Resolve temp names.
processed = processed.replace("QTFIX_TentacledMediaType", "TentacledMediaType");
processed = processed.replace("QTFIX_TentacledType", "TentacledType");
processed = processed.replace("QTFIX_IndecentType", "IndecentType");
processed = processed.replace("QTFIX_IndigoType", "IndigoType");
processed = processed.replace("QTFIX_StickyType", "StickyType");

// Phase 5: Fix enum variant names.
// Quicktype prefixes String variants with "Purple" to disambiguate untagged unions.
processed = processed.replace("PurpleString(String)", "String(String)");
// ToolChoiceType::None was renamed to TypeNone to avoid Rust keyword clash.
processed = processed.replace(
" #[serde(rename = \"none\")]\n TypeNone,",
" None,",
);

// Phase 6: Request/Response web search result error structs.
// Quicktype shortened these to "Request"/"Response" which are too generic.
processed = processed.replace(
"pub struct Request {",
"pub struct RequestWebSearchToolResultError {",
);
processed = processed.replace(
" Request(Request),",
" RequestWebSearchToolResultError(RequestWebSearchToolResultError),",
);
processed = processed.replace(
"pub request_type:",
"pub request_web_search_tool_result_error_type:",
);
processed = processed.replace(
"pub struct Response {",
"pub struct ResponseWebSearchToolResultError {",
);
processed = processed.replace(
" Response(Response),",
" ResponseWebSearchToolResultError(ResponseWebSearchToolResultError),",
);
processed = processed.replace(
"pub response_type:",
"pub response_web_search_tool_result_error_type:",
);

// Phase 7: Tool type enum rename.
processed = processed.replace("pub enum Type {", "pub enum ToolType {");

// Fix HashMap to serde_json::Map for proper JavaScript object serialization
// This ensures that JSON objects serialize to plain JS objects {} instead of Maps
processed = processed.replace(
Expand Down Expand Up @@ -997,9 +1079,11 @@ pub struct ToolSearchTool {
),
);

// Insert ToolSearchTool variants before the untagged Custom fallback.
// Anchor on the Custom variant which is always last.
with_struct.replace(
" #[serde(rename = \"web_search_20260209\")]\n WebSearch20260209(WebSearchTool20260209),\n\n #[serde(untagged)]",
" #[serde(rename = \"web_search_20260209\")]\n WebSearch20260209(WebSearchTool20260209),\n\n #[serde(rename = \"tool_search_tool_bm25\")]\n ToolSearchToolBm25(ToolSearchTool),\n\n #[serde(rename = \"tool_search_tool_bm25_20251119\")]\n ToolSearchToolBm2520251119(ToolSearchTool),\n\n #[serde(rename = \"tool_search_tool_regex\")]\n ToolSearchToolRegex(ToolSearchTool),\n\n #[serde(rename = \"tool_search_tool_regex_20251119\")]\n ToolSearchToolRegex20251119(ToolSearchTool),\n\n #[serde(untagged)]",
"\n #[serde(untagged)]\n Custom(CustomTool),",
"\n #[serde(rename = \"tool_search_tool_bm25\")]\n ToolSearchToolBm25(ToolSearchTool),\n\n #[serde(rename = \"tool_search_tool_bm25_20251119\")]\n ToolSearchToolBm2520251119(ToolSearchTool),\n\n #[serde(rename = \"tool_search_tool_regex\")]\n ToolSearchToolRegex(ToolSearchTool),\n\n #[serde(rename = \"tool_search_tool_regex_20251119\")]\n ToolSearchToolRegex20251119(ToolSearchTool),\n\n #[serde(untagged)]\n Custom(CustomTool),",
)
}

Expand Down
103 changes: 103 additions & 0 deletions crates/lingua/src/providers/anthropic/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3679,4 +3679,107 @@ mod tests {
assert!(matches!(err, ConvertError::InvalidToolSchema { .. }));
assert!(err.to_string().contains("root type is required"));
}

#[test]
fn new_tool_variants_roundtrip_via_builtin() {
let web_search_json = json!({
"type": "web_search_20260318",
"name": "web_search",
"allowed_domains": ["example.com"]
});
let tool: Tool = serde_json::from_value(web_search_json.clone()).unwrap();
let universal = UniversalTool::from(&tool);
assert!(matches!(
universal.tool_type,
UniversalToolType::Builtin { .. }
));
let roundtripped = Tool::try_from(&universal).unwrap();
let roundtripped_json = serde_json::to_value(&roundtripped).unwrap();
assert_eq!(roundtripped_json["type"], "web_search_20260318");
assert_eq!(roundtripped_json["name"], "web_search");

let web_fetch_json = json!({
"type": "web_fetch_20260318",
"name": "web_fetch",
"max_content_tokens": 4096
});
let tool: Tool = serde_json::from_value(web_fetch_json).unwrap();
let universal = UniversalTool::from(&tool);
let roundtripped = Tool::try_from(&universal).unwrap();
let roundtripped_json = serde_json::to_value(&roundtripped).unwrap();
assert_eq!(roundtripped_json["type"], "web_fetch_20260318");
assert_eq!(roundtripped_json["name"], "web_fetch");
}

#[test]
fn refusal_category_deserializes_known_variants() {
let categories = ["cyber", "bio", "frontier_llm", "reasoning_extraction"];
for category in categories {
let parsed: generated::RefusalCategory =
serde_json::from_value(json!(category)).unwrap();
let serialized = serde_json::to_value(&parsed).unwrap();
assert_eq!(serialized, json!(category));
}
}

#[test]
fn system_enum_deserializes_both_variants() {
let string_system: generated::System = serde_json::from_value(json!("hello")).unwrap();
assert!(matches!(string_system, generated::System::String(_)));

let array_system: generated::System = serde_json::from_value(json!([
{"type": "text", "text": "hello"}
]))
.unwrap();
assert!(matches!(
array_system,
generated::System::RequestTextBlockArray(_)
));
}

#[test]
fn source_enum_deserializes_both_variants() {
let string_source: generated::Source = serde_json::from_value(json!("url_ref")).unwrap();
assert!(matches!(string_source, generated::Source::String(_)));

let struct_source: generated::Source = serde_json::from_value(json!({
"type": "base64",
"data": "abc123",
"media_type": "image/png"
}))
.unwrap();
assert!(matches!(struct_source, generated::Source::SourceSource(_)));
}

#[test]
fn input_content_block_content_deserializes_all_variants() {
let string_variant: generated::InputContentBlockContent =
serde_json::from_value(json!("text content")).unwrap();
assert!(matches!(
string_variant,
generated::InputContentBlockContent::String(_)
));

let array_variant: generated::InputContentBlockContent = serde_json::from_value(json!([
{"type": "text", "text": "hello"}
]))
.unwrap();
assert!(matches!(
array_variant,
generated::InputContentBlockContent::BlockArray(_)
));
}

#[test]
fn tool_choice_type_none_roundtrips() {
let tc = ToolChoice {
tool_choice_type: ToolChoiceType::None,
name: None,
disable_parallel_tool_use: None,
};
let json = serde_json::to_value(&tc).unwrap();
assert_eq!(json["type"], "none");
let parsed: ToolChoice = serde_json::from_value(json).unwrap();
assert_eq!(parsed.tool_choice_type, ToolChoiceType::None);
}
}
Loading
Loading