Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/chat/test/platform-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
51 changes: 50 additions & 1 deletion packages/folded-runs/src/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string>,
): 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
Expand Down Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions packages/folded-runs/test/launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 };
Expand Down
31 changes: 22 additions & 9 deletions packages/hub-client/src/credential-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
13 changes: 13 additions & 0 deletions packages/tasks/test/launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading