feat(llm): 添加 tracing 日志与 ContentField 扩展
为 OpenAI 消息类型引入 ContentField 以支持 string 和 array 两种 content 格式,新增 reasoning_content 字段;添加 tracing 日志初始化函数及请求 /响应日志;修正多处文件末尾换行与 import 顺序。
This commit is contained in:
@@ -11,3 +11,7 @@ serde_json = "1"
|
|||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
|||||||
+15
@@ -4,3 +4,18 @@
|
|||||||
//! 提示词工程、记忆系统、工具调用、Agent 运行时等领域。
|
//! 提示词工程、记忆系统、工具调用、Agent 运行时等领域。
|
||||||
|
|
||||||
pub mod llm;
|
pub mod llm;
|
||||||
|
|
||||||
|
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
||||||
|
|
||||||
|
static INIT: std::sync::Once = std::sync::Once::new();
|
||||||
|
|
||||||
|
pub fn init_tracing() {
|
||||||
|
INIT.call_once(|| {
|
||||||
|
let filter =
|
||||||
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("agcore=info"));
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(fmt::layer())
|
||||||
|
.with(filter)
|
||||||
|
.init();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+5
-2
@@ -107,7 +107,9 @@ impl LlmCycle {
|
|||||||
let mut messages = self.messages.clone();
|
let mut messages = self.messages.clone();
|
||||||
|
|
||||||
if let Some(sys_prompt) = &self.system_prompt
|
if let Some(sys_prompt) = &self.system_prompt
|
||||||
&& !messages.iter().any(|m| matches!(m, OpenaiChatMessage::System { .. }))
|
&& !messages
|
||||||
|
.iter()
|
||||||
|
.any(|m| matches!(m, OpenaiChatMessage::System { .. }))
|
||||||
{
|
{
|
||||||
messages.insert(0, OpenaiChatMessage::system_text(sys_prompt));
|
messages.insert(0, OpenaiChatMessage::system_text(sys_prompt));
|
||||||
}
|
}
|
||||||
@@ -116,7 +118,8 @@ impl LlmCycle {
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(
|
Some(
|
||||||
tools.iter()
|
tools
|
||||||
|
.iter()
|
||||||
.map(|t| OpenaiTool::Function {
|
.map(|t| OpenaiTool::Function {
|
||||||
function: t.clone(),
|
function: t.clone(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
pub use crate::llm::types::usage::{CompletionTokensDetails, CostTracker, PromptTokensDetails, Usage, Usage as LlmUsage};
|
pub use crate::llm::types::usage::{
|
||||||
|
CompletionTokensDetails, CostTracker, PromptTokensDetails, Usage, Usage as LlmUsage,
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
|
use super::LlmProvider;
|
||||||
use crate::llm::error::LlmError;
|
use crate::llm::error::LlmError;
|
||||||
use crate::llm::types::{ChatRequest, ChatResponse, OpenaiChatResponse};
|
use crate::llm::types::{ChatRequest, ChatResponse, OpenaiChatResponse};
|
||||||
use super::LlmProvider;
|
|
||||||
|
|
||||||
pub struct OpenaiProvider {
|
pub struct OpenaiProvider {
|
||||||
http_client: Client,
|
http_client: Client,
|
||||||
@@ -50,6 +51,8 @@ impl LlmProvider for OpenaiProvider {
|
|||||||
async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, LlmError> {
|
async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, LlmError> {
|
||||||
let url = format!("{}/chat/completions", self.base_url.trim_end_matches('/'));
|
let url = format!("{}/chat/completions", self.base_url.trim_end_matches('/'));
|
||||||
|
|
||||||
|
info!(model = %request.model, max_tokens = request.max_tokens, temperature = request.temperature, "发送 LLM 请求");
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.http_client
|
.http_client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
@@ -57,7 +60,10 @@ impl LlmProvider for OpenaiProvider {
|
|||||||
.json(&request)
|
.json(&request)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(Self::map_reqwest_error)?;
|
.map_err(|e| {
|
||||||
|
error!(error = %e, "请求失败");
|
||||||
|
Self::map_reqwest_error(e)
|
||||||
|
})?;
|
||||||
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let status_code: u16 = status.as_u16();
|
let status_code: u16 = status.as_u16();
|
||||||
@@ -71,6 +77,8 @@ impl LlmProvider for OpenaiProvider {
|
|||||||
.map(Duration::from_secs);
|
.map(Duration::from_secs);
|
||||||
let body_text = response.text().await.unwrap_or_default();
|
let body_text = response.text().await.unwrap_or_default();
|
||||||
|
|
||||||
|
error!(status = status_code, body = %body_text, "请求失败");
|
||||||
|
|
||||||
return match status_code {
|
return match status_code {
|
||||||
401 => Err(LlmError::Authentication(body_text)),
|
401 => Err(LlmError::Authentication(body_text)),
|
||||||
429 => Err(LlmError::RateLimit { retry_after }),
|
429 => Err(LlmError::RateLimit { retry_after }),
|
||||||
@@ -91,10 +99,15 @@ impl LlmProvider for OpenaiProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let chat_response: OpenaiChatResponse = response
|
let body_text = response.text().await.unwrap_or_default();
|
||||||
.json()
|
debug!(body = %body_text, "收到响应体");
|
||||||
.await
|
|
||||||
.map_err(|e| LlmError::Other(format!("响应解析失败: {}", e)))?;
|
let chat_response: OpenaiChatResponse = serde_json::from_str(&body_text).map_err(|e| {
|
||||||
|
error!(error = %e, body = %body_text, "响应解析失败");
|
||||||
|
LlmError::Other(format!("响应解析失败: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
debug!(response = ?chat_response, "收到 LLM 响应");
|
||||||
|
|
||||||
Ok(ChatResponse::from(chat_response))
|
Ok(ChatResponse::from(chat_response))
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-12
@@ -1,6 +1,46 @@
|
|||||||
use crate::llm::types::shared::{AudioFormat, ImageDetail};
|
use crate::llm::types::shared::{AudioFormat, ImageDetail};
|
||||||
use crate::llm::types::tool::OpenaiToolCall;
|
use crate::llm::types::tool::OpenaiToolCall;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ContentField {
|
||||||
|
String(String),
|
||||||
|
Array(Vec<OpenaiContentPart>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for ContentField {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = Value::deserialize(deserializer)?;
|
||||||
|
match value {
|
||||||
|
Value::String(s) => Ok(ContentField::String(s)),
|
||||||
|
Value::Array(arr) => {
|
||||||
|
let parts: Result<Vec<OpenaiContentPart>, _> =
|
||||||
|
serde_json::from_value(Value::Array(arr));
|
||||||
|
match parts {
|
||||||
|
Ok(parts) => Ok(ContentField::Array(parts)),
|
||||||
|
Err(e) => Err(serde::de::Error::custom(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(serde::de::Error::custom("content must be string or array")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for ContentField {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
ContentField::String(s) => s.serialize(serializer),
|
||||||
|
ContentField::Array(arr) => arr.serialize(serializer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ImageURL {
|
pub struct ImageURL {
|
||||||
@@ -50,22 +90,24 @@ pub enum OpenaiContentPart {
|
|||||||
#[serde(rename_all = "snake_case", tag = "role")]
|
#[serde(rename_all = "snake_case", tag = "role")]
|
||||||
pub enum OpenaiChatMessage {
|
pub enum OpenaiChatMessage {
|
||||||
Developer {
|
Developer {
|
||||||
content: Vec<OpenaiContentPart>,
|
content: ContentField,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
},
|
},
|
||||||
System {
|
System {
|
||||||
content: Vec<OpenaiContentPart>,
|
content: ContentField,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
},
|
},
|
||||||
User {
|
User {
|
||||||
content: Vec<OpenaiContentPart>,
|
content: ContentField,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
},
|
},
|
||||||
Assistant {
|
Assistant {
|
||||||
content: Vec<OpenaiContentPart>,
|
content: ContentField,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
reasoning_content: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
refusal: Option<String>,
|
refusal: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -74,11 +116,11 @@ pub enum OpenaiChatMessage {
|
|||||||
tool_calls: Option<Vec<OpenaiToolCall>>,
|
tool_calls: Option<Vec<OpenaiToolCall>>,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
content: Vec<OpenaiContentPart>,
|
content: ContentField,
|
||||||
tool_call_id: String,
|
tool_call_id: String,
|
||||||
},
|
},
|
||||||
Function {
|
Function {
|
||||||
content: Vec<OpenaiContentPart>,
|
content: ContentField,
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -86,14 +128,15 @@ pub enum OpenaiChatMessage {
|
|||||||
impl OpenaiChatMessage {
|
impl OpenaiChatMessage {
|
||||||
pub fn user_text<S: Into<String>>(text: S) -> Self {
|
pub fn user_text<S: Into<String>>(text: S) -> Self {
|
||||||
OpenaiChatMessage::User {
|
OpenaiChatMessage::User {
|
||||||
content: vec![OpenaiContentPart::Text { text: text.into() }],
|
content: ContentField::Array(vec![OpenaiContentPart::Text { text: text.into() }]),
|
||||||
name: None,
|
name: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assistant_text<S: Into<String>>(text: S) -> Self {
|
pub fn assistant_text<S: Into<String>>(text: S) -> Self {
|
||||||
OpenaiChatMessage::Assistant {
|
OpenaiChatMessage::Assistant {
|
||||||
content: vec![OpenaiContentPart::Text { text: text.into() }],
|
content: ContentField::String(text.into()),
|
||||||
|
reasoning_content: None,
|
||||||
refusal: None,
|
refusal: None,
|
||||||
name: None,
|
name: None,
|
||||||
tool_calls: None,
|
tool_calls: None,
|
||||||
@@ -102,23 +145,23 @@ impl OpenaiChatMessage {
|
|||||||
|
|
||||||
pub fn system_text<S: Into<String>>(text: S) -> Self {
|
pub fn system_text<S: Into<String>>(text: S) -> Self {
|
||||||
OpenaiChatMessage::System {
|
OpenaiChatMessage::System {
|
||||||
content: vec![OpenaiContentPart::Text { text: text.into() }],
|
content: ContentField::Array(vec![OpenaiContentPart::Text { text: text.into() }]),
|
||||||
name: None,
|
name: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn developer_text<S: Into<String>>(text: S) -> Self {
|
pub fn developer_text<S: Into<String>>(text: S) -> Self {
|
||||||
OpenaiChatMessage::Developer {
|
OpenaiChatMessage::Developer {
|
||||||
content: vec![OpenaiContentPart::Text { text: text.into() }],
|
content: ContentField::Array(vec![OpenaiContentPart::Text { text: text.into() }]),
|
||||||
name: None,
|
name: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tool_result<S: Into<String>>(tool_call_id: String, content: S) -> Self {
|
pub fn tool_result<S: Into<String>>(tool_call_id: String, content: S) -> Self {
|
||||||
OpenaiChatMessage::Tool {
|
OpenaiChatMessage::Tool {
|
||||||
content: vec![OpenaiContentPart::Text {
|
content: ContentField::Array(vec![OpenaiContentPart::Text {
|
||||||
text: content.into(),
|
text: content.into(),
|
||||||
}],
|
}]),
|
||||||
tool_call_id,
|
tool_call_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ pub mod tool;
|
|||||||
pub mod usage;
|
pub mod usage;
|
||||||
|
|
||||||
pub use message::{
|
pub use message::{
|
||||||
FileData, ImageURL, InputAudio, OpenaiChatMessage, OpenaiContentPart,
|
ContentField, FileData, ImageURL, InputAudio, OpenaiChatMessage, OpenaiContentPart,
|
||||||
};
|
};
|
||||||
pub use request::{OpenaiChatRequest, OpenaiTool, StreamOptions, ToolChoice};
|
pub use request::{OpenaiChatRequest, OpenaiTool, StreamOptions, ToolChoice};
|
||||||
pub use response::{
|
pub use response::{
|
||||||
Annotation, Choice, ChunkChoice, Delta, Logprobs, OpenaiAudio,
|
Annotation, Choice, ChunkChoice, Delta, Logprobs, OpenaiAudio, OpenaiChatChunk,
|
||||||
OpenaiChatChunk, OpenaiChatResponse, TokenLogprob, TopLogprob, URLCitation,
|
OpenaiChatResponse, TokenLogprob, TopLogprob, URLCitation,
|
||||||
};
|
};
|
||||||
pub use shared::{
|
pub use shared::{
|
||||||
AudioFormat, FinishReason, ImageDetail, Modality, ResponseFormat, Role,
|
AudioFormat, FinishReason, ImageDetail, Modality, ResponseFormat, Role, ServiceTier,
|
||||||
ServiceTier, StopSequence,
|
StopSequence,
|
||||||
};
|
};
|
||||||
pub use tool::{FunctionCall, OpenaiToolCall, OpenaiToolDefinition};
|
pub use tool::{FunctionCall, OpenaiToolCall, OpenaiToolDefinition};
|
||||||
pub use usage::{CompletionTokensDetails, CostTracker, PromptTokensDetails, Usage};
|
pub use usage::{CompletionTokensDetails, CostTracker, PromptTokensDetails, Usage};
|
||||||
@@ -34,10 +34,7 @@ impl From<OpenaiChatResponse> for ChatResponse {
|
|||||||
.first()
|
.first()
|
||||||
.map(|c| c.message.clone())
|
.map(|c| c.message.clone())
|
||||||
.unwrap_or_else(|| OpenaiChatMessage::assistant_text(""));
|
.unwrap_or_else(|| OpenaiChatMessage::assistant_text(""));
|
||||||
let stop_reason = response
|
let stop_reason = response.choices.first().and_then(|c| c.finish_reason);
|
||||||
.choices
|
|
||||||
.first()
|
|
||||||
.and_then(|c| c.finish_reason);
|
|
||||||
ChatResponse {
|
ChatResponse {
|
||||||
message,
|
message,
|
||||||
usage: response.usage,
|
usage: response.usage,
|
||||||
|
|||||||
+23
-18
@@ -1,7 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::Value;
|
|
||||||
use crate::llm::types::shared::{ResponseFormat, ServiceTier, StopSequence};
|
use crate::llm::types::shared::{ResponseFormat, ServiceTier, StopSequence};
|
||||||
use crate::llm::types::tool::OpenaiToolDefinition;
|
use crate::llm::types::tool::OpenaiToolDefinition;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct StreamOptions {
|
pub struct StreamOptions {
|
||||||
@@ -16,12 +16,8 @@ pub enum ToolChoice {
|
|||||||
None,
|
None,
|
||||||
Auto,
|
Auto,
|
||||||
Required,
|
Required,
|
||||||
Named {
|
Named { name: String },
|
||||||
name: String,
|
AllowedTools { tool_names: Vec<String> },
|
||||||
},
|
|
||||||
AllowedTools {
|
|
||||||
tool_names: Vec<String>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for ToolChoice {
|
impl Serialize for ToolChoice {
|
||||||
@@ -62,25 +58,36 @@ impl<'de> Deserialize<'de> for ToolChoice {
|
|||||||
"none" => Ok(ToolChoice::None),
|
"none" => Ok(ToolChoice::None),
|
||||||
"auto" => Ok(ToolChoice::Auto),
|
"auto" => Ok(ToolChoice::Auto),
|
||||||
"required" => Ok(ToolChoice::Required),
|
"required" => Ok(ToolChoice::Required),
|
||||||
_ => Err(serde::de::Error::custom(format!("unknown tool choice: {s}"))),
|
_ => Err(serde::de::Error::custom(format!(
|
||||||
|
"unknown tool choice: {s}"
|
||||||
|
))),
|
||||||
},
|
},
|
||||||
Value::Object(obj) => {
|
Value::Object(obj) => {
|
||||||
let typ = obj.get("type").and_then(|v| v.as_str()).ok_or_else(|| {
|
let typ = obj.get("type").and_then(|v| v.as_str()).ok_or_else(|| {
|
||||||
serde::de::Error::custom("missing 'type' field in tool_choice")
|
serde::de::Error::custom("missing 'type' field in tool_choice")
|
||||||
})?;
|
})?;
|
||||||
if typ == "function" {
|
if typ == "function" {
|
||||||
let func = obj.get("function").and_then(|v| v.as_object()).ok_or_else(|| {
|
let func =
|
||||||
serde::de::Error::custom("missing 'function' field in tool_choice")
|
obj.get("function")
|
||||||
})?;
|
.and_then(|v| v.as_object())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
serde::de::Error::custom("missing 'function' field in tool_choice")
|
||||||
|
})?;
|
||||||
let name = func.get("name").and_then(|v| v.as_str()).ok_or_else(|| {
|
let name = func.get("name").and_then(|v| v.as_str()).ok_or_else(|| {
|
||||||
serde::de::Error::custom("missing 'function.name' in tool_choice")
|
serde::de::Error::custom("missing 'function.name' in tool_choice")
|
||||||
})?;
|
})?;
|
||||||
Ok(ToolChoice::Named { name: name.to_string() })
|
Ok(ToolChoice::Named {
|
||||||
|
name: name.to_string(),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(serde::de::Error::custom(format!("unknown tool_choice type: {typ}")))
|
Err(serde::de::Error::custom(format!(
|
||||||
|
"unknown tool_choice type: {typ}"
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(serde::de::Error::custom("tool_choice must be a string or object")),
|
_ => Err(serde::de::Error::custom(
|
||||||
|
"tool_choice must be a string or object",
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,9 +95,7 @@ impl<'de> Deserialize<'de> for ToolChoice {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case", tag = "type")]
|
#[serde(rename_all = "snake_case", tag = "type")]
|
||||||
pub enum OpenaiTool {
|
pub enum OpenaiTool {
|
||||||
Function {
|
Function { function: OpenaiToolDefinition },
|
||||||
function: OpenaiToolDefinition,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use crate::llm::types::shared::{FinishReason, ServiceTier};
|
|
||||||
use crate::llm::types::message::OpenaiChatMessage;
|
use crate::llm::types::message::OpenaiChatMessage;
|
||||||
|
use crate::llm::types::shared::{FinishReason, ServiceTier};
|
||||||
use crate::llm::types::tool::OpenaiToolCall;
|
use crate::llm::types::tool::OpenaiToolCall;
|
||||||
use crate::llm::types::usage::Usage;
|
use crate::llm::types::usage::Usage;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TokenLogprob {
|
pub struct TokenLogprob {
|
||||||
|
|||||||
@@ -20,8 +20,5 @@ pub struct FunctionCall {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case", tag = "type")]
|
#[serde(rename_all = "snake_case", tag = "type")]
|
||||||
pub enum OpenaiToolCall {
|
pub enum OpenaiToolCall {
|
||||||
Function {
|
Function { id: String, function: FunctionCall },
|
||||||
id: String,
|
|
||||||
function: FunctionCall,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user