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,7 +4,7 @@
"id": "nexus-service",
"name": "Nexus · OpenRouter",
"description": "Connects Convax to the Convax Workspace in Nexus for OpenRouter chat and live image-model generation through short-lived Data Tokens.",
"version": "0.3.8",
"version": "0.3.9",
"license": "MIT",
"compatibility": {
"pluginSchema": "convax.plugin/7",
Expand All @@ -13,7 +13,7 @@
"companions": [
{
"command": "convax-nexus-mcp",
"version": "0.3.7",
"version": "0.3.8",
"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.8",
"version": "0.3.9",
"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": "Nexus · OpenRouter",
"description": "Connects Convax to the Convax Workspace in Nexus for OpenRouter chat and live image-model generation through short-lived Data Tokens.",
"version": "0.3.8",
"version": "0.3.9",
"contributes": {
"generation": {
"models": [
Expand Down
3 changes: 2 additions & 1 deletion packages/tools/nexus-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ 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;
image-output models populate the host-rendered Nexus image-model control. Image
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
until the service adds a dedicated video Usage Inspector and quota settlement model.
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.7",
"version": "0.3.8",
"private": true,
"license": "MIT",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/tools/nexus-mcp/src/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export class NexusMcpServer {
this.#sendResult(value.id, {
capabilities: { tools: {} },
protocolVersion,
serverInfo: { name: "convax-nexus-mcp", version: "0.3.7" },
serverInfo: { name: "convax-nexus-mcp", version: "0.3.8" },
});
return;
}
Expand Down
10 changes: 8 additions & 2 deletions packages/tools/nexus-mcp/src/nexus-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const maximumImageCompletionBytes = 16 * 1024 * 1024;
const maximumImageErrorBytes = 64 * 1024;
const openRouterModelIdPattern = /^~?[A-Za-z0-9]+(?:[._/:-][A-Za-z0-9]+)*$/;
const nexusRequestIdPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
const automaticOpenRouterModelIds: ReadonlySet<string> = new Set([
"openrouter/auto",
"openrouter/auto-beta",
]);
const nexusGatewayErrorCodes: ReadonlySet<string> = new Set([
"access_unavailable",
"invalid_gateway_request",
Expand Down Expand Up @@ -221,8 +225,10 @@ export class NexusClient {
}

async imageModels(signal?: AbortSignal): Promise<readonly NexusProviderModel[]> {
return (await this.providerModels(signal)).filter(({ outputModalities }) =>
outputModalities.includes("image"),
return (await this.providerModels(signal)).filter(
({ id, outputModalities }) =>
outputModalities.includes("image") &&
!automaticOpenRouterModelIds.has(id),
);
}

Expand Down
40 changes: 39 additions & 1 deletion packages/tools/nexus-mcp/test/mcp-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ test("Nexus image diagnostics expose only typed bounded HTTP fields", () => {
expect(generic).not.toContain("/private/output/path");
});

test("the first tools/list response includes live image model choices", async () => {
test("MCP excludes automatic routers only from live image model choices", async () => {
const responses: unknown[] = [];
const sessions = {
async read() {
Expand Down Expand Up @@ -118,6 +118,16 @@ test("the first tools/list response includes live image model choices", async ()
if (url.pathname.endsWith("/models")) {
return Response.json({
data: [
{
architecture: { output_modalities: ["image", "text"] },
id: "openrouter/auto",
name: "Auto Router",
},
{
architecture: { output_modalities: ["image", "text"] },
id: "openrouter/auto-beta",
name: "Auto Router Beta",
},
{
architecture: { output_modalities: ["image", "text"] },
id: "openai/gpt-image-1",
Expand Down Expand Up @@ -170,6 +180,34 @@ test("the first tools/list response includes live image model choices", async ()
"x-convax-role": "generation-model-id",
});

controller.enqueue(
new TextEncoder().encode(
`${JSON.stringify({
id: 2,
jsonrpc: "2.0",
method: "tools/call",
params: { arguments: {}, name: "llm.models.list" },
})}\n`,
),
);
for (let attempt = 0; attempt < 100 && responses.length < 2; attempt += 1) {
await Bun.sleep(10);
}
expect(responses).toHaveLength(2);
expect(responses[1]).toMatchObject({
id: 2,
result: {
structuredContent: {
models: [
{ id: "openrouter/auto", name: "Auto Router" },
{ id: "openrouter/auto-beta", name: "Auto Router Beta" },
{ id: "openai/gpt-image-1", name: "GPT Image 1" },
],
schema: "convax.llm-model-catalog/1",
},
},
});

await server.shutdown(1_000);
await running;
});
Expand Down
14 changes: 13 additions & 1 deletion packages/tools/nexus-mcp/test/nexus-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ describe("NexusClient", () => {
});
});

test("loads and bounds the opaque OpenRouter catalog through the Nexus Gateway", async () => {
test("keeps automatic routers in the LLM catalog but excludes them from image models", async () => {
const root = await fs.mkdtemp(
path.join(os.tmpdir(), "convax-nexus-client-"),
);
Expand Down Expand Up @@ -383,6 +383,16 @@ describe("NexusClient", () => {
id: "deepseek/deepseek-v4-flash:free",
name: "DeepSeek V4 Flash Free",
},
{
architecture: { output_modalities: ["image", "text"] },
id: "openrouter/auto",
name: "Auto Router",
},
{
architecture: { output_modalities: ["image", "text"] },
id: "openrouter/auto-beta",
name: "Auto Router Beta",
},
{
architecture: { output_modalities: ["image", "text"] },
id: "openai/gpt-image-1",
Expand All @@ -403,6 +413,8 @@ describe("NexusClient", () => {
id: "deepseek/deepseek-v4-flash:free",
name: "DeepSeek V4 Flash Free",
},
{ id: "openrouter/auto", name: "Auto Router" },
{ id: "openrouter/auto-beta", name: "Auto Router Beta" },
{ id: "openai/gpt-image-1", name: "GPT Image 1" },
],
schema: "convax.llm-model-catalog/1",
Expand Down