mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
feat(wiki): add Atlas Cloud provider preset
Signed-off-by: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com>
This commit is contained in:
parent
021ac30376
commit
5b00b332f6
10 changed files with 128 additions and 17 deletions
|
|
@ -768,6 +768,10 @@ gitnexus wiki
|
|||
gitnexus wiki --model gpt-4o
|
||||
gitnexus wiki --base-url https://api.anthropic.com/v1
|
||||
|
||||
# Use the Atlas Cloud preset
|
||||
export ATLASCLOUD_API_KEY=<key>
|
||||
gitnexus wiki --provider atlascloud
|
||||
|
||||
# Force full regeneration
|
||||
gitnexus wiki --force
|
||||
|
||||
|
|
|
|||
|
|
@ -256,6 +256,7 @@ gitnexus clean # Delete index for current repo
|
|||
gitnexus clean --all --force # Delete all indexes
|
||||
gitnexus wiki [path] # Generate LLM-powered docs from knowledge graph
|
||||
gitnexus wiki --model <model> # Wiki with custom LLM model (default: minimax/minimax-m2.5)
|
||||
ATLASCLOUD_API_KEY=<key> gitnexus wiki --provider atlascloud # Atlas Cloud preset
|
||||
gitnexus wiki --base-url http://llama-box.local:8080/v1 --allow-insecure-connection llama-box.local
|
||||
# Allow an exact LAN/self-hosted HTTP LLM host; env: GITNEXUS_ALLOW_INSECURE_CONNECTION
|
||||
gitnexus doctor # Show runtime platform capabilities and embedding configuration
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ export const en = {
|
|||
'Clean parked LadybugDB recovery sidecars (missing-shadow WAL quarantines and dirty-recovery parks)',
|
||||
'help.option.wiki.force': 'Force full regeneration even if up to date',
|
||||
'help.option.wiki.provider':
|
||||
'LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)',
|
||||
'LLM provider: openai, openrouter, atlascloud, azure, custom, cursor, claude, codex, or opencode (default: openai)',
|
||||
'help.option.wiki.model': 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)',
|
||||
'help.option.wiki.baseUrl':
|
||||
'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1',
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ export const zhCN = {
|
|||
'清理已暂存的 LadybugDB 恢复 sidecar(missing-shadow WAL 隔离文件与 dirty-recovery 暂存文件)',
|
||||
'help.option.wiki.force': '即使已是最新也强制完整重新生成',
|
||||
'help.option.wiki.provider':
|
||||
'LLM 提供商:openai、openrouter、azure、custom、cursor、claude、codex 或 opencode(默认:openai)',
|
||||
'LLM 提供商:openai、openrouter、atlascloud、azure、custom、cursor、claude、codex 或 opencode(默认:openai)',
|
||||
'help.option.wiki.model': 'LLM 模型或 Azure deployment 名称(默认:minimax/minimax-m2.5)',
|
||||
'help.option.wiki.baseUrl':
|
||||
'LLM API base URL。Azure v1:https://{resource}.openai.azure.com/openai/v1',
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ program
|
|||
.option('-f, --force', 'Force full regeneration even if up to date')
|
||||
.option(
|
||||
'--provider <provider>',
|
||||
'LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)',
|
||||
'LLM provider: openai, openrouter, atlascloud, azure, custom, cursor, claude, codex, or opencode (default: openai)',
|
||||
)
|
||||
.option('--model <model>', 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)')
|
||||
.option(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ import {
|
|||
} from '../storage/repo-manager.js';
|
||||
import { WikiGenerator, type WikiOptions } from '../core/wiki/generator.js';
|
||||
import {
|
||||
ATLAS_CLOUD_BASE_URL,
|
||||
ATLAS_CLOUD_DEFAULT_MODEL,
|
||||
getProviderEnvApiKey,
|
||||
parseLLMAllowedInsecureHttpHosts,
|
||||
resolveLLMConfig,
|
||||
type LLMProvider,
|
||||
|
|
@ -206,6 +209,7 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
|
||||
// ── Resolve LLM config (with interactive fallback) ─────────────────
|
||||
// Save any CLI overrides immediately
|
||||
let switchedToAtlasCloud = false;
|
||||
if (
|
||||
options?.apiKey ||
|
||||
options?.model ||
|
||||
|
|
@ -219,6 +223,11 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
if (options.apiKey) updates.apiKey = options.apiKey;
|
||||
if (options.baseUrl) updates.baseUrl = options.baseUrl;
|
||||
if (options.provider) updates.provider = options.provider;
|
||||
if (options.provider === 'atlascloud' && existing.provider !== 'atlascloud') {
|
||||
switchedToAtlasCloud = true;
|
||||
if (!options.baseUrl) updates.baseUrl = ATLAS_CLOUD_BASE_URL;
|
||||
if (!options.model) updates.model = ATLAS_CLOUD_DEFAULT_MODEL;
|
||||
}
|
||||
if (options.apiVersion) updates.apiVersion = options.apiVersion;
|
||||
if (options.reasoningModel !== undefined) updates.isReasoningModel = options.reasoningModel;
|
||||
// Save model to appropriate field based on provider.
|
||||
|
|
@ -257,6 +266,10 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
isReasoningModel: options?.reasoningModel,
|
||||
allowedInsecureHttpHosts,
|
||||
});
|
||||
if (switchedToAtlasCloud && !options?.apiKey && !getProviderEnvApiKey('atlascloud')) {
|
||||
// Do not send a key saved for a different provider to Atlas Cloud.
|
||||
llmConfig.apiKey = '';
|
||||
}
|
||||
|
||||
// Run interactive setup if no saved config and no CLI flags provided
|
||||
// (even if env vars exist — let user explicitly choose their provider)
|
||||
|
|
@ -265,7 +278,9 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
// Non-interactive mode — need either API key or Cursor CLI
|
||||
if (!llmConfig.apiKey && !isLocalProvider(llmConfig.provider)) {
|
||||
console.log(' Error: No LLM API key found.');
|
||||
console.log(' Set OPENAI_API_KEY or GITNEXUS_API_KEY environment variable,');
|
||||
console.log(
|
||||
' Set ATLASCLOUD_API_KEY, OPENAI_API_KEY, or GITNEXUS_API_KEY environment variable,',
|
||||
);
|
||||
console.log(' or pass --api-key <key>, or use --provider cursor|claude|codex|opencode.\n');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
|
|
@ -274,7 +289,7 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
} else {
|
||||
console.log(" No LLM configured. Let's set it up.\n");
|
||||
console.log(
|
||||
' Supports OpenAI, OpenRouter, Azure, any OpenAI-compatible API, Cursor CLI, Claude CLI, Codex CLI, or OpenCode CLI.\n',
|
||||
' Supports OpenAI, OpenRouter, Atlas Cloud, Azure, any OpenAI-compatible API, Cursor CLI, Claude CLI, Codex CLI, or OpenCode CLI.\n',
|
||||
);
|
||||
|
||||
// Check if local agent CLIs are available.
|
||||
|
|
@ -291,8 +306,9 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
console.log(' [1] OpenAI (api.openai.com)');
|
||||
console.log(' [2] OpenRouter (openrouter.ai)');
|
||||
console.log(' [3] Azure OpenAI');
|
||||
console.log(' [4] Custom endpoint');
|
||||
let nextChoice = 5;
|
||||
console.log(' [4] Atlas Cloud (api.atlascloud.ai)');
|
||||
console.log(' [5] Custom endpoint');
|
||||
let nextChoice = 6;
|
||||
if (hasCursor) {
|
||||
const choice = String(nextChoice++);
|
||||
localChoices.push({
|
||||
|
|
@ -413,12 +429,16 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
provider: 'azure',
|
||||
};
|
||||
} else {
|
||||
// OpenAI-compatible provider (OpenAI, OpenRouter, Custom)
|
||||
// OpenAI-compatible provider (OpenAI, OpenRouter, Atlas Cloud, Custom)
|
||||
if (choice === '2') {
|
||||
baseUrl = 'https://openrouter.ai/api/v1';
|
||||
defaultModel = 'minimax/minimax-m2.5';
|
||||
provider = 'openrouter';
|
||||
} else if (choice === '4') {
|
||||
baseUrl = ATLAS_CLOUD_BASE_URL;
|
||||
defaultModel = ATLAS_CLOUD_DEFAULT_MODEL;
|
||||
provider = 'atlascloud';
|
||||
} else if (choice === '5') {
|
||||
baseUrl = await prompt(' Base URL (e.g. http://localhost:11434/v1): ');
|
||||
if (!baseUrl) {
|
||||
console.log('\n No URL provided. Aborting.\n');
|
||||
|
|
@ -438,7 +458,7 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
|
|||
const model = modelInput || defaultModel;
|
||||
|
||||
// API key — pre-fill hint if env var exists
|
||||
const envKey = process.env.GITNEXUS_API_KEY || process.env.OPENAI_API_KEY || '';
|
||||
const envKey = getProviderEnvApiKey(provider);
|
||||
if (envKey) {
|
||||
const masked = envKey.slice(0, 6) + '...' + envKey.slice(-4);
|
||||
const useEnv = await prompt(` Use existing env key (${masked})? (Y/n): `);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { CircuitOpenError, ResilientFetchExhaustedError, resilientFetch } from '
|
|||
* LLM Client for Wiki Generation
|
||||
*
|
||||
* OpenAI-compatible API client using native fetch.
|
||||
* Supports OpenAI, Azure, LiteLLM, Ollama, and any OpenAI-compatible endpoint.
|
||||
* Supports OpenAI, Atlas Cloud, Azure, LiteLLM, Ollama, and any OpenAI-compatible endpoint.
|
||||
*
|
||||
* Config priority: CLI flags > env vars > defaults
|
||||
*/
|
||||
|
|
@ -12,6 +12,7 @@ import { CircuitOpenError, ResilientFetchExhaustedError, resilientFetch } from '
|
|||
export type LLMProvider =
|
||||
| 'openai'
|
||||
| 'openrouter'
|
||||
| 'atlascloud'
|
||||
| 'azure'
|
||||
| 'custom'
|
||||
| 'cursor'
|
||||
|
|
@ -19,6 +20,9 @@ export type LLMProvider =
|
|||
| 'codex'
|
||||
| 'opencode';
|
||||
|
||||
export const ATLAS_CLOUD_BASE_URL = 'https://api.atlascloud.ai/v1';
|
||||
export const ATLAS_CLOUD_DEFAULT_MODEL = 'deepseek-ai/deepseek-v4-pro';
|
||||
|
||||
export interface LLMConfig {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
|
|
@ -45,6 +49,13 @@ export interface LLMResponse {
|
|||
completionTokens?: number;
|
||||
}
|
||||
|
||||
export function getProviderEnvApiKey(provider: LLMProvider | undefined): string {
|
||||
if (provider === 'atlascloud') {
|
||||
return process.env.ATLASCLOUD_API_KEY || process.env.GITNEXUS_API_KEY || '';
|
||||
}
|
||||
return process.env.GITNEXUS_API_KEY || process.env.OPENAI_API_KEY || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve LLM configuration from env vars, saved config, and optional overrides.
|
||||
* Priority: overrides (CLI flags) > env vars > ~/.gitnexus/config.json > error
|
||||
|
|
@ -55,6 +66,8 @@ export async function resolveLLMConfig(overrides?: Partial<LLMConfig>): Promise<
|
|||
const { loadCLIConfig } = await import('../../storage/repo-manager.js');
|
||||
const savedConfig = await loadCLIConfig();
|
||||
const savedProvider = overrides?.provider ?? savedConfig.provider;
|
||||
const providerChanged =
|
||||
overrides?.provider !== undefined && overrides.provider !== savedConfig.provider;
|
||||
const savedLocalModel =
|
||||
savedProvider === 'cursor'
|
||||
? savedConfig.cursorModel
|
||||
|
|
@ -73,23 +86,26 @@ export async function resolveLLMConfig(overrides?: Partial<LLMConfig>): Promise<
|
|||
|
||||
const apiKey =
|
||||
overrides?.apiKey ||
|
||||
process.env.GITNEXUS_API_KEY ||
|
||||
process.env.OPENAI_API_KEY ||
|
||||
savedConfig.apiKey ||
|
||||
getProviderEnvApiKey(savedProvider) ||
|
||||
(providerChanged ? undefined : savedConfig.apiKey) ||
|
||||
'';
|
||||
const defaultBaseUrl =
|
||||
savedProvider === 'atlascloud' ? ATLAS_CLOUD_BASE_URL : 'https://openrouter.ai/api/v1';
|
||||
const defaultModel =
|
||||
savedProvider === 'atlascloud' ? ATLAS_CLOUD_DEFAULT_MODEL : 'minimax/minimax-m2.5';
|
||||
|
||||
return {
|
||||
apiKey,
|
||||
baseUrl:
|
||||
overrides?.baseUrl ||
|
||||
process.env.GITNEXUS_LLM_BASE_URL ||
|
||||
savedConfig.baseUrl ||
|
||||
'https://openrouter.ai/api/v1',
|
||||
(providerChanged ? undefined : savedConfig.baseUrl) ||
|
||||
defaultBaseUrl,
|
||||
model:
|
||||
overrides?.model ||
|
||||
(localProvider ? undefined : process.env.GITNEXUS_MODEL) ||
|
||||
savedLocalModel ||
|
||||
(localProvider ? '' : savedConfig.model || 'minimax/minimax-m2.5'),
|
||||
(localProvider ? '' : (providerChanged ? undefined : savedConfig.model) || defaultModel),
|
||||
maxTokens: overrides?.maxTokens ?? 16_384,
|
||||
temperature: overrides?.temperature ?? 0,
|
||||
provider: savedProvider ?? 'openai',
|
||||
|
|
|
|||
|
|
@ -1877,6 +1877,7 @@ export interface CLIConfig {
|
|||
provider?:
|
||||
| 'openai'
|
||||
| 'openrouter'
|
||||
| 'atlascloud'
|
||||
| 'azure'
|
||||
| 'custom'
|
||||
| 'cursor'
|
||||
|
|
|
|||
|
|
@ -163,6 +163,13 @@ describe('CLI help surface', () => {
|
|||
expect(result.stdout).toContain('-c, --coding-agent <agents>');
|
||||
});
|
||||
|
||||
it('wiki help lists the Atlas Cloud provider preset', () => {
|
||||
const result = runHelp('wiki');
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain('atlascloud');
|
||||
});
|
||||
|
||||
it('localizes every registered CLI command and option description in zh-CN help', () => {
|
||||
const zhHelpOutput = allHelpCommands
|
||||
.map((args) => {
|
||||
|
|
|
|||
|
|
@ -131,9 +131,22 @@ describe('detectLocalCLI', () => {
|
|||
|
||||
describe('resolveLLMConfig', () => {
|
||||
let tmpDir: string;
|
||||
const envKeys = [
|
||||
'ATLASCLOUD_API_KEY',
|
||||
'GITNEXUS_API_KEY',
|
||||
'OPENAI_API_KEY',
|
||||
'GITNEXUS_LLM_BASE_URL',
|
||||
'GITNEXUS_MODEL',
|
||||
] as const;
|
||||
let originalEnv: Record<(typeof envKeys)[number], string | undefined>;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
originalEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])) as Record<
|
||||
(typeof envKeys)[number],
|
||||
string | undefined
|
||||
>;
|
||||
for (const key of envKeys) delete process.env[key];
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'wiki-test-config-'));
|
||||
// Create empty config so loadCLIConfig returns {}
|
||||
const configDir = path.join(tmpDir, '.gitnexus');
|
||||
|
|
@ -143,6 +156,11 @@ describe('resolveLLMConfig', () => {
|
|||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
for (const key of envKeys) {
|
||||
const value = originalEnv[key];
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
|
@ -250,6 +268,27 @@ describe('resolveLLMConfig', () => {
|
|||
expect(config.baseUrl).toBe('https://openrouter.ai/api/v1');
|
||||
});
|
||||
|
||||
it('uses Atlas Cloud defaults and its provider-specific API key', async () => {
|
||||
process.env.ATLASCLOUD_API_KEY = 'atlas-key';
|
||||
process.env.OPENAI_API_KEY = 'openai-key';
|
||||
vi.doMock('../../src/storage/repo-manager.js', () => ({
|
||||
loadCLIConfig: vi.fn().mockResolvedValue({
|
||||
provider: 'openai',
|
||||
apiKey: 'saved-openai-key',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4o-mini',
|
||||
}),
|
||||
}));
|
||||
|
||||
const { resolveLLMConfig } = await import('../../src/core/wiki/llm-client.js');
|
||||
const config = await resolveLLMConfig({ provider: 'atlascloud' });
|
||||
|
||||
expect(config.provider).toBe('atlascloud');
|
||||
expect(config.apiKey).toBe('atlas-key');
|
||||
expect(config.baseUrl).toBe('https://api.atlascloud.ai/v1');
|
||||
expect(config.model).toBe('deepseek-ai/deepseek-v4-pro');
|
||||
});
|
||||
|
||||
it('CLI overrides take priority over saved config', async () => {
|
||||
vi.doMock('../../src/storage/repo-manager.js', () => ({
|
||||
loadCLIConfig: vi.fn().mockResolvedValue({
|
||||
|
|
@ -579,6 +618,7 @@ describe('wikiCommand --timeout mapping', () => {
|
|||
|
||||
async function loadWikiCommandHarness() {
|
||||
let capturedConfig: Record<string, unknown> | undefined;
|
||||
const saveCLIConfig = vi.fn();
|
||||
const resolveLLMConfig = vi.fn().mockImplementation((overrides = {}) =>
|
||||
Promise.resolve({
|
||||
apiKey: 'sk-test',
|
||||
|
|
@ -614,7 +654,7 @@ describe('wikiCommand --timeout mapping', () => {
|
|||
model: 'gpt-4o',
|
||||
provider: 'openai',
|
||||
}),
|
||||
saveCLIConfig: vi.fn(),
|
||||
saveCLIConfig,
|
||||
}));
|
||||
vi.doMock('../../src/core/wiki/llm-client.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../src/core/wiki/llm-client.js')>();
|
||||
|
|
@ -647,9 +687,31 @@ describe('wikiCommand --timeout mapping', () => {
|
|||
consoleSpy,
|
||||
getCapturedConfig: () => capturedConfig,
|
||||
resolveLLMConfig,
|
||||
saveCLIConfig,
|
||||
};
|
||||
}
|
||||
|
||||
it('switches to Atlas Cloud without overwriting a saved key from another provider', async () => {
|
||||
const originalAtlasKey = process.env.ATLASCLOUD_API_KEY;
|
||||
delete process.env.ATLASCLOUD_API_KEY;
|
||||
const harness = await loadWikiCommandHarness();
|
||||
|
||||
try {
|
||||
await harness.wikiCommand('/tmp/repo', { provider: 'atlascloud' });
|
||||
|
||||
expect(harness.saveCLIConfig).toHaveBeenCalledWith({
|
||||
apiKey: 'sk-test',
|
||||
baseUrl: 'https://api.atlascloud.ai/v1',
|
||||
model: 'deepseek-ai/deepseek-v4-pro',
|
||||
provider: 'atlascloud',
|
||||
});
|
||||
expect(harness.getCapturedConfig()?.apiKey).toBe('');
|
||||
} finally {
|
||||
if (originalAtlasKey === undefined) delete process.env.ATLASCLOUD_API_KEY;
|
||||
else process.env.ATLASCLOUD_API_KEY = originalAtlasKey;
|
||||
}
|
||||
});
|
||||
|
||||
it('maps --timeout seconds to requestTimeoutMs before constructing WikiGenerator', async () => {
|
||||
const harness = await loadWikiCommandHarness();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue