diff --git a/extensions/ollama/index.test.ts b/extensions/ollama/index.test.ts index 954f172f32c5..24a30b03d844 100644 --- a/extensions/ollama/index.test.ts +++ b/extensions/ollama/index.test.ts @@ -370,7 +370,7 @@ async function augmentOllamaCatalog( } as never); } -function captureWrappedOllamaPayload( +async function captureWrappedOllamaPayload( thinkingLevel: "off" | "minimal" | "low" | "medium" | "high" | "max" | undefined, route: { provider?: string; modelId?: string; baseUrl?: string } = {}, ) { @@ -418,7 +418,7 @@ function captureWrappedOllamaPayload( if (!wrapped) { throw new Error("expected Ollama thinking stream wrapper"); } - void wrapped( + await wrapped( { api: "ollama", provider: providerId, @@ -2532,12 +2532,17 @@ describe("ollama plugin", () => { expect(auth).toBeUndefined(); }); - it("wraps OpenAI-compatible payloads with num_ctx for Ollama compat routes", () => { + it("wraps OpenAI-compatible payloads with num_ctx for Ollama compat routes", async () => { const provider = registerProvider(); let payloadSeen: Record | undefined; + const payloadResult = Promise.resolve(); + const onPayload = vi.fn((payload: unknown) => { + expect(payload).toEqual({ options: { temperature: 0.1, num_ctx: 32_768 } }); + return payloadResult; + }); const baseStreamFn = vi.fn((_model, _context, options) => { const payload: Record = { options: { temperature: 0.1 } }; - options?.onPayload?.(payload, _model); + expect(options?.onPayload?.(payload, _model)).toBe(payloadResult); payloadSeen = payload; return {} as never; }); @@ -2570,11 +2575,55 @@ describe("ollama plugin", () => { if (!wrapped) { throw new Error("expected Ollama OpenAI-compatible stream wrapper"); } - void wrapped({} as never, {} as never, {}); + await wrapped({} as never, {} as never, { onPayload }); expect(baseStreamFn).toHaveBeenCalledTimes(1); + expect(onPayload).toHaveBeenCalledOnce(); expect((payloadSeen?.options as Record | undefined)?.num_ctx).toBe(32_768); }); + it.each(["ollama", "openai-completions"] as const)( + "does not start the %s stream after cancellation during wrapper preparation", + async (api) => { + const provider = registerProvider(); + const controller = new AbortController(); + const reason = new Error("stream canceled during preparation"); + const baseStreamFn = vi.fn(() => ({}) as never); + const model = { + api, + provider: "ollama", + id: "qwen3:32b", + baseUrl: "http://127.0.0.1:11434/v1", + contextWindow: 32_768, + }; + const wrapped = expectDefined( + provider.wrapStreamFn?.({ + provider: "ollama", + modelId: model.id, + model, + streamFn: baseStreamFn, + thinkingLevel: "high", + }), + "Ollama stream wrapper", + ); + + const pending = wrapped(model as never, { messages: [] }, { signal: controller.signal }); + controller.abort(reason); + + await expect(pending).rejects.toBe(reason); + expect(baseStreamFn).not.toHaveBeenCalled(); + }, + ); + + it("preserves the original stream when no Ollama wrapper applies", () => { + const provider = registerProvider(); + const streamFn = vi.fn(() => ({}) as never); + const context = { provider: "ollama", modelId: "qwen3:32b" }; + + expect(provider.wrapStreamFn?.({ ...context, streamFn })).toBe(streamFn); + expect(provider.wrapStreamFn?.(context)).toBeUndefined(); + expect(streamFn).not.toHaveBeenCalled(); + }); + it("owns replay policy for OpenAI-compatible and native Ollama routes", () => { const provider = registerProvider(); @@ -2757,15 +2806,15 @@ describe("ollama plugin", () => { thinkingLevel: undefined, expectedThink: undefined, }, - ])("$name", ({ thinkingLevel, expectedThink }) => { - const { baseStreamFn, payloadSeen } = captureWrappedOllamaPayload(thinkingLevel); + ])("$name", async ({ thinkingLevel, expectedThink }) => { + const { baseStreamFn, payloadSeen } = await captureWrappedOllamaPayload(thinkingLevel); expect(baseStreamFn).toHaveBeenCalledTimes(1); expect(payloadSeen?.think).toBe(expectedThink); expect((payloadSeen?.options as Record | undefined)?.think).toBeUndefined(); }); - it("preserves native Ollama Cloud max thinking on the wire", () => { - const { payloadSeen } = captureWrappedOllamaPayload("max", { + it("preserves native Ollama Cloud max thinking on the wire", async () => { + const { payloadSeen } = await captureWrappedOllamaPayload("max", { provider: "ollama-cloud", modelId: "glm-5.2", baseUrl: "https://ollama.com", diff --git a/extensions/ollama/src/stream-compat.ts b/extensions/ollama/src/stream-compat.ts index 04a01b409b04..67d54da11189 100644 --- a/extensions/ollama/src/stream-compat.ts +++ b/extensions/ollama/src/stream-compat.ts @@ -1,24 +1,46 @@ import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; +import { + createLazyRuntimeModule, + createLazyRuntimeSurface, +} from "openclaw/plugin-sdk/lazy-runtime"; import type { OpenClawConfig, ProviderRuntimeModel, ProviderWrapStreamFnContext, } from "openclaw/plugin-sdk/plugin-entry"; import { + createMoonshotThinkingWrapper, DEFAULT_CONTEXT_TOKENS, normalizeProviderId, -} from "openclaw/plugin-sdk/provider-model-shared"; -import { - createMoonshotThinkingWrapper, - createPayloadPatchStreamWrapper, resolveMoonshotThinkingType, -} from "openclaw/plugin-sdk/provider-stream-shared"; +} from "openclaw/plugin-sdk/provider-model-shared"; import { isLoopbackHost } from "openclaw/plugin-sdk/ssrf-runtime"; import { shouldWrapOllamaCompatMoonshotThinking } from "./model-behavior.js"; import { supportsOllamaCloudFullThinkingEffort } from "./model-reasoning.js"; export type OllamaThinkValue = boolean | "low" | "medium" | "high" | "max"; +const loadProviderStreamRuntime = createLazyRuntimeModule( + () => import("openclaw/plugin-sdk/provider-stream-shared"), +); + +function createLazyPayloadPatchStreamWrapper( + baseFn: StreamFn | undefined, + patchPayload: Parameters< + (typeof import("openclaw/plugin-sdk/provider-stream-shared"))["createPayloadPatchStreamWrapper"] + >[1], +): StreamFn { + const loadStream = createLazyRuntimeSurface(loadProviderStreamRuntime, (runtime) => + runtime.createPayloadPatchStreamWrapper(baseFn, patchPayload), + ); + return async (model, context, options) => { + options?.signal?.throwIfAborted(); + const stream = await loadStream(); + options?.signal?.throwIfAborted(); + return stream(model, context, options); + }; +} + export function resolveConfiguredOllamaProviderConfig(params: { config?: OpenClawConfig; providerId?: string; @@ -98,7 +120,7 @@ export function shouldInjectOllamaCompatNumCtx(params: { } export function wrapOllamaCompatNumCtx(baseFn: StreamFn | undefined, numCtx: number): StreamFn { - return createPayloadPatchStreamWrapper(baseFn, ({ payload }) => { + return createLazyPayloadPatchStreamWrapper(baseFn, ({ payload }) => { if (!payload.options || typeof payload.options !== "object") { payload.options = {}; } @@ -110,7 +132,7 @@ function createOllamaThinkingWrapper( baseFn: StreamFn | undefined, think: OllamaThinkValue, ): StreamFn { - return createPayloadPatchStreamWrapper(baseFn, ({ payload }) => { + return createLazyPayloadPatchStreamWrapper(baseFn, ({ payload }) => { payload.think = think; }); } diff --git a/packages/ai/README.md b/packages/ai/README.md index e1902ae2a35f..cb9b47c15268 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -24,7 +24,7 @@ provider plugin hooks, and diagnostics logging) can be injected with `configureAiTransportHost`; the defaults are inert. The explicit `@openclaw/ai/internal/anthropic`, `google-model-family`, `openai`, -`openai-responses-payload-policy`, `retry-after`, `runtime`, `shared`, and +`openai-completions-compat`, `openai-responses-payload-policy`, `retry-after`, `runtime`, `shared`, and `tool-schema` subpaths exist for the OpenClaw application itself. They carry no semver guarantee and can change or disappear in any release; do not depend on them outside OpenClaw. diff --git a/packages/ai/package.json b/packages/ai/package.json index 4c4d898a86ce..a0179e09faa8 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -86,6 +86,11 @@ "import": "./dist/internal/openai.mjs", "default": "./dist/internal/openai.mjs" }, + "./internal/openai-completions-compat": { + "types": "./dist/internal/openai-completions-compat.d.mts", + "import": "./dist/internal/openai-completions-compat.mjs", + "default": "./dist/internal/openai-completions-compat.mjs" + }, "./internal/openai-responses-payload-policy": { "types": "./dist/internal/openai-responses-payload-policy.d.mts", "import": "./dist/internal/openai-responses-payload-policy.mjs", diff --git a/packages/ai/src/internal/openai-completions-compat.ts b/packages/ai/src/internal/openai-completions-compat.ts new file mode 100644 index 000000000000..1e8395739564 --- /dev/null +++ b/packages/ai/src/internal/openai-completions-compat.ts @@ -0,0 +1 @@ +export { resolveOpenAICompletionsCompat } from "../transports/openai-completions-compat.js"; diff --git a/scripts/check-openclaw-package-tarball.mts b/scripts/check-openclaw-package-tarball.mts index 9070eedb42d3..32e3fda8e271 100644 --- a/scripts/check-openclaw-package-tarball.mts +++ b/scripts/check-openclaw-package-tarball.mts @@ -126,6 +126,11 @@ const REQUIRED_BUNDLED_WORKSPACE_RUNTIME_ENTRIES = new Map([ entry: "dist/transports.mjs", whenExported: "./transports", }, + { + specifier: "@openclaw/ai/internal/openai-completions-compat", + entry: "dist/internal/openai-completions-compat.mjs", + whenExported: "./internal/openai-completions-compat", + }, { specifier: "@openclaw/ai/internal/openai-responses-payload-policy", entry: "dist/internal/openai-responses-payload-policy.mjs", diff --git a/src/agents/agent-model-discovery.imports.test.ts b/src/agents/agent-model-discovery.imports.test.ts index 1a2c8762ec76..0feac8916338 100644 --- a/src/agents/agent-model-discovery.imports.test.ts +++ b/src/agents/agent-model-discovery.imports.test.ts @@ -2,6 +2,15 @@ import { describe, expect, it } from "vitest"; import { findSourceImportBackedges } from "../../test/helpers/source-import-closure.js"; describe("agent model discovery imports", () => { + it("keeps prepared catalog metadata independent of streaming execution", () => { + expect( + findSourceImportBackedges("src/agents/prepared-model-catalog.worker.ts", [ + "src/agents/ai-transport-runtime-host.ts", + "packages/ai/src/transports.ts", + ]), + ).toEqual([]); + }); + it("keeps model discovery independent of session execution", () => { expect( findSourceImportBackedges("src/agents/agent-model-discovery.ts", [ diff --git a/src/agents/harness/runtime-plugin-load-plan.ts b/src/agents/harness/runtime-plugin-load-plan.ts index 932bafd94a4b..c5d9f9ed6575 100644 --- a/src/agents/harness/runtime-plugin-load-plan.ts +++ b/src/agents/harness/runtime-plugin-load-plan.ts @@ -41,6 +41,8 @@ export type AgentHarnessPluginSelection = { agentId?: string; }; +export type RuntimePluginLoadPurpose = "agent" | "model-catalog"; + function dedupePluginIds(values: readonly string[]): string[] { const result: string[] = []; for (const value of values) { @@ -313,20 +315,26 @@ export function resolveAgentRuntimePluginLoadPlan(params: { basePluginIds?: readonly string[]; selections: readonly AgentHarnessPluginSelection[]; metadataSnapshot: PluginMetadataSnapshot; + purpose?: RuntimePluginLoadPurpose; }): { config?: OpenClawConfig; pluginIds?: string[] } { let config = params.config; - const memoryPluginIds = resolveSelectedMemoryPluginIds({ - config: params.config, - metadataSnapshot: params.metadataSnapshot, - }); - const contextEnginePluginId = resolveSelectedContextEnginePluginId(params.config); + const includeAgentOwners = params.purpose !== "model-catalog"; + const memoryPluginIds = includeAgentOwners + ? resolveSelectedMemoryPluginIds({ + config: params.config, + metadataSnapshot: params.metadataSnapshot, + }) + : []; + const contextEnginePluginId = includeAgentOwners + ? resolveSelectedContextEnginePluginId(params.config) + : undefined; const contextEnginePluginIds = contextEnginePluginId ? [contextEnginePluginId] : []; const basePluginIds = (params.basePluginIds ?? []).filter( (pluginId) => !restrictiveAllowlistOmitsPlugin(params.config, pluginId), ); const pluginIds = [...basePluginIds, ...memoryPluginIds, ...contextEnginePluginIds]; const forceActivatedPluginIds = [...memoryPluginIds, ...contextEnginePluginIds]; - for (const selection of params.selections) { + for (const selection of includeAgentOwners ? params.selections : []) { const runtime = resolveSelectedAgentHarnessRuntime(selection, config); const providerOwnerPluginIds = resolveSelectedProviderOwnerPluginIds({ provider: selection.provider, @@ -386,7 +394,7 @@ export function resolveAgentRuntimePluginLoadPlan(params: { } return { ...(config ? { config } : {}), - ...(params.basePluginIds === undefined && scopedPluginIds.length === 0 + ...(includeAgentOwners && params.basePluginIds === undefined && scopedPluginIds.length === 0 ? {} : { pluginIds: scopedPluginIds }), }; diff --git a/src/agents/harness/runtime-plugin.test.ts b/src/agents/harness/runtime-plugin.test.ts index f87a91e66aa1..4232038fb392 100644 --- a/src/agents/harness/runtime-plugin.test.ts +++ b/src/agents/harness/runtime-plugin.test.ts @@ -601,6 +601,39 @@ describe("harness runtime plugins", () => { expect(plan.config?.plugins?.entries?.["custom-context-engine"]).toEqual({ enabled: true }); }); + it.each([ + { basePluginIds: [], allowed: ["catalog-provider"], expected: [] }, + { + basePluginIds: ["catalog-provider"], + allowed: ["catalog-provider"], + expected: ["catalog-provider"], + }, + { basePluginIds: ["memory-core"], allowed: ["memory-core"], expected: ["memory-core"] }, + { basePluginIds: ["catalog-provider"], allowed: ["other-provider"], expected: [] }, + ])( + "keeps catalog scope $basePluginIds within allowlist $allowed", + ({ basePluginIds, allowed, expected }) => { + const config: OpenClawConfig = { + plugins: { + allow: [...allowed, "memory-lancedb", "custom-context-engine", "codex"], + slots: { memory: "memory-lancedb", contextEngine: "custom-context-engine" }, + }, + }; + const plan = resolveAgentRuntimePluginLoadPlan({ + metadataSnapshot: createMemoryPlanMetadataSnapshot(), + config, + workspaceDir: "/tmp/workspace", + basePluginIds, + selections: [{ provider: "openai", modelId: "gpt-5.5", runtime: "codex" }], + purpose: "model-catalog", + }); + + expect(plan.pluginIds).toEqual(expected); + expect(plan.config?.plugins?.entries).toBeUndefined(); + expect(plan.config?.plugins?.slots).toEqual(config.plugins?.slots); + }, + ); + const memorySelectionCases: Array<{ name: string; config: OpenClawConfig; diff --git a/src/agents/prepared-model-catalog-worker.scope.integration.test.ts b/src/agents/prepared-model-catalog-worker.scope.integration.test.ts index 7e4e97bfeed4..92124a02193a 100644 --- a/src/agents/prepared-model-catalog-worker.scope.integration.test.ts +++ b/src/agents/prepared-model-catalog-worker.scope.integration.test.ts @@ -34,10 +34,15 @@ const { makeTempDir, retireAfterTest, waitForMarker, waitForWorkers } = describe("prepared model catalog worker plugin scope", () => { it.each([ - { first: "full", asyncSyntheticAuth: false, syntheticAuthAvailable: true }, - { first: "scoped", asyncSyntheticAuth: true, syntheticAuthAvailable: false }, - { first: "held", asyncSyntheticAuth: true, syntheticAuthAvailable: false }, - ])("keeps models.list scoped with $first catalog discovery first", async (selection) => { + { first: "full", slot: "memory", asyncSyntheticAuth: false, syntheticAuthAvailable: true }, + { + first: "scoped", + slot: "contextEngine", + asyncSyntheticAuth: true, + syntheticAuthAvailable: false, + }, + { first: "held", slot: "none", asyncSyntheticAuth: true, syntheticAuthAvailable: false }, + ])("keeps models.list scoped with $first discovery and $slot selected", async (selection) => { const root = makeTempDir("openclaw-model-catalog-scope-worker-"); const stateDir = path.join(root, "state"); const agentDir = path.join(stateDir, "agents", "main", "agent"); @@ -48,7 +53,14 @@ describe("prepared model catalog worker plugin scope", () => { fs.mkdirSync(workspaceDir, { recursive: true }); const pluginFile = writeFixturePlugin({ root, spinMs: 0, ...selection }); - const unrelatedPluginFile = writeUnrelatedFixturePlugin(root); + const unrelatedPluginFile = writeUnrelatedFixturePlugin( + root, + selection.slot === "memory" + ? "memory" + : selection.slot === "contextEngine" + ? "context-engine" + : undefined, + ); const config = { agents: { defaults: { @@ -81,6 +93,11 @@ describe("prepared model catalog worker plugin scope", () => { }, plugins: { allow: [PLUGIN_ID, UNRELATED_PLUGIN_ID], + ...(selection.slot === "memory" + ? { slots: { memory: UNRELATED_PLUGIN_ID } } + : selection.slot === "contextEngine" + ? { slots: { contextEngine: UNRELATED_PLUGIN_ID } } + : {}), load: { paths: [pluginFile, unrelatedPluginFile] }, entries: { [PLUGIN_ID]: { enabled: true }, diff --git a/src/agents/prepared-model-catalog-worker.test-support.ts b/src/agents/prepared-model-catalog-worker.test-support.ts index 2910571ddd0e..5ede999bb3ea 100644 --- a/src/agents/prepared-model-catalog-worker.test-support.ts +++ b/src/agents/prepared-model-catalog-worker.test-support.ts @@ -60,7 +60,10 @@ export const EXTERNAL_AUTH_PATH_ENV = "OPENCLAW_WORKER_EXTERNAL_AUTH_PATH"; export const UNRELATED_PLUGIN_ID = "worker-catalog-unrelated"; export const UNRELATED_PLUGIN_WORKER_MARKER_ENV = "OPENCLAW_WORKER_UNRELATED_PLUGIN_MARKER"; -export function writeUnrelatedFixturePlugin(root: string): string { +export function writeUnrelatedFixturePlugin( + root: string, + kind?: "memory" | "context-engine", +): string { const pluginDir = path.join(root, "unrelated-plugin"); fs.mkdirSync(pluginDir, { recursive: true }); const pluginFile = path.join(pluginDir, "index.cjs"); @@ -79,6 +82,7 @@ module.exports = { id: ${JSON.stringify(UNRELATED_PLUGIN_ID)}, register() {} }; path.join(pluginDir, "openclaw.plugin.json"), JSON.stringify({ id: UNRELATED_PLUGIN_ID, + ...(kind ? { kind } : {}), configSchema: { type: "object", additionalProperties: false, properties: {} }, }), "utf8", diff --git a/src/agents/prepared-model-catalog.worker.ts b/src/agents/prepared-model-catalog.worker.ts index 8456d3347aaa..3d6bb48901c0 100644 --- a/src/agents/prepared-model-catalog.worker.ts +++ b/src/agents/prepared-model-catalog.worker.ts @@ -129,13 +129,13 @@ async function prepareWorkerGeneration(value: PreparedModelCatalogWorkerInput) { .map((plugin) => plugin.id) .toSorted((left, right) => left.localeCompare(right)); const prepared = await prepareWorkspaceBuildGroup( - [{ ...value.input, runtimePluginSelections: [] }], + [value.input], "static", { preferBuiltPluginArtifacts: value.preferBuiltPluginArtifacts, basePluginIds, providerDiscoveryProviderIds: value.providerIds, - getConfiguredHarnessRuntimes: () => [], + purpose: "model-catalog", }, undefined, undefined, @@ -285,8 +285,7 @@ export async function runPreparedModelCatalogWorkerRequest( // catalog owners from the captured metadata before binding the authoritative registry. const catalogRegistry = loadAgentRuntimePluginRegistryHandle({ ...value.input, - selections: [], - configuredHarnessRuntimes: [], + purpose: "model-catalog", metadataSnapshot: pluginMetadataSnapshot, preferBuiltPluginArtifacts: value.preferBuiltPluginArtifacts, reusableRegistry: pluginRegistry, diff --git a/src/agents/prepared-model-runtime.facts.ts b/src/agents/prepared-model-runtime.facts.ts index 6edbba57d734..d6cc3bdbfb39 100644 --- a/src/agents/prepared-model-runtime.facts.ts +++ b/src/agents/prepared-model-runtime.facts.ts @@ -38,6 +38,7 @@ import { loadBundledProviderStaticCatalogContextModels, } from "./embedded-agent-runner/model.static-catalog.js"; import { createStaticModelIdMatcher } from "./embedded-agent-runner/model.static-id.js"; +import type { RuntimePluginLoadPurpose } from "./harness/runtime-plugin-load-plan.js"; import { modelCatalogRowToEntry } from "./model-catalog-entry.js"; import { buildConfiguredModelCatalog, @@ -163,6 +164,7 @@ export async function prepareWorkspaceBuildGroup( basePluginIds?: readonly string[]; onStage?: (stage: string) => void; registryResources?: PreparedModelRuntimeBuildResources; + purpose?: RuntimePluginLoadPurpose; } = {}, loadInboundPluginRegistry?: PreparedInboundRegistryLoader, reusablePluginGeneration?: PreparedModelRuntimePluginGeneration, @@ -215,6 +217,7 @@ export async function prepareWorkspaceBuildGroup( options.getConfiguredHarnessRuntimes, options.basePluginIds, options.registryResources, + options.purpose, ); const { inboundPluginRegistry, runtimePluginRegistry, primaryRegistry } = preparingRegistries instanceof Promise ? await preparingRegistries : preparingRegistries; diff --git a/src/agents/prepared-model-runtime.inbound-registry.ts b/src/agents/prepared-model-runtime.inbound-registry.ts index 0af935147034..e8b12b4a19ea 100644 --- a/src/agents/prepared-model-runtime.inbound-registry.ts +++ b/src/agents/prepared-model-runtime.inbound-registry.ts @@ -11,6 +11,7 @@ import { getActivePluginRegistryWorkspaceDir, getActivePluginRuntimeSubagentMode, } from "../plugins/runtime.js"; +import type { RuntimePluginLoadPurpose } from "./harness/runtime-plugin-load-plan.js"; import { prepareOwnedPluginLoadContext } from "./prepared-model-runtime.plugin-context.js"; import type { PreparedModelRuntimeBuildResources } from "./prepared-model-runtime.resources.js"; import type { @@ -154,20 +155,31 @@ export function prepareWorkspacePluginRegistries( getConfiguredHarnessRuntimes?: () => readonly string[], basePluginIds?: readonly string[], registryResources?: PreparedModelRuntimeBuildResources, + purpose?: RuntimePluginLoadPurpose, ): PreparedWorkspacePluginRegistries | Promise { - // Read-only catalog owners stay runtime-free. Executable probes opt in to provider runtime, - // while non-core harness probes carry the exact selected plugin generation. - if (input.readOnly && !input.loadRuntimePlugins && !input.runtimePluginSelections) { + // Passive reads stay runtime-free; catalog workers and executable probes carry explicit scope. + if ( + purpose !== "model-catalog" && + input.readOnly && + !input.loadRuntimePlugins && + !input.runtimePluginSelections + ) { return {}; } // Resolve batch facts only for a registry load; read-only and reused registries need no scan. let primaryRegistry: PluginRegistry | undefined; - const inboundPluginRegistry = input.readOnly - ? undefined - : (reusableGeneration?.inboundPluginRegistry ?? - loadInboundRegistry?.(input, metadataSnapshot, getConfiguredHarnessRuntimes?.(), (source) => { - primaryRegistry = source; - })); + const inboundPluginRegistry = + input.readOnly || purpose === "model-catalog" + ? undefined + : (reusableGeneration?.inboundPluginRegistry ?? + loadInboundRegistry?.( + input, + metadataSnapshot, + getConfiguredHarnessRuntimes?.(), + (source) => { + primaryRegistry = source; + }, + )); const baseRegistry = reusableGeneration?.pluginRegistry ?? inboundPluginRegistry; primaryRegistry ??= reusableGeneration?.mediaCapabilityProviderSource?.registry ?? baseRegistry; let loadedPrimaryRegistry: PluginRegistry | undefined; @@ -175,16 +187,18 @@ export function prepareWorkspacePluginRegistries( ? registryResources.load.bind(registryResources) : loadAgentRuntimePluginRegistryHandle; const runtimePluginRegistry = - input.runtimePluginSelections || !baseRegistry + purpose === "model-catalog" || input.runtimePluginSelections || !baseRegistry ? loadRuntimeRegistry( { - ...(input.loadRuntimePlugins - ? { basePluginIds: [] } - : baseRegistry - ? { basePluginIds: listRuntimePluginIdsFromRegistry(baseRegistry) } - : basePluginIds !== undefined - ? { basePluginIds } - : {}), + ...(purpose === "model-catalog" + ? { basePluginIds: basePluginIds ?? [] } + : input.loadRuntimePlugins + ? { basePluginIds: [] } + : baseRegistry + ? { basePluginIds: listRuntimePluginIdsFromRegistry(baseRegistry) } + : basePluginIds !== undefined + ? { basePluginIds } + : {}), ...(reusableGeneration?.pluginRegistry ? { reusableRegistry: reusableGeneration.pluginRegistry } : {}), @@ -196,6 +210,7 @@ export function prepareWorkspacePluginRegistries( ...(preferBuiltPluginArtifacts ? { preferBuiltPluginArtifacts: true } : {}), selections: input.runtimePluginSelections, configuredHarnessRuntimes: getConfiguredHarnessRuntimes?.(), + ...(purpose ? { purpose } : {}), }, (source) => { loadedPrimaryRegistry = diff --git a/src/agents/runtime-plugins.test.ts b/src/agents/runtime-plugins.test.ts index f4dfb8d738e6..16edaffe315f 100644 --- a/src/agents/runtime-plugins.test.ts +++ b/src/agents/runtime-plugins.test.ts @@ -154,6 +154,37 @@ describe("agent runtime plugin registries", () => { ); }); + it.each([false, true])( + "keeps catalog registries exact with broader reusable scope=%s", + (broader) => { + const reusableRegistry = createEmptyPluginRegistry(); + reusableRegistry.plugins.push(createPluginRecord({ id: "catalog-provider" })); + if (broader) { + reusableRegistry.plugins.push(createPluginRecord({ id: "memory-core" })); + } + const primaryRegistry = createEmptyPluginRegistry(); + primaryRegistry.plugins.push(createPluginRecord({ id: "catalog-provider" })); + hoisted.getActivePluginRegistry.mockReturnValue(createEmptyPluginRegistry()); + hoisted.loadPluginRegistryHandle.mockReturnValue(primaryRegistry); + hoisted.resolveAgentRuntimePluginLoadPlan.mockReturnValue({ + config: {}, + pluginIds: ["catalog-provider"], + }); + + const registry = loadAgentRuntimePluginRegistryHandle({ + config: {}, + basePluginIds: ["catalog-provider"], + reusableRegistry, + purpose: "model-catalog", + }); + + expect(registry).toBe(broader ? primaryRegistry : reusableRegistry); + expect(hoisted.loadPluginRegistryHandle).toHaveBeenCalledTimes(broader ? 1 : 0); + expect(hoisted.adoptRuntimeContextEngineRegistrations).not.toHaveBeenCalled(); + expect(hoisted.adoptRuntimeWidgetPresenterRegistrations).not.toHaveBeenCalled(); + }, + ); + it("uses harness runtimes prepared by the lifecycle batch", () => { const configuredHarnessRuntimes = ["codex"]; diff --git a/src/agents/runtime-plugins.ts b/src/agents/runtime-plugins.ts index 2b67faf52881..3faa78c9b5c6 100644 --- a/src/agents/runtime-plugins.ts +++ b/src/agents/runtime-plugins.ts @@ -36,6 +36,7 @@ import { resolveAgentRuntimePluginLoadPlan, resolveAgentRuntimePluginSelections, type AgentHarnessPluginSelection, + type RuntimePluginLoadPurpose, } from "./harness/runtime-plugin-load-plan.js"; type AgentRuntimePluginRegistryParams = { @@ -53,6 +54,7 @@ type AgentRuntimePluginRegistryParams = { /** Lifecycle-owned selection; standalone/direct generations stay source-default. */ preferBuiltPluginArtifacts?: boolean; metadataSnapshot?: PluginMetadataSnapshot; + purpose?: RuntimePluginLoadPurpose; }; function resolveAgentRuntimePluginRegistryLoad( @@ -86,10 +88,13 @@ function resolveAgentRuntimePluginRegistryLoad( // startup runtime plugin ids plus selected run owners bound the registry scope. const activePluginIds = listLoadedRuntimePluginIds(); const startupPluginIds = - params.basePluginIds ?? - (requestPluginRegistry - ? listRuntimePluginIdsFromRegistry(requestPluginRegistry) - : (metadataSnapshot.pluginIds ?? (activePluginIds.length > 0 ? activePluginIds : undefined))); + params.purpose === "model-catalog" + ? (params.basePluginIds ?? []) + : (params.basePluginIds ?? + (requestPluginRegistry + ? listRuntimePluginIdsFromRegistry(requestPluginRegistry) + : (metadataSnapshot.pluginIds ?? + (activePluginIds.length > 0 ? activePluginIds : undefined)))); const plan = resolveAgentRuntimePluginLoadPlan({ config: params.config, workspaceDir: workspaceDir ?? process.cwd(), @@ -97,9 +102,10 @@ function resolveAgentRuntimePluginRegistryLoad( selections: resolveAgentRuntimePluginSelections( params.config, params.selections ?? [], - params.configuredHarnessRuntimes, + params.purpose === "model-catalog" ? [] : params.configuredHarnessRuntimes, ), metadataSnapshot, + ...(params.purpose ? { purpose: params.purpose } : {}), }); return { ...loadOptions, @@ -119,19 +125,27 @@ function reusableAgentRuntimeRegistry( params: AgentRuntimePluginRegistryParams, loadOptions: PluginLoadOptions, ): PluginRegistry | undefined { + const pluginIds = loadOptions.onlyPluginIds; return params.reusableRegistry && - loadOptions.onlyPluginIds !== undefined && - registryContainsRuntimePluginIds(params.reusableRegistry, loadOptions.onlyPluginIds) + pluginIds !== undefined && + (params.purpose !== "model-catalog" || + listRuntimePluginIdsFromRegistry(params.reusableRegistry).every((pluginId) => + pluginIds.includes(pluginId), + )) && + registryContainsRuntimePluginIds(params.reusableRegistry, pluginIds) ? params.reusableRegistry : undefined; } -function adoptAgentRuntimeRegistrations(pluginRegistry: PluginRegistry): { +function adoptAgentRuntimeRegistrations( + pluginRegistry: PluginRegistry, + purpose: RuntimePluginLoadPurpose | undefined, +): { registry: PluginRegistry; donor?: PluginRegistry; } { const activeRegistry = getActivePluginRegistry(); - if (!activeRegistry) { + if (!activeRegistry || purpose === "model-catalog") { return { registry: pluginRegistry }; } const registry = bindPluginRegistryResourceOwner( @@ -167,7 +181,7 @@ export async function acquireAgentRuntimePluginRegistry( ? withPluginMetadataSnapshotScope(params.metadataSnapshot, acquire) : acquire()); try { - const { registry, donor } = adoptAgentRuntimeRegistrations(acquired.registry); + const { registry, donor } = adoptAgentRuntimeRegistrations(acquired.registry, params.purpose); const primaryResources = getPluginRegistryInspectionResources(acquired.registry); if (!primaryResources) { throw new Error("Acquired prepared registry has no registration resource owner"); @@ -218,7 +232,7 @@ export function loadAgentRuntimePluginRegistryHandle( : load(); // Media providers remain owned by this source when full-only donors require a copy. onPrimaryRegistry?.(pluginRegistry); - return adoptAgentRuntimeRegistrations(pluginRegistry).registry; + return adoptAgentRuntimeRegistrations(pluginRegistry, params.purpose).registry; } /** Binds a scoped plugin generation when a direct host has no Gateway owner. */ diff --git a/src/agents/sessions/agent-session-compaction.test.ts b/src/agents/sessions/agent-session-compaction.test.ts index a4c645eff5bf..e440608ae662 100644 --- a/src/agents/sessions/agent-session-compaction.test.ts +++ b/src/agents/sessions/agent-session-compaction.test.ts @@ -573,6 +573,7 @@ describe("AgentSession compaction", () => { await session.prompt("continue"); + expect(streamMocks.streamSimple).toHaveBeenCalledOnce(); const compactionEvents = onAgentEvent.mock.calls .map(([event]) => event) .filter((event) => event.stream === "compaction"); @@ -605,29 +606,15 @@ describe("AgentSession compaction", () => { ...createAssistant(testModel, [{ type: "text", text: "old answer" }]), timestamp: 2, }); - const handlers = createCompactionHandlers(); const syntheticError = new Error("synthetic manual cancellation rejection"); - const abortActiveCompaction = () => session.abortCompaction(); - handlers.set("session_before_compact", [ - async () => { - abortActiveCompaction(); - throw syntheticError; - }, - ]); streamMocks.streamSimple.mockImplementation( (_activeModel: Model, _context: Context, options?: SimpleStreamOptions) => { - if (options?.signal?.aborted) { - throw syntheticError; - } - return createAssistantResultStream( - createAssistant(testModel, [{ type: "text", text: "unexpected compaction" }]), - ); + expect(options?.signal?.aborted).toBe(false); + session.abortCompaction(); + throw syntheticError; }, ); - const { session } = await createTestSession({ - sessionManager, - resourceLoader: createResourceLoader(handlers), - }); + const { session } = await createTestSession({ sessionManager }); const onAgentEvent = vi.fn(); const subscription = subscribeEmbeddedAgentSession({ session, @@ -637,6 +624,7 @@ describe("AgentSession compaction", () => { await expect(session.compact()).rejects.toBe(syntheticError); + expect(streamMocks.streamSimple).toHaveBeenCalledOnce(); const compactionEvents = onAgentEvent.mock.calls .map(([event]) => event) .filter((event) => event.stream === "compaction"); diff --git a/src/agents/sessions/model-registry-runtime.ts b/src/agents/sessions/model-registry-runtime.ts index 95aae0c384dc..f998f1f1ba06 100644 --- a/src/agents/sessions/model-registry-runtime.ts +++ b/src/agents/sessions/model-registry-runtime.ts @@ -5,7 +5,6 @@ import { type LlmRuntime, } from "@openclaw/ai"; import { registerBuiltInApiProviders } from "@openclaw/ai/providers"; -import "../ai-transport-runtime-host.js"; import { bindStreamLlmRuntime } from "../../llm/model-runtime-binding.js"; import { runPluginStreamConsumer } from "../../plugins/plugin-instance-scope.js"; diff --git a/src/agents/sessions/sdk.ts b/src/agents/sessions/sdk.ts index 7046c165fa3f..7932381c98ab 100644 --- a/src/agents/sessions/sdk.ts +++ b/src/agents/sessions/sdk.ts @@ -455,6 +455,9 @@ async function createAgentSessionImpl( if (!auth.ok) { throw new Error(auth.error); } + // Isolated session streams bypass the process-default stream facade. + await import("../ai-transport-runtime-host.js"); + optionsLocal?.signal?.throwIfAborted(); const providerRetrySettings = settingsManager.getProviderRetrySettings(); const attributionHeaders = getAttributionHeaders(modelResult, settingsManager); return modelRegistryRuntime.llmRuntime.streamSimple(modelResult, context, { diff --git a/src/agents/simple-completion-execution.test.ts b/src/agents/simple-completion-execution.test.ts index ffb2e0efc4b9..6060efbff26c 100644 --- a/src/agents/simple-completion-execution.test.ts +++ b/src/agents/simple-completion-execution.test.ts @@ -1,6 +1,7 @@ import { reasoningTagTextPolicy } from "@openclaw/ai/internal/openai"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { findSourceImportBackedges } from "../../test/helpers/source-import-closure.js"; +import { bindModelCompletionOwner } from "../llm/model-runtime-binding.js"; import type { Model } from "../llm/types.js"; const mocks = vi.hoisted(() => ({ @@ -9,6 +10,7 @@ const mocks = vi.hoisted(() => ({ })); vi.mock("../llm/stream.js", () => ({ completeSimple: mocks.complete })); +vi.mock("./ai-transport-runtime-host.js", () => ({})); vi.mock("@openclaw/ai/transports", async (importOriginal) => ({ ...(await importOriginal()), prepareModelForSimpleCompletion: mocks.prepareModel, @@ -58,6 +60,29 @@ describe("prepared completion import boundary", () => { }); describe("completeWithPreparedSimpleCompletionModel", () => { + it("stops before transport preparation when its owner retires during host initialization", async () => { + const retired = new Error("Completion owner retired."); + let current = true; + const model = bindModelCompletionOwner(baseModel, { + run: (run) => run(), + assertCurrent: () => { + if (!current) { + throw retired; + } + }, + }); + const completion = completeWithPreparedSimpleCompletionModel({ + model, + auth: { apiKey: "test-key", source: "test", mode: "api-key" }, + context, + }); + current = false; + + await expect(completion).rejects.toBe(retired); + expect(mocks.prepareModel).not.toHaveBeenCalled(); + expect(mocks.complete).not.toHaveBeenCalled(); + }); + it.each([ "openai-completions", "openai-responses", diff --git a/src/agents/simple-completion-execution.ts b/src/agents/simple-completion-execution.ts index 92ccf150d1bd..302fb9899e41 100644 --- a/src/agents/simple-completion-execution.ts +++ b/src/agents/simple-completion-execution.ts @@ -70,6 +70,10 @@ export async function completeWithPreparedSimpleCompletionModel( } async function completePreparedModel(params: PreparedCompletionParams): Promise { + // Direct SDK calls prepare transport hooks before entering the stream facade. + await import("./ai-transport-runtime-host.js"); + params.assertCurrent?.(); + params.options?.signal?.throwIfAborted(); const runtime = getModelLlmRuntime(params.model); let completionModel = getModelCompletionTransport(params.model) ?? diff --git a/src/agents/simple-completion-runtime.ts b/src/agents/simple-completion-runtime.ts index 1de939bc5e70..b4700e5fb194 100644 --- a/src/agents/simple-completion-runtime.ts +++ b/src/agents/simple-completion-runtime.ts @@ -432,6 +432,9 @@ async function prepareSimpleCompletionModelCore( }) : fingerprintResolvedProviderAuth(auth) : undefined; + await import("./ai-transport-runtime-host.js"); + assertCurrent?.(); + params.signal?.throwIfAborted(); const modelRuntime = getModelRegistryRuntime(resolved.modelRegistry); const model = applySecretRefHeaderSentinels( applyLocalNoAuthHeaderOverride(resolvedModel, resolvedAuth), diff --git a/src/llm/stream.complete-host.test.ts b/src/llm/stream.complete-host.test.ts index 1cb44d970e8b..8c5cadc4f374 100644 --- a/src/llm/stream.complete-host.test.ts +++ b/src/llm/stream.complete-host.test.ts @@ -8,6 +8,7 @@ import type { } from "@openclaw/llm-core"; import { describe, expect, it, vi } from "vitest"; import { createZeroUsageFixture } from "../agents/test-helpers/usage-fixtures.js"; +import { attachModelProviderRuntimePluginHandle } from "../plugins/provider-hook-runtime.js"; import { bindModelLlmRuntime } from "./model-runtime-binding.js"; import { completeSimple } from "./stream.js"; import { createAssistantMessageEventStream } from "./utils/event-stream.js"; @@ -57,30 +58,51 @@ function createCompletionRuntime( stream: providerStream, streamSimple: providerStream, }); - return { model: bindModelLlmRuntime(model, runtime), message, providerStream }; + return { model: bindModelLlmRuntime(model, runtime), runtime, message, providerStream }; } describe("LLM completion transport host", () => { - it("installs runtime transport ports before a bare simple completion", async () => { + it("installs runtime transport ports before direct preparation and bare completion", async () => { const inertWrapper = getAiTransportHost().plugin.wrapSimpleCompletionStream; - const { model, message, providerStream } = createCompletionRuntime((runtimeModel, context) => { - const wrapper = getAiTransportHost().plugin.wrapSimpleCompletionStream; - expect(wrapper).not.toBe(inertWrapper); - expect( - wrapper({ - provider: runtimeModel.provider, - context: { - provider: runtimeModel.provider, - modelId: runtimeModel.id, - model: runtimeModel, - streamFn: providerStream, - }, - }), - ).toBeUndefined(); - expect(context.messages).toEqual([]); - }); + const { model, runtime, message, providerStream } = createCompletionRuntime( + (_runtimeModel, context) => { + expect(getAiTransportHost().plugin.wrapSimpleCompletionStream).not.toBe(inertWrapper); + expect(context.messages).toEqual([]); + }, + ); + const directModel = bindModelLlmRuntime( + attachModelProviderRuntimePluginHandle(model, { + provider: model.provider, + plugin: { + id: model.provider, + label: "Completion fixture", + auth: [], + wrapSimpleCompletionStreamFn: ({ streamFn }) => + streamFn && + ((target, context, options) => + streamFn(target, context, { + ...options, + headers: { ...options?.headers, "x-runtime-host": "prepared" }, + })), + }, + }), + runtime, + ); + const { completeWithPreparedSimpleCompletionModel } = + await import("../agents/simple-completion-execution.js"); + await expect( + completeWithPreparedSimpleCompletionModel({ + model: directModel, + auth: { apiKey: "fixture-key", source: "test", mode: "api-key" }, + context: { messages: [] }, + }), + ).resolves.toEqual(message); await expect(completeSimple(model, { messages: [] })).resolves.toEqual(message); + expect(providerStream.mock.calls.map((call) => call[2]?.headers)).toEqual([ + { "x-runtime-host": "prepared" }, + undefined, + ]); }); it.each(["current", "retired", "aborted"] as const)( diff --git a/src/plugins/plugin-module-generation.test.ts b/src/plugins/plugin-module-generation.test.ts index 7a71a720f7e3..3aa6b937d451 100644 --- a/src/plugins/plugin-module-generation.test.ts +++ b/src/plugins/plugin-module-generation.test.ts @@ -1,6 +1,6 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs"; -import { createRequire } from "node:module"; +import Module, { createRequire } from "node:module"; import path from "node:path"; import { createJiti } from "jiti"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -777,6 +777,34 @@ describe("plugin module generations", () => { }, ); + it("resolves deferred TypeScript without acquiring the compiler until execution", () => { + const root = temp.make("plugin-resolve-only-typescript-"); + fs.writeFileSync( + path.join(root, "index.cjs"), + "exports.resolve = () => require.resolve('./deferred.ts'); exports.read = () => require('./deferred.ts').value;", + ); + fs.writeFileSync(path.join(root, "deferred.ts"), "exports.value = 42 as number;"); + // oxlint-disable-next-line typescript/unbound-method -- called below with the intercepted module receiver. + const originalRequire = Module.prototype.require; + const compilerGuard = vi.spyOn(Module.prototype, "require").mockImplementation(function ( + this: NodeJS.Module, + id: string, + ) { + if (id === "typescript") { + throw new Error("Resolution must not acquire the TypeScript compiler"); + } + return originalRequire.call(this, id); + }); + let plugin: { resolve(): string; read(): number }; + try { + plugin = load(root, "index.cjs").value as typeof plugin; + expect(fs.existsSync(plugin.resolve())).toBe(true); + } finally { + compilerGuard.mockRestore(); + } + expect(plugin.read()).toBe(42); + }); + it("defers unused TypeScript syntax errors until their module is loaded", async () => { const root = temp.make("plugin-lazy-source-error-"); fs.writeFileSync( diff --git a/src/plugins/plugin-source-build.ts b/src/plugins/plugin-source-build.ts index b62343e45e26..790e6de61a5d 100644 --- a/src/plugins/plugin-source-build.ts +++ b/src/plugins/plugin-source-build.ts @@ -22,7 +22,6 @@ export type PluginSourceFile = { /** Compile captured source into a private namespace; native files stay with their capture owner. */ export function buildPluginTypeScriptSource(root: string) { - const ts: typeof TypeScript = require("typescript"); const directory = fs.mkdtempSync(path.join(path.dirname(root), ".source-")); const outputs = new Map(); const formats = new Map(); @@ -68,21 +67,22 @@ export function buildPluginTypeScriptSource(root: string) { for (const name of fs.readdirSync(root).toSorted()) { include(path.join(root, name)); } - const options: TypeScript.CompilerOptions = { - target: ts.ScriptTarget.ES2022, - module: ts.ModuleKind.NodeNext, - moduleResolution: ts.ModuleResolutionKind.NodeNext, - experimentalDecorators: true, - esModuleInterop: true, - jsx: jsx ? ts.JsxEmit.React : ts.JsxEmit.Preserve, - allowJs: true, - noResolve: true, - noLib: true, - types: [], - outDir: directory, - rootDir: root, - }; const compile = (input: string, destination: string, format: string | null | undefined) => { + const ts: typeof TypeScript = require("typescript"); + const options: TypeScript.CompilerOptions = { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.NodeNext, + moduleResolution: ts.ModuleResolutionKind.NodeNext, + experimentalDecorators: true, + esModuleInterop: true, + jsx: jsx ? ts.JsxEmit.React : ts.JsxEmit.Preserve, + allowJs: true, + noResolve: true, + noLib: true, + types: [], + outDir: directory, + rootDir: root, + }; // TypeScript uses slash-form filenames; native paths stay with filesystem operations. // Map Jiti's .mtsx/.ctsx extensions while retaining their explicit JSX grammar. const compilerInput = input.replaceAll("\\", "/").replace(/\.([cm])tsx$/, ".$1ts"); diff --git a/src/plugins/provider-model-compat.prepared.test.ts b/src/plugins/provider-model-compat.prepared.test.ts index 3623d48111fd..2aeed29cfc59 100644 --- a/src/plugins/provider-model-compat.prepared.test.ts +++ b/src/plugins/provider-model-compat.prepared.test.ts @@ -15,17 +15,6 @@ vi.mock("../agents/provider-attribution.js", () => ({ resolveProviderRequestCapabilities, })); -vi.mock("@openclaw/ai/transports", () => ({ - resolveOpenAICompletionsCompat: ( - model: Model, - resolveCapabilities: (input: Model) => { - supportsDeveloperRole: boolean; - supportsUsageInStreaming: boolean; - supportsStrictMode: boolean; - }, - ) => resolveCapabilities(model), -})); - import { normalizeModelCompat } from "./provider-model-compat.js"; function makeOwners(provider: string): PluginMetadataSnapshotOwnerMaps { diff --git a/src/plugins/provider-model-compat.ts b/src/plugins/provider-model-compat.ts index 5cf9941cc6d2..37534b8bc6b2 100644 --- a/src/plugins/provider-model-compat.ts +++ b/src/plugins/provider-model-compat.ts @@ -1,6 +1,6 @@ +import { resolveOpenAICompletionsCompat } from "@openclaw/ai/internal/openai-completions-compat"; // Normalizes provider model compatibility metadata from plugins. import { resolveUnsupportedToolSchemaKeywords } from "@openclaw/ai/internal/tool-schema"; -import { resolveOpenAICompletionsCompat } from "@openclaw/ai/transports"; import { resolveProviderRequestCapabilities } from "../agents/provider-attribution.js"; import { getModelProviderRequestRouteFacts } from "../agents/provider-request-config.js"; import type { ModelCompatConfig } from "../config/types.models.js"; diff --git a/tsconfig.json b/tsconfig.json index 77c654cd77f5..9f9c7dbca2f5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,6 +38,9 @@ "./packages/ai/src/internal/google-model-family.ts" ], "@openclaw/ai/internal/openai": ["./packages/ai/src/internal/openai.ts"], + "@openclaw/ai/internal/openai-completions-compat": [ + "./packages/ai/src/internal/openai-completions-compat.ts" + ], "@openclaw/ai/internal/openai-responses-payload-policy": [ "./packages/ai/src/internal/openai-responses-payload-policy.ts" ], diff --git a/tsdown.ai.config.ts b/tsdown.ai.config.ts index d095b7a5fd1e..71763e952c40 100644 --- a/tsdown.ai.config.ts +++ b/tsdown.ai.config.ts @@ -27,6 +27,7 @@ const config = { "internal/anthropic": "packages/ai/src/internal/anthropic.ts", "internal/google-model-family": "packages/ai/src/internal/google-model-family.ts", "internal/openai": "packages/ai/src/internal/openai.ts", + "internal/openai-completions-compat": "packages/ai/src/internal/openai-completions-compat.ts", "internal/openai-responses-payload-policy": "packages/ai/src/internal/openai-responses-payload-policy.ts", "internal/retry-after": "packages/ai/src/internal/retry-after.ts",