faces-cli 1.5.14 → 1.6.0

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/client.d.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  export declare class FacesAPIError extends Error {
2
2
  statusCode: number;
3
- constructor(statusCode: number, message: string);
3
+ errorCode?: string;
4
+ fallbackAvailable?: boolean;
5
+ constructor(statusCode: number, message: string, opts?: {
6
+ errorCode?: string;
7
+ fallbackAvailable?: boolean;
8
+ });
9
+ }
10
+ export interface ResponseWithHeaders<T = unknown> {
11
+ data: T;
12
+ headers: Record<string, string>;
4
13
  }
5
14
  export declare class FacesClient {
6
15
  private baseUrl;
@@ -19,6 +28,10 @@ export declare class FacesClient {
19
28
  requireJwt?: boolean;
20
29
  body?: unknown;
21
30
  }): Promise<unknown>;
31
+ postWithHeaders(path: string, opts?: {
32
+ requireJwt?: boolean;
33
+ body?: unknown;
34
+ }): Promise<ResponseWithHeaders>;
22
35
  postNoAuth(path: string, body: unknown): Promise<unknown>;
23
36
  patch(path: string, opts?: {
24
37
  requireJwt?: boolean;
package/dist/client.js CHANGED
@@ -1,9 +1,13 @@
1
1
  export class FacesAPIError extends Error {
2
2
  statusCode;
3
- constructor(statusCode, message) {
3
+ errorCode;
4
+ fallbackAvailable;
5
+ constructor(statusCode, message, opts) {
4
6
  super(message);
5
7
  this.statusCode = statusCode;
6
8
  this.name = 'FacesAPIError';
9
+ this.errorCode = opts?.errorCode;
10
+ this.fallbackAvailable = opts?.fallbackAvailable;
7
11
  }
8
12
  }
9
13
  export class FacesClient {
@@ -40,14 +44,24 @@ export class FacesClient {
40
44
  }
41
45
  async parseError(resp) {
42
46
  let msg = resp.statusText;
47
+ let errorCode;
48
+ let fallbackAvailable;
43
49
  try {
44
50
  const body = await resp.json();
45
- const raw = body.detail ?? body.error ?? body.message ?? msg;
46
- if (typeof raw === 'object' && raw !== null && 'message' in raw) {
47
- msg = String(raw.message);
51
+ // Structured OAuth rejection (422)
52
+ if (body.error === 'oauth_rejected') {
53
+ errorCode = 'oauth_rejected';
54
+ fallbackAvailable = body.fallback_available;
55
+ msg = String(body.detail ?? 'OAuth request rejected');
48
56
  }
49
57
  else {
50
- msg = typeof raw === 'object' ? JSON.stringify(raw) : String(raw);
58
+ const raw = body.detail ?? body.error ?? body.message ?? msg;
59
+ if (typeof raw === 'object' && raw !== null && 'message' in raw) {
60
+ msg = String(raw.message);
61
+ }
62
+ else {
63
+ msg = typeof raw === 'object' ? JSON.stringify(raw) : String(raw);
64
+ }
51
65
  }
52
66
  }
53
67
  catch {
@@ -57,7 +71,7 @@ export class FacesClient {
57
71
  msg = `Payment required: ${msg}`;
58
72
  if (resp.status === 403)
59
73
  msg = `Forbidden: ${msg}`;
60
- return new FacesAPIError(resp.status, msg);
74
+ return new FacesAPIError(resp.status, msg, { errorCode, fallbackAvailable });
61
75
  }
62
76
  async get(path, opts = {}) {
63
77
  let url = this.url(path);
@@ -91,6 +105,26 @@ export class FacesClient {
91
105
  throw await this.parseError(resp);
92
106
  return resp.json();
93
107
  }
108
+ async postWithHeaders(path, opts = {}) {
109
+ let resp;
110
+ try {
111
+ resp = await fetch(this.url(path), {
112
+ method: 'POST',
113
+ headers: { ...this.authHeader(opts.requireJwt), 'Content-Type': 'application/json' },
114
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
115
+ });
116
+ }
117
+ catch (err) {
118
+ this.wrapNetworkError(err, path);
119
+ }
120
+ if (!resp.ok)
121
+ throw await this.parseError(resp);
122
+ const headers = {};
123
+ for (const [k, v] of resp.headers.entries())
124
+ headers[k] = v;
125
+ const data = await resp.json();
126
+ return { data, headers };
127
+ }
94
128
  async postNoAuth(path, body) {
95
129
  let resp;
96
130
  try {
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class AccountPreferences extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ };
9
+ static args: {
10
+ key: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
11
+ value: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
12
+ };
13
+ static examples: string[];
14
+ run(): Promise<unknown>;
15
+ }
@@ -0,0 +1,65 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ import { FacesAPIError } from '../../client.js';
4
+ export default class AccountPreferences extends BaseCommand {
5
+ static description = 'View or update account preferences (api_fallback, default_model)';
6
+ static flags = {
7
+ ...BaseCommand.baseFlags,
8
+ };
9
+ static args = {
10
+ key: Args.string({ description: 'Preference key to update (api_fallback, default_model)' }),
11
+ value: Args.string({ description: 'New value' }),
12
+ };
13
+ static examples = [
14
+ 'faces account:preferences',
15
+ 'faces account:preferences default_model gpt-5.4',
16
+ 'faces account:preferences api_fallback true',
17
+ ];
18
+ async run() {
19
+ const { args, flags } = await this.parse(AccountPreferences);
20
+ const client = this.makeClient(flags);
21
+ // If no key provided, show current preferences
22
+ if (!args.key) {
23
+ let data;
24
+ try {
25
+ data = await client.get('/v1/user/preferences');
26
+ }
27
+ catch (err) {
28
+ if (err instanceof FacesAPIError)
29
+ this.error(`Error (${err.statusCode}): ${err.message}`);
30
+ throw err;
31
+ }
32
+ if (!this.jsonEnabled())
33
+ this.printHuman(data);
34
+ return data;
35
+ }
36
+ // Update a preference
37
+ if (!args.value)
38
+ this.error('Provide a value to set.');
39
+ const validKeys = ['api_fallback', 'default_model'];
40
+ if (!validKeys.includes(args.key)) {
41
+ this.error(`Unknown preference '${args.key}'. Valid keys: ${validKeys.join(', ')}`);
42
+ }
43
+ let parsedValue = args.value;
44
+ if (args.key === 'api_fallback') {
45
+ if (args.value !== 'true' && args.value !== 'false') {
46
+ this.error('api_fallback must be "true" or "false"');
47
+ }
48
+ parsedValue = args.value === 'true';
49
+ }
50
+ let data;
51
+ try {
52
+ data = await client.patch('/v1/user/preferences', { body: { [args.key]: parsedValue } });
53
+ }
54
+ catch (err) {
55
+ if (err instanceof FacesAPIError)
56
+ this.error(`Error (${err.statusCode}): ${err.message}`);
57
+ throw err;
58
+ }
59
+ if (!this.jsonEnabled()) {
60
+ this.log(`Set ${args.key} = ${args.value}`);
61
+ this.printHuman(data);
62
+ }
63
+ return data;
64
+ }
65
+ }
@@ -0,0 +1,10 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class CatalogBackup extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ };
9
+ run(): Promise<unknown>;
10
+ }
@@ -0,0 +1,101 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { BaseCommand } from '../../base.js';
5
+ import { FacesAPIError } from '../../client.js';
6
+ import { flattenBasicFacts } from '../../utils.js';
7
+ export default class CatalogBackup extends BaseCommand {
8
+ static description = 'Snapshot all faces and source material for migration';
9
+ static flags = {
10
+ ...BaseCommand.baseFlags,
11
+ };
12
+ async run() {
13
+ const { flags } = await this.parse(CatalogBackup);
14
+ const client = this.makeClient(flags);
15
+ const json = this.jsonEnabled();
16
+ // Fetch all faces
17
+ let faces;
18
+ try {
19
+ const resp = await client.get('/v1/faces');
20
+ faces = (resp.data ?? resp);
21
+ }
22
+ catch (err) {
23
+ if (err instanceof FacesAPIError)
24
+ this.error(`Error (${err.statusCode}): ${err.message}`);
25
+ throw err;
26
+ }
27
+ if (!json)
28
+ process.stderr.write(`Found ${faces.length} face(s)\n`);
29
+ const backupFaces = [];
30
+ for (const face of faces) {
31
+ const alias = face.alias;
32
+ if (!json)
33
+ process.stderr.write(` ${alias}: `);
34
+ const entry = {
35
+ alias,
36
+ name: face.name,
37
+ basic_facts: face.basic_facts ? flattenBasicFacts(face.basic_facts) : null,
38
+ default_model: face.default_model ?? null,
39
+ default_tools: face.default_tools ?? [],
40
+ formula: face.formula ?? null,
41
+ documents: [],
42
+ threads: [],
43
+ };
44
+ // Fetch documents
45
+ try {
46
+ const docs = await client.get('/v1/compile/documents', { params: { alias } });
47
+ for (const doc of docs) {
48
+ entry.documents.push({
49
+ label: doc.label ?? null,
50
+ content: doc.content,
51
+ perspective: doc.perspective ?? null,
52
+ });
53
+ }
54
+ }
55
+ catch {
56
+ // face may have no documents
57
+ }
58
+ // Fetch threads
59
+ try {
60
+ const threads = await client.get('/v1/compile/threads', { params: { alias } });
61
+ for (const thread of threads) {
62
+ const threadId = thread.thread_id;
63
+ try {
64
+ const full = await client.get(`/v1/compile/threads/${threadId}`);
65
+ const messages = full.messages ?? [];
66
+ entry.threads.push({
67
+ label: thread.label ?? null,
68
+ messages,
69
+ });
70
+ }
71
+ catch {
72
+ // skip threads we can't fetch
73
+ }
74
+ }
75
+ }
76
+ catch {
77
+ // face may have no threads
78
+ }
79
+ if (!json)
80
+ process.stderr.write(`${entry.documents.length} doc(s), ${entry.threads.length} thread(s)\n`);
81
+ backupFaces.push(entry);
82
+ }
83
+ const backup = {
84
+ version: 1,
85
+ created_at: new Date().toISOString(),
86
+ faces: backupFaces,
87
+ };
88
+ const backupDir = path.join(os.homedir(), '.faces', 'backups');
89
+ fs.mkdirSync(backupDir, { recursive: true });
90
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
91
+ const filePath = path.join(backupDir, `${timestamp}.json`);
92
+ fs.writeFileSync(filePath, JSON.stringify(backup, null, 2));
93
+ const totalDocs = backupFaces.reduce((n, f) => n + f.documents.length, 0);
94
+ const totalThreads = backupFaces.reduce((n, f) => n + f.threads.length, 0);
95
+ if (!json) {
96
+ this.log(`\nBackup saved: ${filePath}`);
97
+ this.log(` ${backupFaces.length} face(s), ${totalDocs} document(s), ${totalThreads} thread(s)`);
98
+ }
99
+ return { path: filePath, faces: backupFaces.length, documents: totalDocs, threads: totalThreads };
100
+ }
101
+ }
@@ -0,0 +1,16 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ export default class CatalogRestore extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ compile: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
10
+ static args: {
11
+ file: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
12
+ };
13
+ run(): Promise<unknown>;
14
+ private createFace;
15
+ private findLatestBackup;
16
+ }
@@ -0,0 +1,147 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { Args, Flags } from '@oclif/core';
5
+ import { BaseCommand } from '../../base.js';
6
+ import { FacesAPIError } from '../../client.js';
7
+ import { CatalogService } from '../../catalog.js';
8
+ export default class CatalogRestore extends BaseCommand {
9
+ static description = 'Restore faces and source material from a backup snapshot';
10
+ static flags = {
11
+ ...BaseCommand.baseFlags,
12
+ compile: Flags.boolean({ description: 'Run compile:all after restoring', default: false }),
13
+ };
14
+ static args = {
15
+ file: Args.string({ description: 'Backup file path (default: most recent in ~/.faces/backups/)' }),
16
+ };
17
+ async run() {
18
+ const { args, flags } = await this.parse(CatalogRestore);
19
+ const client = this.makeClient(flags);
20
+ const json = this.jsonEnabled();
21
+ const filePath = args.file ?? this.findLatestBackup();
22
+ if (!fs.existsSync(filePath))
23
+ this.error(`Backup file not found: ${filePath}`);
24
+ const backup = JSON.parse(fs.readFileSync(filePath, 'utf8'));
25
+ if (backup.version !== 1)
26
+ this.error(`Unsupported backup version: ${backup.version}`);
27
+ if (!json) {
28
+ this.log(`Restoring from: ${filePath}`);
29
+ this.log(` Created: ${backup.created_at}`);
30
+ this.log(` Faces: ${backup.faces.length}\n`);
31
+ }
32
+ const catalog = new CatalogService();
33
+ let facesCreated = 0;
34
+ let facesSkipped = 0;
35
+ let docsUploaded = 0;
36
+ let threadsUploaded = 0;
37
+ for (const face of backup.faces) {
38
+ if (!json)
39
+ process.stderr.write(`${face.alias}: `);
40
+ // Create face
41
+ const created = await this.createFace(client, face, json);
42
+ if (!created) {
43
+ facesSkipped++;
44
+ if (!json)
45
+ process.stderr.write('exists, skipping\n');
46
+ continue;
47
+ }
48
+ facesCreated++;
49
+ // Write to local catalog
50
+ try {
51
+ catalog.writeFace({ alias: face.alias, name: face.name, basic_facts: face.basic_facts });
52
+ }
53
+ catch { /* non-fatal */ }
54
+ // Upload documents
55
+ for (const doc of face.documents) {
56
+ try {
57
+ const payload = { alias: face.alias, content: doc.content };
58
+ if (doc.label)
59
+ payload.label = doc.label;
60
+ if (doc.perspective)
61
+ payload.perspective = doc.perspective;
62
+ await client.post('/v1/compile/documents', { body: payload });
63
+ docsUploaded++;
64
+ }
65
+ catch (err) {
66
+ const msg = err instanceof FacesAPIError ? `${err.statusCode}: ${err.message}` : String(err);
67
+ if (!json)
68
+ process.stderr.write(`\n warn: doc upload failed: ${msg}\n `);
69
+ }
70
+ }
71
+ // Upload threads — create thread then PATCH messages in bulk
72
+ for (const thread of face.threads) {
73
+ try {
74
+ const createPayload = { alias: face.alias };
75
+ if (thread.label)
76
+ createPayload.label = thread.label;
77
+ const created = await client.post('/v1/compile/threads', { body: createPayload });
78
+ const threadId = created.thread_id;
79
+ // Overwrite the auto-generated messages with the backup messages
80
+ if (thread.messages.length > 0) {
81
+ await client.patch(`/v1/compile/threads/${threadId}/messages`, {
82
+ body: { messages: thread.messages },
83
+ });
84
+ }
85
+ threadsUploaded++;
86
+ }
87
+ catch (err) {
88
+ const msg = err instanceof FacesAPIError ? `${err.statusCode}: ${err.message}` : String(err);
89
+ if (!json)
90
+ process.stderr.write(`\n warn: thread upload failed: ${msg}\n `);
91
+ }
92
+ }
93
+ if (!json)
94
+ process.stderr.write(`${face.documents.length} doc(s), ${face.threads.length} thread(s)\n`);
95
+ }
96
+ if (!json) {
97
+ this.log(`\nRestore complete:`);
98
+ this.log(` Faces created: ${facesCreated}, skipped: ${facesSkipped}`);
99
+ this.log(` Documents uploaded: ${docsUploaded}`);
100
+ this.log(` Threads uploaded: ${threadsUploaded}`);
101
+ }
102
+ if (flags.compile) {
103
+ if (!json)
104
+ this.log('\nRunning compile:all...\n');
105
+ await this.config.runCommand('compile:all', json ? ['--json'] : []);
106
+ }
107
+ return { faces_created: facesCreated, faces_skipped: facesSkipped, documents: docsUploaded, threads: threadsUploaded };
108
+ }
109
+ async createFace(client, face, _json) {
110
+ const payload = { name: face.name, alias: face.alias };
111
+ if (face.formula) {
112
+ payload.formula = face.formula;
113
+ if (face.default_model)
114
+ payload.default_model = face.default_model;
115
+ }
116
+ else {
117
+ if (face.basic_facts && Object.keys(face.basic_facts).length > 0)
118
+ payload.basic_facts = face.basic_facts;
119
+ if (face.default_tools && face.default_tools.length > 0)
120
+ payload.default_tools = face.default_tools;
121
+ if (face.default_model)
122
+ payload.default_model = face.default_model;
123
+ }
124
+ try {
125
+ await client.post('/v1/faces', { body: payload });
126
+ return true;
127
+ }
128
+ catch (err) {
129
+ if (err instanceof FacesAPIError && err.statusCode === 400) {
130
+ // likely "already have a face with alias"
131
+ return false;
132
+ }
133
+ throw err;
134
+ }
135
+ }
136
+ findLatestBackup() {
137
+ const backupDir = path.join(os.homedir(), '.faces', 'backups');
138
+ if (!fs.existsSync(backupDir))
139
+ this.error('No backups found in ~/.faces/backups/');
140
+ const files = fs.readdirSync(backupDir)
141
+ .filter(f => f.endsWith('.json'))
142
+ .sort();
143
+ if (files.length === 0)
144
+ this.error('No backups found in ~/.faces/backups/');
145
+ return path.join(backupDir, files[files.length - 1]);
146
+ }
147
+ }
@@ -10,6 +10,7 @@ export default class ChatChat extends BaseCommand {
10
10
  temperature: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
12
  responses: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ 'oauth-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
14
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
15
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
16
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -21,4 +22,5 @@ export default class ChatChat extends BaseCommand {
21
22
  private runChatCompletions;
22
23
  private runMessages;
23
24
  private runResponses;
25
+ private extractText;
24
26
  }
@@ -20,6 +20,7 @@ export default class ChatChat extends BaseCommand {
20
20
  temperature: Flags.string({ description: 'Temperature (0.0-2.0)' }),
21
21
  file: Flags.string({ description: 'Read message from file' }),
22
22
  responses: Flags.boolean({ description: 'Use OpenAI Responses API instead of Chat Completions', default: false }),
23
+ 'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
23
24
  };
24
25
  static args = {
25
26
  face_username: Args.string({ description: 'Face alias (or alias@model)', required: true }),
@@ -64,8 +65,15 @@ export default class ChatChat extends BaseCommand {
64
65
  }
65
66
  }
66
67
  catch (err) {
67
- if (err instanceof FacesAPIError)
68
+ if (err instanceof FacesAPIError) {
69
+ if (err.errorCode === 'oauth_rejected') {
70
+ const hint = err.fallbackAvailable
71
+ ? 'Run `faces account:preferences api_fallback true` to allow paid fallback.'
72
+ : 'Run `faces billing:topup` to add credits, then enable fallback with `faces account:preferences api_fallback true`.';
73
+ this.error(`OAuth failed: ${err.message}\n${hint}`);
74
+ }
68
75
  this.error(`Error (${err.statusCode}): ${err.message}`);
76
+ }
69
77
  throw err;
70
78
  }
71
79
  }
@@ -80,6 +88,8 @@ export default class ChatChat extends BaseCommand {
80
88
  payload.max_tokens = flags['max-tokens'];
81
89
  if (flags.temperature !== undefined)
82
90
  payload.temperature = Number.parseFloat(flags.temperature);
91
+ if (flags['oauth-only'])
92
+ payload.oauth_only = true;
83
93
  if (flags.stream) {
84
94
  for await (const line of client.stream('/v1/chat/completions', payload)) {
85
95
  if (!line.startsWith('data: '))
@@ -99,13 +109,25 @@ export default class ChatChat extends BaseCommand {
99
109
  process.stdout.write('\n');
100
110
  return {};
101
111
  }
102
- const data = (await client.post('/v1/chat/completions', { body: payload }));
103
- if (this.jsonEnabled())
104
- return data;
105
- const choices = data.choices;
106
- const message = choices?.[0]?.message;
107
- this.log(String(message?.content ?? ''));
108
- return data;
112
+ const { data, headers } = await client.postWithHeaders('/v1/chat/completions', { body: payload });
113
+ const body = data;
114
+ const routedEndpoint = headers['x-faces-routed-endpoint'];
115
+ // Extract text based on which format the backend used
116
+ const text = this.extractText(body, routedEndpoint);
117
+ if (this.jsonEnabled()) {
118
+ const meta = {};
119
+ if (headers['x-faces-provider'])
120
+ meta.provider = headers['x-faces-provider'];
121
+ if (headers['x-faces-cost-usd'])
122
+ meta.cost_usd = headers['x-faces-cost-usd'];
123
+ if (routedEndpoint)
124
+ meta.routed_endpoint = routedEndpoint;
125
+ if (Object.keys(meta).length > 0)
126
+ body._meta = meta;
127
+ return body;
128
+ }
129
+ this.log(text);
130
+ return body;
109
131
  }
110
132
  async runMessages(client, model, userMessages, flags) {
111
133
  const msgs = userMessages.map((m) => ({ role: 'user', content: m }));
@@ -117,6 +139,8 @@ export default class ChatChat extends BaseCommand {
117
139
  };
118
140
  if (flags.system)
119
141
  payload.system = flags.system;
142
+ if (flags['oauth-only'])
143
+ payload.oauth_only = true;
120
144
  if (flags.stream) {
121
145
  for await (const line of client.stream('/v1/messages', payload)) {
122
146
  if (!line.startsWith('data: '))
@@ -137,21 +161,32 @@ export default class ChatChat extends BaseCommand {
137
161
  process.stdout.write('\n');
138
162
  return {};
139
163
  }
140
- const data = (await client.post('/v1/messages', { body: payload }));
141
- if (this.jsonEnabled())
142
- return data;
164
+ const { data, headers } = await client.postWithHeaders('/v1/messages', { body: payload });
165
+ const body = data;
166
+ if (this.jsonEnabled()) {
167
+ const meta = {};
168
+ if (headers['x-faces-provider'])
169
+ meta.provider = headers['x-faces-provider'];
170
+ if (headers['x-faces-cost-usd'])
171
+ meta.cost_usd = headers['x-faces-cost-usd'];
172
+ if (Object.keys(meta).length > 0)
173
+ body._meta = meta;
174
+ return body;
175
+ }
143
176
  let content = '';
144
- for (const block of data.content ?? []) {
177
+ for (const block of body.content ?? []) {
145
178
  if (block.type === 'text')
146
179
  content += String(block.text ?? '');
147
180
  }
148
181
  this.log(content);
149
- return data;
182
+ return body;
150
183
  }
151
184
  async runResponses(client, model, input, flags) {
152
185
  const payload = { model, input, stream: flags.stream };
153
186
  if (flags.system)
154
187
  payload.instructions = flags.system;
188
+ if (flags['oauth-only'])
189
+ payload.oauth_only = true;
155
190
  if (flags.stream) {
156
191
  for await (const line of client.stream('/v1/responses', payload)) {
157
192
  if (!line.startsWith('data: '))
@@ -172,17 +207,52 @@ export default class ChatChat extends BaseCommand {
172
207
  process.stdout.write('\n');
173
208
  return {};
174
209
  }
175
- const data = (await client.post('/v1/responses', { body: payload }));
176
- if (this.jsonEnabled())
177
- return data;
210
+ const { data, headers } = await client.postWithHeaders('/v1/responses', { body: payload });
211
+ const body = data;
212
+ if (this.jsonEnabled()) {
213
+ const meta = {};
214
+ if (headers['x-faces-provider'])
215
+ meta.provider = headers['x-faces-provider'];
216
+ if (headers['x-faces-cost-usd'])
217
+ meta.cost_usd = headers['x-faces-cost-usd'];
218
+ if (Object.keys(meta).length > 0)
219
+ body._meta = meta;
220
+ return body;
221
+ }
178
222
  let outputText = '';
179
- for (const item of data.output ?? []) {
223
+ for (const item of body.output ?? []) {
180
224
  for (const block of item.content ?? []) {
181
225
  if (block.type === 'output_text')
182
226
  outputText += String(block.text ?? '');
183
227
  }
184
228
  }
185
229
  this.log(outputText);
186
- return data;
230
+ return body;
231
+ }
232
+ extractText(body, routedEndpoint) {
233
+ if (routedEndpoint === '/v1/responses') {
234
+ // OpenAI Responses format
235
+ let text = '';
236
+ for (const item of body.output ?? []) {
237
+ for (const block of item.content ?? []) {
238
+ if (block.type === 'output_text')
239
+ text += String(block.text ?? '');
240
+ }
241
+ }
242
+ return text;
243
+ }
244
+ if (routedEndpoint === '/v1/messages') {
245
+ // Anthropic Messages format
246
+ let text = '';
247
+ for (const block of body.content ?? []) {
248
+ if (block.type === 'text')
249
+ text += String(block.text ?? '');
250
+ }
251
+ return text;
252
+ }
253
+ // Standard chat/completions format (default)
254
+ const choices = body.choices;
255
+ const message = choices?.[0]?.message;
256
+ return String(message?.content ?? '');
187
257
  }
188
258
  }
@@ -6,6 +6,7 @@ export default class ChatMessages extends BaseCommand {
6
6
  system: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
8
  'max-tokens': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'oauth-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
10
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;