omk-ai 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/image-models.generated.d.ts +45 -0
- package/dist/image-models.generated.d.ts.map +1 -1
- package/dist/image-models.generated.js +45 -0
- package/dist/image-models.generated.js.map +1 -1
- package/dist/models.generated.d.ts +1509 -864
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.generated.js +1215 -709
- package/dist/models.generated.js.map +1 -1
- package/dist/providers/cursor-connect.d.ts +3 -0
- package/dist/providers/cursor-connect.d.ts.map +1 -0
- package/dist/providers/cursor-connect.js +9 -0
- package/dist/providers/cursor-connect.js.map +1 -0
- package/dist/providers/cursor.d.ts.map +1 -1
- package/dist/providers/cursor.js +17 -13
- package/dist/providers/cursor.js.map +1 -1
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +6 -0
- package/dist/stream.js.map +1 -1
- package/dist/utils/provider-retry-delay.d.ts +4 -0
- package/dist/utils/provider-retry-delay.d.ts.map +1 -0
- package/dist/utils/provider-retry-delay.js +53 -0
- package/dist/utils/provider-retry-delay.js.map +1 -0
- package/dist/utils/provider-retry-sleep.d.ts +6 -0
- package/dist/utils/provider-retry-sleep.d.ts.map +1 -0
- package/dist/utils/provider-retry-sleep.js +51 -0
- package/dist/utils/provider-retry-sleep.js.map +1 -0
- package/dist/utils/provider-retry.d.ts.map +1 -1
- package/dist/utils/provider-retry.js +6 -44
- package/dist/utils/provider-retry.js.map +1 -1
- package/dist/utils/retry-backoff.d.ts +8 -0
- package/dist/utils/retry-backoff.d.ts.map +1 -0
- package/dist/utils/retry-backoff.js +19 -0
- package/dist/utils/retry-backoff.js.map +1 -0
- package/dist/utils/retry.d.ts +11 -3
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +13 -5
- package/dist/utils/retry.js.map +1 -1
- package/package.json +1 -1
package/dist/stream.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAC;AAI1C,OAAO,KAAK,EACX,GAAG,EACH,gBAAgB,EAChB,2BAA2B,EAC3B,OAAO,EACP,KAAK,EACL,qBAAqB,EACrB,mBAAmB,EAEnB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAwBjD,wBAAgB,MAAM,CAAC,IAAI,SAAS,GAAG,EACtC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC7B,2BAA2B,CAG7B;AAED,wBAAsB,QAAQ,CAAC,IAAI,SAAS,GAAG,EAC9C,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC7B,OAAO,CAAC,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAC;AAI1C,OAAO,KAAK,EACX,GAAG,EACH,gBAAgB,EAChB,2BAA2B,EAC3B,OAAO,EACP,KAAK,EACL,qBAAqB,EACrB,mBAAmB,EAEnB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAwBjD,wBAAgB,MAAM,CAAC,IAAI,SAAS,GAAG,EACtC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC7B,2BAA2B,CAG7B;AAED,wBAAsB,QAAQ,CAAC,IAAI,SAAS,GAAG,EAC9C,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC7B,OAAO,CAAC,gBAAgB,CAAC,CAM3B;AAoBD,wBAAgB,YAAY,CAAC,IAAI,SAAS,GAAG,EAC5C,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,mBAAmB,GAC3B,2BAA2B,CAG7B;AAID,mGAAmG;AACnG,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAGtD;AAED,wBAAsB,cAAc,CAAC,IAAI,SAAS,GAAG,EACpD,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,mBAAmB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAM3B","sourcesContent":["import \"./providers/register-builtins.ts\";\n\nimport { getApiProvider } from \"./api-registry.ts\";\nimport { getEnvApiKey } from \"./env-api-keys.ts\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tAssistantMessageEventStream,\n\tContext,\n\tModel,\n\tProviderStreamOptions,\n\tSimpleStreamOptions,\n\tStreamOptions,\n} from \"./types.ts\";\n\nexport { getEnvApiKey } from \"./env-api-keys.ts\";\n\nfunction hasExplicitApiKey(apiKey: string | undefined): apiKey is string {\n\treturn typeof apiKey === \"string\" && apiKey.trim().length > 0;\n}\n\nfunction withEnvApiKey<TOptions extends StreamOptions>(\n\tmodel: Model<Api>,\n\toptions: TOptions | undefined,\n): TOptions | undefined {\n\tif (hasExplicitApiKey(options?.apiKey)) return options;\n\tconst apiKey = getEnvApiKey(model.provider);\n\tif (!apiKey) return options;\n\treturn { ...options, apiKey } as TOptions;\n}\n\nfunction resolveApiProvider(api: Api) {\n\tconst provider = getApiProvider(api);\n\tif (!provider) {\n\t\tthrow new Error(`No API provider registered for api: ${api}`);\n\t}\n\treturn provider;\n}\n\nexport function stream<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: ProviderStreamOptions,\n): AssistantMessageEventStream {\n\tconst provider = resolveApiProvider(model.api);\n\treturn provider.stream(model, context, withEnvApiKey(model, options) as StreamOptions);\n}\n\nexport async function complete<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: ProviderStreamOptions,\n): Promise<AssistantMessage> {\n\tconst s = stream(model, context, options);\n\t// This API exposes only the final message; consume deltas as they arrive.\n\tfor await (const _event of s) {\n\t}\n\treturn s.result();\n}\n\n/**\n * Brand identifying the built-in stream function.\n *\n * Callers decide whether provider credentials are mandatory by asking \"is this\n * still the built-in stream function?\", and a bare `fn === streamSimple`\n * answers that with reference identity. Reference identity is not dependable\n * here: this package can legitimately load more than once in one process (a\n * workspace symlink beside an installed copy, or two dependents resolving\n * different versions), producing two distinct `streamSimple` functions that\n * behave identically. The comparison then reports \"custom stream function\" for\n * what is really the built-in one, and a credential check silently relaxes.\n *\n * `Symbol.for` keys into the per-realm global registry, so every copy of this\n * module brands its own `streamSimple` with the same symbol and the check\n * survives duplication.\n */\nconst BUILTIN_STREAM_FN = Symbol.for(\"omk-ai.streamSimple\");\n\nexport function streamSimple<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream {\n\tconst provider = resolveApiProvider(model.api);\n\treturn provider.streamSimple(model, context, withEnvApiKey(model, options));\n}\n\nObject.defineProperty(streamSimple, BUILTIN_STREAM_FN, { value: true });\n\n/** True when `fn` is this package's built-in stream function, including a duplicate copy of it. */\nexport function isBuiltinStreamFn(fn: unknown): boolean {\n\tif (typeof fn !== \"function\") return false;\n\treturn Reflect.get(fn, BUILTIN_STREAM_FN) === true;\n}\n\nexport async function completeSimple<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): Promise<AssistantMessage> {\n\tconst s = streamSimple(model, context, options);\n\t// Keep completion-only callers from retaining the full event backlog.\n\tfor await (const _event of s) {\n\t}\n\treturn s.result();\n}\n"]}
|
package/dist/stream.js
CHANGED
|
@@ -26,6 +26,9 @@ export function stream(model, context, options) {
|
|
|
26
26
|
}
|
|
27
27
|
export async function complete(model, context, options) {
|
|
28
28
|
const s = stream(model, context, options);
|
|
29
|
+
// This API exposes only the final message; consume deltas as they arrive.
|
|
30
|
+
for await (const _event of s) {
|
|
31
|
+
}
|
|
29
32
|
return s.result();
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
@@ -58,6 +61,9 @@ export function isBuiltinStreamFn(fn) {
|
|
|
58
61
|
}
|
|
59
62
|
export async function completeSimple(model, context, options) {
|
|
60
63
|
const s = streamSimple(model, context, options);
|
|
64
|
+
// Keep completion-only callers from retaining the full event backlog.
|
|
65
|
+
for await (const _event of s) {
|
|
66
|
+
}
|
|
61
67
|
return s.result();
|
|
62
68
|
}
|
|
63
69
|
//# sourceMappingURL=stream.js.map
|
package/dist/stream.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAC;AAE1C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAYjD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,SAAS,iBAAiB,CAAC,MAA0B,EAAoB;IACxE,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAAA,CAC9D;AAED,SAAS,aAAa,CACrB,KAAiB,EACjB,OAA6B,EACN;IACvB,IAAI,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC;QAAE,OAAO,OAAO,CAAC;IACvD,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAC;IAC5B,OAAO,EAAE,GAAG,OAAO,EAAE,MAAM,EAAc,CAAC;AAAA,CAC1C;AAED,SAAS,kBAAkB,CAAC,GAAQ,EAAE;IACrC,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,uCAAuC,GAAG,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED,MAAM,UAAU,MAAM,CACrB,KAAkB,EAClB,OAAgB,EAChB,OAA+B,EACD;IAC9B,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/C,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,aAAa,CAAC,KAAK,EAAE,OAAO,CAAkB,CAAC,CAAC;AAAA,CACvF;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC7B,KAAkB,EAClB,OAAgB,EAChB,OAA+B,EACH;IAC5B,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1C,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;AAAA,CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,iBAAiB,GAAG,MAAM,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;AAE5D,MAAM,UAAU,YAAY,CAC3B,KAAkB,EAClB,OAAgB,EAChB,OAA6B,EACC;IAC9B,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/C,OAAO,QAAQ,CAAC,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;AAAA,CAC5E;AAED,MAAM,CAAC,cAAc,CAAC,YAAY,EAAE,iBAAiB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAExE,mGAAmG;AACnG,MAAM,UAAU,iBAAiB,CAAC,EAAW,EAAW;IACvD,IAAI,OAAO,EAAE,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IAC3C,OAAO,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,iBAAiB,CAAC,KAAK,IAAI,CAAC;AAAA,CACnD;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CACnC,KAAkB,EAClB,OAAgB,EAChB,OAA6B,EACD;IAC5B,MAAM,CAAC,GAAG,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAChD,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;AAAA,CAClB","sourcesContent":["import \"./providers/register-builtins.ts\";\n\nimport { getApiProvider } from \"./api-registry.ts\";\nimport { getEnvApiKey } from \"./env-api-keys.ts\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tAssistantMessageEventStream,\n\tContext,\n\tModel,\n\tProviderStreamOptions,\n\tSimpleStreamOptions,\n\tStreamOptions,\n} from \"./types.ts\";\n\nexport { getEnvApiKey } from \"./env-api-keys.ts\";\n\nfunction hasExplicitApiKey(apiKey: string | undefined): apiKey is string {\n\treturn typeof apiKey === \"string\" && apiKey.trim().length > 0;\n}\n\nfunction withEnvApiKey<TOptions extends StreamOptions>(\n\tmodel: Model<Api>,\n\toptions: TOptions | undefined,\n): TOptions | undefined {\n\tif (hasExplicitApiKey(options?.apiKey)) return options;\n\tconst apiKey = getEnvApiKey(model.provider);\n\tif (!apiKey) return options;\n\treturn { ...options, apiKey } as TOptions;\n}\n\nfunction resolveApiProvider(api: Api) {\n\tconst provider = getApiProvider(api);\n\tif (!provider) {\n\t\tthrow new Error(`No API provider registered for api: ${api}`);\n\t}\n\treturn provider;\n}\n\nexport function stream<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: ProviderStreamOptions,\n): AssistantMessageEventStream {\n\tconst provider = resolveApiProvider(model.api);\n\treturn provider.stream(model, context, withEnvApiKey(model, options) as StreamOptions);\n}\n\nexport async function complete<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: ProviderStreamOptions,\n): Promise<AssistantMessage> {\n\tconst s = stream(model, context, options);\n\treturn s.result();\n}\n\n/**\n * Brand identifying the built-in stream function.\n *\n * Callers decide whether provider credentials are mandatory by asking \"is this\n * still the built-in stream function?\", and a bare `fn === streamSimple`\n * answers that with reference identity. Reference identity is not dependable\n * here: this package can legitimately load more than once in one process (a\n * workspace symlink beside an installed copy, or two dependents resolving\n * different versions), producing two distinct `streamSimple` functions that\n * behave identically. The comparison then reports \"custom stream function\" for\n * what is really the built-in one, and a credential check silently relaxes.\n *\n * `Symbol.for` keys into the per-realm global registry, so every copy of this\n * module brands its own `streamSimple` with the same symbol and the check\n * survives duplication.\n */\nconst BUILTIN_STREAM_FN = Symbol.for(\"omk-ai.streamSimple\");\n\nexport function streamSimple<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream {\n\tconst provider = resolveApiProvider(model.api);\n\treturn provider.streamSimple(model, context, withEnvApiKey(model, options));\n}\n\nObject.defineProperty(streamSimple, BUILTIN_STREAM_FN, { value: true });\n\n/** True when `fn` is this package's built-in stream function, including a duplicate copy of it. */\nexport function isBuiltinStreamFn(fn: unknown): boolean {\n\tif (typeof fn !== \"function\") return false;\n\treturn Reflect.get(fn, BUILTIN_STREAM_FN) === true;\n}\n\nexport async function completeSimple<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): Promise<AssistantMessage> {\n\tconst s = streamSimple(model, context, options);\n\treturn s.result();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"stream.js","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"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","sourcesContent":["import \"./providers/register-builtins.ts\";\n\nimport { getApiProvider } from \"./api-registry.ts\";\nimport { getEnvApiKey } from \"./env-api-keys.ts\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tAssistantMessageEventStream,\n\tContext,\n\tModel,\n\tProviderStreamOptions,\n\tSimpleStreamOptions,\n\tStreamOptions,\n} from \"./types.ts\";\n\nexport { getEnvApiKey } from \"./env-api-keys.ts\";\n\nfunction hasExplicitApiKey(apiKey: string | undefined): apiKey is string {\n\treturn typeof apiKey === \"string\" && apiKey.trim().length > 0;\n}\n\nfunction withEnvApiKey<TOptions extends StreamOptions>(\n\tmodel: Model<Api>,\n\toptions: TOptions | undefined,\n): TOptions | undefined {\n\tif (hasExplicitApiKey(options?.apiKey)) return options;\n\tconst apiKey = getEnvApiKey(model.provider);\n\tif (!apiKey) return options;\n\treturn { ...options, apiKey } as TOptions;\n}\n\nfunction resolveApiProvider(api: Api) {\n\tconst provider = getApiProvider(api);\n\tif (!provider) {\n\t\tthrow new Error(`No API provider registered for api: ${api}`);\n\t}\n\treturn provider;\n}\n\nexport function stream<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: ProviderStreamOptions,\n): AssistantMessageEventStream {\n\tconst provider = resolveApiProvider(model.api);\n\treturn provider.stream(model, context, withEnvApiKey(model, options) as StreamOptions);\n}\n\nexport async function complete<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: ProviderStreamOptions,\n): Promise<AssistantMessage> {\n\tconst s = stream(model, context, options);\n\t// This API exposes only the final message; consume deltas as they arrive.\n\tfor await (const _event of s) {\n\t}\n\treturn s.result();\n}\n\n/**\n * Brand identifying the built-in stream function.\n *\n * Callers decide whether provider credentials are mandatory by asking \"is this\n * still the built-in stream function?\", and a bare `fn === streamSimple`\n * answers that with reference identity. Reference identity is not dependable\n * here: this package can legitimately load more than once in one process (a\n * workspace symlink beside an installed copy, or two dependents resolving\n * different versions), producing two distinct `streamSimple` functions that\n * behave identically. The comparison then reports \"custom stream function\" for\n * what is really the built-in one, and a credential check silently relaxes.\n *\n * `Symbol.for` keys into the per-realm global registry, so every copy of this\n * module brands its own `streamSimple` with the same symbol and the check\n * survives duplication.\n */\nconst BUILTIN_STREAM_FN = Symbol.for(\"omk-ai.streamSimple\");\n\nexport function streamSimple<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream {\n\tconst provider = resolveApiProvider(model.api);\n\treturn provider.streamSimple(model, context, withEnvApiKey(model, options));\n}\n\nObject.defineProperty(streamSimple, BUILTIN_STREAM_FN, { value: true });\n\n/** True when `fn` is this package's built-in stream function, including a duplicate copy of it. */\nexport function isBuiltinStreamFn(fn: unknown): boolean {\n\tif (typeof fn !== \"function\") return false;\n\treturn Reflect.get(fn, BUILTIN_STREAM_FN) === true;\n}\n\nexport async function completeSimple<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): Promise<AssistantMessage> {\n\tconst s = streamSimple(model, context, options);\n\t// Keep completion-only callers from retaining the full event backlog.\n\tfor await (const _event of s) {\n\t}\n\treturn s.result();\n}\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function validateProviderRetryOptions(maxRetries: number, maxRetryDelayMs: number | undefined): void;
|
|
2
|
+
/** Invalid server metadata falls back to bounded exponential jitter, not a 1ms timer. */
|
|
3
|
+
export declare function computeProviderRetryDelay(headers: Headers | undefined, retryIndex: number, maxRetryDelayMs: number | undefined, providerErrorMessage: string, nowMs: number, randomValue: number): number;
|
|
4
|
+
//# sourceMappingURL=provider-retry-delay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-retry-delay.d.ts","sourceRoot":"","sources":["../../src/utils/provider-retry-delay.ts"],"names":[],"mappings":"AAGA,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAO1G;AAQD,yFAAyF;AACzF,wBAAgB,yBAAyB,CACxC,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,oBAAoB,EAAE,MAAM,EAC5B,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GACjB,MAAM,CA+BR","sourcesContent":["const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;\nconst DECIMAL = /^(?:\\d+(?:\\.\\d*)?|\\.\\d+)$/;\n\nexport function validateProviderRetryOptions(maxRetries: number, maxRetryDelayMs: number | undefined): void {\n\tif (!Number.isSafeInteger(maxRetries) || maxRetries < 0) {\n\t\tthrow new RangeError(\"maxRetries must be a non-negative safe integer\");\n\t}\n\tif (maxRetryDelayMs !== undefined && (!Number.isFinite(maxRetryDelayMs) || maxRetryDelayMs < 0)) {\n\t\tthrow new RangeError(\"maxRetryDelayMs must be finite and non-negative; zero disables the server-delay cap\");\n\t}\n}\n\nfunction decimal(raw: string | null): number | undefined {\n\tif (raw === null || !DECIMAL.test(raw.trim())) return undefined;\n\tconst value = Number(raw.trim());\n\treturn Number.isFinite(value) && value >= 0 ? value : undefined;\n}\n\n/** Invalid server metadata falls back to bounded exponential jitter, not a 1ms timer. */\nexport function computeProviderRetryDelay(\n\theaders: Headers | undefined,\n\tretryIndex: number,\n\tmaxRetryDelayMs: number | undefined,\n\tproviderErrorMessage: string,\n\tnowMs: number,\n\trandomValue: number,\n): number {\n\tif (!Number.isSafeInteger(retryIndex) || retryIndex < 0) throw new RangeError(\"Invalid retryIndex\");\n\tif (!Number.isFinite(nowMs)) throw new RangeError(\"Invalid wall clock\");\n\tif (!Number.isFinite(randomValue) || randomValue < 0 || randomValue >= 1)\n\t\tthrow new RangeError(\"Invalid jitter draw\");\n\tvalidateProviderRetryOptions(0, maxRetryDelayMs);\n\tlet serverDelay = decimal(headers?.get(\"retry-after-ms\") ?? null);\n\tif (serverDelay === undefined) {\n\t\tconst raw = headers?.get(\"retry-after\")?.trim() ?? null;\n\t\tconst seconds = decimal(raw);\n\t\tif (seconds !== undefined) {\n\t\t\tconst milliseconds = seconds * 1000;\n\t\t\tif (Number.isFinite(milliseconds)) serverDelay = milliseconds;\n\t\t} else if (raw !== null && /^[A-Za-z]{3,9},?\\s/.test(raw)) {\n\t\t\t// Accept HTTP date forms; do not reinterpret a numeric suffix or negative\n\t\t\t// duration as a permissively parsed calendar date.\n\t\t\tconst date = Date.parse(raw);\n\t\t\tconst difference = date - nowMs;\n\t\t\tif (Number.isFinite(date) && Number.isFinite(difference)) serverDelay = Math.max(0, difference);\n\t\t}\n\t}\n\tif (serverDelay !== undefined) {\n\t\tconst cap = maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;\n\t\tif (cap > 0 && serverDelay > cap) {\n\t\t\tthrow new Error(\n\t\t\t\t`Server requested ${Math.ceil(serverDelay / 1000)}s retry delay (max: ${Math.ceil(cap / 1000)}s). ${providerErrorMessage}`,\n\t\t\t);\n\t\t}\n\t\treturn serverDelay;\n\t}\n\treturn Math.min(500 * 2 ** Math.min(retryIndex, 4), 8000) * (1 - randomValue * 0.25);\n}\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
|
|
2
|
+
const DECIMAL = /^(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
3
|
+
export function validateProviderRetryOptions(maxRetries, maxRetryDelayMs) {
|
|
4
|
+
if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) {
|
|
5
|
+
throw new RangeError("maxRetries must be a non-negative safe integer");
|
|
6
|
+
}
|
|
7
|
+
if (maxRetryDelayMs !== undefined && (!Number.isFinite(maxRetryDelayMs) || maxRetryDelayMs < 0)) {
|
|
8
|
+
throw new RangeError("maxRetryDelayMs must be finite and non-negative; zero disables the server-delay cap");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function decimal(raw) {
|
|
12
|
+
if (raw === null || !DECIMAL.test(raw.trim()))
|
|
13
|
+
return undefined;
|
|
14
|
+
const value = Number(raw.trim());
|
|
15
|
+
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Invalid server metadata falls back to bounded exponential jitter, not a 1ms timer. */
|
|
18
|
+
export function computeProviderRetryDelay(headers, retryIndex, maxRetryDelayMs, providerErrorMessage, nowMs, randomValue) {
|
|
19
|
+
if (!Number.isSafeInteger(retryIndex) || retryIndex < 0)
|
|
20
|
+
throw new RangeError("Invalid retryIndex");
|
|
21
|
+
if (!Number.isFinite(nowMs))
|
|
22
|
+
throw new RangeError("Invalid wall clock");
|
|
23
|
+
if (!Number.isFinite(randomValue) || randomValue < 0 || randomValue >= 1)
|
|
24
|
+
throw new RangeError("Invalid jitter draw");
|
|
25
|
+
validateProviderRetryOptions(0, maxRetryDelayMs);
|
|
26
|
+
let serverDelay = decimal(headers?.get("retry-after-ms") ?? null);
|
|
27
|
+
if (serverDelay === undefined) {
|
|
28
|
+
const raw = headers?.get("retry-after")?.trim() ?? null;
|
|
29
|
+
const seconds = decimal(raw);
|
|
30
|
+
if (seconds !== undefined) {
|
|
31
|
+
const milliseconds = seconds * 1000;
|
|
32
|
+
if (Number.isFinite(milliseconds))
|
|
33
|
+
serverDelay = milliseconds;
|
|
34
|
+
}
|
|
35
|
+
else if (raw !== null && /^[A-Za-z]{3,9},?\s/.test(raw)) {
|
|
36
|
+
// Accept HTTP date forms; do not reinterpret a numeric suffix or negative
|
|
37
|
+
// duration as a permissively parsed calendar date.
|
|
38
|
+
const date = Date.parse(raw);
|
|
39
|
+
const difference = date - nowMs;
|
|
40
|
+
if (Number.isFinite(date) && Number.isFinite(difference))
|
|
41
|
+
serverDelay = Math.max(0, difference);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (serverDelay !== undefined) {
|
|
45
|
+
const cap = maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
46
|
+
if (cap > 0 && serverDelay > cap) {
|
|
47
|
+
throw new Error(`Server requested ${Math.ceil(serverDelay / 1000)}s retry delay (max: ${Math.ceil(cap / 1000)}s). ${providerErrorMessage}`);
|
|
48
|
+
}
|
|
49
|
+
return serverDelay;
|
|
50
|
+
}
|
|
51
|
+
return Math.min(500 * 2 ** Math.min(retryIndex, 4), 8000) * (1 - randomValue * 0.25);
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=provider-retry-delay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-retry-delay.js","sourceRoot":"","sources":["../../src/utils/provider-retry-delay.ts"],"names":[],"mappings":"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","sourcesContent":["const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;\nconst DECIMAL = /^(?:\\d+(?:\\.\\d*)?|\\.\\d+)$/;\n\nexport function validateProviderRetryOptions(maxRetries: number, maxRetryDelayMs: number | undefined): void {\n\tif (!Number.isSafeInteger(maxRetries) || maxRetries < 0) {\n\t\tthrow new RangeError(\"maxRetries must be a non-negative safe integer\");\n\t}\n\tif (maxRetryDelayMs !== undefined && (!Number.isFinite(maxRetryDelayMs) || maxRetryDelayMs < 0)) {\n\t\tthrow new RangeError(\"maxRetryDelayMs must be finite and non-negative; zero disables the server-delay cap\");\n\t}\n}\n\nfunction decimal(raw: string | null): number | undefined {\n\tif (raw === null || !DECIMAL.test(raw.trim())) return undefined;\n\tconst value = Number(raw.trim());\n\treturn Number.isFinite(value) && value >= 0 ? value : undefined;\n}\n\n/** Invalid server metadata falls back to bounded exponential jitter, not a 1ms timer. */\nexport function computeProviderRetryDelay(\n\theaders: Headers | undefined,\n\tretryIndex: number,\n\tmaxRetryDelayMs: number | undefined,\n\tproviderErrorMessage: string,\n\tnowMs: number,\n\trandomValue: number,\n): number {\n\tif (!Number.isSafeInteger(retryIndex) || retryIndex < 0) throw new RangeError(\"Invalid retryIndex\");\n\tif (!Number.isFinite(nowMs)) throw new RangeError(\"Invalid wall clock\");\n\tif (!Number.isFinite(randomValue) || randomValue < 0 || randomValue >= 1)\n\t\tthrow new RangeError(\"Invalid jitter draw\");\n\tvalidateProviderRetryOptions(0, maxRetryDelayMs);\n\tlet serverDelay = decimal(headers?.get(\"retry-after-ms\") ?? null);\n\tif (serverDelay === undefined) {\n\t\tconst raw = headers?.get(\"retry-after\")?.trim() ?? null;\n\t\tconst seconds = decimal(raw);\n\t\tif (seconds !== undefined) {\n\t\t\tconst milliseconds = seconds * 1000;\n\t\t\tif (Number.isFinite(milliseconds)) serverDelay = milliseconds;\n\t\t} else if (raw !== null && /^[A-Za-z]{3,9},?\\s/.test(raw)) {\n\t\t\t// Accept HTTP date forms; do not reinterpret a numeric suffix or negative\n\t\t\t// duration as a permissively parsed calendar date.\n\t\t\tconst date = Date.parse(raw);\n\t\t\tconst difference = date - nowMs;\n\t\t\tif (Number.isFinite(date) && Number.isFinite(difference)) serverDelay = Math.max(0, difference);\n\t\t}\n\t}\n\tif (serverDelay !== undefined) {\n\t\tconst cap = maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;\n\t\tif (cap > 0 && serverDelay > cap) {\n\t\t\tthrow new Error(\n\t\t\t\t`Server requested ${Math.ceil(serverDelay / 1000)}s retry delay (max: ${Math.ceil(cap / 1000)}s). ${providerErrorMessage}`,\n\t\t\t);\n\t\t}\n\t\treturn serverDelay;\n\t}\n\treturn Math.min(500 * 2 ** Math.min(retryIndex, 4), 8000) * (1 - randomValue * 0.25);\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Largest portable Node timer delay; larger delays are re-armed in chunks. */
|
|
2
|
+
export declare const MAX_RETRY_TIMER_MS = 2147483647;
|
|
3
|
+
export declare function createProviderAbortError(): Error;
|
|
4
|
+
/** A long server delay must never overflow setTimeout into an immediate retry. */
|
|
5
|
+
export declare function sleepProviderRetry(ms: number, signal?: AbortSignal): Promise<void>;
|
|
6
|
+
//# sourceMappingURL=provider-retry-sleep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-retry-sleep.d.ts","sourceRoot":"","sources":["../../src/utils/provider-retry-sleep.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,eAAO,MAAM,kBAAkB,aAAgB,CAAC;AAEhD,wBAAgB,wBAAwB,IAAI,KAAK,CAIhD;AAED,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoClF","sourcesContent":["/** Largest portable Node timer delay; larger delays are re-armed in chunks. */\nexport const MAX_RETRY_TIMER_MS = 2_147_483_647;\n\nexport function createProviderAbortError(): Error {\n\tconst error = new Error(\"Request aborted\");\n\terror.name = \"AbortError\";\n\treturn error;\n}\n\n/** A long server delay must never overflow setTimeout into an immediate retry. */\nexport function sleepProviderRetry(ms: number, signal?: AbortSignal): Promise<void> {\n\tif (!Number.isFinite(ms) || ms < 0)\n\t\treturn Promise.reject(new RangeError(\"Retry sleep must be finite and non-negative\"));\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(createProviderAbortError());\n\t\t\treturn;\n\t\t}\n\t\tconst startedAt = performance.now();\n\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\tlet finished = false;\n\t\tconst cleanup = (): void => {\n\t\t\tif (timer !== undefined) clearTimeout(timer);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t};\n\t\tconst onAbort = (): void => {\n\t\t\tif (finished) return;\n\t\t\tfinished = true;\n\t\t\tcleanup();\n\t\t\treject(createProviderAbortError());\n\t\t};\n\t\tconst tick = (): void => {\n\t\t\tif (finished) return;\n\t\t\tconst remaining = ms - (performance.now() - startedAt);\n\t\t\tif (remaining <= 0) {\n\t\t\t\tfinished = true;\n\t\t\t\tcleanup();\n\t\t\t\tresolve();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttimer = setTimeout(tick, Math.min(MAX_RETRY_TIMER_MS, Math.max(1, Math.ceil(remaining))));\n\t\t};\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\tif (signal?.aborted) onAbort();\n\t\telse timer = setTimeout(tick, Math.min(MAX_RETRY_TIMER_MS, Math.max(0, Math.ceil(ms))));\n\t});\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Largest portable Node timer delay; larger delays are re-armed in chunks. */
|
|
2
|
+
export const MAX_RETRY_TIMER_MS = 2_147_483_647;
|
|
3
|
+
export function createProviderAbortError() {
|
|
4
|
+
const error = new Error("Request aborted");
|
|
5
|
+
error.name = "AbortError";
|
|
6
|
+
return error;
|
|
7
|
+
}
|
|
8
|
+
/** A long server delay must never overflow setTimeout into an immediate retry. */
|
|
9
|
+
export function sleepProviderRetry(ms, signal) {
|
|
10
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
11
|
+
return Promise.reject(new RangeError("Retry sleep must be finite and non-negative"));
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
if (signal?.aborted) {
|
|
14
|
+
reject(createProviderAbortError());
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const startedAt = performance.now();
|
|
18
|
+
let timer;
|
|
19
|
+
let finished = false;
|
|
20
|
+
const cleanup = () => {
|
|
21
|
+
if (timer !== undefined)
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
signal?.removeEventListener("abort", onAbort);
|
|
24
|
+
};
|
|
25
|
+
const onAbort = () => {
|
|
26
|
+
if (finished)
|
|
27
|
+
return;
|
|
28
|
+
finished = true;
|
|
29
|
+
cleanup();
|
|
30
|
+
reject(createProviderAbortError());
|
|
31
|
+
};
|
|
32
|
+
const tick = () => {
|
|
33
|
+
if (finished)
|
|
34
|
+
return;
|
|
35
|
+
const remaining = ms - (performance.now() - startedAt);
|
|
36
|
+
if (remaining <= 0) {
|
|
37
|
+
finished = true;
|
|
38
|
+
cleanup();
|
|
39
|
+
resolve();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
timer = setTimeout(tick, Math.min(MAX_RETRY_TIMER_MS, Math.max(1, Math.ceil(remaining))));
|
|
43
|
+
};
|
|
44
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
45
|
+
if (signal?.aborted)
|
|
46
|
+
onAbort();
|
|
47
|
+
else
|
|
48
|
+
timer = setTimeout(tick, Math.min(MAX_RETRY_TIMER_MS, Math.max(0, Math.ceil(ms))));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=provider-retry-sleep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-retry-sleep.js","sourceRoot":"","sources":["../../src/utils/provider-retry-sleep.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAEhD,MAAM,UAAU,wBAAwB,GAAU;IACjD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAC3C,KAAK,CAAC,IAAI,GAAG,YAAY,CAAC;IAC1B,OAAO,KAAK,CAAC;AAAA,CACb;AAED,kFAAkF;AAClF,MAAM,UAAU,kBAAkB,CAAC,EAAU,EAAE,MAAoB,EAAiB;IACnF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC;QACjC,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,6CAA6C,CAAC,CAAC,CAAC;IACtF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;QACvC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACrB,MAAM,CAAC,wBAAwB,EAAE,CAAC,CAAC;YACnC,OAAO;QACR,CAAC;QACD,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QACpC,IAAI,KAAgD,CAAC;QACrD,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,MAAM,OAAO,GAAG,GAAS,EAAE,CAAC;YAC3B,IAAI,KAAK,KAAK,SAAS;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;YAC7C,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAAA,CAC9C,CAAC;QACF,MAAM,OAAO,GAAG,GAAS,EAAE,CAAC;YAC3B,IAAI,QAAQ;gBAAE,OAAO;YACrB,QAAQ,GAAG,IAAI,CAAC;YAChB,OAAO,EAAE,CAAC;YACV,MAAM,CAAC,wBAAwB,EAAE,CAAC,CAAC;QAAA,CACnC,CAAC;QACF,MAAM,IAAI,GAAG,GAAS,EAAE,CAAC;YACxB,IAAI,QAAQ;gBAAE,OAAO;YACrB,MAAM,SAAS,GAAG,EAAE,GAAG,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,CAAC;YACvD,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;gBACpB,QAAQ,GAAG,IAAI,CAAC;gBAChB,OAAO,EAAE,CAAC;gBACV,OAAO,EAAE,CAAC;gBACV,OAAO;YACR,CAAC;YACD,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QAAA,CAC1F,CAAC;QACF,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3D,IAAI,MAAM,EAAE,OAAO;YAAE,OAAO,EAAE,CAAC;;YAC1B,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAAA,CACxF,CAAC,CAAC;AAAA,CACH","sourcesContent":["/** Largest portable Node timer delay; larger delays are re-armed in chunks. */\nexport const MAX_RETRY_TIMER_MS = 2_147_483_647;\n\nexport function createProviderAbortError(): Error {\n\tconst error = new Error(\"Request aborted\");\n\terror.name = \"AbortError\";\n\treturn error;\n}\n\n/** A long server delay must never overflow setTimeout into an immediate retry. */\nexport function sleepProviderRetry(ms: number, signal?: AbortSignal): Promise<void> {\n\tif (!Number.isFinite(ms) || ms < 0)\n\t\treturn Promise.reject(new RangeError(\"Retry sleep must be finite and non-negative\"));\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(createProviderAbortError());\n\t\t\treturn;\n\t\t}\n\t\tconst startedAt = performance.now();\n\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\tlet finished = false;\n\t\tconst cleanup = (): void => {\n\t\t\tif (timer !== undefined) clearTimeout(timer);\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t};\n\t\tconst onAbort = (): void => {\n\t\t\tif (finished) return;\n\t\t\tfinished = true;\n\t\t\tcleanup();\n\t\t\treject(createProviderAbortError());\n\t\t};\n\t\tconst tick = (): void => {\n\t\t\tif (finished) return;\n\t\t\tconst remaining = ms - (performance.now() - startedAt);\n\t\t\tif (remaining <= 0) {\n\t\t\t\tfinished = true;\n\t\t\t\tcleanup();\n\t\t\t\tresolve();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttimer = setTimeout(tick, Math.min(MAX_RETRY_TIMER_MS, Math.max(1, Math.ceil(remaining))));\n\t\t};\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\tif (signal?.aborted) onAbort();\n\t\telse timer = setTimeout(tick, Math.min(MAX_RETRY_TIMER_MS, Math.max(0, Math.ceil(ms))));\n\t});\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-retry.d.ts","sourceRoot":"","sources":["../../src/utils/provider-retry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"provider-retry.d.ts","sourceRoot":"","sources":["../../src/utils/provider-retry.ts"],"names":[],"mappings":"AAMA,UAAU,oBAAoB;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAyCD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAC3C,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,OAAO,GAAE,oBAAyB,GAChC,OAAO,CAAC,CAAC,CAAC,CAmBZ","sourcesContent":["import { computeProviderRetryDelay, validateProviderRetryOptions } from \"./provider-retry-delay.ts\";\nimport {\n\tsleepProviderRetry as abortableSleep,\n\tcreateProviderAbortError as createAbortError,\n} from \"./provider-retry-sleep.ts\";\n\ninterface ProviderRetryOptions {\n\tmaxRetries?: number;\n\tmaxRetryDelayMs?: number;\n\tsignal?: AbortSignal;\n}\n\ninterface ProviderError extends Error {\n\tstatus: number | undefined;\n\theaders: Headers | undefined;\n}\n\nfunction isProviderError(error: unknown): error is ProviderError {\n\tif (!(error instanceof Error) || !(\"status\" in error) || !(\"headers\" in error)) return false;\n\treturn (\n\t\t(error.status === undefined || typeof error.status === \"number\") &&\n\t\t(error.headers === undefined || error.headers instanceof Headers)\n\t);\n}\n\n/** Mirrors the pinned OpenAI/Anthropic SDK retry policy; review when either SDK is upgraded. */\nfunction isRetryableProviderError(error: ProviderError): boolean {\n\tconst shouldRetry = error.headers?.get(\"x-should-retry\");\n\tif (shouldRetry === \"true\") return true;\n\tif (shouldRetry === \"false\") return false;\n\n\tif (error.status === undefined) return true;\n\treturn (\n\t\terror.status === 408 ||\n\t\terror.status === 409 ||\n\t\terror.status === 429 ||\n\t\t(typeof error.status === \"number\" && error.status >= 500)\n\t);\n}\n\nfunction getRetryDelayMs(error: ProviderError, retryIndex: number, maxRetryDelayMs: number | undefined): number {\n\treturn computeProviderRetryDelay(\n\t\terror.headers,\n\t\tretryIndex,\n\t\tmaxRetryDelayMs,\n\t\terror.message,\n\t\tDate.now(),\n\t\tMath.random(),\n\t);\n}\n\n/**\n * Reproduce the retry behavior used by the OpenAI and Anthropic SDKs while making\n * their backoff sleep interruptible. Their built-in retry timers ignore the\n * request AbortSignal, so callers must invoke the SDK with `maxRetries: 0` and\n * wrap the request with this helper. Provider-requested delays above\n * `maxRetryDelayMs` fail immediately (60 seconds by default); set it to zero to\n * disable the limit.\n */\nexport async function retryProviderRequest<T>(\n\trequest: () => Promise<T>,\n\toptions: ProviderRetryOptions = {},\n): Promise<T> {\n\tconst maxRetries = options.maxRetries ?? 0;\n\tvalidateProviderRetryOptions(maxRetries, options.maxRetryDelayMs);\n\tlet retriesRemaining = maxRetries;\n\n\tfor (;;) {\n\t\tif (options.signal?.aborted) throw createAbortError();\n\t\ttry {\n\t\t\t// Each retry is a fresh SDK request, so X-Stainless-Retry-Count remains zero.\n\t\t\treturn await request();\n\t\t} catch (error) {\n\t\t\tif (options.signal?.aborted) throw createAbortError();\n\t\t\tif (retriesRemaining <= 0 || !isProviderError(error) || !isRetryableProviderError(error)) throw error;\n\n\t\t\tconst retryIndex = maxRetries - retriesRemaining;\n\t\t\tretriesRemaining--;\n\t\t\tawait abortableSleep(getRetryDelayMs(error, retryIndex, options.maxRetryDelayMs), options.signal);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { computeProviderRetryDelay, validateProviderRetryOptions } from "./provider-retry-delay.js";
|
|
2
|
+
import { sleepProviderRetry as abortableSleep, createProviderAbortError as createAbortError, } from "./provider-retry-sleep.js";
|
|
2
3
|
function isProviderError(error) {
|
|
3
4
|
if (!(error instanceof Error) || !("status" in error) || !("headers" in error))
|
|
4
5
|
return false;
|
|
@@ -19,50 +20,8 @@ function isRetryableProviderError(error) {
|
|
|
19
20
|
error.status === 429 ||
|
|
20
21
|
(typeof error.status === "number" && error.status >= 500));
|
|
21
22
|
}
|
|
22
|
-
function validateServerRetryDelayMs(delayMs, maxRetryDelayMs, providerErrorMessage) {
|
|
23
|
-
const maxDelayMs = maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
24
|
-
if (maxDelayMs > 0 && delayMs > maxDelayMs) {
|
|
25
|
-
throw new Error(`Server requested ${Math.ceil(delayMs / 1000)}s retry delay (max: ${Math.ceil(maxDelayMs / 1000)}s). ${providerErrorMessage}`);
|
|
26
|
-
}
|
|
27
|
-
return delayMs;
|
|
28
|
-
}
|
|
29
23
|
function getRetryDelayMs(error, retryIndex, maxRetryDelayMs) {
|
|
30
|
-
|
|
31
|
-
if (retryAfterMs) {
|
|
32
|
-
const value = Number.parseFloat(retryAfterMs);
|
|
33
|
-
if (!Number.isNaN(value))
|
|
34
|
-
return validateServerRetryDelayMs(value, maxRetryDelayMs, error.message);
|
|
35
|
-
}
|
|
36
|
-
const retryAfter = error.headers?.get("retry-after");
|
|
37
|
-
if (retryAfter) {
|
|
38
|
-
const seconds = Number.parseFloat(retryAfter);
|
|
39
|
-
const delayMs = Number.isNaN(seconds) ? Date.parse(retryAfter) - Date.now() : seconds * 1000;
|
|
40
|
-
return validateServerRetryDelayMs(delayMs, maxRetryDelayMs, error.message);
|
|
41
|
-
}
|
|
42
|
-
const exponentialDelay = Math.min(0.5 * 2 ** retryIndex, 8) * 1000;
|
|
43
|
-
return exponentialDelay * (1 - Math.random() * 0.25);
|
|
44
|
-
}
|
|
45
|
-
function createAbortError() {
|
|
46
|
-
const error = new Error("Request aborted");
|
|
47
|
-
error.name = "AbortError";
|
|
48
|
-
return error;
|
|
49
|
-
}
|
|
50
|
-
function abortableSleep(ms, signal) {
|
|
51
|
-
return new Promise((resolve, reject) => {
|
|
52
|
-
if (signal?.aborted) {
|
|
53
|
-
reject(createAbortError());
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const onAbort = () => {
|
|
57
|
-
clearTimeout(timeout);
|
|
58
|
-
reject(createAbortError());
|
|
59
|
-
};
|
|
60
|
-
const timeout = setTimeout(() => {
|
|
61
|
-
signal?.removeEventListener("abort", onAbort);
|
|
62
|
-
resolve();
|
|
63
|
-
}, Math.max(0, ms));
|
|
64
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
65
|
-
});
|
|
24
|
+
return computeProviderRetryDelay(error.headers, retryIndex, maxRetryDelayMs, error.message, Date.now(), Math.random());
|
|
66
25
|
}
|
|
67
26
|
/**
|
|
68
27
|
* Reproduce the retry behavior used by the OpenAI and Anthropic SDKs while making
|
|
@@ -74,8 +33,11 @@ function abortableSleep(ms, signal) {
|
|
|
74
33
|
*/
|
|
75
34
|
export async function retryProviderRequest(request, options = {}) {
|
|
76
35
|
const maxRetries = options.maxRetries ?? 0;
|
|
36
|
+
validateProviderRetryOptions(maxRetries, options.maxRetryDelayMs);
|
|
77
37
|
let retriesRemaining = maxRetries;
|
|
78
38
|
for (;;) {
|
|
39
|
+
if (options.signal?.aborted)
|
|
40
|
+
throw createAbortError();
|
|
79
41
|
try {
|
|
80
42
|
// Each retry is a fresh SDK request, so X-Stainless-Retry-Count remains zero.
|
|
81
43
|
return await request();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-retry.js","sourceRoot":"","sources":["../../src/utils/provider-retry.ts"],"names":[],"mappings":"AAAA,MAAM,
|
|
1
|
+
{"version":3,"file":"provider-retry.js","sourceRoot":"","sources":["../../src/utils/provider-retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AACpG,OAAO,EACN,kBAAkB,IAAI,cAAc,EACpC,wBAAwB,IAAI,gBAAgB,GAC5C,MAAM,2BAA2B,CAAC;AAanC,SAAS,eAAe,CAAC,KAAc,EAA0B;IAChE,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,SAAS,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7F,OAAO,CACN,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC;QAChE,CAAC,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,YAAY,OAAO,CAAC,CACjE,CAAC;AAAA,CACF;AAED,gGAAgG;AAChG,SAAS,wBAAwB,CAAC,KAAoB,EAAW;IAChE,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACzD,IAAI,WAAW,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACxC,IAAI,WAAW,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IAE1C,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,CACN,KAAK,CAAC,MAAM,KAAK,GAAG;QACpB,KAAK,CAAC,MAAM,KAAK,GAAG;QACpB,KAAK,CAAC,MAAM,KAAK,GAAG;QACpB,CAAC,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,CACzD,CAAC;AAAA,CACF;AAED,SAAS,eAAe,CAAC,KAAoB,EAAE,UAAkB,EAAE,eAAmC,EAAU;IAC/G,OAAO,yBAAyB,CAC/B,KAAK,CAAC,OAAO,EACb,UAAU,EACV,eAAe,EACf,KAAK,CAAC,OAAO,EACb,IAAI,CAAC,GAAG,EAAE,EACV,IAAI,CAAC,MAAM,EAAE,CACb,CAAC;AAAA,CACF;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACzC,OAAyB,EACzB,OAAO,GAAyB,EAAE,EACrB;IACb,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC,CAAC;IAC3C,4BAA4B,CAAC,UAAU,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAClE,IAAI,gBAAgB,GAAG,UAAU,CAAC;IAElC,SAAS,CAAC;QACT,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO;YAAE,MAAM,gBAAgB,EAAE,CAAC;QACtD,IAAI,CAAC;YACJ,8EAA8E;YAC9E,OAAO,MAAM,OAAO,EAAE,CAAC;QACxB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO;gBAAE,MAAM,gBAAgB,EAAE,CAAC;YACtD,IAAI,gBAAgB,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,wBAAwB,CAAC,KAAK,CAAC;gBAAE,MAAM,KAAK,CAAC;YAEtG,MAAM,UAAU,GAAG,UAAU,GAAG,gBAAgB,CAAC;YACjD,gBAAgB,EAAE,CAAC;YACnB,MAAM,cAAc,CAAC,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QACnG,CAAC;IACF,CAAC;AAAA,CACD","sourcesContent":["import { computeProviderRetryDelay, validateProviderRetryOptions } from \"./provider-retry-delay.ts\";\nimport {\n\tsleepProviderRetry as abortableSleep,\n\tcreateProviderAbortError as createAbortError,\n} from \"./provider-retry-sleep.ts\";\n\ninterface ProviderRetryOptions {\n\tmaxRetries?: number;\n\tmaxRetryDelayMs?: number;\n\tsignal?: AbortSignal;\n}\n\ninterface ProviderError extends Error {\n\tstatus: number | undefined;\n\theaders: Headers | undefined;\n}\n\nfunction isProviderError(error: unknown): error is ProviderError {\n\tif (!(error instanceof Error) || !(\"status\" in error) || !(\"headers\" in error)) return false;\n\treturn (\n\t\t(error.status === undefined || typeof error.status === \"number\") &&\n\t\t(error.headers === undefined || error.headers instanceof Headers)\n\t);\n}\n\n/** Mirrors the pinned OpenAI/Anthropic SDK retry policy; review when either SDK is upgraded. */\nfunction isRetryableProviderError(error: ProviderError): boolean {\n\tconst shouldRetry = error.headers?.get(\"x-should-retry\");\n\tif (shouldRetry === \"true\") return true;\n\tif (shouldRetry === \"false\") return false;\n\n\tif (error.status === undefined) return true;\n\treturn (\n\t\terror.status === 408 ||\n\t\terror.status === 409 ||\n\t\terror.status === 429 ||\n\t\t(typeof error.status === \"number\" && error.status >= 500)\n\t);\n}\n\nfunction getRetryDelayMs(error: ProviderError, retryIndex: number, maxRetryDelayMs: number | undefined): number {\n\treturn computeProviderRetryDelay(\n\t\terror.headers,\n\t\tretryIndex,\n\t\tmaxRetryDelayMs,\n\t\terror.message,\n\t\tDate.now(),\n\t\tMath.random(),\n\t);\n}\n\n/**\n * Reproduce the retry behavior used by the OpenAI and Anthropic SDKs while making\n * their backoff sleep interruptible. Their built-in retry timers ignore the\n * request AbortSignal, so callers must invoke the SDK with `maxRetries: 0` and\n * wrap the request with this helper. Provider-requested delays above\n * `maxRetryDelayMs` fail immediately (60 seconds by default); set it to zero to\n * disable the limit.\n */\nexport async function retryProviderRequest<T>(\n\trequest: () => Promise<T>,\n\toptions: ProviderRetryOptions = {},\n): Promise<T> {\n\tconst maxRetries = options.maxRetries ?? 0;\n\tvalidateProviderRetryOptions(maxRetries, options.maxRetryDelayMs);\n\tlet retriesRemaining = maxRetries;\n\n\tfor (;;) {\n\t\tif (options.signal?.aborted) throw createAbortError();\n\t\ttry {\n\t\t\t// Each retry is a fresh SDK request, so X-Stainless-Retry-Count remains zero.\n\t\t\treturn await request();\n\t\t} catch (error) {\n\t\t\tif (options.signal?.aborted) throw createAbortError();\n\t\t\tif (retriesRemaining <= 0 || !isProviderError(error) || !isRetryableProviderError(error)) throw error;\n\n\t\t\tconst retryIndex = maxRetries - retriesRemaining;\n\t\t\tretriesRemaining--;\n\t\t\tawait abortableSleep(getRetryDelayMs(error, retryIndex, options.maxRetryDelayMs), options.signal);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backoff before 1-indexed retry `attempt`: `baseDelayMs * 2^(attempt-1)`, capped at the longest
|
|
3
|
+
* delay a Node timer holds, because a longer timer fires after 1 ms. The base converts as the old
|
|
4
|
+
* arithmetic did; `+Infinity` takes the cap, and a base that converts to NaN or a negative number
|
|
5
|
+
* uses the 2 s default. A NaN attempt yields the cap rather than an immediate retry.
|
|
6
|
+
*/
|
|
7
|
+
export declare function retryBackoffDelayMs(baseDelayMs: number, attempt: number): number;
|
|
8
|
+
//# sourceMappingURL=retry-backoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-backoff.d.ts","sourceRoot":"","sources":["../../src/utils/retry-backoff.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOhF","sourcesContent":["import { MAX_RETRY_TIMER_MS } from \"./provider-retry-sleep.ts\";\n\n/** The coding-agent `retry.baseDelayMs` default, which {@link RetryPolicy} mirrors. */\nconst DEFAULT_RETRY_BASE_DELAY_MS = 2000;\n\n/**\n * Backoff before 1-indexed retry `attempt`: `baseDelayMs * 2^(attempt-1)`, capped at the longest\n * delay a Node timer holds, because a longer timer fires after 1 ms. The base converts as the old\n * arithmetic did; `+Infinity` takes the cap, and a base that converts to NaN or a negative number\n * uses the 2 s default. A NaN attempt yields the cap rather than an immediate retry.\n */\nexport function retryBackoffDelayMs(baseDelayMs: number, attempt: number): number {\n\tconst requested = Number(baseDelayMs);\n\t// `>= 0` is false for NaN; +Infinity passes and takes the cap.\n\tconst base = requested >= 0 ? Math.min(requested, MAX_RETRY_TIMER_MS) : DEFAULT_RETRY_BASE_DELAY_MS;\n\tif (base === 0) return 0;\n\tconst delay = base * 2 ** (attempt - 1);\n\treturn delay <= MAX_RETRY_TIMER_MS ? delay : MAX_RETRY_TIMER_MS;\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MAX_RETRY_TIMER_MS } from "./provider-retry-sleep.js";
|
|
2
|
+
/** The coding-agent `retry.baseDelayMs` default, which {@link RetryPolicy} mirrors. */
|
|
3
|
+
const DEFAULT_RETRY_BASE_DELAY_MS = 2000;
|
|
4
|
+
/**
|
|
5
|
+
* Backoff before 1-indexed retry `attempt`: `baseDelayMs * 2^(attempt-1)`, capped at the longest
|
|
6
|
+
* delay a Node timer holds, because a longer timer fires after 1 ms. The base converts as the old
|
|
7
|
+
* arithmetic did; `+Infinity` takes the cap, and a base that converts to NaN or a negative number
|
|
8
|
+
* uses the 2 s default. A NaN attempt yields the cap rather than an immediate retry.
|
|
9
|
+
*/
|
|
10
|
+
export function retryBackoffDelayMs(baseDelayMs, attempt) {
|
|
11
|
+
const requested = Number(baseDelayMs);
|
|
12
|
+
// `>= 0` is false for NaN; +Infinity passes and takes the cap.
|
|
13
|
+
const base = requested >= 0 ? Math.min(requested, MAX_RETRY_TIMER_MS) : DEFAULT_RETRY_BASE_DELAY_MS;
|
|
14
|
+
if (base === 0)
|
|
15
|
+
return 0;
|
|
16
|
+
const delay = base * 2 ** (attempt - 1);
|
|
17
|
+
return delay <= MAX_RETRY_TIMER_MS ? delay : MAX_RETRY_TIMER_MS;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=retry-backoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry-backoff.js","sourceRoot":"","sources":["../../src/utils/retry-backoff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D,uFAAuF;AACvF,MAAM,2BAA2B,GAAG,IAAI,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,WAAmB,EAAE,OAAe,EAAU;IACjF,MAAM,SAAS,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IACtC,+DAA+D;IAC/D,MAAM,IAAI,GAAG,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC,2BAA2B,CAAC;IACpG,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;IACxC,OAAO,KAAK,IAAI,kBAAkB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,kBAAkB,CAAC;AAAA,CAChE","sourcesContent":["import { MAX_RETRY_TIMER_MS } from \"./provider-retry-sleep.ts\";\n\n/** The coding-agent `retry.baseDelayMs` default, which {@link RetryPolicy} mirrors. */\nconst DEFAULT_RETRY_BASE_DELAY_MS = 2000;\n\n/**\n * Backoff before 1-indexed retry `attempt`: `baseDelayMs * 2^(attempt-1)`, capped at the longest\n * delay a Node timer holds, because a longer timer fires after 1 ms. The base converts as the old\n * arithmetic did; `+Infinity` takes the cap, and a base that converts to NaN or a negative number\n * uses the 2 s default. A NaN attempt yields the cap rather than an immediate retry.\n */\nexport function retryBackoffDelayMs(baseDelayMs: number, attempt: number): number {\n\tconst requested = Number(baseDelayMs);\n\t// `>= 0` is false for NaN; +Infinity passes and takes the cap.\n\tconst base = requested >= 0 ? Math.min(requested, MAX_RETRY_TIMER_MS) : DEFAULT_RETRY_BASE_DELAY_MS;\n\tif (base === 0) return 0;\n\tconst delay = base * 2 ** (attempt - 1);\n\treturn delay <= MAX_RETRY_TIMER_MS ? delay : MAX_RETRY_TIMER_MS;\n}\n"]}
|
package/dist/utils/retry.d.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import type { AssistantMessage } from "../types.ts";
|
|
2
2
|
/**
|
|
3
|
-
* Retry policy: bounded attempts with exponential backoff (`baseDelayMs * 2^(attempt-1)
|
|
3
|
+
* Retry policy: bounded attempts with exponential backoff (`baseDelayMs * 2^(attempt-1)`, at most
|
|
4
|
+
* 2,147,483,647 ms).
|
|
4
5
|
* Matches `settings.retry` (`enabled`, `maxRetries`, `baseDelayMs`) in coding-agent; kept
|
|
5
6
|
* here so the classifier and the policy-driven retry loop live together and stay reusable
|
|
6
7
|
* by the SDK and other callers.
|
|
7
8
|
*/
|
|
8
9
|
export interface RetryPolicy {
|
|
9
10
|
enabled: boolean;
|
|
10
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Max retry attempts (0 = no retries). The initial call never counts as a retry. A count that
|
|
13
|
+
* converts to NaN means no retries.
|
|
14
|
+
*/
|
|
11
15
|
maxRetries: number;
|
|
12
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Base delay in ms. Per-attempt delay is `baseDelayMs * 2^(attempt-1)`, capped at 2,147,483,647 ms;
|
|
18
|
+
* no jitter. A base that converts to NaN or a negative number, an omitted one included, uses 2 s;
|
|
19
|
+
* `+Infinity` takes the cap.
|
|
20
|
+
*/
|
|
13
21
|
baseDelayMs: number;
|
|
14
22
|
}
|
|
15
23
|
/** Optional callbacks emitted by {@link retryAssistantCall} around each retry. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA6FpD;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,qFAAqF;IACrF,UAAU,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC9B,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,CAClB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,mFAAmF;IACnF,mBAAmB,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,mFAAmF;IACnF,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnG;AA0BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,EACxC,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,SAAS,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,gBAAgB,CAAC,CA4C3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAK5E","sourcesContent":["import type { AssistantMessage } from \"../types.ts\";\n\nfunction buildProviderErrorPattern(patterns: readonly string[]): RegExp {\n\treturn new RegExp(patterns.join(\"|\"), \"i\");\n}\n\nconst NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([\n\t// OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's\n\t// Zen API. These are subscription/account limits, not transient throttles.\n\t\"GoUsageLimitError\",\n\t\"FreeUsageLimitError\",\n\n\t// OpenCode Go subscription-limit text asks users to enable available-balance\n\t// usage after rolling/weekly/monthly limits are reached.\n\t\"Monthly usage limit reached\",\n\t\"available balance\",\n\n\t// Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's\n\t// quota/billing error code; the other strings cover common gateway wording.\n\t\"insufficient_quota\",\n\t\"insufficient balance\",\n\t\"out of budget\",\n\t\"quota exceeded\",\n\t// Codex token-plan wording: \"Your token-plan 1-week quota has been exhausted\".\n\t\"quota\\\\s+(?:has been\\\\s+|was\\\\s+|is\\\\s+)?exhaust\",\n\t\"billing\",\n]);\n\nconst RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([\n\t// Generic provider load, HTTP status, and server-side transient failures.\n\t\"overloaded\",\n\t\"rate.?limit\",\n\t\"too many requests\",\n\t\"429\",\n\t\"500\",\n\t\"502\",\n\t\"503\",\n\t\"504\",\n\t\"524\",\n\t\"service.?unavailable\",\n\t\"server.?error\",\n\t\"internal.?error\",\n\n\t// Wrapper/provider text for transient upstream failures, including OpenRouter\n\t// \"Provider returned error\" responses (#2264).\n\t\"provider.?returned.?error\",\n\n\t// Network, proxy, and fetch transport failures. This includes OpenAI Codex\n\t// raw-fetch failures such as \"upstream connect\", \"connection refused\", and\n\t// \"reset before headers\" (#733), plus OpenRouter connection drops (#3317).\n\t\"network.?error\",\n\t\"connection.?error\",\n\t\"connection.?refused\",\n\t\"connection.?lost\",\n\t\"other side closed\",\n\t\"fetch failed\",\n\t\"getaddrinfo\",\n\t\"ENOTFOUND\",\n\t\"EAI_AGAIN\",\n\t\"upstream.?connect\",\n\t\"reset before headers\",\n\t\"socket hang up\",\n\t\"socket connection was closed\",\n\t\"timed? out\",\n\t\"timeout\",\n\t\"terminated\",\n\n\t// WebSocket transports can report close/error text instead of HTTP/fetch text.\n\t\"websocket.?closed\",\n\t\"websocket.?error\",\n\n\t// Premature stream endings from SDKs and transports. Anthropic can throw\n\t// \"stream ended without ...\" and \"Anthropic stream ended before message_stop\"\n\t// (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594).\n\t\"ended without\",\n\t\"stream ended before message_stop\",\n\t\"stream ended before a terminal response event\",\n\t\"http2 request did not get a response\",\n\n\t// Provider-requested retry delay cap failures should flow through the outer\n\t// retry policy so callers can surface/abort the backoff (#1123).\n\t\"retry delay\",\n\n\t// Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock\n\t// stream exceptions (#6019).\n\t\"you can retry your request\",\n\t\"try your request again\",\n\t\"please retry your request\",\n\n\t// gRPC based providers (e.g. NVIDIA NIM)\n\t\"ResourceExhausted\",\n]);\n\n/**\n * Retry policy: bounded attempts with exponential backoff (`baseDelayMs * 2^(attempt-1)`).\n * Matches `settings.retry` (`enabled`, `maxRetries`, `baseDelayMs`) in coding-agent; kept\n * here so the classifier and the policy-driven retry loop live together and stay reusable\n * by the SDK and other callers.\n */\nexport interface RetryPolicy {\n\tenabled: boolean;\n\t/** Max retry attempts (0 = no retries). The initial call never counts as a retry. */\n\tmaxRetries: number;\n\t/** Base delay in ms. Per-attempt delay is `baseDelayMs * 2^(attempt-1)` before jitter. */\n\tbaseDelayMs: number;\n}\n\n/** Optional callbacks emitted by {@link retryAssistantCall} around each retry. */\nexport interface RetryCallbacks {\n\t/** Emitted before the backoff sleep of each retry attempt (1-indexed). */\n\tonRetryScheduled?: (\n\t\tattempt: number,\n\t\tmaxAttempts: number,\n\t\tdelayMs: number,\n\t\terrorMessage: string,\n\t) => void | Promise<void>;\n\t/** Emitted after the backoff sleep, immediately before the retried call starts. */\n\tonRetryAttemptStart?: () => void | Promise<void>;\n\t/** Emitted once when the loop ends: success if a later call completed normally. */\n\tonRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void | Promise<void>;\n}\n\nclass RetrySleepAbortError extends Error {\n\tconstructor() {\n\t\tsuper(\"Aborted\");\n\t}\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new RetrySleepAbortError());\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tsignal?.addEventListener(\n\t\t\t\"abort\",\n\t\t\t() => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\treject(new RetrySleepAbortError());\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t});\n}\n\n/**\n * Run a single assistant-producing call with bounded retry on transient errors.\n *\n * Behavior:\n * - A successful response is returned immediately. Aborts are terminal and never\n * retried, but reported as unsuccessful if they happen after a retry was scheduled.\n * Aborts during the backoff sleep are normalized to an aborted `AssistantMessage`\n * too, so callers do not need to care when cancellation happened.\n * - A non-retryable error (per {@link isRetryableAssistantError}, including quota/\n * billing exhaustion) is returned immediately so deterministic errors fail fast.\n * - Otherwise retries up to `maxRetries` times with exponential backoff, emitting\n * `onRetryScheduled` before each sleep, `onRetryAttemptStart` after each sleep before\n * the retried call starts, and `onRetryFinished` once at the end (whether the loop\n * ends in success, exhausted retries, or an aborted backoff).\n *\n * When `policy` is undefined or disabled, the first response is returned unchanged\n * (equivalent to calling `produce()` directly).\n */\nexport async function retryAssistantCall(\n\tproduce: () => Promise<AssistantMessage>,\n\tpolicy: RetryPolicy | undefined,\n\tsignal: AbortSignal | undefined,\n\tcallbacks?: RetryCallbacks,\n): Promise<AssistantMessage> {\n\tconst maxAttempts = policy?.enabled ? policy.maxRetries : 0;\n\n\tlet attempt = 0;\n\tlet lastRetry: { attempt: number; errorMessage: string } | undefined;\n\tfor (;;) {\n\t\tconst response = await produce();\n\n\t\t// Abort: terminal but not successful. Never retry an aborted message.\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Success: non-error, non-abort responses return as-is.\n\t\tif (response.stopReason !== \"error\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(true, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Non-retryable, or budget exhausted: return the final error message.\n\t\tif (attempt >= maxAttempts || !isRetryableAssistantError(response)) {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt, response.errorMessage);\n\t\t\treturn response;\n\t\t}\n\n\t\tattempt++;\n\t\tlastRetry = { attempt, errorMessage: response.errorMessage || \"Unknown error\" };\n\t\tconst delayMs = policy!.baseDelayMs * 2 ** (attempt - 1);\n\t\tawait callbacks?.onRetryScheduled?.(attempt, maxAttempts, delayMs, lastRetry.errorMessage);\n\n\t\t// Normalize aborts during retry backoff to the same AssistantMessage shape as\n\t\t// provider stream aborts, so callers do not need to care when cancellation happened.\n\t\ttry {\n\t\t\tawait sleep(delayMs, signal);\n\t\t} catch (error) {\n\t\t\tawait callbacks?.onRetryFinished?.(false, attempt, lastRetry.errorMessage);\n\t\t\tif (error instanceof RetrySleepAbortError) {\n\t\t\t\treturn { ...response, stopReason: \"aborted\", errorMessage: undefined };\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tawait callbacks?.onRetryAttemptStart?.();\n\t}\n}\n\n/**\n * Classifies whether a failed assistant message looks like a transient provider\n * or transport error, so callers can decide if the last assistant turn should be\n * restarted.\n *\n * This does not implement retry policy. Callers should first handle context\n * overflow separately, then apply their own retry budget, backoff, and reporting\n * before restarting the assistant turn.\n */\nexport function isRetryableAssistantError(message: AssistantMessage): boolean {\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\tconst errorMessage = message.errorMessage;\n\tif (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;\n\treturn RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA8FpD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC9B,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,CAClB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,mFAAmF;IACnF,mBAAmB,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,mFAAmF;IACnF,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnG;AA4BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,EACxC,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,SAAS,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,gBAAgB,CAAC,CA6C3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAK5E","sourcesContent":["import type { AssistantMessage } from \"../types.ts\";\nimport { retryBackoffDelayMs } from \"./retry-backoff.ts\";\n\nfunction buildProviderErrorPattern(patterns: readonly string[]): RegExp {\n\treturn new RegExp(patterns.join(\"|\"), \"i\");\n}\n\nconst NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([\n\t// OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's\n\t// Zen API. These are subscription/account limits, not transient throttles.\n\t\"GoUsageLimitError\",\n\t\"FreeUsageLimitError\",\n\n\t// OpenCode Go subscription-limit text asks users to enable available-balance\n\t// usage after rolling/weekly/monthly limits are reached.\n\t\"Monthly usage limit reached\",\n\t\"available balance\",\n\n\t// Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's\n\t// quota/billing error code; the other strings cover common gateway wording.\n\t\"insufficient_quota\",\n\t\"insufficient balance\",\n\t\"out of budget\",\n\t\"quota exceeded\",\n\t// Codex token-plan wording: \"Your token-plan 1-week quota has been exhausted\".\n\t\"quota\\\\s+(?:has been\\\\s+|was\\\\s+|is\\\\s+)?exhaust\",\n\t\"billing\",\n]);\n\nconst RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([\n\t// Generic provider load, HTTP status, and server-side transient failures.\n\t\"overloaded\",\n\t\"rate.?limit\",\n\t\"too many requests\",\n\t\"429\",\n\t\"500\",\n\t\"502\",\n\t\"503\",\n\t\"504\",\n\t\"524\",\n\t\"service.?unavailable\",\n\t\"server.?error\",\n\t\"internal.?error\",\n\n\t// Wrapper/provider text for transient upstream failures, including OpenRouter\n\t// \"Provider returned error\" responses (#2264).\n\t\"provider.?returned.?error\",\n\n\t// Network, proxy, and fetch transport failures. This includes OpenAI Codex\n\t// raw-fetch failures such as \"upstream connect\", \"connection refused\", and\n\t// \"reset before headers\" (#733), plus OpenRouter connection drops (#3317).\n\t\"network.?error\",\n\t\"connection.?error\",\n\t\"connection.?refused\",\n\t\"connection.?lost\",\n\t\"other side closed\",\n\t\"fetch failed\",\n\t\"getaddrinfo\",\n\t\"ENOTFOUND\",\n\t\"EAI_AGAIN\",\n\t\"upstream.?connect\",\n\t\"reset before headers\",\n\t\"socket hang up\",\n\t\"socket connection was closed\",\n\t\"timed? out\",\n\t\"timeout\",\n\t\"terminated\",\n\n\t// WebSocket transports can report close/error text instead of HTTP/fetch text.\n\t\"websocket.?closed\",\n\t\"websocket.?error\",\n\n\t// Premature stream endings from SDKs and transports. Anthropic can throw\n\t// \"stream ended without ...\" and \"Anthropic stream ended before message_stop\"\n\t// (#4433); Bedrock/Smithy can throw an HTTP/2 no-response error (#3594).\n\t\"ended without\",\n\t\"stream ended before message_stop\",\n\t\"stream ended before a terminal response event\",\n\t\"http2 request did not get a response\",\n\n\t// Provider-requested retry delay cap failures should flow through the outer\n\t// retry policy so callers can surface/abort the backoff (#1123).\n\t\"retry delay\",\n\n\t// Explicit retry guidance emitted mid-stream by OpenAI Responses and Bedrock\n\t// stream exceptions (#6019).\n\t\"you can retry your request\",\n\t\"try your request again\",\n\t\"please retry your request\",\n\n\t// gRPC based providers (e.g. NVIDIA NIM)\n\t\"ResourceExhausted\",\n]);\n\n/**\n * Retry policy: bounded attempts with exponential backoff (`baseDelayMs * 2^(attempt-1)`, at most\n * 2,147,483,647 ms).\n * Matches `settings.retry` (`enabled`, `maxRetries`, `baseDelayMs`) in coding-agent; kept\n * here so the classifier and the policy-driven retry loop live together and stay reusable\n * by the SDK and other callers.\n */\nexport interface RetryPolicy {\n\tenabled: boolean;\n\t/**\n\t * Max retry attempts (0 = no retries). The initial call never counts as a retry. A count that\n\t * converts to NaN means no retries.\n\t */\n\tmaxRetries: number;\n\t/**\n\t * Base delay in ms. Per-attempt delay is `baseDelayMs * 2^(attempt-1)`, capped at 2,147,483,647 ms;\n\t * no jitter. A base that converts to NaN or a negative number, an omitted one included, uses 2 s;\n\t * `+Infinity` takes the cap.\n\t */\n\tbaseDelayMs: number;\n}\n\n/** Optional callbacks emitted by {@link retryAssistantCall} around each retry. */\nexport interface RetryCallbacks {\n\t/** Emitted before the backoff sleep of each retry attempt (1-indexed). */\n\tonRetryScheduled?: (\n\t\tattempt: number,\n\t\tmaxAttempts: number,\n\t\tdelayMs: number,\n\t\terrorMessage: string,\n\t) => void | Promise<void>;\n\t/** Emitted after the backoff sleep, immediately before the retried call starts. */\n\tonRetryAttemptStart?: () => void | Promise<void>;\n\t/** Emitted once when the loop ends: success if a later call completed normally. */\n\tonRetryFinished?: (success: boolean, attempt: number, finalError?: string) => void | Promise<void>;\n}\n\nclass RetrySleepAbortError extends Error {\n\tconstructor() {\n\t\tsuper(\"Aborted\");\n\t}\n}\n\n/** One timer suffices: {@link retryBackoffDelayMs} never exceeds the longest delay a timer holds. */\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new RetrySleepAbortError());\n\t\t\treturn;\n\t\t}\n\t\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\t\tconst onAbort = (): void => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new RetrySleepAbortError());\n\t\t};\n\t\ttimeout = setTimeout(() => {\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\n/**\n * Run a single assistant-producing call with bounded retry on transient errors.\n *\n * Behavior:\n * - A successful response is returned immediately. Aborts are terminal and never\n * retried, but reported as unsuccessful if they happen after a retry was scheduled.\n * Aborts during the backoff sleep are normalized to an aborted `AssistantMessage`\n * too, so callers do not need to care when cancellation happened.\n * - A non-retryable error (per {@link isRetryableAssistantError}, including quota/\n * billing exhaustion) is returned immediately so deterministic errors fail fast.\n * - Otherwise retries up to `maxRetries` times with exponential backoff, emitting\n * `onRetryScheduled` before each sleep, `onRetryAttemptStart` after each sleep before\n * the retried call starts, and `onRetryFinished` once at the end (whether the loop\n * ends in success, exhausted retries, or an aborted backoff).\n *\n * When `policy` is undefined or disabled, the first response is returned unchanged\n * (equivalent to calling `produce()` directly).\n */\nexport async function retryAssistantCall(\n\tproduce: () => Promise<AssistantMessage>,\n\tpolicy: RetryPolicy | undefined,\n\tsignal: AbortSignal | undefined,\n\tcallbacks?: RetryCallbacks,\n): Promise<AssistantMessage> {\n\t// `> 0` also rejects a count that converts to NaN, which `attempt >= maxAttempts` never exhausts.\n\tconst maxAttempts = policy?.enabled && policy.maxRetries > 0 ? policy.maxRetries : 0;\n\n\tlet attempt = 0;\n\tlet lastRetry: { attempt: number; errorMessage: string } | undefined;\n\tfor (;;) {\n\t\tconst response = await produce();\n\n\t\t// Abort: terminal but not successful. Never retry an aborted message.\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Success: non-error, non-abort responses return as-is.\n\t\tif (response.stopReason !== \"error\") {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(true, lastRetry.attempt);\n\t\t\treturn response;\n\t\t}\n\n\t\t// Non-retryable, or budget exhausted: return the final error message.\n\t\tif (attempt >= maxAttempts || !isRetryableAssistantError(response)) {\n\t\t\tif (lastRetry) await callbacks?.onRetryFinished?.(false, lastRetry.attempt, response.errorMessage);\n\t\t\treturn response;\n\t\t}\n\n\t\tattempt++;\n\t\tlastRetry = { attempt, errorMessage: response.errorMessage || \"Unknown error\" };\n\t\tconst delayMs = retryBackoffDelayMs(policy!.baseDelayMs, attempt);\n\t\tawait callbacks?.onRetryScheduled?.(attempt, maxAttempts, delayMs, lastRetry.errorMessage);\n\n\t\t// Normalize aborts during retry backoff to the same AssistantMessage shape as\n\t\t// provider stream aborts, so callers do not need to care when cancellation happened.\n\t\ttry {\n\t\t\tawait sleep(delayMs, signal);\n\t\t} catch (error) {\n\t\t\tawait callbacks?.onRetryFinished?.(false, attempt, lastRetry.errorMessage);\n\t\t\tif (error instanceof RetrySleepAbortError) {\n\t\t\t\treturn { ...response, stopReason: \"aborted\", errorMessage: undefined };\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tawait callbacks?.onRetryAttemptStart?.();\n\t}\n}\n\n/**\n * Classifies whether a failed assistant message looks like a transient provider\n * or transport error, so callers can decide if the last assistant turn should be\n * restarted.\n *\n * This does not implement retry policy. Callers should first handle context\n * overflow separately, then apply their own retry budget, backoff, and reporting\n * before restarting the assistant turn.\n */\nexport function isRetryableAssistantError(message: AssistantMessage): boolean {\n\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\tconst errorMessage = message.errorMessage;\n\tif (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false;\n\treturn RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage);\n}\n"]}
|
package/dist/utils/retry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { retryBackoffDelayMs } from "./retry-backoff.js";
|
|
1
2
|
function buildProviderErrorPattern(patterns) {
|
|
2
3
|
return new RegExp(patterns.join("|"), "i");
|
|
3
4
|
}
|
|
@@ -82,17 +83,23 @@ class RetrySleepAbortError extends Error {
|
|
|
82
83
|
super("Aborted");
|
|
83
84
|
}
|
|
84
85
|
}
|
|
86
|
+
/** One timer suffices: {@link retryBackoffDelayMs} never exceeds the longest delay a timer holds. */
|
|
85
87
|
function sleep(ms, signal) {
|
|
86
88
|
return new Promise((resolve, reject) => {
|
|
87
89
|
if (signal?.aborted) {
|
|
88
90
|
reject(new RetrySleepAbortError());
|
|
89
91
|
return;
|
|
90
92
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
let timeout;
|
|
94
|
+
const onAbort = () => {
|
|
93
95
|
clearTimeout(timeout);
|
|
94
96
|
reject(new RetrySleepAbortError());
|
|
95
|
-
}
|
|
97
|
+
};
|
|
98
|
+
timeout = setTimeout(() => {
|
|
99
|
+
signal?.removeEventListener("abort", onAbort);
|
|
100
|
+
resolve();
|
|
101
|
+
}, ms);
|
|
102
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
96
103
|
});
|
|
97
104
|
}
|
|
98
105
|
/**
|
|
@@ -114,7 +121,8 @@ function sleep(ms, signal) {
|
|
|
114
121
|
* (equivalent to calling `produce()` directly).
|
|
115
122
|
*/
|
|
116
123
|
export async function retryAssistantCall(produce, policy, signal, callbacks) {
|
|
117
|
-
|
|
124
|
+
// `> 0` also rejects a count that converts to NaN, which `attempt >= maxAttempts` never exhausts.
|
|
125
|
+
const maxAttempts = policy?.enabled && policy.maxRetries > 0 ? policy.maxRetries : 0;
|
|
118
126
|
let attempt = 0;
|
|
119
127
|
let lastRetry;
|
|
120
128
|
for (;;) {
|
|
@@ -139,7 +147,7 @@ export async function retryAssistantCall(produce, policy, signal, callbacks) {
|
|
|
139
147
|
}
|
|
140
148
|
attempt++;
|
|
141
149
|
lastRetry = { attempt, errorMessage: response.errorMessage || "Unknown error" };
|
|
142
|
-
const delayMs = policy.baseDelayMs
|
|
150
|
+
const delayMs = retryBackoffDelayMs(policy.baseDelayMs, attempt);
|
|
143
151
|
await callbacks?.onRetryScheduled?.(attempt, maxAttempts, delayMs, lastRetry.errorMessage);
|
|
144
152
|
// Normalize aborts during retry backoff to the same AssistantMessage shape as
|
|
145
153
|
// provider stream aborts, so callers do not need to care when cancellation happened.
|