Skip to content

Commit 94aaa85

Browse files
committed
fix tests with client shutdown, add test cases for fetchStrategy, tweaked types to support tool calling and remove duplicate types from ts-sdk/types.ts that should be coming from the lib/openrouter.ts.
1 parent 2ccd1f5 commit 94aaa85

File tree

7 files changed

+382
-113
lines changed

7 files changed

+382
-113
lines changed

__tests__/ts-sdk/sdk.test.ts

Lines changed: 66 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@ const apiKey = 'sdk_dummy';
99
const projectId = 'project_dummy';
1010

1111
describe('AgentsmithClient (runtime)', () => {
12+
let client: AgentsmithClient<Agency>;
13+
14+
afterEach(async () => {
15+
if (client) {
16+
await client.shutdown();
17+
}
18+
});
19+
1220
it('gets and compiles a prompt that exists on the local file system', async () => {
13-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
21+
client = new AgentsmithClient<Agency>(apiKey, projectId, {
1422
agentsmithDirectory,
1523
});
1624
const prompt = await client.getPrompt('hello-world@0.0.3');
@@ -22,21 +30,19 @@ describe('AgentsmithClient (runtime)', () => {
2230
expect(compiledPrompt).toContain('John');
2331
expect(finalVariables).toHaveProperty('firstName', 'John');
2432
expect(finalVariables).toHaveProperty('lastName', 'Doe');
25-
await client.shutdown();
2633
});
2734

2835
it('compiles a prompt that requires no variables', async () => {
29-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
36+
client = new AgentsmithClient<Agency>(apiKey, projectId, {
3037
agentsmithDirectory,
3138
});
3239
const prompt = await client.getPrompt('hello-world@0.0.1');
3340
const { compiledPrompt } = await prompt.compile();
3441
expect(typeof compiledPrompt).toBe('string');
35-
await client.shutdown();
3642
});
3743

3844
it('compiles with variables and global overrides', async () => {
39-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
45+
client = new AgentsmithClient<Agency>(apiKey, projectId, {
4046
agentsmithDirectory,
4147
});
4248
const prompt = await client.getPrompt('hello-world@0.0.2');
@@ -48,22 +54,20 @@ describe('AgentsmithClient (runtime)', () => {
4854

4955
expect(compiledPrompt).toContain('John');
5056
expect(finalVariables.global.gitHubUrl).toBe('https://github.com/example');
51-
await client.shutdown();
5257
});
5358

5459
it('throws at runtime when required variables are missing', async () => {
55-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
60+
client = new AgentsmithClient<Agency>(apiKey, projectId, {
5661
agentsmithDirectory,
5762
});
5863
const prompt = await client.getPrompt('hello-world@0.0.2');
5964

6065
// Cast to any so we can bypass TS compile-time checks and test runtime behaviour
6166
await expect(prompt.compile({} as any)).rejects.toThrow('Missing required variables');
62-
await client.shutdown();
6367
});
6468

6569
it('executes a prompt and returns a completion', async () => {
66-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
70+
client = new AgentsmithClient<Agency>(apiKey, projectId, {
6771
agentsmithDirectory,
6872
});
6973
const prompt = await client.getPrompt('hello-world@0.0.3');
@@ -76,7 +80,6 @@ describe('AgentsmithClient (runtime)', () => {
7680

7781
expect(nonStreamingResult.completion.choices[0].message.content).toBe('Hello from OpenRouter!');
7882
expect(nonStreamingResult.logUuid).toEqual('mock-log-uuid');
79-
await client.shutdown();
8083
});
8184

8285
describe('Fetch Strategies', () => {
@@ -127,14 +130,21 @@ describe('AgentsmithClient (runtime)', () => {
127130
});
128131

129132
describe('remote-fallback (default)', () => {
133+
let strategyClient: AgentsmithClient<Agency>;
134+
135+
afterEach(async () => {
136+
if (strategyClient) {
137+
await strategyClient.shutdown();
138+
}
139+
});
140+
130141
it('fetches from FS if present and does not call remote', async () => {
131-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
142+
strategyClient = new AgentsmithClient<Agency>(apiKey, projectId, {
132143
agentsmithDirectory,
133144
fetchStrategy: 'remote-fallback',
134145
});
135-
await client.getPrompt('hello-world@0.0.3');
146+
await strategyClient.getPrompt('hello-world@0.0.3');
136147
expect(mockSupabase.from).not.toHaveBeenCalled();
137-
await client.shutdown();
138148
});
139149

140150
it('falls back to remote if not found on FS', async () => {
@@ -143,142 +153,159 @@ describe('AgentsmithClient (runtime)', () => {
143153
createBuilder(table === 'prompt_versions' ? promptVersionResponse : { data: {} }),
144154
);
145155

146-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
156+
strategyClient = new AgentsmithClient<Agency>(apiKey, projectId, {
147157
agentsmithDirectory: '/non/existent/path',
148158
fetchStrategy: 'remote-fallback',
149159
});
150160

151-
const prompt = await client.getPrompt(`${slug}@${version}` as any);
161+
const prompt = await strategyClient.getPrompt(`${slug}@${version}` as any);
152162
const { compiledPrompt } = await (prompt as any).compile({ name: 'John' });
153163

154164
expect(readFileSpy).toHaveBeenCalled();
155165
expect(mockSupabase.from).toHaveBeenCalledWith('prompt_versions');
156166
expect(compiledPrompt).toContain('John');
157-
await client.shutdown();
158167
});
159168
});
160169

161170
describe('fs-only', () => {
171+
let strategyClient: AgentsmithClient<Agency>;
172+
173+
afterEach(async () => {
174+
if (strategyClient) {
175+
await strategyClient.shutdown();
176+
}
177+
});
178+
162179
it('fetches from FS and does not fall back to remote', async () => {
163-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
180+
strategyClient = new AgentsmithClient<Agency>(apiKey, projectId, {
164181
agentsmithDirectory,
165182
fetchStrategy: 'fs-only',
166183
});
167-
const prompt = await client.getPrompt('hello-world@0.0.3');
184+
const prompt = await strategyClient.getPrompt('hello-world@0.0.3');
168185
const { compiledPrompt } = await prompt.compile({
169186
firstName: 'John',
170187
lastName: 'Doe',
171188
});
172189
expect(compiledPrompt).toContain('John');
173190
expect(mockSupabase.from).not.toHaveBeenCalled();
174-
await client.shutdown();
175191
});
176192

177193
it('throws if not found on FS', async () => {
178194
const readFileSpy = jest.spyOn(fs.promises, 'readFile').mockRejectedValue(enoentErr);
179-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
180-
agentsmithDirectory,
195+
196+
strategyClient = new AgentsmithClient<Agency>(apiKey, projectId, {
197+
agentsmithDirectory: '/non/existent/path',
181198
fetchStrategy: 'fs-only',
182199
});
183200

184-
await expect(client.getPrompt('non-existent-prompt@0.0.1' as any)).rejects.toThrow(
201+
await expect(strategyClient.getPrompt('non-existent-prompt@0.0.1' as any)).rejects.toThrow(
185202
'Failed to initialize prompt non-existent-prompt from file system (fs-only strategy)',
186203
);
187204
expect(readFileSpy).toHaveBeenCalled();
188205
expect(mockSupabase.from).not.toHaveBeenCalled();
189-
await client.shutdown();
190206
});
191207
});
192208

193209
describe('remote-only', () => {
210+
let strategyClient: AgentsmithClient<Agency>;
211+
212+
afterEach(async () => {
213+
if (strategyClient) {
214+
await strategyClient.shutdown();
215+
}
216+
});
217+
194218
it('fetches from remote and does not try FS', async () => {
195219
mockSupabase.from.mockImplementation((table: string) =>
196220
createBuilder(table === 'prompt_versions' ? promptVersionResponse : { data: {} }),
197221
);
198-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
222+
strategyClient = new AgentsmithClient<Agency>(apiKey, projectId, {
199223
agentsmithDirectory,
200224
fetchStrategy: 'remote-only',
201225
});
202226

203-
const prompt = await client.getPrompt(`${slug}@${version}` as any);
227+
const prompt = await strategyClient.getPrompt(`${slug}@${version}` as any);
204228
const { compiledPrompt } = await (prompt as any).compile({ name: 'John' });
205229

206230
expect(mockSupabase.from).toHaveBeenCalledWith('prompt_versions');
207231
expect(compiledPrompt).toContain('John');
208-
await client.shutdown();
209232
});
210233

211234
it('throws if not found on remote', async () => {
212235
mockSupabase.from.mockImplementation(() =>
213236
createBuilder({ data: null, error: new Error('Not found') }),
214237
);
215-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
238+
strategyClient = new AgentsmithClient<Agency>(apiKey, projectId, {
216239
agentsmithDirectory,
217240
fetchStrategy: 'remote-only',
218241
});
219242

220-
await expect(client.getPrompt('non-existent-prompt@0.0.1' as any)).rejects.toThrow(
243+
await expect(strategyClient.getPrompt('non-existent-prompt@0.0.1' as any)).rejects.toThrow(
221244
'Failed to initialize prompt non-existent-prompt from remote (remote-only strategy)',
222245
);
223246
expect(mockSupabase.from).toHaveBeenCalled();
224-
await client.shutdown();
225247
});
226248
});
227249

228250
describe('fs-fallback', () => {
251+
let strategyClient: AgentsmithClient<Agency>;
252+
253+
afterEach(async () => {
254+
if (strategyClient) {
255+
await strategyClient.shutdown();
256+
}
257+
});
258+
229259
it('fetches from remote if present and does not try FS', async () => {
230260
mockSupabase.from.mockImplementation((table: string) =>
231261
createBuilder(table === 'prompt_versions' ? promptVersionResponse : { data: {} }),
232262
);
233-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
263+
strategyClient = new AgentsmithClient<Agency>(apiKey, projectId, {
234264
agentsmithDirectory,
235265
fetchStrategy: 'fs-fallback',
236266
});
237267

238-
const prompt = await client.getPrompt(`${slug}@${version}` as any);
268+
const prompt = await strategyClient.getPrompt(`${slug}@${version}` as any);
239269
const { compiledPrompt } = await (prompt as any).compile({ name: 'John' });
240270

241271
expect(mockSupabase.from).toHaveBeenCalledWith('prompt_versions');
242272
expect(compiledPrompt).toContain('John');
243-
await client.shutdown();
244273
});
245274

246275
it('falls back to FS if not found on remote', async () => {
247276
mockSupabase.from.mockImplementation(() =>
248277
createBuilder({ data: null, error: { message: 'Not found' } }),
249278
);
250-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
279+
strategyClient = new AgentsmithClient<Agency>(apiKey, projectId, {
251280
agentsmithDirectory,
252281
fetchStrategy: 'fs-fallback',
253282
});
254283

255-
const prompt = await client.getPrompt('hello-world@0.0.3');
284+
const prompt = await strategyClient.getPrompt('hello-world@0.0.3');
256285
const { compiledPrompt } = await prompt.compile({
257286
firstName: 'John',
258287
lastName: 'Doe',
259288
});
260289

261290
expect(mockSupabase.from).toHaveBeenCalled();
262291
expect(compiledPrompt).toContain('John');
263-
await client.shutdown();
264292
});
265293

266294
it('throws if not found on remote or FS', async () => {
267295
const readFileSpy = jest.spyOn(fs.promises, 'readFile').mockRejectedValue(enoentErr);
268296
mockSupabase.from.mockImplementation(() =>
269297
createBuilder({ data: null, error: new Error('Not found') }),
270298
);
271-
const client = new AgentsmithClient<Agency>(apiKey, projectId, {
272-
agentsmithDirectory,
299+
strategyClient = new AgentsmithClient<Agency>(apiKey, projectId, {
300+
agentsmithDirectory: '/non/existent/path',
273301
fetchStrategy: 'fs-fallback',
274302
});
275303

276-
await expect(client.getPrompt('non-existent-prompt@0.0.1' as any)).rejects.toThrow(
304+
await expect(strategyClient.getPrompt('non-existent-prompt@0.0.1' as any)).rejects.toThrow(
277305
'Failed to initialize prompt from both remote and file system',
278306
);
279307
expect(mockSupabase.from).toHaveBeenCalled();
280308
expect(readFileSpy).toHaveBeenCalled();
281-
await client.shutdown();
282309
});
283310
});
284311
});

src/lib/openrouter.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -268,26 +268,21 @@ export type ImageContentPart = {
268268

269269
export type ContentPart = TextContent | ImageContentPart;
270270

271-
export type Message =
272-
| {
273-
role: 'user' | 'assistant' | 'system';
274-
// ContentParts are only for the "user" role:
275-
content: string | ContentPart[];
276-
// If "name" is included, it will be prepended like this
277-
// for non-OpenAI models: `{name}: {content}`
278-
name?: string;
279-
}
280-
| {
281-
role: 'tool';
282-
content: string;
283-
tool_call_id: string;
284-
name?: string;
285-
};
271+
export type Message = {
272+
role: 'user' | 'assistant' | 'system' | 'tool';
273+
// ContentParts are only for the "user" role:
274+
content: string | ContentPart[] | null;
275+
// If "name" is included, it will be prepended like this
276+
// for non-OpenAI models: `{name}: {content}`
277+
name?: string;
278+
tool_calls?: ToolCall[];
279+
tool_call_id?: string;
280+
};
286281

287282
export type FunctionDescription = {
288283
description?: string;
289284
name: string;
290-
parameters: object; // JSON Schema object
285+
parameters: JSONSchemaDefinition;
291286
};
292287

293288
export type Tool = {
@@ -355,7 +350,7 @@ export type NonStreamingChoice = {
355350
finish_reason: string | null;
356351
native_finish_reason: string | null;
357352
message: {
358-
role: string;
353+
role: 'user' | 'assistant' | 'system' | 'tool';
359354
content: string | null;
360355
refusal?: string | null;
361356
reasoning?: string;
@@ -370,7 +365,7 @@ export type StreamingChoice = {
370365
native_finish_reason: string | null;
371366
delta: {
372367
content: string | null;
373-
role?: string;
368+
role?: 'user' | 'assistant' | 'system' | 'tool';
374369
tool_calls?: ToolCall[];
375370
};
376371
error?: ErrorResponse;
@@ -383,10 +378,13 @@ export type ErrorResponse = {
383378
};
384379

385380
export type ToolCall = {
381+
index: number;
386382
id: string;
387383
type: 'function';
388-
// function: FunctionCall;
389-
function: any;
384+
function: {
385+
name: string;
386+
arguments: string;
387+
};
390388
};
391389

392390
export type OpenrouterResponse = {

0 commit comments

Comments
 (0)