@@ -25,6 +25,50 @@ export interface RateLimitInfo {
2525 code ?: string ;
2626}
2727
28+ export interface EntitlementError {
29+ isEntitlement : true ;
30+ code : string ;
31+ message : string ;
32+ }
33+
34+ /**
35+ * Checks if an error code indicates an entitlement/subscription issue
36+ * These errors should NOT be treated as rate limits because:
37+ * 1. They won't resolve by waiting
38+ * 2. They won't resolve by switching accounts (all accounts likely have same issue)
39+ * 3. User needs to upgrade their subscription
40+ */
41+ export function isEntitlementError ( code : string , bodyText : string ) : boolean {
42+ const haystack = `${ code } ${ bodyText } ` . toLowerCase ( ) ;
43+ // "usage_not_included" means the subscription doesn't include this feature
44+ // This is different from "usage_limit_reached" which is a temporary quota limit
45+ return / u s a g e _ n o t _ i n c l u d e d | n o t .i n c l u d e d .i n .y o u r .p l a n | s u b s c r i p t i o n .d o e s .n o t .i n c l u d e / i. test ( haystack ) ;
46+ }
47+
48+ /**
49+ * Creates a user-friendly entitlement error response
50+ */
51+ export function createEntitlementErrorResponse ( _bodyText : string ) : Response {
52+ const message =
53+ "This model is not included in your ChatGPT subscription. " +
54+ "Please check that your account has access to Codex models (requires ChatGPT Plus/Pro). " +
55+ "If you recently subscribed, try logging out and back in with `opencode auth login`." ;
56+
57+ const payload = {
58+ error : {
59+ message,
60+ type : "entitlement_error" ,
61+ code : "usage_not_included" ,
62+ } ,
63+ } ;
64+
65+ return new Response ( JSON . stringify ( payload ) , {
66+ status : 403 , // Forbidden - not a rate limit
67+ statusText : "Forbidden" ,
68+ headers : { "content-type" : "application/json; charset=utf-8" } ,
69+ } ) ;
70+ }
71+
2872export interface ErrorHandlingResult {
2973 response : Response ;
3074 rateLimit ?: RateLimitInfo ;
@@ -219,6 +263,12 @@ export async function handleErrorResponse(
219263) : Promise < ErrorHandlingResult > {
220264 const bodyText = await safeReadBody ( response ) ;
221265 const mapped = mapUsageLimit404WithBody ( response , bodyText ) ;
266+
267+ // Entitlement errors return a ready-to-use Response with 403 status
268+ if ( mapped && mapped . status === HTTP_STATUS . FORBIDDEN ) {
269+ return { response : mapped , rateLimit : undefined , errorBody : undefined } ;
270+ }
271+
222272 const finalResponse = mapped ?? response ;
223273 const rateLimit = extractRateLimitInfoFromBody ( finalResponse , bodyText ) ;
224274
@@ -291,8 +341,13 @@ function mapUsageLimit404WithBody(response: Response, bodyText: string): Respons
291341 code = "" ;
292342 }
293343
344+ // Check for entitlement errors first - these should NOT be treated as rate limits
345+ if ( isEntitlementError ( code , bodyText ) ) {
346+ return createEntitlementErrorResponse ( bodyText ) ;
347+ }
348+
294349 const haystack = `${ code } ${ bodyText } ` . toLowerCase ( ) ;
295- if ( ! / u s a g e _ l i m i t _ r e a c h e d | u s a g e _ n o t _ i n c l u d e d | r a t e _ l i m i t _ e x c e e d e d | u s a g e l i m i t / i. test ( haystack ) ) {
350+ if ( ! / u s a g e _ l i m i t _ r e a c h e d | r a t e _ l i m i t _ e x c e e d e d | u s a g e l i m i t / i. test ( haystack ) ) {
296351 return null ;
297352 }
298353
@@ -313,9 +368,15 @@ function extractRateLimitInfoFromBody(
313368 const parsed = parseRateLimitBody ( bodyText ) ;
314369
315370 const haystack = `${ parsed ?. code ?? "" } ${ bodyText } ` . toLowerCase ( ) ;
371+
372+ // Entitlement errors should not be treated as rate limits
373+ if ( isEntitlementError ( parsed ?. code ?? "" , bodyText ) ) {
374+ return undefined ;
375+ }
376+
316377 const isRateLimit =
317378 isStatusRateLimit ||
318- / u s a g e _ l i m i t _ r e a c h e d | u s a g e _ n o t _ i n c l u d e d | r a t e _ l i m i t _ e x c e e d e d | r a t e _ l i m i t | u s a g e l i m i t / i. test (
379+ / u s a g e _ l i m i t _ r e a c h e d | r a t e _ l i m i t _ e x c e e d e d | r a t e _ l i m i t | u s a g e l i m i t / i. test (
319380 haystack ,
320381 ) ;
321382 if ( ! isRateLimit ) return undefined ;
0 commit comments