authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-14 01:38:38-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 01:38:38-07:00
log19a67ef5211996a737d9f9fe608ab2c80f8a6714
tree9b03145054a05a8b594f7f43f6df1a36dd949e24
parenta0ea721c47f0b1ac923fbff1043d2994500ecf51
signature Signed by PGP key B5690EEEBB952194

fix(acp): custom models now reach the agent instead of failing to open (#19313)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

3 files changed, 50 insertions(+), 0 deletions(-)

apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts+37
......@@ -1752,4 +1752,41 @@ describe("AcpSessionRuntime", () => {
17521752 Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))),
17531753 );
17541754 });
1755
1756 it.effect("sends configured custom models the agent does not advertise", () => {
1757 const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "acp-runtime-"));
1758 const requestLogPath = NodePath.join(tempDir, "requests.ndjson");
1759 return Effect.gen(function* () {
1760 const runtime = yield* AcpSessionRuntime.AcpSessionRuntime;
1761 yield* runtime.start();
1762
1763 yield* runtime.setModel("cline-free/custom-model");
1764 const error = yield* runtime.setModel("unconfigured/model").pipe(Effect.flip);
1765 expect(error._tag).toBe("AcpRequestError");
1766
1767 const sentModels = NodeFS.readFileSync(requestLogPath, "utf8")
1768 .trim()
1769 .split("\n")
1770 .filter((line) => line.length > 0)
1771 .map((line) => JSON.parse(line) as { method?: string; params?: { value?: unknown } })
1772 .filter((message) => message.method === "session/set_config_option")
1773 .map((message) => message.params?.value);
1774 expect(sentModels).toEqual(["cline-free/custom-model"]);
1775 }).pipe(
1776 Effect.provide(
1777 AcpSessionRuntime.layer({
1778 ...mockRuntimeOptions,
1779 spawn: {
1780 command: mockAgentCommand,
1781 args: mockAgentArgs,
1782 env: { T3_ACP_REQUEST_LOG_PATH: requestLogPath },
1783 },
1784 customModels: ["cline-free/custom-model"],
1785 }),
1786 ),
1787 Effect.scoped,
1788 Effect.provide(NodeServices.layer),
1789 Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))),
1790 );
1791 });
17551792});
packages/provider-acp-registry/src/server/adapter.ts+4
......@@ -9,6 +9,7 @@ import {
99 makeCopilotSubagentRouting,
1010} from "./copilotAcp.ts";
1111import { defaultInstanceIdForDriver, ProviderDriverKind } from "@t3tools/contracts";
12import { readCustomModelEntries } from "@t3tools/shared/model";
1213import { AcpRegistrySettings } from "../settings.ts";
1314import * as HostProcess from "@t3tools/shared/HostProcess";
1415import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime";
......@@ -168,6 +169,9 @@ function makeAcpRegistryRuntime(
168169 env: { ...resolved.spawn.env, ...processEnvironment },
169170 },
170171 ...(options.settings.authMethodId ? { authMethodId: options.settings.authMethodId } : {}),
172 customModels: readCustomModelEntries(options.settings.customModels).map(
173 (entry) => entry.slug,
174 ),
171175 }),
172176 );
173177 return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(
packages/provider-acp/src/server/AcpSessionRuntime.ts+9
......@@ -155,6 +155,12 @@ export interface AcpSessionRuntimeOptions {
155155 readonly onTermination?: (error: EffectAcpErrors.AcpError) => Effect.Effect<void>;
156156 readonly onOutgoingResponseFailure?: EffectAcpClient.AcpClientOptions["onOutgoingResponseFailure"];
157157 readonly onOutgoingResponse?: EffectAcpClient.AcpClientOptions["onOutgoingResponse"];
158 /**
159 * Model ids the user configured for this instance. The model config option
160 * accepts them even when the agent does not advertise them, and the agent
161 * validates them instead.
162 */
163 readonly customModels?: ReadonlyArray<string>;
158164}
159165
160166export interface AcpSessionRequestLogEvent {
......@@ -1977,6 +1983,9 @@ export const make = (
19771983 if (allowedValues.includes(value)) {
19781984 return;
19791985 }
1986 if (configOption.category === "model" && options.customModels?.includes(value)) {
1987 return;
1988 }
19801989 return yield* new EffectAcpErrors.AcpRequestError({
19811990 code: -32602,
19821991 errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`,