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
6 changes: 3 additions & 3 deletions .castiron.stats.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
schema_version: 1
generation_id: 629365c8-c19e-4349-b8d4-1815a9bfa729
generation_id: baa135fa-765a-4ab9-9ec1-5c9e432437ce
openapi_spec_hash: f56829f9ec48b31ed299c5fb497984d7
openapi_transformed_spec_hash: 400d42fb468bbe078e4f0c87c366490b
config_hash: db72038ac88cdb94ae1cafef6aada78e
codegen_sha: a1d286ab5c57c5047e0acc339c0a2359d33678f9
codegen_sha: 17f71f06a352b4e7b5ef168c3fd4182844620954
codegen_hash: e5f930fec1f997ea02bd1746597be5ff2a24c1917743b5bf2d4a2ee617ef8e3b
public_codegen_sha: 5d14ef0360d1e1cfd09ff815f3a8339c8498e604
public_codegen_sha: 249df1f919236e7aa68b460c504dafdee7c23bb1
14 changes: 14 additions & 0 deletions src/openai/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,20 @@ def _process_response_data(
return cast(ResponseT, cast_to.build(response=response, data=data))

if self._strict_response_validation:
# Temporary compatibility for the file-batch discriminator typo. Keep
# the published Literal until the next major, but validate real responses.
# Remove when VectorStoreFileBatch accepts the canonical spelling.
if is_dict(data) and data.get("object") == "vector_store.file_batch":
from .types.vector_stores import VectorStoreFileBatch

if cast_to is VectorStoreFileBatch:
validated = validate_type(
type_=VectorStoreFileBatch, value={**data, "object": "vector_store.files_batch"}
)
# Restore only this field on the validated instance; do not mutate
# the response or skip validation of any other field.
object.__setattr__(validated, "object", data["object"])
return cast(ResponseT, validated)
return cast(ResponseT, validate_type(type_=cast_to, value=data))

return cast(ResponseT, construct_type(type_=cast_to, value=data))
Expand Down
175 changes: 175 additions & 0 deletions tests/test_vector_batch_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from __future__ import annotations

import json
from typing import Any

import httpx2
import pytest
from pydantic import ValidationError

from openai import OpenAI, AsyncOpenAI, APIResponseValidationError
from openai._compat import parse_obj, model_dump
from openai.types.vector_stores import VectorStoreFileBatch


def batch_payload(object_value: Any = "vector_store.file_batch") -> dict[str, Any]:
return {
"id": "vsfb_test",
"object": object_value,
"created_at": 123,
"file_counts": {"cancelled": 0, "completed": 1, "failed": 0, "in_progress": 0, "total": 1},
"status": "completed",
"vector_store_id": "vs_test",
"future_field": {"preserved": True},
}


def transport(payload: dict[str, Any]) -> httpx2.MockTransport:
return httpx2.MockTransport(
lambda _request: httpx2.Response(200, json=payload, headers={"x-request-id": "req_test"})
)


def assert_preserved(result: VectorStoreFileBatch, payload: dict[str, Any]) -> None:
assert model_dump(result) == payload
assert json.loads(result.to_json()) == payload
assert set(payload) - {"future_field"} <= result.model_fields_set
assert result._request_id == "req_test"


@pytest.mark.parametrize("object_value", ["vector_store.file_batch", "vector_store.files_batch"])
@pytest.mark.parametrize("strict", [False, True])
@pytest.mark.parametrize("operation", ["create", "retrieve", "cancel", "poll"])
def test_sync_responses(object_value: str, strict: bool, operation: str) -> None:
payload = batch_payload(object_value)
with OpenAI(
api_key="test-key",
_strict_response_validation=strict,
http_client=httpx2.Client(transport=transport(payload)),
) as client:
batches = client.vector_stores.file_batches
if operation == "create":
result = batches.create(vector_store_id="vs_test", file_ids=["file_test"])
elif operation == "cancel":
result = batches.cancel("vsfb_test", vector_store_id="vs_test")
elif operation == "poll":
result = batches.poll("vsfb_test", vector_store_id="vs_test")
else:
result = batches.retrieve("vsfb_test", vector_store_id="vs_test")
assert_preserved(result, payload)


@pytest.mark.parametrize("object_value", ["vector_store.file_batch", "vector_store.files_batch"])
@pytest.mark.parametrize("strict", [False, True])
@pytest.mark.parametrize("operation", ["create", "retrieve", "cancel", "poll"])
async def test_async_responses(object_value: str, strict: bool, operation: str) -> None:
payload = batch_payload(object_value)
async with AsyncOpenAI(
api_key="test-key",
_strict_response_validation=strict,
http_client=httpx2.AsyncClient(transport=transport(payload)),
) as client:
batches = client.vector_stores.file_batches
if operation == "create":
result = await batches.create(vector_store_id="vs_test", file_ids=["file_test"])
elif operation == "cancel":
result = await batches.cancel("vsfb_test", vector_store_id="vs_test")
elif operation == "poll":
result = await batches.poll("vsfb_test", vector_store_id="vs_test")
else:
result = await batches.retrieve("vsfb_test", vector_store_id="vs_test")
assert_preserved(result, payload)


@pytest.mark.parametrize(
"change",
[
{"object": "unknown"},
{"object": None},
{"object": 123},
{"status": "unknown"},
{"file_counts": {"total": "invalid"}},
{"created_at": "invalid"},
],
)
def test_other_validation_errors_survive(change: dict[str, Any]) -> None:
payload = {**batch_payload(), **change}
with OpenAI(
api_key="test-key",
_strict_response_validation=True,
http_client=httpx2.Client(transport=transport(payload)),
) as client:
with pytest.raises(APIResponseValidationError) as error:
client.vector_stores.file_batches.retrieve("vsfb_test", vector_store_id="vs_test")
assert error.value.body == payload


async def test_async_other_fields_still_validate() -> None:
payload = {**batch_payload(), "status": "unknown"}
async with AsyncOpenAI(
api_key="test-key",
_strict_response_validation=True,
http_client=httpx2.AsyncClient(transport=transport(payload)),
) as client:
with pytest.raises(APIResponseValidationError) as error:
await client.vector_stores.file_batches.retrieve("vsfb_test", vector_store_id="vs_test")
assert error.value.body == payload


def test_missing_discriminator_still_fails() -> None:
payload = batch_payload()
del payload["object"]
with OpenAI(
api_key="test-key",
_strict_response_validation=True,
http_client=httpx2.Client(transport=transport(payload)),
) as client:
with pytest.raises(APIResponseValidationError):
client.vector_stores.file_batches.retrieve("vsfb_test", vector_store_id="vs_test")


def test_validation_copy_does_not_modify_input() -> None:
payload = batch_payload()
with OpenAI(
api_key="test-key",
_strict_response_validation=True,
http_client=httpx2.Client(transport=transport(payload)),
) as client:
result = client._process_response_data(
data=payload, cast_to=VectorStoreFileBatch, response=httpx2.Response(200)
)
assert payload == batch_payload()
assert model_dump(result) == payload
assert (
result.model_fields_set
== parse_obj(VectorStoreFileBatch, batch_payload("vector_store.files_batch")).model_fields_set
)


def test_direct_validation_and_other_models_are_unchanged() -> None:
legacy = batch_payload("vector_store.files_batch")
assert model_dump(parse_obj(VectorStoreFileBatch, legacy)) == legacy
with pytest.raises(ValidationError):
parse_obj(VectorStoreFileBatch, batch_payload())

class OtherBatch(VectorStoreFileBatch):
pass

with OpenAI(
api_key="test-key",
_strict_response_validation=True,
http_client=httpx2.Client(transport=transport(batch_payload())),
) as client:
with pytest.raises(APIResponseValidationError):
client._process_response_data(
data=batch_payload(),
cast_to=OtherBatch,
response=httpx2.Response(200, request=httpx2.Request("GET", "https://example.test")),
)


def test_unknown_value_remains_permissive_without_strict_validation() -> None:
payload = batch_payload("unknown")
with OpenAI(api_key="test-key", http_client=httpx2.Client(transport=transport(payload))) as client:
result = client.vector_stores.file_batches.retrieve("vsfb_test", vector_store_id="vs_test")
assert_preserved(result, payload)
Loading