Compare commits

...
1 Commits
Author SHA1 Message Date
zandClaude Opus 4.8 6fa2d3e224 feat(image): text-to-image via the Hanzo gateway, metered per-user
Enables 'generate an image of X' to render a real image inline through the
DALLE3 agent tool pointed at the Hanzo gateway (/v1/images/generations):
- DALLE3.js: model is configurable (DALLE3_MODEL, e.g. zen3-image); DALL-E-3-only
  knobs (quality/style) are sent ONLY for a dall-e model so the Hanzo image
  backend never sees a param it would reject. Agent path already fetches the
  result server-side and returns it inline (upstream host never reaches client).
- handleTools.js: dalle is now a custom constructor that injects the signed-in
  user's PER-USER hk- key (resolveHanzoCloudKey) so image generation is metered
  to them — mirroring the chat per-user key path. Guests keep the shared key;
  an authed user whose key can't be resolved FAILS CLOSED (no shared-org spend).

Deploy also sets env DALLE_REVERSE_PROXY=https://api.hanzo.ai/v1/images/generations
and DALLE3_MODEL=zen3-image. Zen-brand model id only; upstream never surfaced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:46:03 -07:00
2 changed files with 44 additions and 9 deletions
+18 -8
View File
@@ -143,16 +143,26 @@ class DALLE3 extends Tool {
throw new Error('Missing required field: prompt');
}
// Model is configurable (DALLE3_MODEL) so this tool drives the Hanzo image
// family (e.g. zen3-image) through the same OpenAI /images/generations shape.
// `quality` and `style` are DALL-E-3-only knobs — send them ONLY for a dall-e
// model so a non-DALL-E backend never sees a parameter it may reject.
const model = process.env.DALLE3_MODEL || 'dall-e-3';
const isDallE = model.startsWith('dall-e');
const genParams = {
model,
size,
prompt: this.replaceUnwantedChars(prompt),
n: 1,
};
if (isDallE) {
genParams.quality = quality;
genParams.style = style;
}
let resp;
try {
resp = await this.openai.images.generate({
model: 'dall-e-3',
quality,
style,
size,
prompt: this.replaceUnwantedChars(prompt),
n: 1,
});
resp = await this.openai.images.generate(genParams);
} catch (error) {
logger.error('[DALL-E-3] Problem generating the image:', error);
return this
+26 -1
View File
@@ -12,6 +12,8 @@ const {
loadWebSearchAuth,
buildImageToolContext,
buildWebSearchContext,
resolveHanzoCloudKey,
isHanzoPerUserKeyEnabled,
} = require('@hanzochat/api');
const { getMCPServersRegistry } = require('~/config');
const {
@@ -232,7 +234,30 @@ const loadTools = async ({
const requestedTools = {};
if (functions === true) {
toolConstructors.dalle = DALLE3;
// dalle (image generation) drives the Hanzo image family via a PER-USER hk-
// key so generation is metered to the signed-in user — mirroring the chat
// per-user key path in custom/initialize.ts. A guest keeps the shared env
// key; an authenticated user whose key cannot be resolved FAILS CLOSED
// (throws) rather than silently billing the shared org. (Custom constructors
// take precedence over the generic toolConstructors path in the loop below.)
customConstructors.dalle = async () => {
const authFields = getAuthFields('dalle');
const authValues = await loadAuthValues({ userId: user, authFields });
const billingUser = options.req?.user;
const isAuthenticatedUser = Boolean(
billingUser && !billingUser.guest && billingUser.email,
);
if (isHanzoPerUserKeyEnabled() && isAuthenticatedUser) {
const perUserKey = await resolveHanzoCloudKey(billingUser);
if (!perUserKey) {
throw new Error(
'Your Hanzo Cloud account is not linked for billing yet. Please sign out and back in, then claim your starter credit at https://billing.hanzo.ai',
);
}
authValues.DALLE3_API_KEY = perUserKey;
}
return new DALLE3({ ...imageGenOptions, ...authValues, userId: user });
};
}
/** @type {ImageGenOptions} */