@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4yi/cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "4YI command-line launcher for OAuth login and OpenCode runtime",
5
5
  "type": "module",
6
6
  "bin": {
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 requestJson(codexBaseUrl.replace(/\/+$/, ""), "/responses", {
333
+ const response = await fetch(`${codexBaseUrl.replace(/\/+$/, "")}/responses`, {
324
334
  method: "POST",
325
- token: session.token,
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
- stream: false,
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 EVERY model the server returned so OpenCode's native switcher
28
- // (`Tab` / `/models`) can move between them. Claude stays the default.
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.display_name || model.id,
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