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
62 changes: 62 additions & 0 deletions apps/server/src/mcp/OrchestratorMcpService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2430,6 +2430,68 @@ describe("OrchestratorMcpService provider resolution", () => {
}),
);

it.effect("an unadvertised model refusal names advertised slugs to retry with", () =>
Effect.gen(function* () {
const codex = providerSnapshot({
instanceId: codexInstanceId,
driver: ProviderDriverKind.make("codex"),
});
const layerDependencies = Layer.mergeAll(
NodeServices.layer,
NodeCrypto.layer,
Layer.mock(ThreadManagementService.ThreadManagementService)({
hasReceipt: () => Effect.succeed(false),
getThreadRecords: () => Effect.succeed(parentProjection([])),
}),
ServerSettings.layerTest(),
providerRegistryLayer([
{
...codex,
models: [
{ slug: "claude-opus-4-1", isLegacy: true },
...Array.from({ length: 9 }, (_, index) => ({ slug: `claude-opus-5-${index + 1}` })),
].map((model) => ({ ...model, name: model.slug, isCustom: false, capabilities: null })),
},
]),
adapterRegistryLayer([codexInstanceId]),
Layer.mock(ProjectService.ProjectService)({}),
Layer.mock(SecretRequests.SecretRequests)({}),
Layer.mock(RemoteDelegation.RemoteDelegation)({}),
Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
);

yield* Effect.gen(function* () {
const service = yield* OrchestratorMcpService.OrchestratorMcpService;
const refuse = (model: string) =>
service
.delegateTask(scope, {
task: "Summarize the diff.",
target: { providerInstanceId: codexInstanceId, model },
mode: "async",
clientRequestId: `delegate-unadvertised-${model}`,
})
.pipe(Effect.flip);

const prefixed = yield* refuse("anthropic/claude-opus-5-3");
assert.equal(prefixed.code, "model_unavailable");
assert.equal(
prefixed.message,
"Model anthropic/claude-opus-5-3 is not advertised by provider codex. " +
"Did you mean claude-opus-5-3?",
);

const unmatched = yield* refuse("opus-5.5");
assert.equal(
unmatched.message,
"Model opus-5.5 is not advertised by provider codex. Advertised models: " +
"claude-opus-5-1, claude-opus-5-2, claude-opus-5-3, claude-opus-5-4, claude-opus-5-5, " +
"claude-opus-5-6, claude-opus-5-7, claude-opus-5-8 and 2 more; " +
"orchestrator_capabilities with search finds them.",
);
}).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
}),
);

it.effect("driver-only delegation skips an instance the project turned off", () =>
Effect.gen(function* () {
const altInstanceId = ProviderInstanceId.make("codex-alt");
Expand Down
22 changes: 21 additions & 1 deletion apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,26 @@ function listCapabilityModels(input: {
return { listed, omitted: visible.length - listed.length };
}

/** Names advertised slugs to retry with: same last `/` segment first, else a short list. */
function advertisedModelsHint(models: ReadonlyArray<ServerProviderModel>, requested: string) {
const lastSegment = (slug: string) => slug.slice(slug.lastIndexOf("/") + 1).toLowerCase();
const wanted = lastSegment(requested);
const close = models.filter((model) => lastSegment(model.slug) === wanted);
if (close.length > 0) {
return `Did you mean ${close
.slice(0, LISTED_MODEL_LIMIT)
.map((model) => model.slug)
.join(", ")}?`;
}
const listed = models
.toSorted((a, b) => Number(a.isLegacy ?? false) - Number(b.isLegacy ?? false))
.slice(0, LISTED_MODEL_LIMIT);
const omitted = models.length - listed.length;
return `Advertised models: ${listed.map((model) => model.slug).join(", ")}${
omitted > 0 ? ` and ${omitted} more; orchestrator_capabilities with search finds them` : ""
}.`;
}

/** Option ids and allowed values are all a delegating agent needs; labels are UI copy. */
function capabilityModelOption(descriptor: ProviderOptionDescriptor): OrchestratorMcpModelOption {
const defaultValue = getProviderOptionCurrentValue(descriptor);
Expand All @@ -377,7 +397,7 @@ export const requireAdvertisedModel = (provider: ServerProvider, model: string)
? Effect.fail(
failure(
"model_unavailable",
`Model ${model} is not advertised by provider ${provider.instanceId}.`,
`Model ${model} is not advertised by provider ${provider.instanceId}. ${advertisedModelsHint(provider.models, model)}`,
),
)
: Effect.void;
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/mcp/toolkits/project/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,9 @@ it.effect("a launch with a model its provider does not advertise creates no thre
] as const) {
expect(yield* launchWith(instanceId, model)).toMatchObject({
code: "model_unavailable",
message: `Model ${model} is not advertised by provider ${instanceId}.`,
message: expect.stringContaining(
`Model ${model} is not advertised by provider ${instanceId}. Advertised models: `,
),
});
}
expect(launched).toHaveLength(0);
Expand Down
Loading