Migrate from the legacy AI SDKs

This topic explains how to migrate from the legacy Python and Node.js AI SDKs to the current AI SDKs.

The current SDKs are not drop-in replacements. You must change some workflows when you migrate. You stop evaluating a config and calling the provider yourself, and instead register handlers and invoke the model through the SDK.

The current Python and Node.js AI SDKs are in open beta. Their APIs are subject to change. To learn more about each SDK, read the Python AI SDK reference and Node.js (server-side) AI SDK reference.

How SDK versions differ

In the legacy SDKs, you evaluate an AgentControl config, call your model provider with the returned messages or instructions, and record metrics with a tracker.

In the current SDKs, you register provider handlers and tools one time, then call config(), graph(), or a convenience function. The SDK evaluates the config, routes to the matching handler, calls the provider, and records metrics and OpenTelemetry spans for you.

StepLegacy AI SDKCurrent AI SDK
Evaluate the configCall completion_config() / completionConfig(), agent_config() / agentConfig(), or related methodsCall config(), graph(), or a provider convenience function
Call the providerBuild a provider client, merge messages, and call the provider yourselfA handler package calls the provider
Record metricsCreate a tracker and wrap the provider callAutomatic on every invoke

What changes when you migrate from a legacy SDK

You must change some parts of your application code when you migrate. They are:

  • Packages: Replace the legacy AI SDK packages with the current core package, as well as provider and mode handler packages.
  • Initialization: Replace explicit LaunchDarkly and AI client construction with automatic initialization from the environment, or call init_client() / initClient() when you need control.
  • Model calls: Replace evaluate-then-call-provider-yourself with config().invoke(), graph(), or a provider convenience function.
  • Tools and handlers: Register tool implementations and handlers one time, typically in a Registry, instead of wiring them at every call site.
  • Metrics and traces: Remove tracker setup and manual span wrapping. The SDK records metrics and creates OpenTelemetry spans automatically.
  • Judges and graphs: Attach judges to config variations instead of calling a standalone judge, and replace legacy graph helpers with graph() or resolve_graph() / resolveGraph(), as well as a native runner.

The sections below contain examples for each of these changes.

Replace packages and install handlers

Uninstall the legacy AI SDK packages and install the current core package, as well as one handler package for each provider and mode you use.

LanguageLegacy packagesCurrent packages
Pythonlaunchdarkly-server-sdk, launchdarkly-server-sdk-ai, and optional provider helpers such as ldai_openailaunchdarkly-ai-server and handler packages such as launchdarkly-ai-openai-messages
Node.js@launchdarkly/node-server-sdk, @launchdarkly/server-sdk-ai, and optional provider packages such as @launchdarkly/server-sdk-ai-openai@launchdarkly/ai-node and handler packages such as @launchdarkly/ai-openai-messages

Install the base LaunchDarkly server SDK alongside the AI SDK core and handler packages. Use launchdarkly-server-sdk for Python and @launchdarkly/node-server-sdk for Node.js. For edge or custom runtimes, install the core @launchdarkly/ai-server package instead and pass a pre-initialized client.

For the full handler package matrix, read Install the SDK in the Python AI SDK reference or Install the SDK in the Node.js AI SDK reference.

Here’s how to install the base LaunchDarkly server SDK, the AI SDK core package, and the OpenAI messages handler:

1pip install launchdarkly-server-sdk launchdarkly-ai-server launchdarkly-ai-openai-messages

Update client initialization

The legacy SDKs require you to construct an explicit LaunchDarkly client and an AI client wrapper.

Here’s how the legacy SDKs initialize:

1from ldclient import set_config, get
2from ldclient.config import Config
3from ldai import LDAIClient
4
5set_config(Config("YOUR_SDK_KEY"))
6aiclient = LDAIClient(get())

In the current SDKs, initialization is often automatic. The SDK derives its settings from the environment, reading LD_SDK_KEY and your provider API keys, and initializes the client on your first AI call. You no longer construct a LaunchDarkly client or an AI client wrapper yourself.

If you need to control initialization, such as to pass custom options or pre-warm the client, call init_client() / initClient() yourself. Call shutdown() when your application exits to flush events and spans.

To learn more, read Manage the client lifecycle or Manage the client lifecycle.

Migrate a manual completion call

This is the most common migration. In the legacy SDKs, you evaluate a completion-mode config, merge messages, call the provider, and wrap the call with a tracker.

Here’s the legacy pattern:

1from ldai import AICompletionConfigDefault
2from ldai_openai import get_ai_metrics_from_response
3
4fallback_value = AICompletionConfigDefault(enabled=False)
5
6config = aiclient.completion_config(
7 'example-config-key',
8 context,
9 fallback_value,
10 {'example_custom_variable': 'example_custom_value'},
11)
12
13tracker = config.create_tracker()
14
15if config.enabled:
16 messages = [] if config.messages is None else config.messages
17 completion = tracker.track_metrics_of(
18 get_ai_metrics_from_response,
19 lambda: openai_client.chat.completions.create(
20 model=config.model.name,
21 messages=[message.to_dict() for message in messages],
22 ),
23 )

Use a convenience function

If you call one provider in one mode, replace the evaluate-call-track sequence with that provider’s convenience function. The SDK evaluates the config, calls the provider, and records metrics.

Here’s the same call with a convenience function:

1from dotenv import load_dotenv
2from launchdarkly_ai_openai_messages import openai_messages
3
4load_dotenv()
5
6result = await openai_messages(
7 "What is feature flagging?",
8 {"kind": "user", "key": "user-123"},
9 {"key": "example-config-key"},
10 {"example_custom_variable": "example_custom_value"},
11)
12
13print(result.response)

Use handlers for multi-provider routing

If your config can serve more than one provider or mode, register handlers and call config().invoke(). Pass template variables as the third argument to invoke(). You no longer build a fallback default object, merge UI messages with the user message, or create a tracker.

Here’s how:

1from dotenv import load_dotenv
2from launchdarkly_ai_server import config, shutdown
3from launchdarkly_ai_openai_messages import create_openai_messages_handler
4from launchdarkly_ai_claude_messages import create_claude_messages_handler
5
6load_dotenv()
7
8result = await config(
9 key="example-config-key",
10 handler=[
11 create_openai_messages_handler(),
12 create_claude_messages_handler(),
13 ],
14).invoke(
15 "What is feature flagging?",
16 {"kind": "user", "key": "user-123"},
17 {"example_custom_variable": "example_custom_value"},
18)
19
20print(result.response)
21await shutdown()

If you used Node.js legacy managed objects such as createModel().run(), map those call sites to the same convenience function or config().invoke() path. Managed objects were the closest precursor to the current SDK, but the packages and APIs still change.

Migrate agent mode and tools

In the legacy SDKs, agent mode uses a separate evaluation method, then your application runs the tool loop and records metrics. In the current SDKs, completion and agent mode share config(). The config variation’s mode selects the handler. Register tool implementations by name, or pass them inline.

Tool keys must match the tool names defined in the LaunchDarkly config variation exactly, including case.

Here’s the legacy agent-mode pattern:

1from ldai import AIAgentConfigDefault
2
3agent = aiclient.agent_config(
4 'example-config-key',
5 context,
6 AIAgentConfigDefault(enabled=False),
7 {'example_custom_variable': 'example_custom_value'},
8)
9
10if agent.enabled:
11 tracker = agent.create_tracker()
12 result = example_model_api(agent.instructions)
13 tracker.track_success()

Here’s the same call with handlers and tools:

1from launchdarkly_ai_server import config
2from launchdarkly_ai_openai_agents import create_openai_agent_handler
3
4config_instance = config(
5 key="example-config-key",
6 handler=[create_openai_agent_handler()],
7 tool_handlers={
8 "get-prefs": get_preferences_fn,
9 },
10)
11
12result = await config_instance.invoke(
13 "What are my preferences?",
14 {"kind": "user", "key": "user-123"},
15 {"example_custom_variable": "example_custom_value"},
16)
17
18print(result.response)

Centralize configuration management

Registries are one of the larger cleanup opportunities in this migration. In the legacy SDKs, sharing handler wiring and tool implementations across call sites meant writing your own higher-level wrapper functions or duplicating handling code at each site. In the current SDKs, you register handlers and tools one time in a Registry, then pass that registry to any call. Provider routing, tool dispatch, and metrics all live in one place.

Use a scoped Registry when different parts of your application need different handlers or tools, or the global registry when a single set is appropriate for the whole application. Some providers also expose built-in tools, such as Claude web search, that you assign as tool implementations instead of writing a function.

Here’s how to register handlers and tools one time, then reuse them:

1from launchdarkly_ai_server import Registry, config
2from launchdarkly_ai_openai_messages import create_openai_messages_handler
3from launchdarkly_ai_openai_agents import create_openai_agent_handler
4
5openai_registry = Registry(
6 handlers=[create_openai_messages_handler(), create_openai_agent_handler()],
7 tools={"get-prefs": get_preferences_fn},
8)
9
10# Reuse the same registry across call sites
11result = await config(key="example-config-key", registry=openai_registry).invoke(user_input, context)

To learn more, read Manage handlers and tools with a registry or Create a registry.

Migrate judges

In the current SDKs, you attach judges to a config variation rather than calling a standalone judge in your application code. To keep response latency low, you can skip inline evaluation and run judges in the background instead.

Run attached judges asynchronously. If you don’t include skip_judges=true, judges are run inline by default. Here’s how:

1import asyncio
2from launchdarkly_ai_server import config, global_registry
3
4result = await config(
5 key="example-config-key",
6 registry=global_registry,
7 skip_judges=True,
8).invoke(user_input, context)
9
10for task in result.judge_tasks or []:
11 asyncio.create_task(run_judge_worker(task))

Inside each worker, call run_judge(task, handlers) / runJudge(task, handlers) and track the evaluation metric when present. To learn more, read Run judges asynchronously or Run judges asynchronously.

Migrate agent graphs

Replace legacy graph creation and traversal helpers with graph().invoke(), or resolve the topology and hand it to a native framework runner.

Here’s how to run a graph with the current SDK router:

1from launchdarkly_ai_server import graph, shutdown
2from launchdarkly_ai_claude_agents import create_claude_agents_handler
3from launchdarkly_ai_openai_agents import create_openai_agent_handler
4
5result = await graph(
6 "support-graph",
7 handlers=[create_claude_agents_handler(), create_openai_agent_handler()],
8).invoke(
9 "I was double charged",
10 {"kind": "user", "key": "user-123"},
11 {"account_tier": "pro"},
12)
13
14print(result["response"])
15await shutdown()

To run on a provider framework such as LangGraph or the OpenAI Agents SDK, call resolve_graph() / resolveGraph(), then pass the result to to_lang_graph / toLangGraph, to_openai_agents / toOpenAIAgents, or to_claude_agents / toClaudeAgents. To learn more, read Run an agent graph or Run an agent graph.

API mapping

ConcernLegacyCurrent
Completioncompletion_config() / completionConfig()config().invoke() or a convenience function
Agentagent_config() / agentConfig()config().invoke(); mode comes from the variation
Multi-agentagent_configs() / agentConfigs()graph() or a native framework runner
MetricsTracker methodsAutomatic on every call
Judgescreate_judge() / createJudge()Attached judges, plus run_judge() / runJudge() for async work
Graphscreate_agent_graph(), traverse helpersgraph(), resolve_graph() / resolveGraph()
InitLDAIClient / initAi()Lazy init, or init_client() / initClient()

What’s next