Skip to main content

agent_block_testkit/shapes/
openai.rs

1//! OpenAI Chat Completions response shapes.
2//!
3//! The spec (<https://developers.openai.com/api/docs/guides/function-calling>)
4//! puts tool calls in `message.tool_calls[]` with `function.arguments` as a
5//! JSON **string** and a mandatory `id`. The broken variants below reproduce
6//! deviations observed on OpenAI-compatible stacks in the wild.
7
8use serde_json::{json, Value};
9use std::sync::atomic::{AtomicUsize, Ordering};
10
11static RESPONSE_SEQ: AtomicUsize = AtomicUsize::new(0);
12
13fn next_id(prefix: &str) -> String {
14    format!(
15        "{prefix}-{}",
16        RESPONSE_SEQ.fetch_add(1, Ordering::Relaxed) + 1
17    )
18}
19
20/// Spec-conformant tool call: string `arguments`, `id` present.
21pub fn tool_call(id: &str, name: &str, arguments_json: &str) -> Value {
22    json!({
23        "id": id,
24        "type": "function",
25        "function": { "name": name, "arguments": arguments_json }
26    })
27}
28
29/// Broken-but-observed variant: `arguments` is a JSON **object** and the `id`
30/// field is absent. Emitted through OpenAI-compatible endpoints by Ollama's
31/// native `/api/chat` shape (which has no `id` at all — see ollama
32/// `docs/api.md`), by Gemini's `functionCall.args`, and by some vLLM
33/// tool-call parsers.
34pub fn tool_call_object_args_no_id(name: &str, arguments: Value) -> Value {
35    json!({
36        "type": "function",
37        "function": { "name": name, "arguments": arguments }
38    })
39}
40
41/// Broken variant: `arguments` is not valid JSON. Clients must surface a
42/// recoverable error rather than dying (a model that emits one malformed
43/// payload can be asked to retry).
44pub fn tool_call_malformed_args(id: &str, name: &str) -> Value {
45    json!({
46        "id": id,
47        "type": "function",
48        "function": { "name": name, "arguments": "{not json" }
49    })
50}
51
52/// Assistant text response (`finish_reason: "stop"`).
53pub fn text_response(text: &str) -> Value {
54    json!({
55        "id": next_id("chatcmpl-testkit"),
56        "object": "chat.completion",
57        "choices": [{
58            "index": 0,
59            "message": { "role": "assistant", "content": text },
60            "finish_reason": "stop"
61        }],
62        "usage": { "prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20 }
63    })
64}
65
66/// Assistant tool-calls response (`finish_reason: "tool_calls"`, `content: null`).
67pub fn tool_calls_response(calls: Vec<Value>) -> Value {
68    json!({
69        "id": next_id("chatcmpl-testkit"),
70        "object": "chat.completion",
71        "choices": [{
72            "index": 0,
73            "message": { "role": "assistant", "content": null, "tool_calls": calls },
74            "finish_reason": "tool_calls"
75        }],
76        "usage": { "prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20 }
77    })
78}