Skip to content

Commit 134b0a4

Browse files
Merge branch 'main' into issue-1248
2 parents 8f234c8 + 190765a commit 134b0a4

6 files changed

Lines changed: 53 additions & 28 deletions

File tree

DevProxy.Abstractions/LanguageModel/PricesData.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace DevProxy.Abstractions.LanguageModel;
99
public class ModelPrices
1010
{
1111
public double Input { get; set; }
12+
public double CachedInput { get; set; }
1213
public double Output { get; set; }
1314
}
1415

@@ -44,7 +45,7 @@ public bool TryGetModelPrices(string modelName, out ModelPrices? prices)
4445
return false;
4546
}
4647

47-
public (double Input, double Output) CalculateCost(string modelName, long inputTokens, long outputTokens)
48+
public (double Input, double Output) CalculateCost(string modelName, long inputTokens, long outputTokens, long cachedInputTokens = 0)
4849
{
4950
if (!TryGetModelPrices(modelName, out var prices))
5051
{
@@ -53,8 +54,13 @@ public bool TryGetModelPrices(string modelName, out ModelPrices? prices)
5354

5455
Debug.Assert(prices != null, "Prices data should not be null here.");
5556

56-
// Prices in the data are per 1M tokens
57-
var inputCost = prices.Input * (inputTokens / 1_000_000.0);
57+
// Prices in the data are per 1M tokens.
58+
// When no cached input price is configured, fall back to the
59+
// regular input price so all tokens are billed correctly.
60+
var effectiveCachedPrice = prices.CachedInput > 0 ? prices.CachedInput : prices.Input;
61+
var regularInputTokens = inputTokens - cachedInputTokens;
62+
var inputCost = (prices.Input * (regularInputTokens / 1_000_000.0))
63+
+ (effectiveCachedPrice * (cachedInputTokens / 1_000_000.0));
5864
var outputCost = prices.Output * (outputTokens / 1_000_000.0);
5965

6066
return (inputCost, outputCost);

DevProxy.Abstractions/Utils/ProxyUtils.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Collections.ObjectModel;
1111
using System.Reflection;
1212
using System.Text.Encodings.Web;
13+
using System.Runtime.InteropServices;
1314
using System.Text.Json;
1415
using System.Text.Json.Serialization;
1516
using System.Text.RegularExpressions;
@@ -42,6 +43,23 @@ public static class ProxyUtils
4243

4344
// doesn't end with a path separator
4445
public static string? AppFolder => Path.GetDirectoryName(AppContext.BaseDirectory);
46+
47+
/// <summary>
48+
/// Gets the path to the user data folder for Dev Proxy.
49+
/// On macOS: ~/Library/Application Support/dev-proxy/
50+
/// On Linux: ~/.config/dev-proxy/ (or $XDG_CONFIG_HOME/dev-proxy/)
51+
/// On Windows: %LocalAppData%\dev-proxy\
52+
/// </summary>
53+
public static string DataFolder
54+
{
55+
get
56+
{
57+
var basePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
58+
? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
59+
: Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
60+
return Path.Combine(basePath, "dev-proxy");
61+
}
62+
}
4563
public static JsonSerializerOptions JsonSerializerOptions { get; } = new()
4664
{
4765
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
@@ -175,6 +193,7 @@ public static string ReplacePathTokens(string? path)
175193
return path ?? string.Empty;
176194
}
177195

196+
path = path.Replace("~dataFolder", DataFolder, StringComparison.OrdinalIgnoreCase);
178197
return path.Replace("~appFolder", AppFolder, StringComparison.OrdinalIgnoreCase);
179198
}
180199

DevProxy.Plugins/Inspection/LanguageModelPricingLoader.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,18 @@ protected override void LoadData(string fileContents)
4040
if (modelProperty.Value.TryGetProperty("input", out var inputElement) &&
4141
modelProperty.Value.TryGetProperty("output", out var outputElement))
4242
{
43-
pricesData[modelName] = new()
43+
var modelPrices = new ModelPrices
4444
{
4545
Input = inputElement.GetDouble(),
4646
Output = outputElement.GetDouble()
4747
};
48+
49+
if (modelProperty.Value.TryGetProperty("cached_input", out var cachedInputElement))
50+
{
51+
modelPrices.CachedInput = cachedInputElement.GetDouble();
52+
}
53+
54+
pricesData[modelName] = modelPrices;
4855
}
4956
}
5057

DevProxy.Plugins/Inspection/OpenAITelemetryPlugin.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,8 @@ private void RecordUsageMetrics(Activity activity, OpenAIRequest request, OpenAI
931931
return;
932932
}
933933

934-
var (inputCost, outputCost) = Configuration.Prices.CalculateCost(response.Model, usage.PromptTokens, usage.CompletionTokens);
934+
var cachedTokens = usage.PromptTokensDetails?.CachedTokens ?? 0L;
935+
var (inputCost, outputCost) = Configuration.Prices.CalculateCost(response.Model, usage.PromptTokens, usage.CompletionTokens, cachedTokens);
935936

936937
if (inputCost > 0)
937938
{
@@ -1042,7 +1043,8 @@ private List<OpenAITelemetryPluginReportModelUsageInformation> GetReportModelUsa
10421043
return usagePerModel;
10431044
}
10441045

1045-
var (inputCost, outputCost) = Configuration.Prices.CalculateCost(response.Model, usage.PromptTokens, usage.CompletionTokens);
1046+
var cachedTokens = usage.PromptTokensDetails?.CachedTokens ?? 0L;
1047+
var (inputCost, outputCost) = Configuration.Prices.CalculateCost(response.Model, usage.PromptTokens, usage.CompletionTokens, cachedTokens);
10461048

10471049
if (inputCost > 0)
10481050
{

DevProxy/Commands/ConfigCommand.cs

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ internal static async Task<int> RunValidateStandaloneAsync(string[] args)
106106

107107
using var loggerFactory = LoggerFactory.Create(builder =>
108108
{
109-
builder
109+
_ = builder
110110
.SetMinimumLevel(LogLevel.Information)
111111
.AddConsole(consoleOptions =>
112112
{
@@ -215,17 +215,9 @@ private async Task DownloadConfigAsync(string configId, OutputFormat outputForma
215215
{
216216
try
217217
{
218-
var appFolder = ProxyUtils.AppFolder;
219-
if (string.IsNullOrEmpty(appFolder) || !Directory.Exists(appFolder))
220-
{
221-
if (outputFormat == OutputFormat.Text)
222-
{
223-
_logger.LogError("App folder {AppFolder} not found", appFolder);
224-
}
225-
return;
226-
}
218+
var dataFolder = ProxyUtils.DataFolder;
227219

228-
var configFolderPath = Path.Combine(appFolder, "config");
220+
var configFolderPath = Path.Combine(dataFolder, "configs");
229221
_logger.LogDebug("Checking if config folder {ConfigFolderPath} exists...", configFolderPath);
230222
if (!Directory.Exists(configFolderPath))
231223
{
@@ -235,7 +227,7 @@ private async Task DownloadConfigAsync(string configId, OutputFormat outputForma
235227
}
236228

237229
_logger.LogDebug("Getting target folder path for config {ConfigId}...", configId);
238-
var targetFolderPath = GetTargetFolderPath(appFolder, configId);
230+
var targetFolderPath = GetTargetFolderPath(dataFolder, configId);
239231
_logger.LogDebug("Creating target folder {TargetFolderPath}...", targetFolderPath);
240232
_ = Directory.CreateDirectory(targetFolderPath);
241233

@@ -287,7 +279,7 @@ private async Task DownloadConfigAsync(string configId, OutputFormat outputForma
287279
{
288280
if (_logger.IsEnabled(LogLevel.Information))
289281
{
290-
_logger.LogInformation(" devproxy --config-file \"{ConfigFile}\"", configFile.Replace(appFolder, "~appFolder", StringComparison.OrdinalIgnoreCase));
282+
_logger.LogInformation(" devproxy --config-file \"{ConfigFile}\"", configFile.Replace(dataFolder, "~dataFolder", StringComparison.OrdinalIgnoreCase));
291283
}
292284
}
293285
}
@@ -298,7 +290,7 @@ private async Task DownloadConfigAsync(string configId, OutputFormat outputForma
298290
{
299291
if (_logger.IsEnabled(LogLevel.Information))
300292
{
301-
_logger.LogInformation(" devproxy --mock-file \"{MockFile}\"", mockFile.Replace(appFolder, "~appFolder", StringComparison.OrdinalIgnoreCase));
293+
_logger.LogInformation(" devproxy --mock-file \"{MockFile}\"", mockFile.Replace(dataFolder, "~dataFolder", StringComparison.OrdinalIgnoreCase));
302294
}
303295
}
304296
}
@@ -332,13 +324,13 @@ private ProxyConfigInfo GetConfigInfo(string configFolder)
332324
var configInfo = new ProxyConfigInfo();
333325

334326
_logger.LogDebug("Getting list of config files in {ConfigFolder}...", configFolder);
335-
327+
336328
// Get both JSON and YAML files
337329
var jsonFiles = Directory.GetFiles(configFolder, "*.json");
338330
var yamlFiles = Directory.GetFiles(configFolder, "*.yaml");
339331
var ymlFiles = Directory.GetFiles(configFolder, "*.yml");
340332
var allConfigFiles = jsonFiles.Concat(yamlFiles).Concat(ymlFiles).ToArray();
341-
333+
342334
if (allConfigFiles.Length == 0)
343335
{
344336
_logger.LogDebug("No config files found");
@@ -350,7 +342,7 @@ private ProxyConfigInfo GetConfigInfo(string configFolder)
350342
_logger.LogDebug("Reading file {ConfigFile}...", configFile);
351343

352344
var fileContents = File.ReadAllText(configFile);
353-
345+
354346
// Check for plugins marker (case-insensitive)
355347
// For JSON: "plugins":
356348
// For YAML: plugins:
@@ -561,9 +553,9 @@ private string GetTargetFileName(string name)
561553
}
562554
}
563555

564-
private static string GetTargetFolderPath(string appFolder, string configId)
556+
private static string GetTargetFolderPath(string dataFolder, string configId)
565557
{
566-
var baseFolder = Path.Combine(appFolder, "config", configId);
558+
var baseFolder = Path.Combine(dataFolder, "configs", configId);
567559
var newFolder = baseFolder;
568560
var i = 1;
569561
while (Directory.Exists(newFolder))
@@ -701,7 +693,7 @@ private static async Task<int> ValidateConfigCoreAsync(
701693
if (configDoc.RootElement.TryGetProperty("plugins", out var pluginsElement) &&
702694
pluginsElement.ValueKind == JsonValueKind.Array)
703695
{
704-
ValidatePlugins(pluginsElement, configFileDirectory, errors, warnings, pluginNames);
696+
ValidatePlugins(pluginsElement, configFileDirectory, errors, pluginNames);
705697
}
706698
else
707699
{
@@ -737,7 +729,6 @@ private static void ValidatePlugins(
737729
JsonElement pluginsElement,
738730
string configFileDirectory,
739731
List<ValidationMessage> errors,
740-
List<ValidationMessage> warnings,
741732
List<string> pluginNames)
742733
{
743734
var hasEnabledPlugins = false;

DevProxy/Commands/DevProxyCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ sealed class DevProxyCommand : RootCommand
3333
internal static readonly Option<string?> ConfigFileOption = new(ConfigFileOptionName, "-c")
3434
{
3535
HelpName = "config-file",
36-
Description = "Path to config file. If not specified, Dev Proxy searches for devproxyrc.jsonc or devproxyrc.json in the current directory, then in a .devproxy/ directory, then under the ~appFolder location. Supports ~appFolder token."
36+
Description = "Path to config file. If not specified, Dev Proxy searches for devproxyrc.jsonc or devproxyrc.json in the current directory, then in a .devproxy/ directory, then under the ~appFolder location. Supports ~appFolder and ~dataFolder tokens."
3737
};
3838
internal const string NoFirstRunOptionName = "--no-first-run";
3939
internal const string NoWatchOptionName = "--no-watch";

0 commit comments

Comments
 (0)