faces-cli 1.6.18 → 1.7.1
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/dist/commands/catalog/doctor.js +17 -2
- package/dist/commands/chat/chat.d.ts +3 -1
- package/dist/commands/chat/chat.js +61 -119
- package/dist/commands/chat/messages.js +1 -1
- package/dist/commands/chat/responses.js +1 -1
- package/dist/commands/chat/thread.d.ts +4 -3
- package/dist/commands/chat/thread.js +63 -38
- package/dist/commands/face/list.d.ts +4 -0
- package/dist/commands/face/list.js +47 -4
- package/dist/routing.d.ts +19 -0
- package/dist/routing.js +116 -0
- package/oclif.manifest.json +1042 -1016
- package/package.json +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { FacesClient } from './client.js';
|
|
2
|
+
export declare const MESSAGES_ENDPOINT = "/v1/messages";
|
|
3
|
+
export declare const RESPONSES_ENDPOINT = "/v1/responses";
|
|
4
|
+
export declare const CHAT_COMPLETIONS_ENDPOINT = "/v1/chat/completions";
|
|
5
|
+
export interface RouteInfo {
|
|
6
|
+
endpoint: string;
|
|
7
|
+
/** The resolved LLM id we routed on (undefined if it couldn't be determined). */
|
|
8
|
+
llm?: string;
|
|
9
|
+
/** True when a free (OAuth-subscription) variant of this model exists in the catalog. */
|
|
10
|
+
freeWhenConnected: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Split `alias@llm` → llm; bare `alias` → undefined. */
|
|
13
|
+
export declare function llmFromArg(modelArg: string): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Decide which API endpoint a chat request should be posted to, driven entirely
|
|
16
|
+
* by the model catalog (GET /v1/models). For a bare alias we resolve the face's
|
|
17
|
+
* default model from the local catalog, falling back to GET /v1/faces/{alias}.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveEndpoint(client: FacesClient, modelArg: string): Promise<RouteInfo>;
|
package/dist/routing.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { CATALOG_INDEX } from './catalog.js';
|
|
5
|
+
export const MESSAGES_ENDPOINT = '/v1/messages';
|
|
6
|
+
export const RESPONSES_ENDPOINT = '/v1/responses';
|
|
7
|
+
export const CHAT_COMPLETIONS_ENDPOINT = '/v1/chat/completions';
|
|
8
|
+
const CACHE_PATH = path.join(os.homedir(), '.faces', 'models-cache.json');
|
|
9
|
+
const TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
10
|
+
/** Last-resort routing when the catalog can't tell us where a model lives. */
|
|
11
|
+
function fallbackEndpoint(llm) {
|
|
12
|
+
if (llm && llm.startsWith('claude'))
|
|
13
|
+
return MESSAGES_ENDPOINT;
|
|
14
|
+
return CHAT_COMPLETIONS_ENDPOINT;
|
|
15
|
+
}
|
|
16
|
+
function readCache() {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function writeCache(models) {
|
|
25
|
+
try {
|
|
26
|
+
const dir = path.dirname(CACHE_PATH);
|
|
27
|
+
if (!fs.existsSync(dir))
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
29
|
+
fs.writeFileSync(CACHE_PATH, JSON.stringify({ fetched_at: Date.now(), models }), { mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// a cache write failure is non-fatal
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function fetchModels(client) {
|
|
36
|
+
const raw = (await client.get('/v1/models'));
|
|
37
|
+
const data = Array.isArray(raw) ? raw : (raw.data ?? raw.models ?? []);
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Return the list of catalog models, served from a short-lived on-disk cache.
|
|
42
|
+
* Falls back to a stale cache if the network fetch fails, and to an empty list
|
|
43
|
+
* if there's nothing cached — callers degrade to {@link fallbackEndpoint}.
|
|
44
|
+
*/
|
|
45
|
+
async function getModels(client) {
|
|
46
|
+
const cache = readCache();
|
|
47
|
+
if (cache && Date.now() - cache.fetched_at < TTL_MS)
|
|
48
|
+
return cache.models;
|
|
49
|
+
try {
|
|
50
|
+
const models = await fetchModels(client);
|
|
51
|
+
writeCache(models);
|
|
52
|
+
return models;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return cache?.models ?? [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Build an id → {endpoint, freeWhenConnected} map, deduping paid/oauth twins. */
|
|
59
|
+
function buildMap(models) {
|
|
60
|
+
const map = new Map();
|
|
61
|
+
for (const m of models) {
|
|
62
|
+
if (!m.id || !m.endpoint)
|
|
63
|
+
continue;
|
|
64
|
+
const existing = map.get(m.id);
|
|
65
|
+
const isOauth = m.provider === 'openai_oauth';
|
|
66
|
+
if (existing) {
|
|
67
|
+
existing.freeWhenConnected = existing.freeWhenConnected || isOauth;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
map.set(m.id, { endpoint: m.endpoint, freeWhenConnected: isOauth });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return map;
|
|
74
|
+
}
|
|
75
|
+
/** Read a face's default_model from the local catalog index without a network call. */
|
|
76
|
+
function defaultModelFromCatalog(alias) {
|
|
77
|
+
try {
|
|
78
|
+
const entries = JSON.parse(fs.readFileSync(CATALOG_INDEX, 'utf8'));
|
|
79
|
+
const hit = entries.find((e) => e.alias === alias);
|
|
80
|
+
return hit?.default_model || undefined;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Split `alias@llm` → llm; bare `alias` → undefined. */
|
|
87
|
+
export function llmFromArg(modelArg) {
|
|
88
|
+
const i = modelArg.lastIndexOf('@');
|
|
89
|
+
return i >= 0 ? modelArg.slice(i + 1) : undefined;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Decide which API endpoint a chat request should be posted to, driven entirely
|
|
93
|
+
* by the model catalog (GET /v1/models). For a bare alias we resolve the face's
|
|
94
|
+
* default model from the local catalog, falling back to GET /v1/faces/{alias}.
|
|
95
|
+
*/
|
|
96
|
+
export async function resolveEndpoint(client, modelArg) {
|
|
97
|
+
let llm = llmFromArg(modelArg);
|
|
98
|
+
if (!llm) {
|
|
99
|
+
const alias = modelArg.split('@')[0];
|
|
100
|
+
llm = defaultModelFromCatalog(alias);
|
|
101
|
+
if (!llm) {
|
|
102
|
+
try {
|
|
103
|
+
const face = (await client.get(`/v1/faces/${encodeURIComponent(alias)}`));
|
|
104
|
+
llm = face.default_model || undefined;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// fall through to heuristic
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const map = buildMap(await getModels(client));
|
|
112
|
+
const hit = llm ? map.get(llm) : undefined;
|
|
113
|
+
if (hit)
|
|
114
|
+
return { endpoint: hit.endpoint, llm, freeWhenConnected: hit.freeWhenConnected };
|
|
115
|
+
return { endpoint: fallbackEndpoint(llm), llm, freeWhenConnected: false };
|
|
116
|
+
}
|