Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AIMessage } from '@langchain/core/messages';

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

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

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

it('uses a LDMessage[] as-is without prepending config messages', async () => {
const response = new AIMessage('direct reply');
mockLLM.invoke.mockResolvedValue(response);

const configWithMessages: LDAICompletionConfig = {
...baseConfig,
messages: [{ role: 'system', content: 'You are X' }],
};
const r = new LangChainModelRunner(mockLLM, configWithMessages, mockLogger);
const inputMessages: LDMessage[] = [
{ role: 'user', content: 'direct question' },
];
await r.run(inputMessages);

const passed = mockLLM.invoke.mock.calls[0][0];
expect(passed).toHaveLength(1);
expect(passed[0].content).toBe('direct question');
});

it('marks success=false and warns when content is non-string (multimodal)', async () => {
mockLLM.invoke.mockResolvedValue(new AIMessage([{ type: 'image' }] as any));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,20 @@ export class LangChainModelRunner implements Runner {
}

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

if (outputType !== undefined) {
return this._runStructured(messages, outputType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,27 +75,6 @@ describe('OpenAIModelRunner', () => {
});
});

it('passes a LDMessage[] input directly without prepending config messages', async () => {
const mockResponse = {
choices: [{ message: { content: 'Evaluation result' } }],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
};
(mockOpenAI.chat.completions.create as jest.Mock).mockResolvedValue(mockResponse as any);

const messages = [
{ role: 'system' as const, content: 'You are a judge' },
{ role: 'user' as const, content: 'Rate this: hello' },
];
const result = await runner.run(messages);

expect(mockOpenAI.chat.completions.create).toHaveBeenCalledWith({
model: 'gpt-3.5-turbo',
messages,
});
expect(result.content).toBe('Evaluation result');
expect(result.metrics.success).toBe(true);
});

it('marks the result unsuccessful when response has no content', async () => {
const mockResponse = { choices: [{ message: {} }] };
(mockOpenAI.chat.completions.create as jest.Mock).mockResolvedValue(mockResponse as any);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class OpenAIAgentRunner implements Runner {

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

const toolCalls = getToolCallsFromRunItems(result.newItems ?? []).reduce(
(acc: string[], fnName: string) => {
Expand Down
18 changes: 9 additions & 9 deletions packages/ai-providers/server-ai-openai/src/OpenAIModelRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,20 @@ export class OpenAIModelRunner implements Runner {
}

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

if (outputType !== undefined) {
return this._runStructured(messages, outputType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,30 +90,6 @@ describe('VercelModelRunner', () => {
expect(out.metrics.usage).toEqual({ total: 100, input: 40, output: 60 });
});

it('uses a LDMessage[] directly without prepending config messages', async () => {
(generateText as jest.Mock).mockResolvedValue({
text: 'direct',
usage: { totalTokens: 5, promptTokens: 2, completionTokens: 3 },
});

const configWithMessages: LDAICompletionConfig = {
...baseConfig,
messages: [{ role: 'system', content: 'Should not appear' }],
};
const r = new VercelModelRunner(fakeModel as any, configWithMessages, {}, mockLogger);
const prebuilt = [
{ role: 'system' as const, content: 'Custom system' },
{ role: 'user' as const, content: 'Direct input' },
];
await r.run(prebuilt);

expect(generateText).toHaveBeenCalledWith({
model: fakeModel,
messages: prebuilt,
experimental_telemetry: { isEnabled: true },
});
});

it('returns success=false when generateText throws', async () => {
const err = new Error('boom');
(generateText as jest.Mock).mockRejectedValue(err);
Expand Down
19 changes: 10 additions & 9 deletions packages/ai-providers/server-ai-vercel/src/VercelModelRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,20 @@ export class VercelModelRunner implements Runner {
}

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

if (outputType !== undefined) {
return this._runStructured(messages, outputType);
Expand Down
Loading
Loading