mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-17 14:21:34 +00:00
* refactor: extract reusable AI runtime package * refactor: complete AI provider relocation * refactor: keep llm core internal * refactor(ai): make @openclaw/ai self-contained with host policy ports Move pure transport helpers (tool projections, strict-schema normalization, prompt-cache boundary, stream guards, anthropic/openai compat, request activity) from src into packages/ai; move utf16-slice into normalization-core. Inject host policy (guarded fetch, redaction, strict-tool defaults, diagnostics logging) through AiTransportHost with inert library defaults installed by src/llm/stream.ts. Narrow the public barrel to instance-scoped createApiRegistry/createLlmRuntime; the process-default runtime moves behind internal/ and registerBuiltInApiProviders takes an explicit registry. Delete the src/llm/api-registry re-export facade. * fix(ai): teach node, jiti, and vite resolvers the @openclaw/ai and utf16-slice subpaths The workspace alias tables in root-alias.cjs, plugin-sdk-native-resolver, sdk-alias, the shared vitest config, and the Control UI vite config only knew @openclaw/llm-core; Node-side plugin loading resolved @openclaw/ai through the pnpm symlink to the unbuilt dist (checks-node-compact CI failures), and the Control UI build broke on the new normalization-core/utf16-slice subpath. * chore(ui): drop leftover service-worker debug logging * build(release): ship @openclaw/ai with its own shrinkwrap and honest dependency set packages/ai declares only its six real runtime deps (kysely, chalk, json5, tslog, zod, fs-safe, and proxyline were never imported); orphaned root deps removed. generate-npm-shrinkwrap now treats publishable packages/* like publishable plugins so the AI tarball pins its transitive tree even though workspace deps are omitted from the root shrinkwrap. knip learns the package entry points; the tsdown dts neverBundle option moves to its documented deps.dts home; the README documents the no-semver internal/* contract and host ports. * docs(ai): add minimal external-consumer example app examples/ai-chat consumes only the public @openclaw/ai surface (built dist via the workspace link): isolated runtime, built-in provider registration, one streamed completion. Supports Anthropic/OpenAI via env keys and a keyless local Ollama target; live-verified against Ollama. * docs(ai): document the @openclaw/ai package and workspace shrinkwrap boundary * chore(check): include examples/ in duplicate-scan targets * fix: emit normalization package subpaths * fix: complete AI package boundary artifacts * fix: align AI package boundary contracts * fix(ci): stabilize package release contracts * test: align documentation contract checks * test: keep cron docs guard aligned * test: align restored docs contract guards * test: follow upstream docs contracts * docs: drop superseded talk wording
120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
// LLM Runtime module implements api registry behavior.
|
|
import type {
|
|
Api,
|
|
AssistantMessageEventStreamContract,
|
|
Context,
|
|
Model,
|
|
SimpleStreamOptions,
|
|
StreamFunction,
|
|
StreamOptions,
|
|
} from "@openclaw/llm-core";
|
|
|
|
/** Runtime stream adapter signature stored in the API provider registry. */
|
|
export type ApiStreamFunction = (
|
|
model: Model,
|
|
context: Context,
|
|
options?: StreamOptions,
|
|
) => AssistantMessageEventStreamContract;
|
|
|
|
/** Runtime simple-stream adapter signature stored in the API provider registry. */
|
|
export type ApiStreamSimpleFunction = (
|
|
model: Model,
|
|
context: Context,
|
|
options?: SimpleStreamOptions,
|
|
) => AssistantMessageEventStreamContract;
|
|
|
|
/** Provider implementation registered by core or plugins for a specific model API. */
|
|
export interface ApiProvider<
|
|
TApi extends Api = Api,
|
|
TOptions extends StreamOptions = StreamOptions,
|
|
> {
|
|
/** Model API id this provider handles. */
|
|
api: TApi;
|
|
/** Full streaming adapter for callers that already own structured options. */
|
|
stream: StreamFunction<TApi, TOptions>;
|
|
/** Simple streaming adapter used by agent and plugin runtime defaults. */
|
|
streamSimple: StreamFunction<TApi, SimpleStreamOptions>;
|
|
}
|
|
|
|
/** Type-erased provider returned by a registry after API guards are installed. */
|
|
export interface RegisteredApiProvider {
|
|
api: Api;
|
|
stream: ApiStreamFunction;
|
|
streamSimple: ApiStreamSimpleFunction;
|
|
}
|
|
|
|
type RegisteredApiProviderEntry = {
|
|
provider: RegisteredApiProvider;
|
|
sourceId?: string;
|
|
};
|
|
|
|
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);
|
|
};
|
|
}
|
|
|
|
/** Creates an isolated provider registry for one runtime or tenant. */
|
|
export function createApiRegistry() {
|
|
const providers = new Map<string, RegisteredApiProviderEntry>();
|
|
|
|
function registerApiProvider<TApi extends Api, TOptions extends StreamOptions>(
|
|
provider: ApiProvider<TApi, TOptions>,
|
|
/** Optional source id used to unregister all providers owned by one plugin/runtime. */
|
|
sourceId?: string,
|
|
): void {
|
|
providers.set(provider.api, {
|
|
provider: {
|
|
api: provider.api,
|
|
stream: wrapStream(provider.api, provider.stream),
|
|
streamSimple: wrapStreamSimple(provider.api, provider.streamSimple),
|
|
},
|
|
sourceId,
|
|
});
|
|
}
|
|
|
|
function getApiProvider(api: Api): RegisteredApiProvider | undefined {
|
|
return providers.get(api)?.provider;
|
|
}
|
|
|
|
function getApiProviders(): RegisteredApiProvider[] {
|
|
return Array.from(providers.values(), (entry) => entry.provider);
|
|
}
|
|
|
|
function unregisterApiProviders(sourceId: string): void {
|
|
for (const [api, entry] of providers.entries()) {
|
|
if (entry.sourceId === sourceId) {
|
|
providers.delete(api);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
registerApiProvider,
|
|
getApiProvider,
|
|
getApiProviders,
|
|
unregisterApiProviders,
|
|
clearApiProviders: () => providers.clear(),
|
|
};
|
|
}
|
|
|
|
export type ApiRegistry = ReturnType<typeof createApiRegistry>;
|