diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotModelTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotModelTests.java new file mode 100644 index 00000000..37f7b85d --- /dev/null +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotModelTests.java @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.core.lsp.protocol; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +import com.google.gson.Gson; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel.CopilotModelCustomModel; + +/** + * Tests for {@link CopilotModel}, focusing on the {@code customModel} metadata that carries organization- and + * enterprise-contributed custom (BYOK) models exposed through {@code copilot/models}. + */ +class CopilotModelTests { + + private final Gson gson = new Gson(); + + @Test + void testDeserialize_populatesCustomModelMetadata() { + String json = """ + { + "modelFamily": "custom", + "modelName": "Sonnet (Org)", + "id": "claude-sonnet-org", + "scopes": ["chat-panel", "agent-panel"], + "customModel": { + "keyName": "Contoso Azure Key", + "ownerName": "Contoso", + "ownerType": "organization", + "provider": "azure" + } + } + """; + + CopilotModel model = gson.fromJson(json, CopilotModel.class); + + assertNotNull(model.getCustomModel()); + assertEquals("Contoso Azure Key", model.getCustomModel().keyName()); + assertEquals("Contoso", model.getCustomModel().ownerName()); + assertEquals("organization", model.getCustomModel().ownerType()); + assertEquals("azure", model.getCustomModel().provider()); + } + + @Test + void testDeserialize_customModelAbsentIsNull() { + String json = """ + { + "modelFamily": "gpt-4o", + "modelName": "GPT-4o", + "id": "gpt-4o", + "scopes": ["chat-panel"] + } + """; + + CopilotModel model = gson.fromJson(json, CopilotModel.class); + + assertNull(model.getCustomModel()); + } + + @Test + void testEqualsAndHashCode_accountForCustomModel() { + CopilotModel base = new CopilotModel(); + base.setId("claude-sonnet-org"); + base.setModelName("Sonnet (Org)"); + base.setCustomModel(new CopilotModelCustomModel("Contoso Azure Key", "Contoso", "organization", "azure")); + + CopilotModel same = new CopilotModel(); + same.setId("claude-sonnet-org"); + same.setModelName("Sonnet (Org)"); + same.setCustomModel(new CopilotModelCustomModel("Contoso Azure Key", "Contoso", "organization", "azure")); + + CopilotModel differentOwner = new CopilotModel(); + differentOwner.setId("claude-sonnet-org"); + differentOwner.setModelName("Sonnet (Org)"); + differentOwner.setCustomModel(new CopilotModelCustomModel("Contoso Azure Key", "Fabrikam", "organization", + "azure")); + + assertEquals(base, same); + assertEquals(base.hashCode(), same.hashCode()); + assertNotEquals(base, differentOwner); + } +} diff --git a/com.microsoft.copilot.eclipse.core/copilot-agent/package-lock.json b/com.microsoft.copilot.eclipse.core/copilot-agent/package-lock.json index f389abb6..eda43152 100644 --- a/com.microsoft.copilot.eclipse.core/copilot-agent/package-lock.json +++ b/com.microsoft.copilot.eclipse.core/copilot-agent/package-lock.json @@ -9,18 +9,18 @@ "version": "0.0.1", "hasInstallScript": true, "dependencies": { - "@github/copilot-language-server": "1.502.5", - "@github/copilot-language-server-darwin-arm64": "1.502.5", - "@github/copilot-language-server-darwin-x64": "1.502.5", - "@github/copilot-language-server-linux-arm64": "1.502.5", - "@github/copilot-language-server-linux-x64": "1.502.5", - "@github/copilot-language-server-win32-x64": "1.502.5" + "@github/copilot-language-server": "1.509.5", + "@github/copilot-language-server-darwin-arm64": "1.509.5", + "@github/copilot-language-server-darwin-x64": "1.509.5", + "@github/copilot-language-server-linux-arm64": "1.509.5", + "@github/copilot-language-server-linux-x64": "1.509.5", + "@github/copilot-language-server-win32-x64": "1.509.5" } }, "node_modules/@github/copilot-language-server": { - "version": "1.502.5", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.502.5.tgz", - "integrity": "sha512-vIFbb145TwhDD1KTdJjySAokBcJla6NWsfvqjotM2AsDu3lLtQjPFbt6P90vnMVbQ6Ixc/IbDqsjToaWmXqqIA==", + "version": "1.509.5", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.509.5.tgz", + "integrity": "sha512-cAZVbN5UHrm+ILqroO2NY2gbI7mYojwmiOClCWj8yPD0svXJKSVmmmDonkVrvNZ1VxW3tIbCbE1LRTL6HVN/9g==", "license": "MIT", "dependencies": { "vscode-languageserver-protocol": "^3.17.5" @@ -29,18 +29,18 @@ "copilot-language-server": "dist/language-server.js" }, "optionalDependencies": { - "@github/copilot-language-server-darwin-arm64": "1.502.5", - "@github/copilot-language-server-darwin-x64": "1.502.5", - "@github/copilot-language-server-linux-arm64": "1.502.5", - "@github/copilot-language-server-linux-x64": "1.502.5", - "@github/copilot-language-server-win32-arm64": "1.502.5", - "@github/copilot-language-server-win32-x64": "1.502.5" + "@github/copilot-language-server-darwin-arm64": "1.509.5", + "@github/copilot-language-server-darwin-x64": "1.509.5", + "@github/copilot-language-server-linux-arm64": "1.509.5", + "@github/copilot-language-server-linux-x64": "1.509.5", + "@github/copilot-language-server-win32-arm64": "1.509.5", + "@github/copilot-language-server-win32-x64": "1.509.5" } }, "node_modules/@github/copilot-language-server-darwin-arm64": { - "version": "1.502.5", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-arm64/-/copilot-language-server-darwin-arm64-1.502.5.tgz", - "integrity": "sha512-3QTwdHjX2qpOwRhKIcXj3PU+wr5FodMcHPRnXK2qMv4xiRtibuuqicFVPF7ckO7Xym2o2FfJHfS8e58NNl278w==", + "version": "1.509.5", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-arm64/-/copilot-language-server-darwin-arm64-1.509.5.tgz", + "integrity": "sha512-eGb5bHN1OVUEnQYCop1+Hd4z6CR2fZK0iqFRu0pdGeQ3/y370trt2KR+omJv354ibsCUmpDUIqZXb6hOiIZk/Q==", "cpu": [ "arm64" ], @@ -50,9 +50,9 @@ ] }, "node_modules/@github/copilot-language-server-darwin-x64": { - "version": "1.502.5", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-x64/-/copilot-language-server-darwin-x64-1.502.5.tgz", - "integrity": "sha512-qz3aGl2KtAA/pM65esyXrsv4wBDFoKwUrP4hBAm2XplIPVF931HOluCXGxL0GDU2OMsEAxgFy5xTGODMg1U7kQ==", + "version": "1.509.5", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-x64/-/copilot-language-server-darwin-x64-1.509.5.tgz", + "integrity": "sha512-EonG1QHCx7qmiPc0EsVYSk9oBeWtwHXw0h3fMFOWKN37HOKImMiDeC7JBjv1kG/q9XzHGGhkYjkvReebMKZoOg==", "cpu": [ "x64" ], @@ -62,9 +62,9 @@ ] }, "node_modules/@github/copilot-language-server-linux-arm64": { - "version": "1.502.5", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-arm64/-/copilot-language-server-linux-arm64-1.502.5.tgz", - "integrity": "sha512-8xabGJgGzpa4KVDQc1ZYH3elTzyzVRUWJLlBBQbK4vSpNtsnmGOk4TkY7uJZ7uXtaX1/xDD1venbPWWNOABCig==", + "version": "1.509.5", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-arm64/-/copilot-language-server-linux-arm64-1.509.5.tgz", + "integrity": "sha512-f1SYaxh8drXeZ2Sf5A/gjFC5gg0qnUdwYUWXA2vpJrMZ2bMbylacGS9hwhkwk9jDaHkeOmjOWWh+g2csdLgVTw==", "cpu": [ "arm64" ], @@ -74,9 +74,9 @@ ] }, "node_modules/@github/copilot-language-server-linux-x64": { - "version": "1.502.5", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-x64/-/copilot-language-server-linux-x64-1.502.5.tgz", - "integrity": "sha512-7JcmuKj4yOoDjU3kx4JlVPzC2jDES6nFFqUoNAEnKPFPUjIwf4RJKI2heonIUtFnFXZ9JWT09vqe477oO0F80w==", + "version": "1.509.5", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-x64/-/copilot-language-server-linux-x64-1.509.5.tgz", + "integrity": "sha512-T0ihY6w2H7sdKMaF0k7pHBQV3wTj4Uzq38kx8pGKAFTupUIgivMUHOWysiLdsBgQ9rafByIMzHR3slnpYcH1SQ==", "cpu": [ "x64" ], @@ -86,9 +86,9 @@ ] }, "node_modules/@github/copilot-language-server-win32-arm64": { - "version": "1.502.5", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-arm64/-/copilot-language-server-win32-arm64-1.502.5.tgz", - "integrity": "sha512-7/QHmdWSIOKvbr5s5tH1u8/nQ+AfvqqGbPlpZeokv/hTVwCZZ69ntMVGkoSQVKcSAYVL2h97M5+0zNbofYitow==", + "version": "1.509.5", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-arm64/-/copilot-language-server-win32-arm64-1.509.5.tgz", + "integrity": "sha512-T4fyhZIJCU+ETyp3vcIw0Y+sIvcsnq8C59s2l7RR9jX6lwHacwsU2PUcRYQqETfWOkM5YX7wzKiThLxvsOAc9g==", "cpu": [ "arm64" ], @@ -99,9 +99,9 @@ ] }, "node_modules/@github/copilot-language-server-win32-x64": { - "version": "1.502.5", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-x64/-/copilot-language-server-win32-x64-1.502.5.tgz", - "integrity": "sha512-9jdvyPZnu2muS9Jo6LtmP33NkpTht/2Gc9pWRrkQizuNabWy7jq2KuJdYf1/vwiyEioMiZznGBv+hscrJ9z5LA==", + "version": "1.509.5", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-x64/-/copilot-language-server-win32-x64-1.509.5.tgz", + "integrity": "sha512-tv12/MayELMseV7V+ghdC/AY5GgKh/lyncAU/A7eEpPt8ACNEErBZ51mriAT1F/EqSfB1iEUKVtgZ4/+eTe9Fw==", "cpu": [ "x64" ], diff --git a/com.microsoft.copilot.eclipse.core/copilot-agent/package.json b/com.microsoft.copilot.eclipse.core/copilot-agent/package.json index 7702b184..d6c12fb7 100644 --- a/com.microsoft.copilot.eclipse.core/copilot-agent/package.json +++ b/com.microsoft.copilot.eclipse.core/copilot-agent/package.json @@ -12,11 +12,11 @@ "postinstall": "node copy-binaries.js" }, "dependencies": { - "@github/copilot-language-server": "1.502.5", - "@github/copilot-language-server-win32-x64": "1.502.5", - "@github/copilot-language-server-darwin-x64": "1.502.5", - "@github/copilot-language-server-darwin-arm64": "1.502.5", - "@github/copilot-language-server-linux-x64": "1.502.5", - "@github/copilot-language-server-linux-arm64": "1.502.5" + "@github/copilot-language-server": "1.509.5", + "@github/copilot-language-server-win32-x64": "1.509.5", + "@github/copilot-language-server-darwin-x64": "1.509.5", + "@github/copilot-language-server-darwin-arm64": "1.509.5", + "@github/copilot-language-server-linux-x64": "1.509.5", + "@github/copilot-language-server-linux-arm64": "1.509.5" } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java index de116abb..b7acef42 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageServerConnection.java @@ -708,19 +708,15 @@ private String getModelName(CopilotModel activeModel) { } /** - * Builds the {@link ModelInfo} payload to forward with chat requests. Returns {@code null} when no reasoning effort - * is available so we do not send redundant {@code id}/{@code providerName} fields ahead of the future migration - * away from the legacy {@code model}/{@code modelProviderName} fields. Today the language server only consumes - * {@code modelInfo.reasoningEffort}, so suppressing the payload when there is nothing meaningful to forward keeps - * the protocol surface minimal and avoids implicit behaviour changes if the server starts honouring id/providerName - * before the client migration lands. + * Builds the {@link ModelInfo} payload for chat requests. Always sends the concrete model id (which the server + * prefers over the legacy {@code model} family field), since the family is not unique across models. Returns + * {@code null} when no model id is available. */ private static ModelInfo buildModelInfo(CopilotModel activeModel, String reasoningEffort) { - if (StringUtils.isBlank(reasoningEffort)) { + if (activeModel == null || StringUtils.isBlank(activeModel.getId())) { return null; } - String id = activeModel != null ? activeModel.getId() : null; - String providerName = activeModel != null ? activeModel.getProviderName() : null; - return new ModelInfo(id, providerName, reasoningEffort, null); + String effort = StringUtils.isBlank(reasoningEffort) ? null : reasoningEffort; + return new ModelInfo(activeModel.getId(), activeModel.getProviderName(), effort, null); } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotModel.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotModel.java index dfc8bd3c..b766f49c 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotModel.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/CopilotModel.java @@ -25,6 +25,7 @@ public class CopilotModel { private boolean isChatFallback; private CopilotModelCapabilities capabilities; private CopilotModelBilling billing; + private CopilotModelCustomModel customModel; private String degradationReason; private String providerName; private String modelPickerCategory; @@ -158,6 +159,28 @@ public String toString() { } } + /** + * Metadata describing a custom (BYOK) model that is contributed to the user by an organization or enterprise. When + * present, the model is served through a key that the owner configured, so it is surfaced in the model picker + * automatically without the user having to register their own API key. + * + * @param keyName the display name of the API key the model is served through + * @param ownerName the name of the organization, enterprise, or user that contributed the model + * @param ownerType the type of the owner (e.g. {@code organization}, {@code enterprise}, {@code user}) + * @param provider the underlying model provider (e.g. {@code azure}, {@code openai}) + */ + public record CopilotModelCustomModel(String keyName, String ownerName, String ownerType, String provider) { + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.append("keyName", keyName); + builder.append("ownerName", ownerName); + builder.append("ownerType", ownerType); + builder.append("provider", provider); + return builder.toString(); + } + } + public String getModelFamily() { return modelFamily; } @@ -246,6 +269,14 @@ public void setBilling(CopilotModelBilling billing) { this.billing = billing; } + public CopilotModelCustomModel getCustomModel() { + return customModel; + } + + public void setCustomModel(CopilotModelCustomModel customModel) { + this.customModel = customModel; + } + public String getDegradationReason() { return degradationReason; } @@ -300,6 +331,7 @@ public boolean equals(Object obj) { } CopilotModel other = (CopilotModel) obj; return Objects.equals(billing, other.billing) && Objects.equals(capabilities, other.capabilities) + && Objects.equals(customModel, other.customModel) && Objects.equals(degradationReason, other.degradationReason) && Objects.equals(id, other.id) && isChatDefault == other.isChatDefault && isChatFallback == other.isChatFallback && Objects.equals(modelFamily, other.modelFamily) && Objects.equals(modelName, other.modelName) @@ -312,8 +344,9 @@ public boolean equals(Object obj) { @Override public int hashCode() { - return Objects.hash(billing, capabilities, degradationReason, id, isChatDefault, isChatFallback, modelFamily, - modelName, modelPickerCategory, modelPickerPriceCategory, modelPolicy, preview, providerName, scopes, vendor); + return Objects.hash(billing, capabilities, customModel, degradationReason, id, isChatDefault, isChatFallback, + modelFamily, modelName, modelPickerCategory, modelPickerPriceCategory, modelPolicy, preview, providerName, + scopes, vendor); } @Override @@ -330,6 +363,7 @@ public String toString() { builder.append("isChatFallback", isChatFallback); builder.append("capabilities", capabilities); builder.append("billing", billing); + builder.append("customModel", customModel); builder.append("degradationReason", degradationReason); builder.append("providerName", providerName); builder.append("modelPickerCategory", modelPickerCategory); diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/ModelInfo.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/ModelInfo.java index de61df10..e7ff3e84 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/ModelInfo.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/protocol/ModelInfo.java @@ -7,10 +7,10 @@ * Optional model metadata associated with a conversation turn. Mirrors the {@code modelInfo} field on the * {@code conversation/create} and {@code conversation/turn} LSP requests and responses. * - *

The {@code id} and {@code providerName} fields are reserved for a future migration away from the legacy - * {@code model} / {@code modelProviderName} fields and should not be relied upon today. The {@code reasoningEffort} - * field carries the user-selected reasoning effort level (e.g. {@code low}, {@code medium}, {@code high}) when the - * model surfaces selectable effort levels. + *

The language server resolves the turn's model from {@code id} (and {@code providerName} for BYOK models) in + * preference to the legacy {@code model} / {@code modelProviderName} request fields, so {@code id} should carry the + * concrete model id of the user-selected model. The {@code reasoningEffort} field carries the user-selected reasoning + * effort level (e.g. {@code low}, {@code medium}, {@code high}) when the model surfaces selectable effort levels. * * @param id model identifier (optional) * @param providerName provider name (optional) diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/utils/ModelUtilsTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/utils/ModelUtilsTests.java index e8448ea6..e2185223 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/utils/ModelUtilsTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/utils/ModelUtilsTests.java @@ -17,6 +17,7 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel.CopilotModelCapabilities; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel.CopilotModelCapabilitiesLimits; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel.CopilotModelCapabilitiesSupports; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel.CopilotModelCustomModel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotScope; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokModel; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokModelCapabilities; @@ -150,6 +151,27 @@ void testSupportsReasoningEffortLevel_falseWhenCapabilitiesMissing() { assertFalse(ModelUtils.supportsReasoningEffortLevel(null)); } + @Test + void testGetModelSuffix_customModelUsesProvider() { + // Organization-contributed custom models arrive without a providerName but carry their provider in the + // custom-model metadata; the suffix should surface that provider like a BYOK model. + CopilotModel model = new CopilotModel(); + model.setModelName("Sonnet (Org)"); + model.setCustomModel(new CopilotModelCustomModel("Contoso Azure Key", "Contoso", "organization", "Azure")); + + assertEquals("Azure", ModelUtils.getModelSuffix(model, null)); + } + + @Test + void testGetModelSuffix_providerNameTakesPrecedenceOverCustomModel() { + CopilotModel model = new CopilotModel(); + model.setModelName("GPT-4o"); + model.setProviderName("OpenAI"); + model.setCustomModel(new CopilotModelCustomModel("Key", "Contoso", "organization", "Azure")); + + assertEquals("OpenAI", ModelUtils.getModelSuffix(model, null)); + } + @Test void testIsAutoModel() { CopilotModel auto = new CopilotModel(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ModelPickerGroupsBuilder.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ModelPickerGroupsBuilder.java index fb838528..46a245ee 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ModelPickerGroupsBuilder.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ModelPickerGroupsBuilder.java @@ -66,7 +66,7 @@ public static List build(Map modelMap, List customModels = new ArrayList<>(); for (CopilotModel model : modelMap.values()) { - if (model.getProviderName() != null) { + if (model.getProviderName() != null || model.getCustomModel() != null) { customModels.add(model); } else if (model.getBilling() != null) { if (model.getBilling().isPremium()) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java index cb3fc2dd..642b71b1 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/Messages.java @@ -203,6 +203,7 @@ public final class Messages extends NLS { public static String model_hover_cost; public static String model_hover_thinkingEffort; public static String model_hover_thinkingEffort_default_suffix; + public static String model_hover_customModelInfo; public static String model_reasoningEffort_none; public static String model_reasoningEffort_low; public static String model_reasoningEffort_medium; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties index 3974ed60..bb6b4400 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/i18n/messages.properties @@ -197,6 +197,7 @@ model_hover_contextWindow=Context Window: model_hover_cost=Cost: model_hover_thinkingEffort=Thinking Effort model_hover_thinkingEffort_default_suffix={0} (default) +model_hover_customModelInfo={0} is contributed by {1} using {2} model_reasoningEffort_none=None model_reasoningEffort_low=Low model_reasoningEffort_medium=Medium diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/ModelHoverContentProvider.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/ModelHoverContentProvider.java index 1c963f89..425a6b54 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/ModelHoverContentProvider.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/ModelHoverContentProvider.java @@ -27,6 +27,7 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel.CopilotModelCapabilitiesSupports; +import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel.CopilotModelCustomModel; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.services.ModelService; import com.microsoft.copilot.eclipse.ui.i18n.Messages; @@ -76,6 +77,8 @@ public void configureHover(Composite parent, DropdownItem item, Runnable closeRe addWarningRow(parent, model.getDegradationReason()); } + addCustomModelInfoSection(parent, item.getLabel()); + addContextWindowSection(parent); addPricingSection(parent, model.getModelPickerPriceCategory()); addThinkingEffortSection(parent, closeRequest); @@ -89,6 +92,36 @@ private void renderHeader(Composite parent, DropdownItem item) { titleLabel.setLayoutData(headerGd); } + /** + * Renders the "contributed by" line for organization/enterprise-contributed custom (BYOK) models. Mirrors the + * IntelliJ model tooltip: the row only appears when the model carries custom-model metadata with a non-blank owner + * and key name, communicating that the model was provided by an owner through a specific key. + * + * @param parent the hover composite to render into + * @param displayedName the model name as shown in the hover header + */ + private void addCustomModelInfoSection(Composite parent, String displayedName) { + CopilotModelCustomModel customModel = model.getCustomModel(); + if (customModel == null) { + return; + } + String ownerName = StringUtils.trimToNull(customModel.ownerName()); + String keyName = StringUtils.trimToNull(customModel.keyName()); + if (ownerName == null || keyName == null) { + return; + } + + String modelLabel = StringUtils.defaultIfBlank(displayedName, model.getModelName()); + String infoText = NLS.bind(Messages.model_hover_customModelInfo, + new Object[] { modelLabel, ownerName, keyName }); + Label infoLabel = new Label(parent, SWT.WRAP); + infoLabel.setText(infoText); + setCssClass(infoLabel, POPUP_SECONDARY_TEXT_CLASS); + GridData gd = new GridData(SWT.FILL, SWT.NONE, true, false); + gd.verticalIndent = SECTION_SPACING; + infoLabel.setLayoutData(gd); + } + private void addContextWindowSection(Composite parent) { String contextWindowText = ModelUtils.getContextWindowText(model); if (StringUtils.isBlank(contextWindowText)) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/ModelUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/ModelUtils.java index f806727f..5919742d 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/ModelUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/ModelUtils.java @@ -101,6 +101,11 @@ public static String getModelSuffix(CopilotModel model, String reasoningEffort) if (model.getProviderName() != null) { return model.getProviderName(); } + // Organization/enterprise-contributed custom models arrive through copilot/models without a providerName, but + // still carry their underlying provider in the custom-model metadata. Surface it so they read like BYOK models. + if (model.getCustomModel() != null && StringUtils.isNotBlank(model.getCustomModel().provider())) { + return model.getCustomModel().provider(); + } if (isAutoModel(model)) { return Messages.model_billing_multiplier_variable; }