@autobe/agent
Version:
AI backend server code generator
149 lines • 8.11 kB
JavaScript
;
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