Skip to content

Commit a2732bb

Browse files
Address PR review: optional billing fields
Honor the "all billing fields optional" contract surfaced in review: - Python: deserialize present-but-empty `tokenPrices`/`longContext` objects (`{}`) into instances with all fields None rather than collapsing them to None, using presence (`is not None`) checks instead of truthiness. - Java: change `ModelBilling.multiplier` from primitive `double` to boxed `Double` so "not present on the wire" is distinguishable from an explicit 0.0; the global NON_NULL ObjectMapper omits it when null. Add tests covering both behaviors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0ac3905 commit a2732bb

4 files changed

Lines changed: 45 additions & 6 deletions

File tree

java/src/main/java/com/github/copilot/rpc/ModelBilling.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@
1616
public class ModelBilling {
1717

1818
@JsonProperty("multiplier")
19-
private double multiplier;
19+
private Double multiplier;
2020

2121
@JsonProperty("tokenPrices")
2222
private ModelBillingTokenPrices tokenPrices;
2323

24-
public double getMultiplier() {
24+
public Double getMultiplier() {
2525
return multiplier;
2626
}
2727

28-
public ModelBilling setMultiplier(double multiplier) {
28+
public ModelBilling setMultiplier(Double multiplier) {
2929
this.multiplier = multiplier;
3030
return this;
3131
}

java/src/test/java/com/github/copilot/MetadataApiTest.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ void testModelInfoDeserialization() throws Exception {
186186

187187
// Billing
188188
assertNotNull(model.getBilling());
189-
assertEquals(1.5, model.getBilling().getMultiplier());
189+
assertEquals(Double.valueOf(1.5), model.getBilling().getMultiplier());
190190

191191
// Token prices
192192
ModelBillingTokenPrices tokenPrices = model.getBilling().getTokenPrices();
@@ -206,6 +206,15 @@ void testModelInfoDeserialization() throws Exception {
206206
assertEquals(1000000, longContext.getContextMax());
207207
}
208208

209+
@Test
210+
void testModelBillingSerializationOmitsNullMultiplier() throws Exception {
211+
var billing = new ModelBilling();
212+
213+
String json = MAPPER.writeValueAsString(billing);
214+
215+
assertFalse(json.contains("multiplier"));
216+
}
217+
209218
@Test
210219
void testGetModelsResponseDeserialization() throws Exception {
211220
String json = """

python/copilot/client.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,7 @@ def from_dict(obj: Any) -> ModelBillingTokenPrices:
737737
long_context_dict = obj.get("longContext")
738738
long_context = (
739739
ModelBillingTokenPricesLongContext.from_dict(long_context_dict)
740-
if long_context_dict
740+
if long_context_dict is not None
741741
else None
742742
)
743743
return ModelBillingTokenPrices(
@@ -779,7 +779,9 @@ def from_dict(obj: Any) -> ModelBilling:
779779
multiplier = obj.get("multiplier")
780780
token_prices_dict = obj.get("tokenPrices")
781781
token_prices = (
782-
ModelBillingTokenPrices.from_dict(token_prices_dict) if token_prices_dict else None
782+
ModelBillingTokenPrices.from_dict(token_prices_dict)
783+
if token_prices_dict is not None
784+
else None
783785
)
784786
return ModelBilling(
785787
multiplier=float(multiplier) if multiplier is not None else None,

python/test_client.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,34 @@ def test_token_prices_absent(self):
552552
assert billing.token_prices is None
553553
assert billing.to_dict() == {"multiplier": 1.0}
554554

555+
def test_token_prices_empty_object_round_trip(self):
556+
"""ModelBilling preserves present but empty tokenPrices."""
557+
billing = ModelBilling.from_dict({"tokenPrices": {}})
558+
559+
assert isinstance(billing.token_prices, ModelBillingTokenPrices)
560+
prices = billing.token_prices
561+
assert prices.input_price is None
562+
assert prices.output_price is None
563+
assert prices.cache_price is None
564+
assert prices.batch_size is None
565+
assert prices.context_max is None
566+
assert prices.long_context is None
567+
assert billing.to_dict() == {"tokenPrices": {}}
568+
569+
def test_long_context_empty_object_round_trip(self):
570+
"""ModelBilling preserves present but empty longContext."""
571+
billing = ModelBilling.from_dict({"tokenPrices": {"longContext": {}}})
572+
573+
assert isinstance(billing.token_prices, ModelBillingTokenPrices)
574+
prices = billing.token_prices
575+
assert isinstance(prices.long_context, ModelBillingTokenPricesLongContext)
576+
long_context = prices.long_context
577+
assert long_context.input_price is None
578+
assert long_context.output_price is None
579+
assert long_context.cache_price is None
580+
assert long_context.context_max is None
581+
assert billing.to_dict() == {"tokenPrices": {"longContext": {}}}
582+
555583

556584
class TestOnListModels:
557585
@pytest.mark.asyncio

0 commit comments

Comments
 (0)