@4yi/cli 0.1.9 → 0.1.11
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/connect.mjs +44 -3
- package/src/opencode.mjs +14 -4
package/package.json
CHANGED
package/src/connect.mjs
CHANGED
|
@@ -311,6 +311,16 @@ function buildCodexCatalog(models, template) {
|
|
|
311
311
|
description: `${planModel.display_name || planModel.id} via 4YI Gateway with OpenAI Responses tool support.`,
|
|
312
312
|
supported_in_api: true,
|
|
313
313
|
use_responses_lite: false,
|
|
314
|
+
// The template is whatever `codex debug models --bundled` returns on this
|
|
315
|
+
// machine, and that is not a stable schema: 0.147 carries this field,
|
|
316
|
+
// 0.150 omits it, and connect installs the latest codex -- so it upgrades
|
|
317
|
+
// its own template source into the version that drops it. Codex.app
|
|
318
|
+
// requires it and will not start without one, so it is set here rather
|
|
319
|
+
// than inherited. The endpoint is authoritative when it answers; `true`
|
|
320
|
+
// is the fallback because every model this catalog can describe is served
|
|
321
|
+
// through the Responses ingress, which requires `parallel_tool_calls` on
|
|
322
|
+
// each request.
|
|
323
|
+
supports_parallel_tool_calls: planModel.supports_parallel_tool_calls ?? true,
|
|
314
324
|
tool_mode: undefined,
|
|
315
325
|
})).map((model) => {
|
|
316
326
|
delete model.tool_mode;
|
|
@@ -320,9 +330,16 @@ function buildCodexCatalog(models, template) {
|
|
|
320
330
|
}
|
|
321
331
|
|
|
322
332
|
async function checkCodex(session, codexBaseUrl, model) {
|
|
323
|
-
await
|
|
333
|
+
const response = await fetch(`${codexBaseUrl.replace(/\/+$/, "")}/responses`, {
|
|
324
334
|
method: "POST",
|
|
325
|
-
|
|
335
|
+
headers: {
|
|
336
|
+
"content-type": "application/json",
|
|
337
|
+
authorization: `Bearer ${session.token}`,
|
|
338
|
+
// The Responses ingress only accepts the native Codex request shape.
|
|
339
|
+
// Keep this synthetic compatibility check aligned with the real client
|
|
340
|
+
// so it exercises the same authorization and routing path.
|
|
341
|
+
originator: "codex_cli_rs",
|
|
342
|
+
},
|
|
326
343
|
body: JSON.stringify({
|
|
327
344
|
model,
|
|
328
345
|
instructions: "Reply with pong.",
|
|
@@ -331,16 +348,39 @@ async function checkCodex(session, codexBaseUrl, model) {
|
|
|
331
348
|
tool_choice: "auto",
|
|
332
349
|
parallel_tool_calls: true,
|
|
333
350
|
store: false,
|
|
334
|
-
|
|
351
|
+
// Safe Responses ingress deliberately requires streaming requests. A
|
|
352
|
+
// JSON/non-streaming probe is rejected before routing, even though the
|
|
353
|
+
// same credential and model work in Codex itself.
|
|
354
|
+
stream: true,
|
|
335
355
|
include: [],
|
|
356
|
+
client_metadata: {
|
|
357
|
+
session_id: "4yi-cli-preflight",
|
|
358
|
+
thread_id: "4yi-cli-preflight",
|
|
359
|
+
},
|
|
336
360
|
}),
|
|
337
361
|
});
|
|
362
|
+
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
const text = await response.text();
|
|
365
|
+
let body;
|
|
366
|
+
try { body = text ? JSON.parse(text) : null; } catch { body = null; }
|
|
367
|
+
const message = body?.error?.message || body?.error || text || `HTTP ${response.status}`;
|
|
368
|
+
const error = new Error(typeof message === "string" ? message : JSON.stringify(message));
|
|
369
|
+
error.status = response.status;
|
|
370
|
+
error.body = body;
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// A successful native probe is an SSE stream. Compatibility is established
|
|
375
|
+
// once the response starts; do not consume a model response just for setup.
|
|
376
|
+
if (response.body) await response.body.cancel();
|
|
338
377
|
}
|
|
339
378
|
|
|
340
379
|
async function connectCodex({ session, home, codexHome, codexBaseUrl, stdout, skipCheck = false, tooling = {} }) {
|
|
341
380
|
const paths = connectionPaths({ home, codexHome });
|
|
342
381
|
const { models, defaultModel } = await loadCodexModels(session);
|
|
343
382
|
if (!skipCheck) await checkCodex(session, codexBaseUrl, defaultModel);
|
|
383
|
+
ensureDir(path.dirname(paths.codexConfig));
|
|
344
384
|
const template = parseBundledCatalog(tooling);
|
|
345
385
|
const catalog = buildCodexCatalog(models, template);
|
|
346
386
|
atomicWrite(paths.codexCatalog, `${JSON.stringify(catalog, null, 2)}\n`);
|
|
@@ -437,6 +477,7 @@ export function restoreConnection({ target = "all", home = os.homedir(), stdout
|
|
|
437
477
|
|
|
438
478
|
export const __testing = {
|
|
439
479
|
buildCodexCatalog,
|
|
480
|
+
checkCodex,
|
|
440
481
|
commandAvailable,
|
|
441
482
|
desktopAppCandidates,
|
|
442
483
|
ensureToolCli,
|
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,14 +24,23 @@ 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
42
|
const entry = {
|
|
33
|
-
name: model
|
|
43
|
+
name: modelPickerName(model),
|
|
34
44
|
limit: {
|
|
35
45
|
context: modelContextLimit(model),
|
|
36
46
|
output: modelOutputLimit(model),
|
|
@@ -111,7 +121,7 @@ export function ensureOpenCodeRuntime({ home = os.homedir(), stdout = console.lo
|
|
|
111
121
|
|
|
112
122
|
export async function runCode({ session, home = os.homedir(), argv = [], stdout = console.log, preferredModel = null } = {}) {
|
|
113
123
|
if (!session?.token) throw new Error("Not signed in. Run `4yi login`.");
|
|
114
|
-
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 });
|
|
115
125
|
const list = modelConfig.models || [];
|
|
116
126
|
if (list.length === 0) throw new Error("No chat models available for this organization.");
|
|
117
127
|
|