Skip to content
Open
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
66 changes: 66 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3308,6 +3308,70 @@ public sealed class ModelBilling
/// </summary>
[JsonPropertyName("multiplier")]
public double? Multiplier { get; set; }

/// <summary>
/// Token-level pricing information for this model.
/// </summary>
[JsonPropertyName("tokenPrices")]
public ModelBillingTokenPrices? TokenPrices { get; set; }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need new types for this, or could / should we just return the Rpc type?

In general, I'm wondering about these "higher-level" APIs that are effectively identical to the RPC ones.

}

/// <summary>
/// Token-level pricing information for a model
/// </summary>
public sealed class ModelBillingTokenPrices
{
/// <summary>AI Credits cost per billing batch of input tokens.</summary>
[JsonPropertyName("inputPrice")]
public double? InputPrice { get; set; }

/// <summary>AI Credits cost per billing batch of output tokens.</summary>
[JsonPropertyName("outputPrice")]
public double? OutputPrice { get; set; }

/// <summary>AI Credits cost per billing batch of cached tokens.</summary>
[JsonPropertyName("cachePrice")]
public double? CachePrice { get; set; }

/// <summary>Number of tokens per standard billing batch.</summary>
[JsonPropertyName("batchSize")]
public int? BatchSize { get; set; }

/// <summary>
/// Prompt token budget (max_prompt_tokens) for the default tier. The total
/// context window is this value plus the model's max_output_tokens.
/// </summary>
[JsonPropertyName("contextMax")]
public int? ContextMax { get; set; }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the same naming we use elsewhere for this concept? I'd have expected MaxContextWindow or something like that.


/// <summary>Long context tier pricing (available for models with extended context windows).</summary>
[JsonPropertyName("longContext")]
public ModelBillingTokenPricesLongContext? LongContext { get; set; }
}

/// <summary>
/// Long context tier pricing (available for models with extended context windows)
/// </summary>
public sealed class ModelBillingTokenPricesLongContext
{
/// <summary>AI Credits cost per billing batch of input tokens.</summary>
[JsonPropertyName("inputPrice")]
public double? InputPrice { get; set; }

/// <summary>AI Credits cost per billing batch of output tokens.</summary>
[JsonPropertyName("outputPrice")]
public double? OutputPrice { get; set; }

/// <summary>AI Credits cost per billing batch of cached tokens.</summary>
[JsonPropertyName("cachePrice")]
public double? CachePrice { get; set; }

/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("contextMax")]
public int? ContextMax { get; set; }
}

/// <summary>
Expand Down Expand Up @@ -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))]
Expand Down
50 changes: 50 additions & 0 deletions dotnet/test/Unit/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ModelBilling>(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()
{
Expand Down
69 changes: 69 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
{
Expand Down
35 changes: 34 additions & 1 deletion go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 15 additions & 3 deletions java/src/main/java/com/github/copilot/rpc/ModelBilling.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,26 @@
public class ModelBilling {

@JsonProperty("multiplier")
private double multiplier;
private Double multiplier;

public double getMultiplier() {
@JsonProperty("tokenPrices")
private ModelBillingTokenPrices tokenPrices;

Comment thread
MackinnonBuck marked this conversation as resolved.
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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading