feat(js/plugins/compat-oai): add OpenAI Responses transport and non-streaming runner - #6006
feat(js/plugins/compat-oai): add OpenAI Responses transport and non-streaming runner#6006cabljac wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for the OpenAI Responses API (/v1/responses) in the compat-oai plugin, enabling integration with models served exclusively over this transport (such as gpt-5-pro and o3-pro). It refactors common utilities into utils.ts and adds comprehensive unit and live integration tests. The reviewer feedback suggests replacing generic Error instances with framework-specific GenkitError classes across several validation checks in responses.ts to ensure consistent error handling and telemetry integration.
| throw Error( | ||
| `Invalid data URL format for media: ${part.media.url.substring(0, 50)}...` | ||
| ); |
There was a problem hiding this comment.
To ensure consistent error handling and better integration with Genkit's error reporting and telemetry, please throw a GenkitError with an appropriate status code (such as INVALID_ARGUMENT) instead of a generic Error.
throw new GenkitError({
status: 'INVALID_ARGUMENT',
message: 'Invalid data URL format for media: ' + part.media.url.substring(0, 50) + '...'
});There was a problem hiding this comment.
Done in b468a84 - these throw GenkitError now (INVALID_ARGUMENT for the media/part cases, UNIMPLEMENTED for the unsupported-role branch, which only fires for the tool role deferred to the next slice).
| throw Error( | ||
| `File URLs are not supported. Only base64-encoded files and image URLs are supported. Content type: ${contentType}` | ||
| ); |
There was a problem hiding this comment.
To ensure consistent error handling and better integration with Genkit's error reporting and telemetry, please throw a GenkitError with an appropriate status code (such as INVALID_ARGUMENT) instead of a generic Error.
throw new GenkitError({
status: 'INVALID_ARGUMENT',
message: 'File URLs are not supported. Only base64-encoded files and image URLs are supported. Content type: ' + contentType
});There was a problem hiding this comment.
Done in b468a84 - these throw GenkitError now (INVALID_ARGUMENT for the media/part cases, UNIMPLEMENTED for the unsupported-role branch, which only fires for the tool role deferred to the next slice).
| throw Error( | ||
| `Unsupported genkit part fields encountered for current message role: ${JSON.stringify(part)}.` | ||
| ); |
There was a problem hiding this comment.
To ensure consistent error handling and better integration with Genkit's error reporting and telemetry, please throw a GenkitError with an appropriate status code (such as INVALID_ARGUMENT) instead of a generic Error.
throw new GenkitError({
status: 'INVALID_ARGUMENT',
message: 'Unsupported genkit part fields encountered for current message role: ' + JSON.stringify(part) + '.'
});There was a problem hiding this comment.
Done in b468a84 - these throw GenkitError now (INVALID_ARGUMENT for the media/part cases, UNIMPLEMENTED for the unsupported-role branch, which only fires for the tool role deferred to the next slice).
| throw Error( | ||
| `Unsupported genkit part fields encountered for current message role: ${JSON.stringify(part)}.` | ||
| ); |
There was a problem hiding this comment.
To ensure consistent error handling and better integration with Genkit's error reporting and telemetry, please throw a GenkitError with an appropriate status code (such as INVALID_ARGUMENT) instead of a generic Error.
throw new GenkitError({
status: 'INVALID_ARGUMENT',
message: 'Unsupported genkit part fields encountered for current message role: ' + JSON.stringify(part) + '.'
});There was a problem hiding this comment.
Done in b468a84 - these throw GenkitError now (INVALID_ARGUMENT for the media/part cases, UNIMPLEMENTED for the unsupported-role branch, which only fires for the tool role deferred to the next slice).
| throw Error( | ||
| `role ${message.role} is not supported by the OpenAI Responses API transport.` | ||
| ); |
There was a problem hiding this comment.
To ensure consistent error handling and better integration with Genkit's error reporting and telemetry, please throw a GenkitError with an appropriate status code (such as UNIMPLEMENTED) instead of a generic Error.
throw new GenkitError({
status: 'UNIMPLEMENTED',
message: 'role ' + message.role + ' is not supported by the OpenAI Responses API transport.'
});There was a problem hiding this comment.
Done in b468a84 - these throw GenkitError now (INVALID_ARGUMENT for the media/part cases, UNIMPLEMENTED for the unsupported-role branch, which only fires for the tool role deferred to the next slice).
… API Prepares the plugin to route some models over /v1/responses without changing anything about the models it serves today. A `transport` config key joins the plugin-level keys that the shared Chat Completions body builder destructures away, so it can never reach a wire that 400s on unrecognised arguments. Asking a Chat Completions model for `transport: 'responses'` is rejected outright rather than silently ignored, since opting a dual-transport model onto Responses is not available yet. The OpenAI APIError to GenkitError mapping and the media-part helpers move to utils.ts so a second runner can share them rather than fork them. `UNSUPPORTED_MODEL_MATCHERS` matched 'codex' as a bare substring, which hid gpt-5-codex, gpt-5.1-codex-max and codex-mini-latest from the model list along with the legacy code-* completion models it was aimed at. The matchers are now anchored patterns.
f276a29 to
b468a84
Compare
Eight OpenAI models - gpt-5-pro, the gpt-5/gpt-5.1 codex family, codex-mini-latest, o1-pro and o3-pro - are served only over /v1/responses and so cannot be reached through this plugin at all today. `RESPONSES_ONLY_MODELS` names them, and a single predicate over that const drives both the runtime routing and the `openAI.model()` type union. Any suffixed form of a curated name matches, so o3-pro-2025-06-10 and gpt-5-pro-preview route like their base rather than failing with an opaque OpenAI 400; unknown suffixes fail toward the transport that works. The runner mirrors the Chat Completions one: genkit messages become `input` items with system messages hoisted into `instructions`, output formats map onto `text.format`, unrecognised config keys pass through, and the response `output` items become parts. Prior model turns are serialized part by part rather than flattened to their text, so a structured-output turn replays as its JSON instead of as an empty assistant message. The models declare `constrained: 'all'` so genkit sends the output schema through `text.format` instead of simulating it in the prompt. `store` is pinned to false unless the caller sets it, so this transport retains no more data than the existing one. All eight models register through it, and the resolver, listActions and `openAI.model()` all route by the same predicate. Streaming, tool calling and the encrypted reasoning round-trip are not implemented yet. A streaming caller gets the completed response delivered as a single chunk; a request carrying tools, a response asking for a tool call, and a failed response carrying an `error` are all raised rather than quietly returned as an ordinary answer.
b468a84 to
d3bfa43
Compare
| // Silently ignoring the request would hand back a Chat Completions response | ||
| // to a caller who asked for something else. This builder is shared by every | ||
| // OpenAI-compatible provider, so the message stays provider-neutral. | ||
| if (transport === 'responses') { |
There was a problem hiding this comment.
This fires for deepSeek/xai too - none of the providers on this builder can speak the responses transport, and stripping the key silently seemed worse than rejecting it. Hence the provider-neutral message.
| response, | ||
| request.output?.format === 'json' | ||
| ); | ||
| // The Responses event protocol is not mapped yet, so a streaming caller |
There was a problem hiding this comment.
Weakest part of the slice: streaming callers get the final response as one chunk. Rejecting streaming outright would have made all eight models unusable in the Dev UI, which always streams. Event mapping is #6004.
|
Note: unit and fake-server coverage is in (146 tests, including wire assertions for both transports and an end-to-end generate() with an output schema), but the live smoke tests against the real API have not been run yet. Keeping this as draft until that's done. |
Part of #6002, closes #6003. Design rationale: design notes on the tracking issue.
Adds a second OpenAI transport to the existing
openAI()plugin: the eight models OpenAI serves only over/v1/responses(gpt-5-pro,gpt-5-codex,gpt-5.1-codex,gpt-5.1-codex-mini,gpt-5.1-codex-max,codex-mini-latest,o1-pro,o3-pro) now register and run against that endpoint, in the same namespace with the same action-name scheme. They were unreachable via this plugin before.RESPONSES_ONLY_MODELSconst drives the runtime routing predicate and theopenAI.model()type union; suffixed names (o3-pro-2025-06-10,gpt-5-pro-preview) route like their base, deliberately biased toward the transport that works.client.responses.create.storeis pinned tofalseunless set, keeping retention behavior identical to Chat Completions.supports.constrained: 'all', so output schemas go natively throughtext.formatinstead of genkit's simulated-constrained prompt injection. Prior model turns replay part by part, so structured-output history survives multi-turn.function_calloutput items are rejected (INVALID_ARGUMENT/UNIMPLEMENTED) until the tool-calling slice; afailedresponse throws itserrorinstead of returning an empty completion. Deliberate exception: a streaming caller gets the completed response as a single terminal chunk (the Dev UI always streams), with the event protocol in a follow-up.'codex'substring inUNSUPPORTED_MODEL_MATCHERSwas hiding thegpt-5-codexfamily; matchers are now anchored. Side effect: retiredcode-cushman-001is now filtered.Usage
Shared-layer note (affects
deepSeek()/xai()/openAICompatible()):transportbecomes a reserved config key in the Chat Completions body builder - stripped from the wire (Chat Completions 400s on unknown args), withtransport: 'responses'rejected via a provider-neutralINVALID_ARGUMENTrather than silently ignored.No behavior change for existing models: nothing in
SUPPORTED_GPT_MODELSchanges transport,rawshape, or config;gpt.tsis untouched. Fake-server tests assert endpoints on the wire for both transports (gpt-4o→/v1/chat/completions,gpt-5-pro→/v1/responses), and an end-to-end test through a realgenkit()instance asserts the output schema reaches the wire astext.format.Tests: 146 passing in
js/plugins/compat-oai(+49 vs base); live smoke tests run whenOPENAI_API_KEYis set.Not in this slice (see #6002): streaming events, tool calling, encrypted reasoning round-trip, annotations,
responsesModel()opt-in for dual-transport models, background models.