mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-30 17:11:12 +08:00
Support multiple WorkBuddy models and generic app paths
This commit is contained in:
@@ -315,7 +315,7 @@ export function writeCodexCompatibleAppModelCatalog(
|
||||
|
||||
export function writeWorkbuddyModelsConfig(
|
||||
workbuddyConfigDir: string,
|
||||
profile: Pick<ProfileConfig, "model" | "name" | "providerName">,
|
||||
profile: ProfileConfig,
|
||||
config?: AppConfig
|
||||
): WorkbuddyModelsConfigWriteResult {
|
||||
const file = path.join(workbuddyConfigDir, "models.json");
|
||||
@@ -341,21 +341,38 @@ function workbuddySelectedModel(
|
||||
}
|
||||
|
||||
function workbuddyModelsConfig(
|
||||
model: string,
|
||||
profile: Pick<ProfileConfig, "name" | "providerName">,
|
||||
defaultModel: string,
|
||||
profile: ProfileConfig,
|
||||
config?: AppConfig
|
||||
): Record<string, unknown> {
|
||||
const allowedModels = profileAllowedModels(profile);
|
||||
const catalogItems = codexCompatibleAppModelCatalog(config, defaultModel, "workbuddy", allowedModels).models;
|
||||
const models = catalogItems.map((catalogItem) =>
|
||||
workbuddyModelConfig(catalogItem.slug || catalogItem.id || catalogItem.model, defaultModel, profile, catalogItem, config)
|
||||
);
|
||||
if (models.length === 0) {
|
||||
models.push(workbuddyModelConfig(defaultModel, defaultModel, profile, undefined, config));
|
||||
}
|
||||
return {
|
||||
availableModels: models.map((item) => String(item.id || "")),
|
||||
models
|
||||
};
|
||||
}
|
||||
|
||||
function workbuddyModelConfig(
|
||||
model: string,
|
||||
defaultModel: string,
|
||||
profile: Pick<ProfileConfig, "name" | "providerName">,
|
||||
catalogItem: CodexModelCatalogItem | undefined,
|
||||
config?: AppConfig
|
||||
): Record<string, unknown> {
|
||||
const catalogItem = codexCompatibleAppModelCatalog(config, model, "workbuddy")
|
||||
.models.find((item) => item.slug === model || item.id === model || item.model === model);
|
||||
const vendor = workbuddyModelVendor(model, profile.providerName);
|
||||
const displayName = profile.name?.trim()
|
||||
? `${profile.name.trim()} / ${model}`
|
||||
: `${vendor} / ${model}`;
|
||||
const displayName = model.includes("/") ? model : `${vendor} / ${model}`;
|
||||
const workbuddyModel: Record<string, unknown> = {
|
||||
apiKey: "${CCR_PROFILE_API_KEY}",
|
||||
disabled: false,
|
||||
id: model,
|
||||
isDefault: true,
|
||||
isDefault: model === defaultModel,
|
||||
name: displayName,
|
||||
supportsImages: Boolean(catalogItem?.supports_image_detail_original),
|
||||
supportsReasoning: Boolean(catalogItem?.supports_reasoning_summaries),
|
||||
@@ -375,10 +392,7 @@ function workbuddyModelsConfig(
|
||||
supportedEfforts: reasoningEfforts
|
||||
};
|
||||
}
|
||||
return {
|
||||
availableModels: [model],
|
||||
models: [workbuddyModel]
|
||||
};
|
||||
return workbuddyModel;
|
||||
}
|
||||
|
||||
function workbuddyModelVendor(model: string, providerName?: string): string {
|
||||
|
||||
@@ -397,6 +397,69 @@ test("WorkBuddy AI app profile writes the virtual desktop auth session", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("WorkBuddy AI app profile writes every allowed model to models.json", () => {
|
||||
const configDir = mkdtempSync(path.join(os.tmpdir(), "ccr-workbuddy-app-models-"));
|
||||
try {
|
||||
const profile = {
|
||||
agent: "workbuddy",
|
||||
availableModels: ["Codex API/gpt-5-codex", "Codex API/gpt-5.1-codex"],
|
||||
enabled: true,
|
||||
id: "workbuddy-main",
|
||||
model: "Codex API/gpt-5-codex",
|
||||
name: "WorkBuddy Main",
|
||||
providerId: "claude-code-router",
|
||||
scope: "ccr",
|
||||
surface: "app"
|
||||
};
|
||||
|
||||
const result = writeCodexCompatibleAppModelCatalog(configDir, profile, {
|
||||
Providers: [{
|
||||
models: ["gpt-5-codex", "gpt-5.1-codex", "gpt-4.1"],
|
||||
name: "Codex API",
|
||||
type: "openai_responses"
|
||||
}]
|
||||
});
|
||||
|
||||
const modelsConfig = JSON.parse(readFileSync(result.workbuddyModelsConfig.file, "utf8"));
|
||||
assert.deepEqual(modelsConfig.availableModels, ["Codex API/gpt-5-codex", "Codex API/gpt-5.1-codex"]);
|
||||
assert.deepEqual(modelsConfig.models.map((item) => item.id), ["Codex API/gpt-5-codex", "Codex API/gpt-5.1-codex"]);
|
||||
assert.deepEqual(modelsConfig.models.map((item) => item.name), ["Codex API/gpt-5-codex", "Codex API/gpt-5.1-codex"]);
|
||||
assert.deepEqual(modelsConfig.models.map((item) => item.isDefault), [true, false]);
|
||||
} finally {
|
||||
rmSync(configDir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("WorkBuddy AI app profile writes every catalog model when the allowlist is unrestricted", () => {
|
||||
const configDir = mkdtempSync(path.join(os.tmpdir(), "ccr-workbuddy-app-unrestricted-models-"));
|
||||
try {
|
||||
const profile = {
|
||||
agent: "workbuddy",
|
||||
enabled: true,
|
||||
id: "workbuddy-main",
|
||||
model: "Codex API/gpt-5-codex",
|
||||
name: "WorkBuddy Main",
|
||||
providerId: "claude-code-router",
|
||||
scope: "ccr",
|
||||
surface: "app"
|
||||
};
|
||||
|
||||
const result = writeCodexCompatibleAppModelCatalog(configDir, profile, {
|
||||
Providers: [{
|
||||
models: ["gpt-5-codex", "gpt-5.1-codex", "gpt-4.1"],
|
||||
name: "Codex API",
|
||||
type: "openai_responses"
|
||||
}]
|
||||
});
|
||||
|
||||
const modelsConfig = JSON.parse(readFileSync(result.workbuddyModelsConfig.file, "utf8"));
|
||||
assert.deepEqual(modelsConfig.availableModels, ["Codex API/gpt-5-codex", "Codex API/gpt-5.1-codex", "Codex API/gpt-4.1"]);
|
||||
assert.deepEqual(modelsConfig.models.map((item) => item.isDefault), [true, false, false]);
|
||||
} finally {
|
||||
rmSync(configDir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
function withPlatform(platform, callback) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
||||
Object.defineProperty(process, "platform", {
|
||||
|
||||
@@ -1316,18 +1316,9 @@ function profileNumberDraftValid(value: string, min: number, max: number): boole
|
||||
return Number.isFinite(numeric) && numeric >= min && numeric <= max;
|
||||
}
|
||||
|
||||
function profileAppPathLabel(agent: ProfileConfig["agent"]): "CLAUDE_APP_PATH" | "CHATGPT_APP_PATH" | "OPENCODE_APP_PATH" | "WORKBUDDY_APP_PATH" | undefined {
|
||||
if (agent === "claude-code") {
|
||||
return "CLAUDE_APP_PATH";
|
||||
}
|
||||
if (agent === "codex") {
|
||||
return "CHATGPT_APP_PATH";
|
||||
}
|
||||
if (agent === "opencode") {
|
||||
return "OPENCODE_APP_PATH";
|
||||
}
|
||||
if (agent === "workbuddy") {
|
||||
return "WORKBUDDY_APP_PATH";
|
||||
function profileAppPathLabel(agent: ProfileConfig["agent"]): "APP_PATH" | undefined {
|
||||
if (agent === "claude-code" || agent === "codex" || agent === "opencode" || agent === "workbuddy") {
|
||||
return "APP_PATH";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -605,6 +605,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
|
||||
"Arguments": "Arguments",
|
||||
"Working directory": "Working directory",
|
||||
"API key env": "API key env",
|
||||
"APP_PATH": "APP_PATH",
|
||||
"CLAUDE_APP_PATH": "CLAUDE_APP_PATH",
|
||||
"CHATGPT_APP_PATH": "CHATGPT_APP_PATH",
|
||||
"OPENCODE_APP_PATH": "OPENCODE_APP_PATH",
|
||||
@@ -1751,6 +1752,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
|
||||
"Arguments": "参数",
|
||||
"Working directory": "工作目录",
|
||||
"API key env": "API Key 环境变量",
|
||||
"APP_PATH": "APP_PATH",
|
||||
"CLAUDE_APP_PATH": "CLAUDE_APP_PATH",
|
||||
"CHATGPT_APP_PATH": "CHATGPT_APP_PATH",
|
||||
"OPENCODE_APP_PATH": "OPENCODE_APP_PATH",
|
||||
|
||||
@@ -1204,7 +1204,7 @@ export function profileSummaryItems(
|
||||
: [];
|
||||
const appPath = profile.appPath?.trim() || "";
|
||||
const appPathSummaryItems = appPath && surface !== "cli" && profile.agent !== "zcode"
|
||||
? [{ label: t(profile.agent === "claude-code" ? "CLAUDE_APP_PATH" : profile.agent === "opencode" ? "OPENCODE_APP_PATH" : "CHATGPT_APP_PATH"), value: appPath }]
|
||||
? [{ label: t("APP_PATH"), value: appPath }]
|
||||
: [];
|
||||
const savedBot = profile.botConfigId
|
||||
? config.botConfigs.find((item) => item.id === profile.botConfigId)
|
||||
|
||||
@@ -259,7 +259,7 @@ test("AddProfileForm keeps the default model locked in allowed model lists", ()
|
||||
assert.doesNotMatch(otherLabel, /disabled=""/);
|
||||
});
|
||||
|
||||
test("AddProfileForm places CLAUDE_APP_PATH below Bot settings", () => {
|
||||
test("AddProfileForm places APP_PATH below Bot settings", () => {
|
||||
const config = appConfigFixture();
|
||||
const html = renderToStaticMarkup(
|
||||
<AddProfileForm
|
||||
@@ -274,7 +274,7 @@ test("AddProfileForm places CLAUDE_APP_PATH below Bot settings", () => {
|
||||
/>
|
||||
);
|
||||
const botIndex = html.indexOf(">Bot</span>");
|
||||
const appPathIndex = html.indexOf("CLAUDE_APP_PATH");
|
||||
const appPathIndex = html.indexOf("APP_PATH");
|
||||
const envIndex = html.indexOf("Environment variables");
|
||||
|
||||
assert.ok(botIndex >= 0);
|
||||
@@ -504,6 +504,23 @@ test("profileSummaryItems uses Pi-specific model labels", () => {
|
||||
assert.equal(items[0]?.label, "Pi model");
|
||||
});
|
||||
|
||||
test("profileSummaryItems uses a generic App path label", () => {
|
||||
const config = appConfigFixture();
|
||||
const items = profileSummaryItems({
|
||||
agent: "workbuddy",
|
||||
appPath: "/Applications/WorkBuddy AI.app/Contents/MacOS/Electron",
|
||||
enabled: true,
|
||||
id: "workbuddy-main",
|
||||
model: "openai/gpt-5.2",
|
||||
name: "Workbuddy Main",
|
||||
scope: "ccr",
|
||||
surface: "app"
|
||||
}, config, (value) => value);
|
||||
|
||||
assert.equal(items.find((item) => item.value.includes("WorkBuddy AI.app"))?.label, "APP_PATH");
|
||||
assert.doesNotMatch(items.map((item) => item.label).join(" "), /CHATGPT_APP_PATH|WORKBUDDY_APP_PATH/);
|
||||
});
|
||||
|
||||
test("profileSummaryItems omits disabled profile properties from cards", () => {
|
||||
const config = appConfigFixture();
|
||||
const disabledItems = profileSummaryItems({
|
||||
@@ -745,7 +762,8 @@ test("Workbuddy profiles support local App configuration", () => {
|
||||
);
|
||||
assert.match(html, /App only/);
|
||||
assert.doesNotMatch(html, /CLI only/);
|
||||
assert.match(html, /WORKBUDDY_APP_PATH/);
|
||||
assert.match(html, /APP_PATH/);
|
||||
assert.doesNotMatch(html, /WORKBUDDY_APP_PATH/);
|
||||
|
||||
const profile = normalizeUnknownProfileItem({
|
||||
agent: "work-buddy",
|
||||
|
||||
Reference in New Issue
Block a user