Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getSmallModel } from "@superset/chat/server/shared";
import { sanitizeBranchNameWithMaxLength } from "shared/utils/branch";

const BRANCH_NAME_INSTRUCTIONS =
"Generate a concise git branch name (2-4 words, kebab-case, descriptive). Return ONLY the branch name, nothing else.";
"Generate a concise git branch name (2-4 words, kebab-case, descriptive, 20 characters or less). Return ONLY the branch name, nothing else.";
const MAX_CONFLICT_RESOLUTION_ATTEMPTS = 1000;
const INITIAL_CONFLICT_SUFFIX = 2;

Expand Down Expand Up @@ -58,7 +58,7 @@ export async function generateBranchNameFromPrompt(
existingBranches: string[],
branchPrefix?: string,
): Promise<string | null> {
const model = getSmallModel();
const model = await getSmallModel();
if (!model) return null;

let generated: string | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test";

const getSmallModelMock = mock(
(() => null) as (...args: unknown[]) => unknown | null,
(async () => null) as (...args: unknown[]) => Promise<unknown | null>,
);
const generateTitleFromMessageMock = mock(
(async () => null) as (...args: unknown[]) => Promise<string | null>,
Expand Down Expand Up @@ -79,7 +79,7 @@ const {
describe("generateWorkspaceNameFromPrompt", () => {
beforeEach(() => {
getSmallModelMock.mockClear();
getSmallModelMock.mockReturnValue(null);
getSmallModelMock.mockResolvedValue(null);
generateTitleFromMessageMock.mockClear();
generateTitleFromMessageMock.mockResolvedValue(null);
selectGetMock.mockReset();
Expand All @@ -102,7 +102,7 @@ describe("generateWorkspaceNameFromPrompt", () => {
});

it("returns the model-generated title when a model is available", async () => {
getSmallModelMock.mockReturnValueOnce({ id: "test-model" });
getSmallModelMock.mockResolvedValueOnce({ id: "test-model" });
generateTitleFromMessageMock.mockResolvedValueOnce("Checking In");

await expect(
Expand All @@ -116,13 +116,14 @@ describe("generateWorkspaceNameFromPrompt", () => {
agentModel: { id: "test-model" },
agentId: "workspace-namer",
agentName: "Workspace Namer",
instructions: "You generate concise workspace titles.",
instructions:
"You generate concise workspace titles. 20 characters or less. Return ONLY the title, nothing else.",
tracingContext: { surface: "workspace-auto-name" },
});
});

it("preserves empty-string model results instead of forcing fallback", async () => {
getSmallModelMock.mockReturnValueOnce({ id: "test-model" });
getSmallModelMock.mockResolvedValueOnce({ id: "test-model" });
generateTitleFromMessageMock.mockResolvedValueOnce("");

await expect(
Expand All @@ -134,7 +135,7 @@ describe("generateWorkspaceNameFromPrompt", () => {
});

it("falls back when generation throws", async () => {
getSmallModelMock.mockReturnValueOnce({ id: "test-model" });
getSmallModelMock.mockResolvedValueOnce({ id: "test-model" });
generateTitleFromMessageMock.mockRejectedValueOnce(new Error("boom"));

await expect(
Expand All @@ -155,7 +156,7 @@ afterAll(() => {
describe("attemptWorkspaceAutoRenameFromPrompt", () => {
beforeEach(() => {
getSmallModelMock.mockClear();
getSmallModelMock.mockReturnValue(null);
getSmallModelMock.mockResolvedValue(null);
generateTitleFromMessageMock.mockClear();
generateTitleFromMessageMock.mockResolvedValue(null);
selectGetMock.mockReset();
Expand Down Expand Up @@ -196,7 +197,7 @@ describe("attemptWorkspaceAutoRenameFromPrompt", () => {
isUnnamed: true,
deletingAt: null,
});
getSmallModelMock.mockReturnValueOnce({ id: "test-model" });
getSmallModelMock.mockResolvedValueOnce({ id: "test-model" });
generateTitleFromMessageMock.mockResolvedValueOnce("");

await expect(
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/lib/trpc/routers/workspaces/utils/ai-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,16 @@ export async function generateWorkspaceNameFromPrompt(prompt: string): Promise<{
usedPromptFallback: boolean;
warning?: string;
}> {
const model = getSmallModel();
const model = await getSmallModel();
if (model) {
try {
const generated = await generateTitleFromMessage({
message: prompt,
agentModel: model,
agentId: "workspace-namer",
agentName: "Workspace Namer",
instructions: "You generate concise workspace titles.",
instructions:
"You generate concise workspace titles. 20 characters or less. Return ONLY the title, nothing else.",
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
tracingContext: { surface: "workspace-auto-name" },
});
if (generated !== null && generated !== undefined) {
Expand Down
9 changes: 4 additions & 5 deletions packages/chat/src/server/desktop/auth/provider-ids.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export const ANTHROPIC_AUTH_PROVIDER_ID = "anthropic";
export const OPENAI_AUTH_PROVIDER_ID = "openai-codex";
export const OPENAI_AUTH_PROVIDER_IDS = [
export {
ANTHROPIC_AUTH_PROVIDER_ID,
OPENAI_AUTH_PROVIDER_ID,
"openai",
] as const;
OPENAI_AUTH_PROVIDER_IDS,
} from "../../shared/auth-provider-ids";
8 changes: 8 additions & 0 deletions packages/chat/src/server/shared/auth-provider-ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const ANTHROPIC_AUTH_PROVIDER_ID = "anthropic";
export const OPENAI_AUTH_PROVIDER_ID = "openai-codex";
// Mastracode historically wrote OpenAI under "openai" before the Codex split.
// Read both when resolving credentials.
export const OPENAI_AUTH_PROVIDER_IDS = [
OPENAI_AUTH_PROVIDER_ID,
"openai",
] as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, it } from "bun:test";
import { isAnthropicApiKey, isOpenAIApiKey } from "./get-small-model";

describe("isAnthropicApiKey", () => {
it("accepts a real-shaped key", () => {
expect(
isAnthropicApiKey(
"sk-ant-api03-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
),
).toBe(true);
});

it("rejects dev placeholders", () => {
expect(isAnthropicApiKey("dummy")).toBe(false);
expect(isAnthropicApiKey("placeholder")).toBe(false);
expect(isAnthropicApiKey("")).toBe(false);
});

it("rejects OAuth access tokens (sk-ant-oat…) sent as api keys", () => {
// OAuth tokens fail when sent via x-api-key. Filter them so we fall
// through to the OAuth path which sends them via Authorization Bearer.
expect(
isAnthropicApiKey("sk-ant-oat-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
).toBe(false);
});

it("rejects keys with the prefix but absurd lengths", () => {
expect(isAnthropicApiKey("sk-ant-api")).toBe(false);
});

it("rejects unrelated provider keys", () => {
expect(isAnthropicApiKey("sk-proj-foo")).toBe(false);
});
});

describe("isOpenAIApiKey", () => {
it("accepts legacy, project, and service-account key shapes", () => {
expect(
isOpenAIApiKey("sk-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
).toBe(true);
expect(
isOpenAIApiKey("sk-proj-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
).toBe(true);
expect(
isOpenAIApiKey("sk-svcacct-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
).toBe(true);
});

it("rejects dev placeholders and obviously-fake values", () => {
expect(isOpenAIApiKey("dummy")).toBe(false);
expect(isOpenAIApiKey("sk-")).toBe(false);
expect(isOpenAIApiKey("")).toBe(false);
});

it("rejects values without the sk- prefix", () => {
expect(isOpenAIApiKey("api-key-aaaaaaaaaaaaaaaaaaaaaaaaaaaaa")).toBe(false);
});
});
Loading
Loading