From 0cc4a018824f1c4bae06333ed5e5a659eaf7e307 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 24 Jul 2026 07:28:06 -0400 Subject: [PATCH 1/2] Forward reasoning effort for structured completions --- .../src/server/handler/completions.rs | 3 + lib/apps/fabro-server/src/server/tests.rs | 66 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/lib/apps/fabro-server/src/server/handler/completions.rs b/lib/apps/fabro-server/src/server/handler/completions.rs index b53e56d31..78a973087 100644 --- a/lib/apps/fabro-server/src/server/handler/completions.rs +++ b/lib/apps/fabro-server/src/server/handler/completions.rs @@ -155,6 +155,9 @@ async fn create_completion( if let Some(top_p) = request.top_p { params = params.top_p(top_p); } + if let Some(reasoning_effort) = request.reasoning_effort { + params = params.reasoning_effort(reasoning_effort); + } match generate_object(params, schema).await { Ok(result) => { // `result.finish_reason` / `result.usage` resolve through diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index 454455be0..c4d5cac28 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -15347,6 +15347,72 @@ reasoning = false completion.assert(); } +#[tokio::test] +async fn create_completion_structured_output_forwards_reasoning_effort() { + let upstream = MockServer::start(); + let completion = upstream.mock(|when, then| { + when.method(POST) + .path("/chat/completions") + .json_body_includes(r#"{"model":"kimi-k3","reasoning_effort":"high"}"#); + then.status(200) + .header("content-type", "application/json") + .json_body(json!({ + "id": "chatcmpl-kimi-structured", + "model": "kimi-k3", + "choices": [{ + "message": { + "role": "assistant", + "content": "{\"answer\":42}" + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 4, + "total_tokens": 14 + } + })); + }); + let state = TestAppStateBuilder::new() + .provider_base_url("kimi", upstream.base_url()) + .vault_entries([(EnvVars::KIMI_API_KEY, "test-kimi-api-key")]) + .build(); + let app = crate::test_support::build_test_router(state); + + let req = Request::builder() + .method("POST") + .uri(api("/completions")) + .header("content-type", "application/json") + .body(Body::from( + serde_json::json!({ + "provider": "kimi", + "model": "kimi-k3", + "reasoning_effort": "high", + "stream": false, + "schema": { + "type": "object", + "properties": { + "answer": {"type": "integer"} + }, + "required": ["answer"] + }, + "messages": [ + { + "role": "user", + "content": [{"kind": "text", "data": "Return the answer."}] + } + ] + }) + .to_string(), + )) + .unwrap(); + + let response = app.oneshot(req).await.unwrap(); + let body = response_json!(response, StatusCode::OK).await; + assert_eq!(body["output"], json!({"answer": 42})); + completion.assert_calls(1); +} + #[tokio::test] async fn demo_list_runs_returns_run_list_items() { let state = test_app_state(); From 0cd22ebd7586680b9b4e4f1b8ea762d83ba806ce Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 24 Jul 2026 08:40:55 -0400 Subject: [PATCH 2/2] refactor: build structured-output GenerateParams via struct update Replaces the per-field if-let cascade in the structured completion path with a single struct-update expression. The cascade had to be extended by hand for every request field and silently dropped stop_sequences and provider_options, which the non-structured path already forwarded. Co-Authored-By: Claude Fable 5 --- .../src/server/handler/completions.rs | 36 +++++++++---------- lib/apps/fabro-server/src/server/tests.rs | 2 +- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/lib/apps/fabro-server/src/server/handler/completions.rs b/lib/apps/fabro-server/src/server/handler/completions.rs index eb6dafaf4..146d20808 100644 --- a/lib/apps/fabro-server/src/server/handler/completions.rs +++ b/lib/apps/fabro-server/src/server/handler/completions.rs @@ -139,25 +139,23 @@ async fn create_completion( let msg_id = Ulid::new().to_string(); if let Some(schema) = req.schema { - // Structured output uses generate_object for JSON parsing logic - let mut params = - GenerateParams::new(&request.model, std::sync::Arc::new(client.clone())) - .messages(request.messages); - if let Some(ref p) = request.provider { - params = params.provider(p); - } - if let Some(temp) = request.temperature { - params = params.temperature(temp); - } - if let Some(max_tokens) = request.max_tokens { - params = params.max_tokens(max_tokens); - } - if let Some(top_p) = request.top_p { - params = params.top_p(top_p); - } - if let Some(reasoning_effort) = request.reasoning_effort { - params = params.reasoning_effort(reasoning_effort); - } + // Structured output uses generate_object for JSON parsing logic. + // tools/tool_choice are not forwarded: GenerateParams carries + // executable Arcs, not wire ToolDefinitions, and + // generate_object sets response_format from the schema itself. + let params = GenerateParams { + messages: Some(request.messages), + provider: request.provider, + temperature: request.temperature, + top_p: request.top_p, + max_tokens: request.max_tokens, + stop_sequences: request.stop_sequences, + reasoning_effort: request.reasoning_effort, + speed: request.speed, + metadata: request.metadata, + provider_options: request.provider_options, + ..GenerateParams::new(request.model, std::sync::Arc::new(client.clone())) + }; match generate_object(params, schema).await { Ok(result) => { // `result.finish_reason` / `result.usage` resolve through diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index 8e7b124e7..eb0083948 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -15431,7 +15431,7 @@ async fn create_completion_structured_output_forwards_reasoning_effort() { let response = app.oneshot(req).await.unwrap(); let body = response_json!(response, StatusCode::OK).await; assert_eq!(body["output"], json!({"answer": 42})); - completion.assert_calls(1); + completion.assert(); } #[tokio::test]