@4yi/cli 0.1.6 → 0.1.8
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/package.json +1 -1
- package/src/http.mjs +54 -5
- package/src/opencode.mjs +18 -5
package/package.json
CHANGED
package/src/http.mjs
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
export async function requestJson(baseUrl, path, options = {}) {
|
|
2
2
|
const url = `${baseUrl}${path}`;
|
|
3
|
+
const optionHeaders = normalizeHeaders(options.headers);
|
|
4
|
+
const headers = {
|
|
5
|
+
"content-type": "application/json",
|
|
6
|
+
...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
|
|
7
|
+
...optionHeaders,
|
|
8
|
+
};
|
|
9
|
+
if (shouldGenerateIdempotencyKey(path, options, headers)) {
|
|
10
|
+
headers["Idempotency-Key"] = `cli-${randomId()}`;
|
|
11
|
+
}
|
|
3
12
|
const res = await fetch(url, {
|
|
4
13
|
...options,
|
|
5
|
-
headers
|
|
6
|
-
"content-type": "application/json",
|
|
7
|
-
...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
|
|
8
|
-
...(options.headers || {}),
|
|
9
|
-
},
|
|
14
|
+
headers,
|
|
10
15
|
});
|
|
11
16
|
const text = await res.text();
|
|
12
17
|
let body = null;
|
|
@@ -27,3 +32,47 @@ export async function requestJson(baseUrl, path, options = {}) {
|
|
|
27
32
|
}
|
|
28
33
|
return body;
|
|
29
34
|
}
|
|
35
|
+
|
|
36
|
+
function normalizeHeaders(headers) {
|
|
37
|
+
if (!headers) return {};
|
|
38
|
+
if (headers instanceof Headers) return Object.fromEntries(headers.entries());
|
|
39
|
+
if (Array.isArray(headers)) return Object.fromEntries(headers);
|
|
40
|
+
return { ...headers };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasHeader(headers, name) {
|
|
44
|
+
const expected = name.toLowerCase();
|
|
45
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === expected);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function shouldGenerateIdempotencyKey(path, options, headers) {
|
|
49
|
+
const method = String(options.method || "GET").toUpperCase();
|
|
50
|
+
if (method !== "POST") return false;
|
|
51
|
+
if (!path.endsWith("/chat/completions")) return false;
|
|
52
|
+
if (hasHeader(headers, "Idempotency-Key") || hasHeader(headers, "X-Idempotency-Key")) return false;
|
|
53
|
+
|
|
54
|
+
const body = parseJsonBody(options.body);
|
|
55
|
+
return body && body.stream !== true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseJsonBody(body) {
|
|
59
|
+
if (!body) return null;
|
|
60
|
+
if (typeof body === "string") {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(body);
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (body && typeof body === "object" && !(body instanceof ArrayBuffer)) return body;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function randomId() {
|
|
72
|
+
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
|
|
73
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
|
|
74
|
+
const value = Math.floor(Math.random() * 16);
|
|
75
|
+
const nibble = ch === "x" ? value : (value & 0x3) | 0x8;
|
|
76
|
+
return nibble.toString(16);
|
|
77
|
+
});
|
|
78
|
+
}
|
package/src/opencode.mjs
CHANGED
|
@@ -10,6 +10,7 @@ const DEFAULT_CONTEXT_LIMIT = 200000;
|
|
|
10
10
|
const DEFAULT_OUTPUT_LIMIT = 8192;
|
|
11
11
|
|
|
12
12
|
function isClaudeModel(model) {
|
|
13
|
+
if (model?.family === "claude") return true;
|
|
13
14
|
const id = model?.id || "";
|
|
14
15
|
const name = model?.display_name || "";
|
|
15
16
|
return /claude/i.test(id) || /claude/i.test(name);
|
|
@@ -23,19 +24,31 @@ function modelOutputLimit(model) {
|
|
|
23
24
|
return model.output_limit || model.max_output_tokens || model.max_tokens || DEFAULT_OUTPUT_LIMIT;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
function modelPickerName(model) {
|
|
28
|
+
const displayName = model.display_name || model.id;
|
|
29
|
+
if (model.family === "claude") {
|
|
30
|
+
return `Claude · ${displayName.replace(/^(?:anthropic[ ._-]*)?claude[ ._-]*/i, "")}`;
|
|
31
|
+
}
|
|
32
|
+
if (model.family === "codex") return `Codex · ${displayName}`;
|
|
33
|
+
return displayName;
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
export function buildOpenCodeConfig({ modelConfig, preferredModel = null, tokenEnv = "FOURYI_CLI_TOKEN", orgEnv = "FOURYI_ORG_ID" }) {
|
|
27
|
-
// Register
|
|
28
|
-
// (`Tab` / `/models`) can move between
|
|
37
|
+
// Register every server-approved coding model so OpenCode's native switcher
|
|
38
|
+
// (`Tab` / `/models`) can move between Claude and Codex. Claude stays the default.
|
|
29
39
|
const list = modelConfig.models || [];
|
|
30
40
|
const models = {};
|
|
31
41
|
for (const model of list) {
|
|
32
|
-
|
|
33
|
-
name: model
|
|
42
|
+
const entry = {
|
|
43
|
+
name: modelPickerName(model),
|
|
34
44
|
limit: {
|
|
35
45
|
context: modelContextLimit(model),
|
|
36
46
|
output: modelOutputLimit(model),
|
|
37
47
|
},
|
|
38
48
|
};
|
|
49
|
+
if (typeof model.attachment === "boolean") entry.attachment = model.attachment;
|
|
50
|
+
if (model.modalities && typeof model.modalities === "object") entry.modalities = model.modalities;
|
|
51
|
+
models[model.id] = entry;
|
|
39
52
|
}
|
|
40
53
|
const has = (id) => !!id && list.some((model) => model.id === id);
|
|
41
54
|
const firstClaude = list.find(isClaudeModel)?.id;
|
|
@@ -108,7 +121,7 @@ export function ensureOpenCodeRuntime({ home = os.homedir(), stdout = console.lo
|
|
|
108
121
|
|
|
109
122
|
export async function runCode({ session, home = os.homedir(), argv = [], stdout = console.log, preferredModel = null } = {}) {
|
|
110
123
|
if (!session?.token) throw new Error("Not signed in. Run `4yi login`.");
|
|
111
|
-
const modelConfig = await requestJson(session.baseUrl, "/api/cli/models", { token: session.token });
|
|
124
|
+
const modelConfig = await requestJson(session.baseUrl, "/api/cli/models?runtime=opencode", { token: session.token });
|
|
112
125
|
const list = modelConfig.models || [];
|
|
113
126
|
if (list.length === 0) throw new Error("No chat models available for this organization.");
|
|
114
127
|
|