rename idem_tok -> idempotency_key

This commit is contained in:
Matthew Farrellee 2025-08-19 16:34:02 -04:00
parent 68877f331e
commit 427aba3538
4 changed files with 61 additions and 49 deletions

View file

@ -49,7 +49,7 @@ class Batches(Protocol):
endpoint: str, endpoint: str,
completion_window: Literal["24h"], completion_window: Literal["24h"],
metadata: dict[str, str] | None = None, metadata: dict[str, str] | None = None,
idem_tok: str | None = None, # intentionally bad name idempotency_key: str | None = None,
) -> BatchObject: ) -> BatchObject:
"""Create a new batch for processing multiple API requests. """Create a new batch for processing multiple API requests.
@ -57,7 +57,7 @@ class Batches(Protocol):
:param endpoint: The endpoint to be used for all requests in the batch. :param endpoint: The endpoint to be used for all requests in the batch.
:param completion_window: The time window within which the batch should be processed. :param completion_window: The time window within which the batch should be processed.
:param metadata: Optional metadata for the batch. :param metadata: Optional metadata for the batch.
:param idem_tok: Optional idempotency token. When provided, enables idempotent behavior. :param idempotency_key: Optional idempotency key. When provided, enables idempotent behavior.
:returns: The created batch object. :returns: The created batch object.
""" """
... ...

View file

@ -137,15 +137,15 @@ class ReferenceBatchesImpl(Batches):
endpoint: str, endpoint: str,
completion_window: Literal["24h"], completion_window: Literal["24h"],
metadata: dict[str, str] | None = None, metadata: dict[str, str] | None = None,
idem_tok: str | None = None, idempotency_key: str | None = None,
) -> BatchObject: ) -> BatchObject:
""" """
Create a new batch for processing multiple API requests. Create a new batch for processing multiple API requests.
This implementation provides optional idempotency: when an idempotency token This implementation provides optional idempotency: when an idempotency key
(idem_tok) is provided, a deterministic ID is generated based on the input (idempotency_key) is provided, a deterministic ID is generated based on the input
parameters. If a batch with the same parameters already exists, it will be parameters. If a batch with the same parameters already exists, it will be
returned instead of creating a duplicate. Without an idempotency token, returned instead of creating a duplicate. Without an idempotency key,
each request creates a new batch with a unique ID. each request creates a new batch with a unique ID.
Args: Args:
@ -153,7 +153,7 @@ class ReferenceBatchesImpl(Batches):
endpoint: The endpoint to be used for all requests in the batch. endpoint: The endpoint to be used for all requests in the batch.
completion_window: The time window within which the batch should be processed. completion_window: The time window within which the batch should be processed.
metadata: Optional metadata for the batch. metadata: Optional metadata for the batch.
idem_tok: Optional idempotency token for enabling idempotent behavior. idempotency_key: Optional idempotency key for enabling idempotent behavior.
Returns: Returns:
The created or existing batch object. The created or existing batch object.
@ -190,11 +190,11 @@ class ReferenceBatchesImpl(Batches):
batch_id = f"batch_{uuid.uuid4().hex[:16]}" batch_id = f"batch_{uuid.uuid4().hex[:16]}"
# For idempotent requests, use the idempotency token for the batch ID # For idempotent requests, use the idempotency key for the batch ID
# This ensures the same token always maps to the same batch ID, # This ensures the same key always maps to the same batch ID,
# allowing us to detect parameter conflicts # allowing us to detect parameter conflicts
if idem_tok is not None: if idempotency_key is not None:
hash_input = idem_tok.encode("utf-8") hash_input = idempotency_key.encode("utf-8")
hash_digest = hashlib.sha256(hash_input).hexdigest()[:24] hash_digest = hashlib.sha256(hash_input).hexdigest()[:24]
batch_id = f"batch_{hash_digest}" batch_id = f"batch_{hash_digest}"
@ -208,8 +208,8 @@ class ReferenceBatchesImpl(Batches):
or existing_batch.metadata != metadata or existing_batch.metadata != metadata
): ):
raise ConflictError( raise ConflictError(
f"Idempotency token '{idem_tok}' was previously used with different parameters. " f"Idempotency key '{idempotency_key}' was previously used with different parameters. "
"Either use a new idempotency token or ensure all parameters match the original request." "Either use a new idempotency key or ensure all parameters match the original request."
) )
logger.info(f"Returning existing batch with ID: {batch_id}") logger.info(f"Returning existing batch with ID: {batch_id}")

View file

@ -8,16 +8,16 @@
Integration tests for batch idempotency functionality using the OpenAI client library. Integration tests for batch idempotency functionality using the OpenAI client library.
This module tests the idempotency feature in the batches API using the OpenAI-compatible This module tests the idempotency feature in the batches API using the OpenAI-compatible
client interface. These tests verify that the idempotency token (idem_tok) works correctly client interface. These tests verify that the idempotency key (idempotency_key) works correctly
in a real client-server environment. in a real client-server environment.
Test Categories: Test Categories:
1. Successful Idempotency: Same token returns same batch with identical parameters 1. Successful Idempotency: Same key returns same batch with identical parameters
- test_idempotent_batch_creation_successful: Verifies that requests with the same - test_idempotent_batch_creation_successful: Verifies that requests with the same
idempotency token return identical batches, even with different metadata order idempotency key return identical batches, even with different metadata order
2. Conflict Detection: Same token with conflicting parameters raises HTTP 409 errors 2. Conflict Detection: Same key with conflicting parameters raises HTTP 409 errors
- test_idempotency_conflict_with_different_params: Verifies that reusing an idempotency token - test_idempotency_conflict_with_different_params: Verifies that reusing an idempotency key
with truly conflicting parameters (both file ID and metadata values) raises ConflictError with truly conflicting parameters (both file ID and metadata values) raises ConflictError
""" """
@ -31,7 +31,7 @@ class TestBatchesIdempotencyIntegration:
"""Integration tests for batch idempotency using OpenAI client.""" """Integration tests for batch idempotency using OpenAI client."""
def test_idempotent_batch_creation_successful(self, openai_client): def test_idempotent_batch_creation_successful(self, openai_client):
"""Test that identical requests with same idempotency token return the same batch.""" """Test that identical requests with same idempotency key return the same batch."""
batch1 = openai_client.batches.create( batch1 = openai_client.batches.create(
input_file_id="bogus-id", input_file_id="bogus-id",
endpoint="/v1/chat/completions", endpoint="/v1/chat/completions",
@ -40,7 +40,7 @@ class TestBatchesIdempotencyIntegration:
"test_type": "idempotency_success", "test_type": "idempotency_success",
"purpose": "integration_test", "purpose": "integration_test",
}, },
extra_body={"idem_tok": "test-idempotency-token-1"}, extra_body={"idempotency_key": "test-idempotency-token-1"},
) )
# sleep to ensure different timestamps # sleep to ensure different timestamps
@ -54,7 +54,7 @@ class TestBatchesIdempotencyIntegration:
"purpose": "integration_test", "purpose": "integration_test",
"test_type": "idempotency_success", "test_type": "idempotency_success",
}, # Different order }, # Different order
extra_body={"idem_tok": "test-idempotency-token-1"}, extra_body={"idempotency_key": "test-idempotency-token-1"},
) )
assert batch1.id == batch2.id assert batch1.id == batch2.id
@ -65,13 +65,13 @@ class TestBatchesIdempotencyIntegration:
assert batch1.created_at == batch2.created_at assert batch1.created_at == batch2.created_at
def test_idempotency_conflict_with_different_params(self, openai_client): def test_idempotency_conflict_with_different_params(self, openai_client):
"""Test that using same idempotency token with different params raises conflict error.""" """Test that using same idempotency key with different params raises conflict error."""
batch1 = openai_client.batches.create( batch1 = openai_client.batches.create(
input_file_id="bogus-id-1", input_file_id="bogus-id-1",
endpoint="/v1/chat/completions", endpoint="/v1/chat/completions",
completion_window="24h", completion_window="24h",
metadata={"test_type": "conflict_test_1"}, metadata={"test_type": "conflict_test_1"},
extra_body={"idem_tok": "conflict-token"}, extra_body={"idempotency_key": "conflict-token"},
) )
with pytest.raises(ConflictError) as exc_info: with pytest.raises(ConflictError) as exc_info:
@ -80,7 +80,7 @@ class TestBatchesIdempotencyIntegration:
endpoint="/v1/chat/completions", endpoint="/v1/chat/completions",
completion_window="24h", completion_window="24h",
metadata={"test_type": "conflict_test_2"}, # Different metadata metadata={"test_type": "conflict_test_2"}, # Different metadata
extra_body={"idem_tok": "conflict-token"}, # Same token extra_body={"idempotency_key": "conflict-token"}, # Same token
) )
assert exc_info.value.status_code == 409 assert exc_info.value.status_code == 409

View file

@ -8,14 +8,14 @@
Tests for idempotency functionality in the reference batches provider. Tests for idempotency functionality in the reference batches provider.
This module tests the optional idempotency feature that allows clients to provide This module tests the optional idempotency feature that allows clients to provide
an idempotency token (idem_tok) to ensure that repeated requests with the same token an idempotency key (idempotency_key) to ensure that repeated requests with the same key
and parameters return the same batch, while requests with the same token but different and parameters return the same batch, while requests with the same key but different
parameters result in a conflict error. parameters result in a conflict error.
Test Categories: Test Categories:
1. Core Idempotency: Same parameters with same token return same batch 1. Core Idempotency: Same parameters with same key return same batch
2. Parameter Independence: Different parameters without tokens create different batches 2. Parameter Independence: Different parameters without keys create different batches
3. Conflict Detection: Same token with different parameters raises ConflictError 3. Conflict Detection: Same key with different parameters raises ConflictError
Tests by Category: Tests by Category:
@ -24,20 +24,20 @@ Tests by Category:
- test_idempotent_batch_creation_metadata_order_independence - test_idempotent_batch_creation_metadata_order_independence
2. Parameter Independence: 2. Parameter Independence:
- test_non_idempotent_behavior_without_token - test_non_idempotent_behavior_without_key
- test_different_idempotency_tokens_create_different_batches - test_different_idempotency_keys_create_different_batches
3. Conflict Detection: 3. Conflict Detection:
- test_same_idem_tok_different_params_conflict (parametrized: input_file_id, metadata values, metadata None vs {}) - test_same_idempotency_key_different_params_conflict (parametrized: input_file_id, metadata values, metadata None vs {})
Key Behaviors Tested: Key Behaviors Tested:
- Idempotent batch creation when idem_tok provided with identical parameters - Idempotent batch creation when idempotency_key provided with identical parameters
- Metadata order independence for consistent batch ID generation - Metadata order independence for consistent batch ID generation
- Non-idempotent behavior when no idem_tok provided (random UUIDs) - Non-idempotent behavior when no idempotency_key provided (random UUIDs)
- Conflict detection for parameter mismatches with same idempotency token - Conflict detection for parameter mismatches with same idempotency key
- Deterministic ID generation based solely on idempotency token - Deterministic ID generation based solely on idempotency key
- Proper error handling with detailed conflict messages including token and error codes - Proper error handling with detailed conflict messages including key and error codes
- Protection against idempotency token reuse with different request parameters - Protection against idempotency key reuse with different request parameters
""" """
import asyncio import asyncio
@ -51,14 +51,14 @@ class TestReferenceBatchesIdempotency:
"""Test suite for idempotency functionality in the reference implementation.""" """Test suite for idempotency functionality in the reference implementation."""
async def test_idempotent_batch_creation_same_params(self, provider, sample_batch_data): async def test_idempotent_batch_creation_same_params(self, provider, sample_batch_data):
"""Test that creating batches with identical parameters returns the same batch when idem_tok is provided.""" """Test that creating batches with identical parameters returns the same batch when idempotency_key is provided."""
del sample_batch_data["metadata"] del sample_batch_data["metadata"]
batch1 = await provider.create_batch( batch1 = await provider.create_batch(
**sample_batch_data, **sample_batch_data,
metadata={"test": "value1", "other": "value2"}, metadata={"test": "value1", "other": "value2"},
idem_tok="unique-token-1", idempotency_key="unique-token-1",
) )
# sleep for 1 second to allow created_at timestamps to be different # sleep for 1 second to allow created_at timestamps to be different
@ -67,7 +67,7 @@ class TestReferenceBatchesIdempotency:
batch2 = await provider.create_batch( batch2 = await provider.create_batch(
**sample_batch_data, **sample_batch_data,
metadata={"other": "value2", "test": "value1"}, # Different order metadata={"other": "value2", "test": "value1"}, # Different order
idem_tok="unique-token-1", idempotency_key="unique-token-1",
) )
assert batch1.id == batch2.id assert batch1.id == batch2.id
@ -75,20 +75,32 @@ class TestReferenceBatchesIdempotency:
assert batch1.metadata == batch2.metadata assert batch1.metadata == batch2.metadata
assert batch1.created_at == batch2.created_at assert batch1.created_at == batch2.created_at
async def test_different_idempotency_tokens_create_different_batches(self, provider, sample_batch_data): async def test_different_idempotency_keys_create_different_batches(self, provider, sample_batch_data):
"""Test that different idempotency tokens create different batches even with same params.""" """Test that different idempotency keys create different batches even with same params."""
batch1 = await provider.create_batch( batch1 = await provider.create_batch(
**sample_batch_data, **sample_batch_data,
idem_tok="token-A", idempotency_key="token-A",
) )
batch2 = await provider.create_batch( batch2 = await provider.create_batch(
**sample_batch_data, **sample_batch_data,
idem_tok="token-B", idempotency_key="token-B",
) )
assert batch1.id != batch2.id assert batch1.id != batch2.id
async def test_non_idempotent_behavior_without_key(self, provider, sample_batch_data):
"""Test that batches without idempotency key create unique batches even with identical parameters."""
batch1 = await provider.create_batch(**sample_batch_data)
batch2 = await provider.create_batch(**sample_batch_data)
assert batch1.id != batch2.id
assert batch1.input_file_id == batch2.input_file_id
assert batch1.endpoint == batch2.endpoint
assert batch1.completion_window == batch2.completion_window
assert batch1.metadata == batch2.metadata
@pytest.mark.parametrize( @pytest.mark.parametrize(
"param_name,first_value,second_value", "param_name,first_value,second_value",
[ [
@ -97,17 +109,17 @@ class TestReferenceBatchesIdempotency:
("metadata", None, {}), ("metadata", None, {}),
], ],
) )
async def test_same_idem_tok_different_params_conflict( async def test_same_idempotency_key_different_params_conflict(
self, provider, sample_batch_data, param_name, first_value, second_value self, provider, sample_batch_data, param_name, first_value, second_value
): ):
"""Test that same idem_tok with different parameters raises conflict error.""" """Test that same idempotency_key with different parameters raises conflict error."""
sample_batch_data["idem_tok"] = "same-token" sample_batch_data["idempotency_key"] = "same-token"
sample_batch_data[param_name] = first_value sample_batch_data[param_name] = first_value
batch1 = await provider.create_batch(**sample_batch_data) batch1 = await provider.create_batch(**sample_batch_data)
with pytest.raises(ConflictError, match="Idempotency token.*was previously used with different parameters"): with pytest.raises(ConflictError, match="Idempotency key.*was previously used with different parameters"):
sample_batch_data[param_name] = second_value sample_batch_data[param_name] = second_value
await provider.create_batch(**sample_batch_data) await provider.create_batch(**sample_batch_data)