Skip to content

Commit c2b5c44

Browse files
authored
fix: populate required OpenResponses fields with non-null defaults (#4994)
# What does this PR do? The OpenResponses conformance suite validates responses using strict Zod schemas. Several fields that the OpenAI spec marks as **optional** (omittable) are treated by OpenResponses as **required and non-nullable**. llama-stack was returning \`null\` for these fields, failing Zod validation before any semantic checks could run. ## Why not just change the schema types? The Python schema in `openai_responses.py` is the source of truth for our generated OpenAPI spec, which is diffed against `openai-spec-2.3.0.yml` via `oasdiff --check-regression` in pre-commit. Changing a field from `list | None` to `list` (non-nullable) would alter the generated spec and lower our OpenAI conformance score. **Example `logprobs` on `OutputTextContent`:** The OpenAI spec at `docs/static/openai-spec-2.3.0.yml` defines: ```yaml OutputTextContent: properties: logprobs: items: $ref: '#/components/schemas/LogProb' type: array # non-nullable when present required: - type - text - annotations # logprobs is NOT required — field may be omitted ``` The OpenAI spec says: *if logprobs is present it must be an array, never null* but the field itself is optional (can be absent). Our old code returned `null`, which violates even the OpenAI spec. OpenResponses goes further and requires the field to always be present. The fix: **keep the schema as `list | None = None`** (preserving our oasdiff baseline) but **always emit `[]` in construction code** when logprobs aren't available. This satisfies both: it's a valid non-null array per OpenAI spec, and it's always present per OpenResponses. The same rationale applies to every other field fixed in this PR. ## Fields fixed For each field the schema definition is unchanged; only the construction code is updated to emit a concrete non-null default: - `background`: always `False` for non-background responses (was `None`) - `tool_choice`: defaults to `"auto"` when not specified (was `None`) - `truncation`: defaults to `"disabled"` when not specified (was `None`) - `service_tier`: defaults to `"default"` when not specified (was `None`) - `tools`: always an array; `available_tools()` already returns `[]` not `None` - `temperature`: defaults to `1.0` when not specified (was `None`) - `top_p`: defaults to `1.0` when not specified (was `None`) - `logprobs`: always `[]` when not requested (was `None`, which also violates the OpenAI spec) Closes #4987 Closes #4988 Closes #4990 ## Test Plan - Integration tests for all three modes (docker, library, server) with GPT provider pass - `conformance.mdx` score is unchanged (no regression in OpenAI spec conformance) - OpenResponses conformance test (informational) passes Signed-off-by: Charlie Doern <cdoern@redhat.com>
1 parent 0fc1c91 commit c2b5c44

4 files changed

Lines changed: 17 additions & 10 deletions

File tree

src/llama_stack/providers/inline/agents/meta_reference/responses/streaming.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,16 +215,21 @@ async def _create_refusal_response(self, violation_message: str) -> OpenAIRespon
215215

216216
# Create a completed refusal response
217217
refusal_response = OpenAIResponseObject(
218+
background=False,
218219
id=self.response_id,
219220
created_at=self.created_at,
220221
model=self.ctx.model,
221222
status="completed",
222223
output=[OpenAIResponseMessage(role="assistant", content=[refusal_content], type="message")],
224+
temperature=self.ctx.temperature if self.ctx.temperature is not None else 1.0,
225+
top_p=self.ctx.top_p if self.ctx.top_p is not None else 1.0,
226+
tools=self.ctx.available_tools(),
227+
tool_choice=self.ctx.tool_choice or OpenAIResponseInputToolChoiceMode.auto,
228+
truncation=self.truncation or "disabled",
223229
max_output_tokens=self.max_output_tokens,
224230
safety_identifier=self.safety_identifier,
225-
service_tier=self.service_tier,
231+
service_tier=self.service_tier or "default",
226232
metadata=self.metadata,
227-
truncation=self.truncation,
228233
store=self.store,
229234
prompt_cache_key=self.prompt_cache_key,
230235
)
@@ -250,6 +255,7 @@ def _snapshot_response(
250255
) -> OpenAIResponseObject:
251256
completed_at = int(time.time()) if status == "completed" else None
252257
return OpenAIResponseObject(
258+
background=False,
253259
created_at=self.created_at,
254260
completed_at=completed_at,
255261
id=self.response_id,
@@ -258,9 +264,10 @@ def _snapshot_response(
258264
status=status,
259265
output=self._clone_outputs(outputs),
260266
text=self.text,
261-
top_p=self.ctx.top_p,
267+
temperature=self.ctx.temperature if self.ctx.temperature is not None else 1.0,
268+
top_p=self.ctx.top_p if self.ctx.top_p is not None else 1.0,
262269
tools=self.ctx.available_tools(),
263-
tool_choice=self.ctx.tool_choice,
270+
tool_choice=self.ctx.tool_choice or OpenAIResponseInputToolChoiceMode.auto,
264271
error=error,
265272
incomplete_details=incomplete_details,
266273
usage=self.accumulated_usage,
@@ -271,9 +278,9 @@ def _snapshot_response(
271278
reasoning=self.reasoning,
272279
max_output_tokens=self.max_output_tokens,
273280
safety_identifier=self.safety_identifier,
274-
service_tier=self.service_tier,
281+
service_tier=self.service_tier or "default",
275282
metadata=self.metadata,
276-
truncation=self.truncation,
283+
truncation=self.truncation or "disabled",
277284
store=self.store,
278285
prompt_cache_key=self.prompt_cache_key,
279286
)
@@ -1050,7 +1057,7 @@ async def _process_streaming_chunks(
10501057
OpenAIResponseOutputMessageContentOutputText(
10511058
text=final_text,
10521059
annotations=[],
1053-
logprobs=chat_response_logprobs if chat_response_logprobs else None,
1060+
logprobs=chat_response_logprobs if chat_response_logprobs else [],
10541061
)
10551062
)
10561063

src/llama_stack/providers/inline/agents/meta_reference/responses/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ async def convert_chat_choice_to_response_message(
110110
output_content = choice.message.content or ""
111111

112112
annotations, clean_text = _extract_citations_from_text(output_content, citation_files or {})
113-
logprobs = choice.logprobs.content if choice.logprobs and choice.logprobs.content else None
113+
logprobs = choice.logprobs.content if choice.logprobs and choice.logprobs.content else []
114114

115115
return OpenAIResponseMessage(
116116
id=message_id or f"msg_{uuid.uuid4()}",

src/llama_stack_api/openai_responses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,7 @@ class OpenAIResponseIncompleteDetails(BaseModel):
712712
class OpenAIResponseObject(BaseModel):
713713
"""Complete OpenAI response object containing generation results and metadata.
714714
715-
:param background: Whether this response was run in background mode
715+
:param background: Whether this response was run in background mode (default: False)
716716
:param created_at: Unix timestamp when the response was created
717717
:param completed_at: (Optional) Unix timestamp when the response was completed
718718
:param error: (Optional) Error details if the response generation failed

tests/integration/responses/test_basic_responses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ def test_include_logprobs_non_streaming(client_with_models, text_model_id):
243243
assert len(response_w_o_logprobs.output) == 1
244244
message_outputs = [output for output in response_w_o_logprobs.output if output.type == "message"]
245245
assert len(message_outputs) == 1, f"Expected one message output, got {len(message_outputs)}"
246-
assert message_outputs[0].content[0].logprobs is None, "Expected no logprobs in the returned response"
246+
assert message_outputs[0].content[0].logprobs == [], "Expected no logprobs in the returned response"
247247

248248
# Create a response with include["message.output_text.logprobs"]
249249
response_with_logprobs = client_with_models.responses.create(

0 commit comments

Comments
 (0)