@opengeni/codex 0.2.3 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-K47573CJ.js → chunk-YGKQDUY7.js} +14 -2
- package/dist/{chunk-K47573CJ.js.map → chunk-YGKQDUY7.js.map} +1 -1
- package/dist/constants.d.ts +8 -2
- package/dist/constants.js +13 -1
- package/dist/index.d.ts +231 -5
- package/dist/index.js +1402 -91
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api-client.ts +131 -22
- package/src/bounded-operation.ts +43 -0
- package/src/constants.ts +20 -2
- package/src/fetch.ts +866 -49
- package/src/index.ts +3 -0
- package/src/model-output-truncation.ts +590 -0
- package/src/oversized-image-card.ts +18 -0
- package/src/refresh.ts +21 -10
- package/src/request-context.ts +38 -0
- package/src/reset-credits.ts +157 -0
- package/src/response-timeout.ts +159 -0
- package/src/usage-normalize.ts +20 -2
package/src/request-context.ts
CHANGED
|
@@ -30,6 +30,38 @@ export type CodexUsageHeaderSnapshot = {
|
|
|
30
30
|
checkedAt: Date;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
export type CodexResponseTimeoutClass = "connect" | "headers" | "idle_stream" | "whole_request";
|
|
34
|
+
|
|
35
|
+
export type CodexResponseTimeoutPolicy = {
|
|
36
|
+
/** Maximum wait for response headers, including DNS/TCP/TLS establishment. */
|
|
37
|
+
headersTimeoutMs: number;
|
|
38
|
+
/** Maximum silence between response-body chunks after headers arrive. */
|
|
39
|
+
streamIdleTimeoutMs: number;
|
|
40
|
+
/** Maximum wall time for one logical Responses request. */
|
|
41
|
+
wholeRequestTimeoutMs: number;
|
|
42
|
+
/**
|
|
43
|
+
* Reserved compatibility field. It is currently normalized to zero because
|
|
44
|
+
* an absent response does not prove that the provider never accepted a
|
|
45
|
+
* request, so automatic replay is not safe without an operation receipt.
|
|
46
|
+
*/
|
|
47
|
+
noByteRetries: number;
|
|
48
|
+
retryBackoffMs: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type CodexModelRequestEvent = {
|
|
52
|
+
requestId: string;
|
|
53
|
+
transportAttempt: number;
|
|
54
|
+
phase: "started" | "headers" | "first_byte" | "completed" | "failed" | "timed_out";
|
|
55
|
+
model?: string;
|
|
56
|
+
durationMs: number;
|
|
57
|
+
responseObserved: boolean;
|
|
58
|
+
timeoutPolicy: CodexResponseTimeoutPolicy;
|
|
59
|
+
timeoutClass?: CodexResponseTimeoutClass;
|
|
60
|
+
providerRequestId?: string;
|
|
61
|
+
status?: number;
|
|
62
|
+
willRetry?: boolean;
|
|
63
|
+
};
|
|
64
|
+
|
|
33
65
|
export type CodexRequestContext = {
|
|
34
66
|
clientVersion: string;
|
|
35
67
|
/**
|
|
@@ -58,6 +90,12 @@ export type CodexRequestContext = {
|
|
|
58
90
|
* P2 usage cache once per turn in its `finally` — packages/codex stays db-free.
|
|
59
91
|
*/
|
|
60
92
|
onUsageHeaders?: (snapshot: CodexUsageHeaderSnapshot) => void;
|
|
93
|
+
/** Optional per-run override, primarily for deterministic transport tests. */
|
|
94
|
+
responseTimeoutPolicy?: Partial<CodexResponseTimeoutPolicy>;
|
|
95
|
+
/** Worker-owned durable audit sink; payloads never contain request bodies or auth. */
|
|
96
|
+
onModelRequestEvent?: (event: CodexModelRequestEvent) => Promise<void> | void;
|
|
97
|
+
/** Stable request identity supplied by the owning durable execution. */
|
|
98
|
+
nextRequestId?: () => string;
|
|
61
99
|
};
|
|
62
100
|
|
|
63
101
|
export const codexRequestStorage = new AsyncLocalStorage<CodexRequestContext>();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Exact Codex rust-v0.144.6 rate-limit-reset-credit protocol normalization.
|
|
2
|
+
// Provenance: stable commit 5d1fbf26c43abc65a203928b2e31561cb039e06d;
|
|
3
|
+
// protocol-bearing files are byte-identical from rust-v0.144.1 through v0.144.6.
|
|
4
|
+
//
|
|
5
|
+
// Upstream sources (stable tag target 5d1fbf26c43abc65a203928b2e31561cb039e06d):
|
|
6
|
+
// - codex-rs/backend-client/src/types.rs
|
|
7
|
+
// - codex-rs/backend-client/src/client/rate_limit_resets.rs
|
|
8
|
+
// - codex-rs/app-server-protocol/src/protocol/v2/account.rs
|
|
9
|
+
//
|
|
10
|
+
// The backend wire is snake_case. Public OpenGeni callers only receive the
|
|
11
|
+
// normalized camelCase types below. Unknown reset types/statuses remain visible
|
|
12
|
+
// but fail closed as `unknown`; they are never made actionable by this parser.
|
|
13
|
+
|
|
14
|
+
import * as z from "zod/v4";
|
|
15
|
+
|
|
16
|
+
export const CODEX_RATE_LIMIT_RESET_OUTCOMES = [
|
|
17
|
+
"reset",
|
|
18
|
+
"nothingToReset",
|
|
19
|
+
"noCredit",
|
|
20
|
+
"alreadyRedeemed",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type CodexRateLimitResetOutcome = (typeof CODEX_RATE_LIMIT_RESET_OUTCOMES)[number];
|
|
24
|
+
export type CodexRateLimitResetType = "codexRateLimits" | "unknown";
|
|
25
|
+
export type CodexRateLimitResetCreditStatus = "available" | "redeeming" | "redeemed" | "unknown";
|
|
26
|
+
|
|
27
|
+
export type CodexRateLimitResetCredit = {
|
|
28
|
+
id: string;
|
|
29
|
+
resetType: CodexRateLimitResetType;
|
|
30
|
+
status: CodexRateLimitResetCreditStatus;
|
|
31
|
+
/** Unix seconds, matching account/rateLimits/read in Codex v0.144.6. */
|
|
32
|
+
grantedAt: number;
|
|
33
|
+
/** Unix seconds, or null when the provider says the credit does not expire. */
|
|
34
|
+
expiresAt: number | null;
|
|
35
|
+
title: string | null;
|
|
36
|
+
description: string | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type CodexRateLimitResetCreditsDetails = {
|
|
40
|
+
availableCount: number;
|
|
41
|
+
credits: CodexRateLimitResetCredit[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type CodexRateLimitResetCreditsSummary = {
|
|
45
|
+
availableCount: number;
|
|
46
|
+
/** null means the provider supplied an authoritative count but no detail rows. */
|
|
47
|
+
credits: null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type CodexRateLimitResetConsumeResponse = {
|
|
51
|
+
outcome: CodexRateLimitResetOutcome;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const nonNegativeInteger = z.number().int().nonnegative();
|
|
55
|
+
const backendTimestamp = z.string().datetime({ offset: true });
|
|
56
|
+
|
|
57
|
+
const backendCreditSchema = z
|
|
58
|
+
.object({
|
|
59
|
+
id: z.string().min(1),
|
|
60
|
+
reset_type: z.string().min(1),
|
|
61
|
+
status: z.string().min(1),
|
|
62
|
+
granted_at: backendTimestamp,
|
|
63
|
+
expires_at: backendTimestamp.nullish(),
|
|
64
|
+
title: z.string().nullish(),
|
|
65
|
+
description: z.string().nullish(),
|
|
66
|
+
})
|
|
67
|
+
.passthrough();
|
|
68
|
+
|
|
69
|
+
const backendDetailsSchema = z
|
|
70
|
+
.object({
|
|
71
|
+
credits: z.array(backendCreditSchema),
|
|
72
|
+
available_count: nonNegativeInteger,
|
|
73
|
+
})
|
|
74
|
+
.passthrough();
|
|
75
|
+
|
|
76
|
+
const backendUsageSummarySchema = z
|
|
77
|
+
.object({
|
|
78
|
+
rate_limit_reset_credits: z
|
|
79
|
+
.object({ available_count: nonNegativeInteger })
|
|
80
|
+
.passthrough()
|
|
81
|
+
.nullish(),
|
|
82
|
+
})
|
|
83
|
+
.passthrough();
|
|
84
|
+
|
|
85
|
+
const backendConsumeOutcomes = [
|
|
86
|
+
"reset",
|
|
87
|
+
"nothing_to_reset",
|
|
88
|
+
"no_credit",
|
|
89
|
+
"already_redeemed",
|
|
90
|
+
] as const;
|
|
91
|
+
|
|
92
|
+
const backendConsumeSchema = z
|
|
93
|
+
.object({
|
|
94
|
+
code: z.enum(backendConsumeOutcomes),
|
|
95
|
+
// The app-server intentionally discards this field. OpenGeni also refetches
|
|
96
|
+
// rather than inferring post-redemption state from it.
|
|
97
|
+
windows_reset: nonNegativeInteger.default(0),
|
|
98
|
+
})
|
|
99
|
+
.passthrough();
|
|
100
|
+
|
|
101
|
+
function normalizedResetType(value: string): CodexRateLimitResetType {
|
|
102
|
+
return value === "codex_rate_limits" ? "codexRateLimits" : "unknown";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizedCreditStatus(value: string): CodexRateLimitResetCreditStatus {
|
|
106
|
+
if (value === "available" || value === "redeeming" || value === "redeemed") {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
return "unknown";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Parse the exact detailed-credit backend response. Unknown rows stay view-only. */
|
|
113
|
+
export function parseCodexRateLimitResetCreditsDetails(
|
|
114
|
+
payload: unknown,
|
|
115
|
+
): CodexRateLimitResetCreditsDetails | null {
|
|
116
|
+
const parsed = backendDetailsSchema.safeParse(payload);
|
|
117
|
+
if (!parsed.success) return null;
|
|
118
|
+
return {
|
|
119
|
+
availableCount: parsed.data.available_count,
|
|
120
|
+
credits: parsed.data.credits.map((credit) => ({
|
|
121
|
+
id: credit.id,
|
|
122
|
+
resetType: normalizedResetType(credit.reset_type),
|
|
123
|
+
status: normalizedCreditStatus(credit.status),
|
|
124
|
+
grantedAt: Math.floor(Date.parse(credit.granted_at) / 1000),
|
|
125
|
+
expiresAt:
|
|
126
|
+
credit.expires_at == null ? null : Math.floor(Date.parse(credit.expires_at) / 1000),
|
|
127
|
+
title: credit.title ?? null,
|
|
128
|
+
description: credit.description ?? null,
|
|
129
|
+
})),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Parse the count-only summary carried by GET /wham/usage. */
|
|
134
|
+
export function parseCodexRateLimitResetCreditsSummary(
|
|
135
|
+
payload: unknown,
|
|
136
|
+
): CodexRateLimitResetCreditsSummary | null {
|
|
137
|
+
const parsed = backendUsageSummarySchema.safeParse(payload);
|
|
138
|
+
const availableCount = parsed.success
|
|
139
|
+
? parsed.data.rate_limit_reset_credits?.available_count
|
|
140
|
+
: undefined;
|
|
141
|
+
return availableCount === undefined ? null : { availableCount, credits: null };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Parse one of the exact four v0.144.6 consume outcomes. Unknowns fail closed. */
|
|
145
|
+
export function parseCodexRateLimitResetConsumeResponse(
|
|
146
|
+
payload: unknown,
|
|
147
|
+
): CodexRateLimitResetConsumeResponse | null {
|
|
148
|
+
const parsed = backendConsumeSchema.safeParse(payload);
|
|
149
|
+
if (!parsed.success) return null;
|
|
150
|
+
const outcomes: Record<(typeof backendConsumeOutcomes)[number], CodexRateLimitResetOutcome> = {
|
|
151
|
+
reset: "reset",
|
|
152
|
+
nothing_to_reset: "nothingToReset",
|
|
153
|
+
no_credit: "noCredit",
|
|
154
|
+
already_redeemed: "alreadyRedeemed",
|
|
155
|
+
};
|
|
156
|
+
return { outcome: outcomes[parsed.data.code] };
|
|
157
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CODEX_RESPONSE_HEADERS_TIMEOUT_MS,
|
|
3
|
+
CODEX_RESPONSE_NO_BYTE_RETRIES,
|
|
4
|
+
CODEX_RESPONSE_RETRY_BACKOFF_MS,
|
|
5
|
+
CODEX_RESPONSE_STREAM_IDLE_TIMEOUT_MS,
|
|
6
|
+
CODEX_RESPONSE_WHOLE_TIMEOUT_MS,
|
|
7
|
+
} from "./constants";
|
|
8
|
+
import type { CodexResponseTimeoutClass, CodexResponseTimeoutPolicy } from "./request-context";
|
|
9
|
+
|
|
10
|
+
export const CODEX_RESPONSE_TIMEOUT_ERROR_TYPE = "opengeni_codex_response_timeout";
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_CODEX_RESPONSE_TIMEOUT_POLICY: CodexResponseTimeoutPolicy = Object.freeze({
|
|
13
|
+
headersTimeoutMs: CODEX_RESPONSE_HEADERS_TIMEOUT_MS,
|
|
14
|
+
streamIdleTimeoutMs: CODEX_RESPONSE_STREAM_IDLE_TIMEOUT_MS,
|
|
15
|
+
wholeRequestTimeoutMs: CODEX_RESPONSE_WHOLE_TIMEOUT_MS,
|
|
16
|
+
noByteRetries: CODEX_RESPONSE_NO_BYTE_RETRIES,
|
|
17
|
+
retryBackoffMs: CODEX_RESPONSE_RETRY_BACKOFF_MS,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function positiveFinite(value: number | undefined, fallback: number): number {
|
|
21
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveCodexResponseTimeoutPolicy(
|
|
25
|
+
override: Partial<CodexResponseTimeoutPolicy> | undefined,
|
|
26
|
+
): CodexResponseTimeoutPolicy {
|
|
27
|
+
return {
|
|
28
|
+
headersTimeoutMs: positiveFinite(
|
|
29
|
+
override?.headersTimeoutMs,
|
|
30
|
+
DEFAULT_CODEX_RESPONSE_TIMEOUT_POLICY.headersTimeoutMs,
|
|
31
|
+
),
|
|
32
|
+
streamIdleTimeoutMs: positiveFinite(
|
|
33
|
+
override?.streamIdleTimeoutMs,
|
|
34
|
+
DEFAULT_CODEX_RESPONSE_TIMEOUT_POLICY.streamIdleTimeoutMs,
|
|
35
|
+
),
|
|
36
|
+
wholeRequestTimeoutMs: positiveFinite(
|
|
37
|
+
override?.wholeRequestTimeoutMs,
|
|
38
|
+
DEFAULT_CODEX_RESPONSE_TIMEOUT_POLICY.wholeRequestTimeoutMs,
|
|
39
|
+
),
|
|
40
|
+
// Automatic replay is fail-closed until the provider operation can be
|
|
41
|
+
// durably read/reconciled. Keep the field for policy/event compatibility,
|
|
42
|
+
// but never let a caller opt back into an unproved retry.
|
|
43
|
+
noByteRetries: 0,
|
|
44
|
+
retryBackoffMs:
|
|
45
|
+
override?.retryBackoffMs !== undefined &&
|
|
46
|
+
Number.isFinite(override.retryBackoffMs) &&
|
|
47
|
+
override.retryBackoffMs >= 0
|
|
48
|
+
? override.retryBackoffMs
|
|
49
|
+
: DEFAULT_CODEX_RESPONSE_TIMEOUT_POLICY.retryBackoffMs,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class CodexResponseTimeoutError extends Error {
|
|
54
|
+
readonly code = CODEX_RESPONSE_TIMEOUT_ERROR_TYPE;
|
|
55
|
+
readonly type = CODEX_RESPONSE_TIMEOUT_ERROR_TYPE;
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
readonly timeoutClass: CodexResponseTimeoutClass,
|
|
59
|
+
readonly requestId: string,
|
|
60
|
+
readonly responseObserved: boolean,
|
|
61
|
+
message = `Codex response ${timeoutClass.replaceAll("_", " ")} timed out`,
|
|
62
|
+
) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "CodexResponseTimeoutError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type CodexResponseTimeoutInfo = {
|
|
69
|
+
timeoutClass: CodexResponseTimeoutClass;
|
|
70
|
+
requestId: string | null;
|
|
71
|
+
responseObserved: boolean;
|
|
72
|
+
message: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function parseTimeoutClass(value: unknown): CodexResponseTimeoutClass | null {
|
|
76
|
+
return value === "connect" ||
|
|
77
|
+
value === "headers" ||
|
|
78
|
+
value === "idle_stream" ||
|
|
79
|
+
value === "whole_request"
|
|
80
|
+
? value
|
|
81
|
+
: null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Recover structured transport timeouts through SDK wrapping. The optional
|
|
86
|
+
* legacy match is deliberately opt-in: `Request timed out.` alone has no
|
|
87
|
+
* provider provenance and the worker enables it only for a confirmed Codex
|
|
88
|
+
* subscription turn.
|
|
89
|
+
*/
|
|
90
|
+
export function classifyCodexResponseTimeoutError(
|
|
91
|
+
error: unknown,
|
|
92
|
+
options: { allowLegacyRequestTimeout?: boolean } = {},
|
|
93
|
+
): CodexResponseTimeoutInfo | null {
|
|
94
|
+
let current: unknown = error;
|
|
95
|
+
for (let depth = 0; depth < 8 && current && typeof current === "object"; depth += 1) {
|
|
96
|
+
const value = current as Record<string, unknown>;
|
|
97
|
+
const nested =
|
|
98
|
+
value.error && typeof value.error === "object"
|
|
99
|
+
? (value.error as Record<string, unknown>)
|
|
100
|
+
: undefined;
|
|
101
|
+
const type =
|
|
102
|
+
(typeof value.type === "string" ? value.type : undefined) ??
|
|
103
|
+
(typeof value.code === "string" ? value.code : undefined) ??
|
|
104
|
+
(typeof nested?.type === "string" ? nested.type : undefined) ??
|
|
105
|
+
(typeof nested?.code === "string" ? nested.code : undefined);
|
|
106
|
+
if (type === CODEX_RESPONSE_TIMEOUT_ERROR_TYPE || value.name === "CodexResponseTimeoutError") {
|
|
107
|
+
const klass =
|
|
108
|
+
parseTimeoutClass(value.timeoutClass) ??
|
|
109
|
+
parseTimeoutClass(nested?.timeout_class) ??
|
|
110
|
+
"headers";
|
|
111
|
+
return {
|
|
112
|
+
timeoutClass: klass,
|
|
113
|
+
requestId:
|
|
114
|
+
(typeof value.requestId === "string" ? value.requestId : undefined) ??
|
|
115
|
+
(typeof nested?.request_id === "string" ? nested.request_id : null),
|
|
116
|
+
responseObserved:
|
|
117
|
+
typeof value.responseObserved === "boolean"
|
|
118
|
+
? value.responseObserved
|
|
119
|
+
: nested?.response_observed === true,
|
|
120
|
+
message:
|
|
121
|
+
(typeof value.message === "string" ? value.message : undefined) ??
|
|
122
|
+
(typeof nested?.message === "string" ? nested.message : "Codex response timed out"),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
current = value.cause;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (options.allowLegacyRequestTimeout && error && typeof error === "object") {
|
|
129
|
+
const value = error as Record<string, unknown>;
|
|
130
|
+
if (
|
|
131
|
+
value.name === "APIConnectionTimeoutError" ||
|
|
132
|
+
(value.message === "Request timed out." && value.name === "Error")
|
|
133
|
+
) {
|
|
134
|
+
return {
|
|
135
|
+
timeoutClass: "headers",
|
|
136
|
+
requestId: null,
|
|
137
|
+
responseObserved: false,
|
|
138
|
+
message: String(value.message ?? "Request timed out."),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function isPreHeadersTimeoutError(error: unknown): CodexResponseTimeoutClass | null {
|
|
146
|
+
const structured = classifyCodexResponseTimeoutError(error);
|
|
147
|
+
if (structured && !structured.responseObserved) {
|
|
148
|
+
return structured.timeoutClass;
|
|
149
|
+
}
|
|
150
|
+
if (!error || typeof error !== "object") return null;
|
|
151
|
+
const value = error as Record<string, unknown>;
|
|
152
|
+
const code = typeof value.code === "string" ? value.code : "";
|
|
153
|
+
const name = typeof value.name === "string" ? value.name : "";
|
|
154
|
+
const message = typeof value.message === "string" ? value.message : String(error);
|
|
155
|
+
if (/^(?:ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT)$/i.test(code) || /ConnectTimeout/i.test(name)) {
|
|
156
|
+
return "connect";
|
|
157
|
+
}
|
|
158
|
+
return /connect(?:ion)?[^.]*timed?\s*out/i.test(`${name} ${message}`) ? "connect" : null;
|
|
159
|
+
}
|
package/src/usage-normalize.ts
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
// limit-reached body. The parser is zod over rate_limit.{primary,secondary}_window.
|
|
12
12
|
|
|
13
13
|
import * as z from "zod/v4";
|
|
14
|
+
import {
|
|
15
|
+
parseCodexRateLimitResetCreditsSummary,
|
|
16
|
+
type CodexRateLimitResetCreditsSummary,
|
|
17
|
+
} from "./reset-credits";
|
|
14
18
|
|
|
15
19
|
/** The 5-hour (primary) window's `limit_window_seconds`. */
|
|
16
20
|
export const CODEX_FIVE_HOUR_WINDOW_SECONDS = 18000;
|
|
@@ -46,12 +50,22 @@ export type CodexUsagePayload = {
|
|
|
46
50
|
weekly: CodexUsageWindow | null; // ← rate_limit.secondary_window (604800)
|
|
47
51
|
limitReached: boolean; // rate_limit.limit_reached || !rate_limit.allowed
|
|
48
52
|
fetchedAt: string; // ISO; server stamp
|
|
53
|
+
/**
|
|
54
|
+
* Authoritative count-only reset-credit summary from the usage response.
|
|
55
|
+
* Detail rows are fetched separately and are never synthesized from this.
|
|
56
|
+
*/
|
|
57
|
+
rateLimitResetCredits: CodexRateLimitResetCreditsSummary | null;
|
|
49
58
|
/** Present only on a refresh/auth failure path; carries the precise reason. */
|
|
50
59
|
reason?: "needs_relogin" | undefined;
|
|
51
60
|
// forward-compat, populated but unused in P2:
|
|
52
61
|
additionalLimits?: CodexAdditionalLimit[] | undefined;
|
|
53
62
|
credits?:
|
|
54
|
-
| {
|
|
63
|
+
| {
|
|
64
|
+
hasCredits: boolean;
|
|
65
|
+
unlimited: boolean;
|
|
66
|
+
overageLimitReached: boolean;
|
|
67
|
+
balance: string;
|
|
68
|
+
}
|
|
55
69
|
| undefined;
|
|
56
70
|
};
|
|
57
71
|
|
|
@@ -177,7 +191,10 @@ function pickWindows(
|
|
|
177
191
|
// Track each unplaced window with the slot it came from, so the positional
|
|
178
192
|
// fallback can place it (re-normalizing produces a fresh object that would
|
|
179
193
|
// never match by reference — the bug this replaces).
|
|
180
|
-
const unplaced: Array<{
|
|
194
|
+
const unplaced: Array<{
|
|
195
|
+
slot: "primary" | "secondary";
|
|
196
|
+
window: CodexUsageWindow;
|
|
197
|
+
}> = [];
|
|
181
198
|
for (const [slot, raw] of [
|
|
182
199
|
["primary", primary],
|
|
183
200
|
["secondary", secondary],
|
|
@@ -219,6 +236,7 @@ export function normalizeCodexUsage(httpStatus: number, rawPayload: unknown): Co
|
|
|
219
236
|
weekly: null,
|
|
220
237
|
limitReached: false,
|
|
221
238
|
fetchedAt,
|
|
239
|
+
rateLimitResetCredits: parseCodexRateLimitResetCreditsSummary(rawPayload),
|
|
222
240
|
};
|
|
223
241
|
|
|
224
242
|
// A non-404 HTTP error, or a body we could not parse at all, is an error state.
|