fix(provider): implement chat_with_history for OpenAI Codex and Gemini
Both providers only implemented chat_with_system, so the default chat_with_history trait method was discarding all conversation history except the last user message. This caused the Telegram bot to lose context between messages. Changes: - OpenAiCodexProvider: extract send_responses_request helper, add chat_with_history that maps full ChatMessage history to ResponsesInput - GeminiProvider: extract send_generate_content helper, add chat_with_history that maps ChatMessage history to Gemini Content (with assistant→model role mapping) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6eec888ff0
commit
1b57be7223
2 changed files with 149 additions and 37 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
//! - Gemini CLI OAuth tokens (reuse existing ~/.gemini/ authentication)
|
//! - Gemini CLI OAuth tokens (reuse existing ~/.gemini/ authentication)
|
||||||
//! - Google Cloud ADC (`GOOGLE_APPLICATION_CREDENTIALS`)
|
//! - Google Cloud ADC (`GOOGLE_APPLICATION_CREDENTIALS`)
|
||||||
|
|
||||||
use crate::providers::traits::Provider;
|
use crate::providers::traits::{ChatMessage, Provider};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use directories::UserDirs;
|
use directories::UserDirs;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
@ -326,12 +326,11 @@ impl GeminiProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
impl GeminiProvider {
|
||||||
impl Provider for GeminiProvider {
|
async fn send_generate_content(
|
||||||
async fn chat_with_system(
|
|
||||||
&self,
|
&self,
|
||||||
system_prompt: Option<&str>,
|
contents: Vec<Content>,
|
||||||
message: &str,
|
system_instruction: Option<Content>,
|
||||||
model: &str,
|
model: &str,
|
||||||
temperature: f64,
|
temperature: f64,
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
|
|
@ -345,21 +344,8 @@ impl Provider for GeminiProvider {
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Build request
|
|
||||||
let system_instruction = system_prompt.map(|sys| Content {
|
|
||||||
role: None,
|
|
||||||
parts: vec![Part {
|
|
||||||
text: sys.to_string(),
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
|
|
||||||
let request = GenerateContentRequest {
|
let request = GenerateContentRequest {
|
||||||
contents: vec![Content {
|
contents,
|
||||||
role: Some("user".to_string()),
|
|
||||||
parts: vec![Part {
|
|
||||||
text: message.to_string(),
|
|
||||||
}],
|
|
||||||
}],
|
|
||||||
system_instruction,
|
system_instruction,
|
||||||
generation_config: GenerationConfig {
|
generation_config: GenerationConfig {
|
||||||
temperature,
|
temperature,
|
||||||
|
|
@ -382,12 +368,10 @@ impl Provider for GeminiProvider {
|
||||||
|
|
||||||
let result: GenerateContentResponse = response.json().await?;
|
let result: GenerateContentResponse = response.json().await?;
|
||||||
|
|
||||||
// Check for API error in response body
|
|
||||||
if let Some(err) = result.error {
|
if let Some(err) = result.error {
|
||||||
anyhow::bail!("Gemini API error: {}", err.message);
|
anyhow::bail!("Gemini API error: {}", err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract text from response
|
|
||||||
result
|
result
|
||||||
.candidates
|
.candidates
|
||||||
.and_then(|c| c.into_iter().next())
|
.and_then(|c| c.into_iter().next())
|
||||||
|
|
@ -395,6 +379,84 @@ impl Provider for GeminiProvider {
|
||||||
.and_then(|p| p.text)
|
.and_then(|p| p.text)
|
||||||
.ok_or_else(|| anyhow::anyhow!("No response from Gemini"))
|
.ok_or_else(|| anyhow::anyhow!("No response from Gemini"))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for GeminiProvider {
|
||||||
|
async fn chat_with_system(
|
||||||
|
&self,
|
||||||
|
system_prompt: Option<&str>,
|
||||||
|
message: &str,
|
||||||
|
model: &str,
|
||||||
|
temperature: f64,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
let system_instruction = system_prompt.map(|sys| Content {
|
||||||
|
role: None,
|
||||||
|
parts: vec![Part {
|
||||||
|
text: sys.to_string(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let contents = vec![Content {
|
||||||
|
role: Some("user".to_string()),
|
||||||
|
parts: vec![Part {
|
||||||
|
text: message.to_string(),
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
|
||||||
|
self.send_generate_content(contents, system_instruction, model, temperature)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn chat_with_history(
|
||||||
|
&self,
|
||||||
|
messages: &[ChatMessage],
|
||||||
|
model: &str,
|
||||||
|
temperature: f64,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
let mut system_parts: Vec<&str> = Vec::new();
|
||||||
|
let mut contents: Vec<Content> = Vec::new();
|
||||||
|
|
||||||
|
for msg in messages {
|
||||||
|
match msg.role.as_str() {
|
||||||
|
"system" => {
|
||||||
|
system_parts.push(&msg.content);
|
||||||
|
}
|
||||||
|
"user" => {
|
||||||
|
contents.push(Content {
|
||||||
|
role: Some("user".to_string()),
|
||||||
|
parts: vec![Part {
|
||||||
|
text: msg.content.clone(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"assistant" => {
|
||||||
|
// Gemini API uses "model" role instead of "assistant"
|
||||||
|
contents.push(Content {
|
||||||
|
role: Some("model".to_string()),
|
||||||
|
parts: vec![Part {
|
||||||
|
text: msg.content.clone(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_instruction = if system_parts.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Content {
|
||||||
|
role: None,
|
||||||
|
parts: vec![Part {
|
||||||
|
text: system_parts.join("\n\n"),
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
self.send_generate_content(contents, system_instruction, model, temperature)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
async fn warmup(&self) -> anyhow::Result<()> {
|
async fn warmup(&self) -> anyhow::Result<()> {
|
||||||
if let Some(auth) = self.auth.as_ref() {
|
if let Some(auth) = self.auth.as_ref() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::auth::openai_oauth::extract_account_id_from_jwt;
|
use crate::auth::openai_oauth::extract_account_id_from_jwt;
|
||||||
use crate::auth::AuthService;
|
use crate::auth::AuthService;
|
||||||
use crate::providers::traits::Provider;
|
use crate::providers::traits::{ChatMessage, Provider};
|
||||||
use crate::providers::ProviderRuntimeOptions;
|
use crate::providers::ProviderRuntimeOptions;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
@ -335,14 +335,12 @@ async fn decode_responses_body(response: reqwest::Response) -> anyhow::Result<St
|
||||||
extract_responses_text(&parsed).ok_or_else(|| anyhow::anyhow!("No response from OpenAI Codex"))
|
extract_responses_text(&parsed).ok_or_else(|| anyhow::anyhow!("No response from OpenAI Codex"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
impl OpenAiCodexProvider {
|
||||||
impl Provider for OpenAiCodexProvider {
|
async fn send_responses_request(
|
||||||
async fn chat_with_system(
|
|
||||||
&self,
|
&self,
|
||||||
system_prompt: Option<&str>,
|
input: Vec<ResponsesInput>,
|
||||||
message: &str,
|
instructions: String,
|
||||||
model: &str,
|
model: &str,
|
||||||
_temperature: f64,
|
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
let profile = self
|
let profile = self
|
||||||
.auth
|
.auth
|
||||||
|
|
@ -368,14 +366,8 @@ impl Provider for OpenAiCodexProvider {
|
||||||
|
|
||||||
let request = ResponsesRequest {
|
let request = ResponsesRequest {
|
||||||
model: normalized_model.to_string(),
|
model: normalized_model.to_string(),
|
||||||
input: vec![ResponsesInput {
|
input,
|
||||||
role: "user".to_string(),
|
instructions,
|
||||||
content: vec![ResponsesInputContent {
|
|
||||||
kind: "input_text".to_string(),
|
|
||||||
text: message.to_string(),
|
|
||||||
}],
|
|
||||||
}],
|
|
||||||
instructions: resolve_instructions(system_prompt),
|
|
||||||
store: false,
|
store: false,
|
||||||
stream: true,
|
stream: true,
|
||||||
text: ResponsesTextOptions {
|
text: ResponsesTextOptions {
|
||||||
|
|
@ -411,6 +403,64 @@ impl Provider for OpenAiCodexProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Provider for OpenAiCodexProvider {
|
||||||
|
async fn chat_with_system(
|
||||||
|
&self,
|
||||||
|
system_prompt: Option<&str>,
|
||||||
|
message: &str,
|
||||||
|
model: &str,
|
||||||
|
_temperature: f64,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
let input = vec![ResponsesInput {
|
||||||
|
role: "user".to_string(),
|
||||||
|
content: vec![ResponsesInputContent {
|
||||||
|
kind: "input_text".to_string(),
|
||||||
|
text: message.to_string(),
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
self.send_responses_request(input, resolve_instructions(system_prompt), model)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn chat_with_history(
|
||||||
|
&self,
|
||||||
|
messages: &[ChatMessage],
|
||||||
|
model: &str,
|
||||||
|
_temperature: f64,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
let mut system_parts: Vec<&str> = Vec::new();
|
||||||
|
let mut input: Vec<ResponsesInput> = Vec::new();
|
||||||
|
|
||||||
|
for msg in messages {
|
||||||
|
match msg.role.as_str() {
|
||||||
|
"system" => {
|
||||||
|
system_parts.push(&msg.content);
|
||||||
|
}
|
||||||
|
"user" | "assistant" => {
|
||||||
|
input.push(ResponsesInput {
|
||||||
|
role: msg.role.clone(),
|
||||||
|
content: vec![ResponsesInputContent {
|
||||||
|
kind: "input_text".to_string(),
|
||||||
|
text: msg.content.clone(),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let instructions = if system_parts.is_empty() {
|
||||||
|
DEFAULT_CODEX_INSTRUCTIONS.to_string()
|
||||||
|
} else {
|
||||||
|
system_parts.join("\n\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
self.send_responses_request(input, instructions, model)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue