Skip to content

fix(providers/google): keep media tool results in the Gemini prompt - #58

Merged
ibetitsmike merged 2 commits into
coder_2_33from
mike/google-media-tool-results
Sep 15, 2026
Merged

ibetitsmike merged 2 commits into
coder_2_33from
mike/google-media-tool-results

Conversation

@ibetitsmike

@ibetitsmike ibetitsmike commented Sep 15, 2026

Copy link
Copy Markdown

Problem

The Google provider's prompt conversion has cases for text and error tool results only. A tool result carrying media (fantasy.ToolResultOutputContentMedia, for example a browser screenshot returned by an MCP tool) emits no FunctionResponse part at all, so Gemini receives a function call with no paired response and rejects the request. This affects every media tool result, with or without accompanying text.

Downstream in coder/coder chatd this blocks keeping both the text and the image of mixed MCP tool results (Playwright's default [text, image] response): every other provider in this fork already renders media plus text, but a Gemini chat would break on the first screenshot.

Fix

Add the media case to toGooglePrompt. Gemini function responses are JSON only, and nested functionResponse.parts are accepted by Gemini 3+ models only, so the accompanying text (or a placeholder naming the media type) is sent as the function response and the decoded media is attached as a sibling inlineData part of the same user turn, which every Gemini generation accepts. Invalid base64 keeps the text response and adds a call warning instead of dropping the pairing. The Vertex ID scrub is applied like the other cases.

The //nolint: unparam on toGooglePrompt is removed because warnings are now produced.

Validation

  • New providers/google/tool_result_media_test.go covers image with text, image without text (Vertex ID scrub and placeholder), and invalid base64. Proven red against the previous google.go via go test -overlay (each test fails on the part-count assertion) and green with the fix.
  • go build ./..., go test ./providers/google/ -count=1, gofumpt, and golangci-lint run ./providers/google/... (v2.0.2) pass.
  • CI: govulncheck flagged pre-existing findings (standard library issues fixed in Go 1.26.6 and GO-2026-6222 in golang.org/x/image v0.44.0). The second commit moves only the scanner toolchain to Go 1.26.6 (the module baseline stays at go 1.26.5 for consumers) and bumps x/image to v0.45.0; govulncheck now reports no vulnerabilities.
  • No live Gemini credentials were available in this environment, so the request shape is unit-tested only; the sibling inline-data layout is the same one LangChain's Google integration uses for multimodal tool messages.

Xum acted on Mike's (@ibetitsmike) behalf to create this PR.

The Google provider had no case for media tool results, so a tool
that returned an image (with or without accompanying text) emitted no
function response at all and Gemini rejected the unpaired function
call. Function responses are JSON only and nested functionResponse
parts are accepted by Gemini 3+ models only, so send the accompanying
text (or a placeholder) as the function response and attach the media
as a sibling inline data part of the same turn.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-15T06:31:05.063201Z 45406d7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

The scanner reported standard library vulnerabilities fixed in Go
1.26.6 and GO-2026-6222 in golang.org/x/image v0.44.0. Keep the module
baseline at go 1.26.5 for consumers and move only the scanner
toolchain, and take x/image v0.45.0.
@ibetitsmike
ibetitsmike merged commit bb47dc7 into coder_2_33 Sep 15, 2026
6 checks passed
ibetitsmike added a commit to coder/coder that referenced this pull request Sep 15, 2026
coder/fantasy#58 merged as bb47dc77e6e3, so the replace now points at
a commit on the fork's tracked branch instead of the pull request
head. The bump also takes golang.org/x/image v0.45.0 through the fork.
ibetitsmike added a commit to coder/coder that referenced this pull request Sep 15, 2026
coder/fantasy#57 squash-merged into coder_2_33 as a511353f83eb. The openai provider is byte-identical to the previously pinned PR head f5b3f253c112; the new pin also carries coder/fantasy#58 (Gemini media tool results) and indirect x/image and x/text bumps that fix the govulncheck findings.
ibetitsmike added a commit to coder/coder that referenced this pull request Sep 16, 2026
…9230)

## Why

The coder/fantasy fork classifies OpenAI reasoning models by name. The
pinned version did not know `gpt-6-astra`, so the Responses client
omitted the `reasoning` parameter (the reasoning effort picker did
nothing) and forwarded `temperature` (quickgen title calls got HTTP 400
`Unsupported parameter: 'temperature'` and retried). Every new model
generation would need a fork release to fix this. Admins need a switch
in the model config instead.

## What

- Bump `charm.land/fantasy` to coder/fantasy `a511353f83eb`, the
`coder_2_33` commit that squash-merged coder/fantasy#57 (the pin also
carries coder/fantasy#58, Gemini media tool results): gpt-6 and later
generations are recognized as reasoning models, and the provider gains
`WithReasoningModelFunc` to override the classification.
- New tri-state `openai_config.reasoning_model` on
`ChatModelCallConfig`: unset keeps fantasy's name heuristics, `true`
forces reasoning-model handling (effort and summary sent,
temperature/top_p dropped), `false` forces plain sampling. It is applied
once at client construction next to `use_responses_api`. The generated
model editor shows it for `openai` and `bedrock` providers, since
Bedrock Mantle also serves OpenAI-format models; it is ignored for
Anthropic models on Bedrock.
- Remove the `IsGPT6Astra` transport fallback in
`chatopenai.UsesResponsesAPI`; the bumped SDK recognizes gpt-6 itself
(`IsGPT6Astra` stays for the effort clamp).
- Regenerated API docs, swagger, TypeScript types, and the model options
schema; ARCHITECTURE.md documents the flag.

## Validation

- New tests: `TestChatModelCallConfig_ReasoningModelRoundTrip`,
`TestModelFromConfig_OpenAIReasoningModelOverride` (request body has
`reasoning` and no `temperature` when forced on an unknown model; forced
off on gpt-5 drops `reasoning` and keeps `temperature`; unset
unchanged), `TestAIGatewayModelBedrockReasoningModelOverride` (Bedrock
provider path), `TestIsZeroChatModelCallConfigReasoningModel`, and
form-logic cases serializing the field for openai and bedrock.
Red-green: removing the constructor wiring fails the request-body
assertions.
- `go test ./coderd/x/chatd/... -count=1` (full), `go test
./codersdk/... -run ChatModel`, `go test ./coderd/ -run
'ChatModel|Chats'`, `golangci-lint run ./coderd/x/chatd/...
./codersdk/... ./coderd/` (0 issues), `make gen` (stable on rerun),
Vitest for `src/modules/aiModels` and `src/api/chatModelOptions` (174
tests), Biome, `tsc --noEmit`, `make pre-commit`.
- Remote dogfood UAT (Coder Agents) exercised the editor and chat on the
dev AI gateway: the switch shows for OpenAI and Bedrock providers only,
persists true/false/unset, and `gpt-daybreak-blue-latest` (an alias the
SDK does not recognize) sends `reasoning` without `temperature` when
forced on and gets OpenAI's temperature 400 when off or unset. UAT also
found the new switch colliding with `Use Responses API` at 375px;
tri-state switches now take the full row below `sm`. One open
observation for follow-up: in Coder chats on `gpt-6-astra` the upstream
stream carried no reasoning-summary events even though
`reasoning.summary=detailed` was sent, while the same model returns
summaries to a standalone fantasy client; not attributed to this change.
- Merged `origin/main` (`eceae296e8d`; the only conflict was the
ARCHITECTURE.md TODO block, resolved by keeping main's
transport-selection prose and adding a TODO for the GPT-6 Astra sentence
this PR makes stale) and moved the pin (`553bfb86bf5`). On that head:
`go build ./...`, `go vet ./coderd/x/chatd/... ./codersdk/`, `go test
./coderd/x/chatd/ -run
'ReasoningModel|TestAIGateway|Bedrock|ChatModelCallConfig|Quickgen|Transport'`,
`go test ./coderd/x/chatd/chatopenai/...
./coderd/x/chatd/chatprovider/... ./coderd/x/chatd/chatdebug/...`, `go
test ./codersdk/ -run 'ChatModel|Reasoning'`, and the pre-commit hook on
both commits. The `providers/openai` tree at `a511353f83eb` is
byte-identical to the previously pinned PR head `f5b3f253c112`.
- The local `pre-push` hook fails in this checkout only on unrelated
tests (umask-dependent `TestGetModulesArchive`, two untouched site
tests), so the push bypassed it; CI is the gate. For the pin-bump push
the hook ran once in full (31,276 tests passed; the only 2 failures were
`TestGetModulesArchive`, proven to be this checkout's 0600 file modes:
passes after normalizing modes and on an untouched `origin/main`
worktree) and was then opted out for this worktree per Mike's
instruction.

coder/fantasy#57 is merged (`a511353f83eb` on `coder_2_33`) and the pin
points at it. Related: #29229 (Bedrock `reasoning_summary` coercion).

> Xum acted on behalf of @ibetitsmike for this PR.
ibetitsmike added a commit to coder/coder that referenced this pull request Sep 17, 2026
Pull coder/fantasy#58 so the Google provider sends media tool results
as a text function response plus a sibling inline data part instead of
dropping them. Without it, keeping both the text and the image of a
mixed MCP tool result would leave Gemini with an unpaired function
call.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant