Skip to content
Draft
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
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type WebSearchToolResultErrorCode = "invalid_tool_input" | "max_uses_exceeded" | "query_too_long" | "too_many_requests" | "unavailable";
export type WebSearchToolResultErrorCode = "invalid_tool_input" | "max_uses_exceeded" | "query_too_long" | "request_too_large" | "too_many_requests" | "unavailable";
82 changes: 12 additions & 70 deletions crates/generate-types/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,12 @@ fn generate_anthropic_types() {
}
};

println!("🔍 Parsing JSON OpenAPI spec...");
println!("🔍 Parsing YAML OpenAPI spec...");

let schema: serde_json::Value = match serde_json::from_str(&anthropic_spec) {
let schema: serde_json::Value = match serde_yaml::from_str(&anthropic_spec) {
Ok(value) => value,
Err(e) => {
println!("❌ Failed to parse Anthropic OpenAPI spec as JSON: {}", e);
println!("❌ Failed to parse Anthropic OpenAPI spec as YAML: {}", e);
return;
}
};
Expand Down Expand Up @@ -424,9 +424,9 @@ fn extract_type_name_from_ref(ref_str: &str) -> Option<String> {
fn generate_anthropic_specific_types(anthropic_spec: &str) {
println!("🏗️ Using quicktype for Anthropic type generation...");

// Extract Anthropic OpenAPI spec
// Extract Anthropic OpenAPI spec (YAML format from Stainless)
let full_spec: serde_json::Value =
serde_json::from_str(anthropic_spec).expect("Failed to parse Anthropic OpenAPI spec");
serde_yaml::from_str(anthropic_spec).expect("Failed to parse Anthropic OpenAPI spec");

// Generate types using quicktype approach
match generate_anthropic_types_with_quicktype(
Expand Down Expand Up @@ -545,8 +545,11 @@ fn preprocess_anthropic_schema_for_separation(spec: &serde_json::Value) -> serde
.and_then(|s| s.as_object())
.unwrap_or(&default_map);

// Step 1: Analyze endpoints to identify request vs response schemas
let (request_schemas, response_schemas) = analyze_anthropic_endpoints(spec);
// Use stable schemas (not Beta) - Beta schemas introduce breaking structural changes
// (new required fields on content blocks). Beta-only fields like `strict` are added
// manually to tool structs in tool_generator.rs instead.
let request_schemas = vec!["CreateMessageParams".to_string()];
let response_schemas = vec!["Message".to_string()];

println!(
"🔍 Identified {} request schemas, {} response schemas",
Expand All @@ -556,7 +559,7 @@ fn preprocess_anthropic_schema_for_separation(spec: &serde_json::Value) -> serde

let mut separated_schemas = serde_json::Map::new();

// Step 2: Recursively add all dependencies for the original schemas.
// Recursively add all dependencies for the schemas.
// Tool schemas will be pulled in automatically via $ref links from CreateMessageParams.
for schema_name in &request_schemas {
add_dependencies_recursively(schema_name, all_schemas, &mut separated_schemas);
Expand All @@ -565,7 +568,7 @@ fn preprocess_anthropic_schema_for_separation(spec: &serde_json::Value) -> serde
add_dependencies_recursively(schema_name, all_schemas, &mut separated_schemas);
}

// Step 3: Now clean the main request/response schemas to remove conflicting fields
// Clean the main request/response schemas to remove conflicting fields
for schema_name in &request_schemas {
if let Some(schema) = separated_schemas.get(schema_name) {
let cleaned_schema = remove_response_fields_from_schema(schema);
Expand All @@ -581,7 +584,6 @@ fn preprocess_anthropic_schema_for_separation(spec: &serde_json::Value) -> serde
}

// Step 5: Create root schema with separated types
// Use a different approach: create separate top-level object types to avoid merging
let root_schema = serde_json::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
Expand All @@ -607,66 +609,6 @@ fn preprocess_anthropic_schema_for_separation(spec: &serde_json::Value) -> serde
root_schema
}

fn analyze_anthropic_endpoints(spec: &serde_json::Value) -> (Vec<String>, Vec<String>) {
let mut request_schemas = Vec::new();
let mut response_schemas = Vec::new();

// Analyze the /v1/messages endpoint
if let Some(paths) = spec.get("paths") {
if let Some(messages_path) = paths.get("/v1/messages") {
if let Some(post_op) = messages_path.get("post") {
// Extract request schema from requestBody
if let Some(request_body) = post_op.get("requestBody") {
if let Some(content) = request_body.get("content") {
if let Some(json_content) = content.get("application/json") {
if let Some(schema) = json_content.get("schema") {
if let Some(schema_ref) = schema.get("$ref") {
if let Some(schema_name) = extract_schema_name_from_ref(
schema_ref.as_str().unwrap_or(""),
) {
request_schemas.push(schema_name);
}
}
}
}
}
}

// Extract response schemas from responses
if let Some(responses) = post_op.get("responses") {
if let Some(success_response) = responses.get("200") {
if let Some(content) = success_response.get("content") {
if let Some(json_content) = content.get("application/json") {
if let Some(schema) = json_content.get("schema") {
if let Some(schema_ref) = schema.get("$ref") {
if let Some(schema_name) = extract_schema_name_from_ref(
schema_ref.as_str().unwrap_or(""),
) {
response_schemas.push(schema_name);
}
}
}
}
}
}
}
}
}
}

println!("🔍 Found request schemas: {:?}", request_schemas);
println!("🔍 Found response schemas: {:?}", response_schemas);

(request_schemas, response_schemas)
}

fn extract_schema_name_from_ref(ref_str: &str) -> Option<String> {
// Extract schema name from "#/components/schemas/CreateMessageParams"
ref_str
.rfind('/')
.map(|last_slash| ref_str[last_slash + 1..].to_string())
}

fn remove_response_fields_from_schema(schema: &serde_json::Value) -> serde_json::Value {
let mut cleaned_schema = schema.clone();

Expand Down
9 changes: 8 additions & 1 deletion crates/generate-types/src/tool_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ fn extract_anthropic_tool_schemas(spec: &serde_json::Value) -> ToolSchemas {
let mut result = ToolSchemas::default();

for (schema_name, schema_def) in schemas {
// Skip beta tools for now - Lingua does not (yet) support Anthropic beta features
// Skip Beta schemas - they introduce breaking structural changes (new required
// fields on content blocks). We add Beta-only fields like `strict` manually below.
if schema_name.starts_with("Beta") {
continue;
}
Expand Down Expand Up @@ -365,6 +366,12 @@ fn generate_tool_struct_direct(
}
}

// Add `strict` field - this is a Beta-only field that we add manually since we use
// stable schemas to avoid breaking structural changes from Beta content blocks
output.push_str(" /// Whether to enforce strict schema validation for tool inputs.\n");
output.push_str(" #[serde(skip_serializing_if = \"Option::is_none\")]\n");
output.push_str(" pub strict: Option<bool>,\n");

output.push_str("}\n");
output
}
Expand Down
54 changes: 33 additions & 21 deletions crates/lingua/src/providers/anthropic/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub struct CreateMessageParams {
/// specifies the absolute maximum number of tokens to generate.
///
/// Different models have different maximum values for this parameter. See
/// [models](https://docs.anthropic.com/en/docs/models-overview) for details.
/// [models](https://docs.claude.com/en/docs/models-overview) for details.
pub max_tokens: i64,
/// Input messages.
///
Expand Down Expand Up @@ -95,10 +95,10 @@ pub struct CreateMessageParams {
/// {"role": "user", "content": [{"type": "text", "text": "Hello, Claude"}]}
/// ```
///
/// See [input examples](https://docs.anthropic.com/en/api/messages-examples).
/// See [input examples](https://docs.claude.com/en/api/messages-examples).
///
/// Note that if you want to include a [system
/// prompt](https://docs.anthropic.com/en/docs/system-prompts), you can use the top-level
/// prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level
/// `system` parameter — there is no `"system"` role for input messages in the Messages API.
///
/// There is a limit of 100,000 messages in a single request.
Expand All @@ -111,7 +111,7 @@ pub struct CreateMessageParams {
/// request.
///
/// Anthropic offers different levels of service for your API requests. See
/// [service-tiers](https://docs.anthropic.com/en/api/service-tiers) for details.
/// [service-tiers](https://docs.claude.com/en/api/service-tiers) for details.
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<ServiceTierEnum>,
/// Custom text sequences that will cause the model to stop generating.
Expand All @@ -127,14 +127,14 @@ pub struct CreateMessageParams {
pub stop_sequences: Option<Vec<String>>,
/// Whether to incrementally stream the response using server-sent events.
///
/// See [streaming](https://docs.anthropic.com/en/api/messages-streaming) for details.
/// See [streaming](https://docs.claude.com/en/api/messages-streaming) for details.
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
/// System prompt.
///
/// A system prompt is a way of providing context and instructions to Claude, such as
/// specifying a particular goal or role. See our [guide to system
/// prompts](https://docs.anthropic.com/en/docs/system-prompts).
/// prompts](https://docs.claude.com/en/docs/system-prompts).
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<System>,
/// Amount of randomness injected into the response.
Expand All @@ -158,9 +158,9 @@ pub struct CreateMessageParams {
///
/// There are two types of tools: **client tools** and **server tools**. The behavior
/// described below applies to client tools. For [server
/// tools](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview\#server-tools),
/// tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\#server-tools),
/// see their individual documentation as each has its own behavior (e.g., the [web search
/// tool](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/web-search-tool)).
/// tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)).
///
/// Each tool definition includes:
///
Expand Down Expand Up @@ -221,7 +221,7 @@ pub struct CreateMessageParams {
/// more generally whenever you want the model to produce a particular JSON structure of
/// output.
///
/// See our [guide](https://docs.anthropic.com/en/docs/tool-use) for more details.
/// See our [guide](https://docs.claude.com/en/docs/tool-use) for more details.
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
/// Only sample from the top K options for each subsequent token.
Expand Down Expand Up @@ -627,6 +627,8 @@ pub enum WebSearchToolResultErrorCode {
MaxUsesExceeded,
#[serde(rename = "query_too_long")]
QueryTooLong,
#[serde(rename = "request_too_large")]
RequestTooLarge,
#[serde(rename = "too_many_requests")]
TooManyRequests,
Unavailable,
Expand Down Expand Up @@ -687,7 +689,7 @@ pub struct Metadata {
/// request.
///
/// Anthropic offers different levels of service for your API requests. See
/// [service-tiers](https://docs.anthropic.com/en/api/service-tiers) for details.
/// [service-tiers](https://docs.claude.com/en/api/service-tiers) for details.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "anthropic/")]
Expand All @@ -701,7 +703,7 @@ pub enum ServiceTierEnum {
///
/// A system prompt is a way of providing context and instructions to Claude, such as
/// specifying a particular goal or role. See our [guide to system
/// prompts](https://docs.anthropic.com/en/docs/system-prompts).
/// prompts](https://docs.claude.com/en/docs/system-prompts).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[serde(untagged)]
#[ts(export_to = "anthropic/")]
Expand All @@ -717,7 +719,7 @@ pub enum System {
/// towards your `max_tokens` limit.
///
/// See [extended
/// thinking](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking) for
/// thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for
/// details.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export_to = "anthropic/")]
Expand All @@ -729,7 +731,7 @@ pub struct Thinking {
/// Must be ≥1024 and less than `max_tokens`.
///
/// See [extended
/// thinking](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking) for
/// thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for
/// details.
pub budget_tokens: Option<i64>,
#[serde(rename = "type")]
Expand Down Expand Up @@ -803,6 +805,9 @@ pub struct CustomTool {
///
/// This is how the tool will be called by the model and in `tool_use` blocks.
pub name: String,
/// Whether to enforce strict schema validation for tool inputs.
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
Expand All @@ -816,6 +821,9 @@ pub struct BashTool20250124 {
///
/// This is how the tool will be called by the model and in `tool_use` blocks.
pub name: String,
/// Whether to enforce strict schema validation for tool inputs.
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
Expand All @@ -829,6 +837,9 @@ pub struct TextEditor20250124 {
///
/// This is how the tool will be called by the model and in `tool_use` blocks.
pub name: String,
/// Whether to enforce strict schema validation for tool inputs.
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
Expand All @@ -842,6 +853,9 @@ pub struct TextEditor20250429 {
///
/// This is how the tool will be called by the model and in `tool_use` blocks.
pub name: String,
/// Whether to enforce strict schema validation for tool inputs.
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
Expand All @@ -858,6 +872,9 @@ pub struct TextEditor20250728 {
///
/// This is how the tool will be called by the model and in `tool_use` blocks.
pub name: String,
/// Whether to enforce strict schema validation for tool inputs.
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
Expand All @@ -884,6 +901,9 @@ pub struct WebSearchTool20250305 {
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(type = "unknown")]
pub user_location: Option<serde_json::Value>,
/// Whether to enforce strict schema validation for tool inputs.
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
Expand Down Expand Up @@ -1179,19 +1199,13 @@ pub enum ContentBlockType {
WebSearchToolResult,
}

/// Object type.
///
/// For Messages, this is always `"message"`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "anthropic/")]
pub enum ResponseType {
Message,
}

/// Conversational role of the generated message.
///
/// This will always be `"assistant"`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "anthropic/")]
Expand All @@ -1207,8 +1221,6 @@ pub enum StopReason {
EndTurn,
#[serde(rename = "max_tokens")]
MaxTokens,
#[serde(rename = "model_context_window_exceeded")]
ModelContextWindowExceeded,
#[serde(rename = "pause_turn")]
PauseTurn,
Refusal,
Expand Down
2 changes: 1 addition & 1 deletion pipelines/generate-provider-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ OpenAPI Spec Download → Automated Type Generation → Build Integration → Va

**Spec sources**:
- **OpenAI**: `https://app.stainless.com/api/spec/documented/openai/openapi.documented.yml` → `specs/openai/openapi.yml`
- **Anthropic**: `https://raw.githubusercontent.com/laszukdawid/anthropic-openapi-spec/main/hosted_spec.json` → `specs/anthropic/openapi.json`
- **Anthropic**: `https://app.stainless.com/api/spec/documented/anthropic/openapi.documented.yml` → `specs/anthropic/openapi.yml`

**What this provides**:
- Official API specification (always up-to-date)
Expand Down
Loading