Skip to content

Commit 7010c17

Browse files
committed
feat: LLM host as pure proxy with function calling support
- SDK builds complete OpenAI-compatible requests (tools, function calling, response_format) - Host proxies request with stream:false, adds auth, forwards to endpoint - LlmResponse now includes tool_calls and finish_reason - Challenge WASM fully independent in building LLM requests
1 parent 68f228a commit 7010c17

File tree

2 files changed

+619
-122
lines changed

2 files changed

+619
-122
lines changed

crates/challenge-sdk-wasm/src/llm_types.rs

Lines changed: 212 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,232 @@ use alloc::string::String;
22
use alloc::vec::Vec;
33
use serde::{Deserialize, Serialize};
44

5+
/// Full OpenAI-compatible chat completion request.
6+
/// The challenge WASM builds this completely; the host just proxies it.
57
#[derive(Clone, Debug, Serialize, Deserialize)]
68
pub struct LlmRequest {
79
pub model: String,
810
pub messages: Vec<LlmMessage>,
9-
pub max_tokens: u32,
10-
pub temperature: f32,
11+
#[serde(skip_serializing_if = "Option::is_none")]
12+
pub max_tokens: Option<u32>,
13+
#[serde(skip_serializing_if = "Option::is_none")]
14+
pub temperature: Option<f32>,
15+
#[serde(skip_serializing_if = "Option::is_none")]
16+
pub top_p: Option<f32>,
17+
#[serde(skip_serializing_if = "Option::is_none")]
18+
pub frequency_penalty: Option<f32>,
19+
#[serde(skip_serializing_if = "Option::is_none")]
20+
pub presence_penalty: Option<f32>,
21+
#[serde(skip_serializing_if = "Option::is_none")]
22+
pub stop: Option<Vec<String>>,
23+
/// OpenAI function calling / tools
24+
#[serde(skip_serializing_if = "Option::is_none")]
25+
pub tools: Option<Vec<Tool>>,
26+
#[serde(skip_serializing_if = "Option::is_none")]
27+
pub tool_choice: Option<ToolChoice>,
28+
#[serde(skip_serializing_if = "Option::is_none")]
29+
pub response_format: Option<ResponseFormat>,
30+
}
31+
32+
impl LlmRequest {
33+
pub fn simple(model: &str, messages: Vec<LlmMessage>, max_tokens: u32) -> Self {
34+
Self {
35+
model: String::from(model),
36+
messages,
37+
max_tokens: Some(max_tokens),
38+
temperature: Some(0.1),
39+
top_p: None,
40+
frequency_penalty: None,
41+
presence_penalty: None,
42+
stop: None,
43+
tools: None,
44+
tool_choice: None,
45+
response_format: None,
46+
}
47+
}
48+
49+
pub fn with_tools(
50+
model: &str,
51+
messages: Vec<LlmMessage>,
52+
tools: Vec<Tool>,
53+
max_tokens: u32,
54+
) -> Self {
55+
Self {
56+
model: String::from(model),
57+
messages,
58+
max_tokens: Some(max_tokens),
59+
temperature: Some(0.1),
60+
top_p: None,
61+
frequency_penalty: None,
62+
presence_penalty: None,
63+
stop: None,
64+
tools: Some(tools),
65+
tool_choice: Some(ToolChoice::Auto),
66+
response_format: None,
67+
}
68+
}
1169
}
1270

1371
#[derive(Clone, Debug, Serialize, Deserialize)]
1472
pub struct LlmMessage {
1573
pub role: String,
16-
pub content: String,
74+
pub content: Option<String>,
75+
#[serde(skip_serializing_if = "Option::is_none")]
76+
pub name: Option<String>,
77+
#[serde(skip_serializing_if = "Option::is_none")]
78+
pub tool_calls: Option<Vec<ToolCall>>,
79+
#[serde(skip_serializing_if = "Option::is_none")]
80+
pub tool_call_id: Option<String>,
81+
}
82+
83+
impl LlmMessage {
84+
pub fn system(content: &str) -> Self {
85+
Self {
86+
role: String::from("system"),
87+
content: Some(String::from(content)),
88+
name: None,
89+
tool_calls: None,
90+
tool_call_id: None,
91+
}
92+
}
93+
94+
pub fn user(content: &str) -> Self {
95+
Self {
96+
role: String::from("user"),
97+
content: Some(String::from(content)),
98+
name: None,
99+
tool_calls: None,
100+
tool_call_id: None,
101+
}
102+
}
103+
104+
pub fn assistant(content: &str) -> Self {
105+
Self {
106+
role: String::from("assistant"),
107+
content: Some(String::from(content)),
108+
name: None,
109+
tool_calls: None,
110+
tool_call_id: None,
111+
}
112+
}
113+
114+
pub fn assistant_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
115+
Self {
116+
role: String::from("assistant"),
117+
content: None,
118+
name: None,
119+
tool_calls: Some(tool_calls),
120+
tool_call_id: None,
121+
}
122+
}
123+
124+
pub fn tool(tool_call_id: &str, content: &str) -> Self {
125+
Self {
126+
role: String::from("tool"),
127+
content: Some(String::from(content)),
128+
name: None,
129+
tool_calls: None,
130+
tool_call_id: Some(String::from(tool_call_id)),
131+
}
132+
}
133+
}
134+
135+
#[derive(Clone, Debug, Serialize, Deserialize)]
136+
pub struct Tool {
137+
#[serde(rename = "type")]
138+
pub tool_type: String,
139+
pub function: FunctionDef,
140+
}
141+
142+
impl Tool {
143+
pub fn function(name: &str, description: &str, parameters: &str) -> Self {
144+
Self {
145+
tool_type: String::from("function"),
146+
function: FunctionDef {
147+
name: String::from(name),
148+
description: Some(String::from(description)),
149+
parameters: Some(String::from(parameters)),
150+
},
151+
}
152+
}
153+
}
154+
155+
#[derive(Clone, Debug, Serialize, Deserialize)]
156+
pub struct FunctionDef {
157+
pub name: String,
158+
#[serde(skip_serializing_if = "Option::is_none")]
159+
pub description: Option<String>,
160+
/// JSON Schema string for the function parameters
161+
#[serde(skip_serializing_if = "Option::is_none")]
162+
pub parameters: Option<String>,
163+
}
164+
165+
#[derive(Clone, Debug, Serialize, Deserialize)]
166+
#[serde(untagged)]
167+
pub enum ToolChoice {
168+
Auto,
169+
None,
170+
Required,
171+
Specific { function: ToolChoiceFunction },
172+
}
173+
174+
#[derive(Clone, Debug, Serialize, Deserialize)]
175+
pub struct ToolChoiceFunction {
176+
pub name: String,
177+
}
178+
179+
#[derive(Clone, Debug, Serialize, Deserialize)]
180+
pub struct ResponseFormat {
181+
#[serde(rename = "type")]
182+
pub format_type: String,
183+
}
184+
185+
impl ResponseFormat {
186+
pub fn json() -> Self {
187+
Self {
188+
format_type: String::from("json_object"),
189+
}
190+
}
191+
192+
pub fn text() -> Self {
193+
Self {
194+
format_type: String::from("text"),
195+
}
196+
}
17197
}
18198

199+
#[derive(Clone, Debug, Serialize, Deserialize)]
200+
pub struct ToolCall {
201+
pub id: String,
202+
#[serde(rename = "type")]
203+
pub call_type: String,
204+
pub function: FunctionCall,
205+
}
206+
207+
#[derive(Clone, Debug, Serialize, Deserialize)]
208+
pub struct FunctionCall {
209+
pub name: String,
210+
pub arguments: String,
211+
}
212+
213+
/// Response from the host LLM proxy.
19214
#[derive(Clone, Debug, Serialize, Deserialize)]
20215
pub struct LlmResponse {
21-
pub content: String,
216+
pub content: Option<String>,
217+
#[serde(default)]
218+
pub tool_calls: Vec<ToolCall>,
22219
pub usage: Option<LlmUsage>,
220+
pub finish_reason: Option<String>,
221+
}
222+
223+
impl LlmResponse {
224+
pub fn text(&self) -> &str {
225+
self.content.as_deref().unwrap_or("")
226+
}
227+
228+
pub fn has_tool_calls(&self) -> bool {
229+
!self.tool_calls.is_empty()
230+
}
23231
}
24232

25233
#[derive(Clone, Debug, Serialize, Deserialize)]

0 commit comments

Comments
 (0)