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
47 changes: 47 additions & 0 deletions bindings/typescript/src/converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import type { ChatCompletionRequestMessage } from "./generated/openai/ChatComple
import type { InputItem } from "./generated/openai/InputItem";
import type { InputMessage } from "./generated/anthropic/InputMessage";

type GoogleContent = Record<string, unknown>;

type GoogleWasmExports = {
google_contents_to_lingua: (value: unknown) => unknown;
lingua_to_google_contents: (value: unknown) => unknown;
};

// ============================================================================
// Error handling
// ============================================================================
Expand Down Expand Up @@ -262,6 +269,46 @@ export const linguaToAnthropicMessages = createFromLinguaConverter<
InputMessage[]
>(() => getWasm().lingua_to_anthropic_messages, "Anthropic");

// ============================================================================
// Google Conversions
// ============================================================================

/**
* Convert array of Google Content items to Lingua Messages
*
* Returns messages in Lingua's universal format. Accepts contents from:
* - Google GenerateContent requests (`contents` array)
*
* @example
* const lingua = googleContentsToLingua(contents)
*
* @throws {ConversionError} If conversion fails
*/
export const googleContentsToLingua = createToLinguaConverter<Message[]>(
() =>
(getWasm() as unknown as GoogleWasmExports).google_contents_to_lingua,
"Google"
);

/**
* Convert array of Lingua Messages to Google Content items
*
* Returns contents in Google GenerateContent format.
*
* @example
* const contents = linguaToGoogleContents(lingua)
*
* @throws {ConversionError} If conversion fails
*/
export const linguaToGoogleContents = createFromLinguaConverter<
Message[],
GoogleContent[]
>(
() =>
(getWasm() as unknown as GoogleWasmExports).lingua_to_google_contents,
"Google"
);

// ============================================================================
// Processing functions
// ============================================================================
Expand Down
2 changes: 1 addition & 1 deletion bindings/typescript/src/generated/AssistantContentPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ export type AssistantContentPart = { "type": "text" } & TextContentPart | { "typ
* Providers will occasionally return encrypted content for reasoning parts which can
* be useful when you send a follow up message.
*/
encrypted_content?: string, } | { "type": "tool_call", tool_call_id: string, tool_name: string, arguments: ToolCallArguments, provider_options?: ProviderOptions, provider_executed?: boolean, } | { "type": "tool_result", tool_call_id: string, tool_name: string, output: unknown, provider_options?: ProviderOptions, };
encrypted_content?: string, } | { "type": "tool_call", tool_call_id: string, tool_name: string, arguments: ToolCallArguments, encrypted_content?: string, provider_options?: ProviderOptions, provider_executed?: boolean, } | { "type": "tool_result", tool_call_id: string, tool_name: string, output: unknown, provider_options?: ProviderOptions, };
81 changes: 81 additions & 0 deletions crates/lingua/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ fn main() {
generate_test_cases(&workspace);
generate_chat_completions_test_cases(&workspace);
generate_anthropic_test_cases(&workspace);
generate_google_test_cases(&workspace);
}

fn generate_test_cases(workspace: &Path) {
Expand Down Expand Up @@ -274,3 +275,83 @@ fn {test_fn_name}() {{
// Write the generated tests
fs::write(&dest_path, generated_tests).unwrap();
}

fn generate_google_test_cases(workspace: &Path) {
let snapshots_dir = workspace.join("payloads/snapshots");

// Tell cargo to re-run if the snapshots directory changes
println!("cargo:rerun-if-changed={}", snapshots_dir.display());

let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("generated_google_tests.rs");

if !snapshots_dir.exists() {
// Create empty generated tests file if no snapshots directory
fs::write(&dest_path, "// No test cases found").unwrap();
return;
}

let mut generated_tests = String::new();

generated_tests
.push_str("// Auto-generated Google test cases from payloads/snapshots directory\n");
generated_tests.push_str("// DO NOT EDIT - regenerated on each build\n\n");

// Scan for test case directories
if let Ok(entries) = fs::read_dir(&snapshots_dir) {
for entry in entries.flatten() {
let path = entry.path();

if !path.is_dir() {
continue;
}

let test_case_name = path.file_name().unwrap().to_str().unwrap();

// Skip hidden directories and cache files
if test_case_name.starts_with('.') {
continue;
}

// Check if this test case has google directory
let google_dir = path.join("google");
if !google_dir.exists() {
continue;
}

// Generate tests for both turns if they exist
if google_dir.join("request.json").exists() {
let test_fn_name = format!("test_roundtrip_{}_first_turn", test_case_name);
let full_case_name = format!("{}_google_first_turn", test_case_name);

generated_tests.push_str(&format!(
r#"
#[test]
fn {test_fn_name}() {{
super::run_single_test_case("{full_case_name}")
.unwrap_or_else(|e| panic!("Test failed for {full_case_name}: {{}}", e));
}}
"#
));
}

if google_dir.join("followup-request.json").exists() {
let test_fn_name = format!("test_roundtrip_{}_followup_turn", test_case_name);
let full_case_name = format!("{}_google_followup_turn", test_case_name);

generated_tests.push_str(&format!(
r#"
#[test]
fn {test_fn_name}() {{
super::run_single_test_case("{full_case_name}")
.unwrap_or_else(|e| panic!("Test failed for {full_case_name}: {{}}", e));
}}
"#
));
}
}
}

// Write the generated tests
fs::write(&dest_path, generated_tests).unwrap();
}
1 change: 1 addition & 0 deletions crates/lingua/src/processing/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ fn parse_assistant_content(value: &Value) -> Option<AssistantContent> {
tool_call_id,
tool_name,
arguments,
encrypted_content: None,
provider_options: None,
provider_executed: None,
});
Expand Down
4 changes: 4 additions & 0 deletions crates/lingua/src/providers/anthropic/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ impl TryFromLLM<generated::InputMessage> for Message {
arguments: serde_json::to_string(&input)
.unwrap_or_else(|_| "{}".to_string())
.into(),
encrypted_content: None,
provider_options: None,
provider_executed: None,
});
Expand All @@ -302,6 +303,7 @@ impl TryFromLLM<generated::InputMessage> for Message {
arguments: serde_json::to_string(&input)
.unwrap_or_else(|_| "{}".to_string())
.into(),
encrypted_content: None,
provider_options: None,
provider_executed: Some(true), // Mark as server-executed
});
Expand Down Expand Up @@ -859,6 +861,7 @@ impl TryFromLLM<Vec<generated::ContentBlock>> for Vec<Message> {
arguments: serde_json::to_string(&input)
.unwrap_or_else(|_| "{}".to_string())
.into(),
encrypted_content: None,
provider_options: None,
provider_executed: None,
});
Expand All @@ -879,6 +882,7 @@ impl TryFromLLM<Vec<generated::ContentBlock>> for Vec<Message> {
arguments: serde_json::to_string(&input)
.unwrap_or_else(|_| "{}".to_string())
.into(),
encrypted_content: None,
provider_options: None,
provider_executed: Some(true), // Mark as server-executed
});
Expand Down
4 changes: 4 additions & 0 deletions crates/lingua/src/providers/bedrock/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ impl TryFromLLM<BedrockMessage> for Message {
arguments: ToolCallArguments::from(
serde_json::to_string(&tool_use.input).unwrap_or_default(),
),
encrypted_content: None,
provider_options: None,
provider_executed: None,
});
Expand Down Expand Up @@ -305,6 +306,7 @@ impl TryFromLLM<BedrockOutputMessage> for Message {
arguments: ToolCallArguments::from(
serde_json::to_string(&tool_use.input).unwrap_or_default(),
),
encrypted_content: None,
provider_options: None,
provider_executed: None,
});
Expand Down Expand Up @@ -507,6 +509,7 @@ mod tests {
tool_call_id: "tool_123".to_string(),
tool_name: "get_weather".to_string(),
arguments: ToolCallArguments::from(r#"{"location":"SF"}"#.to_string()),
encrypted_content: None,
provider_options: None,
provider_executed: None,
}]),
Expand Down Expand Up @@ -624,6 +627,7 @@ mod tests {
tool_call_id: "tool_123".to_string(),
tool_name: "get_weather".to_string(),
arguments: ToolCallArguments::from(r#"{"location":"SF"}"#.to_string()),
encrypted_content: None,
provider_options: None,
provider_executed: None,
}]),
Expand Down
Loading