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
16 changes: 15 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,35 @@ const MODEL_TTL_MS = 6 * 60 * 60 * 1000; // 6h

// Static baseline (context windows from the CLI). Seeds the provider on first
// run / when logged out; live discovery replaces it once a request has run.
// Keep the capability flags in sync with what /models reports — a baseline that
// understates them registers the model as text-only until the cache warms.
const STATIC_MODELS: DiscoveredModel[] = [
{ id: "k3", display_name: "Kimi K3", context_length: 1_048_576, supports_reasoning: true, supports_tool_use: true },
{
id: "k3",
display_name: "Kimi K3",
context_length: 1_048_576,
supports_reasoning: true,
supports_tool_use: true,
supports_image_in: true,
supports_video_in: true,
},
{
id: "kimi-for-coding",
display_name: "Kimi K2.7 Coding",
context_length: 262_144,
supports_reasoning: true,
supports_tool_use: true,
supports_image_in: true,
supports_video_in: true,
},
{
id: "kimi-for-coding-highspeed",
display_name: "Kimi K2.7 Coding Highspeed",
context_length: 262_144,
supports_reasoning: true,
supports_tool_use: true,
supports_image_in: true,
supports_video_in: true,
},
];

Expand Down
12 changes: 12 additions & 0 deletions src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export type UpstreamModel = {
// /models fetch filter).
export type DiscoveredModel = UpstreamModel & { id: string };

// OpenCode's model-config modality literals.
export type Modality = "text" | "audio" | "image" | "video" | "pdf";

export type OAuthRecord = { type: "oauth"; access: string; refresh: string; expires: number };

export type ModelCache = { ts: number; models: DiscoveredModel[] };
Expand Down Expand Up @@ -59,12 +62,21 @@ export function toAuth(data: Record<string, any>, now = Date.now()): OAuthRecord

// Map an upstream model to OpenCode's provider-config model shape.
export function toConfigModel(m: DiscoveredModel) {
const input: Modality[] = ["text"];
if (m.supports_image_in) input.push("image");
if (m.supports_video_in) input.push("video");

return {
name: m.display_name || m.id,
limit: { context: Number(m.context_length) || 262_144, output: DEFAULT_OUTPUT_LIMIT },
reasoning: !!m.supports_reasoning || m.supports_thinking_type === "only" || m.supports_thinking_type === "both",
tool_call: m.supports_tool_use !== false,
attachment: !!(m.supports_image_in || m.supports_video_in),
// `attachment` alone is not enough: OpenCode gates what it will actually
// send on `modalities.input`, which defaults to text-only. Without this an
// image-capable model still has its attachments dropped before the request
// is built, and it replies that it cannot read images.
modalities: { input, output: ["text"] as Modality[] },
// kimi coding models only accept temperature === 1; hide the knob.
temperature: false,
};
Expand Down
15 changes: 15 additions & 0 deletions test/unit.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ describe("toConfigModel", () => {
assert.equal(toConfigModel({ id: "m", supports_video_in: true }).attachment, true);
assert.equal(toConfigModel({ id: "m" }).attachment, false);
});
it("mirrors input support into modalities so OpenCode actually sends them", () => {
assert.deepEqual(toConfigModel({ id: "m", supports_image_in: true }).modalities.input, ["text", "image"]);
assert.deepEqual(toConfigModel({ id: "m", supports_video_in: true }).modalities.input, ["text", "video"]);
assert.deepEqual(toConfigModel({ id: "m", supports_image_in: true, supports_video_in: true }).modalities.input, [
"text",
"image",
"video",
]);
});
it("keeps modalities text-only when the model has no image/video input", () => {
assert.deepEqual(toConfigModel({ id: "m" }).modalities.input, ["text"]);
});
it("always reports text output", () => {
assert.deepEqual(toConfigModel({ id: "m", supports_image_in: true }).modalities.output, ["text"]);
});
it("always hides the temperature knob", () => {
assert.equal(toConfigModel({ id: "m" }).temperature, false);
});
Expand Down