@@ -13,6 +13,7 @@ import { provideTmpdirInstance } from "../fixture/fixture"
1313import { testEffect } from "../lib/effect"
1414
1515const providerID = ProviderID . make ( "test" )
16+ const retryProvider = "test"
1617const it = testEffect ( Layer . mergeAll ( SessionStatus . defaultLayer , CrossSpawnSpawner . defaultLayer ) )
1718
1819function apiError ( headers ?: Record < string , string > ) : MessageV2 . APIError {
@@ -92,6 +93,7 @@ describe("session.retry.delay", () => {
9293
9394 const step = yield * Schedule . toStepWithMetadata (
9495 SessionRetry . policy ( {
96+ provider : "test" ,
9597 parse : ( err ) => MessageV2 . APIError . Schema . parse ( err ) ,
9698 set : ( info ) =>
9799 status . set ( sessionID , {
@@ -118,47 +120,47 @@ describe("session.retry.delay", () => {
118120describe ( "session.retry.retryable" , ( ) => {
119121 test ( "maps too_many_requests json messages" , ( ) => {
120122 const error = wrap ( JSON . stringify ( { type : "error" , error : { type : "too_many_requests" } } ) )
121- expect ( SessionRetry . retryable ( error ) ) . toEqual ( { message : "Too Many Requests" } )
123+ expect ( SessionRetry . retryable ( error , retryProvider ) ) . toEqual ( { message : "Too Many Requests" } )
122124 } )
123125
124126 test ( "maps overloaded provider codes" , ( ) => {
125127 const error = wrap ( JSON . stringify ( { code : "resource_exhausted" } ) )
126- expect ( SessionRetry . retryable ( error ) ) . toEqual ( { message : "Provider is overloaded" } )
128+ expect ( SessionRetry . retryable ( error , retryProvider ) ) . toEqual ( { message : "Provider is overloaded" } )
127129 } )
128130
129131 test ( "does not retry unknown json messages" , ( ) => {
130132 const error = wrap ( JSON . stringify ( { error : { message : "no_kv_space" } } ) )
131- expect ( SessionRetry . retryable ( error ) ) . toBeUndefined ( )
133+ expect ( SessionRetry . retryable ( error , retryProvider ) ) . toBeUndefined ( )
132134 } )
133135
134136 test ( "does not throw on numeric error codes" , ( ) => {
135137 const error = wrap ( JSON . stringify ( { type : "error" , error : { code : 123 } } ) )
136- const result = SessionRetry . retryable ( error )
138+ const result = SessionRetry . retryable ( error , retryProvider )
137139 expect ( result ) . toBeUndefined ( )
138140 } )
139141
140142 test ( "returns undefined for non-json message" , ( ) => {
141143 const error = wrap ( "not-json" )
142- expect ( SessionRetry . retryable ( error ) ) . toBeUndefined ( )
144+ expect ( SessionRetry . retryable ( error , retryProvider ) ) . toBeUndefined ( )
143145 } )
144146
145147 test ( "retries plain text rate limit errors from Alibaba" , ( ) => {
146148 const msg =
147149 "Upstream error from Alibaba: Request rate increased too quickly. To ensure system stability, please adjust your client logic to scale requests more smoothly over time."
148150 const error = wrap ( msg )
149- expect ( SessionRetry . retryable ( error ) ) . toEqual ( { message : msg } )
151+ expect ( SessionRetry . retryable ( error , retryProvider ) ) . toEqual ( { message : msg } )
150152 } )
151153
152154 test ( "retries plain text rate limit errors" , ( ) => {
153155 const msg = "Rate limit exceeded, please try again later"
154156 const error = wrap ( msg )
155- expect ( SessionRetry . retryable ( error ) ) . toEqual ( { message : msg } )
157+ expect ( SessionRetry . retryable ( error , retryProvider ) ) . toEqual ( { message : msg } )
156158 } )
157159
158160 test ( "retries too many requests in plain text" , ( ) => {
159161 const msg = "Too many requests, please slow down"
160162 const error = wrap ( msg )
161- expect ( SessionRetry . retryable ( error ) ) . toEqual ( { message : msg } )
163+ expect ( SessionRetry . retryable ( error , retryProvider ) ) . toEqual ( { message : msg } )
162164 } )
163165
164166 test ( "does not retry context overflow errors" , ( ) => {
@@ -167,7 +169,7 @@ describe("session.retry.retryable", () => {
167169 responseBody : '{"error":{"code":"context_length_exceeded"}}' ,
168170 } ) . toObject ( )
169171
170- expect ( SessionRetry . retryable ( error ) ) . toBeUndefined ( )
172+ expect ( SessionRetry . retryable ( error , retryProvider ) ) . toBeUndefined ( )
171173 } )
172174
173175 test ( "retries 500 errors even when isRetryable is false" , ( ) => {
@@ -180,7 +182,7 @@ describe("session.retry.retryable", () => {
180182 } ) . toObject ( ) ,
181183 )
182184
183- expect ( SessionRetry . retryable ( error ) ) . toEqual ( { message : "Internal server error" } )
185+ expect ( SessionRetry . retryable ( error , retryProvider ) ) . toEqual ( { message : "Internal server error" } )
184186 } )
185187
186188 test ( "retries 502 bad gateway errors" , ( ) => {
@@ -192,7 +194,7 @@ describe("session.retry.retryable", () => {
192194 } ) . toObject ( ) ,
193195 )
194196
195- expect ( SessionRetry . retryable ( error ) ) . toEqual ( { message : "Bad gateway" } )
197+ expect ( SessionRetry . retryable ( error , retryProvider ) ) . toEqual ( { message : "Bad gateway" } )
196198 } )
197199
198200 test ( "retries 503 service unavailable errors" , ( ) => {
@@ -204,7 +206,7 @@ describe("session.retry.retryable", () => {
204206 } ) . toObject ( ) ,
205207 )
206208
207- expect ( SessionRetry . retryable ( error ) ) . toEqual ( { message : "Service unavailable" } )
209+ expect ( SessionRetry . retryable ( error , retryProvider ) ) . toEqual ( { message : "Service unavailable" } )
208210 } )
209211
210212 test ( "does not retry 4xx errors when isRetryable is false" , ( ) => {
@@ -216,7 +218,7 @@ describe("session.retry.retryable", () => {
216218 } ) . toObject ( ) ,
217219 )
218220
219- expect ( SessionRetry . retryable ( error ) ) . toBeUndefined ( )
221+ expect ( SessionRetry . retryable ( error , retryProvider ) ) . toBeUndefined ( )
220222 } )
221223
222224 test ( "retries ZlibError decompression failures" , ( ) => {
@@ -228,7 +230,7 @@ describe("session.retry.retryable", () => {
228230 } ) . toObject ( ) ,
229231 )
230232
231- const retryable = SessionRetry . retryable ( error )
233+ const retryable = SessionRetry . retryable ( error , retryProvider )
232234 expect ( retryable ) . toBeDefined ( )
233235 expect ( retryable ) . toEqual ( { message : "Response decompression failed" } )
234236 } )
@@ -246,9 +248,11 @@ describe("session.retry.retryable", () => {
246248 } ) . toObject ( ) ,
247249 )
248250
249- expect ( SessionRetry . retryable ( error ) ) . toEqual ( {
251+ expect ( SessionRetry . retryable ( error , "opencode" ) ) . toEqual ( {
250252 message : SessionRetry . GO_UPSELL_MESSAGE ,
251253 action : {
254+ reason : "free_tier_limit" ,
255+ provider : "opencode" ,
252256 title : "Free limit reached" ,
253257 message : "Subscribe to OpenCode Go for reliable access to the best open-source models, starting at $5/month." ,
254258 label : "subscribe" ,
@@ -280,10 +284,12 @@ describe("session.retry.retryable", () => {
280284 } ) . toObject ( ) ,
281285 )
282286
283- expect ( SessionRetry . retryable ( error ) ) . toEqual ( {
287+ expect ( SessionRetry . retryable ( error , "opencode-go" ) ) . toEqual ( {
284288 message :
285289 "5 hour usage limit reached. It will reset in 5 hours 23 minutes. To continue using this model now, enable usage from your available balance - https://opencode.ai/workspace/wrk_01K6XGM22R6FM8JVABE9XDQXGH/go" ,
286290 action : {
291+ reason : "account_rate_limit" ,
292+ provider : "opencode-go" ,
287293 title : "Go limit reached" ,
288294 message :
289295 "5 hour usage limit reached. It will reset in 5 hours 23 minutes. To continue using this model now, enable usage from your available balance" ,
@@ -292,6 +298,33 @@ describe("session.retry.retryable", () => {
292298 } ,
293299 } )
294300 } )
301+
302+ test ( "maps Go subscription limits without limit metadata" , ( ) => {
303+ const error = MessageV2 . APIError . Schema . parse (
304+ new MessageV2 . APIError ( {
305+ message : "Subscription quota exceeded. You can continue using free models." ,
306+ isRetryable : true ,
307+ statusCode : 429 ,
308+ responseHeaders : {
309+ "retry-after" : "900" ,
310+ } ,
311+ responseBody : JSON . stringify ( {
312+ type : "error" ,
313+ error : {
314+ type : "GoUsageLimitError" ,
315+ message : "Subscription quota exceeded. You can continue using free models." ,
316+ } ,
317+ metadata : {
318+ workspace : "wrk_01K6XGM22R6FM8JVABE9XDQXGH" ,
319+ } ,
320+ } ) ,
321+ } ) . toObject ( ) ,
322+ )
323+
324+ expect ( SessionRetry . retryable ( error , "opencode-go" ) ?. action ?. message ) . toBe (
325+ "Usage limit reached. It will reset in 15 minutes. To continue using this model now, enable usage from your available balance" ,
326+ )
327+ } )
295328} )
296329
297330describe ( "session.message-v2.fromError" , ( ) => {
@@ -341,7 +374,7 @@ describe("session.message-v2.fromError", () => {
341374 } ) . toObject ( ) ,
342375 )
343376
344- const retryable = SessionRetry . retryable ( error )
377+ const retryable = SessionRetry . retryable ( error , retryProvider )
345378 expect ( retryable ) . toBeDefined ( )
346379 expect ( retryable ) . toEqual ( { message : "Connection reset by server" } )
347380 } )
@@ -381,6 +414,6 @@ describe("session.message-v2.fromError", () => {
381414 expect ( MessageV2 . APIError . isInstance ( result ) ) . toBe ( true )
382415 if ( ! MessageV2 . APIError . isInstance ( result ) ) throw new Error ( "expected APIError" )
383416 expect ( result . data . isRetryable ) . toBe ( true )
384- expect ( SessionRetry . retryable ( result ) ) . toEqual ( { message : "An error occurred while processing your request." } )
417+ expect ( SessionRetry . retryable ( result , retryProvider ) ) . toEqual ( { message : "An error occurred while processing your request." } )
385418 } )
386419} )
0 commit comments