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
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to the **OpenCode Go BYOK Provider** extension are documented here.

## [Unreleased]

### Added

- **`[Vision]` Transparent vision proxy for text-only models (#74).** Run **OpenCode Go: Configure Vision Proxy** from the Command Palette to pick a vision-capable model. When a non-vision OpenCode model receives an image, the extension forwards it to the configured model, receives a text description, and feeds that to the original model β€” so text-only models "see" images with zero extra steps. The picker shows only vision-capable models (filtered by `models.dev` metadata), with a **None** option to disable and a **Customize prompt** entry to edit the description instruction. No settings to toggle β€” if a model is configured, the proxy is on. Implemented with `vscode.lm.selectChatModels` + `sendRequest`.

### Fixed

- **`[Streaming]` Context overflow 400 when prompt + output approached the model's limit.** `estimateTokenCount` can underestimate by 0–2%, pushing payloads past the context window on large prompts. Added a 64-token safety margin to `promptReserve` in `modelLimits()`. Affects all models (reported with GLM-5.2).
- **`[Streaming]` Empty response warning no longer steals focus to the Output pane (#67).** Removed the stray `.show(true)` call on the empty-response diagnostic log.

## [0.4.0] β€” 2026-07-13

### Added
Expand All @@ -15,7 +26,6 @@ All notable changes to the **OpenCode Go BYOK Provider** extension are documente

- **`[Usage]` 5-hour rolling usage now isolates per profile in multi-profile mode.** The shared `opencode.db` SQLite database has no API key column, so reading it in multi-account mode mixed quota from all accounts. The tracker now skips SQLite entirely when multiple profiles exist and falls back to extension-tracked entries (which are namespaced per profile). This trades billed-accuracy for correct per-profile isolation.


## [0.3.7] β€” 2026-07-09

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
| 🌍 **30+ frontier models** | DeepSeek V4, Kimi K2.6, GLM-5.1, Qwen3.7 Max, MiMo V2.5, MiniMax M2.7, Big Pickle, Nemotron β€” **all in one picker** |
| πŸ€– **Full Agent Mode** | Tool-calling (read files, edit, run terminal) works natively β€” not just chat. Models also appear in the **Agents window** (Copilot CLI session) |
| 🧠 **Thinking controls** | Per-model reasoning effort (DeepSeek `max`, Qwen `thinking_budget`, MiniMax `on/off`, Mimo `low/med/high`) |
| πŸ–ΌοΈ **Vision proxy** | Text-only models can "see" images via a configured vision model. Run **OpenCode Go: Configure Vision Proxy** from the Command Palette to set it up. |
| πŸ“Š **Live usage tracking** | Status bar shows Go subscription burn-rate across 5h / weekly / monthly tiers |
| πŸ”Œ **Dual providers** | OpenCode **Go** ($10/mo subscription) + OpenCode **Zen** (free + paid models) β€” run both at once, switch instantly |
| 🎯 **Smart routing** | Each model family auto-routes to its native transport (`/responses`, `/messages`, `streamGenerateContent`, `/chat/completions`) |
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@
{
"command": "opencodego.deleteProfile",
"title": "OpenCode Go: Delete Profile"
},
{
"command": "opencodego.configureVisionProxy",
"title": "OpenCode Go: Configure Vision Proxy"
}
],
"configuration": {
Expand Down
320 changes: 312 additions & 8 deletions src/extension.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ const MODELS_WITHOUT_TEMPERATURE = new Set([
"kimi-k2.7-code",
]);

const VISION_CAPABLE_MODELS = new Set([
export const VISION_CAPABLE_MODELS = new Set([
"minimax-m2.7",
"minimax-m2.5",
"minimax-m2.5-free",
Expand Down
3 changes: 2 additions & 1 deletion src/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ export async function streamChatCompletions(
options.output?.appendLine(
`[warn] empty response from model=${options.modelId} (no text, no tool calls, no reasoning). Try a different free model or enable opencodego.debugReasoning to inspect raw SSE.`,
);
options.output?.show(true);
// Intentionally not calling .show(true) β€” the diagnostic log is
// available in the Output pane when the user opens it manually.
}
}

Expand Down
24 changes: 23 additions & 1 deletion src/test/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { fallbackModelMetadata } from "../metadata.js";
import { fallbackModelMetadata, VISION_CAPABLE_MODELS } from "../metadata.js";
import { GO_VENDOR, ZEN_VENDOR } from "../providerTypes.js";

/**
Expand Down Expand Up @@ -72,3 +72,25 @@ describe("fallbackModelMetadata β€” non-kimi models unaffected", () => {
assert.notEqual(meta?.temperature, false);
});
});

describe("VISION_CAPABLE_MODELS", () => {
it("includes known vision models (minimax-m2.7, kimi-k2.6, mimo-v2.5)", () => {
assert.ok(VISION_CAPABLE_MODELS.has("minimax-m2.7"));
assert.ok(VISION_CAPABLE_MODELS.has("kimi-k2.6"));
assert.ok(VISION_CAPABLE_MODELS.has("mimo-v2.5"));
assert.ok(VISION_CAPABLE_MODELS.has("glm-5.1"));
assert.ok(VISION_CAPABLE_MODELS.has("mimo-v2.5-pro"));
});

it("does NOT include text-only models (deepseek-v4-flash, hy3-preview, big-pickle)", () => {
assert.ok(!VISION_CAPABLE_MODELS.has("deepseek-v4-flash"));
assert.ok(!VISION_CAPABLE_MODELS.has("deepseek-v4-pro"));
assert.ok(!VISION_CAPABLE_MODELS.has("hy3-preview"));
assert.ok(!VISION_CAPABLE_MODELS.has("big-pickle"));
});

it("is an exported Set", () => {
assert.ok(VISION_CAPABLE_MODELS instanceof Set);
assert.ok(VISION_CAPABLE_MODELS.size > 10);
});
});
90 changes: 90 additions & 0 deletions src/test/visionProxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, it, mock } from "node:test";
import assert from "node:assert/strict";

/**
* Vision proxy condition tests.
*
* The core fix for #74 is caching `metadata.supportsVision` in
* `actuallySupportsVision` BEFORE `modelCapabilities` overrides it.
* The proxy condition is:
*
* hasImageInput && !actuallySupportsVision && visionProxyModelId
*
* where `actuallySupportsVision` is the RAW model metadata (true =
* model natively supports images), NOT the enhanced capabilities.
*/

type ProxyConditionInput = {
hasImageInput: boolean;
actuallySupportsVision: boolean;
visionProxyModelId: string;
};

function shouldProxy({ hasImageInput, actuallySupportsVision, visionProxyModelId }: ProxyConditionInput): boolean {
return Boolean(hasImageInput && !actuallySupportsVision && visionProxyModelId);
}

describe("vision proxy condition (shouldProxy)", () => {
it("enters proxy when text-only model receives images with proxy configured", () => {
assert.ok(shouldProxy({ hasImageInput: true, actuallySupportsVision: false, visionProxyModelId: "gpt-5.5" }));
});

it("skips proxy when no images present", () => {
assert.ok(!shouldProxy({ hasImageInput: false, actuallySupportsVision: false, visionProxyModelId: "gpt-5.5" }));
});

it("skips proxy when model natively supports vision", () => {
assert.ok(!shouldProxy({ hasImageInput: true, actuallySupportsVision: true, visionProxyModelId: "gpt-5.5" }));
});

it("skips proxy when no vision model is configured (empty string)", () => {
assert.ok(!shouldProxy({ hasImageInput: true, actuallySupportsVision: false, visionProxyModelId: "" }));
});

it("skips proxy when all conditions are false", () => {
assert.ok(!shouldProxy({ hasImageInput: false, actuallySupportsVision: true, visionProxyModelId: "" }));
});

it("cached supportsVision (actuallySupportsVision) prevents circular regression", () => {
// This is the fix for #74: even if modelCapabilities overrides
// metadata.supportsVision to true (because proxy is enabled),
// the CACHED value (actuallySupportsVision) stays false for
// text-only models β€” so the proxy fires correctly.
const textOnlyModel = { hasImageInput: true, actuallySupportsVision: false, visionProxyModelId: "gpt-5.5" };
const visionModel = { hasImageInput: true, actuallySupportsVision: true, visionProxyModelId: "gpt-5.5" };

// Before fix: visionModel.actuallySupportsVision was false β†’ proxy fired
// After fix: both behave correctly
assert.ok(shouldProxy(textOnlyModel), "text-only model: proxy fires");
assert.ok(!shouldProxy(visionModel), "vision model: proxy does NOT fire");
});
});

describe("modelCapabilities vision proxy flag", () => {
// modelCapabilities() returns imageInput: true when:
// metadata.supportsVision (native) OR isVisionProxyEnabled()
// This tells VS Code NOT to strip images from requests.

it("returns imageInput: true when proxy is enabled on text-only models", () => {
const capabilities = simulateModelCapabilities(false, true);
assert.equal(capabilities.imageInput, true);
});

it("returns imageInput: true when model natively supports vision", () => {
const capabilities = simulateModelCapabilities(true, false);
assert.equal(capabilities.imageInput, true);
});

it("returns imageInput: false only when no vision support and no proxy", () => {
const capabilities = simulateModelCapabilities(false, false);
assert.equal(capabilities.imageInput, false);
});
});

function simulateModelCapabilities(
metadataSupportsVision: boolean,
visionProxyEnabled: boolean,
): { imageInput: boolean } {
const supportsVision = metadataSupportsVision || visionProxyEnabled;
return { imageInput: supportsVision };
}
Loading