Skip to content

Commit 8577657

Browse files
Merge pull request #9 from PredictabilityAtScale/codex/add-support-for-new-conversation-body-format
# Conflicts: # README.md # docs/providers.md # src/providers/gemini.ts # tests/providers.test.ts
2 parents 3859ed0 + ab48cbb commit 8577657

15 files changed

Lines changed: 774 additions & 15 deletions

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ Supported values for `warnings.contextSize` are `auto`, `off`, `result-only`, `c
135135
- **Composition**`includes` to share system instructions across prompts, with circular detection
136136
- **Folder defaults**`defaults.md` inheritance for shared provider, model, metadata, and system instructions
137137
- **Overrides** — Environment and tier-based overrides (base → env → tier → runtime)
138-
- **4 provider adapters** — OpenAI, Anthropic, Gemini, OpenRouter — body-only output
138+
- **5 provider adapters** — OpenAI (Chat), OpenAI (Responses), Anthropic, Gemini, OpenRouter — body-only output
139139
- **Provider-aware input caching controls** — optional `cache` front matter maps to OpenAI prompt cache hints, Anthropic `cache_control`, and Gemini `cachedContent`
140140
- **Validation** — Zod schema validation, Levenshtein-based "did you mean?" for typos, variable usage checks
141141
- **Context hardening** — structured regexes with flags, `/pattern/i` convenience syntax, and built-in `non_empty` / `reject_secrets` validators
@@ -194,6 +194,7 @@ Provider adapters are also available as direct imports:
194194

195195
```typescript
196196
import { openaiAdapter } from 'promptopskit/openai';
197+
import { openaiResponsesAdapter } from 'promptopskit/openai-responses';
197198
import { anthropicAdapter } from 'promptopskit/anthropic';
198199
import { geminiAdapter } from 'promptopskit/gemini';
199200
import { openrouterAdapter } from 'promptopskit/openrouter';
@@ -502,14 +503,15 @@ Renders a prompt for a specific provider. Returns `{ resolved, request?, returnM
502503
|--------|------|-------------|
503504
| `path` | `string` | Prompt path (no extension), e.g. `'support/reply'` |
504505
| `source` | `string` | Inline prompt source (alternative to path) |
505-
| `provider` | `string` | `'openai'`, `'anthropic'`, `'gemini'`, `'openrouter'` |
506+
| `provider` | `string` | `'openai'`, `'openai-responses'`, `'anthropic'`, `'gemini'`, `'openrouter'` |
506507
| `variables` | `Record<string, string>` | Template variables |
507508
| `onContextOverflow` | `(info) => string` | Optional callback to transform oversized context values before rendering |
508509
| `environment` | `string` | Environment override name |
509510
| `tier` | `string` | Tier override name |
510511
| `history` | `Array<{ role, content }>` | Conversation history |
511512
| `toolRegistry` | `Record<string, unknown>` | Tool definitions for resolving string tool references |
512513
| `strict` | `boolean` | Fail on missing variables |
514+
| `openaiResponses` | `object` | Optional Responses API extras (`previous_response_id`, `conversation`, `instructions`, `parallel_tool_calls`, `max_tool_calls`, `store`, `metadata`, `include`, `background`) |
513515

514516
### `kit.loadPrompt(path)` / `kit.resolvePrompt(path, options)` / `kit.validatePrompt(path)`
515517

@@ -529,7 +531,7 @@ Prompt files use YAML front matter with these fields:
529531
|-------|------|-------------|
530532
| `id` | `string` | Unique prompt identifier (required) |
531533
| `schema_version` | `number` | Schema version, currently `1` |
532-
| `provider` | `string` | `openai`, `anthropic`, `gemini` (or `google`), `openrouter`, `any` |
534+
| `provider` | `string` | `openai`, `openai-responses`, `anthropic`, `gemini` (or `google`), `openrouter`, `any` |
533535
| `model` | `string` | Model name |
534536
| `fallback_models` | `string[]` | Fallback model list |
535537
| `reasoning` | `object` | `{ effort, budget_tokens }` |

docs/api-reference.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,15 @@ const result = await kit.renderPrompt({
6363
|--------|------|-------------|
6464
| `path` | `string` | Prompt path (no extension), e.g. `'support/reply'` |
6565
| `source` | `string` | Inline prompt source (alternative to `path`) |
66-
| `provider` | `string` | `'openai'`, `'anthropic'`, `'gemini'`, `'openrouter'` (required) |
66+
| `provider` | `string` | `'openai'`, `'openai-responses'`, `'anthropic'`, `'gemini'`, `'openrouter'` (required) |
6767
| `variables` | `Record<string, string>` | Template variables |
6868
| `onContextOverflow` | `(info) => string` | Optional callback to transform an oversized context value before rendering |
6969
| `environment` | `string` | Environment override name |
7070
| `tier` | `string` | Tier override name |
7171
| `history` | `Array<{ role, content }>` | Conversation history |
7272
| `toolRegistry` | `Record<string, unknown>` | Tool definitions for resolving string tool references |
7373
| `strict` | `boolean` | Fail on missing variables (default `false`) |
74+
| `openaiResponses` | `object` | Optional Responses API extras (`previous_response_id`, `conversation`, `instructions`, `parallel_tool_calls`, `max_tool_calls`, `store`, `metadata`, `include`, `background`) |
7475

7576
Either `path` or `source` must be provided.
7677

docs/providers.md

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,53 @@
11
# Provider Adapters
22

3-
PromptOpsKit ships four provider adapters. Direct `render()` calls always produce a `{ body, provider, model }` object shaped for the target API. Async `renderPrompt()` helpers may instead return `{ provider, model, returnMessage }` when context validation is configured to short-circuit before request shaping. You handle the HTTP call — no auth, no headers, no HTTP client opinions.
3+
PromptOpsKit ships five provider adapters. Direct `render()` calls always produce a `{ body, provider, model }` object shaped for the target API. Async `renderPrompt()` helpers may instead return `{ provider, model, returnMessage }` when context validation is configured to short-circuit before request shaping. You handle the HTTP call — no auth, no headers, no HTTP client opinions.
44

55
## Supported providers
66

77
| Provider | Front matter value | Adapter |
88
|----------|-------------------|---------|
9-
| OpenAI | `openai` | `openaiAdapter` |
9+
| OpenAI (Chat Completions) | `openai` | `openaiAdapter` |
10+
| OpenAI (Responses API) | `openai-responses` | `openaiResponsesAdapter` |
1011
| Anthropic | `anthropic` | `anthropicAdapter` |
1112
| Google Gemini | `gemini` or `google` | `geminiAdapter` |
1213
| OpenRouter | `openrouter` | `openrouterAdapter` |
1314

15+
16+
## Normalized front matter vs provider-specific options
17+
18+
PromptOpsKit already normalizes common settings across providers via front matter fields like `sampling`, `reasoning`, `response`, and `tools`.
19+
20+
When a provider has extra knobs with no clean cross-provider equivalent, use `provider_options`:
21+
22+
```yaml
23+
provider_options:
24+
anthropic:
25+
top_k: 50
26+
tool_choice:
27+
type: auto
28+
gemini:
29+
candidate_count: 2
30+
top_k: 20
31+
seed: 42
32+
response_modalities: ["TEXT"]
33+
thinking_budget_tokens: 2048
34+
```
35+
36+
This keeps portable settings in normalized fields, while still exposing advanced provider-specific controls.
37+
38+
39+
## Streaming support
40+
41+
`response.stream` support differs by provider:
42+
43+
| Provider | `response.stream` behavior |
44+
|----------|----------------------------|
45+
| `openai` | Mapped to body `stream` |
46+
| `openai-responses` | Mapped to body `stream` |
47+
| `anthropic` | Mapped to body `stream` |
48+
| `openrouter` | Mapped to body `stream` (same as OpenAI) |
49+
| `gemini` | **Not body-mapped**; Gemini streaming is endpoint-based (`streamGenerateContent`) |
50+
1451
## Usage via `renderPrompt`
1552

1653
```typescript
@@ -42,6 +79,7 @@ When a prompt includes multiple cache blocks (for example `cache.openai` + `cach
4279

4380
```typescript
4481
import { openaiAdapter } from 'promptopskit/openai';
82+
import { openaiResponsesAdapter } from 'promptopskit/openai-responses';
4583
import { anthropicAdapter } from 'promptopskit/anthropic';
4684
import { geminiAdapter } from 'promptopskit/gemini';
4785
import { openrouterAdapter } from 'promptopskit/openrouter';
@@ -63,7 +101,7 @@ interface ProviderAdapter {
63101
}
64102
```
65103

66-
Direct adapter rendering accepts the same `environment` and `tier` selectors as `kit.renderPrompt()`. Use the synchronous `validate()` and `render()` methods when you already have a compiled `ResolvedPromptAsset`, and use the async `validatePrompt()` and `renderPrompt()` helpers when you want the adapter to resolve either markdown source or a compiled artifact from disk. Context input validation runs through the same shared prompt-input wrapper for OpenAI, Anthropic, Gemini, and OpenRouter, so `allow_regex`, `deny_regex`, `non_empty`, `reject_secrets`, and `return_message` behave consistently across all four.
104+
Direct adapter rendering accepts the same `environment` and `tier` selectors as `kit.renderPrompt()`. Use the synchronous `validate()` and `render()` methods when you already have a compiled `ResolvedPromptAsset`, and use the async `validatePrompt()` and `renderPrompt()` helpers when you want the adapter to resolve either markdown source or a compiled artifact from disk. Context input validation runs through the same shared prompt-input wrapper for OpenAI, OpenAI Responses, Anthropic, Gemini, and OpenRouter, so `allow_regex`, `deny_regex`, `non_empty`, `reject_secrets`, and `return_message` behave consistently across all five.
67105

68106
Server-side example:
69107

@@ -179,7 +217,7 @@ If you want UsageTap begin/end tracking around a provider call, use the optional
179217

180218
See [UsageTap](./usagetap.md) for setup, lifecycle helpers, entitlement behavior, tool gating, standalone usage extractors, and provider examples.
181219

182-
## OpenAI
220+
## OpenAI (`openai`)
183221

184222
Body shape: [Chat Completions API](https://platform.openai.com/docs/api-reference/chat)
185223

@@ -195,6 +233,44 @@ Body shape: [Chat Completions API](https://platform.openai.com/docs/api-referenc
195233
}
196234
```
197235

236+
## OpenAI Responses (`openai-responses`)
237+
238+
Body shape: [Responses API](https://platform.openai.com/docs/api-reference/responses)
239+
240+
```json
241+
{
242+
"model": "gpt-5.4",
243+
"instructions": "...",
244+
"input": [
245+
{ "role": "user", "content": "..." }
246+
],
247+
"temperature": 0.7,
248+
"reasoning": { "effort": "medium" }
249+
}
250+
```
251+
252+
Field mapping (differences from `openai`):
253+
254+
| Front matter | Body field (`openai-responses`) |
255+
|-------------|----------------------------------|
256+
| `sampling.max_output_tokens` | `max_output_tokens` |
257+
| `reasoning.effort` | `reasoning: { effort }` |
258+
| `response.format: json` | `text: { format: { type: "json_object" } }` |
259+
| `response.schema` | `text: { format: { type: "json_schema", name, schema, strict } }` |
260+
| `sections.system_instructions` | `instructions` (top-level) |
261+
| `history + prompt_template` | `input` items instead of `messages` |
262+
| `tools` | Responses function tools (`{ type, name, description, parameters }`) |
263+
264+
Warnings:
265+
- `reasoning.budget_tokens` is ignored (Responses uses `reasoning.effort`).
266+
267+
Extra supported options via `renderPrompt(..., { openaiResponses: { ... } })` or direct adapter runtime:
268+
- `previous_response_id` (conversation chaining)
269+
- `conversation` (mutually exclusive with `previous_response_id`)
270+
- `parallel_tool_calls`, `max_tool_calls`
271+
- `store`, `metadata`, `include`, `background`
272+
- `instructions` override (runtime override for top-level instructions)
273+
198274
Field mapping:
199275

200276
| Front matter | Body field |
@@ -208,6 +284,7 @@ Field mapping:
208284
| `sampling.max_output_tokens` | `max_tokens` |
209285
| `reasoning.effort` | `reasoning_effort` |
210286
| `response.format: json` | `response_format: { type: "json_object" }` |
287+
| `response.schema` | `response_format: { type: "json_schema", json_schema: { name, schema, strict } }` |
211288
| `response.stream` | `stream` |
212289
| `cache.openai.prompt_cache_key` | `prompt_cache_key` |
213290
| `cache.openai.retention` | `prompt_cache_retention` |
@@ -244,6 +321,8 @@ Key differences from OpenAI:
244321
- `cache.anthropic.mode: automatic` maps to top-level `cache_control`.
245322
- `cache.anthropic.mode: explicit` applies `cache_control` at block level for selected sections/tools.
246323
- `cache.anthropic.ttl` supports `5m` (default) or `1h`.
324+
- `provider_options.anthropic.top_k` maps to `top_k`.
325+
- `provider_options.anthropic.tool_choice` maps to `tool_choice`.
247326

248327
Warnings:
249328
- `frequency_penalty` and `presence_penalty` are not supported — ignored with a warning.
@@ -276,8 +355,16 @@ Key differences:
276355
- Sampling parameters are nested under `generationConfig`.
277356
- `top_p` maps to `topP`, `max_output_tokens` maps to `maxOutputTokens`, `stop` maps to `stopSequences`.
278357
- `response.format: json` maps to `generationConfig.responseMimeType: "application/json"`.
358+
- `response.schema` maps to `generationConfig.responseSchema` (portable normalized schema shape).
359+
- `response.stream` is not body-mapped for Gemini; use the streaming endpoint (`streamGenerateContent`).
279360
- `reasoning.effort` maps to `thinkingConfig.thinkingBudget` (high=8192, medium=4096, low=1024).
280361
- `cache.gemini.cached_content` (or `cache.google.cached_content`) maps to top-level `cachedContent`.
362+
- `provider_options.gemini.candidate_count` maps to `generationConfig.candidateCount`.
363+
- `provider_options.gemini.top_k` maps to `generationConfig.topK`.
364+
- `provider_options.gemini.seed` maps to `generationConfig.seed`.
365+
- `provider_options.gemini.response_schema` maps to `generationConfig.responseSchema`.
366+
- `provider_options.gemini.response_modalities` maps to `generationConfig.responseModalities`.
367+
- `provider_options.gemini.thinking_budget_tokens` overrides effort-derived thinking budget.
281368

282369
Warnings:
283370
- `frequency_penalty` and `presence_penalty` are not supported — ignored with a warning.

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@
7979
"types": "./dist/providers/openrouter.d.cts",
8080
"default": "./dist/providers/openrouter.cjs"
8181
}
82+
},
83+
"./openai-responses": {
84+
"import": {
85+
"types": "./dist/providers/openai-responses.d.ts",
86+
"default": "./dist/providers/openai-responses.js"
87+
},
88+
"require": {
89+
"types": "./dist/providers/openai-responses.d.cts",
90+
"default": "./dist/providers/openai-responses.cjs"
91+
}
8292
}
8393
},
8494
"files": [

src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export { interpolate, extractVariables } from './renderer/index.js';
6363
export { resolveIncludes } from './composition/index.js';
6464
export { applyOverrides } from './overrides/index.js';
6565
export { validateAsset, validateAssetWithIncludes } from './validation/index.js';
66-
export { getAdapter, openaiAdapter } from './providers/index.js';
66+
export { getAdapter, openaiAdapter, openaiResponsesAdapter } from './providers/index.js';
6767
export { anthropicAdapter } from './providers/anthropic.js';
6868
export { geminiAdapter } from './providers/gemini.js';
6969
export { openrouterAdapter } from './providers/openrouter.js';
@@ -121,6 +121,8 @@ export interface RenderPromptOptions {
121121
toolRegistry?: Record<string, unknown>;
122122
/** Strict mode — fail on missing variables */
123123
strict?: boolean;
124+
/** OpenAI Responses API-specific request options */
125+
openaiResponses?: RuntimeRenderOptions['openaiResponses'];
124126
}
125127

126128
// --- Result ---
@@ -263,6 +265,7 @@ export class PromptOpsKit {
263265
history: options.history,
264266
toolRegistry: options.toolRegistry,
265267
strict: options.strict,
268+
openaiResponses: options.openaiResponses,
266269
});
267270

268271
return {

src/overrides/apply-overrides.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,20 @@ function mergeOverride(
5656
result.response = { ...result.response, ...override.response };
5757
}
5858

59+
if (override.provider_options !== undefined) {
60+
result.provider_options = {
61+
...result.provider_options,
62+
...override.provider_options,
63+
anthropic: {
64+
...result.provider_options?.anthropic,
65+
...override.provider_options.anthropic,
66+
},
67+
gemini: {
68+
...result.provider_options?.gemini,
69+
...override.provider_options.gemini,
70+
},
71+
};
72+
}
73+
5974
return result;
6075
}

src/providers/anthropic.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ export const anthropicAdapter: ProviderAdapter = withPromptInputSupport({
3434
if (resolvedAsset.reasoning?.effort !== undefined) {
3535
warnings.push('Anthropic uses budget_tokens for thinking, not effort. effort will be mapped approximately.');
3636
}
37+
if (resolvedAsset.response?.schema !== undefined) {
38+
warnings.push('Anthropic does not support response.schema structured output in this adapter. It will be ignored.');
39+
}
40+
41+
if (resolvedAsset.provider_options?.anthropic?.top_k !== undefined && resolvedAsset.provider_options.anthropic.top_k < 0) {
42+
errors.push('Anthropic provider_options.top_k must be >= 0.');
43+
}
3744

3845
return { valid: errors.length === 0, errors, warnings };
3946
},
@@ -108,6 +115,11 @@ export const anthropicAdapter: ProviderAdapter = withPromptInputSupport({
108115
};
109116
}
110117

118+
// Provider-specific options
119+
if (resolvedAsset.provider_options?.anthropic?.top_k !== undefined) {
120+
body.top_k = resolvedAsset.provider_options.anthropic.top_k;
121+
}
122+
111123
// Streaming
112124
if (resolvedAsset.response?.stream !== undefined) {
113125
body.stream = resolvedAsset.response.stream;
@@ -146,6 +158,10 @@ export const anthropicAdapter: ProviderAdapter = withPromptInputSupport({
146158
});
147159
}
148160

161+
if (resolvedAsset.provider_options?.anthropic?.tool_choice !== undefined) {
162+
body.tool_choice = resolvedAsset.provider_options.anthropic.tool_choice;
163+
}
164+
149165
return {
150166
body,
151167
provider: 'anthropic',

src/providers/gemini.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export const geminiAdapter: ProviderAdapter = withPromptInputSupport({
3636
if (geminiCache && googleCache && geminiCache !== googleCache) {
3737
warnings.push('Both cache.gemini.cached_content and cache.google.cached_content are set. Gemini uses cache.gemini.cached_content.');
3838
}
39+
if (resolvedAsset.response?.stream !== undefined) {
40+
warnings.push('Gemini streaming is endpoint-based (streamGenerateContent), not body-based. response.stream will be ignored.');
41+
}
3942

4043
return { valid: errors.length === 0, errors, warnings };
4144
},
@@ -82,17 +85,31 @@ export const geminiAdapter: ProviderAdapter = withPromptInputSupport({
8285
// Generation config
8386
const generationConfig: Record<string, unknown> = {};
8487

88+
const geminiOptions = resolvedAsset.provider_options?.gemini;
89+
8590
if (resolvedAsset.sampling?.temperature !== undefined) generationConfig.temperature = resolvedAsset.sampling.temperature;
8691
if (resolvedAsset.sampling?.top_p !== undefined) generationConfig.topP = resolvedAsset.sampling.top_p;
8792
if (resolvedAsset.sampling?.max_output_tokens !== undefined) generationConfig.maxOutputTokens = resolvedAsset.sampling.max_output_tokens;
8893
if (resolvedAsset.sampling?.stop !== undefined) generationConfig.stopSequences = resolvedAsset.sampling.stop;
8994

95+
if (geminiOptions?.candidate_count !== undefined) generationConfig.candidateCount = geminiOptions.candidate_count;
96+
if (geminiOptions?.top_k !== undefined) generationConfig.topK = geminiOptions.top_k;
97+
if (geminiOptions?.seed !== undefined) generationConfig.seed = geminiOptions.seed;
98+
if (geminiOptions?.response_schema !== undefined) generationConfig.responseSchema = geminiOptions.response_schema;
99+
if (geminiOptions?.response_modalities !== undefined) generationConfig.responseModalities = geminiOptions.response_modalities;
100+
101+
if (resolvedAsset.response?.schema !== undefined) generationConfig.responseSchema = resolvedAsset.response.schema;
102+
90103
if (resolvedAsset.response?.format === 'json') {
91104
generationConfig.responseMimeType = 'application/json';
92105
}
93106

94107
// Thinking config
95-
if (resolvedAsset.reasoning?.effort) {
108+
if (geminiOptions?.thinking_budget_tokens !== undefined) {
109+
body.thinkingConfig = {
110+
thinkingBudget: geminiOptions.thinking_budget_tokens,
111+
};
112+
} else if (resolvedAsset.reasoning?.effort) {
96113
body.thinkingConfig = {
97114
thinkingBudget: resolvedAsset.reasoning.effort === 'high' ? 8192 : resolvedAsset.reasoning.effort === 'medium' ? 4096 : 1024,
98115
};

src/providers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,21 @@ export type {
88
RuntimeRenderOptions,
99
} from './types.js';
1010
export { openaiAdapter } from './openai.js';
11+
export { openaiResponsesAdapter } from './openai-responses.js';
1112
export { anthropicAdapter } from './anthropic.js';
1213
export { geminiAdapter } from './gemini.js';
1314
export { openrouterAdapter } from './openrouter.js';
1415

1516
import type { ProviderAdapter } from './types.js';
1617
import { openaiAdapter } from './openai.js';
18+
import { openaiResponsesAdapter } from './openai-responses.js';
1719
import { anthropicAdapter } from './anthropic.js';
1820
import { geminiAdapter } from './gemini.js';
1921
import { openrouterAdapter } from './openrouter.js';
2022

2123
const adapters: Record<string, ProviderAdapter> = {
2224
openai: openaiAdapter,
25+
'openai-responses': openaiResponsesAdapter,
2326
anthropic: anthropicAdapter,
2427
google: geminiAdapter,
2528
gemini: geminiAdapter,

0 commit comments

Comments
 (0)