Skip to content
Draft
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
2 changes: 1 addition & 1 deletion gui/src/pages/claude-manual-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

export type SidecarBackend = "openai" | "anthropic";
export interface SidecarOverride { backend?: SidecarBackend; model?: string }
export interface SidecarOverride { backend?: SidecarBackend | null; model?: string }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Persist Auto without requiring a model override

When a global sidecar backend is explicitly configured and a dashboard user selects Auto while leaving the Claude-specific model blank, widening this state type is insufficient: applySidecarBackendChange produces an override without a backend, but serializeSidecarOverride returns null for that blank-model state, so ClaudeCode.save sends a null section and the management API deletes the override. After reload it becomes “Use main setting,” and Claude requests still inherit the global backend—the exact regression this change intends to fix. Serialize this selection as { backend: null } and cover the blank-model case.

AGENTS.md reference: gui/AGENTS.md:L9-L10

Useful? React with 👍 / 👎.


export interface ClaudeManualEnvState {
/**
Expand Down
14 changes: 12 additions & 2 deletions src/server/claude-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,25 @@ function isRec(v: unknown): v is Rec {

/** Resolve Claude-only sidecar overrides without mutating the shared server config. */
export function buildClaudeReplayConfig(config: OcxConfig): OcxConfig {
const webSearchOverride = config.claudeCode?.webSearchSidecar;
const visionOverride = config.claudeCode?.visionSidecar;
const { backend: webSearchBackend, ...webSearchFields } = webSearchOverride ?? {};
const { backend: visionBackend, ...visionFields } = visionOverride ?? {};
return {
...config,
webSearchSidecar: {
...config.webSearchSidecar,
...config.claudeCode?.webSearchSidecar,
...webSearchFields,
...(webSearchOverride && "backend" in webSearchOverride
? { backend: webSearchBackend ?? undefined }
: {}),
},
visionSidecar: {
...config.visionSidecar,
...config.claudeCode?.visionSidecar,
...visionFields,
...(visionOverride && "backend" in visionOverride
? { backend: visionBackend ?? undefined }
: {}),
},
};
}
Expand Down
3 changes: 1 addition & 2 deletions src/server/management/agent-settings-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1021,8 +1021,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
}
const requested = section as { backend?: "openai" | "anthropic" | null; model?: string };
const override: NonNullable<OcxClaudeCodeConfig[typeof field]> = { ...next[field] };
if (requested.backend === null) delete override.backend;
else if (requested.backend !== undefined) override.backend = requested.backend;
if (requested.backend !== undefined) override.backend = requested.backend;
if (requested.model === "") delete override.model;
else if (requested.model !== undefined) override.model = requested.model;
if (Object.keys(override).length > 0) next[field] = override;
Expand Down
8 changes: 4 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,10 +476,10 @@ export interface OcxClaudeCodeConfig {
* definition. Unset inherits the parent session effort.
*/
subagentEffort?: "low" | "medium" | "high" | "xhigh" | "max";
/** Claude-originated web-search override. Unset fields inherit the global sidecar settings. */
webSearchSidecar?: { backend?: "openai" | "anthropic"; model?: string };
/** Claude-originated vision override. Unset fields inherit the global sidecar settings. */
visionSidecar?: { backend?: "openai" | "anthropic"; model?: string };
/** Claude-originated web-search override. Null backend means Auto; an absent field inherits global. */
webSearchSidecar?: { backend?: "openai" | "anthropic" | null; model?: string };
/** Claude-originated vision override. Null backend means Auto; an absent field inherits global. */
visionSidecar?: { backend?: "openai" | "anthropic" | null; model?: string };
/** Persisted Claude Desktop four-family routing profile. */
desktopProfile?: OcxClaudeDesktopProfile;
/** Auto-reconcile Desktop 3P config when provider catalog changes. Default: enabled. */
Expand Down
10 changes: 5 additions & 5 deletions tests/claude-management-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,17 +329,17 @@ test("Claude sidecar overrides round-trip, partially update, clear, and reject u
expect(loadConfig().claudeCode?.webSearchSidecar).toEqual({ backend: "anthropic", model: "claude-search-2" });
expect(loadConfig().claudeCode?.visionSidecar).toEqual({ backend: "openai", model: "gpt-vision" });

// null backend is the explicit Auto/inherit transition; empty model deletes only model.
// null backend is explicit Auto; empty model deletes only model.
response = await put({
webSearchSidecar: { backend: null },
visionSidecar: { backend: null, model: "" },
});
expect(response.status).toBe(200);
expect(loadConfig().claudeCode?.webSearchSidecar).toEqual({ model: "claude-search-2" });
expect(loadConfig().claudeCode?.visionSidecar).toBeUndefined();
expect(loadConfig().claudeCode?.webSearchSidecar).toEqual({ backend: null, model: "claude-search-2" });
expect(loadConfig().claudeCode?.visionSidecar).toEqual({ backend: null });
get = await fetch(new URL("/api/claude-code", server.url)).then(r => r.json()) as Record<string, unknown>;
expect(get.webSearchSidecar).toEqual({ model: "claude-search-2" });
expect(get.visionSidecar).toBeUndefined();
expect(get.webSearchSidecar).toEqual({ backend: null, model: "claude-search-2" });
expect(get.visionSidecar).toEqual({ backend: null });

// null and empty sections both clear the whole override.
response = await put({ webSearchSidecar: null, visionSidecar: {} });
Expand Down
18 changes: 18 additions & 0 deletions tests/claude-sidecar-override.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,21 @@ test("unset Claude overrides inherit the global sidecar backend and model", () =
settings: { model: "global-vision" },
});
});

test("explicit Claude Auto backends do not inherit global backend choices", () => {
const config: OcxConfig = {
port: 10100,
defaultProvider: "routed",
providers: { routed, forward },
webSearchSidecar: { backend: "openai", model: "global-search" },
visionSidecar: { backend: "openai", model: "global-vision" },
claudeCode: {
webSearchSidecar: { backend: null, model: "claude-search" },
visionSidecar: { backend: null, model: "claude-vision" },
},
};

const effective = buildClaudeReplayConfig(config);
expect(effective.webSearchSidecar).toEqual({ backend: undefined, model: "claude-search" });
expect(effective.visionSidecar).toEqual({ backend: undefined, model: "claude-vision" });
});
Loading