fix(usage): wrap malformed usage json · openclaw/openclaw@a118e11
vincentkoc
·
2026-05-15
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -96,6 +96,7 @@ Docs: https://docs.openclaw.ai
|
96 | 96 | - Tlon/Urbit: report malformed SSE event JSON with an owned parser error instead of logging raw parser failures. |
97 | 97 | - Signal: return a stable installer error when GitHub release metadata is malformed JSON. |
98 | 98 | - ClawHub: report malformed successful marketplace JSON responses with owned errors instead of leaking raw parser failures. |
| 99 | +- Provider usage: report malformed successful usage JSON responses with stable provider errors instead of leaking raw parser failures. |
99 | 100 | - Matrix: ignore malformed percent-encoding in optional location URI parameters instead of letting a bad `geo:` event abort inbound message handling. |
100 | 101 | - Web search: auto-detect Brave through its legacy `tools.web.search.apiKey` compatibility fallback while keeping doctor migration to `plugins.entries.brave.config.webSearch.apiKey` as the canonical repair, so allowlisted isolated cron runs do not report `web_search` unavailable before migration. Fixes #81538. Thanks @atomicmonk. |
101 | 102 | - Plugins: memoize repeated in-process plugin metadata snapshots and keep vanished managed-install residue from forcing full derived discovery, reducing gateway/status startup scans under large plugin sets. Fixes #81143 and #79806. (#81570) Thanks @Kaspre, @holgergruenhagen, @JanPlessow, and @mjamiv. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -128,6 +128,15 @@ describe("fetchClaudeUsage", () => {
|
128 | 128 | expect(result.windows).toHaveLength(0); |
129 | 129 | }); |
130 | 130 | |
| 131 | +it("returns a stable error for malformed successful oauth usage JSON", async () => { |
| 132 | +const mockFetch = createProviderUsageFetch(async () => makeResponse(200, "{not json")); |
| 133 | + |
| 134 | +const result = await fetchClaudeUsage("token", 5000, mockFetch); |
| 135 | + |
| 136 | +expect(result.error).toBe("Malformed usage response"); |
| 137 | +expect(result.windows).toHaveLength(0); |
| 138 | +}); |
| 139 | + |
131 | 140 | it("falls back to claude web usage when oauth scope is missing", async () => { |
132 | 141 | vi.stubEnv("CLAUDE_AI_SESSION_KEY", "sk-ant-session-key"); |
133 | 142 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | | -import { buildUsageHttpErrorSnapshot, fetchJson } from "./provider-usage.fetch.shared.js"; |
| 1 | +import { |
| 2 | +buildUsageHttpErrorSnapshot, |
| 3 | +fetchJson, |
| 4 | +readUsageJson, |
| 5 | +} from "./provider-usage.fetch.shared.js"; |
2 | 6 | import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; |
3 | 7 | import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; |
4 | 8 | |
@@ -83,7 +87,11 @@ async function fetchClaudeWebUsage(
|
83 | 87 | return null; |
84 | 88 | } |
85 | 89 | |
86 | | -const orgs = (await orgRes.json()) as ClaudeWebOrganizationsResponse; |
| 90 | +const parsedOrgs = await readUsageJson("anthropic", orgRes); |
| 91 | +if (!parsedOrgs.ok) { |
| 92 | +return null; |
| 93 | +} |
| 94 | +const orgs = parsedOrgs.data as ClaudeWebOrganizationsResponse; |
87 | 95 | const orgId = orgs?.[0]?.uuid?.trim(); |
88 | 96 | if (!orgId) { |
89 | 97 | return null; |
@@ -99,7 +107,11 @@ async function fetchClaudeWebUsage(
|
99 | 107 | return null; |
100 | 108 | } |
101 | 109 | |
102 | | -const data = (await usageRes.json()) as ClaudeWebUsageResponse; |
| 110 | +const parsedUsage = await readUsageJson("anthropic", usageRes); |
| 111 | +if (!parsedUsage.ok) { |
| 112 | +return null; |
| 113 | +} |
| 114 | +const data = parsedUsage.data as ClaudeWebUsageResponse; |
103 | 115 | const windows = buildClaudeUsageWindows(data); |
104 | 116 | |
105 | 117 | if (windows.length === 0) { |
@@ -166,7 +178,11 @@ export async function fetchClaudeUsage(
|
166 | 178 | }); |
167 | 179 | } |
168 | 180 | |
169 | | -const data = (await res.json()) as ClaudeUsageResponse; |
| 181 | +const parsed = await readUsageJson("anthropic", res); |
| 182 | +if (!parsed.ok) { |
| 183 | +return parsed.snapshot; |
| 184 | +} |
| 185 | +const data = parsed.data as ClaudeUsageResponse; |
170 | 186 | const windows = buildClaudeUsageWindows(data); |
171 | 187 | |
172 | 188 | return { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -23,6 +23,15 @@ describe("fetchCodexUsage", () => {
|
23 | 23 | expect(result.windows).toHaveLength(0); |
24 | 24 | }); |
25 | 25 | |
| 26 | +it("returns a stable error for malformed successful usage JSON", async () => { |
| 27 | +const mockFetch = createProviderUsageFetch(async () => makeResponse(200, "{not json")); |
| 28 | + |
| 29 | +const result = await fetchCodexUsage("token", undefined, 5000, mockFetch); |
| 30 | + |
| 31 | +expect(result.error).toBe("Malformed usage response"); |
| 32 | +expect(result.windows).toHaveLength(0); |
| 33 | +}); |
| 34 | + |
26 | 35 | it("parses windows, reset times, and plan balance", async () => { |
27 | 36 | const mockFetch = createProviderUsageFetch(async (_url, init) => { |
28 | 37 | const headers = (init?.headers as Record<string, string> | undefined) ?? {}; |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { resolveProviderRequestHeaders } from "../agents/provider-request-config.js"; |
2 | | -import { buildUsageHttpErrorSnapshot, fetchJson } from "./provider-usage.fetch.shared.js"; |
| 2 | +import { |
| 3 | +buildUsageHttpErrorSnapshot, |
| 4 | +fetchJson, |
| 5 | +readUsageJson, |
| 6 | +} from "./provider-usage.fetch.shared.js"; |
3 | 7 | import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; |
4 | 8 | import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; |
5 | 9 | |
@@ -85,7 +89,11 @@ export async function fetchCodexUsage(
|
85 | 89 | }); |
86 | 90 | } |
87 | 91 | |
88 | | -const data = (await res.json()) as CodexUsageResponse; |
| 92 | +const parsed = await readUsageJson("openai-codex", res); |
| 93 | +if (!parsed.ok) { |
| 94 | +return parsed.snapshot; |
| 95 | +} |
| 96 | +const data = parsed.data as CodexUsageResponse; |
89 | 97 | const windows: UsageWindow[] = []; |
90 | 98 | |
91 | 99 | if (data.rate_limit?.primary_window) { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -15,6 +15,14 @@ describe("fetchGeminiUsage", () => {
|
15 | 15 | expect(result.windows).toHaveLength(0); |
16 | 16 | }); |
17 | 17 | |
| 18 | +it("returns a stable error for malformed successful usage JSON", async () => { |
| 19 | +const mockFetch = createProviderUsageFetch(async () => makeResponse(200, "{not json")); |
| 20 | +const result = await fetchGeminiUsage("token", 5000, mockFetch, usageProvider); |
| 21 | + |
| 22 | +expect(result.error).toBe("Malformed usage response"); |
| 23 | +expect(result.windows).toHaveLength(0); |
| 24 | +}); |
| 25 | + |
18 | 26 | it("selects the lowest remaining fraction per model family", async () => { |
19 | 27 | const mockFetch = createProviderUsageFetch(async (_url, init) => { |
20 | 28 | const headers = (init?.headers as Record<string, string> | undefined) ?? {}; |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; |
2 | | -import { buildUsageHttpErrorSnapshot, fetchJson } from "./provider-usage.fetch.shared.js"; |
| 2 | +import { |
| 3 | +buildUsageHttpErrorSnapshot, |
| 4 | +fetchJson, |
| 5 | +readUsageJson, |
| 6 | +} from "./provider-usage.fetch.shared.js"; |
3 | 7 | import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; |
4 | 8 | import type { |
5 | 9 | ProviderUsageSnapshot, |
@@ -38,7 +42,11 @@ export async function fetchGeminiUsage(
|
38 | 42 | }); |
39 | 43 | } |
40 | 44 | |
41 | | -const data = (await res.json()) as GeminiUsageResponse; |
| 45 | +const parsed = await readUsageJson(provider, res); |
| 46 | +if (!parsed.ok) { |
| 47 | +return parsed.snapshot; |
| 48 | +} |
| 49 | +const data = parsed.data as GeminiUsageResponse; |
42 | 50 | const quotas: Record<string, number> = {}; |
43 | 51 | |
44 | 52 | for (const bucket of data.buckets || []) { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -50,3 +50,17 @@ export function buildUsageHttpErrorSnapshot(
|
50 | 50 | const suffix = options.message?.trim() ? `: ${options.message.trim()}` : ""; |
51 | 51 | return buildUsageErrorSnapshot(options.provider, `HTTP ${options.status}${suffix}`); |
52 | 52 | } |
| 53 | + |
| 54 | +export async function readUsageJson( |
| 55 | +provider: UsageProviderId, |
| 56 | +response: Response, |
| 57 | +): Promise<{ ok: true; data: unknown } | { ok: false; snapshot: ProviderUsageSnapshot }> { |
| 58 | +try { |
| 59 | +return { ok: true, data: await response.json() }; |
| 60 | +} catch { |
| 61 | +return { |
| 62 | +ok: false, |
| 63 | +snapshot: buildUsageErrorSnapshot(provider, "Malformed usage response"), |
| 64 | +}; |
| 65 | +} |
| 66 | +} |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -11,6 +11,14 @@ describe("fetchZaiUsage", () => {
|
11 | 11 | expect(result.windows).toHaveLength(0); |
12 | 12 | }); |
13 | 13 | |
| 14 | +it("returns a stable error for malformed successful usage JSON", async () => { |
| 15 | +const mockFetch = createProviderUsageFetch(async () => makeResponse(200, "{not json")); |
| 16 | +const result = await fetchZaiUsage("key", 5000, mockFetch); |
| 17 | + |
| 18 | +expect(result.error).toBe("Malformed usage response"); |
| 19 | +expect(result.windows).toHaveLength(0); |
| 20 | +}); |
| 21 | + |
14 | 22 | it("returns API message errors for unsuccessful payloads", async () => { |
15 | 23 | const mockFetch = createProviderUsageFetch(async () => |
16 | 24 | makeResponse(200, { |
|
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | | -import { buildUsageHttpErrorSnapshot, fetchJson } from "./provider-usage.fetch.shared.js"; |
| 1 | +import { |
| 2 | +buildUsageHttpErrorSnapshot, |
| 3 | +fetchJson, |
| 4 | +readUsageJson, |
| 5 | +} from "./provider-usage.fetch.shared.js"; |
2 | 6 | import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; |
3 | 7 | import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; |
4 | 8 | |
@@ -44,7 +48,11 @@ export async function fetchZaiUsage(
|
44 | 48 | }); |
45 | 49 | } |
46 | 50 | |
47 | | -const data = (await res.json()) as ZaiUsageResponse; |
| 51 | +const parsed = await readUsageJson("zai", res); |
| 52 | +if (!parsed.ok) { |
| 53 | +return parsed.snapshot; |
| 54 | +} |
| 55 | +const data = parsed.data as ZaiUsageResponse; |
48 | 56 | if (!data.success || data.code !== 200) { |
49 | 57 | const errorMessage = typeof data.msg === "string" ? data.msg.trim() : ""; |
50 | 58 | return { |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。