Skip to content

Commit ee28b9d

Browse files
rmarquisbchapuis
andauthored
Add nodes for the main OpenAI models (#141)
* feat(api): add OpenAI SDK dependency and environment configuration - Add OPENAI_API_KEY to .dev.vars.example following established pattern - Prepare infrastructure for OpenAI node implementation * feat(api): implement OpenAI model nodes for GPT-4.1, GPT-5, GPT-5 Mini, and GPT-5 Nano - Add GPT-4.1 node with compute cost 25 - Add GPT-5 node with compute cost 35 - Add GPT-5 Mini node with compute cost 15 - Add GPT-5 Nano node with compute cost 5 - All nodes follow established patterns with proper error handling - Uses OpenAI SDK for chat completions API - Supports system instructions and user input parameters * feat(api): integrate OpenAI nodes into CloudflareNodeRegistry - Add imports for GPT-4.1, GPT-5, GPT-5 Mini, and GPT-5 Nano nodes - Add hasOpenAI environment check for OPENAI_API_KEY - Register OpenAI nodes conditionally when API key is available - Follow existing Anthropic node registration patterns - Add OPENAI_API_KEY env var to runtime context * test(api): add comprehensive unit tests for OpenAI nodes and fix runtime context - Add unit tests for GPT-4.1, GPT-5, GPT-5 Mini, and GPT-5 Nano nodes - Each test suite validates node type definitions, instantiation, and error handling - Test coverage includes API key validation, input validation, and parameter definitions - Fix runtime context to pass OPENAI_API_KEY to node execution environment - All tests pass successfully, completing Phase 4 of OpenAI integration --------- Co-authored-by: Bertil Chapuis <bchapuis@gmail.com>
1 parent 1108ec8 commit ee28b9d

15 files changed

Lines changed: 726 additions & 1 deletion

apps/api/.dev.vars.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ RESEND_DEFAULT_FROM=CHANGE_ME
2828
AWS_ACCESS_KEY_ID=CHANGE_ME
2929
AWS_SECRET_ACCESS_KEY=CHANGE_ME
3030
AWS_REGION=CHANGE_ME
31+
3132
SES_DEFAULT_FROM=CHANGE_ME
3233

33-
ANTHROPIC_API_KEY=CHANGE_ME
34+
OPENAI_API_KEY=CHANGE_ME
35+
36+
ANTHROPIC_API_KEY=CHANGE_ME

apps/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"jose": "^6.0.10",
6464
"jsonpath-plus": "^10.3.0",
6565
"mailparser": "^3.7.2",
66+
"openai": "^5.13.1",
6667
"resend": "^4.5.1",
6768
"twilio": "^5.5.2",
6869
"uuid": "^11.1.0",

apps/api/src/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface Bindings {
3737
AWS_SECRET_ACCESS_KEY?: string;
3838
AWS_REGION?: string;
3939
SES_DEFAULT_FROM?: string;
40+
OPENAI_API_KEY?: string;
4041
ANTHROPIC_API_KEY?: string;
4142
}
4243

apps/api/src/nodes/cloudflare-node-registry.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ import { Hermes2ProMistral7BNode } from "./text/hermes-2-pro-mistral-7b-node";
213213
import { InputTextNode } from "./text/input-text-node";
214214
import { Llama318BInstructFastNode } from "./text/llama-3-1-8b-instruct-fast-node";
215215
import { Llama3370BInstructFastNode } from "./text/llama-3-3-70b-instruct-fp8-fast-node";
216+
import { Gpt41Node } from "./text/gpt-41-node";
217+
import { Gpt5Node } from "./text/gpt-5-node";
218+
import { Gpt5MiniNode } from "./text/gpt-5-mini-node";
219+
import { Gpt5NanoNode } from "./text/gpt-5-nano-node";
216220
import { GptOss120BNode } from "./text/gpt-oss-120b-node";
217221
import { GptOss20BNode } from "./text/gpt-oss-20b-node";
218222
import { Llama4Scout17B16EInstructNode } from "./text/llama-4-scout-17b-16e-instruct-node";
@@ -268,6 +272,7 @@ export class CloudflareNodeRegistry extends BaseNodeRegistry {
268272
this.env.AWS_REGION &&
269273
this.env.SES_DEFAULT_FROM
270274
);
275+
const hasOpenAI = !!this.env.OPENAI_API_KEY;
271276
const hasAnthropic = !!this.env.ANTHROPIC_API_KEY;
272277

273278
// Register all core nodes
@@ -542,6 +547,14 @@ export class CloudflareNodeRegistry extends BaseNodeRegistry {
542547
this.registerImplementation(WktGeometryNode);
543548
}
544549

550+
// OpenAI models
551+
if (hasOpenAI) {
552+
this.registerImplementation(Gpt41Node);
553+
this.registerImplementation(Gpt5Node);
554+
this.registerImplementation(Gpt5MiniNode);
555+
this.registerImplementation(Gpt5NanoNode);
556+
}
557+
545558
// Anthropic Claude nodes
546559
if (hasAnthropic) {
547560
this.registerImplementation(ClaudeOpus41Node);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Node } from "@dafthunk/types";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { NodeContext } from "../types";
5+
import { Gpt41Node } from "./gpt-41-node";
6+
7+
describe("Gpt41Node", () => {
8+
const mockNode = {
9+
id: "test-node",
10+
type: "gpt-4.1",
11+
} as Node;
12+
13+
it("should have correct node type definition", () => {
14+
expect(Gpt41Node.nodeType).toBeDefined();
15+
expect(Gpt41Node.nodeType.id).toBe("gpt-4.1");
16+
expect(Gpt41Node.nodeType.name).toBe("GPT-4.1");
17+
expect(Gpt41Node.nodeType.type).toBe("gpt-4.1");
18+
expect(Gpt41Node.nodeType.description).toBe("Latest GPT-4 iteration with enhanced capabilities");
19+
expect(Gpt41Node.nodeType.tags).toEqual(["Text", "AI"]);
20+
expect(Gpt41Node.nodeType.computeCost).toBe(25);
21+
expect(Gpt41Node.nodeType.inputs).toHaveLength(2);
22+
expect(Gpt41Node.nodeType.outputs).toHaveLength(1);
23+
});
24+
25+
it("should instantiate correctly", () => {
26+
const node = new Gpt41Node(mockNode);
27+
expect(node).toBeDefined();
28+
});
29+
30+
it("should fail without API key", async () => {
31+
const node = new Gpt41Node(mockNode);
32+
const context = {
33+
nodeId: "test-node",
34+
workflowId: "test-workflow",
35+
organizationId: "test-org",
36+
inputs: { input: "Hello" },
37+
env: {},
38+
} as NodeContext;
39+
40+
const result = await node.execute(context);
41+
expect(result.status).toBe("error");
42+
expect(result.error).toBe("OPENAI_API_KEY is not configured");
43+
});
44+
45+
it("should fail without input", async () => {
46+
const node = new Gpt41Node(mockNode);
47+
const context = {
48+
nodeId: "test-node",
49+
workflowId: "test-workflow",
50+
organizationId: "test-org",
51+
inputs: {},
52+
env: {
53+
OPENAI_API_KEY: "test-key"
54+
},
55+
} as NodeContext;
56+
57+
const result = await node.execute(context);
58+
expect(result.status).toBe("error");
59+
expect(result.error).toBe("Input is required");
60+
});
61+
62+
it("should have correct input parameters", () => {
63+
const inputs = Gpt41Node.nodeType.inputs;
64+
65+
const instructionsInput = inputs.find(input => input.name === "instructions");
66+
expect(instructionsInput).toBeDefined();
67+
expect(instructionsInput?.type).toBe("string");
68+
expect(instructionsInput?.required).toBe(false);
69+
expect(instructionsInput?.value).toBe("You are a helpful assistant.");
70+
71+
const inputParam = inputs.find(input => input.name === "input");
72+
expect(inputParam).toBeDefined();
73+
expect(inputParam?.type).toBe("string");
74+
expect(inputParam?.required).toBe(true);
75+
});
76+
77+
it("should have correct output parameters", () => {
78+
const outputs = Gpt41Node.nodeType.outputs;
79+
80+
const responseOutput = outputs.find(output => output.name === "response");
81+
expect(responseOutput).toBeDefined();
82+
expect(responseOutput?.type).toBe("string");
83+
expect(responseOutput?.description).toBe("Generated text response from GPT-4.1");
84+
});
85+
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import OpenAI from 'openai';
2+
import { NodeExecution, NodeType } from "@dafthunk/types";
3+
4+
import { ExecutableNode } from "../types";
5+
import { NodeContext } from "../types";
6+
7+
/**
8+
* GPT-4.1 node implementation using the OpenAI SDK
9+
* Latest GPT-4 iteration with enhanced capabilities
10+
*/
11+
export class Gpt41Node extends ExecutableNode {
12+
public static readonly nodeType: NodeType = {
13+
id: "gpt-4.1",
14+
name: "GPT-4.1",
15+
type: "gpt-4.1",
16+
description: "Latest GPT-4 iteration with enhanced capabilities",
17+
tags: ["Text", "AI"],
18+
icon: "sparkles",
19+
computeCost: 25,
20+
asTool: true,
21+
inputs: [
22+
{
23+
name: "instructions",
24+
type: "string",
25+
description: "System instructions for GPT-4.1's behavior",
26+
required: false,
27+
value: "You are a helpful assistant.",
28+
},
29+
{
30+
name: "input",
31+
type: "string",
32+
description: "The input text or question for GPT-4.1",
33+
required: true,
34+
},
35+
],
36+
outputs: [
37+
{
38+
name: "response",
39+
type: "string",
40+
description: "Generated text response from GPT-4.1",
41+
},
42+
],
43+
};
44+
45+
async execute(context: NodeContext): Promise<NodeExecution> {
46+
try {
47+
const { instructions, input } = context.inputs;
48+
49+
if (!context.env.OPENAI_API_KEY) {
50+
return this.createErrorResult("OPENAI_API_KEY is not configured");
51+
}
52+
53+
if (!input) {
54+
return this.createErrorResult("Input is required");
55+
}
56+
57+
const client = new OpenAI({
58+
apiKey: context.env.OPENAI_API_KEY,
59+
timeout: 60000
60+
});
61+
62+
const completion = await client.chat.completions.create({
63+
model: "gpt-4.1",
64+
max_tokens: 1024,
65+
messages: [
66+
...(instructions ? [{ role: "system" as const, content: instructions }] : []),
67+
{ role: "user" as const, content: input }
68+
]
69+
});
70+
71+
const responseText = completion.choices[0]?.message?.content || "";
72+
73+
return this.createSuccessResult({
74+
response: responseText,
75+
});
76+
} catch (error) {
77+
console.error(error);
78+
if (error instanceof OpenAI.APIError) {
79+
return this.createErrorResult(`OpenAI API error: ${error.message}`);
80+
}
81+
return this.createErrorResult(
82+
error instanceof Error ? error.message : "Unknown error"
83+
);
84+
}
85+
}
86+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Node } from "@dafthunk/types";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { NodeContext } from "../types";
5+
import { Gpt5MiniNode } from "./gpt-5-mini-node";
6+
7+
describe("Gpt5MiniNode", () => {
8+
const mockNode = {
9+
id: "test-node",
10+
type: "gpt-5-mini",
11+
} as Node;
12+
13+
it("should have correct node type definition", () => {
14+
expect(Gpt5MiniNode.nodeType).toBeDefined();
15+
expect(Gpt5MiniNode.nodeType.id).toBe("gpt-5-mini");
16+
expect(Gpt5MiniNode.nodeType.name).toBe("GPT-5 Mini");
17+
expect(Gpt5MiniNode.nodeType.type).toBe("gpt-5-mini");
18+
expect(Gpt5MiniNode.nodeType.description).toBe("Faster, cost-effective version of GPT-5");
19+
expect(Gpt5MiniNode.nodeType.tags).toEqual(["Text", "AI"]);
20+
expect(Gpt5MiniNode.nodeType.computeCost).toBe(15);
21+
expect(Gpt5MiniNode.nodeType.inputs).toHaveLength(2);
22+
expect(Gpt5MiniNode.nodeType.outputs).toHaveLength(1);
23+
});
24+
25+
it("should instantiate correctly", () => {
26+
const node = new Gpt5MiniNode(mockNode);
27+
expect(node).toBeDefined();
28+
});
29+
30+
it("should fail without API key", async () => {
31+
const node = new Gpt5MiniNode(mockNode);
32+
const context = {
33+
nodeId: "test-node",
34+
workflowId: "test-workflow",
35+
organizationId: "test-org",
36+
inputs: { input: "Hello" },
37+
env: {},
38+
} as NodeContext;
39+
40+
const result = await node.execute(context);
41+
expect(result.status).toBe("error");
42+
expect(result.error).toBe("OPENAI_API_KEY is not configured");
43+
});
44+
45+
it("should fail without input", async () => {
46+
const node = new Gpt5MiniNode(mockNode);
47+
const context = {
48+
nodeId: "test-node",
49+
workflowId: "test-workflow",
50+
organizationId: "test-org",
51+
inputs: {},
52+
env: {
53+
OPENAI_API_KEY: "test-key"
54+
},
55+
} as NodeContext;
56+
57+
const result = await node.execute(context);
58+
expect(result.status).toBe("error");
59+
expect(result.error).toBe("Input is required");
60+
});
61+
62+
it("should have correct input parameters", () => {
63+
const inputs = Gpt5MiniNode.nodeType.inputs;
64+
65+
const instructionsInput = inputs.find(input => input.name === "instructions");
66+
expect(instructionsInput).toBeDefined();
67+
expect(instructionsInput?.type).toBe("string");
68+
expect(instructionsInput?.required).toBe(false);
69+
expect(instructionsInput?.value).toBe("You are a helpful assistant.");
70+
71+
const inputParam = inputs.find(input => input.name === "input");
72+
expect(inputParam).toBeDefined();
73+
expect(inputParam?.type).toBe("string");
74+
expect(inputParam?.required).toBe(true);
75+
});
76+
77+
it("should have correct output parameters", () => {
78+
const outputs = Gpt5MiniNode.nodeType.outputs;
79+
80+
const responseOutput = outputs.find(output => output.name === "response");
81+
expect(responseOutput).toBeDefined();
82+
expect(responseOutput?.type).toBe("string");
83+
expect(responseOutput?.description).toBe("Generated text response from GPT-5 Mini");
84+
});
85+
});

0 commit comments

Comments
 (0)