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
8 changes: 0 additions & 8 deletions apps/web/src/domains/settings/ai/ai-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,6 @@ export type DaemonConfigPatch = {
};
};

export interface DaemonConfigReconciliation {
inferenceProvider?: string;
selectedModel?: string;
webSearchMode?: ServiceMode;
webSearchProvider?: string;
imageGenMode?: ServiceMode;
}

export interface InferenceTokenBudgetState {
maxOutputTokens: number;
maxOutputTouched: boolean;
Expand Down
24 changes: 0 additions & 24 deletions apps/web/src/domains/settings/ai/ai-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ import type {
CallSiteOverrideDraft,
DaemonConfig,
DaemonConfigPatch,
DaemonConfigReconciliation,
InferenceTokenBudgetState,
ProfileEntry,
ProfileWithName,
ServiceMode,
} from "@/domains/settings/ai/ai-types";
import { TOKEN_SLIDER_MIN_TOKENS } from "@/domains/settings/ai/ai-types";

Expand Down Expand Up @@ -47,28 +45,6 @@ export function assertProvisionSuccess(result: unknown): void {
}
}

export function reconcileFromDaemonConfig(config: DaemonConfig): DaemonConfigReconciliation {
const services = config.services ?? {};
const llm = config.llm ?? {};
const result: DaemonConfigReconciliation = {};

const provider = llm.default?.provider;
if (provider) result.inferenceProvider = provider;

const model = llm.default?.model;
if (model) result.selectedModel = model;

const wsMode = services["web-search"]?.mode;
if (wsMode === "managed" || wsMode === "your-own") result.webSearchMode = wsMode as ServiceMode;
const wsProvider = services["web-search"]?.provider;
if (wsProvider) result.webSearchProvider = wsProvider;

const igMode = services["image-generation"]?.mode;
if (igMode === "managed" || igMode === "your-own") result.imageGenMode = igMode as ServiceMode;

return result;
}

export function clampTokenBudget(
value: number,
max: number,
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/domains/settings/ai/image-generation-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
LS_IMAGE_GEN_MODE,
LS_IMAGE_GEN_MODEL,
} from "@/domains/settings/ai/ai-types";
import { reconcileFromDaemonConfig } from "@/domains/settings/ai/ai-utils";

import { ServiceCard, SaveButton, ResetButton } from "@/domains/settings/ai/ai-shared-ui";
import { useAssistantId, useDaemonConfigQuery, useDaemonConfigMutation, useProvisionProviderKey } from "@/domains/settings/ai/use-daemon-config";
import { useDraftOverride } from "@/domains/settings/ai/use-draft-override";
Expand All @@ -36,8 +36,8 @@ export function ImageGenerationCard() {
// Updates automatically when the cache refreshes.
const serverImageGenMode = useMemo<ServiceMode>(() => {
if (!daemonConfig) return getLocalSetting(LS_IMAGE_GEN_MODE, "your-own") as ServiceMode;
const reconciled = reconcileFromDaemonConfig(daemonConfig);
return reconciled.imageGenMode ?? (getLocalSetting(LS_IMAGE_GEN_MODE, "your-own") as ServiceMode);
const mode = daemonConfig.services?.["image-generation"]?.mode;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Image generation mode now accepts invalid raw config values without validation

serverImageGenMode now directly casts daemonConfig.services["image-generation"].mode to ServiceMode, so any non-enum string in raw config is treated as a valid UI mode. The daemon’s GET /v1/config response is based on raw config (loadRawConfig) rather than schema-validated config, and PATCH /v1/config accepts arbitrary objects via deep-merge, so malformed/stale mode values can exist and be returned (assistant/src/runtime/routes/conversation-query-routes.ts:447-452, assistant/src/config/loader.ts:875-916, assistant/src/runtime/routes/conversation-query-routes.ts:586-603). This regresses the previous guard logic and can leave the segment control in an invalid state and allow re-saving the invalid mode value.

Suggested change
const mode = daemonConfig.services?.["image-generation"]?.mode;
return ((daemonConfig.services?.["image-generation"]?.mode === "managed" || daemonConfig.services?.["image-generation"]?.mode === "your-own") ? daemonConfig.services["image-generation"]?.mode : getLocalSetting(LS_IMAGE_GEN_MODE, "your-own")) as ServiceMode;
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in b5d1110 — the mode validation guard was restored in the previous commit.

return (mode === "managed" || mode === "your-own" ? mode : getLocalSetting(LS_IMAGE_GEN_MODE, "your-own")) as ServiceMode;
}, [daemonConfig]);

const [imageGenMode, setDraftImageGenMode] = useDraftOverride(serverImageGenMode);
Expand Down
9 changes: 5 additions & 4 deletions apps/web/src/domains/settings/ai/web-search-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { secretPlaceholder } from "@/domains/settings/ai/secret-placeholder";

import type { ServiceMode } from "@/domains/settings/ai/ai-types";
import { LS_WEB_SEARCH_MODE, LS_WEB_SEARCH_PROVIDER } from "@/domains/settings/ai/ai-types";
import { getWebSearchProviderKeyStorage, reconcileFromDaemonConfig } from "@/domains/settings/ai/ai-utils";
import { getWebSearchProviderKeyStorage } from "@/domains/settings/ai/ai-utils";
import { ServiceCard, SaveButton, ResetButton } from "@/domains/settings/ai/ai-shared-ui";
import { useDaemonConfigQuery, useDaemonConfigMutation, useProvisionProviderKey } from "@/domains/settings/ai/use-daemon-config";
import { useDraftOverride } from "@/domains/settings/ai/use-draft-override";
Expand All @@ -46,10 +46,11 @@ export function WebSearchCard() {
serverWebSearchProvider: getLocalSetting(LS_WEB_SEARCH_PROVIDER, "inference-provider-native"),
};
}
const reconciled = reconcileFromDaemonConfig(daemonConfig);
const wsService = daemonConfig.services?.["web-search"];
const mode = wsService?.mode;
return {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Web search mode now uses unchecked raw config values

serverWebSearchMode now uses wsService?.mode with a direct cast to ServiceMode instead of validating that the value is actually "managed" | "your-own". Because /v1/config returns raw file contents and config patching does not enforce schema-level validation on incoming partials, invalid mode strings can reach this component (assistant/src/runtime/routes/conversation-query-routes.ts:447-452, assistant/src/config/loader.ts:875-916, assistant/src/runtime/routes/conversation-query-routes.ts:586-603). That can produce a mode value that is not one of the segment options and can be re-submitted back to config, propagating bad state.

Suggested change
return {
serverWebSearchMode: ((wsService?.mode === "managed" || wsService?.mode === "your-own") ? wsService.mode : getLocalSetting(LS_WEB_SEARCH_MODE, "your-own")) as ServiceMode,
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in b5d1110 — both cards now validate mode === "managed" || mode === "your-own" before using the daemon value, matching the original guard logic.

serverWebSearchMode: (reconciled.webSearchMode ?? getLocalSetting(LS_WEB_SEARCH_MODE, "your-own")) as ServiceMode,
serverWebSearchProvider: reconciled.webSearchProvider ?? getLocalSetting(LS_WEB_SEARCH_PROVIDER, "inference-provider-native"),
serverWebSearchMode: (mode === "managed" || mode === "your-own" ? mode : getLocalSetting(LS_WEB_SEARCH_MODE, "your-own")) as ServiceMode,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Provider fallback semantic change depends on whether empty-string providers can still exist

serverWebSearchProvider now uses nullish fallback (wsService?.provider ?? ...) (apps/web/src/domains/settings/ai/web-search-card.tsx:52), whereas the removed helper only propagated truthy providers. I did not mark this as a confirmed bug because validated configs should keep provider as a non-empty enum, but /v1/config is sourced from raw config (assistant/src/runtime/routes/conversation-query-routes.ts:447-452) and loadRawConfig only enforces object shape, not field-level schema (assistant/src/config/loader.ts:875-916). If legacy or manual writes can still persist "", this may surface as an invalid dropdown value and should be verified.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid finding — the old code used if (wsProvider) (truthy check) which treats empty string as falsy. Fixed in 85a4ec5 by switching from ?? to || to preserve the original semantics.

serverWebSearchProvider: wsService?.provider || getLocalSetting(LS_WEB_SEARCH_PROVIDER, "inference-provider-native"),
};
}, [daemonConfig]);

Expand Down