Skip to content

feat(js/plugins/compat-oai): add OpenAI Responses transport and non-streaming runner - #6006

Draft
cabljac wants to merge 2 commits into
mainfrom
feat/compat-oai-responses-slice1
Draft

feat(js/plugins/compat-oai): add OpenAI Responses transport and non-streaming runner#6006
cabljac wants to merge 2 commits into
mainfrom
feat/compat-oai-responses-slice1

Conversation

@cabljac

@cabljac cabljac commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

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.

  • One RESPONSES_ONLY_MODELS const drives the runtime routing predicate and the openAI.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.
  • Non-streaming runner over client.responses.create. store is pinned to false unless set, keeping retention behavior identical to Chat Completions.
  • supports.constrained: 'all', so output schemas go natively through text.format instead of genkit's simulated-constrained prompt injection. Prior model turns replay part by part, so structured-output history survives multi-turn.
  • Fails loudly instead of degrading: genkit tools and function_call output items are rejected (INVALID_ARGUMENT/UNIMPLEMENTED) until the tool-calling slice; a failed response throws its error instead 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.
  • Model listing fix: the 'codex' substring in UNSUPPORTED_MODEL_MATCHERS was hiding the gpt-5-codex family; matchers are now anchored. Side effect: retired code-cushman-001 is now filtered.

Usage

import { genkit, z } from 'genkit';
import { openAI } from '@genkit-ai/compat-oai/openai';

const ai = genkit({ plugins: [openAI()] });

// Previously unreachable models now work by name:
const { text } = await ai.generate({
  model: openAI.model('gpt-5-pro'),
  prompt: 'Prove there are infinitely many primes.',
});

// Output schemas go natively through text.format:
const { output } = await ai.generate({
  model: openAI.model('gpt-5-pro'),
  prompt: 'Extract the invoice fields.',
  output: { schema: z.object({ total: z.number(), currency: z.string() }) },
});

Shared-layer note (affects deepSeek()/xai()/openAICompatible()): transport becomes a reserved config key in the Chat Completions body builder - stripped from the wire (Chat Completions 400s on unknown args), with transport: 'responses' rejected via a provider-neutral INVALID_ARGUMENT rather than silently ignored.

No behavior change for existing models: nothing in SUPPORTED_GPT_MODELS changes transport, raw shape, or config; gpt.ts is 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 real genkit() instance asserts the output schema reaches the wire as text.format.

Tests: 146 passing in js/plugins/compat-oai (+49 vs base); live smoke tests run when OPENAI_API_KEY is 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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread js/plugins/compat-oai/src/responses.ts Outdated
Comment on lines +103 to +105
throw Error(
`Invalid data URL format for media: ${part.media.url.substring(0, 50)}...`
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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) + '...'
        });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread js/plugins/compat-oai/src/responses.ts Outdated
Comment on lines +114 to +116
throw Error(
`File URLs are not supported. Only base64-encoded files and image URLs are supported. Content type: ${contentType}`
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
    });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread js/plugins/compat-oai/src/responses.ts Outdated
Comment on lines +118 to +120
throw Error(
`Unsupported genkit part fields encountered for current message role: ${JSON.stringify(part)}.`
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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) + '.'
  });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread js/plugins/compat-oai/src/responses.ts Outdated
Comment on lines +146 to +148
throw Error(
`Unsupported genkit part fields encountered for current message role: ${JSON.stringify(part)}.`
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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) + '.'
      });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread js/plugins/compat-oai/src/responses.ts Outdated
Comment on lines +192 to +194
throw Error(
`role ${message.role} is not supported by the OpenAI Responses API transport.`
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.'
        });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
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.
@cabljac
cabljac force-pushed the feat/compat-oai-responses-slice1 branch from b468a84 to d3bfa43 Compare August 10, 2026 17:30
// 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') {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@cabljac

cabljac commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

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.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[JS] compat-oai: transport boundary + non-streaming Responses runner

1 participant