authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-14 04:19:23-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 04:19:23-07:00
log3062701c24ac6b6bde3d238fab8689e48ce2396b
treea7014e1f1b430ca5c190bc5bdb88a6eecb71ab28
parentc08093c2b136353bc755459febf3e8afd46bf50e
signature Signed by PGP key B5690EEEBB952194

fix(server): commit and title generation use your custom model when no text model is set (#19386)

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

4 files changed, 61 insertions(+), 3 deletions(-)

apps/server/src/serverSettings.test.ts+38
...@@ -813,6 +813,44 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -813,6 +813,44 @@ it.layer(NodeServices.layer)("server settings", (it) => {
813 }).pipe(Effect.provide(layerServerSettings())),813 }).pipe(Effect.provide(layerServerSettings())),
814 );814 );
815815
816 // A backend behind a custom model may refuse the built-in default (#12651).
817 it.effect("falls back to the thread default when it is a custom model on that instance", () =>
818 Effect.gen(function* () {
819 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
820 const claude = ProviderInstanceId.make("claudeAgent");
821 const providerInstances = {
822 [ProviderInstanceId.make("codex")]: {
823 driver: ProviderDriverKind.make("codex"),
824 enabled: false,
825 config: {},
826 },
827 [claude]: {
828 driver: ProviderDriverKind.make("claudeAgent"),
829 config: { customModels: ["z-ai/glm-5.3-flash"] },
830 },
831 };
832
833 const custom = yield* serverSettings.updateSettings({
834 providerInstances,
835 defaultModelSelection: {
836 instanceId: claude,
837 model: "z-ai/glm-5.3-flash",
838 options: [{ id: "thinking", value: false }],
839 },
840 });
841 assert.deepEqual(custom.textGenerationModelSelection, {
842 instanceId: claude,
843 model: "z-ai/glm-5.3-flash",
844 options: [{ id: "thinking", value: false }],
845 });
846
847 const builtIn = yield* serverSettings.updateSettings({
848 defaultModelSelection: { instanceId: claude, model: "claude-fable-5-1" },
849 });
850 assert.equal(builtIn.textGenerationModelSelection.model, "claude-haiku-5-5");
851 }).pipe(Effect.provide(layerServerSettings())),
852 );
853
816 it.effect("preserves enabled text generation selections for non-built-in drivers", () =>854 it.effect("preserves enabled text generation selections for non-built-in drivers", () =>
817 Effect.gen(function* () {855 Effect.gen(function* () {
818 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;856 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
apps/server/src/serverSettings.ts+1
...@@ -484,6 +484,7 @@ function fallbackTextGenerationProvider(settings: ServerSettings): ServerSetting...@@ -484,6 +484,7 @@ function fallbackTextGenerationProvider(settings: ServerSettings): ServerSetting
484 resolveProviderInstanceEnabled(484 resolveProviderInstanceEnabled(
485 settings.providerInstances[instanceId] ?? { driver, config: {} },485 settings.providerInstances[instanceId] ?? { driver, config: {} },
486 ),486 ),
487 { defaultModelSelection: settings.defaultModelSelection },
487 );488 );
488 return fallback === null ? settings : { ...settings, textGenerationModelSelection: fallback };489 return fallback === null ? settings : { ...settings, textGenerationModelSelection: fallback };
489}490}
packages/shared/src/projectSettings.ts+4-1
...@@ -199,7 +199,10 @@ function resolveProjectOverrides(...@@ -199,7 +199,10 @@ function resolveProjectOverrides(
199 const fallback = fallbackTextGenerationModelSelection(199 const fallback = fallbackTextGenerationModelSelection(
200 settings.providerInstances,200 settings.providerInstances,
201 (instanceId) => resolveProjectProviderInstanceEnabled(settings, projectId, instanceId),201 (instanceId) => resolveProjectProviderInstanceEnabled(settings, projectId, instanceId),
202 { includeConfiguredInstances: true },202 {
203 includeConfiguredInstances: true,
204 defaultModelSelection: effective.defaultModelSelection as ModelSelection | null,
205 },
203 );206 );
204 if (fallback !== null) effective.textGenerationModelSelection = fallback;207 if (fallback !== null) effective.textGenerationModelSelection = fallback;
205 }208 }
packages/shared/src/serverSettings.ts+18-2
...@@ -23,7 +23,7 @@ import * as Option from "effect/Option";...@@ -23,7 +23,7 @@ import * as Option from "effect/Option";
23import * as Schema from "effect/Schema";23import * as Schema from "effect/Schema";
24import { deepMerge } from "./Struct.ts";24import { deepMerge } from "./Struct.ts";
25import { fromLenientJson } from "./schemaJson.ts";25import { fromLenientJson } from "./schemaJson.ts";
26import { createModelSelection } from "./model.ts";26import { createModelSelection, readCustomModelEntries } from "./model.ts";
27import {27import {
28 getBackgroundActivityBaseProfile,28 getBackgroundActivityBaseProfile,
29 normalizeBackgroundActivitySettings,29 normalizeBackgroundActivitySettings,
...@@ -95,6 +95,11 @@ const TEXT_GENERATION_FALLBACK_DRIVERS = [...@@ -95,6 +95,11 @@ const TEXT_GENERATION_FALLBACK_DRIVERS = [
95 * `includeConfiguredInstances` is set, configured instances of those95 * `includeConfiguredInstances` is set, configured instances of those
96 * drivers. `null` when none is.96 * drivers. `null` when none is.
97 *97 *
98 * The model is the driver's cheap default, unless `defaultModelSelection`
99 * (the thread default) is one of that instance's custom models. A custom
100 * model usually means a backend that may refuse the built-in default, and
101 * the thread default, with its options, is known to work there.
102 *
98 * `includeConfiguredInstances` is for the project-scoped caller only: a103 * `includeConfiguredInstances` is for the project-scoped caller only: a
99 * project that disabled every built-in instance should still be able to104 * project that disabled every built-in instance should still be able to
100 * fall back to one it configured itself. The machine-level caller leaves it105 * fall back to one it configured itself. The machine-level caller leaves it
...@@ -104,7 +109,10 @@ const TEXT_GENERATION_FALLBACK_DRIVERS = [...@@ -104,7 +109,10 @@ const TEXT_GENERATION_FALLBACK_DRIVERS = [
104export function fallbackTextGenerationModelSelection(109export function fallbackTextGenerationModelSelection(
105 providerInstances: ServerSettings["providerInstances"],110 providerInstances: ServerSettings["providerInstances"],
106 isEnabled: (instanceId: ProviderInstanceId, driver: ProviderDriverKind) => boolean,111 isEnabled: (instanceId: ProviderInstanceId, driver: ProviderDriverKind) => boolean,
107 options?: { readonly includeConfiguredInstances?: boolean },112 options?: {
113 readonly includeConfiguredInstances?: boolean;
114 readonly defaultModelSelection?: ModelSelection | null;
115 },
108): ModelSelection | null {116): ModelSelection | null {
109 const candidates = [117 const candidates = [
110 ...TEXT_GENERATION_FALLBACK_DRIVERS.map(118 ...TEXT_GENERATION_FALLBACK_DRIVERS.map(
...@@ -121,6 +129,14 @@ export function fallbackTextGenerationModelSelection(...@@ -121,6 +129,14 @@ export function fallbackTextGenerationModelSelection(
121 const match = candidates.find(([instanceId, driver]) => isEnabled(instanceId, driver));129 const match = candidates.find(([instanceId, driver]) => isEnabled(instanceId, driver));
122 if (match === undefined) return null;130 if (match === undefined) return null;
123 const [instanceId, driver] = match;131 const [instanceId, driver] = match;
132 const threadDefault = options?.defaultModelSelection;
133 const config = providerInstances[instanceId]?.config as { customModels?: unknown } | undefined;
134 if (
135 threadDefault?.instanceId === instanceId &&
136 readCustomModelEntries(config?.customModels).some((entry) => entry.slug === threadDefault.model)
137 ) {
138 return threadDefault;
139 }
124 return {140 return {
125 instanceId,141 instanceId,
126 model:142 model: