diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs
index 7a2ad2951..3b379d70f 100644
--- a/dotnet/src/Types.cs
+++ b/dotnet/src/Types.cs
@@ -3308,6 +3308,70 @@ public sealed class ModelBilling
///
[JsonPropertyName("multiplier")]
public double? Multiplier { get; set; }
+
+ ///
+ /// Token-level pricing information for this model.
+ ///
+ [JsonPropertyName("tokenPrices")]
+ public ModelBillingTokenPrices? TokenPrices { get; set; }
+}
+
+///
+/// Token-level pricing information for a model
+///
+public sealed class ModelBillingTokenPrices
+{
+ /// AI Credits cost per billing batch of input tokens.
+ [JsonPropertyName("inputPrice")]
+ public double? InputPrice { get; set; }
+
+ /// AI Credits cost per billing batch of output tokens.
+ [JsonPropertyName("outputPrice")]
+ public double? OutputPrice { get; set; }
+
+ /// AI Credits cost per billing batch of cached tokens.
+ [JsonPropertyName("cachePrice")]
+ public double? CachePrice { get; set; }
+
+ /// Number of tokens per standard billing batch.
+ [JsonPropertyName("batchSize")]
+ public int? BatchSize { get; set; }
+
+ ///
+ /// Prompt token budget (max_prompt_tokens) for the default tier. The total
+ /// context window is this value plus the model's max_output_tokens.
+ ///
+ [JsonPropertyName("contextMax")]
+ public int? ContextMax { get; set; }
+
+ /// Long context tier pricing (available for models with extended context windows).
+ [JsonPropertyName("longContext")]
+ public ModelBillingTokenPricesLongContext? LongContext { get; set; }
+}
+
+///
+/// Long context tier pricing (available for models with extended context windows)
+///
+public sealed class ModelBillingTokenPricesLongContext
+{
+ /// AI Credits cost per billing batch of input tokens.
+ [JsonPropertyName("inputPrice")]
+ public double? InputPrice { get; set; }
+
+ /// AI Credits cost per billing batch of output tokens.
+ [JsonPropertyName("outputPrice")]
+ public double? OutputPrice { get; set; }
+
+ /// AI Credits cost per billing batch of cached tokens.
+ [JsonPropertyName("cachePrice")]
+ public double? CachePrice { get; set; }
+
+ ///
+ /// Prompt token budget (max_prompt_tokens) for the long context tier. The total
+ /// context window is this value plus the model's max_output_tokens.
+ ///
+ [JsonPropertyName("contextMax")]
+ public int? ContextMax { get; set; }
}
///
@@ -3509,6 +3573,8 @@ public sealed class SystemMessageTransformRpcResponse
[JsonSerializable(typeof(McpServerConfig))]
[JsonSerializable(typeof(MessageOptions))]
[JsonSerializable(typeof(ModelBilling))]
+[JsonSerializable(typeof(ModelBillingTokenPrices))]
+[JsonSerializable(typeof(ModelBillingTokenPricesLongContext))]
[JsonSerializable(typeof(ModelCapabilities))]
[JsonSerializable(typeof(ModelCapabilitiesOverride))]
[JsonSerializable(typeof(ModelInfo))]
diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs
index b0797d34b..424d79abf 100644
--- a/dotnet/test/Unit/SerializationTests.cs
+++ b/dotnet/test/Unit/SerializationTests.cs
@@ -50,6 +50,56 @@ public void ProviderConfig_CanSerializeHeaders_WithSdkOptions()
Assert.Equal(4096, deserialized.MaxOutputTokens);
}
+ [Fact]
+ public void ModelBilling_CanSerializeTokenPrices_WithSdkOptions()
+ {
+ var options = GetSerializerOptions();
+ var original = new ModelBilling
+ {
+ Multiplier = 1.5,
+ TokenPrices = new ModelBillingTokenPrices
+ {
+ InputPrice = 2.0,
+ OutputPrice = 8.0,
+ CachePrice = 0.5,
+ BatchSize = 1_000_000,
+ ContextMax = 128_000,
+ LongContext = new ModelBillingTokenPricesLongContext
+ {
+ InputPrice = 4.0,
+ OutputPrice = 16.0,
+ CachePrice = 1.0,
+ ContextMax = 1_000_000
+ }
+ }
+ };
+
+ var json = JsonSerializer.Serialize(original, options);
+ using var document = JsonDocument.Parse(json);
+ var root = document.RootElement;
+ Assert.Equal(1.5, root.GetProperty("multiplier").GetDouble());
+ var tokenPrices = root.GetProperty("tokenPrices");
+ Assert.Equal(2.0, tokenPrices.GetProperty("inputPrice").GetDouble());
+ Assert.Equal(8.0, tokenPrices.GetProperty("outputPrice").GetDouble());
+ Assert.Equal(0.5, tokenPrices.GetProperty("cachePrice").GetDouble());
+ Assert.Equal(1_000_000, tokenPrices.GetProperty("batchSize").GetInt32());
+ Assert.Equal(128_000, tokenPrices.GetProperty("contextMax").GetInt32());
+ var longContext = tokenPrices.GetProperty("longContext");
+ Assert.Equal(4.0, longContext.GetProperty("inputPrice").GetDouble());
+ Assert.Equal(1_000_000, longContext.GetProperty("contextMax").GetInt32());
+
+ var deserialized = JsonSerializer.Deserialize(json, options);
+ Assert.NotNull(deserialized);
+ Assert.Equal(1.5, deserialized.Multiplier);
+ Assert.NotNull(deserialized.TokenPrices);
+ Assert.Equal(2.0, deserialized.TokenPrices.InputPrice);
+ Assert.Equal(1_000_000, deserialized.TokenPrices.BatchSize);
+ Assert.Equal(128_000, deserialized.TokenPrices.ContextMax);
+ Assert.NotNull(deserialized.TokenPrices.LongContext);
+ Assert.Equal(16.0, deserialized.TokenPrices.LongContext.OutputPrice);
+ Assert.Equal(1_000_000, deserialized.TokenPrices.LongContext.ContextMax);
+ }
+
[Fact]
public void MessageOptions_CanSerializeRequestHeaders_WithSdkOptions()
{
diff --git a/go/client_test.go b/go/client_test.go
index d5ba47da8..bf1fa3431 100644
--- a/go/client_test.go
+++ b/go/client_test.go
@@ -892,6 +892,75 @@ func TestListModelsWithCustomHandler(t *testing.T) {
}
}
+func TestModelBillingTokenPricesJSON(t *testing.T) {
+ wire := `{
+ "multiplier": 1.5,
+ "tokenPrices": {
+ "inputPrice": 2.0,
+ "outputPrice": 8.0,
+ "cachePrice": 0.5,
+ "batchSize": 1000000,
+ "contextMax": 128000,
+ "longContext": {
+ "inputPrice": 4.0,
+ "outputPrice": 16.0,
+ "cachePrice": 1.0,
+ "contextMax": 1000000
+ }
+ }
+ }`
+
+ var billing ModelBilling
+ if err := json.Unmarshal([]byte(wire), &billing); err != nil {
+ t.Fatalf("unmarshal failed: %v", err)
+ }
+
+ if billing.TokenPrices == nil {
+ t.Fatal("expected TokenPrices to be set")
+ }
+ tp := billing.TokenPrices
+ if tp.InputPrice == nil || *tp.InputPrice != 2.0 {
+ t.Errorf("unexpected InputPrice: %v", tp.InputPrice)
+ }
+ if tp.OutputPrice == nil || *tp.OutputPrice != 8.0 {
+ t.Errorf("unexpected OutputPrice: %v", tp.OutputPrice)
+ }
+ if tp.CachePrice == nil || *tp.CachePrice != 0.5 {
+ t.Errorf("unexpected CachePrice: %v", tp.CachePrice)
+ }
+ if tp.BatchSize == nil || *tp.BatchSize != 1000000 {
+ t.Errorf("unexpected BatchSize: %v", tp.BatchSize)
+ }
+ if tp.ContextMax == nil || *tp.ContextMax != 128000 {
+ t.Errorf("unexpected ContextMax: %v", tp.ContextMax)
+ }
+ if tp.LongContext == nil {
+ t.Fatal("expected LongContext to be set")
+ }
+ lc := tp.LongContext
+ if lc.InputPrice == nil || *lc.InputPrice != 4.0 {
+ t.Errorf("unexpected LongContext.InputPrice: %v", lc.InputPrice)
+ }
+ if lc.ContextMax == nil || *lc.ContextMax != 1000000 {
+ t.Errorf("unexpected LongContext.ContextMax: %v", lc.ContextMax)
+ }
+
+ // Round-trip back to JSON and ensure the nested structure survives.
+ out, err := json.Marshal(billing)
+ if err != nil {
+ t.Fatalf("marshal failed: %v", err)
+ }
+ var reparsed ModelBilling
+ if err := json.Unmarshal(out, &reparsed); err != nil {
+ t.Fatalf("re-unmarshal failed: %v", err)
+ }
+ if reparsed.TokenPrices == nil || reparsed.TokenPrices.LongContext == nil ||
+ reparsed.TokenPrices.LongContext.ContextMax == nil ||
+ *reparsed.TokenPrices.LongContext.ContextMax != 1000000 {
+ t.Errorf("round-trip lost token price data: %s", out)
+ }
+}
+
func TestListModelsHandlerCachesResults(t *testing.T) {
customModels := []ModelInfo{
{
diff --git a/go/types.go b/go/types.go
index 7ffd454a3..a4e1aa956 100644
--- a/go/types.go
+++ b/go/types.go
@@ -1599,7 +1599,40 @@ type ModelPolicy struct {
// ModelBilling contains model billing information
type ModelBilling struct {
- Multiplier *float64 `json:"multiplier,omitempty"`
+ Multiplier *float64 `json:"multiplier,omitempty"`
+ TokenPrices *ModelBillingTokenPrices `json:"tokenPrices,omitempty"`
+}
+
+// ModelBillingTokenPrices contains token-level pricing information for a model
+type ModelBillingTokenPrices struct {
+ // InputPrice is the AI Credits cost per billing batch of input tokens
+ InputPrice *float64 `json:"inputPrice,omitempty"`
+ // OutputPrice is the AI Credits cost per billing batch of output tokens
+ OutputPrice *float64 `json:"outputPrice,omitempty"`
+ // CachePrice is the AI Credits cost per billing batch of cached tokens
+ CachePrice *float64 `json:"cachePrice,omitempty"`
+ // BatchSize is the number of tokens per standard billing batch
+ BatchSize *int `json:"batchSize,omitempty"`
+ // ContextMax is the prompt token budget (max_prompt_tokens) for the default
+ // tier. The total context window is this value plus the model's max_output_tokens.
+ ContextMax *int `json:"contextMax,omitempty"`
+ // LongContext is the long context tier pricing (available for models with
+ // extended context windows)
+ LongContext *ModelBillingTokenPricesLongContext `json:"longContext,omitempty"`
+}
+
+// ModelBillingTokenPricesLongContext contains long context tier pricing
+// (available for models with extended context windows)
+type ModelBillingTokenPricesLongContext struct {
+ // InputPrice is the AI Credits cost per billing batch of input tokens
+ InputPrice *float64 `json:"inputPrice,omitempty"`
+ // OutputPrice is the AI Credits cost per billing batch of output tokens
+ OutputPrice *float64 `json:"outputPrice,omitempty"`
+ // CachePrice is the AI Credits cost per billing batch of cached tokens
+ CachePrice *float64 `json:"cachePrice,omitempty"`
+ // ContextMax is the prompt token budget (max_prompt_tokens) for the long
+ // context tier. The total context window is this value plus the model's max_output_tokens.
+ ContextMax *int `json:"contextMax,omitempty"`
}
// ModelInfo contains information about an available model
diff --git a/java/src/main/java/com/github/copilot/rpc/ModelBilling.java b/java/src/main/java/com/github/copilot/rpc/ModelBilling.java
index c7bfc72b5..0deb12bf4 100644
--- a/java/src/main/java/com/github/copilot/rpc/ModelBilling.java
+++ b/java/src/main/java/com/github/copilot/rpc/ModelBilling.java
@@ -16,14 +16,26 @@
public class ModelBilling {
@JsonProperty("multiplier")
- private double multiplier;
+ private Double multiplier;
- public double getMultiplier() {
+ @JsonProperty("tokenPrices")
+ private ModelBillingTokenPrices tokenPrices;
+
+ public Double getMultiplier() {
return multiplier;
}
- public ModelBilling setMultiplier(double multiplier) {
+ public ModelBilling setMultiplier(Double multiplier) {
this.multiplier = multiplier;
return this;
}
+
+ public ModelBillingTokenPrices getTokenPrices() {
+ return tokenPrices;
+ }
+
+ public ModelBilling setTokenPrices(ModelBillingTokenPrices tokenPrices) {
+ this.tokenPrices = tokenPrices;
+ return this;
+ }
}
diff --git a/java/src/main/java/com/github/copilot/rpc/ModelBillingTokenPrices.java b/java/src/main/java/com/github/copilot/rpc/ModelBillingTokenPrices.java
new file mode 100644
index 000000000..57c1d327f
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/rpc/ModelBillingTokenPrices.java
@@ -0,0 +1,109 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Token-level pricing information for a model.
+ *
+ * @since 1.0.2
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ModelBillingTokenPrices {
+
+ /**
+ * AI Credits cost per billing batch of input tokens.
+ */
+ @JsonProperty("inputPrice")
+ private Double inputPrice;
+
+ /**
+ * AI Credits cost per billing batch of output tokens.
+ */
+ @JsonProperty("outputPrice")
+ private Double outputPrice;
+
+ /**
+ * AI Credits cost per billing batch of cached tokens.
+ */
+ @JsonProperty("cachePrice")
+ private Double cachePrice;
+
+ /**
+ * Number of tokens per standard billing batch.
+ */
+ @JsonProperty("batchSize")
+ private Integer batchSize;
+
+ /**
+ * Prompt token budget (max_prompt_tokens) for the default tier. The total
+ * context window is this value plus the model's max_output_tokens.
+ */
+ @JsonProperty("contextMax")
+ private Integer contextMax;
+
+ /**
+ * Long context tier pricing (available for models with extended context
+ * windows).
+ */
+ @JsonProperty("longContext")
+ private ModelBillingTokenPricesLongContext longContext;
+
+ public Double getInputPrice() {
+ return inputPrice;
+ }
+
+ public ModelBillingTokenPrices setInputPrice(Double inputPrice) {
+ this.inputPrice = inputPrice;
+ return this;
+ }
+
+ public Double getOutputPrice() {
+ return outputPrice;
+ }
+
+ public ModelBillingTokenPrices setOutputPrice(Double outputPrice) {
+ this.outputPrice = outputPrice;
+ return this;
+ }
+
+ public Double getCachePrice() {
+ return cachePrice;
+ }
+
+ public ModelBillingTokenPrices setCachePrice(Double cachePrice) {
+ this.cachePrice = cachePrice;
+ return this;
+ }
+
+ public Integer getBatchSize() {
+ return batchSize;
+ }
+
+ public ModelBillingTokenPrices setBatchSize(Integer batchSize) {
+ this.batchSize = batchSize;
+ return this;
+ }
+
+ public Integer getContextMax() {
+ return contextMax;
+ }
+
+ public ModelBillingTokenPrices setContextMax(Integer contextMax) {
+ this.contextMax = contextMax;
+ return this;
+ }
+
+ public ModelBillingTokenPricesLongContext getLongContext() {
+ return longContext;
+ }
+
+ public ModelBillingTokenPrices setLongContext(ModelBillingTokenPricesLongContext longContext) {
+ this.longContext = longContext;
+ return this;
+ }
+}
diff --git a/java/src/main/java/com/github/copilot/rpc/ModelBillingTokenPricesLongContext.java b/java/src/main/java/com/github/copilot/rpc/ModelBillingTokenPricesLongContext.java
new file mode 100644
index 000000000..edb0ea7b1
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/rpc/ModelBillingTokenPricesLongContext.java
@@ -0,0 +1,79 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Long context tier pricing (available for models with extended context
+ * windows).
+ *
+ * @since 1.0.2
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ModelBillingTokenPricesLongContext {
+
+ /**
+ * AI Credits cost per billing batch of input tokens.
+ */
+ @JsonProperty("inputPrice")
+ private Double inputPrice;
+
+ /**
+ * AI Credits cost per billing batch of output tokens.
+ */
+ @JsonProperty("outputPrice")
+ private Double outputPrice;
+
+ /**
+ * AI Credits cost per billing batch of cached tokens.
+ */
+ @JsonProperty("cachePrice")
+ private Double cachePrice;
+
+ /**
+ * Prompt token budget (max_prompt_tokens) for the long context tier. The total
+ * context window is this value plus the model's max_output_tokens.
+ */
+ @JsonProperty("contextMax")
+ private Integer contextMax;
+
+ public Double getInputPrice() {
+ return inputPrice;
+ }
+
+ public ModelBillingTokenPricesLongContext setInputPrice(Double inputPrice) {
+ this.inputPrice = inputPrice;
+ return this;
+ }
+
+ public Double getOutputPrice() {
+ return outputPrice;
+ }
+
+ public ModelBillingTokenPricesLongContext setOutputPrice(Double outputPrice) {
+ this.outputPrice = outputPrice;
+ return this;
+ }
+
+ public Double getCachePrice() {
+ return cachePrice;
+ }
+
+ public ModelBillingTokenPricesLongContext setCachePrice(Double cachePrice) {
+ this.cachePrice = cachePrice;
+ return this;
+ }
+
+ public Integer getContextMax() {
+ return contextMax;
+ }
+
+ public ModelBillingTokenPricesLongContext setContextMax(Integer contextMax) {
+ this.contextMax = contextMax;
+ return this;
+ }
+}
diff --git a/java/src/test/java/com/github/copilot/MetadataApiTest.java b/java/src/test/java/com/github/copilot/MetadataApiTest.java
index b2c775eb1..e4e3c6711 100644
--- a/java/src/test/java/com/github/copilot/MetadataApiTest.java
+++ b/java/src/test/java/com/github/copilot/MetadataApiTest.java
@@ -143,7 +143,20 @@ void testModelInfoDeserialization() throws Exception {
"terms": "https://example.com/terms"
},
"billing": {
- "multiplier": 1.5
+ "multiplier": 1.5,
+ "tokenPrices": {
+ "inputPrice": 2.0,
+ "outputPrice": 8.0,
+ "cachePrice": 0.5,
+ "batchSize": 1000000,
+ "contextMax": 128000,
+ "longContext": {
+ "inputPrice": 4.0,
+ "outputPrice": 16.0,
+ "cachePrice": 1.0,
+ "contextMax": 1000000
+ }
+ }
}
}
""";
@@ -173,7 +186,33 @@ void testModelInfoDeserialization() throws Exception {
// Billing
assertNotNull(model.getBilling());
- assertEquals(1.5, model.getBilling().getMultiplier());
+ assertEquals(Double.valueOf(1.5), model.getBilling().getMultiplier());
+
+ // Token prices
+ ModelBillingTokenPrices tokenPrices = model.getBilling().getTokenPrices();
+ assertNotNull(tokenPrices);
+ assertEquals(2.0, tokenPrices.getInputPrice());
+ assertEquals(8.0, tokenPrices.getOutputPrice());
+ assertEquals(0.5, tokenPrices.getCachePrice());
+ assertEquals(1000000, tokenPrices.getBatchSize());
+ assertEquals(128000, tokenPrices.getContextMax());
+
+ // Long context tier
+ ModelBillingTokenPricesLongContext longContext = tokenPrices.getLongContext();
+ assertNotNull(longContext);
+ assertEquals(4.0, longContext.getInputPrice());
+ assertEquals(16.0, longContext.getOutputPrice());
+ assertEquals(1.0, longContext.getCachePrice());
+ assertEquals(1000000, longContext.getContextMax());
+ }
+
+ @Test
+ void testModelBillingSerializationOmitsNullMultiplier() throws Exception {
+ var billing = new ModelBilling();
+
+ String json = MAPPER.writeValueAsString(billing);
+
+ assertFalse(json.contains("multiplier"));
}
@Test
diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts
index c044f2b94..df2a3cf64 100644
--- a/nodejs/src/index.ts
+++ b/nodejs/src/index.ts
@@ -81,6 +81,8 @@ export type {
DefaultAgentConfig,
MessageOptions,
ModelBilling,
+ ModelBillingTokenPrices,
+ ModelBillingTokenPricesLongContext,
ModelCapabilities,
ModelCapabilitiesOverride,
ModelInfo,
diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts
index 75aa5159f..eb36f1b76 100644
--- a/nodejs/src/types.ts
+++ b/nodejs/src/types.ts
@@ -2376,7 +2376,48 @@ export interface ModelPolicy {
* Model billing information
*/
export interface ModelBilling {
+ /** Billing cost multiplier relative to the base rate */
multiplier?: number;
+ /** Token-level pricing information for this model */
+ tokenPrices?: ModelBillingTokenPrices;
+}
+
+/**
+ * Token-level pricing information for a model
+ */
+export interface ModelBillingTokenPrices {
+ /** AI Credits cost per billing batch of input tokens */
+ inputPrice?: number;
+ /** AI Credits cost per billing batch of output tokens */
+ outputPrice?: number;
+ /** AI Credits cost per billing batch of cached tokens */
+ cachePrice?: number;
+ /** Number of tokens per standard billing batch */
+ batchSize?: number;
+ /**
+ * Prompt token budget (max_prompt_tokens) for the default tier. The total
+ * context window is this value plus the model's max_output_tokens.
+ */
+ contextMax?: number;
+ /** Long context tier pricing (available for models with extended context windows) */
+ longContext?: ModelBillingTokenPricesLongContext;
+}
+
+/**
+ * Long context tier pricing (available for models with extended context windows)
+ */
+export interface ModelBillingTokenPricesLongContext {
+ /** AI Credits cost per billing batch of input tokens */
+ inputPrice?: number;
+ /** AI Credits cost per billing batch of output tokens */
+ outputPrice?: number;
+ /** AI Credits cost per billing batch of cached tokens */
+ cachePrice?: number;
+ /**
+ * Prompt token budget (max_prompt_tokens) for the long context tier. The
+ * total context window is this value plus the model's max_output_tokens.
+ */
+ contextMax?: number;
}
/**
diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts
index 9352eb627..0d552652b 100644
--- a/nodejs/test/client.test.ts
+++ b/nodejs/test/client.test.ts
@@ -1440,6 +1440,22 @@ describe("CopilotClient", () => {
supports: { vision: false, reasoningEffort: false },
limits: { max_context_window_tokens: 128000 },
},
+ billing: {
+ multiplier: 1.5,
+ tokenPrices: {
+ inputPrice: 2.0,
+ outputPrice: 8.0,
+ cachePrice: 0.5,
+ batchSize: 1000000,
+ contextMax: 128000,
+ longContext: {
+ inputPrice: 4.0,
+ outputPrice: 16.0,
+ cachePrice: 1.0,
+ contextMax: 1000000,
+ },
+ },
+ },
},
];
@@ -1451,6 +1467,7 @@ describe("CopilotClient", () => {
const models = await client.listModels();
expect(handler).toHaveBeenCalledTimes(1);
expect(models).toEqual(customModels);
+ expect(models[0].billing?.tokenPrices?.longContext?.contextMax).toBe(1000000);
});
it("caches onListModels results on subsequent calls", async () => {
diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py
index 3f1a84d25..a03922737 100644
--- a/python/copilot/__init__.py
+++ b/python/copilot/__init__.py
@@ -36,6 +36,8 @@
GetStatusResponse,
LogLevel,
ModelBilling,
+ ModelBillingTokenPrices,
+ ModelBillingTokenPricesLongContext,
ModelCapabilities,
ModelCapabilitiesOverride,
ModelInfo,
@@ -202,6 +204,8 @@
"MCPServerConfig",
"MCPStdioServerConfig",
"ModelBilling",
+ "ModelBillingTokenPrices",
+ "ModelBillingTokenPricesLongContext",
"ModelCapabilities",
"ModelCapabilitiesOverride",
"ModelInfo",
diff --git a/python/copilot/client.py b/python/copilot/client.py
index 7dcec6e8f..3a684d676 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -667,24 +667,133 @@ def to_dict(self) -> dict:
return result
+@dataclass
+class ModelBillingTokenPricesLongContext:
+ """Long context tier pricing (available for models with extended context windows)"""
+
+ # AI Credits cost per billing batch of input tokens
+ input_price: float | None = None
+ # AI Credits cost per billing batch of output tokens
+ output_price: float | None = None
+ # AI Credits cost per billing batch of cached tokens
+ cache_price: float | None = None
+ # Prompt token budget (max_prompt_tokens) for the long context tier. The total
+ # context window is this value plus the model's max_output_tokens.
+ context_max: int | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> ModelBillingTokenPricesLongContext:
+ assert isinstance(obj, dict)
+ input_price = obj.get("inputPrice")
+ output_price = obj.get("outputPrice")
+ cache_price = obj.get("cachePrice")
+ context_max = obj.get("contextMax")
+ return ModelBillingTokenPricesLongContext(
+ input_price=float(input_price) if input_price is not None else None,
+ output_price=float(output_price) if output_price is not None else None,
+ cache_price=float(cache_price) if cache_price is not None else None,
+ context_max=int(context_max) if context_max is not None else None,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ if self.input_price is not None:
+ result["inputPrice"] = self.input_price
+ if self.output_price is not None:
+ result["outputPrice"] = self.output_price
+ if self.cache_price is not None:
+ result["cachePrice"] = self.cache_price
+ if self.context_max is not None:
+ result["contextMax"] = self.context_max
+ return result
+
+
+@dataclass
+class ModelBillingTokenPrices:
+ """Token-level pricing information for a model"""
+
+ # AI Credits cost per billing batch of input tokens
+ input_price: float | None = None
+ # AI Credits cost per billing batch of output tokens
+ output_price: float | None = None
+ # AI Credits cost per billing batch of cached tokens
+ cache_price: float | None = None
+ # Number of tokens per standard billing batch
+ batch_size: int | None = None
+ # Prompt token budget (max_prompt_tokens) for the default tier. The total
+ # context window is this value plus the model's max_output_tokens.
+ context_max: int | None = None
+ # Long context tier pricing (available for models with extended context windows)
+ long_context: ModelBillingTokenPricesLongContext | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> ModelBillingTokenPrices:
+ assert isinstance(obj, dict)
+ input_price = obj.get("inputPrice")
+ output_price = obj.get("outputPrice")
+ cache_price = obj.get("cachePrice")
+ batch_size = obj.get("batchSize")
+ context_max = obj.get("contextMax")
+ long_context_dict = obj.get("longContext")
+ long_context = (
+ ModelBillingTokenPricesLongContext.from_dict(long_context_dict)
+ if long_context_dict is not None
+ else None
+ )
+ return ModelBillingTokenPrices(
+ input_price=float(input_price) if input_price is not None else None,
+ output_price=float(output_price) if output_price is not None else None,
+ cache_price=float(cache_price) if cache_price is not None else None,
+ batch_size=int(batch_size) if batch_size is not None else None,
+ context_max=int(context_max) if context_max is not None else None,
+ long_context=long_context,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ if self.input_price is not None:
+ result["inputPrice"] = self.input_price
+ if self.output_price is not None:
+ result["outputPrice"] = self.output_price
+ if self.cache_price is not None:
+ result["cachePrice"] = self.cache_price
+ if self.batch_size is not None:
+ result["batchSize"] = self.batch_size
+ if self.context_max is not None:
+ result["contextMax"] = self.context_max
+ if self.long_context is not None:
+ result["longContext"] = self.long_context.to_dict()
+ return result
+
+
@dataclass
class ModelBilling:
"""Model billing information"""
multiplier: float | None = None
+ token_prices: ModelBillingTokenPrices | None = None
@staticmethod
def from_dict(obj: Any) -> ModelBilling:
assert isinstance(obj, dict)
multiplier = obj.get("multiplier")
- if multiplier is None:
- return ModelBilling()
- return ModelBilling(multiplier=float(multiplier))
+ token_prices_dict = obj.get("tokenPrices")
+ token_prices = (
+ ModelBillingTokenPrices.from_dict(token_prices_dict)
+ if token_prices_dict is not None
+ else None
+ )
+ return ModelBilling(
+ multiplier=float(multiplier) if multiplier is not None else None,
+ token_prices=token_prices,
+ )
def to_dict(self) -> dict:
result: dict = {}
if self.multiplier is not None:
result["multiplier"] = self.multiplier
+ if self.token_prices is not None:
+ result["tokenPrices"] = self.token_prices.to_dict()
return result
diff --git a/python/test_client.py b/python/test_client.py
index 502d410ab..9f11267ce 100644
--- a/python/test_client.py
+++ b/python/test_client.py
@@ -18,6 +18,9 @@
from copilot.client import (
CloudSessionOptions,
CloudSessionRepository,
+ ModelBilling,
+ ModelBillingTokenPrices,
+ ModelBillingTokenPricesLongContext,
ModelCapabilities,
ModelInfo,
ModelLimits,
@@ -504,6 +507,80 @@ async def mock_request(method, params, **kwargs):
await client.force_stop()
+class TestModelBilling:
+ def test_token_prices_round_trip(self):
+ """ModelBilling.from_dict/to_dict round-trips tokenPrices and longContext."""
+ wire = {
+ "multiplier": 1.5,
+ "tokenPrices": {
+ "inputPrice": 2.0,
+ "outputPrice": 8.0,
+ "cachePrice": 0.5,
+ "batchSize": 1000000,
+ "contextMax": 128000,
+ "longContext": {
+ "inputPrice": 4.0,
+ "outputPrice": 16.0,
+ "cachePrice": 1.0,
+ "contextMax": 1000000,
+ },
+ },
+ }
+
+ billing = ModelBilling.from_dict(wire)
+
+ assert billing.multiplier == 1.5
+ assert isinstance(billing.token_prices, ModelBillingTokenPrices)
+ prices = billing.token_prices
+ assert prices.input_price == 2.0
+ assert prices.output_price == 8.0
+ assert prices.cache_price == 0.5
+ assert prices.batch_size == 1000000
+ assert prices.context_max == 128000
+ assert isinstance(prices.long_context, ModelBillingTokenPricesLongContext)
+ long_context = prices.long_context
+ assert long_context.input_price == 4.0
+ assert long_context.output_price == 16.0
+ assert long_context.cache_price == 1.0
+ assert long_context.context_max == 1000000
+
+ assert billing.to_dict() == wire
+
+ def test_token_prices_absent(self):
+ """ModelBilling without tokenPrices leaves token_prices unset."""
+ billing = ModelBilling.from_dict({"multiplier": 1.0})
+ assert billing.token_prices is None
+ assert billing.to_dict() == {"multiplier": 1.0}
+
+ def test_token_prices_empty_object_round_trip(self):
+ """ModelBilling preserves present but empty tokenPrices."""
+ billing = ModelBilling.from_dict({"tokenPrices": {}})
+
+ assert isinstance(billing.token_prices, ModelBillingTokenPrices)
+ prices = billing.token_prices
+ assert prices.input_price is None
+ assert prices.output_price is None
+ assert prices.cache_price is None
+ assert prices.batch_size is None
+ assert prices.context_max is None
+ assert prices.long_context is None
+ assert billing.to_dict() == {"tokenPrices": {}}
+
+ def test_long_context_empty_object_round_trip(self):
+ """ModelBilling preserves present but empty longContext."""
+ billing = ModelBilling.from_dict({"tokenPrices": {"longContext": {}}})
+
+ assert isinstance(billing.token_prices, ModelBillingTokenPrices)
+ prices = billing.token_prices
+ assert isinstance(prices.long_context, ModelBillingTokenPricesLongContext)
+ long_context = prices.long_context
+ assert long_context.input_price is None
+ assert long_context.output_price is None
+ assert long_context.cache_price is None
+ assert long_context.context_max is None
+ assert billing.to_dict() == {"tokenPrices": {"longContext": {}}}
+
+
class TestOnListModels:
@pytest.mark.asyncio
async def test_list_models_with_custom_handler(self):
diff --git a/rust/src/types.rs b/rust/src/types.rs
index 8b9b5960a..505afef49 100644
--- a/rust/src/types.rs
+++ b/rust/src/types.rs
@@ -4170,7 +4170,8 @@ impl InputFormat {
/// [`crate::rpc`]; they live here so the crate-root
/// `pub use types::*` surfaces them alongside hand-written SDK types.
pub use crate::generated::api_types::{
- Model, ModelBilling, ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
+ Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
+ ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
};
diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs
index 244885697..6677b3c98 100644
--- a/rust/tests/session_test.rs
+++ b/rust/tests/session_test.rs
@@ -1202,7 +1202,27 @@ async fn list_models_returns_typed_model_info() {
"id": id,
"result": {
"models": [
- { "id": "gpt-4", "name": "GPT-4", "capabilities": {} },
+ {
+ "id": "gpt-4",
+ "name": "GPT-4",
+ "capabilities": {},
+ "billing": {
+ "multiplier": 1.5,
+ "tokenPrices": {
+ "inputPrice": 2.0,
+ "outputPrice": 8.0,
+ "cachePrice": 0.5,
+ "batchSize": 1000000,
+ "contextMax": 128000,
+ "longContext": {
+ "inputPrice": 4.0,
+ "outputPrice": 16.0,
+ "cachePrice": 1.0,
+ "contextMax": 1000000
+ }
+ }
+ }
+ },
{ "id": "claude-sonnet-4", "name": "Claude Sonnet", "capabilities": {} },
]
},
@@ -1213,6 +1233,22 @@ async fn list_models_returns_typed_model_info() {
assert_eq!(models.len(), 2);
assert_eq!(models[0].id, "gpt-4");
assert_eq!(models[1].name, "Claude Sonnet");
+
+ // Token prices are surfaced through the re-exported public types.
+ let token_prices: &github_copilot_sdk::types::ModelBillingTokenPrices = models[0]
+ .billing
+ .as_ref()
+ .expect("billing")
+ .token_prices
+ .as_ref()
+ .expect("token prices");
+ assert_eq!(token_prices.input_price, Some(2.0));
+ assert_eq!(token_prices.batch_size, Some(1000000));
+ assert_eq!(token_prices.context_max, Some(128000));
+ let long_context: &github_copilot_sdk::types::ModelBillingTokenPricesLongContext =
+ token_prices.long_context.as_ref().expect("long context");
+ assert_eq!(long_context.output_price, Some(16.0));
+ assert_eq!(long_context.context_max, Some(1000000));
}
#[tokio::test]