5.3 KiB
llmorbiter LLM tool-calling orchestrator with provider
abstraction.
Layout
src/
index.js # public entry — re-exports everything
LLMOrbiter.js # core orchestrator (~364 lines)
Tool.js # defineTool() + ToolError + DirectReturnError
providers/
OpenRouterProvider.js # streaming OpenRouter SSE provider
examples/
weather.js # minimal tool-calling demo
tool-error-retry.js # tool-error + LLM self-correction demo (has PASS/FAIL asserts)
pdf-pages.js # PDF -> per-page LLM processing (500 lines, largest)
tools/weather.js # sample static-data weather tool
Public API (src/index.js)
Exports: LLMOrbiter, OpenRouterProvider, defineTool, ToolError, DirectReturnError.
defineTool({ name, description, parameters, strict=false, invoke })->{ spec, invoke, name }.spec: OpenAI-compatible function schema ({ type: 'function', function: { name, description, parameters, strict } }).parameters: JSON Schema object (defaults to{ type: 'object', properties: {}, additionalProperties: false }).invoke(args, { signal }): local async implementation.
ToolError(message)— throw from a tool to signal a recoverable error back to the LLM.DirectReturnError(data)— throw from a tool to stop the loop immediately;run()resolves with{ directReturn: true, data, ... }. Any remaining tool calls in the same turn are closed with synthetic skipped tool responses so the conversation transcript remains valid.
new LLMOrbiter(provider, opts)
opts are lifecycle callbacks:
onLLMCall({ model, toolCount, tools, promptSize, messageCount, messages, iteration })— called before each LLM call;messagesis a comma-separated summary of roles (e.g.'user,assistant,tool_response').onUsage({ cached, input, output, cost })— called after each LLM call with that call's usage; provider's raw usage object is attached on non-enumerable.raw.onToolCall({ name, args })— called before executing a tool.onToolResult({ name, result })— called on successful tool execution.onToolError({ name, error, args, rawError })— called when tool execution throws, returns{ error }, has invalid JSON args, or references an unknown tool.onStatus(status)— forwarded from provider:'connected'(HTTP headers received),'streaming'(first SSE chunk received), or'working'(tool calls started streaming).onContent(chunk)— forwarded from provider: incremental output content string (choice.delta.content).onReasoning(text)— forwarded from provider: accumulated reasoning string so far (aggregated fromdelta.reasoning,delta.reasoning_content, anddelta.reasoning_details).
orbiter.run(opts)
{ prompt, messages, model, tools, maxIterations=10, tool_choice, extraBody, signal }.
prompt: user message string to append (omit or passnullto continue without adding one).messages: transcript array to seed from; a leading{ role: 'system' }is the system prompt.tools: array of tool objects created withdefineTool().maxIterations: safety limit on tool-calling loop iterations (default:10).tool_choice: tool choice setting (defaults to'auto'when tools are present).extraBody: extra parameters merged into provider request payload.signal: externalAbortSignal.
Loop: send messages + tool specs -> if tool_calls come back, invoke locals and feed results
back -> repeat until no more tool calls or maxIterations.
Returns:
content: final assistant message text (ornull).toolResults:{ [toolName]: [result, ...] }map of all tool results executed across iterations.usage: aggregated usage object{ input, output, cached, tokenIn, tokenOut, tokenCached, costTotal, cost, raw }.rawUsage: single provider usage object (if 1 iteration) or array of usage objects (if multiple iterations).rawUsages: array of all iteration usage objects from the provider.iterations: count of iterations performed.messages: conversation transcript array for continuity in subsequent runs.directReturn:trueif terminated byDirectReturnError.data: data payload passed toDirectReturnError.
orbiter.abort(reason) cancels the active run; aborts surface as DOMException('...', 'AbortError').
OpenRouterProvider
new OpenRouterProvider({ apiKey, apiUrl, siteUrl, siteName }).
apiUrldefaults tohttps://openrouter.ai/api/v1/chat/completions.siteUrlsetsHTTP-Refererheader.siteNamesetsX-Titleheader.
Method:
chatCompletions({ model, messages, tools, tool_choice, signal, extraBody, onStatus, onContent, onReasoning })
— streams SSE, assembles { content, tool_calls, finish_reason, usage, reasoning_details, reasoning }.
- If reasoning was returned, automatically appends
{ role: 'assistant', content: result.reasoning }tomessages. - Delta handling:
delta.content-> passes chunk toonContent(chunk).delta.reasoning/delta.reasoning_content/delta.reasoning_details-> passes accumulated string toonReasoning(text).delta.tool_calls-> triggersonStatus('working')once when tool calls begin streaming (tool calls are assembled intoresult.tool_callswithout per-chunk streaming callbacks).