-
Notifications
You must be signed in to change notification settings - Fork 612
feat(responses): deliver structured output to routed openai-chat models #985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
4a00efb
8f82412
7ff30e6
df4dbe7
1ebdd83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,9 +50,10 @@ existing providers, routing, OAuth, and sidecars apply. | |
| The compatibility surface supports `model`, `messages`, `stream`, function tools | ||
| and tool choice, token limits, temperature/top-p/stop, reasoning effort, parallel | ||
| tool calls, prompt cache keys, metadata, and `response_format` on native Responses | ||
| routes. Routed `openai-chat` models reject `response_format` with HTTP 400 because | ||
| their structured-output support is not verified. Other Chat Completions fields, | ||
| including penalties, `n`, and logprobs, are not currently supported. | ||
| routes and routed `openai-chat` models (`json_object` and `json_schema` are | ||
| forwarded as-is; a backend without structured-output support returns its own | ||
| error). Other Chat Completions fields, including penalties, `n`, and logprobs, | ||
|
Comment on lines
+53
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Correct the native Responses wire-field description.
State that routed Proposed fix- tool calls, prompt cache keys, metadata, and `response_format` on native Responses
- routes and routed `openai-chat` models (`json_object` and `json_schema` are
- forwarded as-is; a backend without structured-output support returns its own
- error). Other Chat Completions fields, including penalties, `n`, and logprobs,
+ tool calls, prompt cache keys, metadata, and structured output. Chat Completions
+ `response_format` is translated to Responses `text.format` for native Responses
+ providers and forwarded as `response_format` to routed `openai-chat` providers.
+ A backend without structured-output support returns its own error. Other Chat
+ Completions fields, including penalties, `n`, and logprobs,🤖 Prompt for AI Agents |
||
| are not currently supported. | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -818,6 +818,26 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (provider.promptCacheKey && parsed.options.promptCacheKey !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body.prompt_cache_key = parsed.options.promptCacheKey; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Responses `text.format` -> chat `response_format`. json_object maps 1:1; json_schema | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // re-nests the flattened Responses fields under `json_schema` — the exact inverse of | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // responseFormatToText in src/chat/inbound.ts. Forwarded unconditionally (like `stop`): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // response_format is a first-class Chat Completions field, it is only present when the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // caller explicitly asked for structured output, and a backend that rejects it should | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // fail loud rather than silently return prose the caller will try to JSON.parse. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const textFormat = parsed.options.textFormat; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (textFormat?.type === "json_object") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body.response_format = { type: "json_object" }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (textFormat?.type === "json_schema" && textFormat.schema !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body.response_format = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: "json_schema", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json_schema: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: textFormat.name ?? "response", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...(textFormat.description !== undefined ? { description: textFormat.description } : {}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| schema: textFormat.schema, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...(textFormat.strict !== undefined ? { strict: textFormat.strict } : {}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+827
to
+840
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Preserve schema-less Line 830 drops Remove the Proposed fix- } else if (textFormat?.type === "json_schema" && textFormat.schema !== undefined) {
+ } else if (textFormat?.type === "json_schema") {
body.response_format = {
type: "json_schema",
json_schema: {
name: textFormat.name ?? "response",
...(textFormat.description !== undefined ? { description: textFormat.description } : {}),
- schema: textFormat.schema,
+ ...(textFormat.schema !== undefined ? { schema: textFormat.schema } : {}),
...(textFormat.strict !== undefined ? { strict: textFormat.strict } : {}),
},
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (tools) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Default-ON for chat-completions providers (user decision 260709): the buffered | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1741,6 +1741,15 @@ async function handleResponsesInner( | |
| delete parsed._webSearch; | ||
| delete parsed.options.toolChoice; | ||
| delete parsed.options.parallelToolCalls; | ||
| // The compaction turn is a plain prose summary; a surviving structured-output format | ||
| // would force schema-constrained JSON into the synthetic compaction item. The flag and | ||
| // the raw `text` controls go too: Kiro's capability guard reads both and would reject | ||
| // the turn outright, and the key-mode openai-responses adapter builds from _rawBody. | ||
| delete parsed.options.textFormat; | ||
|
coderabbitai[bot] marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When routed compaction is sent to a noncanonical Useful? React with 👍 / 👎. |
||
| delete parsed._structuredOutput; | ||
| if (parsed._rawBody && typeof parsed._rawBody === "object") { | ||
| delete (parsed._rawBody as Record<string, unknown>).text; | ||
| } | ||
| parsed.context.messages.push({ role: "user", content: COMPACT_PROMPT, timestamp: Date.now() }); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -398,7 +398,9 @@ describe("routed compaction for key-mode openai-responses (#422)", () => { | |
| }) as typeof fetch; | ||
|
|
||
| const res = await handleResponses( | ||
| compactionRequest(baseCompactionBody()), | ||
| compactionRequest(baseCompactionBody({ | ||
| text: { format: { type: "json_schema", name: "answer", schema: { type: "object" } } }, | ||
| })), | ||
| keyProviderConfig(), | ||
| { model: "", provider: "" }, | ||
| ); | ||
|
|
@@ -411,13 +413,39 @@ describe("routed compaction for key-mode openai-responses (#422)", () => { | |
| expect(sent.tools).toBeUndefined(); | ||
| expect(sent.tool_choice).toBeUndefined(); | ||
| expect(sent.parallel_tool_calls).toBeUndefined(); | ||
| // The summarizer must stay prose: a surviving text.format would force schema JSON. | ||
| expect(sent.text).toBeUndefined(); | ||
| expect(JSON.stringify(input)).toContain("CONTEXT CHECKPOINT COMPACTION"); | ||
|
|
||
| const json = await res.json() as { output?: Array<{ type?: string }> }; | ||
| const compactionItems = (json.output ?? []).filter(item => item.type === "compaction"); | ||
|
Comment on lines
420
to
421
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Remove the duplicate Each test callback declares
Based on learnings, repeated 📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Learnings |
||
| expect(compactionItems.length).toBe(1); | ||
| }); | ||
|
|
||
| test("routed chat compaction drops the structured-output format", async () => { | ||
| const bodies: Array<Record<string, unknown>> = []; | ||
| globalThis.fetch = (async (_url: unknown, init?: RequestInit) => { | ||
| bodies.push(JSON.parse(String(init?.body ?? "{}")) as Record<string, unknown>); | ||
| return jsonResponse({ | ||
| choices: [{ index: 0, message: { role: "assistant", content: "handoff summary" }, finish_reason: "stop" }], | ||
| usage: { prompt_tokens: 10, completion_tokens: 5 }, | ||
| }); | ||
| }) as typeof fetch; | ||
|
|
||
| const res = await handleResponses( | ||
| compactionRequest(baseCompactionBody({ text: { format: { type: "json_object" } } })), | ||
| keyProviderConfig({ adapter: "openai-chat" }), | ||
| { model: "", provider: "" }, | ||
| ); | ||
|
|
||
| expect(bodies.length).toBe(1); | ||
| // The compaction turn is a prose summary; the caller's structured-output request must not | ||
| // constrain it (core.ts routedCompaction deletes options.textFormat). | ||
| expect(bodies[0]!.response_format).toBeUndefined(); | ||
| const json = await res.json() as { output?: Array<{ type?: string }> }; | ||
| expect((json.output ?? []).filter(item => item.type === "compaction").length).toBe(1); | ||
| }); | ||
|
|
||
| test("strips additional_tools even when top-level tools are absent", async () => { | ||
| const bodies: Array<Record<string, unknown>> = []; | ||
| globalThis.fetch = (async (_url: unknown, init?: RequestInit) => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this change exposes
response_formatsupport to OpenAI-compatible clients, updating onlydocs/github-copilot-app.mdleaves the hosteddocs-sitereference/guides without the new behavior; users reading the public docs still have no indication that routedopenai-chatcan accept structured output. Add the correspondingdocs-site/update, including locales if relevant, alongside this docs change.AGENTS.md reference: AGENTS.md:L224-L225
Useful? React with 👍 / 👎.