mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 19:54:07 +00:00
refactor: extract LLM core packages (#88117)
* refactor: extract llm core packages * chore: drop generated llm package artifacts * fix: align llm package export artifacts * test: fix moving main CI expectations * fix: align llm core subpath aliases * fix: use llm package exports * fix: stabilize llm package boundary artifacts * fix: sync llm boundary path contract * test: isolate crabbox provider env * test: pin crabbox configured-provider cases * test: apply crabbox lease provider override
This commit is contained in:
committed by
GitHub
parent
17e75f8641
commit
aa0d6e1bca
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -601,7 +601,7 @@ jobs:
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .artifacts/build-all-cache
|
||||
key: ${{ runner.os }}-build-all-v3-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'npm-shrinkwrap.json', 'packages/plugin-sdk/package.json', 'packages/memory-host-sdk/package.json', 'scripts/build-all.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entries.mjs', 'tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'src/plugin-sdk/**', 'packages/memory-host-sdk/src/**', 'src/types/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'scripts/copy-export-html-templates.ts', 'scripts/lib/copy-assets.ts', 'src/auto-reply/reply/export-html/**') }}
|
||||
key: ${{ runner.os }}-build-all-v3-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'npm-shrinkwrap.json', 'packages/plugin-sdk/package.json', 'packages/llm-core/package.json', 'packages/memory-host-sdk/package.json', 'scripts/build-all.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entries.mjs', 'tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'src/plugin-sdk/**', 'packages/llm-core/src/**', 'packages/memory-host-sdk/src/**', 'src/types/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'scripts/copy-export-html-templates.ts', 'scripts/lib/copy-assets.ts', 'src/auto-reply/reply/export-html/**') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-all-v3-
|
||||
|
||||
@@ -1403,7 +1403,7 @@ jobs:
|
||||
packages/plugin-sdk/dist
|
||||
extensions/*/dist/.boundary-tsc.tsbuildinfo
|
||||
extensions/*/dist/.boundary-tsc.stamp
|
||||
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/auto-reply/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
|
||||
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'packages/llm-core/package.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/auto-reply/**', 'packages/llm-core/src/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-extension-package-boundary-v1-
|
||||
|
||||
@@ -1420,10 +1420,14 @@ jobs:
|
||||
find src \
|
||||
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
|
||||
-exec touch -t 200001010000 {} +
|
||||
find packages/llm-core/src \
|
||||
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
|
||||
-exec touch -t 200001010000 {} +
|
||||
touch -t 200001010000 \
|
||||
tsconfig.json \
|
||||
tsconfig.plugin-sdk.dts.json \
|
||||
packages/plugin-sdk/tsconfig.json \
|
||||
packages/llm-core/package.json \
|
||||
scripts/check-extension-package-tsc-boundary.mjs \
|
||||
scripts/prepare-extension-package-boundary-artifacts.mjs \
|
||||
scripts/write-plugin-sdk-entry-dts.ts \
|
||||
|
||||
@@ -90,6 +90,18 @@
|
||||
"@openclaw/discord/api.js": ["../dist/plugin-sdk/extensions/discord/api.d.ts"],
|
||||
"@openclaw/slack/api.js": ["../dist/plugin-sdk/extensions/slack/api.d.ts"],
|
||||
"@openclaw/whatsapp/api.js": ["../dist/plugin-sdk/extensions/whatsapp/api.d.ts"],
|
||||
"@openclaw/llm-core": ["../dist/plugin-sdk/packages/llm-core/src/index.d.ts"],
|
||||
"@openclaw/llm-core/diagnostics": [
|
||||
"../dist/plugin-sdk/packages/llm-core/src/utils/diagnostics.d.ts"
|
||||
],
|
||||
"@openclaw/llm-core/event-stream": [
|
||||
"../dist/plugin-sdk/packages/llm-core/src/utils/event-stream.d.ts"
|
||||
],
|
||||
"@openclaw/llm-core/types": ["../dist/plugin-sdk/packages/llm-core/src/types.d.ts"],
|
||||
"@openclaw/llm-core/validation": [
|
||||
"../dist/plugin-sdk/packages/llm-core/src/validation.d.ts"
|
||||
],
|
||||
"@openclaw/llm-core/*": ["../dist/plugin-sdk/packages/llm-core/src/*.d.ts"],
|
||||
"@openclaw/*.js": ["../packages/plugin-sdk/dist/extensions/*.d.ts", "../extensions/*"],
|
||||
"@openclaw/*": ["../packages/plugin-sdk/dist/extensions/*", "../extensions/*"],
|
||||
"openclaw/plugin-sdk/qa-channel": ["../dist/plugin-sdk/src/plugin-sdk/qa-channel.d.ts"],
|
||||
|
||||
@@ -93,6 +93,24 @@
|
||||
"../../dist/plugin-sdk/ssrf-runtime.d.ts"
|
||||
],
|
||||
"@openclaw/qa-channel/api.js": ["../../dist/plugin-sdk/extensions/qa-channel/api.d.ts"],
|
||||
"@openclaw/llm-core": [
|
||||
"../../dist/plugin-sdk/packages/llm-core/src/index.d.ts"
|
||||
],
|
||||
"@openclaw/llm-core/diagnostics": [
|
||||
"../../dist/plugin-sdk/packages/llm-core/src/utils/diagnostics.d.ts"
|
||||
],
|
||||
"@openclaw/llm-core/event-stream": [
|
||||
"../../dist/plugin-sdk/packages/llm-core/src/utils/event-stream.d.ts"
|
||||
],
|
||||
"@openclaw/llm-core/types": [
|
||||
"../../dist/plugin-sdk/packages/llm-core/src/types.d.ts"
|
||||
],
|
||||
"@openclaw/llm-core/validation": [
|
||||
"../../dist/plugin-sdk/packages/llm-core/src/validation.d.ts"
|
||||
],
|
||||
"@openclaw/llm-core/*": [
|
||||
"../../dist/plugin-sdk/packages/llm-core/src/*.d.ts"
|
||||
],
|
||||
"@openclaw/*.js": ["../../packages/plugin-sdk/dist/extensions/*.d.ts", "../*"],
|
||||
"@openclaw/*": ["../*"],
|
||||
"openclaw/plugin-sdk/qa-channel": [
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@openclaw/llm-core": "workspace:*",
|
||||
"ignore": "7.0.5",
|
||||
"typebox": "1.1.38",
|
||||
"yaml": "2.9.0"
|
||||
|
||||
@@ -3,7 +3,16 @@
|
||||
* Transforms to Message[] only at the LLM call boundary.
|
||||
*/
|
||||
|
||||
import { type AssistantMessage, type Context, EventStream, type ToolResultMessage } from "./llm.js";
|
||||
// Keep the runtime class on the package specifier so built agent-core shares
|
||||
// constructor identity with @openclaw/llm-core; source types keep SDK d.ts bundled.
|
||||
import { EventStream as LlmEventStream } from "@openclaw/llm-core";
|
||||
import {
|
||||
type AssistantMessage,
|
||||
type Context,
|
||||
type EventStream,
|
||||
type ToolResultMessage,
|
||||
} from "../../llm-core/src/index.js";
|
||||
import type { EventStream as SourceEventStream } from "../../llm-core/src/index.js";
|
||||
import { type AgentCoreStreamRuntimeDeps, resolveAgentCoreStreamFn } from "./runtime-deps.js";
|
||||
import type {
|
||||
AgentContext,
|
||||
@@ -28,6 +37,8 @@ const EMPTY_USAGE = {
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
};
|
||||
|
||||
const EventStreamConstructor: typeof SourceEventStream = LlmEventStream;
|
||||
|
||||
/**
|
||||
* Start an agent loop with a new prompt message.
|
||||
* The prompt is added to the context and events are emitted for it.
|
||||
@@ -52,11 +63,13 @@ export function agentLoop(
|
||||
signal,
|
||||
streamFn,
|
||||
runtime,
|
||||
).then((messages) => {
|
||||
stream.end(messages);
|
||||
}).catch((error) => {
|
||||
pushLoopFailure(stream, config, error, signal?.aborted === true);
|
||||
});
|
||||
)
|
||||
.then((messages) => {
|
||||
stream.end(messages);
|
||||
})
|
||||
.catch((error) => {
|
||||
pushLoopFailure(stream, config, error, signal?.aborted === true);
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
@@ -95,11 +108,13 @@ export function agentLoopContinue(
|
||||
signal,
|
||||
streamFn,
|
||||
runtime,
|
||||
).then((messages) => {
|
||||
stream.end(messages);
|
||||
}).catch((error) => {
|
||||
pushLoopFailure(stream, config, error, signal?.aborted === true);
|
||||
});
|
||||
)
|
||||
.then((messages) => {
|
||||
stream.end(messages);
|
||||
})
|
||||
.catch((error) => {
|
||||
pushLoopFailure(stream, config, error, signal?.aborted === true);
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
@@ -157,7 +172,7 @@ export async function runAgentLoopContinue(
|
||||
}
|
||||
|
||||
function createAgentStream(): EventStream<AgentEvent, AgentMessage[]> {
|
||||
return new EventStream<AgentEvent, AgentMessage[]>(
|
||||
return new EventStreamConstructor<AgentEvent, AgentMessage[]>(
|
||||
(event: AgentEvent) => event.type === "agent_end",
|
||||
(event: AgentEvent) => (event.type === "agent_end" ? event.messages : []),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.js";
|
||||
import {
|
||||
type ImageContent,
|
||||
type Message,
|
||||
@@ -7,7 +6,8 @@ import {
|
||||
type TextContent,
|
||||
type ThinkingBudgets,
|
||||
type Transport,
|
||||
} from "./llm.js";
|
||||
} from "../../llm-core/src/index.js";
|
||||
import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.js";
|
||||
import { type AgentCoreStreamRuntimeDeps, resolveAgentCoreStreamFn } from "./runtime-deps.js";
|
||||
import type {
|
||||
AfterToolCallContext,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import {
|
||||
type AssistantMessage,
|
||||
type ImageContent,
|
||||
type Model,
|
||||
type UserMessage,
|
||||
} from "../../../llm-core/src/index.js";
|
||||
import { runAgentLoop } from "../agent-loop.js";
|
||||
import { type AssistantMessage, type ImageContent, type Model, type UserMessage } from "../llm.js";
|
||||
import { type AgentCoreRuntimeDeps, resolveAgentCoreStreamFn } from "../runtime-deps.js";
|
||||
import type {
|
||||
AgentContext,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Model, StreamFn } from "../../llm.js";
|
||||
import type { Model, StreamFn } from "../../../../llm-core/src/index.js";
|
||||
import {
|
||||
type AgentCoreCompletionRuntimeDeps,
|
||||
resolveAgentCoreCompleteFn,
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
SimpleStreamOptions,
|
||||
StreamFn,
|
||||
Usage,
|
||||
} from "../../llm.js";
|
||||
} from "../../../../llm-core/src/index.js";
|
||||
import {
|
||||
type AgentCoreCompletionRuntimeDeps,
|
||||
resolveAgentCoreCompleteFn,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Message } from "../../llm.js";
|
||||
import type { Message } from "../../../../llm-core/src/index.js";
|
||||
import type { AgentMessage } from "../../types.js";
|
||||
|
||||
/** File paths touched by a session branch or compaction range. */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ImageContent, Message, TextContent } from "../llm.js";
|
||||
import type { ImageContent, Message, TextContent } from "../../../llm-core/src/index.js";
|
||||
import type { AgentMessage } from "../types.js";
|
||||
import { requireSessionTimestampMs } from "./session/timestamps.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ImageContent, TextContent } from "../../llm.js";
|
||||
import type { ImageContent, TextContent } from "../../../../llm-core/src/index.js";
|
||||
import type { AgentMessage } from "../../types.js";
|
||||
import {
|
||||
createBranchSummaryMessage,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../index.js";
|
||||
import type {
|
||||
ImageContent,
|
||||
Model,
|
||||
@@ -6,7 +5,8 @@ import type {
|
||||
StreamFn,
|
||||
TextContent,
|
||||
Transport,
|
||||
} from "../llm.js";
|
||||
} from "../../../llm-core/src/index.js";
|
||||
import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../index.js";
|
||||
import type { AgentCoreCompletionRuntimeDeps, AgentCoreRuntimeDeps } from "../runtime-deps.js";
|
||||
import type { Session } from "./session/session.js";
|
||||
|
||||
|
||||
@@ -1,268 +1 @@
|
||||
import type { TSchema } from "typebox";
|
||||
|
||||
export type Api = string;
|
||||
export type CacheRetention = "none" | "short" | "long";
|
||||
export type Transport = "sse" | "websocket" | "websocket-cached" | "auto";
|
||||
export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
||||
export type ModelThinkingLevel = "off" | ThinkingLevel;
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
export interface ProviderResponse {
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ThinkingBudgets {
|
||||
minimal?: number;
|
||||
low?: number;
|
||||
medium?: number;
|
||||
high?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export interface DiagnosticErrorInfo {
|
||||
name?: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
code?: string | number;
|
||||
}
|
||||
|
||||
export interface AssistantMessageDiagnostic {
|
||||
type: string;
|
||||
timestamp: number;
|
||||
error?: DiagnosticErrorInfo;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SimpleStreamOptions {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
apiKey?: string;
|
||||
transport?: Transport;
|
||||
cacheRetention?: CacheRetention;
|
||||
sessionId?: string;
|
||||
onPayload?: (payload: unknown, model: Model) => MaybePromise<unknown>;
|
||||
onResponse?: (response: ProviderResponse, model: Model) => void | Promise<void>;
|
||||
headers?: Record<string, string>;
|
||||
timeoutMs?: number;
|
||||
maxRetries?: number;
|
||||
maxRetryDelayMs?: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
reasoning?: ThinkingLevel;
|
||||
thinkingBudgets?: ThinkingBudgets;
|
||||
}
|
||||
|
||||
export interface TextContent {
|
||||
type: "text";
|
||||
text: string;
|
||||
textSignature?: string;
|
||||
}
|
||||
|
||||
export interface ThinkingContent {
|
||||
type: "thinking";
|
||||
thinking: string;
|
||||
thinkingSignature?: string;
|
||||
redacted?: boolean;
|
||||
}
|
||||
|
||||
export interface ImageContent {
|
||||
type: "image";
|
||||
data: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
type: "toolCall";
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
thoughtSignature?: string;
|
||||
executionMode?: "sequential" | "parallel";
|
||||
}
|
||||
|
||||
export interface Usage {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
cost: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type StopReason = "stop" | "length" | "toolUse" | "aborted" | "error";
|
||||
|
||||
export interface UserMessage {
|
||||
role: "user";
|
||||
content: string | (TextContent | ImageContent)[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface AssistantMessage {
|
||||
role: "assistant";
|
||||
content: (TextContent | ThinkingContent | ToolCall)[];
|
||||
api: Api;
|
||||
provider: string;
|
||||
model: string;
|
||||
responseModel?: string;
|
||||
responseId?: string;
|
||||
diagnostics?: AssistantMessageDiagnostic[];
|
||||
stopReason: StopReason;
|
||||
errorMessage?: string;
|
||||
timestamp: number;
|
||||
usage: Usage;
|
||||
}
|
||||
|
||||
export interface ToolResultMessage {
|
||||
role: "toolResult";
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
content: (TextContent | ImageContent)[];
|
||||
isError: boolean;
|
||||
details?: unknown;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export type Message = UserMessage | AssistantMessage | ToolResultMessage;
|
||||
|
||||
export interface Context {
|
||||
systemPrompt?: string;
|
||||
messages: Message[];
|
||||
tools?: Tool[];
|
||||
}
|
||||
|
||||
export interface Model<TApi extends Api = Api> {
|
||||
id: string;
|
||||
name: string;
|
||||
api: TApi;
|
||||
provider: string;
|
||||
baseUrl: string;
|
||||
input: ("text" | "image")[];
|
||||
reasoning: boolean;
|
||||
thinkingLevelMap?: Partial<Record<ModelThinkingLevel, string | null>>;
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
cost: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
};
|
||||
headers?: Record<string, string>;
|
||||
// Provider-owned compatibility payload; core carries it without inspecting it.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
compat?: any;
|
||||
}
|
||||
|
||||
export interface Tool<TParameters extends TSchema = TSchema> {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: TParameters;
|
||||
}
|
||||
|
||||
export type AssistantMessageEvent =
|
||||
| { type: "start"; partial: AssistantMessage }
|
||||
| { type: "text_start"; contentIndex: number; partial: AssistantMessage }
|
||||
| { type: "text_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
||||
| { type: "text_end"; contentIndex: number; content: string; partial: AssistantMessage }
|
||||
| { type: "thinking_start"; contentIndex: number; partial: AssistantMessage }
|
||||
| { type: "thinking_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
||||
| { type: "thinking_end"; contentIndex: number; content: string; partial: AssistantMessage }
|
||||
| { type: "toolcall_start"; contentIndex: number; partial: AssistantMessage }
|
||||
| { type: "toolcall_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
||||
| { type: "toolcall_end"; contentIndex: number; toolCall: ToolCall; partial: AssistantMessage }
|
||||
| {
|
||||
type: "done";
|
||||
reason: Extract<StopReason, "stop" | "length" | "toolUse">;
|
||||
message: AssistantMessage;
|
||||
}
|
||||
| { type: "error"; reason: Extract<StopReason, "aborted" | "error">; error: AssistantMessage };
|
||||
|
||||
export class EventStream<T, R = T> implements AsyncIterable<T> {
|
||||
private queue: T[] = [];
|
||||
private waiting: ((value: IteratorResult<T>) => void)[] = [];
|
||||
private done = false;
|
||||
private finalResultPromise: Promise<R>;
|
||||
private resolveFinalResult!: (result: R) => void;
|
||||
|
||||
constructor(
|
||||
private readonly isComplete: (event: T) => boolean,
|
||||
private readonly extractResult: (event: T) => R,
|
||||
) {
|
||||
this.finalResultPromise = new Promise((resolve) => {
|
||||
this.resolveFinalResult = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
push(event: T): void {
|
||||
if (this.done) {
|
||||
return;
|
||||
}
|
||||
if (this.isComplete(event)) {
|
||||
this.done = true;
|
||||
this.resolveFinalResult(this.extractResult(event));
|
||||
}
|
||||
const waiter = this.waiting.shift();
|
||||
if (waiter) {
|
||||
waiter({ value: event, done: false });
|
||||
} else {
|
||||
this.queue.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
end(result?: R): void {
|
||||
this.done = true;
|
||||
if (result !== undefined) {
|
||||
this.resolveFinalResult(result);
|
||||
}
|
||||
while (this.waiting.length > 0) {
|
||||
this.waiting.shift()?.({ value: undefined as unknown as T, done: true });
|
||||
}
|
||||
}
|
||||
|
||||
async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
while (true) {
|
||||
if (this.queue.length > 0) {
|
||||
yield this.queue.shift()!;
|
||||
} else if (this.done) {
|
||||
return;
|
||||
} else {
|
||||
const result = await new Promise<IteratorResult<T>>((resolve) =>
|
||||
this.waiting.push(resolve),
|
||||
);
|
||||
if (result.done) {
|
||||
return;
|
||||
}
|
||||
yield result.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result(): Promise<R> {
|
||||
return this.finalResultPromise;
|
||||
}
|
||||
}
|
||||
|
||||
export interface AssistantMessageEventStream extends AsyncIterable<AssistantMessageEvent> {
|
||||
result(): Promise<AssistantMessage>;
|
||||
}
|
||||
|
||||
export type StreamFn = (
|
||||
model: Model,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
|
||||
|
||||
export type CompleteSimpleFn = (
|
||||
model: Model,
|
||||
context: Pick<Context, "systemPrompt" | "messages">,
|
||||
options?: SimpleStreamOptions,
|
||||
) => Promise<AssistantMessage>;
|
||||
|
||||
export type ValidateToolArgumentsFn = (tool: Tool, toolCall: ToolCall) => unknown;
|
||||
export * from "@openclaw/llm-core";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CompleteSimpleFn, StreamFn } from "./llm.js";
|
||||
import type { CompleteSimpleFn, StreamFn } from "../../llm-core/src/index.js";
|
||||
|
||||
export interface AgentCoreRuntimeDeps {
|
||||
streamSimple: StreamFn;
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
TextContent,
|
||||
Tool,
|
||||
ToolResultMessage,
|
||||
} from "./llm.js";
|
||||
} from "../../llm-core/src/index.js";
|
||||
|
||||
/**
|
||||
* Stream function used by the agent loop.
|
||||
|
||||
@@ -1,324 +1 @@
|
||||
import { Compile } from "typebox/compile";
|
||||
import type { TLocalizedValidationError } from "typebox/error";
|
||||
import { Value } from "typebox/value";
|
||||
import type { Tool, ToolCall } from "./llm.js";
|
||||
|
||||
const validatorCache = new WeakMap<object, ReturnType<typeof Compile>>();
|
||||
const TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
|
||||
|
||||
interface JsonSchemaObject {
|
||||
type?: string | string[];
|
||||
properties?: Record<string, JsonSchemaObject>;
|
||||
items?: JsonSchemaObject | JsonSchemaObject[];
|
||||
additionalProperties?: boolean | JsonSchemaObject;
|
||||
allOf?: JsonSchemaObject[];
|
||||
anyOf?: JsonSchemaObject[];
|
||||
oneOf?: JsonSchemaObject[];
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isJsonSchemaObject(value: unknown): value is JsonSchemaObject {
|
||||
return isRecord(value);
|
||||
}
|
||||
|
||||
function hasTypeBoxMetadata(schema: unknown): boolean {
|
||||
return isRecord(schema) && Object.getOwnPropertySymbols(schema).includes(TYPEBOX_KIND);
|
||||
}
|
||||
|
||||
function getSchemaTypes(schema: JsonSchemaObject): string[] {
|
||||
if (typeof schema.type === "string") {
|
||||
return [schema.type];
|
||||
}
|
||||
if (Array.isArray(schema.type)) {
|
||||
return schema.type.filter((type): type is string => typeof type === "string");
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function matchesJsonType(value: unknown, type: string): boolean {
|
||||
switch (type) {
|
||||
case "number":
|
||||
return typeof value === "number";
|
||||
case "integer":
|
||||
return typeof value === "number" && Number.isInteger(value);
|
||||
case "boolean":
|
||||
return typeof value === "boolean";
|
||||
case "string":
|
||||
return typeof value === "string";
|
||||
case "null":
|
||||
return value === null;
|
||||
case "array":
|
||||
return Array.isArray(value);
|
||||
case "object":
|
||||
return isRecord(value) && !Array.isArray(value);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidatorSchema(value: unknown): value is Tool["parameters"] {
|
||||
return isRecord(value);
|
||||
}
|
||||
|
||||
const JSON_NUMBER_TOKEN_RE = /^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:e[+-]?\d+)?$/iu;
|
||||
|
||||
function parseJsonNumberString(value: string): number | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || !JSON_NUMBER_TOKEN_RE.test(trimmed)) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number(trimmed);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function parseJsonIntegerString(value: string): number | undefined {
|
||||
const parsed = parseJsonNumberString(value);
|
||||
return parsed !== undefined && Number.isSafeInteger(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function getSubSchemaValidator(schema: JsonSchemaObject): ReturnType<typeof Compile> | undefined {
|
||||
if (!isValidatorSchema(schema)) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return getValidator(schema);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function coercePrimitiveByType(value: unknown, type: string): unknown {
|
||||
switch (type) {
|
||||
case "number": {
|
||||
if (value === null) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
const parsed = parseJsonNumberString(value);
|
||||
if (parsed !== undefined) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
case "integer": {
|
||||
if (value === null) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
const parsed = parseJsonIntegerString(value);
|
||||
if (parsed !== undefined) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
case "boolean": {
|
||||
if (value === null) {
|
||||
return false;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
if (value === "true") {
|
||||
return true;
|
||||
}
|
||||
if (value === "false") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
if (value === 1) {
|
||||
return true;
|
||||
}
|
||||
if (value === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
case "string": {
|
||||
if (value === null) {
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
case "null": {
|
||||
if (value === "" || value === 0 || value === false) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function applySchemaObjectCoercion(value: Record<string, unknown>, schema: JsonSchemaObject): void {
|
||||
const properties = schema.properties;
|
||||
const definedKeys = new Set<string>(properties ? Object.keys(properties) : []);
|
||||
|
||||
if (properties) {
|
||||
for (const [key, propertySchema] of Object.entries(properties)) {
|
||||
if (key in value) {
|
||||
value[key] = coerceWithJsonSchema(value[key], propertySchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.additionalProperties && isJsonSchemaObject(schema.additionalProperties)) {
|
||||
for (const [key, propertyValue] of Object.entries(value)) {
|
||||
if (!definedKeys.has(key)) {
|
||||
value[key] = coerceWithJsonSchema(propertyValue, schema.additionalProperties);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applySchemaArrayCoercion(value: unknown[], schema: JsonSchemaObject): void {
|
||||
if (Array.isArray(schema.items)) {
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const itemSchema = schema.items[index];
|
||||
if (itemSchema) {
|
||||
value[index] = coerceWithJsonSchema(value[index], itemSchema);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isJsonSchemaObject(schema.items)) {
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
value[index] = coerceWithJsonSchema(value[index], schema.items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function coerceWithUnionSchema(value: unknown, schemas: JsonSchemaObject[]): unknown {
|
||||
for (const schema of schemas) {
|
||||
const candidate = structuredClone(value);
|
||||
const coerced = coerceWithJsonSchema(candidate, schema);
|
||||
const validator = getSubSchemaValidator(schema);
|
||||
if (validator?.Check(coerced)) {
|
||||
return coerced;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function coerceWithJsonSchema(value: unknown, schema: JsonSchemaObject): unknown {
|
||||
let nextValue = value;
|
||||
|
||||
if (Array.isArray(schema.allOf)) {
|
||||
for (const nested of schema.allOf) {
|
||||
nextValue = coerceWithJsonSchema(nextValue, nested);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(schema.anyOf)) {
|
||||
nextValue = coerceWithUnionSchema(nextValue, schema.anyOf);
|
||||
}
|
||||
|
||||
if (Array.isArray(schema.oneOf)) {
|
||||
nextValue = coerceWithUnionSchema(nextValue, schema.oneOf);
|
||||
}
|
||||
|
||||
const schemaTypes = getSchemaTypes(schema);
|
||||
const matchesUnionMember =
|
||||
schemaTypes.length > 1 &&
|
||||
schemaTypes.some((schemaType) => matchesJsonType(nextValue, schemaType));
|
||||
if (schemaTypes.length > 0 && !matchesUnionMember) {
|
||||
for (const schemaType of schemaTypes) {
|
||||
const candidate = coercePrimitiveByType(nextValue, schemaType);
|
||||
if (candidate !== nextValue) {
|
||||
nextValue = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schemaTypes.includes("object") && isRecord(nextValue) && !Array.isArray(nextValue)) {
|
||||
applySchemaObjectCoercion(nextValue, schema);
|
||||
}
|
||||
|
||||
if (schemaTypes.includes("array") && Array.isArray(nextValue)) {
|
||||
applySchemaArrayCoercion(nextValue, schema);
|
||||
}
|
||||
|
||||
return nextValue;
|
||||
}
|
||||
|
||||
function getValidator(schema: Tool["parameters"]): ReturnType<typeof Compile> {
|
||||
const key = schema as object;
|
||||
const cached = validatorCache.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const validator = Compile(schema);
|
||||
validatorCache.set(key, validator);
|
||||
return validator;
|
||||
}
|
||||
|
||||
function formatValidationPath(error: TLocalizedValidationError): string {
|
||||
if (error.keyword === "required") {
|
||||
const requiredProperty = (error.params as { requiredProperties?: string[] })
|
||||
.requiredProperties?.[0];
|
||||
if (requiredProperty) {
|
||||
const basePath = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
|
||||
return basePath ? `${basePath}.${requiredProperty}` : requiredProperty;
|
||||
}
|
||||
}
|
||||
const path = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
|
||||
return path || "root";
|
||||
}
|
||||
|
||||
export function validateToolCall(tools: Tool[], toolCall: ToolCall): unknown {
|
||||
const tool = tools.find((t) => t.name === toolCall.name);
|
||||
if (!tool) {
|
||||
throw new Error(`Tool "${toolCall.name}" not found`);
|
||||
}
|
||||
return validateToolArguments(tool, toolCall);
|
||||
}
|
||||
|
||||
export function validateToolArguments(tool: Tool, toolCall: ToolCall): unknown {
|
||||
const args = structuredClone(toolCall.arguments);
|
||||
Value.Convert(tool.parameters, args);
|
||||
|
||||
const validator = getValidator(tool.parameters);
|
||||
if (!hasTypeBoxMetadata(tool.parameters) && isJsonSchemaObject(tool.parameters)) {
|
||||
const coerced = coerceWithJsonSchema(args, tool.parameters);
|
||||
if (coerced !== args) {
|
||||
if (isRecord(args) && isRecord(coerced)) {
|
||||
for (const key of Object.keys(args)) {
|
||||
delete args[key];
|
||||
}
|
||||
Object.assign(args, coerced);
|
||||
} else {
|
||||
return validator.Check(coerced) ? coerced : args;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validator.Check(args)) {
|
||||
return args;
|
||||
}
|
||||
|
||||
const errors =
|
||||
validator
|
||||
.Errors(args)
|
||||
.map((error) => ` - ${formatValidationPath(error)}: ${error.message}`)
|
||||
.join("\n") || "Unknown validation error";
|
||||
|
||||
throw new Error(
|
||||
`Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(toolCall.arguments, null, 2)}`,
|
||||
);
|
||||
}
|
||||
export { validateToolArguments, validateToolCall } from "@openclaw/llm-core";
|
||||
|
||||
41
packages/llm-core/package.json
Normal file
41
packages/llm-core/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@openclaw/llm-core",
|
||||
"version": "0.0.0-private",
|
||||
"private": true,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"import": "./dist/index.mjs",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types.d.mts",
|
||||
"import": "./dist/types.mjs",
|
||||
"default": "./dist/types.mjs"
|
||||
},
|
||||
"./diagnostics": {
|
||||
"types": "./dist/utils/diagnostics.d.mts",
|
||||
"import": "./dist/utils/diagnostics.mjs",
|
||||
"default": "./dist/utils/diagnostics.mjs"
|
||||
},
|
||||
"./event-stream": {
|
||||
"types": "./dist/utils/event-stream.d.mts",
|
||||
"import": "./dist/utils/event-stream.mjs",
|
||||
"default": "./dist/utils/event-stream.mjs"
|
||||
},
|
||||
"./validation": {
|
||||
"types": "./dist/validation.d.mts",
|
||||
"import": "./dist/validation.mjs",
|
||||
"default": "./dist/validation.mjs"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"typebox": "1.1.38"
|
||||
}
|
||||
}
|
||||
4
packages/llm-core/src/index.ts
Normal file
4
packages/llm-core/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./types.js";
|
||||
export * from "./utils/diagnostics.js";
|
||||
export * from "./utils/event-stream.js";
|
||||
export * from "./validation.js";
|
||||
582
packages/llm-core/src/types.ts
Normal file
582
packages/llm-core/src/types.ts
Normal file
@@ -0,0 +1,582 @@
|
||||
export type { AssistantMessageDiagnostic, DiagnosticErrorInfo } from "./utils/diagnostics.js";
|
||||
import type { AssistantMessageDiagnostic } from "./utils/diagnostics.js";
|
||||
|
||||
export type KnownApi =
|
||||
| "openai-completions"
|
||||
| "mistral-conversations"
|
||||
| "openai-responses"
|
||||
| "azure-openai-responses"
|
||||
| "openai-codex-responses"
|
||||
| "anthropic-messages"
|
||||
| "bedrock-converse-stream"
|
||||
| "google-generative-ai"
|
||||
| "google-vertex";
|
||||
|
||||
export type Api = KnownApi | (string & {});
|
||||
|
||||
export type KnownImagesApi = "openrouter-images";
|
||||
|
||||
export type ImagesApi = KnownImagesApi | (string & {});
|
||||
|
||||
export type Provider = string;
|
||||
|
||||
export type KnownImagesProvider = "openrouter";
|
||||
|
||||
export type ImagesProvider = string;
|
||||
|
||||
export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
||||
export type ModelThinkingLevel = "off" | ThinkingLevel;
|
||||
export type ThinkingLevelMap = Partial<Record<ModelThinkingLevel, string | null>>;
|
||||
|
||||
/** Token budgets for each thinking level (token-based providers only) */
|
||||
export interface ThinkingBudgets {
|
||||
minimal?: number;
|
||||
low?: number;
|
||||
medium?: number;
|
||||
high?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
// Base options all providers share
|
||||
export type CacheRetention = "none" | "short" | "long";
|
||||
|
||||
export type Transport = "sse" | "websocket" | "websocket-cached" | "auto";
|
||||
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
export interface ProviderResponse {
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface StreamOptions {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
apiKey?: string;
|
||||
/**
|
||||
* Preferred transport for providers that support multiple transports.
|
||||
* Providers that do not support this option ignore it.
|
||||
*/
|
||||
transport?: Transport;
|
||||
/**
|
||||
* Prompt cache retention preference. Providers map this to their supported values.
|
||||
* Default: "short".
|
||||
*/
|
||||
cacheRetention?: CacheRetention;
|
||||
/**
|
||||
* Optional session identifier for providers that support session-based caching.
|
||||
* Providers can use this to enable prompt caching, request routing, or other
|
||||
* session-aware features. Ignored by providers that don't support it.
|
||||
*/
|
||||
sessionId?: string;
|
||||
/**
|
||||
* Optional provider prompt-cache affinity key, distinct from transcript/session identity.
|
||||
* Providers that do not support separate cache affinity ignore it.
|
||||
*/
|
||||
promptCacheKey?: string;
|
||||
/**
|
||||
* Optional callback for inspecting or replacing provider payloads before sending.
|
||||
* Return undefined to keep the payload unchanged.
|
||||
*/
|
||||
onPayload?: (payload: unknown, model: Model) => MaybePromise<unknown>;
|
||||
/**
|
||||
* Optional callback invoked after an HTTP response is received and before
|
||||
* its body stream is consumed.
|
||||
*/
|
||||
onResponse?: (response: ProviderResponse, model: Model) => void | Promise<void>;
|
||||
/**
|
||||
* Optional custom HTTP headers to include in API requests.
|
||||
* Merged with provider defaults; can override default headers.
|
||||
* Not supported by all providers (e.g., AWS Bedrock uses SDK auth).
|
||||
*/
|
||||
headers?: Record<string, string>;
|
||||
/**
|
||||
* HTTP request timeout in milliseconds for providers/SDKs that support it.
|
||||
* For example, OpenAI and Anthropic SDK clients default to 10 minutes.
|
||||
*/
|
||||
timeoutMs?: number;
|
||||
/**
|
||||
* Maximum retry attempts for providers/SDKs that support client-side retries.
|
||||
* For example, OpenAI and Anthropic SDK clients default to 2.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
/**
|
||||
* Maximum delay in milliseconds to wait for a retry when the server requests a long wait.
|
||||
* If the server's requested delay exceeds this value, the request fails immediately
|
||||
* with an error containing the requested delay, allowing higher-level retry logic
|
||||
* to handle it with user visibility.
|
||||
* Default: 60000 (60 seconds). Set to 0 to disable the cap.
|
||||
*/
|
||||
maxRetryDelayMs?: number;
|
||||
/**
|
||||
* Optional metadata to include in API requests.
|
||||
* Providers extract the fields they understand and ignore the rest.
|
||||
* For example, Anthropic uses `user_id` for abuse tracking and rate limiting.
|
||||
*/
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type ProviderStreamOptions = StreamOptions & Record<string, unknown>;
|
||||
|
||||
export interface ImagesOptions {
|
||||
signal?: AbortSignal;
|
||||
apiKey?: string;
|
||||
/**
|
||||
* Optional callback for inspecting or replacing provider payloads before sending.
|
||||
* Return undefined to keep the payload unchanged.
|
||||
*/
|
||||
onPayload?: (payload: unknown, model: ImagesModel) => MaybePromise<unknown>;
|
||||
/**
|
||||
* Optional callback invoked after an HTTP response is received.
|
||||
*/
|
||||
onResponse?: (response: ProviderResponse, model: ImagesModel) => void | Promise<void>;
|
||||
/**
|
||||
* Optional custom HTTP headers to include in API requests.
|
||||
* Merged with provider defaults; can override default headers.
|
||||
*/
|
||||
headers?: Record<string, string>;
|
||||
/**
|
||||
* HTTP request timeout in milliseconds for providers/SDKs that support it.
|
||||
*/
|
||||
timeoutMs?: number;
|
||||
/**
|
||||
* Maximum retry attempts for providers/SDKs that support client-side retries.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
/**
|
||||
* Maximum delay in milliseconds to wait for a retry when the server requests a long wait.
|
||||
* If the server's requested delay exceeds this value, the request fails immediately
|
||||
* with an error containing the requested delay, allowing higher-level retry logic
|
||||
* to handle it with user visibility.
|
||||
* Default: 60000 (60 seconds). Set to 0 to disable the cap.
|
||||
*/
|
||||
maxRetryDelayMs?: number;
|
||||
/**
|
||||
* Optional metadata to include in API requests.
|
||||
* Providers extract the fields they understand and ignore the rest.
|
||||
*/
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type ProviderImagesOptions = ImagesOptions & Record<string, unknown>;
|
||||
|
||||
// Unified options with reasoning passed to streamSimple() and completeSimple()
|
||||
export interface SimpleStreamOptions extends StreamOptions {
|
||||
reasoning?: ThinkingLevel;
|
||||
/** Custom token budgets for thinking levels (token-based providers only) */
|
||||
thinkingBudgets?: ThinkingBudgets;
|
||||
}
|
||||
|
||||
// Generic StreamFunction with typed options.
|
||||
//
|
||||
// Contract:
|
||||
// - Must return an AssistantMessageEventStream.
|
||||
// - Once invoked, request/model/runtime failures should be encoded in the
|
||||
// returned stream, not thrown.
|
||||
// - Error termination must produce an AssistantMessage with stopReason
|
||||
// "error" or "aborted" and errorMessage, emitted via the stream protocol.
|
||||
export type StreamFunction<
|
||||
TApi extends Api = Api,
|
||||
TOptions extends StreamOptions = StreamOptions,
|
||||
> = (
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: TOptions,
|
||||
) => AssistantMessageEventStreamContract;
|
||||
|
||||
export type ImagesFunction<
|
||||
TApi extends ImagesApi = ImagesApi,
|
||||
TOptions extends ImagesOptions = ImagesOptions,
|
||||
> = (
|
||||
model: ImagesModel<TApi>,
|
||||
context: ImagesContext,
|
||||
options?: TOptions,
|
||||
) => Promise<AssistantImages>;
|
||||
|
||||
export interface TextSignatureV1 {
|
||||
v: 1;
|
||||
id: string;
|
||||
phase?: "commentary" | "final_answer";
|
||||
}
|
||||
|
||||
export interface TextContent {
|
||||
type: "text";
|
||||
text: string;
|
||||
textSignature?: string; // e.g., for OpenAI responses, message metadata (legacy id string or TextSignatureV1 JSON)
|
||||
}
|
||||
|
||||
export interface ThinkingContent {
|
||||
type: "thinking";
|
||||
thinking: string;
|
||||
thinkingSignature?: string; // e.g., for OpenAI responses, the reasoning item ID
|
||||
/** When true, the thinking content was redacted by safety filters. The opaque
|
||||
* encrypted payload is stored in `thinkingSignature` so it can be passed back
|
||||
* to the API for multi-turn continuity. */
|
||||
redacted?: boolean;
|
||||
}
|
||||
|
||||
export interface ImageContent {
|
||||
type: "image";
|
||||
data: string; // base64 encoded image data
|
||||
mimeType: string; // e.g., "image/jpeg", "image/png"
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
type: "toolCall";
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
thoughtSignature?: string; // Google-specific: opaque signature for reusing thought context
|
||||
executionMode?: "sequential" | "parallel";
|
||||
}
|
||||
|
||||
export interface Usage {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
cost: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type StopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
|
||||
|
||||
export interface UserMessage {
|
||||
role: "user";
|
||||
content: string | (TextContent | ImageContent)[];
|
||||
timestamp: number; // Unix timestamp in milliseconds
|
||||
}
|
||||
|
||||
export interface AssistantMessage {
|
||||
role: "assistant";
|
||||
content: (TextContent | ThinkingContent | ToolCall)[];
|
||||
api: Api;
|
||||
provider: Provider;
|
||||
model: string;
|
||||
responseModel?: string; // Concrete `chunk.model` when different from the requested `model` (e.g. OpenRouter `auto` -> `anthropic/...`)
|
||||
responseId?: string; // Provider-specific response/message identifier when the upstream API exposes one
|
||||
diagnostics?: AssistantMessageDiagnostic[]; // Redacted provider/runtime diagnostics for failures and recoveries.
|
||||
usage: Usage;
|
||||
stopReason: StopReason;
|
||||
errorMessage?: string;
|
||||
timestamp: number; // Unix timestamp in milliseconds
|
||||
}
|
||||
|
||||
export interface ToolResultMessage<TDetails = unknown> {
|
||||
role: "toolResult";
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
content: (TextContent | ImageContent)[]; // Supports text and images
|
||||
details?: TDetails;
|
||||
isError: boolean;
|
||||
timestamp: number; // Unix timestamp in milliseconds
|
||||
}
|
||||
|
||||
export type Message = UserMessage | AssistantMessage | ToolResultMessage;
|
||||
|
||||
export type ImagesInputContent = TextContent | ImageContent;
|
||||
export type ImagesOutputContent = TextContent | ImageContent;
|
||||
|
||||
export interface ImagesContext {
|
||||
input: ImagesInputContent[];
|
||||
}
|
||||
|
||||
export type ImagesStopReason = "stop" | "error" | "aborted";
|
||||
|
||||
export interface AssistantImages {
|
||||
api: ImagesApi;
|
||||
provider: ImagesProvider;
|
||||
model: string;
|
||||
output: ImagesOutputContent[];
|
||||
responseId?: string;
|
||||
usage?: Usage;
|
||||
stopReason: ImagesStopReason;
|
||||
errorMessage?: string;
|
||||
timestamp: number; // Unix timestamp in milliseconds
|
||||
}
|
||||
|
||||
import type { TSchema } from "typebox";
|
||||
|
||||
export interface Tool<TParameters extends TSchema = TSchema> {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: TParameters;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
systemPrompt?: string;
|
||||
messages: Message[];
|
||||
tools?: Tool[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Event protocol for AssistantMessageEventStream.
|
||||
*
|
||||
* Streams should emit `start` before partial updates, then terminate with either:
|
||||
* - `done` carrying the final successful AssistantMessage, or
|
||||
* - `error` carrying the final AssistantMessage with stopReason "error" or "aborted"
|
||||
* and errorMessage.
|
||||
*/
|
||||
export type AssistantMessageEvent =
|
||||
| { type: "start"; partial: AssistantMessage }
|
||||
| { type: "text_start"; contentIndex: number; partial: AssistantMessage }
|
||||
| { type: "text_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
||||
| { type: "text_end"; contentIndex: number; content: string; partial: AssistantMessage }
|
||||
| { type: "thinking_start"; contentIndex: number; partial: AssistantMessage }
|
||||
| { type: "thinking_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
||||
| { type: "thinking_end"; contentIndex: number; content: string; partial: AssistantMessage }
|
||||
| { type: "toolcall_start"; contentIndex: number; partial: AssistantMessage }
|
||||
| { type: "toolcall_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
||||
| { type: "toolcall_end"; contentIndex: number; toolCall: ToolCall; partial: AssistantMessage }
|
||||
| {
|
||||
type: "done";
|
||||
reason: Extract<StopReason, "stop" | "length" | "toolUse">;
|
||||
message: AssistantMessage;
|
||||
}
|
||||
| { type: "error"; reason: Extract<StopReason, "aborted" | "error">; error: AssistantMessage };
|
||||
|
||||
export interface AssistantMessageEventStreamContract extends AsyncIterable<AssistantMessageEvent> {
|
||||
push(event: AssistantMessageEvent): void;
|
||||
end(result?: AssistantMessage): void;
|
||||
result(): Promise<AssistantMessage>;
|
||||
}
|
||||
|
||||
export interface AssistantMessageEventStreamLike extends AsyncIterable<AssistantMessageEvent> {
|
||||
result(): Promise<AssistantMessage>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatibility settings for OpenAI-compatible completions APIs.
|
||||
* Use this to override URL-based auto-detection for custom providers.
|
||||
*/
|
||||
export interface OpenAICompletionsCompat {
|
||||
/** Whether the provider supports the `store` field. Default: auto-detected from URL. */
|
||||
supportsStore?: boolean;
|
||||
/** Whether the provider supports the `developer` role (vs `system`). Default: auto-detected from URL. */
|
||||
supportsDeveloperRole?: boolean;
|
||||
/** Whether the provider supports `reasoning_effort`. Default: auto-detected from URL. */
|
||||
supportsReasoningEffort?: boolean;
|
||||
/** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */
|
||||
supportsUsageInStreaming?: boolean;
|
||||
/** Which field to use for max tokens. Default: auto-detected from URL. */
|
||||
maxTokensField?: "max_completion_tokens" | "max_tokens";
|
||||
/** Whether tool results require the `name` field. Default: auto-detected from URL. */
|
||||
requiresToolResultName?: boolean;
|
||||
/** Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL. */
|
||||
requiresAssistantAfterToolResult?: boolean;
|
||||
/** Whether thinking blocks must be converted to text blocks with <thinking> delimiters. Default: auto-detected from URL. */
|
||||
requiresThinkingAsText?: boolean;
|
||||
/** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */
|
||||
requiresReasoningContentOnAssistantMessages?: boolean;
|
||||
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses top-level enable_thinking: boolean, "qwen" uses top-level enable_thinking: boolean, and "qwen-chat-template" uses chat_template_kwargs.enable_thinking. Default: "openai". */
|
||||
thinkingFormat?:
|
||||
| "openai"
|
||||
| "openrouter"
|
||||
| "deepseek"
|
||||
| "together"
|
||||
| "zai"
|
||||
| "qwen"
|
||||
| "qwen-chat-template";
|
||||
/** OpenRouter-specific routing preferences. Only used when baseUrl points to OpenRouter. */
|
||||
openRouterRouting?: OpenRouterRouting;
|
||||
/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */
|
||||
vercelGatewayRouting?: VercelGatewayRouting;
|
||||
/** Whether z.ai supports top-level `tool_stream: true` for streaming tool call deltas. Default: false. */
|
||||
zaiToolStream?: boolean;
|
||||
/** Whether the provider supports the `strict` field in tool definitions. Default: true. */
|
||||
supportsStrictMode?: boolean;
|
||||
/** Cache control convention for prompt caching. "anthropic" applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content. */
|
||||
cacheControlFormat?: "anthropic";
|
||||
/** Whether to send known session-affinity headers (`session_id`, `x-client-request-id`, `x-session-affinity`) from `options.sessionId` when caching is enabled. Default: false. */
|
||||
sendSessionAffinityHeaders?: boolean;
|
||||
/** Whether the provider supports long prompt cache retention (`prompt_cache_retention: "24h"` or Anthropic-style `cache_control.ttl: "1h"`, depending on format). Default: true. */
|
||||
supportsLongCacheRetention?: boolean;
|
||||
}
|
||||
|
||||
/** Compatibility settings for OpenAI Responses APIs. */
|
||||
export interface OpenAIResponsesCompat {
|
||||
/** Whether to send the OpenAI `session_id` cache-affinity header from `options.sessionId` when caching is enabled. Default: true. */
|
||||
sendSessionIdHeader?: boolean;
|
||||
/** Whether the provider supports `prompt_cache_retention: "24h"`. Default: true. */
|
||||
supportsLongCacheRetention?: boolean;
|
||||
}
|
||||
|
||||
/** Compatibility settings for Anthropic Messages-compatible APIs. */
|
||||
export interface AnthropicMessagesCompat {
|
||||
/**
|
||||
* Whether the provider accepts per-tool `eager_input_streaming`.
|
||||
* When false, the Anthropic provider omits `tools[].eager_input_streaming`
|
||||
* and sends the legacy `fine-grained-tool-streaming-2025-05-14` beta header
|
||||
* for tool-enabled requests.
|
||||
* Default: true.
|
||||
*/
|
||||
supportsEagerToolInputStreaming?: boolean;
|
||||
/** Whether the provider supports Anthropic long cache retention (`cache_control.ttl: "1h"`). Default: true. */
|
||||
supportsLongCacheRetention?: boolean;
|
||||
/**
|
||||
* Whether to send the `x-session-affinity` header from `options.sessionId`
|
||||
* when caching is enabled. Required for providers like Fireworks that use
|
||||
* session affinity for prompt cache routing (requests to the same replica
|
||||
* maximize cache hits).
|
||||
* Default: false.
|
||||
*/
|
||||
sendSessionAffinityHeaders?: boolean;
|
||||
/**
|
||||
* Whether the provider supports Anthropic-style `cache_control` markers on
|
||||
* tool definitions. When false, `cache_control` is omitted from tool params.
|
||||
* Some Anthropic-compatible providers (e.g., Fireworks) do not support this
|
||||
* field on tools and may reject or ignore it.
|
||||
* Default: true.
|
||||
*/
|
||||
supportsCacheControlOnTools?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenRouter provider routing preferences.
|
||||
* Controls which upstream providers OpenRouter routes requests to.
|
||||
* Sent as the `provider` field in the OpenRouter API request body.
|
||||
* @see https://openrouter.ai/docs/guides/routing/provider-selection
|
||||
*/
|
||||
export interface OpenRouterRouting {
|
||||
/** Whether to allow backup providers to serve requests. Default: true. */
|
||||
allow_fallbacks?: boolean;
|
||||
/** Whether to filter providers to only those that support all parameters in the request. Default: false. */
|
||||
require_parameters?: boolean;
|
||||
/** Data collection setting. "allow" (default): allow providers that may store/train on data. "deny": only use providers that don't collect user data. */
|
||||
data_collection?: "deny" | "allow";
|
||||
/** Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. */
|
||||
zdr?: boolean;
|
||||
/** Whether to restrict routing to only models that allow text distillation. */
|
||||
enforce_distillable_text?: boolean;
|
||||
/** An ordered list of provider names/slugs to try in sequence, falling back to the next if unavailable. */
|
||||
order?: string[];
|
||||
/** List of provider names/slugs to exclusively allow for this request. */
|
||||
only?: string[];
|
||||
/** List of provider names/slugs to skip for this request. */
|
||||
ignore?: string[];
|
||||
/** A list of quantization levels to filter providers by (e.g., ["fp16", "bf16", "fp8", "fp6", "int8", "int4", "fp4", "fp32"]). */
|
||||
quantizations?: string[];
|
||||
/** Sorting strategy. Can be a string (e.g., "price", "throughput", "latency") or an object with `by` and `partition`. */
|
||||
sort?:
|
||||
| string
|
||||
| {
|
||||
/** The sorting metric: "price", "throughput", "latency". */
|
||||
by?: string;
|
||||
/** Partitioning strategy: "model" (default) or "none". */
|
||||
partition?: string | null;
|
||||
};
|
||||
/** Maximum price per million tokens (USD). */
|
||||
max_price?: {
|
||||
/** Price per million prompt tokens. */
|
||||
prompt?: number | string;
|
||||
/** Price per million completion tokens. */
|
||||
completion?: number | string;
|
||||
/** Price per image. */
|
||||
image?: number | string;
|
||||
/** Price per audio unit. */
|
||||
audio?: number | string;
|
||||
/** Price per request. */
|
||||
request?: number | string;
|
||||
};
|
||||
/** Preferred minimum throughput (tokens/second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. */
|
||||
preferred_min_throughput?:
|
||||
| number
|
||||
| {
|
||||
/** Minimum tokens/second at the 50th percentile. */
|
||||
p50?: number;
|
||||
/** Minimum tokens/second at the 75th percentile. */
|
||||
p75?: number;
|
||||
/** Minimum tokens/second at the 90th percentile. */
|
||||
p90?: number;
|
||||
/** Minimum tokens/second at the 99th percentile. */
|
||||
p99?: number;
|
||||
};
|
||||
/** Preferred maximum latency (seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. */
|
||||
preferred_max_latency?:
|
||||
| number
|
||||
| {
|
||||
/** Maximum latency in seconds at the 50th percentile. */
|
||||
p50?: number;
|
||||
/** Maximum latency in seconds at the 75th percentile. */
|
||||
p75?: number;
|
||||
/** Maximum latency in seconds at the 90th percentile. */
|
||||
p90?: number;
|
||||
/** Maximum latency in seconds at the 99th percentile. */
|
||||
p99?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vercel AI Gateway routing preferences.
|
||||
* Controls which upstream providers the gateway routes requests to.
|
||||
* @see https://vercel.com/docs/ai-gateway/models-and-providers/provider-options
|
||||
*/
|
||||
export interface VercelGatewayRouting {
|
||||
/** List of provider slugs to exclusively use for this request (e.g., ["bedrock", "anthropic"]). */
|
||||
only?: string[];
|
||||
/** List of provider slugs to try in order (e.g., ["anthropic", "openai"]). */
|
||||
order?: string[];
|
||||
}
|
||||
|
||||
// Model interface for the unified model system
|
||||
export interface Model<TApi extends Api = Api> {
|
||||
id: string;
|
||||
name: string;
|
||||
api: TApi;
|
||||
provider: Provider;
|
||||
baseUrl: string;
|
||||
reasoning: boolean;
|
||||
/**
|
||||
* Maps OpenClaw thinking levels to provider/model-specific values.
|
||||
* Missing keys use provider defaults. null marks a level as unsupported.
|
||||
*/
|
||||
thinkingLevelMap?: ThinkingLevelMap;
|
||||
input: ("text" | "image")[];
|
||||
cost: {
|
||||
input: number; // $/million tokens
|
||||
output: number; // $/million tokens
|
||||
cacheRead: number; // $/million tokens
|
||||
cacheWrite: number; // $/million tokens
|
||||
};
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
headers?: Record<string, string>;
|
||||
/** Compatibility overrides for OpenAI-compatible APIs. If not set, auto-detected from baseUrl. */
|
||||
compat?: TApi extends "openai-completions"
|
||||
? OpenAICompletionsCompat
|
||||
: TApi extends "openai-responses"
|
||||
? OpenAIResponsesCompat
|
||||
: TApi extends "anthropic-messages"
|
||||
? AnthropicMessagesCompat
|
||||
: never;
|
||||
}
|
||||
|
||||
export interface ImagesModel<TApi extends ImagesApi = ImagesApi> extends Omit<
|
||||
Model,
|
||||
"api" | "provider" | "reasoning" | "contextWindow" | "maxTokens" | "compat"
|
||||
> {
|
||||
api: TApi;
|
||||
provider: ImagesProvider;
|
||||
output: ("text" | "image")[];
|
||||
}
|
||||
|
||||
export type StreamFn = (
|
||||
model: Model,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
) => AssistantMessageEventStreamLike | Promise<AssistantMessageEventStreamLike>;
|
||||
|
||||
export type CompleteSimpleFn = (
|
||||
model: Model,
|
||||
context: Pick<Context, "systemPrompt" | "messages">,
|
||||
options?: SimpleStreamOptions,
|
||||
) => Promise<AssistantMessage>;
|
||||
|
||||
export type ValidateToolArgumentsFn = (tool: Tool, toolCall: ToolCall) => unknown;
|
||||
51
packages/llm-core/src/utils/diagnostics.ts
Normal file
51
packages/llm-core/src/utils/diagnostics.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export interface DiagnosticErrorInfo {
|
||||
name?: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
code?: string | number;
|
||||
}
|
||||
|
||||
export interface AssistantMessageDiagnostic {
|
||||
type: string;
|
||||
timestamp: number;
|
||||
error?: DiagnosticErrorInfo;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function formatThrownValue(value: unknown): string {
|
||||
if (value instanceof Error) {
|
||||
return value.message || value.name;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function extractDiagnosticError(error: unknown): DiagnosticErrorInfo {
|
||||
if (!(error instanceof Error)) {
|
||||
return { name: "ThrownValue", message: formatThrownValue(error) };
|
||||
}
|
||||
const code = (error as Error & { code?: unknown }).code;
|
||||
return {
|
||||
name: error.name || undefined,
|
||||
message: error.message || error.name,
|
||||
stack: error.stack,
|
||||
code: typeof code === "string" || typeof code === "number" ? code : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function createAssistantMessageDiagnostic(
|
||||
type: string,
|
||||
error: unknown,
|
||||
details?: Record<string, unknown>,
|
||||
): AssistantMessageDiagnostic {
|
||||
return { type, timestamp: Date.now(), error: extractDiagnosticError(error), details };
|
||||
}
|
||||
|
||||
export function appendAssistantMessageDiagnostic(
|
||||
message: { diagnostics?: AssistantMessageDiagnostic[] },
|
||||
diagnostic: AssistantMessageDiagnostic,
|
||||
): void {
|
||||
message.diagnostics = [...(message.diagnostics ?? []), diagnostic];
|
||||
}
|
||||
101
packages/llm-core/src/utils/event-stream.ts
Normal file
101
packages/llm-core/src/utils/event-stream.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type {
|
||||
AssistantMessage,
|
||||
AssistantMessageEvent,
|
||||
AssistantMessageEventStreamContract,
|
||||
} from "../types.js";
|
||||
|
||||
// Generic event stream class for async iteration
|
||||
export class EventStream<T, R = T> implements AsyncIterable<T> {
|
||||
private queue: T[] = [];
|
||||
private waiting: ((value: IteratorResult<T>) => void)[] = [];
|
||||
private done = false;
|
||||
private finalResultPromise: Promise<R>;
|
||||
private resolveFinalResult!: (result: R) => void;
|
||||
private isComplete: (event: T) => boolean;
|
||||
private extractResult: (event: T) => R;
|
||||
|
||||
constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R) {
|
||||
this.isComplete = isComplete;
|
||||
this.extractResult = extractResult;
|
||||
this.finalResultPromise = new Promise((resolve) => {
|
||||
this.resolveFinalResult = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
push(event: T): void {
|
||||
if (this.done) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isComplete(event)) {
|
||||
this.done = true;
|
||||
this.resolveFinalResult(this.extractResult(event));
|
||||
}
|
||||
|
||||
// Deliver to waiting consumer or queue it
|
||||
const waiter = this.waiting.shift();
|
||||
if (waiter) {
|
||||
waiter({ value: event, done: false });
|
||||
} else {
|
||||
this.queue.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
end(result?: R): void {
|
||||
this.done = true;
|
||||
if (result !== undefined) {
|
||||
this.resolveFinalResult(result);
|
||||
}
|
||||
// Notify all waiting consumers that we're done
|
||||
while (this.waiting.length > 0) {
|
||||
const waiter = this.waiting.shift()!;
|
||||
waiter({ value: undefined as unknown, done: true });
|
||||
}
|
||||
}
|
||||
|
||||
async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
while (true) {
|
||||
if (this.queue.length > 0) {
|
||||
yield this.queue.shift()!;
|
||||
} else if (this.done) {
|
||||
return;
|
||||
} else {
|
||||
const result = await new Promise<IteratorResult<T>>((resolve) =>
|
||||
this.waiting.push(resolve),
|
||||
);
|
||||
if (result.done) {
|
||||
return;
|
||||
}
|
||||
yield result.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result(): Promise<R> {
|
||||
return this.finalResultPromise;
|
||||
}
|
||||
}
|
||||
|
||||
export class AssistantMessageEventStream
|
||||
extends EventStream<AssistantMessageEvent, AssistantMessage>
|
||||
implements AssistantMessageEventStreamContract
|
||||
{
|
||||
constructor() {
|
||||
super(
|
||||
(event) => event.type === "done" || event.type === "error",
|
||||
(event) => {
|
||||
if (event.type === "done") {
|
||||
return event.message;
|
||||
} else if (event.type === "error") {
|
||||
return event.error;
|
||||
}
|
||||
throw new Error("Unexpected event type for final result");
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Factory function for AssistantMessageEventStream (for use in extensions) */
|
||||
export function createAssistantMessageEventStream(): AssistantMessageEventStream {
|
||||
return new AssistantMessageEventStream();
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Tool } from "./llm.js";
|
||||
import type { Tool } from "./types.js";
|
||||
import { validateToolArguments } from "./validation.js";
|
||||
|
||||
const decimalTool = {
|
||||
324
packages/llm-core/src/validation.ts
Normal file
324
packages/llm-core/src/validation.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import { Compile } from "typebox/compile";
|
||||
import type { TLocalizedValidationError } from "typebox/error";
|
||||
import { Value } from "typebox/value";
|
||||
import type { Tool, ToolCall } from "./types.js";
|
||||
|
||||
const validatorCache = new WeakMap<object, ReturnType<typeof Compile>>();
|
||||
const TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
|
||||
|
||||
interface JsonSchemaObject {
|
||||
type?: string | string[];
|
||||
properties?: Record<string, JsonSchemaObject>;
|
||||
items?: JsonSchemaObject | JsonSchemaObject[];
|
||||
additionalProperties?: boolean | JsonSchemaObject;
|
||||
allOf?: JsonSchemaObject[];
|
||||
anyOf?: JsonSchemaObject[];
|
||||
oneOf?: JsonSchemaObject[];
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isJsonSchemaObject(value: unknown): value is JsonSchemaObject {
|
||||
return isRecord(value);
|
||||
}
|
||||
|
||||
function hasTypeBoxMetadata(schema: unknown): boolean {
|
||||
return isRecord(schema) && Object.getOwnPropertySymbols(schema).includes(TYPEBOX_KIND);
|
||||
}
|
||||
|
||||
function getSchemaTypes(schema: JsonSchemaObject): string[] {
|
||||
if (typeof schema.type === "string") {
|
||||
return [schema.type];
|
||||
}
|
||||
if (Array.isArray(schema.type)) {
|
||||
return schema.type.filter((type): type is string => typeof type === "string");
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function matchesJsonType(value: unknown, type: string): boolean {
|
||||
switch (type) {
|
||||
case "number":
|
||||
return typeof value === "number";
|
||||
case "integer":
|
||||
return typeof value === "number" && Number.isInteger(value);
|
||||
case "boolean":
|
||||
return typeof value === "boolean";
|
||||
case "string":
|
||||
return typeof value === "string";
|
||||
case "null":
|
||||
return value === null;
|
||||
case "array":
|
||||
return Array.isArray(value);
|
||||
case "object":
|
||||
return isRecord(value) && !Array.isArray(value);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidatorSchema(value: unknown): value is Tool["parameters"] {
|
||||
return isRecord(value);
|
||||
}
|
||||
|
||||
const JSON_NUMBER_TOKEN_RE = /^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:e[+-]?\d+)?$/iu;
|
||||
|
||||
function parseJsonNumberString(value: string): number | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || !JSON_NUMBER_TOKEN_RE.test(trimmed)) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number(trimmed);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function parseJsonIntegerString(value: string): number | undefined {
|
||||
const parsed = parseJsonNumberString(value);
|
||||
return parsed !== undefined && Number.isSafeInteger(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function getSubSchemaValidator(schema: JsonSchemaObject): ReturnType<typeof Compile> | undefined {
|
||||
if (!isValidatorSchema(schema)) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return getValidator(schema);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function coercePrimitiveByType(value: unknown, type: string): unknown {
|
||||
switch (type) {
|
||||
case "number": {
|
||||
if (value === null) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
const parsed = parseJsonNumberString(value);
|
||||
if (parsed !== undefined) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
case "integer": {
|
||||
if (value === null) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
const parsed = parseJsonIntegerString(value);
|
||||
if (parsed !== undefined) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value ? 1 : 0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
case "boolean": {
|
||||
if (value === null) {
|
||||
return false;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
if (value === "true") {
|
||||
return true;
|
||||
}
|
||||
if (value === "false") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
if (value === 1) {
|
||||
return true;
|
||||
}
|
||||
if (value === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
case "string": {
|
||||
if (value === null) {
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
case "null": {
|
||||
if (value === "" || value === 0 || value === false) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function applySchemaObjectCoercion(value: Record<string, unknown>, schema: JsonSchemaObject): void {
|
||||
const properties = schema.properties;
|
||||
const definedKeys = new Set<string>(properties ? Object.keys(properties) : []);
|
||||
|
||||
if (properties) {
|
||||
for (const [key, propertySchema] of Object.entries(properties)) {
|
||||
if (key in value) {
|
||||
value[key] = coerceWithJsonSchema(value[key], propertySchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.additionalProperties && isJsonSchemaObject(schema.additionalProperties)) {
|
||||
for (const [key, propertyValue] of Object.entries(value)) {
|
||||
if (!definedKeys.has(key)) {
|
||||
value[key] = coerceWithJsonSchema(propertyValue, schema.additionalProperties);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applySchemaArrayCoercion(value: unknown[], schema: JsonSchemaObject): void {
|
||||
if (Array.isArray(schema.items)) {
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const itemSchema = schema.items[index];
|
||||
if (itemSchema) {
|
||||
value[index] = coerceWithJsonSchema(value[index], itemSchema);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isJsonSchemaObject(schema.items)) {
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
value[index] = coerceWithJsonSchema(value[index], schema.items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function coerceWithUnionSchema(value: unknown, schemas: JsonSchemaObject[]): unknown {
|
||||
for (const schema of schemas) {
|
||||
const candidate = structuredClone(value);
|
||||
const coerced = coerceWithJsonSchema(candidate, schema);
|
||||
const validator = getSubSchemaValidator(schema);
|
||||
if (validator?.Check(coerced)) {
|
||||
return coerced;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function coerceWithJsonSchema(value: unknown, schema: JsonSchemaObject): unknown {
|
||||
let nextValue = value;
|
||||
|
||||
if (Array.isArray(schema.allOf)) {
|
||||
for (const nested of schema.allOf) {
|
||||
nextValue = coerceWithJsonSchema(nextValue, nested);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(schema.anyOf)) {
|
||||
nextValue = coerceWithUnionSchema(nextValue, schema.anyOf);
|
||||
}
|
||||
|
||||
if (Array.isArray(schema.oneOf)) {
|
||||
nextValue = coerceWithUnionSchema(nextValue, schema.oneOf);
|
||||
}
|
||||
|
||||
const schemaTypes = getSchemaTypes(schema);
|
||||
const matchesUnionMember =
|
||||
schemaTypes.length > 1 &&
|
||||
schemaTypes.some((schemaType) => matchesJsonType(nextValue, schemaType));
|
||||
if (schemaTypes.length > 0 && !matchesUnionMember) {
|
||||
for (const schemaType of schemaTypes) {
|
||||
const candidate = coercePrimitiveByType(nextValue, schemaType);
|
||||
if (candidate !== nextValue) {
|
||||
nextValue = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schemaTypes.includes("object") && isRecord(nextValue) && !Array.isArray(nextValue)) {
|
||||
applySchemaObjectCoercion(nextValue, schema);
|
||||
}
|
||||
|
||||
if (schemaTypes.includes("array") && Array.isArray(nextValue)) {
|
||||
applySchemaArrayCoercion(nextValue, schema);
|
||||
}
|
||||
|
||||
return nextValue;
|
||||
}
|
||||
|
||||
function getValidator(schema: Tool["parameters"]): ReturnType<typeof Compile> {
|
||||
const key = schema as object;
|
||||
const cached = validatorCache.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const validator = Compile(schema);
|
||||
validatorCache.set(key, validator);
|
||||
return validator;
|
||||
}
|
||||
|
||||
function formatValidationPath(error: TLocalizedValidationError): string {
|
||||
if (error.keyword === "required") {
|
||||
const requiredProperty = (error.params as { requiredProperties?: string[] })
|
||||
.requiredProperties?.[0];
|
||||
if (requiredProperty) {
|
||||
const basePath = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
|
||||
return basePath ? `${basePath}.${requiredProperty}` : requiredProperty;
|
||||
}
|
||||
}
|
||||
const path = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
|
||||
return path || "root";
|
||||
}
|
||||
|
||||
export function validateToolCall(tools: Tool[], toolCall: ToolCall): unknown {
|
||||
const tool = tools.find((t) => t.name === toolCall.name);
|
||||
if (!tool) {
|
||||
throw new Error(`Tool "${toolCall.name}" not found`);
|
||||
}
|
||||
return validateToolArguments(tool, toolCall);
|
||||
}
|
||||
|
||||
export function validateToolArguments(tool: Tool, toolCall: ToolCall): unknown {
|
||||
const args = structuredClone(toolCall.arguments);
|
||||
Value.Convert(tool.parameters, args);
|
||||
|
||||
const validator = getValidator(tool.parameters);
|
||||
if (!hasTypeBoxMetadata(tool.parameters) && isJsonSchemaObject(tool.parameters)) {
|
||||
const coerced = coerceWithJsonSchema(args, tool.parameters);
|
||||
if (coerced !== args) {
|
||||
if (isRecord(args) && isRecord(coerced)) {
|
||||
for (const key of Object.keys(args)) {
|
||||
delete args[key];
|
||||
}
|
||||
Object.assign(args, coerced);
|
||||
} else {
|
||||
return validator.Check(coerced) ? coerced : args;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validator.Check(args)) {
|
||||
return args;
|
||||
}
|
||||
|
||||
const errors =
|
||||
validator
|
||||
.Errors(args)
|
||||
.map((error) => ` - ${formatValidationPath(error)}: ${error.message}`)
|
||||
.join("\n") || "Unknown validation error";
|
||||
|
||||
throw new Error(
|
||||
`Validation failed for tool "${toolCall.name}":\n${errors}\n\nReceived arguments:\n${JSON.stringify(toolCall.arguments, null, 2)}`,
|
||||
);
|
||||
}
|
||||
8
packages/llm-core/tsconfig.json
Normal file
8
packages/llm-core/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
31
packages/llm-runtime/package.json
Normal file
31
packages/llm-runtime/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@openclaw/llm-runtime",
|
||||
"version": "0.0.0-private",
|
||||
"private": true,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"import": "./dist/index.mjs",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"./api-registry": {
|
||||
"types": "./dist/api-registry.d.mts",
|
||||
"import": "./dist/api-registry.mjs",
|
||||
"default": "./dist/api-registry.mjs"
|
||||
},
|
||||
"./stream": {
|
||||
"types": "./dist/stream.d.mts",
|
||||
"import": "./dist/stream.mjs",
|
||||
"default": "./dist/stream.mjs"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@openclaw/llm-core": "workspace:*"
|
||||
}
|
||||
}
|
||||
41
packages/llm-runtime/src/api-registry.test.ts
Normal file
41
packages/llm-runtime/src/api-registry.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { createAssistantMessageEventStream, type Model } from "@openclaw/llm-core";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { getApiProvider, registerApiProvider, unregisterApiProviders } from "./api-registry.js";
|
||||
|
||||
const TEST_SOURCE_ID = "test:llm-runtime-api-registry";
|
||||
|
||||
const model = {
|
||||
id: "test-model",
|
||||
name: "Test Model",
|
||||
api: "test-api",
|
||||
provider: "test-provider",
|
||||
baseUrl: "https://example.invalid",
|
||||
input: ["text"],
|
||||
reasoning: false,
|
||||
contextWindow: 1000,
|
||||
maxTokens: 100,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
} satisfies Model;
|
||||
|
||||
describe("LLM API registry", () => {
|
||||
afterEach(() => {
|
||||
unregisterApiProviders(TEST_SOURCE_ID);
|
||||
});
|
||||
|
||||
it("rejects mismatched model API calls", () => {
|
||||
registerApiProvider(
|
||||
{
|
||||
api: "test-api",
|
||||
stream: () => createAssistantMessageEventStream(),
|
||||
streamSimple: () => createAssistantMessageEventStream(),
|
||||
},
|
||||
TEST_SOURCE_ID,
|
||||
);
|
||||
|
||||
const provider = getApiProvider("test-api");
|
||||
expect(provider).toBeDefined();
|
||||
expect(() => provider?.streamSimple({ ...model, api: "other-api" }, { messages: [] })).toThrow(
|
||||
"Mismatched api: other-api expected test-api",
|
||||
);
|
||||
});
|
||||
});
|
||||
104
packages/llm-runtime/src/api-registry.ts
Normal file
104
packages/llm-runtime/src/api-registry.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessageEventStreamContract,
|
||||
Context,
|
||||
Model,
|
||||
SimpleStreamOptions,
|
||||
StreamFunction,
|
||||
StreamOptions,
|
||||
} from "../../llm-core/src/index.js";
|
||||
|
||||
// Type-only source import keeps plugin SDK declarations self-contained; package
|
||||
// runtime emits no llm-core import from this module.
|
||||
|
||||
export type ApiStreamFunction = (
|
||||
model: Model,
|
||||
context: Context,
|
||||
options?: StreamOptions,
|
||||
) => AssistantMessageEventStreamContract;
|
||||
|
||||
export type ApiStreamSimpleFunction = (
|
||||
model: Model,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
) => AssistantMessageEventStreamContract;
|
||||
|
||||
export interface ApiProvider<
|
||||
TApi extends Api = Api,
|
||||
TOptions extends StreamOptions = StreamOptions,
|
||||
> {
|
||||
api: TApi;
|
||||
stream: StreamFunction<TApi, TOptions>;
|
||||
streamSimple: StreamFunction<TApi, SimpleStreamOptions>;
|
||||
}
|
||||
|
||||
interface ApiProviderInternal {
|
||||
api: Api;
|
||||
stream: ApiStreamFunction;
|
||||
streamSimple: ApiStreamSimpleFunction;
|
||||
}
|
||||
|
||||
type RegisteredApiProvider = {
|
||||
provider: ApiProviderInternal;
|
||||
sourceId?: string;
|
||||
};
|
||||
|
||||
const apiProviderRegistry = new Map<string, RegisteredApiProvider>();
|
||||
|
||||
function wrapStream<TApi extends Api, TOptions extends StreamOptions>(
|
||||
api: TApi,
|
||||
stream: StreamFunction<TApi, TOptions>,
|
||||
): ApiStreamFunction {
|
||||
return (model, context, options) => {
|
||||
if (model.api !== api) {
|
||||
throw new Error(`Mismatched api: ${model.api} expected ${api}`);
|
||||
}
|
||||
return stream(model as Model<TApi>, context, options as TOptions);
|
||||
};
|
||||
}
|
||||
|
||||
function wrapStreamSimple<TApi extends Api>(
|
||||
api: TApi,
|
||||
streamSimple: StreamFunction<TApi, SimpleStreamOptions>,
|
||||
): ApiStreamSimpleFunction {
|
||||
return (model, context, options) => {
|
||||
if (model.api !== api) {
|
||||
throw new Error(`Mismatched api: ${model.api} expected ${api}`);
|
||||
}
|
||||
return streamSimple(model as Model<TApi>, context, options);
|
||||
};
|
||||
}
|
||||
|
||||
export function registerApiProvider<TApi extends Api, TOptions extends StreamOptions>(
|
||||
provider: ApiProvider<TApi, TOptions>,
|
||||
sourceId?: string,
|
||||
): void {
|
||||
apiProviderRegistry.set(provider.api, {
|
||||
provider: {
|
||||
api: provider.api,
|
||||
stream: wrapStream(provider.api, provider.stream),
|
||||
streamSimple: wrapStreamSimple(provider.api, provider.streamSimple),
|
||||
},
|
||||
sourceId,
|
||||
});
|
||||
}
|
||||
|
||||
export function getApiProvider(api: Api): ApiProviderInternal | undefined {
|
||||
return apiProviderRegistry.get(api)?.provider;
|
||||
}
|
||||
|
||||
export function getApiProviders(): ApiProviderInternal[] {
|
||||
return Array.from(apiProviderRegistry.values(), (entry) => entry.provider);
|
||||
}
|
||||
|
||||
export function unregisterApiProviders(sourceId: string): void {
|
||||
for (const [api, entry] of apiProviderRegistry.entries()) {
|
||||
if (entry.sourceId === sourceId) {
|
||||
apiProviderRegistry.delete(api);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function clearApiProviders(): void {
|
||||
apiProviderRegistry.clear();
|
||||
}
|
||||
9
packages/llm-runtime/src/index.ts
Normal file
9
packages/llm-runtime/src/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
clearApiProviders,
|
||||
getApiProvider,
|
||||
getApiProviders,
|
||||
registerApiProvider,
|
||||
unregisterApiProviders,
|
||||
type ApiProvider,
|
||||
} from "./api-registry.js";
|
||||
export { complete, completeSimple, stream, streamSimple } from "./stream.js";
|
||||
57
packages/llm-runtime/src/stream.ts
Normal file
57
packages/llm-runtime/src/stream.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessage,
|
||||
AssistantMessageEventStreamContract,
|
||||
Context,
|
||||
Model,
|
||||
ProviderStreamOptions,
|
||||
SimpleStreamOptions,
|
||||
StreamOptions,
|
||||
} from "../../llm-core/src/index.js";
|
||||
// Type-only source import keeps plugin SDK declarations self-contained; package
|
||||
// runtime emits no llm-core import from this module.
|
||||
import { getApiProvider } from "./api-registry.js";
|
||||
|
||||
function resolveApiProvider(api: Api) {
|
||||
const provider = getApiProvider(api);
|
||||
if (!provider) {
|
||||
throw new Error(`No API provider registered for api: ${api}`);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
export function stream<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: ProviderStreamOptions,
|
||||
): AssistantMessageEventStreamContract {
|
||||
const provider = resolveApiProvider(model.api);
|
||||
return provider.stream(model, context, options as StreamOptions);
|
||||
}
|
||||
|
||||
export async function complete<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: ProviderStreamOptions,
|
||||
): Promise<AssistantMessage> {
|
||||
const s = stream(model, context, options);
|
||||
return s.result();
|
||||
}
|
||||
|
||||
export function streamSimple<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
): AssistantMessageEventStreamContract {
|
||||
const provider = resolveApiProvider(model.api);
|
||||
return provider.streamSimple(model, context, options);
|
||||
}
|
||||
|
||||
export async function completeSimple<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
): Promise<AssistantMessage> {
|
||||
const s = streamSimple(model, context, options);
|
||||
return s.result();
|
||||
}
|
||||
8
packages/llm-runtime/tsconfig.json
Normal file
8
packages/llm-runtime/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -1769,6 +1769,9 @@ importers:
|
||||
|
||||
packages/agent-core:
|
||||
dependencies:
|
||||
'@openclaw/llm-core':
|
||||
specifier: workspace:*
|
||||
version: link:../llm-core
|
||||
ignore:
|
||||
specifier: 7.0.5
|
||||
version: 7.0.5
|
||||
@@ -1797,6 +1800,18 @@ importers:
|
||||
specifier: 1.1.38
|
||||
version: 1.1.38
|
||||
|
||||
packages/llm-core:
|
||||
dependencies:
|
||||
typebox:
|
||||
specifier: 1.1.38
|
||||
version: 1.1.38
|
||||
|
||||
packages/llm-runtime:
|
||||
dependencies:
|
||||
'@openclaw/llm-core':
|
||||
specifier: workspace:*
|
||||
version: link:../llm-core
|
||||
|
||||
packages/memory-host-sdk: {}
|
||||
|
||||
packages/net-policy:
|
||||
|
||||
@@ -46,10 +46,12 @@ export const BUILD_ALL_STEPS = [
|
||||
"pnpm-lock.yaml",
|
||||
"npm-shrinkwrap.json",
|
||||
"packages/plugin-sdk/package.json",
|
||||
"packages/llm-core/package.json",
|
||||
"packages/memory-host-sdk/package.json",
|
||||
"tsconfig.json",
|
||||
"tsconfig.plugin-sdk.dts.json",
|
||||
"src/plugin-sdk",
|
||||
"packages/llm-core/src",
|
||||
"packages/memory-host-sdk/src",
|
||||
"src/types",
|
||||
"src/video-generation/dashscope-compatible.ts",
|
||||
|
||||
@@ -49,6 +49,16 @@ export const EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS = {
|
||||
"@openclaw/discord/api.js": ["../dist/plugin-sdk/extensions/discord/api.d.ts"],
|
||||
"@openclaw/slack/api.js": ["../dist/plugin-sdk/extensions/slack/api.d.ts"],
|
||||
"@openclaw/whatsapp/api.js": ["../dist/plugin-sdk/extensions/whatsapp/api.d.ts"],
|
||||
"@openclaw/llm-core": ["../dist/plugin-sdk/packages/llm-core/src/index.d.ts"],
|
||||
"@openclaw/llm-core/diagnostics": [
|
||||
"../dist/plugin-sdk/packages/llm-core/src/utils/diagnostics.d.ts",
|
||||
],
|
||||
"@openclaw/llm-core/event-stream": [
|
||||
"../dist/plugin-sdk/packages/llm-core/src/utils/event-stream.d.ts",
|
||||
],
|
||||
"@openclaw/llm-core/types": ["../dist/plugin-sdk/packages/llm-core/src/types.d.ts"],
|
||||
"@openclaw/llm-core/validation": ["../dist/plugin-sdk/packages/llm-core/src/validation.d.ts"],
|
||||
"@openclaw/llm-core/*": ["../dist/plugin-sdk/packages/llm-core/src/*.d.ts"],
|
||||
"@openclaw/*.js": ["../packages/plugin-sdk/dist/extensions/*.d.ts", "../extensions/*"],
|
||||
"@openclaw/*": ["../packages/plugin-sdk/dist/extensions/*", "../extensions/*"],
|
||||
"openclaw/plugin-sdk/qa-channel": ["../dist/plugin-sdk/src/plugin-sdk/qa-channel.d.ts"],
|
||||
|
||||
@@ -15,6 +15,7 @@ const PLUGIN_SDK_TYPE_INPUTS = [
|
||||
"tsconfig.json",
|
||||
"src/plugin-sdk",
|
||||
"src/auto-reply",
|
||||
"packages/llm-core/src",
|
||||
"packages/memory-host-sdk/src",
|
||||
"src/video-generation/dashscope-compatible.ts",
|
||||
"src/video-generation/types.ts",
|
||||
@@ -26,6 +27,11 @@ const ROOT_DTS_REQUIRED_OUTPUTS = [
|
||||
"dist/plugin-sdk/packages/memory-host-sdk/src/engine-embeddings.d.ts",
|
||||
"dist/plugin-sdk/packages/memory-host-sdk/src/secret.d.ts",
|
||||
"dist/plugin-sdk/packages/memory-host-sdk/src/status.d.ts",
|
||||
"dist/plugin-sdk/packages/llm-core/src/index.d.ts",
|
||||
"dist/plugin-sdk/packages/llm-core/src/types.d.ts",
|
||||
"dist/plugin-sdk/packages/llm-core/src/utils/diagnostics.d.ts",
|
||||
"dist/plugin-sdk/packages/llm-core/src/utils/event-stream.d.ts",
|
||||
"dist/plugin-sdk/packages/llm-core/src/validation.d.ts",
|
||||
"dist/plugin-sdk/error-runtime.d.ts",
|
||||
"dist/plugin-sdk/plugin-entry.d.ts",
|
||||
"dist/plugin-sdk/provider-auth.d.ts",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ExecAllowlistEntry } from "../infra/exec-approvals.types.js";
|
||||
import { MAX_SAFE_TIMEOUT_DELAY_MS } from "../utils/timer-delay.js";
|
||||
|
||||
type StrictInlineEvalBoundary =
|
||||
@@ -74,7 +75,7 @@ const requiresExecApprovalMock = vi.hoisted(() => vi.fn(() => true));
|
||||
const hasDurableExecApprovalMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const resolveExecHostApprovalContextMock = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
approvals: { allowlist: [], file: { version: 1, agents: {} } },
|
||||
approvals: { allowlist: [] as ExecAllowlistEntry[], file: { version: 1, agents: {} } },
|
||||
hostSecurity: "full",
|
||||
hostAsk: "off",
|
||||
askFallback: "deny",
|
||||
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
Agent as CoreAgent,
|
||||
type AgentOptions as CoreAgentOptions,
|
||||
} from "../../../packages/agent-core/src/agent.js";
|
||||
import type { CompleteSimpleFn, StreamFn } from "../../../packages/agent-core/src/llm.js";
|
||||
import type { AgentCoreRuntimeDeps } from "../../../packages/agent-core/src/runtime-deps.js";
|
||||
import type { CompleteSimpleFn, StreamFn } from "../../../packages/llm-core/src/index.js";
|
||||
import { completeSimple, streamSimple } from "../../plugin-sdk/llm.js";
|
||||
|
||||
export const openClawAgentCoreRuntime = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { StreamFn as CoreStreamFn } from "../../../../packages/agent-core/src/llm.js";
|
||||
import type { StreamFn as CoreStreamFn } from "../../../../packages/llm-core/src/index.js";
|
||||
import type { Model } from "../../../llm/types.js";
|
||||
import {
|
||||
calculateContextTokens,
|
||||
|
||||
@@ -2,7 +2,11 @@ import type { ThinkLevel } from "../auto-reply/thinking.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { completeSimple } from "../llm/stream.js";
|
||||
import type { Model, ThinkingLevel as SimpleCompletionThinkingLevel } from "../llm/types.js";
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Model,
|
||||
ThinkingLevel as SimpleCompletionThinkingLevel,
|
||||
} from "../llm/types.js";
|
||||
import { prepareProviderRuntimeAuth } from "../plugins/provider-runtime.runtime.js";
|
||||
import { resolveAgentDir, resolveAgentEffectiveModelPrimary } from "./agent-scope.js";
|
||||
import { DEFAULT_PROVIDER } from "./defaults.js";
|
||||
@@ -330,7 +334,7 @@ export async function completeWithPreparedSimpleCompletionModel(params: {
|
||||
context: Parameters<typeof completeSimple>[1];
|
||||
cfg?: OpenClawConfig;
|
||||
options?: SimpleCompletionModelOptions;
|
||||
}) {
|
||||
}): Promise<AssistantMessage> {
|
||||
const completionModel = prepareModelForSimpleCompletion({ model: params.model, cfg: params.cfg });
|
||||
const { reasoning: rawReasoning, ...options } = params.options ?? {};
|
||||
const reasoning = normalizeSimpleCompletionReasoning(rawReasoning);
|
||||
|
||||
@@ -1,101 +1 @@
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessageEventStreamContract,
|
||||
Context,
|
||||
Model,
|
||||
SimpleStreamOptions,
|
||||
StreamFunction,
|
||||
StreamOptions,
|
||||
} from "./types.js";
|
||||
|
||||
export type ApiStreamFunction = (
|
||||
model: Model,
|
||||
context: Context,
|
||||
options?: StreamOptions,
|
||||
) => AssistantMessageEventStreamContract;
|
||||
|
||||
export type ApiStreamSimpleFunction = (
|
||||
model: Model,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
) => AssistantMessageEventStreamContract;
|
||||
|
||||
export interface ApiProvider<
|
||||
TApi extends Api = Api,
|
||||
TOptions extends StreamOptions = StreamOptions,
|
||||
> {
|
||||
api: TApi;
|
||||
stream: StreamFunction<TApi, TOptions>;
|
||||
streamSimple: StreamFunction<TApi, SimpleStreamOptions>;
|
||||
}
|
||||
|
||||
interface ApiProviderInternal {
|
||||
api: Api;
|
||||
stream: ApiStreamFunction;
|
||||
streamSimple: ApiStreamSimpleFunction;
|
||||
}
|
||||
|
||||
type RegisteredApiProvider = {
|
||||
provider: ApiProviderInternal;
|
||||
sourceId?: string;
|
||||
};
|
||||
|
||||
const apiProviderRegistry = new Map<string, RegisteredApiProvider>();
|
||||
|
||||
function wrapStream<TApi extends Api, TOptions extends StreamOptions>(
|
||||
api: TApi,
|
||||
stream: StreamFunction<TApi, TOptions>,
|
||||
): ApiStreamFunction {
|
||||
return (model, context, options) => {
|
||||
if (model.api !== api) {
|
||||
throw new Error(`Mismatched api: ${model.api} expected ${api}`);
|
||||
}
|
||||
return stream(model as Model<TApi>, context, options as TOptions);
|
||||
};
|
||||
}
|
||||
|
||||
function wrapStreamSimple<TApi extends Api>(
|
||||
api: TApi,
|
||||
streamSimple: StreamFunction<TApi, SimpleStreamOptions>,
|
||||
): ApiStreamSimpleFunction {
|
||||
return (model, context, options) => {
|
||||
if (model.api !== api) {
|
||||
throw new Error(`Mismatched api: ${model.api} expected ${api}`);
|
||||
}
|
||||
return streamSimple(model as Model<TApi>, context, options);
|
||||
};
|
||||
}
|
||||
|
||||
export function registerApiProvider<TApi extends Api, TOptions extends StreamOptions>(
|
||||
provider: ApiProvider<TApi, TOptions>,
|
||||
sourceId?: string,
|
||||
): void {
|
||||
apiProviderRegistry.set(provider.api, {
|
||||
provider: {
|
||||
api: provider.api,
|
||||
stream: wrapStream(provider.api, provider.stream),
|
||||
streamSimple: wrapStreamSimple(provider.api, provider.streamSimple),
|
||||
},
|
||||
sourceId,
|
||||
});
|
||||
}
|
||||
|
||||
export function getApiProvider(api: Api): ApiProviderInternal | undefined {
|
||||
return apiProviderRegistry.get(api)?.provider;
|
||||
}
|
||||
|
||||
export function getApiProviders(): ApiProviderInternal[] {
|
||||
return Array.from(apiProviderRegistry.values(), (entry) => entry.provider);
|
||||
}
|
||||
|
||||
export function unregisterApiProviders(sourceId: string): void {
|
||||
for (const [api, entry] of apiProviderRegistry.entries()) {
|
||||
if (entry.sourceId === sourceId) {
|
||||
apiProviderRegistry.delete(api);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function clearApiProviders(): void {
|
||||
apiProviderRegistry.clear();
|
||||
}
|
||||
export * from "../../packages/llm-runtime/src/api-registry.js";
|
||||
|
||||
@@ -404,5 +404,3 @@ export function resetApiProviders(): void {
|
||||
unregisterApiProviders(BUILT_IN_API_PROVIDER_SOURCE_ID);
|
||||
registerBuiltInApiProviders();
|
||||
}
|
||||
|
||||
registerBuiltInApiProviders();
|
||||
|
||||
@@ -1,58 +1,11 @@
|
||||
import "./providers/register-builtins.js";
|
||||
import { getApiProvider } from "./api-registry.js";
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessage,
|
||||
AssistantMessageEventStreamContract,
|
||||
Context,
|
||||
Model,
|
||||
ProviderStreamOptions,
|
||||
SimpleStreamOptions,
|
||||
StreamOptions,
|
||||
} from "./types.js";
|
||||
import { registerBuiltInApiProviders } from "./providers/register-builtins.js";
|
||||
|
||||
registerBuiltInApiProviders();
|
||||
|
||||
export {
|
||||
complete,
|
||||
completeSimple,
|
||||
stream,
|
||||
streamSimple,
|
||||
} from "../../packages/llm-runtime/src/stream.js";
|
||||
export { getEnvApiKey } from "./env-api-keys.js";
|
||||
|
||||
function resolveApiProvider(api: Api) {
|
||||
const provider = getApiProvider(api);
|
||||
if (!provider) {
|
||||
throw new Error(`No API provider registered for api: ${api}`);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
export function stream<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: ProviderStreamOptions,
|
||||
): AssistantMessageEventStreamContract {
|
||||
const provider = resolveApiProvider(model.api);
|
||||
return provider.stream(model, context, options as StreamOptions);
|
||||
}
|
||||
|
||||
export async function complete<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: ProviderStreamOptions,
|
||||
): Promise<AssistantMessage> {
|
||||
const s = stream(model, context, options);
|
||||
return s.result();
|
||||
}
|
||||
|
||||
export function streamSimple<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
): AssistantMessageEventStreamContract {
|
||||
const provider = resolveApiProvider(model.api);
|
||||
return provider.streamSimple(model, context, options);
|
||||
}
|
||||
|
||||
export async function completeSimple<TApi extends Api>(
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
): Promise<AssistantMessage> {
|
||||
const s = streamSimple(model, context, options);
|
||||
return s.result();
|
||||
}
|
||||
|
||||
563
src/llm/types.ts
563
src/llm/types.ts
@@ -1,562 +1 @@
|
||||
import type { AssistantMessageDiagnostic } from "./utils/diagnostics.js";
|
||||
|
||||
export type KnownApi =
|
||||
| "openai-completions"
|
||||
| "mistral-conversations"
|
||||
| "openai-responses"
|
||||
| "azure-openai-responses"
|
||||
| "openai-codex-responses"
|
||||
| "anthropic-messages"
|
||||
| "bedrock-converse-stream"
|
||||
| "google-generative-ai"
|
||||
| "google-vertex";
|
||||
|
||||
export type Api = KnownApi | (string & {});
|
||||
|
||||
export type KnownImagesApi = "openrouter-images";
|
||||
|
||||
export type ImagesApi = KnownImagesApi | (string & {});
|
||||
|
||||
export type Provider = string;
|
||||
|
||||
export type KnownImagesProvider = "openrouter";
|
||||
|
||||
export type ImagesProvider = string;
|
||||
|
||||
export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
||||
export type ModelThinkingLevel = "off" | ThinkingLevel;
|
||||
export type ThinkingLevelMap = Partial<Record<ModelThinkingLevel, string | null>>;
|
||||
|
||||
/** Token budgets for each thinking level (token-based providers only) */
|
||||
export interface ThinkingBudgets {
|
||||
minimal?: number;
|
||||
low?: number;
|
||||
medium?: number;
|
||||
high?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
// Base options all providers share
|
||||
export type CacheRetention = "none" | "short" | "long";
|
||||
|
||||
export type Transport = "sse" | "websocket" | "websocket-cached" | "auto";
|
||||
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
export interface ProviderResponse {
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface StreamOptions {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
apiKey?: string;
|
||||
/**
|
||||
* Preferred transport for providers that support multiple transports.
|
||||
* Providers that do not support this option ignore it.
|
||||
*/
|
||||
transport?: Transport;
|
||||
/**
|
||||
* Prompt cache retention preference. Providers map this to their supported values.
|
||||
* Default: "short".
|
||||
*/
|
||||
cacheRetention?: CacheRetention;
|
||||
/**
|
||||
* Optional session identifier for providers that support session-based caching.
|
||||
* Providers can use this to enable prompt caching, request routing, or other
|
||||
* session-aware features. Ignored by providers that don't support it.
|
||||
*/
|
||||
sessionId?: string;
|
||||
/**
|
||||
* Optional provider prompt-cache affinity key, distinct from transcript/session identity.
|
||||
* Providers that do not support separate cache affinity ignore it.
|
||||
*/
|
||||
promptCacheKey?: string;
|
||||
/**
|
||||
* Optional callback for inspecting or replacing provider payloads before sending.
|
||||
* Return undefined to keep the payload unchanged.
|
||||
*/
|
||||
onPayload?: (payload: unknown, model: Model) => MaybePromise<unknown>;
|
||||
/**
|
||||
* Optional callback invoked after an HTTP response is received and before
|
||||
* its body stream is consumed.
|
||||
*/
|
||||
onResponse?: (response: ProviderResponse, model: Model) => void | Promise<void>;
|
||||
/**
|
||||
* Optional custom HTTP headers to include in API requests.
|
||||
* Merged with provider defaults; can override default headers.
|
||||
* Not supported by all providers (e.g., AWS Bedrock uses SDK auth).
|
||||
*/
|
||||
headers?: Record<string, string>;
|
||||
/**
|
||||
* HTTP request timeout in milliseconds for providers/SDKs that support it.
|
||||
* For example, OpenAI and Anthropic SDK clients default to 10 minutes.
|
||||
*/
|
||||
timeoutMs?: number;
|
||||
/**
|
||||
* Maximum retry attempts for providers/SDKs that support client-side retries.
|
||||
* For example, OpenAI and Anthropic SDK clients default to 2.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
/**
|
||||
* Maximum delay in milliseconds to wait for a retry when the server requests a long wait.
|
||||
* If the server's requested delay exceeds this value, the request fails immediately
|
||||
* with an error containing the requested delay, allowing higher-level retry logic
|
||||
* to handle it with user visibility.
|
||||
* Default: 60000 (60 seconds). Set to 0 to disable the cap.
|
||||
*/
|
||||
maxRetryDelayMs?: number;
|
||||
/**
|
||||
* Optional metadata to include in API requests.
|
||||
* Providers extract the fields they understand and ignore the rest.
|
||||
* For example, Anthropic uses `user_id` for abuse tracking and rate limiting.
|
||||
*/
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type ProviderStreamOptions = StreamOptions & Record<string, unknown>;
|
||||
|
||||
export interface ImagesOptions {
|
||||
signal?: AbortSignal;
|
||||
apiKey?: string;
|
||||
/**
|
||||
* Optional callback for inspecting or replacing provider payloads before sending.
|
||||
* Return undefined to keep the payload unchanged.
|
||||
*/
|
||||
onPayload?: (payload: unknown, model: ImagesModel) => MaybePromise<unknown>;
|
||||
/**
|
||||
* Optional callback invoked after an HTTP response is received.
|
||||
*/
|
||||
onResponse?: (response: ProviderResponse, model: ImagesModel) => void | Promise<void>;
|
||||
/**
|
||||
* Optional custom HTTP headers to include in API requests.
|
||||
* Merged with provider defaults; can override default headers.
|
||||
*/
|
||||
headers?: Record<string, string>;
|
||||
/**
|
||||
* HTTP request timeout in milliseconds for providers/SDKs that support it.
|
||||
*/
|
||||
timeoutMs?: number;
|
||||
/**
|
||||
* Maximum retry attempts for providers/SDKs that support client-side retries.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
/**
|
||||
* Maximum delay in milliseconds to wait for a retry when the server requests a long wait.
|
||||
* If the server's requested delay exceeds this value, the request fails immediately
|
||||
* with an error containing the requested delay, allowing higher-level retry logic
|
||||
* to handle it with user visibility.
|
||||
* Default: 60000 (60 seconds). Set to 0 to disable the cap.
|
||||
*/
|
||||
maxRetryDelayMs?: number;
|
||||
/**
|
||||
* Optional metadata to include in API requests.
|
||||
* Providers extract the fields they understand and ignore the rest.
|
||||
*/
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type ProviderImagesOptions = ImagesOptions & Record<string, unknown>;
|
||||
|
||||
// Unified options with reasoning passed to streamSimple() and completeSimple()
|
||||
export interface SimpleStreamOptions extends StreamOptions {
|
||||
reasoning?: ThinkingLevel;
|
||||
/** Custom token budgets for thinking levels (token-based providers only) */
|
||||
thinkingBudgets?: ThinkingBudgets;
|
||||
}
|
||||
|
||||
// Generic StreamFunction with typed options.
|
||||
//
|
||||
// Contract:
|
||||
// - Must return an AssistantMessageEventStream.
|
||||
// - Once invoked, request/model/runtime failures should be encoded in the
|
||||
// returned stream, not thrown.
|
||||
// - Error termination must produce an AssistantMessage with stopReason
|
||||
// "error" or "aborted" and errorMessage, emitted via the stream protocol.
|
||||
export type StreamFunction<
|
||||
TApi extends Api = Api,
|
||||
TOptions extends StreamOptions = StreamOptions,
|
||||
> = (
|
||||
model: Model<TApi>,
|
||||
context: Context,
|
||||
options?: TOptions,
|
||||
) => AssistantMessageEventStreamContract;
|
||||
|
||||
export type ImagesFunction<
|
||||
TApi extends ImagesApi = ImagesApi,
|
||||
TOptions extends ImagesOptions = ImagesOptions,
|
||||
> = (
|
||||
model: ImagesModel<TApi>,
|
||||
context: ImagesContext,
|
||||
options?: TOptions,
|
||||
) => Promise<AssistantImages>;
|
||||
|
||||
export interface TextSignatureV1 {
|
||||
v: 1;
|
||||
id: string;
|
||||
phase?: "commentary" | "final_answer";
|
||||
}
|
||||
|
||||
export interface TextContent {
|
||||
type: "text";
|
||||
text: string;
|
||||
textSignature?: string; // e.g., for OpenAI responses, message metadata (legacy id string or TextSignatureV1 JSON)
|
||||
}
|
||||
|
||||
export interface ThinkingContent {
|
||||
type: "thinking";
|
||||
thinking: string;
|
||||
thinkingSignature?: string; // e.g., for OpenAI responses, the reasoning item ID
|
||||
/** When true, the thinking content was redacted by safety filters. The opaque
|
||||
* encrypted payload is stored in `thinkingSignature` so it can be passed back
|
||||
* to the API for multi-turn continuity. */
|
||||
redacted?: boolean;
|
||||
}
|
||||
|
||||
export interface ImageContent {
|
||||
type: "image";
|
||||
data: string; // base64 encoded image data
|
||||
mimeType: string; // e.g., "image/jpeg", "image/png"
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
type: "toolCall";
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
thoughtSignature?: string; // Google-specific: opaque signature for reusing thought context
|
||||
}
|
||||
|
||||
export interface Usage {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
cost: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type StopReason = "stop" | "length" | "toolUse" | "error" | "aborted";
|
||||
|
||||
export interface UserMessage {
|
||||
role: "user";
|
||||
content: string | (TextContent | ImageContent)[];
|
||||
timestamp: number; // Unix timestamp in milliseconds
|
||||
}
|
||||
|
||||
export interface AssistantMessage {
|
||||
role: "assistant";
|
||||
content: (TextContent | ThinkingContent | ToolCall)[];
|
||||
api: Api;
|
||||
provider: Provider;
|
||||
model: string;
|
||||
responseModel?: string; // Concrete `chunk.model` when different from the requested `model` (e.g. OpenRouter `auto` -> `anthropic/...`)
|
||||
responseId?: string; // Provider-specific response/message identifier when the upstream API exposes one
|
||||
diagnostics?: AssistantMessageDiagnostic[]; // Redacted provider/runtime diagnostics for failures and recoveries.
|
||||
usage: Usage;
|
||||
stopReason: StopReason;
|
||||
errorMessage?: string;
|
||||
timestamp: number; // Unix timestamp in milliseconds
|
||||
}
|
||||
|
||||
export interface ToolResultMessage<TDetails = unknown> {
|
||||
role: "toolResult";
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
content: (TextContent | ImageContent)[]; // Supports text and images
|
||||
details?: TDetails;
|
||||
isError: boolean;
|
||||
timestamp: number; // Unix timestamp in milliseconds
|
||||
}
|
||||
|
||||
export type Message = UserMessage | AssistantMessage | ToolResultMessage;
|
||||
|
||||
export type ImagesInputContent = TextContent | ImageContent;
|
||||
export type ImagesOutputContent = TextContent | ImageContent;
|
||||
|
||||
export interface ImagesContext {
|
||||
input: ImagesInputContent[];
|
||||
}
|
||||
|
||||
export type ImagesStopReason = "stop" | "error" | "aborted";
|
||||
|
||||
export interface AssistantImages {
|
||||
api: ImagesApi;
|
||||
provider: ImagesProvider;
|
||||
model: string;
|
||||
output: ImagesOutputContent[];
|
||||
responseId?: string;
|
||||
usage?: Usage;
|
||||
stopReason: ImagesStopReason;
|
||||
errorMessage?: string;
|
||||
timestamp: number; // Unix timestamp in milliseconds
|
||||
}
|
||||
|
||||
import type { TSchema } from "typebox";
|
||||
|
||||
export interface Tool<TParameters extends TSchema = TSchema> {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: TParameters;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
systemPrompt?: string;
|
||||
messages: Message[];
|
||||
tools?: Tool[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Event protocol for AssistantMessageEventStream.
|
||||
*
|
||||
* Streams should emit `start` before partial updates, then terminate with either:
|
||||
* - `done` carrying the final successful AssistantMessage, or
|
||||
* - `error` carrying the final AssistantMessage with stopReason "error" or "aborted"
|
||||
* and errorMessage.
|
||||
*/
|
||||
export type AssistantMessageEvent =
|
||||
| { type: "start"; partial: AssistantMessage }
|
||||
| { type: "text_start"; contentIndex: number; partial: AssistantMessage }
|
||||
| { type: "text_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
||||
| { type: "text_end"; contentIndex: number; content: string; partial: AssistantMessage }
|
||||
| { type: "thinking_start"; contentIndex: number; partial: AssistantMessage }
|
||||
| { type: "thinking_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
||||
| { type: "thinking_end"; contentIndex: number; content: string; partial: AssistantMessage }
|
||||
| { type: "toolcall_start"; contentIndex: number; partial: AssistantMessage }
|
||||
| { type: "toolcall_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
||||
| { type: "toolcall_end"; contentIndex: number; toolCall: ToolCall; partial: AssistantMessage }
|
||||
| {
|
||||
type: "done";
|
||||
reason: Extract<StopReason, "stop" | "length" | "toolUse">;
|
||||
message: AssistantMessage;
|
||||
}
|
||||
| { type: "error"; reason: Extract<StopReason, "aborted" | "error">; error: AssistantMessage };
|
||||
|
||||
export interface AssistantMessageEventStreamContract extends AsyncIterable<AssistantMessageEvent> {
|
||||
push(event: AssistantMessageEvent): void;
|
||||
end(result?: AssistantMessage): void;
|
||||
result(): Promise<AssistantMessage>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatibility settings for OpenAI-compatible completions APIs.
|
||||
* Use this to override URL-based auto-detection for custom providers.
|
||||
*/
|
||||
export interface OpenAICompletionsCompat {
|
||||
/** Whether the provider supports the `store` field. Default: auto-detected from URL. */
|
||||
supportsStore?: boolean;
|
||||
/** Whether the provider supports the `developer` role (vs `system`). Default: auto-detected from URL. */
|
||||
supportsDeveloperRole?: boolean;
|
||||
/** Whether the provider supports `reasoning_effort`. Default: auto-detected from URL. */
|
||||
supportsReasoningEffort?: boolean;
|
||||
/** Whether the provider supports `stream_options: { include_usage: true }` for token usage in streaming responses. Default: true. */
|
||||
supportsUsageInStreaming?: boolean;
|
||||
/** Which field to use for max tokens. Default: auto-detected from URL. */
|
||||
maxTokensField?: "max_completion_tokens" | "max_tokens";
|
||||
/** Whether tool results require the `name` field. Default: auto-detected from URL. */
|
||||
requiresToolResultName?: boolean;
|
||||
/** Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL. */
|
||||
requiresAssistantAfterToolResult?: boolean;
|
||||
/** Whether thinking blocks must be converted to text blocks with <thinking> delimiters. Default: auto-detected from URL. */
|
||||
requiresThinkingAsText?: boolean;
|
||||
/** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */
|
||||
requiresReasoningContentOnAssistantMessages?: boolean;
|
||||
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses top-level enable_thinking: boolean, "qwen" uses top-level enable_thinking: boolean, and "qwen-chat-template" uses chat_template_kwargs.enable_thinking. Default: "openai". */
|
||||
thinkingFormat?:
|
||||
| "openai"
|
||||
| "openrouter"
|
||||
| "deepseek"
|
||||
| "together"
|
||||
| "zai"
|
||||
| "qwen"
|
||||
| "qwen-chat-template";
|
||||
/** OpenRouter-specific routing preferences. Only used when baseUrl points to OpenRouter. */
|
||||
openRouterRouting?: OpenRouterRouting;
|
||||
/** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */
|
||||
vercelGatewayRouting?: VercelGatewayRouting;
|
||||
/** Whether z.ai supports top-level `tool_stream: true` for streaming tool call deltas. Default: false. */
|
||||
zaiToolStream?: boolean;
|
||||
/** Whether the provider supports the `strict` field in tool definitions. Default: true. */
|
||||
supportsStrictMode?: boolean;
|
||||
/** Cache control convention for prompt caching. "anthropic" applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content. */
|
||||
cacheControlFormat?: "anthropic";
|
||||
/** Whether to send known session-affinity headers (`session_id`, `x-client-request-id`, `x-session-affinity`) from `options.sessionId` when caching is enabled. Default: false. */
|
||||
sendSessionAffinityHeaders?: boolean;
|
||||
/** Whether the provider supports long prompt cache retention (`prompt_cache_retention: "24h"` or Anthropic-style `cache_control.ttl: "1h"`, depending on format). Default: true. */
|
||||
supportsLongCacheRetention?: boolean;
|
||||
}
|
||||
|
||||
/** Compatibility settings for OpenAI Responses APIs. */
|
||||
export interface OpenAIResponsesCompat {
|
||||
/** Whether to send the OpenAI `session_id` cache-affinity header from `options.sessionId` when caching is enabled. Default: true. */
|
||||
sendSessionIdHeader?: boolean;
|
||||
/** Whether the provider supports `prompt_cache_retention: "24h"`. Default: true. */
|
||||
supportsLongCacheRetention?: boolean;
|
||||
}
|
||||
|
||||
/** Compatibility settings for Anthropic Messages-compatible APIs. */
|
||||
export interface AnthropicMessagesCompat {
|
||||
/**
|
||||
* Whether the provider accepts per-tool `eager_input_streaming`.
|
||||
* When false, the Anthropic provider omits `tools[].eager_input_streaming`
|
||||
* and sends the legacy `fine-grained-tool-streaming-2025-05-14` beta header
|
||||
* for tool-enabled requests.
|
||||
* Default: true.
|
||||
*/
|
||||
supportsEagerToolInputStreaming?: boolean;
|
||||
/** Whether the provider supports Anthropic long cache retention (`cache_control.ttl: "1h"`). Default: true. */
|
||||
supportsLongCacheRetention?: boolean;
|
||||
/**
|
||||
* Whether to send the `x-session-affinity` header from `options.sessionId`
|
||||
* when caching is enabled. Required for providers like Fireworks that use
|
||||
* session affinity for prompt cache routing (requests to the same replica
|
||||
* maximize cache hits).
|
||||
* Default: false.
|
||||
*/
|
||||
sendSessionAffinityHeaders?: boolean;
|
||||
/**
|
||||
* Whether the provider supports Anthropic-style `cache_control` markers on
|
||||
* tool definitions. When false, `cache_control` is omitted from tool params.
|
||||
* Some Anthropic-compatible providers (e.g., Fireworks) do not support this
|
||||
* field on tools and may reject or ignore it.
|
||||
* Default: true.
|
||||
*/
|
||||
supportsCacheControlOnTools?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenRouter provider routing preferences.
|
||||
* Controls which upstream providers OpenRouter routes requests to.
|
||||
* Sent as the `provider` field in the OpenRouter API request body.
|
||||
* @see https://openrouter.ai/docs/guides/routing/provider-selection
|
||||
*/
|
||||
export interface OpenRouterRouting {
|
||||
/** Whether to allow backup providers to serve requests. Default: true. */
|
||||
allow_fallbacks?: boolean;
|
||||
/** Whether to filter providers to only those that support all parameters in the request. Default: false. */
|
||||
require_parameters?: boolean;
|
||||
/** Data collection setting. "allow" (default): allow providers that may store/train on data. "deny": only use providers that don't collect user data. */
|
||||
data_collection?: "deny" | "allow";
|
||||
/** Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. */
|
||||
zdr?: boolean;
|
||||
/** Whether to restrict routing to only models that allow text distillation. */
|
||||
enforce_distillable_text?: boolean;
|
||||
/** An ordered list of provider names/slugs to try in sequence, falling back to the next if unavailable. */
|
||||
order?: string[];
|
||||
/** List of provider names/slugs to exclusively allow for this request. */
|
||||
only?: string[];
|
||||
/** List of provider names/slugs to skip for this request. */
|
||||
ignore?: string[];
|
||||
/** A list of quantization levels to filter providers by (e.g., ["fp16", "bf16", "fp8", "fp6", "int8", "int4", "fp4", "fp32"]). */
|
||||
quantizations?: string[];
|
||||
/** Sorting strategy. Can be a string (e.g., "price", "throughput", "latency") or an object with `by` and `partition`. */
|
||||
sort?:
|
||||
| string
|
||||
| {
|
||||
/** The sorting metric: "price", "throughput", "latency". */
|
||||
by?: string;
|
||||
/** Partitioning strategy: "model" (default) or "none". */
|
||||
partition?: string | null;
|
||||
};
|
||||
/** Maximum price per million tokens (USD). */
|
||||
max_price?: {
|
||||
/** Price per million prompt tokens. */
|
||||
prompt?: number | string;
|
||||
/** Price per million completion tokens. */
|
||||
completion?: number | string;
|
||||
/** Price per image. */
|
||||
image?: number | string;
|
||||
/** Price per audio unit. */
|
||||
audio?: number | string;
|
||||
/** Price per request. */
|
||||
request?: number | string;
|
||||
};
|
||||
/** Preferred minimum throughput (tokens/second). Can be a number (applies to p50) or an object with percentile-specific cutoffs. */
|
||||
preferred_min_throughput?:
|
||||
| number
|
||||
| {
|
||||
/** Minimum tokens/second at the 50th percentile. */
|
||||
p50?: number;
|
||||
/** Minimum tokens/second at the 75th percentile. */
|
||||
p75?: number;
|
||||
/** Minimum tokens/second at the 90th percentile. */
|
||||
p90?: number;
|
||||
/** Minimum tokens/second at the 99th percentile. */
|
||||
p99?: number;
|
||||
};
|
||||
/** Preferred maximum latency (seconds). Can be a number (applies to p50) or an object with percentile-specific cutoffs. */
|
||||
preferred_max_latency?:
|
||||
| number
|
||||
| {
|
||||
/** Maximum latency in seconds at the 50th percentile. */
|
||||
p50?: number;
|
||||
/** Maximum latency in seconds at the 75th percentile. */
|
||||
p75?: number;
|
||||
/** Maximum latency in seconds at the 90th percentile. */
|
||||
p90?: number;
|
||||
/** Maximum latency in seconds at the 99th percentile. */
|
||||
p99?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vercel AI Gateway routing preferences.
|
||||
* Controls which upstream providers the gateway routes requests to.
|
||||
* @see https://vercel.com/docs/ai-gateway/models-and-providers/provider-options
|
||||
*/
|
||||
export interface VercelGatewayRouting {
|
||||
/** List of provider slugs to exclusively use for this request (e.g., ["bedrock", "anthropic"]). */
|
||||
only?: string[];
|
||||
/** List of provider slugs to try in order (e.g., ["anthropic", "openai"]). */
|
||||
order?: string[];
|
||||
}
|
||||
|
||||
// Model interface for the unified model system
|
||||
export interface Model<TApi extends Api = Api> {
|
||||
id: string;
|
||||
name: string;
|
||||
api: TApi;
|
||||
provider: Provider;
|
||||
baseUrl: string;
|
||||
reasoning: boolean;
|
||||
/**
|
||||
* Maps OpenClaw thinking levels to provider/model-specific values.
|
||||
* Missing keys use provider defaults. null marks a level as unsupported.
|
||||
*/
|
||||
thinkingLevelMap?: ThinkingLevelMap;
|
||||
input: ("text" | "image")[];
|
||||
cost: {
|
||||
input: number; // $/million tokens
|
||||
output: number; // $/million tokens
|
||||
cacheRead: number; // $/million tokens
|
||||
cacheWrite: number; // $/million tokens
|
||||
};
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
headers?: Record<string, string>;
|
||||
/** Compatibility overrides for OpenAI-compatible APIs. If not set, auto-detected from baseUrl. */
|
||||
compat?: TApi extends "openai-completions"
|
||||
? OpenAICompletionsCompat
|
||||
: TApi extends "openai-responses"
|
||||
? OpenAIResponsesCompat
|
||||
: TApi extends "anthropic-messages"
|
||||
? AnthropicMessagesCompat
|
||||
: never;
|
||||
}
|
||||
|
||||
export interface ImagesModel<TApi extends ImagesApi = ImagesApi> extends Omit<
|
||||
Model,
|
||||
"api" | "provider" | "reasoning" | "contextWindow" | "maxTokens" | "compat"
|
||||
> {
|
||||
api: TApi;
|
||||
provider: ImagesProvider;
|
||||
output: ("text" | "image")[];
|
||||
}
|
||||
export * from "../../packages/llm-core/src/types.js";
|
||||
|
||||
@@ -1,51 +1 @@
|
||||
export interface DiagnosticErrorInfo {
|
||||
name?: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
code?: string | number;
|
||||
}
|
||||
|
||||
export interface AssistantMessageDiagnostic {
|
||||
type: string;
|
||||
timestamp: number;
|
||||
error?: DiagnosticErrorInfo;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function formatThrownValue(value: unknown): string {
|
||||
if (value instanceof Error) {
|
||||
return value.message || value.name;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function extractDiagnosticError(error: unknown): DiagnosticErrorInfo {
|
||||
if (!(error instanceof Error)) {
|
||||
return { name: "ThrownValue", message: formatThrownValue(error) };
|
||||
}
|
||||
const code = (error as Error & { code?: unknown }).code;
|
||||
return {
|
||||
name: error.name || undefined,
|
||||
message: error.message || error.name,
|
||||
stack: error.stack,
|
||||
code: typeof code === "string" || typeof code === "number" ? code : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function createAssistantMessageDiagnostic(
|
||||
type: string,
|
||||
error: unknown,
|
||||
details?: Record<string, unknown>,
|
||||
): AssistantMessageDiagnostic {
|
||||
return { type, timestamp: Date.now(), error: extractDiagnosticError(error), details };
|
||||
}
|
||||
|
||||
export function appendAssistantMessageDiagnostic(
|
||||
message: { diagnostics?: AssistantMessageDiagnostic[] },
|
||||
diagnostic: AssistantMessageDiagnostic,
|
||||
): void {
|
||||
message.diagnostics = [...(message.diagnostics ?? []), diagnostic];
|
||||
}
|
||||
export * from "../../../packages/llm-core/src/utils/diagnostics.js";
|
||||
|
||||
@@ -1,97 +1 @@
|
||||
import type { AssistantMessage, AssistantMessageEvent } from "../types.js";
|
||||
|
||||
// Generic event stream class for async iteration
|
||||
export class EventStream<T, R = T> implements AsyncIterable<T> {
|
||||
private queue: T[] = [];
|
||||
private waiting: ((value: IteratorResult<T>) => void)[] = [];
|
||||
private done = false;
|
||||
private finalResultPromise: Promise<R>;
|
||||
private resolveFinalResult!: (result: R) => void;
|
||||
private isComplete: (event: T) => boolean;
|
||||
private extractResult: (event: T) => R;
|
||||
|
||||
constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R) {
|
||||
this.isComplete = isComplete;
|
||||
this.extractResult = extractResult;
|
||||
this.finalResultPromise = new Promise((resolve) => {
|
||||
this.resolveFinalResult = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
push(event: T): void {
|
||||
if (this.done) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isComplete(event)) {
|
||||
this.done = true;
|
||||
this.resolveFinalResult(this.extractResult(event));
|
||||
}
|
||||
|
||||
// Deliver to waiting consumer or queue it
|
||||
const waiter = this.waiting.shift();
|
||||
if (waiter) {
|
||||
waiter({ value: event, done: false });
|
||||
} else {
|
||||
this.queue.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
end(result?: R): void {
|
||||
this.done = true;
|
||||
if (result !== undefined) {
|
||||
this.resolveFinalResult(result);
|
||||
}
|
||||
// Notify all waiting consumers that we're done
|
||||
while (this.waiting.length > 0) {
|
||||
const waiter = this.waiting.shift()!;
|
||||
waiter({ value: undefined as unknown, done: true });
|
||||
}
|
||||
}
|
||||
|
||||
async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
while (true) {
|
||||
if (this.queue.length > 0) {
|
||||
yield this.queue.shift()!;
|
||||
} else if (this.done) {
|
||||
return;
|
||||
} else {
|
||||
const result = await new Promise<IteratorResult<T>>((resolve) =>
|
||||
this.waiting.push(resolve),
|
||||
);
|
||||
if (result.done) {
|
||||
return;
|
||||
}
|
||||
yield result.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result(): Promise<R> {
|
||||
return this.finalResultPromise;
|
||||
}
|
||||
}
|
||||
|
||||
export class AssistantMessageEventStream extends EventStream<
|
||||
AssistantMessageEvent,
|
||||
AssistantMessage
|
||||
> {
|
||||
constructor() {
|
||||
super(
|
||||
(event) => event.type === "done" || event.type === "error",
|
||||
(event) => {
|
||||
if (event.type === "done") {
|
||||
return event.message;
|
||||
} else if (event.type === "error") {
|
||||
return event.error;
|
||||
}
|
||||
throw new Error("Unexpected event type for final result");
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Factory function for AssistantMessageEventStream (for use in extensions) */
|
||||
export function createAssistantMessageEventStream(): AssistantMessageEventStream {
|
||||
return new AssistantMessageEventStream();
|
||||
}
|
||||
export * from "../../../packages/llm-core/src/utils/event-stream.js";
|
||||
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
Agent as CoreAgent,
|
||||
type AgentOptions as CoreAgentOptions,
|
||||
} from "../../packages/agent-core/src/agent.js";
|
||||
import type { CompleteSimpleFn, StreamFn } from "../../packages/agent-core/src/llm.js";
|
||||
import type { AgentCoreRuntimeDeps } from "../../packages/agent-core/src/runtime-deps.js";
|
||||
import type { CompleteSimpleFn, StreamFn } from "../../packages/llm-core/src/index.js";
|
||||
import { completeSimple, streamSimple } from "./llm.js";
|
||||
|
||||
export const openClawAgentCoreRuntime = {
|
||||
|
||||
@@ -44,11 +44,8 @@ export type {
|
||||
export {
|
||||
AssistantMessageEventStream,
|
||||
createAssistantMessageEventStream,
|
||||
} from "../llm/utils/event-stream.js";
|
||||
} from "../../packages/llm-core/src/utils/event-stream.js";
|
||||
export { parseStreamingJson } from "../llm/utils/json-parse.js";
|
||||
export { createHttpProxyAgentsForTarget } from "../llm/utils/node-http-proxy.js";
|
||||
export { sanitizeSurrogates } from "../llm/utils/sanitize-unicode.js";
|
||||
export {
|
||||
validateToolArguments,
|
||||
validateToolCall,
|
||||
} from "../../packages/agent-core/src/validation.js";
|
||||
export { validateToolArguments, validateToolCall } from "../../packages/llm-core/src/validation.js";
|
||||
|
||||
@@ -224,6 +224,7 @@ function runWrapper(
|
||||
PATH: [...(options.extraPathEntries ?? []), binDir, gitBinDir, process.env.PATH ?? ""]
|
||||
.filter(Boolean)
|
||||
.join(path.delimiter),
|
||||
CRABBOX_PROVIDER: "",
|
||||
OPENCLAW_CRABBOX_WRAPPER_IGNORE_REPO_BINARY: "1",
|
||||
...(options.configJson
|
||||
? { OPENCLAW_FAKE_CRABBOX_CONFIG_JSON: JSON.stringify(options.configJson) }
|
||||
@@ -415,6 +416,7 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
const result = runWrapper(
|
||||
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
|
||||
["run", "--target", "windows", "--", "echo ok"],
|
||||
{ env: { CRABBOX_PROVIDER: "aws" } },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
@@ -429,15 +431,11 @@ describe.concurrent("scripts/crabbox-wrapper", () => {
|
||||
});
|
||||
|
||||
it("keeps existing Windows lease selections on the configured provider", () => {
|
||||
const result = runWrapper(azureProviderHelp, [
|
||||
"run",
|
||||
"--id",
|
||||
"cbx_existing",
|
||||
"--target",
|
||||
"windows",
|
||||
"--",
|
||||
"echo ok",
|
||||
]);
|
||||
const result = runWrapper(
|
||||
azureProviderHelp,
|
||||
["run", "--id", "cbx_existing", "--target", "windows", "--", "echo ok"],
|
||||
{ env: { CRABBOX_PROVIDER: "aws" } },
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toEqual([
|
||||
|
||||
@@ -221,6 +221,22 @@ export const sharedVitestConfig = {
|
||||
find: "@openclaw/gateway-protocol",
|
||||
replacement: path.join(repoRoot, "packages", "gateway-protocol", "src", "index.ts"),
|
||||
},
|
||||
{
|
||||
find: "@openclaw/llm-core/diagnostics",
|
||||
replacement: path.join(repoRoot, "packages", "llm-core", "src", "utils", "diagnostics.ts"),
|
||||
},
|
||||
{
|
||||
find: "@openclaw/llm-core/event-stream",
|
||||
replacement: path.join(repoRoot, "packages", "llm-core", "src", "utils", "event-stream.ts"),
|
||||
},
|
||||
{
|
||||
find: "@openclaw/llm-core/validation",
|
||||
replacement: path.join(repoRoot, "packages", "llm-core", "src", "validation.ts"),
|
||||
},
|
||||
{
|
||||
find: "@openclaw/llm-core",
|
||||
replacement: path.join(repoRoot, "packages", "llm-core", "src", "index.ts"),
|
||||
},
|
||||
{
|
||||
find: "@openclaw/net-policy/ip",
|
||||
replacement: path.join(repoRoot, "packages", "net-policy", "src", "ip.ts"),
|
||||
|
||||
@@ -29,6 +29,13 @@
|
||||
"openclaw/plugin-sdk/*": ["./src/plugin-sdk/*.ts"],
|
||||
"@openclaw/agent-core": ["./packages/agent-core/src/index.ts"],
|
||||
"@openclaw/agent-core/*": ["./packages/agent-core/src/*"],
|
||||
"@openclaw/llm-core": ["./packages/llm-core/src/index.ts"],
|
||||
"@openclaw/llm-core/diagnostics": ["./packages/llm-core/src/utils/diagnostics.ts"],
|
||||
"@openclaw/llm-core/event-stream": ["./packages/llm-core/src/utils/event-stream.ts"],
|
||||
"@openclaw/llm-core/validation": ["./packages/llm-core/src/validation.ts"],
|
||||
"@openclaw/llm-core/*": ["./packages/llm-core/src/*"],
|
||||
"@openclaw/llm-runtime": ["./packages/llm-runtime/src/index.ts"],
|
||||
"@openclaw/llm-runtime/*": ["./packages/llm-runtime/src/*"],
|
||||
"@openclaw/gateway-client": ["./packages/gateway-client/src/index.ts"],
|
||||
"@openclaw/gateway-client/*": ["./packages/gateway-client/src/*"],
|
||||
"@openclaw/gateway-protocol": ["./packages/gateway-protocol/src/index.ts"],
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"include": [
|
||||
"src/plugin-sdk/**/*.ts",
|
||||
"packages/llm-core/src/**/*.ts",
|
||||
"packages/memory-host-sdk/src/**/*.ts",
|
||||
"src/video-generation/dashscope-compatible.ts",
|
||||
"src/video-generation/types.ts",
|
||||
|
||||
@@ -393,8 +393,28 @@ function buildSpeechCoreDistEntries(): Record<string, string> {
|
||||
};
|
||||
}
|
||||
|
||||
function buildLlmCoreDistEntries(): Record<string, string> {
|
||||
return {
|
||||
index: "packages/llm-core/src/index.ts",
|
||||
types: "packages/llm-core/src/types.ts",
|
||||
"utils/diagnostics": "packages/llm-core/src/utils/diagnostics.ts",
|
||||
"utils/event-stream": "packages/llm-core/src/utils/event-stream.ts",
|
||||
validation: "packages/llm-core/src/validation.ts",
|
||||
};
|
||||
}
|
||||
|
||||
function buildLlmRuntimeDistEntries(): Record<string, string> {
|
||||
return {
|
||||
index: "packages/llm-runtime/src/index.ts",
|
||||
"api-registry": "packages/llm-runtime/src/api-registry.ts",
|
||||
stream: "packages/llm-runtime/src/stream.ts",
|
||||
};
|
||||
}
|
||||
|
||||
function shouldExternalizeAgentCoreDependency(id: string): boolean {
|
||||
return (
|
||||
id === "@openclaw/llm-core" ||
|
||||
id.startsWith("@openclaw/llm-core/") ||
|
||||
id === "ignore" ||
|
||||
id === "openclaw" ||
|
||||
id.startsWith("openclaw/") ||
|
||||
@@ -426,6 +446,14 @@ function shouldExternalizeSpeechCoreDependency(id: string): boolean {
|
||||
return id === "openclaw" || id.startsWith("openclaw/");
|
||||
}
|
||||
|
||||
function shouldExternalizeLlmCoreDependency(id: string): boolean {
|
||||
return id === "typebox" || id.startsWith("typebox/");
|
||||
}
|
||||
|
||||
function shouldExternalizeLlmRuntimeDependency(id: string): boolean {
|
||||
return id === "@openclaw/llm-core" || id.startsWith("@openclaw/llm-core/");
|
||||
}
|
||||
|
||||
const coreDistEntries = buildCoreDistEntries();
|
||||
const dockerE2eHarnessEntries = buildDockerE2eHarnessEntries();
|
||||
const rootBundledPluginBuildEntries = bundledPluginBuildEntries.filter(
|
||||
@@ -504,6 +532,24 @@ export default defineConfig([
|
||||
neverBundle: shouldExternalizeSpeechCoreDependency,
|
||||
},
|
||||
}),
|
||||
nodeWorkspacePackageBuildConfig({
|
||||
clean: true,
|
||||
dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined,
|
||||
entry: buildLlmCoreDistEntries(),
|
||||
outDir: "packages/llm-core/dist",
|
||||
deps: {
|
||||
neverBundle: shouldExternalizeLlmCoreDependency,
|
||||
},
|
||||
}),
|
||||
nodeWorkspacePackageBuildConfig({
|
||||
clean: true,
|
||||
dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined,
|
||||
entry: buildLlmRuntimeDistEntries(),
|
||||
outDir: "packages/llm-runtime/dist",
|
||||
deps: {
|
||||
neverBundle: shouldExternalizeLlmRuntimeDependency,
|
||||
},
|
||||
}),
|
||||
nodeBuildConfig({
|
||||
// Build core entrypoints, plugin-sdk subpaths, bundled plugin entrypoints,
|
||||
// and bundled hooks in one graph so runtime singletons are emitted once.
|
||||
|
||||
Reference in New Issue
Block a user