Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Contributing

## How it works

`quirks` are JSON on `InferenceSource` (persisted, sent over the wire).
`hooks` are code, applied once at `responsesAdapterFactory` construction.
Defaults are protocol-native: system prompt, `maxTokens`, and `temperature`
go through unless a quirk opts a backend out. The host owns provider ids; a
reasoning signature is tagged with the id in effect when it was issued.

## Development

```sh
git clone https://github.com/corbitsdev/corbits-openai-responses.git
cd corbits-openai-responses
bun install
bun run check
```

`bun run check` is typecheck + lint + format:check + test. `bun run format`
rewrites the tree.
208 changes: 140 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,88 +1,160 @@
# @corbits/openai-responses

An Interchange `ProviderAdapter` for the OpenAI Responses API wire protocol:
text, tool calls, reasoning with `encrypted_content` replay, image and PDF
input, SSE and non-streaming. Vendor differences (Codex, xAI/Grok, plain
OpenAI) are a `ResponsesQuirks` bag, not a forked adapter. Current scope covers
text, tool calls, reasoning replay, and image/PDF input.
An `@intx/inference` provider adapter for the OpenAI Responses API (`/v1/responses`): SSE and JSON responses, text, tool calls, image and PDF input, and reasoning replay. An inference provider for Corbits and Interchange agents that also works in any host that runs `@intx/inference`.

## Runtime support
## Why @corbits/openai-responses?

Bun >= 1.2 is the development runtime. The package ships compiled `dist/`
(`main`/`types` and `exports` point at `dist/index.js` / `dist/index.d.ts`,
built with `bun run build`); both Bun and Node >= 24 load the built output.
`@intx/inference` and `@intx/types`
are peer dependencies and must resolve to the host's own copy.
1. **One adapter for every Responses backend.** OpenAI, Codex, xAI and Ollama's `/v1` differ in paths, headers and body fields. Those differences are a JSON `quirks` object on the source, not forked code.
2. **Reasoning survives across turns.** Encrypted reasoning items are tagged with the provider that issued them. They are replayed only to the same provider and model, so multi-turn reasoning keeps its context and the backend never rejects a signature it did not issue.
3. **Interchange error and retry semantics.** Requests run through `runInference`, so rate limits, pacing headers and auth failures behave the same as for the built-in providers.

## Quickstart
It speaks only the Responses protocol. For Chat Completions, use the built-in OpenAI adapter in `@intx/inference`.

## Install

```sh
npm add @corbits/openai-responses
pnpm add @corbits/openai-responses
yarn add @corbits/openai-responses
bun add @corbits/openai-responses
```bash
bun add @corbits/openai-responses @intx/inference@^0.4.0 @intx/types@^0.4.0
```

Bake a vendor's wire shape into a quirks bag, then hand it to
`responsesAdapterFactory` to get back an `AdapterFactory`. This mirrors how
`@corbits/xai-provider` wires up Grok's Responses-speaking CLI proxy:
Runs on Bun >= 1.2 or Node >= 24.

## Quickstart

Needs `OPENAI_API_KEY` set.

```ts
import { createDependencies, runInference } from "@intx/inference";
import {
responsesAdapterFactory,
type ResponsesQuirks,
createOpenAIResponsesAdapter,
OPENAI_RESPONSES_PROVIDER,
} from "@corbits/openai-responses";
import type { AdapterFactory } from "@intx/inference";

// Mirrors the vendor's own request shape: headers, system-prompt
// placement, reasoning summary depth, which stock fields to suppress.
const grokResponsesQuirks: ResponsesQuirks = {
path: "/v1/responses",
headers: {
static: { "x-grok-client-identifier": "my-harness" },

const deps = createDependencies({
has: (provider) => provider === OPENAI_RESPONSES_PROVIDER,
resolve: (source, quirks) => createOpenAIResponsesAdapter(source, quirks),
});

let seq = 0;
for await (const event of runInference({
deps,
source: {
id: "openai",
provider: OPENAI_RESPONSES_PROVIDER,
baseURL: "https://api.openai.com/v1",
credentialId: "OPENAI_API_KEY",
model: "gpt-5-mini",
},
turns: [
{
role: "user",
timestamp: Date.now(),
content: [{ type: "text", text: "Say hello." }],
},
],
nextSeq: () => seq++,
readMaterial: (id) => {
const secret = process.env[id];
if (secret === undefined) throw new Error(`${id} is not set`);
return { secret };
},
sessionIdOption: "sessionId",
systemPrompt: { role: "system", shape: "string" },
contentShape: "flat",
reasoning: { summary: "detailed" },
maxOutputTokens: false,
temperature: false,
};

export const createGrokResponsesAdapter: AdapterFactory =
responsesAdapterFactory(grokResponsesQuirks);
})) {
if (event.type === "inference.text.delta")
process.stdout.write(event.data.token);
if (event.type === "inference.error")
throw new Error(event.data.error.message);
}
process.stdout.write("\n");
```

A sidecar host registers the resulting export on its
`SIDECAR_ADAPTER_MANIFEST` (one entry per provider id, `specifier` naming an
already-installed module) — this package ships no manifest entry itself; the
vendor package wrapping it does, e.g.
`{"provider":"xai","specifier":"@corbits/xai-provider","export":"createXaiResponsesAdapter"}`.

## How it works

`quirks` are JSON on `InferenceSource` (persisted, sent over the wire).
`hooks` are code, applied once at `responsesAdapterFactory` construction.
Defaults are protocol-native: system prompt, `maxTokens`, and `temperature`
go through unless a quirk opts a backend out. The host owns provider ids; a
reasoning signature is tagged with the id in effect when it was issued.

## Development

```sh
git clone https://github.com/corbitsdev/corbits-openai-responses.git
cd corbits-openai-responses
bun install
bun run typecheck
bun run lint
bun run format:check
bun run test
bun run check
## Where it fits

[Interchange](https://github.com/faremeter/interchange) runs AI agents as principals (accounts that hold their own identity, permissions and credentials). Corbits packages add what an agent product needs around it.

- **Runs in:** the agent sidecar (the runtime next to each agent), or any process that calls `runInference`. No hub is required.
- **Plugs into:** the [`@intx/inference`](https://github.com/faremeter/interchange/tree/main/packages/inference) adapter registry, as the factory for the `openai-responses` provider id.
- **Pairs with:** [`@corbits/ollama-adapter`](https://github.com/corbitsdev/corbits-ollama-adapter) and [`@corbits/system-one`](https://github.com/corbitsdev/corbits-system-one), the other Corbits inference providers.

## Reference

| Export | Description |
| ----------------------------------------- | ----------------------------------------------------------------------------------- |
| `createOpenAIResponsesAdapter` | `AdapterFactory`. Reads quirks from the source on every resolve. |
| `responsesAdapterFactory(quirks, hooks?)` | Returns an `AdapterFactory` with fixed quirks. Any per-source `quirks` are ignored. |
| `OPENAI_RESPONSES_PROVIDER` | The `"openai-responses"` provider id. |
| `OPENAI_COMPATIBLE_RESPONSES_PROVIDER` | Deprecated `"openai-compatible-responses"` id from 0.1. Removed in 0.3.0. |
| `responsesAdapterFactories` | Record mapping both provider ids to `createOpenAIResponsesAdapter`. |
| `ResponsesQuirks` | Schema and type for the `quirks` object. |
| `ResponsesHooks` | Code hooks: `wrapSystemPrompt`, `includeReasoningEffort`. |

### Quirks

Every field is optional. An absent field keeps the protocol default. Unknown keys are rejected.

| Quirk | Type | Default | Effect on the request |
| ------------------------ | --------------------------------------------------------------- | ------------------------------------- | ----------------------------------------------------------------------------- |
| `path` | `string` | `"/responses"` | Request path appended to the source's `baseURL`. |
| `headers.static` | `Record<string, string>` | `{}` | Headers added verbatim; they can override the stock headers. |
| `headers.modelHeader` | `string` | unset | Header name that carries the model id. |
| `headers.fromOption` | `{ optionKey, header }[]` | `[]` | Copies each non-empty string `providerOptions[optionKey]` into `header`. |
| `sessionIdOption` | `string` | unset | `providerOptions` key whose value is sent as `prompt_cache_key`. |
| `sessionIdHeader` | `string` | unset | Also sends that session id in this header. Ignored without `sessionIdOption`. |
| `systemPrompt` | `{ role: "system" \| "developer", shape: "string" \| "parts" }` | `{ role: "system", shape: "string" }` | Role and content shape of the leading system-prompt item. |
| `contentShape` | `"typed" \| "flat"` | `"typed"` | `flat` sends text-only content as a plain string instead of typed parts. |
| `parallelToolCalls` | `boolean` | unset | Unset omits `parallel_tool_calls`; a boolean is sent as given. |
| `maxOutputTokens` | `boolean` | `true` | `false` omits `max_output_tokens` even when the caller sets `maxTokens`. |
| `temperature` | `boolean` | `true` | `false` omits `temperature` even when the caller sets it. |
| `store` | `boolean` | `false` | Sent as `store`. |
| `stream` | `boolean` | `true` | Sent as `stream`; `false` also sends `accept: application/json`. |
| `reasoning.summary` | `"auto" \| "detailed"` | unset | Sent as `reasoning.summary`. |
| `reasoning.effortOption` | `string` | unset | `providerOptions` key whose value is sent as `reasoning.effort`. |
| `instructions` | `string` | unset | Sent as `instructions`. |

## Using with Interchange

Interchange loads custom adapters from an operator-configured `AdapterManifest`. Add one entry per provider id you serve:

```ts
import { createDependencies, type AdapterManifest } from "@intx/inference";
import { loadAdapterRegistry } from "@intx/inference/providers";

const manifest: AdapterManifest = [
{
provider: "openai-responses",
specifier: "@corbits/openai-responses",
export: "createOpenAIResponsesAdapter",
},
{
provider: "openai-compatible-responses",
specifier: "@corbits/openai-responses",
export: "createOpenAIResponsesAdapter",
},
];
const deps = createDependencies(await loadAdapterRegistry(manifest));
```

`bun run format` rewrites the tree. `bun run check` is typecheck + lint +
format:check + test.
Pass `deps` to `runInference`. Sources with `provider: "openai-responses"` (or the deprecated `"openai-compatible-responses"`) then resolve to this adapter, and each source's `quirks` configures its backend. To serve a vendor under its own id, add another entry with that `provider` and the same export. Manifest entries override built-in adapters with the same id, so don't reuse `openai` or another built-in id.

For a vendor that needs code hooks, bake its quirks into a factory in your own module and point a manifest entry at that export:

```ts
import { responsesAdapterFactory } from "@corbits/openai-responses";

export const createVendorAdapter = responsesAdapterFactory(
{ path: "/v1/responses", contentShape: "flat", temperature: false },
{ wrapSystemPrompt: (prompt) => `<system>${prompt}</system>` },
);
```

Its manifest entry names your module and that export, for example `{ provider: "vendor-responses", specifier: "./vendor-adapter.js", export: "createVendorAdapter" }`.

## Upgrading from 0.1

- No host change is needed. `responsesAdapterFactories` still maps `openai-compatible-responses` to `createOpenAIResponsesAdapter`, so sources stored under that id keep resolving and running.
- `OPENAI_COMPATIBLE_RESPONSES_PROVIDER` is deprecated and removed in 0.3.0. Move stored sources to `openai-responses` before then.
- `isResponsesStreamTerminal` is no longer exported. The adapter still applies it.
- `@intx/inference` and `@intx/types` peers are now `^0.4.0`.
- Quirks and reasoning signatures are unchanged.

## License

LGPL-2.1-only.
[LGPL-2.1-only](https://github.com/corbitsdev/corbits-openai-responses/blob/main/LICENSE)
Loading