use super::core::types::{TokenExtractor, TokenExtractionResult};
#[derive(Debug, Clone)]
pub struct JsonPath(pub Vec<JsonPathSegment>);
#[derive(Debug, Clone)]
pub enum JsonPathSegment {
Key(&'static str),
Index(usize),
}
impl JsonPath {
pub fn navigate<'a>(&self, value: &'a serde_json::Value) -> Option<&'a serde_json::Value> {
let mut current = value;
for segment in &self.0 {
current = match segment {
JsonPathSegment::Key(key) => current.get(*key)?,
JsonPathSegment::Index(idx) => current.get(*idx)?,
};
}
Some(current)
}
}
#[derive(Debug, Clone)]
pub enum EndCondition {
JsonPathEquals {
path: JsonPath,
value: &'static str,
},
JsonPathExists(JsonPath),
PayloadEquals(&'static str),
None,
}
#[derive(Debug, Clone)]
pub struct ExtractorConfig {
pub token_path: JsonPath,
pub json_end_condition: EndCondition,
pub end_payload: Option<&'static str>,
}
#[derive(Debug, Clone)]
pub struct JsonPathExtractor {
config: ExtractorConfig,
}
impl JsonPathExtractor {
pub const fn new(config: ExtractorConfig) -> Self {
Self { config }
}
fn check_json_end(&self, value: &serde_json::Value) -> bool {
match &self.config.json_end_condition {
EndCondition::JsonPathEquals { path, value: expected } => {
path.navigate(value)
.and_then(|v| v.as_str())
.is_some_and(|s| s == *expected)
}
EndCondition::JsonPathExists(path) => {
path.navigate(value)
.is_some_and(|v| !v.is_null())
}
EndCondition::PayloadEquals(_) | EndCondition::None => false,
}
}
}
impl TokenExtractor for JsonPathExtractor {
fn extract_token(&self, json_value: &serde_json::Value) -> TokenExtractionResult {
if self.check_json_end(json_value) {
return TokenExtractionResult::EndStream;
}
if let Some(token) = self.config.token_path.navigate(json_value).and_then(|v| v.as_str()) {
TokenExtractionResult::Token(token.to_string())
} else {
TokenExtractionResult::NoToken
}
}
fn is_end_payload(&self, payload: &str) -> bool {
self.config.end_payload
.is_some_and(|end| payload.trim() == end)
}
}
pub fn openai_config() -> ExtractorConfig {
use JsonPathSegment::*;
ExtractorConfig {
token_path: JsonPath(vec![Key("choices"), Index(0), Key("delta"), Key("content")]),
json_end_condition: EndCondition::JsonPathExists(
JsonPath(vec![Key("choices"), Index(0), Key("finish_reason")])
),
end_payload: Some("[DONE]"),
}
}
pub fn claude_config() -> ExtractorConfig {
use JsonPathSegment::*;
ExtractorConfig {
token_path: JsonPath(vec![Key("delta"), Key("text")]),
json_end_condition: EndCondition::JsonPathEquals {
path: JsonPath(vec![Key("type")]),
value: "message_stop",
},
end_payload: None,
}
}
pub type OpenAITokenExtractor = JsonPathExtractor;
pub type ClaudeTokenExtractor = JsonPathExtractor;
pub fn openai_extractor() -> OpenAITokenExtractor {
JsonPathExtractor::new(openai_config())
}
pub fn claude_extractor() -> ClaudeTokenExtractor {
JsonPathExtractor::new(claude_config())
}