diff --git a/examples/openai-responses-integration.ts b/examples/openai-responses-integration.ts new file mode 100644 index 0000000..54f1b50 --- /dev/null +++ b/examples/openai-responses-integration.ts @@ -0,0 +1,60 @@ +/** + * This example shows how to use StackOne tools with OpenAI's Responses API. + */ + +import assert from 'node:assert'; +import process from 'node:process'; +import { StackOneToolSet } from '@stackone/ai'; +import OpenAI from 'openai'; + +const apiKey = process.env.STACKONE_API_KEY; +if (!apiKey) { + console.error('STACKONE_API_KEY environment variable is required'); + process.exit(1); +} + +// Replace with your actual account ID from StackOne dashboard +const accountId = 'your-stackone-account-id'; + +const openaiResponsesIntegration = async (): Promise => { + // Initialise StackOne + const toolset = new StackOneToolSet({ accountId }); + + // Fetch HRIS tools via MCP + const tools = await toolset.fetchTools({ + actions: ['_list_*'], + }); + const openAIResponsesTools = tools.toOpenAIResponses(); + + // Initialise OpenAI client + const openai = new OpenAI(); + + // Create a response with tool calls using the Responses API + const response = await openai.responses.create({ + model: 'gpt-5.1', + instructions: 'You are a helpful assistant that can access various tools.', + input: 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?', + tools: openAIResponsesTools, + }); + + // Verify the response contains expected data + assert(response.id, 'Expected response to have an ID'); + assert(response.model, 'Expected response to have a model'); + + // Check if the model made any tool calls + const toolCalls = response.output.filter( + (item): item is OpenAI.Responses.ResponseFunctionToolCall => item.type === 'function_call', + ); + + assert(toolCalls.length > 0, 'Expected at least one tool call'); + + const toolCall = toolCalls[0]; + assert(toolCall.name === 'hris_get_employee', 'Expected tool call to be hris_get_employee'); + + // Parse the arguments to verify they contain the expected fields + const args = JSON.parse(toolCall.arguments); + assert(args.id === 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', 'Expected id to match the query'); +}; + +// Run the example +await openaiResponsesIntegration(); diff --git a/src/tool.test.ts b/src/tool.test.ts index 14e9cde..1fa00c2 100644 --- a/src/tool.test.ts +++ b/src/tool.test.ts @@ -55,7 +55,7 @@ describe('StackOneTool', () => { }); }); - it('should convert to OpenAI tool format', () => { + it('should convert to OpenAI Chat Completions API tool format', () => { const tool = createMockTool(); const openAIFormat = tool.toOpenAI(); @@ -72,6 +72,41 @@ describe('StackOneTool', () => { ).toBe('string'); }); + it('should convert to OpenAI Responses API tool format', () => { + const tool = createMockTool(); + const responsesFormat = tool.toOpenAIResponses(); + + expect(responsesFormat.type).toBe('function'); + expect(responsesFormat.name).toBe('test_tool'); + expect(responsesFormat.description).toBe('Test tool'); + expect(responsesFormat.strict).toBe(true); + expect(responsesFormat.parameters?.type).toBe('object'); + expect( + ( + responsesFormat.parameters as { + properties: { id: { type: string } }; + additionalProperties: boolean; + } + ).properties.id.type, + ).toBe('string'); + expect( + (responsesFormat.parameters as { additionalProperties: boolean }).additionalProperties, + ).toBe(false); + }); + + it('should convert to OpenAI Responses API tool format with strict disabled', () => { + const tool = createMockTool(); + const responsesFormat = tool.toOpenAIResponses({ strict: false }); + + expect(responsesFormat.type).toBe('function'); + expect(responsesFormat.name).toBe('test_tool'); + expect(responsesFormat.strict).toBe(false); + expect(responsesFormat.parameters).toBeDefined(); + expect( + (responsesFormat.parameters as { additionalProperties?: boolean }).additionalProperties, + ).toBeUndefined(); + }); + it('should convert to AI SDK tool format', async () => { const tool = createMockTool(); @@ -317,6 +352,58 @@ describe('Tools', () => { expect(openAITools[1].function.name).toBe('tool2'); }); + it('should convert all tools to OpenAI Responses API tools', () => { + const tool1 = new StackOneTool( + 'tool1', + 'Tool 1', + { + type: 'object', + properties: { id: { type: 'string' } }, + }, + { + kind: 'http', + method: 'GET', + url: 'https://api.example.com/test/{id}', + bodyType: 'json', + params: [], + }, + ); + const tool2 = new StackOneTool( + 'tool2', + 'Tool 2', + { + type: 'object', + properties: { name: { type: 'string' } }, + }, + { + kind: 'http', + method: 'POST', + url: 'https://api.example.com/test', + bodyType: 'json', + params: [], + }, + ); + + const tools = new Tools([tool1, tool2]); + const responsesTools = tools.toOpenAIResponses(); + + expect(responsesTools).toBeInstanceOf(Array); + expect(responsesTools.length).toBe(2); + expect(responsesTools[0].type).toBe('function'); + expect(responsesTools[0].name).toBe('tool1'); + expect(responsesTools[0].strict).toBe(true); + expect(responsesTools[1].name).toBe('tool2'); + expect(responsesTools[1].strict).toBe(true); + }); + + it('should convert all tools to OpenAI Responses API tools with strict disabled', () => { + const tool1 = createMockTool(); + const tools = new Tools([tool1]); + const responsesTools = tools.toOpenAIResponses({ strict: false }); + + expect(responsesTools[0].strict).toBe(false); + }); + it('should convert all tools to AI SDK tools', async () => { const tool1 = createMockTool(); const tool2 = new StackOneTool( diff --git a/src/tool.ts b/src/tool.ts index 5ecdd97..f60ada5 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -1,5 +1,6 @@ import * as orama from '@orama/orama'; import type { ChatCompletionFunctionTool } from 'openai/resources/chat/completions'; +import type { FunctionTool as OpenAIResponsesFunctionTool } from 'openai/resources/responses/responses'; import { DEFAULT_HYBRID_ALPHA } from './consts'; import { RequestBuilder } from './requestBuilder'; import type { @@ -164,7 +165,7 @@ export class BaseTool { } /** - * Convert the tool to OpenAI format + * Convert the tool to OpenAI Chat Completions API format */ toOpenAI(): ChatCompletionFunctionTool { return { @@ -181,6 +182,26 @@ export class BaseTool { }; } + /** + * Convert the tool to OpenAI Responses API format + * @see https://platform.openai.com/docs/api-reference/responses + */ + toOpenAIResponses(options: { strict?: boolean } = {}): OpenAIResponsesFunctionTool { + const { strict = true } = options; + return { + type: 'function', + name: this.name, + description: this.description, + strict, + parameters: { + type: 'object', + properties: this.parameters.properties, + required: this.parameters.required, + ...(strict ? { additionalProperties: false } : {}), + }, + }; + } + /** * Convert the tool to AI SDK format */ @@ -352,12 +373,20 @@ export class Tools implements Iterable { } /** - * Convert all tools to OpenAI format + * Convert all tools to OpenAI Chat Completions API format */ toOpenAI(): ChatCompletionFunctionTool[] { return this.tools.map((tool) => tool.toOpenAI()); } + /** + * Convert all tools to OpenAI Responses API format + * @see https://platform.openai.com/docs/api-reference/responses + */ + toOpenAIResponses(options: { strict?: boolean } = {}): OpenAIResponsesFunctionTool[] { + return this.tools.map((tool) => tool.toOpenAIResponses(options)); + } + /** * Convert all tools to AI SDK format */