@animus-labs/cortex 0.2.2 → 0.2.4
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/cortex-agent.d.ts +1 -0
- package/dist/cortex-agent.d.ts.map +1 -1
- package/dist/cortex-agent.js +29 -9
- package/dist/cortex-agent.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/provider-manager.d.ts +39 -3
- package/dist/provider-manager.d.ts.map +1 -1
- package/dist/provider-manager.js +187 -56
- package/dist/provider-manager.js.map +1 -1
- package/dist/provider-registry.d.ts +7 -9
- package/dist/provider-registry.d.ts.map +1 -1
- package/dist/provider-registry.js +11 -19
- package/dist/provider-registry.js.map +1 -1
- package/dist/utility-model-inference.d.ts +5 -0
- package/dist/utility-model-inference.d.ts.map +1 -0
- package/dist/utility-model-inference.js +174 -0
- package/dist/utility-model-inference.js.map +1 -0
- package/package.json +1 -1
- package/src/cortex-agent.ts +28 -9
- package/src/index.ts +4 -1
- package/src/provider-manager.ts +266 -64
- package/src/provider-registry.ts +12 -19
- package/src/utility-model-inference.ts +203 -0
package/src/provider-manager.ts
CHANGED
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
import {
|
|
18
18
|
PROVIDER_REGISTRY,
|
|
19
19
|
OAUTH_PROVIDER_IDS,
|
|
20
|
-
|
|
20
|
+
UTILITY_MODEL_OVERRIDES,
|
|
21
21
|
} from './provider-registry.js';
|
|
22
22
|
import { createRequire } from 'node:module';
|
|
23
23
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
24
24
|
import type { ThinkingLevel } from './types.js';
|
|
25
25
|
import type { ProviderInfo, ModelInfo } from './provider-registry.js';
|
|
26
26
|
import { wrapModel } from './model-wrapper.js';
|
|
27
|
+
import { inferUtilityModelId } from './utility-model-inference.js';
|
|
27
28
|
import type { CortexModel } from './model-wrapper.js';
|
|
28
29
|
|
|
29
30
|
const nodeRequire = createRequire(import.meta.url);
|
|
@@ -74,6 +75,16 @@ export interface OAuthCallbacks {
|
|
|
74
75
|
* localhost callback routes and is restored immediately after the login flow.
|
|
75
76
|
*/
|
|
76
77
|
renderCallbackPage?: OAuthCallbackPageRenderer | undefined;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Overall timeout for the OAuth flow, in milliseconds. pi-ai's
|
|
81
|
+
* callback-server flows (e.g. Anthropic) do not honor an abort signal and
|
|
82
|
+
* hang forever if the callback never arrives or arrives with an error, so
|
|
83
|
+
* Cortex enforces this timeout itself and rejects with an
|
|
84
|
+
* `OAuthError('timed_out')`. Defaults to 5 minutes. Pass `0` or a negative
|
|
85
|
+
* value to disable (not recommended).
|
|
86
|
+
*/
|
|
87
|
+
timeoutMs?: number | undefined;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
/** Status of the browser callback page produced by an OAuth flow. */
|
|
@@ -151,6 +162,48 @@ export interface OAuthRefreshResult {
|
|
|
151
162
|
changed: boolean;
|
|
152
163
|
}
|
|
153
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Discriminant for OAuth flow failures, so consumers can render specific
|
|
167
|
+
* UX instead of parsing error strings.
|
|
168
|
+
*
|
|
169
|
+
* - `unsupported_provider`: provider has no OAuth support.
|
|
170
|
+
* - `callback_port_in_use`: the provider's fixed loopback callback port is
|
|
171
|
+
* already bound (e.g. another Anthropic app on 53692, or a leftover flow).
|
|
172
|
+
* Detected before the browser opens.
|
|
173
|
+
* - `cancelled`: the flow was cancelled via `cancelOAuth()`.
|
|
174
|
+
* - `timed_out`: the flow exceeded its timeout (pi-ai's callback servers do
|
|
175
|
+
* not honor an abort signal, so this is the backstop against hangs).
|
|
176
|
+
* - `callback_failed`: the browser callback fired but the provider reported
|
|
177
|
+
* an error (e.g. state mismatch). Surfaced immediately instead of hanging.
|
|
178
|
+
*/
|
|
179
|
+
export type OAuthErrorCode =
|
|
180
|
+
| 'unsupported_provider'
|
|
181
|
+
| 'callback_port_in_use'
|
|
182
|
+
| 'cancelled'
|
|
183
|
+
| 'timed_out'
|
|
184
|
+
| 'callback_failed';
|
|
185
|
+
|
|
186
|
+
/** Structured error thrown by initiateOAuth. */
|
|
187
|
+
export class OAuthError extends Error {
|
|
188
|
+
readonly code: OAuthErrorCode;
|
|
189
|
+
readonly provider: string;
|
|
190
|
+
/** The fixed callback port, when relevant (`callback_port_in_use`). */
|
|
191
|
+
readonly port?: number | undefined;
|
|
192
|
+
|
|
193
|
+
constructor(
|
|
194
|
+
code: OAuthErrorCode,
|
|
195
|
+
provider: string,
|
|
196
|
+
message: string,
|
|
197
|
+
options?: { port?: number | undefined; cause?: unknown },
|
|
198
|
+
) {
|
|
199
|
+
super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);
|
|
200
|
+
this.name = 'OAuthError';
|
|
201
|
+
this.code = code;
|
|
202
|
+
this.provider = provider;
|
|
203
|
+
this.port = options?.port;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
154
207
|
/** Configuration for creating a custom model endpoint. */
|
|
155
208
|
export interface CustomModelConfig {
|
|
156
209
|
/** Base URL of the OpenAI-compatible API (e.g., 'http://localhost:11434/v1'). */
|
|
@@ -255,7 +308,13 @@ interface ActiveOAuthCallbackPageShim {
|
|
|
255
308
|
readonly provider: string;
|
|
256
309
|
readonly providerName: string;
|
|
257
310
|
readonly route: OAuthCallbackRoute;
|
|
258
|
-
readonly render: OAuthCallbackPageRenderer;
|
|
311
|
+
readonly render: OAuthCallbackPageRenderer | undefined;
|
|
312
|
+
/**
|
|
313
|
+
* Notified exactly once when the browser callback fires and its status
|
|
314
|
+
* (success/error) is known. Lets the flow react immediately instead of
|
|
315
|
+
* waiting on pi-ai (which hangs on non-success callbacks).
|
|
316
|
+
*/
|
|
317
|
+
readonly onResult?: ((status: OAuthCallbackPageStatus, context: OAuthCallbackPageContext) => void) | undefined;
|
|
259
318
|
}
|
|
260
319
|
|
|
261
320
|
type ServerResponseEnd = ServerResponse['end'];
|
|
@@ -266,30 +325,88 @@ const OAUTH_CALLBACK_ROUTES: Record<string, OAuthCallbackRoute> = {
|
|
|
266
325
|
};
|
|
267
326
|
|
|
268
327
|
let activeOAuthCallbackPageShim: ActiveOAuthCallbackPageShim | null = null;
|
|
328
|
+
/** Ensures `onResult` fires at most once per installed shim. */
|
|
329
|
+
let oauthCallbackResultNotified = false;
|
|
330
|
+
|
|
331
|
+
/** Default overall OAuth flow timeout (pi-ai hangs without this). */
|
|
332
|
+
const DEFAULT_OAUTH_FLOW_TIMEOUT_MS = 5 * 60_000;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Probe whether something is already listening on a loopback port. Used to
|
|
336
|
+
* fail an OAuth flow fast (before opening a browser) when the provider's
|
|
337
|
+
* fixed callback port is occupied — otherwise pi-ai binds the other stack /
|
|
338
|
+
* the browser hits the wrong listener and the user gets a dead page while
|
|
339
|
+
* pi-ai waits forever.
|
|
340
|
+
*/
|
|
341
|
+
function probeCallbackPortInUse(port: number, host: string): Promise<boolean> {
|
|
342
|
+
const net = nodeRequire('node:net') as typeof import('node:net');
|
|
343
|
+
return new Promise((resolve) => {
|
|
344
|
+
let settled = false;
|
|
345
|
+
const finish = (inUse: boolean) => {
|
|
346
|
+
if (settled) return;
|
|
347
|
+
settled = true;
|
|
348
|
+
socket.destroy();
|
|
349
|
+
resolve(inUse);
|
|
350
|
+
};
|
|
351
|
+
const socket = net.connect({ port, host });
|
|
352
|
+
socket.once('connect', () => finish(true));
|
|
353
|
+
socket.once('error', () => finish(false));
|
|
354
|
+
socket.setTimeout(600, () => finish(false));
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Throw an `OAuthError('callback_port_in_use')` if the provider's fixed
|
|
360
|
+
* callback port is occupied on either IPv4 or IPv6 loopback. No-op for
|
|
361
|
+
* providers without a known callback route (manual/device-code flows).
|
|
362
|
+
*/
|
|
363
|
+
async function assertOAuthCallbackPortAvailable(provider: string): Promise<void> {
|
|
364
|
+
const route = OAUTH_CALLBACK_ROUTES[provider];
|
|
365
|
+
if (!route) return;
|
|
366
|
+
|
|
367
|
+
for (const host of ['127.0.0.1', '::1']) {
|
|
368
|
+
if (await probeCallbackPortInUse(route.port, host)) {
|
|
369
|
+
throw new OAuthError(
|
|
370
|
+
'callback_port_in_use',
|
|
371
|
+
provider,
|
|
372
|
+
`OAuth callback port ${route.port} for "${provider}" is already in use ` +
|
|
373
|
+
`(detected on ${host}). This is a fixed port: another application is ` +
|
|
374
|
+
`holding it, or a previous sign-in did not finish. Close that ` +
|
|
375
|
+
`application (or restart the host process), then try again.`,
|
|
376
|
+
{ port: route.port },
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
269
381
|
|
|
270
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Install the callback-page shim for a flow when there is a known callback
|
|
384
|
+
* route AND something to do with it (a custom renderer and/or a result
|
|
385
|
+
* observer). Returns a release function (a no-op when no shim was installed).
|
|
386
|
+
*
|
|
387
|
+
* Unlike a `try/finally` wrapper around pi-ai's `login()`, the caller owns
|
|
388
|
+
* the release lifecycle: pi-ai's callback-server flows hang forever on a
|
|
389
|
+
* non-success callback, so cleanup must be tied to the flow's own
|
|
390
|
+
* race/timeout, not to awaiting the (possibly never-settling) login promise.
|
|
391
|
+
*/
|
|
392
|
+
function maybeInstallOAuthCallbackShim(
|
|
271
393
|
provider: string,
|
|
272
394
|
providerName: string,
|
|
273
395
|
render: OAuthCallbackPageRenderer | undefined,
|
|
274
|
-
|
|
275
|
-
):
|
|
396
|
+
onResult: ActiveOAuthCallbackPageShim['onResult'],
|
|
397
|
+
): () => void {
|
|
276
398
|
const route = OAUTH_CALLBACK_ROUTES[provider];
|
|
277
|
-
if (!
|
|
278
|
-
return
|
|
399
|
+
if (!route || (!render && !onResult)) {
|
|
400
|
+
return () => {};
|
|
279
401
|
}
|
|
280
402
|
|
|
281
|
-
|
|
403
|
+
return installOAuthCallbackPageShim({
|
|
282
404
|
provider,
|
|
283
405
|
providerName,
|
|
284
406
|
route,
|
|
285
407
|
render,
|
|
408
|
+
onResult,
|
|
286
409
|
});
|
|
287
|
-
|
|
288
|
-
try {
|
|
289
|
-
return await run();
|
|
290
|
-
} finally {
|
|
291
|
-
release();
|
|
292
|
-
}
|
|
293
410
|
}
|
|
294
411
|
|
|
295
412
|
function installOAuthCallbackPageShim(shim: ActiveOAuthCallbackPageShim): () => void {
|
|
@@ -303,6 +420,7 @@ function installOAuthCallbackPageShim(shim: ActiveOAuthCallbackPageShim): () =>
|
|
|
303
420
|
const prototype = http.ServerResponse.prototype;
|
|
304
421
|
const previousEnd = prototype.end;
|
|
305
422
|
activeOAuthCallbackPageShim = shim;
|
|
423
|
+
oauthCallbackResultNotified = false;
|
|
306
424
|
|
|
307
425
|
const patchedEnd = function patchedOAuthCallbackEnd(this: ServerResponse, ...args: unknown[]) {
|
|
308
426
|
const replacement = maybeRenderOAuthCallbackPage(this, args[0]);
|
|
@@ -373,6 +491,19 @@ function maybeRenderOAuthCallbackPage(response: ServerResponse, chunk: unknown):
|
|
|
373
491
|
context.details = details;
|
|
374
492
|
}
|
|
375
493
|
|
|
494
|
+
// Notify the flow that the browser callback fired (once). This lets
|
|
495
|
+
// initiateOAuth react to a failed callback immediately rather than
|
|
496
|
+
// waiting on pi-ai, which hangs on non-success callbacks.
|
|
497
|
+
if (!oauthCallbackResultNotified && shim.onResult) {
|
|
498
|
+
oauthCallbackResultNotified = true;
|
|
499
|
+
try {
|
|
500
|
+
shim.onResult(status, context);
|
|
501
|
+
} catch {
|
|
502
|
+
// An observer must never break the callback response.
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!shim.render) return null;
|
|
376
507
|
try {
|
|
377
508
|
const rendered = shim.render(context);
|
|
378
509
|
return typeof rendered === 'string' && rendered.trim().length > 0 ? rendered : null;
|
|
@@ -639,10 +770,12 @@ function mapRawToModelInfo(
|
|
|
639
770
|
supportsThinking: supportedThinkingLevels.some(level => level !== 'off')
|
|
640
771
|
|| !!(raw['supportsThinking'] || raw['reasoning']),
|
|
641
772
|
supportedThinkingLevels,
|
|
642
|
-
supportsImages:
|
|
773
|
+
supportsImages: Array.isArray(raw['input'])
|
|
774
|
+
? raw['input'].includes('image')
|
|
775
|
+
: !!raw['supportsImages'],
|
|
643
776
|
};
|
|
644
777
|
|
|
645
|
-
const rawPricing = raw['pricing'];
|
|
778
|
+
const rawPricing = raw['pricing'] ?? raw['cost'];
|
|
646
779
|
if (rawPricing && typeof rawPricing === 'object') {
|
|
647
780
|
const pricing = rawPricing as Record<string, unknown>;
|
|
648
781
|
const inputPrice = pricing['input'];
|
|
@@ -729,41 +862,112 @@ export class ProviderManager implements IProviderManager {
|
|
|
729
862
|
* @param provider - OAuth provider identifier
|
|
730
863
|
* @param callbacks - UI callbacks for auth URL, prompts, and progress
|
|
731
864
|
* @returns The OAuth credentials and display metadata
|
|
732
|
-
* @throws
|
|
865
|
+
* @throws {OAuthError} `unsupported_provider`, `callback_port_in_use`,
|
|
866
|
+
* `cancelled`, `timed_out`, or `callback_failed`. Other errors (e.g.
|
|
867
|
+
* network/token-exchange failures from pi-ai) propagate as-is.
|
|
733
868
|
*/
|
|
734
869
|
async initiateOAuth(provider: string, callbacks: OAuthCallbacks): Promise<OAuthResult> {
|
|
735
870
|
const oauthModule = await loadPiAiOAuth();
|
|
736
871
|
const oauthProvider = oauthModule.getOAuthProvider?.(provider);
|
|
737
872
|
if (!oauthProvider) {
|
|
738
|
-
throw new
|
|
873
|
+
throw new OAuthError(
|
|
874
|
+
'unsupported_provider',
|
|
875
|
+
provider,
|
|
876
|
+
`Provider "${provider}" does not support OAuth`,
|
|
877
|
+
);
|
|
739
878
|
}
|
|
740
879
|
|
|
741
|
-
|
|
880
|
+
// (A) Fail fast — before opening a browser — if the provider's fixed
|
|
881
|
+
// callback port is already taken. Otherwise pi-ai binds the other
|
|
882
|
+
// stack, the browser hits the wrong listener, and pi-ai waits forever.
|
|
883
|
+
await assertOAuthCallbackPortAvailable(provider);
|
|
742
884
|
|
|
743
|
-
|
|
744
|
-
|
|
885
|
+
const abort = new AbortController();
|
|
886
|
+
this.activeOAuthAbort = abort;
|
|
887
|
+
|
|
888
|
+
// (C) pi-ai only settles its callback wait on success; on a failed
|
|
889
|
+
// callback (e.g. state mismatch) it hangs. The render shim already sees
|
|
890
|
+
// that response — use it to fail the flow immediately with the reason.
|
|
891
|
+
let failFromCallback!: (err: OAuthError) => void;
|
|
892
|
+
const callbackFailure = new Promise<never>((_, reject) => {
|
|
893
|
+
failFromCallback = reject;
|
|
894
|
+
});
|
|
895
|
+
const handleCallbackResult = (
|
|
896
|
+
status: OAuthCallbackPageStatus,
|
|
897
|
+
ctx: OAuthCallbackPageContext,
|
|
898
|
+
): void => {
|
|
899
|
+
if (status !== 'error') return;
|
|
900
|
+
const detail = ctx.details ? ` (${ctx.details})` : '';
|
|
901
|
+
failFromCallback(new OAuthError(
|
|
902
|
+
'callback_failed',
|
|
745
903
|
provider,
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
onAuth: callbacks.onAuth,
|
|
750
|
-
onPrompt: callbacks.onPrompt,
|
|
751
|
-
onProgress: callbacks.onProgress,
|
|
752
|
-
onManualCodeInput: callbacks.onManualCodeInput,
|
|
753
|
-
onSelect: callbacks.onSelect,
|
|
754
|
-
signal: this.activeOAuthAbort!.signal,
|
|
755
|
-
}),
|
|
756
|
-
);
|
|
904
|
+
`OAuth callback for "${provider}" reported a failure: ${ctx.message}${detail}`,
|
|
905
|
+
));
|
|
906
|
+
};
|
|
757
907
|
|
|
758
|
-
|
|
908
|
+
const releaseShim = maybeInstallOAuthCallbackShim(
|
|
909
|
+
provider,
|
|
910
|
+
oauthProvider.name,
|
|
911
|
+
callbacks.renderCallbackPage,
|
|
912
|
+
handleCallbackResult,
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
// (B) pi-ai callback servers ignore the abort signal, so cancellation
|
|
916
|
+
// and timeout are enforced here. Without this the flow hangs forever.
|
|
917
|
+
const timeoutMs = callbacks.timeoutMs ?? DEFAULT_OAUTH_FLOW_TIMEOUT_MS;
|
|
918
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
919
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
920
|
+
if (timeoutMs > 0 && Number.isFinite(timeoutMs)) {
|
|
921
|
+
timer = setTimeout(() => reject(new OAuthError(
|
|
922
|
+
'timed_out',
|
|
923
|
+
provider,
|
|
924
|
+
`OAuth flow for "${provider}" timed out after ${timeoutMs}ms.`,
|
|
925
|
+
)), timeoutMs);
|
|
926
|
+
timer.unref?.();
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
const cancelled = new Promise<never>((_, reject) => {
|
|
930
|
+
abort.signal.addEventListener('abort', () => reject(new OAuthError(
|
|
931
|
+
'cancelled',
|
|
932
|
+
provider,
|
|
933
|
+
`OAuth flow for "${provider}" was cancelled.`,
|
|
934
|
+
)), { once: true });
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
const login = oauthProvider.login({
|
|
938
|
+
onAuth: callbacks.onAuth,
|
|
939
|
+
onPrompt: callbacks.onPrompt,
|
|
940
|
+
onProgress: callbacks.onProgress,
|
|
941
|
+
onManualCodeInput: callbacks.onManualCodeInput,
|
|
942
|
+
onSelect: callbacks.onSelect,
|
|
943
|
+
signal: abort.signal,
|
|
944
|
+
}) as Promise<Record<string, unknown>>;
|
|
945
|
+
// Whichever promise loses the race may still settle later (pi-ai's
|
|
946
|
+
// login can hang or settle late; the aux promises can reject after the
|
|
947
|
+
// race is decided). Attach inert handlers so a late rejection never
|
|
948
|
+
// surfaces as an unhandled rejection. Promise.race still observes the
|
|
949
|
+
// first settlement independently.
|
|
950
|
+
login.catch(() => {});
|
|
951
|
+
cancelled.catch(() => {});
|
|
952
|
+
timeout.catch(() => {});
|
|
953
|
+
callbackFailure.catch(() => {});
|
|
954
|
+
|
|
955
|
+
try {
|
|
956
|
+
const rawCredentials = await Promise.race([
|
|
957
|
+
login,
|
|
958
|
+
cancelled,
|
|
959
|
+
timeout,
|
|
960
|
+
callbackFailure,
|
|
961
|
+
]);
|
|
759
962
|
|
|
760
963
|
const credentials = JSON.stringify(rawCredentials);
|
|
761
964
|
const meta = buildOAuthMeta(provider, rawCredentials);
|
|
762
965
|
|
|
763
966
|
return { credentials, meta };
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
|
|
967
|
+
} finally {
|
|
968
|
+
if (timer) clearTimeout(timer);
|
|
969
|
+
releaseShim();
|
|
970
|
+
if (this.activeOAuthAbort === abort) this.activeOAuthAbort = null;
|
|
767
971
|
}
|
|
768
972
|
}
|
|
769
973
|
|
|
@@ -830,32 +1034,31 @@ export class ProviderManager implements IProviderManager {
|
|
|
830
1034
|
async validateApiKey(provider: string, apiKey: string): Promise<ApiKeyValidationResult> {
|
|
831
1035
|
const piAi = await loadPiAi();
|
|
832
1036
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
:
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
return this.tryValidation(piAi, provider, firstModelId, apiKey);
|
|
1037
|
+
const models = piAi.getModels(provider) ?? [];
|
|
1038
|
+
if (models.length === 0) {
|
|
1039
|
+
return {
|
|
1040
|
+
provider,
|
|
1041
|
+
modelId: null,
|
|
1042
|
+
valid: false,
|
|
1043
|
+
retryable: false,
|
|
1044
|
+
status: 'resolution_error',
|
|
1045
|
+
message: `No models found for provider "${provider}"`,
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const modelId = this.getSmallestModelId(provider, models);
|
|
1050
|
+
if (!modelId) {
|
|
1051
|
+
return {
|
|
1052
|
+
provider,
|
|
1053
|
+
modelId: null,
|
|
1054
|
+
valid: false,
|
|
1055
|
+
retryable: false,
|
|
1056
|
+
status: 'resolution_error',
|
|
1057
|
+
message: `No usable models found for provider "${provider}"`,
|
|
1058
|
+
};
|
|
856
1059
|
}
|
|
857
1060
|
|
|
858
|
-
return this.tryValidation(piAi, provider,
|
|
1061
|
+
return this.tryValidation(piAi, provider, modelId, apiKey);
|
|
859
1062
|
}
|
|
860
1063
|
|
|
861
1064
|
/**
|
|
@@ -948,11 +1151,10 @@ export class ProviderManager implements IProviderManager {
|
|
|
948
1151
|
// -----------------------------------------------------------------------
|
|
949
1152
|
|
|
950
1153
|
/**
|
|
951
|
-
* Get the cheapest
|
|
952
|
-
* Uses the UTILITY_MODEL_DEFAULTS as a proxy for "smallest model."
|
|
1154
|
+
* Get the cheapest likely utility model ID for a provider.
|
|
953
1155
|
*/
|
|
954
|
-
private getSmallestModelId(provider: string): string | null {
|
|
955
|
-
return
|
|
1156
|
+
private getSmallestModelId(provider: string, models: Array<Record<string, unknown>>): string | null {
|
|
1157
|
+
return UTILITY_MODEL_OVERRIDES[provider] ?? inferUtilityModelId(models);
|
|
956
1158
|
}
|
|
957
1159
|
|
|
958
1160
|
/**
|
package/src/provider-registry.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This module contains:
|
|
5
5
|
* 1. PROVIDER_REGISTRY: metadata for all known providers (auth methods, env vars, key prefixes)
|
|
6
6
|
* 2. OAUTH_PROVIDER_IDS: the subset of providers that support OAuth
|
|
7
|
-
* 3.
|
|
7
|
+
* 3. UTILITY_MODEL_OVERRIDES: per-provider utility model overrides for inference exceptions
|
|
8
8
|
*
|
|
9
9
|
* OAuth flows are resolved through pi-ai's OAuth provider registry at runtime.
|
|
10
10
|
*
|
|
@@ -270,17 +270,9 @@ export const OAUTH_PROVIDER_IDS: string[] = [
|
|
|
270
270
|
];
|
|
271
271
|
|
|
272
272
|
// ---------------------------------------------------------------------------
|
|
273
|
-
//
|
|
273
|
+
// Model Defaults
|
|
274
274
|
// ---------------------------------------------------------------------------
|
|
275
275
|
|
|
276
|
-
/**
|
|
277
|
-
* Default utility model IDs per provider.
|
|
278
|
-
* Used when utilityModel is 'default' or undefined.
|
|
279
|
-
*
|
|
280
|
-
* These are the cheapest capable models for each provider,
|
|
281
|
-
* suitable for internal operations like WebFetch summarization
|
|
282
|
-
* and safety classification.
|
|
283
|
-
*/
|
|
284
276
|
/**
|
|
285
277
|
* Default primary model IDs per provider.
|
|
286
278
|
* Used when a user first connects a provider and no model is explicitly selected.
|
|
@@ -289,21 +281,22 @@ export const OAUTH_PROVIDER_IDS: string[] = [
|
|
|
289
281
|
export const PRIMARY_MODEL_DEFAULTS: Record<string, string> = {
|
|
290
282
|
anthropic: 'claude-sonnet-4-6',
|
|
291
283
|
openai: 'gpt-5.4',
|
|
284
|
+
'openai-codex': 'gpt-5.5',
|
|
292
285
|
google: 'gemini-3.1-pro-preview',
|
|
286
|
+
xai: 'grok-4',
|
|
293
287
|
groq: 'openai/gpt-oss-120b',
|
|
294
288
|
cerebras: 'gpt-oss-120b',
|
|
295
289
|
mistral: 'mistral-large-2512',
|
|
296
290
|
};
|
|
297
291
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
};
|
|
292
|
+
/**
|
|
293
|
+
* Per-provider utility model overrides for inference exceptions.
|
|
294
|
+
* Leave empty unless dynamic inference picks a bad utility model for a provider.
|
|
295
|
+
*/
|
|
296
|
+
export const UTILITY_MODEL_OVERRIDES: Record<string, string> = {};
|
|
297
|
+
|
|
298
|
+
/** Backwards-compatible alias. Prefer UTILITY_MODEL_OVERRIDES for new code. */
|
|
299
|
+
export const UTILITY_MODEL_DEFAULTS = UTILITY_MODEL_OVERRIDES;
|
|
307
300
|
|
|
308
301
|
// ---------------------------------------------------------------------------
|
|
309
302
|
// Cache Retention
|