Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .gemini_security/graphiti.db
Binary file not shown.
Binary file added .gemini_security/pulse.db
Binary file not shown.
Binary file added .gemini_security/second_brain.db
Binary file not shown.
45 changes: 45 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,48 @@ Project artifacts live under `.qwen/`:
| `.qwen/pr-reviews/` | PR review notes |
| `.qwen/investigations/` | Structured debugging journals |
| `.qwen/scripts/` | Utility scripts |

<!-- gitnexus:start -->

# GitNexus — Code Intelligence

This project is indexed by GitNexus as **qwen-code** (56821 symbols, 105059 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

## Always Do

- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.

## Never Do

- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.

## Resources

| Resource | Use for |
| ------------------------------------------ | ---------------------------------------- |
| `gitnexus://repo/qwen-code/context` | Codebase overview, check index freshness |
| `gitnexus://repo/qwen-code/clusters` | All functional areas |
| `gitnexus://repo/qwen-code/processes` | All execution flows |
| `gitnexus://repo/qwen-code/process/{name}` | Step-by-step execution trace |

## CLI

| Task | Read this skill file |
| -------------------------------------------- | ----------------------------------------------------------- |
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |

<!-- gitnexus:end -->
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export default {
'(new, partial)': '(新增,部分統計)',
'(deleted)': '(已刪除)',
'(binary, deleted)': '(二進位,已刪除)',

'Manage subagents for specialized task delegation.':
'管理用於專門任務委派的子智能體',
'Manage existing subagents (view, edit, delete).':
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,10 @@ export default {
'(new)': '(新增)',
'(new, partial)': '(新增,部分统计)',
'(deleted)': '(已删除)',
'(binary, deleted)': '(二进制,已删除)',
'(binary, deleted)': '(已删除)',

// ============================================================================

// Commands - Agents
// ============================================================================
'Manage subagents for specialized task delegation.':
Expand Down
103 changes: 58 additions & 45 deletions packages/core/src/core/baseLlmClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ describe('BaseLlmClient', () => {
embedContent: vi.fn(),
} as unknown as Mocked<ContentGenerator>;

const getResolvedModel = vi.fn();
const getResolvedModelAcrossAuthTypes = vi.fn();
let crossProviderConfig: Mocked<Config>;

beforeEach(() => {
Expand All @@ -486,7 +486,7 @@ describe('BaseLlmClient', () => {
fastGenerateContent.mockReset();
mockCreateContentGenerator.mockReset();
mockBuildAgentContentGeneratorConfig.mockReset();
getResolvedModel.mockReset();
getResolvedModelAcrossAuthTypes.mockReset();

mockCreateContentGenerator.mockResolvedValue(fastContentGenerator);
mockBuildAgentContentGeneratorConfig.mockReturnValue({
Expand All @@ -503,7 +503,9 @@ describe('BaseLlmClient', () => {
getModel: vi.fn().mockReturnValue('main-model'),
getFastModel: vi.fn().mockReturnValue(undefined),
getFastModelForSideQuery: vi.fn().mockReturnValue(undefined),
getModelsConfig: vi.fn().mockReturnValue({ getResolvedModel }),
getModelsConfig: vi
.fn()
.mockReturnValue({ getResolvedModelAcrossAuthTypes }),
} as unknown as Mocked<Config>;
});

Expand All @@ -514,23 +516,24 @@ describe('BaseLlmClient', () => {

expect(resolved.contentGenerator).toBe(mockContentGenerator);
expect(resolved.retryAuthType).toBe(AuthType.QWEN_OAUTH);
expect(getResolvedModel).not.toHaveBeenCalled();
expect(getResolvedModelAcrossAuthTypes).not.toHaveBeenCalled();
expect(mockCreateContentGenerator).not.toHaveBeenCalled();
});

it('builds a per-model generator when model differs and is registered under another authType', async () => {
// Main authType is QWEN_OAUTH; fast model only resolves under USE_ANTHROPIC.
getResolvedModel.mockImplementation((authType: string, model: string) => {
if (authType === AuthType.QWEN_OAUTH) return undefined;
if (authType === AuthType.USE_ANTHROPIC && model === fastModel) {
return {
authType: AuthType.USE_ANTHROPIC,
envKey: 'ANTHROPIC_API_KEY',
baseUrl: 'https://api.anthropic.com',
};
}
return undefined;
});
getResolvedModelAcrossAuthTypes.mockImplementation(
(model: string, _preferredAuthType?: string) => {
if (model === fastModel) {
return {
authType: AuthType.USE_ANTHROPIC,
envKey: 'ANTHROPIC_API_KEY',
baseUrl: 'https://api.anthropic.com',
};
}
return undefined;
},
);

const c = new BaseLlmClient(mockContentGenerator, crossProviderConfig);

Expand All @@ -550,7 +553,7 @@ describe('BaseLlmClient', () => {
});

it('caches the per-model generator across resolveForModel calls', async () => {
getResolvedModel.mockReturnValue({
getResolvedModelAcrossAuthTypes.mockReturnValue({
authType: AuthType.USE_ANTHROPIC,
envKey: 'ANTHROPIC_API_KEY',
});
Expand All @@ -564,7 +567,7 @@ describe('BaseLlmClient', () => {
});

it('clearPerModelGeneratorCache forces a rebuild on the next call', async () => {
getResolvedModel.mockReturnValue({
getResolvedModelAcrossAuthTypes.mockReturnValue({
authType: AuthType.USE_ANTHROPIC,
envKey: 'ANTHROPIC_API_KEY',
});
Expand All @@ -578,7 +581,7 @@ describe('BaseLlmClient', () => {
});

it('falls back to the main generator when the target model is not in the registry', async () => {
getResolvedModel.mockReturnValue(undefined);
getResolvedModelAcrossAuthTypes.mockReturnValue(undefined);

const c = new BaseLlmClient(mockContentGenerator, crossProviderConfig);
const resolved = await c.resolveForModel('unknown-model');
Expand All @@ -590,7 +593,7 @@ describe('BaseLlmClient', () => {
});

it('falls back to the main generator when createContentGenerator throws', async () => {
getResolvedModel.mockReturnValue({
getResolvedModelAcrossAuthTypes.mockReturnValue({
authType: AuthType.USE_ANTHROPIC,
envKey: 'ANTHROPIC_API_KEY',
});
Expand All @@ -608,7 +611,7 @@ describe('BaseLlmClient', () => {
});

it('generateJson routes through the per-model generator and forwards retry authType', async () => {
getResolvedModel.mockReturnValue({
getResolvedModelAcrossAuthTypes.mockReturnValue({
authType: AuthType.USE_ANTHROPIC,
envKey: 'ANTHROPIC_API_KEY',
});
Expand Down Expand Up @@ -638,16 +641,21 @@ describe('BaseLlmClient', () => {
});

it('generateJson accepts authType-qualified selectors and sends the bare model id', async () => {
getResolvedModel.mockImplementation((authType: string, model: string) => {
if (authType === AuthType.USE_OPENAI && model === 'shared-model') {
return {
id: 'shared-model',
authType: AuthType.USE_OPENAI,
envKey: 'OPENAI_API_KEY',
};
}
return undefined;
});
getResolvedModelAcrossAuthTypes.mockImplementation(
(model: string, preferredAuthType?: string) => {
if (
preferredAuthType === AuthType.USE_OPENAI &&
model === 'shared-model'
) {
return {
id: 'shared-model',
authType: AuthType.USE_OPENAI,
envKey: 'OPENAI_API_KEY',
};
}
return undefined;
},
);
fastGenerateContent.mockResolvedValue(
createMockResponseWithFunctionCall({ ok: true }),
);
Expand All @@ -665,9 +673,9 @@ describe('BaseLlmClient', () => {
promptId: 'test',
});

expect(getResolvedModel).toHaveBeenCalledWith(
AuthType.USE_OPENAI,
expect(getResolvedModelAcrossAuthTypes).toHaveBeenCalledWith(
'shared-model',
AuthType.USE_OPENAI,
);
expect(mockBuildAgentContentGeneratorConfig).toHaveBeenCalledWith(
crossProviderConfig,
Expand All @@ -684,16 +692,21 @@ describe('BaseLlmClient', () => {
crossProviderConfig.getFastModelForSideQuery.mockReturnValue(
'openai:shared-model',
);
getResolvedModel.mockImplementation((authType: string, model: string) => {
if (authType === AuthType.USE_OPENAI && model === 'shared-model') {
return {
id: 'shared-model',
authType: AuthType.USE_OPENAI,
envKey: 'OPENAI_API_KEY',
};
}
return undefined;
});
getResolvedModelAcrossAuthTypes.mockImplementation(
(model: string, preferredAuthType?: string) => {
if (
preferredAuthType === AuthType.USE_OPENAI &&
model === 'shared-model'
) {
return {
id: 'shared-model',
authType: AuthType.USE_OPENAI,
envKey: 'OPENAI_API_KEY',
};
}
return undefined;
},
);
fastGenerateContent.mockResolvedValue(
createMockResponseWithFunctionCall({ ok: true }),
);
Expand All @@ -711,9 +724,9 @@ describe('BaseLlmClient', () => {
promptId: 'test',
});

expect(getResolvedModel).toHaveBeenCalledWith(
AuthType.USE_OPENAI,
expect(getResolvedModelAcrossAuthTypes).toHaveBeenCalledWith(
'shared-model',
AuthType.USE_OPENAI,
);
expect(mockBuildAgentContentGeneratorConfig).toHaveBeenCalledWith(
crossProviderConfig,
Expand All @@ -727,7 +740,7 @@ describe('BaseLlmClient', () => {
});

it('generateText routes through the per-model generator and forwards retry authType', async () => {
getResolvedModel.mockReturnValue({
getResolvedModelAcrossAuthTypes.mockReturnValue({
authType: AuthType.USE_ANTHROPIC,
envKey: 'ANTHROPIC_API_KEY',
});
Expand Down
37 changes: 5 additions & 32 deletions packages/core/src/core/baseLlmClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,45 +410,18 @@ export class BaseLlmClient {
this.perModelGeneratorCache.clear();
}

/**
* Resolve a model across all authTypes. Handles the case where the target
* model is registered under a different authType than the main model
* (e.g. main=QWEN_OAUTH, fast=USE_ANTHROPIC).
*/
private resolveModelAcrossAuthTypes(
model: string,
selector: ResolvedModelId | undefined,
): ResolvedModelConfig | undefined {
const modelsConfig = this.config.getModelsConfig?.();
if (!modelsConfig) return undefined;
if (!selector) return undefined;
const modelId = selector.modelId;

if (selector.authType) {
return modelsConfig.getResolvedModel(selector.authType, modelId);
}

const allAuthTypes: AuthType[] = [
AuthType.QWEN_OAUTH,
AuthType.USE_OPENAI,
AuthType.USE_VERTEX_AI,
AuthType.USE_ANTHROPIC,
AuthType.USE_GEMINI,
];
if (!modelsConfig || !selector) return undefined;

const mainAuthType = this.config.getContentGeneratorConfig()?.authType;
if (mainAuthType) {
const resolved = modelsConfig.getResolvedModel(mainAuthType, modelId);
if (resolved) return resolved;
}

for (const authType of allAuthTypes) {
if (authType === mainAuthType) continue;
const resolved = modelsConfig.getResolvedModel(authType, modelId);
if (resolved) return resolved;
}

return undefined;
return modelsConfig.getResolvedModelAcrossAuthTypes(
selector.modelId,
selector.authType ?? mainAuthType,
);
}

private async createContentGeneratorForModel(
Expand Down
Loading
Loading