Skip to content

Commit ee9f3b6

Browse files
fix(runtime-llm): avoid duplicate provider prefix in allowlist diagnostics
normalizeAllowedModelRef() and the resolved override ref interpolated ${provider}/${model} after normalizeModelRef(), so a provider-qualified model id like openrouter/gpt-5.4-mini surfaced as openrouter/openrouter/gpt-5.4-mini in the allowlist set and policy denial message, masking the actionable model ref. Route both sites through modelKey() (src/agents/model-ref-shared.ts) so the provider segment is collapsed when the model id already starts with it. Add regression tests covering allowlist hit and denial paths for the OpenRouter shape. Fixes #84887
1 parent bde07dd commit ee9f3b6

2 files changed

Lines changed: 89 additions & 2 deletions

File tree

src/plugins/runtime/runtime-llm.runtime.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,92 @@ describe("runtime.llm.complete", () => {
306306
expect(hoisted.prepareSimpleCompletionModelForAgent).not.toHaveBeenCalled();
307307
});
308308

309+
it("matches allowlist entries for provider-qualified model ids without doubling the provider prefix", async () => {
310+
const runtimeContext = resolveContextEngineCapabilities({
311+
config: {
312+
...cfg,
313+
plugins: {
314+
entries: {
315+
"lossless-claw": {
316+
llm: {
317+
allowModelOverride: true,
318+
allowedModels: ["openrouter/gpt-5.4-mini"],
319+
},
320+
},
321+
},
322+
},
323+
},
324+
sessionKey: "agent:main:session:abc",
325+
contextEnginePluginId: "lossless-claw",
326+
purpose: "context-engine.compaction",
327+
});
328+
329+
hoisted.prepareSimpleCompletionModelForAgent.mockResolvedValue(
330+
createPreparedModel("openrouter/gpt-5.4-mini"),
331+
);
332+
hoisted.resolveSimpleCompletionSelectionForAgent.mockImplementation(
333+
(params: { agentId: string }) => ({
334+
provider: "openrouter",
335+
modelId: "openrouter/gpt-5.4-mini",
336+
agentDir: `/tmp/${params.agentId}`,
337+
}),
338+
);
339+
340+
await runtimeContext.llm!.complete({
341+
agentId: "main",
342+
model: "openrouter/gpt-5.4-mini",
343+
messages: [{ role: "user", content: "summarize" }],
344+
});
345+
346+
expectSingleCallFirstArg(hoisted.prepareSimpleCompletionModelForAgent, {
347+
agentId: "main",
348+
modelRef: "openrouter/gpt-5.4-mini",
349+
});
350+
});
351+
352+
it("reports denials for provider-qualified model ids without doubling the provider prefix", async () => {
353+
const runtimeContext = resolveContextEngineCapabilities({
354+
config: {
355+
...cfg,
356+
plugins: {
357+
entries: {
358+
"lossless-claw": {
359+
llm: {
360+
allowModelOverride: true,
361+
allowedModels: ["openrouter/gpt-5.4-mini"],
362+
},
363+
},
364+
},
365+
},
366+
},
367+
sessionKey: "agent:main:session:abc",
368+
contextEnginePluginId: "lossless-claw",
369+
purpose: "context-engine.compaction",
370+
});
371+
372+
hoisted.resolveSimpleCompletionSelectionForAgent.mockImplementation(
373+
(params: { agentId: string }) => ({
374+
provider: "openrouter",
375+
modelId: "openrouter/gpt-5.5",
376+
agentDir: `/tmp/${params.agentId}`,
377+
}),
378+
);
379+
380+
let caught: unknown;
381+
try {
382+
await runtimeContext.llm!.complete({
383+
model: "openrouter/gpt-5.5",
384+
messages: [{ role: "user", content: "summarize" }],
385+
});
386+
} catch (error) {
387+
caught = error;
388+
}
389+
const message = caught instanceof Error ? caught.message : String(caught);
390+
expect(message).toContain('"openrouter/gpt-5.5"');
391+
expect(message).not.toContain("openrouter/openrouter/");
392+
expect(hoisted.prepareSimpleCompletionModelForAgent).not.toHaveBeenCalled();
393+
});
394+
309395
it("keeps context-engine attribution and host-derived policy inside plugin runtime scope", async () => {
310396
const runtimeContext = resolveContextEngineCapabilities({
311397
config: {

src/plugins/runtime/runtime-llm.runtime.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Api, Message } from "@earendil-works/pi-ai";
2+
import { modelKey } from "../../agents/model-ref-shared.js";
23
import { normalizeModelRef } from "../../agents/model-selection.js";
34
import type { NormalizedUsage, UsageLike } from "../../agents/usage.js";
45
import { normalizeUsage } from "../../agents/usage.js";
@@ -235,7 +236,7 @@ function normalizeAllowedModelRef(raw: string): string | null {
235236
return null;
236237
}
237238
const normalized = normalizeModelRef(provider, model);
238-
return `${normalized.provider}/${normalized.model}`;
239+
return modelKey(normalized.provider, normalized.model);
239240
}
240241

241242
function buildPolicyFromEntry(entry: {
@@ -402,7 +403,7 @@ export function createRuntimeLlm(options: CreateRuntimeLlmOptions = {}): PluginR
402403
? normalizeModelRef(selection.provider, selection.modelId)
403404
: null;
404405
const resolvedModelRef = normalizedSelection
405-
? `${normalizedSelection.provider}/${normalizedSelection.model}`
406+
? modelKey(normalizedSelection.provider, normalizedSelection.model)
406407
: null;
407408
assertAllowedModelOverride({
408409
resolvedModelRef,

0 commit comments

Comments
 (0)