faces-cli 1.5.15 → 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
+ }
@@ -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>;
@@ -9,6 +9,7 @@ export default class ChatMessages extends BaseCommand {
9
9
  system: Flags.string({ description: 'System prompt' }),
10
10
  stream: Flags.boolean({ description: 'Stream the response', default: false }),
11
11
  'max-tokens': Flags.integer({ description: 'Max tokens', default: 1024 }),
12
+ 'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
12
13
  };
13
14
  static args = {
14
15
  face_model: Args.string({ description: 'Face model (e.g. myface or myface@claude-sonnet-4-6)', required: true }),
@@ -25,6 +26,8 @@ export default class ChatMessages extends BaseCommand {
25
26
  };
26
27
  if (flags.system)
27
28
  payload.system = flags.system;
29
+ if (flags['oauth-only'])
30
+ payload.oauth_only = true;
28
31
  try {
29
32
  if (flags.stream) {
30
33
  for await (const line of client.stream('/v1/messages', payload)) {
@@ -48,20 +51,36 @@ export default class ChatMessages extends BaseCommand {
48
51
  process.stdout.write('\n');
49
52
  return {};
50
53
  }
51
- const data = (await client.post('/v1/messages', { body: payload }));
52
- if (this.jsonEnabled())
53
- return data;
54
+ const { data, headers } = await client.postWithHeaders('/v1/messages', { body: payload });
55
+ const body = data;
56
+ if (this.jsonEnabled()) {
57
+ const meta = {};
58
+ if (headers['x-faces-provider'])
59
+ meta.provider = headers['x-faces-provider'];
60
+ if (headers['x-faces-cost-usd'])
61
+ meta.cost_usd = headers['x-faces-cost-usd'];
62
+ if (Object.keys(meta).length > 0)
63
+ body._meta = meta;
64
+ return body;
65
+ }
54
66
  let content = '';
55
- for (const block of data.content ?? []) {
67
+ for (const block of body.content ?? []) {
56
68
  if (block.type === 'text')
57
69
  content += String(block.text ?? '');
58
70
  }
59
71
  this.log(content);
60
- return data;
72
+ return body;
61
73
  }
62
74
  catch (err) {
63
- if (err instanceof FacesAPIError)
75
+ if (err instanceof FacesAPIError) {
76
+ if (err.errorCode === 'oauth_rejected') {
77
+ const hint = err.fallbackAvailable
78
+ ? 'Run `faces account:preferences api_fallback true` to allow paid fallback.'
79
+ : 'Run `faces billing:topup` to add credits, then enable fallback with `faces account:preferences api_fallback true`.';
80
+ this.error(`OAuth failed: ${err.message}\n${hint}`);
81
+ }
64
82
  this.error(`Error (${err.statusCode}): ${err.message}`);
83
+ }
65
84
  throw err;
66
85
  }
67
86
  }
@@ -5,6 +5,7 @@ export default class ChatResponses extends BaseCommand {
5
5
  message: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
6
  instructions: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  stream: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ 'oauth-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
9
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -8,6 +8,7 @@ export default class ChatResponses extends BaseCommand {
8
8
  message: Flags.string({ char: 'm', description: 'User input message', required: true }),
9
9
  instructions: Flags.string({ description: 'System instructions' }),
10
10
  stream: Flags.boolean({ description: 'Stream the response', default: false }),
11
+ 'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
11
12
  };
12
13
  static args = {
13
14
  face_model: Args.string({ description: 'Face model (e.g. myface or myface@gpt-4o)', required: true }),
@@ -22,6 +23,8 @@ export default class ChatResponses extends BaseCommand {
22
23
  };
23
24
  if (flags.instructions)
24
25
  payload.instructions = flags.instructions;
26
+ if (flags['oauth-only'])
27
+ payload.oauth_only = true;
25
28
  try {
26
29
  if (flags.stream) {
27
30
  for await (const line of client.stream('/v1/responses', payload)) {
@@ -45,22 +48,38 @@ export default class ChatResponses extends BaseCommand {
45
48
  process.stdout.write('\n');
46
49
  return {};
47
50
  }
48
- const data = (await client.post('/v1/responses', { body: payload }));
49
- if (this.jsonEnabled())
50
- return data;
51
+ const { data, headers } = await client.postWithHeaders('/v1/responses', { body: payload });
52
+ const body = data;
53
+ if (this.jsonEnabled()) {
54
+ const meta = {};
55
+ if (headers['x-faces-provider'])
56
+ meta.provider = headers['x-faces-provider'];
57
+ if (headers['x-faces-cost-usd'])
58
+ meta.cost_usd = headers['x-faces-cost-usd'];
59
+ if (Object.keys(meta).length > 0)
60
+ body._meta = meta;
61
+ return body;
62
+ }
51
63
  let outputText = '';
52
- for (const item of data.output ?? []) {
64
+ for (const item of body.output ?? []) {
53
65
  for (const block of item.content ?? []) {
54
66
  if (block.type === 'output_text')
55
67
  outputText += String(block.text ?? '');
56
68
  }
57
69
  }
58
70
  this.log(outputText);
59
- return data;
71
+ return body;
60
72
  }
61
73
  catch (err) {
62
- if (err instanceof FacesAPIError)
74
+ if (err instanceof FacesAPIError) {
75
+ if (err.errorCode === 'oauth_rejected') {
76
+ const hint = err.fallbackAvailable
77
+ ? 'Run `faces account:preferences api_fallback true` to allow paid fallback.'
78
+ : 'Run `faces billing:topup` to add credits, then enable fallback with `faces account:preferences api_fallback true`.';
79
+ this.error(`OAuth failed: ${err.message}\n${hint}`);
80
+ }
63
81
  this.error(`Error (${err.statusCode}): ${err.message}`);
82
+ }
64
83
  throw err;
65
84
  }
66
85
  }
@@ -3,6 +3,7 @@ export default class CompileThreadCreate extends BaseCommand {
3
3
  static description: string;
4
4
  static flags: {
5
5
  label: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
+ 'oauth-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
7
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
8
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -6,6 +6,7 @@ export default class CompileThreadCreate extends BaseCommand {
6
6
  static flags = {
7
7
  ...BaseCommand.baseFlags,
8
8
  label: Flags.string({ description: 'Thread label' }),
9
+ 'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
9
10
  };
10
11
  static args = {
11
12
  face_id: Args.string({ description: 'Face alias', required: true }),
@@ -16,13 +17,22 @@ export default class CompileThreadCreate extends BaseCommand {
16
17
  const payload = { alias: args.face_id };
17
18
  if (flags.label)
18
19
  payload.label = flags.label;
20
+ if (flags['oauth-only'])
21
+ payload.oauth_only = true;
19
22
  let data;
20
23
  try {
21
24
  data = await client.post('/v1/compile/threads', { body: payload });
22
25
  }
23
26
  catch (err) {
24
- if (err instanceof FacesAPIError)
27
+ if (err instanceof FacesAPIError) {
28
+ if (err.errorCode === 'oauth_rejected') {
29
+ const hint = err.fallbackAvailable
30
+ ? 'Run `faces account:preferences api_fallback true` to allow paid fallback.'
31
+ : 'Run `faces billing:topup` to add credits, then enable fallback with `faces account:preferences api_fallback true`.';
32
+ this.error(`OAuth failed: ${err.message}\n${hint}`);
33
+ }
25
34
  this.error(`Error (${err.statusCode}): ${err.message}`);
35
+ }
26
36
  throw err;
27
37
  }
28
38
  if (!this.jsonEnabled())
@@ -3,6 +3,7 @@ export default class CompileThreadMessage extends BaseCommand {
3
3
  static description: string;
4
4
  static flags: {
5
5
  message: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
+ 'oauth-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
7
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
8
  token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -6,6 +6,7 @@ export default class CompileThreadMessage extends BaseCommand {
6
6
  static flags = {
7
7
  ...BaseCommand.baseFlags,
8
8
  message: Flags.string({ char: 'm', description: 'User message to append', required: true }),
9
+ 'oauth-only': Flags.boolean({ description: 'Prevent fallback to paid system key (use OAuth only)', default: false }),
9
10
  };
10
11
  static args = {
11
12
  thread_id: Args.string({ description: 'Thread ID', required: true }),
@@ -13,15 +14,25 @@ export default class CompileThreadMessage extends BaseCommand {
13
14
  async run() {
14
15
  const { args, flags } = await this.parse(CompileThreadMessage);
15
16
  const client = this.makeClient(flags);
17
+ const payload = { message: flags.message };
18
+ if (flags['oauth-only'])
19
+ payload.oauth_only = true;
16
20
  let data;
17
21
  try {
18
22
  data = await client.post(`/v1/compile/threads/${args.thread_id}/messages`, {
19
- body: { message: flags.message },
23
+ body: payload,
20
24
  });
21
25
  }
22
26
  catch (err) {
23
- if (err instanceof FacesAPIError)
27
+ if (err instanceof FacesAPIError) {
28
+ if (err.errorCode === 'oauth_rejected') {
29
+ const hint = err.fallbackAvailable
30
+ ? 'Run `faces account:preferences api_fallback true` to allow paid fallback.'
31
+ : 'Run `faces billing:topup` to add credits, then enable fallback with `faces account:preferences api_fallback true`.';
32
+ this.error(`OAuth failed: ${err.message}\n${hint}`);
33
+ }
24
34
  this.error(`Error (${err.statusCode}): ${err.message}`);
35
+ }
25
36
  throw err;
26
37
  }
27
38
  if (!this.jsonEnabled())