mirror of
https://github.com/hanzoai/extension.git
synced 2026-10-31 21:57:06 +00:00
Office, Outlook, PDF, Clio, Meetings, Google Workspace and Salesforce defaulted to zen-1, Teams offered zen-eco-3b, zen-mini-8b and zen-4b, and JetBrains defaulted to claude-3-5-sonnet-20241022 with a fallback list of retired upstream ids. None of those are in api.hanzo.ai/v1/models, so a first request on each of them named a model the gateway does not serve. zen5 is the id the catalog itself marks as the default: catalog.hanzo.ai /v1/pricing lists it as "Zen5 (Default)", "the everyday Zen5 chat model", and nineteen other surfaces here already defaulted to it. Teams now offers the catalog's Zen 5 family with zen5 first. JetBrains preselects zen5 when the live list carries it, and offers only zen5 when it cannot load the list, rather than a fabricated one. Claude-Session: https://claude.ai/code/session_01Dc8DXc3fEQCRHUa6XWK4Ku
79 lines
2.6 KiB
TypeScript
79 lines
2.6 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { complete, listModels, type AiClient } from '../src/hanzo';
|
|
|
|
// A hand-built stub of the exact client slice hanzo.ts uses, so we assert on
|
|
// the request we shape and the response we read — no network.
|
|
function stubClient(overrides: Partial<AiClient> = {}): {
|
|
client: AiClient;
|
|
create: ReturnType<typeof vi.fn>;
|
|
list: ReturnType<typeof vi.fn>;
|
|
} {
|
|
const create = vi.fn(async () => ({
|
|
choices: [{ message: { content: 'hello from hanzo' } }],
|
|
}));
|
|
const list = vi.fn(async () => ({ data: [{ id: 'zen5-flash' }, { id: 'zen5' }] }));
|
|
const client: AiClient = {
|
|
chat: { completions: { create } },
|
|
models: { list },
|
|
...overrides,
|
|
} as AiClient;
|
|
return { client, create, list };
|
|
}
|
|
|
|
describe('complete', () => {
|
|
it('shapes the request with model, messages, and max_tokens', async () => {
|
|
const { client, create } = stubClient();
|
|
const text = await complete(client, {
|
|
model: 'zen5',
|
|
messages: [{ role: 'user', content: 'hi' }],
|
|
maxTokens: 256,
|
|
temperature: 0.3,
|
|
});
|
|
expect(text).toBe('hello from hanzo');
|
|
expect(create).toHaveBeenCalledWith({
|
|
model: 'zen5',
|
|
messages: [{ role: 'user', content: 'hi' }],
|
|
max_tokens: 256,
|
|
temperature: 0.3,
|
|
});
|
|
});
|
|
|
|
it('omits temperature and max_tokens when not provided', async () => {
|
|
const { client, create } = stubClient();
|
|
await complete(client, { model: 'zen5-flash', messages: [] });
|
|
expect(create).toHaveBeenCalledWith({ model: 'zen5-flash', messages: [] });
|
|
});
|
|
|
|
it('throws on an empty completion', async () => {
|
|
const { client } = stubClient({
|
|
chat: { completions: { create: vi.fn(async () => ({ choices: [] })) } },
|
|
} as Partial<AiClient>);
|
|
await expect(
|
|
complete(client, { model: 'x', messages: [] }),
|
|
).rejects.toThrow(/empty completion/i);
|
|
});
|
|
|
|
it('throws when the message content is null', async () => {
|
|
const { client } = stubClient({
|
|
chat: {
|
|
completions: { create: vi.fn(async () => ({ choices: [{ message: { content: null } }] })) },
|
|
},
|
|
} as Partial<AiClient>);
|
|
await expect(complete(client, { model: 'x', messages: [] })).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('listModels', () => {
|
|
it('returns the model ids from the catalog', async () => {
|
|
const { client } = stubClient();
|
|
expect(await listModels(client)).toEqual(['zen5-flash', 'zen5']);
|
|
});
|
|
|
|
it('returns [] when the catalog is shaped unexpectedly', async () => {
|
|
const { client } = stubClient({
|
|
models: { list: vi.fn(async () => ({})) },
|
|
} as Partial<AiClient>);
|
|
expect(await listModels(client)).toEqual([]);
|
|
});
|
|
});
|