From fe02c0d49f03036612645cc6f898f9f429c0338d Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 21 Aug 2026 20:14:26 -0700 Subject: [PATCH 1/4] Add tests for the Ollama adapter-key correction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CL-6586: an Ollama-backed offering's InferenceSource.provider was "openai-compatible" (its accurate wire format), but the sidecar's adapter registry only recognizes a locally-served source under the key "ollama" — the built-in OpenAI adapter served the request instead and rejected the offering's quirks.default bag outright. Covers the not-yet-existing withOllamaAdapterKey correction in deployAtHead: a catalog-resolved Ollama source should get its provider field corrected before it's pinned into a run's config; a caller-supplied sources override should be left untouched. --- packages/folded-runs/test/launch.test.ts | 86 ++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/packages/folded-runs/test/launch.test.ts b/packages/folded-runs/test/launch.test.ts index 66f5d57a..c5bd966f 100644 --- a/packages/folded-runs/test/launch.test.ts +++ b/packages/folded-runs/test/launch.test.ts @@ -68,12 +68,28 @@ let buildCredentialDeliveryResult: BuildCredentialDeliveryResult = { }; const buildCredentialDeliveryCalls: unknown[] = []; +// `deployAtHead` consults `listVisibleOfferings` once per catalog-resolved +// launch, to correct a source's adapter-registry key when its offering's +// provider is actually named "ollama" (`withOllamaAdapterKey`). Only the +// two fields that decision reads are given here; a real `ResolvedOffering` +// carries far more, none of which this fix touches. +type FakeResolvedOffering = { + offering: { id: string }; + provider: { name: string }; +}; +let listVisibleOfferingsResult: FakeResolvedOffering[] = []; +const listVisibleOfferingsCalls: unknown[] = []; + mock.module("@intx/db", () => ({ ...actualDb, buildCredentialDelivery: async (...args: unknown[]) => { buildCredentialDeliveryCalls.push(args[0]); return buildCredentialDeliveryResult; }, + listVisibleOfferings: async (...args: unknown[]) => { + listVisibleOfferingsCalls.push(args); + return listVisibleOfferingsResult; + }, })); const { @@ -535,6 +551,74 @@ describe("launchFoldedRun", () => { }); }); + // CL-6586: an Ollama-backed offering resolves with `provider: + // "openai-compatible"` — the accurate wire format — but the sidecar's + // adapter registry only recognizes a locally-served source under the + // key "ollama" (`apps/sidecar/src/config.ts`). Left uncorrected, the + // built-in OpenAI adapter serves the request instead and rejects the + // offering's `quirks.default` bag. `deployAtHead` must fix the key up + // before the source reaches the deployed config. + test("corrects the adapter key to \"ollama\" for a catalog-resolved Ollama offering", async () => { + resolveDefinitionSourcesCalls.length = 0; + resolveDefinitionSourcesResult = { + ok: true, + sources: [ + { + id: "off_ollama", + provider: "openai-compatible", + baseURL: "https://home-mac-studio.tail87f5aa.ts.net/v1", + apiKey: "placeholder", + model: "gpt-oss:20b", + quirks: { default: { numCtx: 131_072, maxOutputTokens: 32_768 } }, + }, + ], + defaultSource: "off_ollama", + }; + listVisibleOfferingsResult = [ + { offering: { id: "off_ollama" }, provider: { name: "ollama" } }, + ]; + listVisibleOfferingsCalls.length = 0; + + const db = createFakeDb(); + const sessionService = createFakeSessionService(); + const eventCollectors = createFakeEventCollectors(); + + await launchFoldedRun( + { + db: db as never, + sessionService, + assetService: createFakeAssetService(), + sidecarRouter: createFakeSidecarRouter(), + toolGrantsForPins: () => [], + eventCollectors, + }, + { + tenantId: "ten_1", + instanceId: "ins_workbench1", + triggerAddress: "ins_workbench1@ten1.workbench.test", + definitionId: "wfd_workbench1", + foldedBody: FOLDED_BODY, + launchLabel: "the workbench host", + }, + ); + + expect(listVisibleOfferingsCalls).toEqual([[db, "ten_1"]]); + const deployed = onlyCall(sessionService.adoptedDeployCalls); + expect(deployed.config.sources).toEqual([ + { + id: "off_ollama", + provider: "ollama", + baseURL: "https://home-mac-studio.tail87f5aa.ts.net/v1", + apiKey: "placeholder", + model: "gpt-oss:20b", + quirks: { default: { numCtx: 131_072, maxOutputTokens: 32_768 } }, + }, + ]); + + // Reset for every test after this one. + listVisibleOfferingsResult = []; + }); + // CL-6164: the step's default input selector (`{ from: // "trigger.payload" }`) reads the triggering mail's bare `content` // verbatim and feeds it straight into `agent.send`, which throws on an @@ -1011,6 +1095,7 @@ describe("launchFoldedRun", () => { ok: false, message: "the catalog must not be consulted when an override is given", }; + listVisibleOfferingsCalls.length = 0; const db = createFakeDb(); const sessionService = createFakeSessionService(); @@ -1051,6 +1136,7 @@ describe("launchFoldedRun", () => { expect(result.sessionId).toBeTruthy(); expect(resolveDefinitionSourcesCalls).toHaveLength(0); + expect(listVisibleOfferingsCalls).toHaveLength(0); expect(sessionService.adoptedDeployCalls).toHaveLength(1); const deployed = sessionService.adoptedDeployCalls[0] as { config: { sources: unknown[]; defaultSource: string }; From a8b536f207692f6ef3fed4be4406c95a33dca007 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 21 Aug 2026 20:14:35 -0700 Subject: [PATCH 2/4] Correct the adapter-registry key for a launched Ollama source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveDefinitionSources sets InferenceSource.provider to the winning offering's catalog plugin — accurate as the wire format (openai-compatible for Ollama, since @corbits/ollama-adapter wraps createOpenAIAdapter unmodified) but wrong as the sidecar's adapter-registry dispatch key, which recognizes a locally-served offering only under "ollama" (apps/sidecar/src/config.ts). Left uncorrected, an Ollama source resolved to the built-in OpenAI adapter, whose stricter quirks schema rejects the offering's "default" bag outright. packages/folded-runs/src/launch.ts's deployAtHead is the one place in workbench a catalog-resolved InferenceSource[] becomes a run's pinned config — the only caller of resolveDefinitionSources in the repo. withOllamaAdapterKey corrects the provider field there for any offering whose catalog provider is actually named "ollama", leaving plugin (and everything else) untouched. No vendored file changes and ModelProviderPlugin stays a 4-value enum: provider is a free string, so this needs nothing upstream. --- packages/folded-runs/src/launch.ts | 51 +++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/folded-runs/src/launch.ts b/packages/folded-runs/src/launch.ts index d6cfae3b..1d6f0e55 100644 --- a/packages/folded-runs/src/launch.ts +++ b/packages/folded-runs/src/launch.ts @@ -14,7 +14,7 @@ import { eq } from "drizzle-orm"; import { type } from "arktype"; import type { DBExecutor } from "@intx/db"; -import { buildCredentialDelivery } from "@intx/db"; +import { buildCredentialDelivery, listVisibleOfferings } from "@intx/db"; import type { CredentialBinding } from "@intx/types"; import { agentSession, @@ -98,6 +98,39 @@ export function parseSourcesOverride( */ export type FoldedRunMode = AgentRuntimeConfig["mode"]; +/** + * `resolveDefinitionSources` sets `InferenceSource.provider` to the + * winning offering's catalog `plugin` — accurate as the wire format + * (`openai-compatible` for Ollama, since `@corbits/ollama-adapter` wraps + * `createOpenAIAdapter` unmodified) but wrong as an adapter-registry key: + * the sidecar's registry dispatches a locally-served offering through + * `@corbits/ollama-adapter` under the key `"ollama"` + * (`apps/sidecar/src/config.ts`), which a `plugin`-only source can never + * name. Left uncorrected, an Ollama source resolves to the built-in + * OpenAI adapter, whose stricter `quirks` schema rejects the offering's + * `default` bag outright. `plugin` and this dispatch key are deliberately + * different concepts — the DB `plugin` column stays `openai-compatible` + * (`ModelProviderPlugin` has no `"ollama"` member, nor should it); this + * only corrects the in-flight `provider` field a launch pins into its + * run config. + * + * `ollamaOfferingIds` names every offering whose provider row is + * literally `"ollama"` (`@corbits/hub-client`'s `CATALOG_SEEDS.ollama`) — + * the same identity `quirksForDeployment` keys its own override on + * (`@corbits/inference-catalog`'s `ollama-context-defaults.ts`). A + * source whose id names none of them is returned unchanged. + */ +export function withOllamaAdapterKey( + sources: readonly InferenceSource[], + ollamaOfferingIds: ReadonlySet, +): InferenceSource[] { + return sources.map((source) => + ollamaOfferingIds.has(source.id) + ? { ...source, provider: "ollama" } + : source, + ); +} + /** * The ref a folded run's per-run workflow source tree is committed to * inside its definition asset. Per-run rather than the asset's default @@ -302,6 +335,22 @@ export async function deployAtHead( throw new InferenceResolutionError(params.launchLabel, resolution.message); } + // A caller-supplied override already states the adapter key it wants + // (see `SourcesOverride`'s doc) — only a catalog-resolved chain needs + // its `provider` field corrected for Ollama offerings. + if (sourcesOverride === undefined) { + const offerings = await listVisibleOfferings(deps.db, params.tenantId); + const ollamaOfferingIds = new Set( + offerings + .filter((resolved) => resolved.provider.name === "ollama") + .map((resolved) => resolved.offering.id), + ); + resolution.sources = withOllamaAdapterKey( + resolution.sources, + ollamaOfferingIds, + ); + } + // `create` replaces any collector already registered for this // address (see `EventCollectorRegistry.create`), so this is // idempotent whether this is a fresh launch or a wake of an instance From 512d274d887b66884cc55b2a2f3894bdc8bf299b Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 21 Aug 2026 20:14:41 -0700 Subject: [PATCH 3/4] Update docs: AdapterPluginId is a wire format, not a dispatch key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The doc comment stated the "plugin must be openai-compatible, never a provider's own id" rule as absolute — true for every OpenAI- compatible provider but Ollama, whose registered custom adapter needs the dispatch key "ollama" (CL-6586). Names the correction site (withOllamaAdapterKey) so the next reader doesn't rediscover the same gap. --- packages/hub-client/src/credential-test.ts | 31 +++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/hub-client/src/credential-test.ts b/packages/hub-client/src/credential-test.ts index f7e2c087..8dd9f483 100644 --- a/packages/hub-client/src/credential-test.ts +++ b/packages/hub-client/src/credential-test.ts @@ -31,15 +31,28 @@ export type SupportedCredentialProvider = | "ollama"; /** - * The inference adapter (`@intx/inference`'s runtime provider registry, - * mirrored by `@intx/types`' `ModelProviderPlugin`) that actually serves a - * credential's requests. Deliberately narrower than - * `SupportedCredentialProvider`: OpenRouter, Opencode Zen, Groq, DeepSeek, - * Mistral, and Hugging Face each get their own credential-test probe and onboarding - * card, but at deploy time they all ride the same OpenAI-compatible wire - * shape, so their `ModelSource.provider` and catalog `plugin` value must be - * `"openai-compatible"` — the registry key `byProvider.get(source.provider)` - * resolves against — never their own provider id. + * The wire format (`@intx/inference`'s built-in adapter shape, mirrored by + * `@intx/types`' `ModelProviderPlugin`) that actually serves a credential's + * requests. Deliberately narrower than `SupportedCredentialProvider`: + * OpenRouter, Opencode Zen, Groq, DeepSeek, Mistral, Hugging Face, and + * Ollama each get their own credential-test probe and onboarding card, but + * at deploy time they all ride the same OpenAI-compatible wire shape, so + * their catalog `plugin` value must be `"openai-compatible"` — never their + * own provider id — and `ModelProviderPlugin` gets no wider for them. + * + * This is NOT the same string as the registry key + * `byProvider.get(source.provider)` resolves against (CL-6586). For every + * provider above but Ollama the two happen to be equal, because the + * built-in `"openai-compatible"` adapter is exactly what serves them. Ollama + * is the one exception: it needs `@corbits/ollama-adapter`'s custom factory + * (registered under the key `"ollama"`, `apps/sidecar/src/config.ts`) so an + * offering's `quirks.numCtx` actually reaches `options.num_ctx` — the + * built-in adapter's stricter `quirks` schema rejects that shape outright. + * `packages/folded-runs/src/launch.ts`'s `withOllamaAdapterKey` is the one + * place that correction happens: it leaves `plugin`/`ModelSource.provider` + * as the accurate `"openai-compatible"` wire format and only rewrites the + * launched `InferenceSource.provider` — the actual registry-dispatch + * field — for an offering whose catalog provider is named `"ollama"`. */ export type AdapterPluginId = "anthropic" | "openai" | "openai-compatible" | "google-genai"; From f5e07f9560d846f6334cfa2ae3af28e621ade5a0 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 21 Aug 2026 20:31:11 -0700 Subject: [PATCH 4/4] Fix fixture ripple: mock listVisibleOfferings in launcher/chat tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CL-6586's withOllamaAdapterKey (deployAtHead) calls the real listVisibleOfferings to find Ollama-backed offerings. packages/tasks and packages/chat mock @intx/hub-api's resolveDefinitionSources but never mocked @intx/db, so the real listVisibleOfferings ran against their hand-rolled fakes and crashed on db.query.model.findMany, which those fakes never implement. Mocks @intx/db the same way packages/folded-runs/test/launch.test.ts already does, returning no offerings — none of these fixtures resolve against Ollama, so nothing here should ever need its provider field corrected. The production path stays strict: a missing offering lookup is not tolerated, only faked out in tests that don't exercise it. Also fixes a prettier quote-style nit in launch.test.ts. --- packages/chat/test/platform-adapter.test.ts | 13 +++++++++++++ packages/folded-runs/test/launch.test.ts | 2 +- packages/tasks/test/launcher.test.ts | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/chat/test/platform-adapter.test.ts b/packages/chat/test/platform-adapter.test.ts index e8030b21..47ba651a 100644 --- a/packages/chat/test/platform-adapter.test.ts +++ b/packages/chat/test/platform-adapter.test.ts @@ -72,6 +72,19 @@ mock.module("@intx/hub-api", () => ({ }, })); +// `deployAtHead` (reached through `launchFoldedRun`/`launchInvite`) looks +// up `listVisibleOfferings` once per launch to correct an Ollama +// offering's adapter-registry key (CL-6586's `withOllamaAdapterKey`) -- +// the real implementation walks a drizzle `db.query` surface this file's +// minimal chainable fake `db` never implements. None of these fixtures +// resolve against an Ollama offering, so an empty list is the correct +// fake: nothing here should ever need its `provider` field corrected. +const actualDb = await import("@intx/db"); +mock.module("@intx/db", () => ({ + ...actualDb, + listVisibleOfferings: async () => [], +})); + const { createHubChatPlatform } = await import("../src/platform-adapter"); type SelectChain = { diff --git a/packages/folded-runs/test/launch.test.ts b/packages/folded-runs/test/launch.test.ts index c5bd966f..8b7f8fc5 100644 --- a/packages/folded-runs/test/launch.test.ts +++ b/packages/folded-runs/test/launch.test.ts @@ -558,7 +558,7 @@ describe("launchFoldedRun", () => { // built-in OpenAI adapter serves the request instead and rejects the // offering's `quirks.default` bag. `deployAtHead` must fix the key up // before the source reaches the deployed config. - test("corrects the adapter key to \"ollama\" for a catalog-resolved Ollama offering", async () => { + test('corrects the adapter key to "ollama" for a catalog-resolved Ollama offering', async () => { resolveDefinitionSourcesCalls.length = 0; resolveDefinitionSourcesResult = { ok: true, diff --git a/packages/tasks/test/launcher.test.ts b/packages/tasks/test/launcher.test.ts index 8851ab0f..f07aa211 100644 --- a/packages/tasks/test/launcher.test.ts +++ b/packages/tasks/test/launcher.test.ts @@ -45,6 +45,19 @@ mock.module("@intx/hub-api", () => ({ }, })); +// `deployAtHead` (reached through `launchRun` -> `launchFoldedRun`) looks +// up `listVisibleOfferings` once per launch to correct an Ollama +// offering's adapter-registry key (CL-6586's `withOllamaAdapterKey`) -- +// the real implementation walks a drizzle `db.query` surface this file's +// hand-rolled `createFakeDb` never implements. None of these fixtures +// resolve against an Ollama offering, so an empty list is the correct +// fake: nothing here should ever need its `provider` field corrected. +const actualDb = await import("@intx/db"); +mock.module("@intx/db", () => ({ + ...actualDb, + listVisibleOfferings: async () => [], +})); + const { launchTask, launchTaskLeg,