Skip to content

fix(openai): preserve allowed_tools tool_choice across Chat Completions routes, refs #2504 - #2519

Merged
looplj merged 1 commit into
looplj:unstablefrom
remote-controlled-man:codex/axonhub-2504-tool-choice-fields
Sep 21, 2026
Merged

looplj merged 1 commit into
looplj:unstablefrom
remote-controlled-man:codex/axonhub-2504-tool-choice-fields

Conversation

@remote-controlled-man

@remote-controlled-man remote-controlled-man commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Refs #2504 (case 1).

Problem

On Chat Completions routes, a tool_choice of type allowed_tools was forwarded upstream as {"type":"allowed_tools","function":{"name":""}} — an empty function name, with the caller's mode and tool subset silently gone.

Chat Completions nests the choice as {"type":"allowed_tools","allowed_tools":{"mode":"required","tools":[{"type":"function","function":{"name":"tool_a"}}]}}, but ToolChoice.UnmarshalJSON parsed any object as the plain named shape {type, function}; the nested allowed_tools key was dropped at decode time, and the outbound converter then re-emitted the named shape with an empty function.name. A restriction the caller set was lifted without any error.

Fix

  • model.go: ToolChoice gains an AllowedTools field; unmarshal decodes the nested allowed_tools object (mode + tool subset) before the named fallback, and marshal emits the nested shape for an allowed_tools choice — it can no longer produce an empty function name for it.
  • inbound_convert.go: the parsed mode and subset now land in the unified llm.ToolChoice (mode in ToolChoice, subset in Tools), the same representation the Responses transformer already uses since feat(llm): response namespace transfomer #2468.
  • outbound_convert.go: a ToolChoiceFromLLM helper rebuilds the nested Chat Completions form for allowed_tools.

String choices (auto/none/required) and named single-function choices are unchanged, and llm/tools.go is untouched — the unified model already round-trips the subset for request persistence.

Cross-route note: for an allowed_tools input the inbound converter now also fills the unified llm.ToolChoice.ToolChoice mode, which subtly improves other routes' behavior rather than degrading it — Anthropic outbound now receives {"type":"any"} for mode required and {"type":"auto"} for auto (previously nothing was forwarded), Responses outbound now emits the full {type,mode,tools} object instead of a bare {"type":"allowed_tools"}, and Gemini maps mode auto to AUTO instead of falling into the named branch. The tool subset remains dropped on Anthropic/Gemini — that is cases 2-6, out of scope here.

Scope note

#2504 also reports dropped strict, allowed_callers, and parallel_tool_calls across Anthropic/Gemini routes (cases 2-6). Those are separate transformers with different trade-offs (e.g. whether to reject unmappable restrictions), so this PR stays on the malformed-request case to keep the change reviewable. The open #2220 Responses→Chat compatibility feature is complementary and does not cover this path (its plain Chat codec copies the named choice verbatim).

Tests

  • New TestToolChoice_AllowedToolsMarshalUnmarshalJSON and TestRoundTrip_AllowedToolsToolChoice (model_test.go): the issue's exact payload now round-trips through inbound → unified → outbound byte-faithfully (before: {"type":"allowed_tools","function":{"name":""}}).
  • New TestRequestFromLLM_AllowedToolsToolChoice / TestRequestFromLLM_NamedToolChoiceUnchanged (outbound_convert_test.go): nested form emitted; named choice keeps its existing shape.
  • cd llm && go test ./... passes (one pre-existing websocket_executor test failure also fails on a clean checkout of the base on Windows); go vet ./... and gofmt clean on changed files.

Summary by CodeRabbit

  • New Features

    • Added support for the allowed_tools tool-choice option.
    • Preserves the selection mode and permitted tool subset when converting requests and responses.
  • Bug Fixes

    • Corrected serialization and deserialization of allowed_tools choices.
    • Maintains the existing format for named function choices.

Copilot AI lite review requested due to automatic review settings September 21, 2026 11:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Understand this PR’s impact

Explore downstream dependencies and potential security impact with Blast Radius.

View blast radius →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 233f9a88-9b9d-488d-be40-e66efef0b3f9

📥 Commits

Reviewing files that changed from the base of the PR and between 8545ddd and c7746e8.

📒 Files selected for processing (5)
  • llm/transformer/openai/inbound_convert.go
  • llm/transformer/openai/model.go
  • llm/transformer/openai/model_test.go
  • llm/transformer/openai/outbound_convert.go
  • llm/transformer/openai/outbound_convert_test.go

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The OpenAI transformer now supports allowed_tools tool choices. It preserves the nested mode and restricted tool list during JSON marshaling, unmarshaling, inbound conversion, outbound conversion, and request round trips.

Changes

OpenAI allowed_tools support

Layer / File(s) Summary
Tool-choice wire contract
llm/transformer/openai/model.go
ToolChoice now stores AllowedTools. JSON conversion handles the nested allowed_tools shape with its mode and tool list.
Tool-choice conversion flow
llm/transformer/openai/inbound_convert.go, llm/transformer/openai/outbound_convert.go
Conversion helpers map allowed-tools modes and restricted tool names between OpenAI and LLM representations.
Round-trip validation
llm/transformer/openai/model_test.go, llm/transformer/openai/outbound_convert_test.go
Tests cover JSON round trips, request round trips, allowed tool subsets, and existing named function choices.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant OpenAIRequest
  participant ToLLMRequest
  participant LLMRequest
  participant RequestFromLLM
  OpenAIRequest->>ToLLMRequest: Convert allowed_tools choice
  ToLLMRequest->>LLMRequest: Store mode and allowed tool options
  LLMRequest->>RequestFromLLM: Convert tool choice
  RequestFromLLM->>OpenAIRequest: Restore nested allowed_tools shape
Loading

Suggested reviewers: looplj

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preserving allowed_tools tool_choice values across OpenAI Chat Completions routes. The issue reference is relevant and does not obscure t…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@looplj
looplj merged commit d523743 into looplj:unstable Sep 21, 2026
6 checks passed
Edint386 pushed a commit to Edint386/axonhub that referenced this pull request Oct 9, 2026
@remote-controlled-man
remote-controlled-man deleted the codex/axonhub-2504-tool-choice-fields branch October 13, 2026 01:31
looplj pushed a commit that referenced this pull request Oct 29, 2026
) (#2639)

Model Anthropic's `allowed_callers` tool field on the wire Tool struct and on
the unified llm.Function, and copy it in both conversion directions so it is
no longer dropped.

This is the last remaining sub-item of #2504: the other five dropped tool
fields were already fixed by #2519, #2523 and #2539 and are left untouched.
Only function/custom tools are mapped; server tools (web_search) have no
documented allowed_callers equivalent in this code path and stay unchanged.
An absent value is preserved as absent (never `[]` or `null`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants