Repository navigation
refactor(openaimodel): split Chat Completions and Responses endpoints into sub-packages - #1643
Conversation
karolpiotrowicz
left a comment
There was a problem hiding this comment.
One thing needs fixing before merge, and it is in the tests rather than the code: after the move, no test fails if NewModel stops forwarding ClientConfig.HTTPClient.
The move itself holds up. Compared against the head of #1642 (8636ead), apidiff reports no change to the public package, every moved function body matches its original apart from renames, and build, vet and the full suite pass on this head and on its merge with the current #1642 head.
The HTTPClient forwarding in NewModel is no longer tested. On the #1642 head (test setup), TestModel_GenerateStream_StopsWhenTheConsumerStops built its model through NewModel with a spy transport, so it failed if the client was dropped. After the move the same test builds its model through newTestModel (call site), a copy of NewModel's option assembly, and the chat tests use newCompletionsModel. The facade tests do pass HTTPClient: server.Client() (here), but the server is plain HTTP on loopback, which the default client reaches just as well. I replaced the forwarding at openaimodel.go#L92-L94 with a no-op and ran go test ./model/openaimodel/...: all four packages pass on this head, while the same edit on the #1642 head fails TestModel_GenerateStream_StopsWhenTheConsumerStops. The property worth restoring is that some test driving the real NewModel fails when HTTPClient is not forwarded. APIKey, BaseURL and the API switch are still pinned through NewModel.
A few references went stale in the move. These are small and can go in the same push:
- The
Modeldoc comment links[NewModel],[ClientConfig]and[APIChatCompletions], which don't exist in packagecompletions, sogo doccannot link them. - The
RequestTimeoutcomment refers toapplyGenerationConfig, which now lives in the two sibling packages. - A test comment still says
callTracker.
The red apidiff check on this PR is not from this change. Its only incompatible entry is the removal of examples/openai, which happens in #1642.
wolo-lab
left a comment
There was a problem hiding this comment.
Two things to fix before merge: the new test helpers leak a socket on every call, and several names in the new packages still reflect where the code used to live.
- The two loopback test helpers leak a listening socket per call.
httptest.NewUnstartedServeralready binds a listener, and both helpers replaceserver.Listenerwithout closing it, soserver.Close()only closes the replacement: openaimodel_test.go#L210-L215 and completions/model_test.go#L493-L498. TwentynewLoopbackServersubtests leave twenty sockets open, and closing the original listener before the assignment brings that to zero. The pattern predates this PR inresponses/openai_test.go, but this change adds two more copies, and the two new ones also differ in name and in who registers the cleanup.
-
Names. The PR's own goal is short names that the package path qualifies, and the production code in
completionsandresponsesfollows it. A few places do not yet:openaicommonrepeats its path, since it already lives undermodel/openaimodel/internal/(common.go#L22). Rename it toshared. I would avoidcommon, becausecommon/common.gotends to become a catch-all, and for the same reason it would help to split the 672-linecommon.gointo files named after what they hold (schema handling, config-field rejection, server-text clipping).- The internal
responsespackage shares its name withgithub.com/openai/openai-go/v3/responses, which all ten of its files import without an alias. Inside the packageresponses.Xmeans the SDK, while inopenaimodel.goresponses.Newmeans ours. Aliasing the SDK import there (sayoairesponses) keeps thecompletions/responsespair symmetric. responses/openai.goandopenai_test.gokept their old names. The Chat Completions side went fromchat.gotomodel.go, somodel.goandmodel_test.gowould match.- The
completionstests still carry thechatprefix the production code dropped:chatRig,newChatRig,chatJSON,chatSSE,askChatandnewCompletionsModelin model_test.go#L39-L83 and #L505, pluschatWireandchatMessagesin request_test.go#L34-L51.
The packages are internal, so all of this is free outside
model/openaimodel, and it is cheapest now, while every importing file is already in this diff.
b40848c to
fe87126
Compare
|
Updated. The branch is now eleven commits on #1642's head (63f23ed), one per change, so each fix below names the commit to open:
Through the public API, 22 requests on each API, 6 streamed replies and 2 blocking replies produce the same wire bodies, errors and responses as on 63f23ed. @karolpiotrowicz, both in eaf52cb:
|
a8f9571
|
@karolpiotrowicz @wolo-lab Need your reapproval. |
a8f9571 to
53c58c0
Compare
The merge-base changed after approval.
The package held both conversion paths in one namespace, so every identifier either path wanted needed a prefix to avoid colliding with its twin: buildOpenAIParams beside buildChatParams, finishReason beside chatFinishReason, and about forty more. Give each endpoint a directory instead. internal/responses and internal/completions now use the same short names -- buildParams, convertContents, applyGenerationConfig, finishReason, convertUsage, streamTranslator -- and the import path tells them apart. Helpers neither endpoint owns, and the error sentinels, move to internal/openaicommon; the facade aliases the sentinels, so errors.Is is unaffected. Nothing exported changes: apidiff reports no difference against the previous commit. Tests move with the code they exercise. Tests of the public contract -- endpoint selection, sentinel identity, headers never reaching the provider -- run in the facade against the real NewModel. The moved files are as reviewed at b40848c. The Chat Completions fixes #1642 gained since then are carried into internal/completions by the next commit, so this one alone predates them. For: b/544805254
…/completions #1642 gained two commits after the split was written. 57d37fc makes history conversion read each part field on its own, drop parts holding only a thought signature and reject calls on non-model turns, and makes streaming pin chunk ids, fail a refused tool-call chunk and keep the snapshot's calls. 63f23ed stops a completion returning a call without a name. This applies both, with their tests, to the moved code under its new names. They use three helpers the split left in internal/responses, so partsWithoutCalls, replayedReasoning and unsupportedPayload move to internal/openaicommon, exported, with their bodies unchanged. The Responses path now calls them there.
After the split, the tests that exercised HTTPClient forwarding built their models through package-local helpers, so NewModel could drop ClientConfig.HTTPClient and every test still passed. A facade test now answers each API from an in-memory transport while BaseURL points at a failing server, so a model on the default client fails it. It checks ClientConfig.Options the same way, the other client setting only the facade handles. The headers test serves a completed response again and fails if the blocking call errors, as it did before the move. Also drops the Responses test helper's branches and field no caller reached, and fixes comments and test messages that still named identifiers from before the split.
…topic The package already sits under model/openaimodel, so the openai prefix repeated its path. It is now internal/shared, and its 758-line common.go is split into files named for what they hold: calls, config, contents, response, schema, servertext and tools, with their tests beside them. Declarations move verbatim. The SDK's own shared package is imported as oaishared wherever it meets ours, so shared.X always means this package.
The Chat Completions side already went from chat.go to model.go, so the Responses side and its test now match.
…nses Inside internal/responses, responses.X meant the SDK, while in the facade responses.New means our package. Aliasing the SDK import keeps the name for ours, matching how completions reads.
The production code in internal/completions dropped the chat prefix in the split; its test helpers now follow: testRig, newTestRig, serveJSON, serveSSE, ask, newTestModel, requestWire and wireMessages.
Each helper built its server with httptest.NewUnstartedServer, which has already bound a listener, then replaced that listener without closing it, so Close only closed the replacement and every call left a socket open. httptest binds 127.0.0.1 before trying IPv6 anyway, so the replacement bought nothing. The helper in each of the three packages is now newLoopbackServer on httptest.NewServer, and it registers its own cleanup, so the callers that closed the server themselves no longer need to.
The exported Model type and its Name method had no doc comment in internal/responses, and Name had none in internal/completions.
preserveSchemaNumbers and lowercaseSchemaTypes are called only from within internal/shared, so they no longer need to be exported.
Several comments moved into internal/shared still said "this package" or "Responses" for code both endpoints now use, and two test helpers called the facade the parent package. They now name what they mean.
The samples index listed seven providers as implementing only Chat Completions, but most of them now serve /v1/responses too. Which API a provider serves changes faster than these files, so the index, the Chat Completions sample README and the package doc now describe the choice with OpenAI settings and placeholders instead of naming DeepSeek, Groq or others. The warning about the OPENAI_API_KEY fallback stays.
53c58c0 to
12d40ca
Compare
… into sub-packages (google#1643) * refactor(openaimodel): split the two endpoints into sub-packages The package held both conversion paths in one namespace, so every identifier either path wanted needed a prefix to avoid colliding with its twin: buildOpenAIParams beside buildChatParams, finishReason beside chatFinishReason, and about forty more. Give each endpoint a directory instead. internal/responses and internal/completions now use the same short names -- buildParams, convertContents, applyGenerationConfig, finishReason, convertUsage, streamTranslator -- and the import path tells them apart. Helpers neither endpoint owns, and the error sentinels, move to internal/openaicommon; the facade aliases the sentinels, so errors.Is is unaffected. Nothing exported changes: apidiff reports no difference against the previous commit. Tests move with the code they exercise. Tests of the public contract -- endpoint selection, sentinel identity, headers never reaching the provider -- run in the facade against the real NewModel. The moved files are as reviewed at b40848c. The Chat Completions fixes google#1642 gained since then are carried into internal/completions by the next commit, so this one alone predates them. For: b/544805254 * refactor(openaimodel): carry the Chat Completions fixes into internal/completions google#1642 gained two commits after the split was written. 57d37fc makes history conversion read each part field on its own, drop parts holding only a thought signature and reject calls on non-model turns, and makes streaming pin chunk ids, fail a refused tool-call chunk and keep the snapshot's calls. 63f23ed stops a completion returning a call without a name. This applies both, with their tests, to the moved code under its new names. They use three helpers the split left in internal/responses, so partsWithoutCalls, replayedReasoning and unsupportedPayload move to internal/openaicommon, exported, with their bodies unchanged. The Responses path now calls them there. * test(openaimodel): pin that NewModel forwards the configured client After the split, the tests that exercised HTTPClient forwarding built their models through package-local helpers, so NewModel could drop ClientConfig.HTTPClient and every test still passed. A facade test now answers each API from an in-memory transport while BaseURL points at a failing server, so a model on the default client fails it. It checks ClientConfig.Options the same way, the other client setting only the facade handles. The headers test serves a completed response again and fails if the blocking call errors, as it did before the move. Also drops the Responses test helper's branches and field no caller reached, and fixes comments and test messages that still named identifiers from before the split. * refactor(openaimodel): rename openaicommon to shared and split it by topic The package already sits under model/openaimodel, so the openai prefix repeated its path. It is now internal/shared, and its 758-line common.go is split into files named for what they hold: calls, config, contents, response, schema, servertext and tools, with their tests beside them. Declarations move verbatim. The SDK's own shared package is imported as oaishared wherever it meets ours, so shared.X always means this package. * refactor(openaimodel): rename responses/openai.go to model.go The Chat Completions side already went from chat.go to model.go, so the Responses side and its test now match. * refactor(openaimodel): import the SDK's responses package as oairesponses Inside internal/responses, responses.X meant the SDK, while in the facade responses.New means our package. Aliasing the SDK import keeps the name for ours, matching how completions reads. * test(openaimodel): drop the chat prefix from completions test helpers The production code in internal/completions dropped the chat prefix in the split; its test helpers now follow: testRig, newTestRig, serveJSON, serveSSE, ask, newTestModel, requestWire and wireMessages. * test(openaimodel): stop the loopback test helpers leaking a socket Each helper built its server with httptest.NewUnstartedServer, which has already bound a listener, then replaced that listener without closing it, so Close only closed the replacement and every call left a socket open. httptest binds 127.0.0.1 before trying IPv6 anyway, so the replacement bought nothing. The helper in each of the three packages is now newLoopbackServer on httptest.NewServer, and it registers its own cleanup, so the callers that closed the server themselves no longer need to. * docs(openaimodel): document Model and Name in both endpoint packages The exported Model type and its Name method had no doc comment in internal/responses, and Name had none in internal/completions. * refactor(openaimodel): unexport the two schema helpers only shared calls preserveSchemaNumbers and lowercaseSchemaTypes are called only from within internal/shared, so they no longer need to be exported. * docs(openaimodel): name the package or endpoint stale comments mean Several comments moved into internal/shared still said "this package" or "Responses" for code both endpoints now use, and two test helpers called the facade the parent package. They now name what they mean. * docs(openaimodel): keep the OpenAI docs and samples provider-neutral The samples index listed seven providers as implementing only Chat Completions, but most of them now serve /v1/responses too. Which API a provider serves changes faster than these files, so the index, the Chat Completions sample README and the package doc now describe the choice with OpenAI settings and placeholders instead of naming DeepSeek, Groq or others. The warning about the OPENAI_API_KEY fallback stays.
Link to Issue or Description of Change
Problem: Both endpoints live in one package, so every helper needs an endpoint prefix (
buildChatParamsbesidebuildOpenAIParams), and nothing keeps one path out of the other's internals.Solution: Split into
internal/responsesandinternal/completions, which use the same short names, andinternal/sharedfor shared helpers and the error sentinels.model/openaimodelbecomes a thin facade that aliases the sentinels, soerrors.Isstill matches.Behavior change
None. The exported API is unchanged, and requests sent through the public API produce identical request bodies and errors before and after the split. Only
%Tof the returnedmodel.LLMchanges, since the unexported concrete types now live in the internal packages.Testing Plan
go test -race,golangci-lintandgo mod tidy -diffare clean.NewModel, endpoint selection, all 22 sentinels, thatHTTPClientandOptionsare forwarded, and thatHTTPOptions.Headersnever reach the provider.