fix: honor copilot config and profiles (#705) (thanks @TAGOOZ)
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
### Fixes
|
### Fixes
|
||||||
- Auto-reply: inline `/status` now honors allowlists (authorized stripped + replied inline; unauthorized leaves text for the agent) to match command gating tests.
|
- Auto-reply: inline `/status` now honors allowlists (authorized stripped + replied inline; unauthorized leaves text for the agent) to match command gating tests.
|
||||||
- Telegram: show typing indicator in General forum topics. (#779) — thanks @azade-c.
|
- Telegram: show typing indicator in General forum topics. (#779) — thanks @azade-c.
|
||||||
|
- Models: keep explicit GitHub Copilot provider config and honor agent-dir auth profiles for auto-injection. (#705) — thanks @TAGOOZ.
|
||||||
|
|
||||||
## 2026.1.11
|
## 2026.1.11
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,127 @@ describe("models config", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not override explicit github-copilot provider config", async () => {
|
||||||
|
await withTempHome(async () => {
|
||||||
|
const previous = process.env.COPILOT_GITHUB_TOKEN;
|
||||||
|
process.env.COPILOT_GITHUB_TOKEN = "gh-token";
|
||||||
|
|
||||||
|
try {
|
||||||
|
vi.resetModules();
|
||||||
|
|
||||||
|
vi.doMock("../providers/github-copilot-token.js", () => ({
|
||||||
|
DEFAULT_COPILOT_API_BASE_URL:
|
||||||
|
"https://api.individual.githubcopilot.com",
|
||||||
|
resolveCopilotApiToken: vi.fn().mockResolvedValue({
|
||||||
|
token: "copilot",
|
||||||
|
expiresAt: Date.now() + 60 * 60 * 1000,
|
||||||
|
source: "mock",
|
||||||
|
baseUrl: "https://api.copilot.example",
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { ensureClawdbotModelsJson } = await import("./models-config.js");
|
||||||
|
const { resolveClawdbotAgentDir } = await import("./agent-paths.js");
|
||||||
|
|
||||||
|
await ensureClawdbotModelsJson({
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
"github-copilot": {
|
||||||
|
baseUrl: "https://copilot.local",
|
||||||
|
api: "openai-responses",
|
||||||
|
models: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const agentDir = resolveClawdbotAgentDir();
|
||||||
|
const raw = await fs.readFile(
|
||||||
|
path.join(agentDir, "models.json"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const parsed = JSON.parse(raw) as {
|
||||||
|
providers: Record<string, { baseUrl?: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parsed.providers["github-copilot"]?.baseUrl).toBe(
|
||||||
|
"https://copilot.local",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
process.env.COPILOT_GITHUB_TOKEN = previous;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses agentDir override auth profiles for copilot injection", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
const previous = process.env.COPILOT_GITHUB_TOKEN;
|
||||||
|
const previousGh = process.env.GH_TOKEN;
|
||||||
|
const previousGithub = process.env.GITHUB_TOKEN;
|
||||||
|
delete process.env.COPILOT_GITHUB_TOKEN;
|
||||||
|
delete process.env.GH_TOKEN;
|
||||||
|
delete process.env.GITHUB_TOKEN;
|
||||||
|
|
||||||
|
try {
|
||||||
|
vi.resetModules();
|
||||||
|
|
||||||
|
const agentDir = path.join(home, "agent-override");
|
||||||
|
await fs.mkdir(agentDir, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(agentDir, "auth-profiles.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
"github-copilot:github": {
|
||||||
|
type: "token",
|
||||||
|
provider: "github-copilot",
|
||||||
|
token: "gh-profile-token",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.doMock("../providers/github-copilot-token.js", () => ({
|
||||||
|
DEFAULT_COPILOT_API_BASE_URL:
|
||||||
|
"https://api.individual.githubcopilot.com",
|
||||||
|
resolveCopilotApiToken: vi.fn().mockResolvedValue({
|
||||||
|
token: "copilot",
|
||||||
|
expiresAt: Date.now() + 60 * 60 * 1000,
|
||||||
|
source: "mock",
|
||||||
|
baseUrl: "https://api.copilot.example",
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { ensureClawdbotModelsJson } = await import("./models-config.js");
|
||||||
|
|
||||||
|
await ensureClawdbotModelsJson({ models: { providers: {} } }, agentDir);
|
||||||
|
|
||||||
|
const raw = await fs.readFile(
|
||||||
|
path.join(agentDir, "models.json"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const parsed = JSON.parse(raw) as {
|
||||||
|
providers: Record<string, { baseUrl?: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parsed.providers["github-copilot"]?.baseUrl).toBe(
|
||||||
|
"https://api.copilot.example",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (previous === undefined) delete process.env.COPILOT_GITHUB_TOKEN;
|
||||||
|
else process.env.COPILOT_GITHUB_TOKEN = previous;
|
||||||
|
if (previousGh === undefined) delete process.env.GH_TOKEN;
|
||||||
|
else process.env.GH_TOKEN = previousGh;
|
||||||
|
if (previousGithub === undefined) delete process.env.GITHUB_TOKEN;
|
||||||
|
else process.env.GITHUB_TOKEN = previousGithub;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
let previousHome: string | undefined;
|
let previousHome: string | undefined;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import fs from "node:fs/promises";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { type ClawdbotConfig, loadConfig } from "../config/config.js";
|
import { type ClawdbotConfig, loadConfig } from "../config/config.js";
|
||||||
import type { ModelsConfig as ModelsConfigShape } from "../config/types.js";
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_COPILOT_API_BASE_URL,
|
DEFAULT_COPILOT_API_BASE_URL,
|
||||||
resolveCopilotApiToken,
|
resolveCopilotApiToken,
|
||||||
@@ -12,17 +11,60 @@ import {
|
|||||||
ensureAuthProfileStore,
|
ensureAuthProfileStore,
|
||||||
listProfilesForProvider,
|
listProfilesForProvider,
|
||||||
} from "./auth-profiles.js";
|
} from "./auth-profiles.js";
|
||||||
|
import { resolveEnvApiKey } from "./model-auth.js";
|
||||||
|
|
||||||
type ModelsConfig = NonNullable<ClawdbotConfig["models"]>;
|
type ModelsConfig = NonNullable<ClawdbotConfig["models"]>;
|
||||||
|
type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
|
||||||
type ModelsProviderConfig = NonNullable<ModelsConfigShape["providers"]>[string];
|
|
||||||
|
|
||||||
const DEFAULT_MODE: NonNullable<ModelsConfig["mode"]> = "merge";
|
const DEFAULT_MODE: NonNullable<ModelsConfig["mode"]> = "merge";
|
||||||
|
const MINIMAX_API_BASE_URL = "https://api.minimax.io/anthropic";
|
||||||
|
const MINIMAX_DEFAULT_MODEL_ID = "MiniMax-M2.1";
|
||||||
|
const MINIMAX_DEFAULT_CONTEXT_WINDOW = 200000;
|
||||||
|
const MINIMAX_DEFAULT_MAX_TOKENS = 8192;
|
||||||
|
// Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
|
||||||
|
const MINIMAX_API_COST = {
|
||||||
|
input: 15,
|
||||||
|
output: 60,
|
||||||
|
cacheRead: 2,
|
||||||
|
cacheWrite: 10,
|
||||||
|
};
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeGoogleModelId(id: string): string {
|
||||||
|
if (id === "gemini-3-pro") return "gemini-3-pro-preview";
|
||||||
|
if (id === "gemini-3-flash") return "gemini-3-flash-preview";
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGoogleProvider(provider: ProviderConfig): ProviderConfig {
|
||||||
|
let mutated = false;
|
||||||
|
const models = provider.models.map((model) => {
|
||||||
|
const nextId = normalizeGoogleModelId(model.id);
|
||||||
|
if (nextId === model.id) return model;
|
||||||
|
mutated = true;
|
||||||
|
return { ...model, id: nextId };
|
||||||
|
});
|
||||||
|
return mutated ? { ...provider, models } : provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProviders(
|
||||||
|
providers: ModelsConfig["providers"],
|
||||||
|
): ModelsConfig["providers"] {
|
||||||
|
if (!providers) return providers;
|
||||||
|
let mutated = false;
|
||||||
|
const next: Record<string, ProviderConfig> = {};
|
||||||
|
for (const [key, provider] of Object.entries(providers)) {
|
||||||
|
const normalized =
|
||||||
|
key === "google" ? normalizeGoogleProvider(provider) : provider;
|
||||||
|
if (normalized !== provider) mutated = true;
|
||||||
|
next[key] = normalized;
|
||||||
|
}
|
||||||
|
return mutated ? next : providers;
|
||||||
|
}
|
||||||
|
|
||||||
async function readJson(pathname: string): Promise<unknown> {
|
async function readJson(pathname: string): Promise<unknown> {
|
||||||
try {
|
try {
|
||||||
const raw = await fs.readFile(pathname, "utf8");
|
const raw = await fs.readFile(pathname, "utf8");
|
||||||
@@ -32,12 +74,45 @@ async function readJson(pathname: string): Promise<unknown> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function maybeBuildCopilotProvider(params: {
|
function buildMinimaxApiProvider(): ProviderConfig {
|
||||||
|
return {
|
||||||
|
baseUrl: MINIMAX_API_BASE_URL,
|
||||||
|
api: "anthropic-messages",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: MINIMAX_DEFAULT_MODEL_ID,
|
||||||
|
name: "MiniMax M2.1",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: MINIMAX_API_COST,
|
||||||
|
contextWindow: MINIMAX_DEFAULT_CONTEXT_WINDOW,
|
||||||
|
maxTokens: MINIMAX_DEFAULT_MAX_TOKENS,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveImplicitProviders(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
|
agentDir: string;
|
||||||
|
}): ModelsConfig["providers"] {
|
||||||
|
const providers: Record<string, ProviderConfig> = {};
|
||||||
|
const minimaxEnv = resolveEnvApiKey("minimax");
|
||||||
|
const authStore = ensureAuthProfileStore(params.agentDir);
|
||||||
|
const hasMinimaxProfile =
|
||||||
|
listProfilesForProvider(authStore, "minimax").length > 0;
|
||||||
|
if (minimaxEnv || hasMinimaxProfile) {
|
||||||
|
providers.minimax = buildMinimaxApiProvider();
|
||||||
|
}
|
||||||
|
return providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybeBuildCopilotProvider(params: {
|
||||||
|
agentDir: string;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
}): Promise<ModelsProviderConfig | null> {
|
}): Promise<ProviderConfig | null> {
|
||||||
const env = params.env ?? process.env;
|
const env = params.env ?? process.env;
|
||||||
const authStore = ensureAuthProfileStore();
|
const authStore = ensureAuthProfileStore(params.agentDir);
|
||||||
const hasProfile =
|
const hasProfile =
|
||||||
listProfilesForProvider(authStore, "github-copilot").length > 0;
|
listProfilesForProvider(authStore, "github-copilot").length > 0;
|
||||||
const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN;
|
const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN;
|
||||||
@@ -87,7 +162,7 @@ async function maybeBuildCopilotProvider(params: {
|
|||||||
return {
|
return {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
models: [],
|
models: [],
|
||||||
} satisfies ModelsProviderConfig;
|
} satisfies ProviderConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureClawdbotModelsJson(
|
export async function ensureClawdbotModelsJson(
|
||||||
@@ -95,24 +170,26 @@ export async function ensureClawdbotModelsJson(
|
|||||||
agentDirOverride?: string,
|
agentDirOverride?: string,
|
||||||
): Promise<{ agentDir: string; wrote: boolean }> {
|
): Promise<{ agentDir: string; wrote: boolean }> {
|
||||||
const cfg = config ?? loadConfig();
|
const cfg = config ?? loadConfig();
|
||||||
|
const agentDir = agentDirOverride?.trim()
|
||||||
|
? agentDirOverride.trim()
|
||||||
|
: resolveClawdbotAgentDir();
|
||||||
|
|
||||||
const explicitProviders = cfg.models?.providers ?? {};
|
const explicitProviders = cfg.models?.providers ?? {};
|
||||||
const implicitCopilot = await maybeBuildCopilotProvider({ cfg });
|
const implicitProviders = resolveImplicitProviders({ cfg, agentDir });
|
||||||
const providers = implicitCopilot
|
const providers: Record<string, ProviderConfig> = {
|
||||||
? { ...explicitProviders, "github-copilot": implicitCopilot }
|
...implicitProviders,
|
||||||
: explicitProviders;
|
...explicitProviders,
|
||||||
|
};
|
||||||
|
const implicitCopilot = await maybeBuildCopilotProvider({ agentDir });
|
||||||
|
if (implicitCopilot && !providers["github-copilot"]) {
|
||||||
|
providers["github-copilot"] = implicitCopilot;
|
||||||
|
}
|
||||||
|
|
||||||
if (!providers || Object.keys(providers).length === 0) {
|
if (Object.keys(providers).length === 0) {
|
||||||
const agentDir = agentDirOverride?.trim()
|
|
||||||
? agentDirOverride.trim()
|
|
||||||
: resolveClawdbotAgentDir();
|
|
||||||
return { agentDir, wrote: false };
|
return { agentDir, wrote: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode = cfg.models?.mode ?? DEFAULT_MODE;
|
const mode = cfg.models?.mode ?? DEFAULT_MODE;
|
||||||
const agentDir = agentDirOverride?.trim()
|
|
||||||
? agentDirOverride.trim()
|
|
||||||
: resolveClawdbotAgentDir();
|
|
||||||
const targetPath = path.join(agentDir, "models.json");
|
const targetPath = path.join(agentDir, "models.json");
|
||||||
|
|
||||||
let mergedProviders = providers;
|
let mergedProviders = providers;
|
||||||
@@ -128,7 +205,8 @@ export async function ensureClawdbotModelsJson(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const next = `${JSON.stringify({ providers: mergedProviders }, null, 2)}\n`;
|
const normalizedProviders = normalizeProviders(mergedProviders);
|
||||||
|
const next = `${JSON.stringify({ providers: normalizedProviders }, null, 2)}\n`;
|
||||||
try {
|
try {
|
||||||
existingRaw = await fs.readFile(targetPath, "utf8");
|
existingRaw = await fs.readFile(targetPath, "utf8");
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user