commit - df099bfbf6d802f729aea67587f6dde90fac70ed
commit + 877f4c581cb9d71525b68bf4cebff5152a4e751e
blob - b3b300485a29d1064532b04e8ad1ee9a5bf7e29c
blob + c518f977566828fb20eba8b47b4db91a7070735b
--- src/cli.rs
+++ src/cli.rs
use std::time::Duration;
-use crate::openai::Message;
+use crate::openai::{Message, ReasoningEffort};
use clap::{
arg, builder::PossibleValue, command, value_parser, ArgGroup, ArgMatches, Command, ValueEnum,
};
pub output_format: OutputFormat,
pub file: String,
pub timeout: Duration,
+ #[builder(default)]
+ pub reasoning_effort: Option<ReasoningEffort>,
}
/// The format that invocation's results are in
/// Top-level command
fn cli() -> Command {
command!()
- .arg(arg!(model: -m --model <MODEL> "Sets model. See https://platform.openai.com/docs/models for model identifiers.").default_value("gpt-4-1106-preview"))
+ .arg(arg!(model: -m --model <MODEL> "Sets model. See https://platform.openai.com/docs/models for model identifiers.").default_value("gpt-5"))
.arg(
arg!(temperature: -t --temperature <TEMP> "Sets temperature")
.value_parser(value_parser!(f32))
])
.default_value("plaintext"),
)
+ .arg(
+ arg!(reasoning_effort: --"reasoning-effort" <EFFORT> "Sets reasoning effort (low, medium, high)")
+ .value_parser(value_parser!(ReasoningEffort))
+ .required(false),
+ )
.arg(arg!(--json "Shorthand for --output_format json"))
.arg(arg!(--jsonp "Shorthand for --output_format jsonpretty"))
.group(ArgGroup::new("output_format_short").args(["json", "jsonp"]))
.expect("File is required")
.to_string();
+ let reasoning_effort = matches
+ .get_one::<ReasoningEffort>("reasoning_effort")
+ .copied();
+
Self {
api_key,
messages,
timeout,
output_format,
file,
+ reasoning_effort,
}
}
}
}
}
+impl ValueEnum for ReasoningEffort {
+ fn value_variants<'a>() -> &'a [Self] {
+ &[Self::Low, Self::Medium, Self::High]
+ }
+
+ fn to_possible_value(&self) -> Option<PossibleValue> {
+ Some(match self {
+ Self::Low => PossibleValue::new("low"),
+ Self::Medium => PossibleValue::new("medium"),
+ Self::High => PossibleValue::new("high"),
+ })
+ }
+}
+
#[cfg(test)]
mod test {
use super::*;
}
#[test]
+ fn chat_reasoning_effort_flag() -> Result<()> {
+ let args = cli()
+ .try_get_matches_from(vec!["cogni", "-u", "USER", "--reasoning-effort", "high"])
+ .map(Invocation::from)?;
+
+ assert_eq!(args.reasoning_effort, Some(ReasoningEffort::High));
+ Ok(())
+ }
+
+ #[test]
fn chat_many_msgs_with_system_prompt() -> Result<()> {
let args = cli()
.try_get_matches_from(vec![
blob - 6bfac73a4c54241803810f4357ebad5081198868
blob + 385cb933f50e88ff173b340a8a8252a807509bab
--- src/exec/chat.rs
+++ src/exec/chat.rs
//! Implements chat subcommand
use crate::cli::{Invocation, OutputFormat};
-use crate::openai::{self, ChatCompletion, FinishReason, Message};
+use crate::openai::{self, FinishReason, Message, Reasoning, Response};
use crate::parse;
use crate::Error;
return Err(Error::NoMessagesProvided.into());
}
- // TODO: Lifetimes for `ChatCompletionRequest` fields
- let request = openai::ChatCompletionRequest::builder()
+ // TODO: Lifetimes for `ResponseRequest` fields
+ let mut builder = openai::ResponseRequest::builder();
+
+ builder
.model(args.model.clone())
.messages(msgs)
.temperature(args.temperature)
- .timeout(args.timeout)
+ .timeout(args.timeout);
+
+ if let Some(effort) = args.reasoning_effort {
+ builder.reasoning(Some(Reasoning::from_effort(effort)));
+ }
+
+ let request = builder
.build()
.with_context(|| "failed to create request")?;
let res = client
- .chat_complete(&request)
+ .create_response(&request)
.await
.with_context(|| "failed to fetch request")?;
}
}
-/// Show formatted output for `ChatCompletionRequest`
-fn show_response(dest: impl Write, args: &Invocation, resp: &ChatCompletion) -> Result<(), Error> {
+/// Show formatted output for a Responses API result
+fn show_response(dest: impl Write, args: &Invocation, resp: &Response) -> Result<(), Error> {
let mut writer = BufWriter::new(dest);
let choice = match resp.choices.len() {
1 => &resp.choices[0],
use crate::{
cli::{Invocation, InvocationBuilder, OutputFormat},
- openai::{ChatCompletion, ChatCompletionBuilder, Choice, FinishReason, Message, Usage},
+ openai::{Choice, FinishReason, Message, Response, ResponseBuilder, Usage},
};
use super::*;
.to_owned()
}
- fn default_resp() -> ChatCompletionBuilder {
- ChatCompletion::builder()
+ fn default_resp() -> ResponseBuilder {
+ Response::builder()
.created(DateTime::default())
.choices(vec![])
.model(String::default())
blob - b9f70ce1fb66f6137ea849a3e439e7b8d2c909c9
blob + bf70c9c85ba7ea62c5c56807752895d0c3893300
--- src/openai.rs
+++ src/openai.rs
//! Interactions with OpenAI APIs
+use std::convert::TryFrom;
use std::time::Duration;
use crate::Error;
use derive_builder::Builder;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
-use serde_json::json;
+use serde_json::{json, Value};
-/// Convienience Client for OpenAI Chat Completions API
+/// Convienience Client for OpenAI Responses API
pub struct Client {
/// Inner client
client: reqwest::Client,
base_url: String,
}
-/// Requests for chat_completion
-/// Reference: <https://platform.openai.com/docs/api-reference/chat>
+/// Requests for the Responses API
+/// Reference: <https://platform.openai.com/docs/api-reference/responses>
#[derive(Builder, Default)]
-pub struct ChatCompletionRequest {
+pub struct ResponseRequest {
model: String,
messages: Vec<Message>,
temperature: f32,
timeout: Duration,
+ #[builder(default)]
+ reasoning: Option<Reasoning>,
}
-/// Responses from chat_completion
-/// Reference: <https://platform.openai.com/docs/api-reference/chat>
+/// Responses from the Responses API
+/// Reference: <https://platform.openai.com/docs/api-reference/responses>
#[derive(Builder, Default, Debug, Serialize, Deserialize)]
-pub struct ChatCompletion {
+pub struct Response {
#[serde(with = "ts_seconds")]
pub created: DateTime<Utc>,
pub choices: Vec<Choice>,
error: APIError,
}
-/// Messages in chat completion request and response
+/// Messages in Responses API request and response
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub struct Message {
pub role: Role,
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
pub struct Usage {
- pub prompt_tokens: u32,
- pub completion_tokens: u32,
+ pub input_tokens: u32,
+ pub output_tokens: u32,
pub total_tokens: u32,
}
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct Reasoning {
+ pub effort: ReasoningEffort,
+}
+
+impl Reasoning {
+ pub fn from_effort(effort: ReasoningEffort) -> Self {
+ Self { effort }
+ }
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "lowercase")]
+pub enum ReasoningEffort {
+ Low,
+ Medium,
+ High,
+}
+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FinishReason {
})
}
- pub async fn chat_complete(
- &self,
- request: &ChatCompletionRequest,
- ) -> Result<ChatCompletion, Error> {
+ pub async fn create_response(&self, request: &ResponseRequest) -> Result<Response, Error> {
let api_key = &self.api_key.as_ref().ok_or(Error::NoAPIKey)?;
- let model = &request.model;
let resp = self
.client
- .post(self.chat_endpoint())
+ .post(self.responses_endpoint())
.bearer_auth(api_key)
.timeout(request.timeout)
.header("Content-Type", "application/json")
- .json(&json!({
- "model": model,
- "messages": request.messages,
- "temperature": request.temperature,
- }))
+ .json(&request.to_payload())
.send()
.await
.map_err(Error::FailedToFetch)?;
match resp.status() {
StatusCode::OK => {
- let res: ChatCompletion = resp.json().await.map_err(Error::FailedToFetch)?;
- Ok(res)
+ let responses: ResponsesAPIResponse =
+ resp.json().await.map_err(Error::FailedToFetch)?;
+ let response = Response::try_from(responses).map_err(Error::UnexpectedResponse)?;
+ Ok(response)
}
_ => {
let error = resp
}
}
- fn chat_endpoint(&self) -> String {
- format!("{}{}", self.base_url, "/v1/chat/completions")
+ fn responses_endpoint(&self) -> String {
+ format!("{}{}", self.base_url, "/v1/responses")
}
}
}
}
-impl ChatCompletionRequest {
- pub fn builder() -> ChatCompletionRequestBuilder {
- ChatCompletionRequestBuilder::default()
+impl Message {
+ fn to_responses_input(&self) -> serde_json::Value {
+ json!({
+ "role": self.role.as_str(),
+ "content": [{
+ "type": "text",
+ "text": self.content.clone(),
+ }]
+ })
}
}
-impl ChatCompletion {
- pub fn builder() -> ChatCompletionBuilder {
- ChatCompletionBuilder::default()
+impl Role {
+ fn as_str(&self) -> &'static str {
+ match self {
+ Role::System => "system",
+ Role::Assistant => "assistant",
+ Role::User => "user",
+ }
}
}
+impl ResponseRequest {
+ pub fn builder() -> ResponseRequestBuilder {
+ ResponseRequestBuilder::default()
+ }
+
+ fn to_payload(&self) -> Value {
+ let input = self
+ .messages
+ .iter()
+ .map(Message::to_responses_input)
+ .collect::<Vec<_>>();
+
+ let mut payload = json!({
+ "model": self.model,
+ "input": input,
+ "temperature": self.temperature,
+ });
+
+ if let Some(reasoning) = &self.reasoning {
+ if let Some(obj) = payload.as_object_mut() {
+ obj.insert(
+ "reasoning".to_string(),
+ serde_json::to_value(reasoning).expect("failed to serialize reasoning"),
+ );
+ }
+ }
+
+ payload
+ }
+}
+
+impl Response {
+ pub fn builder() -> ResponseBuilder {
+ ResponseBuilder::default()
+ }
+}
+
+#[derive(Debug, Deserialize)]
+struct ResponsesAPIResponse {
+ #[serde(with = "ts_seconds")]
+ created: DateTime<Utc>,
+ model: String,
+ #[serde(default)]
+ output: Vec<ResponseOutput>,
+ usage: ResponsesUsage,
+}
+
+#[derive(Debug, Deserialize)]
+struct ResponsesUsage {
+ #[serde(default)]
+ input_tokens: u32,
+ #[serde(default)]
+ output_tokens: u32,
+ #[serde(default)]
+ total_tokens: u32,
+}
+
+#[derive(Debug, Deserialize)]
+struct ResponseOutput {
+ #[serde(rename = "type")]
+ item_type: String,
+ role: Option<Role>,
+ #[serde(default)]
+ content: Vec<ResponseContent>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(tag = "type", rename_all = "snake_case")]
+enum ResponseContent {
+ OutputText {
+ text: String,
+ },
+ Text {
+ text: String,
+ },
+ #[serde(other)]
+ Other,
+}
+
+impl ResponseContent {
+ fn as_text(&self) -> Option<&str> {
+ match self {
+ ResponseContent::OutputText { text } => Some(text),
+ ResponseContent::Text { text } => Some(text),
+ ResponseContent::Other => None,
+ }
+ }
+}
+
+impl ResponseOutput {
+ fn aggregated_text(&self) -> Option<String> {
+ let mut aggregated = String::new();
+
+ for part in &self.content {
+ if let Some(text) = part.as_text() {
+ aggregated.push_str(text);
+ }
+ }
+
+ if aggregated.is_empty() {
+ None
+ } else {
+ Some(aggregated)
+ }
+ }
+}
+
+impl TryFrom<ResponsesAPIResponse> for Response {
+ type Error = String;
+
+ fn try_from(value: ResponsesAPIResponse) -> Result<Self, Self::Error> {
+ let mut choices = Vec::new();
+
+ for output in value.output.into_iter() {
+ if output.item_type != "message" {
+ continue;
+ }
+
+ let text = output
+ .aggregated_text()
+ .ok_or_else(|| "response message missing text content".to_string())?;
+
+ let message = Message {
+ role: output.role.unwrap_or(Role::Assistant),
+ content: text,
+ };
+
+ choices.push(Choice {
+ message,
+ finish_reason: FinishReason::Stop,
+ });
+ }
+
+ if choices.is_empty() {
+ return Err("response did not contain any assistant messages".to_string());
+ }
+
+ Ok(Response {
+ created: value.created,
+ choices,
+ model: value.model,
+ usage: Usage {
+ input_tokens: value.usage.input_tokens,
+ output_tokens: value.usage.output_tokens,
+ total_tokens: value.usage.total_tokens,
+ },
+ })
+ }
+}
+
#[cfg(test)]
mod test {
use super::*;
use anyhow::Result;
use chrono::TimeZone;
+ use std::time::Duration;
#[test]
- fn parse_chat_completion_response() -> Result<()> {
+ fn parse_response_payload() -> Result<()> {
let data = r#"{
"created": 1688413145,
- "model": "gpt-3.5-turbo-0613",
- "choices": [{
- "index": 0,
- "message": {
- "role": "assistant",
- "content": "Hello! How can I assist you today?"
- },
- "finish_reason": "stop"
+ "model": "gpt-5",
+ "output": [{
+ "id": "msg_123",
+ "type": "message",
+ "role": "assistant",
+ "content": [{
+ "type": "output_text",
+ "text": "Hello! How can I assist you today?"
+ }]
}],
"usage": {
- "prompt_tokens": 8,
- "completion_tokens": 9,
+ "input_tokens": 8,
+ "output_tokens": 9,
"total_tokens": 17
}
}
"#;
- let resp = serde_json::from_str::<ChatCompletion>(data)?;
+ let resp = serde_json::from_str::<ResponsesAPIResponse>(data)?;
+ let resp = Response::try_from(resp).map_err(|e| anyhow::anyhow!(e))?;
assert_eq!(resp.created, Utc.timestamp_opt(1688413145, 0).unwrap());
assert_eq!(
finish_reason: FinishReason::Stop
}]
);
- assert_eq!(resp.model, "gpt-3.5-turbo-0613");
+ assert_eq!(resp.model, "gpt-5");
assert_eq!(
resp.usage,
Usage {
- prompt_tokens: 8,
- completion_tokens: 9,
+ input_tokens: 8,
+ output_tokens: 9,
total_tokens: 17,
}
);
}
#[test]
- fn parse_chat_completion_error() -> Result<()> {
+ fn response_payload_includes_reasoning() -> Result<()> {
+ let request = ResponseRequest::builder()
+ .model("gpt-5".to_string())
+ .messages(vec![Message::user("Hello")])
+ .temperature(0.0)
+ .timeout(Duration::from_secs(30))
+ .reasoning(Some(Reasoning::from_effort(ReasoningEffort::High)))
+ .build()
+ .expect("request builds");
+
+ let payload = request.to_payload();
+
+ assert_eq!(payload["reasoning"]["effort"], "high");
+ assert_eq!(payload["model"], "gpt-5");
+
+ Ok(())
+ }
+
+ #[test]
+ fn response_payload_omits_reasoning_when_not_set() -> Result<()> {
+ let request = ResponseRequest::builder()
+ .model("gpt-5".to_string())
+ .messages(vec![Message::user("Hello")])
+ .temperature(0.0)
+ .timeout(Duration::from_secs(30))
+ .build()
+ .expect("request builds");
+
+ let payload = request.to_payload();
+
+ assert!(payload.get("reasoning").is_none());
+ Ok(())
+ }
+
+ #[test]
+ fn response_try_from_concatenates_segments() -> Result<()> {
let data = r#"{
+ "created": 1688413145,
+ "model": "gpt-5",
+ "output": [{
+ "id": "msg_123",
+ "type": "message",
+ "role": "assistant",
+ "content": [{
+ "type": "text",
+ "text": "Hello"
+ }, {
+ "type": "output_text",
+ "text": " world"
+ }]
+ }],
+ "usage": {
+ "input_tokens": 3,
+ "output_tokens": 4,
+ "total_tokens": 7
+ }
+ }
+ "#;
+
+ let resp = serde_json::from_str::<ResponsesAPIResponse>(data)?;
+ let resp = Response::try_from(resp).map_err(|e| anyhow::anyhow!(e))?;
+
+ assert_eq!(resp.choices.len(), 1);
+ assert_eq!(resp.choices[0].message.content, "Hello world");
+ Ok(())
+ }
+
+ #[test]
+ fn response_try_from_errors_without_assistant_message() -> Result<()> {
+ let data = r#"{
+ "created": 1688413145,
+ "model": "gpt-5",
+ "output": [{
+ "id": "reasoning_1",
+ "type": "reasoning",
+ "content": [{
+ "type": "text",
+ "text": "Thinking..."
+ }]
+ }],
+ "usage": {
+ "input_tokens": 1,
+ "output_tokens": 1,
+ "total_tokens": 2
+ }
+ }
+ "#;
+
+ let resp = serde_json::from_str::<ResponsesAPIResponse>(data)?;
+ let err = Response::try_from(resp).expect_err("should error");
+
+ assert!(
+ err.contains("did not contain any assistant messages"),
+ "unexpected error text: {err}"
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn parse_response_error() -> Result<()> {
+ let data = r#"{
"error": {
"message": "An error message",
"type": "invalid_request_error",
blob - 34fc261088ffad50b267ea437bfbc819576442a5
blob + 05b4e04c7c77c264b5006ba51802f1006384b482
--- tests/chat.rs
+++ tests/chat.rs
let mut server = mockito::Server::new();
let mock = server
- .mock("POST", "/v1/chat/completions")
+ .mock("POST", "/v1/responses")
.with_header("content-type", "application/json")
.with_header("authorization", "Bearer ABCDE")
.match_body(mockito::Matcher::PartialJson(json!({
- "messages": [{
- "content": "Hello",
- "role": "user",
+ "model": "gpt-5",
+ "input": [{
+ "role": "user",
+ "content": [{
+ "type": "text",
+ "text": "Hello"
+ }]
}]
})))
.with_body(
r#"{
- "id": "chatcmpl-XXXXX",
+ "id": "resp_XXXXX",
"created": 1688413145,
- "model": "gpt-3.5-turbo-0613",
- "choices": [{
- "index": 0,
- "message": {
- "role": "assistant",
- "content": "ASSISTANT REPLY"
- },
- "finish_reason": "stop"
+ "model": "gpt-5",
+ "output": [{
+ "id": "msg_XXXXX",
+ "type": "message",
+ "role": "assistant",
+ "content": [{
+ "type": "output_text",
+ "text": "ASSISTANT REPLY"
+ }]
}],
"usage": {
- "prompt_tokens": 8,
- "completion_tokens": 9,
+ "input_tokens": 8,
+ "output_tokens": 9,
"total_tokens": 17
}
}"#,
let mut server = mockito::Server::new();
let mock = server
- .mock("POST", "/v1/chat/completions")
+ .mock("POST", "/v1/responses")
.with_header("content-type", "application/json")
.with_header("authorization", "Bearer ABCDE")
.match_body(mockito::Matcher::PartialJson(json!({
- "messages": [{
- "content": "Hello",
- "role": "user",
+ "model": "gpt-5",
+ "input": [{
+ "role": "user",
+ "content": [{
+ "type": "text",
+ "text": "Hello"
+ }]
}]
})))
.with_body(
r#"{
- "id": "chatcmpl-XXXXX",
+ "id": "resp_XXXXX",
"created": 1688413145,
- "model": "gpt-3.5-turbo-0613",
- "choices": [{
- "index": 0,
- "message": {
- "role": "assistant",
- "content": "ASSISTANT REPLY"
- },
- "finish_reason": "stop"
+ "model": "gpt-5",
+ "output": [{
+ "id": "msg_XXXXX",
+ "type": "message",
+ "role": "assistant",
+ "content": [{
+ "type": "output_text",
+ "text": "ASSISTANT REPLY"
+ }]
}],
- "usage": {
- "prompt_tokens": 8,
- "completion_tokens": 9,
- "total_tokens": 17
- }
- }"#,
+ "usage": {
+ "input_tokens": 8,
+ "output_tokens": 9,
+ "total_tokens": 17
+ }
+ }"#,
)
.create();
.stdout(predicate::str::contains("ASSISTANT REPLY"));
}
+#[test]
+fn chat_with_reasoning_effort() {
+ let mut server = mockito::Server::new();
+
+ let mock = server
+ .mock("POST", "/v1/responses")
+ .with_header("content-type", "application/json")
+ .with_header("authorization", "Bearer ABCDE")
+ .match_body(mockito::Matcher::PartialJson(json!({
+ "model": "gpt-5",
+ "reasoning": {
+ "effort": "medium"
+ },
+ "input": [{
+ "role": "user",
+ "content": [{
+ "type": "text",
+ "text": "Hello"
+ }]
+ }]
+ })))
+ .with_body(
+ r#"{
+ "id": "resp_XXXXX",
+ "created": 1688413145,
+ "model": "gpt-5",
+ "output": [{
+ "id": "msg_XXXXX",
+ "type": "message",
+ "role": "assistant",
+ "content": [{
+ "type": "output_text",
+ "text": "ASSISTANT REPLY"
+ }]
+ }],
+ "usage": {
+ "input_tokens": 8,
+ "output_tokens": 9,
+ "total_tokens": 17
+ }
+ }"#,
+ )
+ .create();
+
+ let cmd = Command::cargo_bin("cogni")
+ .unwrap()
+ .args(["-u", "Hello", "--reasoning-effort", "medium"])
+ .env("OPENAI_API_ENDPOINT", server.url())
+ .env("OPENAI_API_KEY", "ABCDE")
+ .assert();
+
+ mock.assert();
+
+ cmd.success()
+ .stdout(predicate::str::contains("ASSISTANT REPLY"));
+}
+
/// Test messages provided via
/// - System message flag
/// - Assistant message flag
let mut server = mockito::Server::new();
let mock = server
- .mock("POST", "/v1/chat/completions")
+ .mock("POST", "/v1/responses")
.with_header("content-type", "application/json")
.with_header("authorization", "Bearer ABCDE")
.match_body(mockito::Matcher::PartialJson(json!({
- "messages": [{
+ "model": "gpt-5",
+ "input": [{
"role": "system",
- "content": "SYSTEM",
+ "content": [{
+ "type": "text",
+ "text": "SYSTEM"
+ }],
}, {
"role": "user",
- "content": "USER_1",
+ "content": [{
+ "type": "text",
+ "text": "USER_1"
+ }],
}, {
"role": "assistant",
- "content": "ASSI_1",
+ "content": [{
+ "type": "text",
+ "text": "ASSI_1"
+ }],
}, {
"role": "user",
- "content": "USER_2",
+ "content": [{
+ "type": "text",
+ "text": "USER_2"
+ }],
}, {
"role": "assistant",
- "content": "ASSI_2",
+ "content": [{
+ "type": "text",
+ "text": "ASSI_2"
+ }],
}, {
"role": "user",
- "content": "USER_STDIN",
+ "content": [{
+ "type": "text",
+ "text": "USER_STDIN"
+ }],
}]
})))
.with_body(
r#"{
- "id": "chatcmpl-XXXXX",
+ "id": "resp_XXXXX",
"created": 1688413145,
- "model": "gpt-3.5-turbo-0613",
- "choices": [{
- "index": 0,
- "message": {
- "role": "assistant",
- "content": "ASSISTANT REPLY"
- },
- "finish_reason": "stop"
+ "model": "gpt-5",
+ "output": [{
+ "id": "msg_XXXXX",
+ "type": "message",
+ "role": "assistant",
+ "content": [{
+ "type": "output_text",
+ "text": "ASSISTANT REPLY"
+ }]
}],
"usage": {
- "prompt_tokens": 8,
- "completion_tokens": 9,
+ "input_tokens": 8,
+ "output_tokens": 9,
"total_tokens": 17
}
}"#,
let mut server = mockito::Server::new();
let mock = server
- .mock("POST", "/v1/chat/completions")
+ .mock("POST", "/v1/responses")
.with_status(400)
.with_header("content-type", "application/json")
.with_header("authorization", "Bearer ABCDE")
.match_body(mockito::Matcher::PartialJson(json!({
- "messages": [{
+ "model": "gpt-5",
+ "input": [{
"role": "user",
- "content": "USER",
+ "content": [{
+ "type": "text",
+ "text": "USER"
+ }],
}],
"temperature": 1000.0 // invalid param
})))
infile.write_str("Hello from file").unwrap();
let mock = server
- .mock("POST", "/v1/chat/completions")
+ .mock("POST", "/v1/responses")
.with_header("content-type", "application/json")
.with_header("authorization", "Bearer ABCDE")
.match_body(mockito::Matcher::PartialJson(json!({
- "messages": [{
+ "model": "gpt-5",
+ "input": [{
"role": "user",
- "content": "Hello from file",
+ "content": [{
+ "type": "text",
+ "text": "Hello from file"
+ }],
}],
})))
.with_body(
r#"{
- "id": "chatcmpl-XXXXX",
+ "id": "resp_XXXXX",
"created": 1688413145,
- "model": "gpt-3.5-turbo-0613",
- "choices": [{
- "index": 0,
- "message": {
- "role": "assistant",
- "content": "ASSISTANT REPLY"
- },
- "finish_reason": "stop"
+ "model": "gpt-5",
+ "output": [{
+ "id": "msg_XXXXX",
+ "type": "message",
+ "role": "assistant",
+ "content": [{
+ "type": "output_text",
+ "text": "ASSISTANT REPLY"
+ }]
}],
"usage": {
- "prompt_tokens": 8,
- "completion_tokens": 9,
+ "input_tokens": 8,
+ "output_tokens": 9,
"total_tokens": 17
}
}"#,