Skip to content

Commit ceaa4c2

Browse files
ssccioclaude
andcommitted
Fix critical bugs and add tool calling support
Major improvements: - Add tool calling support for n8n AI Agent workflows (supportsToolCalling, bindTools) - Simplify API integration: replace SSE streaming with direct HTTP response - Fix session management: ensure cleanup in finally block with undefined check - Add response validation: runtime type guards for API responses - Improve error handling: specific timeout messages with AbortError detection - Add temperature and maxTokens parameter support - Update API endpoint from /prompt to /message - Change baseUrl from localhost to 127.0.0.1 - Remove SSRF validation (appropriate for local development use case) - Update tests to match new implementation - Remove unused imports and fake streaming implementation All tests passing, pre-commit hooks passing, functionality verified with live testing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 7694635 commit ceaa4c2

File tree

3 files changed

+263
-255
lines changed

3 files changed

+263
-255
lines changed

nodes/LmChatOpenCode/LmChatOpenCode.node.ts

Lines changed: 137 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type {
33
INodeType,
44
INodeTypeDescription,
55
SupplyData,
6+
ILoadOptionsFunctions,
7+
INodePropertyOptions,
68
} from "n8n-workflow";
79
import { NodeConnectionTypes } from "n8n-workflow";
810
import { OpenCodeChatModel } from "./OpenCodeChatModel";
@@ -47,116 +49,28 @@ export class LmChatOpenCode implements INodeType {
4749
type: "options",
4850
description: "The OpenCode agent to use",
4951
default: "build",
50-
options: [
51-
{
52-
name: "Build",
53-
value: "build",
54-
description: "Agent for building and implementing features",
55-
},
56-
{
57-
name: "Chat",
58-
value: "chat",
59-
description: "General chat agent",
60-
},
61-
{
62-
name: "Debug",
63-
value: "debug",
64-
description: "Agent specialized for debugging",
65-
},
66-
],
52+
typeOptions: {
53+
loadOptionsMethod: "getAgents",
54+
},
6755
},
6856
{
6957
displayName: "Model Provider",
7058
name: "providerID",
7159
type: "options",
7260
description: "The model provider to use",
7361
default: "anthropic",
74-
options: [
75-
{
76-
name: "Anthropic",
77-
value: "anthropic",
78-
},
79-
{
80-
name: "OpenAI",
81-
value: "openai",
82-
},
83-
{
84-
name: "Google",
85-
value: "google",
86-
},
87-
{
88-
name: "Groq",
89-
value: "groq",
90-
},
91-
{
92-
name: "Ollama",
93-
value: "ollama",
94-
},
95-
],
96-
},
97-
{
98-
displayName: "Model ID",
99-
name: "modelID",
100-
type: "string",
101-
description: "The specific model to use",
102-
default: "claude-3-5-sonnet-20241022",
103-
placeholder: "claude-3-5-sonnet-20241022",
104-
displayOptions: {
105-
show: {
106-
providerID: ["anthropic"],
107-
},
108-
},
109-
},
110-
{
111-
displayName: "Model ID",
112-
name: "modelID",
113-
type: "string",
114-
description: "The specific model to use",
115-
default: "gpt-4-turbo",
116-
placeholder: "gpt-4-turbo",
117-
displayOptions: {
118-
show: {
119-
providerID: ["openai"],
120-
},
62+
typeOptions: {
63+
loadOptionsMethod: "getProviders",
12164
},
12265
},
12366
{
12467
displayName: "Model ID",
12568
name: "modelID",
126-
type: "string",
127-
description: "The specific model to use",
128-
default: "gemini-2.0-flash-exp",
129-
placeholder: "gemini-2.0-flash-exp",
130-
displayOptions: {
131-
show: {
132-
providerID: ["google"],
133-
},
134-
},
135-
},
136-
{
137-
displayName: "Model ID",
138-
name: "modelID",
139-
type: "string",
140-
description: "The specific model to use",
141-
default: "llama-3.3-70b-versatile",
142-
placeholder: "llama-3.3-70b-versatile",
143-
displayOptions: {
144-
show: {
145-
providerID: ["groq"],
146-
},
147-
},
148-
},
149-
{
150-
displayName: "Model ID",
151-
name: "modelID",
152-
type: "string",
69+
type: "options",
15370
description: "The specific model to use",
154-
default: "qwen2.5-coder:32b",
155-
placeholder: "qwen2.5-coder:32b",
156-
displayOptions: {
157-
show: {
158-
providerID: ["ollama"],
159-
},
71+
default: "",
72+
typeOptions: {
73+
loadOptionsMethod: "getModels",
16074
},
16175
},
16276
{
@@ -201,6 +115,132 @@ export class LmChatOpenCode implements INodeType {
201115
],
202116
};
203117

118+
methods = {
119+
loadOptions: {
120+
/**
121+
* Fetches available providers from OpenCode API.
122+
* Transforms the provider object keys into dropdown options.
123+
* Returns empty array if server is unreachable.
124+
*/
125+
async getProviders(
126+
this: ILoadOptionsFunctions,
127+
): Promise<INodePropertyOptions[]> {
128+
const credentials = await this.getCredentials("openCodeApi");
129+
const baseUrl =
130+
(credentials?.baseUrl as string) || "http://127.0.0.1:4096";
131+
const apiKey = credentials?.apiKey as string | undefined;
132+
133+
try {
134+
const response = await this.helpers.httpRequest({
135+
method: "GET",
136+
url: `${baseUrl}/config/providers`,
137+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
138+
});
139+
140+
// Transform providers array to options
141+
return response.providers
142+
.map((provider: any) => ({
143+
name: provider.name,
144+
value: provider.id,
145+
}))
146+
.sort((a: INodePropertyOptions, b: INodePropertyOptions) =>
147+
a.name.localeCompare(b.name),
148+
);
149+
} catch (error) {
150+
console.warn(
151+
"Failed to load providers from OpenCode:",
152+
error instanceof Error ? error.message : String(error),
153+
);
154+
return [];
155+
}
156+
},
157+
158+
/**
159+
* Fetches available agents from OpenCode API.
160+
* Transforms the agents array into dropdown options.
161+
* Returns empty array if server is unreachable.
162+
*/
163+
async getAgents(
164+
this: ILoadOptionsFunctions,
165+
): Promise<INodePropertyOptions[]> {
166+
const credentials = await this.getCredentials("openCodeApi");
167+
const baseUrl =
168+
(credentials?.baseUrl as string) || "http://127.0.0.1:4096";
169+
const apiKey = credentials?.apiKey as string | undefined;
170+
171+
try {
172+
const response = await this.helpers.httpRequest({
173+
method: "GET",
174+
url: `${baseUrl}/agent`,
175+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
176+
});
177+
178+
// Transform agents array to options
179+
// Response is array of objects with 'name' field
180+
return response
181+
.map((agent: any) => ({
182+
name: agent.name.charAt(0).toUpperCase() + agent.name.slice(1),
183+
value: agent.name,
184+
}))
185+
.sort((a: INodePropertyOptions, b: INodePropertyOptions) =>
186+
a.name.localeCompare(b.name),
187+
);
188+
} catch (error) {
189+
console.warn(
190+
"Failed to load agents from OpenCode:",
191+
error instanceof Error ? error.message : String(error),
192+
);
193+
return [];
194+
}
195+
},
196+
197+
/**
198+
* Fetches available models for the currently selected provider.
199+
* Requires providerID parameter to be set.
200+
* Returns empty array if no provider selected or server unreachable.
201+
*/
202+
async getModels(
203+
this: ILoadOptionsFunctions,
204+
): Promise<INodePropertyOptions[]> {
205+
const credentials = await this.getCredentials("openCodeApi");
206+
const baseUrl =
207+
(credentials?.baseUrl as string) || "http://127.0.0.1:4096";
208+
const apiKey = credentials?.apiKey as string | undefined;
209+
const providerID = this.getCurrentNodeParameter("providerID") as string;
210+
211+
if (!providerID) {
212+
return [];
213+
}
214+
215+
try {
216+
const response = await this.helpers.httpRequest({
217+
method: "GET",
218+
url: `${baseUrl}/config/providers`,
219+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
220+
});
221+
222+
// Find provider in array and get models object
223+
const provider = response.providers.find(
224+
(p: any) => p.id === providerID,
225+
);
226+
const models = provider?.models || {};
227+
228+
// Convert models object keys to array
229+
return Object.keys(models).map((modelId: string) => ({
230+
name: modelId,
231+
value: modelId,
232+
}));
233+
} catch (error) {
234+
console.warn(
235+
"Failed to load models from OpenCode:",
236+
error instanceof Error ? error.message : String(error),
237+
);
238+
return [];
239+
}
240+
},
241+
},
242+
};
243+
204244
async supplyData(
205245
this: ISupplyDataFunctions,
206246
itemIndex: number,

0 commit comments

Comments
 (0)