From 19a67ef5211996a737d9f9fe608ab2c80f8a6714 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 14 Oct 2026 01:38:38 -0700 Subject: [PATCH] fix(acp): custom models now reach the agent instead of failing to open (#19313) Co-authored-by: Claude Opus 5.5 --- .../provider/acp/AcpJsonRpcConnection.test.ts | 37 +++++++++++++++++++ .../src/server/adapter.ts | 4 ++ .../src/server/AcpSessionRuntime.ts | 9 +++++ 3 files changed, 50 insertions(+) diff --git a/apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts b/apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts index b11fb5baec3a0a391c5e9ca33a3c19fb93237f46..14ff363da47b80f247f6729e597096975784ea37 100644 --- a/apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts +++ b/apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts @@ -1752,4 +1752,41 @@ describe("AcpSessionRuntime", () => { Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))), ); }); + + it.effect("sends configured custom models the agent does not advertise", () => { + const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "acp-runtime-")); + const requestLogPath = NodePath.join(tempDir, "requests.ndjson"); + return Effect.gen(function* () { + const runtime = yield* AcpSessionRuntime.AcpSessionRuntime; + yield* runtime.start(); + + yield* runtime.setModel("cline-free/custom-model"); + const error = yield* runtime.setModel("unconfigured/model").pipe(Effect.flip); + expect(error._tag).toBe("AcpRequestError"); + + const sentModels = NodeFS.readFileSync(requestLogPath, "utf8") + .trim() + .split("\n") + .filter((line) => line.length > 0) + .map((line) => JSON.parse(line) as { method?: string; params?: { value?: unknown } }) + .filter((message) => message.method === "session/set_config_option") + .map((message) => message.params?.value); + expect(sentModels).toEqual(["cline-free/custom-model"]); + }).pipe( + Effect.provide( + AcpSessionRuntime.layer({ + ...mockRuntimeOptions, + spawn: { + command: mockAgentCommand, + args: mockAgentArgs, + env: { T3_ACP_REQUEST_LOG_PATH: requestLogPath }, + }, + customModels: ["cline-free/custom-model"], + }), + ), + Effect.scoped, + Effect.provide(NodeServices.layer), + Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))), + ); + }); }); diff --git a/packages/provider-acp-registry/src/server/adapter.ts b/packages/provider-acp-registry/src/server/adapter.ts index 0fa9a83d05c78fa5d57447495f18823e3a9016a5..4cc847a9931eeda19a52e7c29ac74a4fbd8b8bfe 100644 --- a/packages/provider-acp-registry/src/server/adapter.ts +++ b/packages/provider-acp-registry/src/server/adapter.ts @@ -9,6 +9,7 @@ import { makeCopilotSubagentRouting, } from "./copilotAcp.ts"; import { defaultInstanceIdForDriver, ProviderDriverKind } from "@t3tools/contracts"; +import { readCustomModelEntries } from "@t3tools/shared/model"; import { AcpRegistrySettings } from "../settings.ts"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime"; @@ -168,6 +169,9 @@ function makeAcpRegistryRuntime( env: { ...resolved.spawn.env, ...processEnvironment }, }, ...(options.settings.authMethodId ? { authMethodId: options.settings.authMethodId } : {}), + customModels: readCustomModelEntries(options.settings.customModels).map( + (entry) => entry.slug, + ), }), ); return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe( diff --git a/packages/provider-acp/src/server/AcpSessionRuntime.ts b/packages/provider-acp/src/server/AcpSessionRuntime.ts index 3ae4f8da03e650d888003c2663cc0a9be1c79262..25578f151b3634f91beb9f5bef34eb122388106f 100644 --- a/packages/provider-acp/src/server/AcpSessionRuntime.ts +++ b/packages/provider-acp/src/server/AcpSessionRuntime.ts @@ -155,6 +155,12 @@ export interface AcpSessionRuntimeOptions { readonly onTermination?: (error: EffectAcpErrors.AcpError) => Effect.Effect; readonly onOutgoingResponseFailure?: EffectAcpClient.AcpClientOptions["onOutgoingResponseFailure"]; readonly onOutgoingResponse?: EffectAcpClient.AcpClientOptions["onOutgoingResponse"]; + /** + * Model ids the user configured for this instance. The model config option + * accepts them even when the agent does not advertise them, and the agent + * validates them instead. + */ + readonly customModels?: ReadonlyArray; } export interface AcpSessionRequestLogEvent { @@ -1977,6 +1983,9 @@ export const make = ( if (allowedValues.includes(value)) { return; } + if (configOption.category === "model" && options.customModels?.includes(value)) { + return; + } return yield* new EffectAcpErrors.AcpRequestError({ code: -32602, errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`, -- 2.54.0