fix(models): expose static models list so UI renders entries before listModels resolves
The UI was showing 0 models because the adapter only set listModels (async) without a static models array. The static array is what the UI reads synchronously to populate the selector — listModels is for refresh. - Move the fallback list out as STATIC_MODELS (expanded to cover Opus/Sonnet/Haiku 4.x, GPT-4o/5, Gemini 2.5, Grok 4, DeepSeek) - Set models: STATIC_MODELS on the adapter module - Keep listK8sModels for runtime refresh from `opencode models` (with STATIC_MODELS fallback on error or empty stdout) - Add server-adapter.test.ts asserting models is non-empty - Add models.test.ts coverage for the empty-stdout fallback path chore: bump version to 0.1.33 Fixes FAR-94. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paperclip-adapter-opencode-k8s",
|
||||
"version": "0.1.32",
|
||||
"version": "0.1.33",
|
||||
"description": "Paperclip adapter plugin that runs OpenCode agents as Kubernetes Jobs",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
import type { ServerAdapterModule } from "@paperclipai/adapter-utils";
|
||||
import { getAdapterSessionManagement } from "@paperclipai/adapter-utils";
|
||||
import { type, agentConfigurationDoc } from "../index.js";
|
||||
import { listK8sModels } from "./models.js";
|
||||
import { listK8sModels, STATIC_MODELS } from "./models.js";
|
||||
import { execute } from "./execute.js";
|
||||
import { testEnvironment } from "./test.js";
|
||||
import { sessionCodec } from "./session.js";
|
||||
@@ -14,6 +14,7 @@ export function createServerAdapter(): ServerAdapterModule {
|
||||
execute,
|
||||
testEnvironment,
|
||||
sessionCodec,
|
||||
models: STATIC_MODELS,
|
||||
listModels: listK8sModels,
|
||||
listSkills: listOpenCodeSkills,
|
||||
syncSkills: syncOpenCodeSkills,
|
||||
|
||||
@@ -8,7 +8,7 @@ vi.mock("child_process", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const { listK8sModels } = await import("./models.js");
|
||||
const { listK8sModels, STATIC_MODELS } = await import("./models.js");
|
||||
|
||||
function mockExecResult(stdout: string) {
|
||||
execMock.mockImplementation((_cmd, _opts, cb) => {
|
||||
@@ -71,8 +71,15 @@ describe("listK8sModels", () => {
|
||||
|
||||
const models = await listK8sModels();
|
||||
|
||||
expect(models).toBe(STATIC_MODELS);
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
expect(models.map((m) => m.id)).toContain("anthropic/claude-opus-4-7");
|
||||
expect(models.map((m) => m.id)).toContain("openai/gpt-4o");
|
||||
});
|
||||
|
||||
it("falls back to the static list when the CLI returns empty stdout", async () => {
|
||||
mockExecResult("");
|
||||
|
||||
const models = await listK8sModels();
|
||||
|
||||
expect(models).toBe(STATIC_MODELS);
|
||||
});
|
||||
});
|
||||
|
||||
+21
-16
@@ -4,30 +4,35 @@ import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const STATIC_MODELS: AdapterModel[] = [
|
||||
{ id: "anthropic/claude-opus-4-7", label: "Claude Opus 4.7" },
|
||||
{ id: "anthropic/claude-opus-4-6", label: "Claude Opus 4.6" },
|
||||
{ id: "anthropic/claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
|
||||
{ id: "anthropic/claude-sonnet-4-5", label: "Claude Sonnet 4.5" },
|
||||
{ id: "anthropic/claude-haiku-4-5", label: "Claude Haiku 4.5" },
|
||||
{ id: "openai/gpt-4o", label: "GPT-4o" },
|
||||
{ id: "openai/gpt-4o-mini", label: "GPT-4o mini" },
|
||||
{ id: "openai/gpt-5", label: "GPT-5" },
|
||||
{ id: "openai/gpt-5-mini", label: "GPT-5 mini" },
|
||||
{ id: "google/gemini-2.5-pro", label: "Gemini 2.5 Pro" },
|
||||
{ id: "google/gemini-2.5-flash", label: "Gemini 2.5 Flash" },
|
||||
{ id: "xai/grok-4", label: "Grok 4" },
|
||||
{ id: "deepseek/deepseek-chat", label: "DeepSeek Chat" },
|
||||
];
|
||||
|
||||
export async function listK8sModels(): Promise<AdapterModel[]> {
|
||||
try {
|
||||
const result = await execAsync("opencode models", { timeout: 30_000 });
|
||||
const output = result.stdout;
|
||||
const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
|
||||
const lines = result.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
||||
if (lines.length === 0) return STATIC_MODELS;
|
||||
const models: AdapterModel[] = [];
|
||||
for (const line of lines) {
|
||||
if (!line) continue;
|
||||
const parts = line.split("/");
|
||||
const id = line;
|
||||
const label = parts[parts.length - 1].replace(/-/g, " ").replace(/_/g, " ");
|
||||
models.push({ id, label });
|
||||
models.push({ id: line, label });
|
||||
}
|
||||
return models;
|
||||
} catch {
|
||||
const fallback: AdapterModel[] = [
|
||||
{ id: "anthropic/claude-opus-4-7", label: "Claude Opus 4.7" },
|
||||
{ id: "anthropic/claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
|
||||
{ id: "anthropic/claude-haiku-4-5", label: "Claude Haiku 4.5" },
|
||||
{ id: "openai/gpt-4o", label: "GPT-4o" },
|
||||
{ id: "openai/gpt-4o-mini", label: "GPT-4o mini" },
|
||||
{ id: "google/gemini-2.5-pro", label: "Gemini 2.5 Pro" },
|
||||
{ id: "google/gemini-2.5-flash", label: "Gemini 2.5 Flash" },
|
||||
];
|
||||
return fallback;
|
||||
return STATIC_MODELS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { createServerAdapter } from "./index.js";
|
||||
import { STATIC_MODELS } from "./models.js";
|
||||
|
||||
describe("createServerAdapter", () => {
|
||||
it("declares the opencode_k8s type", () => {
|
||||
const adapter = createServerAdapter();
|
||||
expect(adapter.type).toBe("opencode_k8s");
|
||||
});
|
||||
|
||||
it("exposes a non-empty static models list so the UI never renders zero models", () => {
|
||||
const adapter = createServerAdapter();
|
||||
expect(Array.isArray(adapter.models)).toBe(true);
|
||||
expect(adapter.models!.length).toBeGreaterThan(0);
|
||||
expect(adapter.models).toBe(STATIC_MODELS);
|
||||
for (const m of adapter.models!) {
|
||||
expect(typeof m.id).toBe("string");
|
||||
expect(m.id.length).toBeGreaterThan(0);
|
||||
expect(typeof m.label).toBe("string");
|
||||
expect(m.label.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("also exposes listModels for dynamic refresh", () => {
|
||||
const adapter = createServerAdapter();
|
||||
expect(typeof adapter.listModels).toBe("function");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user