Skip to content

feat(sdk): ProviderSettings.storeResponses for OpenAI Responses API#97

Closed
ketankhairnar wants to merge 1 commit into
withastro:mainfrom
ketankhairnar:feat/sdk-openai-store-knob
Closed

feat(sdk): ProviderSettings.storeResponses for OpenAI Responses API#97
ketankhairnar wants to merge 1 commit into
withastro:mainfrom
ketankhairnar:feat/sdk-openai-store-knob

Conversation

@ketankhairnar
Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in providers.openai.storeResponses?: boolean knob on ProviderSettings. When set, the SDK injects store: <bool> into OpenAI Responses API request payloads via an onPayload hook on the underlying agent runtime. Default unset preserves current behavior.

Resolves the multi-turn 404 reported in #77.

Why

pi-ai (@mariozechner/pi-ai) hardcodes store: false on the OpenAI Responses provider. The conversation converter then emits per-item references (fc_*, msg_*) on subsequent turns, but those items never get persisted server-side. So a second session.prompt() or session.task() call on the same session returns a 404 from OpenAI: "Items are not persisted when store is set to false."

The runtime exposes an onPayload hook that lets us mutate the request just before it's sent. Wiring store: true through that hook is non-breaking and avoids forking pi-ai.

What's in this PR

  • packages/sdk/src/types.ts — adds storeResponses?: boolean to ProviderSettings. Doc-commented with the multi-turn use case + the data-retention trade-off.
  • packages/sdk/src/session.ts:
    • Adds a private buildOpenAIStorePayloadHook(providers) helper that returns an onPayload callback (or undefined when the knob is unset).
    • The callback is model.api === 'openai-responses' guarded — it's a no-op for Codex (which rejects store: true), Azure, Anthropic, and every other provider.
    • Wires the hook into the Agent constructor at session init.

Net diff: +38 / -0 across 2 files.

Non-breaking by design

  • Default unset → no hook is attached → pi-ai's existing payload (store: false) ships unchanged.
  • model.api guard means non-OpenAI-Responses models never see the mutation.
  • Codex specifically rejects store: true server-side; the guard prevents that path from ever firing for openai-codex-responses.

Usage

init({
  providers: { openai: { storeResponses: true } },
});

What's NOT in the PR

Design notes

The flat storeResponses: boolean shape was chosen over a nested openai: { store: boolean } for readability. If reviewers prefer the nested shape (e.g. to keep room for future Responses-only flags like service_tier or prompt_cache_retention), happy to reshape — discussed in MEMO.

The naming (storeResponses vs store) is intentional. store would be ambiguous with persistence-layer concepts (the SessionStore interface uses the same word for client-side persistence). storeResponses makes it clear this is server-side conversation storage on the provider, distinct from any local persistence the user has wired up.

Open question (verified, no blocker)

Per RECIPE.md analysis: in pi-ai's openai-responses provider, the conversation converter currently passes per-item ids but does not set previous_response_id. OpenAI's Responses API supports both linking strategies. Setting store: true alone is sufficient — the per-item references resolve once items are persisted. If a future change wants to also pass previous_response_id, the same onPayload hook is the right seam.

Refs

Test plan

  • pnpm check:types passes on the packages/sdk workspace.
  • Manual smoke: a multi-turn session.task() against openai/gpt-5 succeeds when storeResponses: true is set.
  • Fake-stream + payload-assertion test suite to follow once vitest lands on main.

Adds an opt-in `providers.openai.storeResponses?: boolean` knob on
ProviderSettings. When set, an `onPayload` hook on the underlying agent
runtime mutates OpenAI Responses API request payloads to include
`store: <bool>` just before the request is sent. Default unset preserves
current behavior (pi-ai hardcodes `store: false`).

Resolves the multi-turn 404 reported in withastro#77: pi-ai's conversation
converter emits item references (`fc_*`, `msg_*`) on later turns, but
those items never get persisted server-side, so subsequent
`session.task()` calls get a 404 from the OpenAI Responses API. Setting
`store: true` opts the conversation into OpenAI's server-side storage
so the references resolve.

Scoped to `model.api === 'openai-responses'` only. The hook is a no-op
for Codex (which rejects `store: true`), Azure, Anthropic, and every
other provider — they see `undefined` from the hook and pi-ai keeps the
payload unchanged.

Usage:

  init({
    providers: { openai: { storeResponses: true } },
  });

Privacy note: enabling this opts the conversation into OpenAI's
server-side storage and its retention policy. Default off is
intentional.
@FredKSchott
Copy link
Copy Markdown
Member

Thanks for digging into this @ketankhairnar — your diagnosis (pi-ai's hardcoded store: false + per-item id replay on subsequent turns) was spot on, and pointed me straight at the right onPayload seam.

Landed in 83ba598 with two changes:

  • Added ProviderSettings.storeResponses?: boolean exactly as you proposed, scoped to openai-responses / azure-openai-responses (Codex Responses skipped — it rejects store: true).
  • Also flipped Flue's default thinkingLevel from 'off' to 'medium', matching pi-coding-agent. With reasoning enabled, pi-ai requests reasoning.encrypted_content and the converter replays the encrypted blob inline — so the bug stops manifesting for the common case without anyone changing their config. storeResponses remains as the escape hatch for users who explicitly want thinkingLevel: 'off' on a reasoning model.

You're credited as co-author on the commit. Closing this in favor of the merged commit. Thanks again!

@FredKSchott FredKSchott closed this May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants