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
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,6 @@ private static ModelInfo buildModelInfo(CopilotModel activeModel, String reasoni
}
String id = activeModel != null ? activeModel.getId() : null;
String providerName = activeModel != null ? activeModel.getProviderName() : null;
return new ModelInfo(id, providerName, reasoningEffort);
return new ModelInfo(id, providerName, reasoningEffort, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
public record ContextSizeInfo(
int totalTokenLimit,
int reservedOutputTokens,
int systemPromptTokens,
int toolDefinitionTokens,
int userMessagesTokens,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.List;
import java.util.Objects;

import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.builder.ToStringBuilder;

/**
Expand Down Expand Up @@ -97,17 +98,46 @@ public String toString() {
}

/**
* Per-token prices for the model, returned in USD.
* Per-tier token prices, quoted in USD per {@link CopilotModelBillingTokenPrices#batchSize} tokens and applying up
* to {@code maxContext} context tokens. Requests larger than the {@code default} tier's {@code maxContext} are
* billed at the {@code longContext} tier. All components are optional ({@code null} when the server does not provide
* a value).
*
* @param cachePrice the price for cached input tokens
* @param inputPrice the price for input tokens
* @param outputPrice the price for output tokens
* @param maxContext the maximum number of context (input) tokens this tier applies to
*/
public record CopilotModelBillingTokenPrices(Double cachePrice, Double inputPrice, Double outputPrice,
Double tokenUnit) {
public record CopilotModelTokenPriceTier(Double cachePrice, Double inputPrice, Double outputPrice,
Integer maxContext) {
@Override
public String toString() {
ToStringBuilder builder = new ToStringBuilder(this);
builder.append("cachePrice", cachePrice);
builder.append("inputPrice", inputPrice);
builder.append("outputPrice", outputPrice);
builder.append("tokenUnit", tokenUnit);
builder.append("maxContext", maxContext);
return builder.toString();
}
}

/**
* Per-tier token prices for the model. When token-based billing is enabled the server returns a {@code default}
* tier and, for models that support long context, a {@code longContext} tier.
*
* @param batchSize the number of tokens each tier price is quoted per
* @param defaultTier the {@code default} price tier (deserialized from the {@code default} JSON field)
* @param longContext the {@code longContext} price tier, or {@code null} when the model does not support long
* context
*/
public record CopilotModelBillingTokenPrices(Double batchSize,
@SerializedName("default") CopilotModelTokenPriceTier defaultTier, CopilotModelTokenPriceTier longContext) {
@Override
public String toString() {
ToStringBuilder builder = new ToStringBuilder(this);
builder.append("batchSize", batchSize);
builder.append("defaultTier", defaultTier);
builder.append("longContext", longContext);
return builder.toString();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* @param id model identifier (optional)
* @param providerName provider name (optional)
* @param reasoningEffort user-selected reasoning effort (optional)
* @param contextSize context size (optional)
*/
public record ModelInfo(String id, String providerName, String reasoningEffort) {
public record ModelInfo(String id, String providerName, String reasoningEffort, String contextSize) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public ContextSizeDonut(Composite parent, ContextWindowService contextWindowServ
e.gc.drawArc(arcOffset, arcOffset, arcSize, arcSize, 0, 360);

// Used portion starting from 12 o'clock (90°) going clockwise (negative angle)
double pct = Math.min(info.utilizationPercentage(), 100.0);
double pct = Math.min(contextWindowService.getDisplayUtilizationPercentage(info), 100.0);
int filledAngle = (int) Math.round(pct / 100.0 * 360);
if (filledAngle > 0) {
Color filledColor = pct >= 90 ? CssConstants.getDonutWarningColor(e.display)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ protected void populateContent(Composite parent) {
tokenUsageLabel = createSecondaryTextLabel(tokenRow, formatTokenRow(latestInfo));
tokenUsageLabel.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
percentageLabel = createSecondaryTextLabel(tokenRow,
formatPercentage(latestInfo.utilizationPercentage()));
formatPercentage(contextWindowService.getDisplayUtilizationPercentage(latestInfo)));
percentageLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.NONE, false, false));

progressBar = new ContextWindowBar(parent, SWT.NONE);
progressBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
progressBar.setPercentage((int) Math.round(latestInfo.utilizationPercentage()));
progressBar.setPercentage((int) Math.round(contextWindowService.getDisplayUtilizationPercentage(latestInfo)));

addSeparator(parent, SECTION_SPACING);

Expand Down Expand Up @@ -107,7 +107,7 @@ private void updateLabels(ContextSizeInfo info) {
return;
}
setLabelText(tokenUsageLabel, formatTokenRow(info));
setLabelText(percentageLabel, formatPercentage(info.utilizationPercentage()));
setLabelText(percentageLabel, formatPercentage(contextWindowService.getDisplayUtilizationPercentage(info)));
setLabelText(systemInstructionsValue,
percentageOf(info.systemPromptTokens(), info.totalTokenLimit()));
setLabelText(toolDefinitionsValue,
Expand All @@ -120,7 +120,7 @@ private void updateLabels(ContextSizeInfo info) {
setLabelText(toolResultsValue,
percentageOf(info.toolResultsTokens(), info.totalTokenLimit()));
if (progressBar != null && !progressBar.isDisposed()) {
progressBar.setPercentage((int) Math.round(info.utilizationPercentage()));
progressBar.setPercentage((int) Math.round(contextWindowService.getDisplayUtilizationPercentage(info)));
}
shell.requestLayout();
}
Expand All @@ -146,9 +146,9 @@ private static String formatPercentage(double pct) {
return String.format("%.1f%%", pct);
}

private static String formatTokenRow(ContextSizeInfo info) {
private String formatTokenRow(ContextSizeInfo info) {
return MessageFormat.format(Messages.context_window_tokens,
formatTokens(info.totalUsedTokens()), formatTokens(info.totalTokenLimit()));
formatTokens(info.totalUsedTokens()), formatTokens(contextWindowService.getDisplayTokenLimit(info)));
}

private static String percentageOf(int tokens, int totalLimit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import org.eclipse.swt.widgets.Display;

import com.microsoft.copilot.eclipse.core.lsp.protocol.ContextSizeInfo;
import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel;
import com.microsoft.copilot.eclipse.ui.chat.services.ModelService;
import com.microsoft.copilot.eclipse.ui.utils.ModelUtils;
import com.microsoft.copilot.eclipse.ui.utils.SwtUtils;

/**
Expand All @@ -27,11 +30,15 @@ public class ContextWindowService {

private IObservableValue<ContextSizeInfo> contextSizeObservable;
private final Map<ContextWindowPopup, ISideEffect> popupSideEffects = new HashMap<>();
private final ModelService modelService;

/**
* Creates the service and initializes the observable state on the UI realm.
*
* @param modelService the model service used to resolve the active model's context window
*/
public ContextWindowService() {
public ContextWindowService(ModelService modelService) {
this.modelService = modelService;
AtomicReference<IObservableValue<ContextSizeInfo>> observableRef = new AtomicReference<>();
SwtUtils.invokeOnDisplayThread(() -> {
Realm realm = Realm.getDefault();
Expand All @@ -58,6 +65,68 @@ public ContextSizeInfo getState() {
return result.get();
}

/**
* Returns the context-window limit shown in the donut popup. Prefer the active model's resolved full context window,
* falling back to the language-server usage snapshot when the model metadata is unavailable.
*
* @param info the context usage snapshot
* @return the display context-window limit
*/
public int getDisplayTokenLimit(ContextSizeInfo info) {
if (info == null) {
return 0;
}

Integer outputLimit = getActiveModelOutputLimit();
if (info.reservedOutputTokens() > 0) {
outputLimit = info.reservedOutputTokens();
}
if (outputLimit != null && outputLimit > 0) {
return info.totalTokenLimit() + outputLimit;
}

Integer modelContextWindow = getActiveModelContextWindow();
return modelContextWindow != null && modelContextWindow > 0 ? modelContextWindow : info.totalTokenLimit();
}

/**
* Returns the utilization percentage against the displayed context window.
*
* @param info the context usage snapshot
* @return the utilization percentage
*/
public double getDisplayUtilizationPercentage(ContextSizeInfo info) {
if (info == null) {
return 0;
}
int displayLimit = getDisplayTokenLimit(info);
if (displayLimit <= 0) {
return info.utilizationPercentage();
}
return (double) info.totalUsedTokens() / displayLimit * 100;
}

private Integer getActiveModelContextWindow() {
CopilotModel activeModel = getActiveModel();
if (activeModel == null) {
return null;
}
return ModelUtils.resolveContextWindowSize(activeModel);
}

private Integer getActiveModelOutputLimit() {
CopilotModel activeModel = getActiveModel();
if (activeModel == null || activeModel.getCapabilities() == null
|| activeModel.getCapabilities().limits() == null) {
return null;
}
return activeModel.getCapabilities().limits().maxOutputTokens();
}

private CopilotModel getActiveModel() {
return modelService == null ? null : modelService.getActiveModel();
}

/**
* Updates the current context size state and notifies bound UI.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public ChatServiceManager() {
mcpRuntimeLogger = new McpRuntimeLogger();
persistenceManager = new ConversationPersistenceManager(this.authStatusManager);
chatFontService = new ChatFontService();
contextWindowService = new ContextWindowService();
contextWindowService = new ContextWindowService(modelService);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public final class Messages extends NLS {
public static String model_billing_multiplier_suffix;
public static String model_billing_multiplier_variable;
public static String model_preview_suffix;
public static String model_hover_contextSize;
public static String model_hover_contextWindow;
public static String model_hover_cost;
public static String model_hover_thinkingEffort;
public static String model_hover_thinkingEffort_default_suffix;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ generateCommitMessage_noStagedFiles_message=There are no staged files to generat
addToReference_addFile_title=Add File to Chat
addToReference_addFolder_title=Add Folder to Chat

model_hover_contextSize=Context Size:
model_hover_contextWindow=Context Window:
model_hover_cost=Cost:
model_hover_thinkingEffort=Thinking Effort
model_hover_thinkingEffort_default_suffix={0} (default)
Expand Down
Loading
Loading