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
33 changes: 25 additions & 8 deletions src/browserNodeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { maskTunnelUrl } from "./iicpConsumer.js";
import type { ChatMessage } from "./iicpConsumer.js";
import { createCxKeyPair, decryptPayload } from "./cxConfidentiality.js";
import type { EffectiveCapability } from "./effectiveCapability.js";
import {
BROWSER_NODE_SDK_COMPATIBILITY_VERSION,
BROWSER_NODE_SDK_VERSION,
Expand All @@ -42,6 +43,9 @@ export interface BrowserProviderConfig {
directoryUrl?: string;
/** Model name advertised to the directory (the loaded WebLLM model id). */
model: string;
/** Complete service-path variants. When present, these replace the legacy
* text-only browser advertisement without merging fields across variants. */
effectiveCapabilities?: EffectiveCapability[];
region?: string;
onLog?: (line: string) => void;
/** Called after each served task with the running total. */
Expand Down Expand Up @@ -74,6 +78,23 @@ export interface BrowserProviderDiagnostic {
const CHAT_INTENT = "urn:iicp:intent:llm:chat:v1";
export { BROWSER_NODE_SDK_VERSION } from "./version.js";

/** Resolve the exact variants sent to the directory. Explicit effective
* capabilities take precedence; the historical browser default is preserved
* for callers that do not opt in. */
export function advertisedBrowserCapabilities(
explicit: readonly EffectiveCapability[],
model: string,
): EffectiveCapability[] {
return explicit.length > 0
? explicit.map((capability) => ({ ...capability }))
: [{
intent: CHAT_INTENT,
models: [model],
max_tokens: 1024,
input_modalities: ["text"],
}];
}

/**
* Coarse region autodetect from the browser's timezone (no network, no
* geolocation permission). Matches the mesh's region convention
Expand Down Expand Up @@ -355,14 +376,10 @@ export class BrowserNodeProvider {
node_id: this.nodeId,
endpoint,
region: this.cfg.region ?? detectRegion(),
capabilities: [
{
intent: CHAT_INTENT,
models: [this.cfg.model],
max_tokens: 1024,
input_modalities: ["text"],
},
],
capabilities: advertisedBrowserCapabilities(
this.cfg.effectiveCapabilities ?? [],
this.cfg.model,
),
limits: { max_concurrent: 1, tokens_per_min: 6000 },
transport_method: "turn_relay",
exposure_mode: "relay_required",
Expand Down
57 changes: 57 additions & 0 deletions tests/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
BROWSER_NODE_SDK_VERSION,
BROWSER_NODE_VERSION,
discoverRelay,
advertisedBrowserCapabilities,
encryptPayload,
type BrowserProviderRuntime,
} from "../src/index.ts";
Expand All @@ -34,6 +35,20 @@ function runtime(reply = "ok"): BrowserProviderRuntime {
};
}

test("explicit effective variants replace the legacy browser advertisement", () => {
const explicit = [{
intent: "urn:iicp:intent:llm:chat:v1",
variant_id: "browser-vision",
models: ["custom-browser-model"],
input_modalities: ["text", "image"],
claim_provenance: { source: "runtime_introspection" as const },
}];
assert.deepEqual(
advertisedBrowserCapabilities(explicit, "legacy-browser-model"),
explicit,
);
});

test("discoverRelay requests a short-lived ticket and uses its relay route", async () => {
let url = "";
let requestBody: Record<string, unknown> | null = null;
Expand Down Expand Up @@ -119,6 +134,48 @@ test("start registers browser provider with CX key, relay exposure and current b
assert.deepEqual(order.slice(0, 3), ["register", "ticket", "bind"]);
});

test("start sends configured effective capability variants to the directory", async () => {
let registerBody: Record<string, any> | null = null;
const effectiveCapabilities = [{
intent: "urn:iicp:intent:llm:chat:v1",
variant_id: "browser-vision",
models: ["custom-browser-model"],
input_modalities: ["text", "image"],
output_modalities: ["text"],
claim_provenance: { source: "runtime_introspection" as const },
}];
await withFetch(async (input, init) => {
const url = String(input);
if (url.endsWith("/v1/register") && init?.method === "POST") {
registerBody = JSON.parse(String(init.body));
return new Response(JSON.stringify({ node_token: "node-token" }));
}
if (url.endsWith("/v1/relay/ticket")) {
return new Response(JSON.stringify({ ticket: "signed-bind-ticket" }), { status: 201 });
}
if (url.endsWith("/v1/relay/bind")) {
return new Response(JSON.stringify({ session_token: "relay-session" }));
}
if (url.endsWith("/v1/heartbeat")) return new Response(JSON.stringify({ ok: true }));
if (url.endsWith("/v1/relay/pull")) return new Promise<Response>(() => undefined);
if (url.endsWith("/v1/relay/unbind") || (url.endsWith("/v1/register") && init?.method === "DELETE")) {
return new Response(JSON.stringify({ ok: true }));
}
throw new Error(`unexpected fetch ${url}`);
}, async () => {
const provider = new BrowserNodeProvider(runtime(), {
relayUrl: "https://relay.example",
relayNodeId: "relay-1",
directoryUrl: "https://directory.test/api",
model: "legacy-browser-model",
effectiveCapabilities,
});
await provider.start();
await provider.stop();
});
assert.deepEqual(registerBody?.capabilities, effectiveCapabilities);
});

test("provider exposes deterministic recovery state transitions", async () => {
const diagnostics: string[] = [];
await withFetch(async (input, init) => {
Expand Down
Loading