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
27 changes: 27 additions & 0 deletions packages/user-intent-kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,30 @@ a 50% reduction for that daemon, excluding startup/status changes and retries.
These are schedule-derived counts, not measured Vercel billing or fleet totals.
The test suite separately verifies 720 unchanged-agent writes over a simulated day.
Standalone vitals scripts and secondary agents still contribute additional traffic.

### Model identity discovery

The device publisher checks local servers once per minute (cached and single
flight), piggybacking the result on its existing device PATCH. Defaults:
LM Studio at `127.0.0.1:1234/api/v1/models` and OpenAI-compatible serving
endpoints at ports 8080, 8000 and 8888 (`/v1/models`). LM Studio uses only loaded
LLM instances; downloaded models and embeddings are excluded. Generic OpenAI
IDs are labelled `advertised`, not proof of loaded weights or active generation.

Optional configuration:

- `INTENT_MODEL_SERVER_URL`: explicit server origin, replacing default probes.
- `INTENT_MODEL_SERVER_KIND`: `openai` (default) or `lmstudio`.
- `INTENT_MODEL_SERVER_KEY_FILE`: server-specific bearer token file, used only
for the explicit server. The intent API key is never reused. Redirects are
refused. HTTP is allowed only on loopback; remote endpoints require HTTPS.
- `INTENT_DEVICE_MODEL`: manual fallback when discovery is unavailable, visibly
suffixed `(manual)`. Successful empty results do not fall back to this label.

Payloads include `model`, `models`, `model_status`, `model_source` and
`model_checked_at`. Unknown/unauthorized results explicitly send `model: null`
when there is no manual label, clearing any previous model through PATCH.
A refused/unreachable server means unknown, not verified absence. With automatic
multi-port discovery, absence cannot be verified while any endpoint is unknown.
Neither endpoint discovery nor these read-only probes load a model.
LM Studio schema: https://lmstudio.ai/docs/developer/rest/list .
7 changes: 5 additions & 2 deletions packages/user-intent-kit/src/adapters/desktop.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { platform } from 'node:os';
const IDLE_AFTER_SEC = 300;
import { collectHostTelemetry } from '../host-telemetry.js';
import { StatePublisher } from '../state-publisher.js';
import { createModelDiscovery } from '../model-discovery.js';

/**
* Desktop Adapter - detects active window and context on macOS.
Expand All @@ -22,14 +23,16 @@ export class DesktopAdapter {
#kind;
#pollIntervalMs;
#publisher;
#discoverModels;

/**
* @param {import('../client.js').IntentClient} client
* @param {object} [opts]
* @param {number} [opts.pollIntervalMs=30000] - How often to publish state
*/
constructor(client, { pollIntervalMs = 30000, machine, kind } = {}) {
constructor(client, { pollIntervalMs = 30000, machine, kind, discoverModels = createModelDiscovery() } = {}) {
this.#client = client;
this.#discoverModels = discoverModels;
this.#machine = machine ?? client?.deviceId ?? undefined;
this.#kind = kind;
if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) throw new Error('Invalid pollIntervalMs');
Expand All @@ -46,7 +49,7 @@ export class DesktopAdapter {
*/
async publishState() {
const state = this.#detectState();
await this.#publisher.publish({ ...state, ttl_sec: 90 });
await this.#publisher.publish({ ...state, ...await this.#discoverModels(), ttl_sec: 90 });
}

/**
Expand Down
69 changes: 69 additions & 0 deletions packages/user-intent-kit/src/model-discovery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: AGPL-3.0
import { readFile } from 'node:fs/promises';

/** Local, read-only model discovery. Never uses the intent API credential. */
export function createModelDiscovery({ env = process.env, fetchImpl = fetch,
readKey = path => readFile(path, 'utf8'), now = () => Date.now(), cacheMs = 60000 } = {}) {
let cached, last = -Infinity, pending;
const explicit = env.INTENT_MODEL_SERVER_URL;
const kind = env.INTENT_MODEL_SERVER_KIND || 'openai';
const endpoints = explicit ? [{ url: explicit, kind }] : [
{ url: 'http://127.0.0.1:1234', kind: 'lmstudio' },
...[8080, 8000, 8888].map(port => ({ url: `http://127.0.0.1:${port}`, kind: 'openai' })),
];
async function probe(endpoint) {
try {
const url = new URL(endpoint.url);
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
if (url.username || url.password || url.search || url.hash ||
!(url.protocol === 'https:' || (url.protocol === 'http:' && loopback))) {
return { status: 'invalid_config' };
}
if (!['openai', 'lmstudio'].includes(endpoint.kind)) return { status: 'invalid_config' };
const headers = {};
if (explicit && env.INTENT_MODEL_SERVER_KEY_FILE) {
headers.Authorization = `Bearer ${(await readKey(env.INTENT_MODEL_SERVER_KEY_FILE)).trim()}`;
}
const suffix = endpoint.kind === 'lmstudio' ? '/api/v1/models' : '/v1/models';
const response = await fetchImpl(url.href.replace(/\/$/, '') + suffix, {
headers, redirect: 'error', signal: AbortSignal.timeout(1500),
});
if (!response.ok) return { status: response.status === 401 || response.status === 403 ? 'unauthorized' : 'unknown' };
const data = await response.json();
const rows = endpoint.kind === 'lmstudio' ? data.models : data.data;
if (!Array.isArray(rows)) return { status: 'unknown' };
// LM Studio's OpenAI list includes downloaded/JIT models: use loaded instances.
if (endpoint.kind === 'lmstudio' && rows.some(m => !m || !Array.isArray(m.loaded_instances))) return { status: 'unknown' };
const ids = endpoint.kind === 'lmstudio'
? rows.filter(m => m.type === 'llm').flatMap(m => m.loaded_instances.map(i => i.id))
: rows.map(m => m?.id);
if (ids.some(id => typeof id !== 'string' || !id.trim())) return { status: 'unknown' };
return { status: ids.length ? (endpoint.kind === 'lmstudio' ? 'loaded' : 'advertised') : 'none',
ids, source: endpoint.kind };
} catch { return { status: 'unknown' }; }
}
async function discover() {
const results = await Promise.all(endpoints.map(probe));
const good = results.filter(r => r.ids?.length);
const models = [...new Set(good.flatMap(r => r.ids))].sort();
const allKnown = results.every(r => Array.isArray(r.ids));
let status = models.length ? (good.every(r => r.status === 'loaded') ? 'loaded' : 'advertised')
: allKnown ? 'none' : results.some(r => r.status === 'unauthorized') ? 'unauthorized' : 'unknown';
let source = good.length ? [...new Set(good.map(r => r.source))].join(',') : null;
let model = models.length ? models.join(', ') : null;
if (!model && status !== 'none' && env.INTENT_DEVICE_MODEL?.trim()) {
model = `${env.INTENT_DEVICE_MODEL.trim()} (manual)`;
source = 'manual';
// Keep the failure cause separately; never turn a configured label into live proof.
status = `manual:${status}`;
}
return { model, models, model_status: status, model_source: source,
model_checked_at: new Date(now()).toISOString() };
}
return async () => {
if (cached && now() >= last && now() - last < cacheMs) return structuredClone(cached);
if (!pending) pending = discover().then(value => { cached = value; last = now(); return value; })
.finally(() => { pending = undefined; });
return structuredClone(await pending);
};
}
50 changes: 50 additions & 0 deletions packages/user-intent-kit/test/model-discovery.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createModelDiscovery } from '../src/model-discovery.js';
const env = { INTENT_MODEL_SERVER_URL: 'http://127.0.0.1:8888' };
const ok = data => ({ ok: true, json: async () => data });
test('OpenAI compatible server advertises all IDs without claiming loaded state', async () => {
const probe = createModelDiscovery({ env, fetchImpl: async () => ok({ data: [{ id: 'glm' }, { id: 'qwen' }] }) });
const r = await probe(); assert.equal(r.model, 'glm, qwen'); assert.equal(r.model_status, 'advertised');
});
test('LM Studio only lists loaded LLM instances, not downloaded models/embeddings', async () => {
const probe = createModelDiscovery({ env: { ...env, INTENT_MODEL_SERVER_KIND: 'lmstudio' },
fetchImpl: async url => { assert.ok(url.endsWith('/api/v1/models')); return ok({ models: [
{ type: 'llm', loaded_instances: [{ id: 'loaded' }] },
{ type: 'llm', loaded_instances: [] }, { type: 'embedding', loaded_instances: [{ id: 'embed' }] },
] }); } });
assert.equal((await probe()).model, 'loaded');
});
test('authorized empty list clears previous model and does not revive manual label', async () => {
const p = createModelDiscovery({ env: { ...env, INTENT_DEVICE_MODEL: 'old' }, fetchImpl: async () => ok({ data: [] }) });
const r = await p(); assert.equal(r.model, null); assert.equal(r.model_status, 'none');
});
test('auth failure clears prior live model; manual fallback is explicitly labelled', async () => {
let now = 0, calls = 0;
const p = createModelDiscovery({ env, now: () => now, fetchImpl: async () => ++calls === 1
? ok({ data: [{ id: 'live' }] }) : { ok: false, status: 401 } });
assert.equal((await p()).model, 'live'); now = 61000;
const r = await p(); assert.equal(r.model, null); assert.equal(r.model_status, 'unauthorized');
const manual = createModelDiscovery({ env: { ...env, INTENT_DEVICE_MODEL: 'typed' }, fetchImpl: async () => { throw Error('offline'); } });
assert.equal((await manual()).model, 'typed (manual)');
});
test('server key is bound to explicit endpoint, redirects disabled, never uses intent key', async () => {
let requests = 0;
const p = createModelDiscovery({ env: { ...env, INTENT_API_KEY: 'never-send', INTENT_MODEL_SERVER_KEY_FILE: '/server-key' },
readKey: async path => { assert.equal(path, '/server-key'); return 'server-only'; },
fetchImpl: async (url, opts) => { requests++; assert.equal(opts.headers.Authorization, 'Bearer server-only');
assert.equal(opts.redirect, 'error'); return ok({ data: [] }); } });
await Promise.all([p(), p()]); await p(); assert.equal(requests, 1);
const auto = createModelDiscovery({ env: { INTENT_API_KEY: 'never-send', INTENT_MODEL_SERVER_KEY_FILE: '/server-key' },
readKey: async () => { throw Error('must not read'); }, fetchImpl: async (_, opts) => {
assert.deepEqual(opts.headers, {}); throw Error('offline'); } });
await auto();
});
test('unsafe URLs and malformed payloads fail unknown, not no-model', async () => {
for (const url of ['http://remote.example', 'https://user:pass@example.com', 'file:///tmp/x']) {
const p = createModelDiscovery({ env: { INTENT_MODEL_SERVER_URL: url }, fetchImpl: async () => { assert.fail('unsafe fetch'); } });
assert.equal((await p()).model, null);
}
const p = createModelDiscovery({ env, fetchImpl: async () => ok({ data: [{}] }) });
assert.equal((await p()).model_status, 'unknown');
});