mirror of
https://github.com/hanzoai/extension.git
synced 2026-10-31 21:57:06 +00:00
VS Code's chat participant, agent and llm tools called api.openai.com, api.anthropic.com, Gemini, Mistral and Cohere directly with the user's own provider keys, and its Hanzo path posted to api.hanzo.ai/ext/v1/llm/chat and /llm/complete, which answer 404. The defaults they sent (gpt-4, claude-3-opus-20240229, zen-70b, zen1, hanzo-embed-v1) are not ids api.hanzo.ai/v1/models lists. Every hosted call now goes to POST api.hanzo.ai/v1/chat/completions through one client, src/api/chat.ts, as zen5 unless a model is named. The bearer is the IAM session, else HANZO_API_KEY or the hanzo.llm.hanzo.apiKey setting. hanzo.llm.hanzo.model replaces the OpenAI and Anthropic settings. src/mcp/tools/agent.ts goes: the ai-tools agent registers after it under the same name, so it never ran. The llm consensus tool, which only printed advice, now asks zen5 and zen5-pro. The zen tool no longer goes through core/backend-abstraction, which leaves that module and unified-rxdb-backend without a caller; their completion and embedding paths point at the gateway too. Ollama and LM Studio stay; they run on the user's machine. hanzo-mcp defaulted chat to "auto", which is not in /v1/models; the gateway resolves it only when router auto-routing is on for the org (hanzoai/ai controllers/auto_route.go:118-160), and cloud's FallbackModel comment (hanzo-inc/cloud model.go:111-121) records it never demonstrably serving. Chat now defaults to zen5 and embeddings to zen-embedding. /v1/models lists no image or speech model, so those actions require the caller to name one instead of sending dall-e-3 or tts-1. The files and fine-tuning actions go: /v1/files and /v1/fine_tuning/jobs answer 404. Claude-Session: https://claude.ai/code/session_01Dc8DXc3fEQCRHUa6XWK4Ku
92 lines
4.0 KiB
TypeScript
92 lines
4.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
vi.hoisted(() => {
|
|
delete process.env.API_URL;
|
|
process.env.HANZO_API_KEY = 'sk-test';
|
|
});
|
|
|
|
import { llmRouterTool, spawnAgentTool } from '../src/orchestration/agent-tools';
|
|
import { apiTool } from '../src/tools/hanzo-cloud';
|
|
import { thinkTool } from '../src/tools/think';
|
|
|
|
// Every model is reached through the gateway; each row is the one request an action makes.
|
|
const V1 = 'https://api.hanzo.ai/v1';
|
|
type Call = [url: string, model: unknown];
|
|
|
|
let calls: Call[];
|
|
|
|
beforeEach(() => {
|
|
calls = [];
|
|
vi.stubGlobal('fetch', vi.fn(async (url: string, init: any = {}) => {
|
|
const body = init.body === undefined ? {} : JSON.parse(init.body);
|
|
expect(init.headers.Authorization).toBe('Bearer sk-test');
|
|
calls.push([url, body.model]);
|
|
return new Response(JSON.stringify({ choices: [{ message: { content: 'ok' } }], data: [{ embedding: [0.5] }] }));
|
|
}));
|
|
});
|
|
|
|
const api: Array<[Record<string, unknown>, Call[]]> = [
|
|
[{ action: 'chat', message: 'hi' }, [[`${V1}/chat/completions`, 'zen5']]],
|
|
[{ action: 'chat', message: 'hi', model: 'zen5-pro' }, [[`${V1}/chat/completions`, 'zen5-pro']]],
|
|
[{ action: 'embeddings', input: 'hi' }, [[`${V1}/embeddings`, 'zen-embedding']]],
|
|
[{ action: 'images', prompt: 'a cat', model: 'img' }, [[`${V1}/images/generations`, 'img']]],
|
|
[{ action: 'audio', input: 'hi', model: 'voice' }, [[`${V1}/audio/speech`, 'voice']]],
|
|
];
|
|
|
|
describe('api tool', () => {
|
|
it.each(api)('%o', async (args, want) => {
|
|
const res = await apiTool.handler(args);
|
|
expect(res.isError).toBeUndefined();
|
|
expect(calls).toEqual(want);
|
|
});
|
|
|
|
// /v1/models lists no image or speech model, so there is none to default to.
|
|
it.each(['images', 'audio'])('%s names its model or sends nothing', async (action) => {
|
|
const res = await apiTool.handler({ action, prompt: 'x', input: 'x' });
|
|
expect(res.isError).toBe(true);
|
|
expect(calls).toEqual([]);
|
|
});
|
|
|
|
it('offers only actions the gateway serves', () => {
|
|
expect((apiTool.inputSchema as any).properties.action.enum).toEqual(['models', 'chat', 'embeddings', 'images', 'audio', 'usage', 'health']);
|
|
});
|
|
});
|
|
|
|
const think: Array<[Record<string, unknown>, Call[]]> = [
|
|
[{ action: 'agent', task: 't' }, [[`${V1}/chat/completions`, 'zen5']]],
|
|
[{ action: 'summarize', text: 't' }, [[`${V1}/chat/completions`, 'zen5']]],
|
|
[{ action: 'agent', task: 't', model: 'zen5-coder' }, [[`${V1}/chat/completions`, 'zen5-coder']]],
|
|
[{ action: 'consensus', question: 'q' }, [[`${V1}/chat/completions`, 'zen5'], [`${V1}/chat/completions`, 'zen5-pro']]],
|
|
[{ action: 'embed', text: 't' }, [[`${V1}/embeddings`, 'zen-embedding']]],
|
|
];
|
|
|
|
describe('think tool', () => {
|
|
it.each(think)('%o', async (args, want) => {
|
|
const res = await thinkTool.handler(args);
|
|
expect(res.isError).toBeUndefined();
|
|
expect(calls).toEqual(want);
|
|
});
|
|
});
|
|
|
|
describe('orchestration', () => {
|
|
it('spawns agents on zen5 and invents no fallback chain', () => {
|
|
expect((spawnAgentTool.inputSchema as any).properties.model.default).toBe('zen5');
|
|
expect((llmRouterTool.inputSchema as any).properties.fallbackChain.default).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('no upstream provider', () => {
|
|
const upstream = /api\.openai\.com|api\.anthropic\.com|generativelanguage\.googleapis\.com|api\.cohere\.(ai|com)|api\.mistral\.ai|api\.groq\.com|api\.together\.(xyz|ai)|api\.replicate\.com|OPENAI_API_KEY|ANTHROPIC_API_KEY/;
|
|
const files = (dir: string): string[] => fs.readdirSync(dir, { withFileTypes: true }).flatMap(e =>
|
|
e.isDirectory() ? files(path.join(dir, e.name)) : /\.ts$/.test(e.name) ? [path.join(dir, e.name)] : []);
|
|
|
|
it('is named in src', () => {
|
|
const src = path.resolve(__dirname, '../src');
|
|
const scanned = files(src);
|
|
expect(scanned).toContain(path.join(src, 'tools', 'think.ts'));
|
|
expect(scanned.filter(f => upstream.test(fs.readFileSync(f, 'utf8'))).map(f => path.relative(src, f))).toEqual([]);
|
|
});
|
|
});
|