Skip to content

Commit f4960cc

Browse files
jsonbaileyclaude
andcommitted
fix!: build judge input as string and narrow Runner.run signature
Aligns the JS AI SDK with the spec change implemented in launchdarkly/python-server-sdk-ai#165 and launchdarkly/sdk-specs#160. Judges now build a single formatted string ("MESSAGE HISTORY:\n...\n\n RESPONSE TO EVALUATE:\n...") and pass it to the runner instead of an interpolated message list. Legacy judge configs that contain {{message_history}} or {{response_to_evaluate}} placeholders in non-system messages are stripped at config-construction time so old and new flag values both work without behavioral surprises. BREAKING CHANGE: Runner.run is narrowed from `run(input: string | LDMessage[], outputType?)` to `run(input: string, outputType?)`. The OpenAI, LangChain, and Vercel provider runners no longer accept a pre-built message array; they always prepend any config messages and append the prompt as a user turn. The Judge no longer interpolates {{message_history}} or {{response_to_evaluate}} into config messages — the SDK builds the input string directly and the runner receives that string verbatim. The "Judge configuration must include messages" early-return was removed; a judge with no messages now proceeds to invoke the runner with the formatted input. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 70e4eb9 commit f4960cc

12 files changed

Lines changed: 232 additions & 189 deletions

File tree

packages/ai-providers/server-ai-langchain/__tests__/LangChainModelRunner.test.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AIMessage } from '@langchain/core/messages';
22

3-
import type { LDAICompletionConfig, LDMessage } from '@launchdarkly/server-sdk-ai';
3+
import type { LDAICompletionConfig } from '@launchdarkly/server-sdk-ai';
44

55
import { LangChainModelRunner } from '../src/LangChainModelRunner';
66

@@ -63,25 +63,6 @@ describe('LangChainModelRunner', () => {
6363
expect(passed[1].content).toBe('hi');
6464
});
6565

66-
it('uses a LDMessage[] as-is without prepending config messages', async () => {
67-
const response = new AIMessage('direct reply');
68-
mockLLM.invoke.mockResolvedValue(response);
69-
70-
const configWithMessages: LDAICompletionConfig = {
71-
...baseConfig,
72-
messages: [{ role: 'system', content: 'You are X' }],
73-
};
74-
const r = new LangChainModelRunner(mockLLM, configWithMessages, mockLogger);
75-
const inputMessages: LDMessage[] = [
76-
{ role: 'user', content: 'direct question' },
77-
];
78-
await r.run(inputMessages);
79-
80-
const passed = mockLLM.invoke.mock.calls[0][0];
81-
expect(passed).toHaveLength(1);
82-
expect(passed[0].content).toBe('direct question');
83-
});
84-
8566
it('marks success=false and warns when content is non-string (multimodal)', async () => {
8667
mockLLM.invoke.mockResolvedValue(new AIMessage([{ type: 'image' }] as any));
8768

packages/ai-providers/server-ai-langchain/src/LangChainModelRunner.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,20 @@ export class LangChainModelRunner implements Runner {
2929
}
3030

3131
/**
32-
* Run the LangChain model with the given prompt.
32+
* Run the LangChain model with the given user prompt.
3333
*
34-
* @param input The user prompt string or a pre-built message array to send to the model.
35-
* When a string is provided, config messages are prepended before the user prompt.
36-
* When an {@link LDMessage} array is provided, it is used as-is (config messages are
37-
* not prepended).
34+
* Prepends any messages defined in the AI config (system prompt, etc.) before
35+
* the user prompt.
36+
*
37+
* @param input The user prompt string.
3838
* @param outputType Optional JSON schema for structured output. When provided,
3939
* the parsed result is exposed via {@link RunnerResult.parsed}.
4040
*/
41-
async run(input: string | LDMessage[], outputType?: Record<string, unknown>): Promise<RunnerResult> {
42-
const messages: LDMessage[] = Array.isArray(input)
43-
? input
44-
: [...(this._config.messages ?? []), { role: 'user', content: input }];
41+
async run(input: string, outputType?: Record<string, unknown>): Promise<RunnerResult> {
42+
const messages: LDMessage[] = [
43+
...(this._config.messages ?? []),
44+
{ role: 'user', content: input },
45+
];
4546

4647
if (outputType !== undefined) {
4748
return this._runStructured(messages, outputType);

packages/ai-providers/server-ai-openai/__tests__/OpenAIModelRunner.test.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -75,27 +75,6 @@ describe('OpenAIModelRunner', () => {
7575
});
7676
});
7777

78-
it('passes a LDMessage[] input directly without prepending config messages', async () => {
79-
const mockResponse = {
80-
choices: [{ message: { content: 'Evaluation result' } }],
81-
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
82-
};
83-
(mockOpenAI.chat.completions.create as jest.Mock).mockResolvedValue(mockResponse as any);
84-
85-
const messages = [
86-
{ role: 'system' as const, content: 'You are a judge' },
87-
{ role: 'user' as const, content: 'Rate this: hello' },
88-
];
89-
const result = await runner.run(messages);
90-
91-
expect(mockOpenAI.chat.completions.create).toHaveBeenCalledWith({
92-
model: 'gpt-3.5-turbo',
93-
messages,
94-
});
95-
expect(result.content).toBe('Evaluation result');
96-
expect(result.metrics.success).toBe(true);
97-
});
98-
9978
it('marks the result unsuccessful when response has no content', async () => {
10079
const mockResponse = { choices: [{ message: {} }] };
10180
(mockOpenAI.chat.completions.create as jest.Mock).mockResolvedValue(mockResponse as any);

packages/ai-providers/server-ai-openai/src/OpenAIAgentRunner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class OpenAIAgentRunner implements Runner {
4848

4949
async run(input: string, _outputType?: Record<string, unknown>): Promise<RunnerResult> {
5050
try {
51-
const result = await this._agentRun(this._agent, String(input), { maxTurns: MAX_TURNS });
51+
const result = await this._agentRun(this._agent, input, { maxTurns: MAX_TURNS });
5252

5353
const toolCalls = getToolCallsFromRunItems(result.newItems ?? []).reduce(
5454
(acc: string[], fnName: string) => {

packages/ai-providers/server-ai-openai/src/OpenAIModelRunner.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,20 @@ export class OpenAIModelRunner implements Runner {
3232
}
3333

3434
/**
35-
* Run the OpenAI model with the given prompt or message array.
35+
* Run the OpenAI model with the given user prompt.
3636
*
37-
* When `input` is a string it is wrapped as a user turn and appended to any
38-
* messages defined in the config. When `input` is already a `LDMessage[]`
39-
* (e.g. when called from the Judge evaluation path) it is used as-is.
37+
* Prepends any messages defined in the AI config (system prompt,
38+
* instructions, etc.) before the user prompt.
4039
*
41-
* @param input The user prompt string, or a pre-built message array.
40+
* @param input The user prompt string.
4241
* @param outputType Optional JSON schema for structured output. When provided,
4342
* the response is parsed and exposed via {@link RunnerResult.parsed}.
4443
*/
45-
async run(input: string | LDMessage[], outputType?: Record<string, unknown>): Promise<RunnerResult> {
46-
const messages: LDMessage[] = Array.isArray(input)
47-
? input
48-
: [...(this._config.messages ?? []), { role: 'user', content: input }];
44+
async run(input: string, outputType?: Record<string, unknown>): Promise<RunnerResult> {
45+
const messages: LDMessage[] = [
46+
...(this._config.messages ?? []),
47+
{ role: 'user', content: input },
48+
];
4949

5050
if (outputType !== undefined) {
5151
return this._runStructured(messages, outputType);

packages/ai-providers/server-ai-vercel/__tests__/VercelModelRunner.test.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -90,30 +90,6 @@ describe('VercelModelRunner', () => {
9090
expect(out.metrics.usage).toEqual({ total: 100, input: 40, output: 60 });
9191
});
9292

93-
it('uses a LDMessage[] directly without prepending config messages', async () => {
94-
(generateText as jest.Mock).mockResolvedValue({
95-
text: 'direct',
96-
usage: { totalTokens: 5, promptTokens: 2, completionTokens: 3 },
97-
});
98-
99-
const configWithMessages: LDAICompletionConfig = {
100-
...baseConfig,
101-
messages: [{ role: 'system', content: 'Should not appear' }],
102-
};
103-
const r = new VercelModelRunner(fakeModel as any, configWithMessages, {}, mockLogger);
104-
const prebuilt = [
105-
{ role: 'system' as const, content: 'Custom system' },
106-
{ role: 'user' as const, content: 'Direct input' },
107-
];
108-
await r.run(prebuilt);
109-
110-
expect(generateText).toHaveBeenCalledWith({
111-
model: fakeModel,
112-
messages: prebuilt,
113-
experimental_telemetry: { isEnabled: true },
114-
});
115-
});
116-
11793
it('returns success=false when generateText throws', async () => {
11894
const err = new Error('boom');
11995
(generateText as jest.Mock).mockRejectedValue(err);

packages/ai-providers/server-ai-vercel/src/VercelModelRunner.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,20 @@ export class VercelModelRunner implements Runner {
3636
}
3737

3838
/**
39-
* Run the Vercel AI model with the given prompt.
39+
* Run the Vercel AI model with the given user prompt.
4040
*
41-
* @param input The user prompt string, or a pre-built message array. When a
42-
* string is supplied the config's system messages are prepended automatically.
43-
* When a `LDMessage[]` is supplied it is used as-is (config messages are NOT
44-
* prepended — the caller is responsible for the full message list).
41+
* Prepends any messages defined in the AI config (system prompt, etc.) before
42+
* the user prompt.
43+
*
44+
* @param input The user prompt string.
4545
* @param outputType Optional JSON schema for structured output. When provided,
4646
* the parsed object is exposed via {@link RunnerResult.parsed}.
4747
*/
48-
async run(input: string | LDMessage[], outputType?: Record<string, unknown>): Promise<RunnerResult> {
49-
const messages: LDMessage[] = Array.isArray(input)
50-
? input
51-
: [...(this._config.messages ?? []), { role: 'user', content: input }];
48+
async run(input: string, outputType?: Record<string, unknown>): Promise<RunnerResult> {
49+
const messages: LDMessage[] = [
50+
...(this._config.messages ?? []),
51+
{ role: 'user', content: input },
52+
];
5253

5354
if (outputType !== undefined) {
5455
return this._runStructured(messages, outputType);

0 commit comments

Comments
 (0)