Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to LUOYE Codex Meter are documented here.

## [0.3.5] - 2026-07-13

### Fixed

- Identify five-hour and weekly quota windows by their declared duration instead of their response position
- Leave the five-hour value and reset time blank when Codex only provides the weekly quota window
- Preserve compatibility with older app-server responses that omit window duration metadata

## [0.3.4] - 2026-07-12

Initial public release.
Expand Down
4 changes: 2 additions & 2 deletions src/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
[assembly: AssemblyCopyright("Copyright (c) 2026 LUOYE")]
[assembly: ComVisible(false)]
[assembly: Guid("c2b06a3d-877f-43b1-a2c2-7d9c5585f84f")]
[assembly: AssemblyVersion("0.3.4.0")]
[assembly: AssemblyFileVersion("0.3.4.0")]
[assembly: AssemblyVersion("0.3.5.0")]
[assembly: AssemblyFileVersion("0.3.5.0")]
19 changes: 13 additions & 6 deletions src/MeterWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1355,13 +1355,15 @@ private void ApplySnapshot(QuotaSnapshot snapshot)
fiveHourDetail,
snapshot.FiveHourRemaining,
snapshot.FiveHourReset,
"5 小时额度");
"5 小时额度",
true);
SetMetric(
weeklyMetric,
weeklyDetail,
snapshot.WeeklyRemaining,
snapshot.WeeklyReset,
"本周额度");
"本周额度",
false);
resetCountText.Text = FormatCompactCount(snapshot.ResetCredits);
resetCountText.ToolTip = snapshot.ResetCredits + " 次重置额度";
detailResetCountText.Text = snapshot.ResetCredits + " 次重置额度";
Expand All @@ -1374,16 +1376,21 @@ private void SetMetric(
DetailMetricElements detail,
int? remaining,
DateTime? resetAt,
string label)
string label,
bool emptyWhenUnavailable = false)
{
metric.Remaining = remaining;
detail.Remaining = remaining;
metric.Value.Text = remaining.HasValue ? remaining.Value + "%" : "--";
metric.Value.Text = remaining.HasValue
? remaining.Value + "%"
: emptyWhenUnavailable ? String.Empty : "--";
detail.Value.Text = metric.Value.Text;
detail.Reset.Text = FormatResetTime(resetAt);
detail.Reset.Text = emptyWhenUnavailable && !remaining.HasValue
? String.Empty
: FormatResetTime(resetAt);
var tooltip = remaining.HasValue
? label + ":剩余 " + remaining.Value + "%," + FormatResetTime(resetAt)
: label + "暂不可用";
: emptyWhenUnavailable ? label + ":当前未提供" : label + "暂不可用";
metric.Root.ToolTip = tooltip;
detail.Root.ToolTip = tooltip;

Expand Down
45 changes: 40 additions & 5 deletions src/QuotaService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ internal async Task<QuotaSnapshot> ReadAsync()
process.BeginErrorReadLine();

await process.StandardInput.WriteLineAsync(
"{\"id\":1,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"luoye-codex-meter\",\"version\":\"0.3.4\"}}}");
"{\"id\":1,\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"luoye-codex-meter\",\"version\":\"0.3.5\"}}}");
await process.StandardInput.WriteLineAsync("{\"method\":\"initialized\"}");
await process.StandardInput.WriteLineAsync(
"{\"id\":2,\"method\":\"account/rateLimits/read\",\"params\":null}");
Expand Down Expand Up @@ -328,18 +328,53 @@ private QuotaSnapshot ParseSnapshot(Dictionary<string, object> result)

var primary = GetDictionary(limits, "primary");
var secondary = GetDictionary(limits, "secondary");
var fiveHour = FindWindow(primary, secondary, 5 * 60, "primary");
var weekly = FindWindow(primary, secondary, 7 * 24 * 60, "secondary");
var credits = GetDictionary(result, "rateLimitResetCredits");

return new QuotaSnapshot
{
FiveHourRemaining = RemainingPercent(primary),
WeeklyRemaining = RemainingPercent(secondary),
FiveHourReset = ReadUnixTime(primary, "resetsAt"),
WeeklyReset = ReadUnixTime(secondary, "resetsAt"),
FiveHourRemaining = RemainingPercent(fiveHour),
WeeklyRemaining = RemainingPercent(weekly),
FiveHourReset = ReadUnixTime(fiveHour, "resetsAt"),
WeeklyReset = ReadUnixTime(weekly, "resetsAt"),
ResetCredits = ReadInt(credits, "availableCount") ?? 0
};
}

private static Dictionary<string, object> FindWindow(
Dictionary<string, object> primary,
Dictionary<string, object> secondary,
double expectedDurationMinutes,
string legacyPosition)
{
if (HasDuration(primary, expectedDurationMinutes))
{
return primary;
}
if (HasDuration(secondary, expectedDurationMinutes))
{
return secondary;
}

// Older app-server responses did not identify window durations. Keep
// their positional primary/secondary mapping as a compatibility fallback.
var legacyWindow = String.Equals(legacyPosition, "primary", StringComparison.Ordinal)
? primary
: secondary;
return ReadDouble(legacyWindow, "windowDurationMins").HasValue
? null
: legacyWindow;
}

private static bool HasDuration(
Dictionary<string, object> window,
double expectedDurationMinutes)
{
var actual = ReadDouble(window, "windowDurationMins");
return actual.HasValue && Math.Abs(actual.Value - expectedDurationMinutes) < 0.5;
}

private static bool HasResponseId(Dictionary<string, object> message, int expected)
{
object raw;
Expand Down