Skip to content

Commit 32bb821

Browse files
committed
feat(sdk/js): add Responses API client for OpenAI/OpenResponses compatibility
Add ResponsesClient to the JS SDK v2 with full CRUD support for the Responses API served by Foundry Local's embedded web service. New files: - src/openai/responsesClient.ts: HTTP-based client with SSE streaming - test/openai/responsesClient.test.ts: 35 tests (unit + integration) - examples/responses.ts: end-to-end usage examples Modified files: - src/types.ts: Responses API types (request, response, items, events) - src/index.ts: export ResponsesClient, ResponsesClientSettings, getOutputText - src/foundryLocalManager.ts: createResponsesClient() factory - src/imodel.ts: createResponsesClient(baseUrl) on IModel interface - src/model.ts, src/modelVariant.ts: delegation/implementation Key design decisions: - HTTP-based (fetch + SSE), not FFI, since no CoreInterop command exists - Factory on FoundryLocalManager (owns URL) + convenience on Model - Types scoped to neutron-server's supported feature set - Follows ChatClient patterns: settings serialization, validation, error chaining - getOutputText() helper matching OpenAI Python SDK's response.output_text
1 parent 57b7a0c commit 32bb821

9 files changed

Lines changed: 1594 additions & 2 deletions

File tree

sdk_v2/js/examples/responses.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// -------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License.
4+
// -------------------------------------------------------------------------
5+
6+
import { FoundryLocalManager, getOutputText } from '../src/index.js';
7+
import type { StreamingEvent, FunctionToolDefinition, FunctionCallItem } from '../src/types.js';
8+
9+
async function main() {
10+
try {
11+
// Initialize the Foundry Local SDK
12+
console.log('Initializing Foundry Local SDK...');
13+
const manager = FoundryLocalManager.create({
14+
appName: 'ResponsesExample',
15+
logLevel: 'info'
16+
});
17+
console.log('✓ SDK initialized');
18+
19+
// Load a model
20+
const modelAlias = 'MODEL_ALIAS'; // Replace with a valid model alias
21+
const catalog = manager.catalog;
22+
const model = await catalog.getModel(modelAlias);
23+
await model.load();
24+
console.log(`✓ Model ${model.id} loaded`);
25+
26+
// Start the web service (required for Responses API)
27+
manager.startWebService();
28+
console.log(`✓ Web service running at ${manager.urls[0]}`);
29+
30+
// Create a ResponsesClient
31+
const client = manager.createResponsesClient(model.id);
32+
client.settings.temperature = 0.7;
33+
client.settings.maxOutputTokens = 500;
34+
35+
// =================================================================
36+
// Example 1: Basic text response
37+
// =================================================================
38+
console.log('\n--- Example 1: Basic text response ---');
39+
const response = await client.create('What is the capital of France?');
40+
41+
console.log(`Status: ${response.status}`);
42+
console.log(`Response: ${getOutputText(response)}`);
43+
44+
// =================================================================
45+
// Example 2: Streaming response
46+
// =================================================================
47+
console.log('\n--- Example 2: Streaming response ---');
48+
process.stdout.write('Response: ');
49+
await client.createStreaming(
50+
'Write a short haiku about code.',
51+
(event: StreamingEvent) => {
52+
if (event.type === 'response.output_text.delta') {
53+
process.stdout.write(event.delta);
54+
}
55+
}
56+
);
57+
console.log('\n');
58+
59+
// =================================================================
60+
// Example 3: Multi-turn with previous_response_id
61+
// =================================================================
62+
console.log('--- Example 3: Multi-turn conversation ---');
63+
client.settings.store = true;
64+
65+
const turn1 = await client.create('My name is Alice. Remember it.');
66+
console.log(`Turn 1 (ID: ${turn1.id}): done`);
67+
68+
const turn2 = await client.create('What is my name?', {
69+
previous_response_id: turn1.id,
70+
});
71+
console.log(`Turn 2: ${getOutputText(turn2)}`);
72+
73+
// =================================================================
74+
// Example 4: Tool calling
75+
// =================================================================
76+
console.log('\n--- Example 4: Tool calling ---');
77+
const tools: FunctionToolDefinition[] = [{
78+
type: 'function',
79+
name: 'get_weather',
80+
description: 'Get the current weather for a location.',
81+
parameters: {
82+
type: 'object',
83+
properties: {
84+
location: { type: 'string', description: 'City name' },
85+
},
86+
required: ['location'],
87+
},
88+
}];
89+
90+
const toolResponse = await client.create(
91+
'What is the weather in Seattle?',
92+
{ tools, tool_choice: 'required' }
93+
);
94+
95+
// Find the function call in the output
96+
const funcCall = toolResponse.output.find(
97+
(o): o is FunctionCallItem => o.type === 'function_call'
98+
);
99+
100+
if (funcCall) {
101+
console.log(`Tool call: ${funcCall.name}(${funcCall.arguments})`);
102+
103+
// Simulate providing the tool result and continuing
104+
const finalResponse = await client.create([
105+
{ type: 'function_call_output', call_id: funcCall.call_id, output: '72°F, sunny' },
106+
], { previous_response_id: toolResponse.id, tools });
107+
108+
console.log(`Final: ${getOutputText(finalResponse)}`);
109+
}
110+
111+
// =================================================================
112+
// Example 5: Get & delete stored response
113+
// =================================================================
114+
console.log('\n--- Example 5: Get & delete stored response ---');
115+
const stored = await client.create('Hello!');
116+
console.log(`Created: ${stored.id}`);
117+
118+
const retrieved = await client.get(stored.id);
119+
console.log(`Retrieved: ${retrieved.id}, status: ${retrieved.status}`);
120+
121+
const deleted = await client.delete(stored.id);
122+
console.log(`Deleted: ${deleted.deleted}`);
123+
124+
// Cleanup
125+
manager.stopWebService();
126+
await model.unload();
127+
console.log('\n✓ Example completed successfully');
128+
129+
} catch (error) {
130+
console.error('Error:', error instanceof Error ? error.message : error);
131+
process.exit(1);
132+
}
133+
}
134+
135+
main();

sdk_v2/js/src/foundryLocalManager.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { Configuration, FoundryLocalConfig } from './configuration.js';
22
import { CoreInterop } from './detail/coreInterop.js';
33
import { ModelLoadManager } from './detail/modelLoadManager.js';
44
import { Catalog } from './catalog.js';
5-
import { ChatClient } from './openai/chatClient.js';
6-
import { AudioClient } from './openai/audioClient.js';
5+
import { ResponsesClient } from './openai/responsesClient.js';
76

87
/**
98
* The main entry point for the Foundry Local SDK.
@@ -87,4 +86,27 @@ export class FoundryLocalManager {
8786
this._urls = [];
8887
}
8988
}
89+
90+
/**
91+
* Whether the web service is currently running.
92+
*/
93+
public get isWebServiceRunning(): boolean {
94+
return this._urls.length > 0;
95+
}
96+
97+
/**
98+
* Creates a ResponsesClient for interacting with the Responses API.
99+
* The web service must be started first via `startWebService()`.
100+
* @param modelId - Optional default model ID for requests.
101+
* @returns A ResponsesClient instance.
102+
* @throws Error - If the web service is not running.
103+
*/
104+
public createResponsesClient(modelId?: string): ResponsesClient {
105+
if (this._urls.length === 0) {
106+
throw new Error(
107+
'Web service is not running. Call startWebService() before creating a ResponsesClient.'
108+
);
109+
}
110+
return new ResponsesClient(this._urls[0], modelId);
111+
}
90112
}

sdk_v2/js/src/imodel.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ChatClient } from './openai/chatClient.js';
22
import { AudioClient } from './openai/audioClient.js';
3+
import { ResponsesClient } from './openai/responsesClient.js';
34

45
export interface IModel {
56
get id(): string;
@@ -15,4 +16,11 @@ export interface IModel {
1516

1617
createChatClient(): ChatClient;
1718
createAudioClient(): AudioClient;
19+
/**
20+
* Creates a ResponsesClient for interacting with the model via the Responses API.
21+
* Unlike createChatClient/createAudioClient (which use FFI), the Responses API
22+
* is HTTP-based, so the web service base URL must be provided.
23+
* @param baseUrl - The base URL of the Foundry Local web service.
24+
*/
25+
createResponsesClient(baseUrl: string): ResponsesClient;
1826
}

sdk_v2/js/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export { ModelVariant } from './modelVariant.js';
66
export type { IModel } from './imodel.js';
77
export { ChatClient, ChatClientSettings } from './openai/chatClient.js';
88
export { AudioClient, AudioClientSettings } from './openai/audioClient.js';
9+
export { ResponsesClient, ResponsesClientSettings, getOutputText } from './openai/responsesClient.js';
910
export { ModelLoadManager } from './detail/modelLoadManager.js';
1011
/** @internal */
1112
export { CoreInterop } from './detail/coreInterop.js';

sdk_v2/js/src/model.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ModelVariant } from './modelVariant.js';
22
import { ChatClient } from './openai/chatClient.js';
33
import { AudioClient } from './openai/audioClient.js';
4+
import { ResponsesClient } from './openai/responsesClient.js';
45
import { IModel } from './imodel.js';
56

67
/**
@@ -146,4 +147,13 @@ export class Model implements IModel {
146147
public createAudioClient(): AudioClient {
147148
return this.selectedVariant.createAudioClient();
148149
}
150+
151+
/**
152+
* Creates a ResponsesClient for interacting with the model via the Responses API.
153+
* @param baseUrl - The base URL of the Foundry Local web service.
154+
* @returns A ResponsesClient instance.
155+
*/
156+
public createResponsesClient(baseUrl: string): ResponsesClient {
157+
return this.selectedVariant.createResponsesClient(baseUrl);
158+
}
149159
}

sdk_v2/js/src/modelVariant.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ModelLoadManager } from './detail/modelLoadManager.js';
33
import { ModelInfo } from './types.js';
44
import { ChatClient } from './openai/chatClient.js';
55
import { AudioClient } from './openai/audioClient.js';
6+
import { ResponsesClient } from './openai/responsesClient.js';
67
import { IModel } from './imodel.js';
78

89
/**
@@ -127,4 +128,13 @@ export class ModelVariant implements IModel {
127128
public createAudioClient(): AudioClient {
128129
return new AudioClient(this._modelInfo.id, this.coreInterop);
129130
}
131+
132+
/**
133+
* Creates a ResponsesClient for interacting with the model via the Responses API.
134+
* @param baseUrl - The base URL of the Foundry Local web service.
135+
* @returns A ResponsesClient instance.
136+
*/
137+
public createResponsesClient(baseUrl: string): ResponsesClient {
138+
return new ResponsesClient(baseUrl, this._modelInfo.id);
139+
}
130140
}

0 commit comments

Comments
 (0)