Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.lsp.protocol.byok;

import static org.junit.jupiter.api.Assertions.assertEquals;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.jupiter.api.Test;

class ByokProviderConfigTests {

private static final Gson GSON = new Gson();

@Test
void testProviderConfig_serializesClsFieldNames() {
ByokProviderConfig config = new ByokProviderConfig("Ollama", "http://localhost:11434");

JsonObject json = JsonParser.parseString(GSON.toJson(config)).getAsJsonObject();

assertEquals("Ollama", json.get("providerName").getAsString());
assertEquals("http://localhost:11434", json.get("url").getAsString());
}

@Test
void testListProviderConfigParams_nullProviderSerializesEmptyObject() {
ByokListProviderConfigParams params = new ByokListProviderConfigParams(null);

JsonObject json = JsonParser.parseString(GSON.toJson(params)).getAsJsonObject();

assertEquals(0, json.size());
}

@Test
void testDeleteProviderConfigParams_serializesOnlyProviderName() {
ByokDeleteProviderConfigParams params = new ByokDeleteProviderConfigParams("Ollama");

JsonObject json = JsonParser.parseString(GSON.toJson(params)).getAsJsonObject();

assertEquals(1, json.size());
assertEquals("Ollama", json.get("providerName").getAsString());
}

@Test
void testListProviderConfigResponse_deserializesClsResponse() {
ByokListProviderConfigResponse response = GSON.fromJson(
"{\"providers\":[{\"providerName\":\"Ollama\",\"url\":\"http://localhost:11434\"}]}",
ByokListProviderConfigResponse.class);

assertEquals(1, response.providers().size());
assertEquals(new ByokProviderConfig("Ollama", "http://localhost:11434"), response.providers().get(0));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,14 @@
import com.microsoft.copilot.eclipse.core.lsp.protocol.UpdateMcpToolsStatusParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.WorkspaceFoldersParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokApiKey;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokDeleteProviderConfigParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListApiKeyResponse;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListModelParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListModelResponse;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListProviderConfigParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListProviderConfigResponse;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokModel;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokProviderConfig;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokStatusResponse;
import com.microsoft.copilot.eclipse.core.lsp.protocol.git.GenerateCommitMessageParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.git.GenerateCommitMessageResult;
Expand Down Expand Up @@ -276,6 +280,24 @@ public interface CopilotLanguageServer extends LanguageServer {
@JsonRequest("copilot/byok/listApiKeys")
CompletableFuture<ByokListApiKeyResponse> listByokApiKeys(ByokApiKey apiKey);

/**
* Save a built-in BYOK provider configuration.
*/
@JsonRequest("copilot/byok/saveProviderConfig")
CompletableFuture<ByokStatusResponse> saveByokProviderConfig(ByokProviderConfig providerConfig);

/**
* Delete a built-in BYOK provider configuration.
*/
@JsonRequest("copilot/byok/deleteProviderConfig")
CompletableFuture<ByokStatusResponse> deleteByokProviderConfig(ByokDeleteProviderConfigParams params);

/**
* List built-in BYOK provider configurations.
*/
@JsonRequest("copilot/byok/listProviderConfigs")
CompletableFuture<ByokListProviderConfigResponse> listByokProviderConfigs(ByokListProviderConfigParams params);

/**
* Update the status of the mcp server and tools.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,14 @@
import com.microsoft.copilot.eclipse.core.lsp.protocol.UpdateMcpToolsStatusParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.WorkspaceFoldersParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokApiKey;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokDeleteProviderConfigParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListApiKeyResponse;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListModelParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListModelResponse;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListProviderConfigParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokListProviderConfigResponse;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokModel;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokProviderConfig;
import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokStatusResponse;
import com.microsoft.copilot.eclipse.core.lsp.protocol.git.GenerateCommitMessageParams;
import com.microsoft.copilot.eclipse.core.lsp.protocol.git.GenerateCommitMessageResult;
Expand Down Expand Up @@ -591,6 +595,37 @@ public CompletableFuture<ByokListApiKeyResponse> listByokApiKeys(ByokApiKey apiK
return this.languageServerWrapper.execute(fn);
}

/**
* Save a built-in BYOK provider configuration.
*/
public CompletableFuture<ByokStatusResponse> saveByokProviderConfig(ByokProviderConfig providerConfig) {
Function<LanguageServer, CompletableFuture<ByokStatusResponse>> fn = server -> {
return ((CopilotLanguageServer) server).saveByokProviderConfig(providerConfig);
};
return this.languageServerWrapper.execute(fn);
}

/**
* Delete a built-in BYOK provider configuration.
*/
public CompletableFuture<ByokStatusResponse> deleteByokProviderConfig(ByokDeleteProviderConfigParams params) {
Function<LanguageServer, CompletableFuture<ByokStatusResponse>> fn = server -> {
return ((CopilotLanguageServer) server).deleteByokProviderConfig(params);
};
return this.languageServerWrapper.execute(fn);
}

/**
* List built-in BYOK provider configurations.
*/
public CompletableFuture<ByokListProviderConfigResponse> listByokProviderConfigs(
ByokListProviderConfigParams params) {
Function<LanguageServer, CompletableFuture<ByokListProviderConfigResponse>> fn = server -> {
return ((CopilotLanguageServer) server).listByokProviderConfigs(params);
};
return this.languageServerWrapper.execute(fn);
}

/**
* Save a BYOK API key.
*/
Expand Down Expand Up @@ -664,7 +699,6 @@ public CompletableFuture<SearchPrResponse> searchPr(SearchPrParams params) {
return this.languageServerWrapper.execute(fn);
}


/**
* Notify that an inline edit was shown.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.lsp.protocol.byok;

/**
* Parameters for deleting a built-in BYOK provider configuration.
*
* @param providerName provider name
*/
public record ByokDeleteProviderConfigParams(String providerName) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.lsp.protocol.byok;

import org.eclipse.jdt.annotation.Nullable;

/**
* Parameters for listing built-in BYOK provider configurations.
*
* @param providerName provider name, or {@code null} to list all configured providers
*/
public record ByokListProviderConfigParams(@Nullable String providerName) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.lsp.protocol.byok;

import java.util.List;

/**
* Response model for listing provider-level BYOK configurations.
*
* @param providers provider configurations
*/
public record ByokListProviderConfigResponse(List<ByokProviderConfig> providers) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public enum ByokModelProvider {
GEMINI("Gemini"),
GROQ("Groq"),
OPENROUTER("OpenRouter"),
ANTHROPIC("Anthropic");
ANTHROPIC("Anthropic"),
OLLAMA("Ollama");


private final String displayName;
Expand All @@ -33,6 +34,20 @@ public static boolean isAzure(String providerDisplayName) {
return AZURE.getDisplayName().equals(providerDisplayName);
}

/**
* Utility to check if a provider display name corresponds to Ollama.
*/
public static boolean isOllama(String providerDisplayName) {
return OLLAMA.getDisplayName().equals(providerDisplayName);
}

/**
* Returns whether the provider requires a provider-level API key.
*/
public static boolean requiresApiKey(String providerDisplayName) {
return !isAzure(providerDisplayName) && !isOllama(providerDisplayName);
}

@Override
public String toString() {
return displayName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.lsp.protocol.byok;

/**
* Provider-level BYOK configuration.
*
* @param providerName provider display name
* @param url provider endpoint URL
*/
public record ByokProviderConfig(String providerName, String url) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ Entry points exercised:
com.microsoft.copilot.eclipse.ui.preferences.ByokPreferencePage`.

Providers covered (`ByokModelProvider`): `Azure`, `OpenAI`, `Gemini`, `Groq`,
`OpenRouter`, `Anthropic`. Azure is special-cased: it has no top-level API
key, so the **Change API…** / **Delete API…** buttons stay disabled for it
even when models are configured.
`OpenRouter`, `Anthropic`, `Ollama`. Azure has per-model deployment credentials.
Ollama has no API key and instead uses a provider-level endpoint URL, defaulting
to `http://localhost:11434`.

Not exercised in this plan (separate scenarios):
- Actually issuing chat completions through a registered BYOK model — that's
Expand All @@ -52,6 +52,8 @@ Not exercised in this plan (separate scenarios):
language server's secure store as part of the TC.
- For Azure-specific TCs: a deployment URL and API key for an Azure OpenAI
deployment (or skip the Azure cases).
- For Ollama-specific TCs: Ollama 0.6.4 or newer is running and has at least
one installed model.
- No previously opened Preferences dialog. The probe runner pre-suppresses
Quick Start, What's New, Welcome, and "Terminal Support Unavailable"
pop-ups — keep that contract when authoring follow-up plans.
Expand Down Expand Up @@ -86,8 +88,8 @@ Not exercised in this plan (separate scenarios):
5. Verify the **Provider** group is visible with the description
**Select a provider before adding models.**
6. Verify the tree has two columns — **Custom Models** and **Status** —
and contains exactly the six providers `Azure`, `OpenAI`, `Gemini`,
`Groq`, `OpenRouter`, `Anthropic`.
and contains exactly the seven providers `Azure`, `OpenAI`, `Gemini`,
`Groq`, `OpenRouter`, `Anthropic`, `Ollama`.
7. Verify the action buttons are present on the right side:
**Add Model...**, **Remove Model**, **Enable** / **Disable**, **Reload**,
**Change API...**, **Delete API...**. With no selection, **Add Model...**,
Expand All @@ -96,7 +98,7 @@ Not exercised in this plan (separate scenarios):

#### Expected Result
- The page opens without an error dialog.
- All six providers render as collapsible tree nodes.
- All seven providers render as collapsible tree nodes.
- Button enablement matches the no-selection state described above.
- `workspace.log` contains no `ERROR` entries from
`com.microsoft.copilot.eclipse.ui.preferences.ByokPreferencePage` or
Expand All @@ -105,7 +107,7 @@ Not exercised in this plan (separate scenarios):

#### 📸 Key Screenshots
- [ ] **Loading state** — overlay shown immediately after the page opens.
- [ ] **Loaded state** — provider tree visible with the six providers and
- [ ] **Loaded state** — provider tree visible with the seven providers and
the action buttons on the right.

#### Notes on failure modes
Expand Down Expand Up @@ -275,6 +277,33 @@ Not exercised in this plan (separate scenarios):

---

### TC-005A: Configure Ollama and expose discovered models in the selector

**Type:** `Happy Path`
**Priority:** `P0`

#### Preconditions
- TC-001 preconditions hold.
- Ollama 0.6.4 or newer is running with at least one installed model.

#### Steps
1. Open the BYOK page, select **Ollama**, and click **Add Model...**.
2. Verify the **Configure Ollama** dialog contains an **Endpoint URL** field
defaulted to `http://localhost:11434` and no API key field.
3. Click **OK** and wait for the Ollama loading indicator to clear.
4. Expand **Ollama** and verify the installed models appear as enabled.
5. Open the chat view model selector and verify the enabled Ollama models are
listed under the Ollama provider.
6. Return to Model Management, select **Ollama**, and verify the actions read
**Change URL...** and **Delete URL...**.

#### Expected Result
- The endpoint is persisted as provider-level configuration.
- Discovered Ollama models are registered automatically and appear in the
model selector without requiring an API key or a separate enable action.

---

## 3. Custom model management

### TC-006: Add a custom model under a provider with an API key
Expand Down
Loading
Loading