@gonrocca/zero-pi 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/opencode-models.ts +111 -0
- package/extensions/zero-models.ts +35 -12
- package/package.json +2 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// zero-pi — OpenCode model-catalog discovery.
|
|
2
|
+
//
|
|
3
|
+
// Surfaces the model providers a user can run by cross-referencing OpenCode's
|
|
4
|
+
// model catalog cache with the user's OpenCode auth file. This is the same
|
|
5
|
+
// approach the `zero` installer's model picker uses, so `/zero-models` offers
|
|
6
|
+
// the same providers — opencode-go, openai (codex/gpt), anthropic, … — as the
|
|
7
|
+
// installer, instead of a hardcoded Claude list.
|
|
8
|
+
//
|
|
9
|
+
// Sources, under the user's home (ZERO_HOME-overridable for tests):
|
|
10
|
+
// ~/.cache/opencode/models.json — the full provider/model catalog
|
|
11
|
+
// ~/.local/share/opencode/auth.json — the providers the user authenticated
|
|
12
|
+
//
|
|
13
|
+
// Discovery is total: a missing or corrupt file yields an empty list, so a
|
|
14
|
+
// user without OpenCode simply falls back to pi's own model registry.
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
/** A model discovered from OpenCode's catalog. */
|
|
21
|
+
export interface DiscoveredModel {
|
|
22
|
+
/** The provider id, e.g. `opencode-go`, `openai`, `anthropic`. */
|
|
23
|
+
provider: string;
|
|
24
|
+
/** The model id, e.g. `gpt-5-codex`. */
|
|
25
|
+
id: string;
|
|
26
|
+
/** The human-readable model name. */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Whether the model supports tool calls — required for the SDD phases. */
|
|
29
|
+
toolCall: boolean;
|
|
30
|
+
/** Whether the model exposes a reasoning-effort setting. */
|
|
31
|
+
reasoning: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The home directory discovery resolves OpenCode paths against. */
|
|
35
|
+
function discoveryHome(): string {
|
|
36
|
+
const override = process.env.ZERO_HOME;
|
|
37
|
+
return typeof override === "string" && override.trim() !== "" ? override : homedir();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True for a plain (non-array) object. */
|
|
41
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
42
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read and parse a JSON file; returns null on any failure. */
|
|
46
|
+
function readJson(path: string): unknown {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extract a provider's models from its OpenCode catalog entry.
|
|
56
|
+
*
|
|
57
|
+
* Exported for tests.
|
|
58
|
+
*/
|
|
59
|
+
export function extractModels(
|
|
60
|
+
providerId: string,
|
|
61
|
+
entry: Record<string, unknown>,
|
|
62
|
+
): DiscoveredModel[] {
|
|
63
|
+
const raw = entry.models;
|
|
64
|
+
if (!isObject(raw)) return [];
|
|
65
|
+
|
|
66
|
+
const models: DiscoveredModel[] = [];
|
|
67
|
+
for (const [id, model] of Object.entries(raw)) {
|
|
68
|
+
if (!isObject(model)) continue;
|
|
69
|
+
models.push({
|
|
70
|
+
provider: providerId,
|
|
71
|
+
id,
|
|
72
|
+
name: typeof model.name === "string" ? model.name : id,
|
|
73
|
+
toolCall: model.tool_call === true,
|
|
74
|
+
reasoning: model.reasoning === true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return models;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Discover every model the user can run, flattened across all OpenCode-
|
|
82
|
+
* authenticated providers.
|
|
83
|
+
*
|
|
84
|
+
* `catalogJson` and `authJson` are injectable for tests; in normal use they
|
|
85
|
+
* are read from OpenCode's cache and auth files on disk. The result is empty
|
|
86
|
+
* when either source is absent or malformed.
|
|
87
|
+
*/
|
|
88
|
+
export function discoverModels(
|
|
89
|
+
catalogJson?: unknown,
|
|
90
|
+
authJson?: unknown,
|
|
91
|
+
): DiscoveredModel[] {
|
|
92
|
+
// Only touch disk when an argument is genuinely absent — an explicit value
|
|
93
|
+
// (including `null`) is used as given, so tests never read real files.
|
|
94
|
+
const auth =
|
|
95
|
+
authJson === undefined
|
|
96
|
+
? readJson(join(discoveryHome(), ".local", "share", "opencode", "auth.json"))
|
|
97
|
+
: authJson;
|
|
98
|
+
const catalog =
|
|
99
|
+
catalogJson === undefined
|
|
100
|
+
? readJson(join(discoveryHome(), ".cache", "opencode", "models.json"))
|
|
101
|
+
: catalogJson;
|
|
102
|
+
if (!isObject(auth) || !isObject(catalog)) return [];
|
|
103
|
+
|
|
104
|
+
const models: DiscoveredModel[] = [];
|
|
105
|
+
for (const providerId of Object.keys(auth)) {
|
|
106
|
+
const entry = catalog[providerId];
|
|
107
|
+
if (!isObject(entry)) continue;
|
|
108
|
+
models.push(...extractModels(providerId, entry));
|
|
109
|
+
}
|
|
110
|
+
return models;
|
|
111
|
+
}
|
|
@@ -21,6 +21,7 @@ import { join } from "node:path";
|
|
|
21
21
|
|
|
22
22
|
import { readAutotuneMode, type AutotuneMode } from "./autotune.ts";
|
|
23
23
|
import type { AutotunePending } from "./autotune-extension.ts";
|
|
24
|
+
import { discoverModels } from "./opencode-models.ts";
|
|
24
25
|
|
|
25
26
|
/** The SDD phases, in pipeline order. */
|
|
26
27
|
export const PHASES = ["explore", "plan", "build", "veredicto"] as const;
|
|
@@ -211,34 +212,56 @@ const CUSTOM_MODEL = "— otro modelo (escribir) —";
|
|
|
211
212
|
const CUSTOM_PROVIDER = "— otro provider (escribir) —";
|
|
212
213
|
|
|
213
214
|
/**
|
|
214
|
-
* Group
|
|
215
|
-
*
|
|
216
|
-
*
|
|
215
|
+
* Group models by provider for the picker.
|
|
216
|
+
*
|
|
217
|
+
* The primary source is OpenCode's model catalog — the same one the `zero`
|
|
218
|
+
* installer's picker uses — so every provider the user authenticated there
|
|
219
|
+
* (opencode-go, openai/codex, anthropic, …) is offered, with only tool-call-
|
|
220
|
+
* capable models (the SDD phases need tool calls). When OpenCode is not
|
|
221
|
+
* present it falls back to pi's own model registry, then to an empty map.
|
|
217
222
|
*/
|
|
218
223
|
function providerGroups(registry: PiModelRegistry | undefined): Map<string, string[]> {
|
|
219
|
-
if (!registry || typeof registry.getAll !== "function") return new Map();
|
|
220
224
|
try {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
return groupByProvider(source ?? []);
|
|
225
|
+
const discovered = discoverModels().filter((m) => m.toolCall);
|
|
226
|
+
if (discovered.length > 0) return groupByProvider(discovered);
|
|
224
227
|
} catch {
|
|
225
|
-
|
|
228
|
+
/* fall through to pi's registry */
|
|
229
|
+
}
|
|
230
|
+
if (registry && typeof registry.getAll === "function") {
|
|
231
|
+
try {
|
|
232
|
+
const all = registry.getAll();
|
|
233
|
+
if (all && all.length > 0) return groupByProvider(all);
|
|
234
|
+
} catch {
|
|
235
|
+
/* fall through */
|
|
236
|
+
}
|
|
226
237
|
}
|
|
238
|
+
return new Map();
|
|
227
239
|
}
|
|
228
240
|
|
|
229
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* Find the provider that owns a model id — checked against OpenCode's catalog
|
|
243
|
+
* first, then pi's own registry.
|
|
244
|
+
*/
|
|
230
245
|
function resolveProvider(
|
|
231
246
|
registry: PiModelRegistry | undefined,
|
|
232
247
|
modelId: string,
|
|
233
248
|
): string | undefined {
|
|
234
|
-
if (!registry || typeof registry.getAll !== "function") return undefined;
|
|
235
249
|
try {
|
|
236
|
-
for (const m of
|
|
237
|
-
if (m
|
|
250
|
+
for (const m of discoverModels()) {
|
|
251
|
+
if (m.id === modelId) return m.provider;
|
|
238
252
|
}
|
|
239
253
|
} catch {
|
|
240
254
|
/* ignore */
|
|
241
255
|
}
|
|
256
|
+
if (registry && typeof registry.getAll === "function") {
|
|
257
|
+
try {
|
|
258
|
+
for (const m of registry.getAll()) {
|
|
259
|
+
if (m && m.id === modelId && typeof m.provider === "string") return m.provider;
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
/* ignore */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
242
265
|
return undefined;
|
|
243
266
|
}
|
|
244
267
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gonrocca/zero-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow, skill auto-learning, and an animated ZERO startup banner. Adds capability to pi without modifying pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"extensions/win-tree-kill.ts",
|
|
43
43
|
"extensions/conversation-resume.ts",
|
|
44
44
|
"extensions/zero-models.ts",
|
|
45
|
+
"extensions/opencode-models.ts",
|
|
45
46
|
"extensions/autotune.ts",
|
|
46
47
|
"extensions/autotune-extension.ts",
|
|
47
48
|
"extensions/spec-merge.ts",
|