@leoustc/service-llm 0.2.2 → 0.2.5

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/README.md CHANGED
@@ -15,6 +15,14 @@ Use your ChatGPT Codex subscription through OpenAI-compatible HTTP APIs or MCP.
15
15
  npm install --global @leoustc/service-llm
16
16
  ```
17
17
 
18
+ Show the installed CLI version:
19
+
20
+ ```sh
21
+ service-llm -v
22
+ ```
23
+
24
+ Use `service-llm --help` to show all available commands and server options.
25
+
18
26
  ## Log in
19
27
 
20
28
  ```sh
@@ -144,9 +152,10 @@ Connect an MCP client to:
144
152
  http://127.0.0.1:7060/mcp
145
153
  ```
146
154
 
147
- The server provides one tool:
155
+ The server provides two tools:
148
156
 
149
157
  - `ask`: accepts `prompt` and optional `model`, `tools`, and `tool_choice` arguments.
158
+ - `list_models`: lists available models and reports the currently configured default model and reasoning level.
150
159
 
151
160
  Example JSON-RPC request:
152
161
 
@@ -168,6 +177,14 @@ curl http://127.0.0.1:7060/mcp \
168
177
 
169
178
  Specify a model with the request's `model` field. If it is omitted or unavailable, the service uses your configured default. On first login, the lowest-cost available model is selected with low reasoning effort. Responses always report the model actually used.
170
179
 
180
+ List the models available to both Chat Completions and Responses clients:
181
+
182
+ ```sh
183
+ curl http://127.0.0.1:7060/v1/models
184
+ ```
185
+
186
+ When client authentication is enabled, include the same Bearer or `X-API-Key` header used for generation requests. The response includes `default_model` and `default_reasoning_effort` in addition to the OpenAI-compatible model list.
187
+
171
188
  ## Debug logging
172
189
 
173
190
  Enable request, response, and SSE logs with:
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.2
1
+ 0.2.5
package/dist/cli.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { listModels, login, setDefaultModel } from "./auth.js";
3
3
  import { startServer } from "./http.js";
4
+ import { version } from "./version.js";
4
5
 
5
6
  function usage() {
6
7
  console.error(`Usage:
8
+ service-llm -v | --version
7
9
  service-llm login [--device-code]
8
10
  service-llm model [list]
9
11
  service-llm model set <model-id>
@@ -39,7 +41,9 @@ function valueAfter(args, names, fallback) {
39
41
  }
40
42
 
41
43
  const args = process.argv.slice(2);
42
- if (args.includes("--help") || args.includes("-h")) {
44
+ if (args.length === 1 && ["-v", "--version"].includes(args[0])) {
45
+ console.log(`service-llm v${version}`);
46
+ } else if (args.includes("--help") || args.includes("-h")) {
43
47
  usage();
44
48
  } else if (args[0] === "login") {
45
49
  await login({ deviceCode: args.includes("--device-code") });
package/dist/http.js CHANGED
@@ -5,7 +5,6 @@ import { version } from "./version.js";
5
5
  import {
6
6
  contentFromMessage,
7
7
  createCompletionStream,
8
- defaultModel,
9
8
  messagesFromChat,
10
9
  messagesFromResponses,
11
10
  toolCallsFromMessage,
@@ -26,6 +25,23 @@ function sendJson(response, status, body) {
26
25
  response.end(JSON.stringify(body));
27
26
  }
28
27
 
28
+ export function modelCatalogResponse(catalog) {
29
+ const created = Math.floor(new Date(catalog.updated_at ?? 0).getTime() / 1000) || 0;
30
+ return {
31
+ object: "list",
32
+ data: catalog.models.map((model) => ({
33
+ id: model.id,
34
+ object: "model",
35
+ created,
36
+ owned_by: "openai-codex",
37
+ ...(model.name ? { name: model.name } : {}),
38
+ })),
39
+ default_model: catalog.default_model,
40
+ default_reasoning_effort: catalog.default_reasoning_effort ?? "low",
41
+ updated_at: catalog.updated_at,
42
+ };
43
+ }
44
+
29
45
  async function readJson(request) {
30
46
  const chunks = [];
31
47
  let size = 0;
@@ -151,8 +167,8 @@ async function responses(request, response, body) {
151
167
  sendJson(response, 200, responseObject(id, model, await collect(streamResult)));
152
168
  }
153
169
 
154
- async function ask({ prompt, model = defaultModel, tools, toolChoice }) {
155
- const completion = await createCompletionStream({
170
+ export async function ask({ prompt, model, tools, toolChoice }, createCompletion = createCompletionStream) {
171
+ const completion = await createCompletion({
156
172
  context: messagesFromResponses(prompt),
157
173
  model,
158
174
  tools,
@@ -167,7 +183,20 @@ async function mcp(response, body) {
167
183
  if (body.method === "initialize") return sendJson(response, 200, { ...base, result: { protocolVersion: body.params?.protocolVersion ?? "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "service-llm", version } } });
168
184
  if (body.method === "notifications/initialized") return response.writeHead(202).end();
169
185
  if (body.method === "ping") return sendJson(response, 200, { ...base, result: {} });
170
- if (body.method === "tools/list") return sendJson(response, 200, { ...base, result: { tools: [{ name: "ask", description: "Ask a Codex model a question and optionally allow it to return function tool calls. Tool calls are never executed.", inputSchema: { type: "object", properties: { prompt: { type: "string" }, model: { type: "string" }, tools: { type: "array", items: { type: "object" } }, tool_choice: {} }, required: ["prompt"], additionalProperties: false } }] } });
186
+ if (body.method === "tools/list") return sendJson(response, 200, { ...base, result: { tools: [
187
+ { name: "ask", description: "Ask a Codex model a question and optionally allow it to return function tool calls. Tool calls are never executed.", inputSchema: { type: "object", properties: { prompt: { type: "string" }, model: { type: "string" }, tools: { type: "array", items: { type: "object" } }, tool_choice: {} }, required: ["prompt"], additionalProperties: false } },
188
+ { name: "list_models", description: "List the available Codex models and report the model currently configured as the default.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
189
+ ] } });
190
+ if (body.method === "tools/call" && body.params?.name === "list_models") {
191
+ const catalog = modelCatalogResponse(await listModels());
192
+ return sendJson(response, 200, {
193
+ ...base,
194
+ result: {
195
+ content: [{ type: "text", text: JSON.stringify(catalog, null, 2) }],
196
+ structuredContent: catalog,
197
+ },
198
+ });
199
+ }
171
200
  if (body.method === "tools/call" && body.params?.name === "ask") {
172
201
  if (typeof body.params.arguments?.prompt !== "string") return sendJson(response, 200, { ...base, error: { code: -32602, message: "ask requires a string prompt" } });
173
202
  const answer = await ask({
@@ -209,6 +238,7 @@ export function startServer({ port = 7060, host = "127.0.0.1", apiKeys = [] } =
209
238
  });
210
239
  if (request.method === "GET" && url.pathname === "/health") return sendJson(response, 200, { status: "ok" });
211
240
  if (!isAuthorized(request, allowedKeys)) return sendJson(response, 401, { error: { message: "Invalid API key", type: "authentication_error" } });
241
+ if (request.method === "GET" && url.pathname === "/v1/models") return sendJson(response, 200, modelCatalogResponse(await listModels()));
212
242
  if (request.method !== "POST") return sendJson(response, 405, { error: { message: "Method not allowed" } });
213
243
  const body = await readJson(request);
214
244
  debug("request body", body);
@@ -245,6 +275,7 @@ export function startServer({ port = 7060, host = "127.0.0.1", apiKeys = [] } =
245
275
  }
246
276
  console.error("Endpoints:");
247
277
  console.error(` Health: ${baseUrl}/health`);
278
+ console.error(` Models: ${baseUrl}/v1/models`);
248
279
  console.error(` Chat Completions: ${baseUrl}/v1/chat/completions`);
249
280
  console.error(` Responses: ${baseUrl}/v1/responses`);
250
281
  console.error(` MCP: ${baseUrl}/mcp`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leoustc/service-llm",
3
- "version": "0.2.2",
3
+ "version": "0.2.5",
4
4
  "description": "Standalone CLI service exposing a Codex subscription through OpenAI-compatible and MCP endpoints.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",