diff --git a/packages/client/ui-skill/src/client/index.ts b/packages/client/ui-skill/src/client/index.ts index 5b6a9bb44e..36d1e9778c 100644 --- a/packages/client/ui-skill/src/client/index.ts +++ b/packages/client/ui-skill/src/client/index.ts @@ -58,7 +58,7 @@ interface CatalogFetch { } /** Required services: reference source faces plus the tool-row and locale registries. */ -export const inject = ['inputTriggers', 'connection', 'sessions', 'slots', 'locale', 'remote'] +export const inject = ['inputTriggers', 'connection', 'sessions', 'slots', 'locale', 'remote', 'remote.skills'] /** * Client plugin body: register the '/' source, dictionaries, and keyed tool row. diff --git a/packages/client/ui-skill/tests/browser-plugin.client.spec.ts b/packages/client/ui-skill/tests/browser-plugin.client.spec.ts index 208baac3bc..89111675f3 100644 --- a/packages/client/ui-skill/tests/browser-plugin.client.spec.ts +++ b/packages/client/ui-skill/tests/browser-plugin.client.spec.ts @@ -102,7 +102,7 @@ const req = (query: string, signal?: AbortSignal) => describe('apply', () => { it('declares the services it binds', () => { - expect(inject).toEqual(['inputTriggers', 'connection', 'sessions', 'slots', 'locale', 'remote']) + expect(inject).toEqual(['inputTriggers', 'connection', 'sessions', 'slots', 'locale', 'remote', 'remote.skills']) }) it('registers the dedicated skill row and its locale dictionaries', async () => { diff --git a/packages/host/apiproxy/tests/fetch-carrier.spec.ts b/packages/host/apiproxy/tests/fetch-carrier.spec.ts index 2cf89e675d..844c4d5039 100644 --- a/packages/host/apiproxy/tests/fetch-carrier.spec.ts +++ b/packages/host/apiproxy/tests/fetch-carrier.spec.ts @@ -81,6 +81,16 @@ describe('handler carrier-layer statuses', () => { expect(parsed.result.error?.details.issues.length).toBeGreaterThan(0) }) + it('rejects a request whose envelope method does not match its path', async () => { + const body = JSON.stringify({ type: 'client-request', rpcId: 'r-mismatch', method: 'other.method', payload: {} }) + const response = await handler.fetch(new Request('http://x/api/host.describe', { method: 'POST', headers: { 'content-type': 'application/json' }, body })) + const parsed = await response.json() as { result: { error?: { code: string; message: string } } } + expect(parsed.result.error).toMatchObject({ + code: 'bad-request', + message: 'method "other.method" does not match path "host.describe"', + }) + }) + it('500s when the impl itself throws', async () => { const crashing = toFetchHandler(fakeApi({ crashOn: 'host.describe' })) const body = JSON.stringify({ type: 'client-request', rpcId: 'r-11', method: 'host.describe', payload: {} }) diff --git a/packages/llm/llm/tests/service.spec.ts b/packages/llm/llm/tests/service.spec.ts index 4c624fa862..47309daea1 100644 --- a/packages/llm/llm/tests/service.spec.ts +++ b/packages/llm/llm/tests/service.spec.ts @@ -525,13 +525,20 @@ describe('LlmRuntime', () => { const ctx = new Context() await ctx.plugin(LlmRuntime) const provider = { id: 'catalog', name: 'Catalog Provider' } - const model = { provider: 'catalog', id: 'fast', name: 'Fast', description: 'Low latency' } + const model = { + provider: 'catalog', + id: 'fast', + name: 'Fast', + description: 'Low latency', + inputModalities: ['text'] as const, + } ctx.llm.registerAdapter(['catalog'], new CatalogAdapter(provider, [model])) const providers = ctx.llm.listProviders() const models = await ctx.llm.listModels('catalog') expect(providers).toEqual([provider]) expect(models).toEqual([model]) + expect(models[0]!.inputModalities).not.toBe(model.inputModalities) providers[0]!.name = 'mutated' models[0]!.name = 'mutated' @@ -539,7 +546,7 @@ describe('LlmRuntime', () => { model.name = 'source mutated' expect(ctx.llm.listProviders()).toEqual([{ id: 'catalog', name: 'Catalog Provider' }]) await expect(ctx.llm.listModels('catalog')).resolves.toEqual([{ - provider: 'catalog', id: 'fast', name: 'source mutated', description: 'Low latency', + provider: 'catalog', id: 'fast', name: 'source mutated', description: 'Low latency', inputModalities: ['text'], }]) }) diff --git a/packages/llm/llm/tests/topology.spec.ts b/packages/llm/llm/tests/topology.spec.ts index 8759e5ba69..0fbe8255e6 100644 --- a/packages/llm/llm/tests/topology.spec.ts +++ b/packages/llm/llm/tests/topology.spec.ts @@ -250,6 +250,58 @@ describe('model discovery registry', () => { ]) }) + it('carries cancellation into Remote discovery and maps provider failures', async () => { + const ctx = await setup() + const discover = vi.fn() + .mockResolvedValueOnce([ + { id: 'keep', name: 'Keep', contextWindow: 1024, maxTokens: 256 }, + { id: '' }, + { id: 'keep' }, + { id: 'bare' }, + ]) + .mockRejectedValueOnce(new Error('endpoint offline')) + .mockRejectedValueOnce('provider refused') + ctx.llm.registerModelDiscovery('llm-example', discover) + const signal = new AbortController().signal + + await expect(ctx.llm.remoteDiscoverModels( + 'llm-example', + { baseURL: 'https://gateway.example/v1' }, + signal, + )).resolves.toEqual([ + { id: 'keep', name: 'Keep', contextWindow: 1024, maxTokens: 256 }, + { id: 'bare' }, + ]) + expect(discover).toHaveBeenNthCalledWith( + 1, + { baseURL: 'https://gateway.example/v1' }, + signal, + ) + + await expect(ctx.llm.remoteDiscoverModels( + 'llm-example', + { baseURL: 'https://gateway.example/v1' }, + signal, + )).rejects.toMatchObject({ + failure: { + code: 'model-discovery-failed', + message: 'endpoint offline', + details: { settingsNs: 'llm-example', baseURL: 'https://gateway.example/v1' }, + }, + }) + await expect(ctx.llm.remoteDiscoverModels( + 'llm-example', + { provider: 'known-route' }, + signal, + )).rejects.toMatchObject({ + failure: { + code: 'model-discovery-failed', + message: 'provider refused', + details: { settingsNs: 'llm-example' }, + }, + }) + }) + it('refuses a namespace nothing serves and a draft with no endpoint', async () => { const ctx = await setup() ctx.llm.registerModelDiscovery('llm-example', () => Promise.resolve([]))