Skip to content

Commit 706b284

Browse files
authored
🤖 fix: auto-naming works with mux-gateway (#1231)
When users only have mux-gateway configured (no direct Anthropic/OpenAI API keys), `getPreferredNameModel` now tries gateway versions of preferred models. **The fix:** 1. After trying each preferred model directly (`anthropic:claude-haiku-4-5`), also try the gateway version (`mux-gateway:anthropic/claude-haiku-4-5`) 2. Still prefers direct access when available for lower latency This ensures auto-naming works for gateway-only users while maintaining optimal performance for users with direct API keys. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-5` • Thinking: `high`_
1 parent d84bd63 commit 706b284

File tree

6 files changed

+80
-33
lines changed

6 files changed

+80
-33
lines changed

src/browser/hooks/useGatewayModels.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ export function isGatewayFormat(modelId: string): boolean {
4646
return modelId.startsWith("mux-gateway:");
4747
}
4848

49+
/**
50+
* Convert a canonical model string to mux-gateway format.
51+
* Example: "anthropic:claude-haiku-4-5" → "mux-gateway:anthropic/claude-haiku-4-5"
52+
*
53+
* Unlike toGatewayModel(), this doesn't check if the user enabled gateway for
54+
* this specific model - use it when gateway should be used unconditionally
55+
* (e.g., for name generation with small models).
56+
*/
57+
export function formatAsGatewayModel(modelId: string): string {
58+
const provider = getProvider(modelId);
59+
if (!provider) return modelId;
60+
const model = modelId.slice(provider.length + 1);
61+
return `mux-gateway:${provider}/${model}`;
62+
}
63+
4964
/**
5065
* Migrate a mux-gateway model to canonical format and enable gateway toggle.
5166
* Converts "mux-gateway:provider/model" to "provider:model" and marks it for gateway routing.

src/browser/hooks/useWorkspaceName.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { useState, useRef, useCallback, useEffect, useMemo } from "react";
22
import { useAPI } from "@/browser/contexts/API";
3+
import { useGateway, formatAsGatewayModel } from "@/browser/hooks/useGatewayModels";
4+
import { getKnownModel } from "@/common/constants/knownModels";
35

46
export interface UseWorkspaceNameOptions {
57
/** The user's message to generate a name for */
@@ -48,9 +50,25 @@ export interface UseWorkspaceNameReturn extends WorkspaceNameState {
4850
* but allows manual override. If the user clears the manual name,
4951
* auto-generation resumes.
5052
*/
53+
/** Small, fast models preferred for name generation */
54+
const PREFERRED_NAME_MODELS = [getKnownModel("HAIKU").id, getKnownModel("GPT_MINI").id];
55+
5156
export function useWorkspaceName(options: UseWorkspaceNameOptions): UseWorkspaceNameReturn {
5257
const { message, debounceMs = 500, fallbackModel } = options;
5358
const { api } = useAPI();
59+
// Use global gateway availability (configured + enabled), not per-model toggles.
60+
// Name generation uses utility models (Haiku, GPT-Mini) that users don't explicitly
61+
// add to their model list, so we can't rely on per-model gateway settings.
62+
const { isActive: gatewayConfigured } = useGateway();
63+
64+
// Build preferred models list: try direct first, then gateway versions if available
65+
const preferredModels = useMemo(() => {
66+
const models: string[] = [...PREFERRED_NAME_MODELS];
67+
if (gatewayConfigured) {
68+
models.push(...PREFERRED_NAME_MODELS.map(formatAsGatewayModel));
69+
}
70+
return models;
71+
}, [gatewayConfigured]);
5472

5573
// Generated identity (name + title) from AI
5674
const [generatedIdentity, setGeneratedIdentity] = useState<WorkspaceIdentity | null>(null);
@@ -120,6 +138,7 @@ export function useWorkspaceName(options: UseWorkspaceNameOptions): UseWorkspace
120138
try {
121139
const result = await api.nameGeneration.generate({
122140
message: forMessage,
141+
preferredModels,
123142
fallbackModel,
124143
});
125144

@@ -162,7 +181,7 @@ export function useWorkspaceName(options: UseWorkspaceNameOptions): UseWorkspace
162181
}
163182
}
164183
},
165-
[api, fallbackModel]
184+
[api, preferredModels, fallbackModel]
166185
);
167186

168187
// Debounced generation effect

src/common/orpc/schemas/api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,9 @@ export const nameGeneration = {
485485
generate: {
486486
input: z.object({
487487
message: z.string(),
488-
/** Model to use if preferred small models (Haiku, GPT-Mini) aren't available */
488+
/** Models to try in order (frontend converts to gateway format if needed) */
489+
preferredModels: z.array(z.string()).optional(),
490+
/** Model to use if preferred models aren't available */
489491
fallbackModel: z.string().optional(),
490492
}),
491493
output: ResultSchema(

src/node/orpc/router.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { os } from "@orpc/server";
22
import * as schemas from "@/common/orpc/schemas";
33
import type { ORPCContext } from "./context";
44
import {
5-
getPreferredNameModel,
5+
findAvailableModel,
66
generateWorkspaceIdentity,
77
} from "@/node/services/workspaceTitleGenerator";
88
import type {
@@ -521,8 +521,10 @@ export const router = (authToken?: string) => {
521521
.input(schemas.nameGeneration.generate.input)
522522
.output(schemas.nameGeneration.generate.output)
523523
.handler(async ({ context, input }) => {
524-
// Prefer small/fast models, fall back to user's configured model
525-
const model = (await getPreferredNameModel(context.aiService)) ?? input.fallbackModel;
524+
// Try preferred models in order, fall back to user's configured model
525+
const model =
526+
(await findAvailableModel(context.aiService, input.preferredModels ?? [])) ??
527+
input.fallbackModel;
526528
if (!model) {
527529
return {
528530
success: false,
Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { describe, it, expect } from "bun:test";
2-
import { getPreferredNameModel } from "./workspaceTitleGenerator";
2+
import { findAvailableModel } from "./workspaceTitleGenerator";
33
import type { AIService } from "./aiService";
4-
import { getKnownModel } from "@/common/constants/knownModels";
54
import type { LanguageModel } from "ai";
65
import type { Result } from "@/common/types/result";
76
import type { SendMessageError } from "@/common/types/errors";
@@ -13,7 +12,6 @@ function createMockAIService(availableModels: string[]): AIService {
1312
const service: Partial<AIService> = {
1413
createModel: (modelString: string): Promise<CreateModelResult> => {
1514
if (availableModels.includes(modelString)) {
16-
// Return a minimal success result - data is not used by getPreferredNameModel
1715
const result: CreateModelResult = { success: true, data: null as never };
1816
return Promise.resolve(result);
1917
}
@@ -28,23 +26,36 @@ function createMockAIService(availableModels: string[]): AIService {
2826
}
2927

3028
describe("workspaceTitleGenerator", () => {
31-
const HAIKU_ID = getKnownModel("HAIKU").id;
32-
const GPT_MINI_ID = getKnownModel("GPT_MINI").id;
29+
describe("findAvailableModel", () => {
30+
it("returns null when no models available", async () => {
31+
const aiService = createMockAIService([]);
32+
expect(await findAvailableModel(aiService, ["model-a", "model-b"])).toBeNull();
33+
});
3334

34-
it("getPreferredNameModel returns null when no models available", async () => {
35-
const aiService = createMockAIService([]);
36-
expect(await getPreferredNameModel(aiService)).toBeNull();
37-
});
35+
it("returns null for empty models list", async () => {
36+
const aiService = createMockAIService(["any-model"]);
37+
expect(await findAvailableModel(aiService, [])).toBeNull();
38+
});
3839

39-
it("getPreferredNameModel prefers Haiku when available", async () => {
40-
const aiService = createMockAIService([HAIKU_ID, GPT_MINI_ID]);
41-
const model = await getPreferredNameModel(aiService);
42-
expect(model).toBe(HAIKU_ID);
43-
});
40+
it("returns first available model", async () => {
41+
const aiService = createMockAIService(["model-b", "model-c"]);
42+
const model = await findAvailableModel(aiService, ["model-a", "model-b", "model-c"]);
43+
expect(model).toBe("model-b");
44+
});
45+
46+
it("tries models in order", async () => {
47+
const aiService = createMockAIService(["model-a", "model-b"]);
48+
const model = await findAvailableModel(aiService, ["model-a", "model-b"]);
49+
expect(model).toBe("model-a");
50+
});
4451

45-
it("getPreferredNameModel falls back to GPT Mini when Haiku unavailable", async () => {
46-
const aiService = createMockAIService([GPT_MINI_ID]);
47-
const model = await getPreferredNameModel(aiService);
48-
expect(model).toBe(GPT_MINI_ID);
52+
it("works with gateway-format models", async () => {
53+
const aiService = createMockAIService(["mux-gateway:anthropic/claude-haiku-4-5"]);
54+
const model = await findAvailableModel(aiService, [
55+
"anthropic:claude-haiku-4-5", // direct - unavailable
56+
"mux-gateway:anthropic/claude-haiku-4-5", // gateway - available
57+
]);
58+
expect(model).toBe("mux-gateway:anthropic/claude-haiku-4-5");
59+
});
4960
});
5061
});

src/node/services/workspaceTitleGenerator.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,8 @@ import { log } from "./log";
55
import type { Result } from "@/common/types/result";
66
import { Ok, Err } from "@/common/types/result";
77
import type { SendMessageError } from "@/common/types/errors";
8-
import { getKnownModel } from "@/common/constants/knownModels";
98
import crypto from "crypto";
109

11-
/** Models to try in order of preference for name generation (small, fast models) */
12-
const PREFERRED_MODELS = [getKnownModel("HAIKU").id, getKnownModel("GPT_MINI").id] as const;
13-
1410
/** Schema for AI-generated workspace identity (area name + descriptive title) */
1511
const workspaceIdentitySchema = z.object({
1612
name: z
@@ -36,17 +32,19 @@ export interface WorkspaceIdentity {
3632
}
3733

3834
/**
39-
* Get the preferred model for name generation by testing which models the AIService
40-
* can actually create. This delegates credential checking to AIService, avoiding
41-
* duplication of provider-specific API key logic.
35+
* Find the first model from the list that the AIService can create.
36+
* Frontend is responsible for providing models in the correct format
37+
* (direct or gateway) based on user configuration.
4238
*/
43-
export async function getPreferredNameModel(aiService: AIService): Promise<string | null> {
44-
for (const modelId of PREFERRED_MODELS) {
39+
export async function findAvailableModel(
40+
aiService: AIService,
41+
models: string[]
42+
): Promise<string | null> {
43+
for (const modelId of models) {
4544
const result = await aiService.createModel(modelId);
4645
if (result.success) {
4746
return modelId;
4847
}
49-
// If it's an API key error, try the next model; other errors are also skipped
5048
}
5149
return null;
5250
}

0 commit comments

Comments
 (0)