Skip to content
Closed
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
15 changes: 10 additions & 5 deletions dsl/extdeps/llm/anthropic.dag
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,17 @@ type AnthropicMessages200ContentBlock
// by `server_tool_input_payloads_unmodeled`.
type AnthropicServerToolName = WebSearch | WebFetch | CodeExecution | BashCodeExecution | TextEditorCodeExecution | ToolSearchToolRegex | ToolSearchToolBm25 | Advisor

// 🟢 TERMINAL provider-domain enum for Anthropic Messages `usage.service_tier`.
// Anthropic documents this slot as a closed tier token (`standard`, `priority`,
// `batch`); the emitter uses snake_case string serde matching `AnthropicStopReason`.
type AnthropicMessages200ServiceTier = Standard | Priority | Batch

type AnthropicMessages200Usage {
input_tokens: Int
output_tokens: Int
cache_creation_input_tokens: Int?
cache_read_input_tokens: Int?
service_tier: String?
service_tier: AnthropicMessages200ServiceTier?
}

// Anthropic Messages 200 response role. The API currently returns only
Expand Down Expand Up @@ -212,12 +217,11 @@ type AnthropicMessages200Body {
}

data structural_coverage_gap_anthropic_messages_200_residual: List<String> = [
"closure:anthropic_messages_200_residual|AnthropicMessages200Body|json_pending:container|AnthropicMessages200ContentBlock.MessagesWebSearchToolResultBlock.content|AnthropicMessages200ContentBlock.MessagesWebSearchToolResultBlock.caller|json_pending:web_search_tool_result_payload|trigger:container-management response surface plus web search result/error payload coproduct plus typed caller carrier for web search tool result",
"closure:anthropic_messages_200_residual|AnthropicMessages200Body|opaque_per_api_spec:container|reason:messages_200_response_extension_field_without_closed_public_schema|AnthropicMessages200ContentBlock.MessagesWebSearchToolResultBlock.content|AnthropicMessages200ContentBlock.MessagesWebSearchToolResultBlock.caller|json_pending:web_search_tool_result_payload|trigger:web_search_tool_result_payload_coproduct_plus_typed_caller_carrier",
"closure:anthropic_messages_200_residual|AnthropicMessages200ContentBlock.MessagesToolUseBlock|residue:client_tool_name_input_and_optional_caller_payloads_not_bound_to_declared_tool_schema|trigger:tool_use response block references declared AnthropicToolDefinition through typed caller carrier and input payload schema",
"closure:anthropic_messages_200_residual|AnthropicMessages200ContentBlock.MessagesServerToolUseBlock|residue:server_tool_input_and_optional_caller_payloads_unmodeled|trigger:fold server_tool_use name and input into a server-tool-use coproduct keyed by AnthropicServerToolName",
"closure:anthropic_messages_200_residual|AnthropicServerToolName|external_spec_watch:server_tool_use_name_set|trigger:Anthropic publishes additional server_tool_use.name values beyond the generated SDK literal set",
"closure:anthropic_messages_200_residual|AnthropicMessages200ContentBlock|variant_pending:web_fetch_tool_result|variant_pending:code_execution_tool_result|variant_pending:bash_code_execution_tool_result|variant_pending:text_editor_code_execution_tool_result|variant_pending:tool_search_tool_result|variant_pending:advisor_tool_result|variant_pending:mcp_tool_use|variant_pending:mcp_tool_result|variant_pending:container_upload|trigger:Anthropic beta/server-tool content-block surface modeled structurally",
"closure:anthropic_messages_200_residual|llm.Anthropic.Messages.output.content|blocked:variant_aware_typed_rest_response_projection|path:content/0/text|requires_arm:AnthropicMessages200ContentBlock.MessagesTextBlock|trigger:structural projection metadata carrier lands"
"closure:anthropic_messages_200_residual|AnthropicMessages200ContentBlock|variant_pending:web_fetch_tool_result|variant_pending:code_execution_tool_result|variant_pending:bash_code_execution_tool_result|variant_pending:text_editor_code_execution_tool_result|variant_pending:tool_search_tool_result|variant_pending:advisor_tool_result|variant_pending:mcp_tool_use|variant_pending:mcp_tool_result|variant_pending:container_upload|trigger:Anthropic beta/server-tool content-block surface modeled structurally"
]

// Request wire: Messages API expects string `role` + flat `content[]` objects
Expand All @@ -229,7 +233,8 @@ data structural_coverage_gap_anthropic_messages_200_residual: List<String> = [
data rest_request_wire_serde_alignment_receipt: List<String> = [
"closure:rest_request_wire_serde_alignment|llm.Anthropic.Messages|AnthropicChatMessage|paid:role_internally_tagged_object_user_assistant",
"closure:rest_request_wire_serde_alignment|llm.Anthropic.Messages|AnthropicUserContentBlock|paid:type_internally_tagged_object_text_tool_result",
"closure:rest_request_wire_serde_alignment|llm.Anthropic.Messages|AnthropicAssistantContentBlock|paid:type_internally_tagged_object_text_tool_use"
"closure:rest_request_wire_serde_alignment|llm.Anthropic.Messages|AnthropicAssistantContentBlock|paid:type_internally_tagged_object_text_tool_use",
"closure:rest_request_wire_serde_alignment|llm.Anthropic.Messages|AnthropicMessages200ContentBlock|paid:messages_200_content_block_internally_tagged_object"
]

data structural_coverage_gap_anthropic_tool_result_content_wire_shape: List<String> = [
Expand Down
86 changes: 85 additions & 1 deletion src/v2/05_emit_rust.dag
Original file line number Diff line number Diff line change
Expand Up @@ -5276,6 +5276,90 @@ fn wire_child_for_segment(wire_node: Node, seg: String, source_indices: Map<Stri
}
}

// REST typed-response projection: when the wire path names a record field on a
// coproduct arm (e.g. `content/0/text` on `List<AnthropicMessages200ContentBlock>`),
// struct-style `(...).text` projection fails because the leaf is a Disj. If
// exactly one variant payload carries a field whose wire/authored key matches
// `seg`, emit a `match &*(scrutinee) { Enum::Variant { field, .. } => field.clone(), ... }`.
fn payload_field_child_matching_seg(payload: Node, seg: String, source_indices: Map<String, NewlineIndex>) -> Node? {
if !is_product_type(n: payload) {
none
} else {
match payload.children |> filter(f => {
let authored = authored_name_at(source_indices: source_indices, node: f)
let wire_key = match child_from_key(ch: f, source_indices: source_indices) {
Some { value: k } => k
None => authored
}
wire_key == seg
}) |> first {
Some { value: fch } => Some { value: fch }
None => none
}
}
}

fn try_advance_wire_disjunct_field(
state: WirePathProjection,
seg: String,
full_path: String,
source_indices: Map<String, NewlineIndex>
) -> WirePathProjection {
match state.node {
Some { value: n } => {
let normed = normalize_access_type_node(n: n)
if !is_coproduct_type(n: normed) {
WirePathProjection { expr: state.expr, node: none, ok: false }
} else {
let hit_variants = normed.children |> filter(variant_arm => {
let payload = resolved_type(n: variant_arm)
match payload_field_child_matching_seg(payload: payload, seg: seg, source_indices: source_indices) {
Some { value: _ } => true
None => false
}
})
if hit_variants |> count != 1 {
WirePathProjection { expr: state.expr, node: none, ok: false }
} else {
match hit_variants |> first {
Some { value: variant_arm } => {
let payload = resolved_type(n: variant_arm)
match payload_field_child_matching_seg(payload: payload, seg: seg, source_indices: source_indices) {
Some { value: fch } => {
let enum_name = authored_name_at(source_indices: source_indices, node: normed)
let variant_name = authored_name_at(source_indices: source_indices, node: variant_arm)
let field_rust = emit_ident(name: authored_name_at(source_indices: source_indices, node: fch), target: Rust)
let match_expr = concat(
"match &*(",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKING: Variant-aware response projection emits match &*(...) even after a list-index step, so content/0/text over Rc<Vec<Rc>> matches &Rc<...> instead of &AnthropicMessages200ContentBlock and the generated client does not compile, violating P3 fail-closed emission.

state.expr,
") { ",
enum_name,
"::",
variant_name,
" { ",
field_rust,
", .. } => ",
field_rust,
", _ => return Err(format!(\"REST typed response path ",
full_path,
": expected ",
variant_name,
" arm\").into()) }"
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKING: The new variant-aware projection emits match &*(...) but content/0/text reaches an &Rc, so the generated Rust does not compile instead of failing closed as a valid typed projection.

WirePathProjection { expr: match_expr, node: Some { value: resolved_type(n: fch) }, ok: true }
}
None => WirePathProjection { expr: state.expr, node: none, ok: false }
}
}
None => WirePathProjection { expr: state.expr, node: none, ok: false }
}
}
}
}
None => WirePathProjection { expr: state.expr, node: none, ok: false }
}
}

fn advance_wire_path_projection(state: WirePathProjection, seg: String, full_path: String, source_indices: Map<String, NewlineIndex>) -> WirePathProjection {
if !state.ok { state }
else if path_segment_is_list_index(seg: seg) {
Expand All @@ -5297,7 +5381,7 @@ fn advance_wire_path_projection(state: WirePathProjection, seg: String, full_pat
node: Some { value: resolved_type(n: ch) }
ok: true
}
None => WirePathProjection { expr: state.expr, node: none, ok: false }
None => try_advance_wire_disjunct_field(state: state, seg: seg, full_path: full_path, source_indices: source_indices)
}
None => WirePathProjection { expr: state.expr, node: none, ok: false }
}
Expand Down
114 changes: 109 additions & 5 deletions src/v2/stage0/src/v2_compiler_emit_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13211,6 +13211,102 @@ pub fn wire_child_for_segment(
}
}

fn payload_field_child_matching_seg(
payload: Rc<Node>,
seg: String,
source_indices: Rc<HashMap<String, Rc<NewlineIndex>>>,
) -> Option<Rc<Node>> {
if !is_product_type(payload.clone()) {
return None;
}
let normed = normalize_access_type_node(payload);
for ch in normed.children.iter().cloned() {
let authored = authored_name_at(source_indices.clone(), &ch);
let wire_key = match child_from_key(ch.clone(), source_indices.clone()) {
Some(k) => k.clone(),
None => authored.clone(),
};
if wire_key.as_str() == seg.as_str() {
return Some(ch.clone());
}
}
None
}

fn try_wire_path_coproduct_field(
state: &Rc<WirePathProjection>,
seg: &String,
full_path: String,
source_indices: Rc<HashMap<String, Rc<NewlineIndex>>>,
) -> Option<Rc<WirePathProjection>> {
let n = state.node.clone()?;
let normed = normalize_access_type_node(n.clone());
if !is_coproduct_type(normed.clone()) {
return None;
}
let mut hit_variant: Option<Rc<Node>> = None;
let mut hit_field: Option<Rc<Node>> = None;
for variant_arm in normed.children.iter().cloned() {
let payload = resolved_type(variant_arm.clone());
if let Some(fch) =
payload_field_child_matching_seg(payload, seg.clone(), source_indices.clone())
{
if hit_variant.is_some() {
return None;
}
hit_variant = Some(variant_arm.clone());
hit_field = Some(fch);
}
}
let variant_arm = hit_variant?;
let fch = hit_field?;
let enum_name = authored_name_at(source_indices.clone(), &normed);
let variant_name = authored_name_at(source_indices.clone(), &variant_arm);
let field_rust = emit_ident(
authored_name_at(source_indices.clone(), &fch),
RenderTarget::Rust,
);
let match_expr = v2_rt::concat(
v2_rt::concat(
v2_rt::concat(
v2_rt::concat(
v2_rt::concat("match &*(".to_string(), state.expr.clone()),
") { ".to_string(),
),
v2_rt::concat(
v2_rt::concat(
v2_rt::concat(
v2_rt::concat(v2_rt::concat(enum_name, "::".to_string()), variant_name.clone()),
" { ".to_string(),
),
field_rust.clone(),
),
", .. } => ".to_string(),
),
),
field_rust,
),
v2_rt::concat(
v2_rt::concat(
v2_rt::concat(
v2_rt::concat(
", _ => return Err(format!(\"REST typed response path ".to_string(),
full_path,
),
": expected ".to_string(),
),
variant_name,
),
" arm\").into()) }".to_string(),
),
);
Some(Rc::new(WirePathProjection {
expr: match_expr,
node: Some(resolved_type(fch.clone())),
ok: true,
}))
}

pub fn advance_wire_path_projection(
state: &Rc<WirePathProjection>,
seg: &String,
Expand Down Expand Up @@ -13275,11 +13371,19 @@ pub fn advance_wire_path_projection(
node: Some(resolved_type(ch.clone())),
ok: true,
}),
None => Rc::new(WirePathProjection {
expr: state.expr.clone(),
node: None,
ok: false,
}),
None => match try_wire_path_coproduct_field(
&state,
&seg,
full_path.clone(),
source_indices.clone(),
) {
Some(proj) => proj,
None => Rc::new(WirePathProjection {
expr: state.expr.clone(),
node: None,
ok: false,
}),
},
}
}
None => Rc::new(WirePathProjection {
Expand Down
39 changes: 37 additions & 2 deletions src/v2/tests/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6512,6 +6512,18 @@ fn anthropic_request_coproduct_wire_contracts_emit_targeted_serde() {
"#[serde(rename = \"tool_use\")]",
][..],
),
(
"pub enum AnthropicMessages200ContentBlock",
"#[serde(tag = \"type\")]",
&[
"#[serde(rename = \"text\")]",
"#[serde(rename = \"thinking\")]",
"#[serde(rename = \"redacted_thinking\")]",
"#[serde(rename = \"tool_use\")]",
"#[serde(rename = \"server_tool_use\")]",
"#[serde(rename = \"web_search_tool_result\")]",
][..],
),
] {
let attrs = attrs_immediately_above_enum(&content, enum_decl);
assert!(
Expand Down Expand Up @@ -6559,6 +6571,26 @@ fn anthropic_request_coproduct_wire_contracts_emit_targeted_serde() {
response_role_block.contains("Assistant,"),
"expected AnthropicMessages200Role singleton Assistant variant; got:\n{response_role_block}"
);

let service_tier_attrs =
attrs_immediately_above_enum(&content, "pub enum AnthropicMessages200ServiceTier");
assert!(
service_tier_attrs.contains(&"#[serde(rename_all = \"snake_case\")]"),
"expected AnthropicMessages200ServiceTier snake_case wire serde; attrs: {:?}",
service_tier_attrs
);
assert!(
!service_tier_attrs.iter().any(|attr| attr.contains("tag =")),
"AnthropicMessages200ServiceTier must remain a string enum, not a tagged object; attrs: {:?}",
service_tier_attrs
);
let service_tier_block = enum_block(&content, "pub enum AnthropicMessages200ServiceTier");
for needle in ["Standard,", "Priority,", "Batch,"] {
assert!(
service_tier_block.contains(needle),
"expected variant {needle} in AnthropicMessages200ServiceTier; got:\n{service_tier_block}"
);
}
}

#[test]
Expand Down Expand Up @@ -7588,9 +7620,12 @@ fn anthropic_messages_uses_typed_200_body_projection() {
);
assert!(
content.contains("(__rest_wire).content")
&& content.contains(".text.clone()")
&& content.contains("(__rest_wire).usage).input_tokens.clone()"),
"expected Anthropic Messages output fields to project from the typed 200 body, got:\n{content}"
"expected Anthropic Messages usage + content paths to project from the typed 200 body, got:\n{content}"
);
assert!(
content.contains("MessagesTextBlock") && content.contains("match &*("),
"expected variant-aware typed projection for assistant text (`MessagesTextBlock` + `match &*(`), got:\n{content}"
);
assert!(
!content.contains("json_body.pointer(\"/content/0/text\")"),
Expand Down
Loading