diff --git a/package.json b/package.json index ed36bef..aa84db5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/server/index.ts b/src/server/index.ts index 1f6aa1d..626969f 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -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, diff --git a/src/server/models.test.ts b/src/server/models.test.ts index 418401f..dcbd44e 100644 --- a/src/server/models.test.ts +++ b/src/server/models.test.ts @@ -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); }); }); diff --git a/src/server/models.ts b/src/server/models.ts index e0f8702..feb7fc1 100644 --- a/src/server/models.ts +++ b/src/server/models.ts @@ -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 { 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; } -} \ No newline at end of file +} diff --git a/src/server/server-adapter.test.ts b/src/server/server-adapter.test.ts new file mode 100644 index 0000000..40e5744 --- /dev/null +++ b/src/server/server-adapter.test.ts @@ -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"); + }); +});