|
19 | 19 | } |
20 | 20 | use validator::Validate; |
21 | 21 |
|
22 | | -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] |
| 22 | +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] |
23 | 23 | #[serde(rename_all = "snake_case")] |
24 | 24 | pub enum ResponseInputImageDetail { |
| 25 | + #[default] |
25 | 26 | Auto, |
26 | 27 | High, |
27 | 28 | Low, |
@@ -268,6 +269,7 @@ pub enum ResponseInputContentItem { |
268 | 269 | cache_control: Option<CacheControl>, |
269 | 270 | }, |
270 | 271 | InputImage { |
| 272 | + #[serde(default)] |
271 | 273 | detail: ResponseInputImageDetail, |
272 | 274 | #[serde(skip_serializing_if = "Option::is_none")] |
273 | 275 | image_url: Option<String>, |
@@ -530,9 +532,9 @@ pub enum ResponsesAnnotation { |
530 | 532 | pub enum OutputMessageContentItem { |
531 | 533 | OutputText { |
532 | 534 | text: String, |
533 | | - #[serde(default, skip_serializing_if = "Vec::is_empty")] |
| 535 | + #[serde(default)] |
534 | 536 | annotations: Vec<ResponsesAnnotation>, |
535 | | - #[serde(default, skip_serializing_if = "Vec::is_empty")] |
| 537 | + #[serde(default)] |
536 | 538 | logprobs: Vec<serde_json::Value>, |
537 | 539 | }, |
538 | 540 | Refusal { |
@@ -1332,6 +1334,16 @@ pub struct CreateResponsesPayload { |
1332 | 1334 | #[cfg_attr(feature = "utoipa", schema(value_type = Object))] |
1333 | 1335 | pub truncation: Option<serde_json::Value>, |
1334 | 1336 |
|
| 1337 | + /// **Hadrian Extension:** Presence penalty (-2.0 to 2.0) |
| 1338 | + #[validate(range(min = -2.0, max = 2.0))] |
| 1339 | + #[serde(skip_serializing_if = "Option::is_none")] |
| 1340 | + pub presence_penalty: Option<f64>, |
| 1341 | + |
| 1342 | + /// **Hadrian Extension:** Frequency penalty (-2.0 to 2.0) |
| 1343 | + #[validate(range(min = -2.0, max = 2.0))] |
| 1344 | + #[serde(skip_serializing_if = "Option::is_none")] |
| 1345 | + pub frequency_penalty: Option<f64>, |
| 1346 | + |
1335 | 1347 | /// Enable streaming |
1336 | 1348 | #[serde(default)] |
1337 | 1349 | pub stream: bool, |
@@ -1428,53 +1440,248 @@ pub struct CreateResponsesResponse { |
1428 | 1440 | pub user: Option<String>, |
1429 | 1441 | #[serde(skip_serializing_if = "Option::is_none")] |
1430 | 1442 | pub output_text: Option<String>, |
1431 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1432 | 1443 | pub prompt_cache_key: Option<String>, |
1433 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1434 | 1444 | pub safety_identifier: Option<String>, |
1435 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1436 | 1445 | pub error: Option<ResponsesErrorField>, |
1437 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1438 | 1446 | pub incomplete_details: Option<ResponsesIncompleteDetails>, |
1439 | 1447 | #[serde(skip_serializing_if = "Option::is_none")] |
1440 | 1448 | pub usage: Option<ResponsesUsage>, |
1441 | | - #[serde(skip_serializing_if = "Option::is_none")] |
| 1449 | + pub completed_at: Option<f64>, |
1442 | 1450 | pub max_tool_calls: Option<f64>, |
1443 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1444 | 1451 | pub top_logprobs: Option<f64>, |
1445 | | - #[serde( |
1446 | | - skip_serializing_if = "Option::is_none", |
1447 | | - serialize_with = "serialize_as_integer" |
1448 | | - )] |
| 1452 | + #[serde(serialize_with = "serialize_as_integer")] |
1449 | 1453 | pub max_output_tokens: Option<f64>, |
1450 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1451 | 1454 | pub temperature: Option<f64>, |
1452 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1453 | 1455 | pub top_p: Option<f64>, |
1454 | | - #[serde(skip_serializing_if = "Option::is_none")] |
| 1456 | + pub presence_penalty: Option<f64>, |
| 1457 | + pub frequency_penalty: Option<f64>, |
1455 | 1458 | pub instructions: Option<serde_json::Value>, |
1456 | 1459 | #[serde(skip_serializing_if = "Option::is_none")] |
1457 | 1460 | pub metadata: Option<HashMap<String, String>>, |
1458 | | - #[serde(default, skip_serializing_if = "Option::is_none")] |
1459 | 1461 | pub tools: Option<Vec<ResponsesToolDefinition>>, |
1460 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1461 | 1462 | pub tool_choice: Option<ResponsesToolChoice>, |
1462 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1463 | 1463 | pub parallel_tool_calls: Option<bool>, |
1464 | 1464 | #[serde(skip_serializing_if = "Option::is_none")] |
1465 | 1465 | pub prompt: Option<ResponsesPrompt>, |
1466 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1467 | 1466 | pub background: Option<bool>, |
1468 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1469 | 1467 | pub previous_response_id: Option<String>, |
1470 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1471 | 1468 | pub reasoning: Option<ResponsesReasoningConfigOutput>, |
1472 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1473 | 1469 | pub service_tier: Option<ResponsesServiceTier>, |
1474 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1475 | 1470 | pub store: Option<bool>, |
1476 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1477 | 1471 | pub truncation: Option<ResponsesTruncation>, |
1478 | | - #[serde(skip_serializing_if = "Option::is_none")] |
1479 | 1472 | pub text: Option<ResponseTextConfig>, |
1480 | 1473 | } |
| 1474 | + |
| 1475 | +impl CreateResponsesResponse { |
| 1476 | + /// Serialize to JSON with echo fields merged in per OpenAI Responses API spec. |
| 1477 | + pub fn to_json_with_echo( |
| 1478 | + &self, |
| 1479 | + echo_fields: serde_json::Map<String, serde_json::Value>, |
| 1480 | + ) -> serde_json::Value { |
| 1481 | + let mut val = serde_json::to_value(self).unwrap_or_default(); |
| 1482 | + if let serde_json::Value::Object(ref mut map) = val { |
| 1483 | + if self.status == Some(ResponsesResponseStatus::Completed) { |
| 1484 | + map.insert( |
| 1485 | + "completed_at".into(), |
| 1486 | + serde_json::json!(chrono::Utc::now().timestamp() as f64), |
| 1487 | + ); |
| 1488 | + } |
| 1489 | + for (k, v) in echo_fields { |
| 1490 | + // Don't overwrite reasoning if the struct already has it set from conversion |
| 1491 | + if k == "reasoning" && self.reasoning.is_some() { |
| 1492 | + continue; |
| 1493 | + } |
| 1494 | + map.insert(k, v); |
| 1495 | + } |
| 1496 | + } |
| 1497 | + val |
| 1498 | + } |
| 1499 | +} |
| 1500 | + |
| 1501 | +/// Build a Responses API `response` JSON object for streaming events. |
| 1502 | +/// |
| 1503 | +/// Shared by all provider stream transformers (Anthropic, Bedrock, Vertex). |
| 1504 | +pub fn build_streaming_response_json( |
| 1505 | + id: &str, |
| 1506 | + model: &str, |
| 1507 | + created_at: f64, |
| 1508 | + status: &str, |
| 1509 | + output: serde_json::Value, |
| 1510 | + echo_fields: &serde_json::Map<String, serde_json::Value>, |
| 1511 | +) -> serde_json::Map<String, serde_json::Value> { |
| 1512 | + let mut obj = serde_json::Map::new(); |
| 1513 | + obj.insert("id".into(), serde_json::json!(id)); |
| 1514 | + obj.insert("object".into(), serde_json::json!("response")); |
| 1515 | + obj.insert("created_at".into(), serde_json::json!(created_at)); |
| 1516 | + obj.insert("model".into(), serde_json::json!(model)); |
| 1517 | + obj.insert("status".into(), serde_json::json!(status)); |
| 1518 | + obj.insert("output".into(), output); |
| 1519 | + obj.insert("completed_at".into(), serde_json::Value::Null); |
| 1520 | + obj.insert("error".into(), serde_json::Value::Null); |
| 1521 | + obj.insert("incomplete_details".into(), serde_json::Value::Null); |
| 1522 | + obj.insert("usage".into(), serde_json::Value::Null); |
| 1523 | + for (k, v) in echo_fields { |
| 1524 | + obj.insert(k.clone(), v.clone()); |
| 1525 | + } |
| 1526 | + obj |
| 1527 | +} |
| 1528 | + |
| 1529 | +impl CreateResponsesPayload { |
| 1530 | + /// Produce a JSON map of echo fields for streaming response.completed events. |
| 1531 | + pub fn echo_fields_json(&self) -> serde_json::Map<String, serde_json::Value> { |
| 1532 | + let mut m = serde_json::Map::new(); |
| 1533 | + |
| 1534 | + // Echo tools with required fields (strict, description) filled in |
| 1535 | + let tools_json = |
| 1536 | + serde_json::to_value(self.tools.clone().unwrap_or_default()).unwrap_or_default(); |
| 1537 | + let tools_json = if let serde_json::Value::Array(tools) = tools_json { |
| 1538 | + serde_json::Value::Array( |
| 1539 | + tools |
| 1540 | + .into_iter() |
| 1541 | + .map(|mut t| { |
| 1542 | + if let serde_json::Value::Object(ref mut obj) = t { |
| 1543 | + let is_function = |
| 1544 | + obj.get("type").and_then(|v| v.as_str()) == Some("function"); |
| 1545 | + if is_function { |
| 1546 | + obj.entry("strict").or_insert(serde_json::Value::Null); |
| 1547 | + obj.entry("description").or_insert(serde_json::Value::Null); |
| 1548 | + } |
| 1549 | + } |
| 1550 | + t |
| 1551 | + }) |
| 1552 | + .collect(), |
| 1553 | + ) |
| 1554 | + } else { |
| 1555 | + tools_json |
| 1556 | + }; |
| 1557 | + m.insert("tools".into(), tools_json); |
| 1558 | + m.insert( |
| 1559 | + "tool_choice".into(), |
| 1560 | + serde_json::to_value( |
| 1561 | + self.tool_choice |
| 1562 | + .clone() |
| 1563 | + .unwrap_or(ResponsesToolChoice::String( |
| 1564 | + ResponsesToolChoiceDefault::Auto, |
| 1565 | + )), |
| 1566 | + ) |
| 1567 | + .unwrap_or_default(), |
| 1568 | + ); |
| 1569 | + m.insert( |
| 1570 | + "parallel_tool_calls".into(), |
| 1571 | + serde_json::Value::Bool(self.parallel_tool_calls.unwrap_or(true)), |
| 1572 | + ); |
| 1573 | + m.insert( |
| 1574 | + "temperature".into(), |
| 1575 | + serde_json::json!(self.temperature.unwrap_or(1.0)), |
| 1576 | + ); |
| 1577 | + m.insert("top_p".into(), serde_json::json!(self.top_p.unwrap_or(1.0))); |
| 1578 | + m.insert( |
| 1579 | + "store".into(), |
| 1580 | + serde_json::Value::Bool(self.store.unwrap_or(true)), |
| 1581 | + ); |
| 1582 | + m.insert( |
| 1583 | + "background".into(), |
| 1584 | + serde_json::Value::Bool(self.background.unwrap_or(false)), |
| 1585 | + ); |
| 1586 | + m.insert( |
| 1587 | + "truncation".into(), |
| 1588 | + serde_json::to_value( |
| 1589 | + self.truncation |
| 1590 | + .as_ref() |
| 1591 | + .and_then(|v| serde_json::from_value::<ResponsesTruncation>(v.clone()).ok()) |
| 1592 | + .unwrap_or(ResponsesTruncation::Disabled), |
| 1593 | + ) |
| 1594 | + .unwrap_or_default(), |
| 1595 | + ); |
| 1596 | + m.insert( |
| 1597 | + "text".into(), |
| 1598 | + serde_json::to_value(self.text.clone().unwrap_or(ResponseTextConfig { |
| 1599 | + format: Some(ResponseFormatTextConfig::Text), |
| 1600 | + verbosity: None, |
| 1601 | + })) |
| 1602 | + .unwrap_or_default(), |
| 1603 | + ); |
| 1604 | + m.insert( |
| 1605 | + "service_tier".into(), |
| 1606 | + serde_json::to_value( |
| 1607 | + self.service_tier |
| 1608 | + .as_ref() |
| 1609 | + .and_then(|v| serde_json::from_value::<ResponsesServiceTier>(v.clone()).ok()) |
| 1610 | + .unwrap_or(ResponsesServiceTier::Default), |
| 1611 | + ) |
| 1612 | + .unwrap_or_default(), |
| 1613 | + ); |
| 1614 | + m.insert( |
| 1615 | + "presence_penalty".into(), |
| 1616 | + serde_json::json!(self.presence_penalty.unwrap_or(0.0)), |
| 1617 | + ); |
| 1618 | + m.insert( |
| 1619 | + "frequency_penalty".into(), |
| 1620 | + serde_json::json!(self.frequency_penalty.unwrap_or(0.0)), |
| 1621 | + ); |
| 1622 | + m.insert( |
| 1623 | + "instructions".into(), |
| 1624 | + self.instructions |
| 1625 | + .as_ref() |
| 1626 | + .map(|s| serde_json::Value::String(s.clone())) |
| 1627 | + .unwrap_or(serde_json::Value::Null), |
| 1628 | + ); |
| 1629 | + m.insert( |
| 1630 | + "previous_response_id".into(), |
| 1631 | + self.previous_response_id |
| 1632 | + .as_ref() |
| 1633 | + .map(|s| serde_json::Value::String(s.clone())) |
| 1634 | + .unwrap_or(serde_json::Value::Null), |
| 1635 | + ); |
| 1636 | + m.insert( |
| 1637 | + "prompt_cache_key".into(), |
| 1638 | + self.prompt_cache_key |
| 1639 | + .as_ref() |
| 1640 | + .map(|s| serde_json::Value::String(s.clone())) |
| 1641 | + .unwrap_or(serde_json::Value::Null), |
| 1642 | + ); |
| 1643 | + m.insert( |
| 1644 | + "safety_identifier".into(), |
| 1645 | + self.safety_identifier |
| 1646 | + .as_ref() |
| 1647 | + .map(|s| serde_json::Value::String(s.clone())) |
| 1648 | + .unwrap_or(serde_json::Value::Null), |
| 1649 | + ); |
| 1650 | + m.insert( |
| 1651 | + "max_output_tokens".into(), |
| 1652 | + self.max_output_tokens |
| 1653 | + .map(|v| { |
| 1654 | + if v.fract() == 0.0 { |
| 1655 | + serde_json::json!(v as i64) |
| 1656 | + } else { |
| 1657 | + serde_json::json!(v) |
| 1658 | + } |
| 1659 | + }) |
| 1660 | + .unwrap_or(serde_json::Value::Null), |
| 1661 | + ); |
| 1662 | + m.insert( |
| 1663 | + "metadata".into(), |
| 1664 | + self.metadata |
| 1665 | + .as_ref() |
| 1666 | + .map(|md| serde_json::to_value(md).unwrap_or_default()) |
| 1667 | + .unwrap_or_else(|| serde_json::json!({})), |
| 1668 | + ); |
| 1669 | + m.insert("max_tool_calls".into(), serde_json::Value::Null); |
| 1670 | + // top_logprobs is not a request parameter on the Responses API; default to 0 per spec |
| 1671 | + m.insert("top_logprobs".into(), serde_json::json!(0)); |
| 1672 | + // Ensure reasoning is echoed (null if not configured) |
| 1673 | + m.insert( |
| 1674 | + "reasoning".into(), |
| 1675 | + self.reasoning |
| 1676 | + .as_ref() |
| 1677 | + .map(|r| { |
| 1678 | + serde_json::json!({ |
| 1679 | + "effort": r.effort.as_ref().map(|e| serde_json::to_value(e).unwrap_or_default()), |
| 1680 | + "summary": r.summary.as_ref().map(|s| serde_json::to_value(s).unwrap_or_default()), |
| 1681 | + }) |
| 1682 | + }) |
| 1683 | + .unwrap_or(serde_json::Value::Null), |
| 1684 | + ); |
| 1685 | + m |
| 1686 | + } |
| 1687 | +} |
0 commit comments