|
6 | 6 | from pydantic import BaseModel, Field |
7 | 7 |
|
8 | 8 | from bespokelabs import curator |
| 9 | +from bespokelabs.curator.request_processor.batch.azure_batch_request_processor import AzureBatchRequestProcessor |
9 | 10 | from bespokelabs.curator.request_processor.batch.mistral_batch_request_processor import MistralBatchRequestProcessor |
10 | 11 | from bespokelabs.curator.request_processor.config import BatchRequestProcessorConfig |
11 | 12 | from bespokelabs.curator.types.generic_batch import GenericBatch, GenericBatchRequestCounts, GenericBatchStatus |
@@ -84,6 +85,83 @@ def test_mistral_batch_response_includes_normalized_finish_reason() -> None: |
84 | 85 | assert response.finish_reason == "length" |
85 | 86 |
|
86 | 87 |
|
| 88 | +def test_azure_batch_requires_base_url() -> None: |
| 89 | + """Azure batch backend needs a resource-specific base_url; there's no sensible default.""" |
| 90 | + config = BatchRequestProcessorConfig(model="gpt-4o", azure_deployment="my-gpt4o-deployment") |
| 91 | + with pytest.raises(ValueError, match="base_url"): |
| 92 | + AzureBatchRequestProcessor(config) |
| 93 | + |
| 94 | + |
| 95 | +def test_azure_batch_requires_deployment_name() -> None: |
| 96 | + """Azure batch backend needs an explicit deployment name distinct from the model name.""" |
| 97 | + config = BatchRequestProcessorConfig(model="gpt-4o", base_url="https://my-resource.openai.azure.com/openai/v1/") |
| 98 | + with pytest.raises(ValueError, match="azure_deployment"): |
| 99 | + AzureBatchRequestProcessor(config) |
| 100 | + |
| 101 | + |
| 102 | +def test_azure_batch_endpoint_omits_v1_prefix() -> None: |
| 103 | + """Azure's batches.create() `endpoint` argument omits the "/v1" prefix OpenAI's API uses.""" |
| 104 | + assert AzureBatchRequestProcessor._BATCH_ENDPOINT == "/chat/completions" |
| 105 | + |
| 106 | + |
| 107 | +def test_azure_batch_request_uses_deployment_name_not_model() -> None: |
| 108 | + """The request body's `model` field must be the Azure deployment name, not curator's `model` config. |
| 109 | +
|
| 110 | + `model` stays the underlying model name (e.g. "gpt-4o") since that's what's used for litellm-based |
| 111 | + cost lookups, while Azure requires the deployment name to route the actual API request. |
| 112 | + """ |
| 113 | + processor = AzureBatchRequestProcessor.__new__(AzureBatchRequestProcessor) |
| 114 | + processor.config = BatchRequestProcessorConfig( |
| 115 | + model="gpt-4o", |
| 116 | + base_url="https://my-resource.openai.azure.com/openai/v1/", |
| 117 | + azure_deployment="my-gpt4o-deployment", |
| 118 | + ) |
| 119 | + |
| 120 | + api_request = processor.create_api_specific_request_batch(_generic_request()) |
| 121 | + |
| 122 | + assert api_request["body"]["model"] == "my-gpt4o-deployment" |
| 123 | + assert api_request["url"] == "/v1/chat/completions" |
| 124 | + |
| 125 | + |
| 126 | +def test_azure_cost_processor_prefixes_model_for_litellm_lookup(monkeypatch) -> None: |
| 127 | + """_AzureCostProcessor should query litellm's `azure/`-prefixed pricing table, not the bare model name.""" |
| 128 | + import litellm |
| 129 | + |
| 130 | + from bespokelabs.curator.cost import _AzureCostProcessor |
| 131 | + |
| 132 | + config = BatchRequestProcessorConfig(model="gpt-4o", base_url="https://x.openai.azure.com/openai/v1/", azure_deployment="d") |
| 133 | + processor = _AzureCostProcessor(config=config, batch=False) |
| 134 | + |
| 135 | + monkeypatch.setitem(litellm.model_cost, "azure/gpt-4o", {"input_cost_per_token": 1.0, "output_cost_per_token": 1.0}) |
| 136 | + |
| 137 | + calls = {} |
| 138 | + |
| 139 | + def fake_completion_cost(**kwargs): |
| 140 | + calls.update(kwargs) |
| 141 | + return 1.23 |
| 142 | + |
| 143 | + monkeypatch.setattr(litellm, "completion_cost", fake_completion_cost) |
| 144 | + |
| 145 | + cost = processor.cost(model="gpt-4o", prompt="hi", completion="there") |
| 146 | + |
| 147 | + assert calls["model"] == "azure/gpt-4o" |
| 148 | + assert cost == 1.23 |
| 149 | + |
| 150 | + |
| 151 | +def test_azure_cost_processor_falls_back_without_azure_specific_pricing() -> None: |
| 152 | + """When litellm has no `azure/`-prefixed entry, fall back to the default bare-model lookup.""" |
| 153 | + from bespokelabs.curator.cost import _AzureCostProcessor |
| 154 | + |
| 155 | + config = BatchRequestProcessorConfig(model="some-unlisted-model", base_url="https://x.openai.azure.com/openai/v1/", azure_deployment="d") |
| 156 | + processor = _AzureCostProcessor(config=config, batch=False) |
| 157 | + |
| 158 | + # Neither "azure/some-unlisted-model" nor "some-unlisted-model" are real litellm models, |
| 159 | + # so the fallback lookup should just return 0.0 rather than raising. |
| 160 | + cost = processor.cost(model="some-unlisted-model", prompt="hi", completion="there") |
| 161 | + |
| 162 | + assert cost == 0.0 |
| 163 | + |
| 164 | + |
87 | 165 | def batch_call(model_name, prompts): |
88 | 166 | dataset: Dataset = Dataset.from_dict({"prompt": prompts}) |
89 | 167 | llm = curator.LLM( |
|
0 commit comments