UNPKG

@autobe/agent

Version:

AI backend server code generator

149 lines 8.11 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.supportFunctionCallFallback = void 0; const uuid_1 = require("uuid"); const parseTextFunctionCall_1 = require("../utils/parseTextFunctionCall"); /** * Applies function call fallback patches to MicroAgentica agent. * * Some models return function calls as plain text/JSON in `message.content` * instead of the proper `tool_calls` field. This function wraps * `vendor.api.chat.completions.create` to intercept non-streaming responses and * parse text-based function calls into proper `tool_calls`. * * Without this patch, text-based function calls are treated as assistant * messages, causing `enforceFunctionCall` checks to fail. * * The wrapping is idempotent — calling this multiple times with the same vendor * will only wrap once (guarded by a Symbol). * * @param agent MicroAgentica instance (unused, kept for signature consistency * with supportMistral) * @param vendor Vendor configuration containing API instance */ const supportFunctionCallFallback = (_agent, vendor) => { const completions = vendor.api.chat.completions; if (completions[WRAPPED]) return; const originalCreate = completions.create.bind(completions); completions.create = function wrappedCreate(body, options) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c; const retryState = { upstream: 0, empty: 0, total: 0 }; while (retryState.total < TOTAL_RETRY_CAP) { const result = yield originalCreate(body, options); // OpenRouter returns upstream errors (502, etc.) as HTTP 200 with error body const maybeError = result; if ((maybeError === null || maybeError === void 0 ? void 0 : maybeError.error) && typeof maybeError.error === "object" && ((_a = maybeError.error) === null || _a === void 0 ? void 0 : _a.code)) { const err = maybeError.error; retryState.upstream++; retryState.total++; console.warn(`[FunctionCallFallback] OpenRouter upstream error (${err.code}): ${(_b = err.message) !== null && _b !== void 0 ? _b : "unknown"} — retry ${retryState.upstream}/${UPSTREAM_502_RETRY}`); if (retryState.upstream >= UPSTREAM_502_RETRY) break; yield upstreamBackoffDelay(retryState.upstream - 1); continue; } // Empty response: model returned nothing (no content, no tool_calls) if (!body.stream && ((_c = body.tools) === null || _c === void 0 ? void 0 : _c.length)) { const comp = result; if (isEmptyCompletion(comp)) { retryState.empty++; retryState.total++; console.warn(`[FunctionCallFallback] Empty response from model — retry ${retryState.empty}/${EMPTY_RESPONSE_RETRY}`); if (retryState.empty >= EMPTY_RESPONSE_RETRY) break; yield upstreamBackoffDelay(retryState.empty - 1); continue; } patchCompletionIfNeeded(comp, body.tools); // Re-check after patching: malformed tool_calls may have been // filtered out, leaving choices with no content and no valid // tool_calls. if (isEmptyCompletion(comp)) { retryState.empty++; retryState.total++; console.warn(`[FunctionCallFallback] Completion became empty after filtering malformed tool_calls — retry ${retryState.empty}/${EMPTY_RESPONSE_RETRY}`); if (retryState.empty >= EMPTY_RESPONSE_RETRY) break; yield upstreamBackoffDelay(retryState.empty - 1); continue; } } return result; } throw new Error(`OpenRouter upstream error: retries exhausted (upstream=${retryState.upstream}/${UPSTREAM_502_RETRY}, empty=${retryState.empty}/${EMPTY_RESPONSE_RETRY}, total=${retryState.total}/${TOTAL_RETRY_CAP})`); }); }; completions[WRAPPED] = true; }; exports.supportFunctionCallFallback = supportFunctionCallFallback; // ────────────────────────────────────────────── // Internal types (local shapes, no openai import) // ────────────────────────────────────────────── const WRAPPED = Symbol.for("autobe:function-call-fallback-wrapped"); const UPSTREAM_502_RETRY = 15; const EMPTY_RESPONSE_RETRY = 5; const TOTAL_RETRY_CAP = 17; const UPSTREAM_BASE_DELAY = 1000; const UPSTREAM_MAX_DELAY = 15000; function upstreamBackoffDelay(attempt) { const delay = Math.min(UPSTREAM_BASE_DELAY * 2 ** attempt, UPSTREAM_MAX_DELAY); const jittered = delay * (0.5 + Math.random() * 0.5); return new Promise((resolve) => setTimeout(resolve, jittered)); } function isEmptyCompletion(completion) { var _a; const choices = (_a = completion.choices) !== null && _a !== void 0 ? _a : []; if (choices.length === 0) return true; return choices.every((c) => { var _a, _b; return !((_a = c.message.content) === null || _a === void 0 ? void 0 : _a.trim()) && !((_b = c.message.tool_calls) === null || _b === void 0 ? void 0 : _b.length); }); } /** * Inspects each choice in the completion. If `tool_calls` is empty but * `content` contains text-based function calls, parse them and inject as proper * `tool_calls`. */ function patchCompletionIfNeeded(completion, tools) { var _a, _b, _c; const toolNames = tools .filter((t) => t.type === "function") .map((t) => t.function.name); for (const choice of (_a = completion.choices) !== null && _a !== void 0 ? _a : []) { // Filter out malformed tool_calls (missing function field) if ((_b = choice.message.tool_calls) === null || _b === void 0 ? void 0 : _b.length) { choice.message.tool_calls = choice.message.tool_calls.filter((tc) => { var _a; return (_a = tc.function) === null || _a === void 0 ? void 0 : _a.name; }); if (choice.message.tool_calls.length) continue; } const content = (_c = choice.message.content) === null || _c === void 0 ? void 0 : _c.trim(); if (!content) continue; const parsed = (0, parseTextFunctionCall_1.parseTextFunctionCall)(content, toolNames); if (parsed.length === 0) continue; // Convert parsed calls to proper tool_calls structure choice.message.tool_calls = parsed.map((call) => ({ id: `call_${(0, uuid_1.v7)()}`, type: "function", function: { name: call.name, arguments: call.arguments, }, })); // Clear content since it was actually a function call, not a message choice.message.content = null; } } //# sourceMappingURL=supportFunctionCallFallback.js.map