mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* feat(media): route Open Design Cloud generation through Vela Add Vela image and video adapters behind od media generate with trusted workspace attribution, multi-image support, video polling, and fail-closed behavior. Expose AMR-specific media defaults and pin the packaged Vela CLI used by the integration. * fix(nix): refresh pnpm dependency hashes Refresh the daemon and web fixed-output hashes after the bundled Vela CLI lockfile update. The values come from the failed Nix CI derivations. * chore(pack): bundle Vela CLI 0.0.31-test.0 0.0.29-test.0 is the build QA hit the 413 with: an agent that reads a generated image turns it into base64 for the next model request, and a 1.7 MB PNG became 2.13 MiB of attachment that the ingress in front of Link rejected. 0.0.31-test.0 configures OpenCode to downscale read images to 512px and cap them at 1 MiB of base64. Verified against this build: the same 1,678,735-byte PNG now reaches the model as a 512x288 attachment, 326,762 bytes of base64 -- 6.85x smaller -- and the model still describes the image correctly from it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDg4zffeaNboRdhAh17evB * chore(pack): bundle Vela CLI 0.0.31-test.1 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDg4zffeaNboRdhAh17evB * feat(media): request published aspect ratios from Vela The Vela image adapter refused every aspect but 1:1 and then sent no size at all, so the one shape it did accept was still whatever the provider defaulted to -- the guard proved nothing about the output. Vela now publishes (aspect_ratio, resolution) profiles per model and per request kind, and all five image models carry 1:1, 16:9, 9:16, 4:3, and 3:4. Resolve the pair from `vela media models --json` and pass --aspect-ratio and --resolution, which the CLI requires together. When one aspect ratio publishes several resolutions, prefer the model's default tier: asking for a shape should not quietly change quality or price. When the requested ratio is not published, fail with the ratios that are, before spawning a render. The lookup runs per request rather than from a cache. Vela owns which shapes a model can deliver, so a catalogue that gains or loses one has to take effect immediately, and one CLI call is nothing beside the generation behind it. The prompt contract already advertised these five ratios, so this closes the gap where the prompt promised what the adapter rejected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDg4zffeaNboRdhAh17evB * fix(media): harden multimodal generation and delivery (#6607) * fix(media): harden multimodal generation and delivery Use daemon-authored artifact paths for created and modified outputs. Preserve media references through CLI staging and make Cloud model routing deterministic. Recover one transient post-tool continuation, surface useful generation progress, and pin the verified Vela CLI package. * fix(web): make produced rows keyboard accessible Expose the row-level open action to keyboard and assistive technology users while preserving the nested Open and Download controls. Cover Enter and Space activation in the component regression test. * build(nix): refresh pnpm dependency hashes Update the daemon and web fixed-output hashes after the bundled Vela CLI lockfile change. --------- Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com> * fix(media): keep image errors user-friendly and diagnosable * feat(media): forward quality and resolution to Vela images `od media generate` had no way to name an image quality tier or output resolution, so neither reached the Vela CLI. A user asking for "high quality, 1K" got whatever the model's default profile produced, with the request surviving only as prose inside the prompt. Thread both through the existing layers -- CLI flag whitelist, the generate route, generateMedia, MediaContext -- and append them to the `vela image gen|edit` argv. Unspecified stays unspecified. Tiers are priced more than an order of magnitude apart, so substituting a tier for a caller who named none would silently re-price every unqualified request; omitting `--quality` is what leaves the model's own default in charge. Resolution keeps its existing default-profile selection, because Vela's CLI refuses a resolution without an aspect ratio and an aspect ratio is always sent. Both values are validated against `vela media models` before spawning: quality is published per model (only gpt-image-2 is tiered today) and resolution must be published for the requested aspect. Failing here costs nothing, versus a round trip that ends in a provider rejection. Bump the bundled Vela CLI to 0.0.32, the first release whose `image gen`/`image edit` publish `--quality`. * feat(media): teach the agent to map a named size or tier onto the flags The forwarding change added `--quality` / `--resolution` and told the agent when NOT to send them, but never said what counts as the user asking. A request for "2k" came back as `16:9 2K · model default quality`: the size survived only as prose inside the prompt, where the provider never sees it. Name the mapping in both prompt copies, in one sentence each. The golden system-prompt matrix tracks total prompt length, so its two character counts move with the added text. * fix(media): match published Vela resolutions and tiers case-insensitively A real run asked for `--resolution 2k`. Vela publishes `2K`, the lookup compared the two exactly, and the request failed before reaching the provider; the agent read the supported value out of the error and retried ten seconds later with the shifted letter. Fold case on both sides when matching, and put the catalogue's own spelling on the wire rather than the caller's. No published value differs from another by case alone, so nothing becomes ambiguous, and a request that names a shape or tier the model genuinely lacks still fails the same way. --------- Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com> Co-authored-by: a1chzt <chizblank@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
import { spawn } from 'node:child_process';
|
|
import http from 'node:http';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
const daemonRoot = fileURLToPath(new URL('..', import.meta.url));
|
|
const cliEntry = fileURLToPath(new URL('../src/cli.ts', import.meta.url));
|
|
|
|
let server: http.Server | undefined;
|
|
let baseUrl = '';
|
|
let seenBodies: string[] = [];
|
|
|
|
beforeEach(async () => {
|
|
seenBodies = [];
|
|
server = http.createServer((req, res) => {
|
|
let body = '';
|
|
req.setEncoding('utf8');
|
|
req.on('data', (chunk) => (body += chunk));
|
|
req.on('end', () => {
|
|
if (req.method === 'POST' && (req.url ?? '').includes('/media/generate')) {
|
|
seenBodies.push(body);
|
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ taskId: 'task-images', status: 'queued' }));
|
|
return;
|
|
}
|
|
if ((req.url ?? '').includes('/media/tasks/')) {
|
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ status: 'done', file: { name: 'out.png', size: 3 } }));
|
|
return;
|
|
}
|
|
res.writeHead(404).end();
|
|
});
|
|
});
|
|
await new Promise<void>((resolve) => server!.listen(0, '127.0.0.1', resolve));
|
|
const address = server.address() as { port: number };
|
|
baseUrl = `http://127.0.0.1:${address.port}`;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (server) await new Promise<void>((resolve) => server!.close(() => resolve()));
|
|
server = undefined;
|
|
});
|
|
|
|
async function runCli(images: string[]): Promise<{ code: number; stderr: string }> {
|
|
const args = [
|
|
'--import',
|
|
'tsx',
|
|
cliEntry,
|
|
'media',
|
|
'generate',
|
|
'--surface',
|
|
'image',
|
|
'--model',
|
|
'vela/gpt-image-2',
|
|
'--prompt',
|
|
'Edit the references',
|
|
'--daemon-url',
|
|
baseUrl,
|
|
...images.flatMap((image) => ['--image', image]),
|
|
];
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, args, {
|
|
cwd: daemonRoot,
|
|
env: { ...process.env, OD_PROJECT_ID: 'project-1' },
|
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
});
|
|
let stderr = '';
|
|
child.stderr.setEncoding('utf8');
|
|
child.stderr.on('data', (chunk) => (stderr += chunk));
|
|
child.on('error', reject);
|
|
child.on('close', (code) => resolve({ code: code ?? -1, stderr }));
|
|
});
|
|
}
|
|
|
|
describe('od media generate repeated --image', () => {
|
|
it('posts all five images and preserves the legacy primary image field', async () => {
|
|
const images = ['one.png', 'two.png', 'three.png', 'four.png', 'five.png'];
|
|
const result = await runCli(images);
|
|
|
|
expect(result.code, result.stderr).toBe(0);
|
|
expect(seenBodies).toHaveLength(1);
|
|
const body = JSON.parse(seenBodies[0]!);
|
|
expect(body.image).toBe('one.png');
|
|
expect(body.images).toEqual(images);
|
|
});
|
|
|
|
it('rejects six Vela images before making an HTTP request', async () => {
|
|
const images = Array.from({ length: 6 }, (_, index) => `ref-${index}.png`);
|
|
const result = await runCli(images);
|
|
|
|
expect(result.code).toBe(2);
|
|
expect(result.stderr).toContain('at most 5 --image values');
|
|
expect(seenBodies).toHaveLength(0);
|
|
});
|
|
});
|