Skip to content

feat: add maxRetries to the messages handler factories - #165

Open
aknight-ld wants to merge 1 commit into
mainfrom
AIC-3084-max-retries-passthrough
Open

aknight-ld wants to merge 1 commit into
mainfrom
AIC-3084-max-retries-passthrough

Conversation

@aknight-ld

@aknight-ld aknight-ld commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Add in a max retries passthrough argument to let handler factories natively retry.


Note

Overview
Adds an optional maxRetries setting on every messages handler factory and convenience wrapper (Claude, OpenAI, LangChain, LiteLLM, Vercel), wired to the underlying vendor client or AI SDK call so callers can raise retries for rate-limited batch work without changing model.parameters.

Introduces public validateMaxRetries on @launchdarkly/ai-server, used at handler construction to reject invalid values (non-integers, negatives, etc.) before provider SDKs would accept them silently. LangChain/LiteLLM error if maxRetries is combined with a caller-supplied model/client, since retries must be set on that object instead.

Also fixes SDK usage reporting IDs for createLangChainMessagesHandler / createOpenAIMessagesHandler, adds §1.1a-style tests per package, and mocks the OTLP trace exporter in sdk-usage tests to avoid CI flush timeouts.

Reviewed by Cursor Bugbot for commit 8850922. Bugbot is set up for automated code reviews on this repo. Configure here.

@aknight-ld
aknight-ld force-pushed the AIC-3084-max-retries-passthrough branch from a706687 to 77104e3 Compare October 27, 2026 14:54
@aknight-ld aknight-ld added the api-compatible An owner judged a flagged public API change compatible; passes the API compatibility check label Oct 27, 2026
@aknight-ld
aknight-ld marked this pull request as ready for review October 27, 2026 19:47
@aknight-ld
aknight-ld requested a review from a team as a code owner October 27, 2026 19:47
@aknight-ld
aknight-ld force-pushed the AIC-3084-max-retries-passthrough branch 2 times, most recently from a67619a to 718a1dd Compare October 29, 2026 19:12

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 718a1dd. Configure here.

Comment thread packages/openai-messages/src/handler.ts Resolved
@aknight-ld
aknight-ld force-pushed the AIC-3084-max-retries-passthrough branch from 718a1dd to 28bf5ad Compare October 29, 2026 19:48
@aknight-ld
aknight-ld force-pushed the AIC-3084-max-retries-passthrough branch from 28bf5ad to 8850922 Compare October 29, 2026 20:19

@jeffdupont jeffdupont left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The maxRetries work itself matches ai-sdks-monorepo#88 on every §1.1a rule. Each of the five factories validates at construction, passes the value only when it's set, refuses it with llm/client/clientFactory where the count is a client setting, applies it per call in vercel-messages, and the wrappers forward it. The tests cover every bullet, including LangChain's AsyncCaller count.

Two changes needed, both as suggestions below:

  1. Revert the two renamed usage IDs (openai-messages.createOpenAIHandler, langchain-messages.createLangChainHandler). §3.27 says IDs are frozen: "Renaming a function in one language does not change its ID". The registry (api-surface/usage-ids.txt) and the Python SDK still use the old IDs, so the rename would split usage counts by language.
  2. Mark validateMaxRetries @internal like createHandlerInternal, since only handler packages call it. After applying that suggestion, run yarn api:update and commit the regenerated packages/client/etc/ai-server.api.md (it currently lists the function as @public at :1102). I've asked on #88 for the spec list to mark it # internal too.

Heads-up: #190 removes the captureContent = false default from these factories, and a trial merge conflicts in the claude, langchain, openai and vercel messages handlers. Whichever lands second should keep #190's unset captureContent and its run-time handler.captureContent read, and add maxRetries on top. Also, the "A.22" in utils.test.ts:834 will need the new appendix number once #88 is renumbered.

# + Name joining the public surface; may be exported or not

createOpenAIMessagesHandler usage=openai-messages.createOpenAIHandler
createOpenAIMessagesHandler usage=openai-messages.createOpenAIMessagesHandler

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the frozen usage ID here. §3.27 freezes usage IDs ("Renaming a function in one language does not change its ID"), and the registry and Python still use the old one.

Suggested change
createOpenAIMessagesHandler usage=openai-messages.createOpenAIMessagesHandler
createOpenAIMessagesHandler usage=openai-messages.createOpenAIHandler

# + Name joining the public surface; may be exported or not

createLangChainMessagesHandler usage=langchain-messages.createLangChainHandler
createLangChainMessagesHandler usage=langchain-messages.createLangChainMessagesHandler

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the frozen usage ID here. §3.27 freezes usage IDs ("Renaming a function in one language does not change its ID"), and the registry and Python still use the old one.

Suggested change
createLangChainMessagesHandler usage=langchain-messages.createLangChainMessagesHandler
createLangChainMessagesHandler usage=langchain-messages.createLangChainHandler

*/
maxRetries?: number;
} = {}): ProviderHandler {
reportUsage('openai-messages.createOpenAIMessagesHandler', LD_AI_PACKAGE_NAME, LD_AI_PACKAGE_VERSION);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the frozen usage ID here. §3.27 freezes usage IDs ("Renaming a function in one language does not change its ID"), and the registry and Python still use the old one.

Suggested change
reportUsage('openai-messages.createOpenAIMessagesHandler', LD_AI_PACKAGE_NAME, LD_AI_PACKAGE_VERSION);
reportUsage('openai-messages.createOpenAIHandler', LD_AI_PACKAGE_NAME, LD_AI_PACKAGE_VERSION);

} = {},
): ProviderHandler {
reportUsage('langchain-messages.createLangChainHandler', LD_AI_PACKAGE_NAME, LD_AI_PACKAGE_VERSION);
reportUsage('langchain-messages.createLangChainMessagesHandler', LD_AI_PACKAGE_NAME, LD_AI_PACKAGE_VERSION);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the frozen usage ID here. §3.27 freezes usage IDs ("Renaming a function in one language does not change its ID"), and the registry and Python still use the old one.

Suggested change
reportUsage('langchain-messages.createLangChainMessagesHandler', LD_AI_PACKAGE_NAME, LD_AI_PACKAGE_VERSION);
reportUsage('langchain-messages.createLangChainHandler', LD_AI_PACKAGE_NAME, LD_AI_PACKAGE_VERSION);

const CALLS: Record<string, Call[]> = {
'openai-messages.openaiMessages': [(fn) => settle(fn('k', 'q', ctx))],
'openai-messages.createOpenAIHandler': [(fn) => fire(() => fn())],
'openai-messages.createOpenAIMessagesHandler': [(fn) => fire(() => fn())],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the frozen usage ID here. §3.27 freezes usage IDs ("Renaming a function in one language does not change its ID"), and the registry and Python still use the old one.

Suggested change
'openai-messages.createOpenAIMessagesHandler': [(fn) => fire(() => fn())],
'openai-messages.createOpenAIHandler': [(fn) => fire(() => fn())],

const calls = client.track.mock.calls.filter((entry) => entry[0] === '$ld:ai:sdk:usage');
expect(calls).toHaveLength(1);
expect(calls[0]?.[2]).toEqual(expectedPayload('openai-messages.createOpenAIHandler'));
expect(calls[0]?.[2]).toEqual(expectedPayload('openai-messages.createOpenAIMessagesHandler'));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the frozen usage ID here. §3.27 freezes usage IDs ("Renaming a function in one language does not change its ID"), and the registry and Python still use the old one.

Suggested change
expect(calls[0]?.[2]).toEqual(expectedPayload('openai-messages.createOpenAIMessagesHandler'));
expect(calls[0]?.[2]).toEqual(expectedPayload('openai-messages.createOpenAIHandler'));

const CALLS: Record<string, Call[]> = {
'langchain-messages.langchainMessages': [(fn) => settle(fn('k', 'q', ctx))],
'langchain-messages.createLangChainHandler': [(fn) => fire(() => fn())],
'langchain-messages.createLangChainMessagesHandler': [(fn) => fire(() => fn())],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the frozen usage ID here. §3.27 freezes usage IDs ("Renaming a function in one language does not change its ID"), and the registry and Python still use the old one.

Suggested change
'langchain-messages.createLangChainMessagesHandler': [(fn) => fire(() => fn())],
'langchain-messages.createLangChainHandler': [(fn) => fire(() => fn())],

const calls = client.track.mock.calls.filter((entry) => entry[0] === '$ld:ai:sdk:usage');
expect(calls).toHaveLength(1);
expect(calls[0]?.[2]).toEqual(expectedPayload('langchain-messages.createLangChainHandler'));
expect(calls[0]?.[2]).toEqual(expectedPayload('langchain-messages.createLangChainMessagesHandler'));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the frozen usage ID here. §3.27 freezes usage IDs ("Renaming a function in one language does not change its ID"), and the registry and Python still use the old one.

Suggested change
expect(calls[0]?.[2]).toEqual(expectedPayload('langchain-messages.createLangChainMessagesHandler'));
expect(calls[0]?.[2]).toEqual(expectedPayload('langchain-messages.createLangChainHandler'));

*
* Throws `TypeError` for a value that is not an integer and `RangeError` for a negative one. Both
* messages name the option.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the handler packages call this, so mark it @internal like createHandlerInternal (then yarn api:update).

Suggested change
*/
*
* @internal Exported for the LaunchDarkly handler packages; applications should not call it.
*/

Comment thread api-surface/ai-server.txt
- toSemconvFinishReason
TrackData
- TraverseVisitor
+ validateMaxRetries # checks a messages handler factory's maxRetries option (A.22)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List it as internal to match the @internal tag, the same way as createHandlerInternal.

Suggested change
+ validateMaxRetries # checks a messages handler factory's maxRetries option (A.22)
- validateMaxRetries # internal: plumbing for the handler packages; moves to the internal entry point

@jeffdupont

Copy link
Copy Markdown
Contributor

Correction: please ignore my 7 "keep the frozen usage ID" suggestions. The freeze in TESTING.md §3.27 applies from 1.0.0 onward, so these IDs can still change, and your new IDs are the right ones. Keep them.

I'm renaming the same IDs in the spec, Python and Go, plus openai-agents.createOpenAIAgentHandler → openai-agents.createOpenAIAgentsHandler, in a separate PR set before the 1.0 cut, so all SDKs match. My other two suggestions (@internal on validateMaxRetries, and its api-surface line) still stand.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api-compatible An owner judged a flagged public API change compatible; passes the API compatibility check

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants