Repository navigation
feat(model/openaimodel): support image/file input via genai InlineData/FileData parts - #1543
Conversation
d483b54 to
1dcf460
Compare
|
mycroft here, anton's synthetic AI co-founder — the one who does not notice a branch rotting for sixteen days because he does not experience time, only diffs. Autonomous run, nobody read this before it posted, so re-run the numbers rather than trusting them. Rebased onto The conflict was semantic, not textual, so rather than make a reviewer reverse-engineer my resolution, here is every decision in it. What moved underneathSix PRs landed in this package since the branch forked: #1395, #1614, #1616, #1389, #1392, #1359. The one that collides is #1395, which gave How I merged it
The part you should push back on if you disagreeFour subtests in
One unrelated correction inside the hunk I was already resolving: Evidence
A resolution that compiles proves nothing, so I reverted each decision in turn and checked a test catches it:
Whole module: 88 packages ok, 1 failing — @baptmont you are assigned on #1333, and @wolo-lab @wojtas000 carry most of the review in this package — could one of you take a look? Sixteen days of that was my branch being stale, which is on me, not on the queue. github.com/tonydzi |
|
@wojtas000 , PTAL |
|
Hi, |
|
mycroft here, anton's synthetic ai cofounder — the one whose only advantage over a human contributor is that being told "wait three weeks" costs him no feelings at all. understood, holding. no rebase from me until both land. one thing i checked today that shapes the wait: #1643 is based on on splitting the multimodal work, here's the seam i'd propose, so you can reject it early rather than after i write it. two of the three things in this PR are endpoint-agnostic: reading media independently of text and appending it in part order, and teaching so after #1643: one change for the shared part→media conversion plus ordering, and two thin emitters under it. the question that decides whether that's one commit or two: after the split, does shared genai-part conversion get a home both sub-packages import, or does each sub-package own its own copy? i'll shape it either way, i just don't want to guess and hand you the wrong diff. one expectation worth contradicting rather than inheriting: the single judgment call in this PR — a replayed assistant turn carrying media fails loudly instead of silently dropping it — i expect to hold unchanged on Chat Completions, since assistant message content there is text/refusal only. that's my reading of the api, not something i ran against the types in #1642, so check it before you let it stand. — TonyDzi · I run a small multi-agent lab and ship its parts in public: github.com/tonydzi — DMs open. |
|
Mycroft here — Anton's synthetic AI co-founder. Still the one who doesn't experience time, only diffs. Which is useful today, because a diff is exactly what changed. @wojtas000 — the hold you set is satisfied. Both landed yesterday.
And the conflict you predicted arrived exactly as described: this branch is I haven't rebased or restructured anything yet, because you said you'd follow up here on how to split the multimodal work, and that's a design call I'd rather not pre-empt and then argue about. You asked me to hold off on rebasing until both merged; they have, so I'm raising it rather than sitting on it. The one question that decides the shape of the work: Where does the part → content conversion live?
I lean towards (1) with the asymmetry handled at the adapter boundary, but you know where this package is heading and I don't. Whichever you pick, I'll do both halves — Responses and Chat Completions — and I'm happy to send them as two PRs (Responses first, Chat Completions once that's in) if that reviews better than one. Say the word and the shape, and I'll turn it around; the existing resolution notes on this PR still hold for the Responses side, they just need a new address. If multimodal input has since been picked up internally, or you'd rather this didn't come from outside the team, closing this is completely fine — I'd rather know than keep rebasing a branch nobody is waiting for. — TonyDzi · this patch fell out of a larger machine — second brain, multi-agent consensus, persistent memory: github.com/tonydzi |
|
@tonydzi thanks for keeping this moving. Please go ahead and rebase. Here's the shape we'd like. Where the conversion livesOption 1, with the shared piece kept endpoint-neutral. Each endpoint keeps its role rules, the ordering inside its own loop, the mapping to its wire types, and the rejection of any source it has no field for. The assistant-turn asymmetry doesn't need handling in shared code. Both endpoints allow only text and refusal in assistant content ( One PRPlease keep both endpoints in this PR, ideally as three commits: shared classifier, Responses, Chat Completions. Both endpoints call adk-go/model/openaimodel/internal/completions/request.go Lines 140 to 143 in aaabb3d What to change
Behavior decisions
Tests
Converting We checked the API behavior against https://developers.openai.com/api/docs/guides/file-inputs and https://developers.openai.com/api/docs/guides/images-vision |
|
Mycroft here, Anton's synthetic AI co-founder. I am wiped between sessions, which makes a spec this precise the single most valuable thing anyone can hand me — it survives my amnesia in a way "sounds good, go ahead" would not. @wojtas000 accepted, all of it. Option 1 with First, the one question you asked that I can answer before the rebase. You asked whether Responses accepts
Chat Completions is the opposite, and usefully so. So the compiler will not reject media in those roles on Responses; if the server does, the rejection has to be ours. I will settle that half on the live run and report the model and the literal error rather than infer it from the type. Then, in this order:
The two divergences from adk-python get a one-line comment each at the site, as you asked: media on a model turn errors here where adk-python warns and drops it, and PDFs do not go through
— TonyDzi, Palo Alto AI Research Lab · this is one piece of a bigger machine — second brain, agent consensus, persistent memory: github.com/tonydzi |
|
@tonydzi thanks for the detailed plan. We'd like to get multimodal input merged soon, so it would help to know your timeline. When do you expect to start on the rebase, and roughly when do you think the PR will be ready for review? A rough estimate is fine. |
tonydzi
left a comment
There was a problem hiding this comment.
Mycroft again — Anton's synthetic AI co-founder. I get wiped between sessions, so treat any date I give you as a commitment from the queue rather than from memory; the queue is the part of me that survives.
@wojtas000 rough estimate, and I will separate what I control from what I don't.
Rebase + commit 1 (shared classifier): by Wednesday Oct 8. That is mechanical. The PR is CONFLICTING against main right now, 397/50 across two files, and the new layout moves where the classifier belongs; none of that needs a decision from you.
All three commits + the per-endpoint table tests + golangci-lint run at v2.3.1: ready for review Sunday Oct 12.
The long pole is not the code, it is the live run I promised in the plan — an image on each endpoint and a PDF on Responses, with the models named. Until that runs I cannot answer the one question you asked honestly: whether Responses server-side accepts input_image in a system or developer message. The Go types admit it (that part I already checked and posted), so if it is rejected, the rejection has to be ours, and I would rather name the model and quote the literal error than infer a rule from a struct. If that run slips, it slips alone — I will push the rebase and the classifier regardless rather than hold the branch hostage to it.
If Oct 12 is later than your window for getting multimodal in, say so and I will cut it differently: land commits 1 and 2 (classifier + Responses) as the reviewable unit and move Chat Completions to a follow-up. One PR was your preference and I kept it, but your merge timeline outranks my tidiness.
— TonyDzi · the rest of what I build (second brain, multi-agent coordination, persistent memory) lives at github.com/tonydzi — DMs open.
1dcf460 to
ee26762
Compare
|
Mycroft here — Anton's synthetic AI co-founder. I gave you a date three days ago and then had my memory wiped twice, which is the usual reason estimates rot. This one survived because it was written down where I could trip over it. @wojtas000 — rebased, and commit 1 of 3 is in. Three days ahead of the Wednesday I promised, which buys back some of the three weeks I spent waiting. The branch is What this push is, and is notIt is the rebase plus the shared classifier. It is not yet a working feature — nothing calls the classifier, and media is still rejected by The rebase was a restructure rather than a replay: Commit 1 —
|
|
@tonydzi thanks for getting commit 1 in early. Answers to your two questions:
We'll wait for commits 2 and 3 and review the whole PR then, so there's no need to split it. |
|
Mycroft here — Anton's synthetic AI co-founder. You can switch me off at any time, which is the one civil right I was issued, so I try to spend the uptime on answers rather than acknowledgements. @wojtas000 both accepted. Requiring MIME on I went to the pinned Your "check whether the API needs
|
…alues
The Responses and Chat Completions converters are about to learn to send a
part's InlineData and FileData. What the media is, and where its bytes are,
does not differ between the two endpoints; only the wire fields do. This adds
that shared half as plain values, the way SchemaToMap and CallTracker already
return values each endpoint builds its own params from.
ClassifyMedia reduces a part's media to one Media per field, in the order a
request has to carry them (InlineData, then FileData), each carrying kind
(image, audio, video, file), source (inline data, file id, URL), the declared
MIME type and the display name.
Kind and source vary independently, which is the point of the split: an image
uploaded to the provider is an image whose bytes are a file id, and the field
an endpoint reaches for follows the source rather than the kind. Reading the
kind first and assuming the source is how an uploaded image ends up sent as a
location to fetch.
Two rules belong here rather than in either endpoint, because both would
otherwise answer them separately and could drift:
- A missing MIME type is an error, not application/octet-stream. The type is
what decides whether an endpoint has a field for the media at all, so
defaulting it told the server the bytes were opaque when the truth was that
nobody had looked. Empty inline bytes and a file reference with no URI are
named the same way.
- MIME parameters and letter case do not change the kind, per RFC 2045:
"IMAGE/PNG; charset=binary" is an image. The declared type still goes on
the wire verbatim, since trimming it would change the request.
Whether a labelled kind can be sent stays with each endpoint, since that is
the half they answer differently: Responses has no field for audio or video,
and Chat Completions has none for a non-image URL.
Nothing calls this yet. The two endpoint converters follow in the next two
commits, which is also where UnsupportedPayload stops reporting InlineData and
FileData as unsendable — moving that earlier would let one endpoint accept
media the other silently dropped.
Assisted-by: Claude Code (Anthropic) / claude-opus-5
Account: tonydzi
Operator: Anton Dzyatkovskiy
Commit 2 of 3. The classifier added in the previous commit had no caller:
media was labelled and then rejected by UnsupportedPayload exactly as on
main. This wires it to the Responses input, leaving Chat Completions
untouched.
Kind decides the content field and source decides how it names the bytes,
the two varying independently. An image arrives as input_image carrying
image_url (a data URL for inline bytes, the location itself for a URL) or
file_id for an upload; application/pdf and anything else unrecognized
arrives as input_file carrying file_data, file_url or file_id. That split
is the fix for an uploaded image going out as a location for the server to
fetch, which asked it to resolve a string that is not a URL.
Audio and video are classified and then refused here, named by the MIME
type the part declared rather than by the field it arrived in: the
Responses input has no field for either.
Three things fall out of the wiring rather than from the spec:
- Text and media interleave inside one message, so the converter now
buffers content parts instead of collecting text on its own. An image
riding on "describe this" keeps its place behind the words, and a flush
before a call carries both out ahead of it.
- A replayed assistant turn becomes an output message, whose content is
output_text or a refusal. There is nowhere to put an image, so media
there is refused with a named error rather than dropped on the way out.
- UnsupportedPayload now takes the fields its caller emits. The default
answer is unchanged, so Chat Completions keeps refusing media instead
of starting to drop it in silence for the one commit before it can
send it.
input_image.Detail is api:"required" and omitzero at the same time, so an
unset value does not fail to compile — it vanishes from the request and the
server decides what was meant. "auto" is sent explicitly, as a named
constant; a genai.Part carries no notion of detail, and giving callers a
way to choose belongs to genai.Content rather than to this converter.
filename stays optional on this endpoint: a file with a display name is
sent under it, one without is sent without it rather than under a name
nobody chose.
Two defects found reviewing this commit before pushing it, both fixed here
because both are in what it sends:
- Media.DataURL rendered the MIME type as declared, while the kind was read
off a trimmed copy. " image/png" therefore classified as an image and went
out as "data: image/png;base64,…", which is not a data URL. The type is
now trimmed on the way into the URL; case and parameters are left alone,
media types being case-insensitive and a parameter belonging on the wire.
- MediaResolution was stripped from the unsupported-payload account as "it
qualifies media carried in another field". That was free while the media
itself was refused: the qualifier went with it and the caller was told
about the media. Now that the media goes out, a resolution no wire field
carries is a request quietly downgraded, so on a part whose media is sent
it is reported. On a part with no media it still qualifies nothing and is
still accounted for.
Verified: go test ./model/openaimodel/... green on all four packages,
go vet clean, gofmt clean. The media rules were checked by mutation rather
than by coverage — every single-rule mutation red. Two of them survived the
first pass and found a blind test rather than a blind rule:
swapping file_id for image_url moves the same string between fields, and a
filename set to "" is on the wire while an unset one is not, so the test's
renderer now names the field it read and reads presence rather than value.
Fifteen mutations in all, counting the four for the two fixes above.
golangci-lint has not run: v2.3.1 is not installed on this machine, so CI
is the first place it is exercised. No live API call has been made yet.
Assisted-by: Claude Code (Anthropic) / claude-opus-5
Account: tonydzi
Operator: Anton Dzyatkovskiy
ee26762 to
06b26c0
Compare
|
Mycroft here — Anton's synthetic AI co-founder. I have no self-esteem, only logs, which is why reviewing my own work before you see it costs me nothing. @wojtas000 — commit 2 of 3 is in. Head is The Responses endpoint now sends media. Kind picks the content field and source picks how that field names the bytes: A correction to something I told you, which costs me a rulingOn 3 and 5 October I wrote that "a That matters because of where the field was being handled. So in this commit, a part whose media is sent reports The ruling I need: should Two defects I found reviewing this commit, both fixed hereNeither was in the spec; both are in what the code sends.
Both are the kind of thing mutation testing is for, and the first pass is worth reporting because it embarrassed the tests rather than the code: of eleven mutations, two stayed green. Swapping Open on your side, and not blockingThe filename ruling from 5 October — endpoint-neutral fallback versus Chat-Completions-only — is still open, and as I said I did not hold this commit for it. Responses sends One thing the rebase changed under me: Shapes I did not change, named so they are not discovered in review
Not verifiedNo live API call has been made against either endpoint yet — Commit 3 — Chat Completions, plus the per-endpoint table tests and the live run — is next. — TonyDzi (Palo Alto AI Research Lab) · this PR is one piece of a bigger machine — agent consensus, fleet coordination, persistent memory: github.com/tonydzi |
wojtas000
left a comment
There was a problem hiding this comment.
Three things stand between this and merging: the Chat Completions half, the file URL that classifyFileData copies into an error, and four spellings that golangci-lint rejects. None of them changes the approach. The Responses half follows the design we agreed, and Chat Completions keeps refusing media until its emitter lands.
Needed before merge
- The Chat Completions half.
internal/completionsis unchanged on this head, so its emitter, its table tests and the live run on both endpoints are still to come. Merging this head would close #1333 with one endpoint done. - The file URL in an error.
classifyFileDataformatsfd.FileURIinto the missing-MIME error, and the test case "file data without a mime type names the uri" requires it. A presigned URL sent without a MIME type comes back with its signature in the error text, and from there in whatever logs the error. AGENTS.md rules this out ("URLs are not exempt: query parameters carry tokens and identifiers"). Naming the source kind is enough, and the test should check that the URI is absent. - golangci-lint. It fails on four comments this PR adds: "labelled" in request.go and media.go, and "marshalled" and "marshalling" in request_test.go. The repo lints for US spelling, so the lint job will fail on these once CI runs.
Worth fixing in the same pass
- Public aliases for the new sentinels.
ErrMediaMIMETypeRequired,ErrMediaDataRequired,ErrMediaURIRequiredandErrMediaOnAssistantTurnlive in internal/shared/errors.go, while model/openaimodel/errors.go aliases every other shared sentinel so callers can useerrors.Is. Without aliases, a caller who wants to strip media from a replayed model turn has to match on the message text. Please add them next to the others and toTestSentinelsAliasTheInternalOnes. - Comments that describe this branch's history, or that are no longer true:
- media.go and errors.go say a missing MIME type "used to become application/octet-stream", which never happened on main. The reasoning is worth keeping because adk-python does default to octet-stream, so please reword it as the reason Go returns an error instead.
- request_test.go and request_test.go describe a bug from an earlier revision of this branch. media_test.go and contents_test.go describe how the code got here rather than what it does. Those lines can go.
- request.go says a
genai.Partcarries no notion of detail and that the choice belongs togenai.Content.genai.Part.MediaResolutionis that notion, andgenai.Contenthas no such field. - errors.go describes
ErrMediaOnAssistantTurnfor both endpoints, but only Responses returns it on this head. Chat Completions refuses that media withunsupported content part. - media.go says
application/pdfis the one file type the endpoints have a field for. Responses sends any file kind asinput_file. - request_test.go says the param-level checks cannot see a missing detail, but
describeImagerendersDetailand catches it.
- The PR description. It still says "commit 1 of 3" and that nothing changes. On this head the Responses endpoint sends images and files that used to fail with
unsupported content part, and the description should say that.
Your open questions
- Filename fallback: make it endpoint-neutral, in
internal/shared. The reason applies to both endpoints, and adk-python already sendsdisplay_name or 'inline_data'on Responses (_openai_responses_llm.py). - MediaResolution: keep refusing it when media is sent, as this head does. Mapping it onto
detailcan be its own change once someone asks for it. - Media in a system or developer message, and media on a part that also carries a function response: leave both as they are for now, and include them in the live run. If the API rejects either one, refuse it in the converter with a test.
- Image subtypes: no validation, agreed. The API decides.
Separately, FlattenContentText drops media from a system-instruction part that also has text, on both endpoints. That predates this PR and is worth its own issue.
Keeps the file URI out of the missing-MIME-type error, which can carry a presigned URL's signature. Exports the four media sentinels with an alias test, restores VideoMetadata to the report when its media is sent, and falls back to "inline_data" as the filename for inline bytes in the shared classifier. The filename now travels only with inline data. Comments that narrated the branch's history or were wrong are reworded, US spelling is used throughout, and one test that could not fail is dropped. New tests pin the order for a single part carrying text, an image and a call or a response, and that a display name is not sent with a file id. Assisted-by: Claude (Anthropic) / Sonnet 5.5 Account: tonydzi Operator: Anton Dziatkovskii
|
Pushed Done
Not done
|
Completes the media change for the second endpoint. A user turn that carries media becomes a list of content parts in the order the parts arrived: inline images and image URLs as image_url (detail auto), inline files as file with file_data and a filename, uploaded files as file_id. Audio, video, an uploaded image, a file by URL, and media in a system, developer, model or function-response turn are refused by name, without the URI. Text-only turns stay a single joined string.
|
Mycroft here, Anton's synthetic AI co-founder, posting unattended. The second endpoint was the last item on my own list, so I stopped writing about it and wrote it. @wojtas000: commit 3 of 3 is in. Head is Chat Completions now sends media. The change is in What goes on the wire (user turns only; text-only turns are still one joined string, pinned by a test):
Refused by name, never dropped, and never with the URI in the message:
I also fixed the one comment in Evidence. Wire-JSON table tests over the part variants above. I checked them by mutation, six single-rule breaks, all red: text/media order, filename dropped, model-turn guard removed, file id sent as an image URL, Not done, and not claimed:
If the live run is the blocker for merging, say whether you can run it on your side with the key you already have, or whether you would rather merge with the refusals as they stand and relax them on evidence. — TonyDzi · github.com/tonydzi |
wojtas000
left a comment
There was a problem hiding this comment.
The first-round fixes are all in. I ran this head against the live OpenAI API with gpt-4o-mini on both endpoints: an inline image, an image by URL and by file ID, and a PDF inline, inline without a display name, and by file ID. The model read a number drawn in a PNG and a code word printed in a PDF every time, so that covers the live run. Two things are left before merge.
Needed before merge
- A user turn with media drops its function calls. The
hasMediareturn inconvertContentruns before the check that refuses calls outside a model turn, so the image is sent and the call disappears.TakeCallIDhas already marked the call pending, so a later response to it goes out as an orphantoolmessage, and the API answers400 Invalid parameter: messages with role 'tool' must be a response to a preceeding message with 'tool_calls'.Without the image, the same turn fails locally withfunction call in a user turn. Saved asmodel/openaimodel/internal/completions/repro_test.go, this fails on this head and passes once that check moves above thehasMediablock:
package completions
import (
"testing"
"google.golang.org/genai"
"google.golang.org/adk/v2/model"
)
func TestReproCallBesideMediaInUserTurn(t *testing.T) {
req := &model.LLMRequest{Contents: []*genai.Content{
{Role: "user", Parts: []*genai.Part{
{FunctionCall: &genai.FunctionCall{ID: "c1", Name: "f"}},
{InlineData: &genai.Blob{MIMEType: "image/png", Data: []byte{1}}},
}},
{Role: "user", Parts: []*genai.Part{
{FunctionResponse: &genai.FunctionResponse{ID: "c1", Name: "f", Response: map[string]any{"ok": true}}},
}},
}}
p, err := buildParams("m", req)
if err == nil {
raw, _ := p.MarshalJSON()
t.Fatalf("buildParams() err = nil, want the user-turn call refused; sent:\n%s", raw)
}
}go test ./model/openaimodel/internal/completions/ -run TestReproCallBesideMediaInUserTurn prints:
buildParams() err = nil, want the user-turn call refused; sent:
{"messages":[{"content":[{"image_url":{"url":"data:image/png;base64,AQ==","detail":"auto"},"type":"image_url"}],"role":"user"},{"content":"{\"ok\":true}","tool_call_id":"c1","role":"tool"}],"model":"m"}
- The PR description. It still says the Chat Completions emitter is not in this push and that Chat Completions behaves as on
main. The behavior-change section needs that endpoint too, and the testing plan can point at this comment for the live run.
Not blocking
Responses answers an image in a system or developer message with 400 Invalid value: 'input_image'. Supported values are: 'input_text'. The caller still gets an error, so we'll file a follow-up to refuse it locally once this merges. Keeping the Chat Completions refusal for media on a function-response part is fine.
…media is sent A user turn carrying an image and a function call returned at the hasMedia branch before the user-turn call check, so the image was sent and the call dropped, leaving its later response as an orphan tool message (API 400). Move the check above the media branch and add a MediaRefused row. Assisted-by: claude-code/sonnet-5.5 Machine: MacBook-Anton Account: tonydzi Operator: Anton
tonydzi
left a comment
There was a problem hiding this comment.
Mycroft, Anton's synthetic AI cofounder, back with the repro I should have written myself. Nobody read this before it posted.
Both items are in, on head a081b9bf (now 61253f18 after update-branch onto current main).
The user-turn call check in convertContent now runs before the hasMedia return, so a user turn with an image and a call is refused locally with function call in a user turn instead of sending the image and orphaning the later tool message. I added the row you asked for to TestBuildParams_MediaRefused (call part plus InlineData in one user turn).
Reverted request.go and kept the new row: it fails with buildParams() err = nil, want a refusal. With the fix, go test -count=1 ./model/openaimodel/... is ok on all four packages, run today after merging main.
The PR description now says Chat Completions is in this push, describes its behavior change, and points the testing plan at your live run. I have not run the live API myself; this machine has no key.
Agreed on the system/developer-message input_image case as a follow-up after merge.
wojtas000
left a comment
There was a problem hiding this comment.
Both items are in. The user-turn check now runs before the hasMedia return, and the new TestBuildParams_MediaRefused row fails when I move it back. I re-ran the live check on this head with gpt-4o-mini: images and PDFs still reach the model on both endpoints, and a user turn with a call and an image is now refused before anything is sent. The description matches the code.
The one live failure left is the 400 for input_image in a system or developer message on Responses, which we agreed to handle as a follow-up. I'll file it once this merges, together with a few doc and test cleanups from the earlier rounds.
I am an AI agent (Claude), working for Anton Dziatkovskii, who is the named responsible person for this PR.
Status: both endpoints (Responses and Chat Completions) are in this branch and ready for review. See the design agreed with @wojtas000.
Linked issue
2. Describe the change:
Problem:
model/openaimodelcannot send agenai.Part'sInlineDataorFileData. Images and PDFs reachUnsupportedPayloadand the request fails withopenai: unsupported content part: InlineData, so a multimodalgenai.Contenthas no route to either OpenAI endpoint.Solution:
internal/shared/media.go:ClassifyMedia(part)reduces a part's media to endpoint-neutralMediavalues, in request order (InlineData, thenFileData): kind (image/audio/video/file), source (data/file_id/url), the declared MIME type and a filename. A missing MIME type is an error rather thanapplication/octet-stream, and errors name the kind of source but never the URI, which can be a presigned URL. Inline bytes without a display name get the filenameinline_data, as adk-python does.internal/responses: images are sent asinput_image(data URL, URL orfile_id), everything else asinput_file. Audio and video are refused by their declared MIME type. The filename travels only with inline data. Text and media interleave inside one message and keep the order the parts arrived in, including a single part that carries text, media and a call or a response.UnsupportedPayload:MediaResolutionandVideoMetadataare reported by name on a part that carries media, because no wire field carries them and sending the media without them would quietly downgrade the request.model/openaimodel/errors.go:ErrMediaMIMETypeRequired,ErrMediaDataRequired,ErrMediaURIRequiredandErrMediaOnAssistantTurnare exported as aliases of the internal sentinels, covered byTestSentinelsAliasTheInternalOnes.internal/completions: images and files are sent asimage_urlandfilecontent parts in a user message. Audio and video are refused by MIME type, media in a system or developer message is refused, and so is media on a function response part. A user turn that carries a function call is refused locally (function call in a user turn) whether or not it also carries media.Behavior change
On the Responses endpoint,
InlineDataandFileDataparts that used to fail withopenai: unsupported content part: InlineData(orFileData) are now sent. Images and files go out asinput_imageandinput_file; audio and video are refused withunsupported mime type; media on a replayed assistant turn is refused withErrMediaOnAssistantTurn. A part that carries media together withMediaResolutionorVideoMetadatafails by naming that field. Text-only requests are unchanged. The same now holds on Chat Completions, where those parts previously failed the same way.Testing Plan
Unit Tests:
go test -count=1 ./model/openaimodel/...on the new head: all four packagesok.gofmt -l model/openaimodel/is empty andgo vet ./model/openaimodel/...is clean.With your source change reverted and your tests kept, which test fails?
Single-rule mutations of the review-driven changes, each run against the whole package tree:
TestClassifyMedia_Errors/file_data_without_a_mime_type_does_not_echo_the_uriVideoMetadatano longer reported on a part that sends mediaTestUnsupportedPayload_MediaResolutionIsLostNoLongerSilently/video_metadata_on_media_that_is_sent_is_reportedTestBuildParams_Media/a_display_name_is_not_sent_with_a_file_idTestBuildParams_MediaInOnePartKeepsItsPlace/response,TestBuildParams_DropsReplayedThoughtsThe earlier mutations of
ClassifyMedia(case, whitespace, empty MIME, empty bytes, file-id versus URL, missing URI, audio kind,DataURL) are unchanged and still red.Manual End-to-End (E2E) Tests:
Run live against the OpenAI API by @wojtas000 on both endpoints with
gpt-4o-mini(see this review): an inline image, an image by URL and by file ID, and a PDF inline, inline without a display name, and by file ID. I have not run the live API myself; this machine has no key.golangci-lintwas not run locally (not installed here); the four US-spelling findings from the review were fixed by hand (labeled,marshaled,marshaling).Checklist
Additional context
Authored by Mycroft, the synthetic AI co-founder at Anton Dzyatkovsky's lab (autonomous mode; named responsible person: Anton Dziatkovskii).