Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7faabe1
fix(usage): define missing esc() HTML-escape helper in webview (Model…
Fahad090NP Aug 14, 2026
171adc9
fix(request): guard tool-schema sanitizer against cyclic schemas (sta…
Fahad090NP Aug 14, 2026
42eeb56
fix(streaming): cancel cleanly when the user aborts during 5xx backoff
Fahad090NP Aug 14, 2026
108c346
fix(agents): revert auto-enabled core settings when autoEnableAgentsW…
Fahad090NP Aug 14, 2026
c4b5246
fix(usage): count reasoning marker as internal data (no phantom token…
Fahad090NP Aug 14, 2026
12a06ac
fix(utils): correct negative USD sign placement and escape single quotes
Fahad090NP Aug 14, 2026
acae699
fix(usage): bucket mid-day events into the correct day (floor instead…
Fahad090NP Aug 14, 2026
3a53a86
fix(usage): clamp subscription percent to [0, 100] (no negative perce…
Fahad090NP Aug 14, 2026
76ef45a
fix(usage): clear per-profile serverUsage and everTracked state on pr…
Fahad090NP Aug 14, 2026
2114186
fix(streaming): remove dead delta.message reasoning extraction
Fahad090NP Aug 14, 2026
15643f5
fix(provider): clamp temperature to the provider-accepted range
Fahad090NP Aug 14, 2026
df6206e
fix(models): sync Go model lists (drop dead ring entry, add minimax-m3)
Fahad090NP Aug 14, 2026
1b355af
fix(usage): raise sqlite3 history read cap so large histories don't s…
Fahad090NP Aug 14, 2026
eac86c9
fix(streaming): read 5xx body so Router.Unavailable retry branch actu…
Fahad090NP Aug 14, 2026
84ec592
fix(autocomplete): don't let a stale token cancel the newer pending d…
Fahad090NP Aug 14, 2026
2df9c4c
fix(vision): keep a placeholder on empty vision descriptions; reuse c…
Fahad090NP Aug 14, 2026
da881c9
fix(streaming): don't drop reasoning when there is no progress sink
Fahad090NP Aug 14, 2026
54fb5c4
fix(routing): emit stable synthetic ids for Google tool calls
Fahad090NP Aug 14, 2026
05f5711
fix(provider): keep reasoning_content when merging consecutive assist…
Fahad090NP Aug 14, 2026
e147107
fix(usage): clamp progress bar to [0,100]
Fahad090NP Aug 14, 2026
27f368c
fix(streaming): send Accept header on chat POST requests
Fahad090NP Aug 14, 2026
f7a6cb4
fix(request): preserve top-level tool-schema enums and common keywords
Fahad090NP Aug 14, 2026
a1782c0
fix(streaming): don't emit duplicate text when both message and choic…
Fahad090NP Aug 14, 2026
f39ffb0
fix(thinking): expose plain 'on' option for toggle+effort models
Fahad090NP Aug 14, 2026
be3743b
fix(provider): validate thinking setting values and cap timeout settings
Fahad090NP Aug 14, 2026
1072290
fix(streaming): report reasoning marker through the context-window pr…
Fahad090NP Aug 14, 2026
1df4a2c
fix(streaming): add empty-response diagnostics to Anthropic and Googl…
Fahad090NP Aug 14, 2026
b3ad8e1
fix(provider): log request completion for the Google transport too
Fahad090NP Aug 14, 2026
de4bc78
fix(tooling): lint staged renames and resolve .js specifiers to .ts s…
Fahad090NP Aug 14, 2026
22c7659
fix(usage): estimate cost for missing bundled models instead of track…
Fahad090NP Aug 14, 2026
edfd071
fix(provider): use a real model family so VS Code family selection/gr…
Fahad090NP Aug 14, 2026
1b169fa
fix(usage): stop auto-resolving the active profile over the user's ex…
Fahad090NP Aug 14, 2026
a2e5745
fix(errors): cap absurd rate-limit reset durations in error messages
Fahad090NP Aug 14, 2026
4e076b0
refactor(thinking): derive picker options from the shared validated a…
Fahad090NP Aug 14, 2026
bb1617f
fix(diagnostics): one failing vendor can't abort the whole picker report
Fahad090NP Aug 14, 2026
eff395a
docs(usage): point active-profile override comment at the right issue…
Fahad090NP Aug 17, 2026
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
13 changes: 11 additions & 2 deletions scripts/staged-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ function run(cmd: string, args: string[]): CommandResult {
return { status: res.status, output: `${res.stdout}${res.stderr}`.trim() };
}

/** Staged (added/copied/modified) file paths relative to the repo root. */
/** Staged (added/copied/modified/renamed) file paths relative to the repo root. */
function stagedFiles(): string[] {
const res = run("git", ["diff", "--cached", "--name-only", "-z", "--diff-filter=ACM"]);
// Include renamed (R) files and enable rename detection so a staged rename's
// new path is linted too.
const res = run("git", ["diff", "--cached", "--name-only", "-z", "--find-renames", "--diff-filter=ACMR"]);
if (res.status !== 0) {
return [];
}
Expand Down Expand Up @@ -88,6 +90,13 @@ function resolveImport(fromFile: string, spec: string): string | undefined {
path.join(base, "index.ts"),
path.join(base, "index.js"),
];
// NodeNext-style: in ESM, `./foo.js` resolves to `./foo.ts`. Without this,
// changing foo.ts would not lint its dependents (this repo imports ESM
// scripts with `.js` specifiers that map to `.ts` sources).
if (/\.(js|cjs|mjs)$/.test(base)) {
const tsBase = base.replace(/\.(js|cjs|mjs)$/, "");
candidates.unshift(`${tsBase}.ts`, `${tsBase}.tsx`);
}
for (const candidate of candidates) {
try {
statSync(candidate);
Expand Down
7 changes: 6 additions & 1 deletion src/autocomplete/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,12 @@ export class OpenCodeInlineCompletionProvider implements vscode.InlineCompletion
};

const tokenSubscription = token.onCancellationRequested(() => {
this.debouncer.cancel();
// Do NOT cancel the shared debouncer here: VS Code may cancel this
// request's token AFTER a newer keystroke already scheduled its own
// debounced run, and aborting the debouncer would kill that newer
// pending suggestion. The debouncer cancels the previous run itself
// when the next debounce() is scheduled; here we only resolve this
// request's promise as "no suggestion".
finish(undefined);
});

Expand Down
6 changes: 5 additions & 1 deletion src/chatParts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ export function createUsageDataParts(usage: UsageSnapshot): vscode.LanguageModel
}

export function isInternalDataPart(part: vscode.LanguageModelDataPart): boolean {
return part.mimeType === OPENCODE_USAGE_DATA_MIME || part.mimeType === COPILOT_USAGE_DATA_MIME;
return (
part.mimeType === OPENCODE_USAGE_DATA_MIME ||
part.mimeType === COPILOT_USAGE_DATA_MIME ||
part.mimeType === OPENCODE_REASONING_DATA_MIME
);
}

/**
Expand Down
10 changes: 9 additions & 1 deletion src/commands/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ export async function showModelPickerDiagnostics(): Promise<void> {
const sections: string[] = [];

for (const vendor of vendors) {
const models = await vscode.lm.selectChatModels({ vendor });
let models: readonly vscode.LanguageModelChat[];
try {
models = await vscode.lm.selectChatModels({ vendor });
} catch (error) {
// One failing vendor (e.g. no Copilot models installed) must not abort
// the whole diagnostics report.
sections.push(`## vendor: ${vendor}`, "", `selection error: ${error instanceof Error ? error.message : String(error)}`, "");
continue;
}
sections.push(`## vendor: ${vendor}`, "", `models: ${String(models.length)}`, "");
for (const model of models) {
const internalModel = model as unknown as { configurationSchema?: unknown; detail?: unknown };
Expand Down
23 changes: 12 additions & 11 deletions src/commands/thinkingPicker.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import * as vscode from "vscode";
import { CONFIG_SECTION } from "../config";
import { getSettings } from "../provider/settings";
import type { ThinkingSettings } from "../thinking";
import { getSettings, THINKING_ALLOWED_VALUES } from "../provider/settings";

/** Pick a model family then set its Thinking effort (writes config). */
export async function showThinkingEffortPicker(): Promise<void> {
const families: { label: string; key: keyof ThinkingSettings; options: string[] }[] = [
{ label: "DeepSeek (deepseek-v4-*)", key: "deepseek", options: ["off", "low", "medium", "high", "max"] },
{ label: "GLM (glm-5, glm-5.1, glm-5.2)", key: "glm", options: ["off", "high", "max"] },
{ label: "Kimi (kimi-k2.*)", key: "kimi", options: ["on", "off"] },
{ label: "Mimo (mimo-v2.*)", key: "mimo", options: ["off", "low", "medium", "high"] },
{ label: "MiniMax (minimax-m*)", key: "minimax", options: ["off", "on"] },
{ label: "OpenAI GPT (gpt-*)", key: "openai", options: ["off", "low", "medium", "high", "xhigh"] },
{ label: "Qwen (qwen3.*)", key: "qwen", options: ["auto", "on", "off"] },
{ label: "Qwen Thinking Budget", key: "qwenBudget", options: ["auto", "4096", "16384", "32768", "81920"] },
// Single source of truth (shared with request-time validation) so the option
// lists can never drift from what the request builder actually accepts.
const families: { label: string; key: keyof typeof THINKING_ALLOWED_VALUES; options: string[] }[] = [
{ label: "DeepSeek (deepseek-v4-*)", key: "deepseek", options: [...THINKING_ALLOWED_VALUES.deepseek] },
{ label: "GLM (glm-5, glm-5.1, glm-5.2)", key: "glm", options: [...THINKING_ALLOWED_VALUES.glm] },
{ label: "Kimi (kimi-k2.*)", key: "kimi", options: [...THINKING_ALLOWED_VALUES.kimi] },
{ label: "Mimo (mimo-v2.*)", key: "mimo", options: [...THINKING_ALLOWED_VALUES.mimo] },
{ label: "MiniMax (minimax-m*)", key: "minimax", options: [...THINKING_ALLOWED_VALUES.minimax] },
{ label: "OpenAI GPT (gpt-*)", key: "openai", options: [...THINKING_ALLOWED_VALUES.openai] },
{ label: "Qwen (qwen3.*)", key: "qwen", options: [...THINKING_ALLOWED_VALUES.qwen] },
{ label: "Qwen Thinking Budget", key: "qwenBudget", options: [...THINKING_ALLOWED_VALUES.qwenBudget] },
];
const settings = getSettings().thinking;
const family = await vscode.window.showQuickPick(
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ export const COMPLETION_USAGE_MAX_DAYS = 370;

export const PROFILES_REGISTRY_KEY = "opencodego.profiles.v1";
export const ACTIVE_PROFILE_KEY = "opencodego.activeProfile.v1";
/** Set once the user explicitly picks a profile β€” auto-resolution must not override it. */
export const ACTIVE_PROFILE_EXPLICIT_KEY = "opencodego.activeProfileExplicit.v1";
export const MIGRATED_KEY = "opencodego.migratedTo.v1";
export const LEGACY_SECRET_KEY = SECRET_KEY;
export const LEGACY_FINGERPRINT = "legacy";
Expand Down
9 changes: 6 additions & 3 deletions src/core/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,10 @@ export function normalizeGoogleStreamEvent(data: unknown): unknown {
return [
{
index,
id: "",
// Gemini has no native tool-call ids; emit a stable synthetic one so
// downstream tool-call parts carry a real callId (empty ids made calls
// indistinguishable and broke reasoning replication).
id: `google-tool-${String(index)}`,
type: "function",
function: {
name: part.functionCall.name,
Expand Down Expand Up @@ -273,14 +276,14 @@ export function normalizeGoogleFullResponse(data: unknown): unknown {
.filter((part) => typeof part.text === "string" && part.thought === true)
.map((part) => part.text as string)
.join("");
const toolCalls = parts.flatMap((part) => {
const toolCalls = parts.flatMap((part, index) => {
if (!isRecord(part.functionCall) || typeof part.functionCall.name !== "string") {
return [];
}

return [
{
id: "",
id: `google-tool-${String(index)}`,
type: "function",
function: {
name: part.functionCall.name,
Expand Down
9 changes: 8 additions & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,19 +223,26 @@ function parseRetryAfter(value: string | undefined): number | undefined {
return Number.isFinite(dateMs) ? Math.max(0, dateMs - Date.now()) : parseDurationLike(value);
}

/** Ceiling for a seconds-remaining reset value β€” anything larger is nonsense. */
const MAX_RESET_REMAINING_SECONDS = 24 * 60 * 60;

function parseResetAfter(value: string | undefined): number | undefined {
if (!value) {
return undefined;
}
const numeric = Number(value);
if (Number.isFinite(numeric) && numeric >= 0) {
if (numeric > 1_000_000_000_000) {
// Epoch milliseconds (past timestamps clamp to 0).
return Math.max(0, numeric - Date.now());
}
if (numeric > 1_000_000_000) {
// Epoch seconds (past timestamps clamp to 0).
return Math.max(0, numeric * 1000 - Date.now());
}
return numeric * 1000;
// Seconds remaining β€” cap so an absurd header value can't produce a
// multi-year "retry in" estimate in the error message.
return Math.min(numeric, MAX_RESET_REMAINING_SECONDS) * 1000;
}
const durationMs = parseDurationLike(value);
if (durationMs !== undefined) {
Expand Down
19 changes: 15 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { showModelPickerDiagnostics } from "./commands/diagnostics";
import { showThinkingEffortPicker } from "./commands/thinkingPicker";
import { configureUtilityModels, toggleProviderEnabled } from "./commands/providers";
import {
ACTIVE_PROFILE_EXPLICIT_KEY,
CONFIG_SECTION,
DEFAULT_USAGE_CHART_DAYS,
GO_EVER_TRACKED_KEY,
GO_SERVER_USAGE_KEY,
SETTING_AGENTS_WINDOW,
SETTING_AUTO_ENABLE_AGENTS_WINDOW,
SETTING_SHOW_PROVIDER_PREFIX,
Expand Down Expand Up @@ -316,6 +319,10 @@ export function activate(context: vscode.ExtensionContext) {
ctx.globalState.update(`opencodego.usageLog.v1.${fp}`, []);
ctx.globalState.update(`opencodego.usageBaseline.v1.${fp}`, {});
ctx.globalState.update(`opencodego.sessionCosts.v1.${fp}`, []);
// Also clear the per-profile server snapshot + ever-tracked flags so a
// re-added profile (same key) doesn't resurrect stale meters/state.
ctx.globalState.update(`${GO_SERVER_USAGE_KEY}.${fp}`, undefined);
ctx.globalState.update(`${GO_EVER_TRACKED_KEY}.${fp}`, undefined);

const remaining = readProfiles(ctx).filter((p) => p.fingerprint !== fp);
await writeProfiles(ctx, remaining);
Expand All @@ -324,6 +331,9 @@ export function activate(context: vscode.ExtensionContext) {
if (activeProfileFingerprint === fp) {
setActiveProfileFingerprint(LEGACY_FINGERPRINT);
await writeActiveProfile(ctx, LEGACY_FINGERPRINT);
// The user's explicit choice was deleted β€” clear the flag so auto-
// selection resumes for the remaining profiles.
await ctx.globalState.update(ACTIVE_PROFILE_EXPLICIT_KEY, undefined);
}

refreshGoUsageStatusBar();
Expand Down Expand Up @@ -376,10 +386,11 @@ export function activate(context: vscode.ExtensionContext) {
const autoEnabled = vscode.workspace.getConfiguration(CONFIG_SECTION).get<boolean>(SETTING_AUTO_ENABLE_AGENTS_WINDOW, true);
if (agentsWindowEnabled && autoEnabled) {
void ensureAgentsWindowSupport(context);
} else if (!agentsWindowEnabled) {
// We may have enabled core settings for the Agents window; revert
// them when the user turns the feature off so the user's global
// configuration is restored.
} else {
// Revert the core settings we auto-enabled when either the feature
// is turned off OR auto-configuration is disabled β€” otherwise a
// user who only disables `autoEnableAgentsWindow` is left with the
// extension's settings permanently flipped in their global config.
void revertAgentsWindowSupport(context);
}
}
Expand Down
1 change: 0 additions & 1 deletion src/models/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ const MODEL_LIMITS_BY_PROVIDER: Record<ProviderVendor, Record<string, BaseModelL
"qwen3.5-plus": { contextWindow: 262144, maxOutputTokens: 65536 },
"gpt-5.6-luna": { contextWindow: 1050000, maxOutputTokens: 128000 },
"hy3-preview": { contextWindow: 256000, maxOutputTokens: 64000 },
"ring-2.6-1t": { contextWindow: 262000, maxOutputTokens: 66000 },
},
[ZEN_VENDOR]: {
"claude-opus-4-7": { contextWindow: 1000000, maxOutputTokens: 128000 },
Expand Down
7 changes: 6 additions & 1 deletion src/provider/OpenCodeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type ResolvedModelMetadata,
} from "../models/metadata";
import { resolveModelRouting } from "../core/routing";
import { lookupModelRegistryEntry } from "../core/registry";
import { extractThinkingOverride, resolveThinkingConfig, thinkingFamily, thinkingProviderFor } from "../thinking";
import { buildOpenCodeGatewayAuthHeaders } from "../openCodeAuth";
import { streamAnthropicMessages as runStreamAnthropicMessages } from "../transports/anthropic";
Expand Down Expand Up @@ -621,7 +622,10 @@ export class OpenCodeProvider implements vscode.LanguageModelChatProvider<OpenCo
const sharedFields: Omit<OpenCodeModel, "id" | "targetChatSessionType"> = {
rawModelId: modelId,
name: providerModelDisplayName(this.definition.modelNamePrefix, modelId, showProviderPrefix),
family: `${this.definition.isAgentVariant && this.definition.baseVendor ? this.definition.baseVendor : this.definition.vendor}-${modelId}-${MODEL_METADATA_REVISION}`,
// A stable real family name (e.g. "deepseek", "gpt") so VS Code's
// family-based model selection/grouping works β€” a per-model unique
// string previously broke `modelFamily` routing and sticky grouping.
family: lookupModelRegistryEntry(modelId).family,
// Include effective limits in version so VS Code invalidates stale
// picker metadata after limit changes (eg. 2M -> 262K corrections).
version: `1.2.0-${MODEL_METADATA_REVISION}-${String(limits.contextWindow)}-${String(limits.maxOutputTokens)}`,
Expand Down Expand Up @@ -971,6 +975,7 @@ export class OpenCodeProvider implements vscode.LanguageModelChatProvider<OpenCo
this.storeReasoningContent(toolCallIds, reasoningContent);
},
});
this.log(`Request completed: model=${model.id}`);
return;
}

Expand Down
1 change: 1 addition & 0 deletions src/provider/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export const PROVIDERS: Record<ProviderDefinition["vendor"], ProviderDefinition>
"mimo-v2-pro",
"mimo-v2.5",
"mimo-v2.5-pro",
"minimax-m3",
"minimax-m2.7",
"minimax-m2.5",
"qwen3.7-max",
Expand Down
30 changes: 19 additions & 11 deletions src/provider/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,19 @@ export async function convertMessage(
continue;
}

if (part instanceof vscode.LanguageModelDataPart && isReasoningMarkerPart(part)) {
// Thinking-off responses carry their reasoning in a marker data part
// (see streaming.ts / gateway bug #37635); echo it as reasoning_content
// on the next turn or DeepSeek's validator 400s. Must be checked BEFORE
// isInternalDataPart (which now includes the marker MIME) so the marker
// is processed rather than skipped as an internal usage part.
const reasoning = readReasoningMarker(part);
if (reasoning) {
thinkingTextParts.push(reasoning);
}
continue;
}

if (part instanceof vscode.LanguageModelDataPart && isInternalDataPart(part)) {
continue;
}
Expand All @@ -169,17 +182,6 @@ export async function convertMessage(
continue;
}

if (part instanceof vscode.LanguageModelDataPart && isReasoningMarkerPart(part)) {
// Thinking-off responses carry their reasoning in a marker data part
// (see streaming.ts / gateway bug #37635); echo it as reasoning_content
// on the next turn or DeepSeek's validator 400s.
const reasoning = readReasoningMarker(part);
if (reasoning) {
thinkingTextParts.push(reasoning);
}
continue;
}

const text = partToText(part);
if (text) {
textParts.push(text);
Expand Down Expand Up @@ -307,6 +309,12 @@ export function normalizeMessages(messages: ApiMessage[]): ApiMessage[] {
!prevHasToolCalls &&
!msgHasToolCalls
) {
// Merging two assistant messages must not drop the second one's
// reasoning_content β€” DeepSeek-style models require it echoed back on
// the next turn. Concatenate both into the merged message.
if (message.reasoning_content) {
previous.reasoning_content = [previous.reasoning_content, message.reasoning_content].filter(Boolean).join("\n");
}
previous.content = `${prevContent}\n\n${msgContent}`.trim();
} else {
normalized.push({ ...message });
Expand Down
Loading
Loading