diff --git a/crates/rig-core/src/providers/copilot/mod.rs b/crates/rig-core/src/providers/copilot/mod.rs index 90c6e5326..c9bc10d3c 100644 --- a/crates/rig-core/src/providers/copilot/mod.rs +++ b/crates/rig-core/src/providers/copilot/mod.rs @@ -555,7 +555,7 @@ fn route_for_model(model: &str) -> CompletionRoute { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "api", rename_all = "snake_case")] pub enum CopilotCompletionResponse { - Chat(ChatCompletionResponse), + Chat(Box), Responses(Box), } @@ -865,7 +865,7 @@ where Ok(completion::CompletionResponse { choice: core.choice, usage: core.usage, - raw_response: CopilotCompletionResponse::Chat(response), + raw_response: CopilotCompletionResponse::Chat(Box::new(response)), message_id: core.message_id, }) } diff --git a/crates/rig-core/src/providers/groq.rs b/crates/rig-core/src/providers/groq.rs index 82ee0451d..4524b7114 100644 --- a/crates/rig-core/src/providers/groq.rs +++ b/crates/rig-core/src/providers/groq.rs @@ -166,10 +166,12 @@ pub(super) struct GroqCompletionRequest { pub messages: Vec, #[serde(skip_serializing_if = "Option::is_none")] temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, #[serde(skip_serializing_if = "Vec::is_empty")] tools: Vec, #[serde(skip_serializing_if = "Option::is_none")] - tool_choice: Option, + tool_choice: Option, #[serde(flatten, skip_serializing_if = "Option::is_none")] pub additional_params: Option, pub(super) stream: bool, @@ -177,6 +179,72 @@ pub(super) struct GroqCompletionRequest { pub(super) stream_options: Option, } +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +enum GroqToolChoice { + Mode(GroqToolChoiceMode), + Function { + r#type: GroqToolChoiceFunctionKind, + function: GroqToolChoiceFunction, + }, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +enum GroqToolChoiceMode { + #[default] + Auto, + None, + Required, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +enum GroqToolChoiceFunctionKind { + Function, +} + +#[derive(Debug, Serialize, Deserialize)] +struct GroqToolChoiceFunction { + name: String, +} + +impl TryFrom for GroqToolChoice { + type Error = CompletionError; + + fn try_from(value: message::ToolChoice) -> Result { + match value { + message::ToolChoice::Auto => Ok(Self::Mode(GroqToolChoiceMode::Auto)), + message::ToolChoice::None => Ok(Self::Mode(GroqToolChoiceMode::None)), + message::ToolChoice::Required => Ok(Self::Mode(GroqToolChoiceMode::Required)), + message::ToolChoice::Specific { function_names } => { + specific_groq_tool_choice(function_names) + } + } + } +} + +fn specific_groq_tool_choice( + function_names: Vec, +) -> Result { + let mut names = function_names.into_iter(); + let Some(name) = names.next() else { + return Err(CompletionError::RequestError( + "ToolChoice::Specific requires at least one function name".into(), + )); + }; + if names.next().is_some() { + return Err(CompletionError::RequestError( + "Groq only supports forcing one specific tool".into(), + )); + } + + Ok(GroqToolChoice::Function { + r#type: GroqToolChoiceFunctionKind::Function, + function: GroqToolChoiceFunction { name }, + }) +} + #[derive(Debug, Serialize, Deserialize, Default)] pub(super) struct StreamOptions { pub(super) include_usage: bool, @@ -187,9 +255,6 @@ impl TryFrom<(&str, CompletionRequest)> for GroqCompletionRequest { fn try_from((model, mut req): (&str, CompletionRequest)) -> Result { let chat_history = req.chat_history_with_documents(); - if req.output_schema.is_some() { - tracing::warn!("Structured outputs currently not supported for Groq"); - } let model = req.model.clone().unwrap_or_else(|| model.to_string()); // Build up the order of messages. let mut partial_history = vec![]; @@ -215,13 +280,39 @@ impl TryFrom<(&str, CompletionRequest)> for GroqCompletionRequest { let tool_choice = req .tool_choice .clone() - .map(crate::providers::openai::ToolChoice::try_from) + .map(GroqToolChoice::try_from) .transpose()?; let mut additional_params_payload = req.additional_params.take().unwrap_or(Value::Null); let native_tools = extract_native_tools_from_additional_params(&mut additional_params_payload)?; + if let Some(schema) = req.output_schema { + let name = schema + .as_object() + .and_then(|object| object.get("title")) + .and_then(serde_json::Value::as_str) + .unwrap_or("response_schema") + .to_string(); + let mut schema_value = schema.to_value(); + super::openai::sanitize_schema(&mut schema_value); + let response_format = serde_json::json!({ + "response_format": { + "type": "json_schema", + "json_schema": { + "name": name, + "strict": true, + "schema": schema_value, + } + } + }); + additional_params_payload = if additional_params_payload.is_null() { + response_format + } else { + json_utils::merge(additional_params_payload, response_format) + }; + } + let mut additional_params: Option = if additional_params_payload.is_null() { None @@ -234,6 +325,7 @@ impl TryFrom<(&str, CompletionRequest)> for GroqCompletionRequest { model: model.to_string(), messages: full_history, temperature: req.temperature, + max_tokens: req.max_tokens, tools: req .tools .clone() @@ -415,7 +507,7 @@ where span.record("gen_ai.usage.input_tokens", usage.prompt_tokens); span.record( "gen_ai.usage.output_tokens", - usage.total_tokens - usage.prompt_tokens, + usage.token_usage().output_tokens, ); span.record( "gen_ai.usage.cache_read.input_tokens", @@ -629,9 +721,27 @@ enum StreamingDelta { }, } +#[derive(Deserialize, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +enum FinishReason { + ToolCalls, + Stop, + Length, + ContentFilter, + FunctionCall, + #[serde(untagged)] + Other(String), +} + #[derive(Deserialize, Debug)] struct StreamingChoice { delta: StreamingDelta, + finish_reason: Option, +} + +#[derive(Deserialize, Debug)] +struct StreamingGroqMetadata { + usage: Option, } #[derive(Deserialize, Debug)] @@ -640,6 +750,7 @@ struct StreamingCompletionChunk { model: Option, choices: Vec, usage: Option, + x_groq: Option, } #[derive(Clone, Deserialize, Serialize, Debug)] @@ -676,32 +787,44 @@ impl CompatibleStreamProfile for GroqCompatibleProfile { } }; + let usage = data + .usage + .or_else(|| data.x_groq.and_then(|metadata| metadata.usage)); + Ok(Some( openai_chat_completions_compatible::normalize_first_choice_chunk( data.id, data.model, - data.usage, + usage, &data.choices, - |choice| match &choice.delta { - StreamingDelta::Reasoning { reasoning } => CompatibleChoiceData { - finish_reason: CompatibleFinishReason::Other, - text: None, - reasoning: Some(reasoning.clone()), - tool_calls: Vec::new(), - details: Vec::new(), - }, - StreamingDelta::MessageContent { - content, - tool_calls, - } => CompatibleChoiceData { - finish_reason: CompatibleFinishReason::Other, - text: content.clone(), - reasoning: None, - tool_calls: openai_chat_completions_compatible::tool_call_chunks( + |choice| { + let finish_reason = match choice.finish_reason { + Some(FinishReason::ToolCalls | FinishReason::FunctionCall) => { + CompatibleFinishReason::ToolCalls + } + _ => CompatibleFinishReason::Other, + }; + match &choice.delta { + StreamingDelta::Reasoning { reasoning } => CompatibleChoiceData { + finish_reason, + text: None, + reasoning: Some(reasoning.clone()), + tool_calls: Vec::new(), + details: Vec::new(), + }, + StreamingDelta::MessageContent { + content, tool_calls, - ), - details: Vec::new(), - }, + } => CompatibleChoiceData { + finish_reason, + text: content.clone(), + reasoning: None, + tool_calls: openai_chat_completions_compatible::tool_call_chunks( + tool_calls, + ), + details: Vec::new(), + }, + } }, ), )) @@ -742,10 +865,12 @@ where mod tests { use crate::{ OneOrMany, + completion::CompletionRequestBuilder, providers::{ groq::{GroqAdditionalParameters, GroqCompletionRequest}, openai::{Message, UserContent}, }, + test_utils::MockCompletionModel, }; #[test] @@ -759,6 +884,7 @@ mod tests { let groq = GroqCompletionRequest { model: "openai/gpt-120b-oss".to_string(), temperature: None, + max_tokens: None, tool_choice: None, stream_options: None, tools: Vec::new(), @@ -790,6 +916,34 @@ mod tests { }) ) } + #[test] + fn groq_request_maps_output_schema_max_tokens_and_specific_tool_choice() { + let request = CompletionRequestBuilder::new(MockCompletionModel::default(), "Return JSON") + .max_tokens(64) + .tool(crate::completion::ToolDefinition { + name: "choose_beta".to_string(), + description: "Choose beta".to_string(), + parameters: serde_json::json!({"type":"object","properties":{},"required":[]}), + }) + .tool_choice(crate::message::ToolChoice::Specific { + function_names: vec!["choose_beta".to_string()], + }) + .output_schema(schemars::schema_for!(serde_json::Value)) + .build(); + + let groq = GroqCompletionRequest::try_from(("llama-3.3-70b-versatile", request)) + .expect("Groq request should serialize"); + let json = serde_json::to_value(groq).expect("request should serialize"); + + assert_eq!(json["max_tokens"], 64); + assert_eq!( + json["tool_choice"], + serde_json::json!({"type":"function","function":{"name":"choose_beta"}}) + ); + assert_eq!(json["response_format"]["type"], "json_schema"); + assert_eq!(json["response_format"]["json_schema"]["strict"], true); + } + #[test] fn test_client_initialization() { let _client = diff --git a/crates/rig-core/src/providers/openai/completion/mod.rs b/crates/rig-core/src/providers/openai/completion/mod.rs index 59b8032e8..e048eedc0 100644 --- a/crates/rig-core/src/providers/openai/completion/mod.rs +++ b/crates/rig-core/src/providers/openai/completion/mod.rs @@ -959,19 +959,7 @@ impl TryFrom for completion::CompletionResponse, pub total_tokens: usize, #[serde(skip_serializing_if = "Option::is_none")] pub prompt_tokens_details: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub completion_tokens_details: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub queue_time: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub prompt_time: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub completion_time: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub total_time: Option, } impl Usage { pub fn new() -> Self { Self { prompt_tokens: 0, + completion_tokens: None, total_tokens: 0, prompt_tokens_details: None, + completion_tokens_details: None, + queue_time: None, + prompt_time: None, + completion_time: None, + total_time: None, } } } @@ -1103,15 +1116,23 @@ impl fmt::Display for Usage { impl GetTokenUsage for Usage { fn token_usage(&self) -> crate::completion::Usage { - crate::providers::internal::completion_usage( + let mut usage = crate::providers::internal::completion_usage( self.prompt_tokens as u64, - (self.total_tokens - self.prompt_tokens) as u64, + self.completion_tokens + .unwrap_or_else(|| self.total_tokens.saturating_sub(self.prompt_tokens)) + as u64, self.total_tokens as u64, self.prompt_tokens_details .as_ref() .map(|d| d.cached_tokens as u64) .unwrap_or(0), - ) + ); + usage.reasoning_tokens = self + .completion_tokens_details + .as_ref() + .map(|d| d.reasoning_tokens as u64) + .unwrap_or(0); + usage } } diff --git a/tests/cassettes/groq/agent_tool_sessions/json_object_response_format_roundtrip.yaml b/tests/cassettes/groq/agent_tool_sessions/json_object_response_format_roundtrip.yaml new file mode 100644 index 000000000..a8bfb05da --- /dev/null +++ b/tests/cassettes/groq/agent_tool_sessions/json_object_response_format_roundtrip.yaml @@ -0,0 +1,16 @@ +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"max_tokens":128,"messages":[{"content":[{"text":"Return only valid JSON. No markdown.","type":"text"}],"role":"system"},{"content":"Return a JSON object with release lane canary, risk low, and checks compile=true and replay=true.","role":"user"}],"model":"llama-3.3-70b-versatile","response_format":{"type":"json_object"},"stream":false}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"stop","index":0,"logprobs":null,"message":{"content":"{\n \"release_lane\": \"canary\",\n \"risk\": \"low\",\n \"checks\": {\n \"compile\": true,\n \"replay\": true\n }\n}","role":"assistant"}}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.080923801,"completion_tokens":38,"prompt_time":0.038802288,"prompt_tokens":80,"queue_time":0.17809775,"total_time":0.119726089,"total_tokens":118},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_1","seed":788503977}}' diff --git a/tests/cassettes/groq/agent_tool_sessions/json_schema_structured_output_roundtrip.yaml b/tests/cassettes/groq/agent_tool_sessions/json_schema_structured_output_roundtrip.yaml new file mode 100644 index 000000000..32e86cb7d --- /dev/null +++ b/tests/cassettes/groq/agent_tool_sessions/json_schema_structured_output_roundtrip.yaml @@ -0,0 +1,16 @@ +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"max_tokens":128,"messages":[{"content":[{"text":"Return only the requested structured object.","type":"text"}],"role":"system"},{"content":"Return lane=canary, risk=low, checks.compile=true, and checks.replay=true.","role":"user"}],"model":"openai/gpt-oss-20b","response_format":{"json_schema":{"name":"StructuredReleasePlan","schema":{"$defs":{"StructuredChecks":{"additionalProperties":false,"properties":{"compile":{"type":"boolean"},"replay":{"type":"boolean"}},"required":["compile","replay"],"type":"object"}},"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"checks":{"$ref":"#/$defs/StructuredChecks"},"lane":{"type":"string"},"risk":{"type":"string"}},"required":["lane","checks","risk"],"title":"StructuredReleasePlan","type":"object"},"strict":true},"type":"json_schema"},"stream":false}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"stop","index":0,"logprobs":null,"message":{"content":"{\"lane\":\"canary\",\"risk\":\"low\",\"checks\":{\"compile\":true,\"replay\":true}}","reasoning":"We need to output the JSON object that matches the schema. So:\n\n{\n \"lane\":\"canary\",\n \"risk\":\"low\",\n \"checks\":{\n \"compile\":true,\n \"replay\":true\n }\n}\n\nCompact JSON, no whitespace? Use minimal whitespace. The schema requires those fields. Ensure no extra. We''ll output exactly that.","role":"assistant"}}],"created":0,"id":"chatcmpl-REDACTED_1","model":"openai/gpt-oss-20b","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.120161944,"completion_tokens":104,"completion_tokens_details":{"reasoning_tokens":74},"prompt_time":0.012686144,"prompt_tokens":260,"queue_time":0.213966354,"total_time":0.132848088,"total_tokens":364},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_1","seed":1226432470}}' diff --git a/tests/cassettes/groq/agent_tool_sessions/long_history_replay_with_tool_result_continuation.yaml b/tests/cassettes/groq/agent_tool_sessions/long_history_replay_with_tool_result_continuation.yaml new file mode 100644 index 000000000..1f5894086 --- /dev/null +++ b/tests/cassettes/groq/agent_tool_sessions/long_history_replay_with_tool_result_continuation.yaml @@ -0,0 +1,16 @@ +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are concise and should rely on the provided chat history.","type":"text"}],"role":"system"},{"content":"My favorite color is teal. Please remember it.","role":"user"},{"content":[{"text":"Noted: your favorite color is teal.","type":"text"}],"role":"assistant"},{"content":"For this release, use the canary lane.","role":"user"},{"content":[{"text":"Understood: the release lane is canary.","type":"text"}],"role":"assistant"},{"content":"Look up the harbor label with the tool.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"lookup_harbor_label"},"id":"call_REDACTED_1","type":"function"}]},{"content":"crimson-harbor","role":"tool","tool_call_id":"call_REDACTED_1"},{"content":[{"text":"The harbor label is crimson-harbor.","type":"text"}],"role":"assistant"},{"content":"Answer in one short sentence: what is my favorite color, which label came from the tool, and which release lane did I choose? Do not call any tools.","role":"user"}],"model":"llama-3.3-70b-versatile","stream":false,"tool_choice":"none","tools":[{"function":{"description":"Return the alpha signal marker.","name":"lookup_harbor_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"stop","index":0,"logprobs":null,"message":{"content":"Your favorite color is teal, the harbor label is crimson-harbor, and you chose the canary release lane.","role":"assistant"}}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.060355109,"completion_tokens":25,"prompt_time":0.012750972,"prompt_tokens":222,"queue_time":0.373757595,"total_time":0.073106081,"total_tokens":247},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_1","seed":1144826916}}' diff --git a/tests/cassettes/groq/agent_tool_sessions/low_latency_streaming_text_surfaces_final_usage.yaml b/tests/cassettes/groq/agent_tool_sessions/low_latency_streaming_text_surfaces_final_usage.yaml new file mode 100644 index 000000000..103e53d31 --- /dev/null +++ b/tests/cassettes/groq/agent_tool_sessions/low_latency_streaming_text_surfaces_final_usage.yaml @@ -0,0 +1,54 @@ +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: text/event-stream + - name: content-type + value: application/json + body: '{"max_tokens":64,"messages":[{"content":[{"text":"Stream the requested short sequence exactly.","type":"text"}],"role":"system"},{"content":"Reply with exactly this comma-separated sequence and no extra words: alpha,beta,gamma,delta,epsilon,zeta,eta,theta","role":"user"}],"model":"llama-3.3-70b-versatile","stream":true,"stream_options":{"include_usage":true}}' +then: + status: 200 + header: + - name: content-type + value: text/event-stream + body: |+ + data: {"choices":[{"delta":{"content":"","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1","x_groq":{"id":"req_REDACTED_1","seed":1715824332}} + + data: {"choices":[{"delta":{"content":"alpha"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":",b"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":"eta"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":",g"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":"amma"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":",d"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":"elta"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":","},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":"epsilon"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":",z"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":"eta"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":","},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":"eta"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":","},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"content":"theta"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{},"finish_reason":"stop","index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.023123364,"completion_tokens":16,"prompt_time":0.004669371,"prompt_tokens":69,"queue_time":0.191142898,"total_time":0.027792735,"total_tokens":85},"x_groq":{"id":"req_REDACTED_1","usage":{"completion_time":0.023123364,"completion_tokens":16,"prompt_time":0.004669371,"prompt_tokens":69,"queue_time":0.191142898,"total_time":0.027792735,"total_tokens":85}}} + + data: {"choices":[],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.023123364,"completion_tokens":16,"prompt_time":0.004669371,"prompt_tokens":69,"queue_time":0.191142898,"total_time":0.027792735,"total_tokens":85}} + + data: [DONE] + diff --git a/tests/cassettes/groq/agent_tool_sessions/parallel_tool_calls_single_turn_nonstreaming.yaml b/tests/cassettes/groq/agent_tool_sessions/parallel_tool_calls_single_turn_nonstreaming.yaml new file mode 100644 index 000000000..71e621e5e --- /dev/null +++ b/tests/cassettes/groq/agent_tool_sessions/parallel_tool_calls_single_turn_nonstreaming.yaml @@ -0,0 +1,33 @@ +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a precise assistant. When tools are available, you must use them instead of guessing. Call both `lookup_harbor_label` and `lookup_orchard_label` before writing any normal text. Never call the same tool twice once you already have its result.","type":"text"}],"role":"system"},{"content":"Call `lookup_harbor_label` and `lookup_orchard_label` exactly once each before answering. After both tool results are available, stop calling tools and respond in one short sentence that includes both exact tool outputs.","role":"user"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":true,"stream":false,"tools":[{"function":{"description":"Return the alpha signal marker.","name":"lookup_harbor_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Return the beta signal marker.","name":"lookup_orchard_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"tool_calls","index":0,"logprobs":null,"message":{"role":"assistant","tool_calls":[{"function":{"arguments":"null","name":"lookup_harbor_label"},"id":"call_REDACTED_1","type":"function"},{"function":{"arguments":"null","name":"lookup_orchard_label"},"id":"call_REDACTED_2","type":"function"}]}}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.101852651,"completion_tokens":35,"prompt_time":0.144640062,"prompt_tokens":359,"queue_time":0.330405175,"total_time":0.246492713,"total_tokens":394},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_1","seed":574027631}}' +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a precise assistant. When tools are available, you must use them instead of guessing. Call both `lookup_harbor_label` and `lookup_orchard_label` before writing any normal text. Never call the same tool twice once you already have its result.","type":"text"}],"role":"system"},{"content":"Call `lookup_harbor_label` and `lookup_orchard_label` exactly once each before answering. After both tool results are available, stop calling tools and respond in one short sentence that includes both exact tool outputs.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"null","name":"lookup_harbor_label"},"id":"call_REDACTED_1","type":"function"},{"function":{"arguments":"null","name":"lookup_orchard_label"},"id":"call_REDACTED_2","type":"function"}]},{"content":"crimson-harbor","role":"tool","tool_call_id":"call_REDACTED_1"},{"content":"silver-orchard","role":"tool","tool_call_id":"call_REDACTED_2"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":true,"stream":false,"tools":[{"function":{"description":"Return the alpha signal marker.","name":"lookup_harbor_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Return the beta signal marker.","name":"lookup_orchard_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"stop","index":0,"logprobs":null,"message":{"content":"The results from the tools are crimson-harbor and silver-orchard.","role":"assistant"}}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_2","usage":{"completion_time":0.067834622,"completion_tokens":17,"prompt_time":0.036641237,"prompt_tokens":409,"queue_time":0.166550983,"total_time":0.104475859,"total_tokens":426},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_2","seed":2068743589}}' diff --git a/tests/cassettes/groq/agent_tool_sessions/parallel_tool_calls_single_turn_streaming.yaml b/tests/cassettes/groq/agent_tool_sessions/parallel_tool_calls_single_turn_streaming.yaml new file mode 100644 index 000000000..328bee967 --- /dev/null +++ b/tests/cassettes/groq/agent_tool_sessions/parallel_tool_calls_single_turn_streaming.yaml @@ -0,0 +1,93 @@ +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: text/event-stream + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a precise assistant. When tools are available, you must use them instead of guessing. Call both `lookup_harbor_label` and `lookup_orchard_label` before writing any normal text. Never call the same tool twice once you already have its result.","type":"text"}],"role":"system"},{"content":"Call `lookup_harbor_label` and `lookup_orchard_label` exactly once each before answering. After both tool results are available, stop calling tools and respond in one short sentence that includes both exact tool outputs.","role":"user"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":true,"stream":true,"stream_options":{"include_usage":true},"tools":[{"function":{"description":"Return the alpha signal marker.","name":"lookup_harbor_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Return the beta signal marker.","name":"lookup_orchard_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: text/event-stream + body: |+ + data: {"choices":[{"delta":{"content":null,"role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1","x_groq":{"id":"req_REDACTED_1","seed":446932484}} + + data: {"choices":[{"delta":{"tool_calls":[{"function":{"arguments":"null","name":"lookup_harbor_label"},"id":"call_REDACTED_1","index":0,"type":"function"}]},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{"tool_calls":[{"function":{"arguments":"null","name":"lookup_orchard_label"},"id":"call_REDACTED_2","index":1,"type":"function"}]},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{},"finish_reason":"tool_calls","index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.098884052,"completion_tokens":35,"prompt_time":0.122020281,"prompt_tokens":359,"queue_time":0.548385398,"total_time":0.220904333,"total_tokens":394},"x_groq":{"id":"req_REDACTED_1","usage":{"completion_time":0.098884052,"completion_tokens":35,"prompt_time":0.122020281,"prompt_tokens":359,"queue_time":0.548385398,"total_time":0.220904333,"total_tokens":394}}} + + data: {"choices":[],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.098884052,"completion_tokens":35,"prompt_time":0.122020281,"prompt_tokens":359,"queue_time":0.548385398,"total_time":0.220904333,"total_tokens":394}} + + data: [DONE] + +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: text/event-stream + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a precise assistant. When tools are available, you must use them instead of guessing. Call both `lookup_harbor_label` and `lookup_orchard_label` before writing any normal text. Never call the same tool twice once you already have its result.","type":"text"}],"role":"system"},{"content":"Call `lookup_harbor_label` and `lookup_orchard_label` exactly once each before answering. After both tool results are available, stop calling tools and respond in one short sentence that includes both exact tool outputs.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"null","name":"lookup_harbor_label"},"id":"call_REDACTED_1","type":"function"},{"function":{"arguments":"null","name":"lookup_orchard_label"},"id":"call_REDACTED_2","type":"function"}]},{"content":"crimson-harbor","role":"tool","tool_call_id":"call_REDACTED_1"},{"content":"silver-orchard","role":"tool","tool_call_id":"call_REDACTED_2"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":true,"stream":true,"stream_options":{"include_usage":true},"tools":[{"function":{"description":"Return the alpha signal marker.","name":"lookup_harbor_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Return the beta signal marker.","name":"lookup_orchard_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: text/event-stream + body: |+ + data: {"choices":[{"delta":{"content":"","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2","x_groq":{"id":"req_REDACTED_2","seed":1773706693}} + + data: {"choices":[{"delta":{"content":"The"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":" alpha"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":" signal"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":" marker"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":" is"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":" crimson"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":"-h"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":"ar"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":"bor"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":" and"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":" the"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":" beta"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":" signal"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":" marker"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":" is"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":" silver"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":"-"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":"orch"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":"ard"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{"content":"."},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{},"finish_reason":"stop","index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2","usage":{"completion_time":0.05055923,"completion_tokens":21,"prompt_time":0.064808971,"prompt_tokens":409,"queue_time":0.212925597,"total_time":0.115368201,"total_tokens":430},"x_groq":{"id":"req_REDACTED_2","usage":{"completion_time":0.05055923,"completion_tokens":21,"prompt_time":0.064808971,"prompt_tokens":409,"queue_time":0.212925597,"total_time":0.115368201,"total_tokens":430}}} + + data: {"choices":[],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_2","usage":{"completion_time":0.05055923,"completion_tokens":21,"prompt_time":0.064808971,"prompt_tokens":409,"queue_time":0.212925597,"total_time":0.115368201,"total_tokens":430}} + + data: [DONE] + diff --git a/tests/cassettes/groq/agent_tool_sessions/raw_stream_complex_tool_call_deltas_have_object_arguments.yaml b/tests/cassettes/groq/agent_tool_sessions/raw_stream_complex_tool_call_deltas_have_object_arguments.yaml new file mode 100644 index 000000000..b844a5633 --- /dev/null +++ b/tests/cassettes/groq/agent_tool_sessions/raw_stream_complex_tool_call_deltas_have_object_arguments.yaml @@ -0,0 +1,26 @@ +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: text/event-stream + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"Use the requested tool call and no prose before it.","type":"text"}],"role":"system"},{"content":"Call inspect_manifest exactly once for project rig-groq with critical=true, retries=2, steps [{name: plan, weight: 1}, {name: verify, weight: 2}], and note `streamed nested JSON`. Do not write normal text before the tool call.","role":"user"}],"model":"llama-3.3-70b-versatile","stream":true,"stream_options":{"include_usage":true},"tool_choice":"required","tools":[{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: text/event-stream + body: |+ + data: {"choices":[{"delta":{"content":null,"role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1","x_groq":{"id":"req_REDACTED_1","seed":100850752}} + + data: {"choices":[{"delta":{"tool_calls":[{"function":{"arguments":"{\"flags\":{\"critical\":true,\"retries\":2},\"note\":\"streamed nested JSON\",\"project\":\"rig-groq\",\"steps\":[{\"name\":\"plan\",\"weight\":1},{\"name\":\"verify\",\"weight\":2}]}","name":"inspect_manifest"},"id":"call_REDACTED_1","index":0,"type":"function"}]},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{},"finish_reason":"tool_calls","index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.097952422,"completion_tokens":66,"prompt_time":0.199207555,"prompt_tokens":369,"queue_time":0.179273032,"total_time":0.297159977,"total_tokens":435},"x_groq":{"id":"req_REDACTED_1","usage":{"completion_time":0.097952422,"completion_tokens":66,"prompt_time":0.199207555,"prompt_tokens":369,"queue_time":0.179273032,"total_time":0.297159977,"total_tokens":435}}} + + data: {"choices":[],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.097952422,"completion_tokens":66,"prompt_time":0.199207555,"prompt_tokens":369,"queue_time":0.179273032,"total_time":0.297159977,"total_tokens":435}} + + data: [DONE] + diff --git a/tests/cassettes/groq/agent_tool_sessions/sequential_complex_tool_calls_nonstreaming.yaml b/tests/cassettes/groq/agent_tool_sessions/sequential_complex_tool_calls_nonstreaming.yaml new file mode 100644 index 000000000..fb6f3f457 --- /dev/null +++ b/tests/cassettes/groq/agent_tool_sessions/sequential_complex_tool_calls_nonstreaming.yaml @@ -0,0 +1,101 @@ +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. For the production-readiness scenario, call exactly one tool at a time in this order: 1. ping_empty with an empty JSON object. 2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. 3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. 4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. 5. escape_echo with the exact escaped text from the user. After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","type":"text"}],"role":"system"},{"content":"Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`.","role":"user"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":false,"stream":false,"tools":[{"function":{"description":"Return EMPTY-OK. This tool takes no arguments.","name":"ping_empty","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"},{"function":{"description":"Join label strings with the requested separator.","name":"join_labels","parameters":{"properties":{"labels":{"items":{"type":"string"},"type":"array"},"separator":{"type":"string"}},"required":["labels","separator"],"type":"object"}},"type":"function"},{"function":{"description":"Validate optional and nullable argument serialization.","name":"optional_nullable_probe","parameters":{"properties":{"name":{"type":"string"},"note":{"type":["string","null"]},"nullable_code":{"type":["string","null"]}},"required":["name","nullable_code"],"type":"object"}},"type":"function"},{"function":{"description":"Echo a string containing escaping-sensitive characters.","name":"escape_echo","parameters":{"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"tool_calls","index":0,"logprobs":null,"message":{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"ping_empty"},"id":"call_REDACTED_1","type":"function"}]}}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.043628303,"completion_tokens":10,"prompt_time":0.573932644,"prompt_tokens":765,"queue_time":0.337230076,"total_time":0.617560947,"total_tokens":775},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_1","seed":1362993320}}' +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. For the production-readiness scenario, call exactly one tool at a time in this order: 1. ping_empty with an empty JSON object. 2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. 3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. 4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. 5. escape_echo with the exact escaped text from the user. After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","type":"text"}],"role":"system"},{"content":"Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"ping_empty"},"id":"call_REDACTED_1","type":"function"}]},{"content":"EMPTY-OK","role":"tool","tool_call_id":"call_REDACTED_1"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":false,"stream":false,"tools":[{"function":{"description":"Return EMPTY-OK. This tool takes no arguments.","name":"ping_empty","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"},{"function":{"description":"Join label strings with the requested separator.","name":"join_labels","parameters":{"properties":{"labels":{"items":{"type":"string"},"type":"array"},"separator":{"type":"string"}},"required":["labels","separator"],"type":"object"}},"type":"function"},{"function":{"description":"Validate optional and nullable argument serialization.","name":"optional_nullable_probe","parameters":{"properties":{"name":{"type":"string"},"note":{"type":["string","null"]},"nullable_code":{"type":["string","null"]}},"required":["name","nullable_code"],"type":"object"}},"type":"function"},{"function":{"description":"Echo a string containing escaping-sensitive characters.","name":"escape_echo","parameters":{"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"tool_calls","index":0,"logprobs":null,"message":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"flags\":{\"critical\":true,\"retries\":2},\"note\":\"line one; line two says \\\"hello\\\" and path C:/rig/groq\",\"project\":\"rig-groq\",\"steps\":[{\"name\":\"plan\",\"weight\":1},{\"name\":\"verify\",\"weight\":2}]}","name":"inspect_manifest"},"id":"call_REDACTED_2","type":"function"}]}}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_2","usage":{"completion_time":0.163966283,"completion_tokens":82,"prompt_time":0.428032833,"prompt_tokens":788,"queue_time":0.341869392,"total_time":0.591999116,"total_tokens":870},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_2","seed":1667657194}}' +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. For the production-readiness scenario, call exactly one tool at a time in this order: 1. ping_empty with an empty JSON object. 2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. 3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. 4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. 5. escape_echo with the exact escaped text from the user. After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","type":"text"}],"role":"system"},{"content":"Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"ping_empty"},"id":"call_REDACTED_1","type":"function"}]},{"content":"EMPTY-OK","role":"tool","tool_call_id":"call_REDACTED_1"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"flags\":{\"critical\":true,\"retries\":2},\"note\":\"line one; line two says \\\"hello\\\" and path C:/rig/groq\",\"project\":\"rig-groq\",\"steps\":[{\"name\":\"plan\",\"weight\":1},{\"name\":\"verify\",\"weight\":2}]}","name":"inspect_manifest"},"id":"call_REDACTED_2","type":"function"}]},{"content":"MANIFEST-OK project=rig-groq steps=2 retries=2","role":"tool","tool_call_id":"call_REDACTED_2"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":false,"stream":false,"tools":[{"function":{"description":"Return EMPTY-OK. This tool takes no arguments.","name":"ping_empty","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"},{"function":{"description":"Join label strings with the requested separator.","name":"join_labels","parameters":{"properties":{"labels":{"items":{"type":"string"},"type":"array"},"separator":{"type":"string"}},"required":["labels","separator"],"type":"object"}},"type":"function"},{"function":{"description":"Validate optional and nullable argument serialization.","name":"optional_nullable_probe","parameters":{"properties":{"name":{"type":"string"},"note":{"type":["string","null"]},"nullable_code":{"type":["string","null"]}},"required":["name","nullable_code"],"type":"object"}},"type":"function"},{"function":{"description":"Echo a string containing escaping-sensitive characters.","name":"escape_echo","parameters":{"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"tool_calls","index":0,"logprobs":null,"message":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"labels\":[\"north\",\"beta gamma\",\"quote:\\\"delta\\\"\",\"slash\\\\path\"],\"separator\":\"|\"}","name":"join_labels"},"id":"call_REDACTED_3","type":"function"}]}}],"created":0,"id":"chatcmpl-REDACTED_3","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_2","usage":{"completion_time":0.076183234,"completion_tokens":32,"prompt_time":0.309515314,"prompt_tokens":880,"queue_time":0.297718512,"total_time":0.385698548,"total_tokens":912},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_3","seed":2030064102}}' +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. For the production-readiness scenario, call exactly one tool at a time in this order: 1. ping_empty with an empty JSON object. 2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. 3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. 4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. 5. escape_echo with the exact escaped text from the user. After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","type":"text"}],"role":"system"},{"content":"Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"ping_empty"},"id":"call_REDACTED_1","type":"function"}]},{"content":"EMPTY-OK","role":"tool","tool_call_id":"call_REDACTED_1"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"flags\":{\"critical\":true,\"retries\":2},\"note\":\"line one; line two says \\\"hello\\\" and path C:/rig/groq\",\"project\":\"rig-groq\",\"steps\":[{\"name\":\"plan\",\"weight\":1},{\"name\":\"verify\",\"weight\":2}]}","name":"inspect_manifest"},"id":"call_REDACTED_2","type":"function"}]},{"content":"MANIFEST-OK project=rig-groq steps=2 retries=2","role":"tool","tool_call_id":"call_REDACTED_2"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"labels\":[\"north\",\"beta gamma\",\"quote:\\\"delta\\\"\",\"slash\\\\path\"],\"separator\":\"|\"}","name":"join_labels"},"id":"call_REDACTED_3","type":"function"}]},{"content":"LABELS-OK north|beta gamma|quote:\"delta\"|slash\\path","role":"tool","tool_call_id":"call_REDACTED_3"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":false,"stream":false,"tools":[{"function":{"description":"Return EMPTY-OK. This tool takes no arguments.","name":"ping_empty","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"},{"function":{"description":"Join label strings with the requested separator.","name":"join_labels","parameters":{"properties":{"labels":{"items":{"type":"string"},"type":"array"},"separator":{"type":"string"}},"required":["labels","separator"],"type":"object"}},"type":"function"},{"function":{"description":"Validate optional and nullable argument serialization.","name":"optional_nullable_probe","parameters":{"properties":{"name":{"type":"string"},"note":{"type":["string","null"]},"nullable_code":{"type":["string","null"]}},"required":["name","nullable_code"],"type":"object"}},"type":"function"},{"function":{"description":"Echo a string containing escaping-sensitive characters.","name":"escape_echo","parameters":{"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"tool_calls","index":0,"logprobs":null,"message":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"name\":\"sentinel\",\"note\":null,\"nullable_code\":null}","name":"optional_nullable_probe"},"id":"call_REDACTED_4","type":"function"}]}}],"created":0,"id":"chatcmpl-REDACTED_4","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_2","usage":{"completion_time":0.043867424,"completion_tokens":24,"prompt_time":0.330235001,"prompt_tokens":935,"queue_time":0.95571374,"total_time":0.374102425,"total_tokens":959},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_4","seed":2005912776}}' +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. For the production-readiness scenario, call exactly one tool at a time in this order: 1. ping_empty with an empty JSON object. 2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. 3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. 4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. 5. escape_echo with the exact escaped text from the user. After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","type":"text"}],"role":"system"},{"content":"Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"ping_empty"},"id":"call_REDACTED_1","type":"function"}]},{"content":"EMPTY-OK","role":"tool","tool_call_id":"call_REDACTED_1"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"flags\":{\"critical\":true,\"retries\":2},\"note\":\"line one; line two says \\\"hello\\\" and path C:/rig/groq\",\"project\":\"rig-groq\",\"steps\":[{\"name\":\"plan\",\"weight\":1},{\"name\":\"verify\",\"weight\":2}]}","name":"inspect_manifest"},"id":"call_REDACTED_2","type":"function"}]},{"content":"MANIFEST-OK project=rig-groq steps=2 retries=2","role":"tool","tool_call_id":"call_REDACTED_2"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"labels\":[\"north\",\"beta gamma\",\"quote:\\\"delta\\\"\",\"slash\\\\path\"],\"separator\":\"|\"}","name":"join_labels"},"id":"call_REDACTED_3","type":"function"}]},{"content":"LABELS-OK north|beta gamma|quote:\"delta\"|slash\\path","role":"tool","tool_call_id":"call_REDACTED_3"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"name\":\"sentinel\",\"note\":null,\"nullable_code\":null}","name":"optional_nullable_probe"},"id":"call_REDACTED_4","type":"function"}]},{"content":"OPTIONAL-OK name=sentinel note=missing nullable=null","role":"tool","tool_call_id":"call_REDACTED_4"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":false,"stream":false,"tools":[{"function":{"description":"Return EMPTY-OK. This tool takes no arguments.","name":"ping_empty","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"},{"function":{"description":"Join label strings with the requested separator.","name":"join_labels","parameters":{"properties":{"labels":{"items":{"type":"string"},"type":"array"},"separator":{"type":"string"}},"required":["labels","separator"],"type":"object"}},"type":"function"},{"function":{"description":"Validate optional and nullable argument serialization.","name":"optional_nullable_probe","parameters":{"properties":{"name":{"type":"string"},"note":{"type":["string","null"]},"nullable_code":{"type":["string","null"]}},"required":["name","nullable_code"],"type":"object"}},"type":"function"},{"function":{"description":"Echo a string containing escaping-sensitive characters.","name":"escape_echo","parameters":{"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"tool_calls","index":0,"logprobs":null,"message":{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"text\":\"Line 1\\nLine \\\"2\\\" with colon: ok and unicode snowman ☃\"}","name":"escape_echo"},"id":"call_REDACTED_5","type":"function"}]}}],"created":0,"id":"chatcmpl-REDACTED_5","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_3","usage":{"completion_time":0.080306,"completion_tokens":33,"prompt_time":0.066234737,"prompt_tokens":982,"queue_time":0.265095603,"total_time":0.146540737,"total_tokens":1015},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_5","seed":2035336206}}' +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. For the production-readiness scenario, call exactly one tool at a time in this order: 1. ping_empty with an empty JSON object. 2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. 3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. 4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. 5. escape_echo with the exact escaped text from the user. After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","type":"text"}],"role":"system"},{"content":"Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"ping_empty"},"id":"call_REDACTED_1","type":"function"}]},{"content":"EMPTY-OK","role":"tool","tool_call_id":"call_REDACTED_1"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"flags\":{\"critical\":true,\"retries\":2},\"note\":\"line one; line two says \\\"hello\\\" and path C:/rig/groq\",\"project\":\"rig-groq\",\"steps\":[{\"name\":\"plan\",\"weight\":1},{\"name\":\"verify\",\"weight\":2}]}","name":"inspect_manifest"},"id":"call_REDACTED_2","type":"function"}]},{"content":"MANIFEST-OK project=rig-groq steps=2 retries=2","role":"tool","tool_call_id":"call_REDACTED_2"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"labels\":[\"north\",\"beta gamma\",\"quote:\\\"delta\\\"\",\"slash\\\\path\"],\"separator\":\"|\"}","name":"join_labels"},"id":"call_REDACTED_3","type":"function"}]},{"content":"LABELS-OK north|beta gamma|quote:\"delta\"|slash\\path","role":"tool","tool_call_id":"call_REDACTED_3"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"name\":\"sentinel\",\"note\":null,\"nullable_code\":null}","name":"optional_nullable_probe"},"id":"call_REDACTED_4","type":"function"}]},{"content":"OPTIONAL-OK name=sentinel note=missing nullable=null","role":"tool","tool_call_id":"call_REDACTED_4"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"text\":\"Line 1\\nLine \\\"2\\\" with colon: ok and unicode snowman ☃\"}","name":"escape_echo"},"id":"call_REDACTED_5","type":"function"}]},{"content":"ESCAPE-OK Line 1\nLine \"2\" with colon: ok and unicode snowman ☃","role":"tool","tool_call_id":"call_REDACTED_5"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":false,"stream":false,"tools":[{"function":{"description":"Return EMPTY-OK. This tool takes no arguments.","name":"ping_empty","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"},{"function":{"description":"Join label strings with the requested separator.","name":"join_labels","parameters":{"properties":{"labels":{"items":{"type":"string"},"type":"array"},"separator":{"type":"string"}},"required":["labels","separator"],"type":"object"}},"type":"function"},{"function":{"description":"Validate optional and nullable argument serialization.","name":"optional_nullable_probe","parameters":{"properties":{"name":{"type":"string"},"note":{"type":["string","null"]},"nullable_code":{"type":["string","null"]}},"required":["name","nullable_code"],"type":"object"}},"type":"function"},{"function":{"description":"Echo a string containing escaping-sensitive characters.","name":"escape_echo","parameters":{"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"stop","index":0,"logprobs":null,"message":{"content":"The production-readiness scenario is complete with results EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","role":"assistant"}}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_2","usage":{"completion_time":0.091762311,"completion_tokens":34,"prompt_time":0.302737876,"prompt_tokens":1045,"queue_time":0.304016229,"total_time":0.394500187,"total_tokens":1079},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_6","seed":720386132}}' diff --git a/tests/cassettes/groq/agent_tool_sessions/sequential_complex_tool_calls_streaming.yaml b/tests/cassettes/groq/agent_tool_sessions/sequential_complex_tool_calls_streaming.yaml new file mode 100644 index 000000000..4370d4c86 --- /dev/null +++ b/tests/cassettes/groq/agent_tool_sessions/sequential_complex_tool_calls_streaming.yaml @@ -0,0 +1,227 @@ +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: text/event-stream + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. For the production-readiness scenario, call exactly one tool at a time in this order: 1. ping_empty with an empty JSON object. 2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. 3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. 4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. 5. escape_echo with the exact escaped text from the user. After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","type":"text"}],"role":"system"},{"content":"Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`.","role":"user"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":false,"stream":true,"stream_options":{"include_usage":true},"tools":[{"function":{"description":"Return EMPTY-OK. This tool takes no arguments.","name":"ping_empty","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"},{"function":{"description":"Join label strings with the requested separator.","name":"join_labels","parameters":{"properties":{"labels":{"items":{"type":"string"},"type":"array"},"separator":{"type":"string"}},"required":["labels","separator"],"type":"object"}},"type":"function"},{"function":{"description":"Validate optional and nullable argument serialization.","name":"optional_nullable_probe","parameters":{"properties":{"name":{"type":"string"},"note":{"type":["string","null"]},"nullable_code":{"type":["string","null"]}},"required":["name","nullable_code"],"type":"object"}},"type":"function"},{"function":{"description":"Echo a string containing escaping-sensitive characters.","name":"escape_echo","parameters":{"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: text/event-stream + body: |+ + data: {"choices":[{"delta":{"content":null,"role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1","x_groq":{"id":"req_REDACTED_1","seed":1766400539}} + + data: {"choices":[{"delta":{"tool_calls":[{"function":{"arguments":"{}","name":"ping_empty"},"id":"call_REDACTED_1","index":0,"type":"function"}]},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1"} + + data: {"choices":[{"delta":{},"finish_reason":"tool_calls","index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.043296652,"completion_tokens":10,"prompt_time":0.147359276,"prompt_tokens":765,"queue_time":0.224573904,"total_time":0.190655928,"total_tokens":775},"x_groq":{"id":"req_REDACTED_1","usage":{"completion_time":0.043296652,"completion_tokens":10,"prompt_time":0.147359276,"prompt_tokens":765,"queue_time":0.224573904,"total_time":0.190655928,"total_tokens":775}}} + + data: {"choices":[],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.043296652,"completion_tokens":10,"prompt_time":0.147359276,"prompt_tokens":765,"queue_time":0.224573904,"total_time":0.190655928,"total_tokens":775}} + + data: [DONE] + +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: text/event-stream + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. For the production-readiness scenario, call exactly one tool at a time in this order: 1. ping_empty with an empty JSON object. 2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. 3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. 4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. 5. escape_echo with the exact escaped text from the user. After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","type":"text"}],"role":"system"},{"content":"Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"ping_empty"},"id":"call_REDACTED_1","type":"function"}]},{"content":"EMPTY-OK","role":"tool","tool_call_id":"call_REDACTED_1"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":false,"stream":true,"stream_options":{"include_usage":true},"tools":[{"function":{"description":"Return EMPTY-OK. This tool takes no arguments.","name":"ping_empty","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"},{"function":{"description":"Join label strings with the requested separator.","name":"join_labels","parameters":{"properties":{"labels":{"items":{"type":"string"},"type":"array"},"separator":{"type":"string"}},"required":["labels","separator"],"type":"object"}},"type":"function"},{"function":{"description":"Validate optional and nullable argument serialization.","name":"optional_nullable_probe","parameters":{"properties":{"name":{"type":"string"},"note":{"type":["string","null"]},"nullable_code":{"type":["string","null"]}},"required":["name","nullable_code"],"type":"object"}},"type":"function"},{"function":{"description":"Echo a string containing escaping-sensitive characters.","name":"escape_echo","parameters":{"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: text/event-stream + body: |+ + data: {"choices":[{"delta":{"content":null,"role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2","x_groq":{"id":"req_REDACTED_2","seed":1715070597}} + + data: {"choices":[{"delta":{"tool_calls":[{"function":{"arguments":"{\"flags\":{\"critical\":true,\"retries\":2},\"note\":\"line one; line two says \\\"hello\\\" and path C:/rig/groq\",\"project\":\"rig-groq\",\"steps\":[{\"name\":\"plan\",\"weight\":1},{\"name\":\"verify\",\"weight\":2}]}","name":"inspect_manifest"},"id":"call_REDACTED_2","index":0,"type":"function"}]},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{},"finish_reason":"tool_calls","index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2","usage":{"completion_time":0.1421373,"completion_tokens":82,"prompt_time":0.128359325,"prompt_tokens":788,"queue_time":0.174773933,"total_time":0.270496625,"total_tokens":870},"x_groq":{"id":"req_REDACTED_2","usage":{"completion_time":0.1421373,"completion_tokens":82,"prompt_time":0.128359325,"prompt_tokens":788,"queue_time":0.174773933,"total_time":0.270496625,"total_tokens":870}}} + + data: {"choices":[],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_2","usage":{"completion_time":0.1421373,"completion_tokens":82,"prompt_time":0.128359325,"prompt_tokens":788,"queue_time":0.174773933,"total_time":0.270496625,"total_tokens":870}} + + data: [DONE] + +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: text/event-stream + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. For the production-readiness scenario, call exactly one tool at a time in this order: 1. ping_empty with an empty JSON object. 2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. 3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. 4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. 5. escape_echo with the exact escaped text from the user. After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","type":"text"}],"role":"system"},{"content":"Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"ping_empty"},"id":"call_REDACTED_1","type":"function"}]},{"content":"EMPTY-OK","role":"tool","tool_call_id":"call_REDACTED_1"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"flags\":{\"critical\":true,\"retries\":2},\"note\":\"line one; line two says \\\"hello\\\" and path C:/rig/groq\",\"project\":\"rig-groq\",\"steps\":[{\"name\":\"plan\",\"weight\":1},{\"name\":\"verify\",\"weight\":2}]}","name":"inspect_manifest"},"id":"call_REDACTED_2","type":"function"}]},{"content":"MANIFEST-OK project=rig-groq steps=2 retries=2","role":"tool","tool_call_id":"call_REDACTED_2"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":false,"stream":true,"stream_options":{"include_usage":true},"tools":[{"function":{"description":"Return EMPTY-OK. This tool takes no arguments.","name":"ping_empty","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"},{"function":{"description":"Join label strings with the requested separator.","name":"join_labels","parameters":{"properties":{"labels":{"items":{"type":"string"},"type":"array"},"separator":{"type":"string"}},"required":["labels","separator"],"type":"object"}},"type":"function"},{"function":{"description":"Validate optional and nullable argument serialization.","name":"optional_nullable_probe","parameters":{"properties":{"name":{"type":"string"},"note":{"type":["string","null"]},"nullable_code":{"type":["string","null"]}},"required":["name","nullable_code"],"type":"object"}},"type":"function"},{"function":{"description":"Echo a string containing escaping-sensitive characters.","name":"escape_echo","parameters":{"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: text/event-stream + body: |+ + data: {"choices":[{"delta":{"content":null,"role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_3","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2","x_groq":{"id":"req_REDACTED_3","seed":2106189776}} + + data: {"choices":[{"delta":{"tool_calls":[{"function":{"arguments":"{\"labels\":[\"north\",\"beta gamma\",\"quote:\\\"delta\\\"\",\"slash\\\\path\"],\"separator\":\"|\"}","name":"join_labels"},"id":"call_REDACTED_3","index":0,"type":"function"}]},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_3","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2"} + + data: {"choices":[{"delta":{},"finish_reason":"tool_calls","index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_3","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_2","usage":{"completion_time":0.084761171,"completion_tokens":32,"prompt_time":0.096337056,"prompt_tokens":880,"queue_time":0.538692009,"total_time":0.181098227,"total_tokens":912},"x_groq":{"id":"req_REDACTED_3","usage":{"completion_time":0.084761171,"completion_tokens":32,"prompt_time":0.096337056,"prompt_tokens":880,"queue_time":0.538692009,"total_time":0.181098227,"total_tokens":912}}} + + data: {"choices":[],"created":0,"id":"chatcmpl-REDACTED_3","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_2","usage":{"completion_time":0.084761171,"completion_tokens":32,"prompt_time":0.096337056,"prompt_tokens":880,"queue_time":0.538692009,"total_time":0.181098227,"total_tokens":912}} + + data: [DONE] + +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: text/event-stream + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. For the production-readiness scenario, call exactly one tool at a time in this order: 1. ping_empty with an empty JSON object. 2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. 3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. 4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. 5. escape_echo with the exact escaped text from the user. After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","type":"text"}],"role":"system"},{"content":"Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"ping_empty"},"id":"call_REDACTED_1","type":"function"}]},{"content":"EMPTY-OK","role":"tool","tool_call_id":"call_REDACTED_1"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"flags\":{\"critical\":true,\"retries\":2},\"note\":\"line one; line two says \\\"hello\\\" and path C:/rig/groq\",\"project\":\"rig-groq\",\"steps\":[{\"name\":\"plan\",\"weight\":1},{\"name\":\"verify\",\"weight\":2}]}","name":"inspect_manifest"},"id":"call_REDACTED_2","type":"function"}]},{"content":"MANIFEST-OK project=rig-groq steps=2 retries=2","role":"tool","tool_call_id":"call_REDACTED_2"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"labels\":[\"north\",\"beta gamma\",\"quote:\\\"delta\\\"\",\"slash\\\\path\"],\"separator\":\"|\"}","name":"join_labels"},"id":"call_REDACTED_3","type":"function"}]},{"content":"LABELS-OK north|beta gamma|quote:\"delta\"|slash\\path","role":"tool","tool_call_id":"call_REDACTED_3"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":false,"stream":true,"stream_options":{"include_usage":true},"tools":[{"function":{"description":"Return EMPTY-OK. This tool takes no arguments.","name":"ping_empty","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"},{"function":{"description":"Join label strings with the requested separator.","name":"join_labels","parameters":{"properties":{"labels":{"items":{"type":"string"},"type":"array"},"separator":{"type":"string"}},"required":["labels","separator"],"type":"object"}},"type":"function"},{"function":{"description":"Validate optional and nullable argument serialization.","name":"optional_nullable_probe","parameters":{"properties":{"name":{"type":"string"},"note":{"type":["string","null"]},"nullable_code":{"type":["string","null"]}},"required":["name","nullable_code"],"type":"object"}},"type":"function"},{"function":{"description":"Echo a string containing escaping-sensitive characters.","name":"escape_echo","parameters":{"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: text/event-stream + body: |+ + data: {"choices":[{"delta":{"content":null,"role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_4","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_3","x_groq":{"id":"req_REDACTED_4","seed":589451132}} + + data: {"choices":[{"delta":{"tool_calls":[{"function":{"arguments":"{\"name\":\"sentinel\",\"note\":null,\"nullable_code\":null}","name":"optional_nullable_probe"},"id":"call_REDACTED_4","index":0,"type":"function"}]},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_4","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_3"} + + data: {"choices":[{"delta":{},"finish_reason":"tool_calls","index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_4","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_3","usage":{"completion_time":0.045571667,"completion_tokens":24,"prompt_time":0.104547249,"prompt_tokens":935,"queue_time":0.218531379,"total_time":0.150118916,"total_tokens":959},"x_groq":{"id":"req_REDACTED_4","usage":{"completion_time":0.045571667,"completion_tokens":24,"prompt_time":0.104547249,"prompt_tokens":935,"queue_time":0.218531379,"total_time":0.150118916,"total_tokens":959}}} + + data: {"choices":[],"created":0,"id":"chatcmpl-REDACTED_4","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_3","usage":{"completion_time":0.045571667,"completion_tokens":24,"prompt_time":0.104547249,"prompt_tokens":935,"queue_time":0.218531379,"total_time":0.150118916,"total_tokens":959}} + + data: [DONE] + +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: text/event-stream + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. For the production-readiness scenario, call exactly one tool at a time in this order: 1. ping_empty with an empty JSON object. 2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. 3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. 4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. 5. escape_echo with the exact escaped text from the user. After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","type":"text"}],"role":"system"},{"content":"Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"ping_empty"},"id":"call_REDACTED_1","type":"function"}]},{"content":"EMPTY-OK","role":"tool","tool_call_id":"call_REDACTED_1"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"flags\":{\"critical\":true,\"retries\":2},\"note\":\"line one; line two says \\\"hello\\\" and path C:/rig/groq\",\"project\":\"rig-groq\",\"steps\":[{\"name\":\"plan\",\"weight\":1},{\"name\":\"verify\",\"weight\":2}]}","name":"inspect_manifest"},"id":"call_REDACTED_2","type":"function"}]},{"content":"MANIFEST-OK project=rig-groq steps=2 retries=2","role":"tool","tool_call_id":"call_REDACTED_2"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"labels\":[\"north\",\"beta gamma\",\"quote:\\\"delta\\\"\",\"slash\\\\path\"],\"separator\":\"|\"}","name":"join_labels"},"id":"call_REDACTED_3","type":"function"}]},{"content":"LABELS-OK north|beta gamma|quote:\"delta\"|slash\\path","role":"tool","tool_call_id":"call_REDACTED_3"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"name\":\"sentinel\",\"note\":null,\"nullable_code\":null}","name":"optional_nullable_probe"},"id":"call_REDACTED_4","type":"function"}]},{"content":"OPTIONAL-OK name=sentinel note=missing nullable=null","role":"tool","tool_call_id":"call_REDACTED_4"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":false,"stream":true,"stream_options":{"include_usage":true},"tools":[{"function":{"description":"Return EMPTY-OK. This tool takes no arguments.","name":"ping_empty","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"},{"function":{"description":"Join label strings with the requested separator.","name":"join_labels","parameters":{"properties":{"labels":{"items":{"type":"string"},"type":"array"},"separator":{"type":"string"}},"required":["labels","separator"],"type":"object"}},"type":"function"},{"function":{"description":"Validate optional and nullable argument serialization.","name":"optional_nullable_probe","parameters":{"properties":{"name":{"type":"string"},"note":{"type":["string","null"]},"nullable_code":{"type":["string","null"]}},"required":["name","nullable_code"],"type":"object"}},"type":"function"},{"function":{"description":"Echo a string containing escaping-sensitive characters.","name":"escape_echo","parameters":{"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: text/event-stream + body: |+ + data: {"choices":[{"delta":{"content":null,"role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_5","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_3","x_groq":{"id":"req_REDACTED_5","seed":148234886}} + + data: {"choices":[{"delta":{"tool_calls":[{"function":{"arguments":"{\"text\":\"Line 1\\nLine \\\"2\\\" with colon: ok and unicode snowman ☃\"}","name":"escape_echo"},"id":"call_REDACTED_5","index":0,"type":"function"}]},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_5","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_3"} + + data: {"choices":[{"delta":{},"finish_reason":"tool_calls","index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_5","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_3","usage":{"completion_time":0.082137107,"completion_tokens":33,"prompt_time":0.13966577,"prompt_tokens":982,"queue_time":0.318172277,"total_time":0.221802877,"total_tokens":1015},"x_groq":{"id":"req_REDACTED_5","usage":{"completion_time":0.082137107,"completion_tokens":33,"prompt_time":0.13966577,"prompt_tokens":982,"queue_time":0.318172277,"total_time":0.221802877,"total_tokens":1015}}} + + data: {"choices":[],"created":0,"id":"chatcmpl-REDACTED_5","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_3","usage":{"completion_time":0.082137107,"completion_tokens":33,"prompt_time":0.13966577,"prompt_tokens":982,"queue_time":0.318172277,"total_time":0.221802877,"total_tokens":1015}} + + data: [DONE] + +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: text/event-stream + - name: content-type + value: application/json + body: '{"messages":[{"content":[{"text":"You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. For the production-readiness scenario, call exactly one tool at a time in this order: 1. ping_empty with an empty JSON object. 2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. 3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. 4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. 5. escape_echo with the exact escaped text from the user. After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK.","type":"text"}],"role":"system"},{"content":"Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`.","role":"user"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"ping_empty"},"id":"call_REDACTED_1","type":"function"}]},{"content":"EMPTY-OK","role":"tool","tool_call_id":"call_REDACTED_1"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"flags\":{\"critical\":true,\"retries\":2},\"note\":\"line one; line two says \\\"hello\\\" and path C:/rig/groq\",\"project\":\"rig-groq\",\"steps\":[{\"name\":\"plan\",\"weight\":1},{\"name\":\"verify\",\"weight\":2}]}","name":"inspect_manifest"},"id":"call_REDACTED_2","type":"function"}]},{"content":"MANIFEST-OK project=rig-groq steps=2 retries=2","role":"tool","tool_call_id":"call_REDACTED_2"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"labels\":[\"north\",\"beta gamma\",\"quote:\\\"delta\\\"\",\"slash\\\\path\"],\"separator\":\"|\"}","name":"join_labels"},"id":"call_REDACTED_3","type":"function"}]},{"content":"LABELS-OK north|beta gamma|quote:\"delta\"|slash\\path","role":"tool","tool_call_id":"call_REDACTED_3"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"name\":\"sentinel\",\"note\":null,\"nullable_code\":null}","name":"optional_nullable_probe"},"id":"call_REDACTED_4","type":"function"}]},{"content":"OPTIONAL-OK name=sentinel note=missing nullable=null","role":"tool","tool_call_id":"call_REDACTED_4"},{"role":"assistant","tool_calls":[{"function":{"arguments":"{\"text\":\"Line 1\\nLine \\\"2\\\" with colon: ok and unicode snowman ☃\"}","name":"escape_echo"},"id":"call_REDACTED_5","type":"function"}]},{"content":"ESCAPE-OK Line 1\nLine \"2\" with colon: ok and unicode snowman ☃","role":"tool","tool_call_id":"call_REDACTED_5"}],"model":"llama-3.3-70b-versatile","parallel_tool_calls":false,"stream":true,"stream_options":{"include_usage":true},"tools":[{"function":{"description":"Return EMPTY-OK. This tool takes no arguments.","name":"ping_empty","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Validate a nested deployment manifest.","name":"inspect_manifest","parameters":{"properties":{"flags":{"properties":{"critical":{"type":"boolean"},"retries":{"type":"integer"}},"required":["critical","retries"],"type":"object"},"note":{"type":"string"},"project":{"type":"string"},"steps":{"items":{"properties":{"name":{"type":"string"},"weight":{"type":"integer"}},"required":["name","weight"],"type":"object"},"type":"array"}},"required":["project","flags","steps","note"],"type":"object"}},"type":"function"},{"function":{"description":"Join label strings with the requested separator.","name":"join_labels","parameters":{"properties":{"labels":{"items":{"type":"string"},"type":"array"},"separator":{"type":"string"}},"required":["labels","separator"],"type":"object"}},"type":"function"},{"function":{"description":"Validate optional and nullable argument serialization.","name":"optional_nullable_probe","parameters":{"properties":{"name":{"type":"string"},"note":{"type":["string","null"]},"nullable_code":{"type":["string","null"]}},"required":["name","nullable_code"],"type":"object"}},"type":"function"},{"function":{"description":"Echo a string containing escaping-sensitive characters.","name":"escape_echo","parameters":{"properties":{"text":{"type":"string"}},"required":["text"],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: text/event-stream + body: |+ + data: {"choices":[{"delta":{"content":"","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4","x_groq":{"id":"req_REDACTED_6","seed":392900653}} + + data: {"choices":[{"delta":{"content":"The"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":" production"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"-read"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"iness"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":" scenario"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":" has"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":" completed"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":" with"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":" results"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":":"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":" EMPTY"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"-"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"OK"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":","},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":" MAN"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"IFEST"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"-"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"OK"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":","},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":" LABEL"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"S"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"-"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"OK"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":","},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":" OPTIONAL"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"-"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"OK"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":","},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":" and"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":" ESC"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"APE"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"-"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"OK"},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{"content":"."},"finish_reason":null,"index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4"} + + data: {"choices":[{"delta":{},"finish_reason":"stop","index":0,"logprobs":null}],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","system_fingerprint":"fp_REDACTED_4","usage":{"completion_time":0.083832012,"completion_tokens":35,"prompt_time":0.084013899,"prompt_tokens":1045,"queue_time":0.206479096,"total_time":0.167845911,"total_tokens":1080},"x_groq":{"id":"req_REDACTED_6","usage":{"completion_time":0.083832012,"completion_tokens":35,"prompt_time":0.084013899,"prompt_tokens":1045,"queue_time":0.206479096,"total_time":0.167845911,"total_tokens":1080}}} + + data: {"choices":[],"created":0,"id":"chatcmpl-REDACTED_6","model":"llama-3.3-70b-versatile","object":"chat.completion.chunk","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_4","usage":{"completion_time":0.083832012,"completion_tokens":35,"prompt_time":0.084013899,"prompt_tokens":1045,"queue_time":0.206479096,"total_time":0.167845911,"total_tokens":1080}} + + data: [DONE] + diff --git a/tests/cassettes/groq/agent_tool_sessions/tool_choice_auto_required_specific_and_none.yaml b/tests/cassettes/groq/agent_tool_sessions/tool_choice_auto_required_specific_and_none.yaml new file mode 100644 index 000000000..fc92a6a20 --- /dev/null +++ b/tests/cassettes/groq/agent_tool_sessions/tool_choice_auto_required_specific_and_none.yaml @@ -0,0 +1,67 @@ +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":"Call lookup_harbor_label exactly once with an empty object.","role":"user"}],"model":"llama-3.3-70b-versatile","stream":false,"tool_choice":"auto","tools":[{"function":{"description":"Return the alpha signal marker.","name":"lookup_harbor_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"tool_calls","index":0,"logprobs":null,"message":{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"lookup_harbor_label"},"id":"call_REDACTED_1","type":"function"}]}}],"created":0,"id":"chatcmpl-REDACTED_1","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.016450879,"completion_tokens":13,"prompt_time":0.012078655,"prompt_tokens":223,"queue_time":0.176464712,"total_time":0.028529534,"total_tokens":236},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_1","seed":1679597799}}' +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":"Call lookup_harbor_label exactly once with an empty object and do not answer in prose.","role":"user"}],"model":"llama-3.3-70b-versatile","stream":false,"tool_choice":"required","tools":[{"function":{"description":"Return the alpha signal marker.","name":"lookup_harbor_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"tool_calls","index":0,"logprobs":null,"message":{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"lookup_harbor_label"},"id":"call_REDACTED_2","type":"function"}]}}],"created":0,"id":"chatcmpl-REDACTED_2","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_1","usage":{"completion_time":0.017010148,"completion_tokens":10,"prompt_time":0.038180495,"prompt_tokens":245,"queue_time":1.081336217,"total_time":0.055190643,"total_tokens":255},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_2","seed":1082240246}}' +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":"Call the orchard-label tool exactly once with an empty object and do not call any other tool.","role":"user"}],"model":"llama-3.3-70b-versatile","stream":false,"tool_choice":{"function":{"name":"lookup_orchard_label"},"type":"function"},"tools":[{"function":{"description":"Return the alpha signal marker.","name":"lookup_harbor_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"},{"function":{"description":"Return the beta signal marker.","name":"lookup_orchard_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"tool_calls","index":0,"logprobs":null,"message":{"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"lookup_orchard_label"},"id":"call_REDACTED_3","type":"function"}]}}],"created":0,"id":"chatcmpl-REDACTED_3","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_2","usage":{"completion_time":0.027428093,"completion_tokens":5,"prompt_time":0.022998352,"prompt_tokens":304,"queue_time":0.359437857,"total_time":0.050426445,"total_tokens":309},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_3","seed":98218926}}' +--- +when: + path: /openai/v1/chat/completions + method: POST + query_param: [] + header: + - name: accept + value: '*/*' + - name: content-type + value: application/json + body: '{"messages":[{"content":"Do not call tools. Reply with exactly this phrase: no-tool-answer","role":"user"}],"model":"llama-3.3-70b-versatile","stream":false,"tool_choice":"none","tools":[{"function":{"description":"Return the alpha signal marker.","name":"lookup_harbor_label","parameters":{"properties":{},"required":[],"type":"object"}},"type":"function"}]}' +then: + status: 200 + header: + - name: content-type + value: application/json + body: '{"choices":[{"finish_reason":"stop","index":0,"logprobs":null,"message":{"content":"no-tool-answer","role":"assistant"}}],"created":0,"id":"chatcmpl-REDACTED_4","model":"llama-3.3-70b-versatile","object":"chat.completion","service_tier":"on_demand","system_fingerprint":"fp_REDACTED_3","usage":{"completion_time":0.008695341,"completion_tokens":4,"prompt_time":0.003336324,"prompt_tokens":76,"queue_time":0.174372834,"total_time":0.012031665,"total_tokens":80},"usage_breakdown":null,"x_groq":{"id":"req_REDACTED_4","seed":1298742563}}' diff --git a/tests/common/cassette_safety.rs b/tests/common/cassette_safety.rs index f1d874c5d..3d479fe0d 100644 --- a/tests/common/cassette_safety.rs +++ b/tests/common/cassette_safety.rs @@ -80,6 +80,11 @@ const PROVIDER_CASSETTE_SUITES: &[ProviderCassetteSuite] = &[ source_dir: "tests/providers/deepseek", wrapper_names: &["with_deepseek_cassette", "with_deepseek_cassette_result"], }, + ProviderCassetteSuite { + provider: "groq", + source_dir: "tests/providers/groq", + wrapper_names: &["with_groq_cassette_result"], + }, ProviderCassetteSuite { provider: "mistral", source_dir: "tests/providers/mistral", diff --git a/tests/groq.rs b/tests/groq.rs index 0385fd2a1..3421a8eae 100644 --- a/tests/groq.rs +++ b/tests/groq.rs @@ -6,6 +6,10 @@ clippy::unreachable )] +#[path = "common/cassette_safety.rs"] +mod cassette_safety; +#[path = "common/cassettes.rs"] +mod cassettes; #[path = "common/reasoning.rs"] mod reasoning; #[path = "common/support.rs"] diff --git a/tests/providers/groq/agent_tool_sessions.rs b/tests/providers/groq/agent_tool_sessions.rs new file mode 100644 index 000000000..a64f59743 --- /dev/null +++ b/tests/providers/groq/agent_tool_sessions.rs @@ -0,0 +1,982 @@ +//! Cassette-backed Groq long-session and OpenAI-compatible chat regression tests. +//! +//! These scenarios exercise Rig's Groq chat-completions path with multi-turn +//! tool loops, low-latency SSE streaming, complex JSON tool arguments, caller- +//! owned history, JSON response formats, explicit tool choice, usage accounting, +//! and provider metadata preservation. + +use std::sync::{Arc, Mutex}; + +use anyhow::Result; +use futures::StreamExt; +use rig::OneOrMany; +use rig::client::CompletionClient; +use rig::completion::{Chat, CompletionModel, GetTokenUsage, Message, ToolDefinition}; +use rig::message::{AssistantContent, ToolChoice}; +use rig::providers::openai; +use rig::streaming::{StreamedAssistantContent, StreamingChat, StreamingPrompt}; +use rig::tool::Tool; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::support::{ + ALPHA_SIGNAL_OUTPUT, AlphaSignal, BETA_SIGNAL_OUTPUT, BetaSignal, TWO_TOOL_STREAM_PREAMBLE, + TWO_TOOL_STREAM_PROMPT, assert_contains_all_case_insensitive, assert_nonempty_response, + assert_raw_stream_tool_call_arguments_are_objects, assert_two_tool_roundtrip_contract, + assistant_text_response, collect_raw_stream_observation, collect_stream_observation, +}; + +use super::support::with_groq_cassette_result; + +const SESSION_MODEL: &str = "llama-3.3-70b-versatile"; +const JSON_OBJECT_MODEL: &str = "llama-3.3-70b-versatile"; +const JSON_SCHEMA_MODEL: &str = "openai/gpt-oss-20b"; + +const COMPLEX_SESSION_PREAMBLE: &str = "\ +You are a deterministic Groq tool orchestration test harness. Use the tools instead of inventing values. \ +For the production-readiness scenario, call exactly one tool at a time in this order: \ +1. ping_empty with an empty JSON object. \ +2. inspect_manifest with project rig-groq, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. \ +3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. \ +4. optional_nullable_probe with required name sentinel, optional note omitted if possible, and nullable_code null. \ +5. escape_echo with the exact escaped text from the user. \ +After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, OPTIONAL-OK, and ESCAPE-OK."; + +const COMPLEX_SESSION_PROMPT: &str = "\ +Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:/rig/groq`. \ +The escaped text is `Line 1\nLine \"2\" with colon: ok and unicode snowman ☃`."; + +#[derive(Clone, Debug, PartialEq)] +struct ToolInvocation { + name: &'static str, + args: serde_json::Value, +} + +type InvocationLog = Arc>>; + +fn push_invocation(log: &InvocationLog, name: &'static str, args: &T) { + log.lock() + .expect("tool invocation log lock should not be poisoned") + .push(ToolInvocation { + name, + args: serde_json::to_value(args).expect("tool args should serialize"), + }); +} + +#[derive(Clone)] +struct PingEmpty { + log: InvocationLog, +} + +#[derive(Clone)] +struct InspectManifest { + log: InvocationLog, +} + +#[derive(Clone)] +struct JoinLabels { + log: InvocationLog, +} + +#[derive(Clone)] +struct OptionalNullableProbe { + log: InvocationLog, +} + +#[derive(Clone)] +struct EscapeEcho { + log: InvocationLog, +} + +#[derive(Debug, Deserialize, Serialize)] +struct EmptyArgs {} + +#[derive(Debug, Deserialize, Serialize)] +struct ManifestArgs { + project: String, + flags: ManifestFlags, + steps: Vec, + note: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct ManifestFlags { + critical: bool, + retries: u8, +} + +#[derive(Debug, Deserialize, Serialize)] +struct ManifestStep { + name: String, + weight: i32, +} + +#[derive(Debug, Deserialize, Serialize)] +struct JoinArgs { + labels: Vec, + separator: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct OptionalNullableArgs { + name: String, + #[serde(default)] + note: Option, + #[serde(default)] + nullable_code: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +struct EchoArgs { + text: String, +} + +#[derive(Debug, thiserror::Error)] +#[error("session tool error")] +struct SessionToolError; + +impl Tool for PingEmpty { + const NAME: &'static str = "ping_empty"; + type Error = SessionToolError; + type Args = EmptyArgs; + type Output = String; + + async fn definition(&self, _prompt: String) -> ToolDefinition { + ToolDefinition { + name: Self::NAME.to_string(), + description: "Return EMPTY-OK. This tool takes no arguments.".to_string(), + parameters: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + } + } + + async fn call(&self, args: Self::Args) -> Result { + push_invocation(&self.log, Self::NAME, &args); + Ok("EMPTY-OK".to_string()) + } +} + +impl Tool for InspectManifest { + const NAME: &'static str = "inspect_manifest"; + type Error = SessionToolError; + type Args = ManifestArgs; + type Output = String; + + async fn definition(&self, _prompt: String) -> ToolDefinition { + ToolDefinition { + name: Self::NAME.to_string(), + description: "Validate a nested deployment manifest.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "project": { "type": "string" }, + "flags": { + "type": "object", + "properties": { + "critical": { "type": "boolean" }, + "retries": { "type": "integer" } + }, + "required": ["critical", "retries"] + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "weight": { "type": "integer" } + }, + "required": ["name", "weight"] + } + }, + "note": { "type": "string" } + }, + "required": ["project", "flags", "steps", "note"] + }), + } + } + + async fn call(&self, args: Self::Args) -> Result { + push_invocation(&self.log, Self::NAME, &args); + Ok(format!( + "MANIFEST-OK project={} steps={} retries={}", + args.project, + args.steps.len(), + args.flags.retries + )) + } +} + +impl Tool for JoinLabels { + const NAME: &'static str = "join_labels"; + type Error = SessionToolError; + type Args = JoinArgs; + type Output = String; + + async fn definition(&self, _prompt: String) -> ToolDefinition { + ToolDefinition { + name: Self::NAME.to_string(), + description: "Join label strings with the requested separator.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "labels": { "type": "array", "items": { "type": "string" } }, + "separator": { "type": "string" } + }, + "required": ["labels", "separator"] + }), + } + } + + async fn call(&self, args: Self::Args) -> Result { + push_invocation(&self.log, Self::NAME, &args); + Ok(format!("LABELS-OK {}", args.labels.join(&args.separator))) + } +} + +impl Tool for OptionalNullableProbe { + const NAME: &'static str = "optional_nullable_probe"; + type Error = SessionToolError; + type Args = OptionalNullableArgs; + type Output = String; + + async fn definition(&self, _prompt: String) -> ToolDefinition { + ToolDefinition { + name: Self::NAME.to_string(), + description: "Validate optional and nullable argument serialization.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "name": { "type": "string" }, + "note": { "type": ["string", "null"] }, + "nullable_code": { "type": ["string", "null"] } + }, + "required": ["name", "nullable_code"] + }), + } + } + + async fn call(&self, args: Self::Args) -> Result { + push_invocation(&self.log, Self::NAME, &args); + Ok(format!( + "OPTIONAL-OK name={} note={} nullable={}", + args.name, + args.note.unwrap_or_else(|| "missing".to_string()), + args.nullable_code.unwrap_or_else(|| "null".to_string()) + )) + } +} + +impl Tool for EscapeEcho { + const NAME: &'static str = "escape_echo"; + type Error = SessionToolError; + type Args = EchoArgs; + type Output = String; + + async fn definition(&self, _prompt: String) -> ToolDefinition { + ToolDefinition { + name: Self::NAME.to_string(), + description: "Echo a string containing escaping-sensitive characters.".to_string(), + parameters: json!({ + "type": "object", + "properties": { "text": { "type": "string" } }, + "required": ["text"] + }), + } + } + + async fn call(&self, args: Self::Args) -> Result { + push_invocation(&self.log, Self::NAME, &args); + Ok(format!("ESCAPE-OK {}", args.text)) + } +} + +fn complex_tools( + log: &InvocationLog, +) -> ( + PingEmpty, + InspectManifest, + JoinLabels, + OptionalNullableProbe, + EscapeEcho, +) { + ( + PingEmpty { log: log.clone() }, + InspectManifest { log: log.clone() }, + JoinLabels { log: log.clone() }, + OptionalNullableProbe { log: log.clone() }, + EscapeEcho { log: log.clone() }, + ) +} + +fn assert_complex_invocations(log: &InvocationLog) { + let invocations = log + .lock() + .expect("tool invocation log lock should not be poisoned") + .clone(); + let names = invocations + .iter() + .map(|invocation| invocation.name) + .collect::>(); + assert_eq!( + names, + vec![ + PingEmpty::NAME, + InspectManifest::NAME, + JoinLabels::NAME, + OptionalNullableProbe::NAME, + EscapeEcho::NAME, + ] + ); + assert_eq!(invocations[0].args, json!({})); + assert_eq!(invocations[1].args["project"], "rig-groq"); + assert_eq!(invocations[1].args["flags"]["critical"], true); + assert_eq!(invocations[1].args["flags"]["retries"], 2); + assert_eq!( + invocations[1].args["steps"].as_array().map(Vec::len), + Some(2) + ); + assert_eq!( + invocations[2].args["labels"], + json!(["north", "beta gamma", "quote:\"delta\"", "slash\\path"]) + ); + assert_eq!(invocations[2].args["separator"], "|"); + assert_eq!(invocations[3].args["name"], "sentinel"); + assert!( + invocations[3].args.get("note").is_none() + || invocations[3].args["note"].is_null() + || invocations[3].args["note"].as_str().is_some(), + "optional note should be omitted, null, or a string when supplied" + ); + assert!(invocations[3].args["nullable_code"].is_null()); + assert_eq!( + invocations[4].args["text"], + "Line 1\nLine \"2\" with colon: ok and unicode snowman ☃" + ); +} + +struct HistoryToolCall { + message_index: usize, + name: String, +} + +struct HistoryToolResult { + message_index: usize, +} + +fn history_tool_calls(history: &[Message]) -> Vec { + history + .iter() + .enumerate() + .flat_map(|(message_index, message)| match message { + Message::Assistant { content, .. } => content + .iter() + .filter_map(move |content| match content { + AssistantContent::ToolCall(tool_call) => Some(HistoryToolCall { + message_index, + name: tool_call.function.name.clone(), + }), + _ => None, + }) + .collect::>(), + _ => Vec::new(), + }) + .collect() +} + +fn history_tool_results(history: &[Message]) -> Vec { + history + .iter() + .enumerate() + .flat_map(|(message_index, message)| match message { + Message::User { content } => content + .iter() + .filter_map(move |content| match content { + rig::message::UserContent::ToolResult(_) => { + Some(HistoryToolResult { message_index }) + } + _ => None, + }) + .collect::>(), + _ => Vec::new(), + }) + .collect() +} + +fn assert_history_records_sequential_tool_roundtrips(history: &[Message], expected_tools: &[&str]) { + let calls = history_tool_calls(history); + let results = history_tool_results(history); + assert_eq!( + calls + .iter() + .map(|call| call.name.as_str()) + .collect::>(), + expected_tools, + "caller-owned chat history should preserve tool call order" + ); + assert_eq!(results.len(), expected_tools.len()); + + for (index, call) in calls.iter().enumerate() { + let result = &results[index]; + assert!(call.message_index < result.message_index); + if let Some(next_call) = calls.get(index + 1) { + assert!(result.message_index < next_call.message_index); + } + } +} + +fn assert_response_metadata( + response: &rig::completion::CompletionResponse, +) { + assert_nonempty_response(&response.raw_response.id); + assert_nonempty_response(&response.raw_response.model); + if let Some(system_fingerprint) = &response.raw_response.system_fingerprint { + assert_nonempty_response(system_fingerprint); + } + assert!( + response + .raw_response + .choices + .iter() + .all(|choice| !choice.finish_reason.is_empty()), + "raw Groq choices should preserve finish reasons" + ); + let raw_usage = response + .raw_response + .usage + .as_ref() + .expect("raw response should preserve usage"); + assert!( + response.usage.input_tokens > 0, + "usage should include input tokens" + ); + if let Some(completion_tokens) = raw_usage.completion_tokens { + assert_eq!(response.usage.output_tokens, completion_tokens as u64); + } + assert_eq!(response.usage.total_tokens, raw_usage.total_tokens as u64); + if let Some(queue_time) = raw_usage.queue_time { + assert!(queue_time >= 0.0); + } + if let Some(total_time) = raw_usage.total_time { + assert!(total_time >= 0.0); + } +} + +#[tokio::test] +async fn sequential_complex_tool_calls_nonstreaming() -> Result<()> { + with_groq_cassette_result( + "agent_tool_sessions/sequential_complex_tool_calls_nonstreaming", + |client| async move { + let log = Arc::new(Mutex::new(Vec::new())); + let (ping, manifest, labels, optional, echo) = complex_tools(&log); + let agent = client + .agent(SESSION_MODEL) + .preamble(COMPLEX_SESSION_PREAMBLE) + .tool(ping) + .tool(manifest) + .tool(labels) + .tool(optional) + .tool(echo) + .additional_params(json!({"parallel_tool_calls": false})) + .default_max_turns(10) + .build(); + let mut history = Vec::::new(); + + let response = agent.chat(COMPLEX_SESSION_PROMPT, &mut history).await?; + + assert_contains_all_case_insensitive( + &response, + &[ + "EMPTY-OK", + "MANIFEST-OK", + "LABELS-OK", + "OPTIONAL-OK", + "ESCAPE-OK", + ], + ); + assert_complex_invocations(&log); + assert_history_records_sequential_tool_roundtrips( + &history, + &[ + PingEmpty::NAME, + InspectManifest::NAME, + JoinLabels::NAME, + OptionalNullableProbe::NAME, + EscapeEcho::NAME, + ], + ); + + Ok(()) + }, + ) + .await +} + +#[tokio::test] +async fn sequential_complex_tool_calls_streaming() -> Result<()> { + with_groq_cassette_result( + "agent_tool_sessions/sequential_complex_tool_calls_streaming", + |client| async move { + let log = Arc::new(Mutex::new(Vec::new())); + let (ping, manifest, labels, optional, echo) = complex_tools(&log); + let agent = client + .agent(SESSION_MODEL) + .preamble(COMPLEX_SESSION_PREAMBLE) + .tool(ping) + .tool(manifest) + .tool(labels) + .tool(optional) + .tool(echo) + .additional_params(json!({"parallel_tool_calls": false})) + .build(); + + let mut stream = agent + .stream_chat(COMPLEX_SESSION_PROMPT, Vec::::new()) + .multi_turn(10) + .await; + let observation = collect_stream_observation(&mut stream).await; + + anyhow::ensure!( + observation.errors.is_empty(), + "stream should not emit errors: {:?}", + observation.errors + ); + anyhow::ensure!( + observation.tool_calls + == vec![ + PingEmpty::NAME.to_string(), + InspectManifest::NAME.to_string(), + JoinLabels::NAME.to_string(), + OptionalNullableProbe::NAME.to_string(), + EscapeEcho::NAME.to_string(), + ], + "stream should expose ordered tool calls, saw {:?}", + observation.tool_calls + ); + anyhow::ensure!( + observation.tool_results == 5, + "expected 5 streamed tool results" + ); + let response = observation + .final_response_text + .as_deref() + .ok_or_else(|| anyhow::anyhow!("stream should produce final response text"))?; + assert_contains_all_case_insensitive( + response, + &[ + "EMPTY-OK", + "MANIFEST-OK", + "LABELS-OK", + "OPTIONAL-OK", + "ESCAPE-OK", + ], + ); + assert_complex_invocations(&log); + + Ok(()) + }, + ) + .await +} + +#[tokio::test] +async fn parallel_tool_calls_single_turn_nonstreaming() -> Result<()> { + with_groq_cassette_result( + "agent_tool_sessions/parallel_tool_calls_single_turn_nonstreaming", + |client| async move { + let agent = client + .agent(SESSION_MODEL) + .preamble(TWO_TOOL_STREAM_PREAMBLE) + .tool(AlphaSignal) + .tool(BetaSignal) + .additional_params(json!({"parallel_tool_calls": true})) + .default_max_turns(5) + .build(); + let mut history = Vec::::new(); + + let response = agent.chat(TWO_TOOL_STREAM_PROMPT, &mut history).await?; + + assert_contains_all_case_insensitive( + &response, + &[ALPHA_SIGNAL_OUTPUT, BETA_SIGNAL_OUTPUT], + ); + let calls = history_tool_calls(&history); + let call_names = calls + .iter() + .map(|call| call.name.as_str()) + .collect::>(); + anyhow::ensure!( + calls.len() == 2 + && call_names.contains(&AlphaSignal::NAME) + && call_names.contains(&BetaSignal::NAME), + "expected both zero-argument tools, saw {:?}", + call_names + ); + anyhow::ensure!( + calls[0].message_index == calls[1].message_index, + "parallel tool calls should be recorded on one assistant message" + ); + anyhow::ensure!( + history_tool_results(&history).len() == 2, + "expected two tool results" + ); + + Ok(()) + }, + ) + .await +} + +#[tokio::test] +async fn parallel_tool_calls_single_turn_streaming() -> Result<()> { + with_groq_cassette_result( + "agent_tool_sessions/parallel_tool_calls_single_turn_streaming", + |client| async move { + let agent = client + .agent(SESSION_MODEL) + .preamble(TWO_TOOL_STREAM_PREAMBLE) + .tool(AlphaSignal) + .tool(BetaSignal) + .additional_params(json!({"parallel_tool_calls": true})) + .build(); + + let mut stream = agent + .stream_prompt(TWO_TOOL_STREAM_PROMPT) + .multi_turn(5) + .await; + let observation = collect_stream_observation(&mut stream).await; + + assert_two_tool_roundtrip_contract( + &observation, + &[AlphaSignal::NAME, BetaSignal::NAME], + &[ALPHA_SIGNAL_OUTPUT, BETA_SIGNAL_OUTPUT], + ); + + Ok(()) + }, + ) + .await +} + +#[tokio::test] +async fn raw_stream_complex_tool_call_deltas_have_object_arguments() -> Result<()> { + with_groq_cassette_result( + "agent_tool_sessions/raw_stream_complex_tool_call_deltas_have_object_arguments", + |client| async move { + let log = Arc::new(Mutex::new(Vec::new())); + let model = client.completion_model(SESSION_MODEL); + let tool = InspectManifest { log }; + let request = model + .completion_request( + "Call inspect_manifest exactly once for project rig-groq with critical=true, retries=2, \ + steps [{name: plan, weight: 1}, {name: verify, weight: 2}], and note `streamed nested JSON`. \ + Do not write normal text before the tool call.", + ) + .preamble("Use the requested tool call and no prose before it.".to_string()) + .tool(tool.definition(String::new()).await) + .tool_choice(ToolChoice::Required) + .build(); + + let observation = collect_raw_stream_observation(model.stream(request).await?).await; + + assert_raw_stream_tool_call_arguments_are_objects( + &observation, + &[InspectManifest::NAME], + ); + let tool_call = observation + .tool_calls + .iter() + .find(|tool_call| tool_call.function.name == InspectManifest::NAME) + .ok_or_else(|| anyhow::anyhow!("raw stream should emit inspect_manifest"))?; + anyhow::ensure!(tool_call.function.arguments["project"] == "rig-groq"); + anyhow::ensure!(tool_call.function.arguments["flags"]["critical"] == true); + anyhow::ensure!( + tool_call.function.arguments["steps"].as_array().map(Vec::len) == Some(2) + ); + anyhow::ensure!( + observation.events.contains(&"tool_call_delta"), + "raw stream should surface tool-call deltas before final tool calls" + ); + + Ok(()) + }, + ) + .await +} + +#[tokio::test] +async fn long_history_replay_with_tool_result_continuation() -> Result<()> { + with_groq_cassette_result( + "agent_tool_sessions/long_history_replay_with_tool_result_continuation", + |client| async move { + let model = client.completion_model(SESSION_MODEL); + let tool_call_id = "call_REDACTED_1"; + let request = model + .completion_request( + "Answer in one short sentence: what is my favorite color, which label came from the tool, \ + and which release lane did I choose? Do not call any tools.", + ) + .preamble("You are concise and should rely on the provided chat history.".to_string()) + .message(Message::user("My favorite color is teal. Please remember it.")) + .message(Message::assistant("Noted: your favorite color is teal.")) + .message(Message::user("For this release, use the canary lane.")) + .message(Message::assistant("Understood: the release lane is canary.")) + .message(Message::user("Look up the harbor label with the tool.")) + .message(Message::Assistant { + id: None, + content: OneOrMany::one(AssistantContent::tool_call( + tool_call_id, + AlphaSignal::NAME, + json!({}), + )), + }) + .message(Message::tool_result_with_call_id( + tool_call_id, + Some(tool_call_id.to_string()), + ALPHA_SIGNAL_OUTPUT, + )) + .message(Message::assistant("The harbor label is crimson-harbor.")) + .tool(AlphaSignal.definition(String::new()).await) + .tool_choice(ToolChoice::None) + .build(); + + let response = model.completion(request).await?; + let text = assistant_text_response(&response.choice) + .ok_or_else(|| anyhow::anyhow!("response should include assistant text"))?; + + assert_contains_all_case_insensitive(&text, &["teal", ALPHA_SIGNAL_OUTPUT, "canary"]); + assert_response_metadata(&response); + + Ok(()) + }, + ) + .await +} + +#[tokio::test] +async fn tool_choice_auto_required_specific_and_none() -> Result<()> { + with_groq_cassette_result( + "agent_tool_sessions/tool_choice_auto_required_specific_and_none", + |client| async move { + let model = client.completion_model(SESSION_MODEL); + + let auto = model + .completion( + model + .completion_request("Call lookup_harbor_label exactly once with an empty object.") + .tool(AlphaSignal.definition(String::new()).await) + .tool_choice(ToolChoice::Auto) + .build(), + ) + .await?; + anyhow::ensure!( + auto.choice.iter().any(|content| matches!( + content, + AssistantContent::ToolCall(tool_call) + if tool_call.function.name == AlphaSignal::NAME + && tool_call.function.arguments == json!({}) + )), + "auto tool choice should allow lookup_harbor_label" + ); + + let required = model + .completion( + model + .completion_request("Call lookup_harbor_label exactly once with an empty object and do not answer in prose.") + .tool(AlphaSignal.definition(String::new()).await) + .tool_choice(ToolChoice::Required) + .build(), + ) + .await?; + anyhow::ensure!( + required.choice.iter().any(|content| matches!( + content, + AssistantContent::ToolCall(tool_call) + if tool_call.function.name == AlphaSignal::NAME + && tool_call.function.arguments == json!({}) + )), + "required tool choice should force lookup_harbor_label" + ); + + let specific = model + .completion( + model + .completion_request("Call the orchard-label tool exactly once with an empty object and do not call any other tool.") + .tool(AlphaSignal.definition(String::new()).await) + .tool(BetaSignal.definition(String::new()).await) + .tool_choice(ToolChoice::Specific { + function_names: vec![BetaSignal::NAME.to_string()], + }) + .build(), + ) + .await?; + let specific_calls = specific + .choice + .iter() + .filter_map(|content| match content { + AssistantContent::ToolCall(tool_call) => Some(tool_call.function.name.as_str()), + _ => None, + }) + .collect::>(); + anyhow::ensure!( + specific_calls == vec![BetaSignal::NAME], + "specific tool choice should force only lookup_orchard_label, saw {:?}", + specific_calls + ); + + let none = model + .completion( + model + .completion_request("Do not call tools. Reply with exactly this phrase: no-tool-answer") + .tool(AlphaSignal.definition(String::new()).await) + .tool_choice(ToolChoice::None) + .build(), + ) + .await?; + let none_text = assistant_text_response(&none.choice) + .ok_or_else(|| anyhow::anyhow!("ToolChoice::None response should contain text"))?; + assert_contains_all_case_insensitive(&none_text, &["no-tool-answer"]); + anyhow::ensure!( + none.choice + .iter() + .all(|content| !matches!(content, AssistantContent::ToolCall(_))), + "ToolChoice::None should not surface tool calls" + ); + + Ok(()) + }, + ) + .await +} + +#[tokio::test] +async fn json_object_response_format_roundtrip() -> Result<()> { + with_groq_cassette_result( + "agent_tool_sessions/json_object_response_format_roundtrip", + |client| async move { + let model = client.completion_model(JSON_OBJECT_MODEL); + let request = model + .completion_request( + "Return a JSON object with release lane canary, risk low, and checks compile=true and replay=true.", + ) + .preamble("Return only valid JSON. No markdown.".to_string()) + .additional_params(json!({"response_format": { "type": "json_object" }})) + .max_tokens(128) + .build(); + + let response = model.completion(request).await?; + let text = assistant_text_response(&response.choice) + .ok_or_else(|| anyhow::anyhow!("JSON response should contain text"))?; + let plan: serde_json::Value = serde_json::from_str(&text)?; + + let serialized = plan.to_string(); + assert_contains_all_case_insensitive( + &serialized, + &["canary", "low", "compile", "replay"], + ); + assert_response_metadata(&response); + + Ok(()) + }, + ) + .await +} + +#[derive(Debug, Deserialize, JsonSchema)] +struct StructuredReleasePlan { + lane: String, + checks: StructuredChecks, + risk: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +struct StructuredChecks { + compile: bool, + replay: bool, +} + +#[tokio::test] +async fn json_schema_structured_output_roundtrip() -> Result<()> { + with_groq_cassette_result( + "agent_tool_sessions/json_schema_structured_output_roundtrip", + |client| async move { + let model = client.completion_model(JSON_SCHEMA_MODEL); + let request = model + .completion_request( + "Return lane=canary, risk=low, checks.compile=true, and checks.replay=true.", + ) + .preamble("Return only the requested structured object.".to_string()) + .output_schema(schemars::schema_for!(StructuredReleasePlan)) + .max_tokens(128) + .build(); + + let response = model.completion(request).await?; + let text = assistant_text_response(&response.choice) + .ok_or_else(|| anyhow::anyhow!("structured response should contain text"))?; + let plan: StructuredReleasePlan = serde_json::from_str(&text)?; + + anyhow::ensure!(plan.lane.eq_ignore_ascii_case("canary")); + anyhow::ensure!(plan.risk.eq_ignore_ascii_case("low")); + anyhow::ensure!(plan.checks.compile); + anyhow::ensure!(plan.checks.replay); + assert_response_metadata(&response); + + Ok(()) + }, + ) + .await +} + +#[tokio::test] +async fn low_latency_streaming_text_surfaces_final_usage() -> Result<()> { + with_groq_cassette_result( + "agent_tool_sessions/low_latency_streaming_text_surfaces_final_usage", + |client| async move { + let model = client.completion_model(SESSION_MODEL); + let mut stream = model + .stream( + model + .completion_request( + "Reply with exactly this comma-separated sequence and no extra words: alpha,beta,gamma,delta,epsilon,zeta,eta,theta", + ) + .preamble("Stream the requested short sequence exactly.".to_string()) + .max_tokens(64) + .build(), + ) + .await?; + + let mut text_chunks = 0usize; + let mut final_usage = None; + while let Some(item) = stream.next().await { + match item? { + StreamedAssistantContent::Text(text) => { + if !text.text.is_empty() { + text_chunks += 1; + } + } + StreamedAssistantContent::Final(response) => { + final_usage = Some(response.token_usage()); + } + _ => {} + } + } + + anyhow::ensure!(text_chunks > 0, "stream should emit text deltas"); + let usage = final_usage.ok_or_else(|| anyhow::anyhow!("stream should emit final usage"))?; + anyhow::ensure!(usage.input_tokens > 0, "stream usage should include input tokens"); + anyhow::ensure!(usage.output_tokens > 0, "stream usage should include output tokens"); + anyhow::ensure!( + usage.total_tokens >= usage.input_tokens + usage.output_tokens, + "stream usage totals should cover input + output tokens: {:?}", + usage + ); + + Ok(()) + }, + ) + .await +} diff --git a/tests/providers/groq/mod.rs b/tests/providers/groq/mod.rs index fafd7f602..0e062c8fa 100644 --- a/tests/providers/groq/mod.rs +++ b/tests/providers/groq/mod.rs @@ -1,4 +1,7 @@ +mod support; + mod agent; +mod agent_tool_sessions; mod context; mod extractor; mod extractor_usage; diff --git a/tests/providers/groq/support.rs b/tests/providers/groq/support.rs new file mode 100644 index 000000000..e8472c327 --- /dev/null +++ b/tests/providers/groq/support.rs @@ -0,0 +1,31 @@ +use std::future::Future; +use std::panic::AssertUnwindSafe; + +use futures::FutureExt; +use rig::providers::groq; + +use crate::cassettes::{CassetteSpec, ProviderCassette}; + +async fn groq_cassette(spec: impl Into) -> (ProviderCassette, groq::Client) { + let cassette = ProviderCassette::start("groq", spec, "https://api.groq.com/openai/v1").await; + let client = groq::Client::builder() + .api_key(cassette.api_key("GROQ_API_KEY")) + .base_url(cassette.base_url()) + .build() + .expect("Groq client should build"); + + (cassette, client) +} + +pub(super) async fn with_groq_cassette_result( + spec: impl Into, + test_body: F, +) -> Result<(), E> +where + F: FnOnce(groq::Client) -> Fut, + Fut: Future>, +{ + let (cassette, client) = groq_cassette(spec).await; + let result = AssertUnwindSafe(test_body(client)).catch_unwind().await; + cassette.finish_after_test_result(result).await +}