Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/plugins/nexus-service/convax-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
"id": "nexus-service",
"name": "Convax Account",
"description": "Connects Convax to your Convax Account for OpenRouter chat and live image-model generation through short-lived Data Tokens.",
"version": "0.3.15",
"version": "0.3.16",
"companions": [
{
"command": "convax-nexus-mcp",
"version": "0.3.12",
"version": "0.3.13",
"source": "packages/tools/nexus-mcp",
"targets": [
{
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/nexus-service/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@microvoid/convax-plugin-nexus-service",
"version": "0.3.15",
"version": "0.3.16",
"private": true,
"type": "module",
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/nexus-service/package/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"id": "nexus-service",
"name": "Convax Account",
"description": "Connects Convax to your Convax Account for OpenRouter chat and live image-model generation through short-lived Data Tokens.",
"version": "0.3.15",
"version": "0.3.16",
"contributes": {
"generation": {
"models": [
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/storyboard-studio/package/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Project 相对引用,不会冒充已经播放、下载或重新验证了二进
这个过程由多次校验操作组成,不声称原子完成。

当前公开 ABI 没有 Project 左侧栏同级目录贡献、Canvas 外的 Project 原生插件文档面,
也没有“拖入一个故事后原子展开整棵图”的声明。因此 0.1.1 使用以下诚实等价实现
也没有“拖入一个故事后原子展开整棵图”的声明。因此当前版本使用以下诚实等价实现

- `Storyboards/` 作为用户可见的顶层 Project 文件目录;
- 完整故事/分集/片段树位于插件工作台及其全屏形态;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1716,6 +1716,7 @@ select:focus-visible {
grid-template-columns: 1fr;
}

.launch-composer,
.pipeline-panel {
min-height: auto;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/plugins/storyboard-studio/test/ui-contract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,12 @@ describe("storyboard production workbench", () => {
expect(app).toContain("故事板已创建,部分载入")
expect(app).toContain("已刷新,故事板部分载入")
})

test("lets both stacked launcher cards size to their content at the packaged viewport", async () => {
const css = await readFile(path.join(packageRoot, "assets/styles.css"), "utf8")

expect(css).toMatch(
/@media \(max-width: 840px\) \{[\s\S]*?\.launch-composer,\s*\.pipeline-panel\s*\{\s*min-height:\s*auto;/u,
)
})
})
17 changes: 13 additions & 4 deletions packages/tools/nexus-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,26 @@ First-party Convax companion for Nexus Hosted Auth and its OpenAI-compatible Gat

The companion requests the complete OpenRouter model catalog through Nexus and
keeps output modalities bounded locally. Text-output models feed the LLM provider;
concrete image-output models populate the host-rendered Nexus image-model control,
while OpenRouter's automatic routers remain available only to the LLM provider. Image
generation uses the already-metered Chat Completions path and returns only validated
embedded image artifacts to the host. Nexus video endpoints remain unavailable
concrete image-and-text-output models populate the host-rendered Nexus image-model
control, while OpenRouter's automatic routers remain available only to the LLM
provider. Image generation uses the already-metered Chat Completions path, requests
the selected model's declared image and text outputs, and returns only validated
embedded image artifacts to the host. Image-only models remain hidden because
OpenRouter's dedicated Images route is not yet admitted by Nexus metering. Nexus
video endpoints remain unavailable
until the service adds a dedicated video Usage Inspector and quota settlement model.
When the live image catalog is bounded, the companion marks its model field so a
compatible host can present each image model as a direct choice. If that catalog
cannot be loaded, has no image models, or exceeds the bounded choice limit, the
companion omits image generation from `tools/list` instead of exposing a free-text
model field.

The companion keeps the validated image catalog, Provider route, and short-lived
Data Token together in one bounded in-memory route. The `tools/call` that follows
`tools/list` reuses that exact route and never repeats authorization, Provider, or
model discovery inside the generation call. Route credentials remain private to
the companion and are invalidated before their token refresh window.

Each image request sends the host operation id as `x-nexus-request-id` for safe
diagnostic correlation. A rejected request exposes only its HTTP status, an
allow-listed Gateway error code, and that locally generated operation id to the MCP
Expand Down
2 changes: 1 addition & 1 deletion packages/tools/nexus-mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@microvoid/convax-nexus-mcp",
"version": "0.3.12",
"version": "0.3.13",
"private": true,
"license": "MIT",
"type": "module",
Expand Down
67 changes: 47 additions & 20 deletions packages/tools/nexus-mcp/src/image-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
type GenerationArtifact,
type GenerationCall,
} from "./contracts.ts";
import type { NexusClient } from "./nexus-client.ts";
import type { NexusImageRoute } from "./nexus-client.ts";

const maximumPromptBytes = 20_000;
const maximumImageBytes = 12 * 1024 * 1024;
Expand All @@ -33,10 +33,9 @@ interface DecodedImage {
export class NexusImageGenerator {
readonly #operations = new Map<string, TrackedOperation>();

constructor(private readonly client: NexusClient) {}

generate(
value: unknown,
resolveRoute: () => NexusImageRoute,
signal: AbortSignal,
): Promise<readonly GenerationArtifact[]> {
const call = parseGenerationCall(value);
Expand All @@ -57,25 +56,28 @@ export class NexusImageGenerator {
"Nexus generation operation history is full; restart the companion before generating again",
);
}
const result = this.#generate(call, signal);
const result = this.#generate(call, resolveRoute, signal);
this.#operations.set(call.operation_id, { fingerprint, result });
return result;
}

async #generate(
call: GenerationCall,
resolveRoute: () => NexusImageRoute,
signal: AbortSignal,
): Promise<readonly GenerationArtifact[]> {
if (signal.aborted) throw abortError();
const available = await this.client.imageModels(signal);
if (!available.some(({ id }) => id === call.model)) {
throw new Error("The selected Nexus image model is unavailable");
}
const outputDirectory = await validateOutputDirectory(
call.output_directory,
);
const response = await this.client.imageCompletion(
call.model,
if (signal.aborted) throw abortError();
const route = resolveRoute();
const model = route.models.find(({ id }) => id === call.model);
if (!model) {
throw new Error("The selected Nexus image model is unavailable");
}
const response = await route.complete(
model,
call.prompt,
call.operation_id,
signal,
Expand Down Expand Up @@ -148,7 +150,12 @@ function parseGenerationCall(value: unknown): GenerationCall {
const model = trimmedString(input.model, "Nexus image model", 191);
if (!modelIdPattern.test(model))
throw new Error("Nexus image model is invalid");
const prompt = trimmedString(input.prompt, "Nexus image prompt", 20_000, true);
const prompt = trimmedString(
input.prompt,
"Nexus image prompt",
20_000,
true,
);
if (Buffer.byteLength(prompt, "utf8") > maximumPromptBytes) {
throw new Error("Nexus image prompt is too large");
}
Expand Down Expand Up @@ -195,7 +202,11 @@ function parseImages(value: unknown): readonly DecodedImage[] {
throw new Error("Nexus image response choices are invalid");
}
const images: DecodedImage[] = [];
for (let choiceIndex = 0; choiceIndex < response.choices.length; choiceIndex += 1) {
for (
let choiceIndex = 0;
choiceIndex < response.choices.length;
choiceIndex += 1
) {
const choice = asRecord(
response.choices[choiceIndex],
`Nexus image response choice ${choiceIndex}`,
Expand All @@ -205,7 +216,10 @@ function parseImages(value: unknown): readonly DecodedImage[] {
`Nexus image response choice ${choiceIndex} message`,
);
if (message.images === undefined) continue;
if (!Array.isArray(message.images) || message.images.length > maximumImages) {
if (
!Array.isArray(message.images) ||
message.images.length > maximumImages
) {
throw new Error("Nexus image response image list is invalid");
}
for (const image of message.images) {
Expand All @@ -228,11 +242,14 @@ function decodeImageDataUrl(value: unknown): DecodedImage {
if (typeof value !== "string" || value.length > maximumImageBytes * 2) {
throw new Error("Nexus generated image URL is invalid");
}
const match = /^data:(image\/(?:jpeg|png|webp));base64,([A-Za-z0-9+/]+={0,2})$/u.exec(
value,
);
const match =
/^data:(image\/(?:jpeg|png|webp));base64,([A-Za-z0-9+/]+={0,2})$/u.exec(
value,
);
if (!match?.[1] || !match[2])
throw new Error("Nexus generated image must be an embedded supported image");
throw new Error(
"Nexus generated image must be an embedded supported image",
);
if (match[2].length % 4 !== 0)
throw new Error("Nexus generated image encoding is invalid");
const decoded = Buffer.from(match[2], "base64");
Expand All @@ -244,7 +261,9 @@ function decodeImageDataUrl(value: unknown): DecodedImage {
}
const mimeType = match[1] as DecodedImage["mimeType"];
if (!matchesImageSignature(bytes, mimeType)) {
throw new Error("Nexus generated image bytes do not match their media type");
throw new Error(
"Nexus generated image bytes do not match their media type",
);
}
return {
bytes,
Expand All @@ -263,10 +282,18 @@ function matchesImageSignature(
mimeType: DecodedImage["mimeType"],
) {
if (mimeType === "image/jpeg")
return bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
return (
bytes.length >= 3 &&
bytes[0] === 0xff &&
bytes[1] === 0xd8 &&
bytes[2] === 0xff
);
if (mimeType === "image/png") {
const signature = [137, 80, 78, 71, 13, 10, 26, 10];
return bytes.length >= signature.length && signature.every((value, index) => bytes[index] === value);
return (
bytes.length >= signature.length &&
signature.every((value, index) => bytes[index] === value)
);
}
return (
bytes.length >= 12 &&
Expand Down
Loading