Home

Wiki

Review

Docs

PR #12668

  • 1ToolReference: Struct → Enum

3 files+122−34

common.rssgl-router/src/protocols

chat.rssgl-router/src/protocols

chat_completion.rssgl-router/tests/spec

Read explanation

The tool_choice parameter can reference different tool types. The old struct couldn't represent this:

rust

// BEFOREstruct ToolReference {    tool_type: String,  // "function"    name: String,}// AFTERenum ToolReference {    Function { name: String },    Mcp { server_label: String,           name: Option<String> },    FileSearch,    WebSearchPreview,    // ...}

Consequence → Chat API validation

Chat Completion API only supports function tools. Added validation to reject other types with a clear error message chat.rs:483-496

Consequence → Test updates

Tests updated to use new enum syntax.

  • 2Responses API Validation

1 file+78

responses.rssgl-router/src/protocols

Read explanation

Added cross-field validation for tool_choice + tools in ResponsesRequest:

rust

fn validate_responses_cross_parameters(req) {    // If tool_choice references a function,    // verify it exists in tools array    if let Function { name } = tool_choice {        if !tools.contains(name) {            return Err("function not found")        }    }        // Validate AllowedTools mode is     // "auto" or "required"    if let AllowedTools { mode, .. } = tool_choice {        if mode != "auto" && mode != "required" {            return Err("invalid mode")        }    }}
  • 3Harmony Router: tool_choice Constraint Generation

4 files+64−9

preparation.rssgl-router/src/routers/grpc/harmony/stages

request_building.rssgl-router/src/routers/grpc/harmony/stages

responses.rs(1/2)sgl-router/src/routers/grpc/harmony

sglang_scheduler.rssgl-router/src/grpc_client

Read explanation

The Harmony router uses "structural tags" to constrain model output. Added tool constraint generation for Responses API in the preparation stage:

preparation.rs:131-148

rust

// 1. Extract function + MCP toolslet tools = extract_tools_from_response_tools(    request.tools,     true  // include MCP);// 2. Filter by tool_choice if neededif let Some(filtered) =     filter_tools_by_tool_choice(&tools, &tool_choice) {    tools = filtered;}// 3. Generate Harmony structural tagslet constraints = generate_harmony_structural_tag(    &tools, &tool_choice)?;

The constraints are passed through request building request_building.rs:87 to the gRPC client, which now accepts tool_call_constraint for Responses API requests sglang_scheduler.rs:316

Infinite loop prevention: After receiving tool results, switch to "auto" so the model can choose to stop responses.rs:1183-1186

  • 4Regular Router: tool_choice Pass-through

2 files+101−95

conversions.rssgl-router/src/routers/grpc/regular/responses

tool_loop.rssgl-router/src/routers/grpc/regular/responses

Read explanation

The Regular router converts Responses API requests to Chat API format. Now extracts function tools and passes tool_choice through:

conversions.rs:154-165

rust

// Extract ONLY function tools here// (MCP tools merged later by tool loop)let function_tools = extract_tools_from_response_tools(    req.tools,     false  // exclude MCP);// Pass through tool_choice unchangedOk(ChatCompletionRequest {    tools: Some(function_tools),    tool_choice: req.tool_choice.clone(),    // ...})

Tool loop: merging MCP tools

When MCP tools are present, the tool loop merges them with function tools before calling the chat pipeline. The iteration logic prevents infinite loops:

tool_loop.rs:70-92

rust

fn prepare_chat_tools_and_choice(    chat_request: &mut ChatCompletionRequest,    mcp_tools: &[Tool],    iteration: usize,) {    // Merge function + MCP tools    let mut all = chat_request.tools.unwrap_or_default();    all.extend(mcp_tools.iter().cloned());    chat_request.tools = Some(all);    // Iteration 0: use user's tool_choice    // Iteration 1+: force "auto" to avoid loops    chat_request.tool_choice = if iteration == 0 {        chat_request.tool_choice.clone()            .or(Some(ToolChoice::Value(Auto)))    } else {        Some(ToolChoice::Value(Auto))    };}
  • 5Shared Utilities

2 files+88−25

utils.rssgl-router/src/routers/grpc/common/responses

utils.rssgl-router/src/routers/grpc

Read explanation

Extracted common logic to reduce duplication between routers:

extract_tools_from_response_tools() - Extracts function tools (and optionally MCP tools) from ResponseTools:

  • Harmony: include_mcp: true (MCP schemas pre-populated)
    • Regular: include_mcp: false (MCP merged later in tool loop)

utils.rs:97-125

filter_tools_by_tool_choice() - Generic tool filtering extracted from the Chat-specific function. Now used by both Chat and Responses APIs:

utils.rs:276-305

rust

// Generic: returns filtered tools or Nonefn filter_tools_by_tool_choice(    tools: &[Tool],    tool_choice: &Option<ToolChoice>,) -> Option<Vec<Tool>>// Chat-specific: returns Cow<ChatCompletionRequest>fn filter_chat_request_by_tool_choice(    body: &ChatCompletionRequest,) -> Cow<'_, ChatCompletionRequest>
  • 6Miscellaneous

3 files+4−21

handlers.rssgl-router/src/routers/grpc/regular/responses

preparation.rssgl-router/src/routers/grpc/regular/stages/chat

responses.rs(2/2)sgl-router/src/routers/grpc/harmony

Read explanation

  • Import changes and doc comment cleanup
    • ToolChoice::serialize_to_string() helper for response serialization common.rs:186-196
    • Function rename: filter_tools_for_request → filter_chat_request_by_tool_choice

Merged

sgl-project/sglang #12668

[router][grpc] Implement tool_choice support for Responses API

CACatherineSue

main chang/responses-fix

14 files

+457−184

DescriptionDiscussion2Commits3

Devin's AI analysis

This PR adds tool_choice support to the /v1/responses endpoint, allowing users to control tool calling behavior:

tool_choice: "none"      → prevent tool callstool_choice: "auto"      → let model decidetool_choice: "required"  → force tool calltool_choice: {function}  → force specific tooltool_choice: {allowed}   → restrict to subset

Key changes:

  1. ToolReference becomes a tagged enum to properly represent different tool types (functions, MCP, hosted tools). This cascades to Chat API validation and tests.

  2. Harmony router generates structural tag constraints from tool_choice in the preparation stage preparation.rs:131-148

  3. Regular router passes tool_choice through to the chat pipeline. For MCP requests, tools are merged before constraint generation tool_loop.rs:70-92

  4. Infinite loop prevention: When tool_choice: "required", the model would loop forever. After the first iteration, both routers switch to "auto"responses.rs:1183-1186

Motivation

The Responses API (/v1/responses) did not support the tool_choice parameter, which is a critical feature for controlling tool calling behavior according to OpenAI's Responses API specification. This limitation prevented users from:

  • Forcing the model to call specific tools (tool_choice: {type: "function", function: {name: "..."}})
  • Requiring at least one tool call (tool_choice: "required")
  • Restricting tool calls to a subset of available tools (tool_choice: {type: "allowed_tools", ...})
  • Preventing tool calls entirely (tool_choice: "none")

This PR implements full tool_choice support for the Responses API in both Harmony and Regular routers, following the same pattern as the existing Chat Completion API implementation.

Modifications

1. Harmony Router - Responses API tool_choice Support

File: src/routers/grpc/harmony/stages/preparation.rs

  • Added tool_choice constraint generation for Responses API requests
  • Extracts both Function and MCP tools from ResponseTools (schemas populated before pipeline)
  • Implements AllowedTools filtering to restrict tools based on user's tool_choice
  • Generates Harmony structural tags using triggered_tags format for tool constraints
  • Supports all tool_choice modes: none, auto, required, specific function, allowed_tools

2. Regular Router - Responses API tool_choice Support

Files:

  • src/routers/grpc/regular/responses/conversions.rs
  • src/routers/grpc/regular/responses/tool_loop.rs

Flow:

  • conversions.rs: Extracts function tools and passes through tool_choice unchanged
  • Without MCP: Function tools → chat pipeline → tool_choice constraints applied
  • With MCP: Tool loop merges function + MCP tools → chat pipeline → constraints applied to ALL tools
  • Loop iteration logic: Iteration 0 uses user's tool_choice, iteration 1+ uses "auto" to prevent infinite loops

3. ToolReference Protocol Enhancement

File: src/protocols/common.rs

Converted ToolReference from a simple struct to a tagged enum to properly support different tool types:

rust

// Before (struct)pub struct ToolReference {    pub tool_type: String,    pub name: String,}// After (enum)pub enum ToolReference {    Function { name: String },    Mcp { server_label: String, name: Option<String> },    FileSearch,    WebSearchPreview,    ComputerUsePreview,    CodeInterpreter,    ImageGeneration,}

Benefits:

  • Type-safe representation of different tool types
  • Supports MCP and hosted tools (not just functions)
  • Helper methods: identifier(), function_name()

4. Chat API Validation

File: src/protocols/chat.rs

Added validation that Chat Completion API ONLY accepts Function type ToolReference in tool_choice. Rejects MCP and hosted tools with clear error messages, enforcing the API contract.

5. Code Deduplication

File: src/routers/grpc/common/responses/utils.rs

Created shared utility function extract_tools_from_response_tools() to eliminate duplication:

  • Used by both Harmony preparation stage and Regular conversions
  • include_mcp parameter controls whether to extract MCP tools
  • Comprehensive documentation explains usage differences between routers

6. Helper Function for Chat Requests

File: src/routers/grpc/regular/responses/tool_loop.rs

Created prepare_chat_tools_and_choice() helper to:

  • Merge function tools from request with MCP tools
  • Set tool_choice based on iteration (user's choice on iteration 0, "auto" on iteration 1+)
  • Reduces duplication in tool loop

7. Bug Fixes

  • Fixed unnecessary pub use: Changed pub use crate::tokenizer::StopSequenceDecoder to regular use in utils.rs
  • Updated test cases: Fixed 6 test functions in tests/spec/chat_completion.rs to use new ToolReference enum syntax

8. Documentation

Added comprehensive comments explaining:

  • Tool extraction flow in conversions.rs (why MCP path "wastes" initial extraction)
  • Differences between Harmony and Regular router tool handling
  • Tool loop iteration behavior with tool_choice

Accuracy Tests

Benchmarking and Profiling

Checklist

Filter

GE

gemini-code-assist177d

[!WARNING] You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

641 lines left

1ToolReference: Struct → Enum

0 / 3

common.rssgl-router/src/protocols

+78−4

Mark as viewed

5 linesAll 161 lines5 lines

5 linesAll 161 lines5 lines

162

/// Tool choice for both Chat Completion and Responses APIs

162

/// Tool choice for both Chat Completion and Responses APIs

163

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

163

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

164

#[serde(untagged)]

164

#[serde(untagged)]

165

pub enum ToolChoice {

165

pub enum ToolChoice {

5 linesAll 12 lines5 lines

5 linesAll 12 lines5 lines

178

}

178

}

179

179

180

impl Default for ToolChoice {

180

impl Default for ToolChoice {

181

fn default() -> Self {

181

fn default() -> Self {

182

Self::Value(ToolChoiceValue::Auto)

182

Self::Value(ToolChoiceValue::Auto)

183

}

183

}

184

}

184

}

185

185

186

impl ToolChoice {

187

/// Serialize tool_choice to string for ResponsesResponse

188

///

189

/// Returns the JSON-serialized tool_choice or "auto" as default

190

pub fn serialize_to_string(tool_choice: &Option<ToolChoice>) -> String {

191

tool_choice

192

.as_ref()

193

.map(|tc| serde_json::to_string(tc).unwrap_or_else(|_| "auto".to_string()))

194

.unwrap_or_else(|| "auto".to_string())

195

}

196

}

197

All 6 lines

All 6 lines

192

/// Tool reference for ToolChoice::AllowedTools

204

/// Tool reference for ToolChoice::AllowedTools

205

///

206

/// Represents a reference to a specific tool in the allowed_tools array.

207

/// Different tool types have different required fields.

193

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

208

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

194

pub struct ToolReference {

209

#[serde(tag = "type")]

195

#[serde(rename = "type")]

210

#[serde(rename_all = "snake_case")]

196

pub tool_type: String, // "function"

211

pub enum ToolReference {

197

pub name: String,

212

/// Reference to a function tool

213

#[serde(rename = "function")]

214

Function { name: String },

215

216

/// Reference to an MCP tool

217

#[serde(rename = "mcp")]

218

Mcp {

219

server_label: String,

220

#[serde(skip_serializing_if = "Option::is_none")]

221

name: Option<String>,

222

},

223

224

/// File search hosted tool

225

#[serde(rename = "file_search")]

226

FileSearch,

227

228

/// Web search preview hosted tool

229

#[serde(rename = "web_search_preview")]

230

WebSearchPreview,

231

232

/// Computer use preview hosted tool

233

#[serde(rename = "computer_use_preview")]

234

ComputerUsePreview,

235

236

/// Code interpreter hosted tool

237

#[serde(rename = "code_interpreter")]

238

CodeInterpreter,

239

240

/// Image generation hosted tool

241

#[serde(rename = "image_generation")]

242

ImageGeneration,

243

}

244

245

impl ToolReference {

246

/// Get a unique identifier for this tool reference

247

pub fn identifier(&self) -> String {

248

match self {

249

ToolReference::Function { name } => format!("function:{}", name),

250

ToolReference::Mcp { server_label, name } => {

251

if let Some(n) = name {

252

format!("mcp:{}:{}", server_label, n)

253

} else {

254

format!("mcp:{}", server_label)

255

}

256

}

257

ToolReference::FileSearch => "file_search".to_string(),

258

ToolReference::WebSearchPreview => "web_search_preview".to_string(),

259

ToolReference::ComputerUsePreview => "computer_use_preview".to_string(),

260

ToolReference::CodeInterpreter => "code_interpreter".to_string(),

261

ToolReference::ImageGeneration => "image_generation".to_string(),

262

}

263

}

264

265

/// Get the tool name if this is a function tool

266

pub fn function_name(&self) -> Option<&str> {

267

match self {

268

ToolReference::Function { name } => Some(name.as_str()),

269

_ => None,

270

}

271

}

198

}

272

}

5 linesAll 148 lines5 lines

5 linesAll 148 lines5 lines

chat.rssgl-router/src/protocols

+36−14

Mark as viewed

5 linesAll 337 lines5 lines

5 linesAll 337 lines5 lines

338

fn validate_chat_cross_parameters(

338

fn validate_chat_cross_parameters(

339

req: &ChatCompletionRequest,

339

req: &ChatCompletionRequest,

340

) -> Result<(), validator::ValidationError> {

340

) -> Result<(), validator::ValidationError> {

5 linesAll 68 lines5 lines

5 linesAll 68 lines5 lines

409

if let Some(ref tool_choice) = req.tool_choice {

409

if let Some(ref tool_choice) = req.tool_choice {

5 linesAll 12 lines5 lines

5 linesAll 12 lines5 lines

422

if has_tools {

422

if has_tools {

423

let tools = req.tools.as_ref().unwrap();

423

let tools = req.tools.as_ref().unwrap();

424

424

425

match tool_choice {

425

match tool_choice {

5 linesAll 19 lines5 lines

5 linesAll 19 lines5 lines

445

ToolChoice::AllowedTools {

445

ToolChoice::AllowedTools {

446

mode,

446

mode,

447

tools: allowed_tools,

447

tools: allowed_tools,

448

..

448

..

449

} => {

449

} => {

5 linesAll 10 lines5 lines

5 linesAll 10 lines5 lines

460

// Validate that all referenced tool names exist in tools

460

// Validate that all ToolReferences are Function type (Chat API only supports function tools)

461

for tool_ref in allowed_tools {

461

for tool_ref in allowed_tools {

462

let tool_exists = tools.iter().any(|tool| {

462

match tool_ref {

463

tool.tool_type == tool_ref.tool_type

463

ToolReference::Function { name } => {

464

&& tool.function.name == tool_ref.name

464

// Validate that the function exists in tools array

465

});

465

let tool_exists = tools.iter().any(|tool| {

466

466

tool.tool_type == "function" && tool.function.name == *name

467

if !tool_exists {

467

});

468

let mut e =

468

469

validator::ValidationError::new("tool_choice_tool_not_found");

469

if !tool_exists {

470

e.message = Some(format!(

470

let mut e = validator::ValidationError::new(

471

"Invalid value for 'tool_choice.tools': tool '{}' not found in 'tools'.",

471

"tool_choice_tool_not_found",

472

tool_ref.name

472

);

473

).into());

473

e.message = Some(

474

return Err(e);

474

format!(

475

"Invalid value for 'tool_choice.tools': tool '{}' not found in 'tools'.",

476

name

477

)

478

.into(),

479

);

480

return Err(e);

481

}

482

}

483

_ => {

484

// Chat Completion API only supports function tools in tool_choice

485

let mut e = validator::ValidationError::new(

486

"tool_choice_invalid_tool_type",

487

);

488

e.message = Some(

489

format!(

490

"Invalid value for 'tool_choice.tools': Chat Completion API only supports function tools, got '{}'.",

491

tool_ref.identifier()

492

)

493

.into(),

494

);

495

return Err(e);

496

}

475

}

497

}

476

}

498

}

477

}

499

}

478

_ => {}

500

_ => {}

479

}

501

}

480

}

502

}

481

}

503

}

482

504

483

Ok(())

505

Ok(())

484

}

506

}

5 linesAll 197 lines5 lines

5 linesAll 197 lines5 lines

chat_completion.rssgl-router/tests/spec

+8−16

Mark as viewed

5 linesAll 332 lines5 lines

5 linesAll 332 lines5 lines

333

#[test]

333

#[test]

334

fn test_tool_choice_allowed_tools_invalid_mode() {

334

fn test_tool_choice_allowed_tools_invalid_mode() {

335

let req = ChatCompletionRequest {

335

let req = ChatCompletionRequest {

5 linesAll 14 lines5 lines

5 linesAll 14 lines5 lines

350

tool_choice: Some(ToolChoice::AllowedTools {

350

tool_choice: Some(ToolChoice::AllowedTools {

351

mode: "invalid_mode".to_string(),

351

mode: "invalid_mode".to_string(),

352

tools: vec![ToolReference {

352

tools: vec![ToolReference::Function {

353

tool_type: "function".to_string(),

354

name: "get_weather".to_string(),

353

name: "get_weather".to_string(),

355

}],

354

}],

356

tool_type: "function".to_string(),

355

tool_type: "function".to_string(),

357

}),

356

}),

358

..Default::default()

357

..Default::default()

359

};

358

};

All 9 lines

All 9 lines

369

}

368

}

370

369

371

#[test]

370

#[test]

372

fn test_tool_choice_allowed_tools_valid_mode_auto() {

371

fn test_tool_choice_allowed_tools_valid_mode_auto() {

373

let req = ChatCompletionRequest {

372

let req = ChatCompletionRequest {

5 linesAll 14 lines5 lines

5 linesAll 14 lines5 lines

388

tool_choice: Some(ToolChoice::AllowedTools {

387

tool_choice: Some(ToolChoice::AllowedTools {

389

mode: "auto".to_string(),

388

mode: "auto".to_string(),

390

tools: vec![ToolReference {

389

tools: vec![ToolReference::Function {

391

tool_type: "function".to_string(),

392

name: "get_weather".to_string(),

390

name: "get_weather".to_string(),

393

}],

391

}],

394

tool_type: "function".to_string(),

392

tool_type: "function".to_string(),

395

}),

393

}),

396

..Default::default()

394

..Default::default()

397

};

395

};

398

396

399

let result = req.validate();

397

let result = req.validate();

400

assert!(result.is_ok(), "Should accept 'auto' mode");

398

assert!(result.is_ok(), "Should accept 'auto' mode");

401

}

399

}

402

400

403

#[test]

401

#[test]

404

fn test_tool_choice_allowed_tools_valid_mode_required() {

402

fn test_tool_choice_allowed_tools_valid_mode_required() {

405

let req = ChatCompletionRequest {

403

let req = ChatCompletionRequest {

5 linesAll 14 lines5 lines

5 linesAll 14 lines5 lines

420

tool_choice: Some(ToolChoice::AllowedTools {

418

tool_choice: Some(ToolChoice::AllowedTools {

421

mode: "required".to_string(),

419

mode: "required".to_string(),

422

tools: vec![ToolReference {

420

tools: vec![ToolReference::Function {

423

tool_type: "function".to_string(),

424

name: "get_weather".to_string(),

421

name: "get_weather".to_string(),

425

}],

422

}],

426

tool_type: "function".to_string(),

423

tool_type: "function".to_string(),

427

}),

424

}),

428

..Default::default()

425

..Default::default()

429

};

426

};

430

427

431

let result = req.validate();

428

let result = req.validate();

432

assert!(result.is_ok(), "Should accept 'required' mode");

429

assert!(result.is_ok(), "Should accept 'required' mode");

433

}

430

}

434

431

435

#[test]

432

#[test]

436

fn test_tool_choice_allowed_tools_tool_not_found() {

433

fn test_tool_choice_allowed_tools_tool_not_found() {

437

let req = ChatCompletionRequest {

434

let req = ChatCompletionRequest {

5 linesAll 14 lines5 lines

5 linesAll 14 lines5 lines

452

tool_choice: Some(ToolChoice::AllowedTools {

449

tool_choice: Some(ToolChoice::AllowedTools {

453

mode: "auto".to_string(),

450

mode: "auto".to_string(),

454

tools: vec![ToolReference {

451

tools: vec![ToolReference::Function {

455

tool_type: "function".to_string(),

456

name: "nonexistent_tool".to_string(),

452

name: "nonexistent_tool".to_string(),

457

}],

453

}],

458

tool_type: "function".to_string(),

454

tool_type: "function".to_string(),

459

}),

455

}),

460

..Default::default()

456

..Default::default()

461

};

457

};

All 9 lines

All 9 lines

471

}

467

}

472

468

473

#[test]

469

#[test]

474

fn test_tool_choice_allowed_tools_multiple_tools_valid() {

470

fn test_tool_choice_allowed_tools_multiple_tools_valid() {

475

let req = ChatCompletionRequest {

471

let req = ChatCompletionRequest {

5 linesAll 25 lines5 lines

5 linesAll 25 lines5 lines

501

tool_choice: Some(ToolChoice::AllowedTools {

497

tool_choice: Some(ToolChoice::AllowedTools {

502

mode: "auto".to_string(),

498

mode: "auto".to_string(),

503

tools: vec![

499

tools: vec![

504

ToolReference {

500

ToolReference::Function {

505

tool_type: "function".to_string(),

506

name: "get_weather".to_string(),

501

name: "get_weather".to_string(),

507

},

502

},

508

ToolReference {

503

ToolReference::Function {

509

tool_type: "function".to_string(),

510

name: "get_time".to_string(),

504

name: "get_time".to_string(),

511

},

505

},

512

],

506

],

513

tool_type: "function".to_string(),

507

tool_type: "function".to_string(),

514

}),

508

}),

515

..Default::default()

509

..Default::default()

516

};

510

};

517

511

518

let result = req.validate();

512

let result = req.validate();

519

assert!(result.is_ok(), "Should accept all valid tool references");

513

assert!(result.is_ok(), "Should accept all valid tool references");

520

}

514

}

521

515

522

#[test]

516

#[test]

523

fn test_tool_choice_allowed_tools_one_invalid_among_valid() {

517

fn test_tool_choice_allowed_tools_one_invalid_among_valid() {

524

let req = ChatCompletionRequest {

518

let req = ChatCompletionRequest {

5 linesAll 25 lines5 lines

5 linesAll 25 lines5 lines

550

tool_choice: Some(ToolChoice::AllowedTools {

544

tool_choice: Some(ToolChoice::AllowedTools {

551

mode: "auto".to_string(),

545

mode: "auto".to_string(),

552

tools: vec![

546

tools: vec![

553

ToolReference {

547

ToolReference::Function {

554

tool_type: "function".to_string(),

555

name: "get_weather".to_string(),

548

name: "get_weather".to_string(),

556

},

549

},

557

ToolReference {

550

ToolReference::Function {

558

tool_type: "function".to_string(),

559

name: "nonexistent_tool".to_string(),

551

name: "nonexistent_tool".to_string(),

560

},

552

},

561

],

553

],

562

tool_type: "function".to_string(),

554

tool_type: "function".to_string(),

563

}),

555

}),

564

..Default::default()

556

..Default::default()

565

};

557

};

5 linesAll 12 lines5 lines

5 linesAll 12 lines5 lines

578

}

570

}

2Responses API Validation

0 / 1

responses.rssgl-router/src/protocols

+78

Mark as viewed

5 linesAll 448 lines5 lines

5 linesAll 448 lines5 lines

449

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

449

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

450

#[validate(schema(function = "validate_responses_cross_parameters"))]

450

pub struct ResponsesRequest {

451

pub struct ResponsesRequest {

5 linesAll 98 lines5 lines

5 linesAll 98 lines5 lines

549

550

5 linesAll 10 lines5 lines

5 linesAll 10 lines5 lines

560

pub stop: Option<StringOrArray>,

561

pub stop: Option<StringOrArray>,

561

562

5 linesAll 162 lines5 lines

5 linesAll 162 lines5 lines

1

Copy + paste725–800

725

/// Schema-level validation for cross-field dependencies

726

fn validate_responses_cross_parameters(

727

request: &ResponsesRequest,

728

) -> Result<(), validator::ValidationError> {

729

use super::common::{ToolChoice, ToolReference};

730

731

// Only validate if both tools and tool_choice are present

732

if let (Some(tools), Some(tool_choice)) = (&request.tools, &request.tool_choice) {

733

// Extract function tool names from ResponseTools

734

let function_tool_names: Vec<&str> = tools

735

.iter()

736

.filter_map(|t| match t.r#type {

737

ResponseToolType::Function => t.function.as_ref().map(|f| f.name.as_str()),

738

_ => None,

739

})

740

.collect();

741

742

match tool_choice {

743

ToolChoice::Function { function, .. } => {

744

// Validate the specific function exists

745

if !function_tool_names.contains(&function.name.as_str()) {

746

let mut e = validator::ValidationError::new("tool_choice_function_not_found");

747

e.message = Some(

748

format!(

749

"Invalid value for 'tool_choice': function '{}' not found in 'tools'.",

750

function.name

751

)

752

.into(),

753

);

754

return Err(e);

755

}

756

}

757

ToolChoice::AllowedTools {

758

mode,

759

tools: allowed_tools,

760

..

761

} => {

762

// Validate mode is "auto" or "required"

763

if mode != "auto" && mode != "required" {

764

let mut e = validator::ValidationError::new("tool_choice_invalid_mode");

765

e.message = Some(

766

format!(

767

"Invalid value for 'tool_choice.mode': must be 'auto' or 'required', got '{}'.",

768

mode

769

)

770

.into(),

771

);

772

return Err(e);

773

}

774

775

// Validate that all function tool references exist

InformationalR776-793

776

for tool_ref in allowed_tools {

777

if let ToolReference::Function { name } = tool_ref {

778

if !function_tool_names.contains(&name.as_str()) {

779

let mut e =

780

validator::ValidationError::new("tool_choice_tool_not_found");

781

e.message = Some(

782

format!(

783

"Invalid value for 'tool_choice.tools': tool '{}' not found in 'tools'.",

784

name

785

)

786

.into(),

787

);

788

return Err(e);

789

}

790

}

791

// Note: MCP and hosted tools don't need existence validation here

792

// as they are resolved dynamically at runtime

793

}

794

}

795

_ => {}

796

}

797

}

798

799

Ok(())

800

}

801

5 linesAll 236 lines5 lines

5 linesAll 236 lines5 lines

3Harmony Router: tool_choice Constraint Generation

0 / 4

preparation.rssgl-router/src/routers/grpc/harmony/stages

+24−5

Mark as viewed

All 7 lines

All 7 lines

8

use crate::{

8

use crate::{

All 5 lines

All 5 lines

14

routers::grpc::{

14

routers::grpc::{

15

common::stages::PipelineStage,

15

common::{responses::utils::extract_tools_from_response_tools, stages::PipelineStage},

16

context::{PreparationOutput, RequestContext, RequestType},

16

context::{PreparationOutput, RequestContext, RequestType},

17

error, utils,

17

error, utils,

18

},

18

},

19

};

19

};

All 5 lines

All 5 lines

25

pub struct HarmonyPreparationStage {

25

pub struct HarmonyPreparationStage {

26

builder: HarmonyBuilder,

26

builder: HarmonyBuilder,

27

}

27

}

5 linesAll 44 lines5 lines

5 linesAll 44 lines5 lines

72

impl HarmonyPreparationStage {

72

impl HarmonyPreparationStage {

73

/// Prepare a chat completion request using Harmony encoding

73

/// Prepare a chat completion request using Harmony encoding

74

async fn prepare_chat(

74

async fn prepare_chat(

75

&self,

75

&self,

76

ctx: &mut RequestContext,

76

ctx: &mut RequestContext,

77

request: &ChatCompletionRequest,

77

request: &ChatCompletionRequest,

78

) -> Result<Option<Response>, Response> {

78

) -> Result<Option<Response>, Response> {

All 6 lines

All 6 lines

85

85

86

// Step 1: Filter tools if needed

86

// Step 1: Filter tools if needed

87

let body_ref = utils::filter_tools_for_request(request);

87

let body_ref = utils::filter_chat_request_by_tool_choice(request);

88

88

5 linesAll 28 lines5 lines

5 linesAll 28 lines5 lines

117

});

117

});

118

118

119

Ok(None)

119

Ok(None)

120

}

120

}

121

121

122

/// Prepare a responses API request using Harmony encoding

122

/// Prepare a responses API request using Harmony encoding

123

///

123

///

124

/// For responses API, we build from conversation history using the same Harmony

124

/// For responses API, we build from conversation history using the same Harmony

125

/// encoding that the builder provides. This handles the MCP loop integration.

125

/// encoding that the builder provides. This handles the MCP loop integration.

126

pub async fn prepare_responses(

126

pub async fn prepare_responses(

127

&self,

127

&self,

128

ctx: &mut RequestContext,

128

ctx: &mut RequestContext,

129

request: &ResponsesRequest,

129

request: &ResponsesRequest,

130

) -> Result<Option<Response>, Response> {

130

) -> Result<Option<Response>, Response> {

InformationalR131-140

131

// Build via Harmony from responses API request

131

// Step 1: Extract function and MCP tools with schemas from ResponseTools

132

let mut function_tools = extract_tools_from_response_tools(request.tools.as_deref(), true);

133

134

// Step 2: Filter tools based on tool_choice (AllowedTools or Function)

135

// Note: Tool existence is already validated in ResponsesRequest::validate()

136

if let Some(filtered) =

137

utils::filter_tools_by_tool_choice(&function_tools, &request.tool_choice)

138

{

139

function_tools = filtered;

140

}

141

142

// Step 3: Generate Harmony structural tags from filtered tools

143

let tool_constraints = if !function_tools.is_empty() {

144

Self::generate_harmony_structural_tag(&function_tools, &request.tool_choice)

145

.map_err(|e| *e)?

146

} else {

147

None

148

};

149

150

// Step 3: Build via Harmony from responses API request

132

let build_output = self

151

let build_output = self

133

.builder

152

.builder

134

.build_from_responses(request)

153

.build_from_responses(request)

135

.map_err(|e| error::bad_request(format!("Harmony build failed: {}", e)))?;

154

.map_err(|e| error::bad_request(format!("Harmony build failed: {}", e)))?;

136

155

137

// Store results in preparation output

156

// Step 4: Store results with tool_constraints

138

ctx.state.preparation = Some(PreparationOutput {

157

ctx.state.preparation = Some(PreparationOutput {

139

original_text: None,

158

original_text: None,

140

token_ids: build_output.input_ids,

159

token_ids: build_output.input_ids,

141

processed_messages: None,

160

processed_messages: None,

142

tool_constraints: None,

161

tool_constraints,

143

filtered_request: None,

162

filtered_request: None,

144

harmony_mode: true,

163

harmony_mode: true,

145

selection_text: Some(build_output.selection_text),

164

selection_text: Some(build_output.selection_text),

146

harmony_messages: Some(build_output.harmony_messages),

165

harmony_messages: Some(build_output.harmony_messages),

147

harmony_stop_ids: Some(build_output.stop_token_ids),

166

harmony_stop_ids: Some(build_output.stop_token_ids),

148

});

167

});

149

168

150

Ok(None)

169

Ok(None)

151

}

170

}

5 linesAll 93 lines5 lines

5 linesAll 93 lines5 lines

245

}

264

}

request_building.rssgl-router/src/routers/grpc/harmony/stages

+1

Mark as viewed

5 linesAll 17 lines5 lines

5 linesAll 17 lines5 lines

18

pub struct HarmonyRequestBuildingStage {

18

pub struct HarmonyRequestBuildingStage {

19

inject_pd_metadata: bool,

19

inject_pd_metadata: bool,

20

}

20

}

All 9 lines

All 9 lines

30

impl PipelineStage for HarmonyRequestBuildingStage {

30

impl PipelineStage for HarmonyRequestBuildingStage {

31

async fn execute(&self, ctx: &mut RequestContext) -> Result<Option<Response>, Response> {

31

async fn execute(&self, ctx: &mut RequestContext) -> Result<Option<Response>, Response> {

5 linesAll 32 lines5 lines

5 linesAll 32 lines5 lines

64

let mut proto_request = match &ctx.input.request_type {

64

let mut proto_request = match &ctx.input.request_type {

5 linesAll 15 lines5 lines

5 linesAll 15 lines5 lines

80

RequestType::Responses(request) => builder_client

80

RequestType::Responses(request) => builder_client

81

.build_generate_request_from_responses(

81

.build_generate_request_from_responses(

82

request_id,

82

request_id,

83

request.as_ref(),

83

request.as_ref(),

84

placeholder_processed_text,

84

placeholder_processed_text,

85

prep.token_ids.clone(),

85

prep.token_ids.clone(),

86

prep.harmony_stop_ids.clone(),

86

prep.harmony_stop_ids.clone(),

87

prep.tool_constraints.clone(),

87

)

88

)

88

.map_err(|e| error::bad_request(format!("Invalid request parameters: {}", e)))?,

89

.map_err(|e| error::bad_request(format!("Invalid request parameters: {}", e)))?,

89

_ => unreachable!(),

90

_ => unreachable!(),

90

};

91

};

5 linesAll 23 lines5 lines

5 linesAll 23 lines5 lines

114

}

115

}

All 4 lines

All 4 lines

119

}

120

}

responses.rssgl-router/src/routers/grpc/harmony(1 of 2)

+5

Mark as viewed

5 linesAll 1097 lines5 lines

5 linesAll 1097 lines5 lines

1098

fn build_next_request_with_tools(

1089

fn build_next_request_with_tools(

1099

mut request: ResponsesRequest,

1090

mut request: ResponsesRequest,

1100

tool_calls: Vec<ToolCall>,

1091

tool_calls: Vec<ToolCall>,

1101

tool_results: Vec<ToolResult>,

1092

tool_results: Vec<ToolResult>,

1102

analysis: Option<String>,

1093

analysis: Option<String>,

1103

partial_text: String,

1094

partial_text: String,

1104

) -> Result<ResponsesRequest, Box<Response>> {

1095

) -> Result<ResponsesRequest, Box<Response>> {

5 linesAll 83 lines5 lines

5 linesAll 83 lines5 lines

1188

1179

1189

// Update request with new items

1180

// Update request with new items

1190

request.input = ResponseInput::Items(items);

1181

request.input = ResponseInput::Items(items);

1191

1182

InformationalR1183-1187

1183

// Switch tool_choice to "auto" for subsequent iterations

1184

// This prevents infinite loops when original tool_choice was "required" or specific function

1185

// After receiving tool results, the model should be free to decide whether to call more tools or finish

1186

request.tool_choice = Some(ToolChoice::Value(ToolChoiceValue::Auto));

1187

1192

Ok(request)

1188

Ok(request)

1193

}

1189

}

5 linesAll 214 lines5 lines

5 linesAll 214 lines5 lines

sglang_scheduler.rssgl-router/src/grpc_client

+34−4

Mark as viewed

5 linesAll 124 lines5 lines

5 linesAll 124 lines5 lines

125

impl SglangSchedulerClient {

125

impl SglangSchedulerClient {

5 linesAll 179 lines5 lines

5 linesAll 179 lines5 lines

305

/// Build a GenerateRequest from ResponsesRequest (OpenAI Responses API)

305

/// Build a GenerateRequest from ResponsesRequest (OpenAI Responses API)

306

///

307

/// NOTE: This is used by the Harmony router only. The Regular router uses

308

/// responses_to_chat() conversion and goes through the chat pipeline.

306

pub fn build_generate_request_from_responses(

309

pub fn build_generate_request_from_responses(

307

&self,

310

&self,

308

request_id: String,

311

request_id: String,

309

body: &ResponsesRequest,

312

body: &ResponsesRequest,

310

processed_text: String,

313

processed_text: String,

311

token_ids: Vec<u32>,

314

token_ids: Vec<u32>,

312

harmony_stop_ids: Option<Vec<u32>>,

315

harmony_stop_ids: Option<Vec<u32>>,

316

tool_call_constraint: Option<(String, String)>,

313

) -> Result<proto::GenerateRequest, String> {

317

) -> Result<proto::GenerateRequest, String> {

314

// Build sampling params from ResponsesRequest

318

// Build sampling params from ResponsesRequest

315

let mut sampling_params = self.build_grpc_sampling_params_from_responses(body)?;

319

let mut sampling_params =

320

self.build_grpc_sampling_params_from_responses(body, tool_call_constraint)?;

5 linesAll 22 lines5 lines

5 linesAll 22 lines5 lines

338

Ok(grpc_request)

343

Ok(grpc_request)

339

}

344

}

5 linesAll 101 lines5 lines

5 linesAll 101 lines5 lines

441

fn build_grpc_sampling_params_from_responses(

446

fn build_grpc_sampling_params_from_responses(

442

&self,

447

&self,

443

request: &ResponsesRequest,

448

request: &ResponsesRequest,

449

tool_call_constraint: Option<(String, String)>,

444

) -> Result<proto::SamplingParams, String> {

450

) -> Result<proto::SamplingParams, String> {

445

// ResponsesRequest doesn't have stop sequences in the same way

451

// ResponsesRequest doesn't have stop sequences in the same way

446

// Tools are handled externally by MCP loop, not via constraints

452

// For Harmony router: Tools are handled via structural_tag constraints

447

453

448

let max_new_tokens = request.max_output_tokens.map(|v| v as i32);

454

let max_new_tokens = request.max_output_tokens.map(|v| v as i32);

449

455

450

Ok(proto::SamplingParams {

456

Ok(proto::SamplingParams {

5 linesAll 14 lines5 lines

5 linesAll 14 lines5 lines

465

n: 1, // Responses API doesn't support n>1

471

n: 1, // Responses API doesn't support n>1

466

constraint: None, // No constraints - tools handled by MCP

472

constraint: self.build_constraint_for_responses(tool_call_constraint)?,

467

..Default::default()

473

..Default::default()

468

})

474

})

469

}

475

}

470

476

477

/// Build constraint for Responses API (simpler than Chat API's build_constraint)

478

///

479

/// Responses API doesn't support response_format, ebnf, or regex constraints,

480

/// so this only handles tool_call_constraint.

481

fn build_constraint_for_responses(

482

&self,

483

tool_call_constraint: Option<(String, String)>,

484

) -> Result<Option<proto::sampling_params::Constraint>, String> {

485

if let Some((constraint_type, constraint_value)) = tool_call_constraint {

1

Copy + paste486–494

486

let tool_constraint = match constraint_type.as_str() {

487

"structural_tag" => {

488

proto::sampling_params::Constraint::StructuralTag(constraint_value)

489

}

490

"json_schema" => proto::sampling_params::Constraint::JsonSchema(constraint_value),

491

"ebnf" => proto::sampling_params::Constraint::EbnfGrammar(constraint_value),

492

"regex" => proto::sampling_params::Constraint::Regex(constraint_value),

493

_ => return Err(format!("Unknown constraint type: {}", constraint_type)),

494

};

495

Ok(Some(tool_constraint))

496

} else {

497

Ok(None)

498

}

499

}

500

5 linesAll 101 lines5 lines

5 linesAll 101 lines5 lines

572

}

602

}

5 linesAll 167 lines5 lines

5 linesAll 167 lines5 lines

4Regular Router: tool_choice Pass-through

0 / 2

conversions.rssgl-router/src/routers/grpc/regular/responses

+56−71

Mark as viewed

All 9 lines

All 9 lines

10

use crate::protocols::{

10

use crate::{

11

chat::{ChatCompletionRequest, ChatCompletionResponse, ChatMessage, UserMessageContent},

11

protocols::{

12

common::{FunctionCallResponse, StreamOptions, ToolCall, UsageInfo},

12

chat::{ChatCompletionRequest, ChatCompletionResponse, ChatMessage, UserMessageContent},

13

responses::{

13

common::{FunctionCallResponse, StreamOptions, ToolCall, ToolChoice, UsageInfo},

14

ResponseContentPart, ResponseInput, ResponseInputOutputItem, ResponseOutputItem,

14

responses::{

15

ResponseReasoningContent::ReasoningText, ResponseStatus, ResponsesRequest,

15

ResponseContentPart, ResponseInput, ResponseInputOutputItem, ResponseOutputItem,

16

ResponsesResponse, ResponsesUsage, StringOrContentParts,

16

ResponseReasoningContent::ReasoningText, ResponseStatus, ResponsesRequest,

17

ResponsesResponse, ResponsesUsage, StringOrContentParts,

18

},

17

},

19

},

20

routers::grpc::common::responses::utils::extract_tools_from_response_tools,

18

};

21

};

19

22

20

/// Convert a ResponsesRequest to ChatCompletionRequest for processing through the chat pipeline

23

/// Convert a ResponsesRequest to ChatCompletionRequest for processing through the chat pipeline

21

///

24

///

22

/// # Conversion Logic

25

/// # Conversion Logic

23

/// - `input` (text/items) → `messages` (chat messages)

26

/// - `input` (text/items) → `messages` (chat messages)

24

/// - `instructions` → system message (prepended)

27

/// - `instructions` → system message (prepended)

25

``/// - max_output_tokens → `max_completion_tokens```

28

``/// - max_output_tokens → `max_completion_tokens```

26

/// - Tool-related fields are passed through

29

/// - `tools` → function tools extracted from ResponseTools

30

/// - `tool_choice` → passed through from request

27

/// - Response-specific fields (previous_response_id, conversation) are handled by router

31

/// - Response-specific fields (previous_response_id, conversation) are handled by router

28

pub fn responses_to_chat(req: &ResponsesRequest) -> Result<ChatCompletionRequest, String> {

32

pub fn responses_to_chat(req: &ResponsesRequest) -> Result<ChatCompletionRequest, String> {

5 linesAll 10 lines5 lines

5 linesAll 10 lines5 lines

39

// 2. Convert input to chat messages

43

// 2. Convert input to chat messages

40

match &req.input {

44

match &req.input {

All 7 lines

All 7 lines

48

ResponseInput::Items(items) => {

52

ResponseInput::Items(items) => {

49

// Structured items → convert each to appropriate chat message

53

// Structured items → convert each to appropriate chat message

50

for item in items {

54

for item in items {

51

match item {

55

match item {

52

ResponseInputOutputItem::SimpleInputMessage { content, role, .. } => {

56

ResponseInputOutputItem::SimpleInputMessage { content, role, .. } => {

5 linesAll 18 lines5 lines

5 linesAll 18 lines5 lines

1

Move71–99

71

match role.as_str() {

75

messages.push(role_to_chat_message(role.as_str(), text));

72

"user" => {

73

messages.push(ChatMessage::User {

74

content: UserMessageContent::Text(text),

75

name: None,

76

});

77

}

78

"assistant" => {

79

messages.push(ChatMessage::Assistant {

80

content: Some(text),

81

name: None,

82

tool_calls: None,

83

reasoning_content: None,

84

});

85

}

86

"system" => {

87

messages.push(ChatMessage::System {

88

content: text,

89

name: None,

90

});

91

}

92

_ => {

93

// Unknown role, treat as user message

94

messages.push(ChatMessage::User {

95

content: UserMessageContent::Text(text),

96

name: None,

97

});

98

}

99

}

100

}

76

}

101

ResponseInputOutputItem::Message { role, content, .. } => {

77

ResponseInputOutputItem::Message { role, content, .. } => {

102

// Extract text from content parts

78

// Extract text from content parts

103

let text = extract_text_from_content(content);

79

let text = extract_text_from_content(content);

104

80

105

match role.as_str() {

81

messages.push(role_to_chat_message(role.as_str(), text));

106

"user" => {

107

messages.push(ChatMessage::User {

108

content: UserMessageContent::Text(text),

109

name: None,

110

});

111

}

112

"assistant" => {

113

messages.push(ChatMessage::Assistant {

114

content: Some(text),

115

name: None,

116

tool_calls: None,

117

reasoning_content: None,

118

});

119

}

120

"system" => {

121

messages.push(ChatMessage::System {

122

content: text,

123

name: None,

124

});

125

}

126

_ => {

127

// Unknown role, treat as user message

128

messages.push(ChatMessage::User {

129

content: UserMessageContent::Text(text),

130

name: None,

131

});

132

}

133

}

134

}

82

}

5 linesAll 61 lines5 lines

5 linesAll 61 lines5 lines

196

}

144

}

197

}

145

}

198

}

146

}

199

}

147

}

All 6 lines

All 6 lines

206

// 3. Build ChatCompletionRequest

154

// 3. Extract function tools from ResponseTools

155

// Only function tools are extracted here (include_mcp: false).

156

// MCP tools are merged later by the tool loop (see tool_loop.rs:prepare_chat_tools_and_choice)

157

// before the chat pipeline, where tool_choice constraints are applied to ALL tools combined.

158

let function_tools = extract_tools_from_response_tools(req.tools.as_deref(), false);

159

let tools = if function_tools.is_empty() {

160

None

161

} else {

162

Some(function_tools)

163

};

164

165

// 4. Build ChatCompletionRequest

207

let is_streaming = req.stream.unwrap_or(false);

166

let is_streaming = req.stream.unwrap_or(false);

208

167

209

Ok(ChatCompletionRequest {

168

Ok(ChatCompletionRequest {

5 linesAll 19 lines5 lines

5 linesAll 19 lines5 lines

229

skip_special_tokens: true,

188

skip_special_tokens: true,

230

// Note: tools and tool_choice will be handled separately for MCP transformation

189

tools,

231

tools: None, // Will be set by caller if needed

190

tool_choice: req.tool_choice.clone(),

232

tool_choice: None, // Will be set by caller if needed

233

..Default::default()

191

..Default::default()

234

})

192

})

235

}

193

}

5 linesAll 14 lines5 lines

5 linesAll 14 lines5 lines

1

Move208–232

208

/// Convert role and text to ChatMessage

209

fn role_to_chat_message(role: &str, text: String) -> ChatMessage {

210

match role {

211

"user" => ChatMessage::User {

212

content: UserMessageContent::Text(text),

213

name: None,

214

},

215

"assistant" => ChatMessage::Assistant {

216

content: Some(text),

217

name: None,

218

tool_calls: None,

219

reasoning_content: None,

220

},

221

"system" => ChatMessage::System {

222

content: text,

223

name: None,

224

},

225

_ => {

226

// Unknown role, treat as user message

227

ChatMessage::User {

228

content: UserMessageContent::Text(text),

229

name: None,

230

}

231

}

232

}

233

}

234

250

/// Convert a ChatCompletionResponse to ResponsesResponse

235

/// Convert a ChatCompletionResponse to ResponsesResponse

All 7 lines

All 7 lines

258

pub fn chat_to_responses(

243

pub fn chat_to_responses(

259

chat_resp: &ChatCompletionResponse,

244

chat_resp: &ChatCompletionResponse,

260

original_req: &ResponsesRequest,

245

original_req: &ResponsesRequest,

261

response_id_override: Option<String>,

246

response_id_override: Option<String>,

262

) -> Result<ResponsesResponse, String> {

247

) -> Result<ResponsesResponse, String> {

5 linesAll 76 lines5 lines

5 linesAll 76 lines5 lines

339

// Generate response

324

// Generate response

340

Ok(ResponsesResponse {

325

Ok(ResponsesResponse {

5 linesAll 14 lines5 lines

5 linesAll 14 lines5 lines

355

temperature: original_req.temperature,

340

temperature: original_req.temperature,

356

text: None,

341

text: None,

357

tool_choice: "auto".to_string(), // TODO: Map from original request

342

tool_choice: ToolChoice::serialize_to_string(&original_req.tool_choice),

358

tools: original_req.tools.clone().unwrap_or_default(),

343

tools: original_req.tools.clone().unwrap_or_default(),

All 6 lines

All 6 lines

365

})

350

})

366

}

351

}

5 linesAll 63 lines5 lines

5 linesAll 63 lines5 lines

tool_loop.rssgl-router/src/routers/grpc/regular/responses

+45−24

Mark as viewed

All 8 lines

All 8 lines

9

use axum::{

9

use axum::{

10

body::Body,

10

body::Body,

11

http::{header, StatusCode},

11

http::{header, StatusCode},

12

response::Response,

12

response::Response,

13

};

13

};

14

use bytes::Bytes;

14

use bytes::Bytes;

15

use futures_util::StreamExt;

15

use futures_util::StreamExt;

16

use serde_json::json;

16

use serde_json::{json, Value};

17

use tokio::sync::mpsc;

17

use tokio::sync::mpsc;

All 5 lines

All 5 lines

23

use crate::{

23

use crate::{

24

mcp::{self, McpManager},

24

mcp::{self, McpManager},

25

protocols::{

25

protocols::{

26

chat::{

26

chat::{

27

ChatChoice, ChatCompletionMessage, ChatCompletionResponse, ChatCompletionStreamResponse,

27

ChatChoice, ChatCompletionMessage, ChatCompletionRequest, ChatCompletionResponse,

28

ChatCompletionStreamResponse,

28

},

29

},

All 6 lines

All 6 lines

35

},

36

},

All 4 lines

All 4 lines

40

};

41

};

5 linesAll 28 lines5 lines

5 linesAll 28 lines5 lines

70

/// Merge function tools from request with MCP tools and set tool_choice based on iteration

71

fn prepare_chat_tools_and_choice(

72

chat_request: &mut ChatCompletionRequest,

73

mcp_chat_tools: &[Tool],

74

iteration: usize,

75

) {

76

// Merge function tools from request with MCP tools

77

let mut all_tools = chat_request.tools.clone().unwrap_or_default();

78

all_tools.extend(mcp_chat_tools.iter().cloned());

79

chat_request.tools = Some(all_tools);

80

81

// Set tool_choice based on iteration

82

// - Iteration 0: Use user's tool_choice or default to auto

83

// - Iteration 1+: Always use auto to avoid infinite loops

84

chat_request.tool_choice = if iteration == 0 {

85

chat_request

86

.tool_choice

87

.clone()

88

.or(Some(ToolChoice::Value(ToolChoiceValue::Auto)))

89

} else {

90

Some(ToolChoice::Value(ToolChoiceValue::Auto))

91

};

92

}

93

5 linesAll 96 lines5 lines

5 linesAll 96 lines5 lines

165

fn build_mcp_list_tools_item(mcp: &Arc<McpManager>, server_label: &str) -> ResponseOutputItem {

190

fn build_mcp_list_tools_item(mcp: &Arc<McpManager>, server_label: &str) -> ResponseOutputItem {

166

let tools = mcp.list_tools();

191

let tools = mcp.list_tools();

167

let tools_info: Vec<McpToolInfo> = tools

192

let tools_info: Vec<McpToolInfo> = tools

168

.iter()

193

.iter()

169

.map(|t| {

194

.map(|t| McpToolInfo {

170

use serde_json::Value;

195

name: t.name.to_string(),

171

McpToolInfo {

196

description: t.description.as_ref().map(|d| d.to_string()),

172

name: t.name.to_string(),

197

input_schema: Value::Object((*t.input_schema).clone()),

173

description: t.description.as_ref().map(|d| d.to_string()),

198

annotations: Some(json!({

174

input_schema: Value::Object((*t.input_schema).clone()),

199

"read_only": false

175

annotations: Some(json!({

200

})),

176

"read_only": false

177

})),

178

}

179

})

201

})

180

.collect();

202

.collect();

All 6 lines

All 6 lines

187

}

209

}

5 linesAll 29 lines5 lines

5 linesAll 29 lines5 lines

217

pub(super) async fn execute_tool_loop(

239

pub(super) async fn execute_tool_loop(

All 6 lines

All 6 lines

224

) -> Result<ResponsesResponse, Response> {

246

) -> Result<ResponsesResponse, Response> {

5 linesAll 23 lines5 lines

5 linesAll 23 lines5 lines

248

// Get MCP tools and convert to chat format (do this once before loop)

270

// Get MCP tools and convert to chat format (do this once before loop)

249

let mcp_tools = ctx.mcp_manager.list_tools();

271

let mcp_tools = ctx.mcp_manager.list_tools();

250

let chat_tools = convert_mcp_tools_to_chat_tools(&mcp_tools);

272

let mcp_chat_tools = convert_mcp_tools_to_chat_tools(&mcp_tools);

251

debug!("Converted {} MCP tools to chat format", chat_tools.len());

273

debug!(

274

"Converted {} MCP tools to chat format",

275

mcp_chat_tools.len()

276

);

252

277

InformationalR278-301

253

loop {

278

loop {

254

// Convert to chat request

279

// Convert to chat request

255

let mut chat_request = conversions::responses_to_chat(&current_request)

280

let mut chat_request = conversions::responses_to_chat(&current_request)

256

.map_err(|e| error::bad_request(format!("Failed to convert request: {}", e)))?;

281

.map_err(|e| error::bad_request(format!("Failed to convert request: {}", e)))?;

257

282

258

// Add MCP tools to chat request so LLM knows about them

283

// Prepare tools and tool_choice for this iteration

259

chat_request.tools = Some(chat_tools.clone());

284

prepare_chat_tools_and_choice(&mut chat_request, &mcp_chat_tools, state.iteration);

260

chat_request.tool_choice = Some(ToolChoice::Value(ToolChoiceValue::Auto));

5 linesAll 180 lines5 lines

5 linesAll 180 lines5 lines

441

}

465

}

442

}

466

}

5 linesAll 73 lines5 lines

5 linesAll 73 lines5 lines

516

async fn execute_tool_loop_streaming_internal(

540

async fn execute_tool_loop_streaming_internal(

All 6 lines

All 6 lines

523

) -> Result<(), String> {

547

) -> Result<(), String> {

5 linesAll 32 lines5 lines

5 linesAll 32 lines5 lines

556

// Get MCP tools and convert to chat format (do this once before loop)

580

// Get MCP tools and convert to chat format (do this once before loop)

557

let mcp_tools = ctx.mcp_manager.list_tools();

581

let mcp_tools = ctx.mcp_manager.list_tools();

558

let chat_tools = convert_mcp_tools_to_chat_tools(&mcp_tools);

582

let mcp_chat_tools = convert_mcp_tools_to_chat_tools(&mcp_tools);

559

debug!(

583

debug!(

560

"Streaming: Converted {} MCP tools to chat format",

584

"Streaming: Converted {} MCP tools to chat format",

561

chat_tools.len()

585

mcp_chat_tools.len()

562

);

586

);

All 4 lines

All 4 lines

1

Potential BugR591-662

567

loop {

591

loop {

5 linesAll 11 lines5 lines

5 linesAll 11 lines5 lines

579

if !mcp_list_tools_emitted {

603

if !mcp_list_tools_emitted {

580

let (output_index, item_id) =

604

let (output_index, item_id) =

581

emitter.allocate_output_index(OutputItemType::McpListTools);

605

emitter.allocate_output_index(OutputItemType::McpListTools);

582

606

583

// Build tools list for item structure

607

// Build tools list for item structure

584

let tool_items: Vec<_> = mcp_tools

608

let tool_items: Vec<_> = mcp_tools

585

.iter()

609

.iter()

586

.map(|t| {

610

.map(|t| {

587

use serde_json::Value;

588

json!({

611

json!({

589

"name": t.name,

612

"name": t.name,

590

"description": t.description,

613

"description": t.description,

591

"input_schema": Value::Object((*t.input_schema).clone())

614

"input_schema": Value::Object((*t.input_schema).clone())

592

})

615

})

593

})

616

})

594

.collect();

617

.collect();

5 linesAll 37 lines5 lines

5 linesAll 37 lines5 lines

632

}

655

}

633

656

634

// Convert to chat request

657

// Convert to chat request

635

let mut chat_request = conversions::responses_to_chat(&current_request)

658

let mut chat_request = conversions::responses_to_chat(&current_request)

636

.map_err(|e| format!("Failed to convert request: {}", e))?;

659

.map_err(|e| format!("Failed to convert request: {}", e))?;

637

660

638

// Add MCP tools to chat request so LLM knows about them

661

// Prepare tools and tool_choice for this iteration (same logic as non-streaming)

639

chat_request.tools = Some(chat_tools.clone());

662

prepare_chat_tools_and_choice(&mut chat_request, &mcp_chat_tools, state.iteration);

640

chat_request.tool_choice = Some(ToolChoice::Value(ToolChoiceValue::Auto));

5 linesAll 268 lines5 lines

5 linesAll 268 lines5 lines

909

}

931

}

910

932

911

Ok(())

933

Ok(())

912

}

934

}

913

935

914

/// Convert MCP tools to Chat API tool format

936

/// Convert MCP tools to Chat API tool format

915

fn convert_mcp_tools_to_chat_tools(mcp_tools: &[mcp::Tool]) -> Vec<Tool> {

937

fn convert_mcp_tools_to_chat_tools(mcp_tools: &[mcp::Tool]) -> Vec<Tool> {

916

use serde_json::Value;

917

mcp_tools

938

mcp_tools

918

.iter()

939

.iter()

919

.map(|tool_info| Tool {

940

.map(|tool_info| Tool {

All 8 lines

All 8 lines

928

.collect()

949

.collect()

929

}

950

}

930

951

5 linesAll 141 lines5 lines

5 linesAll 141 lines5 lines

5Shared Utilities

0 / 2

utils.rssgl-router/src/routers/grpc/common/responses

+48−1

Mark as viewed

5 linesAll 10 lines5 lines

5 linesAll 10 lines5 lines

11

use crate::{

11

use crate::{

12

core::WorkerRegistry,

12

core::WorkerRegistry,

13

mcp::McpManager,

13

mcp::McpManager,

14

protocols::responses::{ResponseTool, ResponseToolType},

14

protocols::{

15

common::Tool,

16

responses::{ResponseTool, ResponseToolType},

17

},

15

routers::{grpc::error, openai::mcp::ensure_request_mcp_client},

18

routers::{grpc::error, openai::mcp::ensure_request_mcp_client},

16

};

19

};

5 linesAll 62 lines5 lines

5 linesAll 62 lines5 lines

82

83

/// Extract function tools (and optionally MCP tools) from ResponseTools

84

///

85

/// This utility consolidates the logic for extracting tools with schemas from ResponseTools.

86

/// It's used by both Harmony and Regular routers for different purposes:

87

///

88

/// - **Harmony router**: Extracts both Function and MCP tools (with `include_mcp: true`)

89

/// because MCP schemas are populated by convert_mcp_tools_to_response_tools() before the

90

/// pipeline runs. These tools are used to generate structural constraints in the

91

/// Harmony preparation stage.

92

///

93

/// - **Regular router**: Extracts only Function tools (with `include_mcp: false`) during

94

/// the initial conversion from ResponsesRequest to ChatCompletionRequest. MCP tools

95

/// are merged later by the tool loop before being sent to the chat pipeline, where

96

/// tool_choice constraints are generated for ALL tools (function + MCP combined).

97

pub fn extract_tools_from_response_tools(

98

response_tools: Option<&[ResponseTool]>,

99

include_mcp: bool,

100

) -> Vec<Tool> {

101

let Some(tools) = response_tools else {

102

return Vec::new();

103

};

104

105

tools

106

.iter()

107

.filter_map(|rt| {

108

match rt.r#type {

109

// Function tools: Schema in request

110

ResponseToolType::Function => rt.function.as_ref().map(|f| Tool {

111

tool_type: "function".to_string(),

112

function: f.clone(),

113

}),

114

// MCP tools: Schema populated by convert_mcp_tools_to_response_tools()

115

// Only include if requested (Harmony case)

116

ResponseToolType::Mcp if include_mcp => rt.function.as_ref().map(|f| Tool {

117

tool_type: "function".to_string(),

118

function: f.clone(),

119

}),

120

// Hosted tools: No schema available, skip

121

_ => None,

122

}

123

})

124

.collect()

125

}

utils.rssgl-router/src/routers/grpc

+40−24

Mark as viewed

5 linesAll 10 lines5 lines

5 linesAll 10 lines5 lines

11

use super::{error, ProcessedMessages};

11

use super::{error, ProcessedMessages};

12

pub use crate::tokenizer::StopSequenceDecoder;

13

use crate::{

12

use crate::{

5 linesAll 14 lines5 lines

5 linesAll 14 lines5 lines

28

tokenizer::{

27

tokenizer::{

29

cache::CachedTokenizer,

28

cache::CachedTokenizer,

30

chat_template::{ChatTemplateContentFormat, ChatTemplateParams},

29

chat_template::{ChatTemplateContentFormat, ChatTemplateParams},

30

stop::StopSequenceDecoderBuilder,

31

traits::Tokenizer,

31

traits::Tokenizer,

32

HuggingFaceTokenizer,

32

HuggingFaceTokenizer, StopSequenceDecoder,

33

},

33

},

34

tool_parser::{

34

tool_parser::{

35

ParserFactory as ToolParserFactory, PooledParser as ToolPooledParser, ToolParser,

35

ParserFactory as ToolParserFactory, PooledParser as ToolPooledParser, ToolParser,

36

},

36

},

37

};

37

};

5 linesAll 238 lines5 lines

5 linesAll 238 lines5 lines

276

/// Filter tools based on tool_choice (shared by both routers)

276

/// Filter tools based on tool_choice (generic helper)

277

/// Returns a reference to the original body if no filtering needed,

277

///

278

/// otherwise returns a cloned and filtered body

278

/// Returns filtered tools if filtering is needed, otherwise returns None.

279

pub fn filter_tools_for_request(

279

/// Used by both Chat API and Responses API (Harmony) for constraint generation.

280

body: &ChatCompletionRequest,

280

pub fn filter_tools_by_tool_choice(

281

) -> std::borrow::Cow<'_, ChatCompletionRequest> {

281

tools: &[Tool],

282

match &body.tool_choice {

282

tool_choice: &Option<ToolChoice>,

283

Some(ToolChoice::AllowedTools { tools: allowed, .. }) if body.tools.is_some() => {

283

) -> Option<Vec<Tool>> {

284

let mut filtered_body = body.clone();

284

match tool_choice {

1

InvestigateR285-293

285

let all_tools = filtered_body.tools.as_ref().unwrap();

285

Some(ToolChoice::AllowedTools { tools: allowed, .. }) => {

286

let allowed_names: std::collections::HashSet<&str> =

286

let allowed_names: std::collections::HashSet<&str> =

287

allowed.iter().map(|t| t.name.as_str()).collect();

287

allowed.iter().filter_map(|t| t.function_name()).collect();

288

let filtered_tools: Vec<Tool> = all_tools

288

let filtered: Vec<Tool> = tools

289

.iter()

289

.iter()

290

.filter(|t| allowed_names.contains(t.function.name.as_str()))

290

.filter(|t| allowed_names.contains(t.function.name.as_str()))

291

.cloned()

291

.cloned()

292

.collect();

292

.collect();

293

filtered_body.tools = Some(filtered_tools);

293

Some(filtered)

294

std::borrow::Cow::Owned(filtered_body)

295

}

294

}

296

Some(ToolChoice::Function { function, .. }) if body.tools.is_some() => {

295

Some(ToolChoice::Function { function, .. }) => {

297

let mut filtered_body = body.clone();

296

let filtered: Vec<Tool> = tools

298

let all_tools = filtered_body.tools.as_ref().unwrap();

299

let filtered_tools: Vec<Tool> = all_tools

300

.iter()

297

.iter()

301

.filter(|t| t.function.name == function.name)

298

.filter(|t| t.function.name == function.name)

302

.cloned()

299

.cloned()

303

.collect();

300

.collect();

301

Some(filtered)

302

}

303

_ => None, // No filtering needed

304

}

305

}

306

307

/// Filter ChatCompletionRequest by tool_choice

308

///

309

/// Returns a reference to the original request if no filtering needed,

310

/// otherwise returns a cloned request with filtered tools.

311

///

312

/// Note: Tool existence is validated earlier in ChatCompletionRequest::validate(),

313

/// so this function assumes tool_choice references valid tools.

314

pub fn filter_chat_request_by_tool_choice(

315

body: &ChatCompletionRequest,

316

) -> std::borrow::Cow<'_, ChatCompletionRequest> {

317

if let Some(tools) = &body.tools {

318

if let Some(filtered_tools) = filter_tools_by_tool_choice(tools, &body.tool_choice) {

319

let mut filtered_body = body.clone();

304

filtered_body.tools = Some(filtered_tools);

320

filtered_body.tools = Some(filtered_tools);

305

std::borrow::Cow::Owned(filtered_body)

321

return std::borrow::Cow::Owned(filtered_body);

306

}

322

}

307

_ => std::borrow::Cow::Borrowed(body), // No filtering needed, use original

308

}

323

}

324

325

// No filtering needed - return original request

326

std::borrow::Cow::Borrowed(body)

309

}

327

}

5 linesAll 124 lines5 lines

5 linesAll 124 lines5 lines

434

pub fn create_stop_decoder(

452

pub fn create_stop_decoder(

435

tokenizer: &Arc<dyn Tokenizer>,

453

tokenizer: &Arc<dyn Tokenizer>,

436

stop: Option<&StringOrArray>,

454

stop: Option<&StringOrArray>,

437

stop_token_ids: Option<&Vec<u32>>,

455

stop_token_ids: Option<&Vec<u32>>,

438

skip_special_tokens: bool,

456

skip_special_tokens: bool,

439

no_stop_trim: bool,

457

no_stop_trim: bool,

440

) -> StopSequenceDecoder {

458

) -> StopSequenceDecoder {

441

use crate::tokenizer::stop::StopSequenceDecoderBuilder;

442

5 linesAll 32 lines5 lines

5 linesAll 32 lines5 lines

475

}

491

}

5 linesAll 644 lines5 lines

5 linesAll 644 lines5 lines

6Miscellaneous

0 / 3

handlers.rssgl-router/src/routers/grpc/regular/responses

+2−2

Mark as viewed

5 linesAll 53 lines5 lines

5 linesAll 53 lines5 lines

54

use crate::{

54

use crate::{

All 4 lines

All 4 lines

59

protocols::{

59

protocols::{

60

chat::{self, ChatCompletionStreamResponse},

60

chat::{self, ChatCompletionStreamResponse},

61

common,

61

common::{self, ToolChoice},

62

responses::{

62

responses::{

63

self, ResponseContentPart, ResponseInput, ResponseInputOutputItem, ResponseOutputItem,

63

self, ResponseContentPart, ResponseInput, ResponseInputOutputItem, ResponseOutputItem,

64

ResponseReasoningContent, ResponseStatus, ResponsesRequest, ResponsesResponse,

64

ResponseReasoningContent, ResponseStatus, ResponsesRequest, ResponsesResponse,

65

ResponsesUsage,

65

ResponsesUsage,

66

},

66

},

67

},

67

},

All 9 lines

All 9 lines

77

};

77

};

5 linesAll 422 lines5 lines

5 linesAll 422 lines5 lines

500

impl StreamingResponseAccumulator {

500

impl StreamingResponseAccumulator {

5 linesAll 87 lines5 lines

5 linesAll 87 lines5 lines

588

fn finalize(self) -> ResponsesResponse {

588

fn finalize(self) -> ResponsesResponse {

5 linesAll 54 lines5 lines

5 linesAll 54 lines5 lines

643

ResponsesResponse {

643

ResponsesResponse {

5 linesAll 15 lines5 lines

5 linesAll 15 lines5 lines

659

text: None,

659

text: None,

660

tool_choice: "auto".to_string(),

660

tool_choice: ToolChoice::serialize_to_string(&self.original_request.tool_choice),

661

tools: self.original_request.tools.clone().unwrap_or_default(),

661

tools: self.original_request.tools.clone().unwrap_or_default(),

All 6 lines

All 6 lines

668

}

668

}

669

}

669

}

670

}

670

}

5 linesAll 206 lines5 lines

5 linesAll 206 lines5 lines

preparation.rssgl-router/src/routers/grpc/regular/stages/chat

+1−1

Mark as viewed

5 linesAll 35 lines5 lines

5 linesAll 35 lines5 lines

36

impl ChatPreparationStage {

36

impl ChatPreparationStage {

37

async fn prepare_chat(

37

async fn prepare_chat(

38

&self,

38

&self,

39

ctx: &mut RequestContext,

39

ctx: &mut RequestContext,

40

request: &ChatCompletionRequest,

40

request: &ChatCompletionRequest,

41

) -> Result<(), Response> {

41

) -> Result<(), Response> {

42

// Step 1: Filter tools if needed

42

// Step 1: Filter tools if needed

43

let body_ref = utils::filter_tools_for_request(request);

43

let body_ref = utils::filter_chat_request_by_tool_choice(request);

5 linesAll 59 lines5 lines

5 linesAll 59 lines5 lines

103

}

103

}

104

}

104

}

responses.rssgl-router/src/routers/grpc/harmony(2 of 2)

+1−18

Mark as viewed

5 linesAll 50 lines5 lines

5 linesAll 50 lines5 lines

51

use crate::{

51

use crate::{

52

data_connector::{ResponseId, ResponseStorage},

52

data_connector::{ResponseId, ResponseStorage},

53

mcp::{self, McpManager},

53

mcp::{self, McpManager},

54

protocols::{

54

protocols::{

55

common::{Function, ToolCall, Usage},

55

common::{Function, ToolCall, ToolChoice, ToolChoiceValue, Usage},

56

responses::{

56

responses::{

All 4 lines

All 4 lines

61

},

61

},

62

},

62

},

5 linesAll 10 lines5 lines

5 linesAll 10 lines5 lines

73

};

73

};

5 linesAll 383 lines5 lines

5 linesAll 383 lines5 lines

457

/// Serve Harmony Responses API with streaming (SSE)

457

/// Serve Harmony Responses API with streaming (SSE)

5 linesAll 11 lines5 lines

5 linesAll 11 lines5 lines

469

``/// - Handles errors with `response.failed```

469

``/// - Handles errors with `response.failed```

470

///

471

/// # Arguments

472

///

473

/// * `ctx` - Harmony responses context with pipeline and dependencies

474

/// * `request` - Responses API request

475

///

476

/// # Returns

477

///

478

/// SSE stream response with proper headers

479

pub async fn serve_harmony_responses_stream(

470

pub async fn serve_harmony_responses_stream(

480

ctx: &HarmonyResponsesContext,

471

ctx: &HarmonyResponsesContext,

481

request: ResponsesRequest,

472

request: ResponsesRequest,

482

) -> Response {

473

) -> Response {

5 linesAll 54 lines5 lines

5 linesAll 54 lines5 lines

537

}

528

}

5 linesAll 560 lines5 lines

5 linesAll 560 lines5 lines

1098

fn build_next_request_with_tools(

1089

fn build_next_request_with_tools(

All 5 lines

All 5 lines

1104

) -> Result<ResponsesRequest, Box<Response>> {

1095

) -> Result<ResponsesRequest, Box<Response>> {

5 linesAll 92 lines5 lines

5 linesAll 92 lines5 lines

1192

Ok(request)

1188

Ok(request)

1193

}

1189

}

5 linesAll 19 lines5 lines

5 linesAll 19 lines5 lines

1213

/// Convert MCP tools to Responses API tool format

1209

/// Convert MCP tools to Responses API tool format

1214

///

1210

///

1215

/// Converts MCP Tool entries (from rmcp SDK) to ResponseTool format so the model

1211

/// Converts MCP Tool entries (from rmcp SDK) to ResponseTool format so the model

1216

/// knows about available MCP tools when making tool calls.

1212

/// knows about available MCP tools when making tool calls.

1217

///

1218

/// # Arguments

1219

///

1220

/// * `mcp_tools` - MCP tools from the MCP manager inventory (rmcp::model::Tool)

1221

///

1222

/// # Returns

1223

///

1224

/// Vector of ResponseTool entries in MCP format

1225

pub fn convert_mcp_tools_to_response_tools(mcp_tools: &[mcp::Tool]) -> Vec<ResponseTool> {

1213

pub fn convert_mcp_tools_to_response_tools(mcp_tools: &[mcp::Tool]) -> Vec<ResponseTool> {

5 linesAll 18 lines5 lines

5 linesAll 18 lines5 lines

1244

}

1232

}

5 linesAll 163 lines5 lines

5 linesAll 163 lines5 lines

End of changesChat about this PR

InfoChat

1 Potential bug

Streaming tool loop never uses user's tool_choice due to premature iteration increment

Bugtool_loop.rs:591

5 Flags

Tool filtering returns empty vec for AllowedTools with only non-function references

Investigateutils.rs:285-293

Harmony preparation extracts both Function and MCP tools for constraint generation

preparation.rs:131-140

MCP tool references bypass validation intentionally but may cause runtime errors

responses.rs:776-793

Harmony router correctly switches tool_choice to auto for subsequent iterations

responses.rs:1183-1187

Non-streaming tool loop iteration tracking is correct

tool_loop.rs:278-301

ChecksPartial121/122

PR Test

pr-test-finishRequired3s

Reviewers3

KEkey4ng

BYByronHsu

SLslin1237

Assignees

No assignees

Labels4

enhancementhigh priorityrouterrun-ci

Introducing Devin Review

Devin Review is an intelligent PR review tool that helps you better understand your team's code before you ship it.

1

Intelligently organizes your diff

Groups changes into logical sections with clear explanations

→

2

Detects moved or copied code

Keeps the diff clean by highlighting copy-pasted or moved code

3

Analyzes your PR

Highlights potential bugs and flags areas that deserve extra careful review

Ask questions or request edits in the chat.

Ask Devin anything about this PR (Ctrl+I)

Authorization Response

Authorization Response