u
This commit is contained in:
@@ -105,9 +105,9 @@ class LLMOrbiter {
|
||||
this._activeController = controller;
|
||||
const runSignal = controller.signal;
|
||||
|
||||
const call = (fn, arg) => {
|
||||
const call = (fn, ...args) => {
|
||||
if (fn) {
|
||||
try { fn(arg); } catch (_) {}
|
||||
try { fn(...args); } catch (_) {}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -187,7 +187,7 @@ class LLMOrbiter {
|
||||
signal: runSignal,
|
||||
extraBody,
|
||||
onStatus: (s) => call(onStatus, s),
|
||||
onContent: (c) => call(onContent, c),
|
||||
onContent: (...args) => call(onContent, ...args),
|
||||
onReasoning: (r) => call(onReasoning, r),
|
||||
});
|
||||
|
||||
@@ -213,6 +213,8 @@ class LLMOrbiter {
|
||||
role: 'assistant',
|
||||
content: content || null,
|
||||
...(hasToolCalls ? { tool_calls: toolCalls } : {}),
|
||||
...(completion.reasoning_details?.length ? { reasoning_details: completion.reasoning_details } : {}),
|
||||
...(completion.reasoning ? { reasoning: completion.reasoning } : {}),
|
||||
});
|
||||
|
||||
// No tool calls → done
|
||||
@@ -247,7 +249,7 @@ class LLMOrbiter {
|
||||
}
|
||||
|
||||
if (!isError) {
|
||||
call(onToolCall, { name: fnName, args });
|
||||
call(onToolCall, { name: fnName, args, id: callItem.id, callItem, tool_calls: toolCalls, reasoning_details: completion.reasoning_details, reasoning: completion.reasoning, content: content || null });
|
||||
|
||||
try {
|
||||
result = await invokeFn(args, {
|
||||
@@ -267,10 +269,12 @@ class LLMOrbiter {
|
||||
// calls in the batch with synthetic tool messages, so the
|
||||
// returned transcript stays valid to send back to the API
|
||||
// (every tool_call must have a matching tool response).
|
||||
recordToolResult(callItem, err.data);
|
||||
for (let j = callIdx + 1; j < toolCalls.length; j++) {
|
||||
const pending = toolCalls[j];
|
||||
recordToolResult(pending, 'Skipped: run terminated by direct return');
|
||||
if (err.recordResult !== false) {
|
||||
recordToolResult(callItem, err.data);
|
||||
for (let j = callIdx + 1; j < toolCalls.length; j++) {
|
||||
const pending = toolCalls[j];
|
||||
recordToolResult(pending, 'Skipped: run terminated by direct return');
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -285,7 +289,7 @@ class LLMOrbiter {
|
||||
errorMsg = 'Tool crashed due to an unspecified internal error';
|
||||
}
|
||||
result = { error: errorMsg };
|
||||
call(onToolError, { name: fnName, error: errorMsg, args, rawError: err });
|
||||
call(onToolError, { name: fnName, error: errorMsg, args, rawError: err, id: callItem.id, callItem });
|
||||
}
|
||||
|
||||
// Also handle tools that return { error: ... } instead of throwing
|
||||
@@ -310,7 +314,7 @@ class LLMOrbiter {
|
||||
allToolResults[fnName].push(result);
|
||||
|
||||
if (!isError) {
|
||||
call(onToolResult, { name: fnName, result });
|
||||
call(onToolResult, { name: fnName, result, id: callItem.id, callItem });
|
||||
}
|
||||
|
||||
// Feed result back to conversation
|
||||
|
||||
@@ -50,9 +50,10 @@ export class ToolError extends Error {
|
||||
* with { directReturn: true, data, ... } instead of continuing the loop.
|
||||
*/
|
||||
export class DirectReturnError extends Error {
|
||||
constructor(data) {
|
||||
constructor(data, { recordResult = true } = {}) {
|
||||
super('Direct return requested by tool');
|
||||
this.name = 'DirectReturnError';
|
||||
this.data = data;
|
||||
this.recordResult = recordResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const DEFAULT_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||
const PROCESS_SESSION_ID = randomUUID();
|
||||
|
||||
/**
|
||||
* OpenRouter provider for LLMOrbiter.
|
||||
@@ -13,31 +16,42 @@ const DEFAULT_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||
* });
|
||||
*/
|
||||
class OpenRouterProvider {
|
||||
static get processSessionId() {
|
||||
return PROCESS_SESSION_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.apiKey — OpenRouter API key
|
||||
* @param {string} [opts.apiUrl] — API endpoint (default: OpenRouter)
|
||||
* @param {string} [opts.siteUrl] — HTTP-Referer header
|
||||
* @param {string} [opts.siteName] — X-Title header
|
||||
* @param {string} [opts.sessionId] — Session ID / cache key (default: process-lifetime UUID)
|
||||
*/
|
||||
constructor({ apiKey, apiUrl, siteUrl, siteName } = {}) {
|
||||
constructor({ apiKey, apiUrl, siteUrl, siteName, sessionId } = {}) {
|
||||
if (!apiKey) throw new Error('OpenRouterProvider requires an apiKey');
|
||||
this.apiKey = apiKey;
|
||||
this.apiUrl = apiUrl || DEFAULT_API_URL;
|
||||
this.siteUrl = siteUrl || '';
|
||||
this.siteName = siteName || '';
|
||||
this.sessionId = sessionId || PROCESS_SESSION_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Streaming chat completion against OpenRouter.
|
||||
* Returns assembled { content, tool_calls, finish_reason, usage, reasoning_details }.
|
||||
*/
|
||||
async chatCompletions({ model, messages, tools, tool_choice, signal, extraBody = {}, onStatus, onContent, onReasoning }) {
|
||||
async chatCompletions({ model, messages, tools, tool_choice, signal, extraBody = {}, onStatus, onContent, onReasoning, sessionId }) {
|
||||
if (!model) throw new Error('model is required');
|
||||
const effectiveSessionId = sessionId || extraBody.session_id || this.sessionId;
|
||||
const body = {
|
||||
model,
|
||||
messages,
|
||||
reasoning: {
|
||||
effort: 'minimal'
|
||||
},
|
||||
stream: true,
|
||||
...(effectiveSessionId ? { session_id: effectiveSessionId } : {}),
|
||||
...extraBody,
|
||||
};
|
||||
if (tools?.length) body.tools = tools;
|
||||
@@ -49,6 +63,7 @@ class OpenRouterProvider {
|
||||
};
|
||||
if (this.siteUrl) headers['HTTP-Referer'] = this.siteUrl;
|
||||
if (this.siteName) headers['X-Title'] = this.siteName;
|
||||
if (effectiveSessionId) headers['x-session-id'] = effectiveSessionId;
|
||||
|
||||
const response = await fetch(this.apiUrl, {
|
||||
method: 'POST',
|
||||
@@ -102,14 +117,7 @@ class OpenRouterProvider {
|
||||
}
|
||||
}
|
||||
|
||||
if (result.reasoning) {
|
||||
if (Array.isArray(messages)) {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: result.reasoning,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -132,7 +140,7 @@ class OpenRouterProvider {
|
||||
// Content
|
||||
if (choice.delta?.content) {
|
||||
result.content = (result.content || '') + choice.delta.content;
|
||||
onContent?.(choice.delta.content);
|
||||
onContent?.(choice.delta.content, result.content);
|
||||
}
|
||||
|
||||
// Tool calls
|
||||
|
||||
Reference in New Issue
Block a user