From 136386e1c3c20589db0121fe4b3bc52f3afd0f9d Mon Sep 17 00:00:00 2001 From: jayp-oai Date: Sun, 28 Jun 2026 15:23:58 -0700 Subject: [PATCH 1/4] Expose rate-limit reset credit details --- .../schema/json/ClientRequest.json | 35 ++++ .../codex_app_server_protocol.schemas.json | 107 ++++++++++++ .../codex_app_server_protocol.v2.schemas.json | 107 ++++++++++++ ...AccountRateLimitResetCreditListParams.json | 6 + ...countRateLimitResetCreditListResponse.json | 72 ++++++++ ...sumeAccountRateLimitResetCreditParams.json | 7 + .../schema/typescript/ClientRequest.ts | 3 +- .../AccountRateLimitResetCreditListParams.ts | 11 ++ ...AccountRateLimitResetCreditListResponse.ts | 12 ++ ...onsumeAccountRateLimitResetCreditParams.ts | 7 +- .../typescript/v2/RateLimitResetCredit.ts | 19 ++ .../v2/RateLimitResetCreditStatus.ts | 5 + .../typescript/v2/RateLimitResetType.ts | 5 + .../schema/typescript/v2/index.ts | 5 + .../src/protocol/common.rs | 6 + .../src/protocol/v2/account.rs | 58 +++++++ codex-rs/app-server/README.md | 55 +++++- codex-rs/app-server/src/message_processor.rs | 5 + codex-rs/app-server/src/request_processors.rs | 6 + .../request_processors/account_processor.rs | 8 + .../account_processor/rate_limit_resets.rs | 98 ++++++++++- .../tests/common/test_app_server.rs | 11 ++ .../suite/v2/rate_limit_reset_credits.rs | 162 ++++++++++++++++++ .../app-server/tests/suite/v2/rate_limits.rs | 6 + .../src/client/rate_limit_resets.rs | 44 ++++- .../src/client/rate_limit_resets_tests.rs | 73 ++++++++ codex-rs/backend-client/src/lib.rs | 2 + codex-rs/backend-client/src/types.rs | 15 ++ codex-rs/tui/src/app/background_requests.rs | 5 +- 29 files changed, 940 insertions(+), 15 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListParams.json create mode 100644 codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListResponse.json create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListParams.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListResponse.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCredit.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditStatus.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetType.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index f3983e31f41c..3c2724351721 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -5,6 +5,10 @@ "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "type": "string" }, + "AccountRateLimitResetCreditListParams": { + "description": "Parameters for listing available reset credits.\n\nThe upstream API counts every available credit but returns details for at most the first 10 and does not expose a pagination cursor, so this method cannot truthfully expose `cursor` or `limit`.", + "type": "object" + }, "AddCreditsNudgeCreditType": { "enum": [ "credits", @@ -556,6 +560,13 @@ }, "ConsumeAccountRateLimitResetCreditParams": { "properties": { + "creditId": { + "description": "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.", + "type": [ + "string", + "null" + ] + }, "idempotencyKey": { "description": "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.", "type": "string" @@ -6445,6 +6456,30 @@ "title": "Account/rateLimits/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/rateLimitResetCredit/list" + ], + "title": "Account/rateLimitResetCredit/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AccountRateLimitResetCreditListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Account/rateLimitResetCredit/listRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index b77b512b36e3..4c0683a94785 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -1857,6 +1857,30 @@ "title": "Account/rateLimits/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "account/rateLimitResetCredit/list" + ], + "title": "Account/rateLimitResetCredit/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/AccountRateLimitResetCreditListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Account/rateLimitResetCredit/listRequest", + "type": "object" + }, { "properties": { "id": { @@ -5975,6 +5999,34 @@ "title": "AccountLoginCompletedNotification", "type": "object" }, + "AccountRateLimitResetCreditListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for listing available reset credits.\n\nThe upstream API counts every available credit but returns details for at most the first 10 and does not expose a pagination cursor, so this method cannot truthfully expose `cursor` or `limit`.", + "title": "AccountRateLimitResetCreditListParams", + "type": "object" + }, + "AccountRateLimitResetCreditListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Available reset credits returned by the backend.\n\nThis is a non-pageable snapshot because the upstream API does not expose a cursor. `data` contains details for at most 10 credits, so it can contain fewer rows than `availableCount`.", + "properties": { + "availableCount": { + "format": "int64", + "type": "integer" + }, + "data": { + "items": { + "$ref": "#/definitions/v2/RateLimitResetCredit" + }, + "type": "array" + } + }, + "required": [ + "availableCount", + "data" + ], + "title": "AccountRateLimitResetCreditListResponse", + "type": "object" + }, "AccountRateLimitsUpdatedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Sparse rolling rate-limit update.\n\nClients should merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and does not clear a previously observed value.", @@ -8688,6 +8740,13 @@ "ConsumeAccountRateLimitResetCreditParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { + "creditId": { + "description": "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.", + "type": [ + "string", + "null" + ] + }, "idempotencyKey": { "description": "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.", "type": "string" @@ -14509,6 +14568,48 @@ ], "type": "string" }, + "RateLimitResetCredit": { + "properties": { + "expiresAt": { + "description": "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "grantedAt": { + "description": "Unix timestamp in seconds when the credit was granted.", + "format": "int64", + "type": "integer" + }, + "id": { + "description": "Opaque backend identifier for this reset credit.", + "type": "string" + }, + "resetType": { + "$ref": "#/definitions/v2/RateLimitResetType" + }, + "status": { + "$ref": "#/definitions/v2/RateLimitResetCreditStatus" + } + }, + "required": [ + "grantedAt", + "id", + "resetType", + "status" + ], + "type": "object" + }, + "RateLimitResetCreditStatus": { + "enum": [ + "available", + "redeeming", + "redeemed" + ], + "type": "string" + }, "RateLimitResetCreditsSummary": { "properties": { "availableCount": { @@ -14521,6 +14622,12 @@ ], "type": "object" }, + "RateLimitResetType": { + "enum": [ + "codexRateLimits" + ], + "type": "string" + }, "RateLimitSnapshot": { "properties": { "credits": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index bdd61d6b9436..1e4074d4a0d1 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -101,6 +101,34 @@ "title": "AccountLoginCompletedNotification", "type": "object" }, + "AccountRateLimitResetCreditListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for listing available reset credits.\n\nThe upstream API counts every available credit but returns details for at most the first 10 and does not expose a pagination cursor, so this method cannot truthfully expose `cursor` or `limit`.", + "title": "AccountRateLimitResetCreditListParams", + "type": "object" + }, + "AccountRateLimitResetCreditListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Available reset credits returned by the backend.\n\nThis is a non-pageable snapshot because the upstream API does not expose a cursor. `data` contains details for at most 10 credits, so it can contain fewer rows than `availableCount`.", + "properties": { + "availableCount": { + "format": "int64", + "type": "integer" + }, + "data": { + "items": { + "$ref": "#/definitions/RateLimitResetCredit" + }, + "type": "array" + } + }, + "required": [ + "availableCount", + "data" + ], + "title": "AccountRateLimitResetCreditListResponse", + "type": "object" + }, "AccountRateLimitsUpdatedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Sparse rolling rate-limit update.\n\nClients should merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and does not clear a previously observed value.", @@ -2887,6 +2915,30 @@ "title": "Account/rateLimits/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/rateLimitResetCredit/list" + ], + "title": "Account/rateLimitResetCredit/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AccountRateLimitResetCreditListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Account/rateLimitResetCredit/listRequest", + "type": "object" + }, { "properties": { "id": { @@ -4928,6 +4980,13 @@ "ConsumeAccountRateLimitResetCreditParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { + "creditId": { + "description": "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.", + "type": [ + "string", + "null" + ] + }, "idempotencyKey": { "description": "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.", "type": "string" @@ -10913,6 +10972,48 @@ ], "type": "string" }, + "RateLimitResetCredit": { + "properties": { + "expiresAt": { + "description": "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "grantedAt": { + "description": "Unix timestamp in seconds when the credit was granted.", + "format": "int64", + "type": "integer" + }, + "id": { + "description": "Opaque backend identifier for this reset credit.", + "type": "string" + }, + "resetType": { + "$ref": "#/definitions/RateLimitResetType" + }, + "status": { + "$ref": "#/definitions/RateLimitResetCreditStatus" + } + }, + "required": [ + "grantedAt", + "id", + "resetType", + "status" + ], + "type": "object" + }, + "RateLimitResetCreditStatus": { + "enum": [ + "available", + "redeeming", + "redeemed" + ], + "type": "string" + }, "RateLimitResetCreditsSummary": { "properties": { "availableCount": { @@ -10925,6 +11026,12 @@ ], "type": "object" }, + "RateLimitResetType": { + "enum": [ + "codexRateLimits" + ], + "type": "string" + }, "RateLimitSnapshot": { "properties": { "credits": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListParams.json b/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListParams.json new file mode 100644 index 000000000000..ea65abee0a0e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListParams.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for listing available reset credits.\n\nThe upstream API counts every available credit but returns details for at most the first 10 and does not expose a pagination cursor, so this method cannot truthfully expose `cursor` or `limit`.", + "title": "AccountRateLimitResetCreditListParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListResponse.json new file mode 100644 index 000000000000..f0e101fde325 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListResponse.json @@ -0,0 +1,72 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "RateLimitResetCredit": { + "properties": { + "expiresAt": { + "description": "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "grantedAt": { + "description": "Unix timestamp in seconds when the credit was granted.", + "format": "int64", + "type": "integer" + }, + "id": { + "description": "Opaque backend identifier for this reset credit.", + "type": "string" + }, + "resetType": { + "$ref": "#/definitions/RateLimitResetType" + }, + "status": { + "$ref": "#/definitions/RateLimitResetCreditStatus" + } + }, + "required": [ + "grantedAt", + "id", + "resetType", + "status" + ], + "type": "object" + }, + "RateLimitResetCreditStatus": { + "enum": [ + "available", + "redeeming", + "redeemed" + ], + "type": "string" + }, + "RateLimitResetType": { + "enum": [ + "codexRateLimits" + ], + "type": "string" + } + }, + "description": "Available reset credits returned by the backend.\n\nThis is a non-pageable snapshot because the upstream API does not expose a cursor. `data` contains details for at most 10 credits, so it can contain fewer rows than `availableCount`.", + "properties": { + "availableCount": { + "format": "int64", + "type": "integer" + }, + "data": { + "items": { + "$ref": "#/definitions/RateLimitResetCredit" + }, + "type": "array" + } + }, + "required": [ + "availableCount", + "data" + ], + "title": "AccountRateLimitResetCreditListResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConsumeAccountRateLimitResetCreditParams.json b/codex-rs/app-server-protocol/schema/json/v2/ConsumeAccountRateLimitResetCreditParams.json index c9bf023a639d..3d9d2c1e9817 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConsumeAccountRateLimitResetCreditParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConsumeAccountRateLimitResetCreditParams.json @@ -1,6 +1,13 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { + "creditId": { + "description": "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.", + "type": [ + "string", + "null" + ] + }, "idempotencyKey": { "description": "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 6af04ef40119..1d0223eb3d8a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -7,6 +7,7 @@ import type { GetConversationSummaryParams } from "./GetConversationSummaryParam import type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams"; import type { InitializeParams } from "./InitializeParams"; import type { RequestId } from "./RequestId"; +import type { AccountRateLimitResetCreditListParams } from "./v2/AccountRateLimitResetCreditListParams"; import type { AppsListParams } from "./v2/AppsListParams"; import type { CancelLoginAccountParams } from "./v2/CancelLoginAccountParams"; import type { CommandExecParams } from "./v2/CommandExecParams"; @@ -88,4 +89,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/rateLimitResetCredit/consume", id: RequestId, params: ConsumeAccountRateLimitResetCreditParams, } | { "method": "account/usage/read", id: RequestId, params: undefined, } | { "method": "account/workspaceMessages/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "externalAgentConfig/import/readHistories", id: RequestId, params: undefined, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/rateLimitResetCredit/list", id: RequestId, params: AccountRateLimitResetCreditListParams, } | { "method": "account/rateLimitResetCredit/consume", id: RequestId, params: ConsumeAccountRateLimitResetCreditParams, } | { "method": "account/usage/read", id: RequestId, params: undefined, } | { "method": "account/workspaceMessages/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "externalAgentConfig/import/readHistories", id: RequestId, params: undefined, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListParams.ts new file mode 100644 index 000000000000..72a7f4279040 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListParams.ts @@ -0,0 +1,11 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Parameters for listing available reset credits. + * + * The upstream API counts every available credit but returns details for at most the first 10 and + * does not expose a pagination cursor, so this method cannot truthfully expose `cursor` or `limit`. + */ +export type AccountRateLimitResetCreditListParams = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListResponse.ts new file mode 100644 index 000000000000..58d40b39f453 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListResponse.ts @@ -0,0 +1,12 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RateLimitResetCredit } from "./RateLimitResetCredit"; + +/** + * Available reset credits returned by the backend. + * + * This is a non-pageable snapshot because the upstream API does not expose a cursor. `data` + * contains details for at most 10 credits, so it can contain fewer rows than `availableCount`. + */ +export type AccountRateLimitResetCreditListResponse = { availableCount: number, data: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConsumeAccountRateLimitResetCreditParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConsumeAccountRateLimitResetCreditParams.ts index c3cef64f36a5..f1c5bf351ed3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConsumeAccountRateLimitResetCreditParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConsumeAccountRateLimitResetCreditParams.ts @@ -7,4 +7,9 @@ export type ConsumeAccountRateLimitResetCreditParams = { * Identifies one logical reset attempt. A UUID is recommended; reuse the same value when * retrying that attempt. */ -idempotencyKey: string, }; +idempotencyKey: string, +/** + * Opaque reset-credit identifier to redeem. When omitted, the backend selects the next + * available credit. + */ +creditId?: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCredit.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCredit.ts new file mode 100644 index 000000000000..d393eb09eb41 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCredit.ts @@ -0,0 +1,19 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RateLimitResetCreditStatus } from "./RateLimitResetCreditStatus"; +import type { RateLimitResetType } from "./RateLimitResetType"; + +export type RateLimitResetCredit = { +/** + * Opaque backend identifier for this reset credit. + */ +id: string, resetType: RateLimitResetType, status: RateLimitResetCreditStatus, +/** + * Unix timestamp in seconds when the credit was granted. + */ +grantedAt: number, +/** + * Unix timestamp in seconds when the credit expires, or `null` if it does not expire. + */ +expiresAt: number | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditStatus.ts new file mode 100644 index 000000000000..254923c2bf25 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RateLimitResetCreditStatus = "available" | "redeeming" | "redeemed"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetType.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetType.ts new file mode 100644 index 000000000000..6c77b0b16071 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RateLimitResetType = "codexRateLimits"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index c405082c4c8f..54626e7324bf 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -2,6 +2,8 @@ export type { Account } from "./Account"; export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification"; +export type { AccountRateLimitResetCreditListParams } from "./AccountRateLimitResetCreditListParams"; +export type { AccountRateLimitResetCreditListResponse } from "./AccountRateLimitResetCreditListResponse"; export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification"; export type { AccountTokenUsageDailyBucket } from "./AccountTokenUsageDailyBucket"; export type { AccountTokenUsageSummary } from "./AccountTokenUsageSummary"; @@ -335,7 +337,10 @@ export type { ProcessOutputDeltaNotification } from "./ProcessOutputDeltaNotific export type { ProcessOutputStream } from "./ProcessOutputStream"; export type { ProcessTerminalSize } from "./ProcessTerminalSize"; export type { RateLimitReachedType } from "./RateLimitReachedType"; +export type { RateLimitResetCredit } from "./RateLimitResetCredit"; +export type { RateLimitResetCreditStatus } from "./RateLimitResetCreditStatus"; export type { RateLimitResetCreditsSummary } from "./RateLimitResetCreditsSummary"; +export type { RateLimitResetType } from "./RateLimitResetType"; export type { RateLimitSnapshot } from "./RateLimitSnapshot"; export type { RateLimitWindow } from "./RateLimitWindow"; export type { RawResponseItemCompletedNotification } from "./RawResponseItemCompletedNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index d8db99d4ffeb..4f5b6d8a7077 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1017,6 +1017,12 @@ client_request_definitions! { response: v2::GetAccountRateLimitsResponse, }, + AccountRateLimitResetCreditList => "account/rateLimitResetCredit/list" { + params: v2::AccountRateLimitResetCreditListParams, + serialization: None, + response: v2::AccountRateLimitResetCreditListResponse, + }, + ConsumeAccountRateLimitResetCredit => "account/rateLimitResetCredit/consume" { params: v2::ConsumeAccountRateLimitResetCreditParams, serialization: global("account-auth"), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/account.rs b/codex-rs/app-server-protocol/src/protocol/v2/account.rs index 635d001bbad1..07e34246cf39 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -286,6 +286,60 @@ pub struct RateLimitResetCreditsSummary { pub available_count: i64, } +/// Parameters for listing available reset credits. +/// +/// The upstream API counts every available credit but returns details for at most the first 10 and +/// does not expose a pagination cursor, so this method cannot truthfully expose `cursor` or `limit`. +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountRateLimitResetCreditListParams {} + +/// Available reset credits returned by the backend. +/// +/// This is a non-pageable snapshot because the upstream API does not expose a cursor. `data` +/// contains details for at most 10 credits, so it can contain fewer rows than `availableCount`. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountRateLimitResetCreditListResponse { + #[ts(type = "number")] + pub available_count: i64, + pub data: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RateLimitResetCredit { + /// Opaque backend identifier for this reset credit. + pub id: String, + pub reset_type: RateLimitResetType, + pub status: RateLimitResetCreditStatus, + /// Unix timestamp in seconds when the credit was granted. + #[ts(type = "number")] + pub granted_at: i64, + /// Unix timestamp in seconds when the credit expires, or `null` if it does not expire. + #[ts(type = "number | null")] + pub expires_at: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/", rename_all = "camelCase")] +pub enum RateLimitResetType { + CodexRateLimits, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/", rename_all = "camelCase")] +pub enum RateLimitResetCreditStatus { + Available, + Redeeming, + Redeemed, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -293,6 +347,10 @@ pub struct ConsumeAccountRateLimitResetCreditParams { /// Identifies one logical reset attempt. A UUID is recommended; reuse the same value when /// retrying that attempt. pub idempotency_key: String, + /// Opaque reset-credit identifier to redeem. When omitted, the backend selects the next + /// available credit. + #[ts(optional = nullable)] + pub credit_id: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c9ac2f5fb27c..0a7a3dc2592b 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1929,8 +1929,9 @@ Codex supports these authentication modes. The current mode is surfaced in `acco - `account/login/cancel` — cancel a pending managed ChatGPT login by `loginId`. - `account/logout` — sign out; triggers `account/updated`. - `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, `personalAccessToken`, or `null`) and includes the current ChatGPT `planType` when available. -- `account/rateLimits/read` — fetch ChatGPT rate limits, an optional effective monthly credit limit, and the number of earned rate-limit resets currently available. Rate-limit updates arrive via `account/rateLimits/updated` (notify); the reset count is snapshot-only. -- `account/rateLimitResetCredit/consume` — consume one earned reset using a caller-provided idempotency key. +- `account/rateLimits/read` — fetch ChatGPT rate limits, an optional effective monthly credit limit, and the earned rate-limit reset count. Rate-limit updates arrive via `account/rateLimits/updated` (notify); reset-credit data is snapshot-only. +- `account/rateLimitResetCredit/list` — fetch a non-pageable snapshot with details for up to 10 available earned resets, including expiry timestamps, plus the full available count. +- `account/rateLimitResetCredit/consume` — consume one earned reset using a caller-provided idempotency key, optionally selecting a reset-credit ID returned by `account/rateLimitResetCredit/list`. - `account/usage/read` — fetch ChatGPT account token-activity summary and daily buckets. - `account/workspaceMessages/read` — fetch active workspace messages, including workspace notification headlines when available. - `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change. This is a sparse rolling update; merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. @@ -2032,7 +2033,19 @@ Field notes: ```json { "method": "account/rateLimits/read", "id": 7 } -{ "id": 7, "result": { "rateLimits": { "primary": { "usedPercent": 25, "windowDurationMins": 15, "resetsAt": 1730947200 }, "secondary": null, "rateLimitReachedType": null }, "rateLimitResetCredits": { "availableCount": 2 } } } +{ + "id": 7, + "result": { + "rateLimits": { + "primary": { "usedPercent": 25, "windowDurationMins": 15, "resetsAt": 1730947200 }, + "secondary": null, + "rateLimitReachedType": null + }, + "rateLimitResetCredits": { + "availableCount": 2 + } + } +} { "method": "account/rateLimits/updated", "params": { "rateLimits": { … } } } ``` @@ -2043,23 +2056,51 @@ Field notes: - `resetsAt` is a Unix timestamp (seconds) for the next reset. - `rateLimitReachedType` identifies the backend-classified limit state when one has been reached. - `individualLimit` describes the effective monthly credit limit when available. In an `account/rateLimits/read` response, `null` means no monthly limit is available. In a sparse `account/rateLimits/updated` notification, nullable account metadata may be unavailable and does not clear a previously observed value. -- `rateLimitResetCredits` contains the available earned-reset count when the backend provides it; otherwise it is `null`. Refetch `account/rateLimits/read` after consuming a reset. +- `rateLimitResetCredits` contains the available earned-reset count when the backend provides it; otherwise it is `null`. +- Refetch `account/rateLimits/read` after consuming a reset. ### 8) Earned rate-limit resets (ChatGPT) ```json -{ "method": "account/rateLimitResetCredit/consume", "id": 8, "params": { "idempotencyKey": "8ae96ff3-3425-4f4c-8772-b6fd61502868" } } -{ "id": 8, "result": { "outcome": "reset" } } +{ "method": "account/rateLimitResetCredit/list", "id": 8, "params": {} } +{ + "id": 8, + "result": { + "availableCount": 2, + "data": [ + { + "id": "RateLimitResetCredit_1", + "resetType": "codexRateLimits", + "status": "available", + "grantedAt": 1781654400, + "expiresAt": 1784246400 + }, + { + "id": "RateLimitResetCredit_2", + "resetType": "codexRateLimits", + "status": "available", + "grantedAt": 1781740800, + "expiresAt": null + } + ] + } +} +{ "method": "account/rateLimitResetCredit/consume", "id": 9, "params": { "idempotencyKey": "8ae96ff3-3425-4f4c-8772-b6fd61502868", "creditId": "RateLimitResetCredit_1" } } +{ "id": 9, "result": { "outcome": "reset" } } ``` Field notes: +- `availableCount` is the authoritative total. `data` contains details for at most the first 10 available credits. The backend does not expose a cursor, so the count can be greater than the number of rows and this method cannot paginate the remainder. +- Reset-credit `id` values are opaque strings. `grantedAt` and `expiresAt` are Unix timestamps in seconds; `expiresAt` is `null` for a credit that does not expire. +- `resetType` is currently `codexRateLimits`. `status` reports the credit lifecycle state: `available`, `redeeming`, or `redeemed`. - `idempotencyKey` must be non-empty. A UUID is recommended for each logical redemption attempt; reuse the same value when retrying that attempt. +- `creditId` is optional. When provided, it must be a non-empty opaque ID returned by `account/rateLimitResetCredit/list`; when omitted, the backend selects the next available credit. - `reset` means a credit was consumed. - `alreadyRedeemed` means the same redemption completed previously. Treat it as an idempotent success and refresh account limits. - `nothingToReset` means there is no eligible rate-limit window to reset. - `noCredit` means the account has no earned reset credits available. -- Refetch `account/rateLimits/read` after consuming a reset instead of inferring updated windows from this response. +- Refetch `account/rateLimits/read` and `account/rateLimitResetCredit/list` after consuming a reset instead of inferring updated state from this response. ### 9) Workspace messages (ChatGPT) diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 375307e804f7..7b1e787b01cb 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1422,6 +1422,11 @@ impl MessageProcessor { ClientRequest::GetAccountRateLimits { .. } => { self.account_processor.get_account_rate_limits().await } + ClientRequest::AccountRateLimitResetCreditList { .. } => { + self.account_processor + .account_rate_limit_reset_credit_list() + .await + } ClientRequest::ConsumeAccountRateLimitResetCredit { params, .. } => { self.account_processor .consume_account_rate_limit_reset_credit(params) diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 587325c246dd..7e14761e4f2e 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -22,6 +22,7 @@ use codex_analytics::InputError; use codex_analytics::TurnSteerRequestError; use codex_app_server_protocol::Account; use codex_app_server_protocol::AccountLoginCompletedNotification; +use codex_app_server_protocol::AccountRateLimitResetCreditListResponse; use codex_app_server_protocol::AccountTokenUsageDailyBucket; use codex_app_server_protocol::AccountTokenUsageSummary; use codex_app_server_protocol::AccountUpdatedNotification; @@ -156,7 +157,10 @@ use codex_app_server_protocol::PluginSource; use codex_app_server_protocol::PluginSummary; use codex_app_server_protocol::PluginUninstallParams; use codex_app_server_protocol::PluginUninstallResponse; +use codex_app_server_protocol::RateLimitResetCredit; +use codex_app_server_protocol::RateLimitResetCreditStatus; use codex_app_server_protocol::RateLimitResetCreditsSummary; +use codex_app_server_protocol::RateLimitResetType; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ReviewDelivery as ApiReviewDelivery; use codex_app_server_protocol::ReviewStartParams; @@ -298,6 +302,8 @@ use codex_backend_client::CodexWorkspaceMessage as BackendWorkspaceMessage; use codex_backend_client::CodexWorkspaceMessageType as BackendWorkspaceMessageType; use codex_backend_client::CodexWorkspaceMessagesResponse as BackendWorkspaceMessagesResponse; use codex_backend_client::ConsumeRateLimitResetCreditCode as BackendConsumeRateLimitResetCreditCode; +use codex_backend_client::RateLimitResetCreditDetails as BackendRateLimitResetCreditDetails; +use codex_backend_client::RateLimitResetCreditsDetails as BackendRateLimitResetCreditsDetails; use codex_backend_client::RequestError as BackendRequestError; use codex_backend_client::TokenUsageProfile; use codex_chatgpt::connectors; diff --git a/codex-rs/app-server/src/request_processors/account_processor.rs b/codex-rs/app-server/src/request_processors/account_processor.rs index 4691e222044c..ceaa142ba223 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -138,6 +138,14 @@ impl AccountRequestProcessor { .map(|response| Some(response.into())) } + pub(crate) async fn account_rate_limit_reset_credit_list( + &self, + ) -> Result, JSONRPCErrorError> { + self.account_rate_limit_reset_credit_list_response() + .await + .map(|response| Some(response.into())) + } + pub(crate) async fn get_account_token_usage( &self, ) -> Result, JSONRPCErrorError> { diff --git a/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs b/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs index c8a397d3a7be..80f6c661ce77 100644 --- a/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs +++ b/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs @@ -1,11 +1,27 @@ use super::*; const RATE_LIMIT_RESET_REQUEST_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 10); +const RATE_LIMIT_RESET_DETAILS_REQUEST_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 5); #[cfg(debug_assertions)] const RATE_LIMIT_RESET_REQUEST_TIMEOUT_ENV_VAR: &str = "CODEX_TEST_RATE_LIMIT_RESET_REQUEST_TIMEOUT_MS"; impl AccountRequestProcessor { + pub(super) async fn account_rate_limit_reset_credit_list_response( + &self, + ) -> Result { + let client = self.rate_limit_reset_backend_client().await?; + let details = tokio::time::timeout( + RATE_LIMIT_RESET_DETAILS_REQUEST_TIMEOUT, + client.list_rate_limit_reset_credits(), + ) + .await + .map_err(|_| internal_error("rate limit reset credit detail request timed out"))? + .map_err(|_| internal_error("failed to fetch rate limit reset credit details"))?; + rate_limit_reset_credits_from_backend(details) + .map_err(|_| internal_error("failed to parse rate limit reset credit details")) + } + pub(crate) async fn consume_account_rate_limit_reset_credit( &self, params: ConsumeAccountRateLimitResetCreditParams, @@ -13,6 +29,9 @@ impl AccountRequestProcessor { if params.idempotency_key.is_empty() { return Err(invalid_request("idempotencyKey must not be empty")); } + if params.credit_id.as_deref().is_some_and(str::is_empty) { + return Err(invalid_request("creditId must not be empty")); + } let client = self.rate_limit_reset_backend_client().await?; let request_timeout = RATE_LIMIT_RESET_REQUEST_TIMEOUT; @@ -22,10 +41,20 @@ impl AccountRequestProcessor { .and_then(|value| value.parse::().ok()) .map(Duration::from_millis) .unwrap_or(request_timeout); - let response = tokio::time::timeout( - request_timeout, - client.consume_rate_limit_reset_credit(¶ms.idempotency_key), - ) + let response = tokio::time::timeout(request_timeout, async { + match params.credit_id.as_deref() { + Some(credit_id) => { + client + .consume_rate_limit_reset_credit_by_id(¶ms.idempotency_key, credit_id) + .await + } + None => { + client + .consume_rate_limit_reset_credit(¶ms.idempotency_key) + .await + } + } + }) .await .map_err(|_| internal_error("rate limit reset consume timed out"))? .map_err(|err| internal_error(format!("failed to consume rate limit reset: {err}")))?; @@ -64,3 +93,64 @@ impl AccountRequestProcessor { .map_err(|err| internal_error(format!("failed to construct backend client: {err}"))) } } + +fn rate_limit_reset_credits_from_backend( + details: BackendRateLimitResetCreditsDetails, +) -> Result { + let data = details + .credits + .into_iter() + .map(rate_limit_reset_credit_from_backend) + .collect::, _>>()?; + Ok(AccountRateLimitResetCreditListResponse { + available_count: details.available_count, + data, + }) +} + +fn rate_limit_reset_credit_from_backend( + credit: BackendRateLimitResetCreditDetails, +) -> Result { + let reset_type = match credit.reset_type.as_str() { + "codex_rate_limits" => RateLimitResetType::CodexRateLimits, + unknown => { + return Err(format!( + "unknown reset type `{unknown}` for credit `{}`", + credit.id + )); + } + }; + let status = match credit.status.as_str() { + "available" => RateLimitResetCreditStatus::Available, + "redeeming" => RateLimitResetCreditStatus::Redeeming, + "redeemed" => RateLimitResetCreditStatus::Redeemed, + unknown => { + return Err(format!( + "unknown status `{unknown}` for credit `{}`", + credit.id + )); + } + }; + let granted_at = rate_limit_reset_credit_timestamp(&credit.granted_at) + .map_err(|err| format!("invalid granted_at for credit `{}`: {err}", credit.id))?; + let expires_at = credit + .expires_at + .as_deref() + .map(rate_limit_reset_credit_timestamp) + .transpose() + .map_err(|err| format!("invalid expires_at for credit `{}`: {err}", credit.id))?; + + Ok(RateLimitResetCredit { + id: credit.id, + reset_type, + status, + granted_at, + expires_at, + }) +} + +fn rate_limit_reset_credit_timestamp(timestamp: &str) -> Result { + DateTime::parse_from_rfc3339(timestamp) + .map(|timestamp| timestamp.timestamp()) + .map_err(|err| format!("failed to parse timestamp `{timestamp}`: {err}")) +} diff --git a/codex-rs/app-server/tests/common/test_app_server.rs b/codex-rs/app-server/tests/common/test_app_server.rs index fbe43e7689b3..7622206b693d 100644 --- a/codex-rs/app-server/tests/common/test_app_server.rs +++ b/codex-rs/app-server/tests/common/test_app_server.rs @@ -448,6 +448,17 @@ impl TestAppServer { .await } + /// Send an `account/rateLimitResetCredit/list` JSON-RPC request. + pub async fn send_account_rate_limit_reset_credit_list_request( + &mut self, + ) -> anyhow::Result { + self.send_request( + "account/rateLimitResetCredit/list", + Some(serde_json::json!({})), + ) + .await + } + /// Send an `account/rateLimitResetCredit/consume` JSON-RPC request. pub async fn send_consume_account_rate_limit_reset_credit_request( &mut self, diff --git a/codex-rs/app-server/tests/suite/v2/rate_limit_reset_credits.rs b/codex-rs/app-server/tests/suite/v2/rate_limit_reset_credits.rs index 86ea09504c65..6d3b221fd90a 100644 --- a/codex-rs/app-server/tests/suite/v2/rate_limit_reset_credits.rs +++ b/codex-rs/app-server/tests/suite/v2/rate_limit_reset_credits.rs @@ -5,6 +5,7 @@ use app_test_support::ChatGptAuthFixture; use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; +use codex_app_server_protocol::AccountRateLimitResetCreditListResponse; use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditOutcome; use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditParams; use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditResponse; @@ -12,6 +13,9 @@ use codex_app_server_protocol::GetAccountParams; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::LoginAccountResponse; +use codex_app_server_protocol::RateLimitResetCredit; +use codex_app_server_protocol::RateLimitResetCreditStatus; +use codex_app_server_protocol::RateLimitResetType; use codex_app_server_protocol::RequestId; use codex_config::types::AuthCredentialsStoreMode; use pretty_assertions::assert_eq; @@ -35,6 +39,103 @@ const SERVER_TIMEOUT_READ_TIMEOUT: std::time::Duration = const INVALID_REQUEST_ERROR_CODE: i64 = -32600; const INTERNAL_ERROR_CODE: i64 = -32603; +#[tokio::test] +async fn account_rate_limit_reset_credit_list_requires_chatgpt_auth() -> Result<()> { + let codex_home = TempDir::new()?; + let mut mcp = initialized_app_server(codex_home.path()).await?; + + let request_id = mcp + .send_account_rate_limit_reset_credit_list_request() + .await?; + let error = read_error_response(&mut mcp, request_id).await?; + assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE); + assert_eq!( + error.error.message, + "codex account authentication required for rate limit reset credits" + ); + + login_with_api_key(&mut mcp, "sk-test-key").await?; + let request_id = mcp + .send_account_rate_limit_reset_credit_list_request() + .await?; + let error = read_error_response(&mut mcp, request_id).await?; + assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE); + assert_eq!( + error.error.message, + "chatgpt authentication required for rate limit reset credits" + ); + Ok(()) +} + +#[tokio::test] +async fn account_rate_limit_reset_credit_list_returns_available_details() -> Result<()> { + let (codex_home, server) = chatgpt_test_context().await?; + Mock::given(method("GET")) + .and(path("/api/codex/rate-limit-reset-credits")) + .and(header("authorization", "Bearer chatgpt-token")) + .and(header("chatgpt-account-id", "account-123")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "credits": [ + { + "id": "credit-1", + "reset_type": "codex_rate_limits", + "status": "available", + "granted_at": "2026-06-17T00:00:00Z", + "expires_at": "2026-07-17T00:00:00Z", + "title": "Full reset", + "profile_user_id": "ignored" + }, + { + "id": "credit-2", + "reset_type": "codex_rate_limits", + "status": "available", + "granted_at": "2026-06-18T00:00:00Z", + "expires_at": null + } + ], + "available_count": 2, + "total_earned_count": 4 + }))) + .expect(1) + .mount(&server) + .await; + + let mut mcp = initialized_app_server(codex_home.path()).await?; + let request_id = mcp + .send_account_rate_limit_reset_credit_list_request() + .await?; + let response: AccountRateLimitResetCreditListResponse = + read_response(&mut mcp, request_id).await?; + + assert_eq!( + response, + AccountRateLimitResetCreditListResponse { + available_count: 2, + data: vec![ + RateLimitResetCredit { + id: "credit-1".to_string(), + reset_type: RateLimitResetType::CodexRateLimits, + status: RateLimitResetCreditStatus::Available, + granted_at: chrono::DateTime::parse_from_rfc3339("2026-06-17T00:00:00Z")? + .timestamp(), + expires_at: Some( + chrono::DateTime::parse_from_rfc3339("2026-07-17T00:00:00Z")?.timestamp() + ), + }, + RateLimitResetCredit { + id: "credit-2".to_string(), + reset_type: RateLimitResetType::CodexRateLimits, + status: RateLimitResetCreditStatus::Available, + granted_at: chrono::DateTime::parse_from_rfc3339("2026-06-18T00:00:00Z")? + .timestamp(), + expires_at: None, + }, + ], + } + ); + Ok(()) +} + #[tokio::test] async fn consume_rate_limit_reset_credit_requires_chatgpt_auth() -> Result<()> { let codex_home = TempDir::new()?; @@ -44,6 +145,7 @@ async fn consume_rate_limit_reset_credit_requires_chatgpt_auth() -> Result<()> { .send_consume_account_rate_limit_reset_credit_request( ConsumeAccountRateLimitResetCreditParams { idempotency_key: "request-1".to_string(), + credit_id: None, }, ) .await?; @@ -120,6 +222,44 @@ async fn consume_account_rate_limit_reset_credit_maps_backend_outcomes() -> Resu Ok(()) } +#[tokio::test] +async fn consume_account_rate_limit_reset_credit_forwards_selected_credit_id() -> Result<()> { + let (codex_home, server) = chatgpt_test_context().await?; + Mock::given(method("POST")) + .and(path("/api/codex/rate-limit-reset-credits/consume")) + .and(header("authorization", "Bearer chatgpt-token")) + .and(header("chatgpt-account-id", "account-123")) + .and(body_json(json!({ + "redeem_request_id": "request-selected", + "credit_id": "credit-123", + }))) + .respond_with( + ResponseTemplate::new(200) + .set_body_json(json!({ "code": "reset", "windows_reset": 2 })), + ) + .expect(1) + .mount(&server) + .await; + + let mut mcp = initialized_app_server(codex_home.path()).await?; + let request_id = mcp + .send_consume_account_rate_limit_reset_credit_request( + ConsumeAccountRateLimitResetCreditParams { + idempotency_key: "request-selected".to_string(), + credit_id: Some("credit-123".to_string()), + }, + ) + .await?; + + assert_eq!( + read_response::(&mut mcp, request_id).await?, + ConsumeAccountRateLimitResetCreditResponse { + outcome: ConsumeAccountRateLimitResetCreditOutcome::Reset, + } + ); + Ok(()) +} + #[tokio::test] async fn consume_account_rate_limit_reset_credit_rejects_empty_idempotency_key() -> Result<()> { let (codex_home, _server) = chatgpt_test_context().await?; @@ -129,6 +269,7 @@ async fn consume_account_rate_limit_reset_credit_rejects_empty_idempotency_key() .send_consume_account_rate_limit_reset_credit_request( ConsumeAccountRateLimitResetCreditParams { idempotency_key: String::new(), + credit_id: None, }, ) .await?; @@ -139,6 +280,26 @@ async fn consume_account_rate_limit_reset_credit_rejects_empty_idempotency_key() Ok(()) } +#[tokio::test] +async fn consume_account_rate_limit_reset_credit_rejects_empty_credit_id() -> Result<()> { + let (codex_home, _server) = chatgpt_test_context().await?; + let mut mcp = initialized_app_server(codex_home.path()).await?; + + let request_id = mcp + .send_consume_account_rate_limit_reset_credit_request( + ConsumeAccountRateLimitResetCreditParams { + idempotency_key: "request-1".to_string(), + credit_id: Some(String::new()), + }, + ) + .await?; + let error = read_error_response(&mut mcp, request_id).await?; + + assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE); + assert_eq!(error.error.message, "creditId must not be empty"); + Ok(()) +} + #[tokio::test] async fn consume_account_rate_limit_reset_credit_surfaces_backend_failure() -> Result<()> { let (codex_home, server) = chatgpt_test_context().await?; @@ -244,6 +405,7 @@ async fn send_consume_reset_credit(mcp: &mut TestAppServer, idempotency_key: &st mcp.send_consume_account_rate_limit_reset_credit_request( ConsumeAccountRateLimitResetCreditParams { idempotency_key: idempotency_key.to_string(), + credit_id: None, }, ) .await diff --git a/codex-rs/app-server/tests/suite/v2/rate_limits.rs b/codex-rs/app-server/tests/suite/v2/rate_limits.rs index 58c31c453b70..f661f38fd721 100644 --- a/codex-rs/app-server/tests/suite/v2/rate_limits.rs +++ b/codex-rs/app-server/tests/suite/v2/rate_limits.rs @@ -170,6 +170,12 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> { .expect(1) .mount(&server) .await; + Mock::given(method("GET")) + .and(path("/api/codex/rate-limit-reset-credits")) + .respond_with(ResponseTemplate::new(500)) + .expect(0) + .mount(&server) + .await; let mut mcp = TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; diff --git a/codex-rs/backend-client/src/client/rate_limit_resets.rs b/codex-rs/backend-client/src/client/rate_limit_resets.rs index b22a4ec91ad0..bed56ba6488b 100644 --- a/codex-rs/backend-client/src/client/rate_limit_resets.rs +++ b/codex-rs/backend-client/src/client/rate_limit_resets.rs @@ -3,6 +3,7 @@ use super::Client; use super::PathStyle; use crate::types::ConsumeRateLimitResetCreditResponse; +use crate::types::RateLimitResetCreditsDetails; use crate::types::RateLimitStatusWithResetCredits; use crate::types::RateLimitsWithResetCredits; use anyhow::Result; @@ -13,6 +14,8 @@ use serde::Serialize; #[derive(Serialize)] struct ConsumeRateLimitResetCreditRequest<'a> { redeem_request_id: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + credit_id: Option<&'a str>, } impl Client { @@ -31,9 +34,34 @@ impl Client { self.decode_json(&url, &ct, &body) } + pub async fn list_rate_limit_reset_credits(&self) -> Result { + let url = self.rate_limit_reset_credits_url(); + let req = self.http.get(&url).headers(self.headers()); + let (body, ct) = self.exec_request(req, "GET", &url).await?; + self.decode_json(&url, &ct, &body) + } + pub async fn consume_rate_limit_reset_credit( &self, redeem_request_id: &str, + ) -> Result { + self.consume_rate_limit_reset_credit_request(redeem_request_id, /*credit_id*/ None) + .await + } + + pub async fn consume_rate_limit_reset_credit_by_id( + &self, + redeem_request_id: &str, + credit_id: &str, + ) -> Result { + self.consume_rate_limit_reset_credit_request(redeem_request_id, Some(credit_id)) + .await + } + + async fn consume_rate_limit_reset_credit_request( + &self, + redeem_request_id: &str, + credit_id: Option<&str>, ) -> Result { let url = self.consume_rate_limit_reset_credit_url(); let req = self @@ -41,7 +69,10 @@ impl Client { .post(&url) .headers(self.headers()) .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) - .json(&ConsumeRateLimitResetCreditRequest { redeem_request_id }); + .json(&ConsumeRateLimitResetCreditRequest { + redeem_request_id, + credit_id, + }); let (body, ct) = self.exec_request(req, "POST", &url).await?; self.decode_json(&url, &ct, &body) } @@ -53,6 +84,17 @@ impl Client { } } + fn rate_limit_reset_credits_url(&self) -> String { + match self.path_style { + PathStyle::CodexApi => { + format!("{}/api/codex/rate-limit-reset-credits", self.base_url) + } + PathStyle::ChatGptApi => { + format!("{}/wham/rate-limit-reset-credits", self.base_url) + } + } + } + fn consume_rate_limit_reset_credit_url(&self) -> String { match self.path_style { PathStyle::CodexApi => { diff --git a/codex-rs/backend-client/src/client/rate_limit_resets_tests.rs b/codex-rs/backend-client/src/client/rate_limit_resets_tests.rs index 89313dbe5d83..03c2f8fb867e 100644 --- a/codex-rs/backend-client/src/client/rate_limit_resets_tests.rs +++ b/codex-rs/backend-client/src/client/rate_limit_resets_tests.rs @@ -1,5 +1,7 @@ use super::*; use crate::types::ConsumeRateLimitResetCreditCode; +use crate::types::RateLimitResetCreditDetails; +use crate::types::RateLimitResetCreditsDetails; use crate::types::RateLimitResetCreditsSummary; use pretty_assertions::assert_eq; @@ -9,6 +11,10 @@ fn rate_limit_reset_contract_uses_expected_paths_and_payloads() { test_client("https://example.test", PathStyle::CodexApi).rate_limit_status_url(), "https://example.test/api/codex/usage" ); + assert_eq!( + test_client("https://example.test", PathStyle::CodexApi).rate_limit_reset_credits_url(), + "https://example.test/api/codex/rate-limit-reset-credits" + ); assert_eq!( test_client("https://example.test", PathStyle::CodexApi) .consume_rate_limit_reset_credit_url(), @@ -19,6 +25,11 @@ fn rate_limit_reset_contract_uses_expected_paths_and_payloads() { .rate_limit_status_url(), "https://chatgpt.com/backend-api/wham/usage" ); + assert_eq!( + test_client("https://chatgpt.com/backend-api", PathStyle::ChatGptApi) + .rate_limit_reset_credits_url(), + "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits" + ); assert_eq!( test_client("https://chatgpt.com/backend-api", PathStyle::ChatGptApi) .consume_rate_limit_reset_credit_url(), @@ -28,10 +39,22 @@ fn rate_limit_reset_contract_uses_expected_paths_and_payloads() { assert_eq!( serde_json::to_value(ConsumeRateLimitResetCreditRequest { redeem_request_id: "redeem-123", + credit_id: None, }) .unwrap(), serde_json::json!({ "redeem_request_id": "redeem-123" }) ); + assert_eq!( + serde_json::to_value(ConsumeRateLimitResetCreditRequest { + redeem_request_id: "redeem-456", + credit_id: Some("credit-123"), + }) + .unwrap(), + serde_json::json!({ + "redeem_request_id": "redeem-456", + "credit_id": "credit-123", + }) + ); let status: RateLimitStatusWithResetCredits = serde_json::from_value(serde_json::json!({ "plan_type": "plus", @@ -43,6 +66,56 @@ fn rate_limit_reset_contract_uses_expected_paths_and_payloads() { Some(RateLimitResetCreditsSummary { available_count: 3 }) ); + let details: RateLimitResetCreditsDetails = serde_json::from_value(serde_json::json!({ + "credits": [ + { + "id": "credit-1", + "reset_type": "codex_rate_limits", + "status": "available", + "granted_at": "2026-06-17T00:00:00Z", + "expires_at": "2026-07-17T00:00:00Z", + "redeem_started_at": null, + "redeemed_at": null, + "profile_image_url": "https://example.test/avatar.png", + "profile_user_id": "@friend", + "title": "Full reset (Weekly + 5 hr)", + "description": "Ready to redeem" + }, + { + "id": "credit-2", + "reset_type": "codex_rate_limits", + "status": "available", + "granted_at": "2026-06-18T00:00:00Z", + "expires_at": null + } + ], + "available_count": 2, + "total_earned_count": 4 + })) + .unwrap(); + assert_eq!( + details, + RateLimitResetCreditsDetails { + credits: vec![ + RateLimitResetCreditDetails { + id: "credit-1".to_string(), + reset_type: "codex_rate_limits".to_string(), + status: "available".to_string(), + granted_at: "2026-06-17T00:00:00Z".to_string(), + expires_at: Some("2026-07-17T00:00:00Z".to_string()), + }, + RateLimitResetCreditDetails { + id: "credit-2".to_string(), + reset_type: "codex_rate_limits".to_string(), + status: "available".to_string(), + granted_at: "2026-06-18T00:00:00Z".to_string(), + expires_at: None, + }, + ], + available_count: 2, + } + ); + let response: ConsumeRateLimitResetCreditResponse = serde_json::from_value(serde_json::json!({ "code": "reset", "credit": { "id": "ignored-by-cli" }, diff --git a/codex-rs/backend-client/src/lib.rs b/codex-rs/backend-client/src/lib.rs index 6cbeaf30b256..06f2cd442ab8 100644 --- a/codex-rs/backend-client/src/lib.rs +++ b/codex-rs/backend-client/src/lib.rs @@ -18,6 +18,8 @@ pub use types::DeliveredConfigToml; pub use types::DeliveredRequirementsToml; pub use types::DeliveredTomlFragment; pub use types::PaginatedListTaskListItem; +pub use types::RateLimitResetCreditDetails; +pub use types::RateLimitResetCreditsDetails; pub use types::RateLimitResetCreditsSummary; pub use types::RateLimitsWithResetCredits; pub use types::TaskListItem; diff --git a/codex-rs/backend-client/src/types.rs b/codex-rs/backend-client/src/types.rs index 18ab32398603..d83712d82fdd 100644 --- a/codex-rs/backend-client/src/types.rs +++ b/codex-rs/backend-client/src/types.rs @@ -23,6 +23,21 @@ pub struct RateLimitResetCreditsSummary { pub available_count: i64, } +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct RateLimitResetCreditsDetails { + pub credits: Vec, + pub available_count: i64, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct RateLimitResetCreditDetails { + pub id: String, + pub reset_type: String, + pub status: String, + pub granted_at: String, + pub expires_at: Option, +} + #[derive(Clone, Debug, PartialEq)] pub struct RateLimitsWithResetCredits { pub rate_limits: Vec, diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index 9391f4b3e3bb..9162b2c868c6 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -798,7 +798,10 @@ pub(super) async fn consume_rate_limit_reset_credit_request( request_handle .request_typed(ClientRequest::ConsumeAccountRateLimitResetCredit { request_id, - params: ConsumeAccountRateLimitResetCreditParams { idempotency_key }, + params: ConsumeAccountRateLimitResetCreditParams { + idempotency_key, + credit_id: None, + }, }) .await .wrap_err("account/rateLimitResetCredit/consume failed in TUI") From bb7a2b0e50ff15b49a200bf546b9f3d902d36897 Mon Sep 17 00:00:00 2001 From: jayp-oai Date: Fri, 3 Jul 2026 17:19:40 -0700 Subject: [PATCH 2/4] Forward reset credit display text --- .../json/codex_app_server_protocol.schemas.json | 14 ++++++++++++++ .../json/codex_app_server_protocol.v2.schemas.json | 14 ++++++++++++++ .../AccountRateLimitResetCreditListResponse.json | 14 ++++++++++++++ .../schema/typescript/v2/RateLimitResetCredit.ts | 10 +++++++++- .../app-server-protocol/src/protocol/v2/account.rs | 4 ++++ codex-rs/app-server/README.md | 9 +++++++-- .../account_processor/rate_limit_resets.rs | 2 ++ .../tests/suite/v2/rate_limit_reset_credits.rs | 7 ++++++- .../src/client/rate_limit_resets_tests.rs | 4 ++++ codex-rs/backend-client/src/types.rs | 2 ++ 10 files changed, 76 insertions(+), 4 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 4c0683a94785..d2bf97c0508e 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -14570,6 +14570,13 @@ }, "RateLimitResetCredit": { "properties": { + "description": { + "description": "Backend-provided display description for this credit, or `null` when unavailable.", + "type": [ + "string", + "null" + ] + }, "expiresAt": { "description": "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.", "format": "int64", @@ -14592,6 +14599,13 @@ }, "status": { "$ref": "#/definitions/v2/RateLimitResetCreditStatus" + }, + "title": { + "description": "Backend-provided display title for this credit, or `null` when unavailable.", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 1e4074d4a0d1..406951a86822 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -10974,6 +10974,13 @@ }, "RateLimitResetCredit": { "properties": { + "description": { + "description": "Backend-provided display description for this credit, or `null` when unavailable.", + "type": [ + "string", + "null" + ] + }, "expiresAt": { "description": "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.", "format": "int64", @@ -10996,6 +11003,13 @@ }, "status": { "$ref": "#/definitions/RateLimitResetCreditStatus" + }, + "title": { + "description": "Backend-provided display title for this credit, or `null` when unavailable.", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListResponse.json index f0e101fde325..b8f51846adc6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListResponse.json @@ -3,6 +3,13 @@ "definitions": { "RateLimitResetCredit": { "properties": { + "description": { + "description": "Backend-provided display description for this credit, or `null` when unavailable.", + "type": [ + "string", + "null" + ] + }, "expiresAt": { "description": "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.", "format": "int64", @@ -25,6 +32,13 @@ }, "status": { "$ref": "#/definitions/RateLimitResetCreditStatus" + }, + "title": { + "description": "Backend-provided display title for this credit, or `null` when unavailable.", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCredit.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCredit.ts index d393eb09eb41..514c1558a27a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCredit.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCredit.ts @@ -16,4 +16,12 @@ grantedAt: number, /** * Unix timestamp in seconds when the credit expires, or `null` if it does not expire. */ -expiresAt: number | null, }; +expiresAt: number | null, +/** + * Backend-provided display title for this credit, or `null` when unavailable. + */ +title: string | null, +/** + * Backend-provided display description for this credit, or `null` when unavailable. + */ +description: string | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/account.rs b/codex-rs/app-server-protocol/src/protocol/v2/account.rs index 07e34246cf39..2159e648bfd3 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -322,6 +322,10 @@ pub struct RateLimitResetCredit { /// Unix timestamp in seconds when the credit expires, or `null` if it does not expire. #[ts(type = "number | null")] pub expires_at: Option, + /// Backend-provided display title for this credit, or `null` when unavailable. + pub title: Option, + /// Backend-provided display description for this credit, or `null` when unavailable. + pub description: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 0a7a3dc2592b..7b7c27fc6576 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -2073,14 +2073,18 @@ Field notes: "resetType": "codexRateLimits", "status": "available", "grantedAt": 1781654400, - "expiresAt": 1784246400 + "expiresAt": 1784246400, + "title": "Full reset (Weekly + 5 hr)", + "description": "Ready to redeem" }, { "id": "RateLimitResetCredit_2", "resetType": "codexRateLimits", "status": "available", "grantedAt": 1781740800, - "expiresAt": null + "expiresAt": null, + "title": null, + "description": null } ] } @@ -2093,6 +2097,7 @@ Field notes: - `availableCount` is the authoritative total. `data` contains details for at most the first 10 available credits. The backend does not expose a cursor, so the count can be greater than the number of rows and this method cannot paginate the remainder. - Reset-credit `id` values are opaque strings. `grantedAt` and `expiresAt` are Unix timestamps in seconds; `expiresAt` is `null` for a credit that does not expire. +- `title` and `description` are backend-provided display text. Either field can be `null` when the backend does not provide it. - `resetType` is currently `codexRateLimits`. `status` reports the credit lifecycle state: `available`, `redeeming`, or `redeemed`. - `idempotencyKey` must be non-empty. A UUID is recommended for each logical redemption attempt; reuse the same value when retrying that attempt. - `creditId` is optional. When provided, it must be a non-empty opaque ID returned by `account/rateLimitResetCredit/list`; when omitted, the backend selects the next available credit. diff --git a/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs b/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs index 80f6c661ce77..b26ea566be2c 100644 --- a/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs +++ b/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs @@ -146,6 +146,8 @@ fn rate_limit_reset_credit_from_backend( status, granted_at, expires_at, + title: credit.title, + description: credit.description, }) } diff --git a/codex-rs/app-server/tests/suite/v2/rate_limit_reset_credits.rs b/codex-rs/app-server/tests/suite/v2/rate_limit_reset_credits.rs index 6d3b221fd90a..ac30bcd4dd58 100644 --- a/codex-rs/app-server/tests/suite/v2/rate_limit_reset_credits.rs +++ b/codex-rs/app-server/tests/suite/v2/rate_limit_reset_credits.rs @@ -82,7 +82,8 @@ async fn account_rate_limit_reset_credit_list_returns_available_details() -> Res "status": "available", "granted_at": "2026-06-17T00:00:00Z", "expires_at": "2026-07-17T00:00:00Z", - "title": "Full reset", + "title": "Full reset (Weekly + 5 hr)", + "description": "Ready to redeem", "profile_user_id": "ignored" }, { @@ -121,6 +122,8 @@ async fn account_rate_limit_reset_credit_list_returns_available_details() -> Res expires_at: Some( chrono::DateTime::parse_from_rfc3339("2026-07-17T00:00:00Z")?.timestamp() ), + title: Some("Full reset (Weekly + 5 hr)".to_string()), + description: Some("Ready to redeem".to_string()), }, RateLimitResetCredit { id: "credit-2".to_string(), @@ -129,6 +132,8 @@ async fn account_rate_limit_reset_credit_list_returns_available_details() -> Res granted_at: chrono::DateTime::parse_from_rfc3339("2026-06-18T00:00:00Z")? .timestamp(), expires_at: None, + title: None, + description: None, }, ], } diff --git a/codex-rs/backend-client/src/client/rate_limit_resets_tests.rs b/codex-rs/backend-client/src/client/rate_limit_resets_tests.rs index 03c2f8fb867e..f706be5da90f 100644 --- a/codex-rs/backend-client/src/client/rate_limit_resets_tests.rs +++ b/codex-rs/backend-client/src/client/rate_limit_resets_tests.rs @@ -103,6 +103,8 @@ fn rate_limit_reset_contract_uses_expected_paths_and_payloads() { status: "available".to_string(), granted_at: "2026-06-17T00:00:00Z".to_string(), expires_at: Some("2026-07-17T00:00:00Z".to_string()), + title: Some("Full reset (Weekly + 5 hr)".to_string()), + description: Some("Ready to redeem".to_string()), }, RateLimitResetCreditDetails { id: "credit-2".to_string(), @@ -110,6 +112,8 @@ fn rate_limit_reset_contract_uses_expected_paths_and_payloads() { status: "available".to_string(), granted_at: "2026-06-18T00:00:00Z".to_string(), expires_at: None, + title: None, + description: None, }, ], available_count: 2, diff --git a/codex-rs/backend-client/src/types.rs b/codex-rs/backend-client/src/types.rs index d83712d82fdd..7fb4346a92b3 100644 --- a/codex-rs/backend-client/src/types.rs +++ b/codex-rs/backend-client/src/types.rs @@ -36,6 +36,8 @@ pub struct RateLimitResetCreditDetails { pub status: String, pub granted_at: String, pub expires_at: Option, + pub title: Option, + pub description: Option, } #[derive(Clone, Debug, PartialEq)] From a15de071aa677a623e5c9937d644e9ba08501830 Mon Sep 17 00:00:00 2001 From: jayp-oai Date: Mon, 6 Jul 2026 00:52:30 -0700 Subject: [PATCH 3/4] Use rate limits read for reset credit details --- .../schema/json/ClientRequest.json | 28 ---- .../codex_app_server_protocol.schemas.json | 62 ++------ .../codex_app_server_protocol.v2.schemas.json | 62 ++------ ...AccountRateLimitResetCreditListParams.json | 6 - ...countRateLimitResetCreditListResponse.json | 86 ---------- .../json/v2/GetAccountRateLimitsResponse.json | 72 +++++++++ .../schema/typescript/ClientRequest.ts | 3 +- .../AccountRateLimitResetCreditListParams.ts | 11 -- ...AccountRateLimitResetCreditListResponse.ts | 12 -- .../v2/RateLimitResetCreditsSummary.ts | 11 +- .../schema/typescript/v2/index.ts | 2 - .../src/protocol/common.rs | 6 - .../src/protocol/v2/account.rs | 28 +--- codex-rs/app-server/README.md | 62 +++----- codex-rs/app-server/src/message_processor.rs | 5 - codex-rs/app-server/src/request_processors.rs | 1 - .../request_processors/account_processor.rs | 31 ++-- .../account_processor/rate_limit_resets.rs | 46 ++++-- .../tests/common/test_app_server.rs | 11 -- .../suite/v2/rate_limit_reset_credits.rs | 106 ------------- .../app-server/tests/suite/v2/rate_limits.rs | 127 ++++++++++++++- codex-rs/tui/src/chatwidget/tests/usage.rs | 150 ++++++++++++++---- 22 files changed, 424 insertions(+), 504 deletions(-) delete mode 100644 codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListParams.json delete mode 100644 codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListResponse.json delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListParams.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListResponse.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 91246a5c7272..2f39f821b6a8 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -5,10 +5,6 @@ "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "type": "string" }, - "AccountRateLimitResetCreditListParams": { - "description": "Parameters for listing available reset credits.\n\nThe upstream API counts every available credit but returns details for at most the first 10 and does not expose a pagination cursor, so this method cannot truthfully expose `cursor` or `limit`.", - "type": "object" - }, "AddCreditsNudgeCreditType": { "enum": [ "credits", @@ -6472,30 +6468,6 @@ "title": "Account/rateLimits/readRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "account/rateLimitResetCredit/list" - ], - "title": "Account/rateLimitResetCredit/listRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/AccountRateLimitResetCreditListParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Account/rateLimitResetCredit/listRequest", - "type": "object" - }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 27c50a1268fc..6c45ebb4b382 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -1857,30 +1857,6 @@ "title": "Account/rateLimits/readRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/v2/RequestId" - }, - "method": { - "enum": [ - "account/rateLimitResetCredit/list" - ], - "title": "Account/rateLimitResetCredit/listRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/AccountRateLimitResetCreditListParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Account/rateLimitResetCredit/listRequest", - "type": "object" - }, { "properties": { "id": { @@ -5999,34 +5975,6 @@ "title": "AccountLoginCompletedNotification", "type": "object" }, - "AccountRateLimitResetCreditListParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Parameters for listing available reset credits.\n\nThe upstream API counts every available credit but returns details for at most the first 10 and does not expose a pagination cursor, so this method cannot truthfully expose `cursor` or `limit`.", - "title": "AccountRateLimitResetCreditListParams", - "type": "object" - }, - "AccountRateLimitResetCreditListResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Available reset credits returned by the backend.\n\nThis is a non-pageable snapshot because the upstream API does not expose a cursor. `data` contains details for at most 10 credits, so it can contain fewer rows than `availableCount`.", - "properties": { - "availableCount": { - "format": "int64", - "type": "integer" - }, - "data": { - "items": { - "$ref": "#/definitions/v2/RateLimitResetCredit" - }, - "type": "array" - } - }, - "required": [ - "availableCount", - "data" - ], - "title": "AccountRateLimitResetCreditListResponse", - "type": "object" - }, "AccountRateLimitsUpdatedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Sparse rolling rate-limit update.\n\nClients should merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and does not clear a previously observed value.", @@ -14653,6 +14601,16 @@ "availableCount": { "format": "int64", "type": "integer" + }, + "credits": { + "description": "Detail rows for available reset credits, when the backend provides them.\n\n`null` means only `availableCount` is known, while an empty array means details were fetched and no available credits were returned. The backend may cap this list, so its length can be less than `availableCount`.", + "items": { + "$ref": "#/definitions/v2/RateLimitResetCredit" + }, + "type": [ + "array", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 7490446ee4cd..bf91599a33cd 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -101,34 +101,6 @@ "title": "AccountLoginCompletedNotification", "type": "object" }, - "AccountRateLimitResetCreditListParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Parameters for listing available reset credits.\n\nThe upstream API counts every available credit but returns details for at most the first 10 and does not expose a pagination cursor, so this method cannot truthfully expose `cursor` or `limit`.", - "title": "AccountRateLimitResetCreditListParams", - "type": "object" - }, - "AccountRateLimitResetCreditListResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Available reset credits returned by the backend.\n\nThis is a non-pageable snapshot because the upstream API does not expose a cursor. `data` contains details for at most 10 credits, so it can contain fewer rows than `availableCount`.", - "properties": { - "availableCount": { - "format": "int64", - "type": "integer" - }, - "data": { - "items": { - "$ref": "#/definitions/RateLimitResetCredit" - }, - "type": "array" - } - }, - "required": [ - "availableCount", - "data" - ], - "title": "AccountRateLimitResetCreditListResponse", - "type": "object" - }, "AccountRateLimitsUpdatedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Sparse rolling rate-limit update.\n\nClients should merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and does not clear a previously observed value.", @@ -2915,30 +2887,6 @@ "title": "Account/rateLimits/readRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "account/rateLimitResetCredit/list" - ], - "title": "Account/rateLimitResetCredit/listRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/AccountRateLimitResetCreditListParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Account/rateLimitResetCredit/listRequest", - "type": "object" - }, { "properties": { "id": { @@ -11057,6 +11005,16 @@ "availableCount": { "format": "int64", "type": "integer" + }, + "credits": { + "description": "Detail rows for available reset credits, when the backend provides them.\n\n`null` means only `availableCount` is known, while an empty array means details were fetched and no available credits were returned. The backend may cap this list, so its length can be less than `availableCount`.", + "items": { + "$ref": "#/definitions/RateLimitResetCredit" + }, + "type": [ + "array", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListParams.json b/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListParams.json deleted file mode 100644 index ea65abee0a0e..000000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListParams.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Parameters for listing available reset credits.\n\nThe upstream API counts every available credit but returns details for at most the first 10 and does not expose a pagination cursor, so this method cannot truthfully expose `cursor` or `limit`.", - "title": "AccountRateLimitResetCreditListParams", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListResponse.json deleted file mode 100644 index b8f51846adc6..000000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/AccountRateLimitResetCreditListResponse.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "RateLimitResetCredit": { - "properties": { - "description": { - "description": "Backend-provided display description for this credit, or `null` when unavailable.", - "type": [ - "string", - "null" - ] - }, - "expiresAt": { - "description": "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.", - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "grantedAt": { - "description": "Unix timestamp in seconds when the credit was granted.", - "format": "int64", - "type": "integer" - }, - "id": { - "description": "Opaque backend identifier for this reset credit.", - "type": "string" - }, - "resetType": { - "$ref": "#/definitions/RateLimitResetType" - }, - "status": { - "$ref": "#/definitions/RateLimitResetCreditStatus" - }, - "title": { - "description": "Backend-provided display title for this credit, or `null` when unavailable.", - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "grantedAt", - "id", - "resetType", - "status" - ], - "type": "object" - }, - "RateLimitResetCreditStatus": { - "enum": [ - "available", - "redeeming", - "redeemed" - ], - "type": "string" - }, - "RateLimitResetType": { - "enum": [ - "codexRateLimits" - ], - "type": "string" - } - }, - "description": "Available reset credits returned by the backend.\n\nThis is a non-pageable snapshot because the upstream API does not expose a cursor. `data` contains details for at most 10 credits, so it can contain fewer rows than `availableCount`.", - "properties": { - "availableCount": { - "format": "int64", - "type": "integer" - }, - "data": { - "items": { - "$ref": "#/definitions/RateLimitResetCredit" - }, - "type": "array" - } - }, - "required": [ - "availableCount", - "data" - ], - "title": "AccountRateLimitResetCreditListResponse", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json b/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json index 11b971251039..cb10c1c18fc5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json @@ -49,11 +49,77 @@ ], "type": "string" }, + "RateLimitResetCredit": { + "properties": { + "description": { + "description": "Backend-provided display description for this credit, or `null` when unavailable.", + "type": [ + "string", + "null" + ] + }, + "expiresAt": { + "description": "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "grantedAt": { + "description": "Unix timestamp in seconds when the credit was granted.", + "format": "int64", + "type": "integer" + }, + "id": { + "description": "Opaque backend identifier for this reset credit.", + "type": "string" + }, + "resetType": { + "$ref": "#/definitions/RateLimitResetType" + }, + "status": { + "$ref": "#/definitions/RateLimitResetCreditStatus" + }, + "title": { + "description": "Backend-provided display title for this credit, or `null` when unavailable.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "grantedAt", + "id", + "resetType", + "status" + ], + "type": "object" + }, + "RateLimitResetCreditStatus": { + "enum": [ + "available", + "redeeming", + "redeemed" + ], + "type": "string" + }, "RateLimitResetCreditsSummary": { "properties": { "availableCount": { "format": "int64", "type": "integer" + }, + "credits": { + "description": "Detail rows for available reset credits, when the backend provides them.\n\n`null` means only `availableCount` is known, while an empty array means details were fetched and no available credits were returned. The backend may cap this list, so its length can be less than `availableCount`.", + "items": { + "$ref": "#/definitions/RateLimitResetCredit" + }, + "type": [ + "array", + "null" + ] } }, "required": [ @@ -61,6 +127,12 @@ ], "type": "object" }, + "RateLimitResetType": { + "enum": [ + "codexRateLimits" + ], + "type": "string" + }, "RateLimitSnapshot": { "properties": { "credits": { diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 1d0223eb3d8a..6af04ef40119 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -7,7 +7,6 @@ import type { GetConversationSummaryParams } from "./GetConversationSummaryParam import type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams"; import type { InitializeParams } from "./InitializeParams"; import type { RequestId } from "./RequestId"; -import type { AccountRateLimitResetCreditListParams } from "./v2/AccountRateLimitResetCreditListParams"; import type { AppsListParams } from "./v2/AppsListParams"; import type { CancelLoginAccountParams } from "./v2/CancelLoginAccountParams"; import type { CommandExecParams } from "./v2/CommandExecParams"; @@ -89,4 +88,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/rateLimitResetCredit/list", id: RequestId, params: AccountRateLimitResetCreditListParams, } | { "method": "account/rateLimitResetCredit/consume", id: RequestId, params: ConsumeAccountRateLimitResetCreditParams, } | { "method": "account/usage/read", id: RequestId, params: undefined, } | { "method": "account/workspaceMessages/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "externalAgentConfig/import/readHistories", id: RequestId, params: undefined, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/rateLimitResetCredit/consume", id: RequestId, params: ConsumeAccountRateLimitResetCreditParams, } | { "method": "account/usage/read", id: RequestId, params: undefined, } | { "method": "account/workspaceMessages/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "externalAgentConfig/import/readHistories", id: RequestId, params: undefined, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListParams.ts deleted file mode 100644 index 72a7f4279040..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListParams.ts +++ /dev/null @@ -1,11 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Parameters for listing available reset credits. - * - * The upstream API counts every available credit but returns details for at most the first 10 and - * does not expose a pagination cursor, so this method cannot truthfully expose `cursor` or `limit`. - */ -export type AccountRateLimitResetCreditListParams = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListResponse.ts deleted file mode 100644 index 58d40b39f453..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AccountRateLimitResetCreditListResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { RateLimitResetCredit } from "./RateLimitResetCredit"; - -/** - * Available reset credits returned by the backend. - * - * This is a non-pageable snapshot because the upstream API does not expose a cursor. `data` - * contains details for at most 10 credits, so it can contain fewer rows than `availableCount`. - */ -export type AccountRateLimitResetCreditListResponse = { availableCount: number, data: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditsSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditsSummary.ts index e42dd8a703c9..46a8eee2e701 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditsSummary.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditsSummary.ts @@ -1,5 +1,14 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RateLimitResetCredit } from "./RateLimitResetCredit"; -export type RateLimitResetCreditsSummary = { availableCount: bigint, }; +export type RateLimitResetCreditsSummary = { availableCount: bigint, +/** + * Detail rows for available reset credits, when the backend provides them. + * + * `null` means only `availableCount` is known, while an empty array means details were fetched + * and no available credits were returned. The backend may cap this list, so its length can be + * less than `availableCount`. + */ +credits: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 54626e7324bf..c335f144ec8f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -2,8 +2,6 @@ export type { Account } from "./Account"; export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification"; -export type { AccountRateLimitResetCreditListParams } from "./AccountRateLimitResetCreditListParams"; -export type { AccountRateLimitResetCreditListResponse } from "./AccountRateLimitResetCreditListResponse"; export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification"; export type { AccountTokenUsageDailyBucket } from "./AccountTokenUsageDailyBucket"; export type { AccountTokenUsageSummary } from "./AccountTokenUsageSummary"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 4f5b6d8a7077..d8db99d4ffeb 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1017,12 +1017,6 @@ client_request_definitions! { response: v2::GetAccountRateLimitsResponse, }, - AccountRateLimitResetCreditList => "account/rateLimitResetCredit/list" { - params: v2::AccountRateLimitResetCreditListParams, - serialization: None, - response: v2::AccountRateLimitResetCreditListResponse, - }, - ConsumeAccountRateLimitResetCredit => "account/rateLimitResetCredit/consume" { params: v2::ConsumeAccountRateLimitResetCreditParams, serialization: global("account-auth"), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/account.rs b/codex-rs/app-server-protocol/src/protocol/v2/account.rs index 2159e648bfd3..5af952a2d47c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -284,28 +284,12 @@ pub struct GetAccountRateLimitsResponse { #[ts(export_to = "v2/")] pub struct RateLimitResetCreditsSummary { pub available_count: i64, -} - -/// Parameters for listing available reset credits. -/// -/// The upstream API counts every available credit but returns details for at most the first 10 and -/// does not expose a pagination cursor, so this method cannot truthfully expose `cursor` or `limit`. -#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct AccountRateLimitResetCreditListParams {} - -/// Available reset credits returned by the backend. -/// -/// This is a non-pageable snapshot because the upstream API does not expose a cursor. `data` -/// contains details for at most 10 credits, so it can contain fewer rows than `availableCount`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct AccountRateLimitResetCreditListResponse { - #[ts(type = "number")] - pub available_count: i64, - pub data: Vec, + /// Detail rows for available reset credits, when the backend provides them. + /// + /// `null` means only `availableCount` is known, while an empty array means details were fetched + /// and no available credits were returned. The backend may cap this list, so its length can be + /// less than `availableCount`. + pub credits: Option>, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index a55ac8139770..c9d006f2d0ac 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1929,9 +1929,8 @@ Codex supports these authentication modes. The current mode is surfaced in `acco - `account/login/cancel` — cancel a pending managed ChatGPT login by `loginId`. - `account/logout` — sign out; triggers `account/updated`. - `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, `personalAccessToken`, or `null`) and includes the current ChatGPT `planType` when available. -- `account/rateLimits/read` — fetch ChatGPT rate limits, an optional effective monthly credit limit, and the earned rate-limit reset count. Rate-limit updates arrive via `account/rateLimits/updated` (notify); reset-credit data is snapshot-only. -- `account/rateLimitResetCredit/list` — fetch a non-pageable snapshot with details for up to 10 available earned resets, including expiry timestamps, plus the full available count. -- `account/rateLimitResetCredit/consume` — consume one earned reset using a caller-provided idempotency key, optionally selecting a reset-credit ID returned by `account/rateLimitResetCredit/list`. +- `account/rateLimits/read` — fetch ChatGPT rate limits, an optional effective monthly credit limit, and the earned rate-limit resets currently available, including expiry details when provided by the backend. Rate-limit updates arrive via `account/rateLimits/updated` (notify); reset-credit data is snapshot-only. +- `account/rateLimitResetCredit/consume` — consume one earned reset using a caller-provided idempotency key, optionally selecting a reset-credit ID returned by `account/rateLimits/read`. - `account/usage/read` — fetch ChatGPT account token-activity summary and daily buckets. - `account/workspaceMessages/read` — fetch active workspace messages, including workspace notification headlines when available. - `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change. This is a sparse rolling update; merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. @@ -2042,7 +2041,18 @@ Field notes: "rateLimitReachedType": null }, "rateLimitResetCredits": { - "availableCount": 2 + "availableCount": 2, + "credits": [ + { + "id": "RateLimitResetCredit_1", + "resetType": "codexRateLimits", + "status": "available", + "grantedAt": 1781654400, + "expiresAt": 1784246400, + "title": "Full reset (Weekly + 5 hr)", + "description": "Ready to redeem" + } + ] } } } @@ -2057,55 +2067,29 @@ Field notes: - `rateLimitReachedType` identifies the backend-classified limit state when one has been reached. - `individualLimit` describes the effective monthly credit limit when available. In an `account/rateLimits/read` response, `null` means no monthly limit is available. In a sparse `account/rateLimits/updated` notification, nullable account metadata may be unavailable and does not clear a previously observed value. - `rateLimitResetCredits` contains the available earned-reset count when the backend provides it; otherwise it is `null`. +- `rateLimitResetCredits.credits` is `null` when only the count is available. An empty array means details were fetched and no available credits were returned. +- The backend may cap `rateLimitResetCredits.credits`, so `availableCount` is the authoritative total and can be greater than the number of detail rows. +- Reset-credit `id` values are opaque strings. `grantedAt` and `expiresAt` are Unix timestamps in seconds; `expiresAt` is `null` for a credit that does not expire. +- `title` and `description` are backend-provided display text. Either field can be `null` when the backend does not provide it. +- `resetType` is currently `codexRateLimits`. `status` reports the credit lifecycle state: `available`, `redeeming`, or `redeemed`. - Refetch `account/rateLimits/read` after consuming a reset. ### 8) Earned rate-limit resets (ChatGPT) ```json -{ "method": "account/rateLimitResetCredit/list", "id": 8, "params": {} } -{ - "id": 8, - "result": { - "availableCount": 2, - "data": [ - { - "id": "RateLimitResetCredit_1", - "resetType": "codexRateLimits", - "status": "available", - "grantedAt": 1781654400, - "expiresAt": 1784246400, - "title": "Full reset (Weekly + 5 hr)", - "description": "Ready to redeem" - }, - { - "id": "RateLimitResetCredit_2", - "resetType": "codexRateLimits", - "status": "available", - "grantedAt": 1781740800, - "expiresAt": null, - "title": null, - "description": null - } - ] - } -} -{ "method": "account/rateLimitResetCredit/consume", "id": 9, "params": { "idempotencyKey": "8ae96ff3-3425-4f4c-8772-b6fd61502868", "creditId": "RateLimitResetCredit_1" } } -{ "id": 9, "result": { "outcome": "reset" } } +{ "method": "account/rateLimitResetCredit/consume", "id": 8, "params": { "idempotencyKey": "8ae96ff3-3425-4f4c-8772-b6fd61502868", "creditId": "RateLimitResetCredit_1" } } +{ "id": 8, "result": { "outcome": "reset" } } ``` Field notes: -- `availableCount` is the authoritative total. `data` contains details for at most the first 10 available credits. The backend does not expose a cursor, so the count can be greater than the number of rows and this method cannot paginate the remainder. -- Reset-credit `id` values are opaque strings. `grantedAt` and `expiresAt` are Unix timestamps in seconds; `expiresAt` is `null` for a credit that does not expire. -- `title` and `description` are backend-provided display text. Either field can be `null` when the backend does not provide it. -- `resetType` is currently `codexRateLimits`. `status` reports the credit lifecycle state: `available`, `redeeming`, or `redeemed`. - `idempotencyKey` must be non-empty. A UUID is recommended for each logical redemption attempt; reuse the same value when retrying that attempt. -- `creditId` is optional. When provided, it must be a non-empty opaque ID returned by `account/rateLimitResetCredit/list`; when omitted, the backend selects the next available credit. +- `creditId` is optional. When provided, it must be a non-empty opaque ID returned by `account/rateLimits/read`; when omitted, the backend selects the next available credit. - `reset` means a credit was consumed. - `alreadyRedeemed` means the same redemption completed previously. Treat it as an idempotent success and refresh account limits. - `nothingToReset` means there is no eligible rate-limit window to reset. - `noCredit` means the account has no earned reset credits available. -- Refetch `account/rateLimits/read` and `account/rateLimitResetCredit/list` after consuming a reset instead of inferring updated state from this response. +- Refetch `account/rateLimits/read` after consuming a reset instead of inferring updated state from this response. ### 9) Workspace messages (ChatGPT) diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 7b1e787b01cb..375307e804f7 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1422,11 +1422,6 @@ impl MessageProcessor { ClientRequest::GetAccountRateLimits { .. } => { self.account_processor.get_account_rate_limits().await } - ClientRequest::AccountRateLimitResetCreditList { .. } => { - self.account_processor - .account_rate_limit_reset_credit_list() - .await - } ClientRequest::ConsumeAccountRateLimitResetCredit { params, .. } => { self.account_processor .consume_account_rate_limit_reset_credit(params) diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 7e14761e4f2e..f03351468faa 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -22,7 +22,6 @@ use codex_analytics::InputError; use codex_analytics::TurnSteerRequestError; use codex_app_server_protocol::Account; use codex_app_server_protocol::AccountLoginCompletedNotification; -use codex_app_server_protocol::AccountRateLimitResetCreditListResponse; use codex_app_server_protocol::AccountTokenUsageDailyBucket; use codex_app_server_protocol::AccountTokenUsageSummary; use codex_app_server_protocol::AccountUpdatedNotification; diff --git a/codex-rs/app-server/src/request_processors/account_processor.rs b/codex-rs/app-server/src/request_processors/account_processor.rs index ceaa142ba223..9592563bece0 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -138,14 +138,6 @@ impl AccountRequestProcessor { .map(|response| Some(response.into())) } - pub(crate) async fn account_rate_limit_reset_credit_list( - &self, - ) -> Result, JSONRPCErrorError> { - self.account_rate_limit_reset_credit_list_response() - .await - .map(|response| Some(response.into())) - } - pub(crate) async fn get_account_token_usage( &self, ) -> Result, JSONRPCErrorError> { @@ -899,9 +891,11 @@ impl AccountRequestProcessor { let client = BackendClient::from_auth(self.config.chatgpt_base_url.clone(), &auth) .map_err(|err| internal_error(format!("failed to construct backend client: {err}")))?; - let response = client - .get_rate_limits_with_reset_credits() - .await + let (response, detailed_rate_limit_reset_credits) = tokio::join!( + client.get_rate_limits_with_reset_credits(), + Self::detailed_rate_limit_reset_credits(&client), + ); + let response = response .map_err(|err| internal_error(format!("failed to fetch codex rate limits: {err}")))?; if response.rate_limits.is_empty() { return Err(internal_error( @@ -928,6 +922,15 @@ impl AccountRequestProcessor { .cloned() .unwrap_or_else(|| response.rate_limits[0].clone()); + let rate_limit_reset_credits = detailed_rate_limit_reset_credits.or_else(|| { + response + .rate_limit_reset_credits + .map(|summary| RateLimitResetCreditsSummary { + available_count: summary.available_count, + credits: None, + }) + }); + Ok(GetAccountRateLimitsResponse { rate_limits: rate_limits.into(), rate_limits_by_limit_id: Some( @@ -936,11 +939,7 @@ impl AccountRequestProcessor { .map(|(limit_id, snapshot)| (limit_id, snapshot.into())) .collect(), ), - rate_limit_reset_credits: response.rate_limit_reset_credits.map(|summary| { - RateLimitResetCreditsSummary { - available_count: summary.available_count, - } - }), + rate_limit_reset_credits, }) } diff --git a/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs b/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs index b26ea566be2c..0170ef01dee4 100644 --- a/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs +++ b/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs @@ -7,19 +7,39 @@ const RATE_LIMIT_RESET_REQUEST_TIMEOUT_ENV_VAR: &str = "CODEX_TEST_RATE_LIMIT_RESET_REQUEST_TIMEOUT_MS"; impl AccountRequestProcessor { - pub(super) async fn account_rate_limit_reset_credit_list_response( - &self, - ) -> Result { - let client = self.rate_limit_reset_backend_client().await?; - let details = tokio::time::timeout( + pub(super) async fn detailed_rate_limit_reset_credits( + client: &BackendClient, + ) -> Option { + let details = match tokio::time::timeout( RATE_LIMIT_RESET_DETAILS_REQUEST_TIMEOUT, client.list_rate_limit_reset_credits(), ) .await - .map_err(|_| internal_error("rate limit reset credit detail request timed out"))? - .map_err(|_| internal_error("failed to fetch rate limit reset credit details"))?; - rate_limit_reset_credits_from_backend(details) - .map_err(|_| internal_error("failed to parse rate limit reset credit details")) + { + Ok(Ok(details)) => details, + Ok(Err(err)) => { + tracing::warn!( + "failed to fetch rate limit reset credit details; falling back to the usage response: {err}" + ); + return None; + } + Err(_) => { + tracing::warn!( + "rate limit reset credit detail request timed out; falling back to the usage response" + ); + return None; + } + }; + + match rate_limit_reset_credits_from_backend(details) { + Ok(summary) => Some(summary), + Err(err) => { + tracing::warn!( + "failed to parse rate limit reset credit details; falling back to the usage response: {err}" + ); + None + } + } } pub(crate) async fn consume_account_rate_limit_reset_credit( @@ -96,15 +116,15 @@ impl AccountRequestProcessor { fn rate_limit_reset_credits_from_backend( details: BackendRateLimitResetCreditsDetails, -) -> Result { - let data = details +) -> Result { + let credits = details .credits .into_iter() .map(rate_limit_reset_credit_from_backend) .collect::, _>>()?; - Ok(AccountRateLimitResetCreditListResponse { + Ok(RateLimitResetCreditsSummary { available_count: details.available_count, - data, + credits: Some(credits), }) } diff --git a/codex-rs/app-server/tests/common/test_app_server.rs b/codex-rs/app-server/tests/common/test_app_server.rs index 7db00b13677a..cbb8ff161e0f 100644 --- a/codex-rs/app-server/tests/common/test_app_server.rs +++ b/codex-rs/app-server/tests/common/test_app_server.rs @@ -503,17 +503,6 @@ impl TestAppServer { .await } - /// Send an `account/rateLimitResetCredit/list` JSON-RPC request. - pub async fn send_account_rate_limit_reset_credit_list_request( - &mut self, - ) -> anyhow::Result { - self.send_request( - "account/rateLimitResetCredit/list", - Some(serde_json::json!({})), - ) - .await - } - /// Send an `account/rateLimitResetCredit/consume` JSON-RPC request. pub async fn send_consume_account_rate_limit_reset_credit_request( &mut self, diff --git a/codex-rs/app-server/tests/suite/v2/rate_limit_reset_credits.rs b/codex-rs/app-server/tests/suite/v2/rate_limit_reset_credits.rs index ac30bcd4dd58..fbb4e6e94064 100644 --- a/codex-rs/app-server/tests/suite/v2/rate_limit_reset_credits.rs +++ b/codex-rs/app-server/tests/suite/v2/rate_limit_reset_credits.rs @@ -5,7 +5,6 @@ use app_test_support::ChatGptAuthFixture; use app_test_support::TestAppServer; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; -use codex_app_server_protocol::AccountRateLimitResetCreditListResponse; use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditOutcome; use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditParams; use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditResponse; @@ -13,9 +12,6 @@ use codex_app_server_protocol::GetAccountParams; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::LoginAccountResponse; -use codex_app_server_protocol::RateLimitResetCredit; -use codex_app_server_protocol::RateLimitResetCreditStatus; -use codex_app_server_protocol::RateLimitResetType; use codex_app_server_protocol::RequestId; use codex_config::types::AuthCredentialsStoreMode; use pretty_assertions::assert_eq; @@ -39,108 +35,6 @@ const SERVER_TIMEOUT_READ_TIMEOUT: std::time::Duration = const INVALID_REQUEST_ERROR_CODE: i64 = -32600; const INTERNAL_ERROR_CODE: i64 = -32603; -#[tokio::test] -async fn account_rate_limit_reset_credit_list_requires_chatgpt_auth() -> Result<()> { - let codex_home = TempDir::new()?; - let mut mcp = initialized_app_server(codex_home.path()).await?; - - let request_id = mcp - .send_account_rate_limit_reset_credit_list_request() - .await?; - let error = read_error_response(&mut mcp, request_id).await?; - assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE); - assert_eq!( - error.error.message, - "codex account authentication required for rate limit reset credits" - ); - - login_with_api_key(&mut mcp, "sk-test-key").await?; - let request_id = mcp - .send_account_rate_limit_reset_credit_list_request() - .await?; - let error = read_error_response(&mut mcp, request_id).await?; - assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE); - assert_eq!( - error.error.message, - "chatgpt authentication required for rate limit reset credits" - ); - Ok(()) -} - -#[tokio::test] -async fn account_rate_limit_reset_credit_list_returns_available_details() -> Result<()> { - let (codex_home, server) = chatgpt_test_context().await?; - Mock::given(method("GET")) - .and(path("/api/codex/rate-limit-reset-credits")) - .and(header("authorization", "Bearer chatgpt-token")) - .and(header("chatgpt-account-id", "account-123")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "credits": [ - { - "id": "credit-1", - "reset_type": "codex_rate_limits", - "status": "available", - "granted_at": "2026-06-17T00:00:00Z", - "expires_at": "2026-07-17T00:00:00Z", - "title": "Full reset (Weekly + 5 hr)", - "description": "Ready to redeem", - "profile_user_id": "ignored" - }, - { - "id": "credit-2", - "reset_type": "codex_rate_limits", - "status": "available", - "granted_at": "2026-06-18T00:00:00Z", - "expires_at": null - } - ], - "available_count": 2, - "total_earned_count": 4 - }))) - .expect(1) - .mount(&server) - .await; - - let mut mcp = initialized_app_server(codex_home.path()).await?; - let request_id = mcp - .send_account_rate_limit_reset_credit_list_request() - .await?; - let response: AccountRateLimitResetCreditListResponse = - read_response(&mut mcp, request_id).await?; - - assert_eq!( - response, - AccountRateLimitResetCreditListResponse { - available_count: 2, - data: vec![ - RateLimitResetCredit { - id: "credit-1".to_string(), - reset_type: RateLimitResetType::CodexRateLimits, - status: RateLimitResetCreditStatus::Available, - granted_at: chrono::DateTime::parse_from_rfc3339("2026-06-17T00:00:00Z")? - .timestamp(), - expires_at: Some( - chrono::DateTime::parse_from_rfc3339("2026-07-17T00:00:00Z")?.timestamp() - ), - title: Some("Full reset (Weekly + 5 hr)".to_string()), - description: Some("Ready to redeem".to_string()), - }, - RateLimitResetCredit { - id: "credit-2".to_string(), - reset_type: RateLimitResetType::CodexRateLimits, - status: RateLimitResetCreditStatus::Available, - granted_at: chrono::DateTime::parse_from_rfc3339("2026-06-18T00:00:00Z")? - .timestamp(), - expires_at: None, - title: None, - description: None, - }, - ], - } - ); - Ok(()) -} - #[tokio::test] async fn consume_rate_limit_reset_credit_requires_chatgpt_auth() -> Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/app-server/tests/suite/v2/rate_limits.rs b/codex-rs/app-server/tests/suite/v2/rate_limits.rs index f661f38fd721..5d0ecaa03005 100644 --- a/codex-rs/app-server/tests/suite/v2/rate_limits.rs +++ b/codex-rs/app-server/tests/suite/v2/rate_limits.rs @@ -10,7 +10,10 @@ use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::LoginAccountResponse; use codex_app_server_protocol::RateLimitReachedType; +use codex_app_server_protocol::RateLimitResetCredit; +use codex_app_server_protocol::RateLimitResetCreditStatus; use codex_app_server_protocol::RateLimitResetCreditsSummary; +use codex_app_server_protocol::RateLimitResetType; use codex_app_server_protocol::RateLimitSnapshot; use codex_app_server_protocol::RateLimitWindow; use codex_app_server_protocol::RequestId; @@ -109,6 +112,16 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> { let secondary_reset_timestamp = chrono::DateTime::parse_from_rfc3339("2025-01-01T01:00:00Z") .expect("parse secondary reset timestamp") .timestamp(); + let reset_credit_granted_at = chrono::DateTime::parse_from_rfc3339("2026-06-17T00:00:00Z") + .expect("parse reset credit grant timestamp") + .timestamp(); + let reset_credit_expires_at = chrono::DateTime::parse_from_rfc3339("2026-07-17T00:00:00Z") + .expect("parse reset credit expiry timestamp") + .timestamp(); + let second_reset_credit_granted_at = + chrono::DateTime::parse_from_rfc3339("2026-06-18T00:00:00Z") + .expect("parse second reset credit grant timestamp") + .timestamp(); let response_body = json!({ "plan_type": "pro", "rate_limit": { @@ -172,8 +185,31 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> { .await; Mock::given(method("GET")) .and(path("/api/codex/rate-limit-reset-credits")) - .respond_with(ResponseTemplate::new(500)) - .expect(0) + .and(header("authorization", "Bearer chatgpt-token")) + .and(header("chatgpt-account-id", "account-123")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "credits": [ + { + "id": "credit-1", + "reset_type": "codex_rate_limits", + "status": "available", + "granted_at": "2026-06-17T00:00:00Z", + "expires_at": "2026-07-17T00:00:00Z", + "title": "Full reset (Weekly + 5 hr)", + "description": "Ready to redeem" + }, + { + "id": "credit-2", + "reset_type": "codex_rate_limits", + "status": "available", + "granted_at": "2026-06-18T00:00:00Z", + "expires_at": null + } + ], + "available_count": 2, + "total_earned_count": 4 + }))) + .expect(1) .mount(&server) .await; @@ -266,13 +302,98 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> { .into_iter() .collect(), ), - rate_limit_reset_credits: Some(RateLimitResetCreditsSummary { available_count: 3 }), + rate_limit_reset_credits: Some(RateLimitResetCreditsSummary { + available_count: 2, + credits: Some(vec![ + RateLimitResetCredit { + id: "credit-1".to_string(), + reset_type: RateLimitResetType::CodexRateLimits, + status: RateLimitResetCreditStatus::Available, + granted_at: reset_credit_granted_at, + expires_at: Some(reset_credit_expires_at), + title: Some("Full reset (Weekly + 5 hr)".to_string()), + description: Some("Ready to redeem".to_string()), + }, + RateLimitResetCredit { + id: "credit-2".to_string(), + reset_type: RateLimitResetType::CodexRateLimits, + status: RateLimitResetCreditStatus::Available, + granted_at: second_reset_credit_granted_at, + expires_at: None, + title: None, + description: None, + }, + ]), + }), }; assert_eq!(received, expected); Ok(()) } +#[tokio::test] +async fn get_account_rate_limits_preserves_count_when_reset_credit_details_fail() -> Result<()> { + let codex_home = TempDir::new()?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("chatgpt-token") + .account_id("account-123") + .plan_type("pro"), + AuthCredentialsStoreMode::File, + )?; + + let server = MockServer::start().await; + write_chatgpt_base_url(codex_home.path(), &server.uri())?; + + Mock::given(method("GET")) + .and(path("/api/codex/usage")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "plan_type": "pro", + "rate_limit": { + "allowed": true, + "limit_reached": false, + "primary_window": { + "used_percent": 42, + "limit_window_seconds": 3600, + "reset_after_seconds": 120, + "reset_at": 1735689720 + } + }, + "rate_limit_reset_credits": { "available_count": 3 } + }))) + .expect(1) + .mount(&server) + .await; + Mock::given(method("GET")) + .and(path("/api/codex/rate-limit-reset-credits")) + .respond_with(ResponseTemplate::new(500).set_body_string("boom")) + .expect(1) + .mount(&server) + .await; + + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp.send_get_account_rate_limits_request().await?; + let response: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let received: GetAccountRateLimitsResponse = to_response(response)?; + + assert_eq!( + received.rate_limit_reset_credits, + Some(RateLimitResetCreditsSummary { + available_count: 3, + credits: None, + }) + ); + + Ok(()) +} + #[tokio::test] async fn send_add_credits_nudge_email_requires_auth() -> Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/tui/src/chatwidget/tests/usage.rs b/codex-rs/tui/src/chatwidget/tests/usage.rs index c05b4a62ffac..22908b11121f 100644 --- a/codex-rs/tui/src/chatwidget/tests/usage.rs +++ b/codex-rs/tui/src/chatwidget/tests/usage.rs @@ -14,7 +14,10 @@ async fn usage_command_opens_menu_when_reset_is_available_snapshot() { assert!(chat.finish_rate_limit_reset_hint_refresh( request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None + }), )); chat.dispatch_command(SlashCommand::Usage); @@ -35,7 +38,10 @@ async fn usage_command_disables_reset_after_cached_zero_snapshot() { assert!(chat.finish_rate_limit_reset_hint_refresh( request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 0 }), + Ok(RateLimitResetCreditsSummary { + available_count: 0, + credits: None + }), )); chat.dispatch_command(SlashCommand::Usage); @@ -63,7 +69,10 @@ async fn usage_menu_refresh_enables_newly_available_reset() { assert!(chat.finish_rate_limit_reset_hint_refresh( request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 0 }), + Ok(RateLimitResetCreditsSummary { + available_count: 0, + credits: None + }), )); chat.dispatch_command(SlashCommand::Usage); @@ -76,7 +85,10 @@ async fn usage_menu_refresh_enables_newly_available_reset() { chat.finish_usage_menu_rate_limit_refresh( /*request_id*/ 1, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 1 }), + Ok(RateLimitResetCreditsSummary { + available_count: 1, + credits: None, + }), ); chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); @@ -92,7 +104,10 @@ async fn usage_menu_refresh_failure_preserves_disabled_known_zero() { assert!(chat.finish_rate_limit_reset_hint_refresh( request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 0 }), + Ok(RateLimitResetCreditsSummary { + available_count: 0, + credits: None + }), )); chat.dispatch_command(SlashCommand::Usage); @@ -122,7 +137,10 @@ async fn account_update_invalidates_usage_menu_refresh_when_visible_state_is_unc assert!(chat.finish_rate_limit_reset_hint_refresh( startup_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 0 }), + Ok(RateLimitResetCreditsSummary { + available_count: 0, + credits: None + }), )); chat.dispatch_command(SlashCommand::Usage); assert_matches!( @@ -139,7 +157,10 @@ async fn account_update_invalidates_usage_menu_refresh_when_visible_state_is_unc chat.finish_usage_menu_rate_limit_refresh( /*request_id*/ 1, vec![snapshot(/*percent*/ 92.0)], - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None, + }), ); assert_eq!(chat.available_rate_limit_reset_credits, None); @@ -185,7 +206,10 @@ async fn usage_menu_rate_limit_reset_entry_opens_reset_flow() { assert!(chat.finish_rate_limit_reset_hint_refresh( request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None + }), )); chat.dispatch_command(SlashCommand::Usage); @@ -206,7 +230,10 @@ async fn rate_limit_reset_popup_states_snapshot() { assert!(chat.finish_rate_limit_reset_credits_refresh( loading_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None + }), )); record_popup(&chat, &mut states); @@ -215,7 +242,10 @@ async fn rate_limit_reset_popup_states_snapshot() { assert!(chat.finish_rate_limit_reset_credits_refresh( empty_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 0 }), + Ok(RateLimitResetCreditsSummary { + available_count: 0, + credits: None + }), )); record_popup(&chat, &mut states); @@ -270,7 +300,10 @@ async fn rate_limit_reset_popup_states_snapshot() { assert!(chat.finish_post_consume_reset_credits_refresh( success_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 1 }), + Ok(RateLimitResetCreditsSummary { + available_count: 1, + credits: None + }), )); record_popup(&chat, &mut states); @@ -288,7 +321,10 @@ async fn usage_limit_reset_confirmation_uses_monthly_copy_for_monthly_limits_sna assert!(chat.finish_rate_limit_reset_credits_refresh( free_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 1 }), + Ok(RateLimitResetCreditsSummary { + available_count: 1, + credits: None + }), )); states.push(format!( "Free:\n{}", @@ -301,7 +337,10 @@ async fn usage_limit_reset_confirmation_uses_monthly_copy_for_monthly_limits_sna assert!(chat.finish_rate_limit_reset_credits_refresh( go_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 1 }), + Ok(RateLimitResetCreditsSummary { + available_count: 1, + credits: None + }), )); states.push(format!("Go:\n{}", render_bottom_popup(&chat, /*width*/ 80))); @@ -317,7 +356,10 @@ async fn usage_limit_reset_confirmation_uses_monthly_copy_for_monthly_limits_sna assert!(chat.finish_rate_limit_reset_credits_refresh( business_request_id, vec![monthly_business_snapshot], - Ok(RateLimitResetCreditsSummary { available_count: 1 }), + Ok(RateLimitResetCreditsSummary { + available_count: 1, + credits: None + }), )); states.push(format!( "Business with monthly window:\n{}", @@ -337,7 +379,10 @@ async fn rate_limit_reset_confirmation_selects_cancel_by_default() { assert!(chat.finish_rate_limit_reset_credits_refresh( request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 1 }), + Ok(RateLimitResetCreditsSummary { + available_count: 1, + credits: None + }), )); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); @@ -353,7 +398,10 @@ async fn rate_limit_reset_confirmation_can_use_reset() { assert!(chat.finish_rate_limit_reset_credits_refresh( request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 1 }), + Ok(RateLimitResetCreditsSummary { + available_count: 1, + credits: None + }), )); chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); @@ -393,7 +441,10 @@ async fn no_credit_outcome_disables_reset_entry_in_usage_menu() { assert!(chat.finish_rate_limit_reset_hint_refresh( startup_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 1 }), + Ok(RateLimitResetCreditsSummary { + available_count: 1, + credits: None + }), )); let consume_request_id = chat.show_rate_limit_reset_consuming_popup(); assert!(!finish_reset_consume_outcome( @@ -438,7 +489,10 @@ async fn rate_limit_reset_redemption_cannot_be_dismissed_while_in_flight() { assert!(chat.finish_post_consume_reset_credits_refresh( request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 1 }), + Ok(RateLimitResetCreditsSummary { + available_count: 1, + credits: None + }), )); dismiss_popup(&mut chat); assert!(chat.bottom_pane.no_modal_or_popup_active()); @@ -469,7 +523,10 @@ async fn already_redeemed_is_an_idempotent_success() { assert!(chat.finish_post_consume_reset_credits_refresh( request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 0 }), + Ok(RateLimitResetCreditsSummary { + available_count: 0, + credits: None + }), )); assert!( render_bottom_popup(&chat, /*width*/ 80) @@ -485,7 +542,10 @@ async fn failed_post_consume_refresh_does_not_keep_stale_reset_count() { assert!(chat.finish_rate_limit_reset_hint_refresh( startup_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None + }), )); let consume_request_id = chat.show_rate_limit_reset_consuming_popup(); assert!(finish_reset_consume_outcome( @@ -522,7 +582,10 @@ async fn account_change_invalidates_pending_reset_requests() { assert!(!chat.finish_rate_limit_reset_credits_refresh( request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None + }), )); assert!(chat.bottom_pane.no_modal_or_popup_active()); } @@ -536,7 +599,10 @@ async fn clearing_pending_reset_hint_preserves_in_flight_redemption() { assert!(chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None + }), )); chat.clear_pending_rate_limit_reset_hint(); @@ -559,7 +625,10 @@ async fn rate_limit_reset_load_result_updates_popup_beneath_overlay() { assert!(chat.finish_rate_limit_reset_credits_refresh( request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None + }), )); assert_eq!( chat.bottom_pane.active_view_id(), @@ -588,7 +657,10 @@ async fn rate_limit_reset_success_updates_popup_beneath_overlay() { assert!(chat.finish_post_consume_reset_credits_refresh( request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 1 }), + Ok(RateLimitResetCreditsSummary { + available_count: 1, + credits: None + }), )); assert_eq!( chat.bottom_pane.active_view_id(), @@ -631,7 +703,10 @@ async fn startup_check_shows_available_reset_hint_snapshot() { assert!(chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None + }), )); let rendered = lines_to_single_string( &chat @@ -654,7 +729,10 @@ async fn startup_reset_hint_waits_for_active_output_snapshot() { assert!(chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None + }), )); assert!(chat.usage_history_insertion_blocked()); @@ -685,7 +763,10 @@ async fn opening_rate_limit_reset_flow_invalidates_in_flight_startup_hint() { assert!(!chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None + }), )); assert!(chat.pending_rate_limit_reset_hint().is_none()); } @@ -698,7 +779,10 @@ async fn starting_rate_limit_reset_redemption_clears_deferred_startup_hint() { assert!(chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None + }), )); assert!(chat.pending_rate_limit_reset_hint().is_some()); @@ -716,7 +800,10 @@ async fn startup_check_omits_reset_hint_when_none_are_available() { assert!(chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 0 }), + Ok(RateLimitResetCreditsSummary { + available_count: 0, + credits: None + }), )); assert!(chat.pending_rate_limit_reset_hint().is_none()); } @@ -731,7 +818,10 @@ async fn startup_check_shows_reset_hint_for_workspace_account_with_credit() { assert!(chat.finish_rate_limit_reset_hint_refresh( hint_request_id, Vec::new(), - Ok(RateLimitResetCreditsSummary { available_count: 2 }), + Ok(RateLimitResetCreditsSummary { + available_count: 2, + credits: None + }), )); assert!(chat.pending_rate_limit_reset_hint().is_some()); assert_eq!(chat.available_rate_limit_reset_credits, Some(2)); From 144c96db37b5cd65028c2e8ad8518046b4b81def Mon Sep 17 00:00:00 2001 From: jayp-oai Date: Mon, 6 Jul 2026 12:14:51 -0700 Subject: [PATCH 4/4] Handle unknown reset credit values --- .../json/codex_app_server_protocol.schemas.json | 6 ++++-- .../json/codex_app_server_protocol.v2.schemas.json | 6 ++++-- .../json/v2/GetAccountRateLimitsResponse.json | 6 ++++-- .../typescript/v2/RateLimitResetCreditStatus.ts | 2 +- .../schema/typescript/v2/RateLimitResetType.ts | 2 +- .../app-server-protocol/src/protocol/v2/account.rs | 4 ++++ codex-rs/app-server/README.md | 3 --- .../account_processor/rate_limit_resets.rs | 14 ++------------ codex-rs/app-server/tests/suite/v2/rate_limits.rs | 8 ++++---- 9 files changed, 24 insertions(+), 27 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 6c45ebb4b382..0eedef903a09 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -14592,7 +14592,8 @@ "enum": [ "available", "redeeming", - "redeemed" + "redeemed", + "unknown" ], "type": "string" }, @@ -14620,7 +14621,8 @@ }, "RateLimitResetType": { "enum": [ - "codexRateLimits" + "codexRateLimits", + "unknown" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index bf91599a33cd..b428f252586b 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -10996,7 +10996,8 @@ "enum": [ "available", "redeeming", - "redeemed" + "redeemed", + "unknown" ], "type": "string" }, @@ -11024,7 +11025,8 @@ }, "RateLimitResetType": { "enum": [ - "codexRateLimits" + "codexRateLimits", + "unknown" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json b/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json index cb10c1c18fc5..a2883a12934c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/GetAccountRateLimitsResponse.json @@ -101,7 +101,8 @@ "enum": [ "available", "redeeming", - "redeemed" + "redeemed", + "unknown" ], "type": "string" }, @@ -129,7 +130,8 @@ }, "RateLimitResetType": { "enum": [ - "codexRateLimits" + "codexRateLimits", + "unknown" ], "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditStatus.ts index 254923c2bf25..fa15861b3a11 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditStatus.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetCreditStatus.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type RateLimitResetCreditStatus = "available" | "redeeming" | "redeemed"; +export type RateLimitResetCreditStatus = "available" | "redeeming" | "redeemed" | "unknown"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetType.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetType.ts index 6c77b0b16071..718145bf47ca 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetType.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/RateLimitResetType.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type RateLimitResetType = "codexRateLimits"; +export type RateLimitResetType = "codexRateLimits" | "unknown"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/account.rs b/codex-rs/app-server-protocol/src/protocol/v2/account.rs index 5af952a2d47c..788c8f7aafda 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -317,6 +317,8 @@ pub struct RateLimitResetCredit { #[ts(export_to = "v2/", rename_all = "camelCase")] pub enum RateLimitResetType { CodexRateLimits, + #[serde(other)] + Unknown, } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] @@ -326,6 +328,8 @@ pub enum RateLimitResetCreditStatus { Available, Redeeming, Redeemed, + #[serde(other)] + Unknown, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c9d006f2d0ac..a2587f5a8eb9 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -2069,9 +2069,6 @@ Field notes: - `rateLimitResetCredits` contains the available earned-reset count when the backend provides it; otherwise it is `null`. - `rateLimitResetCredits.credits` is `null` when only the count is available. An empty array means details were fetched and no available credits were returned. - The backend may cap `rateLimitResetCredits.credits`, so `availableCount` is the authoritative total and can be greater than the number of detail rows. -- Reset-credit `id` values are opaque strings. `grantedAt` and `expiresAt` are Unix timestamps in seconds; `expiresAt` is `null` for a credit that does not expire. -- `title` and `description` are backend-provided display text. Either field can be `null` when the backend does not provide it. -- `resetType` is currently `codexRateLimits`. `status` reports the credit lifecycle state: `available`, `redeeming`, or `redeemed`. - Refetch `account/rateLimits/read` after consuming a reset. ### 8) Earned rate-limit resets (ChatGPT) diff --git a/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs b/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs index 0170ef01dee4..919d30d770a9 100644 --- a/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs +++ b/codex-rs/app-server/src/request_processors/account_processor/rate_limit_resets.rs @@ -133,23 +133,13 @@ fn rate_limit_reset_credit_from_backend( ) -> Result { let reset_type = match credit.reset_type.as_str() { "codex_rate_limits" => RateLimitResetType::CodexRateLimits, - unknown => { - return Err(format!( - "unknown reset type `{unknown}` for credit `{}`", - credit.id - )); - } + _ => RateLimitResetType::Unknown, }; let status = match credit.status.as_str() { "available" => RateLimitResetCreditStatus::Available, "redeeming" => RateLimitResetCreditStatus::Redeeming, "redeemed" => RateLimitResetCreditStatus::Redeemed, - unknown => { - return Err(format!( - "unknown status `{unknown}` for credit `{}`", - credit.id - )); - } + _ => RateLimitResetCreditStatus::Unknown, }; let granted_at = rate_limit_reset_credit_timestamp(&credit.granted_at) .map_err(|err| format!("invalid granted_at for credit `{}`: {err}", credit.id))?; diff --git a/codex-rs/app-server/tests/suite/v2/rate_limits.rs b/codex-rs/app-server/tests/suite/v2/rate_limits.rs index 5d0ecaa03005..423385cea915 100644 --- a/codex-rs/app-server/tests/suite/v2/rate_limits.rs +++ b/codex-rs/app-server/tests/suite/v2/rate_limits.rs @@ -200,8 +200,8 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> { }, { "id": "credit-2", - "reset_type": "codex_rate_limits", - "status": "available", + "reset_type": "future_reset_type", + "status": "future_status", "granted_at": "2026-06-18T00:00:00Z", "expires_at": null } @@ -316,8 +316,8 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> { }, RateLimitResetCredit { id: "credit-2".to_string(), - reset_type: RateLimitResetType::CodexRateLimits, - status: RateLimitResetCreditStatus::Available, + reset_type: RateLimitResetType::Unknown, + status: RateLimitResetCreditStatus::Unknown, granted_at: second_reset_credit_granted_at, expires_at: None, title: None,