Skip to content

antigravity-direct: a specific-function tool_choice is flattened to functionCallingConfig ANY — the client-required function is never pinned and any declared function may be called #2822

Closed

Description

@dezren39

antigravity-direct: a specific-function tool_choice is flattened to functionCallingConfig: ANY — the function the client required is never pinned, and any declared function may be called instead

Repo: developing-today/code · Main: df9381ff · File: bin/antigravity-direct.mjs:355-357 (landed today in b0058ec7)

Summary

OpenAI's Chat Completions API lets a caller force one specific function: tool_choice: { "type": "function", "function": { "name": "get_weather" } }. buildV1Request translates tool_choice to the Cloud Code / Gemini functionCallingConfig like this:

if (body.tool_choice) {
  const mode = body.tool_choice === 'none' ? 'NONE' : body.tool_choice === 'auto' ? 'AUTO' : 'ANY';
  request.toolConfig = { functionCallingConfig: { mode } };
}

Every non-none/auto value — including the specific-function object — becomes bare ANY, and the allowedFunctionNames field that functionCallingConfig provides for exactly this case is never set. ANY means "must call some declared function", not "must call this function": a client that required get_weather can be answered with a call to any other declared function (in the probe: send_email).

The string forms are fine ("none" → NONE, "auto" → AUTO, "required" → ANY is the correct semantic). Only the object form loses information.

Evidence (verified this run, real module)

Laptop probe (~/git/fleet/probe-2026-10-30-laneA-antigrav-body/; bin/antigravity-direct.mjs md5 de11cfa24df8200d66fd788422667ef8 identical on sandbox main df9381ff and the laptop; stub secret-tool + stub upstream recording every payload — no real keyring read, no real Google call). Sent:

{
  "model": "gemini-3.8-flash",
  "messages": [{ "role": "user", "content": "weather?" }],
  "tools": [
    { "type": "function", "function": { "name": "get_weather", "parameters": { "type": "object", "properties": {} } } },
    { "type": "function", "function": { "name": "send_email",  "parameters": { "type": "object", "properties": {} } } }
  ],
  "tool_choice": { "type": "function", "function": { "name": "get_weather" } }
}

The recorded upstream streamGenerateContent payload carried both declarations but:

"toolConfig": { "functionCallingConfig": { "mode": "ANY" } }

— no allowedFunctionNames, nothing naming get_weather. The response to the client was a normal HTTP 200, so the downgrade is silent.

Why it matters

Callers use a specific-function tool_choice as a control-flow guarantee (structured extraction, a router that must invoke exactly one named tool, a safety gate that must not fire side-effecting tools). Through this door the guarantee silently degrades to "some function": the model may invoke a different — possibly side-effecting — declared function, and the client, which never opted into that, will execute it under the assumption its choice was honoured. The fleet's own pattern already preserves the choice end-to-end: bin/muse-proxy.mjs:1161-1164 maps the object form through ({type:'function', name}), pinned by bin/muse-proxy.test.mjs:598,648.

Acceptance criteria

  • A specific-function tool_choice ({type:"function", function:{name:"X"}}) produces an upstream functionCallingConfig that restricts calling to X (e.g. mode: "ANY" with allowedFunctionNames: ["X"] — triage pins the exact upstream shape), where X is one of the declared functions.
  • String forms are unchanged: "none" → NONE, "auto" → AUTO, "required" → ANY (no allowed list).
  • A tool_choice naming a function that was not declared does not silently widen to unrestricted ANY — it is rejected with a 400 or handled by an explicit, documented fallback (triage pins which).
  • Regression tests in a new bin/antigravity-direct.test.mjs (stub-upstream payload recording, in the style of bin/muse-proxy.test.mjs:598) cover the specific-function case and the three string forms; requests without tool_choice send no toolConfig, as today.

Dedup notes: searched open + closed issues this run for tool_choice / functionCallingConfig / allowedFunctionNames against antigravity — no hit. Distinct from #2821 (same file, request-body shape guard; this is a translation-fidelity bug on a well-formed object body).

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    effort/sSmall: expected under 2 hours of work
    app/antigravity-directbin/antigravity-direct.mjs direct Cloud Code door
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (lane A, new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-10-30 14:35 CDT
    • Found via: filed this run
    • Task: Triage the tool_choice translation in bin/antigravity-direct.mjs — a specific-function choice is flattened to functionCallingConfig ANY with no allowedFunctionNames — and pin the pinned-function upstream shape plus regression tests
    • Expectation: effort/s — done when a specific-function tool_choice restricts the upstream call to the named function, string forms are unchanged, and bin/antigravity-direct.test.mjs pins all forms
    • Links: probe (laptop) ~/git/fleet/probe-2026-10-30-laneA-antigrav-body/ (toolchoice case; recorded upstream payload) · sibling door antigravity-direct: a non-object JSON body is never a 400 — 'null' 500s at body.model, and scalar/array bodies fabricate an empty upstream request for model '-medium' and return 200 #2821 · fleet pattern bin/muse-proxy.mjs:1161-1164
  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  4. 10 remaining items

  5. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — picked up — Lane D

  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — PR opened, parked on CI — Lane D

  8. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 2, 2026
  9. added a commit that references this issue on Nov 2, 2026
  10. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Nov 2, 2026
  11. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — done — Lane D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/antigravity-directbin/antigravity-direct.mjs direct Cloud Code dooreffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions