Files
zeekay 71f1e15328 VS Code and MCP reach every model through Hanzo
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
2026-09-29 12:55:02 -07:00

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([]);
});
});