Skip to content

Commit 6aaa996

Browse files
committed
fix(ai): Resolve MCP tool issue mapping to gh cli & update OpenAiCompatible to ES6 (#9767) (#9766)
1 parent 4fc4a47 commit 6aaa996

3 files changed

Lines changed: 69 additions & 14 deletions

File tree

ai/mcp/server/github-workflow/services/IssueService.mjs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class IssueService extends Base {
8787
* @returns {Promise<object>}
8888
*/
8989
async assignIssue({issue_number, assignees}) {
90-
if (!this.hasWritePermission()) {
90+
if (!await this.hasWritePermission()) {
9191
const message = [
9292
`Permission denied. Viewer has '${RepositoryService.viewerPermission}' permission, `,
9393
`but one of [${this.writePermissions.join(', ')}] is required to assign issues.`
@@ -108,12 +108,12 @@ class IssueService extends Base {
108108
if (assignees?.length > 0) {
109109
logger.info(`Attempting to assign issue #${issue_number} to: ${assignees.join(', ')}`);
110110
const assigneeFlags = assignees.map(a => `--add-assignee "${a}"`).join(' ');
111-
command = `gh issue edit ${issue_number} ${assigneeFlags}`;
111+
command = `gh issue edit ${issue_number} ${assigneeFlags} --repo ${aiConfig.owner}/${aiConfig.repo}`;
112112
successMessage = `Successfully assigned issue #${issue_number} to ${assignees.join(', ')}`;
113113
} else {
114114
logger.info(`Attempting to unassign all users from issue #${issue_number}`);
115115
// Passing an empty string to --remove-assignee has been experimentally verified to clear all assignees.
116-
command = `gh issue edit ${issue_number} --remove-assignee ""`;
116+
command = `gh issue edit ${issue_number} --remove-assignee "" --repo ${aiConfig.owner}/${aiConfig.repo}`;
117117
successMessage = `Successfully unassigned all users from issue #${issue_number}`;
118118
}
119119

@@ -220,7 +220,7 @@ class IssueService extends Base {
220220

221221
// Permission check is only required if we are trying to assign users.
222222
if (assignees && assignees.length > 0) {
223-
if (!this.hasWritePermission()) {
223+
if (!await this.hasWritePermission()) {
224224
const message = [
225225
`Permission denied. Viewer has '${RepositoryService.viewerPermission}' permission, `,
226226
`but one of [${this.writePermissions.join(', ')}] is required to assign issues.`
@@ -312,10 +312,11 @@ class IssueService extends Base {
312312

313313
/**
314314
* Convenience shortcut
315-
* @returns {Boolean}
315+
* @returns {Promise<Boolean>}
316316
*/
317-
hasWritePermission() {
318-
return this.writePermissions.includes(RepositoryService.viewerPermission);
317+
async hasWritePermission() {
318+
const {permission} = await RepositoryService.getViewerPermission();
319+
return this.writePermissions.includes(permission);
319320
}
320321

321322
/**
@@ -417,7 +418,7 @@ class IssueService extends Base {
417418
* @returns {Promise<object>}
418419
*/
419420
async unassignIssue({issue_number, assignees}) {
420-
if (!this.hasWritePermission()) {
421+
if (!await this.hasWritePermission()) {
421422
const message = [
422423
`Permission denied. Viewer has '${RepositoryService.viewerPermission}' permission, `,
423424
`but one of [${this.writePermissions.join(', ')}] is required to unassign issues.`
@@ -443,7 +444,7 @@ class IssueService extends Base {
443444

444445
try {
445446
const assigneeFlags = assignees.map(a => `--remove-assignee "${a}"`).join(' ');
446-
const command = `gh issue edit ${issue_number} ${assigneeFlags}`;
447+
const command = `gh issue edit ${issue_number} ${assigneeFlags} --repo ${aiConfig.owner}/${aiConfig.repo}`;
447448

448449
await execAsync(command);
449450

ai/mcp/server/memory-core/services/SessionService.mjs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,60 @@ class SessionService extends Base {
140140
});
141141
}
142142
};
143+
} else if (aiConfig.modelProvider === 'openAiCompatible') {
144+
logger.info(`[SessionService] Initializing generation model via OpenAI-Compatible API (${aiConfig.openAiCompatible.model})`);
145+
this.model = {
146+
generateContent: async (promptText) => {
147+
logger.info(`[OpenAiCompatible] Sending summarization prompt (${promptText.length} chars)`);
148+
return new Promise((resolve, reject) => {
149+
const url = new URL(`${aiConfig.openAiCompatible.host}/v1/chat/completions`);
150+
const client = url.protocol === 'https:' ? https : http;
151+
const payload = {
152+
model: aiConfig.openAiCompatible.model,
153+
messages: [{ role: 'user', content: promptText }],
154+
stream: false
155+
};
156+
const bodyData = Buffer.from(JSON.stringify(payload), 'utf8');
157+
158+
const headers = {
159+
'Content-Type': 'application/json',
160+
'Content-Length': bodyData.length
161+
};
162+
163+
if (aiConfig.openAiCompatible.apiKey) {
164+
headers['Authorization'] = `Bearer ${aiConfig.openAiCompatible.apiKey}`;
165+
}
166+
167+
const req = client.request(url, {
168+
method: 'POST',
169+
headers,
170+
timeout: 60 * 60 * 1000 // 1 hour timeout
171+
}, (res) => {
172+
let chunks = [];
173+
res.on('data', c => chunks.push(c));
174+
res.on('end', () => {
175+
try {
176+
const rawStr = Buffer.concat(chunks).toString('utf8');
177+
if (res.statusCode < 200 || res.statusCode >= 300) {
178+
logger.error(`[OpenAiCompatible] Error ${res.statusCode}: ${rawStr}`);
179+
return reject(new Error(`OpenAiCompatible Status ${res.statusCode}: ${rawStr}`));
180+
}
181+
const data = JSON.parse(rawStr);
182+
const content = data.choices?.[0]?.message?.content || '';
183+
resolve({ response: { text: () => content } });
184+
} catch (e) {
185+
reject(new Error(`OpenAiCompatible Parser Error: ${e.message}`));
186+
}
187+
});
188+
});
189+
190+
req.on('error', e => reject(e));
191+
req.on('timeout', () => { req.destroy(); reject(new Error('OpenAiCompatible Request Timeout')); });
192+
req.write(bodyData);
193+
req.end();
194+
});
195+
}
196+
};
143197
} else {
144198
logger.info(`[SessionService] Initializing generation model via Gemini (${aiConfig.modelName})`);
145199
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);

ai/provider/OpenAiCompatible.mjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class OpenAiCompatibleProvider extends Base {
8282
delete clonedOptions.response_format;
8383
}
8484

85-
if (clonedOptions.tools && clonedOptions.tools.length > 0) {
85+
if (clonedOptions.tools?.length > 0) {
8686
payload.tools = clonedOptions.tools.map(tool => ({
8787
type: 'function',
8888
function: {
@@ -164,14 +164,14 @@ class OpenAiCompatibleProvider extends Base {
164164
const result = await responsePromise;
165165

166166
// OpenAI completions format returns choices array
167-
const message = result.choices && result.choices.length > 0 ? result.choices[0].message : result.message;
167+
const message = result.choices?.[0]?.message ?? result.message;
168168

169169
const resultPayload = {
170170
content: message?.content || '',
171171
raw : result
172172
};
173173

174-
if (message?.tool_calls && message.tool_calls.length > 0) {
174+
if (message?.tool_calls?.length > 0) {
175175
resultPayload.toolCalls = message.tool_calls.map(c => ({
176176
function: {
177177
name : c.function.name,
@@ -229,8 +229,8 @@ class OpenAiCompatibleProvider extends Base {
229229
if (!jsonStr) continue;
230230

231231
const data = JSON.parse(jsonStr);
232-
const delta = data.choices && data.choices[0] && data.choices[0].delta;
233-
if (delta && typeof delta.content === 'string') {
232+
const delta = data.choices?.[0]?.delta;
233+
if (typeof delta?.content === 'string') {
234234
yield delta.content;
235235
}
236236
} catch (e) {

0 commit comments

Comments
 (0)